From d3c5b8b640fc05a656e274c1d6ab942f5ef8a8f4 Mon Sep 17 00:00:00 2001 From: th Date: Wed, 22 Apr 2026 18:55:11 +0200 Subject: [PATCH] 0.10.5: clicking + on an empty button slot moves the editor to that state Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- src/components/Timeline.vue | 7 +++++ src/devtools/tests.ts | 54 +++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index dcc0e7f..381a878 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "animatr", "private": true, - "version": "0.10.4", + "version": "0.10.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 50e4b45..f6447df 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -1251,6 +1251,13 @@ function addKfFromPrev(layerId: string, kfs: Keyframe[], time: number) { const source = prev ?? next if (!source) return store.duplicateKeyframeAt(layerId, source.id, time) + // Move the editor onto the slot we just populated, so the stage reflects it. + if (store.activeButtonAssetId) { + if (Math.abs(time - BUTTON_STATE_TIMES.normal) < 1e-3) store.setActiveButtonState('normal') + else if (Math.abs(time - BUTTON_STATE_TIMES.hover) < 1e-3) store.setActiveButtonState('hover') + else if (Math.abs(time - BUTTON_STATE_TIMES.pressed) < 1e-3) store.setActiveButtonState('pressed') + else if (Math.abs(time - BUTTON_STATE_TIMES.disabled) < 1e-3) store.setActiveButtonState('disabled') + } } const EASING_OPTIONS: { value: Easing; label: string }[] = [ diff --git a/src/devtools/tests.ts b/src/devtools/tests.ts index ce7c377..bce97c9 100644 --- a/src/devtools/tests.ts +++ b/src/devtools/tests.ts @@ -6880,6 +6880,60 @@ export function registerAllTests(): void { expect(probe(BUTTON_STATE_TIMES.pressed)).toBe(false) expect(probe(BUTTON_STATE_TIMES.disabled)).toBe(false) }) + + test('clicking + on an empty button slot inserts the kf AND moves the editor to that state', async () => { + const s = makeFreshStore() + const btn = s.addButtonAsset('B', 100, 100) + s.enterButtonAssetEdit(btn.id) + // addLayer in button-edit seeds a single kf on the current state (normal). + const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#abc' }) + const layer = s.addLayer(a.id)! + expect(layer.keyframes).toHaveLength(1) + expect(s.activeButtonState).toBe('normal') + + const handle = mountComponent(Timeline, {}) + await nextTick() + // With a kf at normal, three empty slots remain: hover, pressed, disabled (in that order). + const adders = handle.host.querySelectorAll('.kf-add') + expect(adders.length).toBe(3) + // Click the last one — the "disabled" slot. + adders[adders.length - 1].click() + await nextTick() + expect(layer.keyframes).toHaveLength(2) + expect(layer.keyframes.some((k) => Math.abs(k.time - BUTTON_STATE_TIMES.disabled) < 1e-6)).toBe(true) + expect(s.activeButtonState).toBe('disabled') + expect(s.playhead).toBe(BUTTON_STATE_TIMES.disabled) + s.exitButtonAssetEdit() + handle.unmount() + }) + + test('clicking + falls back to the NEXT kf when no prev exists, and moves the editor', async () => { + // Pins the prev ?? next branch: with a lone kf at disabled, the + slot + // at hover must copy from the disabled kf and park the editor on hover. + const s = makeFreshStore() + const btn = s.addButtonAsset('B', 100, 100) + s.enterButtonAssetEdit(btn.id) + s.setActiveButtonState('disabled') + const a = s.addAsset({ kind: 'rect', name: 'R', width: 10, height: 10, fill: '#abc' }) + const layer = s.addLayer(a.id)! + expect(layer.keyframes).toHaveLength(1) + expect(layer.keyframes[0].time).toBe(BUTTON_STATE_TIMES.disabled) + + const handle = mountComponent(Timeline, {}) + await nextTick() + // Three empty slots: normal, hover, pressed (in that order). + const adders = handle.host.querySelectorAll('.kf-add') + expect(adders.length).toBe(3) + // Second = hover. + adders[1].click() + await nextTick() + expect(layer.keyframes).toHaveLength(2) + expect(layer.keyframes.some((k) => Math.abs(k.time - BUTTON_STATE_TIMES.hover) < 1e-6)).toBe(true) + expect(s.activeButtonState).toBe('hover') + expect(s.playhead).toBe(BUTTON_STATE_TIMES.hover) + s.exitButtonAssetEdit() + handle.unmount() + }) }) suite('button asset — data model + edit mode', () => {