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:
@@ -284,7 +284,7 @@ function SummaryBlock({
|
||||
return (
|
||||
<div className="border border-border rounded overflow-hidden bg-surface-3/30">
|
||||
<div className="px-3 py-1 border-b border-border bg-black/20">
|
||||
<span className="text-gray-500 text-[10px] uppercase tracking-wide font-semibold">
|
||||
<span className="text-fg-muted text-[10px] uppercase tracking-wide font-semibold">
|
||||
{t("eventDetail.summary")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -293,19 +293,19 @@ function SummaryBlock({
|
||||
<span className="text-base leading-none" aria-hidden="true">
|
||||
{summary.icon}
|
||||
</span>
|
||||
<span className="text-[12px] text-gray-100 font-medium break-words">
|
||||
<span className="text-[12px] text-fg-primary font-medium break-words">
|
||||
{summary.headline}
|
||||
</span>
|
||||
</div>
|
||||
{summary.bullets.length > 0 && (
|
||||
<ul className="list-disc pl-6 space-y-0.5 text-[11px] text-gray-400">
|
||||
<ul className="list-disc pl-6 space-y-0.5 text-[11px] text-fg-secondary">
|
||||
{summary.bullets.map((b, i) => (
|
||||
<li key={i}>{b}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{hint && (
|
||||
<div className="text-[11px] text-gray-500 italic pt-1 border-t border-border/40">
|
||||
<div className="text-[11px] text-fg-muted italic pt-1 border-t border-border/40">
|
||||
↓ {hint}
|
||||
</div>
|
||||
)}
|
||||
@@ -337,7 +337,7 @@ function FieldRow({
|
||||
if (view) {
|
||||
return (
|
||||
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
|
||||
<div className="text-gray-500 font-mono pt-2">{label}</div>
|
||||
<div className="text-fg-muted font-mono pt-2">{label}</div>
|
||||
<div>{view}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -348,7 +348,7 @@ function FieldRow({
|
||||
if (view) {
|
||||
return (
|
||||
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
|
||||
<div className="text-gray-500 font-mono pt-2">{label}</div>
|
||||
<div className="text-fg-muted font-mono pt-2">{label}</div>
|
||||
<div>{view}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -358,8 +358,8 @@ function FieldRow({
|
||||
if (isInlineScalar(value)) {
|
||||
return (
|
||||
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
|
||||
<div className="text-gray-500 font-mono pt-0.5">{label}</div>
|
||||
<div className="text-gray-300 font-mono break-all">
|
||||
<div className="text-fg-muted font-mono pt-0.5">{label}</div>
|
||||
<div className="text-fg-secondary font-mono break-all">
|
||||
<ScalarValue value={value} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,7 +368,7 @@ function FieldRow({
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
|
||||
<div className="text-gray-500 font-mono pt-2">{label}</div>
|
||||
<div className="text-fg-muted font-mono pt-2">{label}</div>
|
||||
<CodeView value={value} />
|
||||
</div>
|
||||
);
|
||||
@@ -382,11 +382,11 @@ function isInlineScalar(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function ScalarValue({ value }: { value: unknown }) {
|
||||
if (value == null) return <span className="text-gray-500 italic">null</span>;
|
||||
if (value == null) return <span className="text-fg-muted italic">null</span>;
|
||||
if (typeof value === "boolean") {
|
||||
const color = value
|
||||
? "text-green-400 border-green-500/30 bg-green-500/10"
|
||||
: "text-gray-400 border-gray-500/30 bg-gray-500/10";
|
||||
: "text-fg-secondary border-border-light/30 bg-surface-4/10";
|
||||
return (
|
||||
<span className={`inline-block px-2 py-0.5 rounded border ${color}`}>{String(value)}</span>
|
||||
);
|
||||
@@ -402,12 +402,12 @@ function CodeView({ value }: { value: unknown }) {
|
||||
return (
|
||||
<div className="relative bg-black/70 border border-border rounded font-mono text-[11px] overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-black/40">
|
||||
<span className="text-gray-500 text-[10px] uppercase tracking-wide">
|
||||
<span className="text-fg-muted text-[10px] uppercase tracking-wide">
|
||||
{typeof value === "string" ? "text" : Array.isArray(value) ? "array" : "json"}
|
||||
</span>
|
||||
<CopyButton text={text} />
|
||||
</div>
|
||||
<pre className="px-3 py-2 text-gray-200 whitespace-pre-wrap break-words max-h-96 overflow-auto">
|
||||
<pre className="px-3 py-2 text-fg-secondary whitespace-pre-wrap break-words max-h-96 overflow-auto">
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user