@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.53",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"types": "./dist/styles/tailwind-preset.d.ts"
|
|
25
25
|
},
|
|
26
26
|
"./theme.css": "./src/styles/theme.css",
|
|
27
|
+
"./styles.css": "./dist/styles.css",
|
|
27
28
|
"./package.json": "./package.json"
|
|
28
29
|
},
|
|
29
30
|
"publishConfig": {
|
|
@@ -50,11 +51,11 @@
|
|
|
50
51
|
"dockview-react": "8.3.1",
|
|
51
52
|
"gsap": "^3.13.0",
|
|
52
53
|
"mediabunny": "^1.45.3",
|
|
53
|
-
"@hyperframes/core": "0.8.
|
|
54
|
-
"@hyperframes/
|
|
55
|
-
"@hyperframes/
|
|
56
|
-
"@hyperframes/parsers": "0.8.
|
|
57
|
-
"@hyperframes/studio-server": "0.8.
|
|
54
|
+
"@hyperframes/core": "0.8.53",
|
|
55
|
+
"@hyperframes/sdk": "0.8.53",
|
|
56
|
+
"@hyperframes/player": "0.8.53",
|
|
57
|
+
"@hyperframes/parsers": "0.8.53",
|
|
58
|
+
"@hyperframes/studio-server": "0.8.53"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
"vite": "^6.4.2",
|
|
71
72
|
"vitest": "^4.1.11",
|
|
72
73
|
"zustand": "^5.0.0",
|
|
73
|
-
"@hyperframes/producer": "0.8.
|
|
74
|
+
"@hyperframes/producer": "0.8.53"
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
77
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
|
|
2
2
|
import { buildProjectApiPath } from "./utils/projectRouting";
|
|
3
3
|
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
|
|
4
|
-
import
|
|
4
|
+
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
|
|
5
5
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
6
6
|
import { usePlayerStore } from "./player";
|
|
7
7
|
import { StudioOverlays } from "./components/StudioOverlays";
|
|
@@ -48,9 +48,9 @@ import type { DomEditSelection } from "./components/editor/domEditing";
|
|
|
48
48
|
import { StudioHeader } from "./components/StudioHeader";
|
|
49
49
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
50
50
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
51
|
-
import {
|
|
51
|
+
import { StudioLeftPanels } from "./components/StudioLeftPanels";
|
|
52
52
|
import { EditorShell } from "./components/EditorShell";
|
|
53
|
-
import {
|
|
53
|
+
import { StudioRightPanels } from "./components/StudioRightPanels";
|
|
54
54
|
import { TimelineToolbar } from "./components/TimelineToolbar";
|
|
55
55
|
import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
|
|
56
56
|
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
|
|
@@ -75,7 +75,6 @@ export function StudioApp() {
|
|
|
75
75
|
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
|
|
76
76
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
77
77
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
78
|
-
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
79
78
|
const captionEditMode = useCaptionStore((s) => s.isEditMode);
|
|
80
79
|
const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
|
|
81
80
|
const captionSync = useCaptionSync(projectId);
|
|
@@ -202,6 +201,13 @@ export function StudioApp() {
|
|
|
202
201
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
203
202
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
204
203
|
});
|
|
204
|
+
const dismissBlockParams = useCallback(() => setActiveBlockParams(null), [setActiveBlockParams]);
|
|
205
|
+
const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
|
|
206
|
+
const layout = useMemo(
|
|
207
|
+
() => ({ ...panelLayout, setRightPanelTab }),
|
|
208
|
+
[panelLayout, setRightPanelTab],
|
|
209
|
+
);
|
|
210
|
+
|
|
205
211
|
const clearDomSelectionRef = useRef<() => void>(() => {});
|
|
206
212
|
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
|
|
207
213
|
type DomEditDelete = (s: DomEditSelection, o?: { expandGroup?: boolean }) => Promise<void>;
|
|
@@ -234,7 +240,6 @@ export function StudioApp() {
|
|
|
234
240
|
showToast,
|
|
235
241
|
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
|
|
236
242
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
237
|
-
leftSidebarRef,
|
|
238
243
|
handleCopy,
|
|
239
244
|
handlePaste,
|
|
240
245
|
handleCut,
|
|
@@ -248,10 +253,6 @@ export function StudioApp() {
|
|
|
248
253
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
249
254
|
onToggleRecording: () => handleToggleRecordingRef.current(),
|
|
250
255
|
});
|
|
251
|
-
const sidebarTabRef = useRef({
|
|
252
|
-
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
|
|
253
|
-
get: () => leftSidebarRef.current?.getTab() ?? "compositions",
|
|
254
|
-
});
|
|
255
256
|
const domEditSession = useDomEditSession({
|
|
256
257
|
projectId,
|
|
257
258
|
activeCompPath,
|
|
@@ -264,7 +265,7 @@ export function StudioApp() {
|
|
|
264
265
|
setSelectedTimelineElementId,
|
|
265
266
|
setTimelineSelectionSet,
|
|
266
267
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
267
|
-
setRightPanelTab
|
|
268
|
+
setRightPanelTab,
|
|
268
269
|
showToast,
|
|
269
270
|
isRecordingRef: isGestureRecordingRef,
|
|
270
271
|
refreshPreviewDocumentVersion,
|
|
@@ -286,8 +287,6 @@ export function StudioApp() {
|
|
|
286
287
|
reloadPreview,
|
|
287
288
|
setRefreshKey,
|
|
288
289
|
openSourceForSelection: fileManager.openSourceForSelection,
|
|
289
|
-
selectSidebarTab: sidebarTabRef.current.select,
|
|
290
|
-
getSidebarTab: sidebarTabRef.current.get,
|
|
291
290
|
sdkSession: editFlowSdkSession,
|
|
292
291
|
publishSdkSession: sdkHandle.publish,
|
|
293
292
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
@@ -367,20 +366,18 @@ export function StudioApp() {
|
|
|
367
366
|
},
|
|
368
367
|
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
369
368
|
);
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
gestureState === "recording",
|
|
383
|
-
);
|
|
369
|
+
const rightPanel = useRightPanelIntent();
|
|
370
|
+
const { inspectorPanelActive, shouldShowMotionPath, shouldShowSelectedDomBounds } =
|
|
371
|
+
useInspectorState(
|
|
372
|
+
rightPanel,
|
|
373
|
+
isPlaying,
|
|
374
|
+
domEditSession.domEditSelection,
|
|
375
|
+
gestureState === "recording",
|
|
376
|
+
);
|
|
377
|
+
// The dock has no separate "railed by window width" state (it shrinks
|
|
378
|
+
// panels, never auto-hides the group), so rightCollapsed is already the
|
|
379
|
+
// value that decides whether the panel is actually showing.
|
|
380
|
+
const inspectorButtonActive = !panelLayout.rightCollapsed && inspectorPanelActive;
|
|
384
381
|
useStudioUrlState({
|
|
385
382
|
projectId,
|
|
386
383
|
activeCompPath,
|
|
@@ -397,7 +394,7 @@ export function StudioApp() {
|
|
|
397
394
|
applyMarqueeSelection: domEditSession.applyMarqueeSelection,
|
|
398
395
|
buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
|
|
399
396
|
applyDomSelection: domEditSession.applyDomSelection,
|
|
400
|
-
setRightPanelTab
|
|
397
|
+
setRightPanelTab,
|
|
401
398
|
initialState: initialUrlStateRef.current,
|
|
402
399
|
});
|
|
403
400
|
const studioCtxValue = buildStudioContextValue({
|
|
@@ -437,7 +434,7 @@ export function StudioApp() {
|
|
|
437
434
|
return (
|
|
438
435
|
<StudioShellProvider value={studioCtxValue}>
|
|
439
436
|
<StudioPlaybackProvider value={studioCtxValue}>
|
|
440
|
-
<PanelLayoutProvider value={
|
|
437
|
+
<PanelLayoutProvider value={layout}>
|
|
441
438
|
<FileManagerProvider value={fileManager}>
|
|
442
439
|
<DomEditProvider value={domEditSession}>
|
|
443
440
|
<div
|
|
@@ -468,26 +465,23 @@ export function StudioApp() {
|
|
|
468
465
|
)}
|
|
469
466
|
<ExternalFileConflictBanner coordinator={externalFileChanges} />
|
|
470
467
|
<EditorShell
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
right={
|
|
487
|
-
panelLayout.effectiveRightCollapsed ? null : (
|
|
488
|
-
<StudioRightPanel
|
|
489
|
-
designPanelActive={designPanelActive}
|
|
468
|
+
panels={
|
|
469
|
+
<>
|
|
470
|
+
<StudioLeftPanels
|
|
471
|
+
onSelectComposition={handleSelectComposition}
|
|
472
|
+
onAddBlock={handleAddBlock}
|
|
473
|
+
onPreviewBlock={setBlockPreview}
|
|
474
|
+
onLint={handleLint}
|
|
475
|
+
linting={linting}
|
|
476
|
+
lintFindingCount={lintModal?.length ?? findingsByFile.size}
|
|
477
|
+
lintFindingsByFile={findingsByFile}
|
|
478
|
+
lintHasError={hasLintError}
|
|
479
|
+
onAddAssetToTimeline={handleAddAssetAtPlayhead}
|
|
480
|
+
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
|
|
481
|
+
/>
|
|
482
|
+
<StudioRightPanels
|
|
490
483
|
activeBlockParams={activeBlockParams}
|
|
484
|
+
onDismissBlockParams={dismissBlockParams}
|
|
491
485
|
onCloseBlockParams={() => {
|
|
492
486
|
setActiveBlockParams(null);
|
|
493
487
|
panelLayout.setRightPanelTab("design");
|
|
@@ -504,7 +498,7 @@ export function StudioApp() {
|
|
|
504
498
|
onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
|
|
505
499
|
onAddMediaOverlay={handleAddMediaOverlay}
|
|
506
500
|
/>
|
|
507
|
-
|
|
501
|
+
</>
|
|
508
502
|
}
|
|
509
503
|
timelineToolbar={timelineToolbar}
|
|
510
504
|
renderClipContent={renderClipContent}
|
|
@@ -42,7 +42,6 @@ vi.mock("./nle/NLEContext", () => ({
|
|
|
42
42
|
useNLEContext: () => ({
|
|
43
43
|
compositionStack: [],
|
|
44
44
|
updateCompositionStack: vi.fn(),
|
|
45
|
-
containerRef: { current: null },
|
|
46
45
|
}),
|
|
47
46
|
}));
|
|
48
47
|
vi.mock("./nle/useTimelineEditCallbacks", () => ({
|
|
@@ -53,7 +52,14 @@ vi.mock("./nle/PreviewOverlays", () => ({ PreviewOverlays: () => null }));
|
|
|
53
52
|
vi.mock("./nle/TimelinePane", () => ({ TimelinePane: () => null }));
|
|
54
53
|
vi.mock("../captions/components/CaptionTimeline", () => ({ CaptionTimeline: () => null }));
|
|
55
54
|
|
|
56
|
-
Object.assign(globalThis, {
|
|
55
|
+
Object.assign(globalThis, {
|
|
56
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
57
|
+
ResizeObserver: class {
|
|
58
|
+
observe() {}
|
|
59
|
+
unobserve() {}
|
|
60
|
+
disconnect() {}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
57
63
|
|
|
58
64
|
afterEach(() => {
|
|
59
65
|
document.body.innerHTML = "";
|
|
@@ -69,8 +75,7 @@ describe("EditorShell timeline selection sync", () => {
|
|
|
69
75
|
act(() => {
|
|
70
76
|
root.render(
|
|
71
77
|
<EditorShell
|
|
72
|
-
|
|
73
|
-
right={null}
|
|
78
|
+
panels={null}
|
|
74
79
|
timelineToolbar={null}
|
|
75
80
|
renderClipContent={() => null}
|
|
76
81
|
handleTimelineElementDelete={vi.fn()}
|
|
@@ -17,6 +17,7 @@ import type { GestureRecordingState } from "./editor/GestureRecordControl";
|
|
|
17
17
|
import { useTimelineSelectionPreviewSync } from "../hooks/useTimelineSelectionPreviewSync";
|
|
18
18
|
import { StudioAgentTools } from "../webmcp/StudioAgentTools";
|
|
19
19
|
import type { TimelineDropPlacement } from "../player/components/timelineCallbacks";
|
|
20
|
+
import { Dock } from "./dock/Dock";
|
|
20
21
|
|
|
21
22
|
type RenderClipContent = (
|
|
22
23
|
element: TimelineElement,
|
|
@@ -26,10 +27,8 @@ type RenderClipContent = (
|
|
|
26
27
|
// The seven move/resize/split/razor handlers come from TimelineEditCallbackDeps
|
|
27
28
|
// (shared with useTimelineEditCallbacks); the rest are drop + wiring props.
|
|
28
29
|
export interface EditorShellProps extends TimelineEditCallbackDeps {
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
/** Right panel (inspector/design) or null when collapsed, in the top row. */
|
|
32
|
-
right: ReactNode;
|
|
30
|
+
/** Dock.Panel elements for every panel except the built-in preview and timeline. */
|
|
31
|
+
panels: ReactNode;
|
|
33
32
|
timelineToolbar: ReactNode;
|
|
34
33
|
renderClipContent: RenderClipContent;
|
|
35
34
|
handleTimelineElementDelete: (element: TimelineElement) => Promise<void> | void;
|
|
@@ -72,12 +71,11 @@ export interface EditorShellProps extends TimelineEditCallbackDeps {
|
|
|
72
71
|
gestureOverlay?: ReactNode;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
// The
|
|
76
|
-
//
|
|
77
|
-
//
|
|
74
|
+
// The dockable shell: every panel lives in one Dock, arranged by the user's
|
|
75
|
+
// saved layout. Owns the shared player + composition-stack state via
|
|
76
|
+
// NLEProvider so every panel shares one player.
|
|
78
77
|
export function EditorShell({
|
|
79
|
-
|
|
80
|
-
right,
|
|
78
|
+
panels,
|
|
81
79
|
timelineToolbar,
|
|
82
80
|
renderClipContent,
|
|
83
81
|
handleTimelineElementDelete,
|
|
@@ -178,8 +176,8 @@ export function EditorShell({
|
|
|
178
176
|
}}
|
|
179
177
|
>
|
|
180
178
|
<EditorShellBody
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
projectId={projectId}
|
|
180
|
+
panels={panels}
|
|
183
181
|
captionEditMode={captionEditMode}
|
|
184
182
|
onSelectTimelineElement={handleTimelineElementSelect}
|
|
185
183
|
onPreviewBlockDrop={handlePreviewBlockDrop}
|
|
@@ -213,8 +211,8 @@ export function EditorShell({
|
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
interface EditorShellBodyProps {
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
panels: ReactNode;
|
|
215
|
+
projectId: string;
|
|
218
216
|
captionEditMode: boolean;
|
|
219
217
|
previewOverlay: ReactNode;
|
|
220
218
|
onSelectTimelineElement: (element: TimelineElement | null) => void;
|
|
@@ -239,8 +237,8 @@ interface EditorShellBodyProps {
|
|
|
239
237
|
}
|
|
240
238
|
|
|
241
239
|
function EditorShellBody({
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
panels,
|
|
241
|
+
projectId,
|
|
244
242
|
captionEditMode,
|
|
245
243
|
previewOverlay,
|
|
246
244
|
onSelectTimelineElement,
|
|
@@ -257,7 +255,7 @@ function EditorShellBody({
|
|
|
257
255
|
onDuplicateClip,
|
|
258
256
|
canPasteClip,
|
|
259
257
|
}: EditorShellBodyProps) {
|
|
260
|
-
const { compositionStack, updateCompositionStack
|
|
258
|
+
const { compositionStack, updateCompositionStack } = useNLEContext();
|
|
261
259
|
|
|
262
260
|
// The caption track's blocks are seek targets; CaptionTimeline took an onSeek
|
|
263
261
|
// prop that nothing ever passed, so clicking a block did nothing.
|
|
@@ -277,7 +275,6 @@ function EditorShellBody({
|
|
|
277
275
|
|
|
278
276
|
return (
|
|
279
277
|
<div
|
|
280
|
-
ref={containerRef}
|
|
281
278
|
// Shell canvas is a step LIGHTER than the near-black panel cards so the
|
|
282
279
|
// gaps between panels read as visible seams (CapCut-style).
|
|
283
280
|
className="flex flex-col flex-1 min-h-0 bg-panel-surface"
|
|
@@ -287,47 +284,46 @@ function EditorShellBody({
|
|
|
287
284
|
{/* Renders nothing; exposes Studio's state to an agentic browser. Mounted
|
|
288
285
|
here rather than in App because it needs the DomEdit contexts. */}
|
|
289
286
|
<StudioAgentTools />
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
287
|
+
<Dock.Root projectId={projectId}>
|
|
288
|
+
<Dock.Panel id="preview">
|
|
289
|
+
<div className="relative flex h-full w-full min-w-0 flex-col">
|
|
290
|
+
<PreviewPane
|
|
291
|
+
previewOverlay={previewOverlay}
|
|
292
|
+
onSelectTimelineElement={onSelectTimelineElement}
|
|
293
|
+
onPreviewBlockDrop={onPreviewBlockDrop}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</Dock.Panel>
|
|
297
|
+
<Dock.Panel id="timeline">
|
|
298
|
+
<TimelinePane
|
|
299
|
+
timelineToolbar={timelineToolbar}
|
|
300
|
+
renderClipContent={renderClipContent}
|
|
301
|
+
onFileDrop={onFileDrop}
|
|
302
|
+
onAssetDrop={onAssetDrop}
|
|
303
|
+
onBlockDrop={onBlockDrop}
|
|
304
|
+
onCompositionDrop={onCompositionDrop}
|
|
305
|
+
onDeleteElement={onDeleteElement}
|
|
306
|
+
onCopyClip={onCopyClip}
|
|
307
|
+
onPasteClip={onPasteClip}
|
|
308
|
+
onDuplicateClip={onDuplicateClip}
|
|
309
|
+
canPasteClip={canPasteClip}
|
|
297
310
|
onSelectTimelineElement={onSelectTimelineElement}
|
|
298
|
-
|
|
311
|
+
timelineFooter={
|
|
312
|
+
captionEditMode ? (
|
|
313
|
+
<div className="border-t border-neutral-800/30 shrink-0" style={{ height: 60 }}>
|
|
314
|
+
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
315
|
+
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
316
|
+
Captions
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
<CaptionTimeline pixelsPerSecond={100} onSeek={seekCaptionTime} />
|
|
320
|
+
</div>
|
|
321
|
+
) : undefined
|
|
322
|
+
}
|
|
299
323
|
/>
|
|
300
|
-
</
|
|
301
|
-
{
|
|
302
|
-
</
|
|
303
|
-
|
|
304
|
-
{/* Full-width timeline row */}
|
|
305
|
-
<TimelinePane
|
|
306
|
-
timelineToolbar={timelineToolbar}
|
|
307
|
-
renderClipContent={renderClipContent}
|
|
308
|
-
onFileDrop={onFileDrop}
|
|
309
|
-
onAssetDrop={onAssetDrop}
|
|
310
|
-
onBlockDrop={onBlockDrop}
|
|
311
|
-
onCompositionDrop={onCompositionDrop}
|
|
312
|
-
onDeleteElement={onDeleteElement}
|
|
313
|
-
onCopyClip={onCopyClip}
|
|
314
|
-
onPasteClip={onPasteClip}
|
|
315
|
-
onDuplicateClip={onDuplicateClip}
|
|
316
|
-
canPasteClip={canPasteClip}
|
|
317
|
-
onSelectTimelineElement={onSelectTimelineElement}
|
|
318
|
-
timelineFooter={
|
|
319
|
-
captionEditMode ? (
|
|
320
|
-
<div className="border-t border-neutral-800/30 shrink-0" style={{ height: 60 }}>
|
|
321
|
-
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
322
|
-
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
323
|
-
Captions
|
|
324
|
-
</span>
|
|
325
|
-
</div>
|
|
326
|
-
<CaptionTimeline pixelsPerSecond={100} onSeek={seekCaptionTime} />
|
|
327
|
-
</div>
|
|
328
|
-
) : undefined
|
|
329
|
-
}
|
|
330
|
-
/>
|
|
324
|
+
</Dock.Panel>
|
|
325
|
+
{panels}
|
|
326
|
+
</Dock.Root>
|
|
331
327
|
</div>
|
|
332
328
|
);
|
|
333
329
|
}
|
|
@@ -33,7 +33,7 @@ vi.mock("../contexts/StudioContext", () => ({
|
|
|
33
33
|
|
|
34
34
|
vi.mock("../contexts/PanelLayoutContext", () => ({
|
|
35
35
|
usePanelLayoutContext: () => ({
|
|
36
|
-
|
|
36
|
+
rightCollapsed: false,
|
|
37
37
|
setRightCollapsed: vi.fn(),
|
|
38
38
|
setRightPanelTab: vi.fn(),
|
|
39
39
|
}),
|
|
@@ -13,16 +13,4 @@ describe("shouldOpenInspector", () => {
|
|
|
13
13
|
it("closes when the inspector is genuinely on screen", () => {
|
|
14
14
|
expect(shouldOpenInspector(false, true)).toBe(false);
|
|
15
15
|
});
|
|
16
|
-
|
|
17
|
-
it("opens when the window railed the panel away", () => {
|
|
18
|
-
// The regression this guards: the button used to branch on the raw
|
|
19
|
-
// rightCollapsed intent, which is still `false` while the window has the
|
|
20
|
-
// panel railed. That took the close branch, wrote rightCollapsed=true, and
|
|
21
|
-
// since that value is synced into the shareable Studio URL, a click that
|
|
22
|
-
// did nothing visible rewrote the link.
|
|
23
|
-
const userIntentIsOpen = false;
|
|
24
|
-
const windowRailedItAway = true;
|
|
25
|
-
expect(shouldOpenInspector(windowRailedItAway, true)).toBe(true);
|
|
26
|
-
expect(shouldOpenInspector(userIntentIsOpen, true)).toBe(false);
|
|
27
|
-
});
|
|
28
16
|
});
|
|
@@ -4,6 +4,7 @@ import { useStudioShellContext } from "../contexts/StudioContext";
|
|
|
4
4
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
5
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
6
6
|
import { Button, buttonBase, buttonSizes, buttonVariants, cn, Tooltip } from "./ui";
|
|
7
|
+
import { Dock } from "./dock/Dock";
|
|
7
8
|
|
|
8
9
|
export interface StudioHeaderProps {
|
|
9
10
|
captureFrameHref: string;
|
|
@@ -16,7 +17,7 @@ export interface StudioHeaderProps {
|
|
|
16
17
|
onExport?: () => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function HyperframesLogo() {
|
|
20
|
+
export function HyperframesLogo() {
|
|
20
21
|
// Full logo from logo-dark.svg (263×79): heygen label + gradient mark + hyperframes wordmark.
|
|
21
22
|
// All fill="black" paths inverted to white for the dark header.
|
|
22
23
|
const height = 28;
|
|
@@ -140,16 +141,15 @@ function HyperframesLogo() {
|
|
|
140
141
|
/**
|
|
141
142
|
* Does the header's Inspector button open the panel, or close it?
|
|
142
143
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* 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.
|
|
147
147
|
*/
|
|
148
148
|
export function shouldOpenInspector(
|
|
149
|
-
|
|
149
|
+
rightCollapsed: boolean,
|
|
150
150
|
inspectorPanelActive: boolean,
|
|
151
151
|
): boolean {
|
|
152
|
-
return
|
|
152
|
+
return rightCollapsed || !inspectorPanelActive;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// fallow-ignore-next-line complexity
|
|
@@ -164,11 +164,7 @@ export function StudioHeader({
|
|
|
164
164
|
onExport,
|
|
165
165
|
}: StudioHeaderProps) {
|
|
166
166
|
const { projectId, renderQueue } = useStudioShellContext();
|
|
167
|
-
|
|
168
|
-
// intent is still "open" while the panel is hidden, so branching on intent
|
|
169
|
-
// made this button write rightCollapsed=true — and that value is synced into
|
|
170
|
-
// the shareable Studio URL, so a dead click would rewrite a link.
|
|
171
|
-
const { effectiveRightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
167
|
+
const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
172
168
|
const isRendering = renderQueue.isRendering;
|
|
173
169
|
const ffmpegMissing = renderQueue.ffmpegMissing;
|
|
174
170
|
|
|
@@ -265,7 +261,7 @@ export function StudioHeader({
|
|
|
265
261
|
</svg>
|
|
266
262
|
}
|
|
267
263
|
onClick={() => {
|
|
268
|
-
if (shouldOpenInspector(
|
|
264
|
+
if (shouldOpenInspector(rightCollapsed, inspectorPanelActive)) {
|
|
269
265
|
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
270
266
|
setRightPanelTab("design");
|
|
271
267
|
setRightCollapsed(false);
|
|
@@ -281,6 +277,7 @@ export function StudioHeader({
|
|
|
281
277
|
</Button>
|
|
282
278
|
</Tooltip>
|
|
283
279
|
</div>
|
|
280
|
+
<Dock.WindowMenu />
|
|
284
281
|
<Tooltip
|
|
285
282
|
label={
|
|
286
283
|
ffmpegMissing
|