1.2.6: dedicated FontPicker component with in-line type previews

Replaces the bare <select> with a custom combobox that fits the
properties panel's visual language:

- Trigger looks like the other inputs (var(--bg)/var(--border)/
  var(--radius-sm)) and shows the selected font's name rendered in
  that font itself, with a rotating caret.
- Options live in a teleported popover anchored beneath the trigger.
  Each option renders its own label in its own typeface so the user
  sees what they're picking before applying it. Active row uses
  var(--accent-soft) to match the rest of the editor.
- Click-outside closes via window pointerdown; Escape closes and
  returns focus to the trigger.
- Storage shape simplified: textFontModel uses '' for system-sans
  instead of the literal 'system-sans' id, so the model field round-
  trips through JSON without a placeholder string ever leaking.

Updated PropertiesPanel test to walk the new picker (click trigger,
read options from .font-popover, click an option) instead of
dispatching a select 'change' event. Picker label text doubles as
the test selector — System Sans / Inter / Permanent Marker confirm
the option list is wired up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-26 10:27:19 +02:00
1 parent 4c2b7c7c35
commit 8c4f744afc
5 files changed
+258 -23

No files matched your search

+25 -15
View File
@@ -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())
}
})