diff --git a/new-ui/CLAUDE.md b/new-ui/CLAUDE.md index d7928b9..ab54be4 100644 --- a/new-ui/CLAUDE.md +++ b/new-ui/CLAUDE.md @@ -56,6 +56,10 @@ In production, configure `apiUrl` in the config panel or pass `?apiUrl=...&apiKe ## Theme -The UI uses a "beehive" theme with amber/gold/honey colors. -Dark mode is default. Toggle via theme button. -CSS variables are defined in `src/styles/globals.css`. +"Mission Control" design — clean, information-dense, professional. +- **Base:** Zinc-neutral palette (shadcn/ui v4 oklch tokens) +- **Accent:** Amber as brand `--primary` only — interactive elements, active states +- **Dark mode** is default. Toggle via header button. +- **Typography:** Space Grotesk (sans) + Space Mono (mono). No display fonts. +- **Status colors:** Semantic — emerald (success), amber (active/busy), red (error), zinc (inactive) +- CSS variables defined in `src/styles/globals.css`. AG Grid themed via `src/styles/ag-grid.css`. diff --git a/new-ui/index.html b/new-ui/index.html index 003c5b8..2ef198e 100644 --- a/new-ui/index.html +++ b/new-ui/index.html @@ -5,7 +5,7 @@ Agent Swarm Dashboard - + @@ -14,7 +14,7 @@ diff --git a/new-ui/src/components/layout/app-header.tsx b/new-ui/src/components/layout/app-header.tsx index d56d1fc..a2d4cba 100644 --- a/new-ui/src/components/layout/app-header.tsx +++ b/new-ui/src/components/layout/app-header.tsx @@ -29,8 +29,8 @@ export function AppHeader() { className={cn( "size-2 rounded-full", isHealthy - ? "bg-terminal-green animate-heartbeat" - : "bg-terminal-red", + ? "bg-emerald-500" + : "bg-red-500", )} /> diff --git a/new-ui/src/components/layout/app-sidebar.tsx b/new-ui/src/components/layout/app-sidebar.tsx index 7750196..ec65698 100644 --- a/new-ui/src/components/layout/app-sidebar.tsx +++ b/new-ui/src/components/layout/app-sidebar.tsx @@ -17,6 +17,7 @@ import { SidebarFooter, SidebarGroup, SidebarGroupContent, + SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, @@ -25,17 +26,35 @@ import { SidebarTrigger, } from "@/components/ui/sidebar"; -const navItems = [ - { title: "Dashboard", path: "/", icon: LayoutDashboard }, - { title: "Agents", path: "/agents", icon: Users }, - { title: "Tasks", path: "/tasks", icon: ListTodo }, - { title: "Epics", path: "/epics", icon: Milestone }, - { title: "Chat", path: "/chat", icon: MessageSquare }, - { title: "Services", path: "/services", icon: Server }, - { title: "Schedules", path: "/schedules", icon: Clock }, - { title: "Usage", path: "/usage", icon: BarChart3 }, - { title: "Config", path: "/config", icon: Settings }, - { title: "Repos", path: "/repos", icon: GitBranch }, +const navGroups = [ + { + label: "Core", + items: [ + { title: "Dashboard", path: "/", icon: LayoutDashboard }, + { title: "Agents", path: "/agents", icon: Users }, + { title: "Tasks", path: "/tasks", icon: ListTodo }, + { title: "Epics", path: "/epics", icon: Milestone }, + ], + }, + { + label: "Communication", + items: [{ title: "Chat", path: "/chat", icon: MessageSquare }], + }, + { + label: "Operations", + items: [ + { title: "Services", path: "/services", icon: Server }, + { title: "Schedules", path: "/schedules", icon: Clock }, + { title: "Usage", path: "/usage", icon: BarChart3 }, + ], + }, + { + label: "System", + items: [ + { title: "Config", path: "/config", icon: Settings }, + { title: "Repos", path: "/repos", icon: GitBranch }, + ], + }, ]; export function AppSidebar() { @@ -50,43 +69,38 @@ export function AppSidebar() { alt="Agent Swarm" className="h-8 w-8 rounded" /> - + Agent Swarm - - - - {navItems.map((item) => { - const isActive = - item.path === "/" - ? location.pathname === "/" - : location.pathname.startsWith(item.path); - return ( - - - - - {item.title} - - - - ); - })} - - - + {navGroups.map((group) => ( + + {group.label} + + + {group.items.map((item) => { + const isActive = + item.path === "/" + ? location.pathname === "/" + : location.pathname.startsWith(item.path); + return ( + + + + + {item.title} + + + + ); + })} + + + + ))} diff --git a/new-ui/src/components/layout/root-layout.tsx b/new-ui/src/components/layout/root-layout.tsx index f65218b..3a4c913 100644 --- a/new-ui/src/components/layout/root-layout.tsx +++ b/new-ui/src/components/layout/root-layout.tsx @@ -22,7 +22,7 @@ export function RootLayout() { -
+
}> diff --git a/new-ui/src/components/shared/data-grid.tsx b/new-ui/src/components/shared/data-grid.tsx index 85eeb1a..53ad526 100644 --- a/new-ui/src/components/shared/data-grid.tsx +++ b/new-ui/src/components/shared/data-grid.tsx @@ -69,13 +69,14 @@ export function DataGrid({ if (loading) { gridRef.current?.api?.showLoadingOverlay(); } + gridRef.current?.api?.sizeColumnsToFit(); }, [loading]); return (
Try Again - + {open && ( +
+ {group.logs.map((log) => ( +
+ {log.content} +
+ ))} +
+ )} +
+ ); +} + +interface SessionLogViewerProps { + logs: SessionLog[]; + className?: string; +} + +export function SessionLogViewer({ logs, className }: SessionLogViewerProps) { + const groups = useMemo(() => groupByIteration(logs), [logs]); + + const [scrollEl, setScrollEl] = useState(null); + const scrollRef = useRef(null); + const { isFollowing, scrollToBottom } = useAutoScroll(scrollEl, [logs]); + + return ( +
+
+ + Session Logs + + {!isFollowing && ( + + )} +
+
{ + scrollRef.current = el; + setScrollEl(el); + }} + className="flex-1 min-h-0 overflow-auto font-mono text-xs leading-relaxed" + > + {groups.map((group) => ( + + ))} +
+
+ ); +} diff --git a/new-ui/src/components/shared/stats-bar.tsx b/new-ui/src/components/shared/stats-bar.tsx index 26a0a35..d57d465 100644 --- a/new-ui/src/components/shared/stats-bar.tsx +++ b/new-ui/src/components/shared/stats-bar.tsx @@ -1,53 +1,38 @@ +import { + Bot, + CheckCircle2, + Clock, + Heart, + Loader2, + XCircle, + Zap, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import { cn } from "@/lib/utils"; -import { formatCompactNumber } from "@/lib/utils"; -interface HexStatProps { +interface StatItemProps { + icon: LucideIcon; label: string; value: number | string; - color?: string; - active?: boolean; - onClick?: () => void; + variant?: "default" | "success" | "warning" | "danger"; } -function HexStat({ label, value, color = "text-amber-400", active, onClick }: HexStatProps) { - const hexClip = "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)"; +const variantStyles = { + default: "text-foreground", + success: "text-emerald-500", + warning: "text-amber-500", + danger: "text-red-500", +} as const; +function StatItem({ icon: Icon, label, value, variant = "default" }: StatItemProps) { return ( - +
+ + {label} + + {value} + +
); } @@ -58,62 +43,41 @@ interface StatsBarProps { healthy?: boolean; } -export function StatsBar({ agents, tasks, epics, healthy }: StatsBarProps) { +export function StatsBar({ agents, tasks, healthy }: StatsBarProps) { return ( -
- {/* Top row */} -
- - 0} - /> - - - -
- {/* Bottom row — offset for honeycomb */} -
- - 0} - /> - - 0 ? "text-red-400" : "text-zinc-500"} - /> -
+
+ + 0 ? "warning" : "default"} + /> + + 0 ? "warning" : "default"} + /> + + 0 ? "danger" : "default"} + /> +
); } diff --git a/new-ui/src/components/shared/status-badge.tsx b/new-ui/src/components/shared/status-badge.tsx index bdd8086..62b3122 100644 --- a/new-ui/src/components/shared/status-badge.tsx +++ b/new-ui/src/components/shared/status-badge.tsx @@ -6,72 +6,39 @@ type Status = AgentStatus | AgentTaskStatus | EpicStatus | ServiceStatus; interface StatusConfig { label: string; - className: string; + dot: string; + text: string; pulse?: boolean; } const statusConfig: Record = { // Agent statuses - idle: { label: "Idle", className: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" }, - busy: { - label: "Busy", - className: "bg-amber-500/15 text-amber-400 border-amber-500/30", - pulse: true, - }, - offline: { label: "Offline", className: "bg-red-500/15 text-red-400 border-red-500/30" }, + idle: { label: "IDLE", dot: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400" }, + busy: { label: "BUSY", dot: "bg-amber-500", text: "text-amber-600 dark:text-amber-400", pulse: true }, + offline: { label: "OFFLINE", dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, // Task statuses - backlog: { label: "Backlog", className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30" }, - unassigned: { - label: "Unassigned", - className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30", - }, - offered: { - label: "Offered", - className: "bg-amber-500/15 text-amber-400 border-amber-500/30", - pulse: true, - }, - reviewing: { - label: "Reviewing", - className: "bg-blue-500/15 text-blue-400 border-blue-500/30", - }, - pending: { label: "Pending", className: "bg-yellow-500/15 text-yellow-400 border-yellow-500/30" }, - in_progress: { - label: "In Progress", - className: "bg-amber-500/15 text-amber-400 border-amber-500/30", - pulse: true, - }, - paused: { label: "Paused", className: "bg-blue-500/15 text-blue-400 border-blue-500/30" }, - completed: { - label: "Completed", - className: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", - }, - failed: { label: "Failed", className: "bg-red-500/15 text-red-400 border-red-500/30" }, - cancelled: { - label: "Cancelled", - className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30", - }, + backlog: { label: "BACKLOG", dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, + unassigned: { label: "UNASSIGNED", dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, + offered: { label: "OFFERED", dot: "bg-amber-500", text: "text-amber-600 dark:text-amber-400", pulse: true }, + reviewing: { label: "REVIEWING", dot: "bg-blue-500", text: "text-blue-600 dark:text-blue-400" }, + pending: { label: "PENDING", dot: "bg-yellow-500", text: "text-yellow-600 dark:text-yellow-400" }, + in_progress: { label: "IN PROGRESS", dot: "bg-amber-500", text: "text-amber-600 dark:text-amber-400", pulse: true }, + paused: { label: "PAUSED", dot: "bg-blue-500", text: "text-blue-600 dark:text-blue-400" }, + completed: { label: "COMPLETED", dot: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400" }, + failed: { label: "FAILED", dot: "bg-red-500", text: "text-red-600 dark:text-red-400" }, + cancelled: { label: "CANCELLED", dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, // Epic statuses - draft: { label: "Draft", className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30" }, - active: { - label: "Active", - className: "bg-amber-500/15 text-amber-400 border-amber-500/30", - pulse: true, - }, + draft: { label: "DRAFT", dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, + active: { label: "ACTIVE", dot: "bg-amber-500", text: "text-amber-600 dark:text-amber-400", pulse: true }, // Service statuses - starting: { - label: "Starting", - className: "bg-yellow-500/15 text-yellow-400 border-yellow-500/30", - }, - healthy: { - label: "Healthy", - className: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", - }, - unhealthy: { label: "Unhealthy", className: "bg-red-500/15 text-red-400 border-red-500/30" }, - stopped: { label: "Stopped", className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30" }, -}; + starting: { label: "STARTING", dot: "bg-yellow-500", text: "text-yellow-600 dark:text-yellow-400" }, + healthy: { label: "HEALTHY", dot: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400" }, + unhealthy: { label: "UNHEALTHY", dot: "bg-red-500", text: "text-red-600 dark:text-red-400" }, + stopped: { label: "STOPPED", dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, +} satisfies Record; interface StatusBadgeProps { status: Status; @@ -82,21 +49,27 @@ interface StatusBadgeProps { export function StatusBadge({ status, size = "sm", className }: StatusBadgeProps) { const config = statusConfig[status] ?? { label: status, - className: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30", + dot: "bg-zinc-400", + text: "text-zinc-500 dark:text-zinc-400", }; return ( - {config.label} + + {config.label} ); } diff --git a/new-ui/src/pages/agents/[id]/page.tsx b/new-ui/src/pages/agents/[id]/page.tsx index 4cf5f8f..eb39b81 100644 --- a/new-ui/src/pages/agents/[id]/page.tsx +++ b/new-ui/src/pages/agents/[id]/page.tsx @@ -92,7 +92,7 @@ export default function AgentDetailPage() { } return ( -
+
) : (
-

{agent.name}

+

{agent.name}

diff --git a/new-ui/src/pages/agents/page.tsx b/new-ui/src/pages/agents/page.tsx index 95a1db7..c2d82ac 100644 --- a/new-ui/src/pages/agents/page.tsx +++ b/new-ui/src/pages/agents/page.tsx @@ -13,7 +13,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Search } from "lucide-react"; +import { Search, Crown } from "lucide-react"; import type { AgentWithTasks, AgentStatus } from "@/api/types"; export default function AgentsPage() { @@ -24,8 +24,8 @@ export default function AgentsPage() { const filteredAgents = useMemo(() => { if (!agents) return []; - if (statusFilter === "all") return agents; - return agents.filter((a) => a.status === statusFilter); + const filtered = statusFilter === "all" ? [...agents] : agents.filter((a) => a.status === statusFilter); + return filtered.sort((a, b) => (b.isLead ? 1 : 0) - (a.isLead ? 1 : 0)); }, [agents, statusFilter]); const columnDefs = useMemo[]>( @@ -34,8 +34,13 @@ export default function AgentsPage() { field: "name", headerName: "Name", width: 200, - cellRenderer: (params: { value: string }) => ( - {params.value} + cellRenderer: (params: { value: string; data: AgentWithTasks | undefined }) => ( + + {params.value} + {params.data?.isLead && ( + + )} + ), }, { field: "role", headerName: "Role", width: 150 }, @@ -90,8 +95,8 @@ export default function AgentsPage() { ); return ( -
-

Agents

+
+

Agents

diff --git a/new-ui/src/pages/chat/page.tsx b/new-ui/src/pages/chat/page.tsx index bbebbc0..7fe6ff8 100644 --- a/new-ui/src/pages/chat/page.tsx +++ b/new-ui/src/pages/chat/page.tsx @@ -5,7 +5,7 @@ import { useAgents } from "@/api/hooks/use-agents"; import { formatRelativeTime, cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { ScrollArea } from "@/components/ui/scroll-area"; + import { Skeleton } from "@/components/ui/skeleton"; import { Hash, Lock, Send, MessageSquare } from "lucide-react"; import type { Channel, ChannelMessage } from "@/api/types"; @@ -33,7 +33,7 @@ function ChannelSidebar({ className={cn( "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors", activeChannelId === ch.id - ? "bg-amber-500/15 text-amber-400" + ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground", )} > @@ -68,7 +68,7 @@ function MessageBubble({ return (
-
+
{initials}
@@ -126,7 +126,7 @@ function MessageInput({ size="icon" onClick={handleSend} disabled={!content.trim() || postMessage.isPending} - className="shrink-0 bg-amber-600 hover:bg-amber-700" + className="shrink-0 bg-primary hover:bg-primary/90" > @@ -166,17 +166,17 @@ export default function ChatPage() { if (channelsLoading) { return ( -
-

Chat

- +
+

Chat

+
); } return ( -
-

Chat

-
+
+

Chat

+
- +
{messagesLoading ? (
@@ -222,7 +222,7 @@ export default function ChatPage() { )}
- +
diff --git a/new-ui/src/pages/config/page.tsx b/new-ui/src/pages/config/page.tsx index d35fe1c..3344ac5 100644 --- a/new-ui/src/pages/config/page.tsx +++ b/new-ui/src/pages/config/page.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useConfig } from "@/hooks/use-config"; import { useConfigs, useUpsertConfig, useDeleteConfig } from "@/api/hooks/use-config-api"; +import { useAgents } from "@/api/hooks/use-agents"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -32,7 +33,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Loader2, CheckCircle2, XCircle, Hexagon, Plus, Pencil, Trash2, Eye, EyeOff, Settings } from "lucide-react"; +import { Loader2, CheckCircle2, XCircle, Hexagon, Plus, Pencil, Trash2, Eye, EyeOff, Settings, Copy, Check } from "lucide-react"; import type { SwarmConfig, SwarmConfigScope } from "@/api/types"; interface ConfigFormData { @@ -64,33 +65,25 @@ function ConfigEntryDialog({ editEntry: SwarmConfig | null; onSubmit: (data: ConfigFormData) => void; }) { - const [form, setForm] = useState( - editEntry - ? { - scope: editEntry.scope, - scopeId: editEntry.scopeId ?? "", - key: editEntry.key, - value: editEntry.isSecret ? "" : editEntry.value, - isSecret: editEntry.isSecret, - description: editEntry.description ?? "", - } - : emptyConfigForm, - ); + const { data: agents } = useAgents(); + const [form, setForm] = useState(emptyConfigForm); useEffect(() => { - setForm( - editEntry - ? { - scope: editEntry.scope, - scopeId: editEntry.scopeId ?? "", - key: editEntry.key, - value: editEntry.isSecret ? "" : editEntry.value, - isSecret: editEntry.isSecret, - description: editEntry.description ?? "", - } - : emptyConfigForm, - ); - }, [editEntry]); + if (open) { + setForm( + editEntry + ? { + scope: editEntry.scope, + scopeId: editEntry.scopeId ?? "", + key: editEntry.key, + value: editEntry.isSecret ? "" : editEntry.value, + isSecret: editEntry.isSecret, + description: editEntry.description ?? "", + } + : emptyConfigForm, + ); + } + }, [editEntry, open]); function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -113,7 +106,7 @@ function ConfigEntryDialog({
- {form.scope !== "global" && ( + {form.scope === "agent" && ( +
+ + {agents && agents.length > 0 ? ( + + ) : ( + setForm({ ...form, scopeId: e.target.value })} + /> + )} +
+ )} + {form.scope === "repo" && (
setForm({ ...form, scopeId: e.target.value })} /> @@ -175,7 +199,7 @@ function ConfigEntryDialog({ - @@ -187,9 +211,16 @@ function ConfigEntryDialog({ function SwarmConfigSection() { const { data: configs, isLoading } = useConfigs(); + const { data: agents } = useAgents(); const upsertConfig = useUpsertConfig(); const deleteConfig = useDeleteConfig(); + const agentMap = useMemo(() => { + const m = new Map(); + agents?.forEach((a) => m.set(a.id, a.name)); + return m; + }, [agents]); + const [dialogOpen, setDialogOpen] = useState(false); const [editEntry, setEditEntry] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); @@ -239,7 +270,7 @@ function SwarmConfigSection() {

Swarm Configuration

-
@@ -266,6 +297,7 @@ function SwarmConfigSection() { Scope + Agent / Scope ID Key Value Description @@ -280,6 +312,13 @@ function SwarmConfigSection() { {cfg.scope} + + {cfg.scope === "agent" && cfg.scopeId + ? agentMap.get(cfg.scopeId) ?? cfg.scopeId.slice(0, 8) + "..." + : cfg.scope === "repo" && cfg.scopeId + ? cfg.scopeId.slice(0, 8) + "..." + : "—"} + {cfg.key} {cfg.isSecret ? ( @@ -366,9 +405,17 @@ export default function ConfigPage() { const [apiUrl, setApiUrl] = useState(config.apiUrl); const [apiKey, setApiKey] = useState(config.apiKey); + const [showApiKey, setShowApiKey] = useState(false); + const [copied, setCopied] = useState(false); const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const [errorMsg, setErrorMsg] = useState(""); + function handleCopyApiKey() { + navigator.clipboard.writeText(apiKey); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + async function handleConnect() { setStatus("loading"); setErrorMsg(""); @@ -404,12 +451,12 @@ export default function ConfigPage() { if (!isConfigured) { return (
- +
- +
- Agent Swarm + Agent Swarm Connect to your Agent Swarm API server to get started. @@ -428,17 +475,38 @@ export default function ConfigPage() {
- setApiKey(e.target.value)} - disabled={status === "loading"} - onKeyDown={(e) => { - if (e.key === "Enter") handleConnect(); - }} - /> +
+ setApiKey(e.target.value)} + disabled={status === "loading"} + onKeyDown={(e) => { + if (e.key === "Enter") handleConnect(); + }} + /> + + +
{status === "error" && ( @@ -458,7 +526,7 @@ export default function ConfigPage() { + +
@@ -528,7 +617,7 @@ export default function ConfigPage() {
-

{epic.name}

+

{epic.name}

{epic.tags?.map((tag) => ( @@ -97,7 +97,7 @@ export default function EpicDetailPage() {
@@ -106,7 +106,7 @@ export default function EpicDetailPage() { {stats && (
{stats.completed} completed - {stats.inProgress} in progress + {stats.inProgress} in progress {stats.pending} pending {stats.failed > 0 && {stats.failed} failed} {stats.total} total @@ -139,7 +139,7 @@ export default function EpicDetailPage() {

{epic.leadAgentId.slice(0, 8)}... diff --git a/new-ui/src/pages/epics/page.tsx b/new-ui/src/pages/epics/page.tsx index 7068762..c009a1d 100644 --- a/new-ui/src/pages/epics/page.tsx +++ b/new-ui/src/pages/epics/page.tsx @@ -68,7 +68,7 @@ export default function EpicsPage() {

@@ -106,8 +106,8 @@ export default function EpicsPage() { const epics = (epicsData?.epics ?? []) as unknown as EpicWithProgress[]; return ( -
-

Epics

+
+

Epics

diff --git a/new-ui/src/pages/repos/page.tsx b/new-ui/src/pages/repos/page.tsx index cdc45b5..38aae65 100644 --- a/new-ui/src/pages/repos/page.tsx +++ b/new-ui/src/pages/repos/page.tsx @@ -132,7 +132,7 @@ function RepoDialog({ - @@ -179,17 +179,17 @@ export default function ReposPage() { if (isLoading) { return (
-

Repos

+

Repos

); } return ( -
+
-

Repos

-
diff --git a/new-ui/src/pages/schedules/[id]/page.tsx b/new-ui/src/pages/schedules/[id]/page.tsx index 95497b4..25dcb59 100644 --- a/new-ui/src/pages/schedules/[id]/page.tsx +++ b/new-ui/src/pages/schedules/[id]/page.tsx @@ -46,7 +46,7 @@ export default function ScheduleDetailPage() { } return ( -
+
-

{schedule.name}

+

{schedule.name}

{schedule.cronExpression ? ( <> - + {schedule.cronExpression} ) : schedule.intervalMs ? ( <> - + Every {formatInterval(schedule.intervalMs)} ) : ( @@ -116,7 +116,7 @@ export default function ScheduleDetailPage() { {schedule.targetAgentId ? ( {agentMap.get(schedule.targetAgentId) ?? schedule.targetAgentId.slice(0, 8) + "..."} diff --git a/new-ui/src/pages/schedules/page.tsx b/new-ui/src/pages/schedules/page.tsx index d3a8989..cc957c5 100644 --- a/new-ui/src/pages/schedules/page.tsx +++ b/new-ui/src/pages/schedules/page.tsx @@ -38,16 +38,16 @@ export default function SchedulesPage() { if (isLoading) { return ( -
-

Schedules

+
+

Schedules

); } return ( -
-

Schedules

+
+

Schedules

{schedules && schedules.length > 0 ? (
diff --git a/new-ui/src/pages/services/page.tsx b/new-ui/src/pages/services/page.tsx index bf53cca..8de7f28 100644 --- a/new-ui/src/pages/services/page.tsx +++ b/new-ui/src/pages/services/page.tsx @@ -35,8 +35,8 @@ export default function ServicesPage() { if (isLoading) { return ( -
-

Services

+
+

Services

{Array.from({ length: 3 }).map((_, i) => ( @@ -47,13 +47,13 @@ export default function ServicesPage() { } return ( -
-

Services

+
+

Services

{services && services.length > 0 ? (
{services.map((svc) => ( - +
@@ -80,7 +80,7 @@ export default function ServicesPage() { Agent: {agentMap.get(svc.agentId) ?? svc.agentId.slice(0, 8) + "..."} @@ -98,7 +98,7 @@ export default function ServicesPage() { href={svc.url} target="_blank" rel="noopener noreferrer" - className="text-amber-400 hover:underline inline-flex items-center gap-0.5" + className="text-primary hover:underline inline-flex items-center gap-0.5" > {svc.url} diff --git a/new-ui/src/pages/tasks/[id]/page.tsx b/new-ui/src/pages/tasks/[id]/page.tsx index 0c3bbe6..c8fa63d 100644 --- a/new-ui/src/pages/tasks/[id]/page.tsx +++ b/new-ui/src/pages/tasks/[id]/page.tsx @@ -1,15 +1,15 @@ -import { useRef, useState } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; import { useTask, useTaskSessionLogs } from "@/api/hooks/use-tasks"; +import { useAgents } from "@/api/hooks/use-agents"; import { StatusBadge } from "@/components/shared/status-badge"; +import { SessionLogViewer } from "@/components/shared/session-log-viewer"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; -import { useAutoScroll } from "@/hooks/use-auto-scroll"; import { formatSmartTime, formatRelativeTime } from "@/lib/utils"; -import { ArrowLeft, ArrowDown } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import type { AgentLog } from "@/api/types"; +import { useMemo } from "react"; function LogTimeline({ logs }: { logs: AgentLog[] }) { return ( @@ -17,7 +17,7 @@ function LogTimeline({ logs }: { logs: AgentLog[] }) { {logs.map((log) => (
-
+
@@ -38,14 +38,16 @@ export default function TaskDetailPage() { const navigate = useNavigate(); const { data: task, isLoading } = useTask(id!); const { data: sessionLogs } = useTaskSessionLogs(id!); + const { data: agents } = useAgents(); - const [scrollEl, setScrollEl] = useState(null); - const scrollRef = useRef(null); - const { isFollowing, scrollToBottom } = useAutoScroll(scrollEl, [sessionLogs]); + const agentName = useMemo(() => { + if (!task?.agentId || !agents) return null; + return agents.find((a) => a.id === task.agentId)?.name ?? null; + }, [task, agents]); if (isLoading) { return ( -
+
@@ -56,8 +58,11 @@ export default function TaskDetailPage() { return

Task not found.

; } + const hasSessionLogs = sessionLogs && sessionLogs.length > 0; + const hasRightColumn = hasSessionLogs || task.output || task.failureReason; + return ( -
+
- {/* Description */} - - - Description - - -
{task.task}
-
-
- - {/* Output / Progress / Failure */} -
- {task.progress && ( + {/* Two-column layout */} +
+ {/* Left column: Description + Progress + Event History */} +
- Progress - - -
-                {task.progress}
-              
-
-
- )} - {task.output && ( - - - Output - - -
-                {task.output}
-              
-
-
- )} - {task.failureReason && ( - - - Failure Reason + Description -
-                {task.failureReason}
-              
+
{task.task}
- )} -
- {/* Session Logs */} - {sessionLogs && sessionLogs.length > 0 && ( - - -
- Session Logs - {!isFollowing && ( - - )} -
-
- -
{ - scrollRef.current = el; - setScrollEl(el); - }} - className="h-80 overflow-auto rounded-md bg-zinc-950 p-3 font-mono text-xs leading-relaxed text-zinc-300" - > - {sessionLogs.map((log) => ( -
- {log.content} -
- ))} -
-
-
- )} + {task.progress && ( + + + Progress + + +
+                  {task.progress}
+                
+
+
+ )} + + {task.logs && task.logs.length > 0 && ( + + + Event History + + + + + + )} +
+ + {/* Right column: Output / Error + Session Logs */} + {hasRightColumn && ( +
+ {task.output && ( + + + Output + + +
+                    {task.output}
+                  
+
+
+ )} + + {task.failureReason && ( + + + Failure Reason + + +
+                    {task.failureReason}
+                  
+
+
+ )} - {/* Task Log Timeline */} - {task.logs && task.logs.length > 0 && ( - - - Event History - - - - - - )} + {hasSessionLogs && ( + + )} +
+ )} +
); } diff --git a/new-ui/src/pages/tasks/page.tsx b/new-ui/src/pages/tasks/page.tsx index 91e9d21..4b225be 100644 --- a/new-ui/src/pages/tasks/page.tsx +++ b/new-ui/src/pages/tasks/page.tsx @@ -74,7 +74,7 @@ export default function TasksPage() { width: 80, cellRenderer: (params: { value: number }) => { const p = params.value ?? 50; - const color = p >= 80 ? "text-red-400" : p >= 60 ? "text-amber-400" : "text-muted-foreground"; + const color = p >= 80 ? "text-red-400" : p >= 60 ? "text-primary" : "text-muted-foreground"; return {p}; }, }, @@ -116,8 +116,8 @@ export default function TasksPage() { ); return ( -
-

Tasks

+
+

Tasks

diff --git a/new-ui/src/pages/usage/page.tsx b/new-ui/src/pages/usage/page.tsx index 71a6794..a1f4d5e 100644 --- a/new-ui/src/pages/usage/page.tsx +++ b/new-ui/src/pages/usage/page.tsx @@ -29,7 +29,7 @@ import { } from "recharts"; import { DollarSign, Coins, Activity, TrendingUp } from "lucide-react"; -const CHART_COLORS = ["#f59e0b", "#8b5cf6", "#10b981", "#ef4444", "#3b82f6"]; +const CHART_COLORS = ["var(--color-chart-1)", "var(--color-chart-2)", "var(--color-chart-3)", "var(--color-chart-4)", "var(--color-chart-5)"]; export default function UsagePage() { const { data: monthlyStats, isLoading: statsLoading } = useMonthlyUsageStats(); @@ -108,7 +108,7 @@ export default function UsagePage() { if (statsLoading || costsLoading) { return (
-

Usage

+

Usage

{Array.from({ length: 4 }).map((_, i) => ( @@ -120,15 +120,15 @@ export default function UsagePage() { } return ( -
-

Usage

+
+

Usage

{/* Summary Cards */}
Monthly Cost - +

@@ -139,7 +139,7 @@ export default function UsagePage() { Monthly Tokens - +

@@ -150,7 +150,7 @@ export default function UsagePage() { Sessions - +

{monthlyStats?.sessionCount ?? 0}

@@ -159,7 +159,7 @@ export default function UsagePage() { Avg Cost/Session - +

@@ -182,16 +182,16 @@ export default function UsagePage() { {dailyData.length > 0 ? ( - - + + `$${v}`} /> @@ -239,8 +239,8 @@ export default function UsagePage() { - - - `$${v}`} /> + + + `$${v}`} /> [`$${Number(value).toFixed(3)}`, "Cost"]} /> - + diff --git a/new-ui/src/styles/ag-grid.css b/new-ui/src/styles/ag-grid.css index 2503ed2..389123e 100644 --- a/new-ui/src/styles/ag-grid.css +++ b/new-ui/src/styles/ag-grid.css @@ -3,20 +3,22 @@ /* Import AG Grid Quartz theme at the grid layer */ @import "ag-grid-community/styles/ag-theme-quartz.css" layer(grid); -/* Override AG Grid variables with beehive colors */ +/* Neutral theme overrides — maps to shadcn/ui CSS vars */ .ag-theme-quartz { - --ag-background-color: var(--color-hive-surface); - --ag-header-background-color: var(--color-hive-earth); - --ag-odd-row-background-color: var(--color-hive-body); - --ag-row-hover-color: rgba(245, 166, 35, 0.05); - --ag-border-color: var(--color-hive-border); - --ag-header-foreground-color: var(--color-hive-text-secondary); - --ag-foreground-color: var(--color-hive-text-primary); - --ag-selected-row-background-color: rgba(245, 166, 35, 0.1); - --ag-font-family: var(--font-sans); + --ag-background-color: var(--color-background); + --ag-foreground-color: var(--color-foreground); + --ag-header-background-color: var(--color-muted); + --ag-header-foreground-color: var(--color-muted-foreground); + --ag-odd-row-background-color: transparent; + --ag-row-hover-color: var(--color-accent); + --ag-selected-row-background-color: var(--color-accent); + --ag-border-color: var(--color-border); + --ag-secondary-border-color: var(--color-border); + --ag-row-border-color: var(--color-border); + --ag-header-column-separator-color: var(--color-border); + --ag-font-family: "Space Grotesk", system-ui, sans-serif; --ag-font-size: 13px; - --ag-row-border-color: var(--color-hive-border); - --ag-header-column-separator-color: var(--color-hive-border); - --ag-range-selection-border-color: var(--color-hive-amber); - --ag-input-focus-border-color: var(--color-hive-amber); + --ag-header-column-resize-handle-color: var(--color-border); + --ag-range-selection-border-color: var(--color-primary); + --ag-input-focus-border-color: var(--color-primary); } diff --git a/new-ui/src/styles/globals.css b/new-ui/src/styles/globals.css index 392ee41..37eb227 100644 --- a/new-ui/src/styles/globals.css +++ b/new-ui/src/styles/globals.css @@ -4,118 +4,87 @@ @custom-variant dark (&:is(.dark *)); @theme { - /* shadcn/ui OKLCH semantic colors — light base (standard Tailwind v4 pattern) */ - --color-background: oklch(0.98 0.005 60); - --color-foreground: oklch(0.15 0.02 60); + /* shadcn/ui Zinc theme — light base with amber primary */ + --color-background: oklch(1 0 0); + --color-foreground: oklch(0.141 0.005 285.823); --color-card: oklch(1 0 0); - --color-card-foreground: oklch(0.15 0.02 60); + --color-card-foreground: oklch(0.141 0.005 285.823); --color-popover: oklch(1 0 0); - --color-popover-foreground: oklch(0.15 0.02 60); - --color-primary: oklch(0.65 0.15 60); - --color-primary-foreground: oklch(0.98 0.005 60); - --color-secondary: oklch(0.96 0.005 60); - --color-secondary-foreground: oklch(0.15 0.02 60); - --color-muted: oklch(0.96 0.005 60); - --color-muted-foreground: oklch(0.45 0.03 60); - --color-accent: oklch(0.96 0.005 60); - --color-accent-foreground: oklch(0.15 0.02 60); - --color-destructive: oklch(0.55 0.2 25); - --color-destructive-foreground: oklch(0.98 0.005 60); - --color-border: oklch(0.9 0.01 60); - --color-input: oklch(0.9 0.01 60); - --color-ring: oklch(0.65 0.15 60); - --color-chart-1: oklch(0.75 0.15 70); - --color-chart-2: oklch(0.65 0.12 160); - --color-chart-3: oklch(0.6 0.15 250); - --color-chart-4: oklch(0.7 0.1 320); - --color-chart-5: oklch(0.8 0.12 90); - --color-sidebar: oklch(0.98 0.005 60); - --color-sidebar-foreground: oklch(0.15 0.02 60); - --color-sidebar-primary: oklch(0.65 0.15 60); - --color-sidebar-primary-foreground: oklch(0.98 0.005 60); - --color-sidebar-accent: oklch(0.96 0.005 60); - --color-sidebar-accent-foreground: oklch(0.15 0.02 60); - --color-sidebar-border: oklch(0.9 0.01 60); - --color-sidebar-ring: oklch(0.65 0.15 60); + --color-popover-foreground: oklch(0.141 0.005 285.823); + --color-primary: oklch(0.555 0.163 48.998); + --color-primary-foreground: oklch(0.985 0 0); + --color-secondary: oklch(0.967 0.001 286.375); + --color-secondary-foreground: oklch(0.21 0.006 285.885); + --color-muted: oklch(0.967 0.001 286.375); + --color-muted-foreground: oklch(0.552 0.016 285.938); + --color-accent: oklch(0.967 0.001 286.375); + --color-accent-foreground: oklch(0.21 0.006 285.885); + --color-destructive: oklch(0.577 0.245 27.325); + --color-destructive-foreground: oklch(0.985 0 0); + --color-border: oklch(0.92 0.004 286.32); + --color-input: oklch(0.92 0.004 286.32); + --color-ring: oklch(0.555 0.163 48.998); + --color-chart-1: oklch(0.646 0.222 41.116); + --color-chart-2: oklch(0.6 0.118 184.704); + --color-chart-3: oklch(0.398 0.07 227.392); + --color-chart-4: oklch(0.828 0.189 84.429); + --color-chart-5: oklch(0.769 0.188 70.08); + --color-sidebar: oklch(0.985 0 0); + --color-sidebar-foreground: oklch(0.141 0.005 285.823); + --color-sidebar-primary: oklch(0.555 0.163 48.998); + --color-sidebar-primary-foreground: oklch(0.985 0 0); + --color-sidebar-accent: oklch(0.967 0.001 286.375); + --color-sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --color-sidebar-border: oklch(0.92 0.004 286.32); + --color-sidebar-ring: oklch(0.555 0.163 48.998); - /* Beehive custom colors — light base */ - --color-hive-amber: #d48806; - --color-hive-honey: #b87300; - --color-hive-gold: #8b6914; - --color-hive-deep: #9a5f00; - --color-hive-rust: #b54242; - --color-hive-blue: #3b82f6; - --color-hive-body: #fdf8f3; - --color-hive-surface: #ffffff; - --color-hive-earth: #f5ede4; - --color-hive-border: #e5d9ca; - --color-hive-text-primary: #1a130e; - --color-hive-text-secondary: #5c4a3d; - --color-hive-text-tertiary: #8b7355; - - /* Terminal palette */ - --color-terminal-bg: #0a0a0a; - --color-terminal-surface: #111111; - --color-terminal-green: #00ff88; - --color-terminal-cyan: #00d4ff; - --color-terminal-amber: #ffaa00; - --color-terminal-red: #ff4444; /* Fonts */ --font-sans: "Space Grotesk", sans-serif; --font-mono: "Space Mono", monospace; - --font-display: "Graduate", cursive; - /* Border radius */ - --radius-sm: 0.25rem; - --radius-md: 0.375rem; - --radius-lg: 0.5rem; + /* Border radius — shadcn/ui default */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.625rem; --radius-xl: 0.75rem; } -/* Dark mode overrides (applied via .dark class on ) */ +/* Dark mode overrides */ .dark, .dark * { - --color-background: oklch(0.145 0.015 60); - --color-foreground: oklch(0.97 0.01 60); - --color-card: oklch(0.17 0.02 55); - --color-card-foreground: oklch(0.97 0.01 60); - --color-popover: oklch(0.145 0.015 60); - --color-popover-foreground: oklch(0.97 0.01 60); - --color-primary: oklch(0.75 0.15 70); - --color-primary-foreground: oklch(0.15 0.02 60); - --color-secondary: oklch(0.25 0.02 55); - --color-secondary-foreground: oklch(0.97 0.01 60); - --color-muted: oklch(0.25 0.02 55); - --color-muted-foreground: oklch(0.65 0.04 60); - --color-accent: oklch(0.25 0.02 55); - --color-accent-foreground: oklch(0.97 0.01 60); - --color-destructive: oklch(0.55 0.2 25); - --color-destructive-foreground: oklch(0.97 0.01 60); - --color-border: oklch(0.3 0.03 55); - --color-input: oklch(0.3 0.03 55); - --color-ring: oklch(0.75 0.15 70); - --color-sidebar: oklch(0.145 0.015 60); - --color-sidebar-foreground: oklch(0.97 0.01 60); - --color-sidebar-primary: oklch(0.75 0.15 70); - --color-sidebar-primary-foreground: oklch(0.15 0.02 60); - --color-sidebar-accent: oklch(0.25 0.02 55); - --color-sidebar-accent-foreground: oklch(0.97 0.01 60); - --color-sidebar-border: oklch(0.3 0.03 55); - --color-sidebar-ring: oklch(0.75 0.15 70); - - --color-hive-amber: #f5a623; - --color-hive-honey: #ffb84d; - --color-hive-gold: #d4a574; - --color-hive-deep: #c67c00; - --color-hive-rust: #a85454; - --color-hive-body: #0d0906; - --color-hive-surface: #1a130e; - --color-hive-earth: #251c15; - --color-hive-border: #3a2d1f; - --color-hive-text-primary: #fff8e7; - --color-hive-text-secondary: #c9b896; - --color-hive-text-tertiary: #8b7355; + --color-background: oklch(0.141 0.005 285.823); + --color-foreground: oklch(0.985 0 0); + --color-card: oklch(0.21 0.006 285.885); + --color-card-foreground: oklch(0.985 0 0); + --color-popover: oklch(0.21 0.006 285.885); + --color-popover-foreground: oklch(0.985 0 0); + --color-primary: oklch(0.769 0.188 70.08); + --color-primary-foreground: oklch(0.21 0.006 285.885); + --color-secondary: oklch(0.274 0.006 286.033); + --color-secondary-foreground: oklch(0.985 0 0); + --color-muted: oklch(0.274 0.006 286.033); + --color-muted-foreground: oklch(0.705 0.015 286.067); + --color-accent: oklch(0.274 0.006 286.033); + --color-accent-foreground: oklch(0.985 0 0); + --color-destructive: oklch(0.704 0.191 22.216); + --color-destructive-foreground: oklch(0.985 0 0); + --color-border: oklch(1 0 0 / 10%); + --color-input: oklch(1 0 0 / 15%); + --color-ring: oklch(0.769 0.188 70.08); + --color-chart-1: oklch(0.488 0.243 264.376); + --color-chart-2: oklch(0.696 0.17 162.48); + --color-chart-3: oklch(0.769 0.188 70.08); + --color-chart-4: oklch(0.627 0.265 303.9); + --color-chart-5: oklch(0.645 0.246 16.439); + --color-sidebar: oklch(0.21 0.006 285.885); + --color-sidebar-foreground: oklch(0.985 0 0); + --color-sidebar-primary: oklch(0.769 0.188 70.08); + --color-sidebar-primary-foreground: oklch(0.985 0 0); + --color-sidebar-accent: oklch(0.274 0.006 286.033); + --color-sidebar-accent-foreground: oklch(0.985 0 0); + --color-sidebar-border: oklch(1 0 0 / 10%); + --color-sidebar-ring: oklch(0.769 0.188 70.08); } :root { @@ -123,243 +92,31 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - --sidebar: hsl(0 0% 98%); - --sidebar-foreground: hsl(240 5.3% 26.1%); - --sidebar-primary: hsl(240 5.9% 10%); - --sidebar-primary-foreground: hsl(0 0% 98%); - --sidebar-accent: hsl(240 4.8% 95.9%); - --sidebar-accent-foreground: hsl(240 5.9% 10%); - --sidebar-border: hsl(220 13% 91%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); } body { margin: 0; - background: var(--color-hive-body); - color: var(--color-hive-text-primary); + background: var(--color-background); + color: var(--color-foreground); font-family: var(--font-sans); min-height: 100vh; } -/* Honeycomb pattern background */ -.honeycomb-bg { - position: relative; -} - -.honeycomb-bg::before { - content: ""; - position: absolute; - inset: 0; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23F5A623' fill-opacity='0.08'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); - pointer-events: none; - z-index: 0; -} - -.honeycomb-bg > * { - position: relative; - z-index: 1; -} - -/* Warm amber scrollbar */ +/* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { - background: var(--color-hive-body); + background: var(--color-background); } ::-webkit-scrollbar-thumb { - background: var(--color-hive-border); + background: var(--color-border); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: var(--color-hive-text-tertiary); -} - -/* Amber glow effects */ -.glow-amber { - text-shadow: - 0 0 10px rgba(245, 166, 35, 0.5), - 0 0 20px rgba(245, 166, 35, 0.3); -} - -.glow-gold { - text-shadow: - 0 0 10px rgba(212, 165, 116, 0.5), - 0 0 20px rgba(212, 165, 116, 0.3); -} - -.glow-rust { - text-shadow: - 0 0 10px rgba(168, 84, 84, 0.5), - 0 0 20px rgba(168, 84, 84, 0.3); -} - -.glow-blue { - text-shadow: - 0 0 10px rgba(59, 130, 246, 0.5), - 0 0 20px rgba(59, 130, 246, 0.3); -} - -/* Box glow effects */ -.box-glow-amber { - box-shadow: - 0 0 15px rgba(245, 166, 35, 0.3), - 0 0 30px rgba(245, 166, 35, 0.15); -} - -.box-glow-gold { - box-shadow: - 0 0 15px rgba(212, 165, 116, 0.3), - 0 0 30px rgba(212, 165, 116, 0.15); -} - -/* Breathing animation for active elements */ -@keyframes breathe { - 0%, - 100% { - opacity: 0.85; - transform: scale(1); - } - 50% { - opacity: 1; - transform: scale(1.02); - } -} - -.animate-breathe { - animation: breathe 3s ease-in-out infinite; -} - -/* Pulse animation */ -@keyframes pulse-amber { - 0%, - 100% { - box-shadow: 0 0 5px rgba(245, 166, 35, 0.4); - } - 50% { - box-shadow: - 0 0 15px rgba(245, 166, 35, 0.6), - 0 0 25px rgba(245, 166, 35, 0.3); - } -} - -.animate-pulse-amber { - animation: pulse-amber 2s ease-in-out infinite; -} - -/* Heartbeat animation for connection status */ -@keyframes heartbeat { - 0%, - 100% { - transform: scale(1); - } - 14% { - transform: scale(1.1); - } - 28% { - transform: scale(1); - } - 42% { - transform: scale(1.1); - } - 70% { - transform: scale(1); - } -} - -.animate-heartbeat { - animation: heartbeat 1.5s ease-in-out infinite; -} - -/* Hexagonal clip path utility */ -.clip-hex { - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); -} - -/* Hexagonal border */ -.hex-border { - position: relative; -} - -.hex-border::before { - content: ""; - position: absolute; - inset: -2px; - clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); - background: linear-gradient(135deg, var(--color-hive-amber) 0%, var(--color-hive-deep) 100%); - z-index: -1; -} - -/* Gradient text for headers */ -.text-gradient-amber { - background: linear-gradient( - 135deg, - var(--color-hive-amber) 0%, - var(--color-hive-honey) 50%, - var(--color-hive-deep) 100% - ); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* Card hover effect */ -.card-hover { - transition: all 0.3s ease; -} - -.card-hover:hover { - border-color: rgba(245, 166, 35, 0.3); - box-shadow: 0 0 20px rgba(245, 166, 35, 0.1); -} - -/* Table row hover */ -.row-hover { - transition: background-color 0.2s ease; -} - -.row-hover:hover { - background-color: rgba(245, 166, 35, 0.05); -} - -/* Focus ring */ -.focus-amber:focus-visible { - outline: 2px solid var(--color-hive-amber); - outline-offset: 2px; -} - -/* Subtle grain overlay for texture */ -.grain-overlay::after { - content: ""; - position: absolute; - inset: 0; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.02'/%3E%3C/svg%3E"); - pointer-events: none; - z-index: 0; -} - -.dark { - --sidebar: hsl(240 5.9% 10%); - --sidebar-foreground: hsl(240 4.8% 95.9%); - --sidebar-primary: hsl(224.3 76.3% 48%); - --sidebar-primary-foreground: hsl(0 0% 100%); - --sidebar-accent: hsl(240 3.7% 15.9%); - --sidebar-accent-foreground: hsl(240 4.8% 95.9%); - --sidebar-border: hsl(240 3.7% 15.9%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); -} - -@theme inline { - --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); + background: var(--color-muted-foreground); } diff --git a/thoughts/taras/plans/2026-02-25-feat-new-ui-visual-redesign-plan.md b/thoughts/taras/plans/2026-02-25-feat-new-ui-visual-redesign-plan.md new file mode 100644 index 0000000..589124f --- /dev/null +++ b/thoughts/taras/plans/2026-02-25-feat-new-ui-visual-redesign-plan.md @@ -0,0 +1,768 @@ +--- +title: "feat: New UI Visual Redesign" +type: feat +status: active +date: 2026-02-25 +deepened: 2026-02-25 +generated_with: compound-engineering/workflows:plan + deepen-plan +--- + +# New UI Visual Redesign + +## Enhancement Summary + +**Deepened on:** 2026-02-25 +**Research agents used:** repo-research-analyst, learnings-researcher, best-practices-researcher, Context7 (shadcn/ui, Tailwind v4, AG Grid), pattern-recognition-specialist, architecture-strategist, kieran-typescript-reviewer, browser-navigator + +### Key Improvements +1. **Concrete CSS variable values** — Exact oklch values for the new Zinc-based palette, sourced from shadcn/ui v4 official theming docs +2. **AG Grid theming code** — Specific CSS variable overrides and JS theme API usage for neutral dark/light modes +3. **shadcn/ui native patterns** — Sidebar CSS variables, chart color tokens, component customization patterns from official docs +4. **Systematic approach for Phase 4** — Grep-and-replace strategy with token mapping table +5. **Critical sequencing fix** — Phase 1 keeps hive tokens as aliases to prevent AG Grid/body breakage + +### New Considerations Discovered +- Tailwind v4 uses `@theme` directive for custom colors (not `tailwind.config.js`) +- AG Grid v35 supports JS theme API (`themeQuartz.withParams()`) for programmatic dark/light mode switching +- shadcn/ui chart colors are defined as `--chart-1` through `--chart-5` CSS variables with oklch — use these instead of custom chart palette +- Border color in dark mode should use `oklch(1 0 0 / 10%)` (white at 10% opacity) for subtle separation — this is the shadcn/ui v4 convention +- **Most beehive CSS utility classes are dead code** — only `animate-breathe` (stats-bar.tsx) and `animate-heartbeat` (app-header.tsx) are actually referenced from TSX files +- **Triple color system conflict** — The `globals.css` has oklch tokens in `@theme`, HSL tokens in `:root`, and an `@theme inline` bridge that overrides the oklch sidebar values with HSL ones. The oklch sidebar values are dead code. +- **44 direct Tailwind amber references** exist across 14 TSX files — this is the real migration work, not the hive CSS variables +- **Three independent status-to-color maps** exist (StatusBadge, services/page.tsx, dashboard/page.tsx) — should be consolidated +- **Sparklines in stats cards are scope creep** — current `StatsBarProps` has no historical data, and adding it would require API changes (violating "no API changes" constraint) + +### Review Agent Findings + +**TypeScript Reviewer** flagged: +- Phase 1 grep must catch `hive-` and `terminal-` Tailwind class usage (not just CSS utility classes) +- `statusConfig` should use `satisfies` for type safety +- Stats bar `color?: string` prop should become constrained variant union +- Phase 4 should be split into sub-phases: (4a) Dashboard + list pages, (4b) detail pages, (4c) Chat + Usage + +**Architecture Strategist** flagged: +- **Sequencing bug**: Phase 1 removes hive tokens that `ag-grid.css`, `body` styles, and scrollbar styles depend on → must keep aliases or update simultaneously +- Phase 1 should be split: 1a (add new tokens alongside old), 1b (migrate references, remove old) +- Phases 2 and 3 are independent — can run in parallel +- Phase 5 (animations) is small enough to fold into Phase 1 + +**Pattern Recognition** found: +- Most `.glow-*`, `.honeycomb-bg`, `.hex-border`, `.text-gradient-amber`, `.card-hover`, `.row-hover`, `.clip-hex` CSS classes are **never referenced in any TSX file** — they're dead CSS +- Only 3 TSX files reference hive/terminal tokens directly: `app-sidebar.tsx`, `app-header.tsx`, `ag-grid.css` +- The real migration effort is 44 hardcoded Tailwind `text-amber-*`/`bg-amber-*` references across page components +- `formatInterval` is duplicated between `schedules/page.tsx` and `schedules/[id]/page.tsx` (out of scope but worth noting) +- Chart colors in `usage/page.tsx` are hardcoded hex (`#f59e0b`) not tokens + +--- + +## Overview + +The `new-ui` dashboard was recently built (Phases 1-6, merged Feb 25) as a modern replacement for the old MUI Joy UI dashboard. While functionally complete, it's visually a 1:1 port of the old UI's "beehive" theme — same hex colors, same honeycomb SVG background, same hexagonal stats bar, same Graduate display font, same glow effects. The result feels like a cheap copy rather than a design evolution. + +The goal is to transform the new-ui into a polished, distinctive dashboard that leverages shadcn/ui's strengths instead of fighting them. Think **Linear**, **Vercel Dashboard**, **Raycast** — clean, sharp, professional, with purposeful use of color and motion. + +## Problem Statement + +**Why it looks like a cheap copy:** + +1. **Aesthetic mismatch** — shadcn/ui is minimal/clean by design. Ornate honeycomb patterns, hexagonal clip-paths, and amber glow effects bolted onto Radix primitives create visual dissonance. +2. **Identical palette** — Exact same hex values (`#f5a623`, `#ffb84d`, `#d4a574`, `#c67c00`) carried over from the MUI Joy version. No design evolution occurred. +3. **Graduate display font** — A novelty cursive font that feels unprofessional in a monitoring dashboard. Does not complement Space Grotesk. +4. **Gimmicky decorations** — Hexagonal clip-path stats, honeycomb SVG backgrounds, breathing/pulsing animations on idle elements, grain overlays. These were coherent in the MUI Joy version but feel like CSS tricks in the shadcn/ui context. +5. **Tiny border radii** (0.25–0.75rem) — Creates sharp corners that clash with the organic "hive" metaphor and don't match shadcn/ui's typical rounded aesthetic. +6. **Color overload** — Amber everywhere (borders, hovers, glows, text, badges, stats, icons) makes everything blend together. No visual hierarchy. + +## Proposed Solution + +**Design Direction: "Mission Control"** — Clean, information-dense, professional. Amber/gold becomes a strategic accent (not the dominant color). Dark mode as the hero experience with cool neutrals and warm highlights for key data. + +### Design Principles + +1. **Let shadcn/ui breathe** — Use the library's native patterns, don't override everything with custom CSS +2. **Color with purpose** — Amber for brand accent and active states only; semantic colors for status; neutral base +3. **Typography hierarchy** — Professional font stack, clear size/weight scale, no novelty fonts +4. **Purposeful motion** — Animations only where they communicate state changes, not decorative loops +5. **Information density** — Pack data efficiently without clutter; let spacing and typography create hierarchy + +## Technical Approach + +### Architecture + +All changes are CSS/component-level within `new-ui/src/`. No API changes, no routing changes, no data model changes. The redesign touches: + +- `src/styles/globals.css` — Color system, CSS variables, animations, utility classes +- `src/styles/ag-grid.css` — Grid theme overrides +- `src/components/layout/` — Sidebar, header +- `src/components/shared/` — Stats bar, status badges, data grid, empty states +- `src/pages/` — Page-specific layout and visual updates +- `src/components/ui/` — Minimal shadcn/ui customization tweaks + +### Implementation Phases + +--- + +#### Phase 1: Color System & Typography Foundation + +**Goal:** Replace the beehive palette with a refined system. Establish typography scale. + +**Color System Changes** (`globals.css`): + +- **Base neutrals:** Replace warm brown-blacks (`#0d0906`, `#1a130e`, `#251c15`) with cool-neutral dark tones (Zinc family from shadcn/ui). +- **Brand accent:** Keep amber but constrain it to `--primary` only — interactive elements, active nav items, focused inputs. One accent, not five shades. +- **Semantic status colors:** Standardize on Tailwind defaults — emerald for success, red for destructive, amber for warning, blue for info. Remove custom `hive-rust`, `hive-blue` redundancies. +- **Text hierarchy:** Use shadcn/ui's native `--foreground` / `--muted-foreground` system. Drop the custom `hive-text-*` vars. +- **Surface elevation:** Use shadcn/ui's `--background` → `--card` → `--popover` hierarchy. + +### Research Insights: Exact CSS Variable Values + +**Use shadcn/ui Zinc theme as the base**, then override `--primary` with amber for brand accent. Here are the exact oklch values from shadcn/ui v4 docs: + +```css +/* globals.css — NEW color system */ +@import "tailwindcss"; + +@layer base { + :root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + /* BRAND: amber-700 as primary for light mode */ + --primary: oklch(0.555 0.163 48.998); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.555 0.163 48.998); /* match primary */ + /* Charts: shadcn/ui defaults — balanced palette */ + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + /* Sidebar: shadcn/ui native vars */ + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.555 0.163 48.998); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.555 0.163 48.998); + } + + .dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + /* BRAND: amber-500 as primary for dark mode (brighter) */ + --primary: oklch(0.769 0.188 70.08); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + /* KEY: white at 10% for borders — shadcn/ui v4 dark convention */ + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.769 0.188 70.08); /* match primary */ + /* Charts: dark mode balanced palette */ + --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 */ + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.769 0.188 70.08); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.769 0.188 70.08); + } +} +``` + +**Key design decisions in this palette:** +- Light `--primary` = Tailwind `amber-700` (`oklch(0.555 0.163 48.998)`) — darker amber for light mode contrast +- Dark `--primary` = Tailwind `amber-500` (`oklch(0.769 0.188 70.08)`) — brighter for dark bg visibility +- All neutrals are Zinc family (hue ~286) — cool gray, not warm brown +- Borders in dark mode: `oklch(1 0 0 / 10%)` — the shadcn/ui v4 standard +- Chart colors are shadcn/ui defaults — blue, teal, slate, yellow, amber. Balanced, not amber-dominant + +**Typography Changes:** + +- **Remove Graduate** display font entirely. Use Space Grotesk bold/semibold for headings. +- **Keep Space Grotesk** — it's a solid geometric sans-serif. Don't add Geist/Inter unless Space Grotesk proves problematic. Fewer fonts = faster loads. +- **Establish scale:** Page titles `text-xl font-semibold`, section headers `text-base font-medium`, body `text-sm`, detail `text-xs`. Reduce overall font sizes — the current `text-2xl` titles are too large for a data dashboard. +- **Keep Space Mono** for monospace (data values, IDs, code blocks) — it works well. + +**Border Radius:** + +- Set to shadcn/ui default: `--radius: 0.625rem` (10px). This gives cards `rounded-xl` feel, buttons `rounded-lg`, badges `rounded-full`. + +**Phase 1a: Add new tokens alongside old ones** + +1. Replace the `@theme` oklch values with the new Zinc-based palette (see CSS above) +2. **Remove the `:root` and `.dark` HSL sidebar variable blocks** (the ones using `hsl()`) — they override the oklch sidebar values via `@theme inline` and create dead code +3. **Remove the `@theme inline` bridge block** that maps `--color-sidebar: var(--sidebar)` — no longer needed +4. **Keep `--color-hive-*` vars temporarily** but redefine them as aliases to the new semantic tokens: + ```css + /* TEMPORARY ALIASES — remove in Phase 3 after AG Grid + body migration */ + --color-hive-body: var(--color-background); + --color-hive-surface: var(--color-card); + --color-hive-earth: var(--color-muted); + --color-hive-border: var(--color-border); + --color-hive-text-primary: var(--color-foreground); + --color-hive-text-secondary: var(--color-muted-foreground); + --color-hive-text-tertiary: var(--color-muted-foreground); + --color-hive-amber: var(--color-primary); + --color-hive-honey: var(--color-primary); + --color-hive-gold: var(--color-primary); + --color-hive-deep: var(--color-primary); + --color-hive-rust: var(--color-destructive); + --color-hive-blue: var(--color-info, oklch(0.623 0.214 259.815)); + ``` + This prevents `ag-grid.css`, `body` styles, and scrollbar styles from breaking. +5. Update `body` rule to use `var(--color-background)` and `var(--color-foreground)` +6. Update scrollbar rules to use `var(--color-background)` and `var(--color-border)` + +**Phase 1b: Remove dead CSS and animations** + +All of these CSS classes are **dead code** (never referenced in any TSX file): +- `.honeycomb-bg`, `.grain-overlay` +- `.glow-amber`, `.glow-gold`, `.glow-rust`, `.glow-blue`, `.box-glow-amber`, `.box-glow-gold` +- `.text-gradient-amber` +- `.hex-border::before`, `.clip-hex` +- `.card-hover`, `.row-hover`, `.focus-amber` +- `animate-pulse-amber` keyframe + +Only 2 animation references exist in TSX: +- `animate-breathe` → used in `stats-bar.tsx:34` (will be removed in Phase 3 rewrite) +- `animate-heartbeat` → used in `app-header.tsx:32` (replace with static dot) + +Safe to remove now: +- All dead CSS utility classes listed above +- `animate-pulse-amber` and `animate-heartbeat` keyframes (fix `app-header.tsx` reference) +- Graduate font from `index.html` Google Fonts link (reconstruct URL to keep Space Grotesk + Space Mono) +- `--font-display` variable from `globals.css` +- All `--color-terminal-*` variables (replace the 2 TSX references: `bg-terminal-green` → `bg-emerald-500` and `bg-terminal-red` → `bg-red-500` in `app-header.tsx`) + +**Files to modify:** +- `src/styles/globals.css` — Complete overhaul (new tokens, aliases, remove dead CSS) +- `src/components/layout/app-header.tsx` — Replace `animate-heartbeat` with static dot, replace `terminal-*` classes +- `index.html` — Reconstruct Google Fonts link without Graduate + +**Verification:** +```bash +cd new-ui && pnpm build +# Verify no build errors +# Check all hive/terminal references are either aliased or fixed: +grep -rE "(hive-|terminal-|honeycomb|glow-amber|glow-gold|text-gradient|hex-border|grain-overlay|font-display|clip-hex|Graduate)" src/ --include="*.tsx" --include="*.ts" --include="*.css" +# Expected: only the temporary aliases in globals.css and animate-breathe in stats-bar.tsx +pnpm dev # Verify UI renders correctly with new palette but same layout +``` + +--- + +#### Phase 2: Layout & Navigation + +**Goal:** Polish sidebar and header to feel native to shadcn/ui. + +**Sidebar** (`app-sidebar.tsx`): + +- Remove amber text for title. Use `text-sidebar-foreground` with the brand accent only on the logo mark. +- Navigation items: Remove left-border active indicator. Use `bg-sidebar-accent text-sidebar-accent-foreground` for active state — this is shadcn/ui's native sidebar pattern. These map to the CSS variables defined in Phase 1. +- Hover: `hover:bg-sidebar-accent/50` subtle highlight, not amber text color change. +- Group navigation items semantically with `SidebarGroup` + `SidebarGroupLabel`: + - **Core:** Dashboard, Agents, Tasks, Epics + - **Communication:** Chat + - **Operations:** Services, Schedules, Usage + - **System:** Config, Repos +- Footer: Add version/connection status indicator (small, muted). + +### Research Insights: shadcn/ui Sidebar Pattern + +shadcn/ui Sidebar uses dedicated CSS variables (`--sidebar-*`) that are independent from the main theme. This is already wired up in the Phase 1 color system. The sidebar should use: +- `data-[active=true]` attribute for active nav items (not custom classes) +- `SidebarMenuButton` component's built-in `isActive` prop +- Tooltip on collapsed state (built into shadcn/ui Sidebar) + +**Header** (`app-header.tsx`): + +- Simplify: breadcrumbs + right-side actions (theme toggle, command menu trigger, health dot). +- Health indicator: Simple green/red 6px circle. No animation. Tooltip on hover for details. +- Remove excess spacing; header should feel tight and functional. + +**Main content area:** + +- Keep full-width for data-dense pages (agents, tasks grids need horizontal space). +- Page padding: `px-6 py-6` desktop, `px-4 py-4` mobile. +- Do NOT add `max-w-7xl` — data grids benefit from full width on wide monitors. + +**Files to modify:** +- `src/components/layout/app-sidebar.tsx` +- `src/components/layout/app-header.tsx` +- `src/components/layout/root-layout.tsx` + +**Verification:** +```bash +cd new-ui && pnpm build +pnpm dev # Visual inspection of sidebar/header at multiple widths +# Check: sidebar active state uses bg-accent, not amber border +# Check: sidebar collapse shows icon-only with tooltips +# Check: header is clean, health dot is simple +``` + +--- + +#### Phase 3: Core Shared Components + +**Goal:** Upgrade the visual quality of reusable components. + +**Stats Display** (`stats-bar.tsx`) — **Complete rewrite:** + +- **Remove hexagonal clip-paths entirely.** Replace with a clean card grid. +- Use a row of compact stat cards: `grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-3`. +- Each stat card: small icon + large number + label. No colored borders — keep it clean. +- Numbers in `font-mono tabular-nums text-2xl font-bold`. +- Labels in `text-xs text-muted-foreground uppercase tracking-wider`. +- **Sparklines/trends: OUT OF SCOPE** — `StatsBarProps` has no historical data, adding it requires API changes. +- Remove breathing animation — stats are static numbers. + +```tsx +// stats-bar.tsx — simplified stat card +function StatCard({ icon: Icon, label, value, trend }: StatCardProps) { + return ( + + +

