@hyperframes/studio 0.7.54 → 0.7.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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +303 -0
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
useRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { useTimelinePlayer, usePlayerStore } from "../../player";
|
|
11
|
+
import type { TimelineElement } from "../../player";
|
|
12
|
+
import type { CompositionLevel } from "./CompositionBreadcrumb";
|
|
13
|
+
import { useCompositionStack } from "./useCompositionStack";
|
|
14
|
+
import { MIN_TIMELINE_H, MIN_PREVIEW_H } from "./TimelineResizeDivider";
|
|
15
|
+
import { setCompositionSourceMap } from "../editor/domEditingDom";
|
|
16
|
+
import { ensureMotionPathPluginLoaded } from "../../utils/gsapSoftReload";
|
|
17
|
+
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
18
|
+
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
19
|
+
|
|
20
|
+
// Timeline gets a generous default height so the preview isn't oversized and the
|
|
21
|
+
// tracks have room to breathe (CapCut-style). Users can still drag the divider.
|
|
22
|
+
const DEFAULT_TIMELINE_H = 340;
|
|
23
|
+
|
|
24
|
+
export function shouldDisableTimelineWhileCompositionLoading(compositionLoading: boolean): boolean {
|
|
25
|
+
return compositionLoading;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NLEContextValue {
|
|
29
|
+
projectId: string;
|
|
30
|
+
// player (from useTimelinePlayer — single instance for the whole shell)
|
|
31
|
+
iframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
32
|
+
togglePlay: () => void;
|
|
33
|
+
seek: (time: number, options?: { keepPlaying?: boolean }) => boolean;
|
|
34
|
+
refreshPlayer: () => void;
|
|
35
|
+
onIframeLoad: () => void;
|
|
36
|
+
// composition stack (from useCompositionStack)
|
|
37
|
+
compositionStack: CompositionLevel[];
|
|
38
|
+
updateCompositionStack: React.Dispatch<React.SetStateAction<CompositionLevel[]>>;
|
|
39
|
+
handleNavigateComposition: (index: number) => void;
|
|
40
|
+
handleDrillDown: (element: TimelineElement) => void;
|
|
41
|
+
compIdToSrc: Map<string, string>;
|
|
42
|
+
// layout state
|
|
43
|
+
timelineH: number;
|
|
44
|
+
setTimelineH: React.Dispatch<React.SetStateAction<number>>;
|
|
45
|
+
persistTimelineH: (height: number) => void;
|
|
46
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
47
|
+
// composition loading
|
|
48
|
+
compositionLoading: boolean;
|
|
49
|
+
setCompositionLoading: (loading: boolean) => void;
|
|
50
|
+
timelineDisabled: boolean;
|
|
51
|
+
hasLoadedOnceRef: React.MutableRefObject<boolean>;
|
|
52
|
+
// preview composition size (for preview block drop)
|
|
53
|
+
previewCompositionSize: { width: number; height: number } | null;
|
|
54
|
+
setPreviewCompositionSize: (size: { width: number; height: number } | null) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NLEContext = createContext<NLEContextValue | null>(null);
|
|
58
|
+
|
|
59
|
+
export function useNLEContext(): NLEContextValue {
|
|
60
|
+
const ctx = useContext(NLEContext);
|
|
61
|
+
if (!ctx) throw new Error("useNLEContext must be used within an NLEProvider");
|
|
62
|
+
return ctx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NLEProviderProps {
|
|
66
|
+
projectId: string;
|
|
67
|
+
refreshKey?: number;
|
|
68
|
+
activeCompositionPath?: string | null;
|
|
69
|
+
onIframeRef?: (iframe: HTMLIFrameElement | null) => void;
|
|
70
|
+
onCompositionChange?: (compositionPath: string | null) => void;
|
|
71
|
+
onCompIdToSrcChange?: (map: Map<string, string>) => void;
|
|
72
|
+
onCompositionLoadingChange?: (loading: boolean) => void;
|
|
73
|
+
children: ReactNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function NLEProvider({
|
|
77
|
+
projectId,
|
|
78
|
+
refreshKey,
|
|
79
|
+
activeCompositionPath,
|
|
80
|
+
onIframeRef,
|
|
81
|
+
onCompositionChange,
|
|
82
|
+
onCompIdToSrcChange,
|
|
83
|
+
onCompositionLoadingChange,
|
|
84
|
+
children,
|
|
85
|
+
}: NLEProviderProps) {
|
|
86
|
+
const {
|
|
87
|
+
iframeRef,
|
|
88
|
+
togglePlay,
|
|
89
|
+
seek,
|
|
90
|
+
onIframeLoad: baseOnIframeLoad,
|
|
91
|
+
refreshPlayer,
|
|
92
|
+
} = useTimelinePlayer();
|
|
93
|
+
|
|
94
|
+
// Reset timeline state when the project changes. Done in an effect, not during
|
|
95
|
+
// render: reset() updates the player store, and updating another store/component
|
|
96
|
+
// mid-render triggers React's "Cannot update a component while rendering a
|
|
97
|
+
// different component" warning. The effect runs right after commit, so the new
|
|
98
|
+
// project's first frame may briefly show prior timeline state before it clears.
|
|
99
|
+
//
|
|
100
|
+
// Also clears the asset preview overlay store: it is project-scoped (see its
|
|
101
|
+
// doc-comment) but nothing else clears it on project change — EditorShell isn't
|
|
102
|
+
// keyed by projectId and the overlay stays mounted, so a preview opened in one
|
|
103
|
+
// project would otherwise keep rendering (and re-fetching from) the old project
|
|
104
|
+
// after switching.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
usePlayerStore.getState().reset();
|
|
107
|
+
useAssetPreviewStore.getState().clearPreviewAsset();
|
|
108
|
+
}, [projectId]);
|
|
109
|
+
|
|
110
|
+
// Authored composition size measured from the loaded preview — drives drop
|
|
111
|
+
// coordinate mapping so blocks land where the user pointed on any comp size.
|
|
112
|
+
const [previewCompositionSize, setPreviewCompositionSize] = useState<{
|
|
113
|
+
width: number;
|
|
114
|
+
height: number;
|
|
115
|
+
} | null>(null);
|
|
116
|
+
|
|
117
|
+
// Lightweight reload: change iframe src instead of destroying the Player.
|
|
118
|
+
const prevRefreshKeyRef = useRef(refreshKey);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (refreshKey === prevRefreshKeyRef.current) return;
|
|
121
|
+
prevRefreshKeyRef.current = refreshKey;
|
|
122
|
+
refreshPlayer();
|
|
123
|
+
}, [refreshKey, refreshPlayer]);
|
|
124
|
+
|
|
125
|
+
const onIframeLoad = useCallback(() => {
|
|
126
|
+
baseOnIframeLoad();
|
|
127
|
+
// Pre-load + register MotionPathPlugin once so adding a motion path in the
|
|
128
|
+
// studio doesn't take the async plugin-load flash path on the first soft
|
|
129
|
+
// reload (the comp may not ship the plugin until it actually uses one).
|
|
130
|
+
ensureMotionPathPluginLoaded(iframeRef.current);
|
|
131
|
+
onIframeRef?.(iframeRef.current);
|
|
132
|
+
}, [baseOnIframeLoad, iframeRef, onIframeRef]);
|
|
133
|
+
|
|
134
|
+
const {
|
|
135
|
+
compositionStack,
|
|
136
|
+
updateCompositionStack,
|
|
137
|
+
handleNavigateComposition,
|
|
138
|
+
handleDrillDown: drillDown,
|
|
139
|
+
compIdToSrc,
|
|
140
|
+
setCompIdToSrc,
|
|
141
|
+
} = useCompositionStack({
|
|
142
|
+
projectId,
|
|
143
|
+
activeCompositionPath,
|
|
144
|
+
onCompositionChange,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Wrap handleDrillDown to also scan the iframe DOM for data-composition-src
|
|
148
|
+
const iframeRef_ = iframeRef;
|
|
149
|
+
const handleDrillDown = useCallback(
|
|
150
|
+
(element: TimelineElement) => {
|
|
151
|
+
if (!element.compositionSrc) return;
|
|
152
|
+
usePlayerStore.getState().setSelectedElementId(null);
|
|
153
|
+
// Check compIdToSrc map first; then scan iframe DOM; then fall through to drillDown
|
|
154
|
+
const compId = element.id;
|
|
155
|
+
let resolvedPath = compIdToSrc.get(compId);
|
|
156
|
+
if (!resolvedPath) {
|
|
157
|
+
try {
|
|
158
|
+
const doc = iframeRef_.current?.contentDocument;
|
|
159
|
+
if (doc) {
|
|
160
|
+
const host = doc.querySelector(
|
|
161
|
+
`[data-composition-id="${CSS.escape(compId)}"][data-composition-src]`,
|
|
162
|
+
);
|
|
163
|
+
if (host) {
|
|
164
|
+
resolvedPath = host.getAttribute("data-composition-src") || undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
/* cross-origin */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Delegate with the resolved compositionSrc (may be same as original)
|
|
172
|
+
drillDown({
|
|
173
|
+
id: compId,
|
|
174
|
+
compositionSrc: resolvedPath ?? element.compositionSrc,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
[compIdToSrc, drillDown, iframeRef_],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Composition ID → file path map from raw index.html
|
|
181
|
+
const compIdToSrcRef = useRef(compIdToSrc);
|
|
182
|
+
compIdToSrcRef.current = compIdToSrc;
|
|
183
|
+
const onCompIdToSrcChangeRef = useRef(onCompIdToSrcChange);
|
|
184
|
+
onCompIdToSrcChangeRef.current = onCompIdToSrcChange;
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
let current = true;
|
|
189
|
+
const emptyMap = new Map<string, string>();
|
|
190
|
+
setCompIdToSrc(emptyMap);
|
|
191
|
+
setCompositionSourceMap(emptyMap);
|
|
192
|
+
onCompIdToSrcChangeRef.current?.(emptyMap);
|
|
193
|
+
|
|
194
|
+
fetch(`/api/projects/${projectId}/files/index.html`, {
|
|
195
|
+
signal: controller.signal,
|
|
196
|
+
})
|
|
197
|
+
.then((r) => {
|
|
198
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
199
|
+
return r.json();
|
|
200
|
+
})
|
|
201
|
+
.then((data: { content?: string }) => {
|
|
202
|
+
if (!current) return;
|
|
203
|
+
const html = data.content || "";
|
|
204
|
+
const map = new Map<string, string>();
|
|
205
|
+
const re =
|
|
206
|
+
/data-composition-id=["']([^"']+)["'][^>]*data-composition-src=["']([^"']+)["']|data-composition-src=["']([^"']+)["'][^>]*data-composition-id=["']([^"']+)["']/g;
|
|
207
|
+
let match;
|
|
208
|
+
while ((match = re.exec(html)) !== null) {
|
|
209
|
+
const id = match[1] || match[4];
|
|
210
|
+
const src = match[2] || match[3];
|
|
211
|
+
if (id && src) map.set(id, src);
|
|
212
|
+
}
|
|
213
|
+
setCompIdToSrc(map);
|
|
214
|
+
// Let DOM source-resolution recover a subcomposition element's source file
|
|
215
|
+
// (the runtime drops the linkage when inlining — see getSourceFileForElement).
|
|
216
|
+
setCompositionSourceMap(map);
|
|
217
|
+
onCompIdToSrcChangeRef.current?.(map);
|
|
218
|
+
})
|
|
219
|
+
.catch((err: unknown) => {
|
|
220
|
+
if (!current || controller.signal.aborted) return;
|
|
221
|
+
// Non-fatal: drill-down still works via the iframe DOM scan; without
|
|
222
|
+
// the map only source-file resolution for sub-comps degrades.
|
|
223
|
+
console.warn("[studio] Couldn't load composition source map from index.html:", err);
|
|
224
|
+
});
|
|
225
|
+
return () => {
|
|
226
|
+
current = false;
|
|
227
|
+
controller.abort();
|
|
228
|
+
};
|
|
229
|
+
}, [projectId, setCompIdToSrc]);
|
|
230
|
+
|
|
231
|
+
// Patch elements with compositionSrc whenever elements or compIdToSrc change.
|
|
232
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (compIdToSrc.size === 0) return;
|
|
235
|
+
const patchElements = (elements: TimelineElement[]): TimelineElement[] | null => {
|
|
236
|
+
const map = compIdToSrcRef.current;
|
|
237
|
+
if (map.size === 0) return null;
|
|
238
|
+
let patched = false;
|
|
239
|
+
const updated = elements.map((el) => {
|
|
240
|
+
if (el.compositionSrc) return el;
|
|
241
|
+
const src = map.get(el.id) ?? map.get(el.id.replace(/-(host|comp|layer)$/, ""));
|
|
242
|
+
if (src) {
|
|
243
|
+
patched = true;
|
|
244
|
+
return { ...el, compositionSrc: src };
|
|
245
|
+
}
|
|
246
|
+
return el;
|
|
247
|
+
});
|
|
248
|
+
return patched ? updated : null;
|
|
249
|
+
};
|
|
250
|
+
const patched = patchElements(usePlayerStore.getState().elements);
|
|
251
|
+
if (patched) usePlayerStore.getState().setElements(patched);
|
|
252
|
+
let patching = false;
|
|
253
|
+
return usePlayerStore.subscribe((state, prev) => {
|
|
254
|
+
if (patching) return;
|
|
255
|
+
if (state.elements === prev.elements || state.elements.length === 0) return;
|
|
256
|
+
if (state.elements.every((el) => el.compositionSrc)) return;
|
|
257
|
+
patching = true;
|
|
258
|
+
const result = patchElements(state.elements);
|
|
259
|
+
if (result) state.setElements(result);
|
|
260
|
+
patching = false;
|
|
261
|
+
});
|
|
262
|
+
}, [compIdToSrc]);
|
|
263
|
+
|
|
264
|
+
// Resizable timeline height — persisted alongside zoom/pan so the user's
|
|
265
|
+
// workspace layout survives reloads.
|
|
266
|
+
const [timelineH, setTimelineH] = useState(() => {
|
|
267
|
+
const stored = readStudioUiPreferences().timelineHeight;
|
|
268
|
+
return stored !== undefined && stored >= MIN_TIMELINE_H ? stored : DEFAULT_TIMELINE_H;
|
|
269
|
+
});
|
|
270
|
+
const persistTimelineH = useCallback((height: number) => {
|
|
271
|
+
writeStudioUiPreferences({ timelineHeight: Math.round(height) });
|
|
272
|
+
}, []);
|
|
273
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
274
|
+
// A height persisted on a tall window can exceed this window's container and
|
|
275
|
+
// collapse the flex-1 preview to 0px — clamp once the container is measurable
|
|
276
|
+
// (the drag/keyboard paths already clamp; the restore path must too).
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const containerH = containerRef.current?.getBoundingClientRect().height;
|
|
279
|
+
if (!containerH) return;
|
|
280
|
+
const max = containerH - MIN_PREVIEW_H;
|
|
281
|
+
setTimelineH((prev) => (prev > max ? Math.max(MIN_TIMELINE_H, max) : prev));
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
284
|
+
const hasLoadedOnceRef = useRef(false);
|
|
285
|
+
const [compositionLoading, setCompositionLoadingRaw] = useState(true);
|
|
286
|
+
const setCompositionLoading = useCallback((loading: boolean) => {
|
|
287
|
+
if (!loading) hasLoadedOnceRef.current = true;
|
|
288
|
+
if (loading && hasLoadedOnceRef.current) return;
|
|
289
|
+
setCompositionLoadingRaw(loading);
|
|
290
|
+
}, []);
|
|
291
|
+
const timelineDisabled = shouldDisableTimelineWhileCompositionLoading(compositionLoading);
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
onCompositionLoadingChange?.(compositionLoading);
|
|
295
|
+
}, [compositionLoading, onCompositionLoadingChange]);
|
|
296
|
+
|
|
297
|
+
const onIframeRefStable = useRef(onIframeRef);
|
|
298
|
+
onIframeRefStable.current = onIframeRef;
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
onIframeRefStable.current?.(iframeRef.current);
|
|
301
|
+
}, [compositionStack.length, refreshKey, iframeRef]);
|
|
302
|
+
|
|
303
|
+
const value: NLEContextValue = {
|
|
304
|
+
projectId,
|
|
305
|
+
iframeRef,
|
|
306
|
+
togglePlay,
|
|
307
|
+
seek,
|
|
308
|
+
refreshPlayer,
|
|
309
|
+
onIframeLoad,
|
|
310
|
+
compositionStack,
|
|
311
|
+
updateCompositionStack,
|
|
312
|
+
handleNavigateComposition,
|
|
313
|
+
handleDrillDown,
|
|
314
|
+
compIdToSrc,
|
|
315
|
+
timelineH,
|
|
316
|
+
setTimelineH,
|
|
317
|
+
persistTimelineH,
|
|
318
|
+
containerRef,
|
|
319
|
+
compositionLoading,
|
|
320
|
+
setCompositionLoading,
|
|
321
|
+
timelineDisabled,
|
|
322
|
+
hasLoadedOnceRef,
|
|
323
|
+
previewCompositionSize,
|
|
324
|
+
setPreviewCompositionSize,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return <NLEContext.Provider value={value}>{children}</NLEContext.Provider>;
|
|
328
|
+
}
|
|
@@ -459,7 +459,7 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
459
459
|
<div className="flex flex-col h-full min-h-0">
|
|
460
460
|
<div
|
|
461
461
|
ref={viewportRef}
|
|
462
|
-
className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-none focus:ring-1 focus:ring-studio-accent/40 bg-neutral-
|
|
462
|
+
className="relative flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0 outline-none focus:ring-1 focus:ring-studio-accent/40 bg-neutral-950"
|
|
463
463
|
tabIndex={0}
|
|
464
464
|
aria-label="Composition preview"
|
|
465
465
|
>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { CaptionOverlay } from "../../captions/components/CaptionOverlay";
|
|
3
|
+
import { DomEditOverlay } from "../editor/DomEditOverlay";
|
|
4
|
+
import { MotionPathOverlay } from "../editor/MotionPathOverlay";
|
|
5
|
+
import { SnapToolbar } from "../editor/SnapToolbar";
|
|
6
|
+
import { useCompositionDimensions } from "../../hooks/useCompositionDimensions";
|
|
7
|
+
import {
|
|
8
|
+
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
9
|
+
STUDIO_KEYFRAMES_ENABLED,
|
|
10
|
+
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
|
11
|
+
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
12
|
+
} from "../editor/manualEditingAvailability";
|
|
13
|
+
import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
|
|
14
|
+
import {
|
|
15
|
+
useDomEditActionsContext,
|
|
16
|
+
useDomEditSelectionContext,
|
|
17
|
+
} from "../../contexts/DomEditContext";
|
|
18
|
+
import { readStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
19
|
+
import { readHfId, type DomEditSelection } from "../editor/domEditing";
|
|
20
|
+
import { buildStableSelector } from "../editor/domEditingDom";
|
|
21
|
+
import type { BlockPreviewInfo } from "../sidebar/BlocksTab";
|
|
22
|
+
import type { GestureRecordingState } from "../editor/GestureRecordControl";
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
|
|
25
|
+
export interface PreviewOverlaysProps {
|
|
26
|
+
shouldShowSelectedDomBounds: boolean;
|
|
27
|
+
blockPreview?: BlockPreviewInfo | null;
|
|
28
|
+
isGestureRecording?: boolean;
|
|
29
|
+
recordingState?: GestureRecordingState;
|
|
30
|
+
onToggleRecording?: () => void;
|
|
31
|
+
gestureOverlay?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ZIndexReorderEntry = {
|
|
35
|
+
element: HTMLElement;
|
|
36
|
+
zIndex: number;
|
|
37
|
+
id?: string;
|
|
38
|
+
selector?: string;
|
|
39
|
+
selectorIndex?: number;
|
|
40
|
+
sourceFile: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Can this element be robustly re-targeted for a persisted z change? */
|
|
44
|
+
function canTargetZIndexElement(
|
|
45
|
+
element: HTMLElement,
|
|
46
|
+
id: string | undefined,
|
|
47
|
+
selector: string | undefined,
|
|
48
|
+
): boolean {
|
|
49
|
+
return Boolean(id || selector || readHfId(element));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The selected element carries its full selection identity. */
|
|
53
|
+
function selectedZIndexEntry(sel: DomEditSelection, zIndex: number): ZIndexReorderEntry {
|
|
54
|
+
return {
|
|
55
|
+
element: sel.element,
|
|
56
|
+
zIndex,
|
|
57
|
+
id: sel.id ?? undefined,
|
|
58
|
+
selector: sel.selector,
|
|
59
|
+
selectorIndex: sel.selectorIndex,
|
|
60
|
+
sourceFile: sel.sourceFile,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sibling elements are raw iframe DOM nodes with no selection object: derive a
|
|
66
|
+
* PatchTarget from the node itself (siblings live in the same document, so they
|
|
67
|
+
* share the selection's sourceFile). Null when it cannot be robustly targeted
|
|
68
|
+
* (no id and no selector) — its z stays live-only.
|
|
69
|
+
*/
|
|
70
|
+
function siblingZIndexEntry(
|
|
71
|
+
element: HTMLElement,
|
|
72
|
+
zIndex: number,
|
|
73
|
+
sourceFile: string,
|
|
74
|
+
): ZIndexReorderEntry | null {
|
|
75
|
+
const id = element.id || undefined;
|
|
76
|
+
const selector = buildStableSelector(element);
|
|
77
|
+
if (!canTargetZIndexElement(element, id, selector)) return null;
|
|
78
|
+
return { element, zIndex, id, selector, selectorIndex: undefined, sourceFile };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Short human-readable label for a dropped sibling, for the console warning below. */
|
|
82
|
+
function describeZIndexElement(element: HTMLElement): string {
|
|
83
|
+
if (element.id) return `#${element.id}`;
|
|
84
|
+
const firstClass = element.classList.item(0);
|
|
85
|
+
return firstClass
|
|
86
|
+
? `${element.tagName.toLowerCase()}.${firstClass}`
|
|
87
|
+
: element.tagName.toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve z-index patches into commit entries; a sibling with no stable
|
|
91
|
+
// id/selector can't be written to source, so it is collected as a label for the
|
|
92
|
+
// revert-on-reload warning instead.
|
|
93
|
+
function resolveZIndexEntries(
|
|
94
|
+
sel: DomEditSelection,
|
|
95
|
+
patches: ReadonlyArray<{ element: HTMLElement; zIndex: number }>,
|
|
96
|
+
): { entries: ZIndexReorderEntry[]; droppedLabels: string[] } {
|
|
97
|
+
const entries: ZIndexReorderEntry[] = [];
|
|
98
|
+
const droppedLabels: string[] = [];
|
|
99
|
+
for (const patch of patches) {
|
|
100
|
+
if (patch.element === sel.element) {
|
|
101
|
+
entries.push(selectedZIndexEntry(sel, patch.zIndex));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const entry = siblingZIndexEntry(patch.element, patch.zIndex, sel.sourceFile);
|
|
105
|
+
if (entry) entries.push(entry);
|
|
106
|
+
else droppedLabels.push(describeZIndexElement(patch.element));
|
|
107
|
+
}
|
|
108
|
+
return { entries, droppedLabels };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// fallow-ignore-next-line complexity
|
|
112
|
+
export function PreviewOverlays({
|
|
113
|
+
shouldShowSelectedDomBounds,
|
|
114
|
+
blockPreview,
|
|
115
|
+
isGestureRecording,
|
|
116
|
+
recordingState,
|
|
117
|
+
onToggleRecording,
|
|
118
|
+
gestureOverlay,
|
|
119
|
+
}: PreviewOverlaysProps) {
|
|
120
|
+
const { activeCompPath, previewIframeRef } = useStudioShellContext();
|
|
121
|
+
const { captionEditMode, compositionLoading, isPlaying } = useStudioPlaybackContext();
|
|
122
|
+
const compositionDimensions = useCompositionDimensions();
|
|
123
|
+
|
|
124
|
+
const { domEditHoverSelection, domEditSelection, domEditGroupSelections } =
|
|
125
|
+
useDomEditSelectionContext();
|
|
126
|
+
const {
|
|
127
|
+
handlePreviewCanvasMouseDown,
|
|
128
|
+
handlePreviewCanvasPointerMove,
|
|
129
|
+
handlePreviewCanvasPointerLeave,
|
|
130
|
+
applyDomSelection,
|
|
131
|
+
handleBlockedDomMove,
|
|
132
|
+
handleDomManualDragStart,
|
|
133
|
+
handleDomPathOffsetCommit,
|
|
134
|
+
handleDomGroupPathOffsetCommit,
|
|
135
|
+
handleDomBoxSizeCommit,
|
|
136
|
+
handleDomRotationCommit,
|
|
137
|
+
handleDomStyleCommit,
|
|
138
|
+
applyMarqueeSelection,
|
|
139
|
+
handleDomEditElementDelete,
|
|
140
|
+
handleDomZIndexReorderCommit,
|
|
141
|
+
} = useDomEditActionsContext();
|
|
142
|
+
|
|
143
|
+
// fallow-ignore-next-line complexity
|
|
144
|
+
const [snapPrefs, setSnapPrefs] = useState(() => {
|
|
145
|
+
const p = readStudioUiPreferences();
|
|
146
|
+
return {
|
|
147
|
+
snapEnabled: p.snapEnabled ?? true,
|
|
148
|
+
gridVisible: p.gridVisible ?? false,
|
|
149
|
+
gridSpacing: p.gridSpacing ?? 50,
|
|
150
|
+
snapToGrid: p.snapToGrid ?? false,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (blockPreview) {
|
|
155
|
+
return (
|
|
156
|
+
<div className="absolute inset-0 z-30 bg-black pointer-events-none">
|
|
157
|
+
{blockPreview.videoUrl ? (
|
|
158
|
+
<video
|
|
159
|
+
src={blockPreview.videoUrl}
|
|
160
|
+
autoPlay
|
|
161
|
+
muted
|
|
162
|
+
loop
|
|
163
|
+
playsInline
|
|
164
|
+
className="w-full h-full object-contain"
|
|
165
|
+
/>
|
|
166
|
+
) : blockPreview.posterUrl ? (
|
|
167
|
+
<img
|
|
168
|
+
src={blockPreview.posterUrl}
|
|
169
|
+
alt={blockPreview.title}
|
|
170
|
+
className="w-full h-full object-contain"
|
|
171
|
+
/>
|
|
172
|
+
) : null}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (captionEditMode) {
|
|
178
|
+
return <CaptionOverlay iframeRef={previewIframeRef} />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return null;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<>
|
|
185
|
+
<DomEditOverlay
|
|
186
|
+
iframeRef={previewIframeRef}
|
|
187
|
+
activeCompositionPath={activeCompPath}
|
|
188
|
+
hoverSelection={
|
|
189
|
+
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode && !compositionLoading && !isPlaying
|
|
190
|
+
? domEditHoverSelection
|
|
191
|
+
: null
|
|
192
|
+
}
|
|
193
|
+
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
194
|
+
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
|
195
|
+
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !isGestureRecording}
|
|
196
|
+
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
|
197
|
+
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
|
198
|
+
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
|
199
|
+
onSelectionChange={applyDomSelection}
|
|
200
|
+
onBlockedMove={handleBlockedDomMove}
|
|
201
|
+
onManualDragStart={handleDomManualDragStart}
|
|
202
|
+
onPathOffsetCommit={handleDomPathOffsetCommit}
|
|
203
|
+
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
|
204
|
+
onBoxSizeCommit={handleDomBoxSizeCommit}
|
|
205
|
+
onRotationCommit={handleDomRotationCommit}
|
|
206
|
+
onStyleCommit={handleDomStyleCommit}
|
|
207
|
+
onDeleteSelection={handleDomEditElementDelete}
|
|
208
|
+
onApplyZIndex={(sel, patches) => {
|
|
209
|
+
const { entries, droppedLabels } = resolveZIndexEntries(sel, patches);
|
|
210
|
+
if (droppedLabels.length > 0) {
|
|
211
|
+
// The optimistic z-index has already been applied live at this point —
|
|
212
|
+
// these siblings just won't be written to source, so they'll revert to
|
|
213
|
+
// their prior stacking order on the next reload with no other signal.
|
|
214
|
+
console.warn(
|
|
215
|
+
"[studio] z-index reorder: dropping sibling(s) with no stable id/selector " +
|
|
216
|
+
"(will revert on reload):",
|
|
217
|
+
droppedLabels.join(", "),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (entries.length > 0) handleDomZIndexReorderCommit(entries);
|
|
221
|
+
}}
|
|
222
|
+
gridVisible={snapPrefs.gridVisible}
|
|
223
|
+
gridSpacing={snapPrefs.gridSpacing}
|
|
224
|
+
recordingState={recordingState}
|
|
225
|
+
onToggleRecording={onToggleRecording}
|
|
226
|
+
onMarqueeSelect={applyMarqueeSelection}
|
|
227
|
+
/>
|
|
228
|
+
<SnapToolbar onSnapChange={setSnapPrefs} />
|
|
229
|
+
{STUDIO_KEYFRAMES_ENABLED && (
|
|
230
|
+
<MotionPathOverlay
|
|
231
|
+
iframeRef={previewIframeRef}
|
|
232
|
+
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
233
|
+
compositionSize={compositionDimensions}
|
|
234
|
+
isPlaying={isPlaying}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
{gestureOverlay}
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
}
|