@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
@@ -289,6 +289,39 @@ describe("a drag moves only with its own pointer pressed", () => {
289
289
  );
290
290
  });
291
291
 
292
+ describe("an outside change during a drag, on a preview that can swap scenes in place", () => {
293
+ it("leaves the dragged node in place and shows the change after the drop, loaded fresh", async () => {
294
+ const { live, overlay, box } = mountEditor(false);
295
+ const swapScenes = vi.fn((html: string) => {
296
+ live.contentDocument!.body.innerHTML = html.replace(/^[\s\S]*<body>|<\/body>[\s\S]*$/g, "");
297
+ });
298
+ Object.assign(live.contentWindow!, { __hfSwapScenes: swapScenes });
299
+ vi.stubGlobal(
300
+ "fetch",
301
+ async () => new Response(served("").contentDocument!.documentElement.outerHTML),
302
+ );
303
+ const dragged = byId(live, "title");
304
+ pointer(box, "pointerdown", 150, 150);
305
+ pointer(overlay, "pointermove", 190, 170);
306
+
307
+ act(() => api().refreshPlayer());
308
+ await settle();
309
+ expect(swapScenes, "swapped under the pointer").not.toHaveBeenCalled();
310
+ expect(byId(live, "title")).toBe(dragged);
311
+ const beforeDrop = await paintShadow(api, served("?_t=1"));
312
+ expect(api().iframeRef.current, "promoted under the pointer").toBe(live);
313
+
314
+ pointer(overlay, "pointerup", 190, 170);
315
+ await settle();
316
+ expect(file.title).toBe("translate: 40px 20px");
317
+ const fresh = served("?_t=2");
318
+ expect(await paintShadow(api, fresh)).toBeGreaterThan(beforeDrop);
319
+ expect(api().iframeRef.current).toBe(fresh);
320
+ expect(byId(fresh, "title").style.getPropertyValue("translate")).toBe("40px 20px");
321
+ vi.unstubAllGlobals();
322
+ });
323
+ });
324
+
292
325
  type Editor = ReturnType<typeof mountEditor>;
293
326
 
294
327
  describe("every way a drag ends without a drop clears its mark and promotes the held reload", () => {
@@ -78,4 +78,12 @@ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
78
78
  true,
79
79
  );
80
80
 
81
+ // A host whose mounted API authenticates with same-origin cookies sets this; studioApiFetch then sends them, and its
82
+ // requests share the preview media's sockets again.
83
+ export const STUDIO_API_SAME_ORIGIN_CREDENTIALS = resolveStudioBooleanEnvFlag(
84
+ env,
85
+ ["VITE_STUDIO_API_SAME_ORIGIN_CREDENTIALS"],
86
+ false,
87
+ );
88
+
81
89
  import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy";
@@ -10,6 +10,7 @@ export {
10
10
  } from "./manualEditsTypes";
11
11
 
12
12
  export {
13
+ mergeFileChangeAffectedCompositions,
13
14
  readFileChangeAffectedCompositions,
14
15
  readFileChangeAffectsPreview,
15
16
  readFileChangeField,
@@ -43,3 +43,10 @@ export function readFileChangeAffectedCompositions(payload: unknown): readonly s
43
43
  if (!Array.isArray(value) || !value.every((path) => typeof path === "string")) return null;
44
44
  return value.map(normalizeStudioFileChangePath);
45
45
  }
46
+
47
+ export function mergeFileChangeAffectedCompositions(waiting: unknown, incoming: unknown): unknown {
48
+ const before = readFileChangeAffectedCompositions(waiting);
49
+ const after = readFileChangeAffectedCompositions(incoming);
50
+ const merged = before && after ? [...new Set([...before, ...after])] : null;
51
+ return { ...asPayloadRecord(incoming), affectedCompositions: merged };
52
+ }
@@ -146,12 +146,12 @@ function getRectCenter(element: HTMLElement): Point | null {
146
146
  return null;
147
147
  }
148
148
 
149
- let point = {
150
- x: rect.left + rect.width / 2,
151
- y: rect.top + rect.height / 2,
152
- };
149
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
150
+ return toTopScreenPoint(element.ownerDocument, point);
151
+ }
153
152
 
