feat: 5-frame button assets + timeline/sidebar polish

Button assets
- Re-introduce button-asset type with 4 content states (normal/hover/
  pressed/disabled) stored as keyframes at BUTTON_STATE_TIMES, plus a
  separate hit-area editor mode.
- useButtonRuntime composable for per-instance state/disabled flags on
  the main stage.
- Sidebar library lists button assets with live 80×80 thumbnails;
  renderButtonThumbnail now accepts an images map so image-kind layers
  inside a button render instead of producing a fully-transparent PNG.
- Compact (no-thumbnail) mode aligns button rows to the library-row
  height.

Scope footer
- Button-edit mode now uses the same sidebar scope-footer pattern as
  clip-edit (back button to "Main timeline" + active scope chip);
  removed the inline exit button from the timeline scrub-bar.

Timeline
- Track-name column default bumped to 260px to match the assets sidebar
  above it (storage key bumped to :v2 so stale values reset).
- scrub-bar z-index raised above sticky track-name columns so rows
  scroll under it instead of over.
- scrub-handle flips z-index based on horizontal scroll: above
  name-spacer when the playhead is in view (no half-clip at t=0),
  below when the playhead has scrolled off the left edge (so the
  toggle row cleanly covers it).
- name-spacer extends ±3/1px to fully cover the obscured handle.
- scrub-handle head pokes 3px above the ruler with rounded top corners
  via clip-path path().

Track context menu
- Added missing i18n keys (renameScriptTrack / deleteScriptTrack /
  renameScriptTrackDialog.{title,confirm}).
- Rename items show a visible pencil icon; empty .check slot removed
  so items line up flush-left; toggle items place their active ✓ on
  the right (kf-menu-trailing-check).

Dev HUD
- Test progress bar flips to --danger as soon as the first failure is
  recorded, instead of only at the end of the run.

Regression tests
- renderButtonThumbnail: null on empty button, renders shape layers,
  accepts custom images map.
- i18n: renameScriptTrack / deleteScriptTrack / dialog keys resolve.
- scrub-handle-obscured logic (visible at t=0, hidden when playhead is
  scrolled off-screen, still visible when playhead is far to the
  right of the scroll offset).
- Updated button state-scrubber expectations to match
  BUTTON_STATE_TIMES (normal=1, hover=2, pressed=3, disabled=4) and
  that hit-mode parks at normal=1 rather than 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-21 19:19:39 +02:00
1 parent 31604052ca
commit c3764201b8
16 files changed
+2560 -349

No files matched your search

+22 -2
View File
@@ -15,13 +15,14 @@ import {
} from '@phosphor-icons/vue'
import { useSceneStore } from '@/stores/scene'
import { sampleLayer } from '@/lib/interpolate'
import type { AnimatableProps } from '@/types/scene'
import type { Action, AnimatableProps } from '@/types/scene'
import RangeSlider from './RangeSlider.vue'
import ActionEditor from './ActionEditor.vue'
import { t } from '@/i18n'
const SECTION_STORAGE_KEY = 'animatr:propSections'
type SectionKey = 'transform' | 'text' | 'polygon' | 'appearance' | 'shadow' | 'mask'
type SectionKey = 'transform' | 'text' | 'polygon' | 'appearance' | 'shadow' | 'mask' | 'button'
const defaultOpen: Record<SectionKey, boolean> = {
transform: true,
@@ -30,6 +31,7 @@ const defaultOpen: Record<SectionKey, boolean> = {
appearance: true,
shadow: false,
mask: false,
button: true,
}
function readStoredOpen(): Record<SectionKey, boolean> {
@@ -92,6 +94,11 @@ const polygonAsset = computed(() => {
return a && a.kind === 'polygon' ? a : null
})
const buttonAsset = computed(() => {
const a = selectedAsset.value
return a && a.kind === 'button' ? a : null
})
const polygonPointCount = computed<number>({
get: () => polygonAsset.value?.points.length ?? 0,
set: (v: number) => {
@@ -620,6 +627,19 @@ function removeKeyframeHere() {
</div>
</section>
<section v-if="buttonAsset" class="prop-group">
<button class="group-header" @click="toggleSection('button')">
<component :is="sectionOpen.button ? PhCaretDown : PhCaretRight" :size="11" />
<span>{{ t('properties.sections.button') }}</span>
</button>
<div v-if="sectionOpen.button" class="group-body">
<ActionEditor
:actions="selected.onClick ?? []"
@update:actions="(v: Action[]) => store.setLayerOnClick(selected!.id, v)"
/>
</div>
</section>
<button class="danger-btn" @click="store.removeLayer(selected.id)">
<PhTrash :size="13" />
{{ t('properties.deleteLayer') }}