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:
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>
|
||||
|
||||
Reference in new issue
Block a user