Skip to content

feat(studio): all panels move onto the dockable, persisted layout - #4192

Merged
miguel-heygen merged 14 commits into
mainfrom
feat/studio-dock-migration
Sep 19, 2026
Merged

miguel-heygen merged 14 commits into
mainfrom
feat/studio-dock-migration

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

What

Moves all ten Studio panels onto the dock (Dock.Root/Dock.Panel) built in #4165 and #4186: users can drag any panel to any side, close and reopen it, resize by dragging a sash or with the keyboard, and the layout persists across a reload. Measured against main with #4165 and #4186 already merged.

Before

Fixed left/right columns, no dragging.

before

After

Default layout, a panel moved, a panel closed then reopened, layout persisted across reload, a panel drag, and a narrow window at 560px.

Default dock layout:
after-default

A panel moved to another zone:
after-moved

A panel closed:
after-closed

The same panel reopened:
after-reopened

Layout persisted across a reload:
after-persisted

Window > Reset layout returns to default:
after-reset

Catalog panel open, a WebGL effect clip on the timeline, playhead at 00:02, rendering correctly on the dock layout:
after-catalog-cosmic-orb

Inspector open with an element selected:
after-inspector-open

Inspector closed:
after-inspector-closed

560px window, everything still fits:
after-560

Dragging a panel to a new dock position:
https://github.com/user-attachments/assets/f3201fb5-5939-47e8-92c3-51be3a7ae5e4

