@hyperframes/studio 0.8.54 → 0.8.56
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-Cy_AwUa8.js → hyperframes-player-k8LzMtXX.js} +1 -1
- package/dist/assets/{index-DPUJoO8n.js → index-BlkJ_Axe.js} +192 -192
- package/dist/assets/{index-CTa-6Kso.js → index-CxR7F2YQ.js} +1 -1
- package/dist/assets/{index-DkAzKuPZ.js → index-DFlILHJe.js} +1 -1
- package/dist/assets/{index-DrHQAuGO.css → index-ZmBtVrEm.css} +1 -1
- package/dist/index.d.ts +16 -8
- package/dist/index.html +2 -2
- package/dist/index.js +3152 -3010
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +14 -1
- package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
- package/src/components/EditorShell.selectionSync.test.tsx +57 -67
- package/src/components/EditorShell.tsx +59 -50
- package/src/components/EditorShellTestMocks.ts +46 -0
- package/src/components/editor/DomEditOverlay.tsx +19 -40
- package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
- package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
- 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 +1 -0
- 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/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/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
- package/src/components/nle/PreviewOverlays.tsx +15 -8
- package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
- package/src/components/nle/TimelinePane.tsx +5 -1
- package/src/components/nle/previewReadOnly.test.tsx +128 -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/gsapDragCommit.ts +1 -1
- package/src/hooks/gsapEditOutcome.test.ts +115 -0
- package/src/hooks/gsapEditOutcome.ts +39 -10
- package/src/hooks/gsapResizeIntercept.test.ts +42 -3
- package/src/hooks/gsapResizeIntercept.ts +12 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +10 -2
- package/src/hooks/gsapRuntimeBridge.ts +14 -3
- 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/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/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/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/player/components/Timeline.tsx +22 -23
- package/src/player/components/TimelineClip.test.tsx +52 -0
- package/src/player/components/TimelineClip.tsx +13 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +3 -3
- 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/useTimelineTrackDerivations.ts +2 -2
- package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
- package/src/player/hooks/timelineSyncHydration.ts +0 -23
- package/src/player/hooks/useTimelineSyncCallbacks.ts +0 -3
- 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/utils/studioSaveDiagnostics.ts +12 -0
- 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.56",
|
|
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/
|
|
55
|
-
"@hyperframes/
|
|
56
|
-
"@hyperframes/
|
|
57
|
-
"@hyperframes/studio-server": "0.8.
|
|
58
|
-
"@hyperframes/
|
|
54
|
+
"@hyperframes/player": "0.8.56",
|
|
55
|
+
"@hyperframes/sdk": "0.8.56",
|
|
56
|
+
"@hyperframes/core": "0.8.56",
|
|
57
|
+
"@hyperframes/studio-server": "0.8.56",
|
|
58
|
+
"@hyperframes/parsers": "0.8.56"
|
|
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.56"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -63,8 +63,16 @@ import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
|
|
|
63
63
|
import { readStudioUrlStateFromWindow, resolveMasterCompositionPath } from "./utils/studioUrlState";
|
|
64
64
|
import { useActiveComposition } from "./hooks/useActiveComposition";
|
|
65
65
|
const getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
|
|
66
|
+
|
|
67
|
+
export interface StudioAppProps {
|
|
68
|
+
/** Clicks still select and report; the preview cannot move, edit or delete anything. */
|
|
69
|
+
readOnlyPreview?: boolean;
|
|
70
|
+
/** Short text shown on disabled hand-edit controls while `readOnlyPreview` is set. */
|
|
71
|
+
readOnlyPreviewReason?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
// fallow-ignore-next-line complexity
|
|
67
|
-
export function StudioApp() {
|
|
75
|
+
export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: StudioAppProps = {}) {
|
|
68
76
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
69
77
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
70
78
|
useStudioSessionStart(projectId, resolving, waitingForServer);
|
|
@@ -252,6 +260,7 @@ export function StudioApp() {
|
|
|
252
260
|
activeCompPath,
|
|
253
261
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
254
262
|
onToggleRecording: () => handleToggleRecordingRef.current(),
|
|
263
|
+
readOnlyPreview,
|
|
255
264
|
});
|
|
256
265
|
const domEditSession = useDomEditSession({
|
|
257
266
|
projectId,
|
|
@@ -291,6 +300,7 @@ export function StudioApp() {
|
|
|
291
300
|
publishSdkSession: sdkHandle.publish,
|
|
292
301
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
293
302
|
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
|
|
303
|
+
readOnlyPreview,
|
|
294
304
|
});
|
|
295
305
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
|
296
306
|
handleDomZIndexReorderCommitRef.current = domEditSession.handleDomZIndexReorderCommit;
|
|
@@ -346,6 +356,7 @@ export function StudioApp() {
|
|
|
346
356
|
previewIframeRef,
|
|
347
357
|
showToast,
|
|
348
358
|
isGestureRecordingRef,
|
|
359
|
+
readOnlyPreview,
|
|
349
360
|
});
|
|
350
361
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
351
362
|
const canvasRectRef = useRef<DOMRect | null>(null);
|
|
@@ -465,6 +476,8 @@ export function StudioApp() {
|
|
|
465
476
|
)}
|
|
466
477
|
<ExternalFileConflictBanner coordinator={externalFileChanges} />
|
|
467
478
|
<EditorShell
|
|
479
|
+
readOnlyPreview={readOnlyPreview}
|
|
480
|
+
readOnlyPreviewReason={readOnlyPreviewReason}
|
|
468
481
|
panels={
|
|
469
482
|
<>
|
|
470
483
|
<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,59 @@ 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
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
176
|
+
onCompositionChange={(compPath) => {
|
|
177
|
+
// Sync activeCompPath when the user drills down via the timeline or
|
|
178
|
+
// navigates back — keeps sidebar + thumbnails in sync. Guard no-ops to
|
|
179
|
+
// avoid circular refresh cascades (activeCompPath → stack → onChange).
|
|
180
|
+
if (compPath !== activeCompPath) {
|
|
181
|
+
setActiveCompPath(compPath);
|
|
182
|
+
refreshPreviewDocumentVersion();
|
|
183
|
+
}
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<EditorShellBody
|
|
187
|
+
projectId={projectId}
|
|
188
|
+
panels={panels}
|
|
189
|
+
captionEditMode={captionEditMode}
|
|
190
|
+
onSelectTimelineElement={handleTimelineElementSelect}
|
|
191
|
+
onPreviewBlockDrop={handlePreviewBlockDrop}
|
|
192
|
+
timelineToolbar={timelineToolbar}
|
|
193
|
+
renderClipContent={renderClipContent}
|
|
194
|
+
onFileDrop={handleTimelineFileDrop}
|
|
195
|
+
onAssetDrop={handleTimelineAssetDrop}
|
|
196
|
+
onBlockDrop={handleTimelineBlockDrop}
|
|
197
|
+
onCompositionDrop={handleTimelineCompositionDrop}
|
|
198
|
+
onDeleteElement={handleTimelineElementDelete}
|
|
199
|
+
onCopyClip={onCopyClip}
|
|
200
|
+
onPasteClip={onPasteClip}
|
|
201
|
+
onDuplicateClip={onDuplicateClip}
|
|
202
|
+
canPasteClip={canPasteClip}
|
|
203
|
+
previewOverlay={
|
|
204
|
+
<PreviewOverlays
|
|
205
|
+
shouldShowMotionPath={shouldShowMotionPath}
|
|
206
|
+
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
|
|
207
|
+
blockPreview={blockPreview}
|
|
208
|
+
isGestureRecording={isGestureRecording}
|
|
209
|
+
recordingState={recordingState}
|
|
210
|
+
onToggleRecording={onToggleRecording}
|
|
211
|
+
gestureOverlay={gestureOverlay}
|
|
212
|
+
/>
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
</NLEProvider>
|
|
216
|
+
</TimelineEditProvider>
|
|
217
|
+
</div>
|
|
218
|
+
</PreviewReadOnlyProvider>
|
|
210
219
|
);
|
|
211
220
|
}
|
|
212
221
|
|
|
@@ -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 }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useEffect, useMemo, useRef,
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, type RefObject } from "react";
|
|
2
2
|
import { type DomEditSelection } from "./domEditing";
|
|
3
3
|
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
|
4
4
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
resolveShiftClickCandidate,
|
|
17
17
|
} from "./domEditOverlayGestures";
|
|
18
18
|
import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
|
|
19
|
-
import { ChildRectOutlines, OffCanvasIndicators
|
|
19
|
+
import { ChildRectOutlines, OffCanvasIndicators } from "./OffCanvasIndicators";
|
|
20
20
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
21
21
|
import { useDomEditNudge } from "./useDomEditNudge";
|
|
22
22
|
import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
|
|
@@ -27,13 +27,13 @@ import { hugRectForElement } from "./domEditOverlayCrop";
|
|
|
27
27
|
import { useCropOverlay } from "../../hooks/useCropOverlay";
|
|
28
28
|
import { readDomEditSelectionShapeStyles, resolveBoxChromeClass } from "./domEditOverlayShape";
|
|
29
29
|
import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
30
|
-
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
31
|
-
import { startOffCanvasIndicatorRefresh } from "./offCanvasIndicatorRefresh";
|
|
32
30
|
import { CanvasContextMenu } from "./CanvasContextMenu";
|
|
33
31
|
import { useInlineTextEditing } from "./useInlineTextEditing";
|
|
32
|
+
import { usePreviewReadOnly } from "./previewReadOnlyContext";
|
|
34
33
|
import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
35
34
|
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
|
|
36
35
|
import { logSelect } from "../../utils/selectDebug";
|
|
36
|
+
import { useOffCanvasIndicators } from "./useOffCanvasIndicators";
|
|
37
37
|
|
|
38
38
|
// Re-exports for external consumers — preserving existing import paths.
|
|
39
39
|
export {
|
|
@@ -137,6 +137,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
137
137
|
onDeleteSelection,
|
|
138
138
|
onApplyZIndex,
|
|
139
139
|
}: DomEditOverlayProps) {
|
|
140
|
+
const readOnly = usePreviewReadOnly();
|
|
140
141
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
141
142
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
142
143
|
const onMarqueeSelectRef = useRef(onMarqueeSelect);
|
|
@@ -220,39 +221,13 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
220
221
|
const boxClipPath = hasCropInsets ? undefined : selectionShapeStyles.clipPath;
|
|
221
222
|
const boxChromeClass = resolveBoxChromeClass(Boolean(cropOutlineInsetPx), boxClipPath);
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const offCanvasObservedDocRef = useRef<Document | null>(null);
|
|
231
|
-
|
|
232
|
-
// Positions depend on live iframe layout, not selection — the selected-element
|
|
233
|
-
// suppression is a render-time filter, so selection/groupSelections stay out
|
|
234
|
-
// of the geometry walk.
|
|
235
|
-
useMountEffect(() =>
|
|
236
|
-
startOffCanvasIndicatorRefresh({
|
|
237
|
-
iframeRef,
|
|
238
|
-
overlayRef,
|
|
239
|
-
compRectRef,
|
|
240
|
-
activeCompositionPathRef,
|
|
241
|
-
dirtyRef: offCanvasDirtyRef,
|
|
242
|
-
sigRef: offCanvasSigRef,
|
|
243
|
-
observerRef: offCanvasObserverRef,
|
|
244
|
-
observedDocRef: offCanvasObservedDocRef,
|
|
245
|
-
elementsRef: offCanvasElementsRef,
|
|
246
|
-
setRects: setOffCanvasRects,
|
|
247
|
-
}),
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// Switching compositions may not swap the iframe document (so the observer's
|
|
251
|
-
// doc-swap detection wouldn't fire) yet changes which elements are off-canvas.
|
|
252
|
-
// Force a recompute explicitly on comp change.
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
offCanvasDirtyRef.current = true;
|
|
255
|
-
}, [activeCompositionPath]);
|
|
224
|
+
const { offCanvasRects, offCanvasElementsRef } = useOffCanvasIndicators({
|
|
225
|
+
iframeRef,
|
|
226
|
+
overlayRef,
|
|
227
|
+
compRectRef,
|
|
228
|
+
activeCompositionPathRef,
|
|
229
|
+
activeCompositionPath,
|
|
230
|
+
});
|
|
256
231
|
|
|
257
232
|
const gestures = createDomEditOverlayGestureHandlers({
|
|
258
233
|
overlayRef,
|
|
@@ -280,12 +255,16 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
280
255
|
snapGuidesRef,
|
|
281
256
|
});
|
|
282
257
|
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (readOnly) gestures.clearPointerState(selectionRef);
|
|
260
|
+
}, [gestures, readOnly, selectionRef]);
|
|
261
|
+
|
|
283
262
|
// Arrow-key nudge (1px, Shift = 10px) — commits through the same
|
|
284
263
|
// path-offset callbacks as a drag, one undo entry per key burst.
|
|
285
264
|
const { flushNudge } = useDomEditNudge({
|
|
286
265
|
selection,
|
|
287
266
|
groupSelections,
|
|
288
|
-
allowCanvasMovement,
|
|
267
|
+
allowCanvasMovement: allowCanvasMovement && !readOnly,
|
|
289
268
|
selectionRef,
|
|
290
269
|
overlayRectRef,
|
|
291
270
|
groupOverlayItemsRef,
|
|
@@ -558,7 +537,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
558
537
|
selection={contextMenu.sel}
|
|
559
538
|
onClose={closeContextMenu}
|
|
560
539
|
onDelete={
|
|
561
|
-
onDeleteSelection
|
|
540
|
+
onDeleteSelection && !readOnly
|
|
562
541
|
? (sel) => {
|
|
563
542
|
closeContextMenu();
|
|
564
543
|
onDeleteSelection(sel);
|
|
@@ -566,7 +545,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
566
545
|
: undefined
|
|
567
546
|
}
|
|
568
547
|
onApplyZIndex={
|
|
569
|
-
onApplyZIndex
|
|
548
|
+
onApplyZIndex && !readOnly
|
|
570
549
|
? (patches, action, crossed) => {
|
|
571
550
|
onApplyZIndex(contextMenu.sel, patches, action, crossed);
|
|
572
551
|
}
|