@hyperframes/studio 0.8.52 → 0.8.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/assets/{hyperframes-player-I5YRzB02.js → hyperframes-player-DKWM-Ons.js} +1 -1
  2. package/dist/assets/index-DrHQAuGO.css +1 -0
  3. package/dist/assets/index-HfP3sap2.js +390 -0
  4. package/dist/assets/{index-BTAfgesM.js → index-UvW6BEwf.js} +1 -1
  5. package/dist/assets/{index-Du5x7KUR.js → index-Y4OnpTEM.js} +1 -1
  6. package/dist/index.css +29 -0
  7. package/dist/index.css.map +1 -0
  8. package/dist/index.d.ts +13 -6
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +7664 -7953
  11. package/dist/index.js.map +1 -1
  12. package/dist/styles.css +1 -0
  13. package/package.json +8 -7
  14. package/src/App.tsx +42 -48
  15. package/src/components/EditorShell.selectionSync.test.tsx +9 -4
  16. package/src/components/EditorShell.tsx +52 -56
  17. package/src/components/StudioHeader.dom.test.tsx +1 -1
  18. package/src/components/StudioHeader.test.ts +0 -12
  19. package/src/components/StudioHeader.tsx +10 -13
  20. package/src/components/StudioLeftPanels.tsx +202 -0
  21. package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -206
  22. package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
  23. package/src/components/dock/Dock.test.tsx +16 -2
  24. package/src/components/dock/Dock.tsx +13 -3
  25. package/src/components/dock/dockLayoutStore.test.ts +28 -0
  26. package/src/components/dock/dockLayoutStore.ts +21 -4
  27. package/src/components/editor/SnapToolbar.test.tsx +0 -3
  28. package/src/components/nle/NLEContext.tsx +0 -43
  29. package/src/components/nle/TimelinePane.tsx +46 -67
  30. package/src/components/panels/SlideshowPanel.tsx +1 -1
  31. package/src/components/panels/VariablesPanel.tsx +1 -1
  32. package/src/components/renders/RenderQueue.test.tsx +133 -25
  33. package/src/components/renders/RenderQueue.tsx +99 -92
  34. package/src/components/renders/RenderQueueItem.tsx +69 -64
  35. package/src/components/renders/RenderQueuePanel.tsx +1 -1
  36. package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
  37. package/src/components/sidebar/CompositionsPanel.tsx +15 -0
  38. package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx} +6 -11
  39. package/src/components/sidebar/SidebarLintButton.tsx +50 -0
  40. package/src/components/ui/Select.tsx +4 -0
  41. package/src/contexts/PanelLayoutContext.tsx +3 -57
  42. package/src/headerExports.test.tsx +23 -0
  43. package/src/hooks/appHotkeysDispatch.ts +3 -4
  44. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
  45. package/src/hooks/useAppHotkeys.test.ts +0 -1
  46. package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
  47. package/src/hooks/useAppHotkeys.ts +0 -4
  48. package/src/hooks/useDomEditPreviewSync.ts +4 -6
  49. package/src/hooks/useDomEditSession.ts +0 -7
  50. package/src/hooks/useDomEditWiring.ts +4 -9
  51. package/src/hooks/usePanelLayout.test.ts +108 -296
  52. package/src/hooks/usePanelLayout.ts +50 -255
  53. package/src/hooks/useRightPanelIntents.test.ts +181 -0
  54. package/src/hooks/useRightPanelIntents.ts +64 -0
  55. package/src/hooks/useSlideshowTabState.test.ts +9 -31
  56. package/src/hooks/useSlideshowTabState.ts +6 -19
  57. package/src/hooks/useStudioContextValue.test.ts +6 -18
  58. package/src/hooks/useStudioContextValue.ts +5 -26
  59. package/src/index.ts +2 -1
  60. package/src/main.tsx +26 -0
  61. package/src/player/components/Timeline.tsx +1 -1
  62. package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
  63. package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
  64. package/src/utils/studioHelpers.ts +0 -7
  65. package/src/utils/studioUiPreferences.test.ts +10 -16
  66. package/src/utils/studioUiPreferences.ts +0 -16
  67. package/dist/assets/index-CxAu_Okg.css +0 -1
  68. package/dist/assets/index-DocJlW2v.js +0 -376
  69. package/src/components/StudioLeftSidebar.tsx +0 -207
  70. package/src/components/nle/TimelineResizeDivider.tsx +0 -101
  71. package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
  72. package/src/components/sidebar/LeftSidebar.tsx +0 -348
  73. package/src/hooks/useInspectorSplitResize.ts +0 -51
  74. package/src/utils/fitPanels.test.ts +0 -164
  75. package/src/utils/fitPanels.ts +0 -151
