@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
@@ -0,0 +1,65 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { usePlayerStore, type TimelineElement } from "../player/store/playerStore";
4
+ import { refreshTimelineRowText } from "./refreshTimelineRowText";
5
+
6
+ afterEach(() => {
7
+ usePlayerStore.getState().reset();
8
+ document.body.innerHTML = "";
9
+ });
10
+
11
+ const row = (key: string, selector: string, sourceFile?: string): TimelineElement => ({
12
+ id: key.split("#").pop()!,
13
+ key,
14
+ tag: "p",
15
+ start: 0,
16
+ duration: 2,
17
+ track: 0,
18
+ selector,
19
+ selectorIndex: 0,
20
+ sourceFile,
21
+ text: { value: "Old" },
22
+ });
23
+
24
+ describe("refreshTimelineRowText", () => {
25
+ it("gives the edited layer's row, addressed by its store key, its new words", () => {
26
+ document.body.innerHTML = `<h1 id="title">New words</h1>`;
27
+ usePlayerStore.getState().setElements([row("index.html#title", "#title")]);
28
+
29
+ refreshTimelineRowText(document.getElementById("title")!);
30
+
31
+ expect(usePlayerStore.getState().elements[0]?.text?.value).toBe("New words");
32
+ });
33
+
34
+ it("finds a row known only by its data-hf-id", () => {
35
+ document.body.innerHTML = `<p data-hf-id="caption-a">New</p>`;
36
+ usePlayerStore
37
+ .getState()
38
+ .setElements([
39
+ { ...row("index.html#caption-a", ""), selector: undefined, hfId: "caption-a" },
40
+ ]);
41
+
42
+ refreshTimelineRowText(document.querySelector("p")!);
43
+
44
+ expect(usePlayerStore.getState().elements[0]?.text?.value).toBe("New");
45
+ });
46
+
47
+ it("finds the row in the edited layer's own composition when two share a selector", () => {
48
+ document.body.innerHTML = `
49
+ <div data-composition-id="a" data-composition-file="a.html"><p class="caption">A</p></div>
50
+ <div data-composition-id="b" data-composition-file="b.html"><p class="caption">B new</p></div>`;
51
+ usePlayerStore
52
+ .getState()
53
+ .setElements([
54
+ row("a.html#a-caption", ".caption", "a.html"),
55
+ row("b.html#b-caption", ".caption", "b.html"),
56
+ ]);
57
+
58
+ refreshTimelineRowText(document.querySelectorAll<HTMLElement>(".caption")[1]!);
59
+
60
+ expect(usePlayerStore.getState().elements.map((element) => element.text?.value)).toEqual([
61
+ "Old",
62
+ "B new",
63
+ ]);
64
+ });
65
+ });
@@ -0,0 +1,29 @@
1
+ import {
2
+ getTimelineElementIdentity,
3
+ getTimelineElementSelector,
4
+ getTimelineElementSelectorIndex,
5
+ getTimelineElementSourceFile,
6
+ } from "../player/lib/timelineElementHelpers";
7
+ import { readTimelineText } from "../player/lib/timelineText";
8
+ import { usePlayerStore } from "../player/store/playerStore";
9
+
10
+ /** A text commit skips the preview reload, so the edited layer's row re-reads its words here. */
11
+ export function refreshTimelineRowText(el: HTMLElement): void {
12
+ const sourceFile = getTimelineElementSourceFile(el);
13
+ const hfId = el.getAttribute("data-hf-id");
14
+ const selector = getTimelineElementSelector(el);
15
+ const selectorIndex = selector
16
+ ? (getTimelineElementSelectorIndex(el.ownerDocument, el, selector) ?? 0)
17
+ : 0;
18
+ const { elements, updateElement } = usePlayerStore.getState();
19
+ const row = elements.find(
20
+ (candidate) =>
21
+ candidate.sourceFile === sourceFile &&
22
+ (hfId
23
+ ? candidate.hfId === hfId
24
+ : !!selector &&
25
+ candidate.selector === selector &&
26
+ (candidate.selectorIndex ?? 0) === selectorIndex),
27
+ );
28
+ if (row) updateElement(getTimelineElementIdentity(row), { text: readTimelineText(el) });
29
+ }
@@ -593,6 +593,7 @@ describe("a timing edit's soft reload restores what GSAP wrote from the files",
593
593
 
594
594
  expect(fetchMock).toHaveBeenCalledWith(
595
595
  expect.stringContaining("/files/compositions%2Fsub.html"),
596
+ { credentials: "omit" },
596
597
  );
597
598
  expect(reloadPreview).not.toHaveBeenCalled();
598
599
  expect(wt.getAttribute("style")).toBe("left: 700px;");
@@ -28,12 +28,13 @@ import {
28
28
  serializeStudioFileMutations,
29
29
  type StudioProjectFileWriter,
30
30
  } from "../utils/studioFileMutationCoordinator";
31
+ import { studioApiFetch } from "../utils/studioApiFetch";
31
32
 
32
33
  export async function readFileContent(projectId: string, targetPath: string): Promise<string> {
33
34
  if (targetPath.includes("\0") || targetPath.includes("..")) {
34
35
  throw new Error(`Unsafe path: ${targetPath}`);
35
36
  }
36
- const response = await fetch(
37
+ const response = await studioApiFetch(
37
38
  `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(targetPath)}`,
38
39
  );
39
40
  if (!response.ok) {
@@ -2,17 +2,22 @@
2
2
 
3
3
  import React, { act } from "react";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
- import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
5
+ import { parseGsapScript, type GsapAnimation } from "@hyperframes/core/gsap-parser";
6
6
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
7
7
  import { usePlayerStore } from "../player/store/playerStore";
8
8
  import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
9
9
  import { mountReactHarness, withInlineLayoutBox } from "./domSelectionTestHarness";
10
10
  import { writeSizeWithCrop } from "../components/editor/cropResize";
11
- import type { CommitMutation } from "./gsapScriptCommitTypes";
11
+ import { trackStudioEvent } from "../utils/studioTelemetry";
12
+ import { trackKeyframeCommit } from "../utils/keyframeUsage";
13
+ import type { CommitMutationOptions, CommitMutation } from "./gsapScriptCommitTypes";
14
+
15
+ vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
12
16
 
13
17
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
14
18
 
15
19
  afterEach(() => {
20
+ vi.clearAllMocks();
16
21
  document.body.innerHTML = "";
17
22
  usePlayerStore.setState({ autoKeyframeEnabled: true, currentTime: 0 });
18
23
  });
@@ -43,7 +48,11 @@ type Commit = (
43
48
  /** Renders the hook and hands its commit function to the caller via a ref callback. */
44
49
  function renderHookWith(
45
50
  animations: GsapAnimation[],
46
- onMutation: (mutation: Record<string, unknown>, label: string) => unknown | Promise<unknown>,
51
+ onMutation: (
52
+ mutation: Record<string, unknown>,
53
+ label: string,
54
+ options: CommitMutationOptions,
55
+ ) => unknown | Promise<unknown>,
47
56
  onReady: (commit: Commit) => void,
48
57
  bumpGsapCache = vi.fn(),
49
58
  onBatch?: (
@@ -56,9 +65,9 @@ function renderHookWith(
56
65
  async (
57
66
  _sel: DomEditSelection,
58
67
  mutation: Record<string, unknown>,
59
- options: { label: string },
68
+ options: CommitMutationOptions,
60
69
  ) => {
61
- await onMutation(mutation, options.label);
70
+ await onMutation(mutation, options.label, options);
62
71
  },
63
72
  onBatch
64
73
  ? {
@@ -401,3 +410,35 @@ describe("useAnimatedPropertyCommit — a size write and its crop are one undo s
401
410
  act(() => root.unmount());
402
411
  });
403
412
  });
413
+
414
+ it("counts one add when an inspector edit first converts a flat animation", async () => {
415
+ usePlayerStore.setState({ autoKeyframeEnabled: true, currentTime: 1 });
416
+ const flat = parseGsapScript(
417
+ 'const tl = gsap.timeline(); tl.to("#box", { rotationX: 0, duration: 2 }, 0);',
418
+ ).animations[0]!;
419
+ const mutations: Record<string, unknown>[] = [];
420
+ let commit!: Commit;
421
+ const root = renderHookWith(
422
+ [flat, keyframedAnim],
423
+ (mutation, _label, options) => {
424
+ mutations.push(mutation);
425
+ trackKeyframeCommit([mutation], { ok: true, changed: true }, options);
426
+ },
427
+ (value) => {
428
+ commit = value;
429
+ },
430
+ );
431
+ try {
432
+ await act(async () => {
433
+ await commit(selection, { rotationX: 20 });
434
+ });
435
+ expect(mutations.map((mutation) => mutation.type)).toEqual([
436
+ "convert-to-keyframes",
437
+ "add-keyframe",
438
+ ]);
439
+ expect(trackStudioEvent).toHaveBeenCalledTimes(1);
440
+ expect(trackStudioEvent).toHaveBeenCalledWith("keyframe", { action: "add" });
441
+ } finally {
442
+ act(() => root.unmount());
443
+ }
444
+ });
@@ -333,7 +333,7 @@ async function commitKeyframeProps(
333
333
  await commit(
334
334
  selection,
335
335
  { type: "convert-to-keyframes", animationId: anim.id },
336
- { label: "Convert to keyframes", skipReload: true },
336
+ { label: "Convert to keyframes", keyframeTelemetry: false, skipReload: true },
337
337
  );
338
338
  }
339
339
  const ct = usePlayerStore.getState().currentTime;
@@ -389,7 +389,7 @@ async function commitKeyframeProps(
389
389
  duration: roundTo3(newDuration),
390
390
  keyframes: remapped,
391
391
  },
392
- { label: `Edit ${primaryProp} (extended keyframe)`, softReload: true },
392
+ { label: `Edit ${primaryProp} (extended keyframe)`, keyframeAction: "add", softReload: true },
393
393
  );
394
394
  return;
395
395
  }
@@ -7,6 +7,7 @@ import { toProjectAbsolutePath, type AgentModalAnchorPoint } from "../utils/stud
7
7
  import { liveMarkupWithoutPreviewMarks } from "../utils/authoredSource";
8
8
  import { buildElementAgentPrompt, type DomEditSelection } from "../components/editor/domEditing";
9
9
  import { usePlayerStore } from "../player";
10
+ import { studioApiFetch } from "../utils/studioApiFetch";
10
11
 
11
12
  // ── Types ──
12
13
 
@@ -55,7 +56,7 @@ export function useAskAgentModal({
55
56
 
56
57
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
57
58
  try {
58
- const response = await fetch(
59
+ const response = await studioApiFetch(
59
60
  buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
60
61
  );
61
62
  if (!response.ok) return;
@@ -5,6 +5,7 @@ import {
5
5
  type BlockCategory,
6
6
  resolveBlockCategory,
7
7
  } from "../utils/blockCategories";
8
+ import { studioApiFetch } from "../utils/studioApiFetch";
8
9
 
9
10
  type CatalogItem = RegistryItem & {
10
11
  category: BlockCategory;
@@ -14,7 +15,7 @@ let catalogCache: CatalogItem[] | null = null;
14
15
  let catalogRequest: Promise<CatalogItem[]> | null = null;
15
16
 
16
17
  function loadCatalog(): Promise<CatalogItem[]> {
17
- catalogRequest ??= fetch("/api/registry/blocks")
18
+ catalogRequest ??= studioApiFetch("/api/registry/blocks")
18
19
  .then((response) => {
19
20
  if (!response.ok) throw new Error("Failed to load catalog");
20
21
  return response.json() as Promise<RegistryItem[]>;
@@ -8,19 +8,28 @@
8
8
 
9
9
  import { useCallback, useRef } from "react";
10
10
  import type { TimelineElement } from "../player";
11
+ import {
12
+ MAX_HAND_EDIT_CLIPS,
13
+ type BlockedTimelineEditIntent,
14
+ } from "../player/components/timelineEditing";
11
15
 
12
16
  const BLOCKED_TOAST_INTERVAL_MS = 1500;
13
17
 
14
18
  export function useBlockedTimelineEditToast(
15
19
  showToast: (message: string, tone?: "info" | "error") => void,
16
- ): (element: TimelineElement) => void {
20
+ ): (element: TimelineElement, intent: BlockedTimelineEditIntent) => void {
17
21
  const lastAtRef = useRef(0);
18
22
  return useCallback(
19
- (_element: TimelineElement) => {
23
+ (_element: TimelineElement, intent: BlockedTimelineEditIntent) => {
20
24
  const now = Date.now();
21
25
  if (now - lastAtRef.current < BLOCKED_TOAST_INTERVAL_MS) return;
22
26
  lastAtRef.current = now;
23
- showToast("This clip can't be moved or resized from the timeline yet.", "info");
27
+ showToast(
28
+ intent === "edit-many"
29
+ ? `Select ${MAX_HAND_EDIT_CLIPS} or fewer clips to move or resize by hand, or describe the change.`
30
+ : "This clip can't be moved or resized from the timeline yet.",
31
+ "info",
32
+ );
24
33
  },
25
34
  [showToast],
26
35
  );
@@ -5,6 +5,7 @@ import { useCaptionStore } from "../captions/store";
5
5
  import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
6
6
  import { useCaptionSync } from "../captions/hooks/useCaptionSync";
7
7
  import { parseCaptionComposition } from "../captions/parser";
8
+ import { studioApiFetch } from "../utils/studioApiFetch";
8
9
 
9
10
  interface UseCaptionDetectionParams {
10
11
  projectId: string | null;
@@ -110,7 +111,7 @@ export function useCaptionDetection({
110
111
 
111
112
  activating = true;
112
113
  const srcPath = captionSrcPath;
113
- fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`))
114
+ studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`))
114
115
  .then((r) => r.json())
115
116
  .then((data: { content?: string }) => {
116
117
  if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
@@ -1,5 +1,6 @@
1
1
  import { buildProjectApiPath } from "../utils/projectRouting";
2
2
  import { useCallback } from "react";
3
+ import { studioApiFetch } from "../utils/studioApiFetch";
3
4
 
4
5
  /**
5
6
  * Loads a composition file's content for the source editor when a composition
@@ -24,7 +25,7 @@ export function useCompositionContentLoader({
24
25
  if (!projectId) return;
25
26
  setActiveCompPath(comp.endsWith(".html") ? comp : null);
26
27
  setEditingFile({ path: comp, content: null });
27
- fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`))
28
+ studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`))
28
29
  .then(async (r) => {
29
30
  if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
30
31
  return r.json();
@@ -7,6 +7,7 @@ import { buildProjectApiPath } from "../utils/projectRouting";
7
7
  import type { DomEditPatchBatch } from "./domEditCommitTypes";
8
8
  import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
9
9
  import { studioWriteHeaders } from "../utils/studioFileVersion";
10
+ import { studioApiFetch } from "../utils/studioApiFetch";
10
11
 
11
12
  export function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
12
13
  return fields.map((field) => field.path).join(", ");
@@ -99,7 +100,7 @@ function isAtomicElementPatchFile(value: unknown): value is AtomicElementPatchFi
99
100
  export async function patchElementBatches(projectId: string, batches: DomEditPatchBatch[]) {
100
101
  const body = JSON.stringify({ batches });
101
102
  try {
102
- const response = await fetch(
103
+ const response = await studioApiFetch(
103
104
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
104
105
  {
105
106
  method: "POST",
@@ -178,7 +179,7 @@ export async function postPatchElement(
178
179
  body: unknown,
179
180
  showToast: ShowToast,
180
181
  ): Promise<PatchElementResponse> {
181
- const response = await fetch(
182
+ const response = await studioApiFetch(
182
183
  buildProjectApiPath(
183
184
  projectId,
184
185
  `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
@@ -33,30 +33,25 @@ export function useDomEditPositionPatchCommit({
33
33
  coalesceMs: options.coalesceMs,
34
34
  skipRefresh: options.skipRefresh ?? true,
35
35
  deferRender: options.deferRender,
36
- })
37
- .then(() => undefined)
38
- .catch((error) => {
39
- // A paused save queue is not worth a toast: the paused-save banner is
40
- // already on screen, and one toast per blocked edit is what this branch
41
- // exists to prevent. It still has to REJECT, though. Swallowing it
42
- // resolved the commit, which skipped the caller's revert, so the element
43
- // stayed where the drag put it while nothing reached the file.
44
- if (error instanceof DomEditSaveQueueOpenError) throw error;
45
- if (!wasAlreadyToasted(error)) {
46
- showToast(error instanceof Error ? error.message : "Failed to save position");
47
- }
48
- trackStudioSaveFailure({
49
- source: "dom_edit",
50
- error,
51
- filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
52
- mutationType: "position",
53
- label: options.label,
54
- targetId: selection.id,
55
- targetSelector: selection.selector,
56
- targetSourceFile: selection.sourceFile,
57
- });
58
- throw error;
36
+ }).catch((error) => {
37
+ // The paused-save banner already explains this refusal. Rethrow so the
38
+ // caller reverts the preview instead of treating an unsaved edit as committed.
39
+ if (error instanceof DomEditSaveQueueOpenError) throw error;
40
+ if (!wasAlreadyToasted(error)) {
41
+ showToast(error instanceof Error ? error.message : "Failed to save position");
42
+ }
43
+ trackStudioSaveFailure({
44
+ source: "dom_edit",
45
+ error,
46
+ filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
47
+ mutationType: "position",
48
+ label: options.label,
49
+ targetId: selection.id,
50
+ targetSelector: selection.selector,
51
+ targetSourceFile: selection.sourceFile,
59
52
  });
53
+ throw error;
54
+ });
60
55
  },
61
56
  [activeCompPath, persistDomEditOperations, showToast],
62
57
  );
@@ -10,6 +10,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
10
10
  import type { TimelineElement } from "../player";
11
11
  import type { UseDomEditSessionParams } from "./useDomEditSession";
12
12
  import { makeSelection } from "./domSelectionTestHarness";
13
+ import { readTranslatePx } from "../components/editor/plainTranslate";
13
14
 
14
15
  const styleOp = (property: string, value: string): PatchOperation => ({
15
16
  type: "inline-style",
@@ -66,6 +67,7 @@ const groupSelectionSpy = vi.fn();
66
67
  const gsapCommitMutation = Object.assign(vi.fn(), { batch: vi.fn() });
67
68
  const neverParsed = new Promise<{ animations: GsapAnimation[] }>(() => undefined);
68
69
  const parsedFile = { current: neverParsed };
70
+ const groupCommitSpy = vi.fn(async () => {});
69
71
 
70
72
  function createSessionParams(
71
73
  overrides: Partial<UseDomEditSessionParams> = {},
@@ -256,7 +258,7 @@ vi.mock("./usePreviewInteraction", () => ({
256
258
  vi.mock("./useGsapAwareEditing", () => ({
257
259
  useGsapAwareEditing: () => ({
258
260
  handleGsapAwarePathOffsetCommit: vi.fn(),
259
- handleGsapAwareGroupPathOffsetCommit: vi.fn(),
261
+ handleGsapAwareGroupPathOffsetCommit: groupCommitSpy,
260
262
  handleGsapAwareBoxSizeCommit: vi.fn(),
261
263
  handleGsapAwareRotationCommit: vi.fn(),
262
264
  commitAnimatedProperty: vi.fn(),
@@ -605,3 +607,30 @@ describe("a shadow reload promoted without a host re-render", () => {
605
607
  act(() => root.unmount());
606
608
  });
607
609
  });
610
+
611
+ describe("handleDomGroupMoveBy", () => {
612
+ it("saves through the group drag's commit, leaving a refusal for the host to explain", async () => {
613
+ const { useDomEditSession } = await import("./useDomEditSession");
614
+ const element = document.createElement("div");
615
+ document.body.append(element);
616
+ element.getBoundingClientRect = () => {
617
+ const { x, y } = readTranslatePx(element);
618
+ return new DOMRect(x, y, 40, 20);
619
+ };
620
+ const selection = makeSelection("card", element);
621
+ let moveBy!: ReturnType<typeof useDomEditSession>["handleDomGroupMoveBy"];
622
+ function Probe() {
623
+ moveBy = useDomEditSession(createSessionParams()).handleDomGroupMoveBy;
624
+ return null;
625
+ }
626
+ const root = createRoot(document.createElement("div"));
627
+ act(() => root.render(<Probe />));
628
+ await act(() => moveBy([{ selection, delta: { x: 10, y: 0 } }]));
629
+ expect(groupCommitSpy).toHaveBeenCalledWith(
630
+ [{ selection, next: { x: 10, y: 0 }, plainTranslate: true }],
631
+ { refusalToast: false },
632
+ );
633
+ act(() => root.unmount());
634
+ element.remove();
635
+ });
636
+ });
@@ -25,6 +25,7 @@ import { membersForDelete, timelineElementsForDelete } from "./domEditDeleteMemb
25
25
  import type { RecordEditInput } from "./domEditDeleteMembers";
26
26
  import type { DomEditTimelineParams } from "./useDomSelectionTypes";
27
27
  import { useLivePreviewIframe } from "./useLivePreviewIframe";
28
+ import { moveDomGroupBy, type DomGroupMove } from "../components/editor/domGroupMoveBy";
28
29
  // Re-exported: the delete rule lives in its own module; callers and its test import it from here.
29
30
  export { membersForDelete };
30
31
 
@@ -445,6 +446,7 @@ export function useDomEditSession({
445
446
  domEditSelection,
446
447
  selectedGsapAnimations,
447
448
  gsapCommitMutation,
449
+ activeCompPath,
448
450
  previewIframeRef,
449
451
  showToast,
450
452
  bumpGsapCache,
@@ -459,6 +461,13 @@ export function useDomEditSession({
459
461
  setArcPath,
460
462
  updateArcSegment,
461
463
  });
464
+ const handleDomGroupMoveBy = useCallback(
465
+ (moves: DomGroupMove[]) =>
466
+ moveDomGroupBy(moves, (updates) =>
467
+ handleGsapAwareGroupPathOffsetCommit(updates, { refusalToast: false }),
468
+ ),
469
+ [handleGsapAwareGroupPathOffsetCommit],
470
+ );
462
471
  const { handleUpdateSegmentEase, handleUpdateKeyframeEase, handleSetAllKeyframeEases } =
463
472
  useKeyframeEaseCommits({ gsapCommitMutation, domEditSelectionRef });
464
473
  const committable = useCommitPreflightCapabilities({
@@ -497,6 +506,7 @@ export function useDomEditSession({
497
506
  handleDomAttributeBatchCommit,
498
507
  handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
499
508
  handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
509
+ handleDomGroupMoveBy,
500
510
  handleDomZIndexReorderCommit,
501
511
  handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
502
512
  handleDomRotationCommit: handleGsapAwareRotationCommit,
@@ -160,6 +160,21 @@ describe("useDomEditTextCommits", () => {
160
160
  };
161
161
  }
162
162
 
163
+ it.each([true, false])(
164
+ "counts in-place text only after a changed write (%s)",
165
+ async (changed) => {
166
+ vi.mocked(trackStudioEvent).mockClear();
167
+ const { persist, save } = richTextProbe();
168
+ persist.mockResolvedValue({ changed, sourceFile: "private.html", version: "v2" });
169
+ await save();
170
+ expect(vi.mocked(trackStudioEvent).mock.calls).toEqual(
171
+ changed
172
+ ? [["feature_used", { feature: "text_edit", surface: "preview", method: "field" }]]
173
+ : [],
174
+ );
175
+ },
176
+ );
177
+
163
178
  it("saves in-place text while the preview is editable, and refreshes the selection it edited", async () => {
164
179
  const applyDomSelection = vi.fn();
165
180
  const { persist, element, save } = richTextProbe(undefined, () => ({ applyDomSelection }));
@@ -335,6 +350,29 @@ describe("useDomEditTextCommits", () => {
335
350
  expect(outcome).toEqual({ ok: true, persistence });
336
351
  });
337
352
 
353
+ it("saves a style map on a selection as one patch, so it is one undo step", async () => {
354
+ const { iframe, element } = previewElement("<div id='card'>Original</div>", "card");
355
+ const persist = vi.fn().mockResolvedValue({ sourceFile: "index.html", changed: true });
356
+ const hook = renderTextCommitHook(
357
+ commitParams({
358
+ previewIframeRef: { current: iframe },
359
+ domEditSelection: null,
360
+ persistDomEditOperations: persist,
361
+ }),
362
+ );
363
+
364
+ await act(async () => {
365
+ await hook.handleDomStyleCommitForSelection(selectionFor(element), {
366
+ "border-width": "4px",
367
+ "border-style": "solid",
368
+ });
369
+ });
370
+
371
+ expect(persist).toHaveBeenCalledTimes(1);
372
+ expect(element.style.borderWidth).toBe("4px");
373
+ expect(element.style.borderStyle).toBe("solid");
374
+ });
375
+
338
376
  it("declines a style commit with no selection, without reaching the writer", async () => {
339
377
  const persistDomEditOperations = vi.fn().mockResolvedValue(undefined);
340
378
  const hook = renderTextCommitHook(
@@ -464,3 +502,6 @@ describe("useDomEditTextCommits", () => {
464
502
  expect(agentElement.textContent).toBe("Edited");
465
503
  });
466
504
  });
505
+
506
+ vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
507
+ import { trackStudioEvent } from "../utils/studioTelemetry";
@@ -1,4 +1,6 @@
1
+ import { trackPreviewFeatureUsed } from "../utils/previewFeatureUsage";
1
2
  import { useCallback, useRef } from "react";
3
+ import { refreshTimelineRowText } from "./refreshTimelineRowText";
2
4
  import { normalizeDomEditStyleValue } from "../utils/studioHelpers";
3
5
  import { injectPreviewGoogleFont, injectPreviewImportedFont } from "../utils/studioFontHelpers";
4
6
  import {
@@ -63,6 +65,7 @@ async function resyncDomTextSelectionFromPreview(
63
65
  if (!doc) return;
64
66
  const refreshed = findElementForSelection(doc, selection, activeCompPath);
65
67
  if (!refreshed) return;
68
+ refreshTimelineRowText(refreshed);
66
69
  const nextSelection = await buildDomSelectionFromTarget(refreshed);
67
70
  if (!nextSelection) return;
68
71
  applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
@@ -106,7 +109,11 @@ export function useDomEditTextCommits({
106
109
  });
107
110
 
108
111
  const handleDomStyleCommitForSelection = useCallback(
109
- (selection: DomEditSelection, property: string, value: string): Promise<DomEditCommitOutcome> =>
112
+ (
113
+ selection: DomEditSelection,
114
+ propertyOrStylesAsOnePatch: string | Record<string, string>,
115
+ value = "",
116
+ ): Promise<DomEditCommitOutcome> =>
110
117
  commitDomStyles(
111
118
  {
112
119
  activeCompPath,
@@ -118,7 +125,9 @@ export function useDomEditTextCommits({
118
125
  resync: refreshDomEditSelectionFromPreview,
119
126
  },
120
127
  selection,
121
- { [property]: value },
128
+ typeof propertyOrStylesAsOnePatch === "string"
129
+ ? { [propertyOrStylesAsOnePatch]: value }
130
+ : propertyOrStylesAsOnePatch,
122
131
  ),
123
132
  [
124
133
  activeCompPath,
@@ -272,11 +281,12 @@ export function useDomEditTextCommits({
272
281
  appliedHtml = element.innerHTML;
273
282
  },
274
283
  persist: async () => {
275
- await persistDomEditOperations(selection, operations, {
284
+ const result = await persistDomEditOperations(selection, operations, {
276
285
  label: "Edit text",
277
286
  skipRefresh: true,
278
287
  shouldSave: isLatestTextCommit,
279
288
  });
289
+ if (result?.changed) trackPreviewFeatureUsed("text_edit", "field");
280
290
  },
281
291
  shouldRevert: () => isLatestTextCommit(),
282
292
  revert: () => {
@@ -104,8 +104,10 @@ export interface UseDomEditWiringParams {
104
104
  animId: string,
105
105
  resolvedFromValues?: Record<string, number | string>,
106
106
  ) => Promise<void>;
107
- removeAllKeyframes: (sel: DomEditSelection, animId: string) => Promise<void>;
108
- handleDomManualEditsReset: (sel: DomEditSelection) => Promise<void>;
107
+ removeAllKeyframes: Parameters<typeof useGsapSelectionHandlers>[0]["removeAllKeyframes"];
108
+ handleDomManualEditsReset: (
109
+ sel: DomEditSelection,
110
+ ) => Promise<void | import("./domEditCommitTypes").DomEditPersistOutcome>;
109
111
  }
110
112
 
111
113
  // fallow-ignore-next-line complexity