+ + {label} +
+
+ + {value} + + {trend && ( + 0 ? "text-emerald-500" : "text-red-500" + )}> + {trend > 0 ? "+" : ""}{trend}% + + )} +
+
+
+ ); +} +``` + +**Status Badges** (`status-badge.tsx`): + +- Keep the color-coded system but refine: + - Use shadcn/ui `Badge` with variant `outline` as base. + - Active states (busy, in_progress): subtle `animate-pulse` on a small dot next to the text, not the whole badge. + - Text: `text-[11px] font-medium` (not bold + uppercase + tracking-wide). + - Small colored dot (6px `rounded-full`) before the text. + +```tsx +// status-badge.tsx — refined +const statusConfig = { + idle: { dot: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400" }, + busy: { dot: "bg-amber-500 animate-pulse", text: "text-amber-600 dark:text-amber-400" }, + offline: { dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, + pending: { dot: "bg-yellow-500", text: "text-yellow-600 dark:text-yellow-400" }, + in_progress: { dot: "bg-amber-500 animate-pulse", text: "text-amber-600 dark:text-amber-400" }, + completed: { dot: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400" }, + failed: { dot: "bg-red-500", text: "text-red-600 dark:text-red-400" }, + cancelled: { dot: "bg-zinc-400", text: "text-zinc-500 dark:text-zinc-400" }, +} as const; + +function StatusBadge({ status }: { status: string }) { + const config = statusConfig[status] ?? statusConfig.offline; + return ( + + + {status.replace("_", " ")} + + ); +} +``` + +**Data Grid** (`data-grid.tsx` + `ag-grid.css`): + +### Research Insights: AG Grid v35 Theming + +AG Grid v35 supports a JS theme API alongside CSS variables. Use CSS variables for the color overrides since they integrate with the shadcn/ui CSS variable system: + +```css +/* ag-grid.css — neutral theme overrides */ +.ag-theme-quartz, +.ag-theme-quartz-dark { + /* Map to shadcn/ui CSS vars */ + --ag-background-color: var(--background); + --ag-foreground-color: var(--foreground); + --ag-header-background-color: var(--muted); + --ag-header-foreground-color: var(--muted-foreground); + --ag-odd-row-background-color: transparent; + --ag-row-hover-color: var(--accent); + --ag-selected-row-background-color: var(--accent); + --ag-border-color: var(--border); + --ag-secondary-border-color: var(--border); + --ag-font-family: "Space Grotesk", system-ui, sans-serif; + --ag-font-size: 13px; + --ag-header-column-resize-handle-color: var(--border); + --ag-range-selection-border-color: var(--primary); +} +``` + +This approach ties AG Grid's colors directly to the shadcn/ui theme vars, so dark/light mode switching works automatically. + +**Empty States** (`empty-state.tsx`): + +- Add illustration or icon + title + description + optional CTA button. +- Center vertically in the available space. +- Muted colors, clean design. + +**Also in Phase 3: Remove hive token aliases from globals.css** + +After AG Grid is rethemed to use standard semantic tokens, the temporary `--color-hive-*` aliases from Phase 1a are no longer needed. Remove them: +```bash +grep -n "TEMPORARY ALIASES" src/styles/globals.css +# Remove the entire alias block +``` + +**Type safety improvements (while touching these files):** +- `StatusBadge`: Use `satisfies Record` for the config map +- Stats bar: Replace `color?: string` prop with `variant?: "amber" | "emerald" | "blue" | "red" | "yellow" | "zinc"` +- Standardize on `cn()` for all conditional class merging + +**Files to modify:** +- `src/components/shared/stats-bar.tsx` — Rewrite +- `src/components/shared/status-badge.tsx` — Refine +- `src/components/shared/data-grid.tsx` — Retheme +- `src/components/shared/empty-state.tsx` — Polish +- `src/styles/ag-grid.css` — Neutral palette (use semantic CSS vars) +- `src/styles/globals.css` — Remove hive aliases + +**Verification:** +```bash +cd new-ui && pnpm build +pnpm dev # Check dashboard stats, agent/task grids, empty states +# Verify: AG Grid colors match theme in both dark and light mode +# Verify: status badges use dot pattern, not full-bg color +# Verify: no more hive aliases in globals.css +grep -rE "color-hive-" src/ --include="*.css" --include="*.tsx" +# Should return 0 results +``` + +--- + +#### Phase 4: Page-by-Page Visual Polish + +**Goal:** Update each page to use the new design system consistently. + +### Research Insights: Systematic Approach + +Instead of editing 13 files individually, use a systematic grep-and-replace strategy: + +**Step 1: Global search-and-replace** for common patterns across all pages: +```bash +# Find all amber hardcoded references +grep -rn "amber-\|hive-\|text-amber\|bg-amber\|border-amber" src/pages/ --include="*.tsx" + +# Find all font-display (Graduate) references +grep -rn "font-display" src/pages/ --include="*.tsx" + +# Find all glow/animation references +grep -rn "glow\|breathe\|heartbeat\|pulse-amber" src/pages/ --include="*.tsx" +``` + +**Step 2: Replace patterns:** +- `text-amber-500` → `text-primary` (uses CSS var, adapts to theme) +- `bg-amber-500` → `bg-primary` +- `border-amber-500/30` → `border-border` +- `text-hive-amber` → `text-primary` +- `font-display` → `font-sans font-semibold` (or just remove, use default) +- `text-2xl` page titles → `text-xl` +- Any amber icon color (`text-amber-400/500`) → `text-muted-foreground` (for decorative icons) or `text-primary` (for interactive icons) + +### Token Mapping Table (for systematic replacement) + +| Old Pattern | New Pattern | Context | +|-------------|-------------|---------| +| `text-hive-amber` | `text-primary` | Brand accent text | +| `border-hive-amber` | `border-primary` | Brand accent border | +| `bg-amber-600 hover:bg-amber-700` | `bg-primary hover:bg-primary/90` | Primary action buttons | +| `text-amber-400` (icons) | `text-muted-foreground` | Decorative icons in cards | +| `text-amber-400` (links) | `text-primary` | Interactive links | +| `bg-amber-500/15 text-amber-400` | `bg-primary/15 text-primary` | Active channel highlight | +| `bg-amber-500/20 text-amber-400` | `bg-muted text-muted-foreground` | Chat avatar circles | +| `bg-amber-500` (progress) | `bg-primary` | Progress bars | +| `hover:border-amber-500/30` | `hover:border-primary/30` | Card hover effect | +| `font-display text-2xl font-bold` | `text-xl font-semibold` | Page titles (21 occurrences!) | +| `hover:text-hive-amber` | `hover:text-primary` | Sidebar hover | +| `#f59e0b` (chart) | `var(--chart-1)` via Recharts | Chart colors | +| `#18181b` (tooltip bg) | CSS var reference | Recharts tooltip | + +**Step 3: Sub-phased page updates** + +**Phase 4a: Dashboard + list pages** (agents, tasks, epics, services, schedules, repos) +- Apply token mapping table globally across these files +- These are mostly grids + filter toolbars — changes are mechanical + +**Phase 4b: Detail pages** (agents/[id], tasks/[id], epics/[id], schedules/[id]) +- Apply token mapping table +- Task detail: terminal-styled log block (dark bg with `bg-zinc-950` and neutral text) +- Metadata sections: clean key-value pairs + +**Phase 4c: Chat + Usage + Config** (most complex pages) +- Chat: avatar circles, send button, active channel highlight, message styling +- Usage: hardcoded `CHART_COLORS` array → use `--chart-*` CSS vars. Replace `background: "#18181b"` tooltip styles with CSS var references. +- Config: connection status, form styling + +**Phase 4d: Consolidate status color maps** +- Extract `services/page.tsx:statusColors` and `dashboard/page.tsx:eventColors` to use shared constants or route through StatusBadge + +**Verification gates between sub-phases:** +```bash +# After each sub-phase: +cd new-ui && pnpm build +pnpm dev # Visual check of affected pages +grep -rn "amber-[0-9]\|hive-\|#f5a623\|#ffb84d\|#d4a574\|#c67c00" src/pages/ --include="*.tsx" +``` + +**Files to modify:** +- All files in `src/pages/` (13 page components) +- Focus on: replacing amber hardcodes with CSS var references, consistent spacing + +**Verification (final):** +```bash +cd new-ui && pnpm build +pnpm dev # Navigate through all pages, check visual consistency +# Verify no remaining hardcoded amber: +grep -rn "amber-[0-9]\|hive-\|#f5a623\|#ffb84d\|#d4a574\|#c67c00\|font-display" src/pages/ --include="*.tsx" +# Should return 0 results +``` + +--- + +#### Phase 5: Animations & Micro-interactions + +**Goal:** Replace decorative animations with purposeful transitions. + +**Remove from `globals.css`:** +```css +/* DELETE these keyframes */ +@keyframes breathe { ... } +@keyframes pulse-amber { ... } +@keyframes heartbeat { ... } +``` + +**Keep (already in Tailwind):** +- `animate-pulse` — Only on actively in-progress status badge dots +- `animate-spin` — For loading spinners + +**Hover transitions** — add to `globals.css`: +```css +/* Minimal utility for interactive element transitions */ +.transition-default { + @apply transition-colors duration-150 ease-in-out; +} +``` + +Actually, don't add a custom class. Just use Tailwind's built-in `transition-colors duration-150` directly in components. Fewer abstractions. + +**Skeleton loading:** shadcn/ui `Skeleton` component already has a shimmer animation. Use it for loading states in data grids and stat cards. + +**Files to modify:** +- `src/styles/globals.css` — Remove animation keyframes +- `src/components/shared/status-badge.tsx` — Ensure pulse only on dot, not whole badge + +**Verification:** +```bash +cd new-ui && pnpm build +pnpm dev # Test interactions, hover states, loading states +# Verify no breathing/heartbeat animations remain: +grep -rn "breathe\|heartbeat\|pulse-amber" src/ --include="*.tsx" --include="*.ts" --include="*.css" +``` + +--- + +#### Phase 6: Dark/Light Mode Consistency & Responsive Polish + +**Goal:** Ensure both themes look great and responsive works well. + +**Dark mode** (primary experience): + +- The Zinc theme values from Phase 1 handle dark mode automatically via the `.dark` class. +- Key contrast check: `--primary` (amber-500, oklch 0.769) on `--background` (oklch 0.141) = sufficient contrast. +- `--muted-foreground` (oklch 0.705) on `--background` (oklch 0.141) = contrast ratio ~5:1, passes WCAG AA. +- Borders at `oklch(1 0 0 / 10%)` provide subtle separation without harsh lines. + +**Light mode:** + +- `--primary` in light mode is amber-700 (oklch 0.555), darker for contrast on white. +- Verify card borders (`oklch(0.92 0.004 286.32)`) are visible but subtle. + +**Responsive:** + +- Test at 320px, 768px, 1024px, 1440px, 1920px. +- Sidebar: Fully collapsed/hidden on mobile, expandable via trigger (built into shadcn/ui Sidebar). +- Stats grid: 2 cols on mobile, 4-5 on desktop. +- AG Grid: Horizontal scroll on mobile, full view on desktop. +- Chat: Full-width on mobile, split view on desktop. + +**Accessibility:** + +- Focus rings: shadcn/ui default ring uses `--ring` CSS var (set to match `--primary` in our palette). +- Color contrast: The Zinc palette is designed for WCAG AA compliance. +- Touch targets: Minimum 44x44px on mobile for buttons and nav items (check sidebar items). + +**Files to modify:** +- `src/styles/globals.css` — Final audit of dark/light variables +- Various components — Responsive class adjustments if needed + +**Verification:** +```bash +cd new-ui && pnpm build +pnpm dev +# Test both themes (toggle via header button) +# Test at: 320px, 768px, 1024px, 1440px, 1920px viewport widths +# Run Lighthouse audit for accessibility score +``` + +--- + +## Acceptance Criteria + +### Functional Requirements + +- [ ] All existing pages render correctly with no visual regressions in functionality +- [ ] Dark mode and light mode both look polished and consistent +- [ ] AG Grid data grids are readable and well-styled in both themes +- [ ] Status badges clearly communicate state with semantic colors +- [ ] Navigation is intuitive with clear active state indication +- [ ] Charts/visualizations use shadcn/ui chart color tokens (`--chart-1` through `--chart-5`) +- [ ] Command palette (Cmd+K) works correctly with new styling + +### Visual Quality + +- [ ] No honeycomb SVG backgrounds anywhere +- [ ] No hexagonal clip-path shapes +- [ ] No Graduate display font +- [ ] No amber glow effects (text-shadow or box-shadow) +- [ ] No breathing/heartbeat animations on static elements +- [ ] No `--color-hive-*` or `--color-terminal-*` CSS variables +- [ ] Amber/gold used only via `--primary` CSS variable, not hardcoded hex +- [ ] Consistent spacing and typography scale across all pages +- [ ] Professional appearance suitable for a monitoring/ops dashboard + +### Non-Functional Requirements + +- [ ] Build succeeds with no TypeScript errors (`pnpm build`) +- [ ] No unused CSS classes or imports +- [ ] Performance: No visible jank from CSS changes +- [ ] Responsive: Works at 320px through 1920px widths +- [ ] Accessibility: WCAG AA contrast ratios on text +- [ ] Font loading: Only Space Grotesk + Space Mono (no Graduate) + +## Success Metrics + +- The dashboard looks like it could be a product page for a YC startup — clean, professional, distinctive +- A screenshot of any page could be shared publicly without embarrassment +- New users can immediately understand status at a glance through color and layout alone +- The design feels native to shadcn/ui, not like a ported theme + +## Dependencies & Risks + +**Dependencies:** +- No external dependencies added. All changes use existing packages. +- shadcn/ui components are already installed. + +**Risks:** +- **Scope creep** — Phase 4 (page-by-page) is the largest. The systematic grep approach mitigates this — most changes are mechanical find-replace. +- **AG Grid theming** — Mapping AG Grid CSS vars to shadcn/ui CSS vars is the key insight. Test with real data to verify colors render correctly. +- **oklch browser support** — oklch is supported in all modern browsers (Chrome 111+, Firefox 113+, Safari 15.4+). Not a concern for a developer tool dashboard. +- **Color accessibility** — The Zinc palette from shadcn/ui is pre-tested for WCAG AA. Our amber accent choices (amber-500 dark, amber-700 light) also pass. + +## Sources & References + +### Internal References +- Old UI theme: `ui/src/lib/theme.ts`, `ui/src/index.css` +- New UI styles: `new-ui/src/styles/globals.css`, `new-ui/src/styles/ag-grid.css` +- Layout: `new-ui/src/components/layout/` +- Shared components: `new-ui/src/components/shared/` +- All pages: `new-ui/src/pages/` + +### External References (from Context7 research) +- shadcn/ui v4 theming (Zinc theme): https://ui.shadcn.com/docs/theming +- shadcn/ui Sidebar CSS variables: https://ui.shadcn.com/docs/components/sidebar +- shadcn/ui Chart color tokens: https://ui.shadcn.com/docs/components/chart +- Tailwind CSS v4 colors (oklch): https://tailwindcss.com/docs/colors +- Tailwind CSS v4 `@theme` directive: https://tailwindcss.com/docs/theme +- AG Grid React theming: https://ag-grid.com/react-data-grid/theming-colors +- AG Grid CSS variable customization: https://ag-grid.com/react-data-grid/theming-v32-customisation + +### Design Inspiration +- Linear (linear.app) — Clean sidebar, neutral palette, subtle animations +- Vercel Dashboard — Information density, dark mode as primary +- Raycast — Typography hierarchy, status indicators + +## Manual E2E Verification + +After all phases: + +```bash +cd new-ui + +# Build check +pnpm build + +# Start dev server +pnpm dev + +# Open in browser and verify each page: +# http://localhost:5274/ — Dashboard with stats, activity feed +# http://localhost:5274/agents — Agent grid with filters +# http://localhost:5274/agents/ — Agent detail with tabs +# http://localhost:5274/tasks — Task grid with filters +# http://localhost:5274/tasks/ — Task detail with logs +# http://localhost:5274/epics — Epics grid +# http://localhost:5274/chat — Chat with channels +# http://localhost:5274/services — Services grid +# http://localhost:5274/schedules — Schedules grid +# http://localhost:5274/usage — Charts and analytics +# http://localhost:5274/config — Config panel +# http://localhost:5274/repos — Repos grid + +# Test both themes (toggle via header button) +# Test sidebar collapse/expand +# Test Cmd+K command palette +# Test at mobile viewport (320px) and desktop (1920px) + +# Verify no CSS remnants: +grep -r "honeycomb\|glow-amber\|glow-gold\|Graduate\|hex-border\|grain-overlay\|breathe\|heartbeat\|pulse-amber\|hive-\|terminal-\|font-display\|clip-hex" src/ --include="*.tsx" --include="*.ts" --include="*.css" +# Should return 0 results +``` diff --git a/thoughts/taras/research/2026-02-25-dashboard-ui-design-best-practices.md b/thoughts/taras/research/2026-02-25-dashboard-ui-design-best-practices.md new file mode 100644 index 0000000..90f3ff2 --- /dev/null +++ b/thoughts/taras/research/2026-02-25-dashboard-ui-design-best-practices.md @@ -0,0 +1,825 @@ +--- +date: 2026-02-25T12:00:00Z +topic: "Modern Dashboard UI Design Best Practices (2025-2026)" +--- + +# Modern Dashboard UI Design Best Practices (2025-2026) + +Research date: 2026-02-25 +Context: Agent Swarm monitoring dashboard redesign, moving from MUI Joy + amber/beehive theme to a modern, polished design system. + +--- + +## Table of Contents + +1. [Color Palette Design for Dark-Mode-First Dashboards](#1-color-palette-design) +2. [Visual Hierarchy, Spacing, and Typography](#2-visual-hierarchy-spacing-typography) +3. [shadcn/ui + Tailwind CSS v4 Dashboard Patterns](#3-shadcn-tailwind-v4) +4. [AG Grid Theming Best Practices](#4-ag-grid-theming) +5. [Sidebar Navigation Design Patterns](#5-sidebar-navigation) +6. [Status Indicators and Badge Design](#6-status-indicators) +7. [Card vs Table Layouts for Monitoring](#7-card-vs-table-layouts) +8. [Micro-Animations and Transitions](#8-micro-animations) +9. [Reference Dashboards and Design Systems](#9-reference-dashboards) +10. [Concrete Recommendations for Agent Swarm](#10-agent-swarm-recommendations) + +--- + +## 1. Color Palette Design + +### Moving Beyond Amber/Gold + +The current Agent Swarm UI uses a warm amber/honey/gold palette (#F5A623 primary, #D4A574 gold, #0d0906 body). While thematic ("beehive"), this approach has several issues: + +- Amber/warm tones on dark backgrounds produce a "dated" or "retro terminal" aesthetic +- Limited semantic range: amber already means "warning" in most color systems, so using it as a primary creates confusion +- The warm brown surfaces (#1a130e, #251c15) reduce contrast compared to neutral darks + +### Modern Dark Mode Color Theory + +**Source: Material Design, Linear, Vercel Geist, shadcn/ui defaults** + +The industry consensus in 2025-2026: + +1. **Use near-black neutrals, not pure black or tinted blacks** + - Pure #000000 is too harsh and eliminates elevation perception + - Tinted blacks (like the current #0d0906 warm brown) bias the entire palette + - Best practice: neutral grays with very slight cool or warm tint + +2. **OKLCH color space** is the new standard for generating perceptually uniform palettes + - Linear migrated to LCH for theme generation + - shadcn/ui now uses OKLCH as default format + - Key advantage: changing lightness doesn't cause hue/saturation drift + +3. **Layered darkness for elevation** (Material Design principle) + - Base: #09111A to #121212 range + - Surface +1: 5% lighter (cards, panels) + - Surface +2: 8-10% lighter (popovers, dropdowns) + - Surface +3: 12-15% lighter (elevated modals) + +### Recommended Dark Palette: Cool Neutral + +A cool neutral palette that doesn't commit to a strong brand hue, keeping the focus on content: + +```css +/* === BACKGROUNDS (layered darkness) === */ +--background: oklch(0.145 0 0); /* ~#0a0a0a - page base */ +--surface-1: oklch(0.175 0.005 270); /* ~#111318 - cards, panels */ +--surface-2: oklch(0.21 0.006 270); /* ~#181b22 - elevated cards */ +--surface-3: oklch(0.25 0.007 270); /* ~#1f2330 - popovers, modals */ + +/* === BORDERS === */ +--border: oklch(1 0 0 / 8%); /* subtle white overlay */ +--border-strong: oklch(1 0 0 / 14%); /* active/focused borders */ + +/* === TEXT === */ +--text-primary: oklch(0.985 0 0); /* ~#fafafa - headings, primary */ +--text-secondary: oklch(0.71 0.01 270); /* ~#a1a1aa - descriptions */ +--text-tertiary: oklch(0.55 0.01 270); /* ~#71717a - timestamps, hints */ +--text-muted: oklch(0.45 0.01 270); /* ~#52525b - disabled text */ +``` + +### Accent Color: Moving to Blue-Indigo + +Instead of amber as the primary accent, use a blue-indigo which: +- Is universally understood as "interactive/clickable" +- Provides clear contrast against warm semantic colors (success green, warning amber, error red) +- Works well for selection highlights, links, and focus rings + +```css +/* === ACCENT (Blue-Indigo) === */ +--accent: oklch(0.62 0.19 264); /* ~#6366f1 - indigo-500 */ +--accent-hover: oklch(0.56 0.21 264); /* ~#4f46e5 - indigo-600 */ +--accent-muted: oklch(0.62 0.19 264 / 15%); /* selection backgrounds */ +--accent-foreground: oklch(0.985 0 0); /* white text on accent */ + +/* === SEMANTIC COLORS === */ +/* Success - Emerald */ +--success: oklch(0.70 0.17 162); /* ~#10b981 - emerald-500 */ +--success-muted: oklch(0.70 0.17 162 / 15%); + +/* Warning - Amber (now properly semantic, not primary) */ +--warning: oklch(0.75 0.18 80); /* ~#f59e0b - amber-500 */ +--warning-muted: oklch(0.75 0.18 80 / 15%); + +/* Error/Destructive - Red */ +--error: oklch(0.63 0.21 25); /* ~#ef4444 - red-500 */ +--error-muted: oklch(0.63 0.21 25 / 15%); + +/* Info - Sky Blue */ +--info: oklch(0.68 0.14 230); /* ~#38bdf8 - sky-400 */ +--info-muted: oklch(0.68 0.14 230 / 15%); +``` + +### Alternative Accent: Teal-Cyan + +If indigo feels too "generic", teal-cyan is a distinctive alternative that's popular in monitoring/DevOps tools (Datadog, Grafana): + +```css +--accent: oklch(0.72 0.15 190); /* ~#14b8a6 - teal-500 */ +--accent-hover: oklch(0.66 0.16 190); /* ~#0d9488 - teal-600 */ +``` + +### Chart Colors (5-color data visualization palette) + +```css +--chart-1: oklch(0.62 0.19 264); /* indigo - primary series */ +--chart-2: oklch(0.70 0.17 162); /* emerald - secondary */ +--chart-3: oklch(0.68 0.14 230); /* sky blue */ +--chart-4: oklch(0.72 0.16 310); /* purple/violet */ +--chart-5: oklch(0.75 0.18 80); /* amber */ +``` + +--- + +## 2. Visual Hierarchy, Spacing, and Typography + +### Typography + +**Source: Linear redesign, font pairing research, data-dense UI studies** + +**Recommendation: Inter + JetBrains Mono (or Geist Sans + Geist Mono)** + +| Use Case | Font | Weight | Size | +|---|---|---|---| +| Page titles | Inter Display (or Inter) | 600 | 20-24px | +| Section headings | Inter | 600 | 16-18px | +| Card titles | Inter | 500 | 14-15px | +| Body text | Inter | 400 | 13-14px | +| Table data | Inter (tabular nums) | 400 | 13px | +| Code/IDs/timestamps | JetBrains Mono | 400 | 12-13px | +| Badges/labels | Inter | 600 | 11-12px (UPPERCASE, 0.05em tracking) | + +Key insight from research: **Use tabular numerals in proportional fonts for numeric data** rather than monospace. Monospace is overkill for numbers. Inter supports `font-variant-numeric: tabular-nums` which gives aligned columns with better readability. + +Current UI uses Space Grotesk + Space Mono. These are fine fonts but: +- Space Grotesk has a more "playful" personality than a monitoring dashboard warrants +- Inter is the industry standard for data-dense UIs (Linear, Vercel, shadcn, Raycast all use it) + +### Spacing Scale + +Follow the Tailwind 4px grid (each unit = 0.25rem = 4px): + +``` +Micro: 4px (gap between icon and label) +Small: 8px (gap between related items, padding in small components) +Medium: 12px (card internal padding, gap between list items) +Regular: 16px (standard content padding) +Large: 24px (section gaps, card padding) +XLarge: 32px (page-level section spacing) +XXLarge: 48px (major layout divisions) +``` + +**Dashboard-specific spacing rules:** +- Card padding: 16px (p-4) for standard, 20px (p-5) for featured cards +- Gap between cards in a grid: 16px (gap-4) +- Sidebar width: 240px expanded, 48px collapsed (icon-only) +- Tab bar height: 40px +- Table row height: 40-44px for data density without feeling cramped +- Header height: 48-56px + +### Visual Hierarchy Principles + +1. **Size contrast for emphasis**: Titles 20-24px, body 13-14px (1.5-1.8x ratio) +2. **Color contrast for importance**: Primary text (#fafafa) vs secondary (#a1a1aa) - clear 2-level system +3. **Weight contrast for scanability**: 600 weight for headings/labels, 400 for body +4. **Spatial grouping**: Related content is 8-12px apart; unrelated content 24-32px apart +5. **Border-free elevation**: Use background color differences instead of visible borders where possible (Linear's approach) + +--- + +## 3. shadcn/ui + Tailwind CSS v4 Dashboard Patterns + +### Tailwind v4 Architecture + +Tailwind v4 fundamentally changes how you configure themes: + +```css +/* No more tailwind.config.js - everything in CSS */ +@import "tailwindcss"; + +@theme { + /* Design tokens become CSS variables that auto-generate utilities */ + --color-background: oklch(0.145 0 0); + --color-surface: oklch(0.175 0.005 270); + --color-accent: oklch(0.62 0.19 264); + + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 0.75rem; + --spacing-lg: 1rem; + + --font-sans: "Inter", system-ui, sans-serif; + --font-mono: "JetBrains Mono", monospace; + + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; +} +``` + +### Three-Layer Token Hierarchy (Best Practice) + +```css +/* Layer 1: Base (raw palette) */ +--color-gray-50: oklch(0.985 0 0); +--color-gray-100: oklch(0.95 0.003 270); +--color-gray-900: oklch(0.21 0.006 270); +--color-gray-950: oklch(0.145 0 0); + +/* Layer 2: Semantic (maps to purpose) */ +--color-background: var(--color-gray-950); +--color-foreground: var(--color-gray-50); +--color-muted: var(--color-gray-900); +--color-muted-foreground: var(--color-gray-400); + +/* Layer 3: Component (variant-specific) */ +--sidebar-background: var(--color-gray-900); +--sidebar-foreground: var(--color-gray-50); +--card-background: var(--color-surface); +``` + +Never skip the semantic layer -- it's what makes refactors safe. + +### shadcn/ui Default Dark Theme (Reference) + +As of 2025-2026, shadcn/ui uses OKLCH by default: + +```css +.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.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); +} +``` + +### Essential shadcn Components for Dashboards + +- **Card** -- metric containers, KPI displays +- **DataTable** -- powered by TanStack Table, sorting/filtering/pagination built in +- **Sidebar** -- collapsible navigation with icon-only mode +- **Tabs** -- content section switching +- **Badge** -- status indicators +- **Sheet** -- mobile-responsive slide-in panels +- **Command** -- command palette (Cmd+K) +- **Chart** -- built on Recharts, inherits theme colors + +--- + +## 4. AG Grid Theming Best Practices + +### Modern AG Grid Theming API + +AG Grid's Theming API (v31+) uses `themeQuartz` with `withParams`: + +```typescript +import { themeQuartz, colorSchemeDark } from "ag-grid-community"; + +const customDarkTheme = themeQuartz + .withPart(colorSchemeDark) + .withParams({ + // Match your dashboard background + backgroundColor: "#111318", + + // Match your text colors + foregroundColor: "#fafafa", + + // Match your accent color + accentColor: "#6366f1", + + // Borders -- subtle + borderColor: "rgba(255, 255, 255, 0.08)", + + // Row alternation + oddRowBackgroundColor: "rgba(255, 255, 255, 0.02)", + + // Header styling + headerBackgroundColor: "#0f1116", + headerTextColor: "#a1a1aa", + headerFontWeight: 600, + headerFontSize: 12, + + // Cell text + cellTextColor: "#e4e4e7", + fontSize: 13, + + // Selection + selectedRowBackgroundColor: "rgba(99, 102, 241, 0.12)", + rangeSelectionBorderColor: "#6366f1", + + // Chrome (panels, toolbars) + chromeBackgroundColor: "#0f1116", + + // Spacing + cellHorizontalPadding: 12, + rowHeight: 40, + headerHeight: 40, + + // Borders + rowBorderColor: "rgba(255, 255, 255, 0.04)", + columnBorderColor: "transparent", + + // Font + fontFamily: "'Inter', system-ui, sans-serif", + }); +``` + +### Key AG Grid Dark Mode Principles + +1. **Match the grid background to your card/surface color** -- the grid should feel embedded, not floating +2. **Reduce border prominence** -- use very subtle borders (4-8% white opacity) or eliminate column borders entirely +3. **Dim the header** -- header should be slightly darker than data rows, with muted text color +4. **Use subtle row alternation** -- 2-3% white overlay for odd rows, not a visible color difference +5. **Accent color for selection only** -- don't overuse the accent; let data be neutral +6. **Match font to your dashboard** -- set fontFamily to match your UI typography +7. **Consistent row height** -- 40px is the sweet spot for data density with touch-friendliness + +### Custom CSS Overrides for Polish + +```css +/* Remove the outer grid border for seamless card integration */ +.ag-root-wrapper { + border: none !important; + border-radius: 0 !important; +} + +/* Soften header separator */ +.ag-header { + border-bottom: 1px solid rgba(255, 255, 255, 0.06) !important; +} + +/* Status cell with colored dot */ +.ag-cell.status-cell { + display: flex; + align-items: center; + gap: 8px; +} + +/* Smooth hover transition */ +.ag-row:hover { + transition: background-color 150ms ease; +} +``` + +--- + +## 5. Sidebar Navigation Design Patterns + +### shadcn/ui Sidebar Architecture + +The shadcn sidebar component provides a battle-tested pattern: + +``` +SidebarProvider (manages state) + Sidebar (collapsible="icon" | "offcanvas" | "none") + SidebarHeader + Logo / App name (hidden when collapsed) + SidebarContent + SidebarGroup (label: "Navigation") + SidebarMenu + SidebarMenuItem -> SidebarMenuButton + SidebarGroup (label: "Management") + Collapsible + SidebarGroupLabel -> CollapsibleTrigger + CollapsibleContent + SidebarMenu -> items + SidebarFooter + User avatar / settings + SidebarRail (thin resize/toggle rail) +``` + +### Design Token Setup + +```css +--sidebar-width: 16rem; /* 256px expanded */ +--sidebar-width-mobile: 18rem; /* 288px on mobile sheet */ +--sidebar-width-icon: 3rem; /* 48px collapsed */ +``` + +### Navigation Grouping for Agent Swarm + +Recommended sidebar structure: + +``` +[Icon] Agent Swarm <- Logo/app name + +--- Core --- + Agents (Users icon) + Tasks (ListTodo icon) + Epics (Target icon) + Chat (MessageSquare icon) + +--- System --- + Services (Server icon) + Schedules (Clock icon) + Repos (GitBranch icon) + +--- Analytics --- + Usage (BarChart3 icon) + +--- Bottom (footer) --- + Config (Settings icon) + Theme toggle (Sun/Moon icon) +``` + +### Collapse Behavior + +1. **Desktop**: Default expanded, user can collapse to icon-only mode +2. **Collapsed mode**: Show only icons (24px), tooltip on hover for label +3. **Mobile**: Off-canvas sheet that slides in from left +4. **Active state**: Highlighted with accent-muted background + accent left border or indicator dot +5. **Persist state**: Store collapse preference in localStorage + +### Active Item Styling + +```css +/* Active nav item */ +.nav-item-active { + background: oklch(0.62 0.19 264 / 12%); /* accent at 12% opacity */ + color: oklch(0.75 0.15 264); /* lighter accent for text */ + border-left: 2px solid oklch(0.62 0.19 264); + font-weight: 500; +} + +/* Hover state */ +.nav-item:hover { + background: oklch(1 0 0 / 5%); +} +``` + +--- + +## 6. Status Indicators and Badge Design + +### Semantic Status Colors + +**Source: Carbon Design System, Material Design, AIA Qi Design System, industry consensus** + +For a monitoring dashboard, define status by semantic meaning, not arbitrary colors: + +| Status | Color | Hex (Dark Mode) | OKLCH | Use Case | +|---|---|---|---|---| +| Success/Online/Completed | Emerald | #10b981 | oklch(0.70 0.17 162) | Healthy, done, active | +| Warning/Busy/In Progress | Amber | #f59e0b | oklch(0.75 0.18 80) | Attention needed | +| Error/Failed | Red | #ef4444 | oklch(0.63 0.21 25) | Critical, broken | +| Info/Reviewing | Blue | #3b82f6 | oklch(0.62 0.16 255) | Informational | +| Neutral/Offline/Cancelled | Gray | #71717a | oklch(0.55 0.01 270) | Inactive, disabled | +| Special/Offered | Violet | #8b5cf6 | oklch(0.58 0.22 293) | Queued, pending action | + +### Badge Design Pattern + +```tsx +// Minimal badge - colored dot + uppercase label + + + Online + +``` + +### Design Rules for Status Badges + +1. **Use a colored dot + text** -- never rely on color alone (accessibility) +2. **Background at 10-15% opacity** of the status color -- just enough to create a "pill" +3. **Border at 15-20% opacity** -- adds definition without heaviness +4. **Text in a lighter shade** of the status color (400 weight in Tailwind scale) +5. **Uppercase, small, tracked** -- 11-12px, font-weight 600, letter-spacing 0.05em +6. **Monospace or tabular font** not needed for badges -- use the UI font +7. **Animated pulse only for truly active states** -- "in_progress" and "busy" only, not everything + +### Dot Indicator Patterns + +For compact spaces (table cells, sidebar), use dot-only indicators: + +```css +/* Pulsing dot for active */ +.status-dot-active { + width: 8px; + height: 8px; + border-radius: 50%; + background: #10b981; + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); + animation: pulse-ring 2s ease-out infinite; +} + +@keyframes pulse-ring { + 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); } + 70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); } + 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } +} + +/* Static dot for stable states */ +.status-dot-static { + width: 8px; + height: 8px; + border-radius: 50%; +} +``` + +--- + +## 7. Card vs Table Layouts for Monitoring + +### When to Use Each + +| Layout | Best For | Agent Swarm Context | +|---|---|---| +| **Card grid** | Aggregate stats, KPIs, individual entity detail | Stats bar (top), agent overview cards, epic cards | +| **Data table** | Large lists, sortable/filterable data, homogeneous items | Tasks list, agent list (when many), services registry | +| **Hybrid** | Master-detail patterns | Agent list (table) + detail panel (cards) | + +### Card Layout Best Practices + +- **Maximum 5-6 cards** in initial viewport -- don't overwhelm +- **Consistent card dimensions** within a row +- **KPI cards**: Large number (24-32px), label below (12-13px muted), optional sparkline +- **Entity cards**: Title, 2-3 key properties, status badge, timestamp +- **Card padding**: 16-20px, border-radius 8-12px +- **Card hover**: Subtle border glow or slight background lighten (not scale transform) + +### KPI Card Example + +```tsx +
+
+ + Active Agents + + +
+
12
+
+ +3 from last hour +
+
+``` + +### Table Layout Best Practices + +- **Row height**: 40-44px -- compact but not cramped +- **Horizontal padding**: 12-16px per cell +- **No visible column dividers** -- use spacing and alignment instead +- **Subtle row hover**: 3-5% white overlay on hover +- **Row alternation**: Optional, very subtle (2% white overlay) +- **Sticky header**: Always visible when scrolling +- **Truncation**: Ellipsis with tooltip for long text, never wrap table text + +--- + +## 8. Micro-Animations and Transitions + +### Timing Constants + +```css +:root { + --duration-fast: 100ms; /* instant feedback (button press) */ + --duration-normal: 200ms; /* standard transitions */ + --duration-slow: 300ms; /* panel open/close */ + --duration-enter: 200ms; /* elements appearing */ + --duration-exit: 150ms; /* elements disappearing (faster = snappier) */ + + --ease-default: cubic-bezier(0.4, 0, 0.2, 1); /* general purpose */ + --ease-in: cubic-bezier(0.4, 0, 1, 1); /* accelerate */ + --ease-out: cubic-bezier(0, 0, 0.2, 1); /* decelerate */ + --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* bouncy */ +} +``` + +### Where to Animate (and Where NOT to) + +**Do animate:** +- Sidebar collapse/expand (width transition, 200ms) +- Panel slide-in (transform translateX, 200ms ease-out) +- Tab content switch (opacity fade, 150ms) +- Hover states (background-color, 150ms) +- Status dot pulse (only for active states, 2s infinite) +- Toast/notification entrance (slide up + fade, 200ms) +- Skeleton loading pulse (1.5s infinite) +- Row hover highlight (background-color, 100ms) + +**Do NOT animate:** +- Table data updates (just swap, no fade) +- Badge color changes (instant) +- Large layout shifts +- Anything during initial page load (except skeleton) +- Multiple animations simultaneously +- Anything that fires more than once per second + +### Key Animation Patterns + +**Fade in on mount:** +```css +@keyframes fade-in { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} +.animate-in { + animation: fade-in 200ms ease-out; +} +``` + +**Skeleton loading:** +```css +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.skeleton { + background: linear-gradient(90deg, + oklch(0.21 0 0) 25%, + oklch(0.26 0 0) 50%, + oklch(0.21 0 0) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + border-radius: 4px; +} +``` + +**Sidebar collapse:** +```css +.sidebar { + width: var(--sidebar-width); + transition: width var(--duration-slow) var(--ease-default); + overflow: hidden; +} +.sidebar[data-collapsed="true"] { + width: var(--sidebar-width-icon); +} +``` + +### Accessibility: Respect prefers-reduced-motion + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +``` + +--- + +## 9. Reference Dashboards and Design Systems + +### Linear + +- **Color approach**: Near-monochrome (neutral blacks/whites) with very few bold accent colors. Migrated to LCH color space. Significantly reduced color usage compared to earlier versions. +- **Typography**: Inter Display for headings, regular Inter for body. +- **Sidebar**: Inverted L-shape. Tight alignment of labels, icons, buttons. Density-focused with reduced visual noise. +- **Layout**: Structured layouts with headers for filters, side panels for meta properties, multiple display types (list, board, timeline, split, fullscreen). +- **Design philosophy**: "Neutral and timeless appearance." Make text and icons darker in light mode, lighter in dark mode for improved contrast. + +### Vercel (Geist Design System) + +- **Color system**: 10 color scales using OKLCH/P3 with numeric steps (100-1000). Two background colors (Background-1 for page, Background-2 for differentiation). +- **Component colors**: Color-1 (default), Color-2 (hover), Color-3 (active). +- **Typography**: Geist Sans + Geist Mono. +- **Key principle**: Minimal color, maximum clarity. Black/white are the primary colors, with accent used very sparingly. +- **URL**: https://vercel.com/geist + +### Cal.com + +- **Design tokens** in CSS variables: + - Dark mode: `--cal-bg: #101010`, `--cal-bg-emphasis: #2b2b2b`, `--cal-bg-subtle: #1c1c1c`, `--cal-bg-muted: #141414` + - Light mode: `--cal-bg: white`, `--cal-bg-emphasis: #e5e7eb`, `--cal-bg-subtle: #f3f4f6`, `--cal-bg-muted: #f9fafb` +- **Approach**: Clean, functional, minimal decoration + +### Raycast + +- **Known for**: Extreme polish, smooth animations, keyboard-first UX +- **Color**: Muted backgrounds, vibrant icons, high contrast text +- **Design system**: Connected to Supernova.io for token management + +### PatternFly (Red Hat) + +- **Dashboard patterns**: Comprehensive documentation on card-based dashboard layouts +- **Status indicators**: Well-documented severity levels and aggregate status patterns +- **Layout**: Dashboard with cards for KPIs, lists for detail, consistent spacing + +--- + +## 10. Concrete Recommendations for Agent Swarm + +### Migration Path + +The current UI uses MUI Joy + custom amber CSS variables. A migration to shadcn/ui + Tailwind v4 would be a significant rewrite. Here is what I recommend for either approach: + +### If Staying with MUI Joy (Evolution) + +1. **Replace the amber palette** with the cool neutral + indigo/teal accent proposed above +2. **Drop the honeycomb background pattern** -- decorative patterns fight against data-dense UIs +3. **Drop the glow effects** -- they scream "template", not "production tool" +4. **Switch to Inter** from Space Grotesk +5. **Simplify the StatusBadge** -- remove the "pulse-amber" animation from non-active states +6. **Add a proper sidebar** instead of tab-based navigation + +### If Migrating to shadcn/ui + Tailwind v4 (Recommended) + +1. **Use shadcn/ui default dark theme as the base** with OKLCH colors (listed in section 3) +2. **Customize the accent** to indigo or teal +3. **Use the shadcn Sidebar component** for navigation +4. **Use shadcn DataTable** (TanStack Table) instead of AG Grid for most tables, or theme AG Grid to match (section 4) +5. **Adopt the three-layer token hierarchy** for maintainability +6. **Inter + JetBrains Mono** font pairing +7. **Follow the animation timing** in section 8 +8. **Use shadcn Badge** for status indicators with the semantic colors in section 6 + +### Top Priority Visual Changes + +Regardless of framework choice: + +1. **Kill the amber/honey/gold theme** -- replace with neutral darks + semantic accents +2. **Remove honeycomb SVG background** -- clean, flat surfaces +3. **Remove glow effects** on text and boxes +4. **Move from horizontal tabs to sidebar navigation** -- tabs don't scale with 9+ sections +5. **Adopt Inter** -- the dashboard standard for a reason +6. **Standardize status colors** to emerald/amber/red/gray/violet (not all amber variations) +7. **Reduce border opacity** -- current #3a2d1f borders are too visible; use 6-10% white overlay instead + +### CSS Variables Migration + +From current: +```css +:root { + --hive-amber: #f5a623; + --hive-honey: #ffb84d; + --hive-gold: #d4a574; + --hive-body: #0d0906; + --hive-surface: #1a130e; + --hive-border: #3a2d1f; +} +``` + +To proposed: +```css +.dark { + --background: oklch(0.13 0.005 270); + --foreground: oklch(0.985 0 0); + --surface: oklch(0.175 0.005 270); + --surface-elevated: oklch(0.21 0.006 270); + --border: oklch(1 0 0 / 8%); + --accent: oklch(0.62 0.19 264); + --accent-muted: oklch(0.62 0.19 264 / 15%); + --muted-foreground: oklch(0.55 0.01 270); +} +``` + +--- + +## Sources + +### Design Systems and Official Documentation +- [shadcn/ui Theming](https://ui.shadcn.com/docs/theming) - OKLCH variables, dark theme defaults +- [shadcn/ui Sidebar](https://ui.shadcn.com/docs/components/radix/sidebar) - Sidebar component architecture +- [Vercel Geist Design System](https://vercel.com/geist) - Color system, typography +- [Vercel Geist Colors](https://vercel.com/geist/colors) - 10 color scales +- [AG Grid Theming Colors](https://www.ag-grid.com/javascript-data-grid/theming-colors/) - Dark mode theming API +- [AG Grid Built-in Themes](https://www.ag-grid.com/javascript-data-grid/themes/) - Theme builder +- [Material Design Dark Theme](https://m2.material.io/design/color/dark-theme.html) - Elevation overlay system +- [Material Design 3 Color Roles](https://m3.material.io/styles/color/roles) - Semantic color system +- [Carbon Design System Status Indicators](https://carbondesignsystem.com/patterns/status-indicator-pattern/) - Status patterns +- [Tailwind CSS Colors](https://tailwindcss.com/docs/colors) - Color palette reference +- [Tailwind CSS Dark Mode](https://tailwindcss.com/docs/dark-mode) - v4 dark mode approach +- [Cal.com Instance Theming](https://cal.com/docs/enterprise-features/instance-wide-theming) - CSS variable tokens + +### Design Articles and Guides +- [Linear UI Redesign](https://linear.app/now/how-we-redesigned-the-linear-ui) - LCH migration, design philosophy +- [Design Tokens That Scale with Tailwind v4](https://www.maviklabs.com/blog/design-tokens-tailwind-v4-2026) - Three-layer token hierarchy +- [Scalable Accessible Dark Mode](https://www.fourzerothree.in/p/scalable-accessible-dark-mode) - Dark theme implementation +- [Dark Mode Design Best Practices 2026](https://www.tech-rz.com/blog/dark-mode-design-best-practices-in-2026/) - Layered darkness +- [Dashboard Design Principles 2025](https://www.uxpin.com/studio/blog/dashboard-design-principles/) - UX patterns +- [CSS/JS Animation Trends 2026](https://webpeak.org/blog/css-js-animation-trends/) - Micro-interactions +- [Best Fonts for Dense Dashboards](https://fontalternatives.com/blog/best-fonts-dense-dashboards/) - Typography +- [Sidebar Navigation Examples 2025](https://www.navbar.gallery/blog/best-side-bar-navigation-menu-design-examples) - Sidebar patterns + +### Tools +- [OKLCH Color Picker](https://oklch.fyi/) - OKLCH converter and generator +- [OKLCH Palette Generator](https://oklch-palette.vercel.app/) - Palette generation +- [Data Viz Color Picker](https://www.learnui.design/tools/data-color-picker.html) - Chart palette generator