@hyperframes/studio 0.6.31 → 0.6.33
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-CSG9kRJg.js +138 -0
- package/dist/assets/index-SKRp8mGz.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/StudioRightPanel.tsx +46 -37
- package/src/components/TimelineToolbar.tsx +62 -55
- 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 +20 -41
- package/src/components/nle/usePreviewBlockDrop.ts +109 -0
- package/src/components/sidebar/BlocksTab.tsx +321 -122
- package/src/components/sidebar/LeftSidebar.tsx +58 -41
- package/src/components/ui/Tooltip.tsx +63 -0
- package/src/components/ui/index.ts +1 -0
- package/src/hooks/useBlockCatalog.ts +5 -1
- package/src/hooks/useDomEditSession.ts +19 -1
- package/src/hooks/useFileManager.ts +3 -0
- package/src/main.tsx +6 -0
- package/src/player/components/PlayerControls.tsx +253 -234
- package/src/player/lib/playbackAdapter.test.ts +3 -3
- package/src/player/lib/playbackAdapter.ts +3 -1
- package/src/utils/blockInstaller.ts +65 -32
- package/src/utils/timelineAssetDrop.test.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +8 -1
- package/dist/assets/index-BWBj8I6Q.css +0 -1
- package/dist/assets/index-Do0kAMcy.js +0 -115
|
@@ -13,6 +13,7 @@ import type { TimelineElement } from "../../player";
|
|
|
13
13
|
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
|
|
14
14
|
import { NLEPreview } from "./NLEPreview";
|
|
15
15
|
import { CompositionBreadcrumb } from "./CompositionBreadcrumb";
|
|
16
|
+
import { usePreviewBlockDrop } from "./usePreviewBlockDrop";
|
|
16
17
|
import { useCompositionStack } from "./useCompositionStack";
|
|
17
18
|
import {
|
|
18
19
|
TIMELINE_TOGGLE_SHORTCUT_LABEL,
|
|
@@ -54,6 +55,10 @@ interface NLELayoutProps {
|
|
|
54
55
|
blockName: string,
|
|
55
56
|
placement: Pick<TimelineElement, "start" | "track">,
|
|
56
57
|
) => Promise<void> | void;
|
|
58
|
+
onPreviewBlockDrop?: (
|
|
59
|
+
blockName: string,
|
|
60
|
+
position: { left: number; top: number },
|
|
61
|
+
) => Promise<void> | void;
|
|
57
62
|
/** Persist timeline move actions back into source HTML */
|
|
58
63
|
onMoveElement?: (
|
|
59
64
|
element: TimelineElement,
|
|
@@ -107,6 +112,7 @@ export const NLELayout = memo(function NLELayout({
|
|
|
107
112
|
onDeleteElement,
|
|
108
113
|
onAssetDrop,
|
|
109
114
|
onBlockDrop,
|
|
115
|
+
onPreviewBlockDrop,
|
|
110
116
|
onMoveElement,
|
|
111
117
|
onResizeElement,
|
|
112
118
|
onBlockedEditAttempt,
|
|
@@ -131,10 +137,26 @@ export const NLELayout = memo(function NLELayout({
|
|
|
131
137
|
usePlayerStore.getState().reset();
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
const stageRefForDrop = useRef<HTMLDivElement | null>(null);
|
|
141
|
+
const handleStageRef = useCallback((ref: React.RefObject<HTMLDivElement | null>) => {
|
|
142
|
+
stageRefForDrop.current = ref.current;
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
isDragOver: previewDragOver,
|
|
147
|
+
handleDragOver: handlePreviewDragOver,
|
|
148
|
+
handleDragLeave: handlePreviewDragLeave,
|
|
149
|
+
handleDrop: handlePreviewDrop,
|
|
150
|
+
} = usePreviewBlockDrop({
|
|
151
|
+
portrait,
|
|
152
|
+
stageRef: stageRefForDrop as React.RefObject<HTMLDivElement | null>,
|
|
153
|
+
onBlockDrop: onPreviewBlockDrop,
|
|
154
|
+
});
|
|
155
|
+
|
|
134
156
|
// Lightweight reload: change iframe src instead of destroying the Player.
|
|
135
157
|
// refreshPlayer() saves the seek position and appends a cache-busting _t
|
|
136
|
-
// param
|
|
137
|
-
//
|
|
158
|
+
// param — the Player instance stays alive so the adapter is available for
|
|
159
|
+
// saveSeekPosition() to read the current time before the reload.
|
|
138
160
|
const prevRefreshKeyRef = useRef(refreshKey);
|
|
139
161
|
useEffect(() => {
|
|
140
162
|
if (refreshKey === prevRefreshKeyRef.current) return;
|
|
@@ -344,7 +366,13 @@ export const NLELayout = memo(function NLELayout({
|
|
|
344
366
|
>
|
|
345
367
|
{/* Preview + player controls */}
|
|
346
368
|
<div className="flex-1 min-h-0 flex flex-col">
|
|
347
|
-
<div
|
|
369
|
+
<div
|
|
370
|
+
className="flex-1 min-h-0 relative"
|
|
371
|
+
data-preview-pan-surface="true"
|
|
372
|
+
onDragOver={handlePreviewDragOver}
|
|
373
|
+
onDragLeave={handlePreviewDragLeave}
|
|
374
|
+
onDrop={handlePreviewDrop}
|
|
375
|
+
>
|
|
348
376
|
<NLEPreview
|
|
349
377
|
projectId={projectId}
|
|
350
378
|
iframeRef={iframeRef}
|
|
@@ -352,9 +380,12 @@ export const NLELayout = memo(function NLELayout({
|
|
|
352
380
|
onCompositionLoadingChange={setCompositionLoading}
|
|
353
381
|
portrait={portrait}
|
|
354
382
|
directUrl={directUrl}
|
|
355
|
-
refreshKey={refreshKey}
|
|
356
383
|
suppressLoadingOverlay={hasLoadedOnceRef.current}
|
|
384
|
+
onStageRef={handleStageRef}
|
|
357
385
|
/>
|
|
386
|
+
{previewDragOver && (
|
|
387
|
+
<div className="absolute inset-2 z-40 rounded-lg border-2 border-dashed border-studio-accent/50 bg-studio-accent/[0.04] pointer-events-none" />
|
|
388
|
+
)}
|
|
358
389
|
{!isFullscreen && previewOverlay}
|
|
359
390
|
</div>
|
|
360
391
|
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
|
|
@@ -112,17 +112,9 @@ function renderPreview() {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
describe("getPreviewPlayerKey", () => {
|
|
115
|
-
it("
|
|
116
|
-
expect(
|
|
117
|
-
|
|
118
|
-
projectId: "timeline-edit-playground",
|
|
119
|
-
refreshKey: 1,
|
|
120
|
-
}),
|
|
121
|
-
).toBe(
|
|
122
|
-
getPreviewPlayerKey({
|
|
123
|
-
projectId: "timeline-edit-playground",
|
|
124
|
-
refreshKey: 2,
|
|
125
|
-
}),
|
|
115
|
+
it("uses projectId as key when no directUrl", () => {
|
|
116
|
+
expect(getPreviewPlayerKey({ projectId: "timeline-edit-playground" })).toBe(
|
|
117
|
+
"timeline-edit-playground",
|
|
126
118
|
);
|
|
127
119
|
});
|
|
128
120
|
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type PreviewZoomState,
|
|
13
13
|
} from "./previewZoom";
|
|
14
14
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/studioUiPreferences";
|
|
15
|
-
|
|
16
15
|
interface NLEPreviewProps {
|
|
17
16
|
projectId: string;
|
|
18
17
|
iframeRef: Ref<HTMLIFrameElement>;
|
|
@@ -20,8 +19,8 @@ interface NLEPreviewProps {
|
|
|
20
19
|
onCompositionLoadingChange?: (loading: boolean) => void;
|
|
21
20
|
portrait?: boolean;
|
|
22
21
|
directUrl?: string;
|
|
23
|
-
refreshKey?: number;
|
|
24
22
|
suppressLoadingOverlay?: boolean;
|
|
23
|
+
onStageRef?: (ref: React.RefObject<HTMLDivElement | null>) => void;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export function getPreviewPlayerKey({
|
|
@@ -30,7 +29,6 @@ export function getPreviewPlayerKey({
|
|
|
30
29
|
}: {
|
|
31
30
|
projectId: string;
|
|
32
31
|
directUrl?: string;
|
|
33
|
-
refreshKey?: number;
|
|
34
32
|
}): string {
|
|
35
33
|
return directUrl ?? projectId;
|
|
36
34
|
}
|
|
@@ -91,16 +89,16 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
91
89
|
onCompositionLoadingChange,
|
|
92
90
|
portrait,
|
|
93
91
|
directUrl,
|
|
94
|
-
refreshKey,
|
|
95
92
|
suppressLoadingOverlay,
|
|
93
|
+
onStageRef,
|
|
96
94
|
}: NLEPreviewProps) {
|
|
97
|
-
const
|
|
98
|
-
const prevRefreshKeyRef = useRef(refreshKey);
|
|
95
|
+
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
|
|
99
96
|
const viewportRef = useRef<HTMLDivElement>(null);
|
|
100
97
|
const stageRef = useRef<HTMLDivElement>(null);
|
|
101
|
-
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
onStageRef?.(stageRef);
|
|
100
|
+
}, [onStageRef]);
|
|
102
101
|
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, portrait));
|
|
103
|
-
const retiringTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
104
102
|
|
|
105
103
|
const zoomRef = useRef<PreviewZoomState>(loadInitialZoom());
|
|
106
104
|
const [settledZoom, setSettledZoom] = useState<PreviewZoomState>(() => zoomRef.current);
|
|
@@ -120,7 +118,6 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
120
118
|
return () => {
|
|
121
119
|
if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
|
|
122
120
|
if (hudTimerRef.current) clearTimeout(hudTimerRef.current);
|
|
123
|
-
if (retiringTimerRef.current) clearTimeout(retiringTimerRef.current);
|
|
124
121
|
};
|
|
125
122
|
}, []);
|
|
126
123
|
|
|
@@ -205,14 +202,6 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
205
202
|
[applyTransform],
|
|
206
203
|
);
|
|
207
204
|
|
|
208
|
-
if (refreshKey !== prevRefreshKeyRef.current) {
|
|
209
|
-
const oldKey = `${baseKey}:${prevRefreshKeyRef.current ?? 0}`;
|
|
210
|
-
prevRefreshKeyRef.current = refreshKey;
|
|
211
|
-
setRetiringKey(oldKey);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const activeKey = `${baseKey}:${refreshKey ?? 0}`;
|
|
215
|
-
|
|
216
205
|
const applyInitialZoom = useCallback(() => {
|
|
217
206
|
const z = zoomRef.current;
|
|
218
207
|
if (Math.abs(z.zoomPercent - 100) > 0.5 || Math.abs(z.panX) > 0.1 || Math.abs(z.panY) > 0.1) {
|
|
@@ -220,16 +209,6 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
220
209
|
}
|
|
221
210
|
}, [writeTransform]);
|
|
222
211
|
|
|
223
|
-
const handleNewPlayerLoad = () => {
|
|
224
|
-
onIframeLoad();
|
|
225
|
-
applyInitialZoom();
|
|
226
|
-
if (retiringTimerRef.current) clearTimeout(retiringTimerRef.current);
|
|
227
|
-
retiringTimerRef.current = setTimeout(() => {
|
|
228
|
-
setRetiringKey(null);
|
|
229
|
-
retiringTimerRef.current = null;
|
|
230
|
-
}, 160);
|
|
231
|
-
};
|
|
232
|
-
|
|
233
212
|
useEffect(() => {
|
|
234
213
|
const viewport = viewportRef.current;
|
|
235
214
|
if (!viewport) return;
|
|
@@ -412,14 +391,14 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
412
391
|
}}
|
|
413
392
|
data-testid="preview-zoom-stage"
|
|
414
393
|
>
|
|
415
|
-
{
|
|
394
|
+
{directUrl?.includes("/components/") && (
|
|
416
395
|
<Player
|
|
417
|
-
key={
|
|
418
|
-
projectId={
|
|
419
|
-
directUrl={directUrl}
|
|
396
|
+
key={`backdrop-${projectId}`}
|
|
397
|
+
projectId={projectId}
|
|
420
398
|
onLoad={() => {}}
|
|
421
399
|
portrait={portrait}
|
|
422
|
-
|
|
400
|
+
suppressLoadingOverlay
|
|
401
|
+
style={{ position: "absolute", inset: 0, zIndex: 0 }}
|
|
423
402
|
/>
|
|
424
403
|
)}
|
|
425
404
|
<Player
|
|
@@ -427,18 +406,18 @@ export const NLEPreview = memo(function NLEPreview({
|
|
|
427
406
|
ref={iframeRef}
|
|
428
407
|
projectId={directUrl ? undefined : projectId}
|
|
429
408
|
directUrl={directUrl}
|
|
430
|
-
onLoad={
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
onIframeLoad();
|
|
435
|
-
applyInitialZoom();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
409
|
+
onLoad={() => {
|
|
410
|
+
onIframeLoad();
|
|
411
|
+
applyInitialZoom();
|
|
412
|
+
}}
|
|
438
413
|
onCompositionLoadingChange={onCompositionLoadingChange}
|
|
439
414
|
portrait={portrait}
|
|
440
|
-
style={retiringKey ? { position: "absolute", inset: 0, zIndex: 1 } : undefined}
|
|
441
415
|
suppressLoadingOverlay={suppressLoadingOverlay}
|
|
416
|
+
style={
|
|
417
|
+
directUrl?.includes("/components/")
|
|
418
|
+
? { position: "absolute", inset: 0, zIndex: 1 }
|
|
419
|
+
: undefined
|
|
420
|
+
}
|
|
442
421
|
/>
|
|
443
422
|
</div>
|
|
444
423
|
</div>
|
|
@@ -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
|
+
}
|