@@ -3,338 +3,150 @@
3
3
  import React, { act } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
- import { readStudioUiPreferences } from "../utils/studioUiPreferences";
6
+ import { useDockLayoutStore, type DockController } from "../components/dock/dockLayoutStore";
7
+ import { PANEL_IDS, type PanelId } from "../components/dock/panelRegistry";
7
8
  import { trackStudioEvent } from "../utils/studioTelemetry";
8
- import { usePanelLayout } from "./usePanelLayout";
9
+ import { usePanelLayout, type InitialPanelLayoutState } from "./usePanelLayout";
9
10
 
10
11
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
11
12
 
12
13
  vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
13
14
 
14
- beforeEach(() => {
15
- vi.mocked(trackStudioEvent).mockClear();
16
- const entries = new Map<string, string>();
17
- Object.defineProperty(window, "localStorage", {
18
- configurable: true,
19
- value: {
20
- get length() {
21
- return entries.size;
22
- },
23
- clear: () => entries.clear(),
24
- getItem: (key: string) => entries.get(key) ?? null,
25
- key: (index: number) => Array.from(entries.keys())[index] ?? null,
26
- removeItem: (key: string) => entries.delete(key),
27
- setItem: (key: string, value: string) => entries.set(key, value),
28
- } satisfies Storage,
29
- });
30
- Object.defineProperty(window, "innerWidth", { configurable: true, value: 1496 });
31
- });
15
+ function fakeController(): DockController {
16
+ return {
17
+ open: vi.fn(),
18
+ activate: vi.fn(),
19
+ close: vi.fn(),
20
+ setTitle: vi.fn(),
21
+ setGroupVisible: vi.fn(),
22
+ reset: vi.fn(),
23
+ };
24
+ }
32
25
 
33
- afterEach(() => {
34
- document.body.innerHTML = "";
35
- vi.doUnmock("../components/editor/manualEditingAvailability");
36
- vi.resetModules();
37
- });
26
+ function showDock(visible: PanelId[], controller = fakeController()) {
27
+ useDockLayoutStore.setState({
28
+ controller,
29
+ openPanels: new Set(PANEL_IDS),
30
+ visiblePanels: new Set(visible),
31
+ lastActive: {},
32
+ activePanel: null,
33
+ });
34
+ return controller;
35
+ }
38
36
 
