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

+43
View File
@@ -685,6 +685,20 @@ export function registerAllTests(): void {
expect(r).toContain('shadowColor')
expect(r).toContain('out.blur')
})
test('runtime rangeOut falls back to scene.duration (not last keyframe)', () => {
// Regression: the runtime used to fall back to `effectiveEnd(scene)`
// — the time of the last keyframe — when scene.outTime was unset.
// That made fullscreen preview loop at the last kf, ignoring any
// blank tail before the OUT mark and any still-frame-past-end
// extension. The fallback is now scene.duration, matching the
// editor's `playbackOut` getter.
const r = generateRuntimeScript()
// The old helper must be gone entirely.
expect(r.indexOf('effectiveEnd')).toBe(-1)
// And the current fallback must be scene.duration.
expect(r).toContain('scene.outTime==null?scene.duration:scene.outTime')
})
})
suite('playback — transport', () => {
@@ -4350,6 +4364,35 @@ export function registerAllTests(): void {
expect(title === 'timeline.renameScriptTrackDialog.title').toBe(false)
expect(confirm === 'timeline.renameScriptTrackDialog.confirm').toBe(false)
})
test('sidebar noClips / noButtons empty-states both suggest "yet"', () => {
// Regression: the clip library used to read "No clips" while the
// button library read "No buttons yet". Minor inconsistency that
// the user called out — both should suggest "more coming", not
// "there will never be any".
expect(i18nT('sidebar.noClips')).toContain('yet')
expect(i18nT('sidebar.noButtons')).toContain('yet')
})
test('holdDisabledHint key is gone (held kfs keep easing now)', () => {
// Regression: "Still frame — easing disabled" used to appear in
// the keyframe context menu and the easing buttons were grayed
// out. A held kf's easing still matters for the segment INTO it
// from the previous keyframe, so the restriction was wrong.
// Pin that the note is no longer emitted.
const v = i18nT('timeline.holdDisabledHint')
expect(v === 'timeline.holdDisabledHint').toBe(true)
})
test('app.saveProject tooltip mentions the Ctrl/Cmd+S shortcut', () => {
// Regression: Cmd/Ctrl+S was added as a keyboard save shortcut
// alongside the dirty-state dot. Surface it in the tooltip so
// the shortcut is discoverable; assert both the idle and the
// unsaved-changes variant stay in sync with the handler.
expect(i18nT('app.saveProject')).toContain('Ctrl/Cmd+S')
expect(i18nT('app.saveProjectDirty')).toContain('Ctrl/Cmd+S')
expect(i18nT('app.saveProjectDirty')).toContain('unsaved')
})
})
suite('timeline — scrub-handle obscured logic', () => {