@hyperframes/studio 0.8.114 → 0.8.116

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 (266) hide show
  1. package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
  2. package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
  3. package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
  4. package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
  5. package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
  6. package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
  7. package/dist/chunk-3F47EFCT.js.map +1 -0
  8. package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
  9. package/dist/chunk-5EKWJZLM.js.map +1 -0
  10. package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
  11. package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
  12. package/dist/index.d.ts +146 -40
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +4021 -3591
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles.css +1 -1
  18. package/package.json +9 -8
  19. package/src/App.tsx +2 -2
  20. package/src/captions/hooks/useCaptionSync.ts +10 -6
  21. package/src/components/LinkedSelectionToggle.tsx +3 -2
  22. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  23. package/src/components/dock/Dock.tsx +5 -7
  24. package/src/components/editor/CanvasContextMenu.tsx +3 -2
  25. package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
  26. package/src/components/editor/DomEditCropHandles.tsx +7 -4
  27. package/src/components/editor/DomEditOverlay.tsx +5 -1
  28. package/src/components/editor/FileTreeNodes.tsx +13 -10
  29. package/src/components/editor/MotionPathOverlay.tsx +7 -2
  30. package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
  31. package/src/components/editor/SnapToolbar.test.tsx +100 -1
  32. package/src/components/editor/SnapToolbar.tsx +43 -5
  33. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
  34. package/src/components/editor/audioFxTelemetry.ts +2 -3
  35. package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
  36. package/src/components/editor/cropResize.ts +4 -3
  37. package/src/components/editor/domEditingDom.ts +4 -0
  38. package/src/components/editor/domEditingLayers.ts +1 -5
  39. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  40. package/src/components/editor/domGroupMoveBy.ts +95 -0
  41. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  42. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  43. package/src/components/editor/manualEditingAvailability.ts +8 -0
  44. package/src/components/editor/manualEdits.ts +1 -0
  45. package/src/components/editor/manualEditsParsing.ts +7 -0
  46. package/src/components/editor/manualOffsetDrag.ts +5 -6
  47. package/src/components/editor/marqueeCommit.test.tsx +106 -37
  48. package/src/components/editor/marqueeCommit.ts +24 -8
  49. package/src/components/editor/motionPathCommit.test.ts +29 -1
  50. package/src/components/editor/motionPathCommit.ts +21 -9
  51. package/src/components/editor/motionPathSelection.test.ts +6 -31
  52. package/src/components/editor/probeSourceElement.test.ts +45 -0
  53. package/src/components/editor/probeSourceElement.ts +41 -16
  54. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
  55. package/src/components/editor/propertyPanelFont.tsx +3 -2
  56. package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
  57. package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
  58. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
  59. package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
  60. package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
  61. package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
  62. package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
  63. package/src/components/editor/propertyPanelFxSection.tsx +30 -28
  64. package/src/components/editor/propertyPanelTypes.ts +6 -3
  65. package/src/components/editor/useColorGradingController.ts +2 -1
  66. package/src/components/editor/useDomEditNudge.test.tsx +37 -0
  67. package/src/components/editor/useDomEditNudge.ts +2 -0
  68. package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
  69. package/src/components/editor/useDomEditZOrder.ts +12 -2
  70. package/src/components/editor/useFxCarveNodes.ts +2 -1
  71. package/src/components/editor/useFxLevelling.ts +2 -1
  72. package/src/components/feedback/projectProvenance.ts +2 -1
  73. package/src/components/nle/NLEContext.test.ts +1 -0
  74. package/src/components/nle/NLEContext.tsx +2 -1
  75. package/src/components/nle/NLEPreview.tsx +12 -10
  76. package/src/components/nle/PreviewPoster.tsx +2 -1
  77. package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
  78. package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
  79. package/src/components/renders/useFfmpegStatus.ts +2 -1
  80. package/src/components/renders/useRenderQueue.ts +5 -4
  81. package/src/components/sidebar/AssetContextMenu.tsx +4 -4
  82. package/src/components/sidebar/AssetsTab.tsx +2 -1
  83. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  84. package/src/components/ui/menuStyle.test.ts +61 -0
  85. package/src/components/ui/menuStyle.ts +10 -0
  86. package/src/contexts/DomEditContext.tsx +5 -1
  87. package/src/contexts/TimelineEditContext.tsx +1 -0
  88. package/src/domEditExports.test.tsx +9 -1
  89. package/src/hooks/domEditCommitRunner.ts +1 -1
  90. package/src/hooks/domSelectionTestHarness.ts +23 -0
  91. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  92. package/src/hooks/domStyleCommit.ts +8 -1
  93. package/src/hooks/draggedGsapPosition.ts +10 -0
  94. package/src/hooks/elementOffsetStager.ts +5 -4
  95. package/src/hooks/gsapDragCommit.test.ts +75 -164
  96. package/src/hooks/gsapDragCommit.ts +4 -125
  97. package/src/hooks/gsapDragPositionCommit.ts +36 -384
  98. package/src/hooks/gsapEditOutcome.ts +22 -2
  99. package/src/hooks/gsapGestureOutcome.test.ts +161 -0
  100. package/src/hooks/gsapGestureOutcome.ts +64 -0
  101. package/src/hooks/gsapGroupPreflight.ts +34 -0
  102. package/src/hooks/gsapKeyframeCommit.ts +3 -12
  103. package/src/hooks/gsapMutationClient.ts +4 -3
  104. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  105. package/src/hooks/gsapParsedTween.test.ts +62 -0
  106. package/src/hooks/gsapParsedTween.ts +243 -0
  107. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  108. package/src/hooks/gsapPositionDetection.ts +52 -33
  109. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  110. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  111. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  112. package/src/hooks/gsapResizeIntercept.test.ts +96 -11
  113. package/src/hooks/gsapResizeIntercept.ts +79 -168
  114. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  115. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  116. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  117. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  118. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  119. package/src/hooks/gsapScriptCommitTypes.ts +13 -0
  120. package/src/hooks/gsapShared.ts +7 -0
  121. package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
  122. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  123. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  124. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  125. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  126. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  127. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  128. package/src/hooks/keyframeCacheAstLoad.ts +8 -6
  129. package/src/hooks/newTweenTarget.test.ts +41 -76
  130. package/src/hooks/plainRotation.ts +3 -2
  131. package/src/hooks/previewPromotionSelection.test.tsx +1 -1
  132. package/src/hooks/refreshTimelineRowText.test.ts +65 -0
  133. package/src/hooks/refreshTimelineRowText.ts +29 -0
  134. package/src/hooks/timelineTimingSync.test.ts +1 -0
  135. package/src/hooks/timelineTimingSync.ts +2 -1
  136. package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
  137. package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
  138. package/src/hooks/useAskAgentModal.ts +2 -1
  139. package/src/hooks/useBlockCatalog.ts +2 -1
  140. package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
  141. package/src/hooks/useCaptionDetection.ts +2 -1
  142. package/src/hooks/useCompositionContentLoader.ts +2 -1
  143. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  144. package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
  145. package/src/hooks/useDomEditSession.test.tsx +30 -1
  146. package/src/hooks/useDomEditSession.ts +10 -0
  147. package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
  148. package/src/hooks/useDomEditTextCommits.ts +13 -3
  149. package/src/hooks/useDomEditWiring.ts +4 -2
  150. package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
  151. package/src/hooks/useDomGeometryCommit.ts +8 -4
  152. package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
  153. package/src/hooks/useDomGeometryCommits.ts +3 -2
  154. package/src/hooks/useDomSelection.test.ts +26 -0
  155. package/src/hooks/useDomSelection.ts +10 -3
  156. package/src/hooks/useDomSelectionTypes.ts +6 -1
  157. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  158. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  159. package/src/hooks/useEditHistoryActions.ts +16 -5
  160. package/src/hooks/useElementLifecycleOps.ts +2 -1
  161. package/src/hooks/useEnableKeyframes.test.ts +34 -3
  162. package/src/hooks/useEnableKeyframes.ts +7 -4
  163. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
  164. package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
  165. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  166. package/src/hooks/useFileManager.ts +14 -10
  167. package/src/hooks/useFileTree.ts +3 -2
  168. package/src/hooks/useFrameCapture.ts +5 -1
  169. package/src/hooks/useFreezeFrame.ts +3 -2
  170. package/src/hooks/useGestureCommit.test.tsx +225 -55
  171. package/src/hooks/useGestureCommit.ts +86 -55
  172. package/src/hooks/useGroupCommits.ts +2 -1
  173. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  174. package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
  175. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  176. package/src/hooks/useGsapAwareEditing.ts +101 -195
  177. package/src/hooks/useGsapAwareGroupMove.ts +179 -0
  178. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  179. package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
  180. package/src/hooks/useGsapKeyframeOps.ts +36 -10
  181. package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
  182. package/src/hooks/useGsapScriptCommits.ts +15 -10
  183. package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
  184. package/src/hooks/useGsapSelectionHandlers.ts +24 -18
  185. package/src/hooks/useGsapTweenCache.ts +18 -12
  186. package/src/hooks/useLintModal.ts +2 -1
  187. package/src/hooks/usePersistentEditHistory.test.ts +277 -1
  188. package/src/hooks/usePersistentEditHistory.ts +47 -8
  189. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
  190. package/src/hooks/usePreviewInteraction.test.ts +21 -0
  191. package/src/hooks/usePreviewInteraction.ts +2 -0
  192. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  193. package/src/hooks/usePreviewPersistence.ts +13 -11
  194. package/src/hooks/useProjectFileWriter.ts +20 -19
  195. package/src/hooks/useRemoveBackground.ts +2 -1
  196. package/src/hooks/useRenderClipContent.test.ts +18 -0
  197. package/src/hooks/useRenderClipContent.ts +4 -5
  198. package/src/hooks/useSdkSession.ts +2 -1
  199. package/src/hooks/useServerConnection.ts +4 -3
  200. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  201. package/src/hooks/useThumbnailStill.test.ts +100 -0
  202. package/src/hooks/useThumbnailStill.ts +37 -0
  203. package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
  204. package/src/hooks/useTimelineDeleteOps.ts +22 -27
  205. package/src/hooks/useTimelineEditing.test.tsx +1 -1
  206. package/src/index.ts +4 -0
  207. package/src/player/components/AudioWaveform.tsx +3 -2
  208. package/src/player/components/AutomationSelectionMenu.tsx +3 -2
  209. package/src/player/components/ClipContextMenu.test.tsx +73 -0
  210. package/src/player/components/ClipContextMenu.tsx +33 -31
  211. package/src/player/components/CompositionThumbnail.tsx +2 -1
  212. package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
  213. package/src/player/components/OutOfSyncBadge.tsx +5 -5
  214. package/src/player/components/TextClipContent.test.ts +19 -0
  215. package/src/player/components/TextClipContent.tsx +27 -0
  216. package/src/player/components/TrackGapContextMenu.tsx +3 -6
  217. package/src/player/components/clipMenuNormalize.ts +2 -1
  218. package/src/player/components/clipMenuToolItems.test.tsx +23 -0
  219. package/src/player/components/clipMenuToolItems.tsx +13 -9
  220. package/src/player/components/clipPeakMap.ts +2 -1
  221. package/src/player/components/timelineCallbacks.ts +1 -0
  222. package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
  223. package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
  224. package/src/player/components/timelineClipGestureHandlers.ts +20 -2
  225. package/src/player/components/timelineEditing.ts +3 -1
  226. package/src/player/components/timelineLinkSelection.ts +35 -1
  227. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
  228. package/src/player/components/useTimelineClipDrag.ts +6 -7
  229. package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
  230. package/src/player/hooks/timelinePlayerSync.ts +6 -1
  231. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  232. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  233. package/src/player/lib/thumbnailScheduler.ts +10 -2
  234. package/src/player/lib/timelineElementHelpers.test.ts +87 -1
  235. package/src/player/lib/timelineElementHelpers.ts +6 -15
  236. package/src/player/lib/timelineText.ts +40 -0
  237. package/src/player/sceneSwap.ts +2 -1
  238. package/src/player/store/timelineElement.ts +12 -0
  239. package/src/styles/theme.css +3 -0
  240. package/src/telemetry/client.ts +2 -1
  241. package/src/utils/blockInstaller.ts +2 -1
  242. package/src/utils/elementGsap.test.ts +9 -0
  243. package/src/utils/elementGsap.ts +7 -0
  244. package/src/utils/gsapSoftReload.ts +2 -3
  245. package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
  246. package/src/utils/gsapUndoRestore.ts +3 -6
  247. package/src/utils/keyframeUsage.ts +79 -0
  248. package/src/utils/previewFeatureUsage.test.ts +32 -0
  249. package/src/utils/previewFeatureUsage.ts +46 -0
  250. package/src/utils/previewPrefetch.ts +2 -1
  251. package/src/utils/razorSplitTransaction.ts +12 -8
  252. package/src/utils/softReloadTargets.ts +1 -3
  253. package/src/utils/studioApiFetch.test.ts +75 -0
  254. package/src/utils/studioApiFetch.ts +13 -0
  255. package/src/utils/studioFileHistory.ts +4 -1
  256. package/src/utils/studioHelpers.ts +7 -1
  257. package/src/utils/studioPendingEdits.test.ts +23 -6
  258. package/src/utils/studioPendingEdits.ts +32 -8
  259. package/src/utils/studioTelemetry.test.ts +107 -0
  260. package/src/utils/studioTelemetry.ts +51 -5
  261. package/src/utils/timelineCompositionInsert.ts +3 -2
  262. package/src/webmcp/StudioAgentTools.tsx +4 -3
  263. package/dist/chunk-BIUXP3PG.js.map +0 -1
  264. package/dist/chunk-K47E2ZJA.js.map +0 -1
  265. /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
  266. /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -127,7 +127,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
