Skip to content

Commit 69dc2ed

Browse files
committed
add editor support
1 parent 8a455cb commit 69dc2ed

File tree

12 files changed

+2472
-95
lines changed

12 files changed

+2472
-95
lines changed

electron/main.ts

Lines changed: 441 additions & 4 deletions
Large diffs are not rendered by default.

electron/preload.cts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { contextBridge, ipcRenderer } from "electron";
2+
3+
contextBridge.exposeInMainWorld("editorAPI", {
4+
readFile: (filePath: string) => ipcRenderer.invoke("editor:readFile", filePath),
5+
readFileOptional: (filePath: string) => ipcRenderer.invoke("editor:readFileOptional", filePath),
6+
writeFile: (filePath: string, content: string) =>
7+
ipcRenderer.invoke("editor:writeFile", { filePath, content }),
8+
stat: (filePath: string) => ipcRenderer.invoke("editor:stat", filePath),
9+
statOptional: (filePath: string) => ipcRenderer.invoke("editor:statOptional", filePath),
10+
readdir: (filePath: string) => ipcRenderer.invoke("editor:readdir", filePath),
11+
readdirOptional: (filePath: string) => ipcRenderer.invoke("editor:readdirOptional", filePath),
12+
mkdir: (filePath: string) => ipcRenderer.invoke("editor:mkdir", filePath),
13+
delete: (filePath: string) => ipcRenderer.invoke("editor:delete", filePath),
14+
rename: (from: string, to: string) => ipcRenderer.invoke("editor:rename", { from, to }),
15+
findClipLabel: (label: string) => ipcRenderer.invoke("editor:findClipLabel", label),
16+
getLspPort: () => ipcRenderer.invoke("editor:getLspPort"),
17+
getProjectRoot: () => ipcRenderer.invoke("editor:getProjectRoot"),
18+
});

package-lock.json

Lines changed: 670 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,21 @@
3333
"mathjax-full": "^3.2.1",
3434
"opentype.js": "^1.3.4",
3535
"puppeteer": "^24.1.1",
36-
"react": "^19.2.0",
37-
"react-dom": "^19.2.0",
38-
"react-router-dom": "^6.28.0",
39-
"three": "^0.166.1"
40-
},
36+
"@monaco-editor/react": "^4.6.0",
37+
"monaco-editor": "^0.52.0",
38+
"monaco-languageclient": "^10.5.0",
39+
"typescript-language-server": "^4.4.0",
40+
"vscode-jsonrpc": "^8.2.0",
41+
"vscode-languageclient": "^9.0.1",
42+
"ws": "^8.18.0",
43+
"react": "^19.2.0",
44+
"react-dom": "^19.2.0",
45+
"react-router-dom": "^6.28.0",
46+
"three": "^0.166.1"
47+
},
48+
"overrides": {
49+
"vscode-semver": "npm:semver@5.7.2"
50+
},
4151
"devDependencies": {
4252
"@eslint/js": "^9.39.1",
4353
"@types/node": "^24.10.1",

project/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../tsconfig.app.json",
3+
"include": ["./**/*", "../src/**/*"]
4+
}

src/StudioApp.tsx

Lines changed: 148 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,33 @@ import { PROJECT, PROJECT_SETTINGS } from "../project/project";
33
import { WithCurrentFrame } from "./lib/frame"
44
import { TimelineUI } from "./ui/timeline";
55
import { ClipVisibilityPanel } from "./ui/clip-visibility";
6+
import { CodeEditor } from "./ui/code-editor";
7+
import { EditorProvider } from "./ui/editor-context";
68
import { Store } from "./util/state";
79
import { StudioStateContext } from "./lib/studio-state"
810

911
// Back-compat re-exports (avoid HMR issues if some modules still import these from StudioApp).
1012
export { StudioStateContext, useIsPlaying, useIsPlayingStore, useIsRender, useSetIsPlaying } from "./lib/studio-state"
1113

