@hyperframes/studio 0.7.57 → 0.7.59

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 (247) hide show
  1. package/dist/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
  2. package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
  3. package/dist/assets/index-CrkAdJkb.js +426 -0
  4. package/dist/assets/index-Dj5p8U_A.css +1 -0
  5. package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
  6. package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
  7. package/dist/chunk-OBAG3GWK.js.map +1 -0
  8. package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
  9. package/dist/index.d.ts +27 -7
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +18029 -11563
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles/tailwind-preset.d.ts +5 -0
  14. package/dist/styles/tailwind-preset.js +8 -1
  15. package/dist/styles/tailwind-preset.js.map +1 -1
  16. package/package.json +7 -7
  17. package/src/App.tsx +6 -6
  18. package/src/components/DesignPanelPromoteProvider.tsx +15 -2
  19. package/src/components/StudioFeedbackBar.tsx +2 -2
  20. package/src/components/StudioRightPanel.tsx +29 -51
  21. package/src/components/editor/AnimationCard.test.tsx +134 -0
  22. package/src/components/editor/AnimationCard.tsx +19 -4
  23. package/src/components/editor/ArcPathControls.tsx +1 -0
  24. package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
  25. package/src/components/editor/CanvasContextMenu.tsx +71 -6
  26. package/src/components/editor/DomEditOverlay.tsx +5 -2
  27. package/src/components/editor/GestureRecordControl.tsx +7 -1
  28. package/src/components/editor/GsapAnimationSection.tsx +109 -14
  29. package/src/components/editor/InspectorHeaderActions.tsx +42 -4
  30. package/src/components/editor/LayersPanel.tsx +110 -99
  31. package/src/components/editor/PropertyPanel.test.tsx +948 -0
  32. package/src/components/editor/PropertyPanel.tsx +144 -148
  33. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  34. package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
  35. package/src/components/editor/PropertyPanelFlat.tsx +580 -0
  36. package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
  37. package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
  38. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
  39. package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
  40. package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
  41. package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
  42. package/src/components/editor/domEditingDom.ts +5 -6
  43. package/src/components/editor/domEditingElement.ts +15 -32
  44. package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
  45. package/src/components/editor/gsapLivePreview.ts +34 -0
  46. package/src/components/editor/layersPanelSort.ts +80 -0
  47. package/src/components/editor/manualEditingAvailability.test.ts +10 -0
  48. package/src/components/editor/manualEditingAvailability.ts +9 -0
  49. package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
  50. package/src/components/editor/propertyPanelColor.test.tsx +28 -0
  51. package/src/components/editor/propertyPanelColor.tsx +31 -1
  52. package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
  53. package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
  54. package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
  55. package/src/components/editor/propertyPanelFill.tsx +22 -5
  56. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
  57. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
  58. package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
  59. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
  60. package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
  61. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
  62. package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
  63. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
  64. package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
  65. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
  66. package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
  67. package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
  68. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
  69. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
  70. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
  71. package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
  72. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
  73. package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
  74. package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
  75. package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
  76. package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
  77. package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
  78. package/src/components/editor/propertyPanelFont.test.tsx +30 -0
  79. package/src/components/editor/propertyPanelFont.tsx +136 -95
  80. package/src/components/editor/propertyPanelHelpers.ts +73 -0
  81. package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
  82. package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
  83. package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
  84. package/src/components/editor/propertyPanelSections.test.tsx +161 -0
  85. package/src/components/editor/propertyPanelSections.tsx +154 -90
  86. package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
  87. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  88. package/src/components/editor/propertyPanelTypes.ts +14 -1
  89. package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
  90. package/src/components/editor/propertyPanelValueTier.ts +28 -0
  91. package/src/components/editor/useColorGradingController.test.ts +418 -0
  92. package/src/components/editor/useColorGradingController.ts +596 -0
  93. package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
  94. package/src/components/editor/useLayerRevealOverride.ts +389 -0
  95. package/src/components/nle/PreviewOverlays.tsx +27 -2
  96. package/src/components/nle/TimelinePane.test.ts +3 -0
  97. package/src/components/nle/TimelinePane.tsx +11 -1
  98. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
  99. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
  100. package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
  101. package/src/components/nle/zLaneGesture.test.ts +176 -0
  102. package/src/components/nle/zLaneGesture.ts +77 -0
  103. package/src/components/panels/VariablesPanel.tsx +4 -0
  104. package/src/components/sidebar/CompositionsTab.test.ts +28 -2
  105. package/src/components/sidebar/CompositionsTab.tsx +3 -1
  106. package/src/components/storyboard/FramePoster.tsx +24 -4
  107. package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
  108. package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
  109. package/src/components/storyboard/StoryboardGrid.tsx +23 -1
  110. package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
  111. package/src/components/storyboard/StoryboardView.tsx +9 -2
  112. package/src/components/storyboard/frameComments.test.ts +95 -0
  113. package/src/components/storyboard/frameComments.ts +115 -0
  114. package/src/components/storyboard/useFrameComments.ts +82 -0
  115. package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
  116. package/src/contexts/DesignPanelInputContext.tsx +48 -0
  117. package/src/contexts/DomEditContext.tsx +4 -0
  118. package/src/contexts/VariablePromoteContext.test.tsx +67 -0
  119. package/src/contexts/VariablePromoteContext.tsx +10 -5
  120. package/src/hooks/domEditCommitRunner.ts +11 -0
  121. package/src/hooks/domEditCommitTypes.ts +17 -1
  122. package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
  123. package/src/hooks/fetchStubTestUtils.ts +16 -0
  124. package/src/hooks/gsapScriptCommitTypes.ts +6 -4
  125. package/src/hooks/timelineEditingHelpers.test.ts +92 -1
  126. package/src/hooks/timelineEditingHelpers.ts +32 -15
  127. package/src/hooks/timelineMoveAdapter.ts +2 -1
  128. package/src/hooks/timelineTimingSync.test.ts +703 -33
  129. package/src/hooks/timelineTimingSync.ts +282 -97
  130. package/src/hooks/timelineTrackVisibility.test.ts +55 -1
  131. package/src/hooks/timelineTrackVisibility.ts +31 -13
  132. package/src/hooks/useAppHotkeys.ts +9 -0
  133. package/src/hooks/useClipboard.ts +19 -17
  134. package/src/hooks/useDomEditAttributeCommits.ts +123 -1
  135. package/src/hooks/useDomEditCommits.test.tsx +316 -38
  136. package/src/hooks/useDomEditCommits.ts +81 -162
  137. package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
  138. package/src/hooks/useDomEditSession.test.tsx +3 -1
  139. package/src/hooks/useDomEditSession.ts +18 -6
  140. package/src/hooks/useDomEditTextCommits.ts +14 -9
  141. package/src/hooks/useDomEditWiring.ts +10 -9
  142. package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
  143. package/src/hooks/useElementLifecycleOps.ts +182 -84
  144. package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
  145. package/src/hooks/useFileManager.ts +82 -60
  146. package/src/hooks/useGsapAnimationOps.ts +7 -6
  147. package/src/hooks/useGsapKeyframeOps.ts +6 -5
  148. package/src/hooks/useGsapPropertyDebounce.ts +43 -28
  149. package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
  150. package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
  151. package/src/hooks/useGsapScriptCommits.ts +150 -50
  152. package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
  153. package/src/hooks/useGsapSelectionHandlers.ts +94 -31
  154. package/src/hooks/useInspectorSplitResize.ts +51 -0
  155. package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
  156. package/src/hooks/usePersistentEditHistory.test.ts +49 -1
  157. package/src/hooks/usePersistentEditHistory.ts +71 -31
  158. package/src/hooks/usePreviewPersistence.ts +2 -2
  159. package/src/hooks/useProjectCompositionVariables.ts +10 -10
  160. package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
  161. package/src/hooks/useProjectSignaturePoll.ts +68 -0
  162. package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
  163. package/src/hooks/useSdkSession.ts +155 -14
  164. package/src/hooks/useSlideshowPersist.ts +7 -1
  165. package/src/hooks/useStoryboard.ts +23 -4
  166. package/src/hooks/useTimelineEditing.test.tsx +306 -155
  167. package/src/hooks/useTimelineEditing.ts +111 -101
  168. package/src/hooks/useTimelineEditingTypes.ts +4 -1
  169. package/src/hooks/useTimelineGroupEditing.ts +51 -5
  170. package/src/hooks/useVariablesPersist.ts +9 -7
  171. package/src/icons/SystemIcons.tsx +2 -0
  172. package/src/player/components/Timeline.test.ts +31 -21
  173. package/src/player/components/Timeline.tsx +62 -91
  174. package/src/player/components/TimelineCanvas.tsx +36 -5
  175. package/src/player/components/TimelineLanes.tsx +32 -10
  176. package/src/player/components/TimelineOverlays.tsx +36 -0
  177. package/src/player/components/TimelineRuler.tsx +18 -5
  178. package/src/player/components/TrackGapContextMenu.tsx +116 -0
  179. package/src/player/components/timelineCallbacks.ts +4 -1
  180. package/src/player/components/timelineClipDragCommit.test.ts +279 -3
  181. package/src/player/components/timelineClipDragCommit.ts +173 -71
  182. package/src/player/components/timelineGapCommit.test.ts +163 -0
  183. package/src/player/components/timelineGapCommit.ts +103 -0
  184. package/src/player/components/timelineGaps.test.ts +221 -0
  185. package/src/player/components/timelineGaps.ts +182 -0
  186. package/src/player/components/timelineLayout.test.ts +2 -1
  187. package/src/player/components/timelineLayout.ts +22 -6
  188. package/src/player/components/timelineMarquee.test.ts +20 -10
  189. package/src/player/components/timelineMarquee.ts +9 -2
  190. package/src/player/components/timelineStackingSync.test.ts +42 -1
  191. package/src/player/components/timelineStackingSync.ts +31 -16
  192. package/src/player/components/timelineZMirror.test.ts +667 -0
  193. package/src/player/components/timelineZMirror.ts +393 -0
  194. package/src/player/components/timelineZoom.test.ts +2 -2
  195. package/src/player/components/timelineZoom.ts +7 -3
  196. package/src/player/components/useTimelineClipDrag.ts +1 -1
  197. package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
  198. package/src/player/components/useTimelineGapHighlights.ts +110 -0
  199. package/src/player/components/useTimelinePlayhead.ts +4 -3
  200. package/src/player/components/useTimelineRangeSelection.ts +6 -4
  201. package/src/player/components/useTimelineScrollViewport.ts +73 -0
  202. package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
  203. package/src/player/components/useTimelineStackingSync.ts +5 -3
  204. package/src/player/components/useTimelineTrackDerivations.ts +38 -0
  205. package/src/player/components/useTrackGapMenu.ts +155 -0
  206. package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
  207. package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
  208. package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
  209. package/src/player/lib/layerOrdering.test.ts +1 -15
  210. package/src/player/lib/layerOrdering.ts +5 -28
  211. package/src/player/lib/timelineDOM.test.ts +72 -0
  212. package/src/player/lib/timelineDOM.ts +3 -0
  213. package/src/player/lib/timelineElementHelpers.ts +35 -11
  214. package/src/player/store/playerStore.ts +12 -1
  215. package/src/styles/studio.css +22 -0
  216. package/src/styles/tailwind-preset.shared.js +7 -0
  217. package/src/telemetry/events.test.ts +10 -0
  218. package/src/telemetry/events.ts +1 -0
  219. package/src/utils/designInputTracking.test.ts +97 -0
  220. package/src/utils/designInputTracking.ts +80 -0
  221. package/src/utils/domEditSaveQueue.ts +4 -3
  222. package/src/utils/editHistory.test.ts +82 -0
  223. package/src/utils/editHistory.ts +14 -2
  224. package/src/utils/gsapSoftReload.test.ts +54 -115
  225. package/src/utils/gsapSoftReload.ts +42 -152
  226. package/src/utils/gsapUndoRestore.test.ts +269 -0
  227. package/src/utils/gsapUndoRestore.ts +269 -0
  228. package/src/utils/sdkCutover.gate.test.ts +7 -7
  229. package/src/utils/sdkCutover.test.ts +490 -57
  230. package/src/utils/sdkCutover.ts +181 -219
  231. package/src/utils/sdkEditTransaction.ts +292 -0
  232. package/src/utils/sdkResolverAttempts.ts +94 -0
  233. package/src/utils/sdkResolverShadow.test.ts +148 -0
  234. package/src/utils/sdkResolverShadow.ts +139 -106
  235. package/src/utils/setSlideshowManifest.test.ts +0 -9
  236. package/src/utils/setSlideshowManifest.ts +20 -27
  237. package/src/utils/sourceScopedSelectorIndex.ts +29 -0
  238. package/src/utils/studioFileHistory.test.ts +41 -0
  239. package/src/utils/studioFileHistory.ts +30 -27
  240. package/src/utils/studioFileMutationCoordinator.ts +45 -0
  241. package/src/utils/studioHelpers.test.ts +45 -0
  242. package/src/utils/studioHelpers.ts +45 -14
  243. package/src/utils/studioSaveDiagnostics.ts +17 -0
  244. package/dist/assets/index-DfmYkU44.js +0 -423
  245. package/dist/assets/index-_pqzyxB1.css +0 -1
  246. package/dist/chunk-5QSIMBEJ.js.map +0 -1
  247. /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
