Files
Claude-Code-Monitor/client/src/index.css
T
nntrivi2001 b673363351 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.
2026-07-31 10:54:31 +07:00

195 lines
6.5 KiB
CSS

/**
* @file index.css
* @description Global styles and Tailwind CSS configuration for the application.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@tailwind base;
@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: rgb(var(--border)) rgb(var(--surface-0));
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: rgb(var(--surface-0));
}
*::-webkit-scrollbar-thumb {
background: rgb(var(--border));
border-radius: 3px;
}
*::-webkit-scrollbar-thumb:hover {
background: rgb(var(--border-light));
}
::selection {
background: rgb(var(--accent) / 0.3);
color: rgb(var(--fg-primary));
}
::-webkit-calendar-picker-indicator {
filter: invert(1) brightness(0.8);
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
::-webkit-calendar-picker-indicator:hover {
opacity: 1;
}
.time-input-custom::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
@layer components {
.card {
@apply bg-surface-3 border border-border rounded-xl;
box-shadow: var(--shadow-card);
}
.card-hover {
@apply card transition-all duration-200 hover:border-border-light hover:bg-surface-4;
}
.btn-primary {
@apply inline-flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg transition-colors duration-150;
}
.btn-ghost {
@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 {
@apply inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-full border whitespace-nowrap shrink-0;
}
.input {
@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;
}
}
@layer utilities {
/* Hide the scrollbar on horizontally scrollable strips (e.g. the Settings
TOC) where chevron affordances replace it. */
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Visually hidden until focused — first Tab lands here before the sidebar. */
.skip-to-content {
position: absolute;
left: 0.75rem;
top: 0.75rem;
z-index: 100;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
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;
transform: translateY(-150%);
transition: transform 0.15s ease;
}
.skip-to-content:focus {
transform: translateY(0);
outline: 2px solid rgb(var(--accent));
outline-offset: 2px;
}
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.animate-pulse-dot {
animation: pulse-dot 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}