feat(FN-3011): merge fusion/fn-3011

Commits merged:
- test(FN-3011): complete Step 2 — harden light-theme source assertions
- feat(FN-3011): complete Step 1 — restore global token scope
- fix(FN-3011): complete Step 3 — satisfy lint/test/typecheck/build gates
- test(FN-3011): complete Step 2 — harden light-theme source assertions
- feat(FN-3011): complete Step 1 — restore default light token ownership

Files changed:
.../app/__tests__/status-colors-theme.test.ts      | 100 +++++--
 .../app/components/CustomModelDropdown.css         |   6 -
 .../__tests__/ModelOnboardingModal.test.tsx        |   4 +-
 packages/dashboard/app/styles.css                  | 299 +++++++++++----------
 packages/dashboard/app/test/cssFixture.ts          |  11 +-
 5 files changed, 242 insertions(+), 178 deletions(-)

Fusion-Task-Id: FN-3011
This commit is contained in:
Fusion
2026-04-29 22:18:18 -07:00
committed by gsxdsm
parent a039e2cf27
commit b78dd3e055
5 changed files with 242 additions and 178 deletions

View File

@@ -1,8 +1,10 @@
import { describe, it, expect } from "vitest";
import { loadAllAppCss } from "../test/cssFixture";
import { loadAllAppCss, loadStylesCss } from "../test/cssFixture";
import fs from "fs";
import path from "path";
const themeDataPath = path.resolve(__dirname, "../public/theme-data.css");
/**
* Theme-safety regression tests for status color tokens across
* TaskCard, GitHubBadge, and PrSection components.
@@ -14,55 +16,57 @@ import path from "path";
describe("Status color CSS custom properties", () => {
let css: string;
let stylesCss: string;
beforeAll(() => {
css = loadAllAppCss();
stylesCss = loadStylesCss();
});
it("defines --status-triage-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-triage-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--triage) 15%, transparent)");
});
it("defines --status-todo-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-todo-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--todo) 15%, transparent)");
});
it("defines --status-in-progress-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-in-progress-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--in-progress) 15%, transparent)");
});
it("defines --status-in-review-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-in-review-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--in-review) 15%, transparent)");
});
it("defines --status-done-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-done-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--done) 15%, transparent)");
});
it("defines --status-error-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-error-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--color-error-dark");
});
it("defines --status-archived-bg custom property in :root using color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--status-archived-bg");
expect(rootBlock).toContain("color-mix(in srgb, var(--text-muted");
});
it("defines --surface-hover in :root using semantic color-mix()", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--surface-hover");
expect(rootBlock).toContain(
"--surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%)"
@@ -70,7 +74,7 @@ describe("Status color CSS custom properties", () => {
});
it("defines light theme override for --surface-hover", () => {
const lightBlock = extractLightThemeBlock(css);
const lightBlock = extractLightThemeBlock(stylesCss);
expect(lightBlock).toContain("--surface-hover");
expect(lightBlock).toContain(
"--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%)"
@@ -78,7 +82,7 @@ describe("Status color CSS custom properties", () => {
});
it("defines semantic neutral surface tiers in :root", () => {
const rootBlock = extractRootBlock(css);
const rootBlock = extractRootBlock(stylesCss);
expect(rootBlock).toContain("--surface-subtle");
expect(rootBlock).toContain("--surface-muted");
expect(rootBlock).toContain("--surface-emphasis");
@@ -86,7 +90,7 @@ describe("Status color CSS custom properties", () => {
});
it("defines semantic neutral surface tier overrides in light theme", () => {
const lightBlock = extractLightThemeBlock(css);
const lightBlock = extractLightThemeBlock(stylesCss);
expect(lightBlock).toContain("--surface-subtle");
expect(lightBlock).toContain("--surface-muted");
expect(lightBlock).toContain("--surface-emphasis");
@@ -94,11 +98,11 @@ describe("Status color CSS custom properties", () => {
});
it("defines semantic neutral tiers with tokenized color-mix expressions", () => {
expect(css).toMatch(/--surface-subtle:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+96%,\s*var\(--text\)\s+4%\)/);
expect(css).toMatch(/--surface-muted:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+94%,\s*var\(--text\)\s+6%\)/);
expect(css).toMatch(/--surface-emphasis:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+92%,\s*var\(--text\)\s+8%\)/);
expect(css).toMatch(/--surface-hover-strong:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+88%,\s*var\(--text\)\s+12%\)/);
expect(css).not.toMatch(/--surface-(subtle|muted|emphasis|hover-strong):\s*rgba\(/);
expect(stylesCss).toMatch(/--surface-subtle:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+96%,\s*var\(--text\)\s+4%\)/);
expect(stylesCss).toMatch(/--surface-muted:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+94%,\s*var\(--text\)\s+6%\)/);
expect(stylesCss).toMatch(/--surface-emphasis:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+92%,\s*var\(--text\)\s+8%\)/);
expect(stylesCss).toMatch(/--surface-hover-strong:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+88%,\s*var\(--text\)\s+12%\)/);
expect(stylesCss).not.toMatch(/--surface-(subtle|muted|emphasis|hover-strong):\s*rgba\(/);
});
it("uses --surface-hover token references with tokenized fallback (no raw rgba)", () => {
@@ -108,17 +112,34 @@ describe("Status color CSS custom properties", () => {
});
it("defines --surface-hover with tokenized color-mix (no raw rgba/hex)", () => {
expect(css).toMatch(/--surface-hover:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+90%,\s*var\(--text\)\s+10%\)/);
expect(css).toMatch(/\[data-theme="light"\]\s*\{[^}]*--surface-hover:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+92%,\s*var\(--text\)\s+8%\)/s);
expect(css).not.toMatch(/--surface-hover:\s*rgba\(/);
expect(css).not.toMatch(/--surface-hover:\s*#[0-9a-fA-F]{3,8}/);
expect(stylesCss).toMatch(/--surface-hover:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+90%,\s*var\(--text\)\s+10%\)/);
expect(stylesCss).toMatch(/:root\[data-theme="light"\]\s*\{[^}]*--surface-hover:\s*color-mix\(in\s+srgb,\s*var\(--surface\)\s+92%,\s*var\(--text\)\s+8%\)/s);
expect(stylesCss).not.toMatch(/--surface-hover:\s*rgba\(/);
expect(stylesCss).not.toMatch(/--surface-hover:\s*#[0-9a-fA-F]{3,8}/);
});
it("defines light theme override for --status-error-bg", () => {
const lightBlock = extractLightThemeBlock(css);
const lightBlock = extractLightThemeBlock(stylesCss);
expect(lightBlock).toContain("--status-error-bg");
expect(lightBlock).toContain("--status-error-bg-deep");
});
it("keeps :root[data-theme=\"light\"] owned by styles.css", () => {
const stylesLightMatches = stylesCss.match(/:root\[data-theme="light"\]\s*\{/g) ?? [];
const allCssLightMatches = css.match(/:root\[data-theme="light"\]\s*\{/g) ?? [];
expect(stylesLightMatches).toHaveLength(1);
expect(allCssLightMatches).toHaveLength(stylesLightMatches.length);
});
it("does not scope global design tokens under runtime-card cobrand selector", () => {
const cobrandMarkBlock = extractSelectorBlock(stylesCss, ".runtime-card__cobrand-mark");
expect(cobrandMarkBlock).not.toContain("--cta-bg");
expect(cobrandMarkBlock).not.toContain("--state-idle-bg");
expect(cobrandMarkBlock).not.toContain("--event-error-text");
expect(cobrandMarkBlock).not.toContain("--star-idle");
});
});
describe("TaskCard theme safety", () => {
@@ -316,8 +337,7 @@ describe("CSS modifier classes for status colors", () => {
});
describe("Accent color per color theme", () => {
// Theme blocks are now in a separate theme-data.css file
const themeDataPath = path.resolve(__dirname, "../public/theme-data.css");
// Theme blocks are in theme-data.css and intentionally outside loadAllAppCss().
let css: string;
let themeData: string;
@@ -513,9 +533,12 @@ function extractRootBlock(css: string): string {
}
function extractLightThemeBlock(css: string): string {
const startMatch = css.match(/\[data-theme="light"\]\s*\{/);
// Intentionally parses the default light token block from styles.css
// (`:root[data-theme="light"]`). Color-theme light variants are validated
// from theme-data.css.
const startMatch = css.match(/:root\[data-theme="light"\]\s*\{/);
if (!startMatch) {
throw new Error("Could not find [data-theme=\"light\"] block");
throw new Error("Could not find :root[data-theme=\"light\"] block");
}
const startIdx = startMatch.index!;
@@ -534,3 +557,28 @@ function extractLightThemeBlock(css: string): string {
return css.slice(startIdx, end + 1);
}
function extractSelectorBlock(css: string, selector: string): string {
const startIdx = css.indexOf(selector);
if (startIdx === -1) {
throw new Error(`Could not find selector block: ${selector}`);
}
const openBraceIdx = css.indexOf("{", startIdx);
if (openBraceIdx === -1) {
throw new Error(`Could not find opening brace for selector: ${selector}`);
}
let depth = 1;
let end = openBraceIdx;
for (let i = openBraceIdx + 1; i < css.length; i++) {
if (css[i] === "{") depth++;
if (css[i] === "}") depth--;
if (depth === 0) {
end = i;
break;
}
}
return css.slice(startIdx, end + 1);
}

View File

@@ -347,12 +347,6 @@ html .column.drag-over * {
--surface: #f6f8fa;
--card: #ffffff;
--card-hover: #f3f4f6;
/* Light mode keeps the same semantic role with slightly softer contrast. */
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--surface-subtle: color-mix(in srgb, var(--surface) 98%, var(--text) 2%);
--surface-muted: color-mix(in srgb, var(--surface) 97%, var(--text) 3%);
--surface-emphasis: color-mix(in srgb, var(--surface) 95%, var(--text) 5%);
--surface-hover-strong: color-mix(in srgb, var(--surface) 94%, var(--text) 6%);
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--border: #d0d7de;

