refactor(design): remove multi-hue color bars, unify to ink blue

The kanban board, performance metric cards, workspace list, and wait
banner all leaked second and third chromatic hues into the UI: red
bars on critical tasks, amber on in-review columns, green on done /
running / selected workspace rows, colored icon tiles and colored
headline values on metric cards. kami rule 2 allows one accent (ink
blue) at no more than 5% of the surface; these surfaces were spending
30-60%.

KanbanBoard.tsx
  - Drop the tone/dotColor fields from KANBAN_COLUMNS; column headers
    are plain eyebrows, no colored status dot.
  - Delete priorityColors map and border-l-[3px] on cards; cards are
    flat bordered rectangles. Priority order still drives the sort,
    but stops painting the chrome.
  - CRITICAL/HIGH small-caps label routes through text-primary (ink
    blue) instead of text-destructive / text-warning.
  - "Running" status dot + text switches from text-success / bg-success
    to text-primary / bg-primary.
  - Hover changes from hover:shadow-sm (banned by kami rule 8) to
    hover:bg-muted/40.

MetricsGrid.tsx + Dashboard.tsx
  - Drop the tone prop and its toneMap / bgMap / borderMap trio. A
    MetricCard renders one way: ivory card, ink-blue-tinted icon tile
    (bg-accent), ink-blue glyph, near-black value. No left border
    color stripe, no hover shadow, no translate-y.
  - Dashboard.tsx stops passing tone on the four performance cards.
    Semantics that used to live in tone (pending approvals warn, all
    agents running success) are intentionally dropped; those
    affordances can come back through typography or a subtle indicator
    when kami allows, not through a second color.

WorkspaceList.tsx / WorkspaceInspector.tsx
  - Replace every border-l / text / group-hover reference to var(--success)
    with var(--primary). Selected + hover states now carry kami's ink
    blue signal; the three "X count" spans that used text-success as
    a decorative pop become text-primary for the same reason.

WaitStateIndicator.tsx
  - The wait banner (wait-for-reply / approval) flips from warning
    amber (border-warning, bg-warning-muted, text-warning, bg-warning
    button) to primary ink blue across border / bg / text / button.
    The banner still reads as an attention signal — the icon shape
    (Hourglass) carries the semantic, color no longer has to shout.

feedback.tsx (Alert) and Toast.tsx retain their tone-based colors:
they are accessibility-critical transient state surfaces where a
single ink-blue lane would collapse success / warn / danger into an
undifferentiated stack. They are out of scope for this pass.

Typecheck and build:verify clean. Grep confirms no remaining
border-l-[color:var(--success|warning|destructive|caution)] in
dashboard or execution modules.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-21 14:34:36 -04:00
1 parent 86ca568b90
commit a5caecbfda
6 files changed
+32 -68

No files matched your search

