@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
@@ -33,7 +33,8 @@ interface ToggleTimelineTrackHiddenInput {
33
33
  }
34
34
 
35
35
  interface ToggleTimelineElementHiddenInput extends Omit<ToggleTimelineTrackHiddenInput, "track"> {
36
- elementKey: string;
36
+ /** One timeline key, or several to hide/show in a single atomic file write. */
37
+ elementKey: string | readonly string[];
37
38
  }
38
39
 
39
40
  interface SetElementsHiddenInput {
@@ -62,7 +63,7 @@ interface UseTimelineTrackVisibilityEditingInput extends Omit<
62
63
 
63
64
  interface UseTimelineElementVisibilityEditingInput extends Omit<
64
65
  ToggleTimelineElementHiddenInput,
65
- "projectId" | "elementKey" | "hidden" | "previewIframe"
66
+ "projectId" | "elementKey" | "hidden" | "previewIframe" | "timelineElements"
66
67
  > {
67
68
  projectIdRef: ReadonlyRef<string | null>;
68
69
  previewIframeRef: ReadonlyRef<HTMLIFrameElement | null>;
@@ -82,9 +83,10 @@ function patchLiveHiddenState(
82
83
  iframe: HTMLIFrameElement | null,
83
84
  elements: readonly TimelineElement[],
84
85
  hidden: boolean,
86
+ activeCompPath: string | null,
85
87
  ): void {
86
88
  for (const element of elements) {
87
- const target = findTimelineElementInIframe(iframe, element);
89
+ const target = findTimelineElementInIframe(iframe, element, activeCompPath);
88
90
  if (!target) continue;
89
91
  if (hidden) {
90
92
  target.setAttribute("data-hidden", "");
@@ -134,7 +136,7 @@ async function setElementsHidden({
134
136
  }: SetElementsHiddenInput): Promise<string[]> {
135
137
  if (elements.length === 0) return [];
136
138
 
137
- patchLiveHiddenState(previewIframe, elements, hidden);
139
+ patchLiveHiddenState(previewIframe, elements, hidden, activeCompPath);
138
140
  reseekPreviewRuntime(previewIframe);
139
141
 
140
142
  const hiddenOperation: PatchOperation = {
@@ -188,7 +190,7 @@ async function setElementsHidden({
188
190
  // The optimistic live patch already ran; a patch-target/save failure here would
189
191
  // otherwise leave the preview showing the wrong visibility until a reload. Revert
190
192
  // the live DOM to the prior state so what's on screen matches what persisted.
191
- patchLiveHiddenState(previewIframe, elements, !hidden);
193
+ patchLiveHiddenState(previewIframe, elements, !hidden, activeCompPath);
192
194
  reseekPreviewRuntime(previewIframe);
193
195
  throw error;
194
196
  }
@@ -232,13 +234,21 @@ export async function toggleTimelineElementHidden({
232
234
  domEditSaveTimestampRef,
233
235
  pendingTimelineEditPathRef,
234
236
  }: ToggleTimelineElementHiddenInput): Promise<string[]> {
235
- const element = timelineElements.find((item) => (item.key ?? item.id) === elementKey);
237
+ const keys = new Set(typeof elementKey === "string" ? [elementKey] : elementKey);
238
+ const elements = timelineElements.filter((item) => keys.has(item.key ?? item.id));
236
239
  return setElementsHidden({
237
240
  projectId,
238
241
  activeCompPath,
239
- elements: element ? [element] : [],
242
+ elements,
240
243
  hidden,
241
- label: hidden ? "Hide element" : "Show element",
244
+ label:
245
+ elements.length > 1
246
+ ? hidden
247
+ ? `Hide ${elements.length} elements`
248
+ : `Show ${elements.length} elements`
249
+ : hidden
250
+ ? "Hide element"
251
+ : "Show element",
242
252
  previewIframe,
243
253
  writeProjectFile,
244
254
  recordEdit,
@@ -312,7 +322,6 @@ export function useTimelineTrackVisibilityEditing({
312
322
  export function useTimelineElementVisibilityEditing({
313
323
  projectIdRef,
314
324
  activeCompPath,
315
- timelineElements,
316
325
  showToast,
317
326
  writeProjectFile,
318
327
  recordEdit,
@@ -322,11 +331,20 @@ export function useTimelineElementVisibilityEditing({
322
331
  isRecordingRef,
323
332
  forceReloadSdkSession,
324
333
  }: UseTimelineElementVisibilityEditingInput): (
325
- elementKey: string,
334
+ elementKey: string | readonly string[],
326
335
  hidden: boolean,
327
336
  ) => Promise<void> {
337
+ // Resolve against the EXPANDED rows, not the raw store list — a nested
338
+ // sub-composition child has no entry of its own in the raw list (only its
339
+ // host does), so an elementKey for such a child (the
340
+ // `sourceFile#domId`-shaped virtual key `resolveTimelineIdForSelection`
341
+ // falls back to) would never match anything there and Hide All would
342
+ // silently no-op for it. The expanded list synthesizes a real, patchable
343
+ // TimelineElement (with matching key/domId/sourceFile) for each visible
344
+ // child whenever its host is currently expanded.
345
+ const expandedElements = useExpandedTimelineElements();
328
346
  return useCallback(
329
- async (elementKey: string, hidden: boolean) => {
347
+ async (elementKey: string | readonly string[], hidden: boolean) => {
330
348
  if (isRecordingRef?.current) {
331
349
  showToast("Cannot edit timeline while recording", "error");
332
350
  return;
@@ -337,7 +355,7 @@ export function useTimelineElementVisibilityEditing({
337
355
  await toggleTimelineElementHidden({
338
356
  projectId: pid,
339
357
  activeCompPath,
340
- timelineElements,
358
+ timelineElements: expandedElements,
341
359
  elementKey,
342
360
  hidden,
343
361
  previewIframe: previewIframeRef.current,
@@ -356,7 +374,7 @@ export function useTimelineElementVisibilityEditing({
356
374
  },
357
375
  [
358
376
  activeCompPath,
359
- timelineElements,
377
+ expandedElements,
360
378
  previewIframeRef,
361
379
  writeProjectFile,
362
380
  recordEdit,
@@ -9,6 +9,7 @@ import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
9
9
  import { canSplitElement } from "../utils/timelineElementSplit";
10
10
  import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
11
11
  import { trackStudioEvent } from "../utils/studioTelemetry";
12
+ import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
12
13
 
13
14
  function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
14
15
  try {
@@ -87,6 +88,7 @@ interface HistoryResult {
87
88
  interface HistoryFileCallbacks {
88
89
  readFile: (path: string) => Promise<string>;
89
90
  writeFile: (path: string, content: string) => Promise<void>;
91
+ serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
90
92
  }
91
93
  interface EditHistoryHandle {
92
94
  undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
@@ -367,6 +369,11 @@ export function useAppHotkeys({
367
369
  },
368
370
  [domEditSaveTimestampRef, writeProjectFile],
369
371
  );
372
+ const serializeHistoryFiles = useCallback(
373
+ <T>(paths: readonly string[], task: () => Promise<T>) =>
374
+ serializeStudioFileMutations(writeProjectFile, paths, task),
375
+ [writeProjectFile],
376
+ );
370
377
 
371
378
  const applyHistory = useCallback(
372
379
  async (direction: "undo" | "redo") => {
@@ -377,6 +384,7 @@ export function useAppHotkeys({
377
384
  const result = await editHistory[direction]({
378
385
  readFile: readHistoryFile,
379
386
  writeFile: writeHistoryFile,
387
+ serialize: serializeHistoryFiles,
380
388
  });
381
389
  if (!result.ok && result.reason === "content-mismatch") {
382
390
  showToast(
@@ -406,6 +414,7 @@ export function useAppHotkeys({
406
414
  syncHistoryPreviewAfterApply,
407
415
  waitForPendingDomEditSaves,
408
416
  writeHistoryFile,
417
+ serializeHistoryFiles,
409
418
  onAfterUndoRedo,
410
419
  activeCompPath,
411
420
  forceReloadSdkSession,
@@ -8,6 +8,7 @@ import { insertTimelineAssetIntoSource } from "../utils/timelineAssetDrop";
8
8
  import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
9
9
  import type { EditHistoryKind } from "../utils/editHistory";
10
10
  import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
11
+ import { findElementForSelection } from "../components/editor/domEditingElement";
11
12
  import { readFileContent } from "./timelineEditingHelpers";
12
13
 
13
14
  interface RecordEditInput {
@@ -34,6 +35,7 @@ interface UseClipboardOptions {
34
35
  function getElementOuterHtml(
35
36
  iframeRef: React.MutableRefObject<HTMLIFrameElement | null>,
36
37
  selection: DomEditSelection,
38
+ activeCompositionPath: string | null,
37
39
  ): string | null {
38
40
  let doc: Document | null = null;
39
41
  try {
@@ -43,15 +45,7 @@ function getElementOuterHtml(
43
45
  }
44
46
  if (!doc) return null;
45
47
 
46
- let el: Element | null = null;
47
- if (selection.id) {
48
- el = doc.getElementById(selection.id);
49
- }
50
- if (!el && selection.selector) {
51
- const matches = doc.querySelectorAll(selection.selector);
52
- el = matches[selection.selectorIndex ?? 0] ?? null;
53
- }
54
- return el && "outerHTML" in el ? (el as Element).outerHTML : null;
48
+ return findElementForSelection(doc, selection, activeCompositionPath)?.outerHTML ?? null;
55
49
  }
56
50
 
57
51
  export function useClipboard({
@@ -71,6 +65,9 @@ export function useClipboard({
71
65
  const projectIdRef = useRef(projectId);
72
66
  projectIdRef.current = projectId;
73
67
 
68
+ // The copy-mode branches predate this change; this diff only replaces its
69
+ // duplicated DOM lookup with the canonical composition-aware resolver.
70
+ // fallow-ignore-next-line complexity
74
71
  const handleCopy = useCallback((): boolean => {
75
72
  const { selectedElementId, elements } = usePlayerStore.getState();
76
73
 
@@ -84,13 +81,18 @@ export function useClipboard({
84
81
  try {
85
82
  const doc = previewIframeRef.current?.contentDocument;
86
83
  if (doc) {
87
- let el: Element | null = null;
88
- if (element.domId) el = doc.getElementById(element.domId);
89
- if (!el && element.selector) {
90
- const matches = doc.querySelectorAll(element.selector);
91
- el = matches[element.selectorIndex ?? 0] ?? null;
92
- }
93
- if (el && "outerHTML" in el) html = (el as Element).outerHTML;
84
+ html =
85
+ findElementForSelection(
86
+ doc,
87
+ {
88
+ hfId: element.hfId,
89
+ id: element.domId,
90
+ selector: element.selector,
91
+ selectorIndex: element.selectorIndex,
92
+ sourceFile: targetPath,
93
+ },
94
+ activeCompPath,
95
+ )?.outerHTML ?? null;
94
96
  }
95
97
  } catch {
96
98
  // cross-origin frame
@@ -110,7 +112,7 @@ export function useClipboard({
110
112
  // DOM element copy
111
113
  const domSelection = domEditSelectionRef.current;
112
114
  if (domSelection) {
113
- const html = getElementOuterHtml(previewIframeRef, domSelection);
115
+ const html = getElementOuterHtml(previewIframeRef, domSelection, activeCompPath);
114
116
  if (!html) {
115
117
  showToast("Unable to copy this element.", "info");
116
118
  return false;
@@ -25,6 +25,7 @@ interface DataAttributeCommitOptions {
25
25
  coalescePrefix: string;
26
26
  skipRefresh: boolean;
27
27
  refreshAfter?: boolean;
28
+ onSettled?: (ok: boolean) => void;
28
29
  }
29
30
 
30
31
  function resolveFullAttrName(attr: string, prefixData: boolean | undefined): string {
@@ -57,6 +58,25 @@ interface CapturedAttributeElement {
57
58
  previousValue: string | null;
58
59
  }
59
60
 
61
+ interface CapturedMultiAttributeElement {
62
+ element: HTMLElement;
63
+ previousValues: Map<string, string | null>;
64
+ }
65
+
66
+ function captureMultiAttributeElement(
67
+ doc: Document | null | undefined,
68
+ selection: DomEditSelection,
69
+ activeCompPath: string | null,
70
+ fullAttrs: string[],
71
+ ): CapturedMultiAttributeElement | null {
72
+ const el = findPreviewAttributeElement(doc, selection, activeCompPath);
73
+ if (!el) return null;
74
+ const previousValues = new Map(
75
+ fullAttrs.map((fullAttr) => [fullAttr, el.getAttribute(fullAttr)]),
76
+ );
77
+ return { element: el, previousValues };
78
+ }
79
+
60
80
  function captureAttributeElement(
61
81
  doc: Document | null | undefined,
62
82
  selection: DomEditSelection,
@@ -128,6 +148,7 @@ export function useDomEditAttributeCommits({
128
148
  onError: (error) => reportDomEditPersistFailure(domEditSelection, [op], error, showToast),
129
149
  shouldResync: () => isLatestCommit() && !!options.refreshAfter,
130
150
  resync: () => refreshDomEditSelectionFromPreview(domEditSelection),
151
+ onSettled: options.onSettled,
131
152
  });
132
153
  },
133
154
  [
@@ -140,6 +161,105 @@ export function useDomEditAttributeCommits({
140
161
  ],
141
162
  );
142
163
 
164
+ // Commits several data-* attributes on the SAME element in ONE persist call
165
+ // — needed when two attributes together describe a single logical value
166
+ // (e.g. a pinned timing range's start+duration): committing them through two
167
+ // separate sequential `commitDataAttribute` calls leaves a window where the
168
+ // second call resolves `domEditSelection` fresh from current hook state, so
169
+ // a selection change between the two awaits would misdirect it at the
170
+ // NEWLY selected element instead of the one being edited, and a failure of
171
+ // just the second call would leave the two attributes in an inconsistent
172
+ // half-applied state. Bundling them into one `PatchOperation[]` against an
173
+ // explicit, caller-supplied `selection` (not the "current" one) closes both
174
+ // gaps — matching `onCommitAnimatedProperties`'s same-shaped fix for GSAP
175
+ // property batches.
176
+ const commitDataAttributes = useCallback(
177
+ async (
178
+ selection: DomEditSelection,
179
+ attrs: Record<string, string | null>,
180
+ options: DataAttributeCommitOptions,
181
+ ) => {
182
+ const iframe = previewIframeRef.current;
183
+ const entries = Object.entries(attrs).map(([attr, value]) => ({
184
+ attr,
185
+ fullAttr: resolveFullAttrName(attr, true),
186
+ value,
187
+ }));
188
+ const commitKey = `${options.coalescePrefix}:${entries
189
+ .map((entry) => entry.attr)
190
+ .sort()
191
+ .join(",")}:${getDomEditTargetKey(selection)}`;
192
+ const isLatestCommit = bumpDomEditCommitMapVersion(
193
+ domAttributeCommitVersionRef.current,
194
+ commitKey,
195
+ );
196
+ const ops: PatchOperation[] = entries.map((entry) => ({
197
+ type: "attribute",
198
+ property: entry.attr,
199
+ value: entry.value,
200
+ }));
201
+ let captured: CapturedMultiAttributeElement | null = null;
202
+
203
+ await runDomEditCommit({
204
+ capture: () => {
205
+ captured = captureMultiAttributeElement(
206
+ iframe?.contentDocument,
207
+ selection,
208
+ activeCompPath,
209
+ entries.map((entry) => entry.fullAttr),
210
+ );
211
+ },
212
+ apply: () => {
213
+ if (!captured) return;
214
+ for (const entry of entries) {
215
+ const nextValue = entry.value === null || entry.value === "" ? null : entry.value;
216
+ setOrRemovePreviewAttribute(captured.element, entry.fullAttr, nextValue);
217
+ }
218
+ },
219
+ persist: () =>
220
+ persistDomEditOperations(selection, ops, {
221
+ label: options.label,
222
+ coalesceKey: commitKey,
223
+ skipRefresh: options.skipRefresh,
224
+ }),
225
+ shouldRevert: () => isLatestCommit(),
226
+ revert: () => {
227
+ if (!captured) return;
228
+ for (const entry of entries) {
229
+ setOrRemovePreviewAttribute(
230
+ captured.element,
231
+ entry.fullAttr,
232
+ captured.previousValues.get(entry.fullAttr) ?? null,
233
+ );
234
+ }
235
+ },
236
+ onError: (error) => reportDomEditPersistFailure(selection, ops, error, showToast),
237
+ shouldResync: () => isLatestCommit() && !!options.refreshAfter,
238
+ resync: () => refreshDomEditSelectionFromPreview(selection),
239
+ onSettled: options.onSettled,
240
+ });
241
+ },
242
+ [
243
+ activeCompPath,
244
+ persistDomEditOperations,
245
+ refreshDomEditSelectionFromPreview,
246
+ showToast,
247
+ previewIframeRef,
248
+ ],
249
+ );
250
+
251
+ const handleDomAttributesCommit = useCallback(
252
+ async (selection: DomEditSelection, attrs: Record<string, string>) => {
253
+ await commitDataAttributes(selection, attrs, {
254
+ label: "Edit timing",
255
+ coalescePrefix: "attrs",
256
+ skipRefresh: false,
257
+ refreshAfter: true,
258
+ });
259
+ },
260
+ [commitDataAttributes],
261
+ );
262
+
143
263
  const handleDomAttributeCommit = useCallback(
144
264
  async (attr: string, value: string) => {
145
265
  await commitDataAttribute(attr, value, {
@@ -153,11 +273,12 @@ export function useDomEditAttributeCommits({
153
273
  );
154
274
 
155
275
  const handleDomAttributeLiveCommit = useCallback(
156
- async (attr: string, value: string | null) => {
276
+ async (attr: string, value: string | null, onSettled?: (ok: boolean) => void) => {
157
277
  await commitDataAttribute(attr, value, {
158
278
  label: `Edit ${attr.replace(/^(data-)?/, "").replace(/-/g, " ")}`,
159
279
  coalescePrefix: "attr-live",
160
280
  skipRefresh: true,
281
+ onSettled,
161
282
  });
162
283
  },
163
284
  [commitDataAttribute],
@@ -223,5 +344,6 @@ export function useDomEditAttributeCommits({
223
344
  handleDomAttributeCommit,
224
345
  handleDomAttributeLiveCommit,
225
346
  handleDomHtmlAttributeCommit,
347
+ handleDomAttributesCommit,
226
348
  };
227
349
  }