View File

@@ -2399,8 +2399,8 @@ describe("ModelOnboardingModal", () => {
expect(mockFetchAuthStatus).toHaveBeenCalled();
// GitHub should show as connected
expect(screen.getByTestId("onboarding-auth-status-github")).toBeTruthy();
expect(screen.getByText("✓ Connected")).toBeTruthy();
expect(await screen.findByTestId("onboarding-auth-status-github")).toBeTruthy();
expect(await screen.findByText("✓ Connected")).toBeTruthy();
// Should show Disconnect instead of Connect
expect(screen.getByText("Disconnect")).toBeTruthy();

View File

@@ -256,6 +256,162 @@ html {
--provider-openclaw: #ff4f40;
--provider-paperclip: var(--text);
--provider-icon-contrast: var(--bg);
/* Task-creation CTA tokens */
--cta-bg: #238636;
--cta-border: #2ea043;
--cta-text: #fff;
--cta-bg-hover: #2ea043;
--cta-border-hover: #3fb950;
--cta-glow: 0 0 8px rgba(46, 160, 67, 0.3);
/* === Agent State Colors === */
--state-idle-bg: rgba(139, 148, 158, 0.15);
--state-idle-text: #8b949e;
--state-idle-border: #8b949e;
--state-active-bg: rgba(46, 160, 67, 0.15);
--state-active-text: #3fb950;
--state-active-border: #3fb950;
--state-paused-bg: rgba(227, 179, 65, 0.15);
--state-paused-text: #e3b541;
--state-paused-border: #e3b541;
--state-error-bg: rgba(248, 81, 73, 0.15);
--state-error-text: #f85149;
--state-error-border: #f85149;
/* === Mission & Milestone Status Colors === */
--mission-planning-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--mission-planning-text: var(--triage);
--mission-active-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--mission-active-text: var(--in-review);
--mission-blocked-bg: color-mix(in srgb, var(--color-error) 15%, transparent);
--mission-blocked-text: var(--color-error);
--mission-complete-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--mission-complete-text: #3b82f6;
--mission-archived-bg: var(--bg-tertiary);
--mission-archived-text: var(--text-secondary);
/* === Slice Status Colors === */
--slice-pending-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--slice-pending-text: var(--triage);
--slice-active-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--slice-active-text: var(--in-review);
--slice-complete-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--slice-complete-text: #3b82f6;
/* === Feature (Task) Status Colors === */
--feature-defined-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--feature-defined-text: var(--triage);
--feature-triaged-bg: color-mix(in srgb, #a855f7 15%, transparent);
--feature-triaged-text: #a855f7;
--feature-in-progress-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--feature-in-progress-text: var(--in-review);
--feature-done-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--feature-done-text: #3b82f6;
/* === Autopilot State Colors === */
--autopilot-inactive-bg: color-mix(in srgb, #94a3b8 15%, transparent);
--autopilot-inactive-text: #94a3b8;
--autopilot-watching-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--autopilot-watching-text: var(--in-review);
--autopilot-activating-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--autopilot-activating-text: #3b82f6;
--autopilot-completing-bg: color-mix(in srgb, #a855f7 15%, transparent);
--autopilot-completing-text: #a855f7;
/* === Loop State Colors === */
--loop-idle-bg: var(--bg-tertiary);
--loop-idle-text: var(--text-secondary);
--loop-idle-indicator: var(--text-dim);
--loop-implementing-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--loop-implementing-text: var(--in-review);
--loop-implementing-indicator: var(--in-review);
--loop-validating-bg: color-mix(in srgb, var(--color-warning) 15%, transparent);
--loop-validating-text: var(--color-warning);
--loop-validating-indicator: var(--color-warning);
--loop-needs-fix-bg: color-mix(in srgb, #f97316 15%, transparent);
--loop-needs-fix-text: #f97316;
--loop-needs-fix-indicator: #f97316;
--loop-passed-bg: color-mix(in srgb, var(--color-success) 15%, transparent);
--loop-passed-text: var(--color-success);
--loop-passed-indicator: var(--color-success);
--loop-blocked-bg: color-mix(in srgb, var(--color-error) 15%, transparent);
--loop-blocked-text: var(--color-error);
--loop-blocked-indicator: var(--color-error);
/* === Assertion Status Colors === */
--assertion-pending-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--assertion-pending-text: var(--triage);
--assertion-passed-bg: color-mix(in srgb, var(--color-success) 15%, transparent);
--assertion-passed-text: var(--color-success);
--assertion-failed-bg: color-mix(in srgb, var(--color-error) 15%, transparent);
--assertion-failed-text: var(--color-error);
--assertion-blocked-bg: color-mix(in srgb, var(--color-warning) 15%, transparent);
--assertion-blocked-text: var(--color-warning);
/* === Interview Modal Icon Colors === */
--icon-milestone: var(--triage);
--icon-slice: var(--in-review);
--icon-feature: #3b82f6;
/* === Accent Color === */
--accent: #7c5cbf;
/* === Log Entry Background Tokens === */
--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;
}
/* Default light-mode token contract.
NOTE: color-theme-specific light overrides live in public/theme-data.css.
This block is the source of truth for non-color-theme light mode. */
:root[data-theme="light"] {
--surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--surface-subtle: color-mix(in srgb, var(--surface) 98%, var(--text) 2%);
--surface-muted: color-mix(in srgb, var(--surface) 97%, var(--text) 3%);
--surface-emphasis: color-mix(in srgb, var(--surface) 95%, var(--text) 5%);
--surface-hover-strong: color-mix(in srgb, var(--surface) 94%, var(--text) 6%);
--status-error-bg: color-mix(in srgb, var(--color-error-dark) 15%, transparent);
--status-error-bg-deep: color-mix(in srgb, var(--color-error-dark) 20%, transparent);
}
/* -------------------------------------------------------------------------
@@ -421,149 +577,6 @@ html {
color: var(--text-muted);
border-radius: 2px;
flex: 0 0 auto;
/* Task-creation CTA tokens */
--cta-bg: #238636;
--cta-border: #2ea043;
--cta-text: #fff;
--cta-bg-hover: #2ea043;
--cta-border-hover: #3fb950;
--cta-glow: 0 0 8px rgba(46, 160, 67, 0.3);
/* === Agent State Colors === */
--state-idle-bg: rgba(139, 148, 158, 0.15);
--state-idle-text: #8b949e;
--state-idle-border: #8b949e;
--state-active-bg: rgba(46, 160, 67, 0.15);
--state-active-text: #3fb950;
--state-active-border: #3fb950;
--state-paused-bg: rgba(227, 179, 65, 0.15);
--state-paused-text: #e3b541;
--state-paused-border: #e3b541;
--state-error-bg: rgba(248, 81, 73, 0.15);
--state-error-text: #f85149;
--state-error-border: #f85149;
/* === Mission & Milestone Status Colors === */
--mission-planning-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--mission-planning-text: var(--triage);
--mission-active-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--mission-active-text: var(--in-review);
--mission-blocked-bg: color-mix(in srgb, var(--color-error) 15%, transparent);
--mission-blocked-text: var(--color-error);
--mission-complete-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--mission-complete-text: #3b82f6;
--mission-archived-bg: var(--bg-tertiary);
--mission-archived-text: var(--text-secondary);
/* === Slice Status Colors === */
--slice-pending-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--slice-pending-text: var(--triage);
--slice-active-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--slice-active-text: var(--in-review);
--slice-complete-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--slice-complete-text: #3b82f6;
/* === Feature (Task) Status Colors === */
--feature-defined-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--feature-defined-text: var(--triage);
--feature-triaged-bg: color-mix(in srgb, #a855f7 15%, transparent);
--feature-triaged-text: #a855f7;
--feature-in-progress-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--feature-in-progress-text: var(--in-review);
--feature-done-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--feature-done-text: #3b82f6;
/* === Autopilot State Colors === */
--autopilot-inactive-bg: color-mix(in srgb, #94a3b8 15%, transparent);
--autopilot-inactive-text: #94a3b8;
--autopilot-watching-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--autopilot-watching-text: var(--in-review);
--autopilot-activating-bg: color-mix(in srgb, #3b82f6 15%, transparent);
--autopilot-activating-text: #3b82f6;
--autopilot-completing-bg: color-mix(in srgb, #a855f7 15%, transparent);
--autopilot-completing-text: #a855f7;
/* === Loop State Colors === */
--loop-idle-bg: var(--bg-tertiary);
--loop-idle-text: var(--text-secondary);
--loop-idle-indicator: var(--text-dim);
--loop-implementing-bg: color-mix(in srgb, var(--in-review) 15%, transparent);
--loop-implementing-text: var(--in-review);
--loop-implementing-indicator: var(--in-review);
--loop-validating-bg: color-mix(in srgb, var(--color-warning) 15%, transparent);
--loop-validating-text: var(--color-warning);
--loop-validating-indicator: var(--color-warning);
--loop-needs-fix-bg: color-mix(in srgb, #f97316 15%, transparent);
--loop-needs-fix-text: #f97316;
--loop-needs-fix-indicator: #f97316;
--loop-passed-bg: color-mix(in srgb, var(--color-success) 15%, transparent);
--loop-passed-text: var(--color-success);
--loop-passed-indicator: var(--color-success);
--loop-blocked-bg: color-mix(in srgb, var(--color-error) 15%, transparent);
--loop-blocked-text: var(--color-error);
--loop-blocked-indicator: var(--color-error);
/* === Assertion Status Colors === */
--assertion-pending-bg: color-mix(in srgb, var(--triage) 15%, transparent);
--assertion-pending-text: var(--triage);
--assertion-passed-bg: color-mix(in srgb, var(--color-success) 15%, transparent);
--assertion-passed-text: var(--color-success);
--assertion-failed-bg: color-mix(in srgb, var(--color-error) 15%, transparent);
--assertion-failed-text: var(--color-error);
--assertion-blocked-bg: color-mix(in srgb, var(--color-warning) 15%, transparent);
--assertion-blocked-text: var(--color-warning);
/* === Interview Modal Icon Colors === */
--icon-milestone: var(--triage);
--icon-slice: var(--in-review);
--icon-feature: #3b82f6;
/* === Accent Color === */
--accent: #7c5cbf;
/* === Log Entry Background Tokens === */
--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,

View File

@@ -5,6 +5,13 @@ const APP_DIR = resolve(__dirname, "..");
const COMPONENTS_DIR = join(APP_DIR, "components");
let cached: string | null = null;
let stylesCached: string | null = null;
export function loadStylesCss(): string {
if (stylesCached !== null) return stylesCached;
stylesCached = readFileSync(join(APP_DIR, "styles.css"), "utf-8");
return stylesCached;
}
export function loadAllAppCss(): string {
if (cached !== null) return cached;
@@ -12,7 +19,9 @@ export function loadAllAppCss(): string {
// tests), then component CSS files in alphabetical order. This keeps both
// styles.css's intra-file order intact AND lets co-located component base
// rules be matched before any cross-file @media override referencing them.
const parts: string[] = [readFileSync(join(APP_DIR, "styles.css"), "utf-8")];
// NOTE: public/theme-data.css is intentionally excluded here; tests that
// validate per-color-theme contracts should load it explicitly.
const parts: string[] = [loadStylesCss()];
const entries = readdirSync(COMPONENTS_DIR, { withFileTypes: true })
.filter((e) => e.isFile() && e.name.endsWith(".css"))
.map((e) => e.name)