@@ -35,8 +35,8 @@ export default function WaitStateIndicator({ waitState, onReply, onNavigateAppro
const duration = timeAgo(waitState.createdAt)
return (
<div className="flex items-center gap-3 rounded-md border-l-2 border-warning bg-warning-muted px-3 py-2">
<Hourglass size={18} weight="fill" className="shrink-0 text-warning" />
<div className="flex items-center gap-3 rounded-md border-l-2 border-primary bg-accent px-3 py-2">
<Hourglass size={18} weight="fill" className="shrink-0 text-primary" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground">{waitState.reason}</p>
<p className="text-xs text-muted-foreground">{t("wait.waitingSince")} {duration}</p>
@@ -44,7 +44,7 @@ export default function WaitStateIndicator({ waitState, onReply, onNavigateAppro
{isApproval && onNavigateApproval ? (
<button
onClick={() => { const id = waitState.relatedApprovalId; if (id) onNavigateApproval(id); }}
className="flex shrink-0 items-center gap-1.5 rounded-md bg-warning px-3 py-1.5 text-xs font-medium text-primary-foreground hover:brightness-95"
className="flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:brightness-95"
>
<CheckCircle size={14} />
{t("wait.review")}
@@ -52,7 +52,7 @@ export default function WaitStateIndicator({ waitState, onReply, onNavigateAppro
) : (
<button
onClick={() => onReply(waitState.id, waitState.threadId)}
className="flex shrink-0 items-center gap-1.5 rounded-md bg-warning px-3 py-1.5 text-xs font-medium text-primary-foreground hover:brightness-95"
className="flex shrink-0 items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:brightness-95"
>
<ChatCircle size={14} />
{isReview ? t("wait.review") : t("wait.reply")}
@@ -87,7 +87,6 @@ export function Dashboard(props: DashboardProps) {
label={t("dash.activeTasks")}
value={activeTasks.length}
icon={Briefcase}
tone={activeTasks.length > 0 ? "accent" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={`${tasks.filter((t) => t.status === "todo").length} ${t("dash.queued")}`}
/>
@@ -95,7 +94,6 @@ export function Dashboard(props: DashboardProps) {
label={t("dash.approvals")}
value={pendingApprovals.length}
icon={ShieldCheck}
tone={pendingApprovals.length > 0 ? "warn" : "default"}
onClick={() => onNavigate("approvals")}
subtitle={pendingApprovals.length > 0 ? t("dash.needsAttention") : t("dash.allClear")}
/>
@@ -103,7 +101,6 @@ export function Dashboard(props: DashboardProps) {
label={t("common.agents")}
value={activeAgents.length}
icon={Robot}
tone={activeAgents.filter((a) => a.status === "running").length > 0 ? "success" : "default"}
onClick={() => onNavigate("agents")}
subtitle={`${activeAgents.filter((a) => a.status === "running").length} ${t("dash.running")}`}
/>
@@ -11,22 +11,15 @@ interface KanbanProps {
onSelectTask: (taskId: string) => void;
}
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; labelKey: string; tone: string; dotColor: string }> = [
{ status: "backlog", labelKey: "kanban.backlog", tone: "text-muted-foreground", dotColor: "bg-[color:var(--muted-foreground)]" },
{ status: "todo", labelKey: "kanban.todo", tone: "text-warning", dotColor: "bg-warning" },
{ status: "in_progress", labelKey: "kanban.inProgress", tone: "text-primary", dotColor: "bg-primary" },
{ status: "in_review", labelKey: "kanban.inReview", tone: "text-warning", dotColor: "bg-warning" },
{ status: "done", labelKey: "kanban.done", tone: "text-success", dotColor: "bg-success" },
{ status: "blocked", labelKey: "kanban.blocked", tone: "text-destructive", dotColor: "bg-destructive" },
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; labelKey: string }> = [
{ status: "backlog", labelKey: "kanban.backlog" },
{ status: "todo", labelKey: "kanban.todo" },
{ status: "in_progress", labelKey: "kanban.inProgress" },
{ status: "in_review", labelKey: "kanban.inReview" },
{ status: "done", labelKey: "kanban.done" },
{ status: "blocked", labelKey: "kanban.blocked" },
];
const priorityColors: Record<string, string> = {
critical: "border-l-[color:var(--destructive)]",
high: "border-l-[color:var(--destructive)]",
medium: "border-l-[color:var(--primary)]",
low: "border-l-[color:var(--muted-foreground)]",
};
export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) {
const t = useT();
const tasksByStatus = useMemo(() => {
@@ -61,9 +54,8 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
return (
<div key={col.status} className="flex w-[220px] min-w-[160px] shrink-0 flex-col lg:w-[240px]">
<div className="mb-2 flex items-center gap-2 px-1">
<span className={`h-2.5 w-2.5 rounded-full ${col.dotColor}`} aria-hidden="true" />
<Eyebrow className={col.tone}>{t(col.labelKey)}</Eyebrow>
<span className="ml-auto rounded-full bg-muted px-2 py-0.5 text-[10px] font-bold tabular-nums text-muted-foreground">
<Eyebrow>{t(col.labelKey)}</Eyebrow>
<span className="ml-auto rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold tabular-nums text-muted-foreground">
{colTasks.length}
</span>
</div>
@@ -77,7 +69,7 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
key={task.id}
type="button"
onClick={() => onSelectTask(task.id)}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg border border-l-[3px] border-border bg-card px-3 py-2.5 text-left transition duration-150 hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg border border-border bg-card px-3 py-2.5 text-left transition duration-150 hover:bg-muted/40"
>
<div className="truncate text-[12px] font-medium leading-snug text-foreground">{task.title}</div>
<div className="mt-1.5 flex items-center gap-2">
@@ -90,14 +82,14 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
<span className="text-[10px] italic text-muted-foreground">{t("kanban.unassigned")}</span>
)}
{isRunning ? (
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-success">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-success" aria-hidden="true" />
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-primary">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-primary" aria-hidden="true" />
{t("kanban.running")}
</span>
) : null}
</div>
{task.priority === "critical" || task.priority === "high" ? (
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-destructive" : "text-warning"}`}>
<div className="mt-1 text-[9px] font-semibold uppercase tracking-[0.12em] text-primary">
{task.priority === "critical" ? t("kanban.priority.critical") : t("kanban.priority.high")}
</div>
) : null}
@@ -5,52 +5,27 @@ export function MetricCard({
label,
value,
icon: Icon,
tone = "default",
onClick,
subtitle,
}: {
label: string;
value: string | number;
icon: React.ElementType;
tone?: "default" | "success" | "warn" | "danger" | "accent";
onClick?: () => void;
subtitle?: string;
}) {
const toneMap = {
default: "text-foreground",
success: "text-success",
warn: "text-warning",
danger: "text-destructive",
accent: "text-primary",
};
const bgMap = {
default: "bg-muted",
success: "bg-success-muted",
warn: "bg-warning-muted",
danger: "bg-destructive-muted",
accent: "bg-accent",
};
const borderMap = {
default: "border-l-transparent",
success: "border-l-[color:var(--success)]",
warn: "border-l-[color:var(--warning)]",
danger: "border-l-[color:var(--destructive)]",
accent: "border-l-[color:var(--primary)]",
};
return (
<button
type="button"
onClick={onClick}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center gap-4 rounded-lg border border-l-[3px] border-border bg-card px-4 py-4 text-left transition duration-200 hover:shadow-md hover:-translate-y-0.5 ${borderMap[tone]}`}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center gap-4 rounded-lg border border-border bg-card px-4 py-4 text-left transition duration-200 hover:bg-muted/40"
>
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-lg ${bgMap[tone]}`}>
<Icon size={24} weight="fill" className={toneMap[tone]} />
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-accent">
<Icon size={24} weight="fill" className="text-primary" />
</div>
<div className="min-w-0 flex-1">
<Eyebrow>{label}</Eyebrow>
<div className={`text-[28px] font-bold tracking-[-0.02em] leading-none ${toneMap[tone]}`} style={{ fontVariantNumeric: "tabular-nums" }}>{value}</div>
<div className="text-[28px] font-semibold tracking-[-0.02em] leading-none text-foreground" style={{ fontVariantNumeric: "tabular-nums" }}>{value}</div>
{subtitle ? <div className="mt-1.5 truncate text-[11px] font-medium text-muted-foreground">{subtitle}</div> : null}
</div>
</button>
@@ -34,15 +34,15 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents:
<section>
<div className="mb-2 flex items-baseline gap-2 border-b border-border pb-1.5 text-[11px] uppercase tracking-[0.12em] text-muted-foreground">
<span>{t("exec.assignedAgents")}</span>
<span className="text-success tracking-normal">{wAgents.length}</span>
<span className="text-primary tracking-normal">{wAgents.length}</span>
</div>
{wAgents.length === 0 ? (
<div className="py-2 pl-2 text-[12px] text-muted-foreground">{t("exec.noAgentsBound")}</div>
) : (
<div className="divide-y divide-border/60">
{wAgents.map((a) => (
<div key={a.id} className="group grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-l-2 border-transparent py-2 pl-2 pr-1 transition-colors hover:border-l-[color:var(--success)]">
<span aria-hidden="true" className="select-none text-muted-foreground transition-colors group-hover:text-success">›</span>
<div key={a.id} className="group grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-l-2 border-transparent py-2 pl-2 pr-1 transition-colors hover:border-l-[color:var(--primary)]">
<span aria-hidden="true" className="select-none text-muted-foreground transition-colors group-hover:text-primary">›</span>
<div className="min-w-0">
<div className="truncate text-[12px] font-medium text-foreground">{a.name}</div>
<div className="truncate text-[11px] text-muted-foreground">{a.role}</div>
@@ -56,15 +56,15 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents:
<section>
<div className="mb-2 flex items-baseline gap-2 border-b border-border pb-1.5 text-[11px] uppercase tracking-[0.12em] text-muted-foreground">
<span>{t("exec.openTasks")}</span>
<span className="text-success tracking-normal">{wTasks.length}</span>
<span className="text-primary tracking-normal">{wTasks.length}</span>
</div>
{wTasks.length === 0 ? (
<div className="py-2 pl-2 text-[12px] text-muted-foreground">{t("exec.noTasksTarget")}</div>
) : (
<div className="divide-y divide-border/60">
{wTasks.slice(0, 6).map((tk) => (
<div key={tk.id} className="group grid grid-cols-[auto_minmax(0,1fr)] items-start gap-3 border-l-2 border-transparent py-2 pl-2 pr-1 transition-colors hover:border-l-[color:var(--success)]">
<span aria-hidden="true" className="select-none pt-[1px] text-muted-foreground transition-colors group-hover:text-success">›</span>
<div key={tk.id} className="group grid grid-cols-[auto_minmax(0,1fr)] items-start gap-3 border-l-2 border-transparent py-2 pl-2 pr-1 transition-colors hover:border-l-[color:var(--primary)]">
<span aria-hidden="true" className="select-none pt-[1px] text-muted-foreground transition-colors group-hover:text-primary">›</span>
<div className="min-w-0">
<div className="flex items-center gap-2">
<PriorityDot priority={tk.priority} />
@@ -80,15 +80,15 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents:
<section>
<div className="mb-2 flex items-baseline gap-2 border-b border-border pb-1.5 text-[11px] uppercase tracking-[0.12em] text-muted-foreground">
<span>{t("exec.recentRuns")}</span>
<span className="text-success tracking-normal">{wRuns.length}</span>
<span className="text-primary tracking-normal">{wRuns.length}</span>
</div>
{wRuns.length === 0 ? (
<div className="py-2 pl-2 text-[12px] text-muted-foreground">{t("exec.noRunsExecuted")}</div>
) : (
<div className="divide-y divide-border/60">
{wRuns.slice(0, 6).map((r) => (
<div key={r.id} className="group grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 border-l-2 border-transparent py-2 pl-2 pr-1 transition-colors hover:border-l-[color:var(--success)]">
<span aria-hidden="true" className="select-none pt-[1px] text-muted-foreground transition-colors group-hover:text-success">›</span>
<div key={r.id} className="group grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3 border-l-2 border-transparent py-2 pl-2 pr-1 transition-colors hover:border-l-[color:var(--primary)]">
<span aria-hidden="true" className="select-none pt-[1px] text-muted-foreground transition-colors group-hover:text-primary">›</span>
<div className="min-w-0">
<div className="truncate text-[12px] font-medium text-foreground">{r.summary || `Run ${r.id.slice(0, 8)}`}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div>
@@ -27,8 +27,8 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
{(() => {
const isSelected = selectedWorkspaceId === workspace.id;
return (
<div className={`group grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-l-2 py-2 pl-2 pr-1 transition-colors ${isSelected ? "border-l-[color:var(--success)]" : "border-l-transparent hover:border-l-border"}`}>
<span aria-hidden="true" className={`select-none transition-colors ${isSelected ? "text-success" : "text-muted-foreground group-hover:text-foreground"}`}>›</span>
<div className={`group grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-l-2 py-2 pl-2 pr-1 transition-colors ${isSelected ? "border-l-[color:var(--primary)]" : "border-l-transparent hover:border-l-border"}`}>
<span aria-hidden="true" className={`select-none transition-colors ${isSelected ? "text-primary" : "text-muted-foreground group-hover:text-foreground"}`}>›</span>
<div className="min-w-0">
<div className="flex items-center gap-2">
<div className="truncate text-[13px] font-medium text-foreground">{workspace.name}</div>