FN-6678: define dashboard surface tokens

Add missing dashboard theme tokens so surface and border styling resolves consistently across themes.

- Define --surface-1, --surface-2, and --border-subtle in base and light dashboard theme blocks.
- Keep the new tokens derived from existing theme variables with color-mix for dark and light mode consistency.
- Add a dashboard CSS guard that requires these tokens in both theme blocks without raw color definitions.

Files changed:
 .../dashboard-component-color-tokenization.test.ts | 81 ++++++++++++++++++++++
 packages/dashboard/app/styles.css                  | 10 +++
 2 files changed, 91 insertions(+)

Fusion-Task-Id: FN-6678

Fusion-Task-Lineage: a866af20-fbbb-46c6-9403-f395708bcd3f
This commit is contained in:
gsxdsm
2026-06-18 21:38:24 -07:00
parent 999163954e
commit a3161bb91c
2 changed files with 91 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { loadStylesCss } from "../test/cssFixture";
const root = resolve(__dirname, "../components");
@@ -40,6 +41,86 @@ function stripVarCalls(line: string): string {
return line.replace(/var\([^)]*\)/g, "");
}
function extractRootBlock(css: string): string {
const rootRegex = /:root\s*\{/g;
let match;
let secondRootIdx = -1;
let count = 0;
while ((match = rootRegex.exec(css)) !== null) {
count++;
if (count === 2) {
secondRootIdx = match.index;
break;
}
}
if (secondRootIdx === -1) {
throw new Error("Could not find second :root block");
}
return extractBlockAt(css, secondRootIdx);
}
function extractLightThemeBlock(css: string): string {
const startMatch = css.match(/:root\[data-theme="light"\]\s*\{/);
if (!startMatch) {
throw new Error("Could not find :root[data-theme=\"light\"] block");
}
return extractBlockAt(css, startMatch.index!);
}
function extractBlockAt(css: string, startIdx: number): string {
const openBraceIdx = startIdx + css.slice(startIdx).indexOf("{");
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);
}
function extractTokenDefinition(block: string, token: string): string {
const escapedToken = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = block.match(new RegExp(`${escapedToken}:\\s*([^;]+);`));
if (!match) {
throw new Error(`Could not find ${token} definition`);
}
return match[1].trim();
}
describe("dashboard surface token definitions", () => {
it("defines neutral surface and subtle border tokens in base and light themes", () => {
const css = loadStylesCss();
const rootBlock = extractRootBlock(css);
const lightThemeBlock = extractLightThemeBlock(css);
const tokens = ["--surface-1", "--surface-2", "--border-subtle"];
/*
FNXC:DashboardSurfaceTokens 2026-06-18-21:29:
FN-6678 keeps these tokens defined in both canonical theme blocks because charts, Command Center surfaces, areas, and NewTaskModal consume them through bare var() calls with no fallback. Require color-mix token derivations so removing a definition or replacing it with a raw color fails the guard.
*/
for (const token of tokens) {
const rootDefinition = extractTokenDefinition(rootBlock, token);
const lightDefinition = extractTokenDefinition(lightThemeBlock, token);
expect(rootDefinition, `${token} must be defined in :root`).toMatch(/^color-mix\(in\s+srgb,/);
expect(lightDefinition, `${token} must be defined in :root[data-theme="light"]`).toMatch(/^color-mix\(in\s+srgb,/);
expect(rootDefinition, `${token} root definition must not use raw colors`).not.toMatch(/rgba\(|#[0-9a-fA-F]{3,8}/);
expect(lightDefinition, `${token} light definition must not use raw colors`).not.toMatch(/rgba\(|#[0-9a-fA-F]{3,8}/);
}
});
});
describe("dashboard component color tokenization", () => {
it("keeps audited compliant files free of raw rgba()", () => {
for (const file of auditedCompliant) {

View File

@@ -231,6 +231,13 @@ svg.spin {
--surface-muted: color-mix(in srgb, var(--surface) 94%, var(--text) 6%);
--surface-emphasis: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--surface-hover-strong: color-mix(in srgb, var(--surface) 88%, var(--text) 12%);
/*
FNXC:DashboardSurfaceTokens 2026-06-18-21:29:
FN-6678 defines neutral surface tiers and a subtle border because the command-center subtree, NewTaskModal, and related dashboard surfaces consume these tokens via bare var() calls. Keep them tokenized so light and dark modes cascade from each theme's --surface, --text, and --border values instead of resolving unset.
*/
--surface-1: color-mix(in srgb, var(--surface) 97%, var(--text) 3%);
--surface-2: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
--border-subtle: color-mix(in srgb, var(--border) 60%, transparent);
--bg-secondary: color-mix(in srgb, var(--surface) 70%, var(--card));
--bg-tertiary: color-mix(in srgb, var(--surface) 40%, var(--card));
--border: #30363d;
@@ -467,6 +474,9 @@ svg.spin {
--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%);
--surface-1: color-mix(in srgb, var(--surface) 98%, var(--text) 2%);
--surface-2: color-mix(in srgb, var(--surface) 95%, var(--text) 5%);
--border-subtle: color-mix(in srgb, var(--border) 60%, transparent);
--bg: #ffffff;
--surface: #f6f8fa;
--card: #ffffff;