@hyperframes/studio 0.6.0-alpha.8 → 0.6.0
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-DOFETgjy.js} +1 -1
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-DUqUmaoH.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +428 -4299
- package/src/components/AskAgentModal.tsx +120 -0
- package/src/components/StudioHeader.tsx +133 -0
- package/src/components/StudioLeftSidebar.tsx +125 -0
- package/src/components/StudioPreviewArea.tsx +163 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +15 -1
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- 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/editor/manualEdits.ts +32 -0
- package/src/components/editor/propertyPanelColor.tsx +371 -0
- package/src/components/editor/propertyPanelFill.tsx +421 -0
- package/src/components/editor/propertyPanelFont.tsx +455 -0
- package/src/components/editor/propertyPanelHelpers.ts +401 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
- package/src/components/editor/propertyPanelSections.tsx +453 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
- package/src/components/nle/NLELayout.tsx +8 -11
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/renders/RenderQueue.tsx +102 -31
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/contexts/DomEditContext.tsx +137 -0
- package/src/contexts/FileManagerContext.tsx +110 -0
- package/src/contexts/PanelLayoutContext.tsx +68 -0
- package/src/contexts/StudioContext.tsx +135 -0
- package/src/hooks/useAppHotkeys.ts +326 -0
- package/src/hooks/useAskAgentModal.ts +162 -0
- package/src/hooks/useCaptionDetection.ts +132 -0
- package/src/hooks/useCompositionDimensions.ts +25 -0
- package/src/hooks/useConsoleErrorCapture.ts +60 -0
- package/src/hooks/useDomEditCommits.ts +437 -0
- package/src/hooks/useDomEditSession.ts +342 -0
- package/src/hooks/useDomEditTextCommits.ts +330 -0
- package/src/hooks/useDomSelection.ts +398 -0
- package/src/hooks/useFileManager.ts +431 -0
- package/src/hooks/useFrameCapture.ts +77 -0
- package/src/hooks/useLintModal.ts +35 -0
- package/src/hooks/useManifestPersistence.ts +492 -0
- package/src/hooks/usePanelLayout.ts +68 -0
- package/src/hooks/usePreviewInteraction.ts +153 -0
- package/src/hooks/useRenderClipContent.ts +124 -0
- package/src/hooks/useTimelineEditing.ts +472 -0
- package/src/player/components/Player.tsx +35 -4
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +10 -103
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/hooks/useTimelinePlayer.ts +140 -103
- package/src/utils/domEditHelpers.ts +50 -0
- package/src/utils/studioFontHelpers.ts +83 -0
- package/src/utils/studioHelpers.ts +214 -0
- package/src/utils/studioPreviewHelpers.ts +185 -0
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-ClYcrksa.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState, useRef, type CSSProperties } from "react";
|
|
2
|
+
import { useMountEffect } from "../hooks/useMountEffect";
|
|
3
|
+
import { type AgentModalAnchorPoint, clampNumber } from "../utils/studioHelpers";
|
|
4
|
+
|
|
5
|
+
function getAgentModalPositionStyle(
|
|
6
|
+
anchorPoint: AgentModalAnchorPoint | null,
|
|
7
|
+
): CSSProperties | undefined {
|
|
8
|
+
if (!anchorPoint || typeof window === "undefined") return undefined;
|
|
9
|
+
|
|
10
|
+
const modalWidth = 480;
|
|
11
|
+
const estimatedModalHeight = 270;
|
|
12
|
+
const margin = 16;
|
|
13
|
+
const left = clampNumber(
|
|
14
|
+
anchorPoint.x,
|
|
15
|
+
margin + modalWidth / 2,
|
|
16
|
+
window.innerWidth - margin - modalWidth / 2,
|
|
17
|
+
);
|
|
18
|
+
const top = clampNumber(
|
|
19
|
+
anchorPoint.y + 12,
|
|
20
|
+
margin,
|
|
21
|
+
window.innerHeight - margin - estimatedModalHeight,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return { left, top, transform: "translateX(-50%)" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AskAgentModal({
|
|
28
|
+
selectionLabel,
|
|
29
|
+
anchorPoint = null,
|
|
30
|
+
onSubmit,
|
|
31
|
+
onClose,
|
|
32
|
+
}: {
|
|
33
|
+
selectionLabel: string;
|
|
34
|
+
anchorPoint?: AgentModalAnchorPoint | null;
|
|
35
|
+
onSubmit: (instruction: string) => void;
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
}) {
|
|
38
|
+
const [value, setValue] = useState("");
|
|
39
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
40
|
+
const modalPositionStyle = getAgentModalPositionStyle(anchorPoint);
|
|
41
|
+
|
|
42
|
+
useMountEffect(() => {
|
|
43
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const handleSubmit = () => {
|
|
47
|
+
if (!value.trim()) return;
|
|
48
|
+
onSubmit(value.trim());
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={
|
|
54
|
+
anchorPoint
|
|
55
|
+
? "fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm"
|
|
56
|
+
: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
57
|
+
}
|
|
58
|
+
onClick={onClose}
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
className={`w-[480px] rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl ${
|
|
62
|
+
anchorPoint ? "fixed" : ""
|
|
63
|
+
}`}
|
|
64
|
+
style={modalPositionStyle}
|
|
65
|
+
onClick={(e) => e.stopPropagation()}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-800/60">
|
|
68
|
+
<div>
|
|
69
|
+
<h3 className="text-sm font-medium text-neutral-200">Ask agent</h3>
|
|
70
|
+
<p className="text-xs text-neutral-500 mt-0.5">
|
|
71
|
+
{selectionLabel.length > 50 ? `${selectionLabel.slice(0, 49)}…` : selectionLabel}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<button
|
|
75
|
+
className="p-1 rounded-md text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800/50"
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
>
|
|
78
|
+
<svg
|
|
79
|
+
width="14"
|
|
80
|
+
height="14"
|
|
81
|
+
viewBox="0 0 24 24"
|
|
82
|
+
fill="none"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth="2"
|
|
85
|
+
strokeLinecap="round"
|
|
86
|
+
>
|
|
87
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
88
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="px-5 py-4">
|
|
93
|
+
<textarea
|
|
94
|
+
ref={inputRef}
|
|
95
|
+
className="w-full h-24 px-3 py-2 rounded-lg border border-neutral-800 bg-neutral-900/60 text-sm text-neutral-200 placeholder-neutral-600 resize-none focus:outline-none focus:border-studio-accent/60 focus:ring-1 focus:ring-studio-accent/30"
|
|
96
|
+
placeholder="Describe what you want to change…"
|
|
97
|
+
value={value}
|
|
98
|
+
onChange={(e) => setValue(e.target.value)}
|
|
99
|
+
onKeyDown={(e) => {
|
|
100
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit();
|
|
101
|
+
if (e.key === "Escape") onClose();
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex items-center justify-between px-5 py-3 border-t border-neutral-800/60">
|
|
106
|
+
<span className="text-[11px] text-neutral-600">
|
|
107
|
+
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to copy
|
|
108
|
+
</span>
|
|
109
|
+
<button
|
|
110
|
+
className="px-4 py-1.5 rounded-lg bg-studio-accent/90 text-xs font-medium text-neutral-950 hover:bg-studio-accent disabled:opacity-40 disabled:cursor-not-allowed"
|
|
111
|
+
disabled={!value.trim()}
|
|
112
|
+
onClick={handleSubmit}
|
|
113
|
+
>
|
|
114
|
+
Copy prompt
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { MouseEvent } from "react";
|
|
2
|
+
import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
|
|
3
|
+
import {
|
|
4
|
+
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
5
|
+
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
|
|
6
|
+
} from "./editor/manualEditingAvailability";
|
|
7
|
+
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
8
|
+
import { useStudioContext } from "../contexts/StudioContext";
|
|
9
|
+
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
10
|
+
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
11
|
+
|
|
12
|
+
export interface StudioHeaderProps {
|
|
13
|
+
captureFrameHref: string;
|
|
14
|
+
captureFrameFilename: string;
|
|
15
|
+
handleCaptureFrameClick: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
16
|
+
refreshCaptureFrameTime: () => void;
|
|
17
|
+
inspectorButtonActive: boolean;
|
|
18
|
+
inspectorPanelActive: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function StudioHeader({
|
|
22
|
+
captureFrameHref,
|
|
23
|
+
captureFrameFilename,
|
|
24
|
+
handleCaptureFrameClick,
|
|
25
|
+
refreshCaptureFrameTime,
|
|
26
|
+
inspectorButtonActive,
|
|
27
|
+
inspectorPanelActive,
|
|
28
|
+
}: StudioHeaderProps) {
|
|
29
|
+
const { projectId, editHistory, handleUndo, handleRedo } = useStudioContext();
|
|
30
|
+
const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
31
|
+
const { clearDomSelection } = useDomEditContext();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-center justify-between h-10 px-3 bg-neutral-900 border-b border-neutral-800 flex-shrink-0">
|
|
35
|
+
{/* Left: project name */}
|
|
36
|
+
<div className="flex items-center gap-2">
|
|
37
|
+
<span className="text-[11px] font-medium text-neutral-400">{projectId}</span>
|
|
38
|
+
</div>
|
|
39
|
+
{/* Right: toolbar buttons */}
|
|
40
|
+
<div className="flex items-center gap-1.5">
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={() => void handleUndo()}
|
|
44
|
+
disabled={!editHistory.canUndo}
|
|
45
|
+
className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${
|
|
46
|
+
editHistory.canUndo
|
|
47
|
+
? "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800"
|
|
48
|
+
: "border-neutral-900 text-neutral-700"
|
|
49
|
+
}`}
|
|
50
|
+
title={
|
|
51
|
+
editHistory.undoLabel
|
|
52
|
+
? `Undo ${editHistory.undoLabel} (${getHistoryShortcutLabel("undo")})`
|
|
53
|
+
: `Undo (${getHistoryShortcutLabel("undo")})`
|
|
54
|
+
}
|
|
55
|
+
aria-label="Undo"
|
|
56
|
+
>
|
|
57
|
+
<RotateCcw size={14} />
|
|
58
|
+
</button>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => void handleRedo()}
|
|
62
|
+
disabled={!editHistory.canRedo}
|
|
63
|
+
className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${
|
|
64
|
+
editHistory.canRedo
|
|
65
|
+
? "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800"
|
|
66
|
+
: "border-neutral-900 text-neutral-700"
|
|
67
|
+
}`}
|
|
68
|
+
title={
|
|
69
|
+
editHistory.redoLabel
|
|
70
|
+
? `Redo ${editHistory.redoLabel} (${getHistoryShortcutLabel("redo")})`
|
|
71
|
+
: `Redo (${getHistoryShortcutLabel("redo")})`
|
|
72
|
+
}
|
|
73
|
+
aria-label="Redo"
|
|
74
|
+
>
|
|
75
|
+
<RotateCw size={14} />
|
|
76
|
+
</button>
|
|
77
|
+
<a
|
|
78
|
+
href={captureFrameHref}
|
|
79
|
+
download={captureFrameFilename}
|
|
80
|
+
onClick={handleCaptureFrameClick}
|
|
81
|
+
onFocus={refreshCaptureFrameTime}
|
|
82
|
+
onPointerDown={refreshCaptureFrameTime}
|
|
83
|
+
className="h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border border-neutral-700 text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800"
|
|
84
|
+
title="Capture current frame"
|
|
85
|
+
aria-label="Capture current frame"
|
|
86
|
+
>
|
|
87
|
+
<Camera size={14} />
|
|
88
|
+
<span>Capture</span>
|
|
89
|
+
</a>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={() => {
|
|
93
|
+
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
94
|
+
if (rightCollapsed || !inspectorPanelActive) {
|
|
95
|
+
setRightPanelTab("design");
|
|
96
|
+
setRightCollapsed(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
clearDomSelection();
|
|
100
|
+
setRightCollapsed(true);
|
|
101
|
+
}}
|
|
102
|
+
disabled={!STUDIO_INSPECTOR_PANELS_ENABLED}
|
|
103
|
+
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
|
|
104
|
+
inspectorButtonActive
|
|
105
|
+
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
106
|
+
: STUDIO_INSPECTOR_PANELS_ENABLED
|
|
107
|
+
? "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
|
|
108
|
+
: "cursor-not-allowed border-transparent text-neutral-700"
|
|
109
|
+
}`}
|
|
110
|
+
title={
|
|
111
|
+
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
|
|
112
|
+
}
|
|
113
|
+
aria-label={
|
|
114
|
+
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
<svg
|
|
118
|
+
width="12"
|
|
119
|
+
height="12"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
fill="none"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
strokeWidth="2"
|
|
124
|
+
>
|
|
125
|
+
<circle cx="12" cy="12" r="10" />
|
|
126
|
+
<polygon points="10 8 16 12 10 16" fill="currentColor" stroke="none" />
|
|
127
|
+
</svg>
|
|
128
|
+
Inspector
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import { SourceEditor } from "./editor/SourceEditor";
|
|
3
|
+
import { LeftSidebar, type LeftSidebarHandle } from "./sidebar/LeftSidebar";
|
|
4
|
+
import { MediaPreview } from "./MediaPreview";
|
|
5
|
+
import { isMediaFile } from "../utils/mediaTypes";
|
|
6
|
+
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
7
|
+
import { useStudioContext } from "../contexts/StudioContext";
|
|
8
|
+
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
9
|
+
|
|
10
|
+
export interface StudioLeftSidebarProps {
|
|
11
|
+
leftSidebarRef: RefObject<LeftSidebarHandle | null>;
|
|
12
|
+
onSelectComposition: (comp: string) => void;
|
|
13
|
+
onLint: () => void;
|
|
14
|
+
linting: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function StudioLeftSidebar({
|
|
18
|
+
leftSidebarRef,
|
|
19
|
+
onSelectComposition,
|
|
20
|
+
onLint,
|
|
21
|
+
linting,
|
|
22
|
+
}: StudioLeftSidebarProps) {
|
|
23
|
+
const {
|
|
24
|
+
leftCollapsed,
|
|
25
|
+
leftWidth,
|
|
26
|
+
toggleLeftSidebar,
|
|
27
|
+
handlePanelResizeStart,
|
|
28
|
+
handlePanelResizeMove,
|
|
29
|
+
handlePanelResizeEnd,
|
|
30
|
+
} = usePanelLayoutContext();
|
|
31
|
+
const { projectId } = useStudioContext();
|
|
32
|
+
const {
|
|
33
|
+
compositions,
|
|
34
|
+
assets,
|
|
35
|
+
editingFile,
|
|
36
|
+
fileTree,
|
|
37
|
+
handleFileSelect,
|
|
38
|
+
handleCreateFile,
|
|
39
|
+
handleCreateFolder,
|
|
40
|
+
handleDeleteFile,
|
|
41
|
+
handleRenameFile,
|
|
42
|
+
handleDuplicateFile,
|
|
43
|
+
handleMoveFile,
|
|
44
|
+
handleImportFiles,
|
|
45
|
+
handleContentChange,
|
|
46
|
+
} = useFileManagerContext();
|
|
47
|
+
|
|
48
|
+
if (leftCollapsed) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={toggleLeftSidebar}
|
|
54
|
+
className="flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
|
55
|
+
title="Show sidebar"
|
|
56
|
+
aria-label="Show sidebar"
|
|
57
|
+
>
|
|
58
|
+
<svg
|
|
59
|
+
width="14"
|
|
60
|
+
height="14"
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeWidth="1.5"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
strokeLinejoin="round"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
>
|
|
69
|
+
<path d="M5 4v16" />
|
|
70
|
+
<path d="m10 7 5 5-5 5" />
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<LeftSidebar
|
|
80
|
+
ref={leftSidebarRef}
|
|
81
|
+
width={leftWidth}
|
|
82
|
+
projectId={projectId}
|
|
83
|
+
compositions={compositions}
|
|
84
|
+
assets={assets}
|
|
85
|
+
activeComposition={editingFile?.path ?? null}
|
|
86
|
+
onSelectComposition={onSelectComposition}
|
|
87
|
+
fileTree={fileTree}
|
|
88
|
+
editingFile={editingFile}
|
|
89
|
+
onSelectFile={handleFileSelect}
|
|
90
|
+
onCreateFile={handleCreateFile}
|
|
91
|
+
onCreateFolder={handleCreateFolder}
|
|
92
|
+
onDeleteFile={handleDeleteFile}
|
|
93
|
+
onRenameFile={handleRenameFile}
|
|
94
|
+
onDuplicateFile={handleDuplicateFile}
|
|
95
|
+
onMoveFile={handleMoveFile}
|
|
96
|
+
onImportFiles={handleImportFiles}
|
|
97
|
+
codeChildren={
|
|
98
|
+
editingFile ? (
|
|
99
|
+
isMediaFile(editingFile.path) ? (
|
|
100
|
+
<MediaPreview projectId={projectId} filePath={editingFile.path} />
|
|
101
|
+
) : (
|
|
102
|
+
<SourceEditor
|
|
103
|
+
content={editingFile.content ?? ""}
|
|
104
|
+
filePath={editingFile.path}
|
|
105
|
+
onChange={handleContentChange}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
) : undefined
|
|
109
|
+
}
|
|
110
|
+
onLint={onLint}
|
|
111
|
+
linting={linting}
|
|
112
|
+
onToggleCollapse={toggleLeftSidebar}
|
|
113
|
+
/>
|
|
114
|
+
<div
|
|
115
|
+
className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center"
|
|
116
|
+
style={{ touchAction: "none" }}
|
|
117
|
+
onPointerDown={(e) => handlePanelResizeStart("left", e)}
|
|
118
|
+
onPointerMove={handlePanelResizeMove}
|
|
119
|
+
onPointerUp={handlePanelResizeEnd}
|
|
120
|
+
>
|
|
121
|
+
<div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
|
122
|
+
</div>
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { NLELayout } from "./nle/NLELayout";
|
|
3
|
+
import { CaptionOverlay } from "../captions/components/CaptionOverlay";
|
|
4
|
+
import { CaptionTimeline } from "../captions/components/CaptionTimeline";
|
|
5
|
+
import { DomEditOverlay } from "./editor/DomEditOverlay";
|
|
6
|
+
import type { TimelineElement } from "../player";
|
|
7
|
+
import type { BlockedTimelineEditIntent } from "../player/components/timelineEditing";
|
|
8
|
+
import {
|
|
9
|
+
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
10
|
+
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
|
|
11
|
+
STUDIO_PREVIEW_SELECTION_ENABLED,
|
|
12
|
+
} from "./editor/manualEditingAvailability";
|
|
13
|
+
import { useStudioContext } from "../contexts/StudioContext";
|
|
14
|
+
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
15
|
+
|
|
16
|
+
export interface StudioPreviewAreaProps {
|
|
17
|
+
timelineToolbar: ReactNode;
|
|
18
|
+
renderClipContent: (
|
|
19
|
+
element: TimelineElement,
|
|
20
|
+
style: { clip: string; label: string },
|
|
21
|
+
) => ReactNode;
|
|
22
|
+
// Timeline editing
|
|
23
|
+
handleTimelineElementDelete: (element: TimelineElement) => Promise<void> | void;
|
|
24
|
+
handleTimelineAssetDrop: (
|
|
25
|
+
assetPath: string,
|
|
26
|
+
placement: Pick<TimelineElement, "start" | "track">,
|
|
27
|
+
) => Promise<void> | void;
|
|
28
|
+
handleTimelineFileDrop: (
|
|
29
|
+
files: File[],
|
|
30
|
+
placement?: Pick<TimelineElement, "start" | "track">,
|
|
31
|
+
) => Promise<void> | void;
|
|
32
|
+
handleTimelineElementMove: (
|
|
33
|
+
element: TimelineElement,
|
|
34
|
+
updates: Pick<TimelineElement, "start" | "track">,
|
|
35
|
+
) => Promise<void> | void;
|
|
36
|
+
handleTimelineElementResize: (
|
|
37
|
+
element: TimelineElement,
|
|
38
|
+
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
39
|
+
) => Promise<void> | void;
|
|
40
|
+
handleBlockedTimelineEdit: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
41
|
+
setCompIdToSrc: (map: Map<string, string>) => void;
|
|
42
|
+
setCompositionLoading: (loading: boolean) => void;
|
|
43
|
+
shouldShowSelectedDomBounds: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function StudioPreviewArea({
|
|
47
|
+
timelineToolbar,
|
|
48
|
+
renderClipContent,
|
|
49
|
+
handleTimelineElementDelete,
|
|
50
|
+
handleTimelineAssetDrop,
|
|
51
|
+
handleTimelineFileDrop,
|
|
52
|
+
handleTimelineElementMove,
|
|
53
|
+
handleTimelineElementResize,
|
|
54
|
+
handleBlockedTimelineEdit,
|
|
55
|
+
setCompIdToSrc,
|
|
56
|
+
setCompositionLoading,
|
|
57
|
+
shouldShowSelectedDomBounds,
|
|
58
|
+
}: StudioPreviewAreaProps) {
|
|
59
|
+
const {
|
|
60
|
+
projectId,
|
|
61
|
+
refreshKey,
|
|
62
|
+
activeCompPath,
|
|
63
|
+
setActiveCompPath,
|
|
64
|
+
captionEditMode,
|
|
65
|
+
compositionLoading,
|
|
66
|
+
isPlaying,
|
|
67
|
+
previewIframeRef,
|
|
68
|
+
refreshPreviewDocumentVersion,
|
|
69
|
+
handlePreviewIframeRef,
|
|
70
|
+
timelineVisible,
|
|
71
|
+
toggleTimelineVisibility,
|
|
72
|
+
} = useStudioContext();
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
domEditHoverSelection,
|
|
76
|
+
domEditSelection,
|
|
77
|
+
domEditGroupSelections,
|
|
78
|
+
handleTimelineElementSelect,
|
|
79
|
+
handlePreviewCanvasMouseDown,
|
|
80
|
+
handlePreviewCanvasPointerMove,
|
|
81
|
+
handlePreviewCanvasPointerLeave,
|
|
82
|
+
applyDomSelection,
|
|
83
|
+
handleBlockedDomMove,
|
|
84
|
+
handleDomManualDragStart,
|
|
85
|
+
handleDomPathOffsetCommit,
|
|
86
|
+
handleDomGroupPathOffsetCommit,
|
|
87
|
+
handleDomBoxSizeCommit,
|
|
88
|
+
handleDomRotationCommit,
|
|
89
|
+
} = useDomEditContext();
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex-1 relative min-w-0">
|
|
93
|
+
<NLELayout
|
|
94
|
+
projectId={projectId}
|
|
95
|
+
refreshKey={refreshKey}
|
|
96
|
+
activeCompositionPath={activeCompPath}
|
|
97
|
+
timelineToolbar={timelineToolbar}
|
|
98
|
+
renderClipContent={renderClipContent}
|
|
99
|
+
onDeleteElement={handleTimelineElementDelete}
|
|
100
|
+
onAssetDrop={handleTimelineAssetDrop}
|
|
101
|
+
onFileDrop={handleTimelineFileDrop}
|
|
102
|
+
onMoveElement={handleTimelineElementMove}
|
|
103
|
+
onResizeElement={handleTimelineElementResize}
|
|
104
|
+
onBlockedEditAttempt={handleBlockedTimelineEdit}
|
|
105
|
+
onSelectTimelineElement={handleTimelineElementSelect}
|
|
106
|
+
onCompIdToSrcChange={setCompIdToSrc}
|
|
107
|
+
onCompositionLoadingChange={setCompositionLoading}
|
|
108
|
+
onCompositionChange={(compPath) => {
|
|
109
|
+
// Sync activeCompPath when user drills down via timeline double-click
|
|
110
|
+
// or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
|
|
111
|
+
setActiveCompPath(compPath);
|
|
112
|
+
refreshPreviewDocumentVersion();
|
|
113
|
+
}}
|
|
114
|
+
onIframeRef={handlePreviewIframeRef}
|
|
115
|
+
previewOverlay={
|
|
116
|
+
captionEditMode ? (
|
|
117
|
+
<CaptionOverlay iframeRef={previewIframeRef} />
|
|
118
|
+
) : STUDIO_INSPECTOR_PANELS_ENABLED ? (
|
|
119
|
+
<DomEditOverlay
|
|
120
|
+
iframeRef={previewIframeRef}
|
|
121
|
+
activeCompositionPath={activeCompPath}
|
|
122
|
+
hoverSelection={
|
|
123
|
+
STUDIO_PREVIEW_SELECTION_ENABLED &&
|
|
124
|
+
!captionEditMode &&
|
|
125
|
+
!compositionLoading &&
|
|
126
|
+
!isPlaying
|
|
127
|
+
? domEditHoverSelection
|
|
128
|
+
: null
|
|
129
|
+
}
|
|
130
|
+
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
131
|
+
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
|
132
|
+
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED}
|
|
133
|
+
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
|
134
|
+
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
|
135
|
+
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
|
136
|
+
onSelectionChange={applyDomSelection}
|
|
137
|
+
onBlockedMove={handleBlockedDomMove}
|
|
138
|
+
onManualDragStart={handleDomManualDragStart}
|
|
139
|
+
onPathOffsetCommit={handleDomPathOffsetCommit}
|
|
140
|
+
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
|
141
|
+
onBoxSizeCommit={handleDomBoxSizeCommit}
|
|
142
|
+
onRotationCommit={handleDomRotationCommit}
|
|
143
|
+
/>
|
|
144
|
+
) : null
|
|
145
|
+
}
|
|
146
|
+
timelineFooter={
|
|
147
|
+
captionEditMode ? (
|
|
148
|
+
<div className="border-t border-neutral-800/30 flex-shrink-0" style={{ height: 60 }}>
|
|
149
|
+
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
150
|
+
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
151
|
+
Captions
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
<CaptionTimeline pixelsPerSecond={100} />
|
|
155
|
+
</div>
|
|
156
|
+
) : undefined
|
|
157
|
+
}
|
|
158
|
+
timelineVisible={timelineVisible}
|
|
159
|
+
onToggleTimeline={toggleTimelineVisibility}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|