diff --git a/CLAUDE.md b/CLAUDE.md index 664541a0..6a56904e 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/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index 3d76dd87..34b2a311 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -3,6 +3,17 @@ import { Suspense, useState } from "react"; import { useSearchParams } from "next/navigation"; import { useAuth } from "../../../lib/auth-context"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "@multica/ui/components/ui/card"; +import { Input } from "@multica/ui/components/ui/input"; +import { Button } from "@multica/ui/components/ui/button"; +import { Label } from "@multica/ui/components/ui/label"; function LoginPageContent() { const { login, isLoading } = useAuth(); @@ -30,38 +41,51 @@ function LoginPageContent() { return (
-
-

Multica

-

AI-native task management

- -
- setName(e.target.value)} - className="w-full rounded-md border bg-background px-3 py-2 text-sm" - /> - setEmail(e.target.value)} - className="w-full rounded-md border bg-background px-3 py-2 text-sm" - required - /> -
- - {error &&

{error}

} - - -
+ + + Multica + AI-native task management + + +
+
+ + setName(e.target.value)} + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+ {error && ( +

{error}

+ )} +
+
+ + + +
); } 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 && ( - + )}
-