@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.
Files changed (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.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 +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. 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
+ }