@hyperframes/studio 0.6.31 → 0.6.32
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/index-C-pv1DOD.js +120 -0
- package/dist/assets/index-Cd3DF1je.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +52 -1
- package/src/components/StudioLeftSidebar.tsx +4 -0
- package/src/components/StudioPreviewArea.tsx +29 -1
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/manualEditingAvailability.ts +10 -1
- package/src/components/nle/NLELayout.tsx +35 -4
- package/src/components/nle/NLEPreview.test.ts +3 -11
- package/src/components/nle/NLEPreview.tsx +10 -46
- package/src/components/nle/usePreviewBlockDrop.ts +109 -0
- package/src/components/sidebar/BlocksTab.tsx +22 -98
- package/src/components/sidebar/LeftSidebar.tsx +12 -4
- package/src/hooks/useDomEditSession.ts +19 -1
- package/src/hooks/useFileManager.ts +3 -0
- package/src/main.tsx +6 -0
- package/src/utils/blockInstaller.ts +65 -32
- package/src/utils/timelineAssetDrop.ts +8 -1
- package/dist/assets/index-BWBj8I6Q.css +0 -1
- package/dist/assets/index-Do0kAMcy.js +0 -115
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useCallback, useState, type RefObject } from "react";
|
|
2
|
+
import { TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
|
3
|
+
|
|
4
|
+
interface UsePreviewBlockDropOptions {
|
|
5
|
+
portrait?: boolean;
|
|
6
|
+
stageRef: RefObject<HTMLDivElement | null>;
|
|
7
|
+
onBlockDrop?: (blockName: string, position: { left: number; top: number }) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BlockDropPayload {
|
|
11
|
+
name: string;
|
|
12
|
+
dimensions?: { width: number; height: number };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseBlockPayload(raw: string): BlockDropPayload | null {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(raw) as {
|
|
18
|
+
name?: string;
|
|
19
|
+
dimensions?: { width: number; height: number };
|
|
20
|
+
};
|
|
21
|
+
return parsed.name ? (parsed as BlockDropPayload) : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveCompositionPosition(
|
|
28
|
+
clientX: number,
|
|
29
|
+
clientY: number,
|
|
30
|
+
stageRect: DOMRect,
|
|
31
|
+
portrait: boolean | undefined,
|
|
32
|
+
): { left: number; top: number } | null {
|
|
33
|
+
if (stageRect.width === 0 || stageRect.height === 0) return null;
|
|
34
|
+
|
|
35
|
+
const normalizedX = (clientX - stageRect.left) / stageRect.width;
|
|
36
|
+
const normalizedY = (clientY - stageRect.top) / stageRect.height;
|
|
37
|
+
|
|
38
|
+
const compWidth = portrait ? 1080 : 1920;
|
|
39
|
+
const compHeight = portrait ? 1920 : 1080;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
left: Math.max(0, Math.min(normalizedX * compWidth, compWidth)),
|
|
43
|
+
top: Math.max(0, Math.min(normalizedY * compHeight, compHeight)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function centerBlockAtPosition(
|
|
48
|
+
pos: { left: number; top: number },
|
|
49
|
+
block: BlockDropPayload,
|
|
50
|
+
): { left: number; top: number } {
|
|
51
|
+
const blockW = block.dimensions?.width ?? 0;
|
|
52
|
+
const blockH = block.dimensions?.height ?? 0;
|
|
53
|
+
return {
|
|
54
|
+
left: Math.max(0, pos.left - blockW / 2),
|
|
55
|
+
top: Math.max(0, pos.top - blockH / 2),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function usePreviewBlockDrop({
|
|
60
|
+
portrait,
|
|
61
|
+
stageRef,
|
|
62
|
+
onBlockDrop,
|
|
63
|
+
}: UsePreviewBlockDropOptions) {
|
|
64
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
65
|
+
|
|
66
|
+
const handleDragOver = useCallback(
|
|
67
|
+
(e: React.DragEvent) => {
|
|
68
|
+
if (!onBlockDrop) return;
|
|
69
|
+
if (!e.dataTransfer.types.includes(TIMELINE_BLOCK_MIME)) return;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.dataTransfer.dropEffect = "copy";
|
|
72
|
+
setIsDragOver(true);
|
|
73
|
+
},
|
|
74
|
+
[onBlockDrop],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const handleDragLeave = useCallback(() => {
|
|
78
|
+
setIsDragOver(false);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// fallow-ignore-next-line complexity
|
|
82
|
+
const handleDrop = useCallback(
|
|
83
|
+
(e: React.DragEvent) => {
|
|
84
|
+
setIsDragOver(false);
|
|
85
|
+
if (!onBlockDrop) return;
|
|
86
|
+
|
|
87
|
+
const payload = e.dataTransfer.getData(TIMELINE_BLOCK_MIME);
|
|
88
|
+
if (!payload) return;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
|
|
91
|
+
const block = parseBlockPayload(payload);
|
|
92
|
+
const stage = stageRef.current;
|
|
93
|
+
if (!block || !stage) return;
|
|
94
|
+
|
|
95
|
+
const pos = resolveCompositionPosition(
|
|
96
|
+
e.clientX,
|
|
97
|
+
e.clientY,
|
|
98
|
+
stage.getBoundingClientRect(),
|
|
99
|
+
portrait,
|
|
100
|
+
);
|
|
101
|
+
if (!pos) return;
|
|
102
|
+
|
|
103
|
+
onBlockDrop(block.name, centerBlockAtPosition(pos, block));
|
|
104
|
+
},
|
|
105
|
+
[onBlockDrop, stageRef, portrait],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return { isDragOver, handleDragOver, handleDragLeave, handleDrop };
|
|
109
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { memo, useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
2
|
import { useBlockCatalog } from "../../hooks/useBlockCatalog";
|
|
4
3
|
import {
|
|
5
4
|
BLOCK_CATEGORIES,
|
|
@@ -8,12 +7,19 @@ import {
|
|
|
8
7
|
} from "../../utils/blockCategories";
|
|
9
8
|
import { TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
|
|
10
9
|
|
|
10
|
+
export interface BlockPreviewInfo {
|
|
11
|
+
videoUrl?: string;
|
|
12
|
+
posterUrl?: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
interface BlocksTabProps {
|
|
12
17
|
onAddBlock: (blockName: string) => void;
|
|
18
|
+
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
// fallow-ignore-next-line complexity
|
|
16
|
-
export const BlocksTab = memo(function BlocksTab({ onAddBlock }: BlocksTabProps) {
|
|
22
|
+
export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }: BlocksTabProps) {
|
|
17
23
|
const { loading, error, search, setSearch, category, setCategory, filteredBlocks } =
|
|
18
24
|
useBlockCatalog();
|
|
19
25
|
|
|
@@ -114,6 +120,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock }: BlocksTabProps)
|
|
|
114
120
|
videoUrl={block.preview?.video}
|
|
115
121
|
dimensions={dims}
|
|
116
122
|
onAdd={() => onAddBlock(block.name)}
|
|
123
|
+
onPreview={onPreviewBlock}
|
|
117
124
|
/>
|
|
118
125
|
);
|
|
119
126
|
})}
|
|
@@ -163,6 +170,7 @@ function BlockCard({
|
|
|
163
170
|
videoUrl,
|
|
164
171
|
dimensions,
|
|
165
172
|
onAdd,
|
|
173
|
+
onPreview,
|
|
166
174
|
}: {
|
|
167
175
|
name: string;
|
|
168
176
|
title: string;
|
|
@@ -173,52 +181,35 @@ function BlockCard({
|
|
|
173
181
|
videoUrl?: string;
|
|
174
182
|
dimensions?: { width: number; height: number };
|
|
175
183
|
onAdd: () => void;
|
|
184
|
+
onPreview?: (preview: BlockPreviewInfo | null) => void;
|
|
176
185
|
}) {
|
|
177
186
|
const [hovered, setHovered] = useState(false);
|
|
178
187
|
const [adding, setAdding] = useState(false);
|
|
179
188
|
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
180
|
-
const leaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
181
|
-
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
182
189
|
const colors = getCategoryColors(category);
|
|
183
190
|
const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
|
|
184
191
|
|
|
185
|
-
const cancelLeave = useCallback(() => {
|
|
186
|
-
if (leaveTimer.current) {
|
|
187
|
-
clearTimeout(leaveTimer.current);
|
|
188
|
-
leaveTimer.current = null;
|
|
189
|
-
}
|
|
190
|
-
}, []);
|
|
191
|
-
|
|
192
192
|
const handleEnter = useCallback(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (hoverTimer.current) {
|
|
199
|
-
clearTimeout(hoverTimer.current);
|
|
200
|
-
hoverTimer.current = null;
|
|
201
|
-
}
|
|
202
|
-
cancelLeave();
|
|
203
|
-
setHovered(false);
|
|
204
|
-
}, [cancelLeave]);
|
|
193
|
+
hoverTimer.current = setTimeout(() => {
|
|
194
|
+
setHovered(true);
|
|
195
|
+
onPreview?.({ videoUrl, posterUrl, title });
|
|
196
|
+
}, 300);
|
|
197
|
+
}, [onPreview, videoUrl, posterUrl, title]);
|
|
205
198
|
|
|
206
199
|
const handleLeave = useCallback(() => {
|
|
207
200
|
if (hoverTimer.current) {
|
|
208
201
|
clearTimeout(hoverTimer.current);
|
|
209
202
|
hoverTimer.current = null;
|
|
210
203
|
}
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
setHovered(false);
|
|
205
|
+
onPreview?.(null);
|
|
206
|
+
}, [onPreview]);
|
|
213
207
|
|
|
214
208
|
useEffect(() => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (e.key === "Escape") dismiss();
|
|
209
|
+
return () => {
|
|
210
|
+
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
|
218
211
|
};
|
|
219
|
-
|
|
220
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
221
|
-
}, [hovered, dismiss]);
|
|
212
|
+
}, []);
|
|
222
213
|
|
|
223
214
|
const handleAdd = useCallback(
|
|
224
215
|
(e: React.MouseEvent) => {
|
|
@@ -251,7 +242,6 @@ function BlockCard({
|
|
|
251
242
|
<div className="aspect-video w-full overflow-hidden relative">
|
|
252
243
|
{hovered && videoUrl ? (
|
|
253
244
|
<video
|
|
254
|
-
ref={videoRef}
|
|
255
245
|
src={videoUrl}
|
|
256
246
|
autoPlay
|
|
257
247
|
muted
|
|
@@ -313,72 +303,6 @@ function BlockCard({
|
|
|
313
303
|
</span>
|
|
314
304
|
</div>
|
|
315
305
|
</div>
|
|
316
|
-
|
|
317
|
-
{/* Fullscreen hover preview */}
|
|
318
|
-
{hovered &&
|
|
319
|
-
(videoUrl || posterUrl) &&
|
|
320
|
-
createPortal(
|
|
321
|
-
<div
|
|
322
|
-
className="fixed inset-0 z-50 flex items-center justify-center cursor-pointer"
|
|
323
|
-
onClick={dismiss}
|
|
324
|
-
onPointerEnter={cancelLeave}
|
|
325
|
-
onPointerLeave={handleLeave}
|
|
326
|
-
>
|
|
327
|
-
<div className="bg-black/80 absolute inset-0" />
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
onClick={dismiss}
|
|
331
|
-
className="absolute top-4 right-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-neutral-800/80 text-neutral-400 hover:text-white hover:bg-neutral-700 transition-colors"
|
|
332
|
-
>
|
|
333
|
-
<svg
|
|
334
|
-
width="16"
|
|
335
|
-
height="16"
|
|
336
|
-
viewBox="0 0 24 24"
|
|
337
|
-
fill="none"
|
|
338
|
-
stroke="currentColor"
|
|
339
|
-
strokeWidth="2"
|
|
340
|
-
strokeLinecap="round"
|
|
341
|
-
strokeLinejoin="round"
|
|
342
|
-
>
|
|
343
|
-
<path d="M18 6 6 18" />
|
|
344
|
-
<path d="m6 6 12 12" />
|
|
345
|
-
</svg>
|
|
346
|
-
</button>
|
|
347
|
-
<div
|
|
348
|
-
className="relative rounded-xl overflow-hidden shadow-2xl border border-neutral-600/30 cursor-default"
|
|
349
|
-
style={{ width: "80vw", maxWidth: 1200, maxHeight: "80vh" }}
|
|
350
|
-
onClick={(e) => e.stopPropagation()}
|
|
351
|
-
>
|
|
352
|
-
<div className="aspect-video bg-neutral-950">
|
|
353
|
-
{videoUrl ? (
|
|
354
|
-
<video
|
|
355
|
-
src={videoUrl}
|
|
356
|
-
autoPlay
|
|
357
|
-
muted
|
|
358
|
-
loop
|
|
359
|
-
playsInline
|
|
360
|
-
className="w-full h-full object-contain"
|
|
361
|
-
/>
|
|
362
|
-
) : (
|
|
363
|
-
<img src={posterUrl} alt={title} className="w-full h-full object-contain" />
|
|
364
|
-
)}
|
|
365
|
-
</div>
|
|
366
|
-
<div className="bg-neutral-900/95 px-4 py-3">
|
|
367
|
-
<div className="text-[14px] font-semibold text-neutral-100">{title}</div>
|
|
368
|
-
<div className="flex items-center gap-2 mt-1">
|
|
369
|
-
<span className={`w-2 h-2 rounded-full ${colors.dot}`} />
|
|
370
|
-
<span className={`text-[11px] ${colors.text}`}>
|
|
371
|
-
{BLOCK_CATEGORIES.find((c) => c.id === category)?.label}
|
|
372
|
-
</span>
|
|
373
|
-
{duration != null && (
|
|
374
|
-
<span className="text-[11px] text-neutral-500">{duration}s</span>
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
</div>,
|
|
380
|
-
document.body,
|
|
381
|
-
)}
|
|
382
306
|
</div>
|
|
383
307
|
);
|
|
384
308
|
}
|
|
@@ -3,13 +3,14 @@ import {
|
|
|
3
3
|
useState,
|
|
4
4
|
useCallback,
|
|
5
5
|
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
6
7
|
forwardRef,
|
|
7
8
|
type ReactNode,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { CompositionsTab } from "./CompositionsTab";
|
|
10
11
|
import { AssetsTab } from "./AssetsTab";
|
|
11
12
|
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
12
|
-
import { BlocksTab } from "./BlocksTab";
|
|
13
|
+
import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
|
|
13
14
|
import { FileTree } from "../editor/FileTree";
|
|
14
15
|
import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
|
|
15
16
|
|
|
@@ -17,6 +18,7 @@ export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
|
|
|
17
18
|
|
|
18
19
|
export interface LeftSidebarHandle {
|
|
19
20
|
selectTab: (tab: SidebarTab) => void;
|
|
21
|
+
getTab: () => SidebarTab;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const STORAGE_KEY = "hf-studio-sidebar-tab";
|
|
@@ -53,6 +55,7 @@ interface LeftSidebarProps {
|
|
|
53
55
|
linting?: boolean;
|
|
54
56
|
onToggleCollapse?: () => void;
|
|
55
57
|
onAddBlock?: (blockName: string) => void;
|
|
58
|
+
onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
|
|
56
59
|
takeoverContent?: ReactNode;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -82,11 +85,14 @@ export const LeftSidebar = memo(
|
|
|
82
85
|
linting,
|
|
83
86
|
onToggleCollapse,
|
|
84
87
|
onAddBlock,
|
|
88
|
+
onPreviewBlock,
|
|
85
89
|
takeoverContent,
|
|
86
90
|
},
|
|
87
91
|
ref,
|
|
88
92
|
) {
|
|
89
93
|
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
|
|
94
|
+
const tabRef = useRef(tab);
|
|
95
|
+
tabRef.current = tab;
|
|
90
96
|
|
|
91
97
|
const selectTab = useCallback((t: SidebarTab) => {
|
|
92
98
|
setTab(t);
|
|
@@ -94,7 +100,9 @@ export const LeftSidebar = memo(
|
|
|
94
100
|
trackStudioEvent("tab_switch", { panel: "left_sidebar", tab: t });
|
|
95
101
|
}, []);
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
const getTab = useCallback(() => tabRef.current, []);
|
|
104
|
+
|
|
105
|
+
useImperativeHandle(ref, () => ({ selectTab, getTab }), [selectTab, getTab]);
|
|
98
106
|
|
|
99
107
|
return (
|
|
100
108
|
<div
|
|
@@ -159,7 +167,7 @@ export const LeftSidebar = memo(
|
|
|
159
167
|
: "text-neutral-500 hover:text-neutral-200"
|
|
160
168
|
}`}
|
|
161
169
|
>
|
|
162
|
-
|
|
170
|
+
Catalog
|
|
163
171
|
</button>
|
|
164
172
|
)}
|
|
165
173
|
</div>
|
|
@@ -239,7 +247,7 @@ export const LeftSidebar = memo(
|
|
|
239
247
|
)}
|
|
240
248
|
|
|
241
249
|
{STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && onAddBlock && (
|
|
242
|
-
<BlocksTab onAddBlock={onAddBlock} />
|
|
250
|
+
<BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
|
|
243
251
|
)}
|
|
244
252
|
|
|
245
253
|
{/* Lint button pinned at the bottom */}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
3
|
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
4
4
|
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
@@ -56,6 +56,7 @@ export interface UseDomEditSessionParams {
|
|
|
56
56
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
57
57
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
58
58
|
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
59
|
+
getSidebarTab?: () => SidebarTab;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// ── Hook ──
|
|
@@ -93,6 +94,7 @@ export function useDomEditSession({
|
|
|
93
94
|
setRefreshKey: _setRefreshKey,
|
|
94
95
|
openSourceForSelection,
|
|
95
96
|
selectSidebarTab,
|
|
97
|
+
getSidebarTab,
|
|
96
98
|
}: UseDomEditSessionParams) {
|
|
97
99
|
void _setRefreshKey;
|
|
98
100
|
|
|
@@ -281,6 +283,22 @@ export function useDomEditSession({
|
|
|
281
283
|
applyStudioManualEditsToPreviewRef,
|
|
282
284
|
]);
|
|
283
285
|
|
|
286
|
+
// Auto-reveal source when an element is selected while the Code tab is active.
|
|
287
|
+
// Use a ref for the callback so the effect only fires on selection changes,
|
|
288
|
+
// not when openSourceForSelection is recreated due to editingFile content updates.
|
|
289
|
+
const openSourceRef = useRef(openSourceForSelection);
|
|
290
|
+
openSourceRef.current = openSourceForSelection;
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!domEditSelection || !openSourceRef.current || !getSidebarTab) return;
|
|
293
|
+
if (!domEditSelection.sourceFile) return;
|
|
294
|
+
if (getSidebarTab() !== "code") return;
|
|
295
|
+
openSourceRef.current(domEditSelection.sourceFile, {
|
|
296
|
+
id: domEditSelection.id,
|
|
297
|
+
selector: domEditSelection.selector,
|
|
298
|
+
selectorIndex: domEditSelection.selectorIndex,
|
|
299
|
+
});
|
|
300
|
+
}, [domEditSelection, getSidebarTab]);
|
|
301
|
+
|
|
284
302
|
return {
|
|
285
303
|
// State
|
|
286
304
|
domEditSelection,
|
|
@@ -122,6 +122,9 @@ export function useFileManager({
|
|
|
122
122
|
const handleFileSelect = useCallback((path: string) => {
|
|
123
123
|
const pid = projectIdRef.current;
|
|
124
124
|
if (!pid) return;
|
|
125
|
+
revealAbortRef.current?.abort();
|
|
126
|
+
revealAbortRef.current = null;
|
|
127
|
+
revealRequestIdRef.current++;
|
|
125
128
|
// Skip fetching binary content for media files — just set the path for preview
|
|
126
129
|
if (isMediaFile(path)) {
|
|
127
130
|
setEditingFile({ path, content: null });
|
package/src/main.tsx
CHANGED
|
@@ -23,6 +23,12 @@ function errorProps(value: unknown): {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
window.addEventListener("error", (event) => {
|
|
26
|
+
if (event.message?.includes("ResizeObserver loop")) {
|
|
27
|
+
event.stopImmediatePropagation();
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
trackStudioEvent("unhandled_error", {
|
|
27
33
|
...errorProps(event.error),
|
|
28
34
|
error_message: event.message,
|
|
@@ -9,11 +9,29 @@ import { formatTimelineAttributeNumber } from "../player/components/timelineEdit
|
|
|
9
9
|
import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
|
10
10
|
import type { EditHistoryKind } from "./editHistory";
|
|
11
11
|
|
|
12
|
+
function getMaxZIndexFromIframe(iframe: HTMLIFrameElement | null): number {
|
|
13
|
+
try {
|
|
14
|
+
const doc = iframe?.contentDocument;
|
|
15
|
+
if (!doc) return 0;
|
|
16
|
+
let max = 0;
|
|
17
|
+
for (const el of doc.body.querySelectorAll("*")) {
|
|
18
|
+
const z = parseInt(getComputedStyle(el).zIndex, 10);
|
|
19
|
+
if (Number.isFinite(z) && z > max) max = z;
|
|
20
|
+
}
|
|
21
|
+
return max;
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
interface AddBlockOptions {
|
|
13
28
|
projectId: string;
|
|
14
29
|
blockName: string;
|
|
15
30
|
activeCompPath: string | null;
|
|
16
31
|
placement?: { start: number; track: number };
|
|
32
|
+
visualPosition?: { left: number; top: number };
|
|
33
|
+
previewIframe?: HTMLIFrameElement | null;
|
|
34
|
+
currentTime?: number;
|
|
17
35
|
timelineElements: TimelineElement[];
|
|
18
36
|
readProjectFile: (path: string) => Promise<string>;
|
|
19
37
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
@@ -44,6 +62,7 @@ export async function addBlockToProject(
|
|
|
44
62
|
blockName,
|
|
45
63
|
activeCompPath,
|
|
46
64
|
placement,
|
|
65
|
+
visualPosition,
|
|
47
66
|
timelineElements,
|
|
48
67
|
readProjectFile,
|
|
49
68
|
writeProjectFile,
|
|
@@ -102,20 +121,18 @@ export async function addBlockToProject(
|
|
|
102
121
|
const isBlock = block.type === "hyperframes:block";
|
|
103
122
|
const hostDims = resolveTimelineAssetInitialGeometry(originalContent);
|
|
104
123
|
|
|
124
|
+
const currentTime = opts.currentTime ?? 0;
|
|
105
125
|
const start = placement
|
|
106
126
|
? Number(formatTimelineAttributeNumber(placement.start))
|
|
107
|
-
:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(max, te) => Math.max(max, (te.start ?? 0) + (te.duration ?? 0)),
|
|
117
|
-
10,
|
|
118
|
-
);
|
|
127
|
+
: Number(formatTimelineAttributeNumber(currentTime));
|
|
128
|
+
const blockDuration =
|
|
129
|
+
"duration" in block ? (block as { duration: number }).duration : undefined;
|
|
130
|
+
const duration =
|
|
131
|
+
blockDuration ??
|
|
132
|
+
relevantElements.reduce(
|
|
133
|
+
(max, te) => Math.max(max, (te.start ?? 0) + (te.duration ?? 0)),
|
|
134
|
+
10,
|
|
135
|
+
);
|
|
119
136
|
const track =
|
|
120
137
|
placement?.track ??
|
|
121
138
|
(isBlock
|
|
@@ -124,26 +141,42 @@ export async function addBlockToProject(
|
|
|
124
141
|
? Math.max(...relevantElements.map((te) => te.track)) + 1
|
|
125
142
|
: 1);
|
|
126
143
|
|
|
127
|
-
const zIndex =
|
|
128
|
-
|
|
129
|
-
const width =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`data-composition-src="${compositionFile}"
|
|
139
|
-
`data-start="${formatTimelineAttributeNumber(start)}"
|
|
140
|
-
`data-duration="${formatTimelineAttributeNumber(duration)}"
|
|
141
|
-
`data-track-index="${track}"
|
|
142
|
-
`data-width="${width}"
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
const zIndex = getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1;
|
|
145
|
+
|
|
146
|
+
const width = hostDims.width;
|
|
147
|
+
const height = hostDims.height;
|
|
148
|
+
|
|
149
|
+
const left = visualPosition ? Math.round(visualPosition.left) : 0;
|
|
150
|
+
const top = visualPosition ? Math.round(visualPosition.top) : 0;
|
|
151
|
+
|
|
152
|
+
const subCompHtml = [
|
|
153
|
+
`<div`,
|
|
154
|
+
` data-composition-id="${compId}"`,
|
|
155
|
+
` data-composition-src="${compositionFile}"`,
|
|
156
|
+
` data-start="${formatTimelineAttributeNumber(start)}"`,
|
|
157
|
+
` data-duration="${formatTimelineAttributeNumber(duration)}"`,
|
|
158
|
+
` data-track-index="${track}"`,
|
|
159
|
+
` data-width="${width}"`,
|
|
160
|
+
` data-height="${height}"`,
|
|
161
|
+
` style="position: absolute; left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px; z-index: ${zIndex}"`,
|
|
162
|
+
`></div>`,
|
|
163
|
+
].join("\n");
|
|
164
|
+
|
|
165
|
+
let patchedContent = insertTimelineAssetIntoSource(originalContent, subCompHtml);
|
|
166
|
+
|
|
167
|
+
const newEnd = start + duration;
|
|
168
|
+
const rootDurMatch = patchedContent.match(
|
|
169
|
+
/(<[^>]*data-composition-id="[^"]*"[^>]*data-duration=")([^"]*)(")/,
|
|
170
|
+
);
|
|
171
|
+
if (rootDurMatch) {
|
|
172
|
+
const rootDur = parseFloat(rootDurMatch[2]!);
|
|
173
|
+
if (newEnd > rootDur) {
|
|
174
|
+
patchedContent = patchedContent.replace(
|
|
175
|
+
rootDurMatch[0],
|
|
176
|
+
`${rootDurMatch[1]}${formatTimelineAttributeNumber(newEnd)}${rootDurMatch[3]}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
147
180
|
|
|
148
181
|
await saveProjectFilesWithHistory({
|
|
149
182
|
projectId,
|
|
@@ -126,5 +126,12 @@ export function insertTimelineAssetIntoSource(source: string, assetHtml: string)
|
|
|
126
126
|
throw new Error("No composition root found in target source");
|
|
127
127
|
}
|
|
128
128
|
const insertAt = match.index + match[0].length;
|
|
129
|
-
|
|
129
|
+
const lineStart = source.lastIndexOf("\n", match.index);
|
|
130
|
+
const leadingWhitespace = source.slice(lineStart + 1, match.index).match(/^(\s*)/)?.[1] ?? "";
|
|
131
|
+
const childIndent = leadingWhitespace + " ";
|
|
132
|
+
const indented = assetHtml
|
|
133
|
+
.split("\n")
|
|
134
|
+
.map((line, i) => (i === 0 ? line : childIndent + line))
|
|
135
|
+
.join("\n");
|
|
136
|
+
return `${source.slice(0, insertAt)}\n${childIndent}${indented}${source.slice(insertAt)}`;
|
|
130
137
|
}
|