Skip to content

fix: improve studio timeline discoverability#431

Merged
miguel-heygen merged 1 commit intomainfrom
fix/studio-timeline-discovery
Apr 23, 2026
Merged

fix: improve studio timeline discoverability#431
miguel-heygen merged 1 commit intomainfrom
fix/studio-timeline-discovery

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Apr 23, 2026

Summary

  • add a dismissible timeline-editor callout that explains drag/resize editing and points people to the recovery shortcut
  • make the Studio timeline toggle explicit in both the header and player controls, including a visible Shift+T hint
  • wire a guarded Shift+T hotkey for showing and hiding the timeline from both the Studio shell and preview iframe

Testing

  • bunx oxlint packages/studio/src/App.tsx packages/studio/src/player/components/PlayerControls.tsx packages/studio/src/utils/timelineDiscovery.ts packages/studio/src/utils/timelineDiscovery.test.ts
  • bun run --filter @hyperframes/studio typecheck
  • bun run --filter @hyperframes/studio test
  • verification against a live hyperframes preview session, including a recording pass for show/hide and dismiss persistence

https://www.loom.com/share/02d914b28434482cbe8ca0bc889a1655

@vanceingalls
Copy link
Copy Markdown
Collaborator

Review notes

Branch is well-scoped and clearly motivated. Two real issues, plus a few nits/follow-ups.

Issues

1. Cross-origin iframe will throw SecurityError

syncPreviewTimelineHotkey blindly calls addEventListener / removeEventListener on iframe.contentWindow:

const syncPreviewTimelineHotkey = useCallback(
  (iframe: HTMLIFrameElement | null) => {
    const nextWindow = iframe?.contentWindow ?? null;
    if (previewHotkeyWindowRef.current === nextWindow) return;
    if (previewHotkeyWindowRef.current) {
      previewHotkeyWindowRef.current.removeEventListener("keydown", handleTimelineToggleHotkey);
    }
    previewHotkeyWindowRef.current = nextWindow;
    nextWindow?.addEventListener("keydown", handleTimelineToggleHotkey);
  },

Today the preview src is same-origin (/api/projects/${projectId}/preview via <hyperframes-player> in packages/studio/src/player/components/Player.tsx), so this works. But the rest of Player.tsx already wraps iframe.contentWindow access in try/catch precisely to defend against transient/cross-origin states (see the comment on hasUnloadedAssets). Any future change that points the preview at a CDN, sandbox URL, or srcdoc with a different origin will throw an unhandled SecurityError here. Worth a try/catch for parity:

try {
  previewHotkeyWindowRef.current?.removeEventListener("keydown", handleTimelineToggleHotkey);
} catch {
  // cross-origin or detached window
}
previewHotkeyWindowRef.current = nextWindow;
try {
  nextWindow?.addEventListener("keydown", handleTimelineToggleHotkey);
} catch {
  previewHotkeyWindowRef.current = null;
}

2. Hint never appears for first-time users on an empty composition

{timelineVisible && timelineElements.length > 0 && !timelineEditorHintDismissed && (...)}

The timelineElements.length > 0 gate means a brand-new project (no clips yet) never sees the hint until they've added a clip — which is exactly the moment the hint becomes redundant ("here's how to drag clips" arrives after they've figured out clip authoring). Recommend dropping the length > 0 gate; the callout still degrades gracefully when empty.

Nits / follow-ups

  • useMountEffect closure trap. useMountEffect(...) for the global window listener and the cleanup useEffect(..., [handleTimelineToggleHotkey]) both close over handleTimelineToggleHotkey. The callback chain (toggleTimelineVisibility[], handleTimelineToggleHotkey[toggleTimelineVisibility]) is stable today, so this is correct. It's fragile though — if anyone adds a dep to toggleTimelineVisibility later, the global-window listener silently captures a stale callback (because useMountEffect discards its dep array). Consider switching the global-window registration to a normal useEffect(..., [handleTimelineToggleHotkey]) for symmetry with the iframe effect.

  • contentWindow identity invariant. syncPreviewTimelineHotkey relies on the fact that iframe.contentWindow identity changes on navigation, so the previewHotkeyWindowRef.current === nextWindow short-circuit + re-bind logic works correctly across composition switches. Worth a one-line code comment near the identity check — the invariant is non-obvious.

  • A11y. The dismiss button doesn't announce to screen readers. Low priority for an opt-out hint, but aria-live=\"polite\" on the callout container would let screen-reader users hear it appear.

  • Wiring test. Tests cover the pure utility but not the App.tsx wiring (hotkey → toggle, dismissal persists). A single RTL test that fires Shift+T on document and asserts setTimelineVisible flips would catch regressions cheaply. Not blocking.

  • CI Lint failure is a registry flake (Fail extracting tarball for \"@fontsource/roboto\" → operation canceled). Re-run; no code change needed.

Verdict

LGTM with two small asks before merge: add try/catch around the iframe listener calls, and drop the timelineElements.length > 0 gate so the hint is reachable from empty compositions. Everything else is post-merge follow-up.

@miguel-heygen miguel-heygen merged commit aea85af into main Apr 23, 2026
40 of 48 checks passed
@miguel-heygen miguel-heygen deleted the fix/studio-timeline-discovery branch April 23, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants