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:
@@ -100,35 +100,35 @@ function StatTile({
|
||||
tone?: "default" | "violet" | "emerald" | "amber" | "rose" | "cyan" | "blue";
|
||||
}) {
|
||||
const palette = {
|
||||
default: "border-surface-3 bg-surface-2 text-gray-200",
|
||||
default: "border-surface-3 bg-surface-2 text-fg-secondary",
|
||||
violet: "border-violet-500/20 bg-violet-500/5 text-violet-200",
|
||||
emerald: "border-emerald-500/20 bg-emerald-500/5 text-emerald-200",
|
||||
amber: "border-amber-500/20 bg-amber-500/5 text-amber-200",
|
||||
emerald: "border-status-success/20 bg-status-success/5 text-status-success",
|
||||
amber: "border-status-warning/20 bg-status-warning/5 text-status-warning",
|
||||
rose: "border-rose-500/20 bg-rose-500/5 text-rose-200",
|
||||
cyan: "border-cyan-500/20 bg-cyan-500/5 text-cyan-200",
|
||||
blue: "border-blue-500/20 bg-blue-500/5 text-blue-200",
|
||||
blue: "border-blue-600/20 bg-blue-600/5 text-blue-300",
|
||||
}[tone];
|
||||
|
||||
const iconTone = {
|
||||
default: "text-gray-500",
|
||||
default: "text-fg-muted",
|
||||
violet: "text-violet-400",
|
||||
emerald: "text-emerald-400",
|
||||
amber: "text-amber-400",
|
||||
emerald: "text-status-success",
|
||||
amber: "text-status-warning",
|
||||
rose: "text-rose-400",
|
||||
cyan: "text-cyan-400",
|
||||
blue: "text-blue-400",
|
||||
blue: "text-blue-500",
|
||||
}[tone];
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border px-3 py-2.5 ${palette}`}>
|
||||
<div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-gray-500">
|
||||
<div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted">
|
||||
<span className={iconTone}>{icon}</span>
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-1 font-mono text-lg font-semibold text-gray-100 leading-tight">
|
||||
<div className="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight">
|
||||
{value}
|
||||
</div>
|
||||
{hint && <div className="text-[10px] text-gray-500 mt-0.5">{hint}</div>}
|
||||
{hint && <div className="text-[10px] text-fg-muted mt-0.5">{hint}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ function ToolUsageRow({ toolName, count, max }: { toolName: string; count: numbe
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-gray-400 w-14 text-right flex-shrink-0">
|
||||
<span className="font-mono text-xs text-fg-secondary w-14 text-right flex-shrink-0">
|
||||
{count.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -286,23 +286,26 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
<div className="space-y-5 mb-6">
|
||||
{/* Active-agent banner - only shows when session is running */}
|
||||
{activeAgent && (
|
||||
<div className="flex items-center gap-2.5 px-3 py-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5">
|
||||
<div className="flex items-center gap-2.5 px-3 py-2 rounded-lg border border-status-success/20 bg-status-success/5">
|
||||
<span className="relative flex h-2 w-2 flex-shrink-0">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-60" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400" />
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success opacity-60" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-status-success" />
|
||||
</span>
|
||||
<Bot className="w-3.5 h-3.5 text-emerald-300 flex-shrink-0" />
|
||||
<span className="text-xs text-emerald-200 font-medium flex-shrink-0">
|
||||
<Bot className="w-3.5 h-3.5 text-status-success flex-shrink-0" />
|
||||
<span className="text-xs text-status-success font-medium flex-shrink-0">
|
||||
{activeAgent.name || "Agent"}
|
||||
</span>
|
||||
{activeAgent.current_tool && (
|
||||
<span className="text-[11px] text-gray-400 font-mono inline-flex items-center gap-1">
|
||||
<span className="text-gray-600">running</span>
|
||||
<span className="text-emerald-300">{activeAgent.current_tool}</span>
|
||||
<span className="text-[11px] text-fg-secondary font-mono inline-flex items-center gap-1">
|
||||
<span className="text-fg-muted">running</span>
|
||||
<span className="text-status-success">{activeAgent.current_tool}</span>
|
||||
</span>
|
||||
)}
|
||||
{activeAgent.task && (
|
||||
<span className="text-[11px] text-gray-400 truncate min-w-0" title={activeAgent.task}>
|
||||
<span
|
||||
className="text-[11px] text-fg-secondary truncate min-w-0"
|
||||
title={activeAgent.task}
|
||||
>
|
||||
· {activeAgent.task}
|
||||
</span>
|
||||
)}
|
||||
@@ -361,16 +364,16 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
{/* Tool usage */}
|
||||
<div className="lg:col-span-2 rounded-lg border border-surface-3 bg-surface-2/60 p-3.5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Wrench className="w-3.5 h-3.5 text-violet-400" />
|
||||
Top tools
|
||||
</h3>
|
||||
<span className="text-[10px] text-gray-500 font-mono">
|
||||
<span className="text-[10px] text-fg-muted font-mono">
|
||||
{stats.tools_used.length} total
|
||||
</span>
|
||||
</div>
|
||||
{stats.tools_used.length === 0 ? (
|
||||
<div className="text-center py-6 text-xs text-gray-500">No tool calls yet.</div>
|
||||
<div className="text-center py-6 text-xs text-fg-muted">No tool calls yet.</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{stats.tools_used.slice(0, 8).map((t) => (
|
||||
@@ -420,26 +423,26 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider flex items-center gap-1.5">
|
||||
<GitBranch className="w-3.5 h-3.5 text-cyan-400" />
|
||||
Subagents
|
||||
</h3>
|
||||
<span className="text-[10px] text-gray-500 font-mono">{totalRuns} runs</span>
|
||||
<span className="text-[10px] text-fg-muted font-mono">{totalRuns} runs</span>
|
||||
</div>
|
||||
{rows.length === 0 ? (
|
||||
<div className="text-center py-6 text-xs text-gray-500">
|
||||
<div className="text-center py-6 text-xs text-fg-muted">
|
||||
No subagents in this session.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{rows.slice(0, 8).map((r) => {
|
||||
const pct = max > 0 ? Math.max(4, Math.round((r.count / max) * 100)) : 0;
|
||||
const barClass = r.isCompaction ? "bg-amber-500/60" : "bg-cyan-500/60";
|
||||
const barClass = r.isCompaction ? "bg-status-warning/60" : "bg-cyan-500/60";
|
||||
return (
|
||||
<div key={r.key} className="flex items-center gap-2">
|
||||
<span
|
||||
className={`font-mono text-xs truncate flex-1 min-w-0 ${
|
||||
r.isCompaction ? "text-amber-300" : "text-gray-300"
|
||||
r.isCompaction ? "text-status-warning" : "text-fg-secondary"
|
||||
}`}
|
||||
title={r.label}
|
||||
>
|
||||
@@ -451,7 +454,7 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-gray-400 w-8 text-right flex-shrink-0">
|
||||
<span className="font-mono text-xs text-fg-secondary w-8 text-right flex-shrink-0">
|
||||
{r.count}
|
||||
</span>
|
||||
</div>
|
||||
@@ -469,11 +472,11 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
{totalTokens > 0 && (
|
||||
<div className="rounded-lg border border-surface-3 bg-surface-2/60 p-3.5">
|
||||
<div className="flex items-center justify-between mb-2.5">
|
||||
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Coins className="w-3.5 h-3.5 text-amber-400" />
|
||||
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Coins className="w-3.5 h-3.5 text-status-warning" />
|
||||
Token flow
|
||||
</h3>
|
||||
<span className="text-[10px] text-gray-500 font-mono">{fmt(totalTokens)} total</span>
|
||||
<span className="text-[10px] text-fg-muted font-mono">{fmt(totalTokens)} total</span>
|
||||
</div>
|
||||
<TokenFlowBar tokens={tokens} total={totalTokens} />
|
||||
</div>
|
||||
@@ -482,8 +485,8 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
{/* Event-type breakdown - secondary, only top 6 */}
|
||||
{stats.events_by_type.length > 0 && (
|
||||
<div className="rounded-lg border border-surface-3 bg-surface-2/60 p-3.5">
|
||||
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider mb-3 flex items-center gap-1.5">
|
||||
<Activity className="w-3.5 h-3.5 text-gray-400" />
|
||||
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider mb-3 flex items-center gap-1.5">
|
||||
<Activity className="w-3.5 h-3.5 text-fg-secondary" />
|
||||
Event mix
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -492,9 +495,9 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
|
||||
key={e.event_type}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-mono bg-surface-3/60 border border-surface-3 rounded-md px-2 py-0.5"
|
||||
>
|
||||
<span className="text-gray-400">{e.event_type}</span>
|
||||
<span className="text-gray-500">·</span>
|
||||
<span className="text-gray-200">{e.count.toLocaleString()}</span>
|
||||
<span className="text-fg-secondary">{e.event_type}</span>
|
||||
<span className="text-fg-muted">·</span>
|
||||
<span className="text-fg-secondary">{e.count.toLocaleString()}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -524,8 +527,8 @@ function TokenFlowBar({ tokens, total }: { tokens: SessionStats["tokens"]; total
|
||||
key: "input",
|
||||
label: "Input",
|
||||
value: tokens.input_tokens,
|
||||
cls: "bg-emerald-500",
|
||||
text: "text-emerald-300",
|
||||
cls: "bg-status-success",
|
||||
text: "text-status-success",
|
||||
},
|
||||
{
|
||||
key: "output",
|
||||
@@ -558,11 +561,11 @@ function TokenFlowBar({ tokens, total }: { tokens: SessionStats["tokens"]; total
|
||||
return (
|
||||
<div key={s.key} className="flex items-center gap-2">
|
||||
<span className={`block w-2 h-2 rounded-full ${s.cls}`} />
|
||||
<span className="text-gray-500 text-[11px]">{s.label}</span>
|
||||
<span className="text-fg-muted text-[11px]">{s.label}</span>
|
||||
<span className={`font-mono ml-auto ${s.text}`}>
|
||||
{fmt(s.value)}
|
||||
{pct > 0 && (
|
||||
<span className="text-gray-600 text-[10px] ml-1">
|
||||
<span className="text-fg-muted text-[10px] ml-1">
|
||||
{pct >= 1 ? Math.round(pct) : pct.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user