feat(agent-team): Tailwind + shadcn foundation for dashboard redesign

Adds the styling foundation for the UI redesign (vibe-kanban / Magentic-UI /
Langflow references): Tailwind CSS + shadcn/ui primitives, HSL design tokens
(index.css) ported from theme.css, @/ alias, and the App.tsx Board|Pipeline tabs
shell. Extends StateResponse with the backend's existing stages[] for the Board.

theme.css kept transiently until components migrate to Tailwind. No backend
changes. Component fan-out (Board, MapNode, TaskDrawer, TopBar) follows.
This commit is contained in:
Adam Moussa 2026-06-24 13:35:56 -04:00
parent 21f2fe54c1
commit 022befa796
22 changed files with 2630 additions and 36 deletions

File diff suppressed because it is too large Load diff

View file

@ -10,14 +10,25 @@
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc -b --noEmit"
"typecheck": "tsc -b"
},
"dependencies": {
"@dagrejs/dagre": "1.1.4",
"@radix-ui/react-collapsible": "^1.1.14",
"@radix-ui/react-scroll-area": "^1.2.12",
"@radix-ui/react-select": "^2.3.1",
"@radix-ui/react-separator": "^1.1.10",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-tabs": "^1.1.15",
"@radix-ui/react-tooltip": "^1.2.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.21.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-markdown": "9.0.1",
"reactflow": "11.11.4"
"reactflow": "11.11.4",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@testing-library/jest-dom": "6.4.8",
@ -26,7 +37,11 @@
"@types/react": "18.3.5",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "4.3.1",
"autoprefixer": "^10.5.1",
"jsdom": "24.1.3",
"postcss": "^8.5.15",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.5.4",
"vite": "5.4.8",
"vitest": "1.6.0"

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LayoutGrid, Workflow as WorkflowIcon } from "lucide-react";
import {
fetchState,
fetchTask,
@ -7,17 +8,22 @@ import {
type TaskDetail,
type Topology,
} from "./api";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Board } from "./components/Board";
import { PipelineMap } from "./components/PipelineMap";
import { TaskDrawer } from "./components/TaskDrawer";
import { TaskList } from "./components/TaskList";
import { TopBar } from "./components/TopBar";
const POLL_MS = 4000;
type View = "board" | "pipeline";
export default function App() {
const [state, setState] = useState<StateResponse | null>(null);
const [topology, setTopology] = useState<Topology | null>(null);
const [connected, setConnected] = useState(false);
const [view, setView] = useState<View>("board");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [detail, setDetail] = useState<TaskDetail | null>(null);
@ -81,6 +87,7 @@ export default function App() {
const toggleNodeFilter = useCallback((nodeId: string) => {
setNodeFilter((cur) => (cur === nodeId ? null : nodeId));
setView("board"); // node clicks filter the board, so surface it
}, []);
// Highlight the nodes a selected task has visited.
@ -105,42 +112,68 @@ export default function App() {
return looped;
}, [selectedId, detail, topology]);
const drawerOpen = selectedId != null;
return (
<div className="app">
<TopBar
state={state}
connected={connected}
generatedAt={state?.generated_at ?? null}
/>
<div className="body">
<TaskList
<TooltipProvider delayDuration={200}>
<div className="flex h-full flex-col bg-background text-foreground">
<TopBar
state={state}
topology={topology}
selectedId={selectedId}
nodeFilter={nodeFilter}
onSelect={selectTask}
onClearNodeFilter={() => setNodeFilter(null)}
connected={connected}
generatedAt={state?.generated_at ?? null}
/>
<main className="center">
{topology ? (
<PipelineMap
topology={topology}
state={state}
pathNodeIds={pathNodeIds}
loopedEdges={loopedEdges}
onNodeClick={toggleNodeFilter}
<div className="flex min-h-0 flex-1">
<Tabs
value={view}
onValueChange={(v) => setView(v as View)}
className="flex min-w-0 flex-1 flex-col"
>
<div className="flex items-center gap-3 border-b border-border px-3 py-2">
<TabsList>
<TabsTrigger value="board">
<LayoutGrid /> Board
</TabsTrigger>
<TabsTrigger value="pipeline">
<WorkflowIcon /> Pipeline
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="board" className="m-0 min-h-0 flex-1">
<Board
state={state}
topology={topology}
selectedId={selectedId}
nodeFilter={nodeFilter}
onSelect={selectTask}
onClearNodeFilter={() => setNodeFilter(null)}
/>
</TabsContent>
<TabsContent value="pipeline" className="m-0 min-h-0 flex-1">
{topology ? (
<PipelineMap
topology={topology}
state={state}
pathNodeIds={pathNodeIds}
loopedEdges={loopedEdges}
onNodeClick={toggleNodeFilter}
/>
) : (
<div className="p-8 text-sm text-muted-foreground">
Loading pipeline…
</div>
)}
</TabsContent>
</Tabs>
{drawerOpen && (
<TaskDrawer
detail={detail}
loading={detailLoading}
error={detailError}
onClose={closeDrawer}
/>
) : (
<div className="loading">Loading pipeline…</div>
)}
</main>
<TaskDrawer
detail={detail}
loading={detailLoading}
error={detailError}
onClose={closeDrawer}
/>
</div>
</div>
</div>
</TooltipProvider>
);
}

View file

@ -13,11 +13,28 @@ export interface TaskView {
waiting_since: string | null;
}
// One pipeline stage as grouped + ordered by the backend (status_page.snapshot).
// Each stage maps to a Kanban column; `tasks` are the live tasks sitting in it.
export interface Stage {
key: string;
label: string;
agent: string;
phases: string[];
gated: boolean;
role_node: boolean;
state: NodeState;
count: number;
tasks: TaskView[];
}
export interface StateResponse {
ok: boolean;
error: string | null;
generated_at: string;
db_path: string;
// Backend already groups tasks into ordered pipeline stages; the Board renders
// one column per stage. Optional for resilience against older daemons.
stages?: Stage[];
tasks: TaskView[];
waiting: TaskView[];
question_counts: Record<string, number>;

View file

@ -0,0 +1,15 @@
import type { StateResponse, Topology } from "../api";
// STUB — replaced in the component fan-out. Keeps the foundation compiling.
interface Props {
state: StateResponse | null;
topology: Topology | null;
selectedId: string | null;
nodeFilter: string | null;
onSelect: (threadId: string) => void;
onClearNodeFilter: () => void;
}
export function Board(_props: Props) {
return <div className="p-4 text-sm text-muted-foreground">Board…</div>;
}

View file

@ -0,0 +1,64 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold leading-none",
{
variants: {
variant: {
default: "bg-secondary text-secondary-foreground",
outline: "border border-border text-muted-foreground",
// Pipeline status variants — tinted surface + bright foreground.
active: "bg-status-active-bg text-status-active",
waiting: "bg-status-waiting-bg text-status-waiting",
parked: "bg-status-parked-bg text-status-parked",
done: "bg-status-done-bg text-status-done",
failed: "bg-status-failed-bg text-status-failed",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<span className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
/**
* Map a backend task status / verdict string to a Badge variant. Centralized so
* TaskCard, TaskList, and TaskDrawer agree on color.
*/
export function statusVariant(
status: string,
): NonNullable<BadgeProps["variant"]> {
switch (status) {
case "active":
return "active";
case "waiting_human":
case "waiting":
return "waiting";
case "parked":
case "request_changes":
return "parked";
case "done":
case "approve":
case "approved":
case "pass":
return status === "done" ? "done" : "active";
case "failed":
return "failed";
default:
return "outline";
}
}
export { Badge, badgeVariants };

View file

@ -0,0 +1,55 @@
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-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background 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 hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
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-6",
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 };

View file

@ -0,0 +1,51 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col gap-1 p-3", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm font-semibold leading-none", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-3 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
export { Card, CardHeader, CardTitle, CardContent };

View file

@ -0,0 +1,7 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -0,0 +1,20 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<
HTMLInputElement,
React.InputHTMLAttributes<HTMLInputElement>
>(({ className, type, ...props }, ref) => (
<input
type={type}
ref={ref}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-secondary px-2.5 py-1 text-sm text-foreground transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
));
Input.displayName = "Input";
export { Input };

View file

@ -0,0 +1,45 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<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.ElementRef<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 border-l border-l-transparent p-px",
orientation === "horizontal" &&
"h-2 flex-col border-t border-t-transparent p-px",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View file

@ -0,0 +1,85 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between gap-1 rounded-md border border-input bg-secondary px-2.5 py-1 text-sm text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="size-4 shrink-0 opacity-60" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
position={position}
className={cn(
"relative z-50 max-h-72 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
position === "popper" && "translate-y-1",
className,
)}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" && "w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-7 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
export {
Select,
SelectValue,
SelectTrigger,
SelectContent,
SelectItem,
};

View file

@ -0,0 +1,28 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<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-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View file

@ -0,0 +1,52 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-sm [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

View file

@ -0,0 +1,27 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className,
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -0,0 +1,94 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
* Design tokens for the agent-team dashboard.
*
* shadcn/ui HSL semantic tokens (background/foreground/card/primary/…) plus a
* pipeline `status.*` palette (active/waiting/parked/done/failed), each with a
* tinted surface and a bright foreground. Values are ported from the original
* theme.css hex palette so the dark look stays recognizable. Density follows
* vibe-kanban: tight spacing, 0.5rem radius.
*/
@layer base {
:root {
color-scheme: dark;
--background: 223 23% 6%; /* #0c0e13 */
--foreground: 225 18% 92%; /* #e6e8ee */
--card: 220 19% 13%; /* #1a1e26 */
--card-foreground: 225 18% 92%;
--popover: 222 22% 9%; /* #12151c */
--popover-foreground: 225 18% 92%;
--primary: 251 100% 68%; /* #7a5cff accent purple */
--primary-foreground: 223 23% 6%;
--secondary: 220 19% 13%;
--secondary-foreground: 225 18% 92%;
--muted: 222 22% 9%;
--muted-foreground: 221 14% 60%; /* #8a93a6 */
--accent: 220 19% 16%; /* subtle hover surface */
--accent-foreground: 225 18% 92%;
--destructive: 345 86% 73%; /* #f57f9c */
--destructive-foreground: 223 23% 6%;
--border: 218 21% 17%; /* #232a36 */
--input: 221 19% 23%; /* #2f3645 */
--ring: 251 100% 68%;
--radius: 0.5rem;
/* Pipeline status palette (foreground + tinted surface). */
--status-active: 149 72% 67%; /* #6ee7a8 */
--status-active-bg: 155 45% 16%; /* #163a2b */
--status-waiting: 47 87% 70%; /* #f5d76e */
--status-waiting-bg: 47 45% 16%; /* #3a3216 */
--status-parked: 24 85% 69%; /* #f3a36e */
--status-parked-bg: 15 45% 16%; /* #3a1f16 */
--status-done: 214 86% 73%; /* #7fb3f5 */
--status-done-bg: 210 35% 17%; /* #1c2b3a */
--status-failed: 345 86% 73%; /* #f57f9c */
--status-failed-bg: 343 45% 16%; /* #3a1620 */
/* Brand gradient stops (logo dot, accents). */
--brand-from: 251 100% 68%;
--brand-to: 215 66% 55%; /* #3f7fd8 */
}
* {
@apply border-border;
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
body {
@apply bg-background text-foreground;
font-family: theme("fontFamily.sans");
line-height: 1.45;
-webkit-font-smoothing: antialiased;
}
}
@layer utilities {
.mono {
font-family: theme("fontFamily.mono");
}
}
/* React Flow: hide the attribution badge (LAN-only, offline). */
.react-flow__attribution {
display: none;
}

View file

@ -0,0 +1,7 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/** shadcn/ui class-name helper: merge conditional classes, dedupe Tailwind. */
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}

View file

@ -1,6 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
// theme.css is being retired as components migrate to Tailwind; kept until the
// last component is converted so intermediate builds still render (removed in
// the integration phase).
import "./theme.css";
createRoot(document.getElementById("root")!).render(

View file

@ -0,0 +1,100 @@
import animate from "tailwindcss-animate";
/**
* Tailwind config for the agent-team dashboard.
*
* Colors are driven by HSL CSS variables defined in src/index.css (shadcn/ui
* convention) so the dark theme stays in one place. On top of the standard
* shadcn tokens we add pipeline `status.*` colors (active/waiting/parked/done/
* failed) that the Board, MapNode, and badges share.
*/
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
// Pipeline status palette — each has a tinted surface (`bg`) and a
// bright foreground (`fg`), mirroring the original theme.css mapping.
status: {
active: "hsl(var(--status-active))",
"active-bg": "hsl(var(--status-active-bg))",
waiting: "hsl(var(--status-waiting))",
"waiting-bg": "hsl(var(--status-waiting-bg))",
parked: "hsl(var(--status-parked))",
"parked-bg": "hsl(var(--status-parked-bg))",
done: "hsl(var(--status-done))",
"done-bg": "hsl(var(--status-done-bg))",
failed: "hsl(var(--status-failed))",
"failed-bg": "hsl(var(--status-failed-bg))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: [
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Helvetica",
"Arial",
"sans-serif",
],
mono: ["ui-monospace", "SFMono-Regular", "Menlo", "monospace"],
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [animate],
};

View file

@ -15,6 +15,10 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["node", "vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src", "vite.config.ts"]

View file

@ -1,4 +1,5 @@
/// <reference types="vitest/config" />
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
@ -8,6 +9,11 @@ import react from "@vitejs/plugin-react";
// locally (the dashboard is offline/LAN-only).
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
outDir: "dist",
emptyOutDir: true,