@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
|
@@ -2,6 +2,11 @@ import { memo, useState, useRef, useEffect } from "react";
|
|
|
2
2
|
import { RenderQueueItem } from "./RenderQueueItem";
|
|
3
3
|
import type { RenderJob, ResolutionPreset } from "./useRenderQueue";
|
|
4
4
|
|
|
5
|
+
export interface CompositionDimensions {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
type StartRenderHandler = (
|
|
6
11
|
format: "mp4" | "webm" | "mov",
|
|
7
12
|
quality: "draft" | "standard" | "high",
|
|
@@ -16,37 +21,97 @@ interface RenderQueueProps {
|
|
|
16
21
|
onClearCompleted: () => void;
|
|
17
22
|
onStartRender: StartRenderHandler;
|
|
18
23
|
isRendering: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Authored dimensions of the active composition. Used to pick the
|
|
26
|
+
* matching preset (landscape / portrait / square) when the user selects
|
|
27
|
+
* a 1080p or 4K scale. `null` falls back to landscape (legacy default).
|
|
28
|
+
*/
|
|
29
|
+
compositionDimensions?: CompositionDimensions | null;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"portrait-4k": {
|
|
33
|
-
label: "4K ↕",
|
|
34
|
-
title: "2160×3840 — supersamples a 1080p portrait composition via Chrome DPR.",
|
|
35
|
-
},
|
|
32
|
+
// Orientation is derived from the composition's authored aspect ratio,
|
|
33
|
+
// not chosen by the user — picking "1080p portrait" for a landscape comp
|
|
34
|
+
// would just produce a wrong-aspect render.
|
|
35
|
+
type RenderScale = "auto" | "1080p" | "4k";
|
|
36
|
+
|
|
37
|
+
const SCALE_OPTION_ORDER: RenderScale[] = ["auto", "1080p", "4k"];
|
|
38
|
+
|
|
39
|
+
const SCALE_LABEL: Record<RenderScale, string> = {
|
|
40
|
+
auto: "Auto",
|
|
41
|
+
"1080p": "1080p",
|
|
42
|
+
"4k": "4K",
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
+
// Mirrors `CANVAS_DIMENSIONS` in @hyperframes/core. Studio can't import from
|
|
46
|
+
// the core barrel (it transitively pulls in node:fs) and the values are stable.
|
|
47
|
+
const CANVAS_DIMENSIONS: Record<ResolutionPreset, CompositionDimensions> = {
|
|
48
|
+
landscape: { width: 1920, height: 1080 },
|
|
49
|
+
portrait: { width: 1080, height: 1920 },
|
|
50
|
+
"landscape-4k": { width: 3840, height: 2160 },
|
|
51
|
+
"portrait-4k": { width: 2160, height: 3840 },
|
|
52
|
+
square: { width: 1080, height: 1080 },
|
|
53
|
+
"square-4k": { width: 2160, height: 2160 },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type CompAspect = "landscape" | "portrait" | "square";
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
function compAspect(dims: CompositionDimensions | null | undefined): CompAspect {
|
|
59
|
+
// Missing dims fall through to landscape (legacy default — "landscape" was
|
|
60
|
+
// the first preset). Studio shows resolved dims inline, so the user can see
|
|
61
|
+
// when this fallback is in effect.
|
|
62
|
+
if (dims == null) return "landscape";
|
|
63
|
+
if (dims.width === dims.height) return "square";
|
|
64
|
+
return dims.height > dims.width ? "portrait" : "landscape";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveResolution(
|
|
68
|
+
scale: RenderScale,
|
|
69
|
+
dims: CompositionDimensions | null | undefined,
|
|
70
|
+
): ResolutionPreset | "auto" {
|
|
71
|
+
if (scale === "auto") return "auto";
|
|
72
|
+
const aspect = compAspect(dims);
|
|
73
|
+
if (scale === "1080p") return aspect;
|
|
74
|
+
return aspect === "landscape"
|
|
75
|
+
? "landscape-4k"
|
|
76
|
+
: aspect === "portrait"
|
|
77
|
+
? "portrait-4k"
|
|
78
|
+
: "square-4k";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolvedDimensions(
|
|
82
|
+
scale: RenderScale,
|
|
83
|
+
dims: CompositionDimensions | null | undefined,
|
|
84
|
+
): CompositionDimensions | null {
|
|
85
|
+
if (scale === "auto") return dims ?? null;
|
|
86
|
+
const preset = resolveResolution(scale, dims);
|
|
87
|
+
return preset === "auto" ? null : CANVAS_DIMENSIONS[preset];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Mirrors the producer's resolveDeviceScaleFactor validation
|
|
91
|
+
// (renderOrchestrator.ts:608): the chosen preset must match the comp's aspect
|
|
92
|
+
// ratio exactly (cross-multiplied), can't downsample, and must be an integer
|
|
93
|
+
// scale factor. Without this guard the user can pick a preset that throws at
|
|
94
|
+
// render time — e.g. 1080p on a 1080×1080 square or 1080p on a 1280×720 comp
|
|
95
|
+
// (1.5× isn't integer).
|
|
96
|
+
function scaleApplies(scale: RenderScale, dims: CompositionDimensions | null | undefined): boolean {
|
|
97
|
+
if (scale === "auto" || dims == null) return true;
|
|
98
|
+
const preset = resolveResolution(scale, dims);
|
|
99
|
+
if (preset === "auto") return true;
|
|
100
|
+
const target = CANVAS_DIMENSIONS[preset];
|
|
101
|
+
if (target.width * dims.height !== target.height * dims.width) return false;
|
|
102
|
+
if (target.width < dims.width) return false;
|
|
103
|
+
return Number.isInteger(target.width / dims.width);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scaleOptionLabel(
|
|
107
|
+
scale: RenderScale,
|
|
108
|
+
dims: CompositionDimensions | null | undefined,
|
|
109
|
+
): string {
|
|
110
|
+
const resolved = resolvedDimensions(scale, dims);
|
|
111
|
+
return resolved
|
|
112
|
+
? `${SCALE_LABEL[scale]} · ${resolved.width}×${resolved.height}`
|
|
113
|
+
: SCALE_LABEL[scale];
|
|
114
|
+
}
|
|
50
115
|
|
|
51
116
|
const FORMAT_INFO: Record<"mp4" | "webm" | "mov", { label: string; desc: string }> = {
|
|
52
117
|
mp4: { label: "MP4", desc: "Best for general use. Smallest file, universal playback." },
|
|
@@ -127,9 +192,11 @@ const QUALITY_OPTIONS: {
|
|
|
127
192
|
function FormatExportButton({
|
|
128
193
|
onStartRender,
|
|
129
194
|
isRendering,
|
|
195
|
+
compositionDimensions,
|
|
130
196
|
}: {
|
|
131
197
|
onStartRender: StartRenderHandler;
|
|
132
198
|
isRendering: boolean;
|
|
199
|
+
compositionDimensions?: CompositionDimensions | null;
|
|
133
200
|
}) {
|
|
134
201
|
const [format, setFormat] = useState<"mp4" | "webm" | "mov">("mp4");
|
|
135
202
|
const [quality, setQuality] = useState<"draft" | "standard" | "high">("standard");
|
|
@@ -150,12 +217,11 @@ function FormatExportButton({
|
|
|
150
217
|
value={resolution}
|
|
151
218
|
onChange={(e) => setResolution(e.target.value as ResolutionPreset | "auto")}
|
|
152
219
|
disabled={isRendering}
|
|
153
|
-
title={RESOLUTION_OPTIONS.find((r) => r.value === resolution)?.title}
|
|
154
220
|
className="h-5 px-1 text-[10px] rounded-l bg-neutral-800 border border-neutral-700 text-neutral-300 outline-none disabled:opacity-50"
|
|
155
221
|
>
|
|
156
|
-
{
|
|
157
|
-
<option key={
|
|
158
|
-
{
|
|
222
|
+
{SCALE_OPTION_ORDER.map((value) => (
|
|
223
|
+
<option key={value} value={value} disabled={!scaleApplies(value, compositionDimensions)}>
|
|
224
|
+
{scaleOptionLabel(value, compositionDimensions)}
|
|
159
225
|
</option>
|
|
160
226
|
))}
|
|
161
227
|
</select>
|
|
@@ -215,6 +281,7 @@ export const RenderQueue = memo(function RenderQueue({
|
|
|
215
281
|
onClearCompleted,
|
|
216
282
|
onStartRender,
|
|
217
283
|
isRendering,
|
|
284
|
+
compositionDimensions,
|
|
218
285
|
}: RenderQueueProps) {
|
|
219
286
|
const listRef = useRef<HTMLDivElement>(null);
|
|
220
287
|
|
|
@@ -241,7 +308,11 @@ export const RenderQueue = memo(function RenderQueue({
|
|
|
241
308
|
Clear
|
|
242
309
|
</button>
|
|
243
310
|
)}
|
|
244
|
-
<FormatExportButton
|
|
311
|
+
<FormatExportButton
|
|
312
|
+
onStartRender={onStartRender}
|
|
313
|
+
isRendering={isRendering}
|
|
314
|
+
compositionDimensions={compositionDimensions}
|
|
315
|
+
/>
|
|
245
316
|
</div>
|
|
246
317
|
</div>
|
|
247
318
|
|
|
@@ -14,8 +14,14 @@ export interface RenderJob {
|
|
|
14
14
|
// Mirrors `CanvasResolution` from @hyperframes/core. Kept local because
|
|
15
15
|
// studio's tsconfig doesn't include node types, and the core barrel
|
|
16
16
|
// transitively pulls in modules with `node:fs` imports. Drift risk is
|
|
17
|
-
// low (
|
|
18
|
-
export type ResolutionPreset =
|
|
17
|
+
// low (6 string literals kept in sync manually with CANVAS_DIMENSIONS).
|
|
18
|
+
export type ResolutionPreset =
|
|
19
|
+
| "landscape"
|
|
20
|
+
| "portrait"
|
|
21
|
+
| "landscape-4k"
|
|
22
|
+
| "portrait-4k"
|
|
23
|
+
| "square"
|
|
24
|
+
| "square-4k";
|
|
19
25
|
|
|
20
26
|
export interface StartRenderOptions {
|
|
21
27
|
fps?: number;
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
forwardRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
3
9
|
import { CompositionsTab } from "./CompositionsTab";
|
|
4
10
|
import { AssetsTab } from "./AssetsTab";
|
|
5
11
|
import { FileTree } from "../editor/FileTree";
|
|
6
12
|
|
|
7
|
-
type SidebarTab = "compositions" | "assets" | "code";
|
|
13
|
+
export type SidebarTab = "compositions" | "assets" | "code";
|
|
14
|
+
|
|
15
|
+
export interface LeftSidebarHandle {
|
|
16
|
+
selectTab: (tab: SidebarTab) => void;
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
const STORAGE_KEY = "hf-studio-sidebar-tab";
|
|
10
20
|
|
|
@@ -39,201 +49,191 @@ interface LeftSidebarProps {
|
|
|
39
49
|
takeoverContent?: ReactNode;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
export const LeftSidebar = memo(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
export const LeftSidebar = memo(
|
|
53
|
+
forwardRef<LeftSidebarHandle, LeftSidebarProps>(function LeftSidebar(
|
|
54
|
+
{
|
|
55
|
+
width = 240,
|
|
56
|
+
projectId,
|
|
57
|
+
compositions,
|
|
58
|
+
assets,
|
|
59
|
+
activeComposition,
|
|
60
|
+
onSelectComposition,
|
|
61
|
+
onImportFiles,
|
|
62
|
+
fileTree: fileProp,
|
|
63
|
+
editingFile,
|
|
64
|
+
onSelectFile,
|
|
65
|
+
onCreateFile,
|
|
66
|
+
onCreateFolder,
|
|
67
|
+
onDeleteFile,
|
|
68
|
+
onRenameFile,
|
|
69
|
+
onDuplicateFile,
|
|
70
|
+
onMoveFile,
|
|
71
|
+
codeChildren,
|
|
72
|
+
onLint,
|
|
73
|
+
linting,
|
|
74
|
+
onToggleCollapse,
|
|
75
|
+
takeoverContent,
|
|
76
|
+
},
|
|
77
|
+
ref,
|
|
78
|
+
) {
|
|
79
|
+
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
const selectTab = useCallback((t: SidebarTab) => {
|
|
82
|
+
setTab(t);
|
|
83
|
+
localStorage.setItem(STORAGE_KEY, t);
|
|
84
|
+
}, []);
|
|
71
85
|
|
|
72
|
-
|
|
73
|
-
useMountEffect(() => {
|
|
74
|
-
const handler = (e: KeyboardEvent) => {
|
|
75
|
-
if (!e.metaKey && !e.ctrlKey) return;
|
|
76
|
-
if (e.key === "1") {
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
selectTab("compositions");
|
|
79
|
-
}
|
|
80
|
-
if (e.key === "2") {
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
selectTab("assets");
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
window.addEventListener("keydown", handler);
|
|
86
|
-
return () => window.removeEventListener("keydown", handler);
|
|
87
|
-
});
|
|
86
|
+
useImperativeHandle(ref, () => ({ selectTab }), [selectTab]);
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
>
|
|
105
|
-
<button
|
|
106
|
-
type="button"
|
|
107
|
-
onClick={() => selectTab("code")}
|
|
108
|
-
className={`rounded-[14px] px-2.5 py-2 text-[10px] font-semibold transition-all ${
|
|
109
|
-
tab === "code"
|
|
110
|
-
? "bg-neutral-800 text-white"
|
|
111
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
112
|
-
}`}
|
|
113
|
-
>
|
|
114
|
-
Code
|
|
115
|
-
</button>
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
onClick={() => selectTab("compositions")}
|
|
119
|
-
className={`rounded-[14px] px-2.5 py-2 text-[10px] font-semibold transition-all ${
|
|
120
|
-
tab === "compositions"
|
|
121
|
-
? "bg-neutral-800 text-white"
|
|
122
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
123
|
-
}`}
|
|
124
|
-
>
|
|
125
|
-
Compositions
|
|
126
|
-
</button>
|
|
127
|
-
<button
|
|
128
|
-
type="button"
|
|
129
|
-
onClick={() => selectTab("assets")}
|
|
130
|
-
className={`rounded-[14px] px-2.5 py-2 text-[10px] font-semibold transition-all ${
|
|
131
|
-
tab === "assets"
|
|
132
|
-
? "bg-neutral-800 text-white"
|
|
133
|
-
: "text-neutral-500 hover:text-neutral-200"
|
|
134
|
-
}`}
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
className="flex flex-col h-full bg-neutral-950 border-r border-neutral-800/50"
|
|
91
|
+
style={{ width }}
|
|
92
|
+
>
|
|
93
|
+
{takeoverContent ? (
|
|
94
|
+
<div className="flex min-h-0 flex-1">{takeoverContent}</div>
|
|
95
|
+
) : (
|
|
96
|
+
<>
|
|
97
|
+
{/* Tabs — Code first */}
|
|
98
|
+
<div className="border-b border-neutral-800/50 px-3 py-3 flex-shrink-0">
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<div
|
|
101
|
+
className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
|
102
|
+
style={{ gridTemplateColumns: "1fr 1fr 1fr" }}
|
|
135
103
|
>
|
|
136
|
-
|
|
137
|
-
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={() => selectTab("code")}
|
|
107
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
108
|
+
tab === "code"
|
|
109
|
+
? "bg-neutral-800 text-white"
|
|
110
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
111
|
+
}`}
|
|
112
|
+
>
|
|
113
|
+
Code
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => selectTab("compositions")}
|
|
118
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
119
|
+
tab === "compositions"
|
|
120
|
+
? "bg-neutral-800 text-white"
|
|
121
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
Comps
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={() => selectTab("assets")}
|
|
129
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
130
|
+
tab === "assets"
|
|
131
|
+
? "bg-neutral-800 text-white"
|
|
132
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
Assets
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
{onToggleCollapse && (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={onToggleCollapse}
|
|
142
|
+
className="flex h-8 w-8 flex-shrink-0 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"
|
|
143
|
+
title="Hide sidebar"
|
|
144
|
+
aria-label="Hide sidebar"
|
|
145
|
+
>
|
|
146
|
+
<svg
|
|
147
|
+
width="14"
|
|
148
|
+
height="14"
|
|
149
|
+
viewBox="0 0 24 24"
|
|
150
|
+
fill="none"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
strokeWidth="1.5"
|
|
153
|
+
strokeLinecap="round"
|
|
154
|
+
strokeLinejoin="round"
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
>
|
|
157
|
+
<path d="m14 7-5 5 5 5" />
|
|
158
|
+
<path d="M19 4v16" />
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
138
162
|
</div>
|
|
139
|
-
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Tab content */}
|
|
166
|
+
{tab === "compositions" && (
|
|
167
|
+
<CompositionsTab
|
|
168
|
+
projectId={projectId}
|
|
169
|
+
compositions={compositions}
|
|
170
|
+
activeComposition={activeComposition}
|
|
171
|
+
onSelect={onSelectComposition}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
{tab === "assets" && (
|
|
175
|
+
<AssetsTab
|
|
176
|
+
projectId={projectId}
|
|
177
|
+
assets={assets}
|
|
178
|
+
onImport={onImportFiles}
|
|
179
|
+
onDelete={onDeleteFile}
|
|
180
|
+
onRename={onRenameFile}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
{tab === "code" && (
|
|
184
|
+
<div className="flex flex-1 min-h-0">
|
|
185
|
+
{(fileProp?.length ?? 0) > 0 && (
|
|
186
|
+
<div className="w-[160px] flex-shrink-0 border-r border-neutral-800 overflow-y-auto">
|
|
187
|
+
<FileTree
|
|
188
|
+
files={fileProp ?? []}
|
|
189
|
+
activeFile={editingFile?.path ?? null}
|
|
190
|
+
onSelectFile={onSelectFile ?? (() => {})}
|
|
191
|
+
onCreateFile={onCreateFile}
|
|
192
|
+
onCreateFolder={onCreateFolder}
|
|
193
|
+
onDeleteFile={onDeleteFile}
|
|
194
|
+
onRenameFile={onRenameFile}
|
|
195
|
+
onDuplicateFile={onDuplicateFile}
|
|
196
|
+
onMoveFile={onMoveFile}
|
|
197
|
+
onImportFiles={onImportFiles}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<div className="flex-1 overflow-hidden min-w-0">
|
|
202
|
+
{codeChildren ?? (
|
|
203
|
+
<div className="flex items-center justify-center h-full text-neutral-600 text-sm">
|
|
204
|
+
Select a file to edit
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{/* Lint button pinned at the bottom */}
|
|
212
|
+
{onLint && (
|
|
213
|
+
<div className="border-t border-neutral-800 p-2 flex-shrink-0">
|
|
140
214
|
<button
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
className="
|
|
144
|
-
title="Hide sidebar"
|
|
145
|
-
aria-label="Hide sidebar"
|
|
215
|
+
onClick={onLint}
|
|
216
|
+
disabled={linting}
|
|
217
|
+
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 hover:text-amber-300 hover:bg-neutral-800 transition-colors disabled:opacity-40"
|
|
146
218
|
>
|
|
147
219
|
<svg
|
|
148
|
-
width="
|
|
149
|
-
height="
|
|
220
|
+
width="12"
|
|
221
|
+
height="12"
|
|
150
222
|
viewBox="0 0 24 24"
|
|
151
223
|
fill="none"
|
|
152
224
|
stroke="currentColor"
|
|
153
|
-
strokeWidth="
|
|
154
|
-
strokeLinecap="round"
|
|
155
|
-
strokeLinejoin="round"
|
|
156
|
-
aria-hidden="true"
|
|
225
|
+
strokeWidth="2"
|
|
157
226
|
>
|
|
158
|
-
<path d="
|
|
159
|
-
<path d="
|
|
227
|
+
<path d="M9 11l3 3L22 4" />
|
|
228
|
+
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
160
229
|
</svg>
|
|
230
|
+
{linting ? "Linting…" : "Lint"}
|
|
161
231
|
</button>
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
{/* Tab content */}
|
|
167
|
-
{tab === "compositions" && (
|
|
168
|
-
<CompositionsTab
|
|
169
|
-
projectId={projectId}
|
|
170
|
-
compositions={compositions}
|
|
171
|
-
activeComposition={activeComposition}
|
|
172
|
-
onSelect={onSelectComposition}
|
|
173
|
-
/>
|
|
174
|
-
)}
|
|
175
|
-
{tab === "assets" && (
|
|
176
|
-
<AssetsTab
|
|
177
|
-
projectId={projectId}
|
|
178
|
-
assets={assets}
|
|
179
|
-
onImport={onImportFiles}
|
|
180
|
-
onDelete={onDeleteFile}
|
|
181
|
-
onRename={onRenameFile}
|
|
182
|
-
/>
|
|
183
|
-
)}
|
|
184
|
-
{tab === "code" && (
|
|
185
|
-
<div className="flex flex-1 min-h-0">
|
|
186
|
-
{(fileProp?.length ?? 0) > 0 && (
|
|
187
|
-
<div className="w-[160px] flex-shrink-0 border-r border-neutral-800 overflow-y-auto">
|
|
188
|
-
<FileTree
|
|
189
|
-
files={fileProp ?? []}
|
|
190
|
-
activeFile={editingFile?.path ?? null}
|
|
191
|
-
onSelectFile={onSelectFile ?? (() => {})}
|
|
192
|
-
onCreateFile={onCreateFile}
|
|
193
|
-
onCreateFolder={onCreateFolder}
|
|
194
|
-
onDeleteFile={onDeleteFile}
|
|
195
|
-
onRenameFile={onRenameFile}
|
|
196
|
-
onDuplicateFile={onDuplicateFile}
|
|
197
|
-
onMoveFile={onMoveFile}
|
|
198
|
-
onImportFiles={onImportFiles}
|
|
199
|
-
/>
|
|
200
|
-
</div>
|
|
201
|
-
)}
|
|
202
|
-
<div className="flex-1 overflow-hidden min-w-0">
|
|
203
|
-
{codeChildren ?? (
|
|
204
|
-
<div className="flex items-center justify-center h-full text-neutral-600 text-sm">
|
|
205
|
-
Select a file to edit
|
|
206
|
-
</div>
|
|
207
|
-
)}
|
|
208
232
|
</div>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
onClick={onLint}
|
|
217
|
-
disabled={linting}
|
|
218
|
-
className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[11px] font-medium text-neutral-500 hover:text-amber-300 hover:bg-neutral-800 transition-colors disabled:opacity-40"
|
|
219
|
-
>
|
|
220
|
-
<svg
|
|
221
|
-
width="12"
|
|
222
|
-
height="12"
|
|
223
|
-
viewBox="0 0 24 24"
|
|
224
|
-
fill="none"
|
|
225
|
-
stroke="currentColor"
|
|
226
|
-
strokeWidth="2"
|
|
227
|
-
>
|
|
228
|
-
<path d="M9 11l3 3L22 4" />
|
|
229
|
-
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
230
|
-
</svg>
|
|
231
|
-
{linting ? "Linting…" : "Lint"}
|
|
232
|
-
</button>
|
|
233
|
-
</div>
|
|
234
|
-
)}
|
|
235
|
-
</>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
);
|
|
239
|
-
});
|
|
233
|
+
)}
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}),
|
|
239
|
+
);
|