@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
@@ -0,0 +1,202 @@
1
+ import { useCallback, type ReactNode } from "react";
2
+ import { SourceEditor } from "./editor/SourceEditor";
3
+ import { FileTree } from "./editor/FileTree";
4
+ import { MediaPreview } from "./MediaPreview";
5
+ import { AssetsTab } from "./sidebar/AssetsTab";
6
+ import { BlocksTab, type BlockPreviewInfo } from "./sidebar/BlocksTab";
7
+ import { CompositionsPanel } from "./sidebar/CompositionsPanel";
8
+ import { SidebarLintButton } from "./sidebar/SidebarLintButton";
9
+ import { Dock } from "./dock/Dock";
10
+ import { useDockLayoutStore } from "./dock/dockLayoutStore";
11
+ import { isMediaFile } from "../utils/mediaTypes";
12
+ import { useStudioShellContext } from "../contexts/StudioContext";
13
+ import { useFileManagerContext } from "../contexts/FileManagerContext";
14
+ import { getPersistedRenderSettings } from "./renders/renderSettings";
15
+
16
+ interface StudioLeftPanelsProps {
17
+ onSelectComposition: (comp: string) => void;
18
+ onAddBlock: (blockName: string) => void;
19
+ onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
20
+ onLint: () => void;
21
+ linting: boolean;
22
+ lintFindingCount?: number;
23
+ lintHasError?: boolean;
24
+ lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
25
+ onAddAssetToTimeline?: (path: string) => void;
26
+ onAddCompositionToTimeline?: (path: string) => void;
27
+ }
28
+
29
+ function PanelColumn({ footer, children }: { footer: ReactNode; children: ReactNode }) {
30
+ return (
31
+ <div className="flex h-full min-h-0 flex-col overflow-hidden bg-neutral-950">
32
+ <div className="flex min-h-0 flex-1 flex-col">{children}</div>
33
+ {footer}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ // fallow-ignore-next-line complexity
39
+ export function StudioLeftPanels({
40
+ onSelectComposition,
41
+ onAddBlock,
42
+ onPreviewBlock,
43
+ onLint,
44
+ linting,
45
+ lintFindingCount,
46
+ lintHasError,
47
+ lintFindingsByFile,
48
+ onAddAssetToTimeline,
49
+ onAddCompositionToTimeline,
50
+ }: StudioLeftPanelsProps) {
51
+ const { projectId, renderQueue, waitForPendingDomEditSaves } = useStudioShellContext();
52
+ const {
53
+ compositions,
54
+ assets,
55
+ editingFile,
56
+ fileTree,
57
+ revealSourceOffset,
58
+ handleFileSelect,
59
+ handleCreateFile,
60
+ handleCreateFolder,
61
+ handleDeleteFile,
62
+ handleRenameFile,
63
+ handleDuplicateFile,
64
+ handleMoveFile,
65
+ handleImportFiles,
66
+ handleContentChange,
67
+ } = useFileManagerContext();
68
+
69
+ const handleRenderComposition = useCallback(
70
+ async (comp: string) => {
71
+ // startRender refuses without an encoder and reports why as a row in the
72
+ // Renders panel, which may be closed or behind another tab: bring it up.
73
+ if (renderQueue.ffmpegMissing) {
74
+ useDockLayoutStore.getState().activatePanel("renders");
75
+ return;
76
+ }
77
+ await waitForPendingDomEditSaves();
78
+ const { format, quality, fps } = getPersistedRenderSettings();
79
+ await renderQueue.startRender({ composition: comp, format, quality, fps });
80
+ },
81
+ [renderQueue, waitForPendingDomEditSaves],
82
+ );
83
+
84
+ const importFiles = async (files: FileList, dir?: string) => {
85
+ await handleImportFiles(files, dir);
86
+ };
87
+ const lintButton = (
88
+ <SidebarLintButton
89
+ onLint={onLint}
90
+ linting={linting}
91
+ findingCount={lintFindingCount}
92
+ hasError={lintHasError}
93
+ />
94
+ );
95
+
96
+ return (
97
+ <>
98
+ <Dock.Panel id="compositions">
99
+ <PanelColumn footer={lintButton}>
100
+ <CompositionsPanel
101
+ projectId={projectId}
102
+ compositions={compositions}
103
+ activeComposition={editingFile?.path ?? null}
104
+ onSelect={onSelectComposition}
105
+ onAddToTimeline={onAddCompositionToTimeline}
106
+ onRenderComposition={handleRenderComposition}
107
+ isRendering={renderQueue.isRendering}
108
+ lintFindingsByFile={lintFindingsByFile}
109
+ />
110
+ </PanelColumn>
111
+ </Dock.Panel>
112
+ <Dock.Panel id="assets">
113
+ <PanelColumn footer={lintButton}>
114
+ <AssetsTab
115
+ projectId={projectId}
116
+ assets={assets}
117
+ onImport={importFiles}
118
+ onDelete={handleDeleteFile}
119
+ onRename={handleRenameFile}
120
+ onAddAssetToTimeline={onAddAssetToTimeline}
121
+ />
122
+ </PanelColumn>
123
+ </Dock.Panel>
124
+ <Dock.Panel id="code">
125
+ <PanelColumn footer={lintButton}>
126
+ <div className="flex min-h-0 flex-1">
127
+ {fileTree.length > 0 && (
128
+ <div className="w-[160px] shrink-0 border-r border-neutral-800 overflow-y-auto">
129
+ <FileTree
130
+ files={fileTree}
131
+ activeFile={editingFile?.path ?? null}
132
+ onSelectFile={handleFileSelect}
133
+ onCreateFile={handleCreateFile}
134
+ onCreateFolder={handleCreateFolder}
135
+ onDeleteFile={handleDeleteFile}
136
+ onRenameFile={handleRenameFile}
137
+ onDuplicateFile={handleDuplicateFile}
138
+ onMoveFile={handleMoveFile}
139
+ onImportFiles={importFiles}
140
+ lintFindingsByFile={lintFindingsByFile}
141
+ />
142
+ </div>
143
+ )}
144
+ <div className="flex-1 overflow-hidden min-w-0">
145
+ <CodeBody
146
+ projectId={projectId}
147
+ editingFile={editingFile}
148
+ revealOffset={revealSourceOffset}
149
+ onChange={handleContentChange}
150
+ />
151
+ </div>
152
+ </div>
153
+ </PanelColumn>
154
+ </Dock.Panel>
155
+ <Dock.Panel id="catalog">
156
+ <PanelColumn footer={lintButton}>
157
+ <BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
158
+ </PanelColumn>
159
+ </Dock.Panel>
160
+ </>
161
+ );
162
+ }
163
+
164
+ function CodeBody({
165
+ projectId,
166
+ editingFile,
167
+ revealOffset,
168
+ onChange,
169
+ }: {
170
+ projectId: string;
171
+ editingFile: { path: string; content: string | null } | null;
172
+ revealOffset: React.ComponentProps<typeof SourceEditor>["revealOffset"];
173
+ onChange: (content: string) => void;
174
+ }) {
175
+ if (!editingFile) {
176
+ return (
177
+ <div className="flex items-center justify-center h-full text-neutral-600 text-sm">
178
+ Select a file to edit
179
+ </div>
180
+ );
181
+ }
182
+ if (isMediaFile(editingFile.path)) {
183
+ return <MediaPreview projectId={projectId} filePath={editingFile.path} />;
184
+ }
185
+ // Never mount the editor on unloaded content: a keystroke would autosave an
186
+ // empty document over the real file.
187
+ if (editingFile.content == null) {
188
+ return (
189
+ <div className="flex h-full items-center justify-center text-[11px] text-neutral-600">
190
+ Loading {editingFile.path}…
191
+ </div>
192
+ );
193
+ }
194
+ return (
195
+ <SourceEditor
196
+ content={editingFile.content}
197
+ filePath={editingFile.path}
198
+ onChange={onChange}
199
+ revealOffset={revealOffset}
200
+ />
201
+ );
202
+ }
@@ -1,7 +1,6 @@
1
1
  import { useCallback } from "react";
2
- import type { StudioRightPanelProps } from "./StudioRightPanel.types";
2
+ import type { StudioRightPanelsProps } from "./StudioRightPanels.types";
3
3
 
4
- export type { StudioRightPanelProps };
5
4
  import { PropertyPanel } from "./editor/PropertyPanel";
6
5
  import { LayersPanel } from "./editor/LayersPanel";
7
6
  import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
@@ -9,14 +8,18 @@ import { BlockParamsPanel } from "./editor/BlockParamsPanel";
9
8
  import { RenderQueuePanel } from "./renders/RenderQueuePanel";
10
9
  import { SlideshowPanel } from "./panels/SlideshowPanel";
11
10
  import { VariablesPanel } from "./panels/VariablesPanel";
12
- import { RightPanelTabs, type RightPanelTabDescriptor } from "./RightPanelTabs";
11
+ import { Dock } from "./dock/Dock";
12
+ import { useDockLayoutStore } from "./dock/dockLayoutStore";
13
13
  import type { RenderJob } from "./renders/useRenderQueue";
14
- import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
15
14
  import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
16
15
  import { useSlideshowTabState } from "../hooks/useSlideshowTabState";
16
+ import {
17
+ useBlockParamsDismissal,
18
+ useCaptionDesignFocus,
19
+ useSlideshowDockPanel,
20
+ } from "../hooks/useRightPanelIntents";
17
21
  import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";
18
22
  import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
19
- import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
20
23
  import { useFileManagerContext } from "../contexts/FileManagerContext";
21
24
  import { useDomEditContext } from "../contexts/DomEditContext";
22
25
  import { usePlayerStore } from "../player";
@@ -27,14 +30,13 @@ import {
27
30
  } from "./studioColorGradingScope";
28
31
  import { timelineKeysForSelections } from "../utils/studioHelpers";
29
32
  import { canHideSelections } from "../utils/timelineInspector";
30
- import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
31
33
  import { useRemoveBackground } from "../hooks/useRemoveBackground";
32
34
 
33
35
  // fallow-ignore-next-line complexity
34
- export function StudioRightPanel({
35
- designPanelActive,
36
+ export function StudioRightPanels({
36
37
  activeBlockParams,
37
38
  onCloseBlockParams,
39
+ onDismissBlockParams,
38
40
  recordingState,
39
41
  recordingDuration,
40
42
  onToggleRecording,
@@ -46,20 +48,7 @@ export function StudioRightPanel({
46
48
  onToggleElementHidden,
47
49
  onAutoGroupCarveSources,
48
50
  onAddMediaOverlay,
49
- }: StudioRightPanelProps) {
50
- const {
51
- rightWidth,
52
- adjustPanelWidth,
53
- rightPanelTab,
54
- setRightPanelTab,
55
- rightInspectorPanes,
56
- toggleRightInspectorPane,
57
- setExclusiveRightInspectorPane,
58
- handlePanelResizeStart,
59
- handlePanelResizeMove,
60
- handlePanelResizeEnd,
61
- } = usePanelLayoutContext();
62
-
51
+ }: StudioRightPanelsProps) {
63
52
  const {
64
53
  previewIframeRef,
65
54
  projectId,
@@ -154,41 +143,20 @@ export function StudioRightPanel({
154
143
  coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
155
144
  });
156
145
 
157
- const {
158
- layersPanePercent,
159
- splitContainerRef,
160
- handleInspectorSplitResizeStart,
161
- handleInspectorSplitResizeMove,
162
- handleInspectorSplitResizeEnd,
163
- } = useInspectorSplitResize();
164
-
165
146
  const renderJobs = renderQueue.jobs as RenderJob[];
166
- const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
167
-
147
+ const slideshowVisible = useDockLayoutStore((state) => state.visiblePanels.has("slideshow"));
168
148
  const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
169
149
  editingFileContent: editingFile?.content,
170
150
  previewIframeRef,
171
151
  refreshKey,
172
- rightPanelTab,
173
- setRightPanelTab,
152
+ slideshowVisible,
174
153
  });
175
- const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
176
- const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers;
177
-
178
- const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
179
- if (!inspectorTabActive) {
180
- setRightPanelTab(pane);
181
- return;
182
- }
183
- // Flat inspector: Layers always renders full-height by itself (see the
184
- // render branch below), so the two panes are mutually exclusive here —
185
- // otherwise both tabs could show "active" while only one actually shows.
186
- if (STUDIO_FLAT_INSPECTOR_ENABLED) {
187
- setExclusiveRightInspectorPane(pane);
188
- return;
189
- }
190
- toggleRightInspectorPane(pane);
191
- };
154
+ useSlideshowDockPanel(isSlideshowComposition);
155
+ useBlockParamsDismissal({
156
+ hasBlockParams: activeBlockParams != null,
157
+ onDismiss: onDismissBlockParams,
158
+ });
159
+ useCaptionDesignFocus(captionEditMode);
192
160
 
193
161
  const handleApplyColorGradingScope = useCallback(
194
162
  async (scope: ColorGradingScope, value: string | null) =>
@@ -337,165 +305,48 @@ export function StudioRightPanel({
337
305
  </DesignPanelPromoteProvider>
338
306
  );
339
307
 
340
- const renderQueuePanel = <RenderQueuePanel />;
341
-
342
- // Slideshow appears only for a slideshow composition, so the strip is built
343
- // rather than written out: a tab that is not in this list is not reachable by
344
- // an arrow key either.
345
- const inspectorTabs: RightPanelTabDescriptor[] = [
346
- {
347
- id: "design",
348
- label: "Design",
349
- tooltip: "Element styles and properties",
350
- active: designPaneOpen,
351
- onSelect: () => handleInspectorPaneButtonClick("design"),
352
- },
353
- {
354
- id: "layers",
355
- label: "Layers",
356
- tooltip: "Composition layer stack",
357
- active: layersPaneOpen,
358
- onSelect: () => handleInspectorPaneButtonClick("layers"),
359
- },
360
- {
361
- id: "renders",
362
- label: renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders",
363
- tooltip: "Render queue and exports",
364
- active: rightPanelTab === "renders",
365
- onSelect: () => setRightPanelTab("renders"),
366
- },
367
- ...(isSlideshowComposition
368
- ? [
369
- {
370
- id: "slideshow",
371
- label: "Slideshow",
372
- tooltip: "Slideshow branching editor",
373
- active: rightPanelTab === "slideshow",
374
- onSelect: () => setRightPanelTab("slideshow"),
375
- },
376
- ]
377
- : []),
378
- {
379
- id: "variables",
380
- label: "Variables",
381
- tooltip: "Template variables — declare, preview with values",
382
- active: rightPanelTab === "variables",
383
- onSelect: () => setRightPanelTab("variables"),
384
- },
385
- ];
308
+ let designBody = propertyPanel;
309
+ if (captionEditMode) {
310
+ designBody = <CaptionPropertyPanel iframeRef={previewIframeRef} />;
311
+ } else if (activeBlockParams) {
312
+ designBody = (
313
+ <BlockParamsPanel
314
+ blockName={activeBlockParams.blockName}
315
+ blockTitle={activeBlockParams.blockTitle}
316
+ params={activeBlockParams.params}
317
+ compositionPath={activeBlockParams.compositionPath}
318
+ onClose={onCloseBlockParams ?? (() => {})}
319
+ />
320
+ );
321
+ }
386
322
 
387
323
  return (
388
324
  <>
389
- {/* Vertical resize divider: 3px visible seam, 13px hit zone via the inner div. */}
390
- <div
391
- role="separator"
392
- aria-label="Resize inspector panel"
393
- aria-orientation="vertical"
394
- tabIndex={0}
395
- className="group relative w-[3px] shrink-0 cursor-col-resize outline-hidden focus-visible:bg-studio-accent/20"
396
- style={{ touchAction: "none" }}
397
- onPointerDown={(e) => handlePanelResizeStart("right", e)}
398
- onPointerMove={handlePanelResizeMove}
399
- onPointerUp={handlePanelResizeEnd}
400
- onPointerCancel={handlePanelResizeEnd}
401
- onKeyDown={(e) => {
402
- if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
403
- e.preventDefault();
404
- // Panel is right-anchored: ArrowLeft grows it, ArrowRight shrinks it.
405
- const delta = e.key === "ArrowLeft" ? 16 : -16;
406
- adjustPanelWidth("right", delta);
407
- }}
408
- >
409
- {/* Asymmetric hit zone: 8px into the preview's p-2 gutter (the only dead
410
- space), the 3px seam, 2px into the card. Stops short of the 24px WCAG
411
- 2.5.8 target because the next pixel each way is live. */}
412
- <div className="absolute inset-y-0 left-[-8px] w-[13px]" />
413
- {/* Visible hairline */}
414
- <div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
415
- </div>
416
- <div
417
- className="flex min-w-0 shrink-0 flex-col overflow-hidden rounded-lg border border-neutral-800 bg-neutral-950"
418
- style={{ width: rightWidth }}
325
+ <Dock.Panel id="design">{designBody}</Dock.Panel>
326
+ <Dock.Panel id="layers">
327
+ <LayersPanel />
328
+ </Dock.Panel>
329
+ <Dock.Panel
330
+ id="renders"
331
+ title={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : undefined}
419
332
  >
420
- {captionEditMode ? (
421
- <CaptionPropertyPanel iframeRef={previewIframeRef} />
422
- ) : (
423
- <>
424
- <RightPanelTabs tabs={inspectorTabs} activateOnFocus={STUDIO_FLAT_INSPECTOR_ENABLED} />
425
- <div className="min-h-0 min-w-0 flex-1 overflow-hidden">
426
- {rightPanelTab === "block-params" && activeBlockParams ? (
427
- <BlockParamsPanel
428
- blockName={activeBlockParams.blockName}
429
- blockTitle={activeBlockParams.blockTitle}
430
- params={activeBlockParams.params}
431
- compositionPath={activeBlockParams.compositionPath}
432
- onClose={onCloseBlockParams ?? (() => {})}
433
- />
434
- ) : rightPanelTab === "slideshow" && isSlideshowComposition ? (
435
- <SlideshowPanel
436
- scenes={slideshowScenes}
437
- onPersist={onPersistSlideshow}
438
- onPersistNotes={onPersistSlideshowNotes}
439
- />
440
- ) : rightPanelTab === "variables" ? (
441
- <VariablesPanel
442
- sdkSession={sdkSession}
443
- publishSdkSession={publishSdkSession}
444
- reloadPreview={reloadPreview}
445
- recordEdit={recordEdit}
446
- />
447
- ) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
448
- <div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
449
- <div
450
- className="min-h-[120px] overflow-hidden"
451
- style={{ flexBasis: `${layersPanePercent}%`, flexShrink: 0 }}
452
- >
453
- <LayersPanel />
454
- </div>
455
- <div
456
- role="separator"
457
- aria-label="Resize Layers and Design panes"
458
- aria-orientation="horizontal"
459
- className="group flex h-2 shrink-0 cursor-row-resize items-center justify-center border-y border-neutral-800 bg-neutral-900"
460
- style={{ touchAction: "none" }}
461
- onPointerDown={handleInspectorSplitResizeStart}
462
- onPointerMove={handleInspectorSplitResizeMove}
463
- onPointerUp={handleInspectorSplitResizeEnd}
464
- onPointerCancel={handleInspectorSplitResizeEnd}
465
- >
466
- <div className="h-px w-10 rounded-full bg-white/12 transition-colors group-hover:bg-white/24 group-active:bg-studio-accent/70" />
467
- </div>
468
- <div className="min-h-0 flex-1 overflow-hidden">{propertyPanel}</div>
469
- </div>
470
- ) : layersPaneOpen ? (
471
- <LayersPanel />
472
- ) : designPaneOpen ? (
473
- propertyPanel
474
- ) : inspectorTabActive ? (
475
- // Inspector tab selected but no pane can render (panes toggled
476
- // off, or inspector inactive during playback/recording): show an
477
- // explanation instead of silently rendering the render queue
478
- // under a highlighted inspector tab.
479
- <div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
480
- <p className="text-xs text-neutral-500">
481
- Inspector is unavailable right now — select the Design or Layers pane above, or
482
- pause playback/recording to inspect elements.
483
- </p>
484
- <button
485
- type="button"
486
- onClick={() => setRightPanelTab("renders")}
487
- className="h-7 rounded-md border border-neutral-800 px-3 text-[11px] font-medium text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200 active:scale-[0.98]"
488
- >
489
- Show Renders
490
- </button>
491
- </div>
492
- ) : (
493
- renderQueuePanel
494
- )}
495
- </div>
496
- </>
497
- )}
498
- </div>
333
+ <RenderQueuePanel />
334
+ </Dock.Panel>
335
+ <Dock.Panel id="variables">
336
+ <VariablesPanel
337
+ sdkSession={sdkSession}
338
+ publishSdkSession={publishSdkSession}
339
+ reloadPreview={reloadPreview}
340
+ recordEdit={recordEdit}
341
+ />
342
+ </Dock.Panel>
343
+ <Dock.Panel id="slideshow">
344
+ <SlideshowPanel
345
+ scenes={slideshowScenes}
346
+ onPersist={onPersistSlideshow}
347
+ onPersistNotes={onPersistSlideshowNotes}
348
+ />
349
+ </Dock.Panel>
499
350
  </>
500
351
  );
501
352
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Props for StudioRightPanel.
2
+ * Props for StudioRightPanels.
3
3
  *
4
4
  * Kept beside the component rather than inside it: the panel is at the file-size
5
5
  * cap, and this block is the part that changes least, so moving it keeps the
@@ -14,8 +14,7 @@ import type { UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
14
14
  import type { AddMediaOverlayHandler } from "./editor/propertyPanelTypes";
15
15
  import type { ToggleHiddenHandler } from "../utils/studioHelpers";
16
16
 
17
- export interface StudioRightPanelProps extends StudioEditPersistenceProps {
18
- designPanelActive: boolean;
17
+ export interface StudioRightPanelsProps extends StudioEditPersistenceProps {
19
18
  activeBlockParams?: {
20
19
  blockName: string;
21
20
  blockTitle: string;
@@ -23,6 +22,8 @@ export interface StudioRightPanelProps extends StudioEditPersistenceProps {
23
22
  compositionPath: string;
24
23
  } | null;
25
24
  onCloseBlockParams?: () => void;
25
+ /** Drops the block-params view without touching the dock (selection or leaving Design). */
26
+ onDismissBlockParams: () => void;
26
27
  recordingState?: "idle" | "recording" | "preview";
27
28
  recordingDuration?: number;
28
29
  onToggleRecording?: () => void;
@@ -54,7 +54,10 @@ const applySideMinimums = vi.mocked(dockLayout.applySideMinimums);
54
54
 
55
55
  let root: Root | null = null;
56
56
 
57
- function mount(projectId: string | null) {
57
+ function mount(
58
+ projectId: string | null,
59
+ titles: Partial<Record<(typeof PANEL_IDS)[number], string>> = {},
60
+ ) {
58
61
  const host = document.createElement("div");
59
62
  document.body.append(host);
60
63
  root = createRoot(host);
@@ -62,7 +65,7 @@ function mount(projectId: string | null) {
62
65
  root?.render(
63
66
  <Dock.Root projectId={projectId}>
64
67
  {PANEL_IDS.map((id) => (
65
- <Dock.Panel key={id} id={id}>
68
+ <Dock.Panel key={id} id={id} title={titles[id]}>
66
69
  <div data-testid={`content-${id}`}>{id}</div>
67
70
  </Dock.Panel>
68
71
  ))}
@@ -142,6 +145,17 @@ describe("Dock on React 19", () => {
142
145
  expect(host.querySelector('[data-testid="content-renders"]')).not.toBeNull();
143
146
  });
144
147
 
148
+ it("keeps a panel's custom title when it is closed and reopened", async () => {
149
+ const host = mount("p1", { renders: "Renders (2)" });
150
+ expect(host.textContent).toContain("Renders (2)");
151
+ act(() => useDockLayoutStore.getState().closePanel("renders"));
152
+ await act(async () => {
153
+ useDockLayoutStore.getState().togglePanel("renders");
154
+ await Promise.resolve();
155
+ });
156
+ expect(host.textContent).toContain("Renders (2)");
157
+ });
158
+
145
159
  it("reopens a closed panel as a tab of its zone's group, not a new group", () => {
146
160
  mount("p1");
147
161
  act(() => useDockLayoutStore.getState().closePanel("compositions"));
@@ -69,8 +69,17 @@ function snapshot(api: DockviewApi): DockSnapshot {
69
69
  openPanels.add(panel.id);
70
70
  if (panel.api.isVisible && panel.group.api.isVisible) visiblePanels.add(panel.id);
71
71
  }
72
+ const groupActivePanels = api.groups.flatMap((group) => {
73
+ const id = group.activePanel?.id;
74
+ return isPanelId(id) ? [id] : [];
75
+ });
72
76
  const active = api.activePanel?.id;
73
- return { openPanels, visiblePanels, activePanel: isPanelId(active) ? active : null };
77
+ return {
78
+ openPanels,
79
+ visiblePanels,
80
+ activePanel: isPanelId(active) ? active : null,
81
+ groupActivePanels,
82
+ };
74
83
  }
75
84
 
76
85
  function createController(api: DockviewApi): DockController {
@@ -206,10 +215,11 @@ function Panel({ id, title, children }: { id: PanelId; title?: string; children:
206
215
  const element = useSlots().slots[id];
207
216
  const visible = useDockLayoutStore((state) => state.visiblePanels.has(id));
208
217
  const controller = useDockLayoutStore((state) => state.controller);
218
+ const open = useDockLayoutStore((state) => state.openPanels.has(id));
209
219
  const label = title ?? PANEL_DEFINITIONS[id].title;
210
220
  useEffect(() => {
211
- controller?.setTitle(id, label);
212
- }, [controller, id, label]);
221
+ if (open) controller?.setTitle(id, label);
222
+ }, [controller, id, label, open]);
213
223
  const definition: PanelDefinition = PANEL_DEFINITIONS[id];
214
224
  const shown = visible || definition.keepMounted;
215
225
  return element && shown ? createPortal(children, element) : null;
@@ -0,0 +1,28 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { useDockLayoutStore } from "./dockLayoutStore";
3
+ import type { PanelId } from "./panelRegistry";
4
+
5
+ const snapshot = (groupActivePanels: PanelId[], activePanel: PanelId | null = null) => ({
6
+ openPanels: new Set<PanelId>(["design", "renders", "compositions"]),
7
+ visiblePanels: new Set<PanelId>(groupActivePanels),
8
+ activePanel,
9
+ groupActivePanels,
10
+ });
11
+
12
+ describe("dock store sync", () => {
13
+ beforeEach(() => useDockLayoutStore.setState({ lastActive: {} }));
14
+
15
+ it("records what every side group shows, not only the globally active panel", () => {
16
+ useDockLayoutStore.getState().sync(snapshot(["compositions", "renders"], "compositions"));
17
+ expect(useDockLayoutStore.getState().lastActive).toEqual({
18
+ left: "compositions",
19
+ right: "renders",
20
+ });
21
+ });
22
+
23
+ it("forgets the previous dock's memory on detach", () => {
24
+ useDockLayoutStore.getState().sync(snapshot(["renders"], "renders"));
25
+ useDockLayoutStore.getState().detach();
26
+ expect(useDockLayoutStore.getState().lastActive).toEqual({});
27
+ });
28
+ });