@hyperframes/studio 0.6.0-alpha.11 → 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-xyVaWqe2.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.11",
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/player": "0.6.0-alpha.11",
36
- "@hyperframes/core": "0.6.0-alpha.11"
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.11"
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;
@@ -157,6 +144,12 @@ function getTimelineElementLabel(element: TimelineElement): string {
157
144
  return element.label || element.id || element.tag;
158
145
  }
159
146
 
147
+ function confirmElementDelete(label: string, kind: "timeline clip" | "element"): boolean {
148
+ return window.confirm(
149
+ `Delete ${kind} "${label}"?\n\nThis removes it from the project source. You can use Undo to restore it.`,
150
+ );
151
+ }
152
+
160
153
  type RightPanelTab = "design" | "motion" | "renders";
161
154
 
162
155
  const GENERIC_FONT_FAMILIES = new Set([
@@ -537,105 +530,6 @@ function readPlaybackTime(target: object | null, key: string): number | null {
537
530
  }
538
531
  }
539
532
 
540
- interface PreviewPlayerCompat {
541
- getTime: () => number;
542
- renderSeek: (timeSeconds: number) => void;
543
- }
544
-
545
- function getPreviewPlayer(win: Window | null | undefined): PreviewPlayerCompat | null {
546
- const player = objectLike(win ? Reflect.get(win, "__player") : null);
547
- if (!player) return null;
548
- const getTime = Reflect.get(player, "getTime");
549
- const renderSeek = Reflect.get(player, "renderSeek");
550
- if (typeof getTime !== "function" || typeof renderSeek !== "function") return null;
551
- return {
552
- getTime: () => {
553
- const value = getTime.call(player);
554
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
555
- },
556
- renderSeek: (timeSeconds: number) => {
557
- renderSeek.call(player, timeSeconds);
558
- },
559
- };
560
- }
561
-
562
- function seekStudioPreview(iframe: HTMLIFrameElement | null, timeSeconds: number): boolean {
563
- const player = getPreviewPlayer(iframe?.contentWindow);
564
- if (!player) return false;
565
- const nextTime = Math.max(0, timeSeconds);
566
- player.renderSeek(nextTime);
567
- usePlayerStore.getState().setCurrentTime(nextTime);
568
- liveTime.notify(nextTime);
569
- return true;
570
- }
571
-
572
- function parseFiniteSeconds(value: string | null): number | null {
573
- if (value == null || value.trim() === "") return null;
574
- const parsed = Number.parseFloat(value);
575
- return Number.isFinite(parsed) ? parsed : null;
576
- }
577
-
578
- function resolveLayerVisibleSeekTime(
579
- layerElement: HTMLElement,
580
- timelineElement: TimelineElement | null,
581
- player: PreviewPlayerCompat | null,
582
- ): number | null {
583
- if (!timelineElement || !player) return null;
584
- const originalTime = player.getTime();
585
-
586
- const clipStart = Math.max(0, timelineElement.start);
587
- const clipEnd = Math.max(clipStart, clipStart + Math.max(0, timelineElement.duration));
588
- const authoredStart = parseFiniteSeconds(
589
- layerElement.getAttribute("data-start") ??
590
- layerElement.closest<HTMLElement>("[data-start]")?.getAttribute("data-start") ??
591
- null,
592
- );
593
- const preferredTime =
594
- authoredStart == null
595
- ? clipStart
596
- : Math.min(clipEnd, Math.max(clipStart, clipStart + authoredStart));
597
- const candidates = [preferredTime, clipStart];
598
- const duration = clipEnd - clipStart;
599
- if (duration > 0) {
600
- const maxSamples = 24;
601
- const frameStep = 1 / 24;
602
- const step = Math.max(frameStep, duration / maxSamples);
603
- for (let time = clipStart; time <= clipEnd + 0.0001; time += step) {
604
- candidates.push(Math.min(clipEnd, time));
605
- }
606
- }
607
- candidates.push(clipEnd);
608
-
609
- let lastTried = preferredTime;
610
- let clearestVisibleTime: number | null = null;
611
- let clearestVisibleOpacity = 0;
612
- let resolvedTime: number | null = null;
613
- const seen = new Set<string>();
614
- try {
615
- for (const candidate of candidates) {
616
- const time = Math.min(clipEnd, Math.max(clipStart, candidate));
617
- const key = time.toFixed(4);
618
- if (seen.has(key)) continue;
619
- seen.add(key);
620
- lastTried = time;
621
- player.renderSeek(time);
622
- const visibility = getTimelineLayerVisibilityInPreview(layerElement);
623
- if (visibility.visible && visibility.compositeOpacity > clearestVisibleOpacity) {
624
- clearestVisibleTime = time;
625
- clearestVisibleOpacity = visibility.compositeOpacity;
626
- }
627
- if (isTimelineLayerVisibleInPreview(layerElement, { minCompositeOpacity: 0.9 })) {
628
- resolvedTime = time;
629
- break;
630
- }
631
- }
632
- } finally {
633
- player.renderSeek(originalTime);
634
- }
635
-
636
- return resolvedTime ?? clearestVisibleTime ?? lastTried;
637
- }
638
-
639
533
  function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number | null {
640
534
  const win = iframe?.contentWindow;
641
535
  if (!win) return null;
@@ -895,9 +789,8 @@ export function StudioApp() {
895
789
  const [copiedAgentPrompt, setCopiedAgentPrompt] = useState(false);
896
790
  const [agentModalOpen, setAgentModalOpen] = useState(false);
897
791
  const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
898
- const [inspectedTimelineElementId, setInspectedTimelineElementId] = useState<string | null>(null);
899
792
  const [compositionLoading, setCompositionLoading] = useState(true);
900
- const [previewDocumentVersion, setPreviewDocumentVersion] = useState(0);
793
+ const [, setPreviewDocumentVersion] = useState(0);
901
794
  const refreshPreviewDocumentVersion = useCallback(() => {
902
795
  setPreviewDocumentVersion((version) => version + 1);
903
796
  window.setTimeout(() => setPreviewDocumentVersion((version) => version + 1), 80);
@@ -1780,6 +1673,8 @@ export function StudioApp() {
1780
1673
  async (element: TimelineElement) => {
1781
1674
  const pid = projectIdRef.current;
1782
1675
  if (!pid) throw new Error("No active project");
1676
+ const label = getTimelineElementLabel(element);
1677
+ if (!confirmElementDelete(label, "timeline clip")) return;
1783
1678
 
1784
1679
  const targetPath = element.sourceFile || activeCompPath || "index.html";
1785
1680
  try {
@@ -1877,6 +1772,7 @@ export function StudioApp() {
1877
1772
  );
1878
1773
  usePlayerStore.getState().setSelectedElementId(null);
1879
1774
  setRefreshKey((k) => k + 1);
1775
+ showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
1880
1776
  } catch (error) {
1881
1777
  const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
1882
1778
  showToast(message);
@@ -1889,6 +1785,8 @@ export function StudioApp() {
1889
1785
  async (selection: DomEditSelection) => {
1890
1786
  const pid = projectIdRef.current;
1891
1787
  if (!pid) return;
1788
+ const label = selection.label || selection.id || selection.selector || selection.tagName;
1789
+ if (!confirmElementDelete(label, "element")) return;
1892
1790
 
1893
1791
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
1894
1792
  try {
@@ -1946,6 +1844,7 @@ export function StudioApp() {
1946
1844
  setDomEditGroupSelections([]);
1947
1845
  usePlayerStore.getState().setSelectedElementId(null);
1948
1846
  setRefreshKey((k) => k + 1);
1847
+ showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
1949
1848
  } catch (error) {
1950
1849
  const message = error instanceof Error ? error.message : "Failed to delete element";
1951
1850
  showToast(message);
@@ -2144,10 +2043,8 @@ export function StudioApp() {
2144
2043
 
2145
2044
  const writeHistoryProjectFile = useCallback(
2146
2045
  async (path: string, content: string): Promise<void> => {
2046
+ domEditSaveTimestampRef.current = Date.now();
2147
2047
  await writeProjectFile(path, content);
2148
- if (path === STUDIO_MANUAL_EDITS_PATH || path === STUDIO_MOTION_PATH) {
2149
- domEditSaveTimestampRef.current = Date.now();
2150
- }
2151
2048
  },
2152
2049
  [writeProjectFile],
2153
2050
  );
@@ -2195,11 +2092,12 @@ export function StudioApp() {
2195
2092
  iframe: HTMLIFrameElement | null = previewIframeRef.current,
2196
2093
  options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean },
2197
2094
  ) => {
2198
- const readRevision = studioManualEditRevisionRef.current;
2199
2095
  const readFromDiskFirst = Boolean(options?.forceFromDisk || options?.readFromDiskFirst);
2200
2096
  if (!readFromDiskFirst) {
2201
2097
  applyCurrentStudioManualEditsToPreview(iframe);
2098
+ return;
2202
2099
  }
2100
+ const readRevision = studioManualEditRevisionRef.current;
2203
2101
  let content: string;
2204
2102
  try {
2205
2103
  content = await readOptionalProjectFile(STUDIO_MANUAL_EDITS_PATH);
@@ -2207,31 +2105,19 @@ export function StudioApp() {
2207
2105
  const message =
2208
2106
  error instanceof Error ? error.message : "Failed to read manual edit manifest";
2209
2107
  showToast(message);
2210
- if (readFromDiskFirst) {
2211
- applyCurrentStudioManualEditsToPreview(iframe);
2212
- }
2108
+ applyCurrentStudioManualEditsToPreview(iframe);
2213
2109
  return;
2214
2110
  }
2215
2111
  if (options?.forceFromDisk || readRevision === studioManualEditRevisionRef.current) {
2216
2112
  studioManualEditManifestRef.current = parseStudioManualEditManifest(content);
2217
2113
  if (options?.forceFromDisk) studioManualEditRevisionRef.current += 1;
2218
- applyCurrentStudioManualEditsToPreview(iframe);
2219
- return;
2220
- }
2221
- if (readFromDiskFirst) {
2222
- applyCurrentStudioManualEditsToPreview(iframe);
2223
2114
  }
2115
+ applyCurrentStudioManualEditsToPreview(iframe);
2224
2116
  },
2225
2117
  [applyCurrentStudioManualEditsToPreview, readOptionalProjectFile, showToast],
2226
2118
  );
2227
2119
  applyStudioManualEditsToPreviewRef.current = applyStudioManualEditsToPreview;
2228
2120
 
2229
- const applyStudioManualEditsToPreviewAfterRefresh = useCallback(
2230
- (iframe: HTMLIFrameElement | null = previewIframeRef.current) =>
2231
- applyStudioManualEditsToPreview(iframe, { readFromDiskFirst: true }),
2232
- [applyStudioManualEditsToPreview],
2233
- );
2234
-
2235
2121
  const applyCurrentStudioMotionToPreview = useCallback(
2236
2122
  (iframe: HTMLIFrameElement | null = previewIframeRef.current) => {
2237
2123
  if (!iframe) return;
@@ -2270,43 +2156,32 @@ export function StudioApp() {
2270
2156
  iframe: HTMLIFrameElement | null = previewIframeRef.current,
2271
2157
  options?: { forceFromDisk?: boolean; readFromDiskFirst?: boolean },
2272
2158
  ) => {
2273
- const readRevision = studioMotionRevisionRef.current;
2274
2159
  const readFromDiskFirst = Boolean(options?.forceFromDisk || options?.readFromDiskFirst);
2275
2160
  if (!readFromDiskFirst) {
2276
2161
  applyCurrentStudioMotionToPreview(iframe);
2162
+ return;
2277
2163
  }
2164
+ const readRevision = studioMotionRevisionRef.current;
2278
2165
  let content: string;
2279
2166
  try {
2280
2167
  content = await readOptionalProjectFile(STUDIO_MOTION_PATH);
2281
2168
  } catch (error) {
2282
2169
  const message = error instanceof Error ? error.message : "Failed to read motion manifest";
2283
2170
  showToast(message);
2284
- if (readFromDiskFirst) {
2285
- applyCurrentStudioMotionToPreview(iframe);
2286
- }
2171
+ applyCurrentStudioMotionToPreview(iframe);
2287
2172
  return;
2288
2173
  }
2289
2174
  if (options?.forceFromDisk || readRevision === studioMotionRevisionRef.current) {
2290
2175
  studioMotionManifestRef.current = parseStudioMotionManifest(content);
2291
2176
  if (options?.forceFromDisk) studioMotionRevisionRef.current += 1;
2292
2177
  setStudioMotionRevision((revision) => revision + 1);
2293
- applyCurrentStudioMotionToPreview(iframe);
2294
- return;
2295
- }
2296
- if (readFromDiskFirst) {
2297
- applyCurrentStudioMotionToPreview(iframe);
2298
2178
  }
2179
+ applyCurrentStudioMotionToPreview(iframe);
2299
2180
  },
2300
2181
  [applyCurrentStudioMotionToPreview, readOptionalProjectFile, showToast],
2301
2182
  );
2302
2183
  applyStudioMotionToPreviewRef.current = applyStudioMotionToPreview;
2303
2184
 
2304
- const applyStudioMotionToPreviewAfterRefresh = useCallback(
2305
- (iframe: HTMLIFrameElement | null = previewIframeRef.current) =>
2306
- applyStudioMotionToPreview(iframe, { readFromDiskFirst: true }),
2307
- [applyStudioMotionToPreview],
2308
- );
2309
-
2310
2185
  const commitStudioManualEditManifestOptimistically = useCallback(
2311
2186
  (
2312
2187
  updateManifest: (manifest: StudioManualEditManifest) => StudioManualEditManifest,
@@ -2463,6 +2338,19 @@ export function StudioApp() {
2463
2338
  return;
2464
2339
  }
2465
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
+ }
2466
2354
  setRefreshKey((key) => key + 1);
2467
2355
  },
2468
2356
  [applyStudioManualEditsToPreview, applyStudioMotionToPreview],
@@ -2625,148 +2513,20 @@ export function StudioApp() {
2625
2513
  [activeCompPath, buildDomSelectionFromTarget, compIdToSrc, isMasterView],
2626
2514
  );
2627
2515
 
2628
- const inspectedTimelineElement = useMemo(
2629
- () =>
2630
- timelineElements.find(
2631
- (element) => getTimelineElementKey(element) === inspectedTimelineElementId,
2632
- ) ?? null,
2633
- [inspectedTimelineElementId, timelineElements],
2634
- );
2635
-
2636
- const timelineLayerChildCounts = useMemo(() => {
2637
- void previewDocumentVersion;
2638
- const counts = new Map<string, number>();
2639
- if (!STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED || !inspectedTimelineElement) return counts;
2640
-
2641
- const key = getTimelineElementKey(inspectedTimelineElement);
2642
- if (key) {
2643
- const selection = buildDomSelectionForTimelineElement(inspectedTimelineElement);
2644
- const count = countDomEditChildLayers(selection?.element, {
2645
- activeCompositionPath: activeCompPath,
2646
- isMasterView,
2647
- });
2648
- if (count > 0) counts.set(key, count);
2649
- }
2650
-
2651
- return counts;
2652
- }, [
2653
- activeCompPath,
2654
- buildDomSelectionForTimelineElement,
2655
- inspectedTimelineElement,
2656
- isMasterView,
2657
- previewDocumentVersion,
2658
- ]);
2659
-
2660
- const inspectedTimelineLayers = useMemo(() => {
2661
- void previewDocumentVersion;
2662
- if (!STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED || !inspectedTimelineElement) return [];
2663
- const selection = buildDomSelectionForTimelineElement(inspectedTimelineElement);
2664
- return collectDomEditLayerItems(selection?.element, {
2665
- activeCompositionPath: activeCompPath,
2666
- isMasterView,
2667
- });
2668
- }, [
2669
- activeCompPath,
2670
- buildDomSelectionForTimelineElement,
2671
- inspectedTimelineElement,
2672
- isMasterView,
2673
- previewDocumentVersion,
2674
- ]);
2675
-
2676
- const selectedTimelineLayerKey = useMemo(
2677
- () => (domEditSelection ? getDomEditLayerKey(domEditSelection) : null),
2678
- [domEditSelection],
2679
- );
2680
-
2681
2516
  const handleTimelineElementSelect = useCallback(
2682
2517
  (element: TimelineElement | null) => {
2683
2518
  if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
2684
2519
  if (!element) {
2685
2520
  applyDomSelection(null, { revealPanel: false });
2686
- setInspectedTimelineElementId(null);
2687
2521
  return;
2688
2522
  }
2689
2523
 
2690
2524
  const selection = buildDomSelectionForTimelineElement(element);
2691
2525
  if (selection) applyDomSelection(selection);
2692
-
2693
- const key = getTimelineElementKey(element);
2694
- if (STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED && key && canInspectTimelineElement(element)) {
2695
- setInspectedTimelineElementId(key);
2696
- setLeftCollapsed(false);
2697
-
2698
- const iframe = previewIframeRef.current;
2699
- if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
2700
- seekStudioPreview(iframe, element.start);
2701
- }
2702
- } else {
2703
- setInspectedTimelineElementId(null);
2704
- }
2705
2526
  },
2706
- [applyDomSelection, buildDomSelectionForTimelineElement, currentTime],
2527
+ [applyDomSelection, buildDomSelectionForTimelineElement],
2707
2528
  );
2708
2529
 
2709
- const handleTimelineElementInspect = useCallback(
2710
- (element: TimelineElement) => {
2711
- if (!STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED || !STUDIO_INSPECTOR_PANELS_ENABLED) return;
2712
- if (!canInspectTimelineElement(element)) {
2713
- showToast("Audio clips do not have visual layers.", "info");
2714
- return;
2715
- }
2716
-
2717
- const key = getTimelineElementKey(element);
2718
- if (!key) return;
2719
- setInspectedTimelineElementId((current) => (current === key ? null : key));
2720
- setLeftCollapsed(false);
2721
-
2722
- const iframe = previewIframeRef.current;
2723
- if (!shouldShowTimelineInspectorBounds(currentTime, element)) {
2724
- seekStudioPreview(iframe, element.start);
2725
- }
2726
-
2727
- const selection = buildDomSelectionForTimelineElement(element);
2728
- if (selection) applyDomSelection(selection);
2729
- },
2730
- [applyDomSelection, buildDomSelectionForTimelineElement, currentTime, showToast],
2731
- );
2732
-
2733
- const handleTimelineLayerSelect = useCallback(
2734
- (layer: DomEditLayerItem) => {
2735
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
2736
-
2737
- const iframe = previewIframeRef.current;
2738
- const player = getPreviewPlayer(iframe?.contentWindow);
2739
- const visibleTime = resolveLayerVisibleSeekTime(
2740
- layer.element,
2741
- inspectedTimelineElement,
2742
- player,
2743
- );
2744
- if (visibleTime != null) {
2745
- seekStudioPreview(iframe, visibleTime);
2746
- }
2747
-
2748
- const selection = buildDomSelectionFromTarget(layer.element, { preferClipAncestor: false });
2749
- if (!selection) {
2750
- showToast("Studio could not resolve this nested layer.", "error");
2751
- return;
2752
- }
2753
-
2754
- applyDomSelection(selection);
2755
- requestAnimationFrame(refreshPreviewDocumentVersion);
2756
- },
2757
- [
2758
- applyDomSelection,
2759
- buildDomSelectionFromTarget,
2760
- inspectedTimelineElement,
2761
- refreshPreviewDocumentVersion,
2762
- showToast,
2763
- ],
2764
- );
2765
-
2766
- const handleTimelineLayerPanelClose = useCallback(() => {
2767
- setInspectedTimelineElementId(null);
2768
- }, []);
2769
-
2770
2530
  const preloadAgentPromptSnippet = useCallback(
2771
2531
  async (selection: DomEditSelection) => {
2772
2532
  const pid = projectIdRef.current;
@@ -3546,8 +3306,8 @@ export function StudioApp() {
3546
3306
  attachErrorCapture();
3547
3307
  syncPreviewHistoryHotkey(previewIframe);
3548
3308
  void (async () => {
3549
- await applyStudioManualEditsToPreviewAfterRefresh(previewIframe);
3550
- await applyStudioMotionToPreviewAfterRefresh(previewIframe);
3309
+ await applyStudioManualEditsToPreviewRef.current(previewIframe);
3310
+ await applyStudioMotionToPreviewRef.current(previewIframe);
3551
3311
  })();
3552
3312
  syncSelectionFromDocument();
3553
3313
  refreshPreviewDocumentVersion();
@@ -3558,8 +3318,8 @@ export function StudioApp() {
3558
3318
  attachErrorCapture();
3559
3319
  syncPreviewHistoryHotkey(previewIframe);
3560
3320
  void (async () => {
3561
- await applyStudioManualEditsToPreviewAfterRefresh(previewIframe);
3562
- await applyStudioMotionToPreviewAfterRefresh(previewIframe);
3321
+ await applyStudioManualEditsToPreviewRef.current(previewIframe);
3322
+ await applyStudioMotionToPreviewRef.current(previewIframe);
3563
3323
  })();
3564
3324
  syncSelectionFromDocument();
3565
3325
  refreshPreviewDocumentVersion();
@@ -3572,8 +3332,6 @@ export function StudioApp() {
3572
3332
  }, [
3573
3333
  activeCompPath,
3574
3334
  applyDomSelection,
3575
- applyStudioManualEditsToPreviewAfterRefresh,
3576
- applyStudioMotionToPreviewAfterRefresh,
3577
3335
  buildDomSelectionFromTarget,
3578
3336
  captionEditMode,
3579
3337
  previewIframe,
@@ -4042,26 +3800,15 @@ export function StudioApp() {
4042
3800
  const motionPanelActive =
4043
3801
  STUDIO_INSPECTOR_PANELS_ENABLED && STUDIO_MOTION_PANEL_ENABLED && rightPanelTab === "motion";
4044
3802
  const inspectorPanelActive = designPanelActive || motionPanelActive;
3803
+ const isPlaying = usePlayerStore((s) => s.isPlaying);
4045
3804
  const shouldShowSelectedDomBounds =
4046
3805
  inspectorPanelActive &&
4047
3806
  !rightCollapsed &&
3807
+ !isPlaying &&
4048
3808
  (!selectedTimelineElement ||
4049
3809
  isTimelineElementActiveAtTime(currentTime, selectedTimelineElement));
4050
3810
  const inspectorButtonActive =
4051
3811
  STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive;
4052
- const timelineLayerPanel =
4053
- STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED &&
4054
- inspectedTimelineElement &&
4055
- inspectedTimelineLayers.length > 0 ? (
4056
- <TimelineLayerPanel
4057
- clipLabel={getTimelineElementLabel(inspectedTimelineElement)}
4058
- layers={inspectedTimelineLayers}
4059
- selectedLayerKey={selectedTimelineLayerKey}
4060
- onSelectLayer={handleTimelineLayerSelect}
4061
- onClose={handleTimelineLayerPanelClose}
4062
- />
4063
- ) : null;
4064
-
4065
3812
  if (resolving || !projectId) {
4066
3813
  return (
4067
3814
  <div className="h-full w-full bg-neutral-950 flex items-center justify-center">
@@ -4275,7 +4022,6 @@ export function StudioApp() {
4275
4022
  onLint={handleLint}
4276
4023
  linting={linting}
4277
4024
  onToggleCollapse={toggleLeftSidebar}
4278
- takeoverContent={timelineLayerPanel}
4279
4025
  />
4280
4026
  )}
4281
4027
 
@@ -4307,16 +4053,12 @@ export function StudioApp() {
4307
4053
  onResizeElement={handleTimelineElementResize}
4308
4054
  onBlockedEditAttempt={handleBlockedTimelineEdit}
4309
4055
  onSelectTimelineElement={handleTimelineElementSelect}
4310
- onInspectTimelineElement={handleTimelineElementInspect}
4311
- inspectedTimelineElementId={inspectedTimelineElementId}
4312
- timelineLayerChildCounts={timelineLayerChildCounts}
4313
4056
  onCompIdToSrcChange={setCompIdToSrc}
4314
4057
  onCompositionLoadingChange={setCompositionLoading}
4315
4058
  onCompositionChange={(compPath) => {
4316
4059
  // Sync activeCompPath when user drills down via timeline double-click
4317
4060
  // or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
4318
4061
  setActiveCompPath(compPath);
4319
- setInspectedTimelineElementId(null);
4320
4062
  refreshPreviewDocumentVersion();
4321
4063
  }}
4322
4064
  onIframeRef={handlePreviewIframeRef}
@@ -4328,7 +4070,10 @@ export function StudioApp() {
4328
4070
  iframeRef={previewIframeRef}
4329
4071
  activeCompositionPath={activeCompPath}
4330
4072
  hoverSelection={
4331
- STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode
4073
+ STUDIO_PREVIEW_SELECTION_ENABLED &&
4074
+ !captionEditMode &&
4075
+ !compositionLoading &&
4076
+ !isPlaying
4332
4077
  ? domEditHoverSelection
4333
4078
  : null
4334
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();