From 0f634981b5cec4bc5c0cf9d9e1cb9f6879f5b792 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:03:45 +0800 Subject: [PATCH] feat: add resizable workspace shell Co-Authored-By: Claude --- web/src/components/layout/WorkspaceShell.tsx | 112 ++++++++++++++++++ .../workspace/FileExplorerPanel.tsx | 99 ++++++++++++++++ web/src/components/workspace/StatusBar.tsx | 39 ++++++ web/src/pages/workspace/WorkspacePage.tsx | 28 +---- 4 files changed, 253 insertions(+), 25 deletions(-) create mode 100644 web/src/components/layout/WorkspaceShell.tsx create mode 100644 web/src/components/workspace/FileExplorerPanel.tsx create mode 100644 web/src/components/workspace/StatusBar.tsx diff --git a/web/src/components/layout/WorkspaceShell.tsx b/web/src/components/layout/WorkspaceShell.tsx new file mode 100644 index 0000000..4dc3b91 --- /dev/null +++ b/web/src/components/layout/WorkspaceShell.tsx @@ -0,0 +1,112 @@ +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 { 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 ( +
+ {/* Toolbar */} +
+

codespace

+ +
+ + +
+
+ + {/* Main layout */} +
+ + {/* Sidebar */} + {!sidebarCollapsed && ( + <> + + + + + + )} + + {/* Main content area */} + + + {/* Editor placeholder */} + +
+
+

Editor placeholder

+
+

+ Active file:{" "} + {activeFilePath} +

+

+ {activeFile + ? `${activeFile.name} | ${activeFile.language ?? "plain text"} | ${activeFile.kind}` + : "No matching mock file"} +

+
+
+
+
+ + {/* Terminal panel */} + {terminalVisible && ( + <> + + +
+ Terminal placeholder +
+
+ + )} +
+
+
+
+ + {/* Status bar */} + +
+ ); +} diff --git a/web/src/components/workspace/FileExplorerPanel.tsx b/web/src/components/workspace/FileExplorerPanel.tsx new file mode 100644 index 0000000..809a0c2 --- /dev/null +++ b/web/src/components/workspace/FileExplorerPanel.tsx @@ -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 ( +
  • + + {node.children?.length ? ( + + ) : null} +
  • + ); +} + +export function FileExplorerPanel({ files }: FileExplorerPanelProps) { + const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath); + const setActiveFilePath = useWorkspaceUiStore((s) => s.setActiveFilePath); + + return ( + + ); +} diff --git a/web/src/components/workspace/StatusBar.tsx b/web/src/components/workspace/StatusBar.tsx new file mode 100644 index 0000000..cd2e632 --- /dev/null +++ b/web/src/components/workspace/StatusBar.tsx @@ -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 ( + + ); +} diff --git a/web/src/pages/workspace/WorkspacePage.tsx b/web/src/pages/workspace/WorkspacePage.tsx index 7f0316e..93e4ec9 100644 --- a/web/src/pages/workspace/WorkspacePage.tsx +++ b/web/src/pages/workspace/WorkspacePage.tsx @@ -1,28 +1,6 @@ -import { findFileByPath, mockWorkspaceFiles } from "@/pages/workspace/mock-data"; -import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store"; +import { mockWorkspaceFiles } from "@/pages/workspace/mock-data"; +import { WorkspaceShell } from "@/components/layout/WorkspaceShell"; export function WorkspacePage() { - const activeFilePath = useWorkspaceUiStore((s) => s.activeFilePath); - const activeFile = findFileByPath(mockWorkspaceFiles, activeFilePath); - - return ( -
    -
    -

    Codespace Workspace

    -
    -
    -

    - Active file: {activeFilePath} -

    -

    - Mock file match:{" "} - - {activeFile - ? `${activeFile.name} (${activeFile.language ?? activeFile.kind})` - : "not found"} - -

    -
    -
    - ); + return ; }