From 8f4680c0e91209ff27e11ff216bee75f712f6f31 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:13:55 +0800 Subject: [PATCH] refactor(web): unify design system with shadcn components and semantic tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add success/warning/info semantic design tokens to globals.css - Replace all raw HTML elements (input, select, textarea, button, label) with shadcn components (Input, Select, Textarea, Button, Label, Dialog) across settings, issues, agents, inbox, knowledge-base, and pair pages - Replace all hardcoded Tailwind colors with design tokens (text-red-500 → text-destructive, text-green-600 → text-success, etc.) - Extract shared ActorAvatar component to packages/ui/components/common - Update status and priority configs to use semantic tokens - Update CLAUDE.md with component organization guidelines - Fix login page tests to use label-based queries Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 3 + apps/web/app/(auth)/login/page.test.tsx | 16 +- .../(dashboard)/_components/app-sidebar.tsx | 118 +++---- apps/web/app/(dashboard)/agents/page.tsx | 319 +++++++++--------- apps/web/app/(dashboard)/inbox/page.tsx | 13 +- apps/web/app/(dashboard)/issues/[id]/page.tsx | 125 +++---- .../(dashboard)/issues/_config/priority.ts | 8 +- .../app/(dashboard)/issues/_config/status.ts | 8 +- apps/web/app/(dashboard)/issues/page.tsx | 180 +++++----- .../app/(dashboard)/knowledge-base/page.tsx | 10 +- apps/web/app/(dashboard)/settings/page.tsx | 135 ++++---- apps/web/app/pair/local/page.tsx | 45 ++- packages/ui/package.json | 1 + .../ui/src/components/common/actor-avatar.tsx | 44 +++ packages/ui/src/styles/globals.css | 12 + 15 files changed, 557 insertions(+), 480 deletions(-) create mode 100644 packages/ui/src/components/common/actor-avatar.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 57c1342a..13c0adb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,6 +62,9 @@ docker compose down # Stop PostgreSQL ## 5. UI/UX Rules - Prefer `packages/ui` shadcn components over custom implementations. +- **shadcn official components** → `packages/ui/src/components/ui/` — keep this directory clean; install missing components via `npx shadcn add`, do not mix in business code. +- **Shared business components & utils** → `packages/ui/src/components/common/` — reusable project-level UI components (e.g. StatusBadge, PriorityIcon) and shared utilities live here. +- Use shadcn design tokens for styling (e.g. `bg-primary`, `text-muted-foreground`, `text-destructive`). Avoid hardcoded color values (e.g. `text-red-500`, `bg-gray-100`). - Do not introduce extra state (useState, context, reducers) unless explicitly required by the design. - Pay close attention to **overflow** (truncate long text, scrollable containers), **alignment**, and **spacing** consistency. - When unsure about interaction or state design, ask — the user will provide direction. diff --git a/apps/web/app/(auth)/login/page.test.tsx b/apps/web/app/(auth)/login/page.test.tsx index d9d97164..84f484ef 100644 --- a/apps/web/app/(auth)/login/page.test.tsx +++ b/apps/web/app/(auth)/login/page.test.tsx @@ -44,9 +44,9 @@ describe("LoginPage", () => { expect(screen.getByText("Multica")).toBeInTheDocument(); expect(screen.getByText("AI-native task management")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("Email")).toBeInTheDocument(); - expect(screen.getByPlaceholderText("Name")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Sign in" })).toBeInTheDocument(); + expect(screen.getByLabelText("Email")).toBeInTheDocument(); + expect(screen.getByLabelText("Name")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /sign in/i })).toBeInTheDocument(); }); it("does not call login when email is empty", async () => { @@ -64,8 +64,8 @@ describe("LoginPage", () => { const user = userEvent.setup(); render(); - await user.type(screen.getByPlaceholderText("Email"), "test@multica.ai"); - await user.type(screen.getByPlaceholderText("Name"), "Test User"); + await user.type(screen.getByLabelText("Email"), "test@multica.ai"); + await user.type(screen.getByLabelText("Name"), "Test User"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { @@ -78,7 +78,7 @@ describe("LoginPage", () => { const user = userEvent.setup(); render(); - await user.type(screen.getByPlaceholderText("Email"), "test@multica.ai"); + await user.type(screen.getByLabelText("Email"), "test@multica.ai"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { @@ -92,7 +92,7 @@ describe("LoginPage", () => { const user = userEvent.setup(); render(); - await user.type(screen.getByPlaceholderText("Email"), "test@multica.ai"); + await user.type(screen.getByLabelText("Email"), "test@multica.ai"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { @@ -105,7 +105,7 @@ describe("LoginPage", () => { const user = userEvent.setup(); render(); - await user.type(screen.getByPlaceholderText("Email"), "test@multica.ai"); + await user.type(screen.getByLabelText("Email"), "test@multica.ai"); await user.click(screen.getByRole("button", { name: "Sign in" })); await waitFor(() => { diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 406f4713..b8999e26 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -26,6 +26,17 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@multica/ui/components/ui/sidebar"; +import { Input } from "@multica/ui/components/ui/input"; +import { Label } from "@multica/ui/components/ui/label"; +import { Button } from "@multica/ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@multica/ui/components/ui/dialog"; import { useAuth } from "../../../lib/auth-context"; import { useTabStore } from "../../../lib/tab-store"; @@ -165,7 +176,7 @@ export function AppSidebar() { setShowMenu(false); logout(); }} - className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-red-500 hover:bg-accent" + className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive hover:bg-accent" > Sign out @@ -230,66 +241,57 @@ export function AppSidebar() { {/* Create Workspace Dialog */} - {showCreateDialog && ( - <> -
setShowCreateDialog(false)} - /> -
-
-

