@hyperframes/studio 0.8.138 → 0.8.140

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 (135) hide show
  1. package/dist/assets/{hyperframes-player-B30apksz.js → hyperframes-player-BLuyxnK7.js} +1 -1
  2. package/dist/assets/{index-DXVvWfQG.js → index-CWfuDCoA.js} +1 -1
  3. package/dist/assets/{index-DIJuIBwI.js → index-Dr3Bo9uH.js} +1 -1
  4. package/dist/assets/{index-BdjA0CLD.js → index-VAHDM_zL.js} +210 -210
  5. package/dist/{chunk-4S3AYCOG.js → chunk-H2DUOXTF.js} +3 -2
  6. package/dist/{chunk-TDWGXBUT.js → chunk-TFLU2MGI.js} +1 -6
  7. package/dist/chunk-TFLU2MGI.js.map +1 -0
  8. package/dist/{chunk-7UQWVN52.js → chunk-UYWON5AX.js} +2 -2
  9. package/dist/{domEditingLayers-IEVLO54Z.js → domEditingLayers-KBTPQKGA.js} +3 -3
  10. package/dist/index.d.ts +20 -3
  11. package/dist/index.html +1 -1
  12. package/dist/index.js +7327 -6265
  13. package/dist/index.js.map +1 -1
  14. package/dist/player.js +2 -2
  15. package/package.json +8 -8
  16. package/src/App.tsx +9 -4
  17. package/src/components/TimelineToolbar.test.tsx +79 -1
  18. package/src/components/TimelineToolbar.tsx +14 -29
  19. package/src/components/editor/DomEditOverlay.tsx +11 -7
  20. package/src/components/editor/PropertyPanel.test.tsx +600 -748
  21. package/src/components/editor/PropertyPanel.tsx +1 -0
  22. package/src/components/editor/PropertyPanelFlat.tsx +1 -0
  23. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +2 -0
  24. package/src/components/editor/domEditOverlayGeometry.ts +7 -7
  25. package/src/components/editor/domEditOverlayGestures.ts +18 -0
  26. package/src/components/editor/domEditOverlayStartGesture.blocked.test.ts +1 -0
  27. package/src/components/editor/domEditOverlayStartGesture.test.ts +3 -0
  28. package/src/components/editor/domEditOverlayStartGesture.ts +9 -1
  29. package/src/components/editor/dragUndoPaint.test.ts +2 -0
  30. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +432 -15
  31. package/src/components/editor/groupDropKeepsSelection.test.ts +1 -0
  32. package/src/components/editor/manualEdits.ts +1 -1
  33. package/src/components/editor/manualEditsDom.ts +7 -7
  34. package/src/components/editor/manualEditsParsing.ts +14 -2
  35. package/src/components/editor/manualEditsTypes.ts +5 -0
  36. package/src/components/editor/manualOffsetDrag.test.ts +23 -0
  37. package/src/components/editor/manualOffsetDrag.ts +5 -0
  38. package/src/components/editor/previewGestureStarts.ts +284 -0
  39. package/src/components/editor/previewReadOnly.overlay.test.tsx +3 -2
  40. package/src/components/editor/propertyPanelFill.tsx +36 -13
  41. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +4 -0
  42. package/src/components/editor/propertyPanelFlatStyleSections.tsx +13 -7
  43. package/src/components/editor/propertyPanelImageFill.test.tsx +164 -0
  44. package/src/components/editor/propertyPanelStyleSections.tsx +10 -7
  45. package/src/components/editor/rotateInlineSave.test.ts +1 -0
  46. package/src/components/editor/shiftDragAxisLock.test.tsx +1 -0
  47. package/src/components/editor/useDomEditOverlayGestures.ts +67 -71
  48. package/src/components/nle/NLEContext.test.ts +26 -0
  49. package/src/components/nle/NLEContext.tsx +8 -3
  50. package/src/components/nle/NLEPreview.test.ts +44 -1
  51. package/src/components/nle/NLEPreview.tsx +7 -2
  52. package/src/hooks/draggedGsapPosition.ts +8 -0
  53. package/src/hooks/editMoment.ts +13 -0
  54. package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
  55. package/src/hooks/gsapDragCommit.ts +2 -1
  56. package/src/hooks/gsapDragPositionCommit.ts +19 -7
  57. package/src/hooks/gsapPositionDetection.ts +5 -3
  58. package/src/hooks/gsapResizeIntercept.ts +11 -4
  59. package/src/hooks/gsapRuntimeBridge.ts +31 -11
  60. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
  61. package/src/hooks/gsapValueAtPlayhead.test.ts +15 -0
  62. package/src/hooks/gsapValueAtPlayhead.ts +12 -5
  63. package/src/hooks/timelineEditingHelpers.test.ts +22 -0
  64. package/src/hooks/timelineEditingHelpers.ts +18 -2
  65. package/src/hooks/useDomSelection.test.ts +21 -2
  66. package/src/hooks/useEditHistoryActions.test.tsx +20 -0
  67. package/src/hooks/useEditHistoryActions.ts +2 -0
  68. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
  69. package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
  70. package/src/hooks/useGsapAwareEditing.test.tsx +38 -0
  71. package/src/hooks/useGsapAwareEditing.ts +148 -136
  72. package/src/hooks/useGsapAwareGroupMove.ts +126 -113
  73. package/src/hooks/useGsapScriptCommits.ts +5 -4
  74. package/src/hooks/useMusicBeatAnalysis.ts +2 -1
  75. package/src/hooks/usePreviewPersistence.history.test.tsx +7 -2
  76. package/src/hooks/usePreviewPersistence.ts +1 -4
  77. package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
  78. package/src/hooks/useSdkSession.ts +4 -1
  79. package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
  80. package/src/hooks/useThumbnailStripSize.ts +23 -1
  81. package/src/hooks/useTimelineDeleteOps.test.tsx +10 -0
  82. package/src/hooks/useTimelineDeleteOps.ts +9 -10
  83. package/src/index.ts +2 -0
  84. package/src/player/components/AudioWaveform.test.tsx +153 -1
  85. package/src/player/components/AudioWaveform.tsx +93 -37
  86. package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
  87. package/src/player/components/Timeline.test.ts +3 -1
  88. package/src/player/components/TimelineCanvas.tsx +4 -0
  89. package/src/player/components/TimelineClip.tsx +20 -6
  90. package/src/player/components/TimelineClipFades.test.tsx +11 -0
  91. package/src/player/components/TimelineClipFades.tsx +4 -4
  92. package/src/player/components/TimelineClipJoins.tsx +23 -18
  93. package/src/player/components/TimelineGestureOverlay.tsx +30 -27
  94. package/src/player/components/TimelineGroupRow.tsx +1 -0
  95. package/src/player/components/TimelineLaneClip.tsx +127 -0
  96. package/src/player/components/TimelineLanes.test.tsx +51 -6
  97. package/src/player/components/TimelineLanes.tsx +166 -178
  98. package/src/player/components/TimelineRuler.tsx +2 -0
  99. package/src/player/components/TimelineTimeLayer.tsx +25 -0
  100. package/src/player/components/VideoThumbnail.test.tsx +204 -12
  101. package/src/player/components/VideoThumbnail.tsx +37 -9
  102. package/src/player/components/thumbnailUtils.test.ts +17 -3
  103. package/src/player/components/thumbnailUtils.ts +17 -1
  104. package/src/player/components/timelineLayout.test.ts +3 -3
  105. package/src/player/components/timelineMotion.test.ts +27 -0
  106. package/src/player/components/timelineMotion.ts +62 -0
  107. package/src/player/components/timelineZoomInput.test.ts +438 -0
  108. package/src/player/components/timelineZoomInput.ts +456 -0
  109. package/src/player/components/useTimelineGeometry.ts +0 -3
  110. package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
  111. package/src/player/components/useTimelinePlayhead.ts +53 -59
  112. package/src/player/components/useTimelineProviderState.tsx +16 -21
  113. package/src/player/components/useTimelineZoom.ts +7 -12
  114. package/src/player/hooks/useShadowPreviewReload.ts +6 -6
  115. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +14 -0
  116. package/src/player/hooks/useTimelinePlayer.ts +8 -5
  117. package/src/player/lib/thumbnailScheduler.test.ts +20 -1
  118. package/src/player/lib/thumbnailScheduler.ts +1 -1
  119. package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
  120. package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
  121. package/src/player/lib/timelineElementHelpers.ts +0 -11
  122. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  123. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  124. package/src/player/previewReloading.test.ts +48 -0
  125. package/src/player/previewReloading.ts +48 -0
  126. package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
  127. package/src/utils/rootDuration.ts +5 -4
  128. package/src/utils/studioPendingEdits.test.ts +27 -0
  129. package/src/utils/studioPendingEdits.ts +9 -0
  130. package/src/utils/timelineAssetDrop.test.ts +13 -4
  131. package/src/utils/timelineAssetDrop.ts +6 -2
  132. package/dist/chunk-TDWGXBUT.js.map +0 -1
  133. /package/dist/{chunk-4S3AYCOG.js.map → chunk-H2DUOXTF.js.map} +0 -0
  134. /package/dist/{chunk-7UQWVN52.js.map → chunk-UYWON5AX.js.map} +0 -0
  135. /package/dist/{domEditingLayers-IEVLO54Z.js.map → domEditingLayers-KBTPQKGA.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
2
2
  import {
3
- mergeFileChangeAffectedCompositions,
3
+ mergeFileChangePayloads,
4
4
  readFileChangeAffectedCompositions,
5
5
  readFileChangeAffectsPreview,
6
6
  readFileChangeField,
@@ -125,14 +125,18 @@ function decodeFileChange(delivery: unknown): unknown {
125
125
  }
126
126
  }
127
127
 
128
- /**
129
- * The production transport. vitest defines `import.meta.hot`, so the selection
130
- * below never reaches this rung under test; the rung itself is exported so a test
131
- * can drive a real `MessageEvent` through the listener it registers.
132
- */
133
- export function sseFileChangeChannel(onDelivery: (delivery: unknown) => void): () => void {
128
+ export function sseFileChangeChannel(
129
+ onDelivery: (delivery: unknown) => void,
130
+ onReconnect: () => void,
131
+ ): () => void {
134
132
  const eventSource = new EventSource("/api/events");
135
133
  eventSource.addEventListener("file-change", onDelivery);
134
+ // Reopening the stream does not replay changes missed during a disconnect.
135
+ let opened = false;
136
+ eventSource.addEventListener("open", () => {
137
+ if (opened) onReconnect();
138
+ opened = true;
139
+ });
136
140
  return () => eventSource.close();
137
141
  }
138
142
 
@@ -168,18 +172,19 @@ export function useExternalFileChangeCoordinator({
168
172
  const generationRef = useRef(0);
169
173
  const mountedRef = useRef(true);
170
174
  const lastEventIdentityRef = useRef<string | null>(null);
171
- const blockedRef = useRef(blocked);
175
+ const blockedRef = useRef<ExternalFileChangeBlockedState | null>(null);
172
176
  const snapshotWriteTailRef = useRef<Promise<void>>(Promise.resolve());
173
177
  const drainingRef = useRef(false);
174
178
  const pendingPayloadRef = useRef<{ payload: unknown } | null>(null);
175
- blockedRef.current = blocked;
176
- // A newer blocked change replaces the held one, so it inherits the thumbnails the held one owed.
179
+ // A newer blocked change replaces the held one, so it inherits what the held one owed.
177
180
  const setBlocked = useCallback((next: ExternalFileChangeBlockedState | null) => {
178
- setBlockedState((held) =>
181
+ const held = blockedRef.current;
182
+ const current =
179
183
  next && held
180
- ? { ...next, payload: mergeFileChangeAffectedCompositions(held.payload, next.payload) }
181
- : next,
182
- );
184
+ ? { ...next, payload: mergeFileChangePayloads(held.payload, next.payload) }
185
+ : next;
186
+ blockedRef.current = current;
187
+ setBlockedState(current);
183
188
  }, []);
184
189
 
185
190
  useEffect(() => {
@@ -190,6 +195,10 @@ export function useExternalFileChangeCoordinator({
190
195
  };
191
196
  }, []);
192
197
 
198
+ useEffect(() => {
199
+ pendingPayloadRef.current = null;
200
+ }, [projectId]);
201
+
193
202
  useEffect(() => {
194
203
  generationRef.current += 1;
195
204
  setBlocked(null);
@@ -240,8 +249,12 @@ export function useExternalFileChangeCoordinator({
240
249
  };
241
250
  }, [loadConflictSnapshot, projectId, recoveryFilePath, setBlocked]);
242
251
 
243
- const reloadAcceptedGeneration = useCallback(
244
- (path: string, affectsPreview = true) => {
252
+ // What a settled change owes: thumbnails (unless its own write's echo brings them), then reloads.
253
+ const deliverOwed = useCallback(
254
+ (owed: unknown, accept = true) => {
255
+ const path = readStudioFileChangePath(owed) ?? ".";
256
+ const affectsPreview = readFileChangeAffectsPreview(owed);
257
+ if (accept) onAcceptedPersistedFileChange(path, readFileChangeAffectedCompositions(owed));
245
258
  logReload(affectsPreview ? "reload" : "file-tree only", {
246
259
  path,
247
260
  by: "external-change coordinator",
@@ -255,7 +268,7 @@ export function useExternalFileChangeCoordinator({
255
268
  // a watcher event the SSE never delivered).
256
269
  void refreshFileTree?.();
257
270
  },
258
- [reloadPreview, reloadSdkSession, refreshFileTree],
271
+ [onAcceptedPersistedFileChange, reloadPreview, reloadSdkSession, refreshFileTree],
259
272
  );
260
273
 
261
274
  const persistSnapshotInOrder = useCallback(async (write: () => Promise<void>) => {
@@ -274,21 +287,28 @@ export function useExternalFileChangeCoordinator({
274
287
  if (!path) return;
275
288
 
276
289
  const generation = ++generationRef.current;
290
+ const savingPath = getPendingCandidate?.()?.path;
277
291
  const result = await drainPendingChanges();
278
292
  if (!mountedRef.current || generation !== generationRef.current) return;
279
293
 
280
294
  if (result.status === "clean") {
281
295
  const previousBlocked = blockedRef.current;
282
- if (previousBlocked?.status === "failed" && deleteConflictSnapshot) {
296
+ // Only saving the failed draft's file retires its copy; restored drafts are never queued.
297
+ if (
298
+ previousBlocked?.status === "failed" &&
299
+ !previousBlocked.recovered &&
300
+ previousBlocked.path === savingPath &&
301
+ deleteConflictSnapshot
302
+ ) {
283
303
  try {
284
- await deleteConflictSnapshot(projectId!, path);
304
+ await deleteConflictSnapshot(projectId!, previousBlocked.path);
285
305
  } catch (error) {
286
306
  if (mountedRef.current && generation === generationRef.current) {
287
307
  setBlocked({
288
308
  ...previousBlocked,
289
309
  generation,
290
310
  error,
291
- payload: mergeFileChangeAffectedCompositions(payload, previousBlocked.payload),
311
+ payload: mergeFileChangePayloads(payload, previousBlocked.payload),
292
312
  });
293
313
  }
294
314
  return;
@@ -296,27 +316,30 @@ export function useExternalFileChangeCoordinator({
296
316
  }
297
317
  if (!mountedRef.current || generation !== generationRef.current) return;
298
318
  setBlocked(null);
299
- const owed = previousBlocked
300
- ? mergeFileChangeAffectedCompositions(previousBlocked.payload, payload)
301
- : payload;
302
- onAcceptedPersistedFileChange(path, readFileChangeAffectedCompositions(owed));
303
- reloadAcceptedGeneration(path, readFileChangeAffectsPreview(payload));
319
+ const restored = previousBlocked?.status === "failed" && previousBlocked.recovered;
320
+ deliverOwed(
321
+ previousBlocked && !restored
322
+ ? mergeFileChangePayloads(previousBlocked.payload, payload)
323
+ : payload,
324
+ );
304
325
  return;
305
326
  }
306
- const content = readFileChangeContent(payload);
307
327
  if (result.status === "failed") {
328
+ // The draft names the file to recover; the payload stays the scope to reload.
308
329
  const candidate = getPendingCandidate?.();
309
- const studioContent = candidate?.path === path ? candidate.content : null;
330
+ const failedPath = candidate?.path ?? (path === "." ? (recoveryFilePath ?? path) : path);
331
+ const studioContent = candidate?.content ?? null;
332
+ const external = failedPath === path ? payload : null;
310
333
  let error = result.error;
311
334
  if (studioContent != null && persistFailureSnapshot) {
312
335
  try {
313
336
  await persistSnapshotInOrder(() =>
314
337
  persistFailureSnapshot(
315
338
  projectId!,
316
- path,
339
+ failedPath,
317
340
  studioContent,
318
- readFileChangeVersion(payload),
319
- content,
341
+ readFileChangeVersion(external),
342
+ readFileChangeContent(external),
320
343
  result.error,
321
344
  ),
322
345
  );
@@ -333,7 +356,7 @@ export function useExternalFileChangeCoordinator({
333
356
  setBlocked({
334
357
  status: "failed",
335
358
  generation,
336
- path,
359
+ path: failedPath,
337
360
  error,
338
361
  payload,
339
362
  studioContent,
@@ -348,7 +371,7 @@ export function useExternalFileChangeCoordinator({
348
371
  setBlocked({
349
372
  status: "failed",
350
373
  generation,
351
- path,
374
+ path: result.error.filePath,
352
375
  error,
353
376
  payload,
354
377
  studioContent: result.error.attemptedContent,
@@ -365,14 +388,17 @@ export function useExternalFileChangeCoordinator({
365
388
  projectId,
366
389
  deleteConflictSnapshot,
367
390
  getPendingCandidate,
391
+ recoveryFilePath,
368
392
  persistConflictSnapshot,
369
393
  persistFailureSnapshot,
370
394
  persistSnapshotInOrder,
371
- reloadAcceptedGeneration,
372
- onAcceptedPersistedFileChange,
395
+ deliverOwed,
373
396
  ],
374
397
  );
375
398
 
399
+ const drainOnePendingRef = useRef(drainOnePending);
400
+ drainOnePendingRef.current = drainOnePending;
401
+
376
402
  const startDrainLoop = useCallback(async () => {
377
403
  if (drainingRef.current) return;
378
404
  drainingRef.current = true;
@@ -381,12 +407,12 @@ export function useExternalFileChangeCoordinator({
381
407
  const pending = pendingPayloadRef.current;
382
408
  if (!pending) break;
383
409
  pendingPayloadRef.current = null;
384
- await drainOnePending(pending.payload);
410
+ await drainOnePendingRef.current(pending.payload);
385
411
  }
386
412
  } finally {
387
413
  drainingRef.current = false;
388
414
  }
389
- }, [drainOnePending]);
415
+ }, []);
390
416
 
391
417
  const processChange = useCallback(
392
418
  // fallow-ignore-next-line complexity
@@ -439,8 +465,7 @@ export function useExternalFileChangeCoordinator({
439
465
  !readFileChangeAffectsPreview(payload);
440
466
  if (!waitingChangeOutranksThis) {
441
467
  pendingPayloadRef.current = {
442
- payload:
443
- waiting == null ? payload : mergeFileChangeAffectedCompositions(waiting, payload),
468
+ payload: waiting == null ? payload : mergeFileChangePayloads(waiting, payload),
444
469
  };
445
470
  }
446
471
  void startDrainLoop();
@@ -462,7 +487,8 @@ export function useExternalFileChangeCoordinator({
462
487
  import.meta.hot.on("hf:file-change", handler);
463
488
  return () => import.meta.hot?.off?.("hf:file-change", handler);
464
489
  }
465
- return sseFileChangeChannel(handler);
490
+ const catchUp = () => processChangeRef.current({ path: "." });
491
+ return sseFileChangeChannel(handler, catchUp);
466
492
  }, []);
467
493
 
468
494
  const retry = useCallback(async () => {
@@ -489,18 +515,16 @@ export function useExternalFileChangeCoordinator({
489
515
  onUseExternalFile?.(path, external);
490
516
  await deleteConflictSnapshot?.(projectId, path);
491
517
  setBlocked(null);
492
- onAcceptedPersistedFileChange(path, readFileChangeAffectedCompositions(current.payload));
493
- reloadAcceptedGeneration(path, readFileChangeAffectsPreview(current.payload));
518
+ deliverOwed(current.payload);
494
519
  },
495
520
  [
496
521
  deleteConflictSnapshot,
497
522
  setBlocked,
498
523
  discardPendingChanges,
499
524
  onUseExternalFile,
500
- onAcceptedPersistedFileChange,
501
525
  projectId,
502
526
  readProjectFile,
503
- reloadAcceptedGeneration,
527
+ deliverOwed,
504
528
  resetSaveQueues,
505
529
  ],
506
530
  );
@@ -550,7 +574,10 @@ export function useExternalFileChangeCoordinator({
550
574
  resetSaveQueues?.();
551
575
  await deleteConflictSnapshot?.(projectId, conflict.filePath);
552
576
  setBlocked(null);
553
- reloadAcceptedGeneration(conflict.filePath);
577
+ deliverOwed(
578
+ mergeFileChangePayloads(current.payload, { path: conflict.filePath, affectsPreview: true }),
579
+ readStudioFileChangePath(current.payload) !== conflict.filePath,
580
+ );
554
581
  }, [
555
582
  deleteConflictSnapshot,
556
583
  setBlocked,
@@ -558,7 +585,7 @@ export function useExternalFileChangeCoordinator({
558
585
  overwriteConflict,
559
586
  projectId,
560
587
  readProjectFile,
561
- reloadAcceptedGeneration,
588
+ deliverOwed,
562
589
  resetSaveQueues,
563
590
  ]);
564
591
 
@@ -42,6 +42,7 @@ vi.mock("./useSafeGsapCommitMutation", () => ({
42
42
  }));
43
43
 
44
44
  import { useGsapAwareEditing } from "./useGsapAwareEditing";
45
+ import { isPreviewChanging } from "../player/previewReloading";
45
46
  import { tryGsapRotationIntercept } from "./gsapRuntimeBridge";
46
47
 
47
48
  afterEach(() => {
@@ -235,6 +236,43 @@ describe("useGsapAwareEditing keeps the route a gesture chose at press", () => {
235
236
  });
236
237
  });
237
238
 
239
+ describe("useGsapAwareEditing holds a canvas press while a drop saves", () => {
240
+ it.each([
241
+ ["a move through the script", false, "move"],
242
+ ["a group move through the script", false, "group"],
243
+ ["a move saved as CSS", true, "move"],
244
+ ])("%s", async (_, plainTranslate, kind) => {
245
+ let land: () => void = () => {};
246
+ const landed = new Promise<void>((resolve) => (land = resolve));
247
+ mocks.drag.mockImplementation(async () => {
248
+ await landed;
249
+ return { status: "persisted" };
250
+ });
251
+ const save = vi.fn().mockReturnValue(landed);
252
+ const stageElementPositionOffset = vi.fn(() => ({ save, rollback: vi.fn() }));
253
+ const { pathOffsetCommit, groupCommit, root } = mountGroupHandler({
254
+ stageElementPositionOffset,
255
+ });
256
+ const box = { element: document.createElement("div"), id: "box", selector: "#box" };
257
+ const selection = box as unknown as DomEditSelection;
258
+ const to = { x: 40, y: 20 };
259
+ let saved!: Promise<unknown>;
260
+ act(() => {
261
+ saved =
262
+ kind === "group"
263
+ ? groupCommit([{ selection, next: to, plainTranslate }])
264
+ : pathOffsetCommit(selection, to, { plainTranslate });
265
+ });
266
+ expect(isPreviewChanging(), "from the drop").toBe(!plainTranslate);
267
+ await act(async () => {
268
+ land();
269
+ await saved;
270
+ });
271
+ expect(isPreviewChanging()).toBe(false);
272
+ act(() => root.unmount());
273
+ });
274
+ });
275
+
238
276
  describe("useGsapAwareEditing keeps the box route a resize chose at press", () => {
239
277
  it.each([
240
278
  ["the CSS route, on an element GSAP has since taken over", true],