Accepted behaviour changes (disclosed, not regressions of a spec)

  • EditorShell takes a panels prop instead of separate left/right props.
  • Old layout prefs (leftCollapsed, leftWidth, rightWidth, timelineHeight) are dropped with no migration; the dock's own persisted layout replaces them.
  • Tab order can change after a panel is closed and reopened (it reopens as a new dock group, not spliced back into its old slot).
  • Zone membership is a property of the panel registry: dragging a right-zone panel into the left column changes what "collapse left" hides.
  • Block params are dismissed whenever the right column collapses or the Design tab stops showing, not only when another right-panel tab is picked. One known gap: clicking the Design tab again while it is already active does not dismiss params (only the panel's own close button does); the dock's tab click goes through the dock store, not the wrapped setter.
  • Left-column tab-switch telemetry is removed (no dock-level replacement yet).
  • The old VITE_STUDIO_FLAT_INSPECTOR_ENABLED=false split-inspector path is removed; docs/weekly-updates.mdx still references it as a historical note.
  • The e2e thumbnail selector widened to .hf-dock img[src*="/thumbnail/"].

Verification

Audited / Trusting / Not exercised

  • Audited: every setRightPanelTab caller and its revealPanel flag, the hotkey handlers, the slideshow effect's transition logic, mutation testing of the above, every rebase conflict hunk diffed against both parents.
  • Trusting: the full-suite pass count as reported by the test run (not independently re-run test-by-test).
  • Not exercised: a manual browser walk of a project switch or full layout reset while block params are open; a transient-null slideshow file reload.

Not in scope

Keyboard sash resize, tab arrow navigation and the narrow-window minimum fit landed in #4186. The desktop-host export surface (letting an embedding app build its own layout from this package) is a separate follow-up PR.

@mintlify

mintlify Bot commented Sep 19, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Sep 19, 2026, 10:03 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not stamping this head, for two reasons that are both cheap to clear. The review below is the real work — the conflict diagnosis at the end should make the rebase a two-minute job.

1. The head is DIRTY — the tree that merges cannot be the tree I read

mergeable: false, mergeable_state: dirty, rebaseable: false, behind_by: 1. Approving here stamps a tree that someone has to resolve first, and on this repo the stamp would be void anyway: require_last_push_approval: true means the resolution push re-gates it (observed live on #4181 earlier today — the approval stayed APPROVED while reviewDecision went back to REVIEW_REQUIRED). So "one approval on this head clears the gate" does not hold at cdfa6060; GitHub will not merge a conflicting PR at any approval count.

The conflict is small, mechanical and entirely mine to have caused. behind_by: 1 is fd6049f3 — the merge of #4189, which I approved eleven minutes before this PR opened. #4189 touched three files; the only one this PR also touches is packages/studio/src/hooks/useAppHotkeys.ts, and this PR's change to it is four deleted lines, no additions, all removing leftSidebarRef / LeftSidebarHandle:

hunk line removed conflicts?
1 import type { LeftSidebarHandle } … yes#4189 deleted the adjacent STUDIO_MOTION_PATH import
2 leftSidebarRef: React.RefObject<…> in UseAppHotkeysParams yes#4189 replaced the adjacent syncHistoryPreviewAfterApply type with UseEditHistoryActionsOptions["syncHistoryPreviewAfterApply"]
3 leftSidebarRef, in the destructure no — context unchanged
4 leftSidebarRef, in cbRef.current no — context unchanged, offset only

Both conflicts are contextual, not semantic: #4189 moved the undo/redo wiring out of that file, this PR removes a sidebar ref from it, and the two never touch the same line. The resolution is to take #4189's version of the file and delete those four leftSidebarRef lines. All four still exist in it.

2. Eight deletions, zero entries in ALLOWED_DELETIONS

scripts/check-no-main-deletions.mjs runs git diff --name-status -M <base>...HEAD and fails on any D path not named in its 37-entry map. This branch proposes eight, and none is in it:

packages/studio/src/components/PanelTabButton.tsx
packages/studio/src/components/StudioLeftSidebar.tsx
packages/studio/src/components/nle/TimelineResizeDivider.tsx
packages/studio/src/components/sidebar/LeftSidebar.storage.test.ts
packages/studio/src/components/sidebar/LeftSidebar.tsx
packages/studio/src/hooks/useInspectorSplitResize.ts
packages/studio/src/utils/fitPanels.test.ts
packages/studio/src/utils/fitPanels.ts

Git's own -M may pair one or two of these with the new files, but fitPanels.ts, useInspectorSplitResize.ts, TimelineResizeDivider.tsx and PanelTabButton.tsx have no plausible partner among the additions, so at minimum those land as D.

I'm raising it as a review point rather than a red check because the reason strings are the thing I'd otherwise be asking you for in prose. The script's own comment puts it well — naming each path "makes every intentional removal a reviewable line in a diff." Eight one-line reasons is the deletion audit for this PR, and it's the artifact the next person reading git log for "where did the resize code go" will actually find.

Findings in the code

visiblePanelInZone is called with openPanels at one site, and the name says the opposite. The helper's parameter is visiblePanels and its doc says "the last one focused if still showing, else any showing" (dockLayoutStore.ts:100-105). useRightPanelIntent (useRightPanelIntents.ts:59-62) passes state.openPanels, and its own doc says "whether or not the column is currently showing" — deliberately the negation. The call is right for what that hook wants; the helper's name and parameter are what's wrong now that it has two meanings. panelInZoneFrom(zone, lastActive, candidates) would let both call sites read honestly.

sync's two writes disagree about the center zone. In dockLayoutStore.ts:59-68, the groupActivePanels loop guards if (zone !== "center"), but the activePanel write two lines below does not, so lastActive.center can be set by one path and never by the other. Inert today — every call site of visiblePanelInZone passes "right", and the new store test only asserts left and right — but the two paths give different answers the moment a center reader exists. Either guard both or neither.

The test-count drop reads worse than it is. usePanelLayout.test.ts goes 18 named cases → 9, −296/+108. I compared the name sets: the departed cases are almost entirely width, rail and auto-collapse behaviour ("caps a panel relative to the window instead of at a flat 600px", "rails both panels once the window cannot fit them", "forgets that reopen once the window is wide again") — they tested fitPanels/useInspectorSplitResize, which this PR deletes, so they left with their subject rather than leaving a hole. The nine replacements cover the dock-backed surface including the URL-restore path. Worth stating in the body, because the raw line count invites the opposite reading.

Credit where the cleanup is careful:

  • .fallowrc.jsonc removes its own now-dead ignore entries for LeftSidebar.tsx rather than leaving them to rot, and fixes the stale StudioRightPanelStudioRightPanels references. Deleting your own suppressions in the same PR is the part that usually gets skipped.
  • detach now clears lastActive (:58), so a second dock doesn't inherit the first one's memory — and the new test pins exactly that.
  • Gating the setTitle effect on open (Dock.tsx:218-222) stops title writes to panels that aren't in the dock.
  • 63 lines of new hook arrived with 181 lines of test, and all five exported hooks have real call sites in App.tsx / StudioRightPanels.tsx — no speculative surface.
  • studioUiPreferences drops the four dead keys from the reader, not just the type, so stale leftWidth / timelineHeight values in someone's storage are ignored rather than half-honoured.

Scope

I audited the dock store and its snapshot producer, usePanelLayout, useRightPanelIntents, the preference reader, the fallow config, the deletion set and the conflict surface. I did not audit EditorShell.tsx, App.tsx's wiring, StudioLeftPanels.tsx, TimelinePane.tsx or the e2e script — at 54 files I scoped to the state machine and the removals rather than claiming the whole diff.

Ping me at the rebased head with the ALLOWED_DELETIONS entries and I'll approve there.

— Rames

…onto it

EditorShell, App.tsx, the left and right panel columns, and the
inspector hooks now render through Dock.Panel instead of the old
fixed-width sidebars and split-pane inspector. usePanelLayout becomes
a thin facade over the dock store; fitPanels, the resize divider and
the split-inspector hook are dead and removed.
…after dock migration

Collapse inspectorButtonActive into inspectorPanelActive, remove unread
layers/design active fields, un-export unused prop types, flatten a nested
ternary, and point fallow ignores at the renamed/deleted files.
Block params dismiss on selection or when Design is hidden, the slideshow
panel can be closed, the selection outline follows the inspector intent,
caption editing focuses Design, and a URL slideshow tab opens once the
panel exists.
…open after the dock migration

Block params stay open over an element that was already selected, caption mode wins over block params, the slideshow panel waits for the dock, the Renders tab title survives a reopen, each side column remembers the tab it last showed, tab switches are reported only when the tab changes, and the e2e thumbnail selector follows the dock.
…ement, and panel titles are covered

The dismissal compares the selected element by its target key instead of object identity, because a preview reload rebuilds the selection for the same element. A closed and reopened panel keeps its custom title, with a test, and the e2e selector is formatted.
…ed slideshow placement survives loading

Block params are now dismissed by the tab setter the way their old tab was, including a re-pick of the same element, instead of by comparing selections. The slideshow panel is only closed after a file that was a slideshow stops being one, so a restored layout is not discarded while the file loads.
The rebase onto main left two identical inspectorButtonActive/inspectorOpen
constants after a conflict resolution; only the first was still referenced.
StudioLeftSidebar.tsx, LeftSidebar.tsx and its storage test,
TimelineResizeDivider.tsx, useInspectorSplitResize.ts and fitPanels
are replaced by the dock; the CI deletion guard needs each named
with its reason.
… its dock panel wrapper

Every dock panel wrapper now carries aria-label=<title>, matching the "Timeline"
title for the timeline panel. Timeline.tsx also set aria-label="Timeline" on its
own content div, so `document.querySelector('[aria-label=\"Timeline\"]')` found
the dock wrapper first and the timeline viewport gate CI check timed out waiting
for an attribute the real content div actually carried. Renamed to "Timeline
track view" and updated every consumer: the production diagnostics selector,
its tests, and the e2e viewport-gate script.
…ase regression

Rebasing PR2 onto main replayed an older local commit whose diff did not
textually overlap main's newer inspectorButtonActive prop, so git applied
it with no conflict marker and silently reverted the header to computing
the value itself instead of receiving it. StudioHeader.dom.test.tsx already
asserts the prop is respected; restoring it makes that test meaningful
again. App.tsx now computes it once (panelLayout.rightCollapsed is already
the dock's effective collapse state, no separate railed-by-width case) and
passes it down.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at 7ff2a740. Both things I held on at cdfa6060 are cleared, and I re-read everything that entered the patch since.

The two conditions

1. The head is no longer DIRTY. The useAppHotkeys.ts conflict with #4189's merge is resolved.

2. All seven deletions are declared. scripts/check-no-main-deletions.mjs gains +25 lines and every removed path is now a key in ALLOWED_DELETIONS, each with a reason that names its replacement:

deleted declared reason checks out
StudioLeftSidebar.tsx StudioLeftPanels.tsx, added at +202
LeftSidebar.tsx → dock-mounted CompositionsPanel.tsx, added at +15
LeftSidebar.storage.test.ts tested fixed-sidebar tab persistence; the dock's own persistence gets dockLayoutStore.test.ts at +28
fitPanels.ts / fitPanels.test.ts computed fixed left/right widths for the old shell
useInspectorSplitResize.ts resized the old split pane
TimelineResizeDivider.tsx superseded by the dock's sash

It was eight when I looked; PanelTabButton.tsx moved to #3624. Worth noting the surviving behavior was carried over rather than dropped — LeftSidebar.masterComposition.test.tsx and LeftSidebar.lintBadge.test.tsx are renames, not deletions.

What changed since I read it, and why I re-read it

Six files entered the patch. Five are one mechanical rename, and I audited it as a contract change because a miss here fails silently: readTimelinePerformanceDiagnostics returns the zero baseline when its selector finds nothing, which is indistinguishable from "timeline not mounted" — a stale consumer would report healthy zeros forever.

aria-label="Timeline""Timeline track view" has exactly four consumers on main, and this PR updates all four: the producer (player/components/Timeline.tsx), the selector (timelinePerformanceDiagnostics.ts), its unit test, and the timeline-virtualization.mjs e2e. Nothing left pointing at the old label.

Same check on the other contract change, since StudioHeader.dom.test.tsx's mock swapped effectiveRightCollapsedrightCollapsed and a stale reader of that key would go quietly undefined/falsy rather than fail: effectiveLeftCollapsed/effectiveRightCollapsed resolve to seven files on main, and all seven are in this patch removing them. The derived auto-rail-by-width state is gone wholesale with the fixed sidebars, not half-removed.

Approving on code merit; I did not use CI as a basis.

— Rames

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-stamp at a17f9c47 — my approval at 7ff2a740 was re-gated by the push, as require_last_push_approval does on this repo.

The only delta is a17f9c47 itself: a doc comment in useRightPanelIntents.ts wrapped from one line to two, 2+/1-, no code change and no wording change. Every file's patch is otherwise byte-identical to the head I reviewed, so the verification in my previous review stands unchanged.

— Rames

@miguel-heygen
miguel-heygen merged commit 98e8372 into main Sep 19, 2026
40 checks passed
@miguel-heygen
miguel-heygen deleted the feat/studio-dock-migration branch September 19, 2026 23:04
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