From a3161bb91cedfd853861ff01b1289da1461a1547 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 18 Jun 2026 21:38:24 -0700 Subject: [PATCH] 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 --- ...board-component-color-tokenization.test.ts | 81 +++++++++++++++++++ packages/dashboard/app/styles.css | 10 +++ 2 files changed, 91 insertions(+) diff --git a/packages/dashboard/app/__tests__/dashboard-component-color-tokenization.test.ts b/packages/dashboard/app/__tests__/dashboard-component-color-tokenization.test.ts index 93c921ff2c..faf53cc80a 100644 --- a/packages/dashboard/app/__tests__/dashboard-component-color-tokenization.test.ts +++ b/packages/dashboard/app/__tests__/dashboard-component-color-tokenization.test.ts @@ -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) { diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index fe9484c172..535d103f5f 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -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;