@@ -4,6 +4,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
4
4
  import { getPinchTimelineZoomPercent } from "./timelineZoom";
5
5
  import {
6
6
  GUTTER,
7
+ TRACKS_LEFT_PAD,
7
8
  getTimelinePlayheadLeft,
8
9
  getTimelineScrollLeftForZoomTransition,
9
10
  getTimelineScrollLeftForZoomAnchor,
@@ -73,7 +74,7 @@ export function useTimelinePlayhead({
73
74
  const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
74
75
  pointerX: scroll.clientWidth / 2,
75
76
  currentScrollLeft: scroll.scrollLeft,
76
- gutter: GUTTER,
77
+ gutter: GUTTER + TRACKS_LEFT_PAD,
77
78
  currentPixelsPerSecond: prevPps,
78
79
  nextPixelsPerSecond: pps,
79
80
  duration: durationRef.current,
@@ -129,7 +130,7 @@ export function useTimelinePlayhead({
129
130
  const el = scrollRef.current;
130
131
  if (!el || effectiveDuration <= 0) return;
131
132
  const rect = el.getBoundingClientRect();
132
- const x = clientX - rect.left + el.scrollLeft - GUTTER;
133
+ const x = clientX - rect.left + el.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
133
134
  if (x < 0) return;
134
135
  const time = Math.max(0, Math.min(effectiveDuration, x / pps));
135
136
  liveTime.notify(time);
@@ -185,7 +186,7 @@ export function useTimelinePlayhead({
185
186
  const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
186
187
  pointerX: e.clientX - rect.left,
187
188
  currentScrollLeft: scroll.scrollLeft,
188
- gutter: GUTTER,
189
+ gutter: GUTTER + TRACKS_LEFT_PAD,
189
190
  currentPixelsPerSecond: ppsRef.current,
190
191
  nextPixelsPerSecond: nextPps,
191
192
  duration: durationRef.current,
@@ -7,7 +7,7 @@ import {
7
7
  } from "./timelineEditing";
8
8
  import type { TimelineElement } from "../store/playerStore";
9
9
  import { liveTime, usePlayerStore } from "../store/playerStore";
10
- import { GUTTER } from "./timelineLayout";
10
+ import { GUTTER, TRACKS_LEFT_PAD } from "./timelineLayout";
11
11
  import {
12
12
  computeMarqueeSelection,
13
13
  getMarqueeRect,
@@ -228,7 +228,8 @@ export function useTimelineRangeSelection({
228
228
  setShowPopover(false);
229
229
  const rect = scrollRef.current?.getBoundingClientRect();
230
230
  if (rect) {
231
- const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
231
+ const x =
232
+ e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER - TRACKS_LEFT_PAD;
232
233
  const time = Math.max(0, x / pps);
233
234
  rangeAnchorTime.current = time;
234
235
  setRangeSelection({ start: time, end: time, anchorX: e.clientX, anchorY: e.clientY });
@@ -285,7 +286,7 @@ export function useTimelineRangeSelection({
285
286
  const el = scrollRef.current;
286
287
  if (el) {
287
288
  const rect = el.getBoundingClientRect();
288
- const x = clientX - rect.left + el.scrollLeft - GUTTER;
289
+ const x = clientX - rect.left + el.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
289
290
  if (x >= 0) {
290
291
  const dur = el.scrollWidth / pps;
291
292
  liveTime.notify(Math.max(0, Math.min(dur, x / pps)));
@@ -309,7 +310,8 @@ export function useTimelineRangeSelection({
309
310
  if (isRangeSelecting.current) {
310
311
  const rect = scrollRef.current?.getBoundingClientRect();
311
312
  if (rect) {
312
- const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
313
+ const x =
314
+ e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER - TRACKS_LEFT_PAD;
313
315
  setRangeSelection((prev) =>
314
316
  prev
315
317
  ? { ...prev, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
@@ -0,0 +1,73 @@
1
+ import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
2
+ import { useMountEffect } from "../../hooks/useMountEffect";
3
+ import { shouldShowTimelineShortcutHint } from "./timelineLayout";
4
+
5
+ /**
6
+ * The timeline scroll container's viewport plumbing — extracted verbatim from
7
+ * Timeline.tsx (600-line studio cap): the ResizeObserver-backed viewport width,
8
+ * the rAF-throttled shortcut-hint visibility sync, and the callback ref that
9
+ * wires both to the scroll element. `resyncShortcutHintOn` re-checks the hint
10
+ * whenever any of its values change (timeline readiness / element count /
11
+ * canvas height), matching the original effect.
12
+ */
13
+ export function useTimelineScrollViewport(
14
+ scrollRef: RefObject<HTMLDivElement | null>,
15
+ resyncShortcutHintOn: ReadonlyArray<unknown>,
16
+ ): {
17
+ viewportWidth: number;
18
+ showShortcutHint: boolean;
19
+ setScrollRef: (el: HTMLDivElement | null) => void;
20
+ } {
21
+ const [viewportWidth, setViewportWidth] = useState(0);
22
+ const [showShortcutHint, setShowShortcutHint] = useState(true);
23
+ const roRef = useRef<ResizeObserver | null>(null);
24
+ const shortcutHintRafRef = useRef(0);
25
+
26
+ const syncShortcutHintVisibility = useCallback(() => {
27
+ const scroll = scrollRef.current;
28
+ setShowShortcutHint(
29
+ scroll ? shouldShowTimelineShortcutHint(scroll.scrollHeight, scroll.clientHeight) : true,
30
+ );
31
+ }, [scrollRef]);
32
+
33
+ const scheduleShortcutHintVisibilitySync = useCallback(() => {
34
+ if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
35
+ shortcutHintRafRef.current = requestAnimationFrame(() => {
36
+ shortcutHintRafRef.current = 0;
37
+ syncShortcutHintVisibility();
38
+ });
39
+ }, [syncShortcutHintVisibility]);
40
+
41
+ const setScrollRef = useCallback(
42
+ (el: HTMLDivElement | null) => {
43
+ if (roRef.current) {
44
+ roRef.current.disconnect();
45
+ roRef.current = null;
46
+ }
47
+ scrollRef.current = el;
48
+ if (!el) return;
49
+
50
+ const syncScrollViewport = () => {
51
+ setViewportWidth(el.clientWidth);
52
+ scheduleShortcutHintVisibilitySync();
53
+ };
54
+
55
+ syncScrollViewport();
56
+ roRef.current = new ResizeObserver(syncScrollViewport);
57
+ roRef.current.observe(el);
58
+ },
59
+ [scrollRef, scheduleShortcutHintVisibilitySync],
60
+ );
61
+
62
+ useMountEffect(() => () => {
63
+ roRef.current?.disconnect();
64
+ if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
65
+ });
66
+
67
+ useEffect(() => {
68
+ syncShortcutHintVisibility();
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [syncShortcutHintVisibility, ...resyncShortcutHintOn]);
71
+
72
+ return { viewportWidth, showShortcutHint, setScrollRef };
73
+ }
@@ -40,7 +40,6 @@ describe("useTimelineStackingSync", () => {
40
40
  start: 0,
41
41
  duration: 2,
42
42
  track: 0,
43
- sourceFile: "nested.html",
44
43
  };
45
44
  let apply: ((patches: Array<{ key: string; zIndex: number }>, key?: string) => unknown) | null =
46
45
  null;
@@ -37,7 +37,7 @@ export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStac
37
37
  id: el.domId ?? el.id,
38
38
  selector: el.selector,
39
39
  selectorIndex: el.selectorIndex,
40
- sourceFile: el.sourceFile,
40
+ sourceFile: el.sourceFile ?? zSyncActiveCompPath ?? "index.html",
41
41
  },
42
42
  zSyncActiveCompPath,
43
43
  );
@@ -61,7 +61,7 @@ export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStac
61
61
 
62
62
  const applyStackingPatches = useCallback(
63
63
  (patches: StackingPatch[], coalesceKey?: string) => {
64
- if (!handleDomZIndexReorderCommit) return;
64
+ if (!handleDomZIndexReorderCommit) return Promise.resolve();
65
65
  const entries = patches.flatMap((p) => {
66
66
  const el = expandedElementsRef.current.find((e) => (e.key ?? e.id) === p.key);
67
67
  const node = el && resolveIframeElement(el);
@@ -82,7 +82,9 @@ export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStac
82
82
  });
83
83
  // Forward the drag-commit's shared coalesce key so the z-reorder history
84
84
  // entry merges with the lane change's move entry into one undo step.
85
- if (entries.length) handleDomZIndexReorderCommit(entries, coalesceKey);
85
+ return entries.length
86
+ ? handleDomZIndexReorderCommit(entries, coalesceKey).then(() => undefined)
87
+ : Promise.resolve();
86
88
  },
87
89
  [handleDomZIndexReorderCommit, resolveIframeElement, zSyncActiveCompPath, expandedElementsRef],
88
90
  );
@@ -0,0 +1,38 @@
1
+ import { useMemo } from "react";
2
+ import type { TimelineElement } from "../store/playerStore";
3
+ import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
4
+
5
+ /**
6
+ * Per-render track derivations Timeline.tsx feeds the canvas/lanes: the lane →
7
+ * clip grouping (`tracks`, ascending), per-lane visual styles, the ascending
8
+ * `trackOrder`, and the z-override badge set. Extracted from Timeline.tsx as a
9
+ * cohesive unit (600-line studio cap); each memo keys on the expanded display
10
+ * element set exactly as before.
11
+ */
12
+ export function useTimelineTrackDerivations(expandedElements: TimelineElement[]): {
13
+ tracks: [number, TimelineElement[]][];
14
+ trackStyles: Map<number, TrackVisualStyle>;
15
+ trackOrder: number[];
16
+ } {
17
+ const tracks = useMemo(() => {
18
+ const map = new Map<number, TimelineElement[]>();
19
+ for (const el of expandedElements) {
20
+ const list = map.get(el.track) ?? [];
21
+ list.push(el);
22
+ map.set(el.track, list);
23
+ }
24
+ return Array.from(map.entries()).sort(([a], [b]) => a - b);
25
+ }, [expandedElements]);
26
+
27
+ const trackStyles = useMemo(() => {
28
+ const map = new Map<number, TrackVisualStyle>();
29
+ for (const [trackNum, els] of tracks) {
30
+ map.set(trackNum, getTrackStyle(els[0]?.tag ?? ""));
31
+ }
32
+ return map;
33
+ }, [tracks]);
34
+
35
+ const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
36
+
37
+ return { tracks, trackStyles, trackOrder };
38
+ }
@@ -0,0 +1,155 @@
1
+ import { useCallback, useMemo, useState, type MutableRefObject } from "react";
2
+ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
3
+ import type { DragCommitDeps } from "./timelineClipDragCommit";
4
+ import {
5
+ laneGapFloor,
6
+ resolveAllGapIntervals,
7
+ resolveAllTrackGaps,
8
+ resolveCloseGapShifts,
9
+ resolveTrackGapAt,
10
+ type TrackGapInterval,
11
+ } from "./timelineGaps";
12
+ import {
13
+ canShiftTrackGapClips,
14
+ commitCloseAllTrackGaps,
15
+ commitCloseTrackGap,
16
+ } from "./timelineGapCommit";
17
+
18
+ /** Right-click anchor on EMPTY lane space: pointer position + clicked lane/time. */
19
+ interface TrackGapMenuAnchor {
20
+ x: number;
21
+ y: number;
22
+ track: number;
23
+ time: number;
24
+ }
25
+
26
+ /** Gap strips to paint on one lane while a menu row is hovered. */
27
+ export interface TrackGapHighlight {
28
+ track: number;
29
+ intervals: TrackGapInterval[];
30
+ }
31
+
32
+ /**
33
+ * Track-gap context menu (right-click on empty lane space) — state, the
34
+ * derived menu model, and the two commit actions. Extracted from Timeline.tsx
35
+ * as a cohesive unit (600-line studio cap); behavior identical.
36
+ *
37
+ * Only the ANCHOR (and the hovered row) is state; the menu model (gap under
38
+ * the pointer, compaction, movability) derives from live `tracks` so an open
39
+ * menu reflects concurrent edits. `gapHighlight` — the strips TimelineCanvas
40
+ * paints while "Close gap" / "Close all gaps" is hovered — derives the same
41
+ * way. Commits are ONE atomic batch each via the existing move-persist
42
+ * pipeline (see timelineGapCommit.ts).
43
+ */
44
+ export function useTrackGapMenu({
45
+ tracks,
46
+ expandedElementsRef,
47
+ trackOrderRef,
48
+ onMoveElement,
49
+ onMoveElements,
50
+ }: {
51
+ tracks: [number, TimelineElement[]][];
52
+ expandedElementsRef: MutableRefObject<TimelineElement[]>;
53
+ trackOrderRef: MutableRefObject<number[]>;
54
+ onMoveElement: DragCommitDeps["onMoveElement"];
55
+ onMoveElements: DragCommitDeps["onMoveElements"];
56
+ }) {
57
+ const updateElement = usePlayerStore((s) => s.updateElement);
58
+ const [gapContextMenu, setGapContextMenu] = useState<TrackGapMenuAnchor | null>(null);
59
+ const [hoveredGapAction, setHoveredGapAction] = useState<"close-gap" | "close-all" | null>(null);
60
+
61
+ const gapMenuLaneElements = useMemo(
62
+ () => (gapContextMenu ? (tracks.find(([t]) => t === gapContextMenu.track)?.[1] ?? []) : null),
63
+ [gapContextMenu, tracks],
64
+ );
65
+ const gapMenuModel = useMemo(() => {
66
+ if (!gapContextMenu || !gapMenuLaneElements) return null;
67
+ const floor = laneGapFloor(gapMenuLaneElements);
68
+ const gap = resolveTrackGapAt(gapMenuLaneElements, gapContextMenu.time, undefined, floor);
69
+ const allShifts = resolveAllTrackGaps(gapMenuLaneElements, undefined, floor);
70
+ return {
71
+ x: gapContextMenu.x,
72
+ y: gapContextMenu.y,
73
+ gapWidth: gap ? gap.gapEnd - gap.gapStart : null,
74
+ canCloseGap:
75
+ gap != null &&
76
+ canShiftTrackGapClips(gapMenuLaneElements, resolveCloseGapShifts(gapMenuLaneElements, gap)),
77
+ hasAnyGaps: allShifts.length > 0,
78
+ canCloseAllGaps:
79
+ allShifts.length > 0 && canShiftTrackGapClips(gapMenuLaneElements, allShifts),
80
+ };
81
+ }, [gapContextMenu, gapMenuLaneElements]);
82
+
83
+ // The strips to paint while a menu row is hovered: the one gap under the
84
+ // pointer for "Close gap", every current gap (leading included) for
85
+ // "Close all gaps". Null when nothing is hovered / nothing would close.
86
+ const gapHighlight = useMemo<TrackGapHighlight | null>(() => {
87
+ if (!gapContextMenu || !gapMenuLaneElements || !hoveredGapAction) return null;
88
+ const floor = laneGapFloor(gapMenuLaneElements);
89
+ if (hoveredGapAction === "close-gap") {
90
+ const gap = resolveTrackGapAt(gapMenuLaneElements, gapContextMenu.time, undefined, floor);
91
+ if (!gap) return null;
92
+ return {
93
+ track: gapContextMenu.track,
94
+ intervals: [{ start: gap.gapStart, end: gap.gapEnd }],
95
+ };
96
+ }
97
+ const intervals = resolveAllGapIntervals(gapMenuLaneElements, undefined, floor);
98
+ return intervals.length > 0 ? { track: gapContextMenu.track, intervals } : null;
99
+ }, [gapContextMenu, gapMenuLaneElements, hoveredGapAction]);
100
+
101
+ const closeTrackGap = useCallback(() => {
102
+ if (!gapContextMenu || !gapMenuLaneElements) return;
103
+ commitCloseTrackGap(gapMenuLaneElements, gapContextMenu.time, {
104
+ elements: expandedElementsRef.current,
105
+ trackOrder: trackOrderRef.current,
106
+ updateElement,
107
+ onMoveElement,
108
+ onMoveElements,
109
+ });
110
+ }, [
111
+ gapContextMenu,
112
+ gapMenuLaneElements,
113
+ expandedElementsRef,
114
+ trackOrderRef,
115
+ updateElement,
116
+ onMoveElement,
117
+ onMoveElements,
118
+ ]);
119
+ const closeAllTrackGaps = useCallback(() => {
120
+ if (!gapMenuLaneElements) return;
121
+ commitCloseAllTrackGaps(gapMenuLaneElements, {
122
+ elements: expandedElementsRef.current,
123
+ trackOrder: trackOrderRef.current,
124
+ updateElement,
125
+ onMoveElement,
126
+ onMoveElements,
127
+ });
128
+ }, [
129
+ gapMenuLaneElements,
130
+ expandedElementsRef,
131
+ trackOrderRef,
132
+ updateElement,
133
+ onMoveElement,
134
+ onMoveElements,
135
+ ]);
136
+
137
+ const openGapMenu = useCallback((anchor: TrackGapMenuAnchor) => {
138
+ setHoveredGapAction(null);
139
+ setGapContextMenu(anchor);
140
+ }, []);
141
+ const dismissGapMenu = useCallback(() => {
142
+ setHoveredGapAction(null);
143
+ setGapContextMenu(null);
144
+ }, []);
145
+
146
+ return {
147
+ gapMenuModel,
148
+ gapHighlight,
149
+ setHoveredGapAction,
150
+ openGapMenu,
151
+ dismissGapMenu,
152
+ closeTrackGap,
153
+ closeAllTrackGaps,
154
+ };
155
+ }
@@ -152,10 +152,34 @@ describe("buildExpandedElements", () => {
152
152
  ["pill-3", "group-1"],
153
153
  ]);
154
154
  const domClipChildren = [
155
- { id: "group-1", parentId: "scene-host", hostId: "scene-host", label: "Group 1" },
156
- { id: "pill-1", parentId: "group-1", hostId: "scene-host", label: "pill-1" },
157
- { id: "pill-2", parentId: "group-1", hostId: "scene-host", label: "pill-2" },
158
- { id: "pill-3", parentId: "group-1", hostId: "scene-host", label: "pill-3" },
155
+ {
156
+ id: "group-1",
157
+ parentId: "scene-host",
158
+ hostId: "scene-host",
159
+ label: "Group 1",
160
+ stackingContextId: "css:0",
161
+ },
162
+ {
163
+ id: "pill-1",
164
+ parentId: "group-1",
165
+ hostId: "scene-host",
166
+ label: "pill-1",
167
+ stackingContextId: "css:0.0",
168
+ },
169
+ {
170
+ id: "pill-2",
171
+ parentId: "group-1",
172
+ hostId: "scene-host",
173
+ label: "pill-2",
174
+ stackingContextId: "css:0.1",
175
+ },
176
+ {
177
+ id: "pill-3",
178
+ parentId: "group-1",
179
+ hostId: "scene-host",
180
+ label: "pill-3",
181
+ stackingContextId: "css:0.1",
182
+ },
159
183
  ];
160
184
 
161
185
  // Expanding pill-3's siblings: topLevel scene-host, immediate parent group-1.
@@ -173,6 +197,7 @@ describe("buildExpandedElements", () => {
173
197
  expect(pills[0]!.start).toBe(5);
174
198
  expect(pills[0]!.duration).toBe(6);
175
199
  expect(pills[0]!.sourceFile).toBe("scene.html");
200
+ expect(pills.map((pill) => pill.stackingContextId)).toEqual(["css:0.0", "css:0.1", "css:0.1"]);
176
201
  // The host row is replaced by its children.
177
202
  expect(out.some((e) => e.domId === "scene-host")).toBe(false);
178
203
  });
@@ -273,3 +298,41 @@ describe("resolveTimelineExpansionRawId", () => {
273
298
  ).toBe("caption");
274
299
  });
275
300
  });
301
+
302
+ describe("buildExpandedElements — collision-free synthetic rows (cross-file lane safety)", () => {
303
+ it("expanded children NEVER share a display track with an unrelated top-level clip", () => {
304
+ // The Deepwork regression: host on track 0 with two children used to put
305
+ // child #2 on integer track 1 — the same lane as index.html#foreign. Lane
306
+ // grouping merges purely by track number, so a gap-close on that "one"
307
+ // lane batch-persisted a foreign file's clip.
308
+ const elements = [
309
+ el({ id: "host", start: 0, duration: 20, track: 0, compositionSrc: "scene.html" }),
310
+ el({ id: "foreign", start: 20, duration: 5, track: 1 }),
311
+ ];
312
+ const manifest = [
313
+ clip({ id: "host", start: 0, duration: 20, compositionSrc: "scene.html" }),
314
+ clip({ id: "c1", start: 0, duration: 5 }),
315
+ clip({ id: "c2", start: 10, duration: 5 }),
316
+ ];
317
+ const parentMap = new Map([
318
+ ["c1", "host"],
319
+ ["c2", "host"],
320
+ ]);
321
+
322
+ const out = buildExpandedElements(elements, manifest, parentMap, "host", "host");
323
+ const foreign = out.find((e) => e.id === "foreign")!;
324
+ const children = out.filter((e) => e.domId === "c1" || e.domId === "c2");
325
+ expect(children).toHaveLength(2);
326
+ for (const child of children) {
327
+ // No lane sharing with the foreign clip…
328
+ expect(child.track).not.toBe(foreign.track);
329
+ // …and structurally impossible to collide with ANY normalized (integer)
330
+ // lane: synthetic rows are strict fractions under the host's lane.
331
+ expect(Number.isInteger(child.track)).toBe(false);
332
+ expect(child.track).toBeGreaterThan(0);
333
+ expect(child.track).toBeLessThan(1);
334
+ }
335
+ // Distinct ordered rows per child.
336
+ expect(children[0].track).not.toBe(children[1].track);
337
+ });
338
+ });
@@ -168,7 +168,17 @@ function buildChildElements(
168
168
  // file's coordinate space) and the runtime-computed `stackingContextId`
169
169
  // must survive verbatim — lane persists and z-sync read them, they are
170
170
  // never reconstructed from display lanes.
171
- track: display.track + result.length,
171
+ //
172
+ // COLLISION-FREE synthetic rows: the old `display.track + index` scheme
173
+ // could equal a REAL clip's integer lane (host on track 0 with two
174
+ // children puts child #2 on track 1 — where an unrelated top-level clip
175
+ // may live). Lane grouping merges purely by track number, so that
176
+ // collision fused clips from DIFFERENT source files into one display
177
+ // lane, and lane-scoped actions (gap close) then batch-persisted foreign
178
+ // clips. Fractions strictly between the host's lane and the next integer
179
+ // can never equal a normalized (integer) lane, while still rendering the
180
+ // children as their own ordered rows directly under the host.
181
+ track: display.track + (result.length + 1) / (siblings.length + 2),
172
182
  authoredTrack: base.authoredTrack,
173
183
  stackingContextId: base.stackingContextId,
174
184
  expandedParentStart: editBasis.start,
@@ -205,6 +215,7 @@ function domSiblingClips(
205
215
  parentCompositionId: host.id ?? null,
206
216
  compositionSrc: host.compositionSrc ?? null,
207
217
  assetUrl: null,
218
+ stackingContextId: c.stackingContextId,
208
219
  }),
209
220
  );
210
221
  }
@@ -11,6 +11,7 @@
11
11
  import { useCallback } from "react";
12
12
  import { liveTime, usePlayerStore } from "../store/playerStore";
13
13
  import type { TimelineElement, DomClipChild } from "../store/playerStore";
14
+ import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
14
15
  import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
15
16
  import {
16
17
  parseTimelineFromDOM,
@@ -199,6 +200,7 @@ export function useTimelineSyncCallbacks({
199
200
  parentId,
200
201
  hostId,
201
202
  label: isGroup ? child.getAttribute("data-hf-group") || child.id : child.id,
203
+ stackingContextId: resolveCssStackingContextId(child),
202
204
  });
203
205
  parentMap.set(child.id, parentId);
204
206
  if (isGroup) collect(child, child.id);
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { describe, expect, it } from "vitest";
4
- import { computeReorderZValues, getElementZIndex, resolveContextOrder } from "./layerOrdering";
4
+ import { getElementZIndex, resolveContextOrder } from "./layerOrdering";
5
5
 
6
6
  function makeElement(zIndex?: string): HTMLElement {
7
7
  const element = document.createElement("div");
@@ -31,20 +31,6 @@ describe("getElementZIndex", () => {
31
31
  });
32
32
  });
33
33
 
34
- describe("computeReorderZValues", () => {
35
- it("preserves distinct existing z-index values and remaps them onto the new order", () => {
36
- expect(computeReorderZValues([1, 8, 3], 0, 2)).toEqual([8, 3, 1]);
37
- });
38
-
39
- it("renumbers an all-tied group to descending contiguous z-index values", () => {
40
- expect(computeReorderZValues([0, 0, 0], 2, 0)).toEqual([3, 2, 1]);
41
- });
42
-
43
- it("renumbers the whole group when any existing z-index values are tied", () => {
44
- expect(computeReorderZValues([5, 5, 1], 2, 0)).toEqual([3, 2, 1]);
45
- });
46
- });
47
-
48
34
  describe("resolveContextOrder", () => {
49
35
  it("sorts a flat sibling group by z-index descending then original order", () => {
50
36
  const ordered = resolveContextOrder([
@@ -1,3 +1,5 @@
1
+ import { readLayerRevealPriorZ } from "./timelineElementHelpers";
2
+
1
3
  export interface StackingContextDescriptor {
2
4
  parentCompositionId: string | null;
3
5
  compositionAncestors: readonly string[];
@@ -11,6 +13,9 @@ export interface ContextOrderItem extends StackingContextDescriptor {
11
13
  // fallow-ignore-next-line complexity
12
14
  export function getElementZIndex(element: HTMLElement): number {
13
15
  try {
16
+ // An active Layers-panel reveal lift reports the element's TRUE z.
17
+ const prior = readLayerRevealPriorZ(element);
18
+ if (prior != null) return prior;
14
19
  const inline = element.style?.zIndex;
15
20
  if (inline && inline !== "auto") {
16
21
  const parsed = parseInt(inline, 10);
@@ -27,34 +32,6 @@ export function getElementZIndex(element: HTMLElement): number {
27
32
  }
28
33
  }
29
34
 
30
- // fallow-ignore-next-line complexity
31
- export function hasExplicitZIndex(element: HTMLElement): boolean {
32
- try {
33
- const inline = element.style?.zIndex;
34
- if (inline) return inline !== "auto";
35
- const win = element.ownerDocument?.defaultView;
36
- if (!win) return false;
37
- const value = win.getComputedStyle(element).zIndex;
38
- return value !== "auto" && value !== "";
39
- } catch {
40
- return false;
41
- }
42
- }
43
-
44
- export function computeReorderZValues(
45
- existingValues: readonly number[],
46
- fromIndex: number,
47
- toIndex: number,
48
- ): number[] {
49
- const reordered = [...existingValues];
50
- const [moved] = reordered.splice(fromIndex, 1);
51
- reordered.splice(toIndex, 0, moved);
52
-
53
- const sorted = [...existingValues].sort((a, b) => b - a);
54
- const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
55
- return hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
56
- }
57
-
58
35
  export function resolveStackingContextKey(item: StackingContextDescriptor): string {
59
36
  return item.stackingContextId ?? item.parentCompositionId ?? item.compositionAncestors[0] ?? "";
60
37
  }
@@ -110,7 +110,79 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
110
110
  });
111
111
  });
112
112
 
113
+ describe("createTimelineElementFromManifestClip — source-scoped selector identity", () => {
114
+ it("ignores an index.html duplicate when indexing a scene.html selector", () => {
115
+ const doc = makeDoc(`
116
+ <div data-composition-id="root" data-composition-file="index.html">
117
+ <div class="sub"></div>
118
+ <div data-composition-id="scene" data-composition-file="scene.html">
119
+ <div class="sub"></div>
120
+ <div class="sub" data-target></div>
121
+ </div>
122
+ </div>
123
+ `);
124
+ const target = doc.querySelector("[data-target]");
125
+ if (!target) throw new Error("missing target");
126
+
127
+ const element = createTimelineElementFromManifestClip({
128
+ clip: {
129
+ id: null,
130
+ label: "Sub",
131
+ kind: "element",
132
+ tagName: "div",
133
+ start: 0,
134
+ duration: 5,
135
+ track: 0,
136
+ compositionId: null,
137
+ parentCompositionId: null,
138
+ compositionSrc: null,
139
+ assetUrl: null,
140
+ },
141
+ fallbackIndex: 0,
142
+ doc,
143
+ hostEl: target,
144
+ });
145
+
146
+ expect(element.sourceFile).toBe("scene.html");
147
+ expect(element.selectorIndex).toBe(1);
148
+ expect(element.key).toBe("scene.html:.sub:1");
149
+ });
150
+ });
151
+
113
152
  describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
153
+ it("uses the runtime root paint scope for implicit siblings of manifest clips", () => {
154
+ const doc = makeDoc(`
155
+ <div data-composition-id="root">
156
+ <div id="timed" data-start="0" data-duration="5"></div>
157
+ <div id="implicit"></div>
158
+ </div>
159
+ `);
160
+ const timedHost = doc.getElementById("timed");
161
+ const timed = createTimelineElementFromManifestClip({
162
+ clip: {
163
+ id: "timed",
164
+ label: "Timed",
165
+ start: 0,
166
+ duration: 5,
167
+ track: 0,
168
+ stackingContextId: "css:root",
169
+ kind: "element",
170
+ tagName: "div",
171
+ compositionId: null,
172
+ parentCompositionId: null,
173
+ compositionSrc: null,
174
+ assetUrl: null,
175
+ },
176
+ fallbackIndex: 0,
177
+ doc,
178
+ hostEl: timedHost,
179
+ });
180
+ const implicit = createImplicitTimelineLayersFromDOM(doc, 5, [timed])[0];
181
+
182
+ expect(implicit?.stackingContextId).toBe("css:root");
183
+ expect(implicit?.stackingContextId).toBe(timed.stackingContextId);
184
+ });
185
+
114
186
  it("harvests hfId from an implicit layer child that has data-hf-id", () => {
115
187
  const doc = makeDoc(`
116
188
  <div data-composition-id="root">