@hyperframes/studio 0.8.86 → 0.8.88

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 (96) hide show
  1. package/dist/assets/hyperframes-player-w3Bbv0kd.js +473 -0
  2. package/dist/assets/{index-Cz3E_ZSj.js → index-B6PGJTzp.js} +1 -1
  3. package/dist/assets/{index-d5DKK15Q.js → index-DE6cZYR5.js} +199 -199
  4. package/dist/assets/{index-8Fgryj7q.js → index-DldknZPp.js} +1 -1
  5. package/dist/assets/{index-mzZCha_N.css → index-Dx79V1Se.css} +1 -1
  6. package/dist/{chunk-7PLTC6SV.js → chunk-KU4F7QZ3.js} +9 -1
  7. package/dist/chunk-KU4F7QZ3.js.map +1 -0
  8. package/dist/{chunk-3IOCVWB6.js → chunk-PQFEGZWM.js} +3 -1
  9. package/dist/chunk-PQFEGZWM.js.map +1 -0
  10. package/dist/{domEditingLayers-ZOOMN7CQ.js → domEditingLayers-3BAP6NNO.js} +2 -2
  11. package/dist/index.d.ts +206 -160
  12. package/dist/index.html +2 -2
  13. package/dist/index.js +2121 -1702
  14. package/dist/index.js.map +1 -1
  15. package/dist/player.js +1 -1
  16. package/dist/styles.css +1 -1
  17. package/package.json +8 -8
  18. package/src/components/TimelineHistoryButtons.dom.test.tsx +14 -3
  19. package/src/components/TimelineToolPicker.tsx +90 -0
  20. package/src/components/TimelineToolbar.tsx +2 -27
  21. package/src/components/editor/DomEditOverlay.tsx +1 -2
  22. package/src/components/editor/DomEditOverlayHover.test.tsx +5 -0
  23. package/src/components/editor/DomEditSelectionChrome.test.tsx +0 -3
  24. package/src/components/editor/DomEditSelectionChrome.tsx +3 -13
  25. package/src/components/editor/LayersPanel.test.ts +14 -0
  26. package/src/components/editor/LayersPanel.tsx +2 -2
  27. package/src/components/editor/TopologyLens.tsx +2 -2
  28. package/src/components/editor/commitPreflightHandles.test.tsx +466 -0
  29. package/src/components/editor/domEditOverlayGestures.ts +0 -1
  30. package/src/components/editor/domEditOverlayStartGesture.ts +10 -1
  31. package/src/components/editor/domEditing.test.ts +29 -0
  32. package/src/components/editor/domEditingDom.ts +1 -0
  33. package/src/components/editor/domEditingTypes.ts +1 -0
  34. package/src/components/editor/marqueeCommit.test.tsx +84 -0
  35. package/src/components/editor/marqueeCommit.ts +13 -1
  36. package/src/components/editor/useColorGradingController.test.ts +24 -0
  37. package/src/components/editor/useColorGradingController.ts +3 -2
  38. package/src/components/editor/useDomEditOverlayGestures.ts +20 -5
  39. package/src/components/nle/useCompositionStack.test.tsx +53 -0
  40. package/src/components/nle/useCompositionStack.ts +4 -8
  41. package/src/hooks/appHotkeysDispatch.ts +8 -0
  42. package/src/hooks/domStyleCommit.ts +129 -0
  43. package/src/hooks/gsapEditOutcome.ts +2 -2
  44. package/src/hooks/gsapElementMatch.ts +38 -0
  45. package/src/hooks/gsapResizeIntercept.ts +10 -31
  46. package/src/hooks/gsapResizePreflight.ts +56 -0
  47. package/src/hooks/gsapRuntimeBridge.ts +66 -26
  48. package/src/hooks/keyframeCacheAstLoad.ts +4 -1
  49. package/src/hooks/useAppHotkeys.test.ts +58 -1
  50. package/src/hooks/useCommitPreflightCapabilities.ts +147 -0
  51. package/src/hooks/useDomEditCommits.ts +17 -182
  52. package/src/hooks/useDomEditPersist.ts +225 -0
  53. package/src/hooks/useDomEditPreviewSync.test.ts +58 -0
  54. package/src/hooks/useDomEditPreviewSync.ts +2 -4
  55. package/src/hooks/useDomEditSession.test.tsx +51 -1
  56. package/src/hooks/useDomEditSession.ts +11 -2
  57. package/src/hooks/useDomEditTextCommits.ts +15 -99
  58. package/src/hooks/useDomStyleCommit.test.tsx +155 -0
  59. package/src/hooks/useDomStyleCommit.ts +74 -0
  60. package/src/hooks/useGsapAnimationFetchFallback.test.ts +12 -0
  61. package/src/hooks/useGsapAnimationFetchFallback.ts +8 -2
  62. package/src/hooks/useGsapAwareEditing.test.tsx +4 -0
  63. package/src/hooks/useGsapAwareEditing.ts +1 -1
  64. package/src/hooks/useGsapTweenCache.ts +2 -48
  65. package/src/index.ts +10 -1
  66. package/src/player/components/Timeline.audioEffects.test.tsx +80 -0
  67. package/src/player/components/Timeline.bandEscape.test.tsx +75 -0
  68. package/src/player/components/Timeline.playheadRemount.test.tsx +3 -27
  69. package/src/player/components/Timeline.showKeyframes.test.tsx +91 -0
  70. package/src/player/components/TimelineGroupHeader.tsx +21 -10
  71. package/src/player/components/TimelineGroupLaneLabels.tsx +7 -2
  72. package/src/player/components/TimelineGroupRow.test.tsx +24 -33
  73. package/src/player/components/TimelineGroupRow.tsx +4 -1
  74. package/src/player/components/TimelineLanes.tsx +5 -5
  75. package/src/player/components/TimelineTrackHeader.test.tsx +27 -0
  76. package/src/player/components/TimelineTrackHeader.tsx +6 -3
  77. package/src/player/components/TimelineTypes.ts +2 -0
  78. package/src/player/components/studioShortcuts.ts +2 -0
  79. package/src/player/components/timelineLaneProps.ts +1 -0
  80. package/src/player/components/timelineMountTestEnv.ts +29 -0
  81. package/src/player/components/useTimelineKeyframeData.ts +14 -0
  82. package/src/player/components/useTimelineLabelColumn.ts +18 -0
  83. package/src/player/components/useTimelineLaneRowIndexes.ts +6 -1
  84. package/src/player/components/useTimelineProviderState.tsx +10 -14
  85. package/src/player/components/useTimelineRangeSelection.ts +13 -12
  86. package/src/player/hooks/useShadowPreviewReload.ts +2 -0
  87. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +191 -0
  88. package/src/player/hooks/useTimelinePlayer.ts +29 -7
  89. package/src/player/lib/runtimeProtocol.test.ts +1 -0
  90. package/src/player/sceneSwap.test.ts +95 -0
  91. package/src/player/sceneSwap.ts +53 -0
  92. package/src/player/store/playerStore.ts +16 -0
  93. package/dist/assets/hyperframes-player-Dx-2Gpsu.js +0 -473
  94. package/dist/chunk-3IOCVWB6.js.map +0 -1
  95. package/dist/chunk-7PLTC6SV.js.map +0 -1
  96. /package/dist/{domEditingLayers-ZOOMN7CQ.js.map → domEditingLayers-3BAP6NNO.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.86",
3
+ "version": "0.8.88",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,12 +60,12 @@
60
60
  "dockview-react": "8.3.1",
61
61
  "gsap": "^3.13.0",
62
62
  "mediabunny": "^1.45.3",
63
- "@hyperframes/core": "0.8.86",
64
- "@hyperframes/sdk": "0.8.86",
65
- "@hyperframes/player": "0.8.86",
66
- "@hyperframes/studio-server": "0.8.86",
67
- "@hyperframes/shader-transitions": "0.8.86",
68
- "@hyperframes/parsers": "0.8.86"
63
+ "@hyperframes/parsers": "0.8.88",
64
+ "@hyperframes/core": "0.8.88",
65
+ "@hyperframes/player": "0.8.88",
66
+ "@hyperframes/studio-server": "0.8.88",
67
+ "@hyperframes/sdk": "0.8.88",
68
+ "@hyperframes/shader-transitions": "0.8.88"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@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.86"
84
+ "@hyperframes/producer": "0.8.88"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "react": "19",
@@ -96,14 +96,25 @@ it("leaves enabled buttons without the disabled attribute", () => {
96
96
  expect(el.className).not.toContain("cursor-not-allowed");
97
97
  });
98
98
 
99
- it("orders the toolbar Undo, Redo, Select, Razor", () => {
99
+ it("orders the toolbar Undo, Redo, then the tool picker showing the active tool", () => {
100
100
  const host = mount(<TimelineToolbar />);
101
101
  const labels = Array.from(host.querySelectorAll("button"))
102
102
  .map((b) => b.getAttribute("aria-label"))
103
103
  .filter((l) => l !== null)
104
- .slice(0, 4);
104
+ .slice(0, 3);
105
105
 
106
- expect(labels).toEqual(["Undo", "Redo", "Selection tool", "Razor tool"]);
106
+ expect(labels).toEqual(["Undo", "Redo", "Timeline tool: Select"]);
107
+ });
108
+
109
+ it("shows the Split tool on the picker while the razor is active", async () => {
110
+ const { usePlayerStore } = await import("../player");
111
+ usePlayerStore.setState({ activeTool: "razor" });
112
+ try {
113
+ const host = mount(<TimelineToolbar />);
114
+ expect(host.querySelector('button[aria-label="Timeline tool: Split"]')).not.toBeNull();
115
+ } finally {
116
+ usePlayerStore.setState({ activeTool: "select" });
117
+ }
107
118
  });
108
119
 
109
120
  it("classifies Undo and Redo for the hotkey filters at their new location (KTD13)", () => {
@@ -0,0 +1,90 @@
1
+ import type { ReactNode } from "react";
2
+ import { ArrowLineLeft, ArrowLineRight, CaretDown } from "@phosphor-icons/react";
3
+ import { usePlayerStore } from "../player";
4
+ import { Scissors } from "../icons/SystemIcons";
5
+ import { Menu, MenuItem } from "./ui";
6
+ import { cn } from "./ui/cn";
7
+ import { flatIdle } from "./timelineToolbarStyles";
8
+
9
+ function SelectIcon() {
10
+ return (
11
+ <svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true">
12
+ <path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" />
13
+ </svg>
14
+ );
15
+ }
16
+
17
+ interface ToolRow {
18
+ label: string;
19
+ shortcut: string;
20
+ icon: ReactNode;
21
+ active: boolean;
22
+ run: () => void;
23
+ }
24
+
25
+ /** One button for the active timeline tool; its menu switches tools or selects clips around the playhead. */
26
+ export function TimelineToolPicker() {
27
+ const activeTool = usePlayerStore((s) => s.activeTool);
28
+ const store = () => usePlayerStore.getState();
29
+ const rows: ToolRow[] = [
30
+ {
31
+ label: "Select",
32
+ shortcut: "V",
33
+ icon: <SelectIcon />,
34
+ active: activeTool === "select",
35
+ run: () => store().setActiveTool("select"),
36
+ },
37
+ {
38
+ label: "Split",
39
+ shortcut: "B",
40
+ icon: <Scissors size={16} />,
41
+ active: activeTool === "razor",
42
+ run: () => store().setActiveTool("razor"),
43
+ },
44
+ {
45
+ label: "Select leftward",
46
+ shortcut: "[",
47
+ icon: <ArrowLineLeft size={16} aria-hidden="true" />,
48
+ active: false,
49
+ run: () => store().selectLeftward(),
50
+ },
51
+ {
52
+ label: "Select rightward",
53
+ shortcut: "]",
54
+ icon: <ArrowLineRight size={16} aria-hidden="true" />,
55
+ active: false,
56
+ run: () => store().selectRightward(),
57
+ },
58
+ ];
59
+ const current = rows.find((row) => row.active) ?? rows[0];
60
+ return (
61
+ <Menu
62
+ aria-label="Timeline tools"
63
+ trigger={
64
+ <button
65
+ type="button"
66
+ aria-label={`Timeline tool: ${current.label}`}
67
+ className={cn(flatIdle, "gap-0.5")}
68
+ >
69
+ {current.icon}
70
+ <CaretDown size={10} weight="bold" aria-hidden="true" />
71
+ </button>
72
+ }
73
+ >
74
+ {rows.map((row) => (
75
+ <MenuItem
76
+ key={row.label}
77
+ shortcut={row.shortcut}
78
+ onClick={row.run}
79
+ data-active={row.active || undefined}
80
+ className={row.active ? "bg-neutral-800 text-white" : undefined}
81
+ >
82
+ <span className="flex items-center gap-2">
83
+ {row.icon}
84
+ {row.label}
85
+ </span>
86
+ </MenuItem>
87
+ ))}
88
+ </Menu>
89
+ );
90
+ }
@@ -26,7 +26,7 @@ import { AudioMetersIcon } from "./icons/AudioMetersIcon";
26
26
  import { RippleEditIcon } from "./icons/RippleEditIcon";
27
27
  import { flatActive, flatBtn, flatDisabled, flatIdle } from "./timelineToolbarStyles";
28
28
  import { TimelineHistoryButtons, type TimelineHistoryButtonsProps } from "./TimelineHistoryButtons";
29
- import { Scissors } from "../icons/SystemIcons";
29
+ import { TimelineToolPicker } from "./TimelineToolPicker";
30
30
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
31
31
  import type { DomEditSelection } from "./editor/domEditingTypes";
32
32
  import { canSplitElement } from "../utils/timelineElementSplit";
@@ -144,8 +144,6 @@ export function TimelineToolbar({
144
144
  showAddBeat = true,
145
145
  showKeyframes = true,
146
146
  }: TimelineToolbarProps) {
147
- const activeTool = usePlayerStore((s) => s.activeTool);
148
- const setActiveTool = usePlayerStore((s) => s.setActiveTool);
149
147
  const timelineSnapEnabled = usePlayerStore((s) => s.timelineSnapEnabled);
150
148
  const setTimelineSnapEnabled = usePlayerStore((s) => s.setTimelineSnapEnabled);
151
149
  const rippleEditEnabled = usePlayerStore((s) => s.rippleEditEnabled);
@@ -216,30 +214,7 @@ export function TimelineToolbar({
216
214
  <div className="flex items-center justify-between px-2 py-0.5">
217
215
  <div className="flex items-center gap-0.5">
218
216
  <TimelineHistoryButtons {...history} />
219
- <Tooltip label="Selection tool (V)">
220
- <button
221
- type="button"
222
- onClick={() => setActiveTool("select")}
223
- aria-label="Selection tool"
224
- aria-pressed={activeTool === "select"}
225
- className={activeTool === "select" ? flatActive : flatIdle}
226
- >
227
- <svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor">
228
- <path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" />
229
- </svg>
230
- </button>
231
- </Tooltip>
232
- <Tooltip label="Razor tool (B) — Shift+click splits all tracks">
233
- <button
234
- type="button"
235
- onClick={() => setActiveTool("razor")}
236
- aria-label="Razor tool"
237
- aria-pressed={activeTool === "razor"}
238
- className={activeTool === "razor" ? flatActive : flatIdle}
239
- >
240
- <Scissors size={16} />
241
- </button>
242
- </Tooltip>
217
+ <TimelineToolPicker />
243
218
  {/* Divider: tool-mode | editing-actions */}
244
219
  <div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
245
220
  <Tooltip label={timelineSnapEnabled ? "Snapping on (N)" : "Snapping off (N)"}>
@@ -47,7 +47,7 @@ export {
47
47
  } from "./domEditOverlayGestures";
48
48
  export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
49
49
 
50
- interface DomEditOverlayProps {
50
+ export interface DomEditOverlayProps {
51
51
  iframeRef: RefObject<HTMLIFrameElement | null>;
52
52
  activeCompositionPath: string | null;
53
53
  selection: DomEditSelection | null;
@@ -518,7 +518,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
518
518
  boxClipPath={boxClipPath}
519
519
  selectionKey={selectionKey}
520
520
  groupSelectionCount={groupSelections.length}
521
- blockedMoveRef={blockedMoveRef}
522
521
  gestures={gestures}
523
522
  onStyleCommit={onStyleCommitRef.current}
524
523
  onBoxMouseDown={suppressBoxMouseDown}
@@ -6,6 +6,7 @@ import { expect, it, vi } from "vitest";
6
6
  import type { DomEditSelection } from "./domEditing";
7
7
  import "./domEditOverlayTestMocks";
8
8
  import { DomEditOverlay } from "./DomEditOverlay";
9
+ import { DomEditOverlay as PackageDomEditOverlay } from "../../index";
9
10
 
10
11
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
11
12
 
@@ -74,3 +75,7 @@ it("rotates the hover box with the element", () => {
74
75
  it("leaves the hover box untransformed at angle zero", () => {
75
76
  expect(renderHover(0)).toBe("");
76
77
  });
78
+
79
+ it("is exported from the package entry, so a host can mount it with its own commits", () => {
80
+ expect(PackageDomEditOverlay).toBe(DomEditOverlay);
81
+ });
@@ -53,7 +53,6 @@ describe("DomEditSelectionChrome crop composition", () => {
53
53
  boxClipPath={undefined}
54
54
  selectionKey="headline"
55
55
  groupSelectionCount={0}
56
- blockedMoveRef={createRef()}
57
56
  gestures={{ startGesture: vi.fn() } as never}
58
57
  onStyleCommit={vi.fn()}
59
58
  onBoxMouseDown={vi.fn()}
@@ -109,7 +108,6 @@ describe("DomEditSelectionChrome crop composition", () => {
109
108
  boxClipPath={undefined}
110
109
  selectionKey="clip"
111
110
  groupSelectionCount={0}
112
- blockedMoveRef={createRef()}
113
111
  gestures={{ startGesture: vi.fn() } as never}
114
112
  onStyleCommit={vi.fn()}
115
113
  onBoxMouseDown={vi.fn()}
@@ -170,7 +168,6 @@ describe("DomEditSelectionChrome while editing text", () => {
170
168
  boxClipPath={undefined}
171
169
  selectionKey="copy"
172
170
  groupSelectionCount={0}
173
- blockedMoveRef={createRef()}
174
171
  gestures={{ startGesture: vi.fn() } as never}
175
172
  onStyleCommit={vi.fn()}
176
173
  onBoxMouseDown={vi.fn()}
@@ -1,7 +1,7 @@
1
1
  import type { RefObject } from "react";
2
2
  import { type DomEditSelection } from "./domEditing";
3
3
  import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
4
- import type { BlockedMoveState, ResizeHandle } from "./domEditOverlayGestures";
4
+ import type { ResizeHandle } from "./domEditOverlayGestures";
5
5
  import type { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
6
6
  import { DomEditCropHandles } from "./DomEditCropHandles";
7
7
  import { DomEditRotateHandle } from "./DomEditRotateHandle";
@@ -103,7 +103,7 @@ export function DomEditGroupChrome({
103
103
  cursor: canManipulate && groupCanMove ? "move" : "default",
104
104
  }}
105
105
  onPointerDown={(e) => {
106
- if (!canManipulate || !groupCanMove || e.shiftKey) return;
106
+ if (!canManipulate || e.shiftKey) return;
107
107
  gestures.startGroupDrag(e);
108
108
  }}
109
109
  onMouseDown={onBoxMouseDown}
@@ -123,7 +123,6 @@ interface DomEditSelectionChromeProps {
123
123
  boxClipPath: string | undefined;
124
124
  selectionKey: string;
125
125
  groupSelectionCount: number;
126
- blockedMoveRef: RefObject<BlockedMoveState | null>;
127
126
  gestures: GestureHandlers;
128
127
  onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
129
128
  onBoxMouseDown: (e: React.MouseEvent) => void;
@@ -152,7 +151,6 @@ export function DomEditSelectionChrome({
152
151
  boxClipPath,
153
152
  selectionKey,
154
153
  groupSelectionCount,
155
- blockedMoveRef,
156
154
  gestures,
157
155
  onStyleCommit,
158
156
  onBoxMouseDown,
@@ -218,15 +216,7 @@ export function DomEditSelectionChrome({
218
216
  gestures.startGesture("drag", e);
219
217
  return;
220
218
  }
221
- e.preventDefault();
222
- e.stopPropagation();
223
- e.currentTarget.setPointerCapture(e.pointerId);
224
- blockedMoveRef.current = {
225
- pointerId: e.pointerId,
226
- startX: e.clientX,
227
- startY: e.clientY,
228
- notified: false,
229
- };
219
+ gestures.startBlockedMove(e, selection);
230
220
  }}
231
221
  onMouseDown={onBoxMouseDown}
232
222
  onClick={onBoxClick}
@@ -9,6 +9,7 @@ import type { DomEditLayerItem } from "./domEditingTypes";
9
9
  import { createRafThrottle, LayersPanel, sortLayersByZIndex } from "./LayersPanel";
10
10
  import { isLayerDraggable } from "./useLayerDrag";
11
11
  import { liveTime } from "../../player";
12
+ import { sceneSwapFor } from "../../player/sceneSwap";
12
13
 
13
14
  (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
14
15
 
@@ -292,4 +293,17 @@ describe("LayersPanel preview promotion", () => {
292
293
  });
293
294
  expect(collectedRootIds()).toEqual(["b"]);
294
295
  });
296
+ it("recollects layers when edited scenes are swapped into the live document", async () => {
297
+ const a = makeIframe("a");
298
+ Object.defineProperty(a, "contentWindow", { value: { __hfSwapScenes: async () => {} } });
299
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("<html></html>"));
300
+ mocks.previewIframeRef.current = a;
301
+ usePreviewIframeStore.setState({ iframe: a });
302
+ act(() => root.render(createElement(LayersPanel)));
303
+ mocks.collect.mockClear();
304
+
305
+ await act(() => sceneSwapFor(a)!("/preview", () => true));
306
+ expect(collectedRootIds()).toEqual(["a"]);
307
+ fetchSpy.mockRestore();
308
+ });
295
309
  });
@@ -1,4 +1,5 @@
1
1
  import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
2
+ import { onPreviewContentReplaced } from "../../player/sceneSwap";
2
3
  import { memo, useState, useCallback, useEffect, useRef } from "react";
3
4
  import {
4
5
  collectDomEditLayerItems,
@@ -164,8 +165,7 @@ export const LayersPanel = memo(function LayersPanel() {
164
165
  prevDocVersionRef.current += 1;
165
166
  collectLayers();
166
167
  };
167
- iframe.addEventListener("load", handleLoad);
168
- return () => iframe.removeEventListener("load", handleLoad);
168
+ return onPreviewContentReplaced(iframe, handleLoad);
169
169
  }, [previewIframeRef, livePreviewIframe, collectLayers]);
170
170
 
171
171
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
2
+ import { onPreviewContentReplaced } from "../../player/sceneSwap";
2
3
  import {
3
4
  useEffect,
4
5
  useLayoutEffect,
@@ -119,8 +120,7 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
119
120
  const iframe = iframeRef.current;
120
121
  if (!iframe || !callId) return;
121
122
  const dismiss = () => studioEditLifecycle.dismiss(callId);
122
- iframe.addEventListener("load", dismiss);
123
- return () => iframe.removeEventListener("load", dismiss);
123
+ return onPreviewContentReplaced(iframe, dismiss);
124
124
  }, [callId, iframeRef, livePreviewIframe]);
125
125
 
126
126
  useEffect(() => {