1214
export const StudioApp = () => {
15+
const rowRef = useRef<HTMLDivElement>(null);
1316
const containerRef = useRef<HTMLDivElement>(null);
1417
const topRef = useRef<HTMLDivElement>(null);
1518
const previewRef = useRef<HTMLDivElement>(null);
1619
const [verticalRatio, setVerticalRatio] = useState(0.6); // top area height ratio
1720
const [horizontalRatio, setHorizontalRatio] = useState(0.3); // clips width ratio within top area
21+
const [editorWidth, setEditorWidth] = useState(460);
22+
const [isEditorVisible, setIsEditorVisible] = useState(true);
1823
const projectWidth = PROJECT_SETTINGS.width || 1920
1924
const projectHeight = PROJECT_SETTINGS.height || 1080
2025
const previewAspect = `${projectWidth} / ${projectHeight}`
2126
const previewAspectValue = projectHeight / projectWidth
2227
const previewMinWidth = 320;
2328
const previewMinHeight = previewMinWidth * previewAspectValue;
2429
const timelineMinHeight = 200;
30+
const leftPanelMinWidth = previewMinWidth + 220 + 6 + 20;
31+
const editorMinWidth = 320;
32+
const rowGap = 10;
2533
const [previewViewport, setPreviewViewport] = useState<{ width: number; height: number }>({ width: 0, height: 0 });
2634
const hasPreviewViewport = previewViewport.width > 0 && previewViewport.height > 0;
2735

@@ -132,108 +140,167 @@ export const StudioApp = () => {
132140
[isPlayingStore]
133141
);
134142

143+
const clampEditorWidth = useCallback(
144+
(nextWidth: number) => {
145+
const row = rowRef.current;
146+
if (!row) {
147+
setEditorWidth(Math.max(editorMinWidth, nextWidth));
148+
return;
149+
}
150+
const maxWidth = Math.max(editorMinWidth, row.clientWidth - rowGap - leftPanelMinWidth);
151+
const clamped = Math.min(Math.max(nextWidth, editorMinWidth), maxWidth);
152+
setEditorWidth(clamped);
153+
},
154+
[editorMinWidth, leftPanelMinWidth, rowGap],
155+
);
156+
157+
useEffect(() => {
158+
const row = rowRef.current;
159+
if (!row) return;
160+
const observer = new ResizeObserver(() => {
161+
setEditorWidth((prev) => {
162+
const maxWidth = Math.max(editorMinWidth, row.clientWidth - rowGap - leftPanelMinWidth);
163+
return Math.min(prev, maxWidth);
164+
});
165+
});
166+
observer.observe(row);
167+
return () => observer.disconnect();
168+
}, [editorMinWidth, leftPanelMinWidth, rowGap]);
169+
135170
return (
136-
<StudioStateContext value={{ isPlaying, setIsPlaying, isPlayingStore, isRender: false }}>
137-
<WithCurrentFrame>
138-
<div style={{ padding: 16, height: "100vh", boxSizing: "border-box", minHeight: 0 }}>
139-
<div
140-
ref={containerRef}
141-
style={{
142-
display: "flex",
143-
flexDirection: "column",
144-
gap: 10,
145-
width: "100%",
146-
height: "100%",
147-
boxSizing: "border-box",
148-
minHeight: 0,
149-
}}
150-
>
151-
<div
152-
ref={topRef}
153-
style={{
154-
display: "flex",
155-
gap: 10,
156-
alignItems: "stretch",
157-
width: "100%",
158-
flexBasis: `${verticalRatio * 100}%`,
159-
minHeight: 240,
160-
maxHeight: "80%",
161-
minWidth: 0,
162-
}}
163-
>
164-
<div style={{ flexBasis: `${horizontalRatio * 100}%`, minWidth: 220 }}>
165-
<ClipVisibilityPanel />
166-
</div>
167-
<div
168-
onPointerDown={startHorizontalDrag}
169-
style={{
170-
width: 6,
171-
cursor: "col-resize",
172-
background: "linear-gradient(180deg, #1f2937, #111827)",
173-
borderRadius: 4,
174-
flexShrink: 0,
175-
}}
176-
/>
177-
<div style={{ flex: 1, minWidth: 320, display: "flex", alignItems: "center", justifyContent: "center", minHeight: previewMinHeight, position: "relative" }}>
171+
<EditorProvider>
172+
<StudioStateContext value={{ isPlaying, setIsPlaying, isPlayingStore, isRender: false }}>
173+
<WithCurrentFrame>
174+
<div style={{ padding: 16, height: "100vh", boxSizing: "border-box", minHeight: 0 }}>
175+
<div ref={rowRef} style={{ display: "flex", gap: 10, height: "100%", minHeight: 0 }}>
176+
<div style={{ flex: 1, minWidth: leftPanelMinWidth, display: "flex", flexDirection: "column", minHeight: 0 }}>
177+
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
178+
<button
179+
type="button"
180+
onClick={() => setIsEditorVisible((prev) => !prev)}
181+
style={{
182+
padding: "6px 10px",
183+
borderRadius: 6,
184+
border: "1px solid #1f2937",
185+
background: "#0f172a",
186+
color: "#cbd5e1",
187+
cursor: "pointer",
188+
fontSize: 12,
189+
fontWeight: 600,
190+
}}
191+
>
192+
{isEditorVisible ? "Hide Editor" : "Show Editor"}
193+
</button>
194+
</div>
195+
178196
<div
179-
ref={previewRef}
197+
ref={containerRef}
180198
style={{
199+
display: "flex",
200+
flexDirection: "column",
201+
gap: 10,
181202
width: "100%",
182203
height: "100%",
183-
display: "flex",
184-
alignItems: "center",
185-
justifyContent: "center",
186204
boxSizing: "border-box",
205+
minHeight: 0,
206+
flex: 1,
187207
}}
188208
>
189209
<div
210+
ref={topRef}
190211
style={{
191-
width: scaledWidth,
192-
height: scaledHeight,
193-
visibility: hasPreviewViewport ? "visible" : "hidden",
194-
aspectRatio: previewAspect,
195-
border: "1px solid #444",
196-
borderRadius: 1,
197-
overflow: "hidden",
198-
backgroundColor: "#000",
199-
boxShadow: "0 10px 30px rgba(0,0,0,0.35)",
200-
position: "relative",
212+
display: "flex",
213+
gap: 10,
214+
alignItems: "stretch",
215+
width: "100%",
216+
flexBasis: `${verticalRatio * 100}%`,
217+
minHeight: 240,
218+
maxHeight: "80%",
219+
minWidth: 0,
201220
}}
202221
>
222+
<div style={{ flexBasis: `${horizontalRatio * 100}%`, minWidth: 220 }}>
223+
<ClipVisibilityPanel />
224+
</div>
203225
<div
226+
onPointerDown={startHorizontalDrag}
204227
style={{
205-
width: projectWidth,
206-
height: projectHeight,
207-
transform: `scale(${scale})`,
208-
transformOrigin: "top left",
228+
width: 6,
229+
cursor: "col-resize",
230+
background: "linear-gradient(180deg, #1f2937, #111827)",
231+
borderRadius: 4,
232+
flexShrink: 0,
209233
}}
210-
>
211-
<PROJECT />
234+
/>
235+
<div style={{ flex: 1, minWidth: 320, display: "flex", alignItems: "center", justifyContent: "center", minHeight: previewMinHeight, position: "relative" }}>
236+
<div
237+
ref={previewRef}
238+
style={{
239+
width: "100%",
240+
height: "100%",
241+
display: "flex",
242+
alignItems: "center",
243+
justifyContent: "center",
244+
boxSizing: "border-box",
245+
}}
246+
>
247+
<div
248+
style={{
249+
width: scaledWidth,
250+
height: scaledHeight,
251+
visibility: hasPreviewViewport ? "visible" : "hidden",
252+
aspectRatio: previewAspect,
253+
border: "1px solid #444",
254+
borderRadius: 1,
255+
overflow: "hidden",
256+
backgroundColor: "#000",
257+
boxShadow: "0 10px 30px rgba(0,0,0,0.35)",
258+
position: "relative",
259+
}}
260+
>
261+
<div
262+
style={{
263+
width: projectWidth,
264+
height: projectHeight,
265+
transform: `scale(${scale})`,
266+
transformOrigin: "top left",
267+
}}
268+
>
269+
<PROJECT />
270+
</div>
271+
</div>
272+
</div>
273+
</div>
274+
</div>
275+
276+
<div
277+
onPointerDown={startVerticalDrag}
278+
style={{
279+
height: 8,
280+
cursor: "row-resize",
281+
background: "linear-gradient(90deg, #1f2937, #111827)",
282+
borderRadius: 4,
283+
flexShrink: 0,
284+
}}
285+
/>
286+
287+
<div style={{ flex: 1, minHeight: 160, display: "flex", minWidth: 0 }}>
288+
<div style={{ flex: 1, minHeight: 0 }}>
289+
<TimelineUI />
212290
</div>
213291
</div>
214292
</div>
215293
</div>
216-
</div>
217294

218-
<div
219-
onPointerDown={startVerticalDrag}
220-
style={{
221-
height: 8,
222-
cursor: "row-resize",
223-
background: "linear-gradient(90deg, #1f2937, #111827)",
224-
borderRadius: 4,
225-
flexShrink: 0,
226-
}}
227-
/>
228-
229-
<div style={{ flex: 1, minHeight: 160, display: "flex", minWidth: 0 }}>
230-
<div style={{ flex: 1, minHeight: 0 }}>
231-
<TimelineUI />
232-
</div>
295+
{isEditorVisible ? (
296+
<div style={{ width: editorWidth, minWidth: editorMinWidth, height: "100%", minHeight: 0, display: "flex" }}>
297+
<CodeEditor width={editorWidth} onWidthChange={clampEditorWidth} />
298+
</div>
299+
) : null}
233300
</div>
234301
</div>
235-
</div>
236-
</WithCurrentFrame>
237-
</StudioStateContext>
302+
</WithCurrentFrame>
303+
</StudioStateContext>
304+
</EditorProvider>
238305
);
239306
};

