@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/assets/{hyperframes-player-DjsVzYFP.js → hyperframes-player-DMgdgHZd.js} +1 -1
- package/dist/assets/index-B0OzpJPU.css +1 -0
- package/dist/assets/index-SEkerIt9.js +110 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +47 -302
- package/src/components/editor/DomEditOverlay.tsx +15 -1
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +158 -27
- package/src/components/editor/domEditing.ts +38 -5
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/nle/NLELayout.tsx +0 -10
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/player/components/Player.tsx +19 -1
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +2 -83
- package/src/player/components/TimelineClip.tsx +9 -244
- package/dist/assets/index-FWg79aJz.css +0 -1
- package/dist/assets/index-xyVaWqe2.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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/
|
|
36
|
-
"@hyperframes/
|
|
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.
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3550
|
-
await
|
|
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
|
|
3562
|
-
await
|
|
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 &&
|
|
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();
|