@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,65 @@
1
+ import {
2
+ inspectRuntimeProtocol,
3
+ runtimeProtocolMetadata,
4
+ type RuntimeProtocolInspection,
5
+ } from "@hyperframes/core/runtime/protocol";
6
+
7
+ export type RuntimeControlMessage = {
8
+ source: "hf-parent";
9
+ type: "control";
10
+ action: string;
11
+ } & ReturnType<typeof runtimeProtocolMetadata> &
12
+ Record<string, unknown>;
13
+
14
+ export function createRuntimeControlMessage(
15
+ action: string,
16
+ payload: Record<string, unknown> = {},
17
+ fps = 30,
18
+ ): RuntimeControlMessage {
19
+ return {
20
+ ...payload,
21
+ source: "hf-parent",
22
+ type: "control",
23
+ action,
24
+ ...runtimeProtocolMetadata(fps),
25
+ };
26
+ }
27
+
28
+ export function postRuntimeControlMessage(
29
+ target: Pick<Window, "postMessage"> | null | undefined,
30
+ action: string,
31
+ payload: Record<string, unknown> = {},
32
+ fps = 30,
33
+ ): void {
34
+ target?.postMessage(createRuntimeControlMessage(action, payload, fps), "*");
35
+ }
36
+
37
+ export function inspectStudioRuntimeMessage(value: unknown): RuntimeProtocolInspection {
38
+ return inspectRuntimeProtocol(value, 30);
39
+ }
40
+
41
+ function dispatchRuntimeProtocolError(inspection: RuntimeProtocolInspection): void {
42
+ if (inspection.status !== "unsupported") return;
43
+ window.dispatchEvent(
44
+ new CustomEvent("runtimeprotocolerror", {
45
+ detail: {
46
+ code: inspection.code,
47
+ receivedVersion: inspection.receivedVersion,
48
+ },
49
+ }),
50
+ );
51
+ }
52
+
53
+ export function acceptStudioRuntimeMessage(
54
+ value: unknown,
55
+ ): Exclude<RuntimeProtocolInspection, { status: "unsupported" }> | null {
56
+ const inspection = inspectStudioRuntimeMessage(value);
57
+ if (inspection.status !== "unsupported") return inspection;
58
+ dispatchRuntimeProtocolError(inspection);
59
+ return null;
60
+ }
61
+
62
+ export function acceptedRuntimeMessageFps(value: unknown): number {
63
+ const inspection = inspectStudioRuntimeMessage(value);
64
+ return inspection.status === "supported" ? inspection.fps : 30;
65
+ }
@@ -3,26 +3,26 @@ import { formatFrameTime, frameToSeconds, secondsToFrame, stepFrameTime, formatT
3
3
 
4
4
  describe("formatTime", () => {
5
5
  it("formats zero seconds", () => {
6
- expect(formatTime(0)).toBe("0:00");
6
+ expect(formatTime(0)).toBe("00:00");
7
7
  });
8
8
 
9
9
  // fallow-ignore-next-line code-duplication
10
10
  it("formats seconds less than a minute", () => {
11
- expect(formatTime(5)).toBe("0:05");
12
- expect(formatTime(30)).toBe("0:30");
13
- expect(formatTime(59)).toBe("0:59");
11
+ expect(formatTime(5)).toBe("00:05");
12
+ expect(formatTime(30)).toBe("00:30");
13
+ expect(formatTime(59)).toBe("00:59");
14
14
  });
15
15
 
16
16
  it("formats exact minutes", () => {
17
- expect(formatTime(60)).toBe("1:00");
18
- expect(formatTime(120)).toBe("2:00");
17
+ expect(formatTime(60)).toBe("01:00");
18
+ expect(formatTime(120)).toBe("02:00");
19
19
  expect(formatTime(600)).toBe("10:00");
20
20
  });
21
21
 
22
22
  it("formats minutes and seconds", () => {
23
- expect(formatTime(65)).toBe("1:05");
24
- expect(formatTime(90)).toBe("1:30");
25
- expect(formatTime(125)).toBe("2:05");
23
+ expect(formatTime(65)).toBe("01:05");
24
+ expect(formatTime(90)).toBe("01:30");
25
+ expect(formatTime(125)).toBe("02:05");
26
26
  });
27
27
 
28
28
  it("formats large values (over an hour)", () => {
@@ -32,28 +32,28 @@ describe("formatTime", () => {
32
32
  });
33
33
 
34
34
  it("floors fractional seconds", () => {
35
- expect(formatTime(0.9)).toBe("0:00");
36
- expect(formatTime(1.5)).toBe("0:01");
37
- expect(formatTime(59.99)).toBe("0:59");
38
- expect(formatTime(60.5)).toBe("1:00");
35
+ expect(formatTime(0.9)).toBe("00:00");
36
+ expect(formatTime(1.5)).toBe("00:01");
37
+ expect(formatTime(59.99)).toBe("00:59");
38
+ expect(formatTime(60.5)).toBe("01:00");
39
39
  });
40
40
 
41
- it("pads single-digit seconds with leading zero", () => {
42
- expect(formatTime(1)).toBe("0:01");
43
- expect(formatTime(61)).toBe("1:01");
41
+ it("pads single-digit minutes and seconds with leading zeros", () => {
42
+ expect(formatTime(1)).toBe("00:01");
43
+ expect(formatTime(61)).toBe("01:01");
44
44
  expect(formatTime(609)).toBe("10:09");
45
45
  });
46
46
 
47
47
  it("guards against negative values", () => {
48
- expect(formatTime(-1)).toBe("0:00");
48
+ expect(formatTime(-1)).toBe("00:00");
49
49
  });
50
50
 
51
51
  it("guards against NaN", () => {
52
- expect(formatTime(NaN)).toBe("0:00");
52
+ expect(formatTime(NaN)).toBe("00:00");
53
53
  });
54
54
 
55
55
  it("guards against Infinity", () => {
56
- expect(formatTime(Infinity)).toBe("0:00");
56
+ expect(formatTime(Infinity)).toBe("00:00");
57
57
  });
58
58
  });
59
59
 
@@ -1,10 +1,14 @@
1
1
  export const STUDIO_PREVIEW_FPS = 30;
2
2
 
3
3
  export function formatTime(time: number): string {
4
- if (!Number.isFinite(time) || time < 0) return "0:00";
4
+ if (!Number.isFinite(time) || time < 0) return "00:00";
5
5
  const mins = Math.floor(time / 60);
6
6
  const secs = Math.floor(time % 60);
7
- return `${mins}:${secs.toString().padStart(2, "0")}`;
7
+ // Zero-pad minutes as well as seconds so every readout is a stable-width
8
+ // MM:SS (e.g. "00:44", not "0:44"). Minutes past 99 keep their extra digits
9
+ // ("120:00"); an hours-style H:MM:SS grouping is applied by callers that need
10
+ // it (see formatTimelineTickLabel), never here.
11
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
8
12
  }
9
13
 
10
14
  export function secondsToFrame(time: number, fps = STUDIO_PREVIEW_FPS): number {
@@ -4,8 +4,13 @@ import {
4
4
  createTimelineElementFromManifestClip,
5
5
  parseTimelineFromDOM,
6
6
  createImplicitTimelineLayersFromDOM,
7
- buildStandaloneRootTimelineElement,
7
+ mergeTimelineElementsPreservingDowngrades,
8
8
  } from "./timelineDOM";
9
+ import type { TimelineElement } from "../store/playerStore";
10
+
11
+ function el(id: string, extra: Partial<TimelineElement> = {}): TimelineElement {
12
+ return { id, tag: "img", start: 0, duration: 5, track: 0, ...extra };
13
+ }
9
14
 
10
15
  function makeDoc(html: string): Document {
11
16
  const d = document.implementation.createHTMLDocument();
@@ -13,29 +18,6 @@ function makeDoc(html: string): Document {
13
18
  return d;
14
19
  }
15
20
 
16
- function makeLiveDoc(html: string): Document {
17
- document.head.innerHTML = "";
18
- document.body.innerHTML = html;
19
- return document;
20
- }
21
-
22
- function mockComputedZIndex(doc: Document, zIndexById: ReadonlyMap<string, string>): void {
23
- const win = doc.defaultView;
24
- if (!win) throw new Error("Expected document window");
25
- const original = win.getComputedStyle.bind(win);
26
- Object.defineProperty(win, "getComputedStyle", {
27
- configurable: true,
28
- value: (element: Element, pseudoElt?: string | null) => {
29
- const style = original(element, pseudoElt);
30
- const zIndex = zIndexById.get(element.id);
31
- if (zIndex != null) {
32
- Object.defineProperty(style, "zIndex", { configurable: true, value: zIndex });
33
- }
34
- return style;
35
- },
36
- });
37
- }
38
-
39
21
  describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
40
22
  it("harvests hfId from a data-start element that has data-hf-id", () => {
41
23
  const doc = makeDoc(`
@@ -126,58 +108,6 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
126
108
 
127
109
  expect(element.hidden).toBe(true);
128
110
  });
129
-
130
- it("captures the effective z-index from the live element, not the runtime inline-only value", () => {
131
- // The runtime reports inline-only z-index (0 for CSS-rule authored z-index),
132
- // which must NOT override the live element's effective z-index — otherwise
133
- // the timeline collapses every CSS-styled clip to a z=0 tie and mis-orders.
134
- const doc = makeDoc(`
135
- <div data-composition-id="root">
136
- <div id="hero" class="clip" data-start="0" data-duration="5" style="z-index: 30"></div>
137
- </div>
138
- `);
139
- const hostEl = doc.getElementById("hero");
140
-
141
- const element = createTimelineElementFromManifestClip({
142
- clip: {
143
- id: "hero",
144
- label: "Hero",
145
- kind: "element",
146
- tagName: "div",
147
- start: 0,
148
- duration: 5,
149
- track: 0,
150
- zIndex: 0,
151
- compositionId: null,
152
- parentCompositionId: null,
153
- compositionSrc: null,
154
- assetUrl: null,
155
- },
156
- fallbackIndex: 0,
157
- doc,
158
- hostEl,
159
- });
160
-
161
- expect(element.zIndex).toBe(30);
162
- expect(element.hasExplicitZIndex).toBe(true);
163
- });
164
-
165
- it("marks parsed inline, CSS-rule, and auto z-index authorship accurately", () => {
166
- const doc = makeLiveDoc(`
167
- <div data-composition-id="root">
168
- <div id="inline" class="clip" data-start="0" data-duration="2" style="z-index: 3"></div>
169
- <div id="rule" class="clip" data-start="0" data-duration="2"></div>
170
- <div id="auto" class="clip" data-start="0" data-duration="2"></div>
171
- </div>
172
- `);
173
- mockComputedZIndex(doc, new Map([["rule", "12"]]));
174
-
175
- const elements = parseTimelineFromDOM(doc, 10);
176
-
177
- expect(elements.find((el) => el.id === "inline")?.hasExplicitZIndex).toBe(true);
178
- expect(elements.find((el) => el.id === "rule")?.hasExplicitZIndex).toBe(true);
179
- expect(elements.find((el) => el.id === "auto")?.hasExplicitZIndex).toBe(false);
180
- });
181
111
  });
182
112
 
183
113
  describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
@@ -211,31 +141,28 @@ describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
211
141
 
212
142
  expect(layers).toEqual([]);
213
143
  });
144
+ });
214
145
 
215
- it("marks implicit layer CSS z-index authorship from computed style", () => {
216
- const doc = makeLiveDoc(`
217
- <div data-composition-id="root">
218
- <div id="layer" class="clip"></div>
219
- </div>
220
- `);
221
- mockComputedZIndex(doc, new Map([["layer", "8"]]));
222
-
223
- const layers = createImplicitTimelineLayersFromDOM(doc, 10);
224
-
225
- expect(layers[0]?.zIndex).toBe(8);
226
- expect(layers[0]?.hasExplicitZIndex).toBe(true);
146
+ describe("mergeTimelineElementsPreservingDowngrades genuine removal vs transient downgrade", () => {
147
+ it("drops a removed TOP-LEVEL element (undo of a split) instead of ghosting it", () => {
148
+ const current = [el("a"), el("a-split")]; // post-split store: original + clone
149
+ const next = [el("a")]; // fresh scan of the reverted file: clone gone
150
+ const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30);
151
+ expect(merged.map((e) => e.id)).toEqual(["a"]);
227
152
  });
228
- });
229
153
 
230
- describe("buildStandaloneRootTimelineElement", () => {
231
- it("marks the standalone root as auto z-index", () => {
232
- const root = buildStandaloneRootTimelineElement({
233
- compositionId: "root",
234
- tagName: "div",
235
- rootDuration: 10,
236
- iframeSrc: "/preview/comp/index.html",
237
- });
154
+ it("still preserves an enriched sub-composition child a bare re-scan drops", () => {
155
+ const current = [el("a"), el("sub-child", { compositionSrc: "sub.html" })];
156
+ const next = [el("a")]; // bare DOM scan misses the enriched sub-comp child
157
+ const merged = mergeTimelineElementsPreservingDowngrades(current, next, 30, 30);
158
+ expect(merged.map((e) => e.id).sort()).toEqual(["a", "sub-child"]);
159
+ });
238
160
 
239
- expect(root?.hasExplicitZIndex).toBe(false);
161
+ it("trusts the fresh scan fully when it is not shorter", () => {
162
+ const current = [el("a"), el("b", { compositionSrc: "sub.html" })];
163
+ const next = [el("a"), el("c")];
164
+ expect(
165
+ mergeTimelineElementsPreservingDowngrades(current, next, 30, 30).map((e) => e.id),
166
+ ).toEqual(["a", "c"]);
240
167
  });
241
168
  });
@@ -10,7 +10,6 @@
10
10
 
11
11
  import type { TimelineElement } from "../store/playerStore";
12
12
  import type { ClipManifestClip } from "./playbackTypes";
13
- import { getElementZIndex, hasExplicitZIndex } from "./layerOrdering";
14
13
  import {
15
14
  resolveMediaElement,
16
15
  applyMediaMetadataFromElement,
@@ -24,6 +23,7 @@ import {
24
23
  buildTimelineElementIdentity,
25
24
  getTimelineElementIdentity,
26
25
  isTimelineIgnoredElement,
26
+ readTimelineElementZIndex,
27
27
  } from "./timelineElementHelpers";
28
28
 
29
29
  // Re-export helpers that were previously public from this module so that
@@ -66,49 +66,6 @@ function resolveClipTag(clip: ClipManifestClip): string {
66
66
  return clip.tagName || clip.kind || "div";
67
67
  }
68
68
 
69
- function resolveDomCompositionContext(
70
- element: Element,
71
- root: Element | null,
72
- ): {
73
- parentCompositionId: string | null;
74
- compositionAncestors: string[];
75
- stackingContextId: string | null;
76
- } {
77
- const ancestors: string[] = [];
78
- let parentCompositionId: string | null = null;
79
- let cursor = element.parentElement;
80
- while (cursor) {
81
- const compositionId = cursor.getAttribute("data-composition-id");
82
- if (compositionId) {
83
- ancestors.push(compositionId);
84
- if (!parentCompositionId && cursor !== root) {
85
- parentCompositionId = compositionId;
86
- }
87
- }
88
- cursor = cursor.parentElement;
89
- }
90
- const compositionAncestors = ancestors.reverse();
91
- return {
92
- parentCompositionId,
93
- compositionAncestors,
94
- stackingContextId: parentCompositionId ?? compositionAncestors[0] ?? null,
95
- };
96
- }
97
-
98
- function isHTMLElement(element: Element | null): element is HTMLElement {
99
- if (!element) return false;
100
- const HtmlElementCtor = element.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;
101
- return typeof HtmlElementCtor !== "undefined" && element instanceof HtmlElementCtor;
102
- }
103
-
104
- function getTimelineElementZIndex(element: Element | null): number | undefined {
105
- return isHTMLElement(element) ? getElementZIndex(element) : undefined;
106
- }
107
-
108
- function getTimelineElementHasExplicitZIndex(element: Element | null): boolean {
109
- return isHTMLElement(element) ? hasExplicitZIndex(element) : false;
110
- }
111
-
112
69
  // fallow-ignore-next-line complexity
113
70
  export function createTimelineElementFromManifestClip(params: {
114
71
  clip: ClipManifestClip;
@@ -130,14 +87,6 @@ export function createTimelineElementFromManifestClip(params: {
130
87
  let sourceFile: string | undefined;
131
88
 
132
89
  let hfId: string | undefined;
133
- const domContext = hostEl
134
- ? resolveDomCompositionContext(hostEl, doc?.querySelector("[data-composition-id]") ?? null)
135
- : null;
136
- const compositionAncestors = clip.compositionAncestors ?? domContext?.compositionAncestors;
137
- const parentCompositionId = clip.parentCompositionId ?? domContext?.parentCompositionId;
138
- const stackingContextId =
139
- clip.stackingContextId ?? parentCompositionId ?? compositionAncestors?.[0] ?? null;
140
-
141
90
  if (hostEl) {
142
91
  domId = hostEl.id || undefined;
143
92
  hfId = hostEl.getAttribute("data-hf-id") || undefined;
@@ -164,15 +113,15 @@ export function createTimelineElementFromManifestClip(params: {
164
113
  start: clip.start,
165
114
  duration: clip.duration,
166
115
  track: clip.track,
167
- // Prefer the effective (computed) z-index read from the live element — the
168
- // same read the reorder commit uses so CSS-rule z-index (not just inline)
169
- // is captured. clip.zIndex from the runtime is inline-only (0 for CSS rules),
170
- // so it can only serve as a fallback when the element isn't live.
171
- zIndex: getTimelineElementZIndex(hostEl) ?? clip.zIndex ?? 0,
172
- hasExplicitZIndex: getTimelineElementHasExplicitZIndex(hostEl),
173
- stackingContextId,
174
- parentCompositionId,
175
- compositionAncestors,
116
+ // clip.track IS the authored data-track-index verbatim (the runtime honors
117
+ // it; see parseAuthoredTrack in core/runtime/timeline.ts). Record it at this
118
+ // translation boundary so later display-lane remaps (normalizeToZones,
119
+ // expanded-child rows) can persist in AUTHORED space instead of
120
+ // reconstructing it from lane occupants.
121
+ authoredTrack: clip.track,
122
+ // Runtime-computed stacking context — authoritative; helpers read it, never
123
+ // re-derive it.
124
+ stackingContextId: clip.stackingContextId ?? null,
176
125
  domId,
177
126
  hfId,
178
127
  selector,
@@ -185,6 +134,7 @@ export function createTimelineElementFromManifestClip(params: {
185
134
  if (hostEl.hasAttribute("data-hidden")) entry.hidden = true;
186
135
  const timelineRole = hostEl.getAttribute("data-timeline-role");
187
136
  if (timelineRole) entry.timelineRole = timelineRole;
137
+ entry.zIndex = readTimelineElementZIndex(hostEl);
188
138
  }
189
139
  if (clip.assetUrl) entry.src = clip.assetUrl;
190
140
  if (clip.kind === "composition" && clip.compositionId) {
@@ -207,8 +157,6 @@ export function createTimelineElementFromManifestClip(params: {
207
157
  }
208
158
  }
209
159
  if (hostEl) {
210
- entry.zIndex = getTimelineElementZIndex(hostEl) ?? entry.zIndex;
211
- entry.hasExplicitZIndex = getTimelineElementHasExplicitZIndex(hostEl);
212
160
  entry.domId = hostEl.id || undefined;
213
161
  entry.hfId = hostEl.getAttribute("data-hf-id") || undefined;
214
162
  entry.selector = getTimelineElementSelector(hostEl);
@@ -269,10 +217,10 @@ export function createImplicitTimelineLayersFromDOM(
269
217
  });
270
218
  if (existingKeys.has(identity.key) || existingKeys.has(identity.id)) continue;
271
219
 
272
- const compositionContext = resolveDomCompositionContext(child, rootComp);
273
220
  layers.push({
274
221
  domId: child.id || undefined,
275
222
  hfId: child.getAttribute("data-hf-id") || undefined,
223
+ zIndex: readTimelineElementZIndex(child),
276
224
  duration: rootDuration,
277
225
  id: identity.id,
278
226
  key: identity.key,
@@ -281,11 +229,6 @@ export function createImplicitTimelineLayersFromDOM(
281
229
  selectorIndex,
282
230
  sourceFile,
283
231
  start: 0,
284
- zIndex: getTimelineElementZIndex(child),
285
- hasExplicitZIndex: getTimelineElementHasExplicitZIndex(child),
286
- stackingContextId: compositionContext.stackingContextId,
287
- parentCompositionId: compositionContext.parentCompositionId,
288
- compositionAncestors: compositionContext.compositionAncestors,
289
232
  tag: child.tagName.toLowerCase(),
290
233
  timingSource: "implicit",
291
234
  track: maxTrack + 1 + layers.length,
@@ -347,7 +290,6 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
347
290
  selectorIndex,
348
291
  sourceFile,
349
292
  });
350
- const compositionContext = resolveDomCompositionContext(el, rootComp);
351
293
  const entry: TimelineElement = {
352
294
  id: identity.id,
353
295
  label,
@@ -356,17 +298,13 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
356
298
  start,
357
299
  duration: dur,
358
300
  track: isNaN(track) ? 0 : track,
359
- zIndex: getTimelineElementZIndex(el),
360
- hasExplicitZIndex: getTimelineElementHasExplicitZIndex(el),
361
- stackingContextId: compositionContext.stackingContextId,
362
- parentCompositionId: compositionContext.parentCompositionId,
363
- compositionAncestors: compositionContext.compositionAncestors,
364
301
  domId: el.id || undefined,
365
302
  hfId: el.getAttribute("data-hf-id") || undefined,
366
303
  selector,
367
304
  selectorIndex,
368
305
  sourceFile,
369
306
  timingSource: "authored",
307
+ zIndex: readTimelineElementZIndex(el),
370
308
  };
371
309
 
372
310
  const mediaEl = resolveMediaElement(el);
@@ -437,7 +375,14 @@ export function mergeTimelineElementsPreservingDowngrades(
437
375
 
438
376
  const nextIdentities = new Set(nextElements.map(getTimelineElementIdentity));
439
377
  const preserved = currentElements.filter(
440
- (element) => !nextIdentities.has(getTimelineElementIdentity(element)),
378
+ (element) =>
379
+ !nextIdentities.has(getTimelineElementIdentity(element)) &&
380
+ // Only preserve enriched sub-composition children (compositionSrc set),
381
+ // which a bare DOM re-scan legitimately drops and enrichMissingCompositions
382
+ // re-adds. A TOP-LEVEL element missing from the fresh scan was genuinely
383
+ // removed (undo of a split, a delete), so let it go — otherwise undoing a
384
+ // split leaves a ghost clip in the timeline even though the file is reverted.
385
+ element.compositionSrc != null,
441
386
  );
442
387
  if (preserved.length === 0) return nextElements;
443
388
  return [...nextElements, ...preserved];
@@ -481,11 +426,6 @@ export function buildStandaloneRootTimelineElement(params: {
481
426
  start: 0,
482
427
  duration: params.rootDuration,
483
428
  track: 0,
484
- zIndex: 0,
485
- hasExplicitZIndex: false,
486
- stackingContextId: params.compositionId,
487
- parentCompositionId: null,
488
- compositionAncestors: [params.compositionId],
489
429
  compositionSrc,
490
430
  selector: params.selector,
491
431
  selectorIndex: params.selectorIndex,
@@ -15,6 +15,31 @@ import { isFinitePositive } from "./playbackAdapter";
15
15
  // Duration attribute helpers
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
+ /**
19
+ * Read a host element's effective CSS stacking order for the timeline's reverse
20
+ * z→lane mapping. Prefers the inline `style.zIndex` (what the canvas context
21
+ * menu and LayersPanel z-edits write via handleDomZIndexReorderCommit), falls
22
+ * back to computed style; "auto" / empty / unparseable ⇒ 0. Works with a
23
+ * detached parse Document (no defaultView) as well as a live iframe. Mirrors
24
+ * canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.
25
+ */
26
+ export function readTimelineElementZIndex(el: Element): number {
27
+ const html = el as HTMLElement;
28
+ const parseZ = (value: string | null | undefined): number | null => {
29
+ if (value == null || value === "" || value === "auto") return null;
30
+ const n = Number.parseInt(value, 10);
31
+ return Number.isFinite(n) ? n : null;
32
+ };
33
+ const fromInline = parseZ(html.style?.zIndex);
34
+ if (fromInline != null) return fromInline;
35
+ const view = el.ownerDocument?.defaultView;
36
+ if (view?.getComputedStyle) {
37
+ const fromComputed = parseZ(view.getComputedStyle(html).zIndex);
38
+ if (fromComputed != null) return fromComputed;
39
+ }
40
+ return 0;
41
+ }
42
+
18
43
  function readDurationAttribute(el: Element | null | undefined): number {
19
44
  if (!el) return 0;
20
45
  const duration =
@@ -36,14 +61,20 @@ export function isTimelineIgnoredElement(el: Element): boolean {
36
61
  );
37
62
  }
38
63
 
39
- export function readTimelineDurationFromDocument(doc: Document | null | undefined): number {
64
+ /**
65
+ * Furthest clip end (start + RAW `data-duration`) over every non-root clip in the
66
+ * document. Reads the authored attribute, NOT any runtime-computed value — so it
67
+ * is immune to the runtime's clamp that truncates a clip's live duration to the
68
+ * composition length. This is the source of truth for content-driven duration:
69
+ * computing it from the store instead would feed the truncated value back in and
70
+ * make the composition length ratchet down (research HANDOFF-3 §6.1 feedback loop).
71
+ */
72
+ export function furthestClipEndFromDocument(doc: Document | null | undefined): number {
40
73
  if (!doc) return 0;
41
- const rootDuration = readDurationAttribute(doc.querySelector("[data-composition-id]"));
42
- if (rootDuration > 0) return rootDuration;
43
-
74
+ const root = doc.querySelector("[data-composition-id]");
44
75
  let maxEnd = 0;
45
76
  for (const node of Array.from(doc.querySelectorAll("[data-start]"))) {
46
- if (isTimelineIgnoredElement(node)) continue;
77
+ if (node === root || isTimelineIgnoredElement(node)) continue;
47
78
  const start = Number.parseFloat(node.getAttribute("data-start") ?? "");
48
79
  const duration = readDurationAttribute(node);
49
80
  if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;
@@ -52,6 +83,24 @@ export function readTimelineDurationFromDocument(doc: Document | null | undefine
52
83
  return maxEnd;
53
84
  }
54
85
 
86
+ export function readTimelineDurationFromDocument(doc: Document | null | undefined): number {
87
+ if (!doc) return 0;
88
+ const rootDuration = readDurationAttribute(doc.querySelector("[data-composition-id]"));
89
+ if (rootDuration > 0) return rootDuration;
90
+ return furthestClipEndFromDocument(doc);
91
+ }
92
+
93
+ /**
94
+ * Furthest clip end parsed straight from a composition SOURCE STRING (the HTML
95
+ * being saved). Uses raw `data-duration`, so it is the correct input for syncing
96
+ * the root duration after an edit — reading the store instead would use the
97
+ * runtime-truncated durations and shrink the composition (the feedback loop).
98
+ */
99
+ export function furthestClipEndFromSource(source: string): number {
100
+ if (!source) return 0;
101
+ return furthestClipEndFromDocument(new DOMParser().parseFromString(source, "text/html"));
102
+ }
103
+
55
104
  // ---------------------------------------------------------------------------
56
105
  // DOM element type guards
57
106
  // ---------------------------------------------------------------------------
@@ -249,6 +298,25 @@ export function getTimelineElementIdentity(element: { key?: string | null; id: s
249
298
  return element.key ?? element.id;
250
299
  }
251
300
 
301
+ /**
302
+ * Timeline store key for a z-reorder entry built OUTSIDE the timeline
303
+ * expansion (canvas context menu / LayersPanel), so the reorder commit can
304
+ * update the store's zIndex synchronously. Matches buildTimelineElementKey's
305
+ * stable branches (`sourceFile#domId`, else the selector-based key). Undefined
306
+ * when the element has neither a DOM id nor a selector — the timeline's
307
+ * fallback branch needs its own fallbackIndex, which these callers don't have,
308
+ * so such an entry simply skips the synchronous store update.
309
+ */
310
+ export function deriveTimelineStoreKey(params: {
311
+ domId?: string;
312
+ selector?: string;
313
+ selectorIndex?: number;
314
+ sourceFile?: string;
315
+ }): string | undefined {
316
+ if (!params.domId && !params.selector) return undefined;
317
+ return buildTimelineElementKey({ id: "", fallbackIndex: 0, ...params });
318
+ }
319
+
252
320
  // ---------------------------------------------------------------------------
253
321
  // DOM node querying
254
322
  // ---------------------------------------------------------------------------
@@ -18,7 +18,9 @@ import {
18
18
  getTimelineElementSelectorIndex,
19
19
  getTimelineElementDisplayLabel,
20
20
  buildTimelineElementIdentity,
21
+ readTimelineElementZIndex,
21
22
  } from "./timelineElementHelpers";
23
+ import { postRuntimeControlMessage } from "./runtimeProtocol";
22
24
 
23
25
  // ---------------------------------------------------------------------------
24
26
  // Viewport / DOM normalisation
@@ -102,10 +104,7 @@ function postPreviewControl(
102
104
  action: string,
103
105
  payload: Record<string, unknown>,
104
106
  ): void {
105
- iframe.contentWindow?.postMessage(
106
- { source: "hf-parent", type: "control", action, ...payload },
107
- "*",
108
- );
107
+ postRuntimeControlMessage(iframe.contentWindow, action, payload);
109
108
  }
110
109
 
111
110
  export function shouldMutePreviewAudio(audioMuted: boolean, playbackRate: number): boolean {
@@ -377,6 +376,7 @@ export function buildMissingCompositionElements(
377
376
  selector,
378
377
  selectorIndex,
379
378
  sourceFile,
379
+ zIndex: readTimelineElementZIndex(el),
380
380
  };
381
381
  if (compSrc) {
382
382
  entry.compositionSrc = compSrc;