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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user