Merge branch 'feat/web'
# Conflicts: # README.md
This commit is contained in:
@@ -24,3 +24,11 @@ coverage.out
|
||||
# Local environment
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Superpowers working artifacts
|
||||
.superpowers/
|
||||
|
||||
# Frontend (web/)
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
web/.env*.local
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
# Web Frontend Initialization Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. This repository also has a stricter project rule: all code generation/refactoring must be delegated to Codex MCP unless the change is trivial documentation/config. Every Codex MCP call must include `model: "kimi/kimi-k2.7-code"`, `sandbox: "danger-full-access"`, and `approval-policy: "on-failure"`.
|
||||
|
||||
**Goal:** Initialize `web/` as an independent pnpm Vite + React 19 + TypeScript frontend and deliver the first runnable codespace IDE shell with a left file explorer, center Monaco editor, and bottom xterm terminal.
|
||||
|
||||
**Architecture:** The frontend is a standalone project under `web/`. `app` wires providers and routing, `pages/workspace` owns mock workspace data, layout components own resizable panel geometry, editor/terminal wrappers isolate third-party lifecycle code, Zustand stores only UI state, and TanStack Query is wired as infrastructure without real backend hooks.
|
||||
|
||||
**Tech Stack:** pnpm, Vite, React 19, TypeScript, React Router v7, Zustand, TanStack Query, Tailwind CSS v4, shadcn/ui-style components, Radix UI primitives, Monaco, xterm.js, react-resizable-panels, lucide-react, react-hook-form, zod.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Package manager: `pnpm`.
|
||||
- Scope: IDE shell skeleton, not a full API-connected MVP.
|
||||
- Layout: VS Code-style shell with left file explorer, center editor, bottom terminal.
|
||||
- Internationalization: not included in the initial scaffold.
|
||||
- Backend API: provider/client reserved files only; no real API calls in the first implementation.
|
||||
- Tests: use build/lint/type validation first; do not add a test framework until real business logic appears.
|
||||
- Do not alter existing Go API signatures, config keys, or backend behavior.
|
||||
- Replace `web/.gitkeep` with the frontend project.
|
||||
- Update `.gitignore` for frontend build artifacts and local brainstorming artifacts.
|
||||
- Code implementation must be performed through Codex MCP using `model: "kimi/kimi-k2.7-code"`, `sandbox: "danger-full-access"`, and `approval-policy: "on-failure"`.
|
||||
- If using xterm.js current packages, prefer `@xterm/xterm` and `@xterm/addon-fit`; this satisfies the xterm.js requirement while avoiding stale package names.
|
||||
|
||||
---
|
||||
|
||||
## File Structure Map
|
||||
|
||||
### Root files
|
||||
|
||||
- Modify `.gitignore`: ignore `.superpowers/`, `web/node_modules/`, `web/dist/`, and Vite local env files.
|
||||
- Keep Go files unchanged.
|
||||
|
||||
### Frontend project files
|
||||
|
||||
- Create `web/package.json`: pnpm scripts and dependencies.
|
||||
- Create `web/pnpm-lock.yaml`: generated by `pnpm install`.
|
||||
- Create `web/index.html`: Vite entry document.
|
||||
- Create `web/vite.config.ts`: React + Tailwind v4 Vite plugins and `@` alias.
|
||||
- Create `web/tsconfig.json`, `web/tsconfig.app.json`, `web/tsconfig.node.json`: TypeScript project config.
|
||||
- Create `web/eslint.config.js`: ESLint config from Vite React TS baseline.
|
||||
- Create `web/components.json`: shadcn/ui-compatible configuration.
|
||||
- Create `web/src/main.tsx`: React root bootstrap.
|
||||
- Create `web/src/styles/index.css`: Tailwind v4 import, theme variables, shell base styles, xterm CSS import if needed.
|
||||
- Create `web/src/app/App.tsx`: top-level app component.
|
||||
- Create `web/src/app/router.tsx`: React Router route definitions.
|
||||
- Create `web/src/app/providers.tsx`: QueryClientProvider and router provider composition.
|
||||
- Create `web/src/app/AppErrorBoundary.tsx`: small generic app boundary.
|
||||
- Create `web/src/pages/workspace/WorkspacePage.tsx`: page-level composition.
|
||||
- Create `web/src/pages/workspace/mock-data.ts`: mock files and helper lookup.
|
||||
- Create `web/src/components/layout/WorkspaceShell.tsx`: resizable shell layout.
|
||||
- Create `web/src/components/editor/EditorPanel.tsx`: Monaco wrapper.
|
||||
- Create `web/src/components/terminal/TerminalPanel.tsx`: xterm wrapper.
|
||||
- Create `web/src/components/workspace/FileExplorerPanel.tsx`: static file tree UI.
|
||||
- Create `web/src/components/workspace/StatusBar.tsx`: bottom status display.
|
||||
- Create `web/src/components/ui/button.tsx`: minimal shadcn-style button.
|
||||
- Create `web/src/components/ui/separator.tsx`: Radix separator wrapper.
|
||||
- Create `web/src/components/ui/scroll-area.tsx`: Radix scroll area wrapper.
|
||||
- Create `web/src/hooks/use-mobile.ts`: optional shadcn-compatible hook if generated by the CLI.
|
||||
- Create `web/src/lib/api/client.ts`: base API URL and typed fetch helper reserved for future calls.
|
||||
- Create `web/src/lib/store/workspace-ui-store.ts`: Zustand UI state.
|
||||
- Create `web/src/lib/utils.ts`: `cn()` helper.
|
||||
- Create `web/README.md`: frontend commands and scope.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Scaffold the pnpm Vite React TypeScript project
|
||||
|
||||
**Files:**
|
||||
- Delete: `web/.gitkeep`
|
||||
- Create: `web/package.json`
|
||||
- Create: `web/index.html`
|
||||
- Create: `web/vite.config.ts`
|
||||
- Create: `web/tsconfig.json`
|
||||
- Create: `web/tsconfig.app.json`
|
||||
- Create: `web/tsconfig.node.json`
|
||||
- Create: `web/eslint.config.js`
|
||||
- Create: `web/src/main.tsx`
|
||||
- Create: `web/src/styles/index.css`
|
||||
- Modify: `.gitignore`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces scripts: `pnpm dev`, `pnpm lint`, `pnpm build`, `pnpm preview`.
|
||||
- Produces alias: `@/*` maps to `web/src/*`.
|
||||
- Produces CSS entry: `src/styles/index.css` imported by `src/main.tsx`.
|
||||
|
||||
- [ ] **Step 1: Verify starting state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
find web -maxdepth 2 -type f -print | sort
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
web/.gitkeep
|
||||
```
|
||||
|
||||
`git status --short` may show the committed plan only if this plan has not been committed. There must be no existing frontend project files in `web/`.
|
||||
|
||||
- [ ] **Step 2: Run Codex to scaffold the frontend project**
|
||||
|
||||
Call Codex MCP with these exact parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi/kimi-k2.7-code",
|
||||
"sandbox": "danger-full-access",
|
||||
"approval-policy": "on-failure",
|
||||
"cwd": "/Users/taochen/llm/codespace/.claude/worktrees/feat+web",
|
||||
"prompt": "## Context\n- Repository: Go backend with empty web/.gitkeep.\n- Design spec: docs/superpowers/specs/2026-07-02-web-frontend-init-design.md.\n- Plan: docs/superpowers/plans/2026-07-02-web-frontend-init.md.\n- Tech Stack: pnpm, Vite, React 19, TypeScript, Tailwind CSS v4.\n\n## Task\nScaffold web/ as a standalone Vite React TypeScript project.\nSteps:\n1. Remove web/.gitkeep.\n2. Create the Vite React TS project files directly under web/.\n3. Use React 19 and React DOM 19.\n4. Add Tailwind CSS v4 via @tailwindcss/vite.\n5. Configure @ alias to web/src.\n6. Import src/styles/index.css from src/main.tsx.\n7. Update .gitignore to ignore .superpowers/, web/node_modules/, web/dist/, and web/.env*.local.\n\n## Constraints\n- Do not modify Go backend files.\n- Do not add react-i18next.\n- Do not add test frameworks.\n- Keep the initial app minimal; later tasks build the IDE shell.\n- No new root package.json. web/ is the only Node project.\n\n## Acceptance\n- cd web && pnpm install succeeds.\n- cd web && pnpm lint succeeds.\n- cd web && pnpm build succeeds.\n- go test ./... still succeeds from repository root."
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Install dependencies**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm install
|
||||
```
|
||||
|
||||
Expected: `pnpm-lock.yaml` is created and install exits with status 0.
|
||||
|
||||
- [ ] **Step 4: Validate scaffold**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm lint
|
||||
cd web && pnpm build
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
pnpm lint exits 0
|
||||
pnpm build exits 0
|
||||
go test ./... exits 0
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit scaffold**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add .gitignore web
|
||||
git commit -m "feat: scaffold web frontend" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Expected: one commit containing only frontend scaffold files and `.gitignore` changes.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add frontend dependencies and shadcn-style UI foundation
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/package.json`
|
||||
- Modify: `web/pnpm-lock.yaml`
|
||||
- Create: `web/components.json`
|
||||
- Modify: `web/src/styles/index.css`
|
||||
- Create: `web/src/lib/utils.ts`
|
||||
- Create: `web/src/components/ui/button.tsx`
|
||||
- Create: `web/src/components/ui/separator.tsx`
|
||||
- Create: `web/src/components/ui/scroll-area.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces `cn(...inputs: ClassValue[]): string` from `@/lib/utils`.
|
||||
- Produces `Button`, `buttonVariants` from `@/components/ui/button`.
|
||||
- Produces `Separator` from `@/components/ui/separator`.
|
||||
- Produces `ScrollArea`, `ScrollBar` from `@/components/ui/scroll-area`.
|
||||
|
||||
- [ ] **Step 1: Run Codex to add dependencies and UI foundation**
|
||||
|
||||
Call Codex MCP with these exact parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi/kimi-k2.7-code",
|
||||
"sandbox": "danger-full-access",
|
||||
"approval-policy": "on-failure",
|
||||
"cwd": "/Users/taochen/llm/codespace/.claude/worktrees/feat+web",
|
||||
"prompt": "## Context\n- web/ is a Vite React 19 TypeScript project from Task 1.\n- Design requires React Router v7, Zustand, TanStack Query, shadcn/ui-style primitives, Monaco, xterm.js, resizable panels, lucide-react, react-hook-form, zod.\n\n## Task\nAdd runtime dependencies and minimal shadcn/ui-compatible foundation.\nSteps:\n1. Add dependencies: react-router, zustand, @tanstack/react-query, @monaco-editor/react, @xterm/xterm, @xterm/addon-fit, react-resizable-panels, lucide-react, react-hook-form, zod, @hookform/resolvers, class-variance-authority, clsx, tailwind-merge, @radix-ui/react-separator, @radix-ui/react-scroll-area.\n2. Create components.json with style new-york, rsc false, tsx true, Tailwind CSS path src/styles/index.css, baseColor neutral, cssVariables true, iconLibrary lucide, and aliases components/utils/ui/lib/hooks.\n3. Create src/lib/utils.ts exporting cn().\n4. Create minimal shadcn-style button, separator, and scroll-area components.\n5. Ensure src/styles/index.css has Tailwind v4 import, CSS variables for dark IDE shell, and base body/root sizing.\n\n## Constraints\n- Do not add react-i18next.\n- Do not add a test framework.\n- Do not bulk-add shadcn components beyond button, separator, scroll-area.\n- Keep component APIs conventional for shadcn so future CLI-generated components fit.\n\n## Acceptance\n- cd web && pnpm install succeeds if package.json changed.\n- cd web && pnpm lint succeeds.\n- cd web && pnpm build succeeds."
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate dependency foundation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm install
|
||||
cd web && pnpm lint
|
||||
cd web && pnpm build
|
||||
```
|
||||
|
||||
Expected: all commands exit 0.
|
||||
|
||||
- [ ] **Step 3: Inspect dependency scope**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm list --depth 0
|
||||
```
|
||||
|
||||
Expected: the output includes the libraries listed in Step 1 and does not include `react-i18next`.
|
||||
|
||||
- [ ] **Step 4: Commit UI foundation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add web/package.json web/pnpm-lock.yaml web/components.json web/src/styles/index.css web/src/lib/utils.ts web/src/components/ui
|
||||
git commit -m "feat: add web ui foundation" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Expected: one commit containing dependency, Tailwind, shadcn-compatible UI, and utility changes.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Wire app providers, router, mock data, and UI store
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/main.tsx`
|
||||
- Create: `web/src/app/App.tsx`
|
||||
- Create: `web/src/app/AppErrorBoundary.tsx`
|
||||
- Create: `web/src/app/providers.tsx`
|
||||
- Create: `web/src/app/router.tsx`
|
||||
- Create: `web/src/pages/workspace/WorkspacePage.tsx`
|
||||
- Create: `web/src/pages/workspace/mock-data.ts`
|
||||
- Create: `web/src/lib/api/client.ts`
|
||||
- Create: `web/src/lib/store/workspace-ui-store.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces `App(): JSX.Element`.
|
||||
- Produces `AppProviders(): JSX.Element`.
|
||||
- Produces `router` from `@/app/router`.
|
||||
- Produces `type MockFileNode` with fields `name`, `path`, `kind`, optional `language`, optional `content`, optional `children`.
|
||||
- Produces `mockWorkspaceFiles: MockFileNode[]`.
|
||||
- Produces `findFileByPath(files: MockFileNode[], path: string): MockFileNode | undefined`.
|
||||
- Produces `useWorkspaceUiStore` with `activeFilePath`, `terminalVisible`, `sidebarCollapsed`, `setActiveFilePath`, `toggleTerminal`, and `toggleSidebar`.
|
||||
- Produces `apiClient(path: string, init?: RequestInit): Promise<Response>` for future backend calls; it must not be used for real calls in this task.
|
||||
|
||||
- [ ] **Step 1: Run Codex to add app infrastructure**
|
||||
|
||||
Call Codex MCP with these exact parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi/kimi-k2.7-code",
|
||||
"sandbox": "danger-full-access",
|
||||
"approval-policy": "on-failure",
|
||||
"cwd": "/Users/taochen/llm/codespace/.claude/worktrees/feat+web",
|
||||
"prompt": "## Context\n- web/ has Vite React TS, Tailwind v4, shadcn-style UI primitives, and dependencies installed.\n- The design requires app/providers/router separation, mock workspace data, Zustand UI state, and TanStack Query infrastructure only.\n\n## Task\nCreate app infrastructure without implementing the full IDE shell yet.\nSteps:\n1. Create AppErrorBoundary as a small class error boundary rendering a dark fallback panel.\n2. Create providers.tsx with a QueryClient and RouterProvider.\n3. Create router.tsx with a single route `/` rendering WorkspacePage.\n4. Create App.tsx returning AppProviders.\n5. Update main.tsx to render App inside React.StrictMode and import styles.\n6. Create WorkspacePage with a simple semantic shell that displays the page title and confirms the mock active file path.\n7. Create mock-data.ts with a small nested file tree and findFileByPath(). Include at least package.json, src/main.tsx, internal/api/router.go, and README.md as mock files.\n8. Create workspace-ui-store.ts using Zustand with activeFilePath defaulting to src/main.tsx, terminalVisible true, sidebarCollapsed false, and the required actions.\n9. Create lib/api/client.ts with a VITE_API_BASE_URL fallback of empty string and apiClient() returning fetch(`${baseUrl}${path}`, init).\n\n## Constraints\n- Do not call the real backend.\n- Do not add API query hooks.\n- Do not implement Monaco or xterm in this task.\n- Keep UI state separate from mock server data.\n\n## Acceptance\n- cd web && pnpm lint succeeds.\n- cd web && pnpm build succeeds.\n- Running the app renders the WorkspacePage without runtime import errors."
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate app infrastructure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm lint
|
||||
cd web && pnpm build
|
||||
```
|
||||
|
||||
Expected: both commands exit 0.
|
||||
|
||||
- [ ] **Step 3: Commit app infrastructure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add web/src/main.tsx web/src/app web/src/pages/workspace web/src/lib/api web/src/lib/store
|
||||
git commit -m "feat: wire web app infrastructure" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Expected: one commit containing app wiring, mock data, API reserved file, and store.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Build the resizable workspace shell with file explorer and status bar
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/pages/workspace/WorkspacePage.tsx`
|
||||
- Create: `web/src/components/layout/WorkspaceShell.tsx`
|
||||
- Create: `web/src/components/workspace/FileExplorerPanel.tsx`
|
||||
- Create: `web/src/components/workspace/StatusBar.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes `MockFileNode`, `mockWorkspaceFiles`, and `findFileByPath()` from `@/pages/workspace/mock-data`.
|
||||
- Consumes `useWorkspaceUiStore` from `@/lib/store/workspace-ui-store`.
|
||||
- Produces `WorkspaceShell({ files }: { files: MockFileNode[] }): JSX.Element`.
|
||||
- Produces `FileExplorerPanel({ files }: { files: MockFileNode[] }): JSX.Element`.
|
||||
- Produces `StatusBar({ activeFilePath }: { activeFilePath: string }): JSX.Element`.
|
||||
|
||||
- [ ] **Step 1: Run Codex to implement shell layout**
|
||||
|
||||
Call Codex MCP with these exact parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi/kimi-k2.7-code",
|
||||
"sandbox": "danger-full-access",
|
||||
"approval-policy": "on-failure",
|
||||
"cwd": "/Users/taochen/llm/codespace/.claude/worktrees/feat+web",
|
||||
"prompt": "## Context\n- App infrastructure and mock data exist from Task 3.\n- Layout choice is A: left file explorer, center editor area, bottom terminal area.\n- Editor and terminal are not implemented yet; use local visual panels in this task that will be replaced by Task 5.\n\n## Task\nBuild the resizable workspace shell with file explorer and status bar.\nSteps:\n1. Create WorkspaceShell using react-resizable-panels. Use a horizontal group: left file explorer panel, right main panel. Use a nested vertical group inside the right main panel: editor region above terminal region.\n2. Create FileExplorerPanel that renders the nested MockFileNode tree, highlights the active file, and calls setActiveFilePath(path) when a file is selected. Directories are display-only in this task.\n3. Create StatusBar showing active file path, mock workspace name `codespace`, and terminal visibility.\n4. Update WorkspacePage to render WorkspaceShell with mockWorkspaceFiles.\n5. Add simple header controls using existing Button and lucide-react icons: collapse sidebar and toggle terminal.\n\n## Constraints\n- Do not implement Monaco or xterm yet.\n- Do not call backend APIs.\n- Keep panel state in Zustand.\n- Keep layout readable; avoid nested conditional rendering deeper than necessary.\n\n## Acceptance\n- The page visually has left explorer, main editor region, bottom terminal region, and status bar.\n- Selecting a mock file updates active file state and status bar.\n- cd web && pnpm lint succeeds.\n- cd web && pnpm build succeeds."
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate shell layout**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm lint
|
||||
cd web && pnpm build
|
||||
```
|
||||
|
||||
Expected: both commands exit 0.
|
||||
|
||||
- [ ] **Step 3: Commit shell layout**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add web/src/pages/workspace/WorkspacePage.tsx web/src/components/layout web/src/components/workspace
|
||||
git commit -m "feat: add resizable workspace shell" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Expected: one commit containing layout, explorer, and status UI.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add Monaco editor and xterm terminal wrappers
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/src/components/layout/WorkspaceShell.tsx`
|
||||
- Create: `web/src/components/editor/EditorPanel.tsx`
|
||||
- Create: `web/src/components/terminal/TerminalPanel.tsx`
|
||||
- Modify: `web/src/styles/index.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes `MockFileNode` from `@/pages/workspace/mock-data`.
|
||||
- Produces `EditorPanel({ file }: { file?: MockFileNode }): JSX.Element`.
|
||||
- Produces `TerminalPanel(): JSX.Element`.
|
||||
- `WorkspaceShell` must pass the active file node into `EditorPanel` and render `TerminalPanel` only when `terminalVisible` is true.
|
||||
|
||||
- [ ] **Step 1: Run Codex to add Monaco and xterm wrappers**
|
||||
|
||||
Call Codex MCP with these exact parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi/kimi-k2.7-code",
|
||||
"sandbox": "danger-full-access",
|
||||
"approval-policy": "on-failure",
|
||||
"cwd": "/Users/taochen/llm/codespace/.claude/worktrees/feat+web",
|
||||
"prompt": "## Context\n- WorkspaceShell exists with visual editor and terminal regions from Task 4.\n- Dependencies include @monaco-editor/react, @xterm/xterm, and @xterm/addon-fit.\n\n## Task\nReplace visual regions with isolated Monaco and xterm wrappers.\nSteps:\n1. Create EditorPanel.tsx using @monaco-editor/react. It receives file?: MockFileNode. If no file or file.kind is not `file`, render a simple empty-state panel. If Monaco reports onValidate markers, keep the data local and do not add global state. Use theme `vs-dark`, automatic layout, readOnly false, minimap disabled, and language from file.language with fallback `typescript`.\n2. Create TerminalPanel.tsx using @xterm/xterm and @xterm/addon-fit. Initialize once in useEffect, write a welcome banner and mock command output, fit on mount and window resize, and dispose terminal/addon/listener on unmount. If initialization throws, render a fallback panel saying terminal failed to initialize.\n3. Import xterm CSS from @xterm/xterm/css/xterm.css in styles or the terminal component, choosing the approach that keeps bundling clean.\n4. Update WorkspaceShell to find the active MockFileNode by path and pass it to EditorPanel. Render TerminalPanel in the bottom panel only when terminalVisible is true.\n\n## Constraints\n- Do not connect to real processes.\n- Do not write terminal output to Zustand.\n- Do not call backend APIs.\n- Do not make editor content persistent.\n- Keep wrappers responsible for third-party lifecycle only.\n\n## Acceptance\n- Monaco renders the active mock file content.\n- xterm renders a banner and mock command output.\n- Toggling terminal hides/shows the terminal panel without runtime errors.\n- cd web && pnpm lint succeeds.\n- cd web && pnpm build succeeds."
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate editor and terminal wrappers**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm lint
|
||||
cd web && pnpm build
|
||||
```
|
||||
|
||||
Expected: both commands exit 0.
|
||||
|
||||
- [ ] **Step 3: Commit editor and terminal wrappers**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add web/src/components/layout/WorkspaceShell.tsx web/src/components/editor web/src/components/terminal web/src/styles/index.css
|
||||
git commit -m "feat: add web editor and terminal panels" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Expected: one commit containing Monaco/xterm wrappers and shell integration.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Add frontend README and run final verification
|
||||
|
||||
**Files:**
|
||||
- Create: `web/README.md`
|
||||
- Optionally modify: `README.md` only if adding a short pointer to `web/README.md`; do not rewrite backend docs.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces documented commands: `pnpm install`, `pnpm dev`, `pnpm lint`, `pnpm build`.
|
||||
- Documents initial scope: IDE shell only, no backend API integration, no i18n.
|
||||
|
||||
- [ ] **Step 1: Run Codex to add concise frontend docs**
|
||||
|
||||
Call Codex MCP with these exact parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "kimi/kimi-k2.7-code",
|
||||
"sandbox": "danger-full-access",
|
||||
"approval-policy": "on-failure",
|
||||
"cwd": "/Users/taochen/llm/codespace/.claude/worktrees/feat+web",
|
||||
"prompt": "## Context\n- web/ is now a runnable frontend project with a mock IDE shell.\n- Root README documents the Go backend.\n\n## Task\nAdd concise frontend documentation.\nSteps:\n1. Create web/README.md with purpose, scope, commands, and non-goals.\n2. If useful, add a short root README pointer to web/README.md without changing existing backend instructions.\n\n## Constraints\n- Do not claim backend API integration exists.\n- Do not mention i18n as implemented.\n- Keep docs short and factual.\n\n## Acceptance\n- web/README.md explains how to run and build the frontend.\n- cd web && pnpm lint succeeds.\n- cd web && pnpm build succeeds.\n- go test ./... succeeds."
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run final verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd web && pnpm lint
|
||||
cd web && pnpm build
|
||||
go test ./...
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
pnpm lint exits 0
|
||||
pnpm build exits 0
|
||||
go test ./... exits 0
|
||||
git status --short shows only intended documentation changes before the final commit
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit docs and final cleanup**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add web/README.md README.md
|
||||
git commit -m "docs: add web frontend usage" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
If `README.md` was not modified, run:
|
||||
|
||||
```bash
|
||||
git add web/README.md
|
||||
git commit -m "docs: add web frontend usage" -m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Expected: one documentation commit.
|
||||
|
||||
- [ ] **Step 4: Summarize implementation state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git log --oneline --decorate -6
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: recent commits include the spec, scaffold, UI foundation, app infrastructure, shell layout, editor/terminal panels, and docs. Working tree should be clean.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Notes
|
||||
|
||||
### Spec coverage
|
||||
|
||||
- Standalone `web/` project: Task 1.
|
||||
- pnpm: Task 1 and all validation commands.
|
||||
- Vite + React 19 + TypeScript: Task 1.
|
||||
- Tailwind CSS v4 + shadcn/ui style: Task 2.
|
||||
- React Router v7: Task 3.
|
||||
- Zustand UI state: Task 3.
|
||||
- TanStack Query provider only: Task 3.
|
||||
- Layout A: Task 4.
|
||||
- Monaco wrapper: Task 5.
|
||||
- xterm wrapper: Task 5.
|
||||
- react-resizable-panels: Task 4.
|
||||
- lucide-react controls: Task 4.
|
||||
- react-hook-form + zod dependencies: Task 2.
|
||||
- No i18n: Global constraints and Task 2 dependency check.
|
||||
- No backend API integration: Global constraints and Tasks 3-5.
|
||||
- Validation: every task has lint/build checks; Task 1 and Task 6 include Go backend tests.
|
||||
|
||||
### Type consistency
|
||||
|
||||
- `MockFileNode` is produced in Task 3 and consumed in Tasks 4-5.
|
||||
- `useWorkspaceUiStore` fields match the design: `activeFilePath`, `terminalVisible`, `sidebarCollapsed`, `setActiveFilePath`, `toggleTerminal`, `toggleSidebar`.
|
||||
- `WorkspaceShell`, `FileExplorerPanel`, `StatusBar`, `EditorPanel`, and `TerminalPanel` signatures are defined before downstream use.
|
||||
|
||||
### Scope check
|
||||
|
||||
This plan is one coherent implementation unit: a runnable frontend shell. It does not include backend integration, WebSockets, auth, Git, LSP, Docker, or i18n.
|
||||
@@ -0,0 +1,216 @@
|
||||
# Web Frontend Initialization Design
|
||||
|
||||
Date: 2026-07-02
|
||||
Branch: feat/web
|
||||
|
||||
## Goal
|
||||
|
||||
Initialize `web/` as an independent Vite + React 19 + TypeScript frontend project and build the first runnable codespace IDE shell: file explorer on the left, Monaco editor in the center, and xterm terminal at the bottom.
|
||||
|
||||
This design deliberately stops before backend integration. The current Go backend already exposes workspace, file, and process APIs, but the first frontend step should prove the UI structure and technology boundaries without mixing server data, editor state, and terminal state too early.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Package manager: `pnpm`.
|
||||
- Scope: IDE shell skeleton, not a full API-connected MVP.
|
||||
- Layout: VS Code-style shell with left file explorer, center editor, bottom terminal.
|
||||
- Internationalization: not included in the initial scaffold.
|
||||
- Backend API: provider/client placeholders only; no real API calls in the first implementation.
|
||||
- Tests: use build/lint/type validation first; do not add a test framework until real business logic appears.
|
||||
|
||||
## Architecture
|
||||
|
||||
`web/` is a standalone frontend project. It should not modify the Go module or force the backend lifecycle to depend on Node tooling.
|
||||
|
||||
Proposed structure:
|
||||
|
||||
```txt
|
||||
web/
|
||||
src/
|
||||
app/
|
||||
App.tsx
|
||||
router.tsx
|
||||
providers.tsx
|
||||
pages/
|
||||
workspace/
|
||||
WorkspacePage.tsx
|
||||
mock-data.ts
|
||||
components/
|
||||
layout/
|
||||
WorkspaceShell.tsx
|
||||
editor/
|
||||
EditorPanel.tsx
|
||||
terminal/
|
||||
TerminalPanel.tsx
|
||||
workspace/
|
||||
FileExplorerPanel.tsx
|
||||
StatusBar.tsx
|
||||
ui/
|
||||
# shadcn/ui generated components
|
||||
hooks/
|
||||
lib/
|
||||
api/
|
||||
client.ts
|
||||
store/
|
||||
workspace-ui-store.ts
|
||||
utils.ts
|
||||
styles/
|
||||
index.css
|
||||
```
|
||||
|
||||
Boundaries:
|
||||
|
||||
- `app` wires router and providers only.
|
||||
- `pages/workspace` composes the workspace page and owns temporary mock data.
|
||||
- `components/layout` owns panel geometry.
|
||||
- `components/editor` wraps `@monaco-editor/react`.
|
||||
- `components/terminal` wraps `xterm.js` and `xterm-addon-fit`.
|
||||
- `components/workspace` contains file explorer and status UI.
|
||||
- `lib/store` contains Zustand UI state only.
|
||||
- `lib/api` is reserved for future TanStack Query-backed API work.
|
||||
|
||||
## Component Design
|
||||
|
||||
Initial route renders `WorkspacePage`, which renders `WorkspaceShell`.
|
||||
|
||||
```txt
|
||||
WorkspaceShell
|
||||
├─ Header / lightweight activity area
|
||||
├─ ResizablePanelGroup horizontal
|
||||
│ ├─ FileExplorerPanel
|
||||
│ └─ ResizablePanelGroup vertical
|
||||
│ ├─ EditorPanel
|
||||
│ └─ TerminalPanel
|
||||
└─ StatusBar
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- `FileExplorerPanel` displays a static mock file tree and lets the user choose an active file path.
|
||||
- `EditorPanel` renders Monaco with sample content for the active file.
|
||||
- `TerminalPanel` renders xterm with a welcome message and mock command output.
|
||||
- `WorkspaceShell` manages layout composition, not editor or terminal internals.
|
||||
- `StatusBar` shows basic mock workspace/editor state.
|
||||
|
||||
## State and Data Flow
|
||||
|
||||
Use Zustand for UI state:
|
||||
|
||||
- `activeFilePath`
|
||||
- `terminalVisible`
|
||||
- `sidebarCollapsed`
|
||||
- `setActiveFilePath(path)`
|
||||
- `toggleTerminal()`
|
||||
- `toggleSidebar()`
|
||||
|
||||
Use TanStack Query only as infrastructure in this phase by installing and wiring `QueryClientProvider`. Do not create real query hooks yet.
|
||||
|
||||
Reason: UI state and server state are different data. Mixing them during scaffolding creates special cases later when real backend data arrives.
|
||||
|
||||
Mock file data belongs in `pages/workspace/mock-data.ts`. Future API integration can replace this with query hooks without rewriting Monaco, xterm, or panel layout components.
|
||||
|
||||
## Styling and UI System
|
||||
|
||||
Use Tailwind CSS v4 with the Vite plugin:
|
||||
|
||||
- Install `tailwindcss` and `@tailwindcss/vite`.
|
||||
- Configure `vite.config.ts` with `tailwindcss()` and `react()` plugins.
|
||||
- Use `@import "tailwindcss";` in the main CSS file.
|
||||
|
||||
Use shadcn/ui with:
|
||||
|
||||
- style: `new-york`
|
||||
- `tsx: true`
|
||||
- `rsc: false`
|
||||
- base color: `neutral`
|
||||
- CSS variables enabled
|
||||
- aliases:
|
||||
- `@/components`
|
||||
- `@/components/ui`
|
||||
- `@/lib`
|
||||
- `@/hooks`
|
||||
|
||||
Only add necessary shadcn components for the shell, such as button, separator, and scroll-area. Do not bulk-install components.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Expected runtime dependencies:
|
||||
|
||||
- `react`
|
||||
- `react-dom`
|
||||
- `react-router`
|
||||
- `zustand`
|
||||
- `@tanstack/react-query`
|
||||
- `@monaco-editor/react`
|
||||
- `xterm`
|
||||
- `xterm-addon-fit`
|
||||
- `react-resizable-panels`
|
||||
- `lucide-react`
|
||||
- `react-hook-form`
|
||||
- `zod`
|
||||
- `@hookform/resolvers`
|
||||
- shadcn support packages such as `class-variance-authority`, `clsx`, and `tailwind-merge`
|
||||
|
||||
Expected dev dependencies:
|
||||
|
||||
- `vite`
|
||||
- `typescript`
|
||||
- `@vitejs/plugin-react`
|
||||
- `tailwindcss`
|
||||
- `@tailwindcss/vite`
|
||||
- ESLint/TypeScript ESLint packages from the Vite React TypeScript template
|
||||
|
||||
Do not add `react-i18next` in this phase.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Keep error handling simple:
|
||||
|
||||
- If Monaco fails to load, show an editor fallback panel.
|
||||
- If xterm initialization fails, show a terminal fallback panel.
|
||||
- Use a small app-level error boundary only if it stays generic and does not introduce logging/reporting infrastructure.
|
||||
- Do not add toast-driven retry behavior before real API calls exist.
|
||||
|
||||
## Compatibility and Breakage
|
||||
|
||||
This change should not alter existing backend behavior.
|
||||
|
||||
Expected repository effects:
|
||||
|
||||
- Replace `web/.gitkeep` with a real frontend project.
|
||||
- Add `web/package.json`, `pnpm-lock.yaml`, Vite/TypeScript config, source files, and shadcn/Tailwind config files.
|
||||
- Update `.gitignore` for frontend build artifacts and local brainstorming artifacts if needed.
|
||||
|
||||
No Go API signatures, config keys, or backend tests should change.
|
||||
|
||||
## Validation
|
||||
|
||||
Implementation should verify:
|
||||
|
||||
```sh
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm lint
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Repository baseline should continue to pass:
|
||||
|
||||
```sh
|
||||
go test ./...
|
||||
```
|
||||
|
||||
If the Vite template does not provide a test runner, do not add Vitest during this phase.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Real workspace creation/listing UI.
|
||||
- File read/write API integration.
|
||||
- Process start/stop/restart integration.
|
||||
- WebSocket streaming.
|
||||
- Authentication.
|
||||
- Docker.
|
||||
- Git integration.
|
||||
- LSP integration.
|
||||
- AI agent orchestration UI.
|
||||
- Internationalization.
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# codespace-web
|
||||
|
||||
Frontend IDE shell for the codespace Go backend.
|
||||
|
||||
## Scope
|
||||
|
||||
This package is a runnable **UI scaffold only**. It provides the workspace layout, a mock file explorer, a Monaco editor wrapper, and an xterm terminal wrapper. It does not yet connect to the Go API.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `pnpm install` | Install dependencies |
|
||||
| `pnpm dev` | Start the Vite dev server |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm build` | Type-check and build for production |
|
||||
|
||||
## Stack
|
||||
|
||||
- Vite + React 19 + TypeScript
|
||||
- Tailwind CSS v4
|
||||
- react-router v7
|
||||
- Zustand for local UI state
|
||||
- TanStack Query (provider only; no real API calls yet)
|
||||
- `@monaco-editor/react` editor wrapper
|
||||
- `@xterm/xterm` + `@xterm/addon-fit` terminal wrapper
|
||||
- `react-resizable-panels` for layout splits
|
||||
- `lucide-react` for icons
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Backend API integration (reserved file only at `src/lib/api/client.ts`)
|
||||
- Authentication
|
||||
- WebSocket terminal or process streaming
|
||||
- i18n
|
||||
- Tests (no test framework until business logic is added)
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Codespace</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "codespace-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"lint": "eslint .",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.7.0",
|
||||
"zustand": "^5.0.2",
|
||||
"@tanstack/react-query": "^5.82.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"zod": "^4.0.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"tw-animate-css": "^1.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.2",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vite": "^7.0.2"
|
||||
}
|
||||
}
|
||||
Generated
+3057
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
allowBuilds:
|
||||
esbuild: false
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AppProviders } from "@/app/providers";
|
||||
|
||||
export function App() {
|
||||
return <AppProviders />;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** Minimal error boundary that renders a dark fallback panel. */
|
||||
export class AppErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, message: error.message };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error("Uncaught application error:", error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-2 bg-neutral-950 p-8 text-neutral-100">
|
||||
<h1 className="text-lg font-semibold">Something went wrong</h1>
|
||||
{this.state.message && (
|
||||
<p className="text-sm text-neutral-400">{this.state.message}</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="mt-2 rounded-md border border-neutral-700 px-3 py-1 text-sm text-neutral-300 hover:bg-neutral-800"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider } from "react-router";
|
||||
|
||||
import { router } from "@/app/router";
|
||||
import { AppErrorBoundary } from "@/app/AppErrorBoundary";
|
||||
|
||||
export function AppProviders() {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60_000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<AppErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createBrowserRouter } from "react-router";
|
||||
|
||||
import { WorkspacePage } from "@/pages/workspace/WorkspacePage";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <WorkspacePage />,
|
||||
},
|
||||
]);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { PanelLeftClose, PanelLeftOpen, Terminal } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 { StatusBar } from "@/components/workspace/StatusBar";
|
||||
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
|
||||
import {
|
||||
findFileByPath,
|
||||
type MockFileNode,
|
||||
} from "@/pages/workspace/mock-data";
|
||||
|
||||
interface WorkspaceShellProps {
|
||||
files: MockFileNode[];
|
||||
}
|
||||
|
||||
export function WorkspaceShell({ files }: WorkspaceShellProps) {
|
||||
const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath);
|
||||
const sidebarCollapsed = useWorkspaceUiStore((s) => s.sidebarCollapsed);
|
||||
const terminalVisible = useWorkspaceUiStore((s) => s.terminalVisible);
|
||||
const toggleSidebar = useWorkspaceUiStore((s) => s.toggleSidebar);
|
||||
const toggleTerminal = useWorkspaceUiStore((s) => s.toggleTerminal);
|
||||
const activeFile = findFileByPath(files, activeFilePath);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-background text-foreground">
|
||||
{/* Toolbar */}
|
||||
<header className="flex h-9 shrink-0 items-center border-b border-border bg-sidebar px-3">
|
||||
<h1 className="text-sm font-semibold">codespace</h1>
|
||||
<Separator orientation="vertical" className="mx-3 h-4" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<PanelLeftOpen className="size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<PanelLeftClose className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={toggleTerminal}
|
||||
>
|
||||
<Terminal className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main layout */}
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<PanelGroup direction="horizontal" className="flex-1">
|
||||
{/* Sidebar */}
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<Panel defaultSize={18} minSize={12} maxSize={30}>
|
||||
<FileExplorerPanel files={files} />
|
||||
</Panel>
|
||||
<PanelResizeHandle className="w-1 bg-border transition-colors hover:bg-accent" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Main content area */}
|
||||
<Panel>
|
||||
<PanelGroup direction="vertical">
|
||||
{/* Editor */}
|
||||
<Panel>
|
||||
<EditorPanel file={activeFile} />
|
||||
</Panel>
|
||||
|
||||
{/* Terminal panel */}
|
||||
{terminalVisible && (
|
||||
<>
|
||||
<PanelResizeHandle className="h-1 bg-border transition-colors hover:bg-accent" />
|
||||
<Panel defaultSize={30} minSize={10} maxSize={70}>
|
||||
<TerminalPanel />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
<StatusBar activeFilePath={activeFilePath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ComponentRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
@@ -0,0 +1,99 @@
|
||||
import { ChevronRight, FileCode2, Folder } from "lucide-react";
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { MockFileNode } from "@/pages/workspace/mock-data";
|
||||
|
||||
interface FileExplorerPanelProps {
|
||||
files: MockFileNode[];
|
||||
}
|
||||
|
||||
interface FileTreeNodeProps {
|
||||
node: MockFileNode;
|
||||
depth: number;
|
||||
activeFilePath: string;
|
||||
onSelectFile: (path: string) => void;
|
||||
}
|
||||
|
||||
function FileTreeNode({
|
||||
node,
|
||||
depth,
|
||||
activeFilePath,
|
||||
onSelectFile,
|
||||
}: FileTreeNodeProps) {
|
||||
const isFile = node.kind === "file";
|
||||
const isActive = isFile && node.path === activeFilePath;
|
||||
const paddingLeft = `${depth * 0.875 + 0.75}rem`;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isFile}
|
||||
onClick={() => isFile && onSelectFile(node.path)}
|
||||
className={cn(
|
||||
"flex h-7 w-full items-center gap-1.5 truncate px-2 text-left text-xs text-muted-foreground transition-colors",
|
||||
isFile && "hover:bg-accent hover:text-accent-foreground",
|
||||
!isFile && "cursor-default text-foreground",
|
||||
isActive && "bg-accent text-accent-foreground",
|
||||
)}
|
||||
style={{ paddingLeft }}
|
||||
>
|
||||
{isFile ? (
|
||||
<FileCode2 className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
) : (
|
||||
<>
|
||||
<ChevronRight
|
||||
className="size-3 shrink-0 rotate-90"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Folder className="size-3.5 shrink-0" aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
<span className="truncate">{node.name}</span>
|
||||
</button>
|
||||
{node.children?.length ? (
|
||||
<ul>
|
||||
{node.children.map((child) => (
|
||||
<FileTreeNode
|
||||
key={child.path}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
activeFilePath={activeFilePath}
|
||||
onSelectFile={onSelectFile}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileExplorerPanel({ files }: FileExplorerPanelProps) {
|
||||
const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath);
|
||||
const setActiveFilePath = useWorkspaceUiStore((s) => s.setActiveFilePath);
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-0 flex-col bg-sidebar text-sidebar-foreground">
|
||||
<div className="flex h-9 shrink-0 items-center border-b border-sidebar-border px-3 text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
Explorer
|
||||
</div>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<nav className="py-2" aria-label="Workspace files">
|
||||
<ul>
|
||||
{files.map((node) => (
|
||||
<FileTreeNode
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={0}
|
||||
activeFilePath={activeFilePath}
|
||||
onSelectFile={setActiveFilePath}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Terminal, FileCode2 } from "lucide-react";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store";
|
||||
|
||||
interface StatusBarProps {
|
||||
activeFilePath: string;
|
||||
}
|
||||
|
||||
export function StatusBar({ activeFilePath }: StatusBarProps) {
|
||||
const terminalVisible = useWorkspaceUiStore((s) => s.terminalVisible);
|
||||
const toggleTerminal = useWorkspaceUiStore((s) => s.toggleTerminal);
|
||||
|
||||
return (
|
||||
<footer className="flex h-6 shrink-0 items-center border-t border-border bg-sidebar px-3 text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode2 className="size-3" aria-hidden="true" />
|
||||
<span className="truncate max-w-48">{activeFilePath}</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-2 h-3" />
|
||||
<span className="font-medium">codespace</span>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTerminal}
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded px-1 py-0.5 transition-colors hover:bg-accent hover:text-accent-foreground",
|
||||
terminalVisible && "text-foreground",
|
||||
)}
|
||||
>
|
||||
<Terminal className="size-3" aria-hidden="true" />
|
||||
<span>Terminal</span>
|
||||
<span>{terminalVisible ? "Visible" : "Hidden"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Reserved API client for future backend calls.
|
||||
*
|
||||
* No real network requests are made in this task; later tasks will
|
||||
* wire this helper to actual endpoints.
|
||||
*/
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "";
|
||||
|
||||
export function apiClient(path: string, init?: RequestInit): Promise<Response> {
|
||||
return fetch(`${baseUrl}${path}`, init);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface WorkspaceUiState {
|
||||
activeFilePath: string;
|
||||
terminalVisible: boolean;
|
||||
sidebarCollapsed: boolean;
|
||||
setActiveFilePath: (path: string) => void;
|
||||
toggleTerminal: () => void;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export const useWorkspaceUiStore = create<WorkspaceUiState>((set) => ({
|
||||
activeFilePath: "src/main.tsx",
|
||||
terminalVisible: true,
|
||||
sidebarCollapsed: false,
|
||||
setActiveFilePath: (path) => set({ activeFilePath: path }),
|
||||
toggleTerminal: () => set((s) => ({ terminalVisible: !s.terminalVisible })),
|
||||
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
|
||||
}));
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "@/styles/index.css";
|
||||
import { App } from "@/app/App";
|
||||
|
||||
document.documentElement.classList.add("dark");
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { mockWorkspaceFiles } from "@/pages/workspace/mock-data";
|
||||
import { WorkspaceShell } from "@/components/layout/WorkspaceShell";
|
||||
|
||||
export function WorkspacePage() {
|
||||
return <WorkspaceShell files={mockWorkspaceFiles} />;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
export type MockFileNode = {
|
||||
name: string;
|
||||
path: string;
|
||||
kind: "file" | "dir";
|
||||
language?: string;
|
||||
content?: string;
|
||||
children?: MockFileNode[];
|
||||
};
|
||||
|
||||
const file = (
|
||||
name: string,
|
||||
path: string,
|
||||
language: string,
|
||||
content: string,
|
||||
): MockFileNode => ({ name, path, kind: "file", language, content });
|
||||
|
||||
const dir = (
|
||||
name: string,
|
||||
path: string,
|
||||
children: MockFileNode[],
|
||||
): MockFileNode => ({ name, path, kind: "dir", children });
|
||||
|
||||
export const mockWorkspaceFiles: MockFileNode[] = [
|
||||
file("package.json", "package.json", "json", `{
|
||||
"name": "codespace",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
`),
|
||||
file("README.md", "README.md", "markdown", `# Codespace
|
||||
|
||||
A self-hosted development environment.
|
||||
`),
|
||||
dir("src", "src", [
|
||||
file(
|
||||
"main.tsx",
|
||||
"src/main.tsx",
|
||||
"typescript",
|
||||
`import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "@/app/App";
|
||||
import "@/styles/index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
`,
|
||||
),
|
||||
]),
|
||||
dir("internal", "internal", [
|
||||
dir("api", "internal/api", [
|
||||
file(
|
||||
"router.go",
|
||||
"internal/api/router.go",
|
||||
"go",
|
||||
`package api
|
||||
|
||||
// Router is reserved for future backend routes.
|
||||
type Router struct{}
|
||||
`,
|
||||
),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
|
||||
/** Depth-first search for a node with the given slash-delimited path. */
|
||||
export function findFileByPath(
|
||||
files: MockFileNode[],
|
||||
path: string,
|
||||
): MockFileNode | undefined {
|
||||
for (const node of files) {
|
||||
if (node.path === path) return node;
|
||||
if (node.children) {
|
||||
const found = findFileByPath(node.children, path);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.922 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.205 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply h-full;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import path from "node:path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user