@hyperframes/studio 0.8.55 → 0.8.57
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-c23wnIfc.js +464 -0
- package/dist/assets/{index-DrHQAuGO.css → index-DKJpHYYd.css} +1 -1
- package/dist/assets/{index-BEIRytvV.js → index-DgkAzx3n.js} +1 -1
- package/dist/assets/{index-DaNpNFJC.js → index-IDNr3X1H.js} +1 -1
- package/dist/assets/{index-TcBZyzNA.js → index-gbOCDCuZ.js} +202 -202
- package/dist/index.d.ts +111 -17
- package/dist/index.html +2 -2
- package/dist/index.js +21534 -20683
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +18 -1
- package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
- package/src/components/EditorShell.selectionSync.test.tsx +57 -67
- package/src/components/EditorShell.tsx +62 -51
- package/src/components/EditorShellTestMocks.ts +46 -0
- package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
- package/src/components/ProjectUnreachableBanner.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +19 -55
- package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
- package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
- package/src/components/editor/GridOverlay.tsx +12 -23
- package/src/components/editor/MotionPathOverlay.test.tsx +66 -0
- package/src/components/editor/MotionPathOverlay.tsx +3 -1
- package/src/components/editor/PreviewGuides.test.tsx +6 -1
- package/src/components/editor/PreviewGuides.tsx +8 -10
- package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
- package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
- package/src/components/editor/PropertyPanel.deselect.test.tsx +71 -0
- package/src/components/editor/PropertyPanel.tsx +4 -3
- package/src/components/editor/SnapToolbar.test.tsx +26 -12
- package/src/components/editor/SnapToolbar.tsx +30 -59
- package/src/components/editor/domEditOverlayTestMocks.ts +16 -0
- package/src/components/editor/previewReadOnly.overlay.test.tsx +260 -0
- package/src/components/editor/previewReadOnlyContext.tsx +59 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +9 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -1
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +3 -13
- package/src/components/editor/useDomEditNudge.ts +9 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +4 -0
- package/src/components/editor/useOffCanvasIndicators.ts +50 -0
- package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
- package/src/components/editor/usePreviewCompositionRect.ts +12 -12
- package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
- package/src/components/editor/useVolumeAutomation.ts +57 -16
- package/src/components/nle/NLEContext.tsx +40 -4
- package/src/components/nle/NLEPreview.test.ts +66 -1
- package/src/components/nle/NLEPreview.tsx +88 -44
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
- package/src/components/nle/PreviewOverlays.tsx +19 -24
- package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
- package/src/components/nle/PreviewPane.tsx +17 -2
- package/src/components/nle/TimelinePane.tsx +5 -1
- package/src/components/nle/previewReadOnly.test.tsx +129 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -6
- package/src/components/nle/usePreviewBlockDrop.ts +3 -1
- package/src/hooks/appHotkeysDispatch.ts +16 -1
- package/src/hooks/newTweenTargetHooks.test.tsx +1 -0
- package/src/hooks/timelineAudioGroupCreate.ts +3 -4
- package/src/hooks/timelineTrackVisibility.ts +7 -8
- package/src/hooks/useAppHotkeys.test.ts +71 -3
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +4 -0
- package/src/hooks/useCompositionDimensions.test.tsx +38 -0
- package/src/hooks/useCompositionDimensions.ts +17 -1
- package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
- package/src/hooks/useDomEditAttributeCommits.ts +14 -3
- package/src/hooks/useDomEditCommits.test.tsx +1 -0
- package/src/hooks/useDomEditCommits.ts +3 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -0
- package/src/hooks/useDomEditSession.ts +7 -4
- package/src/hooks/useDomEditWiring.ts +40 -40
- package/src/hooks/useDomGeometryCommits.test.tsx +85 -2
- package/src/hooks/useDomGeometryCommits.ts +26 -20
- package/src/hooks/useFileTree.test.tsx +56 -0
- package/src/hooks/useFileTree.ts +8 -1
- package/src/hooks/useGestureCommit.test.tsx +29 -0
- package/src/hooks/useGestureCommit.ts +23 -0
- package/src/hooks/useInlineTextEdit.test.tsx +23 -2
- package/src/hooks/useInlineTextEdit.ts +11 -2
- package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
- package/src/hooks/useSdkSession.ts +46 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
- package/src/hooks/useServerConnection.ts +81 -8
- package/src/hooks/useTimelineAssetDropOps.test.tsx +3 -4
- package/src/hooks/useTimelineAssetDropOps.ts +1 -6
- package/src/hooks/useTimelineEditing.test.tsx +10 -7
- package/src/hooks/useTimelineEditing.ts +2 -7
- package/src/index.ts +12 -0
- package/src/player/components/Player.test.ts +141 -13
- package/src/player/components/Player.tsx +58 -2
- package/src/player/components/ShortcutsPanel.test.tsx +4 -10
- package/src/player/components/Timeline.tsx +23 -573
- package/src/player/components/TimelineCanvas.tsx +9 -30
- package/src/player/components/TimelineClip.test.tsx +52 -0
- package/src/player/components/TimelineClip.tsx +13 -2
- package/src/player/components/TimelineOverlays.test.ts +67 -32
- package/src/player/components/TimelineOverlays.tsx +46 -107
- package/src/player/components/TimelineProvider.tsx +191 -0
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/testRootHarness.ts +18 -0
- package/src/player/components/timelineMotionStyles.test.ts +3 -3
- package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
- package/src/player/components/timelineProviderStateBuilders.ts +83 -0
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +34 -30
- package/src/player/components/timelineTrackDisplay.ts +1 -4
- package/src/player/components/useTimelineClipContextMenu.ts +1 -1
- package/src/player/components/useTimelineOverlaysState.ts +100 -0
- package/src/player/components/useTimelineProviderState.tsx +583 -0
- package/src/player/components/useTimelineTrackDerivations.ts +2 -2
- package/src/player/hooks/playbackAdapterResolution.ts +98 -0
- package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
- package/src/player/hooks/timelineSyncHydration.ts +0 -23
- package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
- package/src/player/hooks/useShadowPreviewReload.ts +183 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
- package/src/player/hooks/useTimelinePlayer.ts +65 -99
- package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -91
- package/src/player/store/playerStore.ts +0 -4
- package/src/player/store/timelineElement.ts +2 -4
- package/src/player/store/timelineResetState.ts +0 -1
- package/src/styles/hex-baseline.json +3 -4
- package/src/styles/studio.css +7 -7
- package/src/styles/theme.css +29 -0
- package/src/telemetry/events.test.ts +18 -0
- package/src/telemetry/events.ts +10 -0
- package/src/telemetry/system.test.ts +49 -0
- package/src/telemetry/system.ts +6 -0
- package/src/telemetry/tabId.test.ts +43 -0
- package/src/telemetry/tabId.ts +44 -0
- package/src/utils/previewCompositionSize.ts +26 -0
- package/src/utils/reloadDebug.ts +2 -4
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioTelemetry.test.ts +14 -0
- package/src/utils/studioTelemetry.ts +4 -0
- package/dist/assets/hyperframes-player-BjAGc2ID.js +0 -464
- package/src/player/hooks/useTimelineRowElements.test.ts +0 -28
- package/src/player/hooks/useTimelineRowElements.ts +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.57",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"dockview-react": "8.3.1",
|
|
52
52
|
"gsap": "^3.13.0",
|
|
53
53
|
"mediabunny": "^1.45.3",
|
|
54
|
-
"@hyperframes/core": "0.8.
|
|
55
|
-
"@hyperframes/
|
|
56
|
-
"@hyperframes/parsers": "0.8.
|
|
57
|
-
"@hyperframes/
|
|
58
|
-
"@hyperframes/
|
|
54
|
+
"@hyperframes/core": "0.8.57",
|
|
55
|
+
"@hyperframes/player": "0.8.57",
|
|
56
|
+
"@hyperframes/parsers": "0.8.57",
|
|
57
|
+
"@hyperframes/sdk": "0.8.57",
|
|
58
|
+
"@hyperframes/studio-server": "0.8.57"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"vite": "^6.4.2",
|
|
72
72
|
"vitest": "^4.1.11",
|
|
73
73
|
"zustand": "^5.0.0",
|
|
74
|
-
"@hyperframes/producer": "0.8.
|
|
74
|
+
"@hyperframes/producer": "0.8.57"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { usePlayerStore } from "./player";
|
|
|
7
7
|
import { StudioOverlays } from "./components/StudioOverlays";
|
|
8
8
|
import { SaveQueuePausedBanner } from "./components/SaveQueuePausedBanner";
|
|
9
9
|
import { ExternalFileConflictBanner } from "./components/ExternalFileConflictBanner";
|
|
10
|
+
import { ProjectUnreachableBanner } from "./components/ProjectUnreachableBanner";
|
|
10
11
|
import { useCaptionStore } from "./captions/store";
|
|
11
12
|
import { useCaptionSync } from "./captions/hooks/useCaptionSync";
|
|
12
13
|
import { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
|
|
@@ -63,8 +64,16 @@ import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
|
|
|
63
64
|
import { readStudioUrlStateFromWindow, resolveMasterCompositionPath } from "./utils/studioUrlState";
|
|
64
65
|
import { useActiveComposition } from "./hooks/useActiveComposition";
|
|
65
66
|
const getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
|
|
67
|
+
|
|
68
|
+
export interface StudioAppProps {
|
|
69
|
+
/** Clicks still select and report; the preview cannot move, edit or delete anything. */
|
|
70
|
+
readOnlyPreview?: boolean;
|
|
71
|
+
/** Short text shown on disabled hand-edit controls while `readOnlyPreview` is set. */
|
|
72
|
+
readOnlyPreviewReason?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
// fallow-ignore-next-line complexity
|
|
67
|
-
export function StudioApp() {
|
|
76
|
+
export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: StudioAppProps = {}) {
|
|
68
77
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
69
78
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
70
79
|
useStudioSessionStart(projectId, resolving, waitingForServer);
|
|
@@ -252,6 +261,7 @@ export function StudioApp() {
|
|
|
252
261
|
activeCompPath,
|
|
253
262
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
254
263
|
onToggleRecording: () => handleToggleRecordingRef.current(),
|
|
264
|
+
readOnlyPreview,
|
|
255
265
|
});
|
|
256
266
|
const domEditSession = useDomEditSession({
|
|
257
267
|
projectId,
|
|
@@ -291,6 +301,7 @@ export function StudioApp() {
|
|
|
291
301
|
publishSdkSession: sdkHandle.publish,
|
|
292
302
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
293
303
|
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
|
|
304
|
+
readOnlyPreview,
|
|
294
305
|
});
|
|
295
306
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
|
296
307
|
handleDomZIndexReorderCommitRef.current = domEditSession.handleDomZIndexReorderCommit;
|
|
@@ -346,6 +357,7 @@ export function StudioApp() {
|
|
|
346
357
|
previewIframeRef,
|
|
347
358
|
showToast,
|
|
348
359
|
isGestureRecordingRef,
|
|
360
|
+
readOnlyPreview,
|
|
349
361
|
});
|
|
350
362
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
351
363
|
const canvasRectRef = useRef<DOMRect | null>(null);
|
|
@@ -464,7 +476,12 @@ export function StudioApp() {
|
|
|
464
476
|
/>
|
|
465
477
|
)}
|
|
466
478
|
<ExternalFileConflictBanner coordinator={externalFileChanges} />
|
|
479
|
+
{sdkHandle.unreachableProject && (
|
|
480
|
+
<ProjectUnreachableBanner projectId={sdkHandle.unreachableProject} />
|
|
481
|
+
)}
|
|
467
482
|
<EditorShell
|
|
483
|
+
readOnlyPreview={readOnlyPreview}
|
|
484
|
+
readOnlyPreviewReason={readOnlyPreviewReason}
|
|
468
485
|
panels={
|
|
469
486
|
<>
|
|
470
487
|
<StudioLeftPanels
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
// A host that mounts EditorShell directly — no StudioApp, no App.tsx — must
|
|
4
|
+
// still get its descendants gated purely from the readOnlyPreview prop.
|
|
5
|
+
|
|
6
|
+
import React, { act } from "react";
|
|
7
|
+
import { createRoot } from "react-dom/client";
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import "./EditorShellTestMocks";
|
|
10
|
+
import { EditorShell } from "./EditorShell";
|
|
11
|
+
import { usePreviewBlockDrop } from "./nle/usePreviewBlockDrop";
|
|
12
|
+
import { TIMELINE_BLOCK_MIME } from "../utils/timelineAssetDrop";
|
|
13
|
+
|
|
14
|
+
vi.mock("../hooks/useTimelineSelectionPreviewSync", () => ({
|
|
15
|
+
useTimelineSelectionPreviewSync: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("./nle/PreviewOverlays", () => ({ PreviewOverlays: () => null }));
|
|
18
|
+
|
|
19
|
+
// The real hook — this is the actual Tier-1 consumer a host relies on to gate
|
|
20
|
+
// a drop onto the preview. Only PreviewPane's own DOM/player chrome is mocked.
|
|
21
|
+
const stage = document.createElement("div");
|
|
22
|
+
stage.getBoundingClientRect = () => ({ left: 0, top: 0, width: 100, height: 100 }) as DOMRect;
|
|
23
|
+
const dropHandlers: { current: ReturnType<typeof usePreviewBlockDrop> | null } = { current: null };
|
|
24
|
+
vi.mock("./nle/PreviewPane", () => ({
|
|
25
|
+
PreviewPane: (props: { onPreviewBlockDrop?: (name: string, pos: unknown) => void }) => {
|
|
26
|
+
dropHandlers.current = usePreviewBlockDrop({
|
|
27
|
+
stageRef: { current: stage },
|
|
28
|
+
compositionSize: { width: 1000, height: 1000 },
|
|
29
|
+
onBlockDrop: props.onPreviewBlockDrop,
|
|
30
|
+
});
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
36
|
+
|
|
37
|
+
let root: ReturnType<typeof createRoot> | null = null;
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (root) act(() => root!.unmount());
|
|
41
|
+
root = null;
|
|
42
|
+
document.body.innerHTML = "";
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function mountShellAlone(
|
|
46
|
+
readOnlyPreview: boolean,
|
|
47
|
+
onPreviewBlockDrop: (...args: unknown[]) => void,
|
|
48
|
+
) {
|
|
49
|
+
const host = document.createElement("div");
|
|
50
|
+
document.body.append(host);
|
|
51
|
+
root = createRoot(host);
|
|
52
|
+
act(() =>
|
|
53
|
+
root!.render(
|
|
54
|
+
<EditorShell
|
|
55
|
+
panels={null}
|
|
56
|
+
timelineToolbar={null}
|
|
57
|
+
renderClipContent={() => null}
|
|
58
|
+
handleTimelineElementDelete={vi.fn()}
|
|
59
|
+
handleTimelineAssetDrop={vi.fn()}
|
|
60
|
+
handleTimelineFileDrop={vi.fn()}
|
|
61
|
+
handleTimelineElementMove={vi.fn()}
|
|
62
|
+
handleTimelineElementsMove={vi.fn()}
|
|
63
|
+
handleTimelineElementResize={vi.fn()}
|
|
64
|
+
handleTimelineGroupResize={vi.fn()}
|
|
65
|
+
handleToggleTrackHidden={vi.fn()}
|
|
66
|
+
setAudioGroupAttribute={{ setLive: vi.fn(), setQuiet: vi.fn() }}
|
|
67
|
+
handleBlockedTimelineEdit={vi.fn()}
|
|
68
|
+
handleTimelineElementSplit={vi.fn()}
|
|
69
|
+
handleRazorSplit={vi.fn()}
|
|
70
|
+
handleRazorSplitAll={vi.fn()}
|
|
71
|
+
onCopyClip={vi.fn(() => false)}
|
|
72
|
+
onPasteClip={vi.fn(async () => {})}
|
|
73
|
+
onDuplicateClip={vi.fn(async () => false)}
|
|
74
|
+
canPasteClip={vi.fn(() => false)}
|
|
75
|
+
setCompIdToSrc={vi.fn()}
|
|
76
|
+
setCompositionLoading={vi.fn()}
|
|
77
|
+
shouldShowMotionPath={false}
|
|
78
|
+
shouldShowSelectedDomBounds={false}
|
|
79
|
+
handlePreviewBlockDrop={onPreviewBlockDrop}
|
|
80
|
+
readOnlyPreview={readOnlyPreview}
|
|
81
|
+
/>,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
return { host, root };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function dropEvent() {
|
|
88
|
+
return {
|
|
89
|
+
clientX: 50,
|
|
90
|
+
clientY: 50,
|
|
91
|
+
preventDefault: vi.fn(),
|
|
92
|
+
dataTransfer: {
|
|
93
|
+
types: [TIMELINE_BLOCK_MIME],
|
|
94
|
+
getData: () => JSON.stringify({ name: "lower-third" }),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("a host mounting EditorShell alone (no StudioApp, no App.tsx)", () => {
|
|
100
|
+
it("gates a Tier-1 descendant's drop handling purely from the prop", () => {
|
|
101
|
+
const onBlockDrop = vi.fn();
|
|
102
|
+
mountShellAlone(true, onBlockDrop);
|
|
103
|
+
const event = dropEvent();
|
|
104
|
+
act(() => dropHandlers.current!.handleDrop(event as never));
|
|
105
|
+
expect(onBlockDrop).not.toHaveBeenCalled();
|
|
106
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("control: with the flag off, the same descendant still places a drop", () => {
|
|
110
|
+
const onBlockDrop = vi.fn();
|
|
111
|
+
mountShellAlone(false, onBlockDrop);
|
|
112
|
+
const event = dropEvent();
|
|
113
|
+
act(() => dropHandlers.current!.handleDrop(event as never));
|
|
114
|
+
expect(onBlockDrop).toHaveBeenCalledWith("lower-third", { left: 500, top: 500 });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -3,54 +3,27 @@
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import "./EditorShellTestMocks";
|
|
6
7
|
import { EditorShell } from "./EditorShell";
|
|
8
|
+
import { usePreviewReadOnly } from "./editor/previewReadOnlyContext";
|
|
7
9
|
|
|
8
10
|
const hookMocks = vi.hoisted(() => ({
|
|
9
11
|
useTimelineSelectionPreviewSync: vi.fn(),
|
|
10
12
|
}));
|
|
13
|
+
const previewOverlaysProbe = vi.hoisted(() => ({ readOnly: null as boolean | null }));
|
|
11
14
|
|
|
12
15
|
vi.mock("../hooks/useTimelineSelectionPreviewSync", () => hookMocks);
|
|
13
|
-
vi.mock("
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
refreshPreviewDocumentVersion: vi.fn(),
|
|
18
|
-
timelineElements: [],
|
|
19
|
-
}),
|
|
20
|
-
useStudioShellContext: () => ({
|
|
21
|
-
projectId: "project-1",
|
|
22
|
-
activeCompPath: "index.html",
|
|
23
|
-
setActiveCompPath: vi.fn(),
|
|
24
|
-
handlePreviewIframeRef: vi.fn(),
|
|
25
|
-
showToast: vi.fn(),
|
|
26
|
-
}),
|
|
16
|
+
vi.mock("./nle/PreviewPane", () => ({
|
|
17
|
+
// Renders its previewOverlay prop for real, so the mocked PreviewOverlays
|
|
18
|
+
// below (which reads the read-only context) actually mounts.
|
|
19
|
+
PreviewPane: (props: { previewOverlay?: React.ReactNode }) => props.previewOverlay ?? null,
|
|
27
20
|
}));
|
|
28
|
-
vi.mock("
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
applyMarqueeSelection: vi.fn(),
|
|
34
|
-
}),
|
|
35
|
-
useDomEditSelectionContext: () => ({
|
|
36
|
-
domEditSelection: null,
|
|
37
|
-
domEditGroupSelections: [],
|
|
38
|
-
}),
|
|
39
|
-
}));
|
|
40
|
-
vi.mock("./nle/NLEContext", () => ({
|
|
41
|
-
NLEProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
42
|
-
useNLEContext: () => ({
|
|
43
|
-
compositionStack: [],
|
|
44
|
-
updateCompositionStack: vi.fn(),
|
|
45
|
-
}),
|
|
46
|
-
}));
|
|
47
|
-
vi.mock("./nle/useTimelineEditCallbacks", () => ({
|
|
48
|
-
useTimelineEditCallbacks: () => ({}),
|
|
21
|
+
vi.mock("./nle/PreviewOverlays", () => ({
|
|
22
|
+
PreviewOverlays: () => {
|
|
23
|
+
previewOverlaysProbe.readOnly = usePreviewReadOnly();
|
|
24
|
+
return null;
|
|
25
|
+
},
|
|
49
26
|
}));
|
|
50
|
-
vi.mock("./nle/PreviewPane", () => ({ PreviewPane: () => null }));
|
|
51
|
-
vi.mock("./nle/PreviewOverlays", () => ({ PreviewOverlays: () => null }));
|
|
52
|
-
vi.mock("./nle/TimelinePane", () => ({ TimelinePane: () => null }));
|
|
53
|
-
vi.mock("../captions/components/CaptionTimeline", () => ({ CaptionTimeline: () => null }));
|
|
54
27
|
|
|
55
28
|
Object.assign(globalThis, {
|
|
56
29
|
IS_REACT_ACT_ENVIRONMENT: true,
|
|
@@ -66,6 +39,36 @@ afterEach(() => {
|
|
|
66
39
|
hookMocks.useTimelineSelectionPreviewSync.mockClear();
|
|
67
40
|
});
|
|
68
41
|
|
|
42
|
+
const shell = (readOnlyPreview?: boolean) => (
|
|
43
|
+
<EditorShell
|
|
44
|
+
panels={null}
|
|
45
|
+
timelineToolbar={null}
|
|
46
|
+
renderClipContent={() => null}
|
|
47
|
+
handleTimelineElementDelete={vi.fn()}
|
|
48
|
+
handleTimelineAssetDrop={vi.fn()}
|
|
49
|
+
handleTimelineFileDrop={vi.fn()}
|
|
50
|
+
handleTimelineElementMove={vi.fn()}
|
|
51
|
+
handleTimelineElementsMove={vi.fn()}
|
|
52
|
+
handleTimelineElementResize={vi.fn()}
|
|
53
|
+
handleTimelineGroupResize={vi.fn()}
|
|
54
|
+
handleToggleTrackHidden={vi.fn()}
|
|
55
|
+
setAudioGroupAttribute={{ setLive: vi.fn(), setQuiet: vi.fn() }}
|
|
56
|
+
handleBlockedTimelineEdit={vi.fn()}
|
|
57
|
+
handleTimelineElementSplit={vi.fn()}
|
|
58
|
+
handleRazorSplit={vi.fn()}
|
|
59
|
+
handleRazorSplitAll={vi.fn()}
|
|
60
|
+
onCopyClip={vi.fn(() => false)}
|
|
61
|
+
onPasteClip={vi.fn(async () => {})}
|
|
62
|
+
onDuplicateClip={vi.fn(async () => false)}
|
|
63
|
+
canPasteClip={vi.fn(() => false)}
|
|
64
|
+
setCompIdToSrc={vi.fn()}
|
|
65
|
+
setCompositionLoading={vi.fn()}
|
|
66
|
+
shouldShowMotionPath={false}
|
|
67
|
+
shouldShowSelectedDomBounds={false}
|
|
68
|
+
readOnlyPreview={readOnlyPreview}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
|
|
69
72
|
describe("EditorShell timeline selection sync", () => {
|
|
70
73
|
it("keeps the timeline store mirrored into the preview selection", () => {
|
|
71
74
|
const host = document.createElement("div");
|
|
@@ -73,34 +76,7 @@ describe("EditorShell timeline selection sync", () => {
|
|
|
73
76
|
const root = createRoot(host);
|
|
74
77
|
|
|
75
78
|
act(() => {
|
|
76
|
-
root.render(
|
|
77
|
-
<EditorShell
|
|
78
|
-
panels={null}
|
|
79
|
-
timelineToolbar={null}
|
|
80
|
-
renderClipContent={() => null}
|
|
81
|
-
handleTimelineElementDelete={vi.fn()}
|
|
82
|
-
handleTimelineAssetDrop={vi.fn()}
|
|
83
|
-
handleTimelineFileDrop={vi.fn()}
|
|
84
|
-
handleTimelineElementMove={vi.fn()}
|
|
85
|
-
handleTimelineElementsMove={vi.fn()}
|
|
86
|
-
handleTimelineElementResize={vi.fn()}
|
|
87
|
-
handleTimelineGroupResize={vi.fn()}
|
|
88
|
-
handleToggleTrackHidden={vi.fn()}
|
|
89
|
-
setAudioGroupAttribute={{ setLive: vi.fn(), setQuiet: vi.fn() }}
|
|
90
|
-
handleBlockedTimelineEdit={vi.fn()}
|
|
91
|
-
handleTimelineElementSplit={vi.fn()}
|
|
92
|
-
handleRazorSplit={vi.fn()}
|
|
93
|
-
handleRazorSplitAll={vi.fn()}
|
|
94
|
-
onCopyClip={vi.fn(() => false)}
|
|
95
|
-
onPasteClip={vi.fn(async () => {})}
|
|
96
|
-
onDuplicateClip={vi.fn(async () => false)}
|
|
97
|
-
canPasteClip={vi.fn(() => false)}
|
|
98
|
-
setCompIdToSrc={vi.fn()}
|
|
99
|
-
setCompositionLoading={vi.fn()}
|
|
100
|
-
shouldShowMotionPath={false}
|
|
101
|
-
shouldShowSelectedDomBounds={false}
|
|
102
|
-
/>,
|
|
103
|
-
);
|
|
79
|
+
root.render(shell());
|
|
104
80
|
});
|
|
105
81
|
|
|
106
82
|
expect(hookMocks.useTimelineSelectionPreviewSync).toHaveBeenCalledOnce();
|
|
@@ -115,4 +91,18 @@ describe("EditorShell timeline selection sync", () => {
|
|
|
115
91
|
|
|
116
92
|
act(() => root.unmount());
|
|
117
93
|
});
|
|
94
|
+
|
|
95
|
+
it("threads the read-only prop into descendants via context, and follows it across renders", () => {
|
|
96
|
+
const host = document.createElement("div");
|
|
97
|
+
document.body.append(host);
|
|
98
|
+
const root = createRoot(host);
|
|
99
|
+
|
|
100
|
+
act(() => root.render(shell(true)));
|
|
101
|
+
expect(previewOverlaysProbe.readOnly).toBe(true);
|
|
102
|
+
act(() => root.render(shell(false)));
|
|
103
|
+
expect(previewOverlaysProbe.readOnly).toBe(false);
|
|
104
|
+
act(() => root.render(shell(true)));
|
|
105
|
+
expect(previewOverlaysProbe.readOnly).toBe(true);
|
|
106
|
+
act(() => root.unmount());
|
|
107
|
+
});
|
|
118
108
|
});
|
|
@@ -2,6 +2,7 @@ import { useCallback, type ReactNode } from "react";
|
|
|
2
2
|
import { PreviewPane } from "./nle/PreviewPane";
|
|
3
3
|
import { TimelinePane } from "./nle/TimelinePane";
|
|
4
4
|
import { PreviewOverlays } from "./nle/PreviewOverlays";
|
|
5
|
+
import { PreviewReadOnlyProvider } from "./editor/previewReadOnlyContext";
|
|
5
6
|
import {
|
|
6
7
|
useTimelineEditCallbacks,
|
|
7
8
|
type TimelineEditCallbackDeps,
|
|
@@ -69,6 +70,10 @@ export interface EditorShellProps extends TimelineEditCallbackDeps {
|
|
|
69
70
|
* fullscreen and during a block preview; below the selection overlay past z-index 10.
|
|
70
71
|
*/
|
|
71
72
|
gestureOverlay?: ReactNode;
|
|
73
|
+
/** Clicks still select and report; the preview cannot move, edit or delete anything. */
|
|
74
|
+
readOnlyPreview?: boolean;
|
|
75
|
+
/** Short text shown on disabled hand-edit controls while `readOnlyPreview` is set. */
|
|
76
|
+
readOnlyPreviewReason?: string;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
// The dockable shell: every panel lives in one Dock, arranged by the user's
|
|
@@ -109,6 +114,8 @@ export function EditorShell({
|
|
|
109
114
|
onToggleRecording,
|
|
110
115
|
blockPreview,
|
|
111
116
|
gestureOverlay,
|
|
117
|
+
readOnlyPreview = false,
|
|
118
|
+
readOnlyPreviewReason,
|
|
112
119
|
}: EditorShellProps) {
|
|
113
120
|
const { projectId, activeCompPath, setActiveCompPath, handlePreviewIframeRef, showToast } =
|
|
114
121
|
useStudioShellContext();
|
|
@@ -156,57 +163,60 @@ export function EditorShell({
|
|
|
156
163
|
});
|
|
157
164
|
|
|
158
165
|
return (
|
|
159
|
-
<
|
|
160
|
-
<
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
refreshKey={refreshKey}
|
|
164
|
-
activeCompositionPath={activeCompPath}
|
|
165
|
-
onIframeRef={handlePreviewIframeRef}
|
|
166
|
-
onCompIdToSrcChange={setCompIdToSrc}
|
|
167
|
-
onCompositionLoadingChange={setCompositionLoading}
|
|
168
|
-
onCompositionChange={(compPath) => {
|
|
169
|
-
// Sync activeCompPath when the user drills down via the timeline or
|
|
170
|
-
// navigates back — keeps sidebar + thumbnails in sync. Guard no-ops to
|
|
171
|
-
// avoid circular refresh cascades (activeCompPath → stack → onChange).
|
|
172
|
-
if (compPath !== activeCompPath) {
|
|
173
|
-
setActiveCompPath(compPath);
|
|
174
|
-
refreshPreviewDocumentVersion();
|
|
175
|
-
}
|
|
176
|
-
}}
|
|
177
|
-
>
|
|
178
|
-
<EditorShellBody
|
|
166
|
+
<PreviewReadOnlyProvider readOnly={readOnlyPreview} reason={readOnlyPreviewReason}>
|
|
167
|
+
<div className="flex flex-col flex-1 min-h-0">
|
|
168
|
+
<TimelineEditProvider value={timelineEditCallbacks}>
|
|
169
|
+
<NLEProvider
|
|
179
170
|
projectId={projectId}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
171
|
+
refreshKey={refreshKey}
|
|
172
|
+
activeCompositionPath={activeCompPath}
|
|
173
|
+
onIframeRef={handlePreviewIframeRef}
|
|
174
|
+
onCompIdToSrcChange={setCompIdToSrc}
|
|
175
|
+
onPreviewReloadFailed={(message) => showToast(message, "error")}
|
|
176
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
177
|
+
onCompositionChange={(compPath) => {
|
|
178
|
+
// Sync activeCompPath when the user drills down via the timeline or
|
|
179
|
+
// navigates back — keeps sidebar + thumbnails in sync. Guard no-ops to
|
|
180
|
+
// avoid circular refresh cascades (activeCompPath → stack → onChange).
|
|
181
|
+
if (compPath !== activeCompPath) {
|
|
182
|
+
setActiveCompPath(compPath);
|
|
183
|
+
refreshPreviewDocumentVersion();
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<EditorShellBody
|
|
188
|
+
projectId={projectId}
|
|
189
|
+
panels={panels}
|
|
190
|
+
captionEditMode={captionEditMode}
|
|
191
|
+
onSelectTimelineElement={handleTimelineElementSelect}
|
|
192
|
+
onPreviewBlockDrop={handlePreviewBlockDrop}
|
|
193
|
+
timelineToolbar={timelineToolbar}
|
|
194
|
+
renderClipContent={renderClipContent}
|
|
195
|
+
onFileDrop={handleTimelineFileDrop}
|
|
196
|
+
onAssetDrop={handleTimelineAssetDrop}
|
|
197
|
+
onBlockDrop={handleTimelineBlockDrop}
|
|
198
|
+
onCompositionDrop={handleTimelineCompositionDrop}
|
|
199
|
+
onDeleteElement={handleTimelineElementDelete}
|
|
200
|
+
onCopyClip={onCopyClip}
|
|
201
|
+
onPasteClip={onPasteClip}
|
|
202
|
+
onDuplicateClip={onDuplicateClip}
|
|
203
|
+
canPasteClip={canPasteClip}
|
|
204
|
+
previewOverlay={
|
|
205
|
+
<PreviewOverlays
|
|
206
|
+
shouldShowMotionPath={shouldShowMotionPath}
|
|
207
|
+
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
|
|
208
|
+
blockPreview={blockPreview}
|
|
209
|
+
isGestureRecording={isGestureRecording}
|
|
210
|
+
recordingState={recordingState}
|
|
211
|
+
onToggleRecording={onToggleRecording}
|
|
212
|
+
gestureOverlay={gestureOverlay}
|
|
213
|
+
/>
|
|
214
|
+
}
|
|
215
|
+
/>
|
|
216
|
+
</NLEProvider>
|
|
217
|
+
</TimelineEditProvider>
|
|
218
|
+
</div>
|
|
219
|
+
</PreviewReadOnlyProvider>
|
|
210
220
|
);
|
|
211
221
|
}
|
|
212
222
|
|
|
@@ -255,7 +265,7 @@ function EditorShellBody({
|
|
|
255
265
|
onDuplicateClip,
|
|
256
266
|
canPasteClip,
|
|
257
267
|
}: EditorShellBodyProps) {
|
|
258
|
-
const { compositionStack, updateCompositionStack } = useNLEContext();
|
|
268
|
+
const { compositionStack, updateCompositionStack, containerRef } = useNLEContext();
|
|
259
269
|
|
|
260
270
|
// The caption track's blocks are seek targets; CaptionTimeline took an onSeek
|
|
261
271
|
// prop that nothing ever passed, so clicking a block did nothing.
|
|
@@ -275,6 +285,7 @@ function EditorShellBody({
|
|
|
275
285
|
|
|
276
286
|
return (
|
|
277
287
|
<div
|
|
288
|
+
ref={containerRef}
|
|
278
289
|
// Shell canvas is a step LIGHTER than the near-black panel cards so the
|
|
279
290
|
// gaps between panels read as visible seams (CapCut-style).
|
|
280
291
|
className="flex flex-col flex-1 min-h-0 bg-panel-surface"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
5
|
+
useStudioPlaybackContext: () => ({
|
|
6
|
+
captionEditMode: false,
|
|
7
|
+
refreshKey: 0,
|
|
8
|
+
refreshPreviewDocumentVersion: vi.fn(),
|
|
9
|
+
timelineElements: [],
|
|
10
|
+
}),
|
|
11
|
+
useStudioShellContext: () => ({
|
|
12
|
+
projectId: "project-1",
|
|
13
|
+
activeCompPath: "index.html",
|
|
14
|
+
setActiveCompPath: vi.fn(),
|
|
15
|
+
handlePreviewIframeRef: vi.fn(),
|
|
16
|
+
showToast: vi.fn(),
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("../contexts/DomEditContext", () => ({
|
|
21
|
+
useDomEditActionsContext: () => ({
|
|
22
|
+
handleTimelineElementSelect: vi.fn(),
|
|
23
|
+
buildDomSelectionForTimelineElement: vi.fn(),
|
|
24
|
+
applyDomSelection: vi.fn(),
|
|
25
|
+
applyMarqueeSelection: vi.fn(),
|
|
26
|
+
}),
|
|
27
|
+
useDomEditSelectionContext: () => ({
|
|
28
|
+
domEditSelection: null,
|
|
29
|
+
domEditGroupSelections: [],
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("./nle/NLEContext", () => ({
|
|
34
|
+
NLEProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
35
|
+
useNLEContext: () => ({
|
|
36
|
+
compositionStack: [],
|
|
37
|
+
updateCompositionStack: vi.fn(),
|
|
38
|
+
containerRef: { current: null },
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock("./nle/useTimelineEditCallbacks", () => ({
|
|
43
|
+
useTimelineEditCallbacks: () => ({}),
|
|
44
|
+
}));
|
|
45
|
+
vi.mock("./nle/TimelinePane", () => ({ TimelinePane: () => null }));
|
|
46
|
+
vi.mock("../captions/components/CaptionTimeline", () => ({ CaptionTimeline: () => null }));
|