src/lib/misc/text-decoration.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { CSSProperties } from "react"
2+
3+
type TextProps = {
4+
text: string
5+
size?: number
6+
weight?: number
7+
color?: string
8+
outlineColor?: string
9+
outlineWidth?: number
10+
shadow?: string
11+
letterSpacing?: number | string
12+
lineHeight?: number | string
13+
fontFamily?: string
14+
style?: CSSProperties
15+
}
16+
17+
export const OutLineText = ({
18+
text,
19+
size = 78,
20+
weight = 700,
21+
color = "#ffffff",
22+
outlineColor = "#000000",
23+
outlineWidth = 10,
24+
shadow = "0 0 16px rgba(21, 128, 61, 0.45)",
25+
letterSpacing = "0.04em",
26+
lineHeight = 1,
27+
fontFamily,
28+
style,
29+
}: TextProps) => {
30+
const baseStyle: CSSProperties = {
31+
position: "relative",
32+
display: "inline-block",
33+
fontSize: size,
34+
fontWeight: weight,
35+
letterSpacing,
36+
lineHeight,
37+
fontFamily,
38+
}
39+
40+
return (
41+
<span style={style ? { ...baseStyle, ...style } : baseStyle}>
42+
<span
43+
style={{
44+
position: "absolute",
45+
inset: 0,
46+
WebkitTextStroke: `${outlineWidth}px ${outlineColor}`,
47+
WebkitTextFillColor: "transparent",
48+
color: "transparent",
49+
textShadow: shadow,
50+
whiteSpace: "nowrap",
51+
}}
52+
>
53+
{text}
54+
</span>
55+
<span style={{ position: "relative", color, whiteSpace: "nowrap" }}>{text}</span>
56+
</span>
57+
)
58+
}

0 commit comments

Comments
 (0)