127
127
  y: 20,
128
128
  });
129
129
 
130
- expect(outcome).toEqual({ ok: true });
130
+ expect(outcome).toEqual({ ok: true, changed: true });
131
131
  expect(mutations).toEqual([
132
132
  expect.objectContaining({ type: "add", targetSelector: "#card", method: "set" }),
133
133
  ]);
@@ -146,7 +146,14 @@ describe("useDomGeometryCommit, from the package entry", () => {
146
146
  const url = String(input instanceof Request ? input.url : input);
147
147
  calls.urls.push(url);
148
148
  if (url.includes("/patch-element/")) calls.patches.push(JSON.parse(String(init?.body)));
149
- const saved = { ok: true, changed: true, matched: true, content: "AFTER", version: "v2" };
149
+ const saved = {
150
+ ok: true,
151
+ changed: true,
152
+ matched: true,
153
+ path: "index.html",
154
+ content: "AFTER",
155
+ version: "v2",
156
+ };
150
157
  const body = url.includes("/files/") ? { content: SOURCE } : saved;
151
158
  const status = url.includes("/patch-element/") ? patchStatus : 200;
152
159
  return new Response(JSON.stringify(body), {
@@ -163,7 +170,10 @@ describe("useDomGeometryCommit, from the package entry", () => {
163
170
  const { element, recordEdit, hook, unmount } = renderHost();
164
171
 
165
172
  const card = makeSelection("card", element);
166
- await expect(hook().commitPathOffset(card, { x: 130.5, y: 90 })).resolves.toEqual({ ok: true });
173
+ await expect(hook().commitPathOffset(card, { x: 130.5, y: 90 })).resolves.toEqual({
174
+ ok: true,
175
+ changed: true,
176
+ });
167
177
 
168
178
  expect(element.style.getPropertyValue("translate")).toBe("130.5px 90px");
169
179
  expect(calls.patches).toEqual([
@@ -185,7 +195,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
185
195
 
186
196
  await expect(
187
197
  hook().commitBoxSize(makeSelection("card", element), { width: 300, height: 90 }),
188
- ).resolves.toEqual({ ok: true });
198
+ ).resolves.toEqual({ ok: true, changed: true });
189
199
 
190
200
  expect(element.style.getPropertyValue("width")).toBe("300px");
191
201
  expect(calls.patches).toEqual([
@@ -247,7 +257,7 @@ describe("useDomGeometryCommit, from the package entry", () => {
247
257
  { x: 1, y: 2 },
248
258
  { plainTranslate: true },
249
259
  ),
250
- ).resolves.toEqual({ ok: true });
260
+ ).resolves.toEqual({ ok: true, changed: true });
251
261
  expect(calls.patches).toHaveLength(1);
252
262
  unmount();
253
263
  });
@@ -396,7 +406,14 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
396
406
  headers: { "content-type": "application/json" },
397
407
  });
398
408
  }
399
- return json({ ok: true, changed: true, matched: true, content: "AFTER", version: "v2" });
409
+ return json({
410
+ ok: true,
411
+ changed: true,
412
+ matched: true,
413
+ path: "index.html",
414
+ content: "AFTER",
415
+ version: "v2",
416
+ });
400
417
  }
401
418
  if (url.includes("/api/projects/p1/gsap-mutations/")) {
402
419
  calls.gsapMutations.push(JSON.parse(String(init?.body)));
@@ -426,6 +443,7 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
426
443
 
427
444
  await expect(hook().commitPathOffset(selection, { x: 40, y: 20 })).resolves.toEqual({
428
445
  ok: true,
446
+ changed: true,
429
447
  });
430
448
 
431
449
  expect(calls.gsapMutations).toHaveLength(0);
@@ -455,6 +473,7 @@ describe("useDomGeometryCommit, one word of a staggered phrase", () => {
455
473
  ]);
456
474
  await expect(hook().commitPathOffset(selection, { x: 40, y: 20 })).resolves.toEqual({
457
475
  ok: true,
476
+ changed: true,
458
477
  });
459
478
 
460
479
  expect(calls.patches).toHaveLength(2);
@@ -1,3 +1,4 @@
1
+ import type { GeometryCommitResult } from "../utils/previewFeatureUsage";
1
2
  import type { RotationCommit } from "../components/editor/rotationDraft";
2
3
  import { useCallback, useMemo, useRef, useState } from "react";
3
4
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
@@ -67,7 +68,7 @@ export function useDomGeometryCommit({
67
68
  }: UseDomGeometryCommitOptions): DomGeometryCommits {
68
69
  const projectIdRef = useRef(projectId);
69
70
  projectIdRef.current = projectId;
70
- const pending = useRef(new Set<Promise<void>>()).current;
71
+ const pending = useRef(new Set<Promise<GeometryCommitResult | undefined>>()).current;
71
72
  const editHistory = useMemo(() => ({ recordEdit }), [recordEdit]);
72
73
  const reload = useCallback(
73
74
  () => (reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload()),
@@ -127,6 +128,7 @@ export function useDomGeometryCommit({
127
128
  domEditSelection: null,
128
129
  selectedGsapAnimations: NO_SELECTED_ANIMATIONS,
129
130
  gsapCommitMutation: gsap.commitMutation,
131
+ activeCompPath,
130
132
  previewIframeRef: iframeRef,
131
133
  showToast,
132
134
  bumpGsapCache,
@@ -142,7 +144,10 @@ export function useDomGeometryCommit({
142
144
  updateArcSegment: gsap.updateArcSegment,
143
145
  });
144
146
  const saved = useCallback(
145
- async (commit: () => Promise<void>, restore = noop): Promise<DomEditCommitOutcome> => {
147
+ async (
148
+ commit: () => Promise<GeometryCommitResult | undefined>,
149
+ restore = noop,
150
+ ): Promise<DomEditCommitOutcome> => {
146
151
  const refusal = !projectIdRef.current
147
152
  ? "No project is open"
148
153
  : !isPreviewBooted(projectIdRef.current)
@@ -156,11 +161,10 @@ export function useDomGeometryCommit({
156
161
  const run = commit();
157
162
  pending.add(run);
158
163
  try {
159
- await run;
164
+ return { ok: true, changed: (await run)?.changed === true };
160
165
  } finally {
161
166
  pending.delete(run);
162
167
  }
163
- return { ok: true };
164
168
  },
165
169
  [pending, showToast],
166
170
  );
@@ -305,6 +305,7 @@ describe("Reset after a plain move puts the author's translate back", () => {
305
305
  async (_selection, patches) => {
306
306
  project.history.push(project.file);
307
307
  project.file = patches.reduce((html, op) => applyPatch(html, "layer", op), project.file);
308
+ return undefined;
308
309
  },
309
310
  );
310
311
  const selection = { id: "layer", selector: "#layer", element } as unknown as DomEditSelection;
@@ -1,3 +1,4 @@
1
+ import type { DomEditPersistOutcome } from "./domEditCommitTypes";
1
2
  import type { RotationCommit } from "../components/editor/rotationDraft";
2
3
  import { useCallback } from "react";
3
4
  import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
@@ -39,7 +40,7 @@ export interface UseDomGeometryCommitsParams {
39
40
  skipRefresh?: boolean;
40
41
  deferRender?: boolean;
41
42
  },
42
- ) => Promise<void>;
43
+ ) => Promise<DomEditPersistOutcome | undefined>;
43
44
  readOnlyPreview: boolean;
44
45
  }
45
46
 
@@ -73,7 +74,7 @@ export function useDomGeometryCommits({
73
74
  restore?: () => void,
74
75
  undoKey?: string,
75
76
  ) => {
76
- if (readOnlyPreview) return Promise.resolve();
77
+ if (readOnlyPreview) return Promise.resolve(undefined);
77
78
  const element = selection.element;
78
79
  const beforeSize = captureStudioBoxSize(element);
79
80
  const beforeOffset = captureStudioPathOffset(element);
@@ -334,3 +334,29 @@ describe("useDomSelection", () => {
334
334
  harness.cleanup();
335
335
  });
336
336
  });
337
+
338
+ it("reports unchanged marquee membership and one new additive member", () => {
339
+ const { harness, card, chip } = renderCardAndChip();
340
+ try {
341
+ act(() => harness.current().applyDomSelection(card));
342
+ let receipt!: ReturnType<ReturnType<typeof useDomSelection>["applyMarqueeSelection"]>;
343
+ act(() => {
344
+ receipt = harness.current().applyMarqueeSelection([chip], true);
345
+ });
346
+ expect(receipt).toEqual({ changed: true, count: 2 });
347
+ act(() => {
348
+ receipt = harness.current().applyMarqueeSelection([card, chip], true);
349
+ });
350
+ expect(receipt).toEqual({ changed: false, count: 2 });
351
+ act(() => {
352
+ receipt = harness.current().applyMarqueeSelection([], true);
353
+ });
354
+ expect(receipt).toEqual({ changed: false, count: 2 });
355
+ act(() => {
356
+ receipt = harness.current().applyMarqueeSelection([], false);
357
+ });
358
+ expect(receipt).toEqual({ changed: true, count: 0 });
359
+ } finally {
360
+ harness.cleanup();
361
+ }
362
+ });
@@ -487,12 +487,13 @@ export function useDomSelection({
487
487
  // fallow-ignore-next-line complexity
488
488
  (selections: DomEditSelection[], additive: boolean) => {
489
489
  logSelect("marquee", { hits: selections.length, additive });
490
+ const current = domEditSelectionRef.current;
491
+ const currentGroup = domEditGroupSelectionsRef.current;
490
492
  if (selections.length === 0) {
493
+ const count = seedDomEditGroupWithSelection(currentGroup, current).length;
491
494
  if (!additive) applyDomSelection(null, { revealPanel: false });
492
- return;
495
+ return { changed: !additive && count > 0, count: additive ? count : 0 };
493
496
  }
494
- const current = domEditSelectionRef.current;
495
- const currentGroup = domEditGroupSelectionsRef.current;
496
497
  let nextGroup: DomEditSelection[];
497
498
  if (additive) {
498
499
  nextGroup = seedDomEditGroupWithSelection(currentGroup, current);
@@ -507,11 +508,17 @@ export function useDomSelection({
507
508
  }
508
509
  }
509
510
  const nextSelection = additive && current ? current : selections[0];
511
+ const previous = seedDomEditGroupWithSelection(currentGroup, current);
512
+ const changed =
513
+ previous.length !== nextGroup.length ||
514
+ nextGroup.some((selection) => !domEditSelectionInGroup(previous, selection)) ||
515
+ !domEditSelectionsTargetSame(current, nextSelection);
510
516
  domEditSelectionRef.current = nextSelection;
511
517
  domEditGroupSelectionsRef.current = nextGroup;
512
518
  setDomEditSelection(nextSelection);
513
519
  setDomEditGroupSelections(nextGroup);
514
520
  announceTimelineSelection(nextGroup, nextSelection, true);
521
+ return { changed, count: nextGroup.length };
515
522
  },
516
523
  [applyDomSelection, announceTimelineSelection],
517
524
  );
@@ -3,6 +3,11 @@ import type { DomEditSelection } from "../components/editor/domEditing";
3
3
  import type { SelectElementOptions, TimelineElement } from "../player";
4
4
  import type { RightPanelTab } from "../utils/studioHelpers";
5
5
 
6
+ export interface DomSelectionResult {
7
+ changed: boolean;
8
+ count: number;
9
+ }
10
+
6
11
  export interface ApplyDomSelectionOptions {
7
12
  revealPanel?: boolean;
8
13
  additive?: boolean;
@@ -85,5 +90,5 @@ export interface UseDomSelectionReturn {
85
90
  handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
86
91
  refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
87
92
  refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
88
- applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => void;
93
+ applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => DomSelectionResult;
89
94
  }
@@ -4,7 +4,7 @@ import { createRoot } from "react-dom/client";
4
4
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
- import { afterEach, beforeEach, expect, it, vi } from "vitest";
7
+ import { afterEach, beforeEach, expect, it, vi, type Mock } from "vitest";
8
8
  import {
9
9
  createStudioApi,
10
10
  openProjectHistory,
@@ -168,6 +168,20 @@ it("an undo pressed while a save is still running waits for the server instead o
168
168
  expect(s.box()).toBe("10px");
169
169
  });
170
170
 
171
+ it("an undo waits for a save queued before its key, not one queued after", async () => {
172
+ const s = await studio();
173
+ await s.edit();
174
+ let land!: () => void;
175
+ void s.persistence().queueDomEditSave(() => new Promise<void>((resolve) => (land = resolve)));
176
+ await vi.waitFor(() => expect(land).toBeTypeOf("function"));
177
+
178
+ const undone = s.actions().undo();
179
+ void s.persistence().queueDomEditSave(() => new Promise<void>(() => {}));
180
+ land();
181
+ await act(() => undone);
182
+ expect(s.file()).toBe(BEFORE);
183
+ });
184
+
171
185
  it("an undo after an outside write ends with the preview showing the file the server restored", async () => {
172
186
  const s = await studio();
173
187
  await s.edit();
@@ -202,10 +216,8 @@ function Nudge({ target, save }: { target: HTMLElement; save: () => Promise<void
202
216
  return null;
203
217
  }
204
218
 
205
- it("an undo pressed while a nudge waits for more keys never shows the move before it undone", async () => {
206
- const s = await studio();
207
- await s.edit();
208
- resetNudgeKeys();
219
+ /** A nudge save that waits for `finish()`, then writes and records the nudge as the real save does. */
220
+ function queuedNudgeSave(s: Awaited<ReturnType<typeof studio>>) {
209
221
  let finish!: () => void;
210
222
  const save = vi.fn(async () => {
211
223
  await new Promise<void>((resolve) => (finish = resolve));
@@ -215,6 +227,24 @@ it("an undo pressed while a nudge waits for more keys never shows the move befor
215
227
  files: { "index.html": { before: AFTER, after: NUDGED } },
216
228
  });
217
229
  });
230
+ return { save, finish: () => finish() };
231
+ }
232
+
233
+ /** Mounts the nudge, presses ArrowRight once and waits for its save; returns the nudge's translate reader. */
234
+ async function pressNudge(s: Awaited<ReturnType<typeof studio>>, save: Mock<() => Promise<void>>) {
235
+ s.mount(createElement(Nudge, { target: s.element("box"), save }));
236
+ act(() => {
237
+ window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", cancelable: true }));
238
+ });
239
+ await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1));
240
+ return () => s.element("box").style.getPropertyValue("translate");
241
+ }
242
+
243
+ it("an undo pressed while a nudge waits for more keys never shows the move before it undone", async () => {
244
+ const s = await studio();
245
+ await s.edit();
246
+ resetNudgeKeys();
247
+ const { save, finish } = queuedNudgeSave(s);
218
248
  s.mount(createElement(Nudge, { target: s.element("box"), save }));
219
249
  act(() => {
220
250
  window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", cancelable: true }));
@@ -240,21 +270,8 @@ it("an undo pressed while a nudge's save is queued shows the nudge undone in the
240
270
  const s = await studio();
241
271
  await s.edit();
242
272
  resetNudgeKeys();
243
- let finish!: () => void;
244
- const save = vi.fn(async () => {
245
- await new Promise<void>((resolve) => (finish = resolve));
246
- writeFileSync(s.path, NUDGED);
247
- await s.history().recordEdit({
248
- label: "Move layer",
249
- files: { "index.html": { before: AFTER, after: NUDGED } },
250
- });
251
- });
252
- s.mount(createElement(Nudge, { target: s.element("box"), save }));
253
- act(() => {
254
- window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", cancelable: true }));
255
- });
256
- await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1));
257
- const nudge = () => s.element("box").style.getPropertyValue("translate");
273
+ const { save, finish } = queuedNudgeSave(s);
274
+ const nudge = await pressNudge(s, save);
258
275
  expect(nudge()).not.toBe("");
259
276
 
260
277
  const undone = s.actions().undo();
@@ -267,6 +284,51 @@ it("an undo pressed while a nudge's save is queued shows the nudge undone in the
267
284
  expect(s.box()).toBe("50px");
268
285
  });
269
286
 
287
+ /** A nudge whose save is queued and then rejects, and the history requests the undo sends. */
288
+ async function failingNudge(s: Awaited<ReturnType<typeof studio>>) {
289
+ resetNudgeKeys();
290
+ let fail!: () => void;
291
+ const save = vi.fn(
292
+ () => new Promise<void>((_, reject) => (fail = () => reject(new Error("The save failed.")))),
293
+ );
294
+ const nudge = await pressNudge(s, save);
295
+ const steps: string[] = [];
296
+ const real = globalThis.fetch;
297
+ vi.stubGlobal("fetch", (url: string, init?: RequestInit) => {
298
+ if (/\/history\/(step|undo)$/.test(url)) steps.push(url);
299
+ return real(url, init);
300
+ });
301
+ return { fail: () => fail(), steps, nudge };
302
+ }
303
+
304
+ it("an undo pressed while a nudge's save fails keeps the nudge undone and steps no older edit", async () => {
305
+ const s = await studio();
306
+ await s.edit();
307
+ const n = await failingNudge(s);
308
+ expect(n.nudge()).not.toBe("");
309
+
310
+ const undone = s.actions().undo();
311
+ expect(n.nudge()).toBe("");
312
+ n.fail();
313
+ await act(() => undone);
314
+ expect(n.steps).toEqual([]);
315
+ expect(s.file()).toBe(AFTER);
316
+ expect(s.box()).toBe("50px");
317
+ expect(n.nudge()).toBe("");
318
+ });
319
+
320
+ it("an undo pressed while the only edit's save fails leaves the screen as the file, nothing put back", async () => {
321
+ const s = await studio();
322
+ s.show(BEFORE);
323
+ const n = await failingNudge(s);
324
+
325
+ const undone = s.actions().undo();
326
+ n.fail();
327
+ await act(() => undone);
328
+ expect(s.file()).toBe(BEFORE);
329
+ expect(n.nudge()).toBe("");
330
+ });
331
+
270
332
  it("an undo pressed while a queued save fails undoes the edit before it, file and box alike", async () => {
271
333
  const s = await studio();
272
334
  await s.edit();
@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
5
5
  import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
7
7
  import { useEditHistoryActions, type EditHistoryHandle } from "./useEditHistoryActions";
8
+ import { trackStudioPendingEdit } from "../utils/studioPendingEdits";
8
9
 
9
10
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
10
11
 
@@ -30,6 +31,7 @@ function mount(
30
31
  undo: vi.fn<EditHistoryHandle["undo"]>(async () => result),
31
32
  redo: vi.fn<EditHistoryHandle["redo"]>(async () => result),
32
33
  predict: () => predicted ?? null,
34
+ claims: () => 7,
33
35
  };
34
36
  const putBack = vi.fn();
35
37
  const deps = {
@@ -60,6 +62,19 @@ const PREDICTED = { id: "e1", files: { "index.html": { previous: "B", restored:
60
62
  const SERVER_FILES = { "index.html": { previous: "B", restored: "A2" } };
61
63
 
62
64
  describe("useEditHistoryActions", () => {
65
+ it("asks for the edit claimed after the key's claim count only when an edit was saving at the key", async () => {
66
+ const { deps, actions } = mount({ ok: false, reason: "empty" });
67
+ await act(() => actions.undo());
68
+ expect(deps.editHistory.undo.mock.calls[0]![0].claimedAfter).toBeUndefined();
69
+
70
+ let land!: () => void;
71
+ trackStudioPendingEdit(new Promise<void>((resolve) => (land = resolve)));
72
+ const undone = actions.undo();
73
+ land();
74
+ await act(() => undone);
75
+ expect(deps.editHistory.undo.mock.calls[1]![0].claimedAfter).toBe(7);
76
+ });
77
+
63
78
  it("corrects a shown step from the server's restore, diffed from what the preview shows", async () => {
64
79
  const { deps, actions } = mount(
65
80
  { ok: true, label: "Undid: Move", paths: ["index.html"], undoes: "e1", files: SERVER_FILES },
@@ -3,7 +3,10 @@ import { useCallback, useMemo } from "react";
3
3
  import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
4
4
  import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
5
5
  import type { RestoreFiles } from "../utils/gsapUndoRestore";
6
- import { revertNewestStudioPendingEdit } from "../utils/studioPendingEdits";
6
+ import {
7
+ hasStudioPendingEdits,
8
+ paintBackNewestStudioPendingEdit,
9
+ } from "../utils/studioPendingEdits";
7
10
 
8
11
  interface HistoryResult {
9
12
  ok: boolean;
@@ -18,11 +21,13 @@ interface HistoryResult {
18
21
  interface HistoryFileCallbacks {
19
22
  readFile: (path: string) => Promise<string>;
20
23
  serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
24
+ claimedAfter?: number;
21
25
  }
22
26
  export interface EditHistoryHandle {
23
27
  undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
24
28
  redo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
25
29
  predict?: (direction: "undo" | "redo") => { id: string; files: RestoreFiles } | null;
30
+ claims?: () => number;
26
31
  state: {
27
32
  undo: ReadonlyArray<{ createdAt: number }>;
28
33
  redo: ReadonlyArray<{ createdAt: number }>;
@@ -30,7 +35,7 @@ export interface EditHistoryHandle {
30
35
  }
31
36
 
32
37
  export interface UseEditHistoryActionsOptions {
33
- editHistory: Pick<EditHistoryHandle, "undo" | "redo" | "predict">;
38
+ editHistory: Pick<EditHistoryHandle, "undo" | "redo" | "predict" | "claims">;
34
39
  readOptionalProjectFile: (path: string) => Promise<string>;
35
40
  readProjectFile: (path: string) => Promise<string>;
36
41
  writeProjectFile: (path: string, content: string) => Promise<void>;
@@ -77,22 +82,28 @@ export function useEditHistoryActions({
77
82
  const predicted = editHistory.predict?.(direction) ?? null;
78
83
  const predictedShown = predicted ? (showHistoryRestoreNow?.(predicted.files) ?? null) : null;
79
84
  const pendingEditShown =
80
- !predictedShown && direction === "undo" ? revertNewestStudioPendingEdit() : null;
81
- const putBack = predictedShown ?? pendingEditShown;
85
+ !predictedShown && direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
86
+ const putBack = predictedShown ?? pendingEditShown?.showAgain;
87
+ const claimedAfter =
88
+ direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : undefined;
82
89
  let result: HistoryResult = { ok: false, reason: "failed" };
83
90
  let serverSteppedShown = false;
91
+ let revertIsTheUndo = false;
84
92
  try {
85
93
  await waitForPendingDomEditSaves();
94
+ revertIsTheUndo = Boolean(pendingEditShown && !(await pendingEditShown.landed()));
95
+ if (revertIsTheUndo) return;
86
96
  result = await editHistory[direction]({
87
97
  readFile: readHistoryFile,
88
98
  serialize: serializeHistoryFiles,
99
+ claimedAfter,
89
100
  });
90
101
  const stepped = Boolean(result.ok && result.label);
91
102
  serverSteppedShown = predictedShown
92
103
  ? stepped && result.undoes === predicted?.id
93
104
  : stepped && Boolean(pendingEditShown);
94
105
  } finally {
95
- if (putBack && !serverSteppedShown) putBack();
106
+ if (putBack && !serverSteppedShown && !revertIsTheUndo) putBack();
96
107
  }
97
108
  if (!result.ok && result.reason === "content-mismatch") {
98
109
  showToast(
@@ -24,6 +24,7 @@ import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditComm
24
24
  import { domEditCommitDeclined, type DomEditCommitOutcome } from "./domEditCommitRunner";
25
25
  import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
26
26
  import { studioWriteHeaders } from "../utils/studioFileVersion";
27
+ import { studioApiFetch } from "../utils/studioApiFetch";
27
28
 
28
29
  interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
29
30
  /** Route delete through SDK when session resolves the hf-id. */
@@ -156,7 +157,7 @@ export function useElementLifecycleOps({
156
157
  writeFile: writeProjectFile,
157
158
  recordEdit: editHistory.recordEdit,
158
159
  rewrite: async (originalContent) => {
159
- const removeResponse = await fetch(
160
+ const removeResponse = await studioApiFetch(
160
161
  buildProjectApiPath(
161
162
  pid,
162
163
  `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
@@ -314,7 +314,7 @@ describe("applyArcKeyframeAtPlayhead", () => {
314
314
  ],
315
315
  ease: "none",
316
316
  },
317
- { label: "Add keyframe", softReload: true },
317
+ { label: "Add keyframe", softReload: true, keyframeAction: "add" },
318
318
  );
319
319
  });
320
320
 
@@ -336,7 +336,7 @@ describe("applyArcKeyframeAtPlayhead", () => {
336
336
  duration: 10,
337
337
  keyframes: expect.arrayContaining([{ percentage: 25, properties: { x: 25, y: 25 } }]),
338
338
  }),
339
- { label: "Add keyframe", softReload: true },
339
+ { label: "Add keyframe", softReload: true, keyframeAction: "add" },
340
340
  );
341
341
  });
342
342
  });
@@ -378,7 +378,10 @@ function makeElementSelection(): DomEditSelection {
378
378
  } as DomEditSelection;
379
379
  }
380
380
 
381
- function stubFlatTweenConversion(id: string): {
381
+ function stubFlatTweenConversion(
382
+ id: string,
383
+ easeEach?: string,
384
+ ): {
382
385
  flat: GsapAnimation;
383
386
  converted: GsapAnimation;
384
387
  } {
@@ -393,6 +396,7 @@ function stubFlatTweenConversion(id: string): {
393
396
  { percentage: 0, properties: { x: 0 } },
394
397
  { percentage: 100, properties: { x: 10 } },
395
398
  ],
399
+ ...(easeEach && { easeEach }),
396
400
  },
397
401
  });
398
402
  flatTweenResponses(flat, converted);
@@ -437,6 +441,33 @@ describe("useEnableKeyframes — flat tween transaction", () => {
437
441
  expect(convertOptions?.coalesceKey).toEqual(expect.any(String));
438
442
  });
439
443
 
444
+ it("keeps the converted tween's easeEach when it extends it to the playhead", async () => {
445
+ window.location.hash = "#/project/test-project";
446
+ usePlayerStore.setState({ currentTime: 3 });
447
+ const { flat } = stubFlatTweenConversion("flat-1", "none");
448
+ const commitMutation = vi.fn(async () => undefined);
449
+ const enable = renderEnableKeyframes({
450
+ domEditSelection: makeElementSelection(),
451
+ selectedGsapAnimations: [flat],
452
+ previewIframeRef: {
453
+ current: {
454
+ contentWindow: { gsap: { getProperty: () => 10 } },
455
+ } as unknown as HTMLIFrameElement,
456
+ },
457
+ handleGsapAddAnimation: vi.fn(),
458
+ handleGsapConvertToKeyframes: vi.fn(async () => undefined),
459
+ handleGsapRemoveKeyframe: vi.fn(),
460
+ commitMutation,
461
+ });
462
+
463
+ await act(async () => enable());
464
+
465
+ expect(commitMutation.mock.calls[0]?.[0]).toMatchObject({
466
+ type: "replace-with-keyframes",
467
+ easeEach: "none",
468
+ });
469
+ });
470
+
440
471
  it("passes the convert coalesce key to an inside-range batch edit", async () => {
441
472
  window.location.hash = "#/project/test-project";
442
473
  usePlayerStore.setState({ currentTime: 1.5 });
@@ -15,6 +15,7 @@ import { fetchParsedAnimations, getAnimationsForElement } from "./useGsapTweenCa
15
15
  import {
16
16
  existingTweenTargetSelector,
17
17
  computeElementPercentage,
18
+ keyframeEases,
18
19
  KEYFRAME_PCT_MATCH,
19
20
  isInstantHold,
20
21
  resolveEditableTweenDuration,
@@ -159,7 +160,7 @@ async function replaceSetWithSingleKeyframe(
159
160
  keyframes: [{ percentage: 0, properties: position }],
160
161
  ease: setAnim.ease,
161
162
  },
162
- { label: "Enable keyframes", softReload: true },
163
+ { label: "Enable keyframes", keyframeAction: "add", softReload: true },
163
164
  );
164
165
  }
165
166
 
@@ -266,10 +267,11 @@ async function extendKeyframedTweenToPlayhead(
266
267
  position: extended.position,
267
268
  duration: extended.duration,
268
269
  keyframes: extended.keyframes,
269
- ease: anim.ease,
270
+ ...keyframeEases(anim),
270
271
  },
271
272
  {
272
273
  label: "Add keyframe",
274
+ keyframeAction: "add",
273
275
  softReload: true,
274
276
  ...commitOverrides,
275
277
  },
@@ -377,7 +379,7 @@ export async function promoteSetToKeyframes(
377
379
  ],
378
380
  ease: setAnim.ease,
379
381
  },
380
- { label: "Add keyframe", softReload: true },
382
+ { label: "Add keyframe", keyframeAction: "add", softReload: true },
381
383
  );
382
384
  }
383
385
 
@@ -454,7 +456,7 @@ export async function applyArcKeyframeAtPlayhead(
454
456
  }),
455
457
  ease: "none",
456
458
  },
457
- { label: "Add keyframe", softReload: true },
459
+ { label: "Add keyframe", keyframeAction: "add", softReload: true },
458
460
  );
459
461
  }
460
462
 
@@ -503,6 +505,7 @@ export function useEnableKeyframes(
503
505
  enableKeyframesTransactionCounter += 1;
504
506
  const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
505
507
  const convertCommitOverrides: Partial<CommitMutationOptions> = {
508
+ keyframeTelemetry: false,
506
509
  skipReload: true,
507
510
  coalesceKey,
508
511
  coalesceMs: Number.POSITIVE_INFINITY,