0.8.4: button transitions, locked groups revoke edits, UX polish

Button-state transitions
- State flips on stage-preview buttons (normal / hover / pressed /
  disabled) now ease over 50ms instead of snapping. New
  `transitions` map + rAF ticker in useButtonRuntime drive a
  continuous sample time between BUTTON_STATE_TIMES slots;
  CanvasStage samples each button child at that interpolated time
  so keyframes at the 4 states blend smoothly. Mid-flight flips
  (hover → pressed while still in normal → hover) rebase from
  the current live sample instead of snapping.

Locked groups revoke edits
- Locking a layer directly or its enclosing group deselects the
  layer so the Properties panel can't keep editing it. Panel also
  splits into three branches — editable / locked / no-layer —
  with a dedicated locked-layer hint.
- Timeline keyframe interactions (drag, dblclick-to-remove,
  context menu, dblclick-to-create on lane, add-kf toolbar)
  now consult effective lock (direct + parent group) instead
  of just layer.locked.

Editor polish
- Inactive tracks fade as a whole row (opacity 0.5) instead of
  per-child, so keyframes, segments, and still-frame tails stay
  visually proportional to their active counterparts.
- Dragging a layer onto a group's track-name label now drops
  it INTO the group (was previously only the group-lane area).
- Button assets on the stage show a pointer cursor on hover so
  it's obvious they're interactive.
- Global CSS forces `cursor: pointer !important` on every enabled
  button / role="button" so scoped component styles can no longer
  accidentally shadow the default.
- Timeline resize handle jumped above the scrub-bar (z-index 100)
  so the full 14px grab zone is hit-testable again.

Regression tests
- setTrackLocked deselect paths: direct lock, group lock with
  selected child, group lock with unrelated selection, unlock
  doesn't re-select.
- duplicateKeyframeAt: props + easing + hold + bezier copied,
  collision refusal, unknown-id guards, separated-track routing.
- Button transition runtime: no-transition baseline, mid-flight
  bounds, mid-transition rebase, no-op setState.
- Shared composables tooltips + asset-thumbs: same ref across
  calls, localStorage round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-22 04:31:08 +02:00
1 parent 1d84c34c15
commit af5f99380c
12 files changed
+401 -34

No files matched your search

+17 -1
View File
@@ -74,6 +74,19 @@ const sampled = computed<AnimatableProps | null>(() =>
selected.value ? sampleLayer(selected.value, store.playhead) : null,
)
/** True when the selected layer is locked directly or reached through
* a locked parent group. Suppresses the edit UI — the user must
* unlock the layer (or the group) before changing properties. */
const selectedIsLocked = computed(() => {
const l = selected.value
if (!l) return false
if (l.locked) return true
for (const g of store.scene.groups ?? []) {
if (g.locked && g.childIds.includes(l.id)) return true
}
return false
})
const hasKeyframeHere = computed(() => {
if (!selected.value) return false
return selected.value.keyframes.some((k) => Math.abs(k.time - store.playhead) < 1e-3)
@@ -375,7 +388,7 @@ function removeKeyframeHere() {
<template>
<div class="panel">
<h3>Properties</h3>
<template v-if="selected && sampled">
<template v-if="selected && sampled && !selectedIsLocked">
<label>Name<input v-model="selected.name" /></label>
<div class="kf-row" :class="{ on: hasKeyframeHere }">
@@ -645,6 +658,9 @@ function removeKeyframeHere() {
{{ t('properties.deleteLayer') }}
</button>
</template>
<p v-else-if="selected && selectedIsLocked" class="hint">
{{ t('properties.lockedLayer') }}
</p>
<p v-else class="hint">{{ t('properties.noLayer') }}</p>
</div>
</template>