@hyperframes/studio 0.8.178 → 0.8.179

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.
package/dist/index.d.ts CHANGED
@@ -3463,7 +3463,7 @@ declare function useDomEditSession({ projectId, activeCompPath, compIdToSrc, cap
3463
3463
  handlePreviewCanvasPointerLeave: () => void;
3464
3464
  applyDomSelection: (selection: DomEditSelection | null, options?: ApplyDomSelectionOptions) => void;
3465
3465
  clearDomSelection: () => void;
3466
- refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
3466
+ refreshDomEditSelectionFromPreview: (selection: DomEditSelection, iframe?: HTMLIFrameElement | null) => Promise<void>;
3467
3467
  handleDomStyleCommit: (property: string, value: string) => Promise<DomEditCommitOutcome>;
3468
3468
  handleDomStyleCommitForSelection: (selection: DomEditSelection, propertyOrStylesAsOnePatch: string | Record<string, string>, value?: string) => Promise<DomEditCommitOutcome>;
3469
3469
  handleDomAttributeCommit: (attr: string, value: string) => Promise<void>;
package/dist/index.html CHANGED
@@ -14,7 +14,7 @@
14
14
  document.documentElement.dataset.theme = "paper";
15
15
  }
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-Bi6yVTq9.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-ueMRyAJ2.js"></script>
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-CESvLzz0.css">
19
19
  </head>
20
20
  <body>
package/dist/index.js CHANGED
@@ -79948,8 +79948,9 @@ function useDomSelection({
79948
79948
  });
79949
79949
  const refreshDomEditSelectionFromPreview = useCallback125(
79950
79950
  // fallow-ignore-next-line complexity
79951
- async (selection) => {
79952
- const iframe = previewIframeRef.current;
79951
+ async (selection, iframe = previewIframeRef.current) => {
79952
+ const stillSelected = () => domEditSelectionsTargetSame(domEditSelectionRef.current, selection);
79953
+ if (!stillSelected()) return;
79953
79954
  let doc = null;
79954
79955
  try {
79955
79956
  doc = iframe?.contentDocument ?? null;
@@ -79972,7 +79973,7 @@ function useDomSelection({
79972
79973
  return;
79973
79974
  }
79974
79975
  const nextSelection = await buildDomSelectionFromTarget(element, { previous: selection });
79975
- if (nextSelection) {
79976
+ if (nextSelection && stillSelected()) {
79976
79977
  applyDomSelection(nextSelection, {
79977
79978
  revealPanel: false,
79978
79979
  preserveGroup: true
@@ -80100,7 +80101,6 @@ function useDomSelection({
80100
80101
  buildDomSelectionForTimelineElement,
80101
80102
  handleTimelineElementSelect,
80102
80103
  refreshDomEditSelectionFromPreview,
80103
- refreshDomEditGroupSelectionsFromPreview,
80104
80104
  applyMarqueeSelection
80105
80105
  };
80106
80106
  }
@@ -83658,14 +83658,10 @@ import { useCallback as useCallback142, useEffect as useEffect119, useRef as use
83658
83658
  import { useEffect as useEffect118, useRef as useRef149 } from "react";
83659
83659
  function useDomEditPreviewSync({
83660
83660
  previewIframe,
83661
- activeCompPath,
83662
83661
  captionEditMode,
83663
83662
  domEditSelectionRef,
83664
- domEditGroupSelectionsRef,
83665
83663
  domEditSelection,
83666
- applyDomSelection,
83667
- refreshDomEditGroupSelectionsFromPreview,
83668
- buildDomSelectionFromTarget,
83664
+ refreshDomEditSelectionFromPreview,
83669
83665
  refreshPreviewDocumentVersion,
83670
83666
  syncPreviewHotkeys,
83671
83667
  applyStudioManualEditsToPreviewRef,
@@ -83686,26 +83682,7 @@ function useDomEditPreviewSync({
83686
83682
  }
83687
83683
  if (!doc) return;
83688
83684
  reapplyPositionEditsAfterSeek(doc);
83689
- const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
83690
- if (!nextElement) {
83691
- const group = domEditGroupSelectionsRef.current;
83692
- logSelect("preview-sync-lost", {
83693
- target: currentSelection.selector ?? currentSelection.id ?? null,
83694
- group: group.length
83695
- });
83696
- if (group.length > 1) {
83697
- await refreshDomEditGroupSelectionsFromPreview(group);
83698
- return;
83699
- }
83700
- applyDomSelection(null, { revealPanel: false });
83701
- return;
83702
- }
83703
- const nextSelection = await buildDomSelectionFromTarget(nextElement, {
83704
- previous: currentSelection
83705
- });
83706
- if (nextSelection) {
83707
- applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
83708
- }
83685
+ await refreshDomEditSelectionFromPreview(currentSelection, previewIframe);
83709
83686
  };
83710
83687
  syncPreviewHotkeys(previewIframe);
83711
83688
  void applyStudioManualEditsToPreviewRef.current(previewIframe);
@@ -83719,14 +83696,10 @@ function useDomEditPreviewSync({
83719
83696
  };
83720
83697
  return onPreviewContentReplaced(previewIframe, handleLoad);
83721
83698
  }, [
83722
- activeCompPath,
83723
- applyDomSelection,
83724
- buildDomSelectionFromTarget,
83725
83699
  captionEditMode,
83726
- domEditGroupSelectionsRef,
83727
83700
  domEditSelectionRef,
83728
83701
  previewIframe,
83729
- refreshDomEditGroupSelectionsFromPreview,
83702
+ refreshDomEditSelectionFromPreview,
83730
83703
  refreshPreviewDocumentVersion,
83731
83704
  syncPreviewHotkeys,
83732
83705
  applyStudioManualEditsToPreviewRef,
@@ -84106,8 +84079,7 @@ function useDomEditWiring(params) {
84106
84079
  activeCompPath,
84107
84080
  domEditSelection,
84108
84081
  domEditSelectionRef,
84109
- domEditGroupSelectionsRef,
84110
- refreshDomEditGroupSelectionsFromPreview,
84082
+ refreshDomEditSelectionFromPreview,
84111
84083
  previewIframeRef,
84112
84084
  previewIframe,
84113
84085
  captionEditMode,
@@ -84118,8 +84090,6 @@ function useDomEditWiring(params) {
84118
84090
  refreshPreviewDocumentVersion,
84119
84091
  syncPreviewHotkeys,
84120
84092
  applyStudioManualEditsToPreviewRef,
84121
- applyDomSelection,
84122
- buildDomSelectionFromTarget,
84123
84093
  openSourceForSelection,
84124
84094
  updateGsapProperty,
84125
84095
  updateGsapMeta,
@@ -84216,14 +84186,10 @@ function useDomEditWiring(params) {
84216
84186
  });
84217
84187
  useDomEditPreviewSync({
84218
84188
  previewIframe,
84219
- activeCompPath,
84220
84189
  captionEditMode,
84221
84190
  domEditSelectionRef,
84222
- domEditGroupSelectionsRef,
84223
84191
  domEditSelection,
84224
- applyDomSelection,
84225
- refreshDomEditGroupSelectionsFromPreview,
84226
- buildDomSelectionFromTarget,
84192
+ refreshDomEditSelectionFromPreview,
84227
84193
  refreshPreviewDocumentVersion,
84228
84194
  syncPreviewHotkeys,
84229
84195
  applyStudioManualEditsToPreviewRef,
@@ -86231,7 +86197,6 @@ function useDomEditSession({
86231
86197
  buildDomSelectionForTimelineElement,
86232
86198
  handleTimelineElementSelect,
86233
86199
  refreshDomEditSelectionFromPreview,
86234
- refreshDomEditGroupSelectionsFromPreview,
86235
86200
  applyMarqueeSelection
86236
86201
  } = useDomSelection({
86237
86202
  projectId,
@@ -86471,8 +86436,7 @@ function useDomEditSession({
86471
86436
  activeCompPath,
86472
86437
  domEditSelection,
86473
86438
  domEditSelectionRef,
86474
- domEditGroupSelectionsRef,
86475
- refreshDomEditGroupSelectionsFromPreview,
86439
+ refreshDomEditSelectionFromPreview,
86476
86440
  previewIframeRef,
86477
86441
  previewIframe,
86478
86442
  captionEditMode: previewCaptionEditMode,
@@ -86483,8 +86447,6 @@ function useDomEditSession({
86483
86447
  refreshPreviewDocumentVersion,
86484
86448
  syncPreviewHotkeys,
86485
86449
  applyStudioManualEditsToPreviewRef,
86486
- applyDomSelection,
86487
- buildDomSelectionFromTarget,
86488
86450
  openSourceForSelection,
86489
86451
  ...gsapScriptEdits,
86490
86452
  handleDomManualEditsReset
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.178",
3
+ "version": "0.8.179",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,12 +59,12 @@
59
59
  "dockview-react": "8.3.1",
60
60
  "gsap": "^3.13.0",
61
61
  "mediabunny": "^1.45.3",
62
- "@hyperframes/core": "0.8.178",
63
- "@hyperframes/parsers": "0.8.178",
64
- "@hyperframes/sdk": "0.8.178",
65
- "@hyperframes/player": "0.8.178",
66
- "@hyperframes/studio-server": "0.8.178",
67
- "@hyperframes/shader-transitions": "0.8.178"
62
+ "@hyperframes/core": "0.8.179",
63
+ "@hyperframes/studio-server": "0.8.179",
64
+ "@hyperframes/sdk": "0.8.179",
65
+ "@hyperframes/player": "0.8.179",
66
+ "@hyperframes/shader-transitions": "0.8.179",
67
+ "@hyperframes/parsers": "0.8.179"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@tailwindcss/vite": "^4.3.3",
@@ -81,7 +81,7 @@
81
81
  "vite": "^6.4.2",
82
82
  "vitest": "^4.1.11",
83
83
  "zustand": "^5.0.0",
84
- "@hyperframes/producer": "0.8.178"
84
+ "@hyperframes/producer": "0.8.179"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "react": "19",
@@ -2,7 +2,7 @@
2
2
  import React, { act } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
- import type { DomEditSelection } from "../components/editor/domEditing";
5
+ import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
6
6
  import { resolveElementForOverlay } from "../components/editor/domEditOverlayGeometry";
7
7
  import {
8
8
  makePreview,
@@ -44,14 +44,13 @@ function mountHost(followPromotions: boolean) {
44
44
  const live = useLivePreviewIframe(host);
45
45
  useDomEditPreviewSync({
46
46
  previewIframe: followPromotions ? live : host,
47
- activeCompPath: null,
48
47
  captionEditMode: false,
49
48
  domEditSelectionRef: selectionRef,
50
- domEditGroupSelectionsRef: { current: [] },
51
49
  domEditSelection: selectionRef.current,
52
- refreshDomEditGroupSelectionsFromPreview: async () => {},
53
- applyDomSelection: (selection) => void (selectionRef.current = selection),
54
- buildDomSelectionFromTarget: async (element) => selectionOf(element),
50
+ refreshDomEditSelectionFromPreview: async (selection, iframe) => {
51
+ const element = findElementForSelection(iframe.contentDocument!, selection, null);
52
+ selectionRef.current = element ? selectionOf(element) : null;
53
+ },
55
54
  refreshPreviewDocumentVersion: () => {},
56
55
  syncPreviewHotkeys: () => {},
57
56
  applyStudioManualEditsToPreviewRef: { current: async () => {} },
@@ -25,14 +25,10 @@ describe("useDomEditPreviewSync", () => {
25
25
  function Harness() {
26
26
  useDomEditPreviewSync({
27
27
  previewIframe: iframe,
28
- activeCompPath: null,
29
28
  captionEditMode: false,
30
29
  domEditSelectionRef: { current: null },
31
- domEditGroupSelectionsRef: { current: [] },
32
30
  domEditSelection: null,
33
- refreshDomEditGroupSelectionsFromPreview: async () => {},
34
- applyDomSelection: () => {},
35
- buildDomSelectionFromTarget: async () => null,
31
+ refreshDomEditSelectionFromPreview: async () => {},
36
32
  refreshPreviewDocumentVersion: refreshVersion,
37
33
  syncPreviewHotkeys: () => {},
38
34
  applyStudioManualEditsToPreviewRef: { current: applyManualEdits },
@@ -4,31 +4,22 @@
4
4
  * Extracted from useDomEditSession to keep file sizes under the 600-line limit.
5
5
  */
6
6
  import { useEffect, useRef } from "react";
7
- import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
7
+ import type { DomEditSelection } from "../components/editor/domEditing";
8
8
  import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
9
9
  import { useDockLayoutStore } from "../components/dock/dockLayoutStore";
10
10
  import type { PatchTarget } from "../utils/sourcePatcher";
11
- import { logSelect } from "../utils/selectDebug";
12
11
  import { onPreviewContentReplaced } from "../player/sceneSwap";
13
- import type { ResolveDomSelectionOptions } from "./useDomSelectionTypes";
14
12
 
15
13
  interface UseDomEditPreviewSyncParams {
16
14
  previewIframe: HTMLIFrameElement | null;
17
- activeCompPath: string | null;
18
15
  captionEditMode: boolean;
19
16
  domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
20
- domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
21
17
  domEditSelection: DomEditSelection | null;
22
- /** Re-resolves a whole multi-selection against the current preview document. */
23
- refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
24
- applyDomSelection: (
25
- selection: DomEditSelection | null,
26
- options?: { revealPanel?: boolean; preserveGroup?: boolean },
27
- ) => void;
28
- buildDomSelectionFromTarget: (
29
- element: HTMLElement,
30
- options?: Pick<ResolveDomSelectionOptions, "previous">,
31
- ) => Promise<DomEditSelection | null>;
18
+ /** Re-resolves the selection against the given preview, unless it was deselected meanwhile. */
19
+ refreshDomEditSelectionFromPreview: (
20
+ selection: DomEditSelection,
21
+ iframe: HTMLIFrameElement,
22
+ ) => Promise<void>;
32
23
  refreshPreviewDocumentVersion: () => void;
33
24
  syncPreviewHotkeys: (iframe: HTMLIFrameElement | null) => void;
34
25
  applyStudioManualEditsToPreviewRef: React.MutableRefObject<
@@ -40,14 +31,10 @@ interface UseDomEditPreviewSyncParams {
40
31
 
41
32
  export function useDomEditPreviewSync({
42
33
  previewIframe,
43
- activeCompPath,
44
34
  captionEditMode,
45
35
  domEditSelectionRef,
46
- domEditGroupSelectionsRef,
47
36
  domEditSelection,
48
- applyDomSelection,
49
- refreshDomEditGroupSelectionsFromPreview,
50
- buildDomSelectionFromTarget,
37
+ refreshDomEditSelectionFromPreview,
51
38
  refreshPreviewDocumentVersion,
52
39
  syncPreviewHotkeys,
53
40
  applyStudioManualEditsToPreviewRef,
@@ -73,39 +60,7 @@ export function useDomEditPreviewSync({
73
60
  if (!doc) return;
74
61
 
75
62
  reapplyPositionEditsAfterSeek(doc);
76
-
77
- const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
78
- if (!nextElement) {
79
- // The selected element no longer resolves in the (re-synced) document
80
- // — comp/hot reload, activeCompPath swap, or post-save replacement.
81
- // Clear so overlay geometry isn't computed on a stale, detached node.
82
- // (Drag-release-in-gray-zone is handled separately by
83
- // suppressNextBoxClickRef; the dragged element still resolves here.)
84
- //
85
- // One lost member is not the whole selection though. A multi-select that
86
- // loses its primary here used to be wiped entirely, so moving a group and
87
- // having any one of its elements fail to re-resolve deselected all of
88
- // them. Re-resolve the group instead and keep whoever survived; it only
89
- // clears when nobody did.
90
- const group = domEditGroupSelectionsRef.current;
91
- logSelect("preview-sync-lost", {
92
- target: currentSelection.selector ?? currentSelection.id ?? null,
93
- group: group.length,
94
- });
95
- if (group.length > 1) {
96
- await refreshDomEditGroupSelectionsFromPreview(group);
97
- return;
98
- }
99
- applyDomSelection(null, { revealPanel: false });
100
- return;
101
- }
102
-
103
- const nextSelection = await buildDomSelectionFromTarget(nextElement, {
104
- previous: currentSelection,
105
- });
106
- if (nextSelection) {
107
- applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
108
- }
63
+ await refreshDomEditSelectionFromPreview(currentSelection, previewIframe);
109
64
  };
110
65
 
111
66
  syncPreviewHotkeys(previewIframe);
@@ -122,14 +77,10 @@ export function useDomEditPreviewSync({
122
77
 
123
78
  return onPreviewContentReplaced(previewIframe, handleLoad);
124
79
  }, [
125
- activeCompPath,
126
- applyDomSelection,
127
- buildDomSelectionFromTarget,
128
80
  captionEditMode,
129
- domEditGroupSelectionsRef,
130
81
  domEditSelectionRef,
131
82
  previewIframe,
132
- refreshDomEditGroupSelectionsFromPreview,
83
+ refreshDomEditSelectionFromPreview,
133
84
  refreshPreviewDocumentVersion,
134
85
  syncPreviewHotkeys,
135
86
  applyStudioManualEditsToPreviewRef,
@@ -122,7 +122,6 @@ export function useDomEditSession({
122
122
  buildDomSelectionForTimelineElement,
123
123
  handleTimelineElementSelect,
124
124
  refreshDomEditSelectionFromPreview,
125
- refreshDomEditGroupSelectionsFromPreview,
126
125
  applyMarqueeSelection,
127
126
  } = useDomSelection({
128
127
  projectId,
@@ -394,8 +393,7 @@ export function useDomEditSession({
394
393
  activeCompPath,
395
394
  domEditSelection,
396
395
  domEditSelectionRef,
397
- domEditGroupSelectionsRef,
398
- refreshDomEditGroupSelectionsFromPreview,
396
+ refreshDomEditSelectionFromPreview,
399
397
  previewIframeRef,
400
398
  previewIframe,
401
399
  captionEditMode: previewCaptionEditMode,
@@ -406,8 +404,6 @@ export function useDomEditSession({
406
404
  refreshPreviewDocumentVersion,
407
405
  syncPreviewHotkeys,
408
406
  applyStudioManualEditsToPreviewRef,
409
- applyDomSelection,
410
- buildDomSelectionFromTarget,
411
407
  openSourceForSelection,
412
408
  ...gsapScriptEdits,
413
409
  handleDomManualEditsReset,
@@ -23,8 +23,10 @@ export interface UseDomEditWiringParams {
23
23
  activeCompPath: string | null;
24
24
  domEditSelection: DomEditSelection | null;
25
25
  domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
26
- domEditGroupSelectionsRef: React.MutableRefObject<DomEditSelection[]>;
27
- refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
26
+ refreshDomEditSelectionFromPreview: (
27
+ selection: DomEditSelection,
28
+ iframe: HTMLIFrameElement,
29
+ ) => Promise<void>;
28
30
  previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
29
31
  previewIframe: HTMLIFrameElement | null;
30
32
  captionEditMode: boolean;
@@ -37,11 +39,6 @@ export interface UseDomEditWiringParams {
37
39
  applyStudioManualEditsToPreviewRef: React.MutableRefObject<
38
40
  (iframe: HTMLIFrameElement) => Promise<void>
39
41
  >;
40
- applyDomSelection: (
41
- selection: DomEditSelection | null,
42
- options?: { revealPanel?: boolean; preserveGroup?: boolean },
43
- ) => void;
44
- buildDomSelectionFromTarget: (element: HTMLElement) => Promise<DomEditSelection | null>;
45
42
  openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
46
43
  // GSAP script commit ops (from useGsapScriptCommits)
47
44
  updateGsapProperty: (
@@ -117,8 +114,7 @@ export function useDomEditWiring(params: UseDomEditWiringParams) {
117
114
  activeCompPath,
118
115
  domEditSelection,
119
116
  domEditSelectionRef,
120
- domEditGroupSelectionsRef,
121
- refreshDomEditGroupSelectionsFromPreview,
117
+ refreshDomEditSelectionFromPreview,
122
118
  previewIframeRef,
123
119
  previewIframe,
124
120
  captionEditMode,
@@ -129,8 +125,6 @@ export function useDomEditWiring(params: UseDomEditWiringParams) {
129
125
  refreshPreviewDocumentVersion,
130
126
  syncPreviewHotkeys,
131
127
  applyStudioManualEditsToPreviewRef,
132
- applyDomSelection,
133
- buildDomSelectionFromTarget,
134
128
  openSourceForSelection,
135
129
  updateGsapProperty,
136
130
  updateGsapMeta,
@@ -251,14 +245,10 @@ export function useDomEditWiring(params: UseDomEditWiringParams) {
251
245
 
252
246
  useDomEditPreviewSync({
253
247
  previewIframe,
254
- activeCompPath,
255
248
  captionEditMode,
256
249
  domEditSelectionRef,
257
- domEditGroupSelectionsRef,
258
250
  domEditSelection,
259
- applyDomSelection,
260
- refreshDomEditGroupSelectionsFromPreview,
261
- buildDomSelectionFromTarget,
251
+ refreshDomEditSelectionFromPreview,
262
252
  refreshPreviewDocumentVersion,
263
253
  syncPreviewHotkeys,
264
254
  applyStudioManualEditsToPreviewRef,
@@ -326,6 +326,68 @@ describe("useDomSelection", () => {
326
326
  harness.cleanup();
327
327
  });
328
328
 
329
+ describe("a save's resync after the user moved on", () => {
330
+ function selectHeadlineInPreview() {
331
+ const doc = document.implementation.createHTMLDocument("");
332
+ const element = doc.body.appendChild(
333
+ Object.assign(doc.createElement("div"), { id: "headline" }),
334
+ );
335
+ const headline = makeSelection("Headline", element);
336
+ const harness = renderHarness(
337
+ { activeCompPath: "index.html", projectId: "project-1", refreshKey: 0 },
338
+ { previewDoc: doc },
339
+ );
340
+ act(() => harness.current().applyDomSelection(headline));
341
+ return { headline, harness };
342
+ }
343
+
344
+ it("keeps a deselect made while the save was in flight", async () => {
345
+ const { headline, harness } = selectHeadlineInPreview();
346
+ // Escape after a crop's release, before its save returns.
347
+ act(() => harness.current().applyDomSelection(null, { revealPanel: false }));
348
+
349
+ await act(() => harness.current().refreshDomEditSelectionFromPreview(headline));
350
+
351
+ expect(harness.current().domEditSelection).toBeNull();
352
+ harness.cleanup();
353
+ });
354
+
355
+ it.each([
356
+ ["still in", false],
357
+ ["gone from", true],
358
+ ])(
359
+ "keeps a different pick made while the save was in flight, the saved element %s the preview",
360
+ async (_, removed) => {
361
+ const { headline, harness } = selectHeadlineInPreview();
362
+ const card = makeSelection(
363
+ "Card",
364
+ Object.assign(document.createElement("div"), { id: "card" }),
365
+ );
366
+ act(() => harness.current().applyDomSelection(card));
367
+ if (removed) headline.element.remove();
368
+
369
+ await act(() => harness.current().refreshDomEditSelectionFromPreview(headline));
370
+
371
+ expect(harness.current().domEditSelection).toBe(card);
372
+ harness.cleanup();
373
+ },
374
+ );
375
+
376
+ it("keeps a deselect made while the resync was rebuilding the selection", async () => {
377
+ const { headline, harness } = selectHeadlineInPreview();
378
+ let resync!: Promise<void>;
379
+ act(() => {
380
+ resync = harness.current().refreshDomEditSelectionFromPreview(headline);
381
+ });
382
+ act(() => harness.current().applyDomSelection(null, { revealPanel: false }));
383
+
384
+ await act(() => resync);
385
+
386
+ expect(harness.current().domEditSelection).toBeNull();
387
+ harness.cleanup();
388
+ });
389
+ });
390
+
329
391
  it("clears a committed selection when the active composition path changes", () => {
330
392
  const { selection, harness } = setupSelectedHarness();
331
393
  expect(harness.current().domEditSelection).toBe(selection);
@@ -356,8 +356,10 @@ export function useDomSelection({
356
356
 
357
357
  const refreshDomEditSelectionFromPreview = useCallback(
358
358
  // fallow-ignore-next-line complexity
359
- async (selection: DomEditSelection) => {
360
- const iframe = previewIframeRef.current;
359
+ async (selection: DomEditSelection, iframe = previewIframeRef.current) => {
360
+ const stillSelected = () =>
361
+ domEditSelectionsTargetSame(domEditSelectionRef.current, selection);
362
+ if (!stillSelected()) return;
361
363
  let doc: Document | null = null;
362
364
  try {
363
365
  doc = iframe?.contentDocument ?? null;
@@ -384,7 +386,7 @@ export function useDomSelection({
384
386
  }
385
387
 
386
388
  const nextSelection = await buildDomSelectionFromTarget(element, { previous: selection });
387
- if (nextSelection) {
389
+ if (nextSelection && stillSelected()) {
388
390
  applyDomSelection(nextSelection, {
389
391
  revealPanel: false,
390
392
  preserveGroup: true,
@@ -547,7 +549,6 @@ export function useDomSelection({
547
549
  buildDomSelectionForTimelineElement,
548
550
  handleTimelineElementSelect,
549
551
  refreshDomEditSelectionFromPreview,
550
- refreshDomEditGroupSelectionsFromPreview,
551
552
  applyMarqueeSelection,
552
553
  };
553
554
  }
@@ -89,7 +89,9 @@ export interface UseDomSelectionReturn {
89
89
  element: TimelineElement,
90
90
  ) => Promise<DomEditSelection | null>;
91
91
  handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
92
- refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
93
- refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
92
+ refreshDomEditSelectionFromPreview: (
93
+ selection: DomEditSelection,
94
+ iframe?: HTMLIFrameElement | null,
95
+ ) => Promise<void>;
94
96
  applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => DomSelectionResult;
95
97
  }