@hyperframes/studio 0.6.0-alpha.12 → 0.6.0-alpha.13

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.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>HyperFrames Studio</title>
7
- <script type="module" crossorigin src="/assets/index-xdyn_qRZ.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-FWg79aJz.css">
7
+ <script type="module" crossorigin src="/assets/index-SEkerIt9.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-B0OzpJPU.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.0-alpha.12",
3
+ "version": "0.6.0-alpha.13",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "@phosphor-icons/react": "^2.1.10",
33
33
  "codemirror": "^6.0.1",
34
34
  "motion": "^12.38.0",
35
- "@hyperframes/core": "0.6.0-alpha.12",
36
- "@hyperframes/player": "0.6.0-alpha.12"
35
+ "@hyperframes/core": "0.6.0-alpha.13",
36
+ "@hyperframes/player": "0.6.0-alpha.13"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.0.0",
@@ -47,7 +47,7 @@
47
47
  "vite": "^6.4.2",
48
48
  "vitest": "^3.2.4",
49
49
  "zustand": "^5.0.0",
50
- "@hyperframes/producer": "0.6.0-alpha.12"
50
+ "@hyperframes/producer": "0.6.0-alpha.13"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "^18.0.0 || ^19.0.0",
package/src/App.tsx CHANGED
@@ -74,24 +74,19 @@ import {
74
74
  DomEditOverlay,
75
75
  type DomEditGroupPathOffsetCommit,
76
76
  } from "./components/editor/DomEditOverlay";
77
- import { TimelineLayerPanel } from "./components/editor/TimelineLayerPanel";
78
77
  import {
79
78
  STUDIO_INSPECTOR_PANELS_ENABLED,
80
79
  STUDIO_MANUAL_EDITING_DISABLED_TITLE,
81
80
  STUDIO_MOTION_PANEL_ENABLED,
82
81
  STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
83
82
  STUDIO_PREVIEW_SELECTION_ENABLED,
84
- STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED,
85
83
  } from "./components/editor/manualEditingAvailability";
