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({