@hyperframes/studio 0.6.0-alpha.9 → 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.
Files changed (66) hide show
  1. package/dist/assets/{hyperframes-player-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-DUqUmaoH.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +428 -4299
  8. package/src/components/AskAgentModal.tsx +120 -0
  9. package/src/components/StudioHeader.tsx +133 -0
  10. package/src/components/StudioLeftSidebar.tsx +125 -0
  11. package/src/components/StudioPreviewArea.tsx +163 -0
  12. package/src/components/StudioRightPanel.tsx +198 -0
  13. package/src/components/TimelineToolbar.tsx +89 -0
  14. package/src/components/editor/DomEditOverlay.tsx +15 -1
  15. package/src/components/editor/PropertyPanel.test.ts +0 -49
  16. package/src/components/editor/PropertyPanel.tsx +132 -2763
  17. package/src/components/editor/domEditing.ts +38 -5
  18. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  19. package/src/components/editor/manualEditingAvailability.ts +1 -1
  20. package/src/components/editor/manualEdits.ts +32 -0
  21. package/src/components/editor/propertyPanelColor.tsx +371 -0
  22. package/src/components/editor/propertyPanelFill.tsx +421 -0
  23. package/src/components/editor/propertyPanelFont.tsx +455 -0
  24. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  25. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  26. package/src/components/editor/propertyPanelSections.tsx +453 -0
  27. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  28. package/src/components/nle/NLELayout.tsx +8 -11
  29. package/src/components/nle/NLEPreview.tsx +3 -0
  30. package/src/components/renders/RenderQueue.tsx +102 -31
  31. package/src/components/renders/useRenderQueue.ts +8 -2
  32. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  33. package/src/contexts/DomEditContext.tsx +137 -0
  34. package/src/contexts/FileManagerContext.tsx +110 -0
  35. package/src/contexts/PanelLayoutContext.tsx +68 -0
  36. package/src/contexts/StudioContext.tsx +135 -0
  37. package/src/hooks/useAppHotkeys.ts +326 -0
  38. package/src/hooks/useAskAgentModal.ts +162 -0
  39. package/src/hooks/useCaptionDetection.ts +132 -0
  40. package/src/hooks/useCompositionDimensions.ts +25 -0
  41. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  42. package/src/hooks/useDomEditCommits.ts +437 -0
  43. package/src/hooks/useDomEditSession.ts +342 -0
  44. package/src/hooks/useDomEditTextCommits.ts +330 -0
  45. package/src/hooks/useDomSelection.ts +398 -0
  46. package/src/hooks/useFileManager.ts +431 -0
  47. package/src/hooks/useFrameCapture.ts +77 -0
  48. package/src/hooks/useLintModal.ts +35 -0
  49. package/src/hooks/useManifestPersistence.ts +492 -0
  50. package/src/hooks/usePanelLayout.ts +68 -0
  51. package/src/hooks/usePreviewInteraction.ts +153 -0
  52. package/src/hooks/useRenderClipContent.ts +124 -0
  53. package/src/hooks/useTimelineEditing.ts +472 -0
  54. package/src/player/components/Player.tsx +33 -2
  55. package/src/player/components/Timeline.test.ts +0 -8
  56. package/src/player/components/Timeline.tsx +10 -103
  57. package/src/player/components/TimelineClip.tsx +9 -244
  58. package/src/player/hooks/useTimelinePlayer.ts +140 -103
  59. package/src/utils/domEditHelpers.ts +50 -0
  60. package/src/utils/studioFontHelpers.ts +83 -0
  61. package/src/utils/studioHelpers.ts +214 -0
  62. package/src/utils/studioPreviewHelpers.ts +185 -0
  63. package/src/utils/timelineDiscovery.ts +1 -1
  64. package/dist/assets/index-14zH9lqh.css +0 -1
  65. package/dist/assets/index-DYCiFGWQ.js +0 -108
  66. 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
+ }