From f8b1536d52cbb863b815fbb18f29366ae07aa706 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 22 Jun 2026 11:41:42 -0400 Subject: [PATCH 1/5] Add configurable aspect ratios --- app/components/ControlAspectRatios.vue | 1 + app/components/ModalPreferences.vue | 85 ++++++++++++++++++++ app/components/Preview.vue | 2 +- app/composables/useAspectRatios.js | 33 ++++++-- app/composables/usePreferencesStore.js | 18 +++++ tests/components/ControlAspectRatios.test.js | 31 +++++++ tests/composables/useAspectRatios.test.js | 79 +++++++++++++++++- 7 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 tests/components/ControlAspectRatios.test.js diff --git a/app/components/ControlAspectRatios.vue b/app/components/ControlAspectRatios.vue index c1c24900..6f52fdcc 100644 --- a/app/components/ControlAspectRatios.vue +++ b/app/components/ControlAspectRatios.vue @@ -1,5 +1,6 @@ + + + +
+ + : + + +
+
+ + + + + + + + +
@@ -438,9 +495,11 @@ import { ShareIcon, DownloadIcon, PaletteIcon, + PlusIcon, SunIcon, MoonIcon, SunriseIcon, + XIcon, } from 'lucide-vue-next'; import useFonts from '@/composables/useFonts'; import useSocials from '@/composables/useSocials'; @@ -458,6 +517,8 @@ const { $shiki } = useNuxtApp(); const { options: languageOptions } = useLanguages(); const activeTab = ref('editor'); const isAutoColorScheme = ref(null); +const customAspectRatioWidth = ref(''); +const customAspectRatioHeight = ref(''); const preferences = usePreferencesStore(); const { types: socialTypes, positions: socialPositions } = useSocials(); @@ -484,6 +545,30 @@ const editorThemes = computed(() => { return orderBy(themes, 'title'); }); +const canAddCustomAspectRatio = computed(() => { + return [customAspectRatioWidth.value, customAspectRatioHeight.value].every( + (value) => Number.isFinite(Number(value)) && Number(value) > 0 + ); +}); + +function addCustomAspectRatio() { + if (!canAddCustomAspectRatio.value) { + return; + } + + preferences.previewAspectRatios.push([ + Number(customAspectRatioWidth.value), + Number(customAspectRatioHeight.value), + ]); + + customAspectRatioWidth.value = ''; + customAspectRatioHeight.value = ''; +} + +function deleteAspectRatio(index) { + preferences.previewAspectRatios.splice(index, 1); +} + function setColorMode(mode) { isAutoColorScheme.value = mode === 'auto'; colorMode.value = mode; diff --git a/app/components/Preview.vue b/app/components/Preview.vue index fdacca99..9b3bec07 100644 --- a/app/components/Preview.vue +++ b/app/components/Preview.vue @@ -502,5 +502,5 @@ onMounted(() => { onBeforeUnmount(() => templateGenerationDebounce?.cancel()); -const { aspectRatio, aspectRatios, selectAspectRatio, setCustomAspectRatio } = useAspectRatios(); +const { aspectRatios } = useAspectRatios(); diff --git a/app/composables/useAspectRatios.js b/app/composables/useAspectRatios.js index f3b4af09..f02966be 100644 --- a/app/composables/useAspectRatios.js +++ b/app/composables/useAspectRatios.js @@ -1,9 +1,32 @@ +import { computed } from 'vue'; +import usePreferencesStore from '@/composables/usePreferencesStore'; + +function validAspectRatio(ratio) { + return ( + Array.isArray(ratio) && + ratio.length === 2 && + ratio.every((value) => Number.isFinite(Number(value)) && Number(value) > 0) + ); +} + +function normalizeAspectRatio([x, y]) { + return [Number(x), Number(y)]; +} + export default function () { - const aspectRatios = [ - [16, 9], - [4, 3], - [1, 1], - ]; + const preferences = usePreferencesStore(); + + const aspectRatios = computed(() => { + const ratios = preferences.previewAspectRatios + .filter(validAspectRatio) + .map(normalizeAspectRatio); + + return ratios.filter((ratio, index, collection) => { + const [x, y] = ratio; + + return collection.findIndex(([rx, ry]) => rx === x && ry === y) === index; + }); + }); function calculateAspectRatio([x, y], height) { return Math.round((height / y) * x); diff --git a/app/composables/usePreferencesStore.js b/app/composables/usePreferencesStore.js index 07de956a..2c416a34 100644 --- a/app/composables/usePreferencesStore.js +++ b/app/composables/usePreferencesStore.js @@ -3,6 +3,12 @@ import { useLocalStorage } from '@vueuse/core'; import themes from 'monaco-themes/themes/themelist.json'; import { pick, defaults as applyDefaults } from 'lodash'; +export const defaultAspectRatios = [ + [16, 9], + [4, 3], + [1, 1], +]; + export const defaults = { editorTabSize: 4, editorFontSize: 12, @@ -20,6 +26,7 @@ export const defaults = { previewCodeBlurStrength: 1, previewFontFamily: 'font-mono-lisa', previewThemeName: 'github-dark', + previewAspectRatios: defaultAspectRatios, previewLockToWindow: false, previewLockToWindowPaddingX: 0, @@ -40,6 +47,13 @@ export default defineStore('preferences', { state: () => { const state = useLocalStorage('preferences', defaults); + if (!state.value.previewAspectRatios && state.value.previewCustomAspectRatios) { + state.value.previewAspectRatios = [ + ...defaultAspectRatios, + ...state.value.previewCustomAspectRatios, + ]; + } + // Here we are enforcing the hydration of the default // preference values and also removing any keys // that may have been removed from an update. @@ -69,5 +83,9 @@ export default defineStore('preferences', { this.$state = defaults; } }, + + resetAspectRatios() { + this.previewAspectRatios = defaultAspectRatios.map((ratio) => [...ratio]); + }, }, }); diff --git a/tests/components/ControlAspectRatios.test.js b/tests/components/ControlAspectRatios.test.js new file mode 100644 index 00000000..edcf3bdd --- /dev/null +++ b/tests/components/ControlAspectRatios.test.js @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils'; +import { describe, expect, it } from 'vitest'; +import ControlAspectRatios from '~/components/ControlAspectRatios.vue'; + +describe('ControlAspectRatios', () => { + it('renders available aspect ratios and custom option', () => { + const wrapper = mount(ControlAspectRatios, { + props: { + aspectRatio: [16, 9], + aspectRatios: [[16, 9]], + lockWindowSize: false, + }, + }); + + expect(wrapper.text()).toContain('16:9'); + expect(wrapper.text()).toContain('Custom'); + }); + + it('does not render when no aspect ratios are available', () => { + const wrapper = mount(ControlAspectRatios, { + props: { + aspectRatio: null, + aspectRatios: [], + lockWindowSize: false, + }, + }); + + expect(wrapper.find('button').exists()).toBe(false); + expect(wrapper.text()).toBe(''); + }); +}); diff --git a/tests/composables/useAspectRatios.test.js b/tests/composables/useAspectRatios.test.js index e60ca257..1148bbe7 100644 --- a/tests/composables/useAspectRatios.test.js +++ b/tests/composables/useAspectRatios.test.js @@ -1,11 +1,85 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import useAspectRatios from '~/composables/useAspectRatios'; +import usePreferencesStore from '~/composables/usePreferencesStore'; describe('useAspectRatios', () => { + beforeEach(() => { + localStorage.clear(); + }); + it('returns the default aspect ratios', () => { const { aspectRatios } = useAspectRatios(); - expect(aspectRatios).toEqual([ + expect(aspectRatios.value).toEqual([ + [16, 9], + [4, 3], + [1, 1], + ]); + }); + + it('returns aspect ratios from preferences', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [ + [21, 9], + [5, 4], + ]; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([ + [21, 9], + [5, 4], + ]); + }); + + it('can return no aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = []; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([]); + }); + + it('ignores duplicate aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [ + [16, 9], + [21, 9], + [21, 9], + ]; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([ + [16, 9], + [21, 9], + ]); + }); + + it('ignores invalid aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [ + [21, 9], + [0, 9], + [-1, 9], + [4], + ['wide', 9], + null, + ]; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([[21, 9]]); + }); + + it('resets aspect ratios to defaults', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [[21, 9]]; + + preferences.resetAspectRatios(); + + expect(preferences.previewAspectRatios).toEqual([ [16, 9], [4, 3], [1, 1], @@ -32,4 +106,3 @@ describe('useAspectRatios', () => { expect(calculateAspectRatio([1, 1], 500)).toBe(500); }); }); - From 8595f2e4eae5c0bc43b9426f775fe2002cd7a2ba Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 22 Jun 2026 12:27:20 -0400 Subject: [PATCH 2/5] Bump version to 2.7.0 --- app/content/changelog.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/content/changelog.md b/app/content/changelog.md index f619c058..5a46fd2d 100644 --- a/app/content/changelog.md +++ b/app/content/changelog.md @@ -1,3 +1,16 @@ +## June 22, 2026 — Version 2.7 + +**Added** + +- Added configurable aspect ratios in Preferences +- Added the ability to add, remove, and reset preview aspect ratios + +**Changed** + +- The aspect ratio bar now reflects the configured preference list and hides when no ratios are defined + +--- + ## June 19, 2026 — Version 2.6 **Added** diff --git a/package-lock.json b/package-lock.json index 3ba3f52a..62ed9661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "showcode", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "showcode", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "devDependencies": { "@formkit/auto-animate": "^0.9.0", diff --git a/package.json b/package.json index 29626819..86eebde6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "showcode", - "version": "2.6.0", + "version": "2.7.0", "private": true, "description": "Generate beautiful images of code.", "repository": { From e23a37a41af578703a80ede10ea1c0a6a559fe1d Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 22 Jun 2026 12:51:56 -0400 Subject: [PATCH 3/5] Display aspect ratios as removable badges --- app/components/ModalPreferences.vue | 34 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/app/components/ModalPreferences.vue b/app/components/ModalPreferences.vue index ea6b4382..a30b0efb 100644 --- a/app/components/ModalPreferences.vue +++ b/app/components/ModalPreferences.vue @@ -312,18 +312,30 @@ - - + + {{ x }}:{{ y }} + + + + + + No saved ratios + Date: Mon, 22 Jun 2026 12:55:57 -0400 Subject: [PATCH 4/5] Prevent duplicate aspect ratios --- app/components/ModalPreferences.vue | 9 ++++--- app/composables/usePreferencesStore.js | 18 +++++++++++++ tests/composables/usePreferencesStore.test.js | 27 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/composables/usePreferencesStore.test.js diff --git a/app/components/ModalPreferences.vue b/app/components/ModalPreferences.vue index a30b0efb..b121887f 100644 --- a/app/components/ModalPreferences.vue +++ b/app/components/ModalPreferences.vue @@ -558,8 +558,11 @@ const editorThemes = computed(() => { }); const canAddCustomAspectRatio = computed(() => { - return [customAspectRatioWidth.value, customAspectRatioHeight.value].every( - (value) => Number.isFinite(Number(value)) && Number(value) > 0 + const ratio = [Number(customAspectRatioWidth.value), Number(customAspectRatioHeight.value)]; + + return ( + ratio.every((value) => Number.isFinite(value) && value > 0) && + !preferences.hasAspectRatio(ratio) ); }); @@ -568,7 +571,7 @@ function addCustomAspectRatio() { return; } - preferences.previewAspectRatios.push([ + preferences.addAspectRatio([ Number(customAspectRatioWidth.value), Number(customAspectRatioHeight.value), ]); diff --git a/app/composables/usePreferencesStore.js b/app/composables/usePreferencesStore.js index 2c416a34..c96c1d88 100644 --- a/app/composables/usePreferencesStore.js +++ b/app/composables/usePreferencesStore.js @@ -43,6 +43,10 @@ export const defaults = { socialPosition: 'bottom-center', }; +function sameAspectRatio([leftX, leftY], [rightX, rightY]) { + return Number(leftX) === Number(rightX) && Number(leftY) === Number(rightY); +} + export default defineStore('preferences', { state: () => { const state = useLocalStorage('preferences', defaults); @@ -87,5 +91,19 @@ export default defineStore('preferences', { resetAspectRatios() { this.previewAspectRatios = defaultAspectRatios.map((ratio) => [...ratio]); }, + + hasAspectRatio(ratio) { + return this.previewAspectRatios.some((existingRatio) => + sameAspectRatio(existingRatio, ratio) + ); + }, + + addAspectRatio(ratio) { + if (this.hasAspectRatio(ratio)) { + return; + } + + this.previewAspectRatios.push(ratio); + }, }, }); diff --git a/tests/composables/usePreferencesStore.test.js b/tests/composables/usePreferencesStore.test.js new file mode 100644 index 00000000..bb2ff8a1 --- /dev/null +++ b/tests/composables/usePreferencesStore.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import usePreferencesStore from '~/composables/usePreferencesStore'; + +describe('usePreferencesStore', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('adds aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = []; + + preferences.addAspectRatio([21, 9]); + + expect(preferences.previewAspectRatios).toEqual([[21, 9]]); + }); + + it('does not add duplicate aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [[16, 9]]; + + preferences.addAspectRatio([16, 9]); + preferences.addAspectRatio(['16', '9']); + + expect(preferences.previewAspectRatios).toEqual([[16, 9]]); + }); +}); From e14af78f5be083725e62e124b36ba46b1f4bd440 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 22 Jun 2026 12:58:45 -0400 Subject: [PATCH 5/5] Allow aspect ratio reordering --- app/components/ModalPreferences.vue | 39 ++++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/app/components/ModalPreferences.vue b/app/components/ModalPreferences.vue index b121887f..5725cdb8 100644 --- a/app/components/ModalPreferences.vue +++ b/app/components/ModalPreferences.vue @@ -313,25 +313,29 @@ -
- - {{ x }}:{{ y }} - - -
+ {{ x }}:{{ y }} + + + + No saved ratios @@ -501,6 +505,7 @@