feat(theme): dark/light mode with a Radix Colors-based palette

Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.

- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
  tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
  fg.primary/secondary/muted, status.success/danger/warning. One class flip
  on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
  prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
  scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
  every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
  client/src onto the new tokens, so every badge/button/component pulls the
  same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
  constants — slate/blue/green/red/amber steps 1-12 — adopted after three
  rounds of hand-picked values that kept overshooting (flat, then too dark,
  then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
  dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
  visual language (border + text + translucent wash of the same status
  color); `current` alone stays a solid accent fill, the one state that
  gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
  Workspace lane-detail header already showing them.

Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
2026-07-31 10:54:31 +07:00
parent 4905d63b97
commit b673363351
82 changed files with 3776 additions and 2578 deletions
@@ -92,7 +92,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
if (!hasErrors) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-status-success/10 border border-status-success/20 flex items-center justify-center">
<svg
width="24"
height="24"
@@ -107,10 +107,10 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<span className="text-sm text-emerald-400 font-medium">
<span className="text-sm text-status-success font-medium">
{t("errorPropagation.noErrors")}
</span>
<span className="text-xs text-gray-600">{t("errorPropagation.allSuccess")}</span>
<span className="text-xs text-fg-muted">{t("errorPropagation.allSuccess")}</span>
</div>
);
}
@@ -124,20 +124,20 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
return (
<div className="flex flex-col gap-4">
{/* Error rate summary bar */}
<div className="flex items-center gap-3 p-3 rounded-xl bg-red-500/5 border border-red-500/15">
<div className="flex-shrink-0 min-w-[2.75rem] h-10 px-2 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<span className="text-[13px] font-bold text-red-400 tabular-nums whitespace-nowrap">
<div className="flex items-center gap-3 p-3 rounded-xl bg-status-danger/5 border border-status-danger/15">
<div className="flex-shrink-0 min-w-[2.75rem] h-10 px-2 rounded-lg bg-status-danger/10 border border-status-danger/20 flex items-center justify-center">
<span className="text-[13px] font-bold text-status-danger tabular-nums whitespace-nowrap">
{errorRatePct}%
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-red-300">
<p className="text-xs font-medium text-status-danger">
{t("errorPropagation.sessionsErrorSummary", {
errorSessions: data.sessionsWithErrors,
totalSessions: data.totalSessions,
})}
</p>
<p className="text-[11px] text-gray-500 mt-0.5">
<p className="text-[11px] text-fg-muted mt-0.5">
{totalErrors > 0
? `${totalErrors}${t("errorPropagation.agentErrors")}`
: t("errorPropagation.sessionErrorsOnly")}
@@ -148,7 +148,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* Errors by depth - horizontal bars */}
{hasDepthData && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.errorsByDepth")}
</p>
<div className="flex flex-col gap-1.5">
@@ -165,7 +165,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
onMouseEnter={() => setHoveredDepth(d.depth)}
onMouseLeave={() => setHoveredDepth(null)}
>
<span className="text-[11px] text-gray-500 w-24 flex-shrink-0 text-right truncate">
<span className="text-[11px] text-fg-muted w-24 flex-shrink-0 text-right truncate">
{depthLabel(d.depth)}
</span>
<div className="flex-1 h-5 bg-surface-3 rounded overflow-hidden relative">
@@ -194,7 +194,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* Error-prone agent types */}
{topTypes.length > 0 && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.errorProneTypes")}
</p>
<div className="flex flex-col gap-1">
@@ -210,16 +210,16 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
style={{ backgroundColor: DEPTH_COLORS[Math.min(i, DEPTH_COLORS.length - 1)] }}
/>
<span className="text-xs text-gray-300 truncate flex-1 min-w-0">
<span className="text-xs text-fg-secondary truncate flex-1 min-w-0">
{t.subagent_type}
</span>
<div className="w-16 h-1.5 bg-surface-4 rounded-full overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full bg-red-400/60"
className="h-full rounded-full bg-status-danger/60"
style={{ width: `${Math.max(pct, 8)}%` }}
/>
</div>
<span className="text-[11px] font-semibold text-red-300 tabular-nums w-5 text-right flex-shrink-0">
<span className="text-[11px] font-semibold text-status-danger tabular-nums w-5 text-right flex-shrink-0">
{t.count}
</span>
</div>
@@ -232,14 +232,14 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* API & session errors */}
{data.eventErrors && data.eventErrors.length > 0 && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.apiSessionErrors")}
</p>
<div className="flex flex-col gap-1">
{data.eventErrors.map((e) => (
<div
key={e.summary}
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-amber-500/5 border border-amber-500/10 hover:border-amber-500/20 transition-colors"
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-status-warning/5 border border-status-warning/10 hover:border-status-warning/20 transition-colors"
>
<svg
width="14"
@@ -256,10 +256,13 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<span className="text-xs text-gray-300 truncate flex-1 min-w-0" title={e.summary}>
<span
className="text-xs text-fg-secondary truncate flex-1 min-w-0"
title={e.summary}
>
{e.summary}
</span>
<span className="flex-shrink-0 text-[11px] font-semibold text-amber-400 tabular-nums">
<span className="flex-shrink-0 text-[11px] font-semibold text-status-warning tabular-nums">
{e.count}x
</span>
</div>