From 8124fa376177b36b40df4806cd607b00ae2c5919 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 21 Jun 2026 14:17:45 -0700 Subject: [PATCH] FN-6867: fix roadmap CSS tokens Roadmap dashboard styling now uses defined Fusion theme tokens and has regression coverage for undefined CSS custom properties. - Retoken roadmap surfaces, form controls, and text from retired aliases to defined dashboard tokens. - Document the roadmap styling requirement with an FNXC comment in the plugin CSS. - Add a CSS-token validity test that rejects retired aliases and undefined roadmap custom-property references. Files changed: .../src/dashboard/RoadmapsView.css | 63 ++++----- .../RoadmapsView.css-token-validity.test.ts | 147 +++++++++++++++++++++ 2 files changed, 179 insertions(+), 31 deletions(-) Fusion-Task-Id: FN-6867 Fusion-Task-Lineage: 0d5c71dc-b6ad-4b70-a792-2d8a05fe81f1 --- .../src/dashboard/RoadmapsView.css | 63 ++++---- .../RoadmapsView.css-token-validity.test.ts | 147 ++++++++++++++++++ 2 files changed, 179 insertions(+), 31 deletions(-) create mode 100644 plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.css-token-validity.test.ts diff --git a/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css b/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css index 3057c3da56..76ef2fb883 100644 --- a/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +++ b/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css @@ -1,4 +1,5 @@ /* Roadmaps view */ +/* FNXC:RoadmapStyling 2026-06-21-00:00: Roadmap surfaces must use defined dashboard tokens (--card, --surface, --text). Retired elevated-surface, input-surface, and primary-text aliases rendered surfaces transparent and text uncolored (FN-6867). A CSS-token guard prevents reintroduction. */ .roadmaps-view { display: flex; height: 100%; @@ -76,7 +77,7 @@ display: flex; flex-direction: column; border-right: 1px solid var(--border); - background: var(--surface-elevated); + background: var(--card); } .roadmaps-view__sidebar-header { @@ -90,7 +91,7 @@ .roadmaps-view__sidebar-title { font-size: 1rem; font-weight: 600; - color: var(--text-primary); + color: var(--text); } .roadmaps-view__add-btn { @@ -150,7 +151,7 @@ .roadmaps-view__sidebar-item-title { font-weight: 500; - color: var(--text-primary); + color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -244,7 +245,7 @@ .roadmaps-view__roadmap-title { font-size: 1.5rem; font-weight: 600; - color: var(--text-primary); + color: var(--text); flex: 1; } @@ -276,7 +277,7 @@ flex-shrink: 0; display: flex; flex-direction: column; - background: var(--surface-elevated); + background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); @@ -316,7 +317,7 @@ } .roadmaps-view__drag-handle:hover { - color: var(--text-primary); + color: var(--text); background: var(--surface-hover); } @@ -335,7 +336,7 @@ .roadmaps-view__milestone-title { font-size: 1rem; font-weight: 600; - color: var(--text-primary); + color: var(--text); flex: 1; } @@ -371,7 +372,7 @@ .roadmaps-view__add-feature-btn:hover { background: var(--surface-hover); - color: var(--text-primary); + color: var(--text); border-color: var(--text-muted); } @@ -465,7 +466,7 @@ .roadmaps-view__feature-title { font-weight: 500; - color: var(--text-primary); + color: var(--text); font-size: 0.9rem; } @@ -504,8 +505,8 @@ padding: var(--space-sm); border: 1px solid var(--border); border-radius: var(--radius-sm); - background: var(--surface-input); - color: var(--text-primary); + background: var(--surface); + color: var(--text); font-size: 0.9rem; font-family: inherit; } @@ -526,8 +527,8 @@ padding: var(--space-sm); border: 1px solid var(--border); border-radius: var(--radius-sm); - background: var(--surface-input); - color: var(--text-primary); + background: var(--surface); + color: var(--text); font-size: 0.85rem; font-family: inherit; resize: vertical; @@ -542,7 +543,7 @@ .roadmaps-view__create-form { padding: var(--space-md); border-bottom: 1px solid var(--border); - background: var(--surface-elevated); + background: var(--card); } .roadmaps-view__create-form-actions { @@ -555,8 +556,8 @@ padding: var(--space-sm) var(--space-md); border: 1px solid var(--border); border-radius: var(--radius-sm); - background: var(--surface-input); - color: var(--text-primary); + background: var(--surface); + color: var(--text); font-size: 0.85rem; cursor: pointer; transition: background var(--transition-fast); @@ -616,7 +617,7 @@ .roadmaps-view__add-milestone-btn:hover, .roadmaps-view__add-milestone-fab:hover { background: var(--surface-hover); - color: var(--text-primary); + color: var(--text); border-color: var(--text-muted); } @@ -625,7 +626,7 @@ border: 1px dashed var(--border); border-radius: var(--radius-sm); margin: var(--space-sm); - background: var(--surface-elevated); + background: var(--card); } .roadmaps-view__inline-form { @@ -675,7 +676,7 @@ .roadmap-suggestion-title { font-size: 1rem; font-weight: 600; - color: var(--text-primary); + color: var(--text); margin: 0; } @@ -690,8 +691,8 @@ padding: var(--space-sm) var(--space-md); border: 1px solid var(--border); border-radius: var(--radius-sm); - background: var(--surface-input); - color: var(--text-primary); + background: var(--surface); + color: var(--text); font-size: 0.9rem; font-family: inherit; resize: vertical; @@ -797,7 +798,7 @@ align-items: flex-start; justify-content: space-between; padding: var(--space-md); - background: var(--surface-elevated); + background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); transition: border-color var(--transition-fast), box-shadow var(--transition-fast); @@ -819,7 +820,7 @@ .roadmap-suggestion-card-title { font-size: 0.9rem; font-weight: 500; - color: var(--text-primary); + color: var(--text); } .roadmap-suggestion-card-desc { @@ -869,7 +870,7 @@ width: 28px; height: 28px; padding: 0; - background: var(--surface-elevated); + background: var(--card); color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--radius-sm); @@ -879,11 +880,11 @@ .roadmap-suggestion-edit-btn:hover { background: var(--surface-hover); - color: var(--text-primary); + color: var(--text); } .roadmap-suggestion-card--editing { - background: var(--surface-elevated); + background: var(--card); border-color: var(--accent); } @@ -899,7 +900,7 @@ width: 100%; padding: var(--space-sm); background: var(--bg); - color: var(--text-primary); + color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 0.9rem; @@ -954,7 +955,7 @@ width: 28px; height: 28px; padding: 0; - background: var(--surface-elevated); + background: var(--card); color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--radius-sm); @@ -964,7 +965,7 @@ .roadmap-suggestion-cancel-btn:hover { background: var(--surface-hover); - color: var(--text-primary); + color: var(--text); } /* === Mobile Suggestion Panel Expand/Collapse === */ @@ -979,7 +980,7 @@ border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-md) var(--space-lg); - color: var(--text-primary); + color: var(--text); cursor: pointer; font-size: 0.9rem; font-weight: 600; @@ -1013,7 +1014,7 @@ } .roadmap-suggestion-collapse-btn:hover { - color: var(--text-primary); + color: var(--text); } /* Mobile responsive */ diff --git a/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.css-token-validity.test.ts b/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.css-token-validity.test.ts new file mode 100644 index 0000000000..328bc41553 --- /dev/null +++ b/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.css-token-validity.test.ts @@ -0,0 +1,147 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const TEST_DIR = __dirname; +const REPO_ROOT = path.resolve(TEST_DIR, "../../../../../"); +const DASHBOARD_APP_ROOT = path.join(REPO_ROOT, "packages/dashboard/app"); +const DASHBOARD_COMPONENTS_ROOT = path.join(DASHBOARD_APP_ROOT, "components"); +const ROADMAP_CSS = path.resolve(TEST_DIR, "../RoadmapsView.css"); +const RETIRED_TOKEN_REFERENCES = ["--surface-elevated", "--surface-input", "--text-primary"]; + +/** + * FNXC:RoadmapStyling 2026-06-21-00:00: + * FN-6867 guards roadmap plugin CSS with a raw-text token scan because jsdom does not resolve custom properties. + * Roadmap surfaces and text must reference dashboard-defined tokens so sidebar, lane, form, feature-card, and suggestion surfaces remain opaque in every theme. + */ +function stripCssComments(source: string): string { + return source.replace(/\/\*[\s\S]*?\*\//g, ""); +} + +function collectFiles(dir: string, predicate: (fileName: string) => boolean): string[] { + const out: string[] = []; + + for (const entry of readdirSync(dir)) { + if (entry === "node_modules" || entry === "dist" || entry === "__tests__" || entry.startsWith(".")) continue; + + const fullPath = path.join(dir, entry); + const info = statSync(fullPath); + + if (info.isDirectory()) { + out.push(...collectFiles(fullPath, predicate)); + continue; + } + + if (info.isFile() && predicate(entry)) out.push(fullPath); + } + + return out.sort((left, right) => formatRepoPath(left).localeCompare(formatRepoPath(right))); +} + +function collectDashboardVocabularyCssFiles(): string[] { + const appLevelCss = readdirSync(DASHBOARD_APP_ROOT) + .filter((entry) => entry.endsWith(".css")) + .map((entry) => path.join(DASHBOARD_APP_ROOT, entry)); + const componentCss = collectFiles(DASHBOARD_COMPONENTS_ROOT, (fileName) => fileName.endsWith(".css")); + const themeDataCss = [path.join(DASHBOARD_APP_ROOT, "public/theme-data.css")]; + + return [...appLevelCss, ...themeDataCss, ...componentCss].sort((left, right) => + formatRepoPath(left).localeCompare(formatRepoPath(right)), + ); +} + +function collectDefinedProperties(cssFiles: string[]): Set { + const properties = new Set(); + + for (const filePath of cssFiles) { + const source = stripCssComments(readFileSync(filePath, "utf8")); + for (const match of source.matchAll(/(^|[\s{;])(--[A-Za-z0-9_-]+)\s*:/g)) { + properties.add(match[2]); + } + } + + return properties; +} + +function collectReferencedProperties(source: string): Map { + const references = new Map(); + const uncommented = stripCssComments(source); + + uncommented.split("\n").forEach((line, index) => { + for (const match of line.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) { + const property = match[1]; + const lines = references.get(property) ?? []; + lines.push(index + 1); + references.set(property, lines); + } + }); + + return references; +} + +function findUndefinedReferences(args: { + cssFilesToScan: string[]; + definedProperties: Set; + sourceByFile?: Map; +}): string[] { + const { cssFilesToScan, definedProperties, sourceByFile = new Map() } = args; + const violations: string[] = []; + + for (const filePath of cssFilesToScan) { + const source = sourceByFile.get(filePath) ?? readFileSync(filePath, "utf8"); + for (const [property, lines] of collectReferencedProperties(source)) { + if (definedProperties.has(property)) continue; + violations.push(`${formatRepoPath(filePath)} references ${property} at line(s) ${lines.join(", ")}`); + } + } + + return violations.sort(); +} + +function formatRepoPath(filePath: string): string { + return path.relative(REPO_ROOT, filePath).split(path.sep).join("/"); +} + +describe("RoadmapsView CSS token validity (FN-6867)", () => { + it("flags a synthetic undefined custom-property reference", () => { + const fixturePath = path.join(REPO_ROOT, "fixture.css"); + const fixtureSource = "/* var(--commented-out) */ .x { color: var(--does-not-exist); background: var(--defined-token); }"; + const violations = findUndefinedReferences({ + cssFilesToScan: [fixturePath], + definedProperties: new Set(["--defined-token"]), + sourceByFile: new Map([[fixturePath, fixtureSource]]), + }); + + expect(collectReferencedProperties(fixtureSource)).toEqual( + new Map([ + ["--does-not-exist", [1]], + ["--defined-token", [1]], + ]), + ); + expect(violations).toEqual(["fixture.css references --does-not-exist at line(s) 1"]); + }); + + it("does not reintroduce retired roadmap surface or text token aliases", () => { + const css = readFileSync(ROADMAP_CSS, "utf8"); + const offenders = RETIRED_TOKEN_REFERENCES.filter((token) => css.includes(token)); + + expect( + offenders, + `RoadmapsView.css must use --card, --surface, and --text instead of retired aliases: ${offenders.join(", ")}`, + ).toEqual([]); + }); + + it("references only dashboard-defined or RoadmapsView-local custom properties", () => { + const dashboardVocabularyCssFiles = collectDashboardVocabularyCssFiles(); + const definedProperties = collectDefinedProperties([...dashboardVocabularyCssFiles, ROADMAP_CSS]); + const violations = findUndefinedReferences({ + cssFilesToScan: [ROADMAP_CSS], + definedProperties, + }); + + expect(dashboardVocabularyCssFiles).toContain(path.join(DASHBOARD_APP_ROOT, "styles.css")); + expect(dashboardVocabularyCssFiles).toContain(path.join(DASHBOARD_APP_ROOT, "public/theme-data.css")); + expect(Array.from(definedProperties)).toEqual(expect.arrayContaining(["--card", "--surface", "--text"])); + expect(violations, [`Undefined CSS custom-property references found in RoadmapsView.css:`, ...violations].join("\n")).toEqual([]); + }); +});