fix(studio): show Layers full-height in the flat inspector, not split with Design

The flat inspector split Layers and Design into a vertically-resizable
stacked pair whenever both panes were toggled on, mirroring the legacy
panel's layout. For the flat redesign this reads as two competing panels
crammed into one column; Layers should always render full-height by
itself there instead.

Gate the split-view branch behind !STUDIO_FLAT_INSPECTOR_ENABLED so it
still applies to the legacy panel, and fall through to Layers rendering
alone (the existing `layersPaneOpen` branch already does this — it just
never got reached previously because the split check ran first).

Also added setExclusiveRightInspectorPane (radio-style: selecting one pane
turns the other off) and use it for the Design/Layers tab clicks under the
flat flag, since leaving both panes independently toggleable would highlight
both tabs as "active" while only one actually renders.

New usePanelLayout.test.ts covers both the existing toggle behavior and the
new exclusive variant. Full studio suite (2634 tests) green; typecheck/
oxlint/oxfmt clean.
This commit is contained in:
Vance Ingalls
2026-07-15 19:05:38 -07:00
parent c621fd0f29
commit 79b688f204
4 changed files with 95 additions and 2 deletions
@@ -12,7 +12,10 @@ import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
import type { RenderJob } from "./renders/useRenderQueue";
import type { BlockParam } from "@hyperframes/core/registry";
import type { IframeWindow } from "../player/lib/playbackTypes";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "./editor/manualEditingAvailability";
import {
STUDIO_FLAT_INSPECTOR_ENABLED,
STUDIO_INSPECTOR_PANELS_ENABLED,
} from "./editor/manualEditingAvailability";
import type { Composition } from "@hyperframes/sdk";
import type { EditHistoryKind } from "../utils/editHistory";
import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
@@ -80,6 +83,7 @@ export function StudioRightPanel({
setRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
setExclusiveRightInspectorPane,
handlePanelResizeStart,
handlePanelResizeMove,
handlePanelResizeEnd,
@@ -223,6 +227,13 @@ export function StudioRightPanel({
setRightPanelTab(pane);
return;
}
// Flat inspector: Layers always renders full-height by itself (see the
// render branch below), so the two panes are mutually exclusive here —
// otherwise both tabs could show "active" while only one actually shows.
if (STUDIO_FLAT_INSPECTOR_ENABLED) {
setExclusiveRightInspectorPane(pane);
return;
}
toggleRightInspectorPane(pane);
};
@@ -516,7 +527,7 @@ export function StudioRightPanel({
domEditSaveTimestampRef={domEditSaveTimestampRef}
recordEdit={recordEdit}
/>
) : layersPaneOpen && designPaneOpen ? (
) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
<div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
<div
className="min-h-[120px] overflow-hidden"
@@ -25,6 +25,7 @@ export function PanelLayoutProvider({
setRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
setExclusiveRightInspectorPane,
toggleLeftSidebar,
handlePanelResizeStart,
handlePanelResizeMove,
@@ -49,6 +50,7 @@ export function PanelLayoutProvider({
setRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
setExclusiveRightInspectorPane,
toggleLeftSidebar,
handlePanelResizeStart,
handlePanelResizeMove,
@@ -67,6 +69,7 @@ export function PanelLayoutProvider({
setRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
setExclusiveRightInspectorPane,
toggleLeftSidebar,
handlePanelResizeStart,
handlePanelResizeMove,
@@ -0,0 +1,70 @@
// @vitest-environment happy-dom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it } from "vitest";
import { usePanelLayout } from "./usePanelLayout";
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
afterEach(() => {
document.body.innerHTML = "";
});
function renderPanelLayout() {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
let current: ReturnType<typeof usePanelLayout> | null = null;
function Harness() {
current = usePanelLayout();
return null;
}
act(() => {
root.render(React.createElement(Harness));
});
return {
getState: (): ReturnType<typeof usePanelLayout> => {
if (!current) throw new Error("usePanelLayout did not render");
return current;
},
unmount: () => act(() => root.unmount()),
};
}
describe("usePanelLayout — right inspector panes", () => {
it("toggleRightInspectorPane independently flips one pane, allowing both open at once", () => {
const harness = renderPanelLayout();
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
act(() => harness.getState().toggleRightInspectorPane("layers"));
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
harness.unmount();
});
it("toggleRightInspectorPane refuses to turn off the last remaining pane", () => {
const harness = renderPanelLayout();
act(() => harness.getState().toggleRightInspectorPane("design"));
// Only "design" was on; toggling it off would leave both false — guarded.
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
harness.unmount();
});
it("setExclusiveRightInspectorPane is radio-style — selecting one turns the other off", () => {
const harness = renderPanelLayout();
act(() => harness.getState().toggleRightInspectorPane("layers"));
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
act(() => harness.getState().setExclusiveRightInspectorPane("layers"));
expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: false });
act(() => harness.getState().setExclusiveRightInspectorPane("design"));
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
harness.unmount();
});
});
@@ -96,6 +96,14 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
});
}, []);
// Radio-style variant for the flat inspector: Layers always renders full-
// height by itself there (never split-shared with Design), so leaving both
// panes independently toggleable would highlight both tabs as "active"
// while only one actually shows. Selecting one turns the other off.
const setExclusiveRightInspectorPane = useCallback((pane: RightInspectorPane) => {
setRightInspectorPanes({ design: pane === "design", layers: pane === "layers" });
}, []);
return {
leftWidth,
setLeftWidth,
@@ -109,6 +117,7 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
setRightPanelTab: trackedSetRightPanelTab,
rightInspectorPanes,
toggleRightInspectorPane,
setExclusiveRightInspectorPane,
toggleLeftSidebar,
handlePanelResizeStart,
handlePanelResizeMove,