0.8.1: dirty-state + Cmd/Ctrl+S, fullscreen plays to OUT, UX fixes

- Unsaved-changes indicator: a small accent dot appears on the save
  button whenever the scene has changed since the last save/load.
  Save/load/new all mark the project clean on next tick.
- Cmd/Ctrl+S saves the project (reuses the same flow as the button;
  prompts for a name on first save). Tooltip now mentions the
  shortcut.
- Fullscreen preview plays to the OUT marker (or scene.duration when
  no OUT is set) instead of looping at the last keyframe. Dropped
  the runtime's effectiveEnd helper; RUNTIME_VERSION 13 → 14.
- "Still frame" keyframes no longer disable easing. A held kf's
  easing still drives the segment INTO it from the previous kf, so
  you can ease into a pause. Only the first kf disables easing
  (nothing precedes it). Dropped `timeline.holdDisabledHint`.
- Settings dialog: hint text 11px → 13px (and dropped italic) so
  the inline help is readable; section headings 10px → 13px.
- Canvas stage shadow: `shadowCfg` always emits explicit values plus
  a `shadowEnabled` flag instead of returning `{}`. Konva's default
  shadowOpacity is 1, so bailing with `{}` could leave stale shadow
  attrs on the shape. Preview and editor now gate on the same
  condition deterministically.
- Sidebar empty-state copy: "No clips" → "No clips yet" to match
  "No buttons yet".

Regression tests: runtime rangeOut fallback is scene.duration (not
last kf); sidebar empty-states both contain "yet"; the
holdDisabledHint key is gone; saveProject tooltips mention the
shortcut.

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 22:00:35 +02:00
1 parent 1e89ea86e0
commit 82ee5c98d0
10 files changed
+133 -48

No files matched your search

+6 -2
View File
@@ -840,11 +840,14 @@ const stackItems = computed<StackItem[]>(() => {
})
function shadowCfg(props: AnimatableProps): Record<string, unknown> {
// Konva's default shadowOpacity is 1, so returning {} for the "no
// shadow" case isn't enough — the shape would reuse whatever values
// were set on a previous render. Always emit explicit values and let
// `shadowEnabled` be the single source of truth. The gate matches
// the canvas/preview path: opacity > 0 AND some blur or distance.
const opacity = props.shadowOpacity ?? 0
if (opacity <= 0) return {}
const blur = props.shadowBlur ?? 0
const distance = props.shadowDistance ?? 0
if (blur <= 0 && distance <= 0) return {}
const angle = props.shadowAngle ?? 90
const rad = (angle * Math.PI) / 180
return {
@@ -853,6 +856,7 @@ function shadowCfg(props: AnimatableProps): Record<string, unknown> {
shadowOpacity: opacity,
shadowOffsetX: Math.cos(rad) * distance,
shadowOffsetY: Math.sin(rad) * distance,
shadowEnabled: opacity > 0 && (blur > 0 || distance > 0),
}
}