feat(FN-1357): replace hardcoded colors with semantic CSS tokens

- Add semantic CSS custom property tokens to :root for theme-aware colors
- Replace hardcoded colors in MailboxModal with semantic tokens
- Replace hardcoded colors in mission components with semantic tokens
- Replace hardcoded colors in terminal and model combobox with semantic tokens
- Add light theme overrides for new semantic tokens
- Add MailboxModal CSS regression tests for theme awareness
- Document semantic token naming convention in memory
This commit is contained in:
gsxdsm
2026-04-09 16:29:51 -07:00
parent 04f835f821
commit 351bccb211
3 changed files with 199 additions and 34 deletions

View File

@@ -37,6 +37,7 @@
- There are **48 unique color themes** in `packages/dashboard/app/styles.css` (default, ocean, forest, sunset, zen, berry, high-contrast, industrial, monochrome, slate, ash, graphite, silver, solarized, factory, ayu, one-dark, nord, dracula, gruvbox, tokyo-night, catppuccin-mocha, github-dark, everforest, rose-pine, kanagawa, night-owl, palenight, monokai-pro, slime, brutalist, neon-city, parchment, terminal, glass, horizon, vitesse, outrun, snazzy, porple, espresso, mars, poimandres, ember, rust, copper, foundry, carbon). Each has a dark variant `[data-color-theme="<name>"]` and a light variant `[data-color-theme="<name>"][data-theme="light"]`.
- When adding CSS custom properties that should be theme-aware (like `--accent`, `--status-*-bg`), add them to all 48 theme blocks plus `:root` and `[data-theme="light"]` base blocks. The test in `status-colors-theme.test.ts` iterates all blocks programmatically to prevent regressions.
- **Semantic tokens** (tokens describing purpose, not appearance) that maintain consistent meaning across all color themes (e.g., "autopilot active" is always green-tinted, "event error" is always red-tinted) only need dark/light adaptation via the base `[data-theme="light"]` block. They do NOT need per-color-theme overrides because the semantic meaning is consistent. Examples from FN-1357: `--autopilot-pulse`, `--event-*-text`, `--event-*-bg`, `--terminal-bg`, `--star-idle`, `--star-active`, `--badge-mission-*`, `--fab-*`.
## Plugin System (FN-1111 / FN-1400)

View File

