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:
+77
-12
@@ -8,10 +8,74 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Palette sourced from Radix Colors (radix-ui.com/colors), an accessible
|
||||
* 12-step system built for UI, contrast-checked with APCA — chosen after
|
||||
* three rounds of hand-picked values that kept overshooting (too flat, then
|
||||
* too dark, then too glaring). Radix's own role guidance for a 12-step scale:
|
||||
* 1-2 app/chrome background 6-8 borders (6 subtle, 7-8 stronger)
|
||||
* 3-5 component background 9-10 solid (buttons, vibrant fills)
|
||||
* 11-12 text (11 secondary, 12 primary)
|
||||
* `slate` supplies surface/border/fg here; `blue` supplies accent; `green`/
|
||||
* `red`/`amber` step 11 supplies the status tokens — the same step used for
|
||||
* fg-secondary, since 11 is Radix's guaranteed-readable-text step. Values are
|
||||
* the literal Radix hex constants (steps 1/2/3/4/5/6/7/9/11/12), converted to
|
||||
* RGB triplets for Tailwind's `<alpha-value>` opacity support.
|
||||
*/
|
||||
:root {
|
||||
/* Light theme — the CSS default, so it applies with no class on <html>.
|
||||
Radix `slate` (light), steps 1/2/3/4/5/6/7. */
|
||||
--surface-0: 252 252 253; /* slate-1 #fcfcfd */
|
||||
--surface-1: 249 249 251; /* slate-2 #f9f9fb */
|
||||
--surface-2: 240 240 243; /* slate-3 #f0f0f3 */
|
||||
--surface-3: 232 232 236; /* slate-4 #e8e8ec */
|
||||
--surface-4: 224 225 230; /* slate-5 #e0e1e6 */
|
||||
--surface-5: 217 217 224; /* slate-6 #d9d9e0 */
|
||||
--border: 217 217 224; /* slate-6 #d9d9e0 */
|
||||
--border-light: 205 206 214; /* slate-7 #cdced6 */
|
||||
--accent: 0 144 255; /* blue-9 #0090ff */
|
||||
--accent-hover: 5 136 240; /* blue-10 #0588f0 */
|
||||
--fg-primary: 28 32 36; /* slate-12 #1c2024 */
|
||||
--fg-secondary: 96 100 108; /* slate-11 #60646c */
|
||||
--fg-muted: 139 141 152; /* slate-9 #8b8d98 */
|
||||
--shadow-card: 0 1px 2px rgb(15 23 42 / 0.06), 0 1px 3px rgb(15 23 42 / 0.08);
|
||||
/* Status: green/red/amber step 11 (light) — the same "readable text" step
|
||||
as fg-secondary, so success/danger/warning sit at the same visual weight
|
||||
as ordinary secondary text, not shouting over it. */
|
||||
--status-success: 33 131 88; /* green-11 #218358 */
|
||||
--status-danger: 206 44 49; /* red-11 #ce2c31 */
|
||||
--status-warning: 171 100 0; /* amber-11 #ab6400 */
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Radix `slate` (dark), steps 1/2/3/4/5/6/7. The scale is built in the
|
||||
opposite direction on purpose — step 6 is LIGHTER than step 3/4 here,
|
||||
which is what makes borders rise off a panel instead of sinking into it
|
||||
(the bug in the previous, hand-picked dark border). */
|
||||
--surface-0: 17 17 19; /* slate-1 #111113 */
|
||||
--surface-1: 24 25 27; /* slate-2 #18191b */
|
||||
--surface-2: 33 34 37; /* slate-3 #212225 */
|
||||
--surface-3: 39 42 45; /* slate-4 #272a2d */
|
||||
--surface-4: 46 49 53; /* slate-5 #2e3135 */
|
||||
--surface-5: 54 58 63; /* slate-6 #363a3f */
|
||||
--border: 54 58 63; /* slate-6 #363a3f */
|
||||
--border-light: 67 72 78; /* slate-7 #43484e */
|
||||
--accent: 0 144 255; /* blue-9 #0090ff — identical value both themes, Radix's brand anchor */
|
||||
--accent-hover: 59 158 255; /* blue-10 #3b9eff */
|
||||
--fg-primary: 237 238 240; /* slate-12 #edeef0 */
|
||||
--fg-secondary: 176 180 186; /* slate-11 #b0b4ba */
|
||||
--fg-muted: 105 110 119; /* slate-9 #696e77 */
|
||||
/* Dark surfaces already separate by lightness step; a shadow adds nothing
|
||||
visible against a dark page and reads as a stray dark smudge. */
|
||||
--shadow-card: none;
|
||||
--status-success: 61 214 140; /* green-11 dark #3dd68c */
|
||||
--status-danger: 255 149 146; /* red-11 dark #ff9592 */
|
||||
--status-warning: 255 202 22; /* amber-11 dark #ffca16 */
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #2a2a3d #0c0c14;
|
||||
scrollbar-color: rgb(var(--border)) rgb(var(--surface-0));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
@@ -20,21 +84,21 @@
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #0c0c14;
|
||||
background: rgb(var(--surface-0));
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #2a2a3d;
|
||||
background: rgb(var(--border));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: #363650;
|
||||
background: rgb(var(--border-light));
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
color: #e4e4ed;
|
||||
background: rgb(var(--accent) / 0.3);
|
||||
color: rgb(var(--fg-primary));
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator {
|
||||
@@ -57,6 +121,7 @@
|
||||
@layer components {
|
||||
.card {
|
||||
@apply bg-surface-3 border border-border rounded-xl;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@@ -68,7 +133,7 @@
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply inline-flex items-center gap-2 px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 hover:bg-surface-4 rounded-lg transition-colors duration-150;
|
||||
@apply inline-flex items-center gap-2 px-3 py-1.5 text-sm text-fg-secondary hover:text-fg-primary hover:bg-surface-4 rounded-lg transition-colors duration-150;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -76,7 +141,7 @@
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply bg-surface-2 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-colors;
|
||||
@apply bg-surface-2 border border-border rounded-lg px-3 py-2 text-sm text-fg-primary placeholder-fg-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-colors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +163,9 @@
|
||||
z-index: 100;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #363650;
|
||||
background: #15151f;
|
||||
color: #e4e4ed;
|
||||
border: 1px solid rgb(var(--border-light));
|
||||
background: rgb(var(--surface-2));
|
||||
color: rgb(var(--fg-primary));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
@@ -109,7 +174,7 @@
|
||||
}
|
||||
.skip-to-content:focus {
|
||||
transform: translateY(0);
|
||||
outline: 2px solid #6366f1;
|
||||
outline: 2px solid rgb(var(--accent));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user