39
- function renderPanelLayoutWith(hook: typeof usePanelLayout) {
37
+ function renderLayout(initial?: InitialPanelLayoutState) {
40
38
  const host = document.createElement("div");
41
39
  document.body.append(host);
42
40
  const root = createRoot(host);
43
41
  let current: ReturnType<typeof usePanelLayout> | null = null;
44
-
45
42
  function Harness() {
46
- current = hook();
43
+ current = usePanelLayout(initial);
47
44
  return null;
48
45
  }
49
-
50
- act(() => {
51
- root.render(React.createElement(Harness));
52
- });
53
-
46
+ act(() => root.render(React.createElement(Harness)));
54
47
  return {
55
- getState: (): ReturnType<typeof usePanelLayout> => {
56
- if (!current) throw new Error("usePanelLayout did not render");
48
+ get layout() {
49
+ if (!current) throw new Error("layout not rendered");
57
50
  return current;
58
51
  },
59
52
  unmount: () => act(() => root.unmount()),
60
53
  };
61
54
  }
62
55
 
63
- function renderPanelLayout() {
64
- return renderPanelLayoutWith(usePanelLayout);
65
- }
66
-
67
- function resizeWindowTo(width: number) {
68
- Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
69
- window.dispatchEvent(new Event("resize"));
70
- }
71
-
72
- describe("usePanelLayout — right inspector panes", () => {
73
- it("opens Design with the intended viewport-scaled panel widths", () => {
74
- const harness = renderPanelLayout();
75
-
76
- expect(harness.getState()).toMatchObject({
77
- leftWidth: 384,
78
- rightWidth: 424,
79
- rightCollapsed: false,
80
- rightPanelTab: "design",
81
- });
82
-
83
- harness.unmount();
84
- });
85
-
86
- it("persists the latest pointer width even before React rerenders", () => {
87
- const harness = renderPanelLayout();
88
- const state = harness.getState();
89
- const target = { setPointerCapture: vi.fn() };
90
-
91
- act(() => {
92
- state.handlePanelResizeStart("left", {
93
- preventDefault: vi.fn(),
94
- target,
95
- pointerId: 1,
96
- clientX: 100,
97
- } as unknown as React.PointerEvent);
98
- state.handlePanelResizeMove({ clientX: 140 } as React.PointerEvent);
99
- state.handlePanelResizeEnd();
100
- });
101
-
102
- expect(harness.getState().leftWidth).toBe(424);
103
- expect(readStudioUiPreferences().leftWidth).toBe(424);
104
- harness.unmount();
105
- });
106
-
107
- it("accumulates and persists rapid keyboard resize steps", () => {
108
- const harness = renderPanelLayout();
109
- const state = harness.getState();
110
-
111
- act(() => {
112
- state.adjustPanelWidth("right", 16);
113
- state.adjustPanelWidth("right", 16);
114
- });
115
-
116
- expect(harness.getState().rightWidth).toBe(456);
117
- expect(readStudioUiPreferences().rightWidth).toBe(456);
118
- harness.unmount();
119
- });
120
-
121
- it("toggleRightInspectorPane independently flips one pane, allowing both open at once", () => {
122
- const harness = renderPanelLayout();
123
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
124
-
125
- act(() => harness.getState().toggleRightInspectorPane("layers"));
126
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
127
-
128
- harness.unmount();
129
- });
56
+ beforeEach(() => vi.mocked(trackStudioEvent).mockClear());
57
+ afterEach(() => {
58
+ document.body.innerHTML = "";
59
+ useDockLayoutStore.setState({ controller: null });
60
+ });
130
61
 
131
- it("toggleRightInspectorPane refuses to turn off the last remaining pane", () => {
132
- const harness = renderPanelLayout();
133
- act(() => harness.getState().toggleRightInspectorPane("design"));
134
- // Only "design" was on; toggling it off would leave both false — guarded.
135
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
136
- harness.unmount();
62
+ describe("usePanelLayout over the dock", () => {
63
+ it("reads the tab of the right-zone panel that is showing", () => {
64
+ showDock(["preview", "timeline", "compositions", "renders"]);
65
+ const view = renderLayout();
66
+ expect(view.layout.rightPanelTab).toBe("renders");
67
+ expect(view.layout.rightCollapsed).toBe(false);
137
68
  });
138
69
 
139
- it("setExclusiveRightInspectorPane is radio-style — selecting one turns the other off", () => {
140
- const harness = renderPanelLayout();
141
- act(() => harness.getState().toggleRightInspectorPane("layers"));
142
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
143
-
144
- act(() => harness.getState().setExclusiveRightInspectorPane("layers"));
145
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: false });
146
-
147
- act(() => harness.getState().setExclusiveRightInspectorPane("design"));
148
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
149
-
150
- harness.unmount();
70
+ it("is collapsed when no right-zone panel is showing", () => {
71
+ showDock(["preview", "timeline", "compositions"]);
72
+ const view = renderLayout();
73
+ expect(view.layout.rightCollapsed).toBe(true);
74
+ expect(view.layout.rightPanelTab).toBe("design");
151
75
  });
152
76
 
153
- it("setRightPanelTab additively opens a pane when the flat inspector is off (legacy behavior)", async () => {
154
- vi.resetModules();
155
- vi.doMock("../components/editor/manualEditingAvailability", async () => {
156
- const actual = await vi.importActual<
157
- typeof import("../components/editor/manualEditingAvailability")
158
- >("../components/editor/manualEditingAvailability");
159
- return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: false };
77
+ it("opens the Design panel for the legacy block-params tab and reports the tab switch", () => {
78
+ const controller = showDock(["preview"]);
79
+ const view = renderLayout();
80
+ act(() => view.layout.setRightPanelTab("block-params"));
81
+ expect(controller.setGroupVisible).toHaveBeenCalledWith("design", true);
82
+ expect(controller.activate).toHaveBeenCalledWith("design");
83
+ expect(trackStudioEvent).toHaveBeenCalledWith("tab_switch", {
84
+ panel: "right_panel",
85
+ tab: "block-params",
160
86
  });
161
- const { usePanelLayout: usePanelLayoutFlatOff } = await import("./usePanelLayout");
162
- const harness = renderPanelLayoutWith(usePanelLayoutFlatOff);
163
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
164
-
165
- act(() => harness.getState().setRightPanelTab("layers"));
166
- // Legacy (split-view) behavior: additive, both panes end up open.
167
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true });
168
-
169
- harness.unmount();
170
87
  });
171
88
 
172
- it("tracks only actual right-panel tab changes, including rapid repeated calls", () => {
173
- const harness = renderPanelLayout();
174
-
175
- act(() => {
176
- harness.getState().setRightPanelTab("design");
177
- harness.getState().setRightPanelTab("design");
178
- });
89
+ it("reports a tab switch only when the tab actually changes", () => {
90
+ showDock(["preview", "design"]);
91
+ const view = renderLayout();
92
+ vi.mocked(trackStudioEvent).mockClear();
93
+ act(() => view.layout.setRightPanelTab("design"));
179
94
  expect(trackStudioEvent).not.toHaveBeenCalled();
180
-
181
- act(() => {
182
- harness.getState().setRightPanelTab("layers");
183
- harness.getState().setRightPanelTab("layers");
184
- });
185
- expect(trackStudioEvent).toHaveBeenCalledOnce();
95
+ act(() => view.layout.setRightPanelTab("layers"));
186
96
  expect(trackStudioEvent).toHaveBeenCalledWith("tab_switch", {
187
97
  panel: "right_panel",
188
98
  tab: "layers",
189
99
  });
190
- expect(harness.getState().rightPanelTab).toBe("layers");
191
-
192
- harness.unmount();
193
- });
194
-
195
- it("caps a panel relative to the window instead of at a flat 600px", () => {
196
- resizeWindowTo(700);
197
- const harness = renderPanelLayout();
198
- // The old flat cap let the inspector claim 600 of a 700px window.
199
- expect(harness.getState().rightWidth).toBeLessThanOrEqual(280);
200
- harness.unmount();
201
- });
202
-
203
- it("rails both panels once the window cannot fit them", () => {
204
- resizeWindowTo(560);
205
- const harness = renderPanelLayout();
206
- expect(harness.getState()).toMatchObject({
207
- effectiveLeftCollapsed: true,
208
- effectiveRightCollapsed: true,
209
- leftCollapsed: false,
210
- rightCollapsed: false,
211
- });
212
- harness.unmount();
213
- });
214
-
215
- it("auto-collapse never writes the user's persisted or URL-synced intent", () => {
216
- const harness = renderPanelLayout();
217
- act(() => resizeWindowTo(560));
218
-
219
- expect(harness.getState().effectiveLeftCollapsed).toBe(true);
220
- // localStorage carries leftCollapsed; the shareable URL carries rightCollapsed.
221
- // A ten-second window drag must rewrite neither.
222
- expect(readStudioUiPreferences().leftCollapsed).toBeUndefined();
223
- expect(harness.getState().leftCollapsed).toBe(false);
224
- expect(harness.getState().rightCollapsed).toBe(false);
225
- harness.unmount();
226
- });
227
-
228
- it("returns the user's own width when the window grows back", () => {
229
- const harness = renderPanelLayout();
230
- const wide = harness.getState().leftWidth;
231
-
232
- act(() => resizeWindowTo(560));
233
- expect(harness.getState().leftWidth).toBeLessThan(wide);
234
-
235
- act(() => resizeWindowTo(1496));
236
- expect(harness.getState().leftWidth).toBe(wide);
237
- harness.unmount();
238
- });
239
-
240
- it("keeps an explicitly collapsed sidebar collapsed after a narrow trip", () => {
241
- const harness = renderPanelLayout();
242
- act(() => harness.getState().toggleLeftSidebar());
243
- expect(readStudioUiPreferences().leftCollapsed).toBe(true);
244
-
245
- act(() => resizeWindowTo(560));
246
- act(() => resizeWindowTo(1496));
247
-
248
- expect(harness.getState().effectiveLeftCollapsed).toBe(true);
249
- harness.unmount();
250
- });
251
-
252
- it("lets the user reopen a panel the window auto-collapsed", () => {
253
- const harness = renderPanelLayout();
254
- act(() => resizeWindowTo(560));
255
- expect(harness.getState().effectiveRightCollapsed).toBe(true);
256
-
257
- // Without this the header Inspector button would be dead below 700px.
258
- act(() => harness.getState().setRightCollapsed(false));
259
- expect(harness.getState().effectiveRightCollapsed).toBe(false);
260
- harness.unmount();
261
100
  });
262
101
 
263
- it("opens the sidebar when the rail's own button is clicked", () => {
264
- const harness = renderPanelLayout();
265
- act(() => resizeWindowTo(560));
266
- expect(harness.getState().effectiveLeftCollapsed).toBe(true);
267
-
268
- // Regression: the toggle used to flip stored INTENT, which was already
269
- // false here, so the click persisted leftCollapsed=true and the rail stayed
270
- // railed — a dead button that silently saved a collapse nobody asked for.
271
- act(() => harness.getState().toggleLeftSidebar());
272
-
273
- expect(harness.getState().effectiveLeftCollapsed).toBe(false);
274
- expect(harness.getState().leftCollapsed).toBe(false);
275
- expect(readStudioUiPreferences().leftCollapsed).toBe(false);
276
- // And it gets a real width: rendering an expanded sidebar at the 42px rail
277
- // width would squash its own content. Only a real-UI click caught this.
278
- expect(harness.getState().leftWidth).toBeGreaterThanOrEqual(200);
279
- harness.unmount();
280
- });
281
-
282
- it("closes the sidebar again on the next click", () => {
283
- const harness = renderPanelLayout();
284
- act(() => resizeWindowTo(560));
285
- act(() => harness.getState().toggleLeftSidebar());
286
- act(() => harness.getState().toggleLeftSidebar());
287
-
288
- expect(harness.getState().effectiveLeftCollapsed).toBe(true);
289
- expect(readStudioUiPreferences().leftCollapsed).toBe(true);
290
- harness.unmount();
291
- });
292
-
293
- it("forgets that reopen once the window is wide again", () => {
294
- const harness = renderPanelLayout();
295
- act(() => resizeWindowTo(560));
296
- act(() => harness.getState().setRightCollapsed(false));
297
- expect(harness.getState().effectiveRightCollapsed).toBe(false);
298
-
299
- // Widening past the threshold clears the override, so a later narrow trip
300
- // rails again rather than staying open forever off one old click.
301
- act(() => resizeWindowTo(1496));
302
- act(() => resizeWindowTo(560));
303
- expect(harness.getState().effectiveRightCollapsed).toBe(true);
304
- harness.unmount();
305
- });
306
-
307
- it("setRightPanelTab is flat-aware: exclusivity holds for callers other than a direct in-panel tab click", async () => {
308
- vi.resetModules();
309
- vi.doMock("../components/editor/manualEditingAvailability", async () => {
310
- const actual = await vi.importActual<
311
- typeof import("../components/editor/manualEditingAvailability")
312
- >("../components/editor/manualEditingAvailability");
313
- return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: true };
314
- });
315
- const { usePanelLayout: usePanelLayoutFlatOn } = await import("./usePanelLayout");
316
- const harness = renderPanelLayoutWith(usePanelLayoutFlatOn);
317
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
318
-
319
- // The flat inspector can change its visible pane without changing the
320
- // umbrella rightPanelTab value. A later element selection must still bring
321
- // Design back even though rightPanelTab already says "design".
322
- act(() => harness.getState().setExclusiveRightInspectorPane("layers"));
323
- expect(harness.getState()).toMatchObject({
324
- rightPanelTab: "design",
325
- rightInspectorPanes: { layers: true, design: false },
326
- });
327
- act(() => harness.getState().setRightPanelTab("design"));
328
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
329
-
330
- // Element-select / block-params-close / header Inspector-button callers
331
- // all reach setRightPanelTab directly, not through the in-panel tab
332
- // click's own setExclusiveRightInspectorPane call — this must still
333
- // enforce exclusivity under the flat flag, or both tabs end up
334
- // highlighted while only one pane actually renders.
335
- act(() => harness.getState().setRightPanelTab("layers"));
336
- expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: false });
337
-
338
- harness.unmount();
102
+ it("collapses every open right-zone panel group and leaves the other zones alone", () => {
103
+ const controller = showDock(["preview", "design"]);
104
+ const view = renderLayout();
105
+ act(() => view.layout.setRightCollapsed(true));
106
+ const hidden = vi.mocked(controller.setGroupVisible).mock.calls;
107
+ expect(hidden.map(([id]) => id).sort()).toEqual(
108
+ ["design", "layers", "renders", "slideshow", "variables"].sort(),
109
+ );
110
+ expect(hidden.every(([, visible]) => visible === false)).toBe(true);
111
+ });
112
+
113
+ it("expanding an already-showing right column does nothing to the tab", () => {
114
+ const controller = showDock(["design"]);
115
+ const view = renderLayout();
116
+ act(() => view.layout.setRightCollapsed(false));
117
+ expect(controller.activate).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it("expanding a fully hidden right column brings Design back", () => {
121
+ const controller = showDock(["preview"]);
122
+ const view = renderLayout();
123
+ act(() => view.layout.setRightCollapsed(false));
124
+ expect(controller.activate).toHaveBeenCalledWith("design");
125
+ });
126
+
127
+ it("applies the URL's tab and collapse state once, when the dock attaches", () => {
128
+ useDockLayoutStore.setState({ controller: null, openPanels: new Set(PANEL_IDS) });
129
+ const view = renderLayout({ rightPanelTab: "variables", rightCollapsed: true });
130
+ const controller = fakeController();
131
+ act(() => useDockLayoutStore.getState().attach(controller));
132
+ expect(controller.activate).toHaveBeenCalledWith("variables");
133
+ expect(controller.setGroupVisible).toHaveBeenCalledWith("design", false);
134
+ const later = fakeController();
135
+ act(() => useDockLayoutStore.getState().attach(later));
136
+ expect(later.activate).not.toHaveBeenCalled();
137
+ expect(later.setGroupVisible).not.toHaveBeenCalled();
138
+ view.unmount();
139
+ });
140
+
141
+ it("keeps a URL's slideshow tab until the panel opens, then activates it once", () => {
142
+ const opened = new Set<PanelId>(PANEL_IDS.filter((id) => id !== "slideshow"));
143
+ useDockLayoutStore.setState({ controller: null, openPanels: opened });
144
+ const view = renderLayout({ rightPanelTab: "slideshow" });
145
+ const controller = fakeController();
146
+ act(() => useDockLayoutStore.getState().attach(controller));
147
+ expect(controller.activate).not.toHaveBeenCalled();
148
+ act(() => useDockLayoutStore.setState({ openPanels: new Set(PANEL_IDS) }));
149
+ expect(controller.activate).toHaveBeenCalledWith("slideshow");
150
+ view.unmount();
339
151
  });
340
152
  });