feat: add web editor and terminal panels

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-02 18:35:17 +08:00
co-authored by Claude
parent 0f634981b5
commit 062cde88f8
4 changed files with 116 additions and 20 deletions
+39
View File
@@ -0,0 +1,39 @@
import Editor, { type OnValidate } from "@monaco-editor/react";
import { useState } from "react";
import type { MockFileNode } from "@/pages/workspace/mock-data";
interface EditorPanelProps {
file?: MockFileNode;
}
export function EditorPanel({ file }: EditorPanelProps) {
const [, setMarkers] = useState<Parameters<OnValidate>[0]>([]);
if (!file || file.kind !== "file") {
return (
<section className="flex h-full w-full items-center justify-center bg-background text-sm text-muted-foreground">
<div className="text-center">
<p>No file selected</p>
</div>
</section>
);
}
return (
<section className="h-full w-full overflow-hidden bg-background">
<Editor
key={file.path}
theme="vs-dark"
language={file.language ?? "typescript"}
value={file.content ?? ""}
onValidate={setMarkers}
options={{
automaticLayout: true,
minimap: { enabled: false },
readOnly: false,
}}
/>
</section>
);
}
+5 -20
View File
@@ -3,6 +3,8 @@ import { PanelLeftClose, PanelLeftOpen, Terminal } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { EditorPanel } from "@/components/editor/EditorPanel";
import { TerminalPanel } from "@/components/terminal/TerminalPanel";
import { FileExplorerPanel } from "@/components/workspace/FileExplorerPanel"; import { FileExplorerPanel } from "@/components/workspace/FileExplorerPanel";
import { StatusBar } from "@/components/workspace/StatusBar"; import { StatusBar } from "@/components/workspace/StatusBar";
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store"; import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
@@ -69,24 +71,9 @@ export function WorkspaceShell({ files }: WorkspaceShellProps) {
{/* Main content area */} {/* Main content area */}
<Panel> <Panel>
<PanelGroup direction="vertical"> <PanelGroup direction="vertical">
{/* Editor placeholder */} {/* Editor */}
<Panel> <Panel>
<div className="flex h-full w-full items-center justify-center border-r border-border text-sm text-muted-foreground"> <EditorPanel file={activeFile} />
<div className="text-center">
<p className="mb-2">Editor placeholder</p>
<div className="space-y-1 text-xs">
<p>
Active file:{" "}
<code className="text-foreground">{activeFilePath}</code>
</p>
<p className="text-muted-foreground">
{activeFile
? `${activeFile.name} | ${activeFile.language ?? "plain text"} | ${activeFile.kind}`
: "No matching mock file"}
</p>
</div>
</div>
</div>
</Panel> </Panel>
{/* Terminal panel */} {/* Terminal panel */}
@@ -94,9 +81,7 @@ export function WorkspaceShell({ files }: WorkspaceShellProps) {
<> <>
<PanelResizeHandle className="h-1 bg-border transition-colors hover:bg-accent" /> <PanelResizeHandle className="h-1 bg-border transition-colors hover:bg-accent" />
<Panel defaultSize={30} minSize={10} maxSize={70}> <Panel defaultSize={30} minSize={10} maxSize={70}>
<div className="flex h-full w-full items-center justify-center border-r border-border text-sm text-muted-foreground"> <TerminalPanel />
Terminal placeholder
</div>
</Panel> </Panel>
</> </>
)} )}
@@ -0,0 +1,71 @@
import { FitAddon } from "@xterm/addon-fit";
import { Terminal } from "@xterm/xterm";
import { useEffect, useRef, useState } from "react";
export function TerminalPanel() {
const containerRef = useRef<HTMLDivElement>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let terminal: Terminal | undefined;
let fitAddon: FitAddon | undefined;
try {
terminal = new Terminal({
cursorBlink: true,
convertEol: true,
fontFamily:
'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 12,
theme: {
background: "#171717",
foreground: "#e5e5e5",
},
});
fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(container);
terminal.writeln("codespace terminal");
terminal.writeln("Mock session initialized. No process is attached.");
terminal.writeln("");
terminal.writeln("$ pnpm test");
terminal.writeln("mock: 3 files checked, 0 failures");
terminal.write("$ ");
fitAddon.fit();
} catch {
setFailed(true);
terminal?.dispose();
fitAddon?.dispose();
return;
}
const handleResize = () => {
fitAddon?.fit();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
terminal?.dispose();
fitAddon?.dispose();
};
}, []);
if (failed) {
return (
<section className="flex h-full w-full items-center justify-center bg-background text-sm text-muted-foreground">
Terminal failed to initialize
</section>
);
}
return (
<section className="h-full w-full overflow-hidden bg-[#171717] p-2">
<div ref={containerRef} className="h-full w-full" />
</section>
);
}
+1
View File
@@ -1,5 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "@xterm/xterm/css/xterm.css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));