@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.
- package/dist/assets/{hyperframes-player-I5YRzB02.js → hyperframes-player-DKWM-Ons.js} +1 -1
- package/dist/assets/index-DrHQAuGO.css +1 -0
- package/dist/assets/index-HfP3sap2.js +390 -0
- package/dist/assets/{index-BTAfgesM.js → index-UvW6BEwf.js} +1 -1
- package/dist/assets/{index-Du5x7KUR.js → index-Y4OnpTEM.js} +1 -1
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.html +2 -2
- package/dist/index.js +7664 -7953
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -0
- package/package.json +8 -7
- package/src/App.tsx +42 -48
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +52 -56
- package/src/components/StudioHeader.dom.test.tsx +1 -1
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +10 -13
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -206
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/dock/Dock.test.tsx +16 -2
- package/src/components/dock/Dock.tsx +13 -3
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +21 -4
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/TimelinePane.tsx +46 -67
- package/src/components/panels/SlideshowPanel.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +1 -1
- package/src/components/renders/RenderQueue.test.tsx +133 -25
- package/src/components/renders/RenderQueue.tsx +99 -92
- package/src/components/renders/RenderQueueItem.tsx +69 -64
- package/src/components/renders/RenderQueuePanel.tsx +1 -1
- package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
- package/src/components/sidebar/CompositionsPanel.tsx +15 -0
- package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx} +6 -11
- package/src/components/sidebar/SidebarLintButton.tsx +50 -0
- package/src/components/ui/Select.tsx +4 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/headerExports.test.tsx +23 -0
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.test.ts +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
- package/src/hooks/useAppHotkeys.ts +0 -4
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +64 -0
- package/src/hooks/useSlideshowTabState.test.ts +9 -31
- package/src/hooks/useSlideshowTabState.ts +6 -19
- package/src/hooks/useStudioContextValue.test.ts +6 -18
- package/src/hooks/useStudioContextValue.ts +5 -26
- package/src/index.ts +2 -1
- package/src/main.tsx +26 -0
- package/src/player/components/Timeline.tsx +1 -1
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/utils/studioHelpers.ts +0 -7
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +0 -16
- package/dist/assets/index-CxAu_Okg.css +0 -1
- package/dist/assets/index-DocJlW2v.js +0 -376
- package/src/components/StudioLeftSidebar.tsx +0 -207
- package/src/components/nle/TimelineResizeDivider.tsx +0 -101
- package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
- package/src/components/sidebar/LeftSidebar.tsx +0 -348
- package/src/hooks/useInspectorSplitResize.ts +0 -51
- package/src/utils/fitPanels.test.ts +0 -164
- 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 {
|
|
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 {
|
|
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
|
|
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
|
-
}:
|
|
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
|
|
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
|
-
|
|
173
|
-
setRightPanelTab,
|
|
152
|
+
slideshowVisible,
|
|
174
153
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
+
});
|