154
- let win: Window | null = element.ownerDocument.defaultView;
153
+ export function toTopScreenPoint(doc: Document, point: Point): Point | null {
154
+ let win: Window | null = doc.defaultView;
155
155
  while (win) {
156
156
  const frameElement = getFrameElement(win);
157
157
  if (!frameElement) break;
@@ -166,7 +166,6 @@ function getRectCenter(element: HTMLElement): Point | null {
166
166
  };
167
167
  win = frameElement.ownerDocument.defaultView;
168
168
  }
169
-
170
169
  return point;
171
170
  }
172
171
 
@@ -1,10 +1,14 @@
1
1
  // @vitest-environment happy-dom
2
- import { act, useRef } from "react";
2
+ import { act, useRef, type RefObject } from "react";
3
3
  import type { Root } from "react-dom/client";
4
4
  import { afterEach, beforeEach, expect, it, vi } from "vitest";
5
5
  import { installReactActEnvironment, mountReactHarness } from "../../hooks/domSelectionTestHarness";
6
+ import { trackStudioEvent } from "../../utils/studioTelemetry";
7
+ import type { DomSelectionResult } from "../../hooks/useDomSelectionTypes";
6
8
  import { useMarqueeGestures } from "./marqueeCommit";
7
9
 
10
+ vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
11
+
8
12
  // Layout stands in for a real preview: every element is visible and sits at the band's start.
