From d5cc1c9c6249c27a63269288d8b7771656eb50db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 7 Aug 2026 13:35:39 -0700 Subject: [PATCH] fix(studio): keep the preview alive when the window is tight (#3091) Panel sizes are reconciled against the window on every resize, with the preview holding a 360x200 floor that panels yield to before it gives. Measured preview pane: 760px window 192 -> 433, 560px window 2 -> 516. Windows at or above 1280px are unchanged. - fitPanels owns the who-yields decision for both axes - panel caps are window-relative, replacing a flat 600px inspector cap - below 860 the sidebar rails, below 700 the inspector collapses too - auto-collapse is derived render state and never writes leftCollapsed (localStorage) or rightCollapsed (synced into the shareable URL) - the rail and header toggles act on the effective state, so neither is a dead click that silently persists a collapse the user never asked for --- packages/studio/src/App.tsx | 4 +- .../src/components/StudioHeader.test.ts | 28 +++ .../studio/src/components/StudioHeader.tsx | 23 ++- .../src/components/StudioLeftSidebar.tsx | 4 +- .../studio/src/components/nle/NLEContext.tsx | 23 ++- .../components/nle/TimelineResizeDivider.tsx | 15 +- .../src/contexts/PanelLayoutContext.tsx | 9 +- .../studio/src/hooks/usePanelLayout.test.ts | 117 +++++++++++++ packages/studio/src/hooks/usePanelLayout.ts | 160 ++++++++++++----- packages/studio/src/utils/fitPanels.test.ts | 164 ++++++++++++++++++ packages/studio/src/utils/fitPanels.ts | 151 ++++++++++++++++ 11 files changed, 632 insertions(+), 66 deletions(-) create mode 100644 packages/studio/src/components/StudioHeader.test.ts create mode 100644 packages/studio/src/utils/fitPanels.test.ts create mode 100644 packages/studio/src/utils/fitPanels.ts diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index ffafec908..520b4e855 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -399,7 +399,7 @@ export function StudioApp() { } = useInspectorState( panelLayout.rightPanelTab, panelLayout.rightInspectorPanes, - panelLayout.rightCollapsed, + panelLayout.effectiveRightCollapsed, isPlaying, domEditSession.domEditSelection, gestureState === "recording", @@ -512,7 +512,7 @@ export function StudioApp() { /> } right={ - panelLayout.rightCollapsed ? null : ( + panelLayout.effectiveRightCollapsed ? null : ( { + it("opens when the panel is hidden", () => { + expect(shouldOpenInspector(true, false)).toBe(true); + }); + + it("opens when a non-inspector tab is showing", () => { + expect(shouldOpenInspector(false, false)).toBe(true); + }); + + it("closes when the inspector is genuinely on screen", () => { + expect(shouldOpenInspector(false, true)).toBe(false); + }); + + it("opens when the window railed the panel away", () => { + // The regression this guards: the button used to branch on the raw + // rightCollapsed intent, which is still `false` while the window has the + // panel railed. That took the close branch, wrote rightCollapsed=true, and + // since that value is synced into the shareable Studio URL, a click that + // did nothing visible rewrote the link. + const userIntentIsOpen = false; + const windowRailedItAway = true; + expect(shouldOpenInspector(windowRailedItAway, true)).toBe(true); + expect(shouldOpenInspector(userIntentIsOpen, true)).toBe(false); + }); +}); diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 6154a4d33..0e9855f82 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -196,6 +196,21 @@ export function ViewModeToggle() { ); } +/** + * Does the header's Inspector button open the panel, or close it? + * + * Takes the EFFECTIVE collapse state, so a panel the window has railed away + * counts as closed even though the user's stored intent still says open. The + * argument name is the guard: passing raw intent here is the bug this exists + * to keep out. + */ +export function shouldOpenInspector( + effectiveRightCollapsed: boolean, + inspectorPanelActive: boolean, +): boolean { + return effectiveRightCollapsed || !inspectorPanelActive; +} + // fallow-ignore-next-line complexity export function StudioHeader({ captureFrameHref, @@ -208,7 +223,11 @@ export function StudioHeader({ onExport, }: StudioHeaderProps) { const { projectId, editHistory, handleUndo, handleRedo, renderQueue } = useStudioShellContext(); - const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext(); + // effectiveRightCollapsed, not the raw intent: in the auto-railed state the + // intent is still "open" while the panel is hidden, so branching on intent + // made this button write rightCollapsed=true — and that value is synced into + // the shareable Studio URL, so a dead click would rewrite a link. + const { effectiveRightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext(); const isRendering = renderQueue.isRendering; return ( @@ -328,7 +347,7 @@ export function StudioHeader({