Skip to content

feat(studio): fullscreen preview mode (F key)#997

Merged
miguel-heygen merged 1 commit into
mainfrom
feat/studio-fullscreen-preview
May 21, 2026
Merged

feat(studio): fullscreen preview mode (F key)#997
miguel-heygen merged 1 commit into
mainfrom
feat/studio-fullscreen-preview

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented May 21, 2026

Summary

  • Add distraction-free fullscreen mode using the HTML5 Fullscreen API
  • Press F to toggle fullscreen, Esc to exit
  • Hides timeline, sidebars, and editing overlays while keeping all playback shortcuts (Space, J/K/L, M, arrows, I/O, etc.) active
  • Adds a fullscreen toggle button to the player controls bar
  • Shortcut documented in the keyboard shortcuts panel

Implementation

  • useAppHotkeys handles the F key in the consolidated handler (works from both main window and preview iframe)
  • NLELayout tracks fullscreen state via useSyncExternalStore on fullscreenchange events and conditionally hides timeline/overlays
  • PlayerControls receives isFullscreen + onToggleFullscreen props for the toggle button

Test plan

  • Press F — composition enters native browser fullscreen, timeline and sidebars hidden
  • Press Esc — exits fullscreen, timeline and sidebars restored
  • Click the fullscreen button in player controls — same behavior
  • Space, J/K/L, arrow keys, M, Shift+L work in fullscreen
  • I/O, Shift+I/O, A/E work area shortcuts work in fullscreen
  • F key does NOT trigger when typing in an input/textarea (e.g., frame jump input)
  • Fullscreen button shows expand icon normally, compress icon when active (with accent color)
  • Timeline visibility state preserved after exiting fullscreen

Closes #995

Add distraction-free fullscreen mode using the HTML5 Fullscreen API.
Press F to enter fullscreen (Esc to exit). When active, the composition
fills the screen — timeline, sidebars, and editing overlays are hidden
while all playback shortcuts (Space, J/K/L, arrows, etc.) remain
functional. A fullscreen toggle button is also added to player controls.

Closes #995
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: COMMENT (would-be APPROVE)

Clean implementation, well-scoped (+118/-6 across 3 files), comprehensive test plan in the PR body. No blocking findings. Posting as COMMENT and pinging James on Slack for the merge greenlight per the trusted-stamper policy.


Sanity checks (no concerns — surfacing because "F key" deserves the explicit audit)

  • No F-key conflicts in the studio. Grepped packages/studio/ for event.key === "f", toLowerCase() === "f", key: "F" shortcut-section entries — clean. The strict modifier guard (!metaKey && !ctrlKey && !altKey && !shiftKey) correctly leaves Cmd+F (browser Find), Ctrl+F (Find), and Shift+F to the browser.
  • isEditableTarget coverage is comprehensive. packages/studio/src/utils/timelineDiscovery.ts:14-30 matches input/textarea/select, isContentEditable, role=textbox|searchbox|combobox, and the .cm-editor CodeMirror class. F won't fire while typing anywhere we care about. ✓
  • iframe-to-parent forwarding works. previewAppKeyDownHandler (useAppHotkeys.ts:289) delegates iframe-window keydown into the parent's handleAppKeyDownRef.current. The F-key handler executes in the parent's closure, so document.fullscreenElement, document.querySelector("[data-studio-fullscreen-target]"), and containerRef.current all resolve to the parent context. Same-origin iframe propagates user-activation, so requestFullscreen() succeeds even when F is pressed inside the preview iframe.
  • useSyncExternalStore shape is correct. subscribe/getSnapshot are declared at module scope (stable refs), the subscription cleanup is tidy, and Object.is comparison on Element | null re-renders only on actual fullscreen-state change. The null guard fullscreenElement === containerRef.current && fullscreenElement != null is necessary — without it, initial render with containerRef.current === null would falsely report isFullscreen=true while no fullscreen is active.
  • All three exit paths work: Escape (browser-native), F again (state-aware handler), button click (onToggleFullscreen). ✓

Non-blocking observations

  1. Two parallel toggle implementations. useAppHotkeys.ts:259-263 uses document.querySelector("[data-studio-fullscreen-target]") while NLELayout.tsx:271-277 uses containerRef.current. Both resolve to the same element in practice (single NLELayout instance), but they're decoupled — if a future change moves the data attribute, only one path breaks. Optional consolidation: export a toggleStudioFullscreen() helper that both paths call. Not a blocker.

  2. event.repeat not guarded. Holding F triggers keydown with auto-repeat firing requestFullscreen/exitFullscreen in rapid succession. Browsers tend to no-op back-to-back fullscreen API calls, but a one-liner if (event.repeat) return; at the top of the F-key block eliminates the race and the unsuppressed event.preventDefault() chatter. Tiny.

  3. PostHog tracking on button path only. The button onClick (PlayerControls.tsx:606-609) emits trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen }), but the F-key handler in useAppHotkeys doesn't. This is consistent with the existing convention — hf#992's M/Shift+L hotkeys also don't emit telemetry from the hotkey path; only their buttons do. So this isn't a regression and isn't a finding for this PR. Flagging only because: if accurate fullscreen-adoption metrics matter (keyboard vs button is probably 90/10 for an NLE keyboard shortcut), the convention may want a follow-up cleanup that centralizes telemetry behind the toggle action itself rather than the click handler.

  4. requestFullscreen() rejection swallowed. void containerRef.current.requestFullscreen() and void document.querySelector(...).requestFullscreen() both discard promise rejection. If the browser blocks (rare — usually only sandboxed iframes without allow="fullscreen", which doesn't apply here), the user sees no feedback. A .catch(() => showToast("Couldn't enter fullscreen", "error")) would help; or rely on the absence of state change and accept silence. Cosmetic.

  5. Sub-composition breadcrumb hidden in fullscreen. When the user enters fullscreen from inside a sub-composition, the breadcrumb (NLELayout.tsx:362-367) is hidden alongside the timeline — so the only way to navigate up is to exit fullscreen first (Escape / F / button). Intentional given "distraction-free preview" framing, but worth documenting in the keyboard shortcuts panel description if/when the feature gets a docs pass.


Test coverage note (not a blocker)

No new test added for the F-key handler. useAppHotkeys.ts doesn't have tests today either, so this matches the codebase convention — but hf#992 (the M/Shift+L PR) added usePlaybackKeyboard.test.ts cases for its new shortcuts, including the "doesn't fire in editable target" and "doesn't conflict with the modifier-less L" guard tests. If you want symmetry, the F-key block's predicate (shouldHandleFullscreenHotkey(event) returning bool from modifier + isEditableTarget checks) could be extracted to timelineDiscovery.ts next to shouldHandleTimelineToggleHotkey and unit-tested. Wholly optional.

— Rames Jusso

@miguel-heygen miguel-heygen merged commit 6b541b7 into main May 21, 2026
52 of 154 checks passed
@miguel-heygen miguel-heygen deleted the feat/studio-fullscreen-preview branch May 21, 2026 16:17
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.

feat(studio): Fullscreen (F key) Distraction-Free Composition Preview

2 participants