@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
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readPreviewCompositionSize,
|
|
3
|
+
type PreviewCompositionSize,
|
|
4
|
+
} from "../../utils/previewCompositionSize";
|
|
1
5
|
import { memo, useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
|
2
6
|
import { Player } from "../../player";
|
|
7
|
+
import type { PreviewIframeSlot } from "../../player/hooks/useTimelineSyncCallbacks";
|
|
3
8
|
import {
|
|
4
9
|
DEFAULT_PREVIEW_ZOOM,
|
|
5
10
|
canStartPreviewPan,
|
|
@@ -13,10 +18,17 @@ import {
|
|
|
13
18
|
} from "./previewZoom";
|
|
14
19
|
import { RULER_GUTTER_PX, usePreviewGuidesStore } from "../editor/previewGuidesStore";
|
|
15
20
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
21
|
+
import { usePreviewFirstFrameTelemetry } from "../../player/hooks/usePreviewFirstFrameTelemetry";
|
|
16
22
|
interface NLEPreviewProps {
|
|
17
23
|
projectId: string;
|
|
18
24
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
19
25
|
onIframeLoad: () => void;
|
|
26
|
+
previewSlots: PreviewIframeSlot[];
|
|
27
|
+
onShadowIframeLoad: (gen: number) => void;
|
|
28
|
+
onShadowReadyChange: (gen: number, ready: boolean) => void;
|
|
29
|
+
onShadowError: (gen: number, message: string) => void;
|
|
30
|
+
setShadowIframeNode: (node: HTMLIFrameElement | null) => void;
|
|
31
|
+
resetPreviewSlots: () => void;
|
|
20
32
|
onCompositionLoadingChange?: (loading: boolean) => void;
|
|
21
33
|
portrait?: boolean;
|
|
22
34
|
directUrl?: string;
|
|
@@ -40,10 +52,15 @@ const ZOOM_HUD_TIMEOUT_MS = 1200;
|
|
|
40
52
|
const ZOOM_SETTLE_MS = 200;
|
|
41
53
|
const PREVIEW_STAGE_INSET_PX = 16;
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
// clip-path as well as visibility: the player's loading overlay sets its own
|
|
56
|
+
// visibility:visible and would otherwise paint over the live frame.
|
|
57
|
+
const SHADOW_IFRAME_STYLE: React.CSSProperties = {
|
|
58
|
+
position: "absolute",
|
|
59
|
+
inset: 0,
|
|
60
|
+
visibility: "hidden",
|
|
61
|
+
clipPath: "inset(100%)",
|
|
62
|
+
pointerEvents: "none",
|
|
63
|
+
};
|
|
47
64
|
|
|
48
65
|
function isPreviewAtFit(state: PreviewZoomState): boolean {
|
|
49
66
|
return (
|
|
@@ -64,27 +81,6 @@ function loadInitialZoom(): PreviewZoomState {
|
|
|
64
81
|
: DEFAULT_PREVIEW_ZOOM;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
// fallow-ignore-next-line complexity
|
|
68
|
-
function readPreviewCompositionSize(
|
|
69
|
-
iframe: HTMLIFrameElement | null,
|
|
70
|
-
): PreviewCompositionSize | null {
|
|
71
|
-
try {
|
|
72
|
-
const doc = iframe?.contentDocument;
|
|
73
|
-
const root =
|
|
74
|
-
doc?.querySelector("[data-composition-id][data-width][data-height]") ??
|
|
75
|
-
doc?.querySelector("[data-width][data-height]");
|
|
76
|
-
if (!root) return null;
|
|
77
|
-
const width = Number.parseInt(root.getAttribute("data-width") ?? "", 10);
|
|
78
|
-
const height = Number.parseInt(root.getAttribute("data-height") ?? "", 10);
|
|
79
|
-
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
return { width, height };
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
export function resolvePreviewStageSize(
|
|
89
85
|
viewportWidth: number,
|
|
90
86
|
viewportHeight: number,
|
|
@@ -122,6 +118,12 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
122
118
|
projectId,
|
|
123
119
|
iframeRef,
|
|
124
120
|
onIframeLoad,
|
|
121
|
+
previewSlots,
|
|
122
|
+
onShadowIframeLoad,
|
|
123
|
+
onShadowReadyChange,
|
|
124
|
+
onShadowError,
|
|
125
|
+
setShadowIframeNode,
|
|
126
|
+
resetPreviewSlots,
|
|
125
127
|
onCompositionLoadingChange,
|
|
126
128
|
portrait,
|
|
127
129
|
directUrl,
|
|
@@ -136,6 +138,17 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
136
138
|
useEffect(() => {
|
|
137
139
|
onStageRef?.(stageRef);
|
|
138
140
|
}, [onStageRef]);
|
|
141
|
+
|
|
142
|
+
// Composition switch: drop any in-flight shadow reload (skipped on first mount).
|
|
143
|
+
const previousActiveKeyRef = useRef(activeKey);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (previousActiveKeyRef.current === activeKey) return;
|
|
146
|
+
previousActiveKeyRef.current = activeKey;
|
|
147
|
+
resetPreviewSlots();
|
|
148
|
+
}, [activeKey, resetPreviewSlots]);
|
|
149
|
+
|
|
150
|
+
const liveGenRef = useRef<number | null>(null);
|
|
151
|
+
const reportPreviewFirstFrame = usePreviewFirstFrameTelemetry(previewSlots);
|
|
139
152
|
const [compositionSize, setCompositionSize] = useState<PreviewCompositionSize | null>(null);
|
|
140
153
|
const gutterPx = usePreviewGuidesStore((s) => (s.rulerVisible ? RULER_GUTTER_PX : 0));
|
|
141
154
|
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, null, portrait));
|
|
@@ -299,6 +312,18 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
299
312
|
}
|
|
300
313
|
}, [writeTransform]);
|
|
301
314
|
|
|
315
|
+
// A promotion does not re-fire Player.onLoad: re-sync the local iframe ref, size and zoom.
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
const live = previewSlots.find((slot) => slot.role === "live");
|
|
318
|
+
if (!live || live.gen === liveGenRef.current) return;
|
|
319
|
+
const isPromotion = liveGenRef.current !== null;
|
|
320
|
+
liveGenRef.current = live.gen;
|
|
321
|
+
if (!isPromotion) return;
|
|
322
|
+
previewIframeRef.current = iframeRef.current;
|
|
323
|
+
updateCompositionSizeFromPreview();
|
|
324
|
+
applyInitialZoom();
|
|
325
|
+
}, [previewSlots, iframeRef, updateCompositionSizeFromPreview, applyInitialZoom]);
|
|
326
|
+
|
|
302
327
|
useEffect(() => {
|
|
303
328
|
const viewport = viewportRef.current;
|
|
304
329
|
if (!viewport) return;
|
|
@@ -469,25 +494,44 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
469
494
|
style={{ position: "absolute", inset: 0, zIndex: 0 }}
|
|
470
495
|
/>
|
|
471
496
|
)}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
497
|
+
{previewSlots.map((slot) =>
|
|
498
|
+
slot.role === "live" ? (
|
|
499
|
+
<Player
|
|
500
|
+
key={`${activeKey}-${slot.gen}`}
|
|
501
|
+
ref={setPreviewIframeRef}
|
|
502
|
+
projectId={directUrl ? undefined : projectId}
|
|
503
|
+
directUrl={directUrl}
|
|
504
|
+
onLoad={() => {
|
|
505
|
+
updateCompositionSizeFromPreview();
|
|
506
|
+
onIframeLoad();
|
|
507
|
+
applyInitialZoom();
|
|
508
|
+
}}
|
|
509
|
+
onCompositionLoadingChange={onCompositionLoadingChange}
|
|
510
|
+
onPainted={(details) => reportPreviewFirstFrame(slot, details)}
|
|
511
|
+
portrait={portrait}
|
|
512
|
+
suppressLoadingOverlay={suppressLoadingOverlay}
|
|
513
|
+
style={
|
|
514
|
+
directUrl?.includes("/components/")
|
|
515
|
+
? { position: "absolute", inset: 0, zIndex: 1 }
|
|
516
|
+
: undefined
|
|
517
|
+
}
|
|
518
|
+
/>
|
|
519
|
+
) : (
|
|
520
|
+
// Loads hidden behind the live slot until promoted.
|
|
521
|
+
<Player
|
|
522
|
+
key={`${activeKey}-${slot.gen}`}
|
|
523
|
+
ref={setShadowIframeNode}
|
|
524
|
+
directUrl={slot.url}
|
|
525
|
+
onLoad={() => onShadowIframeLoad(slot.gen)}
|
|
526
|
+
onReadyToShowChange={(ready) => onShadowReadyChange(slot.gen, ready)}
|
|
527
|
+
onPainted={(details) => reportPreviewFirstFrame(slot, details)}
|
|
528
|
+
onPreviewError={(message) => onShadowError(slot.gen, message)}
|
|
529
|
+
portrait={portrait}
|
|
530
|
+
suppressLoadingOverlay
|
|
531
|
+
style={SHADOW_IFRAME_STYLE}
|
|
532
|
+
/>
|
|
533
|
+
),
|
|
534
|
+
)}
|
|
491
535
|
</div>
|
|
492
536
|
</div>
|
|
493
537
|
<div
|
|
@@ -3,21 +3,13 @@
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { getPreviewState, iframeRef } from "./PreviewOverlaysTestMocks";
|
|
6
7
|
import { PreviewOverlays } from "./PreviewOverlays";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const previewState = getPreviewState();
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
const iframeRef = { current: null as HTMLIFrameElement | null };
|
|
11
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
12
|
|
|
13
|
-
vi.mock("../../contexts/StudioContext", () => ({
|
|
14
|
-
useStudioShellContext: () => ({ activeCompPath: "index.html", previewIframeRef: iframeRef }),
|
|
15
|
-
useStudioPlaybackContext: () => ({
|
|
16
|
-
captionEditMode: previewState.captionEditMode,
|
|
17
|
-
compositionLoading: false,
|
|
18
|
-
isPlaying: false,
|
|
19
|
-
}),
|
|
20
|
-
}));
|
|
21
13
|
vi.mock("../../contexts/DomEditContext", () => ({
|
|
22
14
|
useDomEditSelectionContext: () => ({
|
|
23
15
|
domEditHoverSelection: null,
|
|
@@ -41,30 +33,7 @@ vi.mock("../../contexts/DomEditContext", () => ({
|
|
|
41
33
|
handleDomZIndexReorderCommit: vi.fn(),
|
|
42
34
|
}),
|
|
43
35
|
}));
|
|
44
|
-
|
|
45
|
-
const state = {
|
|
46
|
-
model: null,
|
|
47
|
-
dismissed: false,
|
|
48
|
-
syncError: null,
|
|
49
|
-
clearSelection: vi.fn(),
|
|
50
|
-
setDismissed: vi.fn(),
|
|
51
|
-
setEditMode: vi.fn(),
|
|
52
|
-
setSyncError: vi.fn(),
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
useCaptionStore: Object.assign(
|
|
56
|
-
(selector: (value: typeof state) => unknown) => selector(state),
|
|
57
|
-
{ getState: () => state },
|
|
58
|
-
),
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
vi.mock("../../hooks/useCompositionDimensions", () => ({
|
|
62
|
-
useCompositionDimensions: () => null,
|
|
63
|
-
}));
|
|
64
|
-
vi.mock("../../utils/studioUiPreferences", () => ({ readStudioUiPreferences: () => ({}) }));
|
|
65
|
-
vi.mock("./useCanvasZOrderTimelineMirror", () => ({
|
|
66
|
-
useCanvasZOrderTimelineMirror: () => vi.fn(),
|
|
67
|
-
}));
|
|
36
|
+
|
|
68
37
|
vi.mock("../editor/TopologyLens", async () => {
|
|
69
38
|
const { createElement } = await import("react");
|
|
70
39
|
return {
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { useCallback
|
|
1
|
+
import { useCallback } from "react";
|
|
2
2
|
import { CaptionOverlay } from "../../captions/components/CaptionOverlay";
|
|
3
3
|
import { useCaptionStore } from "../../captions/store";
|
|
4
4
|
import { DomEditOverlay } from "../editor/DomEditOverlay";
|
|
5
5
|
import { TopologyLens } from "../editor/TopologyLens";
|
|
6
6
|
import { MotionPathOverlay } from "../editor/MotionPathOverlay";
|
|
7
7
|
import { SnapToolbar } from "../editor/SnapToolbar";
|
|
8
|
+
import { GridOverlay } from "../editor/GridOverlay";
|
|
9
|
+
import { usePreviewReadOnly } from "../editor/previewReadOnlyContext";
|
|
8
10
|
import { useCompositionDimensions } from "../../hooks/useCompositionDimensions";
|
|
9
11
|
import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
|
|
10
12
|
import {
|
|
11
13
|
useDomEditActionsContext,
|
|
12
14
|
useDomEditSelectionContext,
|
|
13
15
|
} from "../../contexts/DomEditContext";
|
|
14
|
-
import { readStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
15
16
|
import { readHfId, type DomEditSelection } from "../editor/domEditing";
|
|
16
17
|
import { buildStableSelector } from "../editor/domEditingDom";
|
|
17
18
|
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
|
|
@@ -140,6 +141,8 @@ export function PreviewOverlays({
|
|
|
140
141
|
const { activeCompPath, previewIframeRef } = useStudioShellContext();
|
|
141
142
|
const { captionEditMode, compositionLoading, isPlaying } = useStudioPlaybackContext();
|
|
142
143
|
const compositionDimensions = useCompositionDimensions(previewIframeRef);
|
|
144
|
+
const readOnly = usePreviewReadOnly();
|
|
145
|
+
const previewCaptionEditMode = captionEditMode && !readOnly;
|
|
143
146
|
|
|
144
147
|
// Caption edit mode is entered automatically when captions are detected;
|
|
145
148
|
// these give the author an explicit way OUT (and back in). Without them the
|
|
@@ -179,17 +182,6 @@ export function PreviewOverlays({
|
|
|
179
182
|
} = useDomEditActionsContext();
|
|
180
183
|
const mirrorZOrderToTimeline = useCanvasZOrderTimelineMirror();
|
|
181
184
|
|
|
182
|
-
// fallow-ignore-next-line complexity
|
|
183
|
-
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
184
|
-
const p = readStudioUiPreferences();
|
|
185
|
-
return {
|
|
186
|
-
snapEnabled: p.snapEnabled ?? true,
|
|
187
|
-
gridVisible: p.gridVisible ?? false,
|
|
188
|
-
gridSpacing: p.gridSpacing ?? 50,
|
|
189
|
-
snapToGrid: p.snapToGrid ?? false,
|
|
190
|
-
};
|
|
191
|
-
});
|
|
192
|
-
|
|
193
185
|
if (blockPreview) {
|
|
194
186
|
return (
|
|
195
187
|
<>
|
|
@@ -216,7 +208,7 @@ export function PreviewOverlays({
|
|
|
216
208
|
);
|
|
217
209
|
}
|
|
218
210
|
|
|
219
|
-
if (
|
|
211
|
+
if (previewCaptionEditMode) {
|
|
220
212
|
return (
|
|
221
213
|
<>
|
|
222
214
|
<TopologyLens iframeRef={previewIframeRef} activeCompositionPath={activeCompPath} />
|
|
@@ -263,11 +255,14 @@ export function PreviewOverlays({
|
|
|
263
255
|
return (
|
|
264
256
|
<>
|
|
265
257
|
<TopologyLens iframeRef={previewIframeRef} activeCompositionPath={activeCompPath} />
|
|
258
|
+
<GridOverlay />
|
|
266
259
|
<DomEditOverlay
|
|
267
260
|
iframeRef={previewIframeRef}
|
|
268
261
|
activeCompositionPath={activeCompPath}
|
|
269
262
|
hoverSelection={
|
|
270
|
-
!
|
|
263
|
+
!previewCaptionEditMode && !compositionLoading && !isPlaying
|
|
264
|
+
? domEditHoverSelection
|
|
265
|
+
: null
|
|
271
266
|
}
|
|
272
267
|
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
273
268
|
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
|
@@ -320,19 +315,19 @@ export function PreviewOverlays({
|
|
|
320
315
|
}),
|
|
321
316
|
}).catch(() => undefined);
|
|
322
317
|
}}
|
|
323
|
-
gridVisible={snapPrefs.gridVisible}
|
|
324
|
-
gridSpacing={snapPrefs.gridSpacing}
|
|
325
318
|
recordingState={recordingState}
|
|
326
319
|
onToggleRecording={onToggleRecording}
|
|
327
320
|
onMarqueeSelect={applyMarqueeSelection}
|
|
328
321
|
/>
|
|
329
|
-
<SnapToolbar
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
322
|
+
<SnapToolbar />
|
|
323
|
+
{!readOnly && (
|
|
324
|
+
<MotionPathOverlay
|
|
325
|
+
iframeRef={previewIframeRef}
|
|
326
|
+
selection={shouldShowMotionPath ? domEditSelection : null}
|
|
327
|
+
compositionSize={compositionDimensions}
|
|
328
|
+
isPlaying={isPlaying}
|
|
329
|
+
/>
|
|
330
|
+
)}
|
|
336
331
|
{gestureOverlay}
|
|
337
332
|
{captionModelPresent && captionDismissed && (
|
|
338
333
|
<button
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const previewState = vi.hoisted(() => ({ captionEditMode: false }));
|
|
4
|
+
export function getPreviewState() {
|
|
5
|
+
return previewState;
|
|
6
|
+
}
|
|
7
|
+
export const iframeRef = { current: null as HTMLIFrameElement | null };
|
|
8
|
+
|
|
9
|
+
vi.mock("../../contexts/StudioContext", () => ({
|
|
10
|
+
useStudioShellContext: () => ({ activeCompPath: "index.html", previewIframeRef: iframeRef }),
|
|
11
|
+
useStudioPlaybackContext: () => ({
|
|
12
|
+
captionEditMode: previewState.captionEditMode,
|
|
13
|
+
compositionLoading: false,
|
|
14
|
+
isPlaying: false,
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("../../captions/store", () => {
|
|
18
|
+
const state = {
|
|
19
|
+
model: null,
|
|
20
|
+
dismissed: false,
|
|
21
|
+
syncError: null,
|
|
22
|
+
clearSelection: vi.fn(),
|
|
23
|
+
setDismissed: vi.fn(),
|
|
24
|
+
setEditMode: vi.fn(),
|
|
25
|
+
setSyncError: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
useCaptionStore: Object.assign(
|
|
29
|
+
(selector: (value: typeof state) => unknown) => selector(state),
|
|
30
|
+
{ getState: () => state },
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
vi.mock("../../hooks/useCompositionDimensions", () => ({
|
|
35
|
+
useCompositionDimensions: () => null,
|
|
36
|
+
}));
|
|
37
|
+
vi.mock("../../utils/studioUiPreferences", () => ({ readStudioUiPreferences: () => ({}) }));
|
|
38
|
+
vi.mock("./useCanvasZOrderTimelineMirror", () => ({
|
|
39
|
+
useCanvasZOrderTimelineMirror: () => vi.fn(),
|
|
40
|
+
}));
|
|
@@ -7,6 +7,7 @@ import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
|
|
|
7
7
|
import { useNLEContext } from "./NLEContext";
|
|
8
8
|
import { AssetPreviewOverlay } from "./AssetPreviewOverlay";
|
|
9
9
|
import { PreviewGuides } from "../editor/PreviewGuides";
|
|
10
|
+
import { PreviewOverlayProvider } from "../editor/PreviewOverlayProvider";
|
|
10
11
|
|
|
11
12
|
function subscribeFullscreen(cb: () => void) {
|
|
12
13
|
document.addEventListener("fullscreenchange", cb);
|
|
@@ -60,6 +61,12 @@ export function PreviewPane({
|
|
|
60
61
|
togglePlay,
|
|
61
62
|
seek,
|
|
62
63
|
onIframeLoad,
|
|
64
|
+
previewSlots,
|
|
65
|
+
onShadowIframeLoad,
|
|
66
|
+
onShadowReadyChange,
|
|
67
|
+
onShadowError,
|
|
68
|
+
setShadowIframeNode,
|
|
69
|
+
resetPreviewSlots,
|
|
63
70
|
compositionStack,
|
|
64
71
|
handleNavigateComposition,
|
|
65
72
|
setCompositionLoading,
|
|
@@ -131,6 +138,12 @@ export function PreviewPane({
|
|
|
131
138
|
projectId={projectId}
|
|
132
139
|
iframeRef={iframeRef}
|
|
133
140
|
onIframeLoad={onIframeLoad}
|
|
141
|
+
previewSlots={previewSlots}
|
|
142
|
+
onShadowIframeLoad={onShadowIframeLoad}
|
|
143
|
+
onShadowReadyChange={onShadowReadyChange}
|
|
144
|
+
onShadowError={onShadowError}
|
|
145
|
+
setShadowIframeNode={setShadowIframeNode}
|
|
146
|
+
resetPreviewSlots={resetPreviewSlots}
|
|
134
147
|
onCompositionLoadingChange={setCompositionLoading}
|
|
135
148
|
portrait={portrait}
|
|
136
149
|
directUrl={directUrl}
|
|
@@ -143,8 +156,10 @@ export function PreviewPane({
|
|
|
143
156
|
)}
|
|
144
157
|
<AssetPreviewOverlay />
|
|
145
158
|
</div>
|
|
146
|
-
<
|
|
147
|
-
|
|
159
|
+
<PreviewOverlayProvider>
|
|
160
|
+
<PreviewGuides />
|
|
161
|
+
{!isFullscreen && previewOverlay}
|
|
162
|
+
</PreviewOverlayProvider>
|
|
148
163
|
</div>
|
|
149
164
|
{/* Transport row: no own background or border — the controls sit flat on
|
|
150
165
|
the preview panel's surface (CapCut-style). */}
|
|
@@ -255,7 +255,11 @@ export function TimelinePane({
|
|
|
255
255
|
);
|
|
256
256
|
|
|
257
257
|
return (
|
|
258
|
-
<div
|
|
258
|
+
<div
|
|
259
|
+
className="relative flex h-full flex-col"
|
|
260
|
+
data-studio-timeline="true"
|
|
261
|
+
aria-disabled={timelineDisabled || undefined}
|
|
262
|
+
>
|
|
259
263
|
<div
|
|
260
264
|
className="flex flex-col flex-1 min-h-0 overflow-hidden rounded-lg border border-neutral-800/50 bg-neutral-950"
|
|
261
265
|
onDoubleClick={(e) => {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { getPreviewState, iframeRef } from "./PreviewOverlaysTestMocks";
|
|
7
|
+
import { PreviewOverlays } from "./PreviewOverlays";
|
|
8
|
+
import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
|
|
9
|
+
import { PreviewReadOnlyProvider } from "../editor/previewReadOnlyContext";
|
|
10
|
+
import { TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
|
11
|
+
|
|
12
|
+
const previewState = getPreviewState();
|
|
13
|
+
|
|
14
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
vi.mock("../../contexts/DomEditContext", () => ({
|
|
17
|
+
useDomEditSelectionContext: () => ({
|
|
18
|
+
domEditHoverSelection: null,
|
|
19
|
+
domEditSelection: null,
|
|
20
|
+
domEditGroupSelections: [],
|
|
21
|
+
}),
|
|
22
|
+
useDomEditActionsContext: () => ({}),
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("../editor/TopologyLens", () => ({ TopologyLens: () => null }));
|
|
25
|
+
vi.mock("../../captions/components/CaptionOverlay", () => ({
|
|
26
|
+
CaptionOverlay: () => <i data-testid="caption-overlay" />,
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../editor/DomEditOverlay", () => ({
|
|
29
|
+
DomEditOverlay: () => <i data-testid="dom-edit-overlay" />,
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../editor/MotionPathOverlay", () => ({
|
|
32
|
+
MotionPathOverlay: () => <i data-testid="motion-path" />,
|
|
33
|
+
}));
|
|
34
|
+
vi.mock("../editor/SnapToolbar", () => ({ SnapToolbar: () => null }));
|
|
35
|
+
vi.mock("../editor/GridOverlay", () => ({ GridOverlay: () => null }));
|
|
36
|
+
|
|
37
|
+
let root: Root;
|
|
38
|
+
let host: HTMLDivElement;
|
|
39
|
+
|
|
40
|
+
function mount(node: React.ReactNode, readOnly = false): void {
|
|
41
|
+
host = document.createElement("div");
|
|
42
|
+
iframeRef.current = document.createElement("iframe");
|
|
43
|
+
document.body.append(host, iframeRef.current);
|
|
44
|
+
root = createRoot(host);
|
|
45
|
+
act(() =>
|
|
46
|
+
root.render(<PreviewReadOnlyProvider readOnly={readOnly}>{node}</PreviewReadOnlyProvider>),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
act(() => root.unmount());
|
|
52
|
+
previewState.captionEditMode = false;
|
|
53
|
+
document.body.replaceChildren();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const overlays = () => (
|
|
57
|
+
<PreviewOverlays shouldShowMotionPath={true} shouldShowSelectedDomBounds={true} />
|
|
58
|
+
);
|
|
59
|
+
const has = (id: string) => host.querySelector(`[data-testid="${id}"]`) !== null;
|
|
60
|
+
|
|
61
|
+
describe("PreviewOverlays with the preview read-only", () => {
|
|
62
|
+
it("control: with the flag off the motion path can be edited", () => {
|
|
63
|
+
mount(overlays());
|
|
64
|
+
expect(has("motion-path")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not mount the motion path editor", () => {
|
|
68
|
+
mount(overlays(), true);
|
|
69
|
+
expect(has("motion-path")).toBe(false);
|
|
70
|
+
expect(has("dom-edit-overlay")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("control: with the flag off caption mode mounts the caption editor", () => {
|
|
74
|
+
previewState.captionEditMode = true;
|
|
75
|
+
mount(overlays());
|
|
76
|
+
expect(has("caption-overlay")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not mount the caption editor, and selection stays on the canvas overlay", () => {
|
|
80
|
+
previewState.captionEditMode = true;
|
|
81
|
+
mount(overlays(), true);
|
|
82
|
+
expect(has("caption-overlay")).toBe(false);
|
|
83
|
+
expect(has("dom-edit-overlay")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("usePreviewBlockDrop with the preview read-only", () => {
|
|
88
|
+
function dropOnPreview(
|
|
89
|
+
onBlockDrop: (name: string, pos: { left: number; top: number }) => void,
|
|
90
|
+
readOnly = false,
|
|
91
|
+
) {
|
|
92
|
+
const stage = document.createElement("div");
|
|
93
|
+
stage.getBoundingClientRect = () => ({ left: 0, top: 0, width: 100, height: 100 }) as DOMRect;
|
|
94
|
+
let handlers: ReturnType<typeof usePreviewBlockDrop> | null = null;
|
|
95
|
+
function Probe() {
|
|
96
|
+
handlers = usePreviewBlockDrop({
|
|
97
|
+
stageRef: { current: stage },
|
|
98
|
+
compositionSize: { width: 1000, height: 1000 },
|
|
99
|
+
onBlockDrop,
|
|
100
|
+
});
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
mount(<Probe />, readOnly);
|
|
104
|
+
const event = {
|
|
105
|
+
clientX: 50,
|
|
106
|
+
clientY: 50,
|
|
107
|
+
preventDefault: vi.fn(),
|
|
108
|
+
dataTransfer: {
|
|
109
|
+
types: [TIMELINE_BLOCK_MIME],
|
|
110
|
+
getData: () => JSON.stringify({ name: "lower-third" }),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
act(() => handlers!.handleDrop(event as never));
|
|
114
|
+
return event;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
it("control: with the flag off a dropped block is placed", () => {
|
|
118
|
+
const onBlockDrop = vi.fn();
|
|
119
|
+
dropOnPreview(onBlockDrop);
|
|
120
|
+
expect(onBlockDrop).toHaveBeenCalledWith("lower-third", { left: 500, top: 500 });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not place a dropped block", () => {
|
|
124
|
+
const onBlockDrop = vi.fn();
|
|
125
|
+
const event = dropOnPreview(onBlockDrop, true);
|
|
126
|
+
expect(onBlockDrop).not.toHaveBeenCalled();
|
|
127
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
afterEach(() => {
|
|
25
25
|
document.body.innerHTML = "";
|
|
26
26
|
// Wrapped in act: mounted harnesses subscribe to the store via
|
|
27
|
-
//
|
|
27
|
+
// the store timeline elements, so the reset re-renders them.
|
|
28
28
|
act(() => usePlayerStore.getState().setElements([]));
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { usePlayerStore } from "../../player";
|
|
3
|
-
import { useTimelineRowElements } from "../../player/hooks/useTimelineRowElements";
|
|
4
3
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
5
4
|
import {
|
|
6
5
|
displayTrackOrder,
|
|
@@ -41,10 +40,8 @@ export interface MirrorZOrderInput {
|
|
|
41
40
|
* commitZMirrorLaneMove) — the shared key is unique per gesture
|
|
42
41
|
* (zReorderCoalesceKey's gesture seq), so the fold stays gesture-scoped.
|
|
43
42
|
*
|
|
44
|
-
* Element source:
|
|
45
|
-
*
|
|
46
|
-
* lanes, expanded sub-comp children on their synthetic rows). No new expansion
|
|
47
|
-
* is built here.
|
|
43
|
+
* Element source: the store's timeline elements, the same set the Timeline
|
|
44
|
+
* renders and the resolver expects. No alternate row expansion is built here.
|
|
48
45
|
*
|
|
49
46
|
* The mirror persists through the SAME machinery as a timeline lane drag
|
|
50
47
|
* (commitZMirrorLaneMove → persistMoveEdits → onMoveElements, with expanded
|
|
@@ -141,7 +138,7 @@ function useMirrorLaneMoveCommit(): (
|
|
|
141
138
|
coalesceKey: string,
|
|
142
139
|
resolveMove: (element: TimelineElement, elements: TimelineElement[]) => ZMirrorLaneMove,
|
|
143
140
|
) => Promise<boolean> {
|
|
144
|
-
const elements =
|
|
141
|
+
const elements = usePlayerStore((state) => state.elements);
|
|
145
142
|
const elementsRef = useRef(elements);
|
|
146
143
|
elementsRef.current = elements;
|
|
147
144
|
const { onMoveElements } = useTimelineEditContextOptional();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef, useState, type RefObject } from "react";
|
|
2
2
|
import { TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
|
3
|
+
import { usePreviewReadOnly } from "../editor/previewReadOnlyContext";
|
|
3
4
|
|
|
4
5
|
interface UsePreviewBlockDropOptions {
|
|
5
6
|
portrait?: boolean;
|
|
@@ -67,8 +68,9 @@ export function usePreviewBlockDrop({
|
|
|
67
68
|
portrait,
|
|
68
69
|
compositionSize,
|
|
69
70
|
stageRef,
|
|
70
|
-
onBlockDrop,
|
|
71
|
+
onBlockDrop: onBlockDropProp,
|
|
71
72
|
}: UsePreviewBlockDropOptions) {
|
|
73
|
+
const onBlockDrop = usePreviewReadOnly() ? undefined : onBlockDropProp;
|
|
72
74
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
73
75
|
// dragenter/dragleave fire for every internal element boundary; a depth
|
|
74
76
|
// counter keeps the drop indicator steady instead of flickering.
|