86
84
  import {
87
85
  buildDomEditStylePatchOperation,
88
86
  buildDomEditTextPatchOperation,
89
87
  buildElementAgentPrompt,
90
- collectDomEditLayerItems,
91
- countDomEditChildLayers,
92
88
  findElementForSelection,
93
89
  findElementForTimelineElement,
94
- getDomEditLayerKey,
95
90
  getDomEditTargetKey,
96
91
  isLargeRasterDomEditSelection,
97
92
  isTextEditableSelection,
@@ -99,7 +94,6 @@ import {
99
94
  serializeDomEditTextFields,
100
95
  resolveDomEditSelection,
101
96
  type DomEditViewport,
102
- type DomEditLayerItem,
103
97
  type DomEditTextField,
104
98
  type DomEditSelection,
105
99
  buildDefaultDomEditTextField,
@@ -134,14 +128,7 @@ import {
134
128
  upsertStudioGsapMotion,
135
129
  } from "./components/editor/studioMotion";
136
130
  import { saveProjectFilesWithHistory } from "./utils/studioFileHistory";
137
- import {
138
- canInspectTimelineElement,
139
- getTimelineElementKey,
140
- getTimelineLayerVisibilityInPreview,
141
- isTimelineElementActiveAtTime,
142
- isTimelineLayerVisibleInPreview,
143
- shouldShowTimelineInspectorBounds,
144
- } from "./utils/timelineInspector";
131
+ import { getTimelineElementKey, isTimelineElementActiveAtTime } from "./utils/timelineInspector";
145
132
 
146
133
  interface EditingFile {
147
134
  path: string;
@@ -543,105 +530,6 @@ function readPlaybackTime(target: object | null, key: string): number | null {
543
530
  }
544
531
  }
545
532
 
546
- interface PreviewPlayerCompat {
547
- getTime: () => number;
548
- renderSeek: (timeSeconds: number) => void;
549
- }
550
-
551
- function getPreviewPlayer(win: Window | null | undefined): PreviewPlayerCompat | null {
552
- const player = objectLike(win ? Reflect.get(win, "__player") : null);
553
- if (!player) return null;
554
- const getTime = Reflect.get(player, "getTime");
555
- const renderSeek = Reflect.get(player, "renderSeek");
556
- if (typeof getTime !== "function" || typeof renderSeek !== "function") return null;
557
- return {
558
- getTime: () => {
559
- const value = getTime.call(player);
560
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
561
- },
562
- renderSeek: (timeSeconds: number) => {
563
- renderSeek.call(player, timeSeconds);
564
- },
565
- };
566
- }
567
-
568
- function seekStudioPreview(iframe: HTMLIFrameElement | null, timeSeconds: number): boolean {
569
- const player = getPreviewPlayer(iframe?.contentWindow);
570
- if (!player) return false;
571
- const nextTime = Math.max(0, timeSeconds);
572
- player.renderSeek(nextTime);
573
- usePlayerStore.getState().setCurrentTime(nextTime);
574
- liveTime.notify(nextTime);
575
- return true;
576
- }
577
-
578
- function parseFiniteSeconds(value: string | null): number | null {
579
- if (value == null || value.trim() === "") return null;
580
- const parsed = Number.parseFloat(value);
581
- return Number.isFinite(parsed) ? parsed : null;
582
- }
583
-
584
- function resolveLayerVisibleSeekTime(
585
- layerElement: HTMLElement,
586
- timelineElement: TimelineElement | null,
587
- player: PreviewPlayerCompat | null,
588
- ): number | null {
589
- if (!timelineElement || !player) return null;
590
- const originalTime = player.getTime();
591
-
592
- const clipStart = Math.max(0, timelineElement.start);
593
- const clipEnd = Math.max(clipStart, clipStart + Math.max(0, timelineElement.duration));
594
- const authoredStart = parseFiniteSeconds(
595
- layerElement.getAttribute("data-start") ??
596
- layerElement.closest<HTMLElement>("[data-start]")?.getAttribute("data-start") ??
597
- null,
598
- );
599
- const preferredTime =
600
- authoredStart == null
601
- ? clipStart
602
- : Math.min(clipEnd, Math.max(clipStart, clipStart + authoredStart));
603
- const candidates = [preferredTime, clipStart];
604
- const duration = clipEnd - clipStart;
605
- if (duration > 0) {
606
- const maxSamples = 24;
607
- const frameStep = 1 / 24;
608
- const step = Math.max(frameStep, duration / maxSamples);
609
- for (let time = clipStart; time <= clipEnd + 0.0001; time += step) {
610
- candidates.push(Math.min(clipEnd, time));
611
- }
612
- }
613
- candidates.push(clipEnd);
614
-
615
- let lastTried = preferredTime;
616
- let clearestVisibleTime: number | null = null;
617
- let clearestVisibleOpacity = 0;
618
- let resolvedTime: number | null = null;
619
- const seen = new Set<string>();
620
- try {
621
- for (const candidate of candidates) {
622
- const time = Math.min(clipEnd, Math.max(clipStart, candidate));
623
- const key = time.toFixed(4);
624
- if (seen.has(key)) continue;
625
- seen.add(key);
626
- lastTried = time;
627
- player.renderSeek(time);
628
- const visibility = getTimelineLayerVisibilityInPreview(layerElement);
629
- if (visibility.visible && visibility.compositeOpacity > clearestVisibleOpacity) {
630
- clearestVisibleTime = time;
631
- clearestVisibleOpacity = visibility.compositeOpacity;
632
- }
633
- if (isTimelineLayerVisibleInPreview(layerElement, { minCompositeOpacity: 0.9 })) {
634
- resolvedTime = time;
635
- break;
636
- }
637
- }
638
- } finally {
639
- player.renderSeek(originalTime);
640
- }
641
-
642
- return resolvedTime ?? clearestVisibleTime ?? lastTried;
643
- }
644
-
645
533
  function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number | null {
646
534
  const win = iframe?.contentWindow;
647
535
  if (!win) return null;
@@ -901,9 +789,8 @@ export function StudioApp() {
901
789
  const [copiedAgentPrompt, setCopiedAgentPrompt] = useState(false);
902
790
  const [agentModalOpen, setAgentModalOpen] = useState(false);
903
791
  const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
904
- const [inspectedTimelineElementId, setInspectedTimelineElementId] = useState<string | null>(null);
905
792
  const [compositionLoading, setCompositionLoading] = useState(true);
906
- const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
793
+ const [, setPreviewDocumentVersion] = useState(0);
907
794
  const refreshPreviewDocumentVersion = useCallback(() => {
908
795
  setPreviewDocumentVersion((version) => version + 1);
909
796
  window.setTimeout(() => setPreviewDocumentVersion((version) => version + 1), 80);
@@ -2156,10 +2043,8 @@ export function StudioApp() {
2156
2043
 
2157
2044
  const writeHistoryProjectFile = useCallback(
2158
2045
  async (path: string, content: string): Promise<void> => {
2046
+ domEditSaveTimestampRef.current = Date.now();
2159
2047
  await writeProjectFile(path, content);
2160
- if (path === STUDIO_MANUAL_EDITS_PATH || path === STUDIO_MOTION_PATH) {
2161
- domEditSaveTimestampRef.current = Date.now();
2162
- }
2163
2048
  },
2164
2049
  [writeProjectFile],
2165
2050
  );
@@ -2207,11 +2092,12 @@ export function StudioApp() {
2207
2092
  iframe: HTMLIFrameElement | null = previewIframeRef.current,
2208
2093
  options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean },
2209
2094
  ) => {
2210
- const readRevision = studioManualEditRevisionRef.current;
2211
2095
  const readFromDiskFirst = Boolean(options?.forceFromDisk || options?.readFromDiskFirst);
2212
2096
  if (!readFromDiskFirst) {
2213
2097
  applyCurrentStudioManualEditsToPreview(iframe);
2098
+ return;
2214
2099
  }
2100
+ const readRevision = studioManualEditRevisionRef.current;
2215
2101
  let content: string;
2216
2102
  try {
2217
2103
  content = await readOptionalProjectFile(STUDIO_MANUAL_EDITS_PATH);
@@ -2219,31 +2105,19 @@ export function StudioApp() {
2219
2105
  const message =
2220
2106
  error instanceof Error ? error.message : "Failed to read manual edit manifest";
2221
2107
  showToast(message);
2222
- if (readFromDiskFirst) {
2223
- applyCurrentStudioManualEditsToPreview(iframe);
2224
- }
2108
+ applyCurrentStudioManualEditsToPreview(iframe);
2225
2109
  return;
2226
2110
  }
2227
2111
  if (options?.forceFromDisk || readRevision === studioManualEditRevisionRef.current) {
2228
2112
  studioManualEditManifestRef.current = parseStudioManualEditManifest(content);
2229
2113
  if (options?.forceFromDisk) studioManualEditRevisionRef.current += 1;
2230
- applyCurrentStudioManualEditsToPreview(iframe);
2231
- return;
2232
- }
2233
- if (readFromDiskFirst) {
2234
- applyCurrentStudioManualEditsToPreview(iframe);
2235
2114
  }
2115
+ applyCurrentStudioManualEditsToPreview(iframe);
2236
2116
  },
2237
2117
  [applyCurrentStudioManualEditsToPreview, readOptionalProjectFile, showToast],
2238
2118
  );
2239
2119
  applyStudioManualEditsToPreviewRef.current = applyStudioManualEditsToPreview;
2240
2120
 
2241
- const applyStudioManualEditsToPreviewAfterRefresh = useCallback(
2242
- (iframe: HTMLIFrameElement | null = previewIframeRef.current) =>
2243
- applyStudioManualEditsToPreview(iframe, { readFromDiskFirst: true }),
2244
- [applyStudioManualEditsToPreview],
2245
- );
2246
-
2247
2121
  const applyCurrentStudioMotionToPreview = useCallback(
2248
2122
  (iframe: HTMLIFrameElement | null = previewIframeRef.current) => {
2249
2123
  if (!iframe) return;
@@ -2282,43 +2156,32 @@ export function StudioApp() {
2282
2156
  iframe: HTMLIFrameElement | null = previewIframeRef.current,
2283
2157
  options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean },
2284
2158
  ) => {
2285
- const readRevision = studioMotionRevisionRef.current;
2286
2159
  const readFromDiskFirst = Boolean(options?.forceFromDisk || options?.readFromDiskFirst);
2287
2160
  if (!readFromDiskFirst) {
2288
2161
  applyCurrentStudioMotionToPreview(iframe);
2162
+ return;
2289
2163
  }
2164
+ const readRevision = studioMotionRevisionRef.current;
2290
2165
  let content: string;
2291
2166
  try {
2292
2167
  content = await readOptionalProjectFile(STUDIO_MOTION_PATH);
2293
2168
  } catch (error) {
2294
2169
  const message = error instanceof Error ? error.message : "Failed to read motion manifest";
2295
2170
  showToast(message);
2296
- if (readFromDiskFirst) {
2297
- applyCurrentStudioMotionToPreview(iframe);
2298
- }
2171
+ applyCurrentStudioMotionToPreview(iframe);
2299
2172
  return;
2300
2173
  }
2301
2174
  if (options?.forceFromDisk || readRevision === studioMotionRevisionRef.current) {
2302
2175
  studioMotionManifestRef.current = parseStudioMotionManifest(content);
2303
2176
  if (options?.forceFromDisk) studioMotionRevisionRef.current += 1;
2304
2177
  setStudioMotionRevision((revision) => revision + 1);
2305
- applyCurrentStudioMotionToPreview(iframe);
2306
- return;
2307
- }
2308
- if (readFromDiskFirst) {
2309
- applyCurrentStudioMotionToPreview(iframe);
2310
2178
  }
2179
+ applyCurrentStudioMotionToPreview(iframe);
2311
2180
  },
2312
2181
  [applyCurrentStudioMotionToPreview, readOptionalProjectFile, showToast],
2313
2182
  );
2314
2183
  applyStudioMotionToPreviewRef.current = applyStudioMotionToPreview;
2315
2184
 
2316
- const applyStudioMotionToPreviewAfterRefresh = useCallback(
2317
- (iframe: HTMLIFrameElement | null = previewIframeRef.current) =>
2318
- applyStudioMotionToPreview(iframe, { readFromDiskFirst: true }),
2319
- [applyStudioMotionToPreview],
2320
- );
2321
-
2322
2185
  const commitStudioManualEditManifestOptimistically = useCallback(
2323
2186
  (
2324
2187
  updateManifest: (manifest: StudioManualEditManifest) => StudioManualEditManifest,
@@ -2475,6 +2338,19 @@ export function StudioApp() {
2475
2338
  return;
2476
2339
  }
2477
2340
 
2341
+ // Reload the iframe in-place rather than recreating the Player component.
2342
+ // This preserves the <hyperframes-player> web component and its shader
2343
+ // transition cache — only the iframe document reloads, so transitions that
2344
+ // weren't touched by the undo/redo don't need to rebuild from scratch.
2345
+ const iframe = previewIframeRef.current;
2346
+ if (iframe?.contentWindow) {
2347
+ try {
2348
+ iframe.contentWindow.location.reload();
2349
+ return;
2350
+ } catch {
2351
+ // Cross-origin or detached — fall through to full refresh
2352
+ }
2353
+ }
2478
2354
  setRefreshKey((key) => key + 1);
2479
2355
  },
2480
2356
  [applyStudioManualEditsToPreview, applyStudioMotionToPreview],
@@ -2637,148 +2513,20 @@ export function StudioApp() {
2637
2513
  [activeCompPath, buildDomSelectionFromTarget, compIdToSrc, isMasterView],
2638
2514
  );
2639
2515
 
2640
- const inspectedTimelineElement = useMemo(
2641
- () =>
2642
- timelineElements.find(
2643
- (element) => getTimelineElementKey(element) === inspectedTimelineElementId,
2644
- ) ?? null,
2645
- [inspectedTimelineElementId, timelineElements],
2646
- );
2647
-
2648
- const timelineLayerChildCounts = useMemo(() => {
2649
- void previewDocumentVersion;
2650
- const counts = new Map<string, number>();
2651
- if (!STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED || !inspectedTimelineElement) return counts;
2652
-
2653
- const key = getTimelineElementKey(inspectedTimelineElement);
2654
- if (key) {
2655
- const selection = buildDomSelectionForTimelineElement(inspectedTimelineElement);
2656
- const count = countDomEditChildLayers(selection?.element, {
2657
- activeCompositionPath: activeCompPath,
2658
- isMasterView,
2659
- });
2660
- if (count > 0) counts.set(key, count);
2661
- }
2662
-
2663
- return counts;
2664
- }, [
2665
- activeCompPath,
2666
- buildDomSelectionForTimelineElement,
2667
- inspectedTimelineElement,
2668
- isMasterView,
2669
- previewDocumentVersion,
2670
- ]);
2671
-
2672
- const inspectedTimelineLayers = useMemo(() => {
2673
- void previewDocumentVersion;
2674
- if (!STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED || !inspectedTimelineElement) return [];
2675
- const selection = buildDomSelectionForTimelineElement(inspectedTimelineElement);
2676
- return collectDomEditLayerItems(selection?.element, {
2677
- activeCompositionPath: activeCompPath,
2678
- isMasterView,
2679
- });
2680
- }, [
2681
- activeCompPath,
2682
- buildDomSelectionForTimelineElement,
2683
- inspectedTimelineElement,
2684
- isMasterView,
2685
- previewDocumentVersion,
2686
- ]);
2687
-
2688
- const selectedTimelineLayerKey = useMemo(
2689
- () => (domEditSelection ? getDomEditLayerKey(domEditSelection) : null),
2690
- [domEditSelection],
2691
- );
2692
-
2693
2516
  const handleTimelineElementSelect = useCallback(
2694
2517
  (element: TimelineElement | null) => {
2695
2518
  if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
2696
2519
  if (!element) {
2697
2520
  applyDomSelection(null, { revealPanel: false });
2698
- setInspectedTimelineElementId(null);
2699
- return;
2700
- }
2701
-
2702
- const selection = buildDomSelectionForTimelineElement(element);
2703
- if (selection) applyDomSelection(selection);
2704
-
2705
- const key = getTimelineElementKey(element);
2706
- if (STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED && key && canInspectTimelineElement(element)) {
2707
- setInspectedTimelineElementId(key);
2708
- setLeftCollapsed(false);
2709
-
2710
- const iframe = previewIframeRef.current;
2711
- if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
2712
- seekStudioPreview(iframe, element.start);
2713
- }
2714
- } else {
2715
- setInspectedTimelineElementId(null);
2716
- }
2717
- },
2718
- [applyDomSelection, buildDomSelectionForTimelineElement, currentTime],
2719
- );
2720
-
2721
- const handleTimelineElementInspect = useCallback(
2722
- (element: TimelineElement) => {
2723
- if (!STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED || !STUDIO_INSPECTOR_PANELS_ENABLED) return;
2724
- if (!canInspectTimelineElement(element)) {
2725
- showToast("Audio clips do not have visual layers.", "info");
2726
2521
  return;
2727
2522
  }
2728
2523
 
2729
- const key = getTimelineElementKey(element);
2730
- if (!key) return;
2731
- setInspectedTimelineElementId((current) => (current === key ? null : key));
2732
- setLeftCollapsed(false);
2733
-
2734
- const iframe = previewIframeRef.current;
2735
- if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
2736
- seekStudioPreview(iframe, element.start);
2737
- }
2738
-
2739
2524
  const selection = buildDomSelectionForTimelineElement(element);
2740
2525
  if (selection) applyDomSelection(selection);
2741
2526
  },
2742
- [applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
2743
- );
2744
-
2745
- const handleTimelineLayerSelect = useCallback(
2746
- (layer: DomEditLayerItem) => {
2747
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
2748
-
2749
- const iframe = previewIframeRef.current;
2750
- const player = getPreviewPlayer(iframe?.contentWindow);
2751
- const visibleTime = resolveLayerVisibleSeekTime(
2752
- layer.element,
2753
- inspectedTimelineElement,
2754
- player,
2755
- );
2756
- if (visibleTime != null) {
2757
- seekStudioPreview(iframe, visibleTime);
2758
- }
2759
-
2760
- const selection = buildDomSelectionFromTarget(layer.element, { preferClipAncestor: false });
2761
- if (!selection) {
2762
- showToast("Studio could not resolve this nested layer.", "error");
2763
- return;
2764
- }
2765
-
2766
- applyDomSelection(selection);
2767
- requestAnimationFrame(refreshPreviewDocumentVersion);
2768
- },
2769
- [
2770
- applyDomSelection,
2771
- buildDomSelectionFromTarget,
2772
- inspectedTimelineElement,
2773
- refreshPreviewDocumentVersion,
2774
- showToast,
2775
- ],
2527
+ [applyDomSelection, buildDomSelectionForTimelineElement],
2776
2528
  );
2777
2529
 
2778
- const handleTimelineLayerPanelClose = useCallback(() => {
2779
- setInspectedTimelineElementId(null);
2780
- }, []);
2781
-
2782
2530
  const preloadAgentPromptSnippet = useCallback(
2783
2531
  async (selection: DomEditSelection) => {
2784
2532
  const pid = projectIdRef.current;
@@ -3558,8 +3306,8 @@ export function StudioApp() {
3558
3306
  attachErrorCapture();
3559
3307
  syncPreviewHistoryHotkey(previewIframe);
3560
3308
  void (async () => {
3561
- await applyStudioManualEditsToPreviewAfterRefresh(previewIframe);
3562
- await applyStudioMotionToPreviewAfterRefresh(previewIframe);
3309
+ await applyStudioManualEditsToPreviewRef.current(previewIframe);
3310
+ await applyStudioMotionToPreviewRef.current(previewIframe);
3563
3311
  })();
3564
3312
  syncSelectionFromDocument();
3565
3313
  refreshPreviewDocumentVersion();
@@ -3570,8 +3318,8 @@ export function StudioApp() {
3570
3318
  attachErrorCapture();
3571
3319
  syncPreviewHistoryHotkey(previewIframe);
3572
3320
  void (async () => {
3573
- await applyStudioManualEditsToPreviewAfterRefresh(previewIframe);
3574
- await applyStudioMotionToPreviewAfterRefresh(previewIframe);
3321
+ await applyStudioManualEditsToPreviewRef.current(previewIframe);
3322
+ await applyStudioMotionToPreviewRef.current(previewIframe);
3575
3323
  })();
3576
3324
  syncSelectionFromDocument();
3577
3325
  refreshPreviewDocumentVersion();
@@ -3584,8 +3332,6 @@ export function StudioApp() {
3584
3332
  }, [
3585
3333
  activeCompPath,
3586
3334
  applyDomSelection,
3587
- applyStudioManualEditsToPreviewAfterRefresh,
3588
- applyStudioMotionToPreviewAfterRefresh,
3589
3335
  buildDomSelectionFromTarget,
3590
3336
  captionEditMode,
3591
3337
  previewIframe,
@@ -4054,26 +3800,15 @@ export function StudioApp() {
4054
3800
  const motionPanelActive =
4055
3801
  STUDIO_INSPECTOR_PANELS_ENABLED && STUDIO_MOTION_PANEL_ENABLED && rightPanelTab === "motion";
4056
3802
  const inspectorPanelActive = designPanelActive || motionPanelActive;
3803
+ const isPlaying = usePlayerStore((s) => s.isPlaying);
4057
3804
  const shouldShowSelectedDomBounds =
4058
3805
  inspectorPanelActive &&
4059
3806
  !rightCollapsed &&
3807
+ !isPlaying &&
4060
3808
  (!selectedTimelineElement ||
4061
3809
  isTimelineElementActiveAtTime(currentTime, selectedTimelineElement));
4062
3810
  const inspectorButtonActive =
4063
3811
  STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive;
4064
- const timelineLayerPanel =
4065
- STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED &&
4066
- inspectedTimelineElement &&
4067
- inspectedTimelineLayers.length > 0 ? (
4068
- <TimelineLayerPanel
4069
- clipLabel={getTimelineElementLabel(inspectedTimelineElement)}
4070
- layers={inspectedTimelineLayers}
4071
- selectedLayerKey={selectedTimelineLayerKey}
4072
- onSelectLayer={handleTimelineLayerSelect}
4073
- onClose={handleTimelineLayerPanelClose}
4074
- />
4075
- ) : null;
4076
-
4077
3812
  if (resolving || !projectId) {
4078
3813
  return (
4079
3814
  <div className="h-full w-full bg-neutral-950 flex items-center justify-center">
@@ -4287,7 +4022,6 @@ export function StudioApp() {
4287
4022
  onLint={handleLint}
4288
4023
  linting={linting}
4289
4024
  onToggleCollapse={toggleLeftSidebar}
4290
- takeoverContent={timelineLayerPanel}
4291
4025
  />
4292
4026
  )}
4293
4027
 
@@ -4319,16 +4053,12 @@ export function StudioApp() {
4319
4053
  onResizeElement={handleTimelineElementResize}
4320
4054
  onBlockedEditAttempt={handleBlockedTimelineEdit}
4321
4055
  onSelectTimelineElement={handleTimelineElementSelect}
4322
- onInspectTimelineElement={handleTimelineElementInspect}
4323
- inspectedTimelineElementId={inspectedTimelineElementId}
4324
- timelineLayerChildCounts={timelineLayerChildCounts}
4325
4056
  onCompIdToSrcChange={setCompIdToSrc}
4326
4057
  onCompositionLoadingChange={setCompositionLoading}
4327
4058
  onCompositionChange={(compPath) => {
4328
4059
  // Sync activeCompPath when user drills down via timeline double-click
4329
4060
  // or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
4330
4061
  setActiveCompPath(compPath);
4331
- setInspectedTimelineElementId(null);
4332
4062
  refreshPreviewDocumentVersion();
4333
4063
  }}
4334
4064
  onIframeRef={handlePreviewIframeRef}
@@ -4340,7 +4070,10 @@ export function StudioApp() {
4340
4070
  iframeRef={previewIframeRef}
4341
4071
  activeCompositionPath={activeCompPath}
4342
4072
  hoverSelection={
4343
- STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
4073
+ STUDIO_PREVIEW_SELECTION_ENABLED &&
4074
+ !captionEditMode &&
4075
+ !compositionLoading &&
4076
+ !isPlaying
4344
4077
  ? domEditHoverSelection
4345
4078
  : null
4346
4079
  }
@@ -85,6 +85,20 @@ interface DomEditOverlayProps {
85
85
  onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
86
86
  }
87
87
 
88
+ function isElementVisibleForOverlay(el: HTMLElement): boolean {
89
+ const win = el.ownerDocument.defaultView;
90
+ if (!win) return true;
91
+ let current: HTMLElement | null = el;
92
+ while (current) {
93
+ const computed = win.getComputedStyle(current);
94
+ if (computed.display === "none" || computed.visibility === "hidden") return false;
95
+ const opacity = Number.parseFloat(computed.opacity);
96
+ if (Number.isFinite(opacity) && opacity <= 0.01) return false;
97
+ current = current.parentElement;
98
+ }
99
+ return true;
100
+ }
101
+
88
102
  function toOverlayRect(
89
103
  overlayEl: HTMLDivElement,
90
104
  iframe: HTMLIFrameElement,
@@ -534,7 +548,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
534
548
 
535
549
  if (sel) {
536
550
  const el = resolveElement(doc, sel, resolvedElementRef);
537
- if (el) {
551
+ if (el && isElementVisibleForOverlay(el)) {
538
552
  setNextOverlayRect(toOverlayRect(overlayEl, iframe, el));
539
553
  } else {
540
554
  clearOverlayRect();