diff --git a/.fusion/memory.md b/.fusion/memory.md index b7f5029b9..68afd2748 100644 --- a/.fusion/memory.md +++ b/.fusion/memory.md @@ -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=""]` and a light variant `[data-color-theme=""][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) diff --git a/packages/dashboard/app/components/__tests__/MailboxModal.test.tsx b/packages/dashboard/app/components/__tests__/MailboxModal.test.tsx index 1b641e4dd..30744c41c 100644 --- a/packages/dashboard/app/components/__tests__/MailboxModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/MailboxModal.test.tsx @@ -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"); + }); + }); }); diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 9524a3b6e..245f42877 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -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); }