Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/ControlAspectRatios.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<div
v-if="aspectRatios.length"
class="hidden shrink-0 justify-center overflow-hidden rounded-xl border border-zinc-200 bg-white/80 shadow-lg backdrop-blur-xl md:flex dark:border-zinc-800 dark:bg-zinc-900/80"
>
<Button
Expand Down
109 changes: 109 additions & 0 deletions app/components/ModalPreferences.vue
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,79 @@
</SettingsRow>
</template>
</SettingsSection>

<SettingsSection title="Aspect Ratios">
<SettingsRow
label="Add Ratio"
description="Add a width and height pair"
>
<div class="flex items-center gap-2">
<Input
min="1"
type="number"
v-model="customAspectRatioWidth"
class="w-16"
/>
<span class="text-xs text-zinc-400">:</span>
<Input
min="1"
type="number"
v-model="customAspectRatioHeight"
class="w-16"
/>
<Button
size="icon-sm"
:disabled="!canAddCustomAspectRatio"
@click="addCustomAspectRatio"
>
<PlusIcon class="size-3.5" />
</Button>
</div>
</SettingsRow>

<SettingsRow label="Saved Ratios">
<Draggable
v-if="preferences.previewAspectRatios.length"
v-model="preferences.previewAspectRatios"
:item-key="aspectRatioKey"
tag="div"
class="flex max-w-96 flex-wrap justify-end gap-1.5"
ghost-class="opacity-50"
>
<template #item="{ element: [x, y], index }">
<span
class="inline-flex h-6 cursor-grab items-center gap-1 rounded-md border border-zinc-200 bg-zinc-50 pr-1 pl-2 text-xs font-medium text-zinc-700 active:cursor-grabbing dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200"
>
{{ x }}:{{ y }}
<button
type="button"
class="inline-flex size-4 cursor-pointer items-center justify-center rounded-sm text-zinc-400 hover:bg-zinc-200 hover:text-zinc-700 dark:hover:bg-zinc-700 dark:hover:text-zinc-200"
@click="deleteAspectRatio(index)"
>
<XIcon class="size-3" />
</button>
</span>
</template>
</Draggable>

<span v-else class="text-xs text-zinc-400">
No saved ratios
</span>
</SettingsRow>

<SettingsRow
label="Reset Defaults"
description="Restore 16:9, 4:3, and 1:1"
>
<Button
size="sm"
variant="outline"
@click="preferences.resetAspectRatios()"
>
Reset
</Button>
</SettingsRow>
</SettingsSection>
</TabsContent>

<!-- Social -->
Expand Down Expand Up @@ -432,15 +505,18 @@
<script setup>
import { orderBy } from 'lodash';
import { storeToRefs } from 'pinia';
import Draggable from 'vuedraggable';
import {
CodeIcon,
EyeIcon,
ShareIcon,
DownloadIcon,
PaletteIcon,
PlusIcon,
SunIcon,
MoonIcon,
SunriseIcon,
XIcon,
} from 'lucide-vue-next';
import useFonts from '@/composables/useFonts';
import useSocials from '@/composables/useSocials';
Expand All @@ -458,6 +534,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();

Expand All @@ -484,6 +562,37 @@ const editorThemes = computed(() => {
return orderBy(themes, 'title');
});

const canAddCustomAspectRatio = computed(() => {
const ratio = [Number(customAspectRatioWidth.value), Number(customAspectRatioHeight.value)];

return (
ratio.every((value) => Number.isFinite(value) && value > 0) &&
!preferences.hasAspectRatio(ratio)
);
});

function addCustomAspectRatio() {
if (!canAddCustomAspectRatio.value) {
return;
}

preferences.addAspectRatio([
Number(customAspectRatioWidth.value),
Number(customAspectRatioHeight.value),
]);

customAspectRatioWidth.value = '';
customAspectRatioHeight.value = '';
}

function aspectRatioKey([x, y]) {
return `${x}:${y}`;
}

function deleteAspectRatio(index) {
preferences.previewAspectRatios.splice(index, 1);
}

function setColorMode(mode) {
isAutoColorScheme.value = mode === 'auto';
colorMode.value = mode;
Expand Down
2 changes: 1 addition & 1 deletion app/components/Preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -502,5 +502,5 @@ onMounted(() => {

onBeforeUnmount(() => templateGenerationDebounce?.cancel());

const { aspectRatio, aspectRatios, selectAspectRatio, setCustomAspectRatio } = useAspectRatios();
const { aspectRatios } = useAspectRatios();
</script>
33 changes: 28 additions & 5 deletions app/composables/useAspectRatios.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
36 changes: 36 additions & 0 deletions app/composables/usePreferencesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,6 +26,7 @@ export const defaults = {
previewCodeBlurStrength: 1,
previewFontFamily: 'font-mono-lisa',
previewThemeName: 'github-dark',
previewAspectRatios: defaultAspectRatios,

previewLockToWindow: false,
previewLockToWindowPaddingX: 0,
Expand All @@ -36,10 +43,21 @@ 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);

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.
Expand Down Expand Up @@ -69,5 +87,23 @@ export default defineStore('preferences', {
this.$state = defaults;
}
},

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);
},
},
});
13 changes: 13 additions & 0 deletions app/content/changelog.md
Original file line number Diff line number Diff line change
@@ -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**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "showcode",
"version": "2.6.0",
"version": "2.7.0",
"private": true,
"description": "Generate beautiful images of code.",
"repository": {
Expand Down
31 changes: 31 additions & 0 deletions tests/components/ControlAspectRatios.test.js
Original file line number Diff line number Diff line change
@@ -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('');
});
});
Loading