@hyperframes/studio 0.8.51 → 0.8.53-desktop.0
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-Cn_PpHqP.js → hyperframes-player-PTEnAHhg.js} +1 -1
- package/dist/assets/{index-CPS3Cvj8.js → index--myXp8ui.js} +1 -1
- package/dist/assets/{index-Clrb11lj.js → index-BjslOS9g.js} +1 -1
- package/dist/assets/index-BnXZuIJD.css +1 -0
- package/dist/assets/index-CNyviK_l.js +390 -0
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +808 -12
- package/dist/index.html +2 -2
- package/dist/index.js +8109 -7583
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/App.tsx +45 -50
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +56 -56
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +117 -160
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +3 -7
- package/src/components/dock/Dock.test.tsx +100 -18
- package/src/components/dock/Dock.tsx +32 -5
- package/src/components/dock/dock.css +5 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +53 -6
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +21 -4
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +4 -2
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +3 -1
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +1 -1
- package/src/components/nle/TimelinePane.tsx +53 -67
- package/src/components/panels/SlideshowPanel.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +1 -1
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/RenderQueue.tsx +1 -1
- 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/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +222 -30
- package/src/components/ui/Button.tsx +88 -125
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/timelineEditPermission.ts +31 -0
- 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 +20 -83
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +63 -0
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- 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/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +322 -3
- package/src/hooks/useTimelineEditing.ts +103 -16
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +87 -0
- package/src/player/components/AutomationSelectionMenu.tsx +1 -1
- package/src/player/components/Timeline.tsx +1 -1
- package/src/player/components/TimelineCanvas.tsx +4 -4
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/theme.css +57 -0
- package/src/styles/theme.test.ts +2 -15
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -8
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +0 -16
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-C05SNrvY.js +0 -360
- package/dist/assets/index-DUR9qzQM.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- 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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { MouseEvent } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
2
|
+
import { Camera } from "../icons/SystemIcons";
|
|
4
3
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
5
4
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
6
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
7
|
-
import { Tooltip } from "./ui";
|
|
6
|
+
import { Button, buttonBase, buttonSizes, buttonVariants, cn, Tooltip } from "./ui";
|
|
7
|
+
import { Dock } from "./dock/Dock";
|
|
8
8
|
|
|
9
9
|
export interface StudioHeaderProps {
|
|
10
10
|
captureFrameHref: string;
|
|
@@ -40,8 +40,8 @@ function HyperframesLogo() {
|
|
|
40
40
|
y2="37.482"
|
|
41
41
|
gradientUnits="userSpaceOnUse"
|
|
42
42
|
>
|
|
43
|
-
<stop stopColor="
|
|
44
|
-
<stop offset="1" stopColor="
|
|
43
|
+
<stop stopColor="var(--color-brand-gradient-from)" />
|
|
44
|
+
<stop offset="1" stopColor="var(--color-brand-gradient-to)" />
|
|
45
45
|
</linearGradient>
|
|
46
46
|
<linearGradient
|
|
47
47
|
id="hf-g1"
|
|
@@ -51,8 +51,8 @@ function HyperframesLogo() {
|
|
|
51
51
|
y2="6.303"
|
|
52
52
|
gradientUnits="userSpaceOnUse"
|
|
53
53
|
>
|
|
54
|
-
<stop stopColor="
|
|
55
|
-
<stop offset="1" stopColor="
|
|
54
|
+
<stop stopColor="var(--color-brand-gradient-from)" />
|
|
55
|
+
<stop offset="1" stopColor="var(--color-brand-gradient-to)" />
|
|
56
56
|
</linearGradient>
|
|
57
57
|
</defs>
|
|
58
58
|
{/* heygen label */}
|
|
@@ -141,16 +141,15 @@ function HyperframesLogo() {
|
|
|
141
141
|
/**
|
|
142
142
|
* Does the header's Inspector button open the panel, or close it?
|
|
143
143
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* to keep out.
|
|
144
|
+
* The dock has no separate "railed by window width" state (it shrinks panels,
|
|
145
|
+
* never auto-hides the group), so `rightCollapsed` here is already the state
|
|
146
|
+
* that decides whether the panel is actually showing.
|
|
148
147
|
*/
|
|
149
148
|
export function shouldOpenInspector(
|
|
150
|
-
|
|
149
|
+
rightCollapsed: boolean,
|
|
151
150
|
inspectorPanelActive: boolean,
|
|
152
151
|
): boolean {
|
|
153
|
-
return
|
|
152
|
+
return rightCollapsed || !inspectorPanelActive;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
// fallow-ignore-next-line complexity
|
|
@@ -164,163 +163,121 @@ export function StudioHeader({
|
|
|
164
163
|
inspectorPanelActive,
|
|
165
164
|
onExport,
|
|
166
165
|
}: StudioHeaderProps) {
|
|
167
|
-
const { projectId,
|
|
168
|
-
|
|
169
|
-
// intent is still "open" while the panel is hidden, so branching on intent
|
|
170
|
-
// made this button write rightCollapsed=true — and that value is synced into
|
|
171
|
-
// the shareable Studio URL, so a dead click would rewrite a link.
|
|
172
|
-
const { effectiveRightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
166
|
+
const { projectId, renderQueue } = useStudioShellContext();
|
|
167
|
+
const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
173
168
|
const isRendering = renderQueue.isRendering;
|
|
174
169
|
const ffmpegMissing = renderQueue.ffmpegMissing;
|
|
175
170
|
|
|
176
171
|
return (
|
|
177
|
-
<div className="flex items-center justify-between h-10 px-3 bg-
|
|
172
|
+
<div className="flex items-center justify-between h-10 px-3 bg-surface border-b border-border-strong shrink-0">
|
|
178
173
|
{/* Left: logo + project name */}
|
|
179
174
|
<div className="flex items-center gap-3">
|
|
180
175
|
<HyperframesLogo />
|
|
181
|
-
<span className="text-
|
|
176
|
+
<span className="text-text-5 select-none" aria-hidden="true">
|
|
182
177
|
|
|
|
183
178
|
</span>
|
|
184
|
-
<span className="text-
|
|
179
|
+
<span className="text-step-11 font-medium text-text-1">{projectId}</span>
|
|
185
180
|
</div>
|
|
186
181
|
{/* Right: toolbar buttons */}
|
|
187
|
-
<div className="flex items-center gap-
|
|
188
|
-
<
|
|
189
|
-
label={
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<svg
|
|
262
|
-
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
263
|
-
viewBox="0 0 24 24"
|
|
264
|
-
fill="none"
|
|
265
|
-
aria-hidden="true"
|
|
266
|
-
>
|
|
267
|
-
<circle
|
|
268
|
-
className="opacity-25"
|
|
269
|
-
cx="12"
|
|
270
|
-
cy="12"
|
|
271
|
-
r="10"
|
|
182
|
+
<div className="flex items-center gap-3">
|
|
183
|
+
<div className="flex h-ctl items-center divide-x divide-border-strong overflow-hidden rounded-md border border-border-strong bg-bg-2">
|
|
184
|
+
<Tooltip label={capturing ? "Capturing frame…" : "Capture current frame"} side="bottom">
|
|
185
|
+
{/* A real download link, so it wears Button's recipe rather than being
|
|
186
|
+
one: `download` on an <a> is what saves the frame, and no <button>
|
|
187
|
+
can do that. `enabled:` never matches a link, so the ghost
|
|
188
|
+
variant's hover look is repeated unprefixed here. */}
|
|
189
|
+
<a
|
|
190
|
+
href={captureFrameHref}
|
|
191
|
+
download={captureFrameFilename}
|
|
192
|
+
onClick={(e) => {
|
|
193
|
+
if (capturing) {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
trackStudioEvent("toolbar_action", { action: "capture_frame" });
|
|
198
|
+
handleCaptureFrameClick(e);
|
|
199
|
+
}}
|
|
200
|
+
onFocus={refreshCaptureFrameTime}
|
|
201
|
+
onPointerDown={refreshCaptureFrameTime}
|
|
202
|
+
aria-disabled={capturing || undefined}
|
|
203
|
+
className={cn(
|
|
204
|
+
buttonBase,
|
|
205
|
+
buttonVariants.ghost,
|
|
206
|
+
buttonSizes.md,
|
|
207
|
+
"h-full rounded-none max-[1000px]:px-2",
|
|
208
|
+
capturing
|
|
209
|
+
? "text-text-4 cursor-default"
|
|
210
|
+
: "hover:bg-hover hover:text-text-0 active:scale-[0.98]",
|
|
211
|
+
)}
|
|
212
|
+
aria-label={capturing ? "Capturing frame" : "Capture current frame"}
|
|
213
|
+
>
|
|
214
|
+
{capturing ? (
|
|
215
|
+
<svg
|
|
216
|
+
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
217
|
+
viewBox="0 0 24 24"
|
|
218
|
+
fill="none"
|
|
219
|
+
aria-hidden="true"
|
|
220
|
+
>
|
|
221
|
+
<circle
|
|
222
|
+
className="opacity-25"
|
|
223
|
+
cx="12"
|
|
224
|
+
cy="12"
|
|
225
|
+
r="10"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
strokeWidth="4"
|
|
228
|
+
/>
|
|
229
|
+
<path
|
|
230
|
+
className="opacity-75"
|
|
231
|
+
fill="currentColor"
|
|
232
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
233
|
+
/>
|
|
234
|
+
</svg>
|
|
235
|
+
) : (
|
|
236
|
+
<Camera size={14} />
|
|
237
|
+
)}
|
|
238
|
+
<span className="max-[1000px]:hidden">{capturing ? "Capturing…" : "Capture"}</span>
|
|
239
|
+
</a>
|
|
240
|
+
</Tooltip>
|
|
241
|
+
<Tooltip label="Inspector" side="bottom">
|
|
242
|
+
<Button
|
|
243
|
+
variant="ghost"
|
|
244
|
+
aria-label="Inspector"
|
|
245
|
+
aria-pressed={inspectorButtonActive}
|
|
246
|
+
className={cn(
|
|
247
|
+
"h-full rounded-none",
|
|
248
|
+
inspectorButtonActive && "bg-hover text-accent enabled:hover:text-accent",
|
|
249
|
+
)}
|
|
250
|
+
icon={
|
|
251
|
+
<svg
|
|
252
|
+
width="12"
|
|
253
|
+
height="12"
|
|
254
|
+
viewBox="0 0 24 24"
|
|
255
|
+
fill="none"
|
|
272
256
|
stroke="currentColor"
|
|
273
|
-
strokeWidth="
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
279
|
-
/>
|
|
280
|
-
</svg>
|
|
281
|
-
) : (
|
|
282
|
-
<Camera size={14} />
|
|
283
|
-
)}
|
|
284
|
-
<span>{capturing ? "Capturing…" : "Capture"}</span>
|
|
285
|
-
</a>
|
|
286
|
-
</Tooltip>
|
|
287
|
-
<Tooltip label="Inspector" side="bottom">
|
|
288
|
-
<button
|
|
289
|
-
type="button"
|
|
290
|
-
onClick={() => {
|
|
291
|
-
if (shouldOpenInspector(effectiveRightCollapsed, inspectorPanelActive)) {
|
|
292
|
-
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
293
|
-
setRightPanelTab("design");
|
|
294
|
-
setRightCollapsed(false);
|
|
295
|
-
return;
|
|
257
|
+
strokeWidth="2"
|
|
258
|
+
>
|
|
259
|
+
<circle cx="12" cy="12" r="10" />
|
|
260
|
+
<polygon points="10 8 16 12 10 16" fill="currentColor" stroke="none" />
|
|
261
|
+
</svg>
|
|
296
262
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
>
|
|
310
|
-
<svg
|
|
311
|
-
width="12"
|
|
312
|
-
height="12"
|
|
313
|
-
viewBox="0 0 24 24"
|
|
314
|
-
fill="none"
|
|
315
|
-
stroke="currentColor"
|
|
316
|
-
strokeWidth="2"
|
|
263
|
+
onClick={() => {
|
|
264
|
+
if (shouldOpenInspector(rightCollapsed, inspectorPanelActive)) {
|
|
265
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
266
|
+
setRightPanelTab("design");
|
|
267
|
+
setRightCollapsed(false);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true });
|
|
271
|
+
// Keep the current selection when collapsing the Inspector — closing
|
|
272
|
+
// the panel shouldn't deselect the element.
|
|
273
|
+
setRightCollapsed(true);
|
|
274
|
+
}}
|
|
317
275
|
>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
</Tooltip>
|
|
276
|
+
Inspector
|
|
277
|
+
</Button>
|
|
278
|
+
</Tooltip>
|
|
279
|
+
</div>
|
|
280
|
+
<Dock.WindowMenu />
|
|
324
281
|
<Tooltip
|
|
325
282
|
label={
|
|
326
283
|
ffmpegMissing
|
|
@@ -331,8 +288,9 @@ export function StudioHeader({
|
|
|
331
288
|
}
|
|
332
289
|
side="bottom"
|
|
333
290
|
>
|
|
334
|
-
<
|
|
335
|
-
|
|
291
|
+
<Button
|
|
292
|
+
variant="primary"
|
|
293
|
+
data-testid="header-export"
|
|
336
294
|
disabled={isRendering}
|
|
337
295
|
onClick={() => {
|
|
338
296
|
if (isRendering) return;
|
|
@@ -346,10 +304,9 @@ export function StudioHeader({
|
|
|
346
304
|
if (ffmpegMissing) return;
|
|
347
305
|
onExport?.();
|
|
348
306
|
}}
|
|
349
|
-
className="h-7 flex items-center gap-1.5 px-3 rounded-md text-[11px] font-semibold bg-studio-accent text-[#09090B] enabled:hover:brightness-110 transition-[filter,transform] enabled:active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
350
307
|
>
|
|
351
308
|
{isRendering ? "Rendering…" : "Export"}
|
|
352
|
-
</
|
|
309
|
+
</Button>
|
|
353
310
|
</Tooltip>
|
|
354
311
|
</div>
|
|
355
312
|
</div>
|
|
@@ -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
|
+
}
|