@hyperframes/studio 0.7.55 → 0.7.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.
Files changed (274) hide show
  1. package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
  2. package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
  3. package/dist/assets/index-DfmYkU44.js +423 -0
  4. package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
  5. package/dist/assets/index-_pqzyxB1.css +1 -0
  6. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  7. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  8. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  9. package/dist/index.d.ts +172 -144
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +43613 -40346
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +106 -96
  15. package/src/components/EditorShell.tsx +253 -0
  16. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  17. package/src/components/StudioLeftSidebar.tsx +13 -3
  18. package/src/components/StudioRightPanel.tsx +8 -3
  19. package/src/components/TimelineToolbar.test.tsx +2 -2
  20. package/src/components/TimelineToolbar.tsx +179 -121
  21. package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
  22. package/src/components/editor/CanvasContextMenu.tsx +33 -18
  23. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  24. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  25. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  26. package/src/components/editor/DomEditOverlay.tsx +126 -125
  27. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  28. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  29. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  30. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  31. package/src/components/editor/LayersPanel.test.ts +66 -2
  32. package/src/components/editor/LayersPanel.tsx +53 -2
  33. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  34. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  35. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  36. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  37. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  38. package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
  39. package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
  40. package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
  41. package/src/components/editor/domEditNudge.test.ts +80 -0
  42. package/src/components/editor/domEditNudge.ts +44 -0
  43. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  44. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  45. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  46. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  47. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  48. package/src/components/editor/domEditResizeLocal.ts +125 -0
  49. package/src/components/editor/domEditingDom.ts +1 -1
  50. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  51. package/src/components/editor/manualOffsetDrag.ts +55 -12
  52. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  53. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  54. package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
  55. package/src/components/editor/resizeDraft.ts +108 -0
  56. package/src/components/editor/snapEngine.test.ts +12 -78
  57. package/src/components/editor/snapEngine.ts +13 -53
  58. package/src/components/editor/useCanvasContextMenuState.ts +92 -0
  59. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  60. package/src/components/editor/useDomEditNudge.ts +254 -0
  61. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  62. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  63. package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
  64. package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
  65. package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
  66. package/src/components/nle/NLEContext.test.ts +144 -0
  67. package/src/components/nle/NLEContext.tsx +328 -0
  68. package/src/components/nle/NLEPreview.tsx +1 -1
  69. package/src/components/nle/PreviewOverlays.tsx +260 -0
  70. package/src/components/nle/PreviewPane.tsx +163 -0
  71. package/src/components/nle/TimelinePane.test.ts +60 -0
  72. package/src/components/nle/TimelinePane.tsx +293 -0
  73. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  74. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  75. package/src/components/nle/useCompositionStack.ts +17 -9
  76. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  77. package/src/components/sidebar/AssetCard.test.tsx +106 -0
  78. package/src/components/sidebar/AssetCard.tsx +343 -0
  79. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  80. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  81. package/src/components/sidebar/AssetsTab.tsx +71 -206
  82. package/src/components/sidebar/AudioRow.tsx +56 -2
  83. package/src/components/sidebar/BlocksTab.tsx +8 -1
  84. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  85. package/src/components/sidebar/assetHelpers.ts +29 -0
  86. package/src/contexts/DomEditContext.tsx +9 -0
  87. package/src/contexts/StudioContext.tsx +9 -8
  88. package/src/contexts/TimelineEditContext.tsx +3 -7
  89. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  90. package/src/hooks/domEditCommitTypes.ts +23 -1
  91. package/src/hooks/domSelectionTestHarness.ts +14 -0
  92. package/src/hooks/gestureTransaction.test.ts +311 -0
  93. package/src/hooks/gestureTransaction.ts +199 -0
  94. package/src/hooks/gsapDragCommit.test.ts +141 -41
  95. package/src/hooks/gsapDragCommit.ts +64 -74
  96. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  97. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  98. package/src/hooks/gsapResizeIntercept.ts +38 -10
  99. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  100. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  101. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  102. package/src/hooks/gsapShared.test.ts +19 -1
  103. package/src/hooks/gsapShared.ts +14 -0
  104. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  105. package/src/hooks/timelineEditingGsap.ts +14 -0
  106. package/src/hooks/timelineEditingHelpers.test.ts +125 -1
  107. package/src/hooks/timelineEditingHelpers.ts +53 -292
  108. package/src/hooks/timelineMoveAdapter.test.ts +81 -0
  109. package/src/hooks/timelineMoveAdapter.ts +39 -0
  110. package/src/hooks/timelineTimingSync.test.ts +150 -0
  111. package/src/hooks/timelineTimingSync.ts +412 -0
  112. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  113. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  114. package/src/hooks/useAppHotkeys.ts +16 -25
  115. package/src/hooks/useBlockHandlers.ts +55 -27
  116. package/src/hooks/useCaptionDetection.ts +2 -0
  117. package/src/hooks/useCompositionDimensions.ts +32 -5
  118. package/src/hooks/useContextMenuDismiss.ts +31 -7
  119. package/src/hooks/useDomEditCommits.test.tsx +245 -1
  120. package/src/hooks/useDomEditCommits.ts +162 -3
  121. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  122. package/src/hooks/useDomEditSession.ts +2 -2
  123. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  124. package/src/hooks/useDomEditTextCommits.ts +8 -3
  125. package/src/hooks/useDomEditWiring.ts +7 -9
  126. package/src/hooks/useDomGeometryCommits.ts +17 -2
  127. package/src/hooks/useDomSelection.test.ts +2 -36
  128. package/src/hooks/useDomSelection.ts +81 -51
  129. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  130. package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
  131. package/src/hooks/useElementLifecycleOps.ts +53 -71
  132. package/src/hooks/useElementPicker.ts +7 -8
  133. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  134. package/src/hooks/useEnableKeyframes.ts +73 -29
  135. package/src/hooks/useGestureCommit.test.tsx +130 -0
  136. package/src/hooks/useGestureCommit.ts +32 -8
  137. package/src/hooks/useGroupCommits.ts +7 -28
  138. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  139. package/src/hooks/useGsapAwareEditing.ts +115 -20
  140. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  141. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  142. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  143. package/src/hooks/useGsapScriptCommits.ts +134 -50
  144. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  145. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  146. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  147. package/src/hooks/usePersistentEditHistory.ts +92 -58
  148. package/src/hooks/usePreviewPersistence.ts +25 -7
  149. package/src/hooks/useRazorSplit.history.test.tsx +148 -18
  150. package/src/hooks/useRazorSplit.test.ts +269 -0
  151. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  152. package/src/hooks/useRazorSplit.ts +107 -36
  153. package/src/hooks/useRenderClipContent.ts +24 -6
  154. package/src/hooks/useStudioContextValue.ts +15 -5
  155. package/src/hooks/useStudioUrlState.ts +2 -4
  156. package/src/hooks/useTimelineAssetDropOps.ts +175 -0
  157. package/src/hooks/useTimelineEditing.test.tsx +398 -241
  158. package/src/hooks/useTimelineEditing.ts +181 -265
  159. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  160. package/src/hooks/useTimelineGroupEditing.ts +167 -132
  161. package/src/index.ts +1 -1
  162. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  163. package/src/player/components/ImageThumbnail.tsx +160 -0
  164. package/src/player/components/PlayerControls.tsx +5 -5
  165. package/src/player/components/PlayheadIndicator.tsx +55 -5
  166. package/src/player/components/Timeline.test.ts +183 -23
  167. package/src/player/components/Timeline.tsx +194 -194
  168. package/src/player/components/TimelineCanvas.tsx +218 -523
  169. package/src/player/components/TimelineClip.tsx +4 -1
  170. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  171. package/src/player/components/TimelineEmptyState.tsx +3 -1
  172. package/src/player/components/TimelineLanes.tsx +487 -0
  173. package/src/player/components/TimelineOverlays.tsx +122 -0
  174. package/src/player/components/TimelineRuler.tsx +58 -48
  175. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  176. package/src/player/components/VideoThumbnail.tsx +28 -7
  177. package/src/player/components/thumbnailUtils.test.ts +127 -0
  178. package/src/player/components/thumbnailUtils.ts +54 -0
  179. package/src/player/components/timelineCallbacks.ts +18 -18
  180. package/src/player/components/timelineClipChildren.tsx +39 -0
  181. package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
  182. package/src/player/components/timelineClipDragCommit.ts +498 -0
  183. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  184. package/src/player/components/timelineClipDragPreview.ts +323 -38
  185. package/src/player/components/timelineClipDragTypes.ts +65 -0
  186. package/src/player/components/timelineCollision.test.ts +42 -2
  187. package/src/player/components/timelineDragDrop.ts +69 -60
  188. package/src/player/components/timelineEditCapabilities.ts +52 -0
  189. package/src/player/components/timelineEditing.test.ts +1 -1
  190. package/src/player/components/timelineEditing.ts +76 -59
  191. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  192. package/src/player/components/timelineGroupEditing.ts +133 -0
  193. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  194. package/src/player/components/timelineLayout.test.ts +95 -23
  195. package/src/player/components/timelineLayout.ts +207 -75
  196. package/src/player/components/timelineMarquee.test.ts +197 -0
  197. package/src/player/components/timelineMarquee.ts +112 -0
  198. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  199. package/src/player/components/timelineRevealScroll.test.ts +133 -0
  200. package/src/player/components/timelineRevealScroll.ts +91 -0
  201. package/src/player/components/timelineStackingSync.test.ts +60 -0
  202. package/src/player/components/timelineStackingSync.ts +27 -2
  203. package/src/player/components/timelineTheme.ts +6 -1
  204. package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
  205. package/src/player/components/timelineZones.test.ts +122 -310
  206. package/src/player/components/timelineZones.ts +82 -135
  207. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  208. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  209. package/src/player/components/useTimelineClipDrag.ts +380 -386
  210. package/src/player/components/useTimelineEditPinning.ts +121 -0
  211. package/src/player/components/useTimelineGeometry.ts +129 -0
  212. package/src/player/components/useTimelinePlayhead.ts +4 -16
  213. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  214. package/src/player/components/useTimelineRevealClip.ts +56 -0
  215. package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
  216. package/src/player/components/useTimelineStackingSync.ts +94 -0
  217. package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
  218. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  219. package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
  220. package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
  221. package/src/player/hooks/useTimelinePlayer.ts +52 -53
  222. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  223. package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
  224. package/src/player/index.ts +5 -2
  225. package/src/player/lib/runtimeProtocol.test.ts +48 -0
  226. package/src/player/lib/runtimeProtocol.ts +65 -0
  227. package/src/player/lib/time.test.ts +19 -19
  228. package/src/player/lib/time.ts +6 -2
  229. package/src/player/lib/timelineDOM.test.ts +25 -98
  230. package/src/player/lib/timelineDOM.ts +21 -81
  231. package/src/player/lib/timelineElementHelpers.ts +73 -5
  232. package/src/player/lib/timelineIframeHelpers.ts +4 -4
  233. package/src/player/store/playerStore.test.ts +38 -13
  234. package/src/player/store/playerStore.ts +99 -3
  235. package/src/styles/studio.css +27 -0
  236. package/src/utils/assetPreviewDismiss.test.ts +27 -0
  237. package/src/utils/assetPreviewDismiss.ts +29 -0
  238. package/src/utils/assetPreviewStore.ts +33 -0
  239. package/src/utils/blockInstaller.ts +10 -2
  240. package/src/utils/editHistory.test.ts +35 -0
  241. package/src/utils/gsapSoftReload.test.ts +119 -1
  242. package/src/utils/gsapSoftReload.ts +153 -0
  243. package/src/utils/mediaTypes.ts +3 -2
  244. package/src/utils/resizeDebug.ts +55 -0
  245. package/src/utils/rootDuration.test.ts +90 -1
  246. package/src/utils/rootDuration.ts +76 -13
  247. package/src/utils/sdkCutover.ts +4 -0
  248. package/src/utils/studioFileHistory.ts +37 -7
  249. package/src/utils/studioHelpers.ts +62 -0
  250. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  251. package/src/utils/studioPreviewHelpers.ts +10 -0
  252. package/src/utils/studioTelemetry.ts +4 -1
  253. package/src/utils/studioUrlState.test.ts +0 -1
  254. package/src/utils/timelineAssetDrop.test.ts +144 -52
  255. package/src/utils/timelineAssetDrop.ts +67 -29
  256. package/src/utils/timelineDiscovery.ts +0 -17
  257. package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
  258. package/dist/assets/index-BRwkMj0w.js +0 -423
  259. package/dist/assets/index-Dq7FEg0K.css +0 -1
  260. package/dist/chunk-SOTCF4DF.js.map +0 -1
  261. package/src/components/StudioPreviewArea.tsx +0 -500
  262. package/src/components/nle/NLELayout.test.ts +0 -12
  263. package/src/components/nle/NLELayout.tsx +0 -591
  264. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  265. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  266. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  267. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  268. package/src/player/components/timelineSnapTargets.ts +0 -164
  269. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  270. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  271. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  272. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  273. package/src/utils/timelineDiscovery.test.ts +0 -90
  274. /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-700"
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,260 @@
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 { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
22
+ import type { BlockPreviewInfo } from "../sidebar/BlocksTab";
23
+ import type { GestureRecordingState } from "../editor/GestureRecordControl";
24
+ import type { ReactNode } from "react";
25
+
26
+ export interface PreviewOverlaysProps {
27
+ shouldShowSelectedDomBounds: boolean;
28
+ blockPreview?: BlockPreviewInfo | null;
29
+ isGestureRecording?: boolean;
30
+ recordingState?: GestureRecordingState;
31
+ onToggleRecording?: () => void;
32
+ gestureOverlay?: ReactNode;
33
+ }
34
+
35
+ type ZIndexReorderEntry = {
36
+ element: HTMLElement;
37
+ zIndex: number;
38
+ id?: string;
39
+ selector?: string;
40
+ selectorIndex?: number;
41
+ sourceFile: string;
42
+ /** Timeline store key — lets the commit update the store zIndex synchronously. */
43
+ key?: string;
44
+ };
45
+
46
+ /** Can this element be robustly re-targeted for a persisted z change? */
47
+ function canTargetZIndexElement(
48
+ element: HTMLElement,
49
+ id: string | undefined,
50
+ selector: string | undefined,
51
+ ): boolean {
52
+ return Boolean(id || selector || readHfId(element));
53
+ }
54
+
55
+ /** The selected element carries its full selection identity. */
56
+ function selectedZIndexEntry(sel: DomEditSelection, zIndex: number): ZIndexReorderEntry {
57
+ return {
58
+ element: sel.element,
59
+ zIndex,
60
+ id: sel.id ?? undefined,
61
+ selector: sel.selector,
62
+ selectorIndex: sel.selectorIndex,
63
+ sourceFile: sel.sourceFile,
64
+ key: deriveTimelineStoreKey({
65
+ domId: sel.id ?? undefined,
66
+ selector: sel.selector,
67
+ selectorIndex: sel.selectorIndex,
68
+ sourceFile: sel.sourceFile,
69
+ }),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Sibling elements are raw iframe DOM nodes with no selection object: derive a
75
+ * PatchTarget from the node itself (siblings live in the same document, so they
76
+ * share the selection's sourceFile). Null when it cannot be robustly targeted
77
+ * (no id and no selector) — its z stays live-only.
78
+ */
79
+ function siblingZIndexEntry(
80
+ element: HTMLElement,
81
+ zIndex: number,
82
+ sourceFile: string,
83
+ ): ZIndexReorderEntry | null {
84
+ const id = element.id || undefined;
85
+ const selector = buildStableSelector(element);
86
+ if (!canTargetZIndexElement(element, id, selector)) return null;
87
+ return {
88
+ element,
89
+ zIndex,
90
+ id,
91
+ selector,
92
+ selectorIndex: undefined,
93
+ sourceFile,
94
+ key: deriveTimelineStoreKey({ domId: id, selector, sourceFile }),
95
+ };
96
+ }
97
+
98
+ /** Short human-readable label for a dropped sibling, for the console warning below. */
99
+ function describeZIndexElement(element: HTMLElement): string {
100
+ if (element.id) return `#${element.id}`;
101
+ const firstClass = element.classList.item(0);
102
+ return firstClass
103
+ ? `${element.tagName.toLowerCase()}.${firstClass}`
104
+ : element.tagName.toLowerCase();
105
+ }
106
+
107
+ // Resolve z-index patches into commit entries; a sibling with no stable
108
+ // id/selector can't be written to source, so it is returned as `dropped` for
109
+ // the revert-on-reload warning (and a live-only style write, so the resolved
110
+ // stacking order still renders coherently). Exported so tests can drive the
111
+ // menu → commit path through the same wiring the app uses.
112
+ export function resolveZIndexEntries(
113
+ sel: DomEditSelection,
114
+ patches: ReadonlyArray<{ element: HTMLElement; zIndex: number }>,
115
+ ): { entries: ZIndexReorderEntry[]; dropped: Array<{ element: HTMLElement; zIndex: number }> } {
116
+ const entries: ZIndexReorderEntry[] = [];
117
+ const dropped: Array<{ element: HTMLElement; zIndex: number }> = [];
118
+ for (const patch of patches) {
119
+ if (patch.element === sel.element) {
120
+ entries.push(selectedZIndexEntry(sel, patch.zIndex));
121
+ continue;
122
+ }
123
+ const entry = siblingZIndexEntry(patch.element, patch.zIndex, sel.sourceFile);
124
+ if (entry) entries.push(entry);
125
+ else dropped.push(patch);
126
+ }
127
+ return { entries, dropped };
128
+ }
129
+
130
+ // fallow-ignore-next-line complexity
131
+ export function PreviewOverlays({
132
+ shouldShowSelectedDomBounds,
133
+ blockPreview,
134
+ isGestureRecording,
135
+ recordingState,
136
+ onToggleRecording,
137
+ gestureOverlay,
138
+ }: PreviewOverlaysProps) {
139
+ const { activeCompPath, previewIframeRef } = useStudioShellContext();
140
+ const { captionEditMode, compositionLoading, isPlaying } = useStudioPlaybackContext();
141
+ const compositionDimensions = useCompositionDimensions();
142
+
143
+ const { domEditHoverSelection, domEditSelection, domEditGroupSelections } =
144
+ useDomEditSelectionContext();
145
+ const {
146
+ handlePreviewCanvasMouseDown,
147
+ handlePreviewCanvasPointerMove,
148
+ handlePreviewCanvasPointerLeave,
149
+ applyDomSelection,
150
+ handleBlockedDomMove,
151
+ handleDomManualDragStart,
152
+ handleDomPathOffsetCommit,
153
+ handleDomGroupPathOffsetCommit,
154
+ handleDomBoxSizeCommit,
155
+ handleDomRotationCommit,
156
+ handleDomStyleCommit,
157
+ applyMarqueeSelection,
158
+ handleDomEditElementDelete,
159
+ handleDomZIndexReorderCommit,
160
+ } = useDomEditActionsContext();
161
+
162
+ // fallow-ignore-next-line complexity
163
+ const [snapPrefs, setSnapPrefs] = useState(() => {
164
+ const p = readStudioUiPreferences();
165
+ return {
166
+ snapEnabled: p.snapEnabled ?? true,
167
+ gridVisible: p.gridVisible ?? false,
168
+ gridSpacing: p.gridSpacing ?? 50,
169
+ snapToGrid: p.snapToGrid ?? false,
170
+ };
171
+ });
172
+
173
+ if (blockPreview) {
174
+ return (
175
+ <div className="absolute inset-0 z-30 bg-black pointer-events-none">
176
+ {blockPreview.videoUrl ? (
177
+ <video
178
+ src={blockPreview.videoUrl}
179
+ autoPlay
180
+ muted
181
+ loop
182
+ playsInline
183
+ className="w-full h-full object-contain"
184
+ />
185
+ ) : blockPreview.posterUrl ? (
186
+ <img
187
+ src={blockPreview.posterUrl}
188
+ alt={blockPreview.title}
189
+ className="w-full h-full object-contain"
190
+ />
191
+ ) : null}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ if (captionEditMode) {
197
+ return <CaptionOverlay iframeRef={previewIframeRef} />;
198
+ }
199
+
200
+ if (!STUDIO_INSPECTOR_PANELS_ENABLED) return null;
201
+
202
+ return (
203
+ <>
204
+ <DomEditOverlay
205
+ iframeRef={previewIframeRef}
206
+ activeCompositionPath={activeCompPath}
207
+ hoverSelection={
208
+ STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode && !compositionLoading && !isPlaying
209
+ ? domEditHoverSelection
210
+ : null
211
+ }
212
+ selection={shouldShowSelectedDomBounds ? domEditSelection : null}
213
+ groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
214
+ allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !isGestureRecording}
215
+ onCanvasMouseDown={handlePreviewCanvasMouseDown}
216
+ onCanvasPointerMove={handlePreviewCanvasPointerMove}
217
+ onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
218
+ onSelectionChange={applyDomSelection}
219
+ onBlockedMove={handleBlockedDomMove}
220
+ onManualDragStart={handleDomManualDragStart}
221
+ onPathOffsetCommit={handleDomPathOffsetCommit}
222
+ onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
223
+ onBoxSizeCommit={handleDomBoxSizeCommit}
224
+ onRotationCommit={handleDomRotationCommit}
225
+ onStyleCommit={handleDomStyleCommit}
226
+ onDeleteSelection={handleDomEditElementDelete}
227
+ onApplyZIndex={(sel, patches, action) => {
228
+ const { entries, dropped } = resolveZIndexEntries(sel, patches);
229
+ if (dropped.length > 0) {
230
+ // These siblings can't be written to source. Apply their live z
231
+ // anyway so the resolved stacking order renders coherently — it
232
+ // just reverts to the prior order on the next reload.
233
+ for (const patch of dropped) patch.element.style.zIndex = String(patch.zIndex);
234
+ console.warn(
235
+ "[studio] z-index reorder: dropping sibling(s) with no stable id/selector " +
236
+ "(will revert on reload):",
237
+ dropped.map((patch) => describeZIndexElement(patch.element)).join(", "),
238
+ );
239
+ }
240
+ if (entries.length > 0) handleDomZIndexReorderCommit(entries, undefined, action);
241
+ }}
242
+ gridVisible={snapPrefs.gridVisible}
243
+ gridSpacing={snapPrefs.gridSpacing}
244
+ recordingState={recordingState}
245
+ onToggleRecording={onToggleRecording}
246
+ onMarqueeSelect={applyMarqueeSelection}
247
+ />
248
+ <SnapToolbar onSnapChange={setSnapPrefs} />
249
+ {STUDIO_KEYFRAMES_ENABLED && (
250
+ <MotionPathOverlay
251
+ iframeRef={previewIframeRef}
252
+ selection={shouldShowSelectedDomBounds ? domEditSelection : null}
253
+ compositionSize={compositionDimensions}
254
+ isPlaying={isPlaying}
255
+ />
256
+ )}
257
+ {gestureOverlay}
258
+ </>
259
+ );
260
+ }