@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
@@ -31,8 +31,10 @@ import {
31
31
  useTimelineTrackVisibilityEditing,
32
32
  } from "./timelineTrackVisibility";
33
33
  import { useTimelineGroupEditing } from "./useTimelineGroupEditing";
34
- import { sdkTimingPersist } from "../utils/sdkCutover";
34
+ import { serializeZLaneGesture } from "../components/nle/zLaneGesture";
35
+ import { cutoverCommittedOrThrow, sdkTimingPersist } from "../utils/sdkCutover";
35
36
  import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
37
+ import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
36
38
 
37
39
  type TimelineMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
38
40
  stackingReorder?: TimelineStackingReorderIntent | null;
@@ -52,6 +54,7 @@ export function useTimelineEditing({
52
54
  uploadProjectFiles,
53
55
  isRecordingRef,
54
56
  sdkSession,
57
+ publishSdkSession,
55
58
  forceReloadSdkSession,
56
59
  handleDomZIndexReorderCommitRef,
57
60
  }: UseTimelineEditingOptions) {
@@ -120,124 +123,131 @@ export function useTimelineEditing({
120
123
  recordEdit,
121
124
  reloadPreview,
122
125
  sdkSession,
126
+ publishSdkSession,
123
127
  showToast,
124
128
  writeProjectFile,
125
129
  });
126
-
127
130
  const handleTimelineElementMove = useCallback(
128
131
  // fallow-ignore-next-line complexity
129
132
  (element: TimelineElement, updates: TimelineMoveUpdates) => {
130
- const targetPath = element.sourceFile || activeCompPath || "index.html";
131
- const startChanged = updates.start !== element.start;
132
- // A vertical-only lane move arrives with start unchanged but track changed
133
- // (on this single-element path the drag commit has already folded the
134
- // AUTHORED persist track into updates.track). It must persist like any
135
- // other move early-returning on !startChanged alone silently dropped
136
- // the file write, so the lane snapped back on reload.
137
- const trackChanged = updates.track !== element.track;
133
+ const commitMove = () => {
134
+ const targetPath = element.sourceFile || activeCompPath || "index.html";
135
+ const startChanged = updates.start !== element.start;
136
+ // A vertical-only lane move arrives with start unchanged but track changed
137
+ // (on this single-element path the drag commit has already folded the
138
+ // AUTHORED persist track into updates.track). It must persist like any
139
+ // other move early-returning on !startChanged alone silently dropped
140
+ // the file write, so the lane snapped back on reload.
141
+ const trackChanged = updates.track !== element.track;
138
142
 
139
- if (startChanged || trackChanged) {
140
- const liveAttrs: Array<[string, string]> = [];
141
- if (startChanged) {
142
- liveAttrs.push(["data-start", formatTimelineAttributeNumber(updates.start)]);
143
- }
144
- if (trackChanged) {
145
- liveAttrs.push(["data-track-index", formatTimelineAttributeNumber(updates.track)]);
143
+ if (startChanged || trackChanged) {
144
+ const liveAttrs: Array<[string, string]> = [];
145
+ if (startChanged) {
146
+ liveAttrs.push(["data-start", formatTimelineAttributeNumber(updates.start)]);
147
+ }
148
+ if (trackChanged) {
149
+ liveAttrs.push(["data-track-index", formatTimelineAttributeNumber(updates.track)]);
150
+ }
151
+ patchIframeDomTiming(previewIframeRef.current, element, liveAttrs, activeCompPath);
146
152
  }
147
- patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
148
- }
149
153
 
150
- const reorderDone = applyTimelineStackingReorder({
151
- element,
152
- stackingReorder: updates.stackingReorder,
153
- timelineElements,
154
- iframe: previewIframeRef.current,
155
- activeCompPath,
156
- commit: handleDomZIndexReorderCommitRef?.current,
157
- });
154
+ const reorderDone = applyTimelineStackingReorder({
155
+ element,
156
+ stackingReorder: updates.stackingReorder,
157
+ timelineElements,
158
+ iframe: previewIframeRef.current,
159
+ activeCompPath,
160
+ commit: handleDomZIndexReorderCommitRef?.current,
161
+ });
158
162
 
159
- if (!startChanged && !trackChanged) return reorderDone;
163
+ if (!startChanged && !trackChanged) return reorderDone;
160
164
 
161
- // Snapshot the duration BEFORE the optimistic updates below so a failed
162
- // persist can roll the readout + live root back (see captureDurationRollback).
163
- const rollbackDuration = captureDurationRollback(previewIframeRef.current);
164
- // needsExtension gates the SDK path (setTiming can't grow the root duration), so read the store BEFORE the readout sync below optimistically updates it.
165
- const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
166
- // Optimistic duration readout: content-driven (grow AND shrink), from the just-patched live DOM. See syncPreviewContentDuration.
167
- syncPreviewContentDuration(previewIframeRef.current);
165
+ // Snapshot the duration BEFORE the optimistic updates below so a failed
166
+ // persist can roll the readout + live root back (see captureDurationRollback).
167
+ const rollbackDuration = captureDurationRollback(previewIframeRef.current);
168
+ // needsExtension gates the SDK path (setTiming can't grow the root duration), so read the store BEFORE the readout sync below optimistically updates it.
169
+ const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
170
+ // Optimistic duration readout: content-driven (grow AND shrink), from the just-patched live DOM. See syncPreviewContentDuration.
171
+ syncPreviewContentDuration(previewIframeRef.current);
168
172
 
169
- const buildMovePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
170
- // Persist lane changes too — data-start-only writes let reload snap the lane back.
171
- const track = trackChanged ? updates.track : undefined;
172
- return buildTimelineMoveTimingPatch(
173
- original,
174
- target,
175
- updates.start,
176
- element.duration,
177
- track,
178
- );
179
- };
180
- const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
181
- const moveFallback = () =>
182
- enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() =>
183
- // Soft-reload with the server's rewritten GSAP script — the timing-only move already patched
184
- // DOM + store, so swapping the script avoids the all-clips flash; falls back to reloadPreview().
185
- finishClipTimingFallback({
186
- iframe: previewIframeRef.current,
187
- reloadPreview,
188
- projectId: projectIdRef.current,
189
- targetPath,
190
- domId: element.domId,
191
- label: "Move timeline clip",
192
- coalesceKey,
193
- recordEdit,
194
- edit: { kind: "shift", delta: updates.start - element.start },
195
- }),
196
- );
197
- return reorderDone
198
- .then(() => {
199
- // The SDK setTiming path writes start only — a lane change must take
200
- // the fallback, whose patch builder writes data-track-index too.
201
- if (sdkSession && element.hfId && !needsExtension && !trackChanged) {
202
- return sdkTimingPersist(
203
- element.hfId,
173
+ const buildMovePatches: PersistTimelineEditInput["buildPatches"] = (original, target) => {
174
+ // Persist lane changes too — data-start-only writes let reload snap the lane back.
175
+ const track = trackChanged ? updates.track : undefined;
176
+ return buildTimelineMoveTimingPatch(
177
+ original,
178
+ target,
179
+ updates.start,
180
+ element.duration,
181
+ track,
182
+ );
183
+ };
184
+ const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
185
+ const moveFallback = () =>
186
+ enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(() =>
187
+ // Soft-reload with the server's rewritten GSAP script — the timing-only move already patched
188
+ // DOM + store, so swapping the script avoids the all-clips flash; falls back to reloadPreview().
189
+ finishClipTimingFallback({
190
+ iframe: previewIframeRef.current,
191
+ reloadPreview,
192
+ projectId: projectIdRef.current,
204
193
  targetPath,
205
- { start: updates.start },
206
- sdkSession,
207
- {
208
- editHistory: { recordEdit },
209
- writeProjectFile,
210
- reloadPreview,
211
- domEditSaveTimestampRef,
212
- compositionPath: activeCompPath,
213
- // Capture on-disk bytes as the undo `before` so undoing a timing move
214
- // restores the file verbatim, not a normalized full-DOM re-emit.
215
- readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
216
- },
217
- { label: "Move timeline clip", coalesceKey },
218
- ).then((handled) => {
219
- if (!handled) return moveFallback();
220
- });
221
- }
222
- return moveFallback();
223
- })
224
- .catch((error) => {
225
- // Failed persist: revert the optimistic duration readout + live root.
226
- rollbackDuration();
227
- throw error;
228
- });
194
+ domId: element.domId,
195
+ label: "Move timeline clip",
196
+ coalesceKey,
197
+ recordEdit,
198
+ edit: { kind: "shift", delta: updates.start - element.start },
199
+ }),
200
+ );
201
+ return reorderDone
202
+ .then(() => {
203
+ // The SDK setTiming path writes start only — a lane change must take
204
+ // the fallback, whose patch builder writes data-track-index too.
205
+ if (sdkSession && element.hfId && !needsExtension && !trackChanged) {
206
+ return sdkTimingPersist(
207
+ element.hfId,
208
+ targetPath,
209
+ { start: updates.start },
210
+ sdkSession,
211
+ {
212
+ editHistory: { recordEdit },
213
+ writeProjectFile,
214
+ reloadPreview,
215
+ domEditSaveTimestampRef,
216
+ compositionPath: activeCompPath,
217
+ // Capture on-disk bytes as the undo `before` so undoing a timing move
218
+ // restores the file verbatim, not a normalized full-DOM re-emit.
219
+ readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
220
+ publishSession: publishSdkSession,
221
+ },
222
+ { label: "Move timeline clip", coalesceKey },
223
+ ).then((result) => {
224
+ if (!cutoverCommittedOrThrow(result)) return moveFallback();
225
+ });
226
+ }
227
+ return moveFallback();
228
+ })
229
+ .catch((error) => {
230
+ // Failed persist: revert the optimistic duration readout + live root.
231
+ rollbackDuration();
232
+ showToast(getStudioSaveErrorMessage(error), "error");
233
+ throw error;
234
+ });
235
+ };
236
+ return updates.stackingReorder ? serializeZLaneGesture(commitMove) : commitMove();
229
237
  },
230
238
  [
231
239
  previewIframeRef,
232
240
  enqueueEdit,
233
241
  activeCompPath,
234
242
  sdkSession,
243
+ publishSdkSession,
235
244
  recordEdit,
236
245
  writeProjectFile,
237
246
  reloadPreview,
238
247
  domEditSaveTimestampRef,
239
248
  timelineElements,
240
249
  handleDomZIndexReorderCommitRef,
250
+ showToast,
241
251
  ],
242
252
  );