9
13
  vi.mock("./domEditingElement", async (importOriginal) => ({
10
14
  ...(await importOriginal<typeof import("./domEditingElement")>()),
@@ -40,9 +44,53 @@ function Overlay() {
40
44
  );
41
45
  }
42
46
 
43
- const pointer = (type: string, clientX: number, clientY: number) =>
47
+ function MarqueeSurface({
48
+ overlayRef,
49
+ marquee,
50
+ }: {
51
+ overlayRef: RefObject<HTMLDivElement | null>;
52
+ marquee: ReturnType<typeof useMarqueeGestures>;
53
+ }) {
54
+ return (
55
+ <div
56
+ ref={overlayRef}
57
+ data-band-overlay
58
+ onPointerDown={marquee.begin}
59
+ onPointerMove={marquee.onPointerMove}
60
+ onPointerUp={marquee.onPointerUp}
61
+ />
62
+ );
63
+ }
64
+
65
+ function TestMarquee({
66
+ iframeRef,
67
+ onSelect,
68
+ resolveHits,
69
+ }: {
70
+ iframeRef: RefObject<HTMLIFrameElement | null>;
71
+ onSelect: (selections: HTMLElement[], additive: boolean) => DomSelectionResult | void;
72
+ resolveHits: (elements: HTMLElement[]) => HTMLElement[] | Promise<HTMLElement[]>;
73
+ }) {
74
+ const overlayRef = useRef<HTMLDivElement>(null);
75
+ const marquee = useMarqueeGestures({
76
+ iframeRef,
77
+ overlayRef,
78
+ activeCompositionPathRef: useRef<string | null>("index.html"),
79
+ onMarqueeSelectRef: { current: onSelect },
80
+ resolveHits,
81
+ });
82
+ return <MarqueeSurface overlayRef={overlayRef} marquee={marquee} />;
83
+ }
84
+
85
+ const pointer = (
86
+ type: string,
87
+ clientX: number,
88
+ clientY: number,
89
+ selector = "[data-overlay]",
90
+ shiftKey = false,
91
+ ) =>
44
92
  act(() => {
45
- document.querySelector("[data-overlay]")!.dispatchEvent(
93
+ document.querySelector(selector)!.dispatchEvent(
46
94
  new PointerEvent(type, {
47
95
  bubbles: true,
48
96
  buttons: type === "pointerup" ? 0 : 1,
@@ -50,6 +98,7 @@ const pointer = (type: string, clientX: number, clientY: number) =>
50
98
  pointerId: 1,
51
99
  clientX,
52
100
  clientY,
101
+ shiftKey,
53
102
  }),
54
103
  );
55
104
  });
@@ -68,6 +117,7 @@ const hostListener = (e: KeyboardEvent) => {
68
117
  };
69
118
 
70
119
  beforeEach(() => {
120
+ vi.clearAllMocks();
71
121
  hostEscapes = [];
72
122
  window.addEventListener("keydown", hostListener);
73
123
  root = mountReactHarness(<Overlay />);
@@ -128,42 +178,17 @@ it("a reload promoted mid-band selects from the preview on screen, not the retir
128
178
  const [retired, live] = [preview(), preview()];
129
179
  const picked: HTMLElement[][] = [];
130
180
  const iframeRef = { current: retired as HTMLIFrameElement | null };
131
- function Band() {
132
- const overlayRef = useRef<HTMLDivElement>(null);
133
- const marquee = useMarqueeGestures({
134
- iframeRef,
135
- overlayRef,
136
- activeCompositionPathRef: useRef<string | null>("index.html"),
137
- onMarqueeSelectRef: useRef(() => undefined),
138
- resolveHits: async (elements: HTMLElement[]) => {
181
+ const band = mountReactHarness(
182
+ <TestMarquee
183
+ iframeRef={iframeRef}
184
+ onSelect={() => undefined}
185
+ resolveHits={async (elements) => {
139
186
  picked.push(elements);
140
187
  return [];
141
- },
142
- });
143
- return (
144
- <div
145
- ref={overlayRef}
146
- data-band-overlay
147
- onPointerDown={marquee.begin}
148
- onPointerMove={marquee.onPointerMove}
149
- onPointerUp={marquee.onPointerUp}
150
- />
151
- );
152
- }
153
- const band = mountReactHarness(<Band />);
154
- const fire = (type: string, x: number, y: number) =>
155
- act(() => {
156
- document.querySelector("[data-band-overlay]")!.dispatchEvent(
157
- new PointerEvent(type, {
158
- bubbles: true,
159
- button: 0,
160
- buttons: type === "pointerup" ? 0 : 1,
161
- pointerId: 1,
162
- clientX: x,
163
- clientY: y,
164
- }),
165
- );
166
- });
188
+ }}
189
+ />,
190
+ );
191
+ const fire = (type: string, x: number, y: number) => pointer(type, x, y, "[data-band-overlay]");
167
192
  try {
168
193
  fire("pointerdown", 0, 0);
169
194
  fire("pointermove", 120, 90);
@@ -175,3 +200,47 @@ it("a reload promoted mid-band selects from the preview on screen, not the retir
175
200
  act(() => band.unmount());
176
201
  }
177
202
  });
203
+
204
+ it.each([
205
+ [2, false, 2, 0],
206
+ [1, true, 2, 1],
207
+ [2, true, 2, 1],
208
+ [1, true, 1, 0],
209
+ ])(
210
+ "counts marquee receipt: %s hits, changed=%s, group=%s",
211
+ async (hits, changed, count, events) => {
212
+ const iframe = document.createElement("iframe");
213
+ const doc = document.implementation.createHTMLDocument("preview");
214
+ doc.body.innerHTML =
215
+ '<div data-composition-id="main"><h1 id="title">Title</h1><p id="subtitle">Subtitle</p></div>';
216
+ Object.defineProperty(iframe, "contentDocument", { value: doc });
217
+ const apply = vi.fn((_selections: HTMLElement[], _additive: boolean) => ({ changed, count }));
218
+ const band = mountReactHarness(
219
+ <TestMarquee
220
+ iframeRef={{ current: iframe }}
221
+ onSelect={apply}
222
+ resolveHits={(elements) => elements.slice(0, hits)}
223
+ />,
224
+ );
225
+ const fire = (type: string, x: number, y: number) =>
226
+ pointer(type, x, y, "[data-band-overlay]", true);
227
+ try {
228
+ fire("pointerdown", 0, 0);
229
+ fire("pointermove", 120, 90);
230
+ await act(async () => {
231
+ fire("pointerup", 120, 90);
232
+ });
233
+ expect(apply).toHaveBeenCalledTimes(1);
234
+ expect(apply.mock.calls[0]![0]).toHaveLength(hits);
235
+ expect(trackStudioEvent).toHaveBeenCalledTimes(events);
236
+ if (events)
237
+ expect(trackStudioEvent).toHaveBeenCalledWith("feature_used", {
238
+ feature: "multi_select",
239
+ surface: "preview",
240
+ method: "drag",
241
+ });
242
+ } finally {
243
+ act(() => band.unmount());
244
+ }
245
+ },
246
+ );
@@ -1,3 +1,5 @@
1
+ import type { DomSelectionResult } from "../../hooks/useDomSelectionTypes";
2
+ import { trackPreviewFeatureUsed } from "../../utils/previewFeatureUsage";
1
3
  // fallow-ignore-file code-duplication
2
4
  import { useCallback, useEffect, useRef, useState } from "react";
3
5
  import type { DomEditSelection } from "./domEditing";
@@ -18,6 +20,16 @@ interface MarqueeState {
18
20
  target: Element;
19
21
  }
20
22
 
23
+ function currentMarqueeCandidates(
24
+ cached: MarqueeHit[] | null,
25
+ iframe: HTMLIFrameElement,
26
+ overlay: HTMLDivElement,
27
+ path: string,
28
+ ): MarqueeHit[] {
29
+ if (cached?.every((hit) => hit.element.ownerDocument === iframe.contentDocument)) return cached;
30
+ return collectMarqueeCandidates(iframe, overlay, path);
31
+ }
32
+
21
33
  const MARQUEE_THRESHOLD_PX = 4;
22
34
 
23
35
  interface MarqueeHit {
@@ -103,7 +115,9 @@ export interface MarqueeGesturesDeps<T = DomEditSelection> {
103
115
  iframeRef: React.RefObject<HTMLIFrameElement | null>;
104
116
  overlayRef: React.RefObject<HTMLDivElement | null>;
105
117
  activeCompositionPathRef: React.RefObject<string | null>;
106
- onMarqueeSelectRef: React.RefObject<((selections: T[], additive: boolean) => void) | undefined>;
118
+ onMarqueeSelectRef: React.RefObject<
119
+ ((selections: T[], additive: boolean) => DomSelectionResult | void) | undefined
120
+ >;
107
121
  /** Turns the elements a drag touched into picks; without it, Studio's edit selections. */
108
122
  resolveHits?: (elements: HTMLElement[]) => T[] | Promise<T[]>;
109
123
  selectionRef?: React.RefObject<DomEditSelection | null>;
@@ -127,6 +141,10 @@ export interface MarqueeGestures {
127
141
  onPointerCancel: () => void;
128
142
  }
129
143
 
144
+ function trackMarqueeSelection(result: DomSelectionResult | void): void {
145
+ if (result?.changed && result.count > 1) trackPreviewFeatureUsed("multi_select", "drag");
146
+ }
147
+
130
148
  function releaseCapture(m: MarqueeState): void {
131
149
  try {
132
150
  m.target.releasePointerCapture(m.pointerId);
@@ -160,18 +178,16 @@ export function useMarqueeGestures<T>(deps: MarqueeGesturesDeps<T>): MarqueeGest
160
178
  const overlay = deps.overlayRef.current;
161
179
  if (!iframe || !overlay || !deps.onMarqueeSelectRef.current) return;
162
180
  const acp = deps.activeCompositionPathRef.current ?? "index.html";
163
- const cached = candidatesRef.current;
164
- const measuredOnScreen = cached?.every(
165
- (hit) => hit.element.ownerDocument === iframe.contentDocument,
166
- );
167
- const candidates =
168
- cached && measuredOnScreen ? cached : collectMarqueeCandidates(iframe, overlay, acp);
181
+ const candidates = currentMarqueeCandidates(candidatesRef.current, iframe, overlay, acp);
169
182
  const elements = hitsWithin(rect, candidates).map((hit) => hit.element);
170
183
  const resolveHits = deps.resolveHits;
171
184
  const picks = resolveHits
172
185
  ? await resolveHits(elements)
173
186
  : ((await resolveDomEditSelections(elements, acp)) as T[]);
174
- deps.onMarqueeSelectRef.current?.(picks, additive);
187
+ const applySelection = deps.onMarqueeSelectRef.current;
188
+ if (!applySelection) return;
189
+ const result = applySelection(picks, additive);
190
+ trackMarqueeSelection(result);
175
191
  },
176
192
  [
177
193
  deps.iframeRef,
@@ -1,5 +1,5 @@
1
1
  // @vitest-environment jsdom
2
- import { describe, it, expect, vi } from "vitest";
2
+ import { beforeEach, describe, it, expect, vi } from "vitest";
3
3
  import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
4
4
  import { editableAnimationId } from "./motionPathSelection";
5
5
  import {
@@ -148,3 +148,31 @@ describe("commitCreatePath", () => {
148
148
  );
149
149
  });
150
150
  });
151
+
152
+ vi.mock("../../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
153
+ import { trackStudioEvent } from "../../utils/studioTelemetry";
154
+ import type { CommitFn } from "./motionPathCommit";
155
+ beforeEach(() => vi.clearAllMocks());
156
+ describe("motion path usage at the writer", () => {
157
+ it("counts a changed drag once without coordinates or target ids", async () => {
158
+ const commit: CommitFn = async (_mutation, options) => {
159
+ options.onResult?.({ ok: true, changed: true });
160
+ };
161
+ await commitNode({ type: "keyframe", pct: 50 }, 120, 30, "private-target", commit);
162
+ expect(trackStudioEvent).toHaveBeenCalledExactlyOnceWith("feature_used", {
163
+ feature: "motion_path",
164
+ surface: "preview",
165
+ method: "drag",
166
+ });
167
+ });
168
+ it.each([undefined, { ok: true }, { ok: true, changed: false }, { ok: false, changed: true }])(
169
+ "does not count absent or unsuccessful results (%j)",
170
+ async (result) => {
171
+ const commit: CommitFn = async (_mutation, options) => {
172
+ if (result) options.onResult?.(result);
173
+ };
174
+ await commitAddWaypoint("private-target", 1, 20, 40, commit);
175
+ expect(trackStudioEvent).not.toHaveBeenCalled();
176
+ },
177
+ );
178
+ });
@@ -1,3 +1,5 @@
1
+ import type { CommitMutationOptions } from "../../hooks/gsapScriptCommitTypes";
2
+ import { trackPreviewFeatureUsed, type PreviewMethod } from "../../utils/previewFeatureUsage";
1
3
  /**
2
4
  * Commit helpers for the motion-path overlay. Each maps a canvas gesture to a
3
5
  * GSAP source mutation routed through the (selection-bound) commit facade, which
@@ -7,11 +9,21 @@ import type { MotionNodeRef } from "./motionPathGeometry";
7
9
 
8
10
  export type CommitFn = (
9
11
  mutation: Record<string, unknown>,
10
- options: { label: string; softReload?: boolean },
12
+ options: CommitMutationOptions,
11
13
  ) => Promise<void>;
12
14
 
13
15
  const NEW_PATH_DURATION = 1.5;
14
16
 
17
+ function motionPathCommitOptions(label: string, method: PreviewMethod): CommitMutationOptions {
18
+ return {
19
+ label,
20
+ softReload: true,
21
+ onResult: (result) => {
22
+ if (result.ok && result.changed === true) trackPreviewFeatureUsed("motion_path", method);
23
+ },
24
+ };
25
+ }
26
+
15
27
  export function commitNode(
16
28
  ref: MotionNodeRef,
17
29
  x: number,
@@ -23,10 +35,10 @@ export function commitNode(
23
35
  ref.type === "keyframe"
24
36
  ? { type: "update-keyframe", animationId, percentage: ref.pct, properties: { x, y } }
25
37
  : { type: "update-motion-path-point", animationId, pointIndex: ref.index, x, y };
26
- return commit(mutation, {
27
- label: ref.type === "keyframe" ? "Move keyframe" : "Move waypoint",
28
- softReload: true,
29
- });
38
+ return commit(
39
+ mutation,
40
+ motionPathCommitOptions(ref.type === "keyframe" ? "Move keyframe" : "Move waypoint", "drag"),
41
+ );
30
42
  }
31
43
 
32
44
  export function commitAddWaypoint(
@@ -38,7 +50,7 @@ export function commitAddWaypoint(
38
50
  ): Promise<void> {
39
51
  return commit(
40
52
  { type: "add-motion-path-point", animationId, index, x, y },
41
- { label: "Add waypoint", softReload: true },
53
+ motionPathCommitOptions("Add waypoint", "button"),
42
54
  );
43
55
  }
44
56
 
@@ -54,7 +66,7 @@ export function commitAddKeyframe(
54
66
  // at that pct) and converts a flat tween to keyframes form when needed.
55
67
  return commit(
56
68
  { type: "add-keyframe", animationId, percentage, properties: { x, y } },
57
- { label: "Add keyframe", softReload: true },
69
+ motionPathCommitOptions("Add keyframe", "button"),
58
70
  );
59
71
  }
60
72
 
@@ -65,7 +77,7 @@ export function commitRemoveWaypoint(
65
77
  ): Promise<void> {
66
78
  return commit(
67
79
  { type: "remove-motion-path-point", animationId, index },
68
- { label: "Remove waypoint", softReload: true },
80
+ motionPathCommitOptions("Remove waypoint", "button"),
69
81
  );
70
82
  }
71
83
 
@@ -78,6 +90,6 @@ export function commitCreatePath(
78
90
  ): Promise<void> {
79
91
  return commit(
80
92
  { type: "add-motion-path", targetSelector, position, duration: NEW_PATH_DURATION, x, y },
81
- { label: "Create motion path", softReload: true },
93
+ motionPathCommitOptions("Create motion path", "button"),
82
94
  );
83
95
  }
@@ -1,41 +1,16 @@
1
1
  // @vitest-environment jsdom
2
2
  import { afterEach, describe, expect, it } from "vitest";
3
- import type { DomEditSelection } from "./domEditingTypes";
4
- import { buildStableSelector, getSelectorIndex } from "./domEditingDom";
3
+ import { mountGroupSiblings, stableSelectionFor } from "../../hooks/domSelectionTestHarness";
5
4
  import { selectorFor } from "./motionPathSelection";
6
5
 
7
6
  afterEach(() => {
8
7
  document.body.innerHTML = "";
9
8
  });
10
9
 
11
- function selectionFor(el: HTMLElement): DomEditSelection {
12
- const selector = buildStableSelector(el);
13
- return {
14
- element: el,
15
- id: el.id || undefined,
16
- hfId: el.getAttribute("data-hf-id") || undefined,
17
- selector,
18
- selectorIndex: getSelectorIndex(document, el, selector, "index.html", null),
19
- sourceFile: "index.html",
20
- dataAttributes: { start: "0", duration: "2" },
21
- } as unknown as DomEditSelection;
22
- }
23
-
24
- function mountGroupSiblings(): HTMLElement[] {
25
- document.body.innerHTML = `
26
- <div id="scene" class="clip" data-start="0" data-duration="2">
27
- <div class="group"></div>
28
- <div class="group"></div>
29
- <div class="group"></div>
30
- </div>
31
- `;
32
- return Array.from(document.querySelectorAll<HTMLElement>(".group"));
33
- }
34
-
35
10
  describe("selectorFor", () => {
36
11
  it("addresses one element for a class-only sibling", () => {
37
- const groups = mountGroupSiblings();
38
- const selector = selectorFor(selectionFor(groups[2]!));
12
+ const groups = mountGroupSiblings(3);
13
+ const selector = selectorFor(stableSelectionFor(groups[2]!));
39
14
 
40
15
  // The bare ".group" both measured home off the FIRST sibling and wrote the
41
16
  // new motion path onto all three.
@@ -48,7 +23,7 @@ describe("selectorFor", () => {
48
23
  document.body.innerHTML = `<div id="hero"></div>`;
49
24
  const el = document.querySelector<HTMLElement>("#hero")!;
50
25
 
51
- expect(selectorFor(selectionFor(el))).toBe("#hero");
26
+ expect(selectorFor(stableSelectionFor(el))).toBe("#hero");
52
27
  });
53
28
 
54
29
  it("returns null with no selection", () => {
@@ -56,8 +31,8 @@ describe("selectorFor", () => {
56
31
  });
57
32
 
58
33
  it("returns null when no rung addresses one element", () => {
59
- const groups = mountGroupSiblings();
60
- const selection = selectionFor(groups[1]!);
34
+ const groups = mountGroupSiblings(3);
35
+ const selection = stableSelectionFor(groups[1]!);
61
36
  groups[1]!.remove();
62
37
 
63
38
  expect(selectorFor(selection)).toBeNull();
@@ -0,0 +1,45 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { probeSourceElement } from "./probeSourceElement";
3
+
4
+ afterEach(() => vi.unstubAllGlobals());
5
+
6
+ const answer = (body: unknown, ok = true) =>
7
+ vi.fn(() => Promise.resolve({ ok, json: () => Promise.resolve(body) }));
8
+
9
+ describe("probeSourceElement", () => {
10
+ it("sends the asks for one file in one tick as a single request", async () => {
11
+ const fetchMock = answer({ exists: [true, false, true] });
12
+ vi.stubGlobal("fetch", fetchMock);
13
+
14
+ const results = await Promise.all([
15
+ probeSourceElement("p", "index.html", { id: "a" }),
16
+ probeSourceElement("p", "index.html", { id: "b" }),
17
+ probeSourceElement("p", "index.html", { id: "c" }),
18
+ ]);
19
+
20
+ expect(results).toEqual([true, false, true]);
21
+ expect(fetchMock).toHaveBeenCalledOnce();
22
+ const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
23
+ expect(JSON.parse(String(init.body)).targets).toEqual([{ id: "a" }, { id: "b" }, { id: "c" }]);
24
+ });
25
+
26
+ it("keeps one request per file", async () => {
27
+ const fetchMock = answer({ exists: [true] });
28
+ vi.stubGlobal("fetch", fetchMock);
29
+ await Promise.all([
30
+ probeSourceElement("p", "index.html", { id: "a" }),
31
+ probeSourceElement("p", "compositions/x.html", { id: "a" }),
32
+ ]);
33
+ expect(fetchMock).toHaveBeenCalledTimes(2);
34
+ });
35
+
36
+ it("reads a failed or malformed answer as existing", async () => {
37
+ vi.stubGlobal("fetch", answer({}, false));
38
+ expect(await probeSourceElement("p", "index.html", { id: "a" })).toBe(true);
39
+ vi.stubGlobal(
40
+ "fetch",
41
+ vi.fn(() => Promise.reject(new Error("offline"))),
42
+ );
43
+ expect(await probeSourceElement("p", "index.html", { id: "a" })).toBe(true);
44
+ });
45
+ });
@@ -1,29 +1,54 @@
1
1
  import { buildProjectApiPath } from "../../utils/projectRouting";
2
+ import { studioApiFetch } from "../../utils/studioApiFetch";
2
3
 
3
- export async function probeSourceElement(
4
- projectId: string,
5
- sourceFile: string,
6
- target: { id?: string; hfId?: string; selector?: string; selectorIndex?: number },
7
- ): Promise<boolean> {
4
+ type ProbeTarget = { id?: string; hfId?: string; selector?: string; selectorIndex?: number };
5
+
6
+ interface PendingProbe {
7
+ target: ProbeTarget;
8
+ resolve: (exists: boolean) => void;
9
+ }
10
+
11
+ const pending = new Map<string, PendingProbe[]>();
12
+
13
+ async function sendBatch(projectId: string, sourceFile: string, probes: PendingProbe[]) {
14
+ let exists: boolean[] = [];
8
15
  try {
9
- const response = await fetch(
16
+ const response = await studioApiFetch(
10
17
  buildProjectApiPath(
11
18
  projectId,
12
- `/file-mutations/probe-element/${encodeURIComponent(sourceFile)}`,
19
+ `/file-mutations/probe-elements/${encodeURIComponent(sourceFile)}`,
13
20
  ),
14
21
  {
15
22
  method: "POST",
16
23
  headers: { "Content-Type": "application/json" },
17
- body: JSON.stringify({ target }),
24
+ body: JSON.stringify({ targets: probes.map((probe) => probe.target) }),
18
25
  },
19
26
  );
20
- if (!response.ok) return true;
21
- const data = await response.json();
22
- if (data && typeof data === "object" && "exists" in data && data.exists === false) {
23
- return false;
27
+ if (response.ok) {
28
+ const data = await response.json();
29
+ if (Array.isArray(data?.exists)) exists = data.exists;
30
+ }
31
+ } catch {}
32
+ probes.forEach((probe, index) => probe.resolve(exists[index] !== false));
33
+ }
34
+
35
+ export function probeSourceElement(
36
+ projectId: string,
37
+ sourceFile: string,
38
+ target: ProbeTarget,
39
+ ): Promise<boolean> {
40
+ return new Promise((resolve) => {
41
+ const key = `${projectId}\0${sourceFile}`;
42
+ let probes = pending.get(key);
43
+ if (!probes) {
44
+ const batch: PendingProbe[] = [];
45
+ probes = batch;
46
+ pending.set(key, batch);
47
+ setTimeout(() => {
48
+ pending.delete(key);
49
+ void sendBatch(projectId, sourceFile, batch);
50
+ }, 0);
24
51
  }
25
- return true;
26
- } catch {
27
- return true;
28
- }
52
+ probes.push({ target, resolve });
53
+ });
29
54
  }