- Create workspace -

-

- Create a new workspace for your team. -

+ + + + Create workspace + + Create a new workspace for your team. + + +
+
+ + handleNameChange(e.target.value)} + placeholder="My Workspace" + className="mt-1" + />
-
-
- - handleNameChange(e.target.value)} - placeholder="My Workspace" - className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" - /> -
-
- - setNewSlug(e.target.value)} - placeholder="my-workspace" - className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" - /> -
-
-
- - +
+ + setNewSlug(e.target.value)} + placeholder="my-workspace" + className="mt-1" + />
- - )} + + + + + +
); } diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index 8879788d..64c8104a 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -12,7 +12,6 @@ import { Timer, Trash2, Save, - X, Key, Link2, Clock, @@ -34,6 +33,18 @@ import type { CreateAgentRequest, UpdateAgentRequest, } from "@multica/types"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@multica/ui/components/ui/dialog"; +import { Button } from "@multica/ui/components/ui/button"; +import { Input } from "@multica/ui/components/ui/input"; +import { Textarea } from "@multica/ui/components/ui/textarea"; +import { Label } from "@multica/ui/components/ui/label"; import { api } from "../../../lib/api"; import { useAuth } from "../../../lib/auth-context"; import { useWSEvent } from "../../../lib/ws-context"; @@ -44,18 +55,18 @@ import { useWSEvent } from "../../../lib/ws-context"; const statusConfig: Record = { idle: { label: "Idle", color: "text-muted-foreground", dot: "bg-muted-foreground" }, - working: { label: "Working", color: "text-green-600", dot: "bg-green-500" }, - blocked: { label: "Blocked", color: "text-yellow-600", dot: "bg-yellow-500" }, - error: { label: "Error", color: "text-red-600", dot: "bg-red-500" }, + working: { label: "Working", color: "text-success", dot: "bg-success" }, + blocked: { label: "Blocked", color: "text-warning", dot: "bg-warning" }, + error: { label: "Error", color: "text-destructive", dot: "bg-destructive" }, offline: { label: "Offline", color: "text-muted-foreground/50", dot: "bg-muted-foreground/40" }, }; const taskStatusConfig: Record = { queued: { label: "Queued", icon: Clock, color: "text-muted-foreground" }, - dispatched: { label: "Dispatched", icon: Play, color: "text-blue-500" }, - running: { label: "Running", icon: Loader2, color: "text-green-500" }, - completed: { label: "Completed", icon: CheckCircle2, color: "text-green-600" }, - failed: { label: "Failed", icon: XCircle, color: "text-red-500" }, + dispatched: { label: "Dispatched", icon: Play, color: "text-info" }, + running: { label: "Running", icon: Loader2, color: "text-success" }, + completed: { label: "Completed", icon: CheckCircle2, color: "text-success" }, + failed: { label: "Failed", icon: XCircle, color: "text-destructive" }, cancelled: { label: "Cancelled", icon: XCircle, color: "text-muted-foreground" }, }; @@ -120,55 +131,49 @@ function CreateAgentDialog({ }; return ( - <> -
-
-
-

Create Agent

- -
-

- Create a new AI agent for your workspace. -

+ { if (!v) onClose(); }}> + + + Create Agent + + Create a new AI agent for your workspace. + + -
+
- - Name + setName(e.target.value)} placeholder="e.g. Deep Research Agent" - className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + className="mt-1" onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
- - Description + setDescription(e.target.value)} placeholder="What does this agent do?" - className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + className="mt-1" />
- +
-
- + {runtimeOpen && ( <> @@ -217,7 +222,7 @@ function CreateAgentDialog({
{device.name} {device.runtime_mode === "cloud" && ( - + Cloud )} @@ -226,7 +231,7 @@ function CreateAgentDialog({
@@ -238,23 +243,19 @@ function CreateAgentDialog({
-
- - -
-
- + + + + ); } @@ -340,21 +341,21 @@ function SkillsTab({

{isDirty && ( - + )}
-