243
253
 
@@ -251,9 +261,6 @@ export function useTimelineEditing({
251
261
  ["data-start", formatTimelineAttributeNumber(updates.start)],
252
262
  ["data-duration", formatTimelineAttributeNumber(updates.duration)],
253
263
  ];
254
- // Patch the live playback-start/media-start attr too, or a resize that
255
- // trims the playback start leaves the preview showing the old in-point
256
- // until the next reload (the persisted patch handles it via pbs below).
257
264
  if (updates.playbackStart != null) {
258
265
  const liveAttr =
259
266
  element.playbackStartAttr === "playback-start"
@@ -261,7 +268,7 @@ export function useTimelineEditing({
261
268
  : "data-media-start";
262
269
  liveAttrs.push([liveAttr, formatTimelineAttributeNumber(updates.playbackStart)]);
263
270
  }
264
- patchIframeDomTiming(previewIframeRef.current, element, liveAttrs);
271
+ patchIframeDomTiming(previewIframeRef.current, element, liveAttrs, activeCompPath);
265
272
  // Snapshot the duration BEFORE the optimistic updates below so a failed
266
273
  // persist can roll the readout + live root back (see captureDurationRollback).
267
274
  const rollbackDuration = captureDurationRollback(previewIframeRef.current);
@@ -315,15 +322,17 @@ export function useTimelineEditing({
315
322
  // Capture on-disk bytes as the undo `before` so undoing a timing
316
323
  // resize restores the file verbatim, not a normalized full-DOM re-emit.
317
324
  readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
325
+ publishSession: publishSdkSession,
318
326
  },
319
327
  { label: "Resize timeline clip", coalesceKey },
320
- ).then((handled) => {
321
- if (!handled) return resizeFallback();
328
+ ).then((result) => {
329
+ if (!cutoverCommittedOrThrow(result)) return resizeFallback();
322
330
  })
323
331
  : resizeFallback();
324
332
  return persistDone.catch((error) => {
325
333
  // Failed persist: revert the optimistic duration readout + live root.
326
334
  rollbackDuration();
335
+ showToast(getStudioSaveErrorMessage(error), "error");
327
336
  throw error;
328
337
  });
329
338
  },
@@ -332,10 +341,12 @@ export function useTimelineEditing({
332
341
  enqueueEdit,
333
342
  activeCompPath,
334
343
  sdkSession,
344
+ publishSdkSession,
335
345
  recordEdit,
336
346
  writeProjectFile,
337
347
  reloadPreview,
338
348
  domEditSaveTimestampRef,
349
+ showToast,
339
350
  ],
340
351
  );
341
352
 
@@ -356,7 +367,6 @@ export function useTimelineEditing({
356
367
  const handleToggleElementHidden = useTimelineElementVisibilityEditing({
357
368
  projectIdRef,
358
369
  activeCompPath,
359
- timelineElements,
360
370
  showToast,
361
371
  writeProjectFile,
362
372
  recordEdit,
@@ -2,6 +2,7 @@ import type { MutableRefObject, RefObject } from "react";
2
2
  import type { Composition } from "@hyperframes/sdk";
3
3
  import type { TimelineElement } from "../player";
4
4
  import type { EditHistoryKind } from "../utils/editHistory";
5
+ import type { PublishSdkSession } from "../utils/sdkCutover";
5
6
 
6
7
  interface RecordEditInput {
7
8
  label: string;
@@ -23,7 +24,7 @@ export type TimelineZIndexReorderCommit = (
23
24
  key?: string;
24
25
  }>,
25
26
  coalesceKey?: string,
26
- ) => Promise<void>;
27
+ ) => Promise<import("./domEditCommitTypes").DomEditPatchBatchesResult | undefined | void>;
27
28
 
28
29
  export interface UseTimelineEditingOptions {
29
30
  projectId: string | null;
@@ -40,6 +41,8 @@ export interface UseTimelineEditingOptions {
40
41
  isRecordingRef?: RefObject<boolean>;
41
42
  /** Stage 7 §3.2: SDK session for routing timing ops through setTiming. */
42
43
  sdkSession?: Composition | null;
44
+ /** Publish a fully persisted candidate SDK session. */
45
+ publishSdkSession?: PublishSdkSession;
43
46
  /** Resync the SDK session after a server-authoritative timeline write. */
44
47
  forceReloadSdkSession?: () => void;
45
48
  handleDomZIndexReorderCommitRef?: MutableRefObject<TimelineZIndexReorderCommit | null>;
@@ -1,7 +1,11 @@
1
1
  import { useCallback, type MutableRefObject, type RefObject } from "react";
2
2
  import type { Composition } from "@hyperframes/sdk";
3
3
  import type { TimelineElement } from "../player";
4
- import { sdkTimingBatchPersist } from "../utils/sdkCutover";
4
+ import {
5
+ cutoverCommittedOrThrow,
6
+ sdkTimingBatchPersist,
7
+ type PublishSdkSession,
8
+ } from "../utils/sdkCutover";
5
9
  import {
6
10
  buildTimelineMoveTimingPatch,
7
11
  buildTimelineResizeTimingPatch,
@@ -20,6 +24,7 @@ import {
20
24
  shiftGsapPositions,
21
25
  syncPreviewContentDuration,
22
26
  } from "./timelineTimingSync";
27
+ import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
23
28
 
24
29
  export interface TimelineGroupMoveChange {
25
30
  element: TimelineElement;
@@ -37,6 +42,8 @@ export interface TimelineGroupResizeChange {
37
42
  export interface TimelineGroupCommitOptions {
38
43
  beforeTiming?: Promise<void>;
39
44
  coalesceKey?: string;
45
+ /** Per-entry undo coalesce window override (ms) — see EditHistoryEntry.coalesceMs. */
46
+ coalesceMs?: number;
40
47
  }
41
48
 
42
49
  interface UseTimelineGroupEditingOptions {
@@ -51,6 +58,7 @@ interface UseTimelineGroupEditingOptions {
51
58
  recordEdit: (input: RecordEditInput) => Promise<void>;
52
59
  reloadPreview: () => void;
53
60
  sdkSession?: Composition | null;
61
+ publishSdkSession?: PublishSdkSession;
54
62
  showToast: (message: string, tone?: "error" | "info") => void;
55
63
  writeProjectFile: (path: string, content: string) => Promise<void>;
56
64
  }
@@ -106,6 +114,7 @@ export function useTimelineGroupEditing({
106
114
  recordEdit,
107
115
  reloadPreview,
108
116
  sdkSession,
117
+ publishSdkSession,
109
118
  showToast,
110
119
  writeProjectFile,
111
120
  }: UseTimelineGroupEditingOptions) {
@@ -137,6 +146,7 @@ export function useTimelineGroupEditing({
137
146
  label: string,
138
147
  batchChanges: PersistTimelineBatchChange[],
139
148
  coalesceKey: string,
149
+ coalesceMs?: number,
140
150
  ) => {
141
151
  await persistTimelineBatchEdit({
142
152
  projectId,
@@ -148,6 +158,7 @@ export function useTimelineGroupEditing({
148
158
  domEditSaveTimestampRef,
149
159
  pendingTimelineEditPathRef,
150
160
  coalesceKey,
161
+ coalesceMs,
151
162
  });
152
163
  forceReloadSdkSession?.();
153
164
  },
@@ -176,6 +187,7 @@ export function useTimelineGroupEditing({
176
187
  needsExtension: boolean;
177
188
  label: string;
178
189
  coalesceKey: string;
190
+ coalesceMs?: number;
179
191
  }): Promise<boolean> => {
180
192
  const sharedPath = allChangesSharePath(input.changes, activeCompPath);
181
193
  const canUseSdk =
@@ -184,7 +196,7 @@ export function useTimelineGroupEditing({
184
196
  input.eligible &&
185
197
  input.sdkChanges.every((change) => change !== null);
186
198
  if (!canUseSdk) return false;
187
- return sdkTimingBatchPersist(
199
+ const result = await sdkTimingBatchPersist(
188
200
  input.sdkChanges.filter((change): change is NonNullable<typeof change> => change !== null),
189
201
  sharedPath,
190
202
  sdkSession,
@@ -195,14 +207,17 @@ export function useTimelineGroupEditing({
195
207
  domEditSaveTimestampRef,
196
208
  compositionPath: activeCompPath,
197
209
  readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
210
+ publishSession: publishSdkSession,
198
211
  },
199
- { label: input.label, coalesceKey: input.coalesceKey },
212
+ { label: input.label, coalesceKey: input.coalesceKey, coalesceMs: input.coalesceMs },
200
213
  );
214
+ return cutoverCommittedOrThrow(result);
201
215
  },
202
216
  [
203
217
  activeCompPath,
204
218
  domEditSaveTimestampRef,
205
219
  projectIdRef,
220
+ publishSdkSession,
206
221
  recordEdit,
207
222
  reloadPreview,
208
223
  sdkSession,
@@ -220,20 +235,38 @@ export function useTimelineGroupEditing({
220
235
  if (change.track != null) {
221
236
  attrs.push(["data-track-index", formatTimelineAttributeNumber(change.track)]);
222
237
  }
223
- patchIframeDomTiming(previewIframeRef.current, change.element, attrs);
238
+ patchIframeDomTiming(previewIframeRef.current, change.element, attrs, activeCompPath);
224
239
  }
225
240
 
241
+ // TRACK-ONLY batch: every change keeps its start (moves never carry a
242
+ // duration change), so nothing timing-related changed — the batch only
243
+ // rewrites data-track-index, which the renderer never reads (documented
244
+ // in core runtime/timeline.ts; track is a studio lane concept). The live
245
+ // DOM patch above + the gesture owner's optimistic store update fully
246
+ // cover the UI, so after the persist there is nothing to GSAP-shift and
247
+ // nothing for the preview to recompute: skip the fallback below entirely.
248
+ // Running it anyway is what made the mirrored z-order lane move blink —
249
+ // a zero-delta batch yields no scriptText, and finishGroupTimingGsapFallback
250
+ // used to full-reload the iframe when there was no script to soft-swap
251
+ // (it now rebinds the runtime timing in place, but a track-only batch
252
+ // needs NO preview sync at all, so the skip stays).
253
+ const trackOnly = changes.every((change) => change.start === change.element.start);
254
+
226
255
  const maxEnd = Math.max(...changes.map((change) => change.start + change.element.duration));
227
256
  // Snapshot the duration BEFORE the optimistic updates below so a failed
228
257
  // persist can roll the readout + live root back (see captureDurationRollback).
229
258
  const rollbackDuration = captureDurationRollback(previewIframeRef.current);
230
259
  // needsExtension gates the SDK path (setTiming can't grow the root duration),
231
260
  // so read the store BEFORE the readout sync below optimistically updates it.
261
+ // Track-only batches leave every clip end unchanged, so both this and the
262
+ // readout sync below are provable no-ops there — kept unconditional so the
263
+ // duration machinery stays on one code path.
232
264
  const needsExtension = extendRootDurationIfNeeded(maxEnd);
233
265
  // Optimistic duration readout: content-driven (grow AND shrink), read from
234
266
  // the just-patched live DOM. See syncPreviewContentDuration.
235
267
  syncPreviewContentDuration(previewIframeRef.current);
236
268
  const coalesceKey = options?.coalesceKey ?? moveCoalesceKey(changes);
269
+ const coalesceMs = options?.coalesceMs;
237
270
  return enqueueGroupOperation("Move timeline clips", async (projectId) => {
238
271
  await options?.beforeTiming;
239
272
  const handledBySdk = await trySdkBatchPersist({
@@ -243,6 +276,7 @@ export function useTimelineGroupEditing({
243
276
  needsExtension,
244
277
  label: "Move timeline clips",
245
278
  coalesceKey,
279
+ coalesceMs,
246
280
  });
247
281
  if (handledBySdk) return;
248
282
 
@@ -261,7 +295,12 @@ export function useTimelineGroupEditing({
261
295
  ),
262
296
  })),
263
297
  coalesceKey,
298
+ coalesceMs,
264
299
  );
300
+ // Track-only: no timing delta → no GSAP positions to shift and no
301
+ // reload (see the trackOnly doc above). Mixed batches (any start
302
+ // change) keep the full fallback below.
303
+ if (trackOnly) return;
265
304
  await finishGroupTimingGsapFallback({
266
305
  projectId,
267
306
  iframe: previewIframeRef.current,
@@ -284,6 +323,7 @@ export function useTimelineGroupEditing({
284
323
  // Failed persist: revert the optimistic duration readout + live root
285
324
  // alongside the gesture owner's store rollback.
286
325
  rollbackDuration();
326
+ showToast(getStudioSaveErrorMessage(error), "error");
287
327
  throw error;
288
328
  });
289
329
  },
@@ -295,6 +335,7 @@ export function useTimelineGroupEditing({
295
335
  recordEdit,
296
336
  reloadPreview,
297
337
  trySdkBatchPersist,
338
+ showToast,
298
339
  ],
299
340
  );
300
341
 
@@ -313,7 +354,7 @@ export function useTimelineGroupEditing({
313
354
  : "data-media-start";
314
355
  liveAttrs.push([liveAttr, formatTimelineAttributeNumber(change.playbackStart)]);
315
356
  }
316
- patchIframeDomTiming(previewIframeRef.current, change.element, liveAttrs);
357
+ patchIframeDomTiming(previewIframeRef.current, change.element, liveAttrs, activeCompPath);
317
358
  }
318
359
 
319
360
  const maxEnd = Math.max(...changes.map((change) => change.start + change.duration));
@@ -327,6 +368,7 @@ export function useTimelineGroupEditing({
327
368
  // the just-patched live DOM. See syncPreviewContentDuration.
328
369
  syncPreviewContentDuration(previewIframeRef.current);
329
370
  const coalesceKey = options?.coalesceKey ?? resizeCoalesceKey(changes);
371
+ const coalesceMs = options?.coalesceMs;
330
372
  return enqueueGroupOperation("Resize timeline clips", async (projectId) => {
331
373
  await options?.beforeTiming;
332
374
  const handledBySdk = await trySdkBatchPersist({
@@ -339,6 +381,7 @@ export function useTimelineGroupEditing({
339
381
  needsExtension,
340
382
  label: "Resize timeline clips",
341
383
  coalesceKey,
384
+ coalesceMs,
342
385
  });
343
386
  if (handledBySdk) return;
344
387
 
@@ -355,6 +398,7 @@ export function useTimelineGroupEditing({
355
398
  }),
356
399
  })),
357
400
  coalesceKey,
401
+ coalesceMs,
358
402
  );
359
403
  await finishGroupTimingGsapFallback({
360
404
  projectId,
@@ -387,6 +431,7 @@ export function useTimelineGroupEditing({
387
431
  // Failed persist: revert the optimistic duration readout + live root
388
432
  // alongside the gesture owner's store rollback.
389
433
  rollbackDuration();
434
+ showToast(getStudioSaveErrorMessage(error), "error");
390
435
  throw error;
391
436
  });
392
437
  },
@@ -398,6 +443,7 @@ export function useTimelineGroupEditing({
398
443
  recordEdit,
399
444
  reloadPreview,
400
445
  trySdkBatchPersist,
446
+ showToast,
401
447
  ],
402
448
  );
403
449
 
@@ -1,6 +1,6 @@
1
1
  import { useCallback } from "react";
2
2
  import type { Composition } from "@hyperframes/sdk";
3
- import { persistSdkSerialize } from "../utils/sdkCutover";
3
+ import { cutoverCommittedOrThrow, persistSdkCandidateMutation } from "../utils/sdkCutover";
4
4
  import type { UseSlideshowPersistParams } from "./useSlideshowPersist";
5
5
 
6
6
  /** Same single-writer dependency set the slideshow persist path uses. */
@@ -21,6 +21,7 @@ export function useVariablesPersist({
21
21
  recordEdit,
22
22
  reloadPreview,
23
23
  domEditSaveTimestampRef,
24
+ publishSdkSession,
24
25
  }: UseVariablesPersistParams): (
25
26
  label: string,
26
27
  mutate: (session: Composition) => void,
@@ -30,11 +31,8 @@ export function useVariablesPersist({
30
31
  if (!sdkSession) return false;
31
32
  const path = activeCompPath ?? "index.html";
32
33
  const originalContent = await readProjectFile(path);
33
- mutate(sdkSession);
34
- const after = sdkSession.serialize();
35
- if (after === originalContent) return false;
36
- await persistSdkSerialize(
37
- after,
34
+ const result = await persistSdkCandidateMutation(
35
+ sdkSession,
38
36
  path,
39
37
  originalContent,
40
38
  {
@@ -43,10 +41,13 @@ export function useVariablesPersist({
43
41
  reloadPreview,
44
42
  domEditSaveTimestampRef,
45
43
  compositionPath: activeCompPath,
44
+ readProjectFile,
45
+ publishSession: publishSdkSession,
46
46
  },
47
+ mutate,
47
48
  { label },
48
49
  );
49
- return true;
50
+ return cutoverCommittedOrThrow(result);
50
51
  },
51
52
  [
52
53
  sdkSession,
@@ -56,6 +57,7 @@ export function useVariablesPersist({
56
57
  recordEdit,
57
58
  reloadPreview,
58
59
  domEditSaveTimestampRef,
60
+ publishSdkSession,
59
61
  ],
60
62
  );
61
63
  }
@@ -21,6 +21,7 @@ import {
21
21
  ArrowClockwise,
22
22
  Gear,
23
23
  Scissors as PhScissors,
24
+ Link as PhLink,
24
25
  } from "@phosphor-icons/react";
25
26
  import type { Icon as PhosphorIcon, IconProps as PhosphorIconProps } from "@phosphor-icons/react";
26
27
 
@@ -69,3 +70,4 @@ export const Camera = makeIcon(PhCamera);
69
70
  export const RotateCw = makeIcon(ArrowClockwise);
70
71
  export const Settings = makeIcon(Gear);
71
72
  export const Scissors = makeIcon(PhScissors);
73
+ export const Link = makeIcon(PhLink);