diff --git a/package.json b/package.json index c382c7a..d9220b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "1.2.5", + "version": "1.2.6", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/FontPicker.vue b/src/components/FontPicker.vue new file mode 100644 index 0000000..03bc600 --- /dev/null +++ b/src/components/FontPicker.vue @@ -0,0 +1,224 @@ + + + + + + + diff --git a/src/components/PropertiesPanel.vue b/src/components/PropertiesPanel.vue index 1175b94..2e42f80 100644 --- a/src/components/PropertiesPanel.vue +++ b/src/components/PropertiesPanel.vue @@ -18,8 +18,8 @@ import { sampleLayer } from '@/lib/interpolate' import type { Action, AnimatableProps, Layer } from '@/types/scene' import RangeSlider from './RangeSlider.vue' import ActionEditor from './ActionEditor.vue' +import FontPicker from './FontPicker.vue' import { t } from '@/i18n' -import { AVAILABLE_FONTS } from '@/lib/fonts' const SECTION_STORAGE_KEY = 'animatr:propSections' @@ -138,11 +138,14 @@ const textAsset = computed(() => { return a && a.kind === 'text' ? a : null }) +// Empty string represents the system-sans default. Stored as +// `undefined` on the asset so the model field round-trips cleanly +// through JSON without leaking a bogus 'system-sans' id. const textFontModel = computed({ - get: () => textAsset.value?.fontFamily ?? 'system-sans', + get: () => textAsset.value?.fontFamily ?? '', set: (id: string) => { if (!textAsset.value) return - textAsset.value.fontFamily = id === 'system-sans' ? undefined : id + textAsset.value.fontFamily = id || undefined }, }) @@ -497,9 +500,7 @@ function deleteSelection() { diff --git a/src/devtools/tests.ts b/src/devtools/tests.ts index f06e088..a6a6139 100644 --- a/src/devtools/tests.ts +++ b/src/devtools/tests.ts @@ -15032,7 +15032,7 @@ export function registerAllTests(): void { expect(fontCssFamily('does-not-exist')).toBe(SYSTEM_SANS.family) }) - test('PropertiesPanel renders a font dropdown for text assets and writes back', async () => { + test('PropertiesPanel renders the font picker for text assets and writes back', async () => { const s = makeFreshStore() const a = s.addAsset({ kind: 'text', name: 'T', text: 'hello', fontSize: 32, fill: '#000', @@ -15043,30 +15043,40 @@ export function registerAllTests(): void { const handle = mountComponent(PropertiesPanel, {}) try { await nextTick() - const select = handle.host.querySelector('select') as HTMLSelectElement | null - expect(select).toBeTruthy() - const options = Array.from(select!.options).map(o => o.value) - expect(options).toContain('system-sans') - expect(options).toContain('inter') - expect(options).toContain('marker') - // Default selection: system-sans (asset.fontFamily is undefined). - expect(select!.value).toBe('system-sans') - // Pick Inter — the asset should mutate. - select!.value = 'inter' - select!.dispatchEvent(new Event('change')) + const trigger = handle.host.querySelector('.font-picker .trigger') as HTMLButtonElement | null + expect(!!trigger).toBe(true) + // Closed state: the label shows the default — System Sans. + expect((trigger!.textContent ?? '').includes('System Sans')).toBe(true) + trigger!.click() + await nextTick() + // Popover lives in document.body via Teleport. + const opts = Array.from(document.querySelectorAll('.font-popover .font-option')) as HTMLButtonElement[] + expect(opts.length).toBeGreaterThan(5) + const labels = opts.map(o => o.textContent?.trim() ?? '') + expect(labels.some(l => l.includes('System Sans'))).toBe(true) + expect(labels.some(l => l.includes('Inter'))).toBe(true) + expect(labels.some(l => l.includes('Permanent Marker'))).toBe(true) + // Click Inter. + const interOpt = opts.find(o => (o.textContent ?? '').trim() === 'Inter')! + interOpt.click() await nextTick() const textAsset = s.scene.assets.find(x => x.id === a.id)! if (textAsset.kind !== 'text') throw new Error('unexpected kind') expect(textAsset.fontFamily).toBe('inter') - // Picking system-sans clears the field (undefined, not the literal id). - select!.value = 'system-sans' - select!.dispatchEvent(new Event('change')) + // Re-open + click System Sans → clears (undefined). + trigger!.click() + await nextTick() + const opts2 = Array.from(document.querySelectorAll('.font-popover .font-option')) as HTMLButtonElement[] + const sysOpt = opts2.find(o => (o.textContent ?? '').trim() === 'System Sans')! + sysOpt.click() await nextTick() const textAsset2 = s.scene.assets.find(x => x.id === a.id)! if (textAsset2.kind !== 'text') throw new Error('unexpected kind') expect(textAsset2.fontFamily === undefined).toBe(true) } finally { handle.unmount() + // Make sure the teleported popover doesn't leak into other tests. + document.querySelectorAll('.font-popover').forEach(el => el.remove()) } }) diff --git a/src/version.ts b/src/version.ts index a6846ca..580b2d8 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const APP_VERSION = '1.2.5' +export const APP_VERSION = '1.2.6' export const APP_NAME = 'RUWIO'