b673363351
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.
195 lines
6.5 KiB
CSS
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;
|
|
}
|