@@ -461,4 +461,94 @@ describe("MailboxModal", () => {
});
});
});
describe("theme-awareness CSS regressions", () => {
// Read CSS file once for all tests in this block
let css: string;
beforeAll(async () => {
const fs = await import("fs");
const path = await import("path");
const cssPath = path.resolve(__dirname, "../../styles.css");
css = fs.readFileSync(cssPath, "utf-8");
});
it("mailbox unread badge uses theme-aware text token", () => {
const blockMatch = css.match(/\.mailbox-unread-badge\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();
expect(blockMatch![1]).toContain("var(--fab-text)");
expect(blockMatch![1]).not.toContain("color: white");
});
it("mailbox tab badge uses theme-aware text token", () => {
const blockMatch = css.match(/\.mailbox-tab-badge\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();
expect(blockMatch![1]).toContain("var(--fab-text)");
expect(blockMatch![1]).not.toContain("color: white");
});
it("mailbox compose FAB uses theme-aware tokens", () => {
const blockMatch = css.match(/\.mailbox-compose-fab\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();
expect(blockMatch![1]).toContain("var(--fab-bg)");
expect(blockMatch![1]).toContain("var(--fab-text)");
expect(blockMatch![1]).not.toContain("background: var(--todo)");
expect(blockMatch![1]).not.toContain("color: white");
// Check hover state
const hoverMatch = css.match(/\.mailbox-compose-fab:hover\s*\{([^}]*)\}/);
expect(hoverMatch).toBeTruthy();
expect(hoverMatch![1]).toContain("var(--fab-bg)");
expect(hoverMatch![1]).toContain("var(--fab-text)");
});
it("mission event type error uses CSS custom properties", () => {
const blockMatch = css.match(/\.mission-event__type--error\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();
expect(blockMatch![1]).toContain("var(--event-error-text)");
expect(blockMatch![1]).toContain("var(--event-error-bg)");
expect(blockMatch![1]).not.toContain("#fca5a5");
expect(blockMatch![1]).not.toContain("rgba(239, 68, 68, 0.15)");
});
it("mission autopilot pulse uses CSS custom property", () => {
const blockMatch = css.match(/\.mission-detail__autopilot-pulse\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();
expect(blockMatch![1]).toContain("var(--autopilot-pulse)");
expect(blockMatch![1]).not.toContain("#22c55e");
});
it("terminal container uses CSS custom property", () => {
const blockMatch = css.match(/\.terminal-container\s*\{([^}]*)\}/);
expect(blockMatch).toBeTruthy();
expect(blockMatch![1]).toContain("var(--terminal-bg)");
expect(blockMatch![1]).not.toContain("#1e1e1e");
});
it("new tokens are defined in :root", () => {
// Find the :root block at the start of the file (before any other selectors)
const rootStart = css.indexOf(":root {");
const afterRoot = css.slice(rootStart);
// Match until we find the closing } followed by html,
const rootMatch = afterRoot.match(/:root\s*\{([\s\S]*?)^}\s*\n\s*html,/m);
expect(rootMatch).toBeTruthy();
const rootContent = rootMatch![1];
expect(rootContent).toContain("--autopilot-icon");
expect(rootContent).toContain("--event-error-text");
expect(rootContent).toContain("--terminal-bg");
expect(rootContent).toContain("--star-idle");
expect(rootContent).toContain("--fab-text");
expect(rootContent).toContain("--badge-mission-text");
});
it("light theme overrides new tokens", () => {
// Find the base [data-theme="light"] block (not combined with other selectors)
const lightBlockMatch = css.match(/^\[data-theme="light"\]\s*\{[\s\S]*?^\}\s*$/m);
expect(lightBlockMatch).toBeTruthy();
const lightContent = lightBlockMatch![0];
expect(lightContent).toContain("--terminal-bg");
expect(lightContent).toContain("--event-error-text");
expect(lightContent).toContain("--autopilot-icon");
expect(lightContent).toContain("--star-active");
});
});
});

View File

@@ -224,6 +224,44 @@
--log-tool-bg: color-mix(in srgb, var(--accent) 8%, transparent);
--log-success-bg: color-mix(in srgb, var(--color-success) 6%, transparent);
--log-error-bg: color-mix(in srgb, var(--color-error) 6%, transparent);
/* === Mailbox compose FAB & badges === */
--fab-bg: var(--todo);
--fab-text: #fff;
/* === Mission autopilot indicators === */
--autopilot-pulse: var(--color-success);
--autopilot-icon: #eab308;
--autopilot-shadow: rgba(34, 197, 94, 0.4);
/* === Mission toggle & badge backgrounds === */
--toggle-checked-bg: rgba(34, 197, 94, 0.2);
--meta-badge-bg: rgba(63, 185, 80, 0.1);
/* === Mission event type colors (semantic — adapted for light in [data-theme="light"]) === */
--event-error-text: #fca5a5;
--event-state-text: #93c5fd;
--event-task-text: #6ee7b7;
--event-slice-text: #fcd34d;
--event-autopilot-text: #d8b4fe;
--event-error-bg: rgba(239, 68, 68, 0.15);
--event-state-bg: rgba(59, 130, 246, 0.15);
--event-task-bg: rgba(16, 185, 129, 0.15);
--event-slice-bg: rgba(245, 158, 11, 0.15);
--event-autopilot-bg: rgba(168, 85, 247, 0.15);
/* === Card mission badge === */
--badge-mission-text: #a78bfa;
--badge-mission-text-hover: #c4b5fd;
--badge-mission-bg: rgba(167, 139, 250, 0.12);
--badge-mission-bg-hover: rgba(167, 139, 250, 0.22);
/* === Terminal background === */
--terminal-bg: #1e1e1e;
/* === Model combobox favorite star === */
--star-idle: #6b7280;
--star-active: #f59e0b;
}
html,
@@ -1381,8 +1419,8 @@ body {
font-size: 10px;
font-weight: 600;
font-family: var(--font-mono);
color: #a78bfa;
background: rgba(167, 139, 250, 0.12);
color: var(--badge-mission-text);
background: var(--badge-mission-bg);
padding: 2px 6px;
border-radius: var(--radius-pill);
cursor: pointer;
@@ -1390,8 +1428,8 @@ body {
}
.card-mission-badge:hover {
color: #c4b5fd;
background: rgba(167, 139, 250, 0.22);
color: var(--badge-mission-text-hover);
background: var(--badge-mission-bg-hover);
}
.card-agent-badge {
@@ -8766,7 +8804,7 @@ body {
flex: 1;
min-height: 0;
position: relative;
background: #1e1e1e;
background: var(--terminal-bg);
overflow: hidden;
}
@@ -8805,7 +8843,7 @@ body {
position: absolute;
inset: 0;
z-index: 2;
background: #1e1e1e;
background: var(--terminal-bg);
gap: var(--space-lg);
color: var(--text-muted);
}
@@ -9491,7 +9529,7 @@ body {
.model-combobox-optgroup-favorite {
padding: 2px 4px;
font-size: 14px;
color: #6b7280;
color: var(--star-idle);
background: none;
border: none;
cursor: pointer;
@@ -9499,17 +9537,17 @@ body {
flex-shrink: 0;
}
.model-combobox-optgroup-favorite:hover {
color: #f59e0b;
color: var(--star-active);
}
.model-combobox-optgroup-favorite--active {
color: #f59e0b;
color: var(--star-active);
}
/* Model favorite star (inside option row) */
.model-combobox-option-favorite {
padding: 2px 4px;
font-size: 12px;
color: #6b7280;
color: var(--star-idle);
background: none;
border: none;
cursor: pointer;
@@ -9518,10 +9556,10 @@ body {
flex-shrink: 0;
}
.model-combobox-option-favorite:hover {
color: #f59e0b;
color: var(--star-active);
}
.model-combobox-option-favorite--active {
color: #f59e0b;
color: var(--star-active);
}
/* Favorited model pinned row */
@@ -9685,6 +9723,42 @@ html .column.drag-over * {
--log-error-bg: color-mix(in srgb, var(--color-error) 5%, transparent);
--shadow: var(--shadow-lg);
/* Mailbox FAB & badge text — ensure contrast on light backgrounds */
--fab-text: #fff;
/* Mission autopilot — darker for light backgrounds */
--autopilot-icon: #ca8a04;
--autopilot-shadow: rgba(34, 197, 94, 0.4);
/* Terminal background */
--terminal-bg: #f6f8fa;
/* Card mission badge — darker purple for light bg */
--badge-mission-text: #7c3aed;
--badge-mission-text-hover: #8b5cf6;
--badge-mission-bg: rgba(124, 58, 237, 0.12);
--badge-mission-bg-hover: rgba(124, 58, 237, 0.22);
/* Mission event type colors — darker/more saturated for light readability */
--event-error-text: #dc2626;
--event-state-text: #2563eb;
--event-task-text: #059669;
--event-slice-text: #d97706;
--event-autopilot-text: #9333ea;
--event-error-bg: rgba(220, 38, 38, 0.12);
--event-state-bg: rgba(37, 99, 235, 0.12);
--event-task-bg: rgba(5, 150, 105, 0.12);
--event-slice-bg: rgba(217, 119, 6, 0.12);
--event-autopilot-bg: rgba(147, 51, 234, 0.12);
/* Mission toggle & badge backgrounds */
--toggle-checked-bg: rgba(34, 197, 94, 0.15);
--meta-badge-bg: rgba(63, 185, 80, 0.08);
/* Star favorites */
--star-idle: #9ca3af;
--star-active: #d97706;
}
/* Light theme specific overrides */
@@ -22536,7 +22610,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
gap: 4px;
font-size: 11px;
color: var(--color-success);
background: rgba(63, 185, 80, 0.1);
background: var(--meta-badge-bg);
padding: 2px 8px;
border-radius: var(--radius-pill);
}
@@ -22594,7 +22668,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
width: 6px;
height: 6px;
border-radius: 50%;
background: #22c55e;
background: var(--autopilot-pulse);
margin-right: 4px;
animation: autopilot-pulse 2s ease-in-out infinite;
}
@@ -22604,7 +22678,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
background: var(--autopilot-pulse);
flex-shrink: 0;
animation: autopilot-pulse 2s ease-in-out infinite;
}
@@ -22654,7 +22728,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
}
.mission-toggle input[type="checkbox"]:checked + .mission-toggle__track {
background: rgba(34, 197, 94, 0.2);
background: var(--toggle-checked-bg);
border-color: var(--color-success, #22c55e);
}
@@ -22690,7 +22764,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
}
.mission-list__item-autopilot-icon {
color: #eab308;
color: var(--autopilot-icon);
flex-shrink: 0;
}
@@ -22798,28 +22872,28 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
}
.mission-event__type--error {
background: rgba(239, 68, 68, 0.15);
color: #fca5a5;
background: var(--event-error-bg);
color: var(--event-error-text);
}
.mission-event__type--state {
background: rgba(59, 130, 246, 0.15);
color: #93c5fd;
background: var(--event-state-bg);
color: var(--event-state-text);
}
.mission-event__type--task {
background: rgba(16, 185, 129, 0.15);
color: #6ee7b7;
background: var(--event-task-bg);
color: var(--event-task-text);
}
.mission-event__type--slice {
background: rgba(245, 158, 11, 0.15);
color: #fcd34d;
background: var(--event-slice-bg);
color: var(--event-slice-text);
}
.mission-event__type--autopilot {
background: rgba(168, 85, 247, 0.15);
color: #d8b4fe;
background: var(--event-autopilot-bg);
color: var(--event-autopilot-text);
}
.mission-event__description {
@@ -22859,11 +22933,11 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
@keyframes autopilot-pulse {
0%, 100% {
opacity: 1;
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
box-shadow: 0 0 0 0 var(--autopilot-shadow);
}
50% {
opacity: 0.7;
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0);
box-shadow: 0 0 0 4px var(--autopilot-shadow);
}
}
@@ -25998,7 +26072,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
padding: 0 6px;
border-radius: 10px;
background: var(--color-error, #ef4444);
color: white;
color: var(--fab-text);
font-size: 0.7rem;
font-weight: 700;
}
@@ -26049,7 +26123,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
padding: 0 4px;
border-radius: 8px;
background: var(--color-error, #ef4444);
color: white;
color: var(--fab-text);
font-size: 0.65rem;
font-weight: 700;
}
@@ -26315,8 +26389,8 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
height: 44px;
border-radius: 50%;
border: none;
background: var(--todo);
color: white;
background: var(--fab-bg);
color: var(--fab-text);
cursor: pointer;
display: flex;
align-items: center;
@@ -26327,7 +26401,7 @@ body[data-color-theme="terminal"][data-theme="light"]::before {
}
.mailbox-compose-fab:hover {
background: color-mix(in srgb, var(--todo) 80%, white);
background: color-mix(in srgb, var(--fab-bg) 80%, var(--fab-text));
transform: scale(1.05);
}