@hyperframes/studio 0.6.0-alpha.9 → 0.6.1
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-CzwFysqv.js +418 -0
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-hYc4aP7M.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +421 -4303
- package/src/captions/components/CaptionOverlay.tsx +13 -246
- package/src/captions/components/CaptionOverlayUtils.ts +221 -0
- 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 +167 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +88 -993
- package/src/components/editor/EaseCurveEditor.tsx +221 -0
- package/src/components/editor/FileTree.tsx +13 -621
- package/src/components/editor/FileTreeIcons.tsx +128 -0
- package/src/components/editor/FileTreeNodes.tsx +496 -0
- package/src/components/editor/MotionPanel.tsx +16 -390
- package/src/components/editor/MotionPanelFields.tsx +185 -0
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- package/src/components/editor/domEditOverlayGeometry.ts +211 -0
- package/src/components/editor/domEditOverlayGestures.ts +138 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
- package/src/components/editor/domEditing.ts +44 -1117
- package/src/components/editor/domEditingAgentPrompt.ts +97 -0
- package/src/components/editor/domEditingDom.ts +266 -0
- package/src/components/editor/domEditingElement.ts +329 -0
- package/src/components/editor/domEditingLayers.ts +460 -0
- package/src/components/editor/domEditingTypes.ts +125 -0
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +84 -1049
- package/src/components/editor/manualEditsDom.ts +436 -0
- package/src/components/editor/manualEditsParsing.ts +280 -0
- package/src/components/editor/manualEditsSnapshot.ts +333 -0
- package/src/components/editor/manualEditsTypes.ts +141 -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/editor/studioMotion.ts +47 -434
- package/src/components/editor/studioMotionOps.ts +299 -0
- package/src/components/editor/studioMotionTypes.ts +168 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
- package/src/components/editor/useDomEditOverlayRects.ts +207 -0
- package/src/components/nle/NLELayout.tsx +68 -155
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/nle/useCompositionStack.ts +126 -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/hooks/useToast.ts +20 -0
- package/src/player/components/Player.tsx +33 -2
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +196 -1518
- package/src/player/components/TimelineCanvas.tsx +434 -0
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/components/TimelineEmptyState.tsx +102 -0
- package/src/player/components/TimelineRuler.tsx +90 -0
- package/src/player/components/timelineIcons.tsx +49 -0
- package/src/player/components/timelineLayout.ts +215 -0
- package/src/player/components/timelineUtils.ts +211 -0
- package/src/player/components/useTimelineClipDrag.ts +388 -0
- package/src/player/components/useTimelinePlayhead.ts +200 -0
- package/src/player/components/useTimelineRangeSelection.ts +135 -0
- package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
- package/src/player/hooks/useTimelinePlayer.ts +105 -1371
- package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
- package/src/player/lib/playbackAdapter.ts +145 -0
- package/src/player/lib/playbackShortcuts.ts +68 -0
- package/src/player/lib/playbackTypes.ts +60 -0
- package/src/player/lib/timelineDOM.ts +373 -0
- package/src/player/lib/timelineElementHelpers.ts +303 -0
- package/src/player/lib/timelineIframeHelpers.ts +269 -0
- 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/hyperframes-player-DjsVzYFP.js +0 -418
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-DYCiFGWQ.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -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,167 @@
|
|
|
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
|
+
// Guard against no-op updates to prevent circular refresh cascades
|
|
112
|
+
// between activeCompPath → compositionStack → onCompositionChange.
|
|
113
|
+
if (compPath !== activeCompPath) {
|
|
114
|
+
setActiveCompPath(compPath);
|
|
115
|
+
refreshPreviewDocumentVersion();
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
onIframeRef={handlePreviewIframeRef}
|
|
119
|
+
previewOverlay={
|
|
120
|
+
captionEditMode ? (
|
|
121
|
+
<CaptionOverlay iframeRef={previewIframeRef} />
|
|
122
|
+
) : STUDIO_INSPECTOR_PANELS_ENABLED ? (
|
|
123
|
+
<DomEditOverlay
|
|
124
|
+
iframeRef={previewIframeRef}
|
|
125
|
+
activeCompositionPath={activeCompPath}
|
|
126
|
+
hoverSelection={
|
|
127
|
+
STUDIO_PREVIEW_SELECTION_ENABLED &&
|
|
128
|
+
!captionEditMode &&
|
|
129
|
+
!compositionLoading &&
|
|
130
|
+
!isPlaying
|
|
131
|
+
? domEditHoverSelection
|
|
132
|
+
: null
|
|
133
|
+
}
|
|
134
|
+
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
|
|
135
|
+
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
|
|
136
|
+
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED}
|
|
137
|
+
onCanvasMouseDown={handlePreviewCanvasMouseDown}
|
|
138
|
+
onCanvasPointerMove={handlePreviewCanvasPointerMove}
|
|
139
|
+
onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
|
|
140
|
+
onSelectionChange={applyDomSelection}
|
|
141
|
+
onBlockedMove={handleBlockedDomMove}
|
|
142
|
+
onManualDragStart={handleDomManualDragStart}
|
|
143
|
+
onPathOffsetCommit={handleDomPathOffsetCommit}
|
|
144
|
+
onGroupPathOffsetCommit={handleDomGroupPathOffsetCommit}
|
|
145
|
+
onBoxSizeCommit={handleDomBoxSizeCommit}
|
|
146
|
+
onRotationCommit={handleDomRotationCommit}
|
|
147
|
+
/>
|
|
148
|
+
) : null
|
|
149
|
+
}
|
|
150
|
+
timelineFooter={
|
|
151
|
+
captionEditMode ? (
|
|
152
|
+
<div className="border-t border-neutral-800/30 flex-shrink-0" style={{ height: 60 }}>
|
|
153
|
+
<div className="flex items-center gap-1.5 px-2 py-0.5">
|
|
154
|
+
<span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
|
|
155
|
+
Captions
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
<CaptionTimeline pixelsPerSecond={100} />
|
|
159
|
+
</div>
|
|
160
|
+
) : undefined
|
|
161
|
+
}
|
|
162
|
+
timelineVisible={timelineVisible}
|
|
163
|
+
onToggleTimeline={toggleTimelineVisibility}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { PropertyPanel } from "./editor/PropertyPanel";
|
|
2
|
+
import { MotionPanel } from "./editor/MotionPanel";
|
|
3
|
+
import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
|
|
4
|
+
import { RenderQueue } from "./renders/RenderQueue";
|
|
5
|
+
import type { RenderJob } from "./renders/useRenderQueue";
|
|
6
|
+
import type { StudioGsapMotion } from "./editor/studioMotion";
|
|
7
|
+
import {
|
|
8
|
+
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
9
|
+
STUDIO_MOTION_PANEL_ENABLED,
|
|
10
|
+
} from "./editor/manualEditingAvailability";
|
|
11
|
+
import { useCallback } from "react";
|
|
12
|
+
import { resolveDomEditSelection, type DomEditLayerItem } from "./editor/domEditing";
|
|
13
|
+
import { useStudioContext } from "../contexts/StudioContext";
|
|
14
|
+
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
15
|
+
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
16
|
+
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
17
|
+
|
|
18
|
+
export interface StudioRightPanelProps {
|
|
19
|
+
selectedStudioMotion: StudioGsapMotion | null;
|
|
20
|
+
designPanelActive: boolean;
|
|
21
|
+
motionPanelActive: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function StudioRightPanel({
|
|
25
|
+
selectedStudioMotion,
|
|
26
|
+
designPanelActive,
|
|
27
|
+
motionPanelActive,
|
|
28
|
+
}: StudioRightPanelProps) {
|
|
29
|
+
const {
|
|
30
|
+
rightWidth,
|
|
31
|
+
rightPanelTab,
|
|
32
|
+
setRightPanelTab,
|
|
33
|
+
handlePanelResizeStart,
|
|
34
|
+
handlePanelResizeMove,
|
|
35
|
+
handlePanelResizeEnd,
|
|
36
|
+
} = usePanelLayoutContext();
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
captionEditMode,
|
|
40
|
+
previewIframeRef,
|
|
41
|
+
projectId,
|
|
42
|
+
activeCompPath,
|
|
43
|
+
compositionDimensions,
|
|
44
|
+
waitForPendingDomEditSaves,
|
|
45
|
+
renderQueue,
|
|
46
|
+
} = useStudioContext();
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
domEditSelection,
|
|
50
|
+
domEditGroupSelections,
|
|
51
|
+
copiedAgentPrompt,
|
|
52
|
+
clearDomSelection,
|
|
53
|
+
handleDomStyleCommit,
|
|
54
|
+
handleDomPathOffsetCommit,
|
|
55
|
+
handleDomBoxSizeCommit,
|
|
56
|
+
handleDomRotationCommit,
|
|
57
|
+
handleDomTextCommit,
|
|
58
|
+
handleDomTextFieldStyleCommit,
|
|
59
|
+
handleDomAddTextField,
|
|
60
|
+
handleDomRemoveTextField,
|
|
61
|
+
handleDomManualEditsReset,
|
|
62
|
+
handleAskAgent,
|
|
63
|
+
handleDomMotionCommit,
|
|
64
|
+
handleDomMotionClear,
|
|
65
|
+
applyDomSelection,
|
|
66
|
+
} = useDomEditContext();
|
|
67
|
+
|
|
68
|
+
const { assets, fontAssets, handleImportFiles, handleImportFonts } = useFileManagerContext();
|
|
69
|
+
|
|
70
|
+
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
71
|
+
const handleSelectLayer = useCallback(
|
|
72
|
+
(layer: DomEditLayerItem) => {
|
|
73
|
+
const selection = resolveDomEditSelection(layer.element, {
|
|
74
|
+
activeCompositionPath: activeCompPath,
|
|
75
|
+
isMasterView,
|
|
76
|
+
preferClipAncestor: false,
|
|
77
|
+
});
|
|
78
|
+
if (selection) applyDomSelection(selection);
|
|
79
|
+
},
|
|
80
|
+
[activeCompPath, isMasterView, applyDomSelection],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const renderJobs = renderQueue.jobs as RenderJob[];
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<div
|
|
88
|
+
className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center"
|
|
89
|
+
style={{ touchAction: "none" }}
|
|
90
|
+
onPointerDown={(e) => handlePanelResizeStart("right", e)}
|
|
91
|
+
onPointerMove={handlePanelResizeMove}
|
|
92
|
+
onPointerUp={handlePanelResizeEnd}
|
|
93
|
+
>
|
|
94
|
+
<div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
|
95
|
+
</div>
|
|
96
|
+
<div
|
|
97
|
+
className="flex flex-col border-l border-neutral-800 bg-neutral-900 flex-shrink-0"
|
|
98
|
+
style={{ width: rightWidth }}
|
|
99
|
+
>
|
|
100
|
+
{captionEditMode ? (
|
|
101
|
+
<CaptionPropertyPanel iframeRef={previewIframeRef} />
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
<div className="flex items-center gap-1 border-b border-neutral-800 px-3 py-2">
|
|
105
|
+
{STUDIO_INSPECTOR_PANELS_ENABLED && (
|
|
106
|
+
<>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setRightPanelTab("design")}
|
|
110
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
|
111
|
+
rightPanelTab === "design"
|
|
112
|
+
? "bg-neutral-800 text-white"
|
|
113
|
+
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
114
|
+
}`}
|
|
115
|
+
>
|
|
116
|
+
Design
|
|
117
|
+
</button>
|
|
118
|
+
{STUDIO_MOTION_PANEL_ENABLED && (
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={() => setRightPanelTab("motion")}
|
|
122
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
|
123
|
+
rightPanelTab === "motion"
|
|
124
|
+
? "bg-neutral-800 text-white"
|
|
125
|
+
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
126
|
+
}`}
|
|
127
|
+
>
|
|
128
|
+
Motion
|
|
129
|
+
</button>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => setRightPanelTab("renders")}
|
|
136
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
|
|
137
|
+
rightPanelTab === "renders"
|
|
138
|
+
? "bg-neutral-800 text-white"
|
|
139
|
+
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
{renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="min-h-0 flex-1">
|
|
146
|
+
{designPanelActive ? (
|
|
147
|
+
<PropertyPanel
|
|
148
|
+
projectId={projectId}
|
|
149
|
+
assets={assets}
|
|
150
|
+
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
151
|
+
multiSelectCount={domEditGroupSelections.length}
|
|
152
|
+
copiedAgentPrompt={copiedAgentPrompt}
|
|
153
|
+
onClearSelection={clearDomSelection}
|
|
154
|
+
onSetStyle={handleDomStyleCommit}
|
|
155
|
+
onSetManualOffset={handleDomPathOffsetCommit}
|
|
156
|
+
onSetManualSize={handleDomBoxSizeCommit}
|
|
157
|
+
onSetManualRotation={handleDomRotationCommit}
|
|
158
|
+
onSetText={handleDomTextCommit}
|
|
159
|
+
onSetTextFieldStyle={handleDomTextFieldStyleCommit}
|
|
160
|
+
onAddTextField={handleDomAddTextField}
|
|
161
|
+
onRemoveTextField={handleDomRemoveTextField}
|
|
162
|
+
onResetManualEdits={handleDomManualEditsReset}
|
|
163
|
+
onAskAgent={handleAskAgent}
|
|
164
|
+
onImportAssets={handleImportFiles}
|
|
165
|
+
fontAssets={fontAssets}
|
|
166
|
+
onImportFonts={handleImportFonts}
|
|
167
|
+
activeCompositionPath={activeCompPath}
|
|
168
|
+
onSelectLayer={handleSelectLayer}
|
|
169
|
+
/>
|
|
170
|
+
) : motionPanelActive ? (
|
|
171
|
+
<MotionPanel
|
|
172
|
+
element={domEditGroupSelections.length > 1 ? null : domEditSelection}
|
|
173
|
+
motion={selectedStudioMotion}
|
|
174
|
+
onClearSelection={clearDomSelection}
|
|
175
|
+
onSetMotion={handleDomMotionCommit}
|
|
176
|
+
onClearMotion={handleDomMotionClear}
|
|
177
|
+
/>
|
|
178
|
+
) : (
|
|
179
|
+
<RenderQueue
|
|
180
|
+
jobs={renderJobs}
|
|
181
|
+
projectId={projectId}
|
|
182
|
+
onDelete={renderQueue.deleteRender}
|
|
183
|
+
onClearCompleted={renderQueue.clearCompleted}
|
|
184
|
+
onStartRender={async (format, quality, resolution, fps) => {
|
|
185
|
+
await waitForPendingDomEditSaves();
|
|
186
|
+
await renderQueue.startRender({ fps, quality, format, resolution });
|
|
187
|
+
}}
|
|
188
|
+
compositionDimensions={compositionDimensions}
|
|
189
|
+
isRendering={renderQueue.isRendering}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|