diff --git a/.changeset/fn-8043-text-secondary-token-migration.md b/.changeset/fn-8043-text-secondary-token-migration.md new file mode 100644 index 0000000000..2cd86a032e --- /dev/null +++ b/.changeset/fn-8043-text-secondary-token-migration.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix dashboard secondary text labels rendering an unintended color from an undefined CSS token. +category: fix +dev: Migrate var(--text-secondary) to var(--text-muted) in four dashboard component stylesheets (FN-8043). diff --git a/packages/dashboard/app/__tests__/deprecated-text-secondary-token-migration.test.ts b/packages/dashboard/app/__tests__/deprecated-text-secondary-token-migration.test.ts new file mode 100644 index 0000000000..830c3e94ab --- /dev/null +++ b/packages/dashboard/app/__tests__/deprecated-text-secondary-token-migration.test.ts @@ -0,0 +1,103 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { loadStylesCss } from "../test/cssFixture"; + +const componentsDir = resolve(__dirname, "../components"); + +const migratedRules = [ + { file: "GrokCliProviderCard.css", selector: ".grok-cli-binary-path-label" }, + { file: "OmpCliProviderCard.css", selector: ".omp-cli-binary-path-label" }, + { file: "CursorCliProviderCard.css", selector: ".cursor-cli-binary-path-label" }, + { + file: "WorkflowNodeEditor.css", + selector: ".wf-lifecycle-warning-code, .wf-lifecycle-warning-node", + }, +]; + +function stripCssComments(css: string): string { + return css.replace(/\/\*[\s\S]*?\*\//g, ""); +} + +function extractBlockAt(css: string, startIdx: number): string { + const openBraceIdx = startIdx + css.slice(startIdx).indexOf("{"); + let depth = 1; + + for (let index = openBraceIdx + 1; index < css.length; index++) { + if (css[index] === "{") depth++; + if (css[index] === "}") depth--; + if (depth === 0) return css.slice(startIdx, index + 1); + } + + throw new Error(`Could not find closing brace for block at ${startIdx}`); +} + +function extractRootBlock(css: string): string { + const rootRegex = /:root\s*\{/g; + let match: RegExpExecArray | null; + let rootCount = 0; + + while ((match = rootRegex.exec(css)) !== null) { + rootCount++; + if (rootCount === 2) return extractBlockAt(css, match.index); + } + + throw new Error("Could not find canonical base :root block"); +} + +function extractLightThemeBlock(css: string): string { + const match = css.match(/:root\[data-theme="light"\]\s*\{/); + if (!match) throw new Error("Could not find :root[data-theme=\"light\"] block"); + return extractBlockAt(css, match.index!); +} + +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(); +} + +function extractSelectorBlock(css: string, selector: string): string { + const selectorPattern = selector + .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + .replace(/\s+/g, "\\s+"); + const match = css.match(new RegExp(`${selectorPattern}\\s*\\{`)); + if (!match) throw new Error(`Could not find ${selector} rule`); + return extractBlockAt(css, match.index!); +} + +/* +FNXC:DashboardTextTokens 2026-07-15-00:00: +FN-8043 requires these four secondary-text rules to use canonical --text-muted, never the undefined +--text-secondary alias. Strip CSS comments before scanning so required migration rationale comments +can name the deprecated alias without violating the executable token-use invariant; require the +replacement token in both base and light theme blocks so it resolves on both supported theme paths. +*/ +describe("deprecated secondary dashboard text token migration", () => { + it("keeps the undefined secondary alias out of uncommented migrated CSS", () => { + const offenders = migratedRules.flatMap(({ file }) => { + const css = stripCssComments(readFileSync(resolve(componentsDir, file), "utf8")); + return /--text-secondary\b/.test(css) ? [file] : []; + }); + + expect(offenders, `Unexpected deprecated --text-secondary usage in: ${offenders.join(", ")}`).toEqual([]); + }); + + it("uses the canonical muted token in each migrated secondary-text rule", () => { + for (const { file, selector } of migratedRules) { + const css = readFileSync(resolve(componentsDir, file), "utf8"); + const rule = extractSelectorBlock(css, selector); + expect(rule, `${file} ${selector} must use --text-muted`).toMatch(/color:\s*var\(--text-muted\);/); + } + }); + + it("defines the muted replacement token in base and light theme blocks", () => { + const css = loadStylesCss(); + const baseDefinition = extractTokenDefinition(extractRootBlock(css), "--text-muted"); + const lightDefinition = extractTokenDefinition(extractLightThemeBlock(css), "--text-muted"); + + expect(baseDefinition, "--text-muted must be defined in the base :root theme").not.toBe(""); + expect(lightDefinition, "--text-muted must be defined in :root[data-theme=\"light\"]").not.toBe(""); + }); +}); diff --git a/packages/dashboard/app/components/CursorCliProviderCard.css b/packages/dashboard/app/components/CursorCliProviderCard.css index 3543b37d5d..4d70380ab7 100644 --- a/packages/dashboard/app/components/CursorCliProviderCard.css +++ b/packages/dashboard/app/components/CursorCliProviderCard.css @@ -25,8 +25,12 @@ binary-path control render flush against the card's left/right/bottom edges. See margin-top: var(--space-sm); } +/* +FNXC:CursorCli 2026-07-15-00:00: +FN-8043 uses --text-muted for secondary labels because --text-secondary is an undefined deprecated alias in both themes. +*/ .cursor-cli-binary-path-label { - color: var(--text-secondary); + color: var(--text-muted); font-size: 0.78rem; font-weight: 600; } diff --git a/packages/dashboard/app/components/GrokCliProviderCard.css b/packages/dashboard/app/components/GrokCliProviderCard.css index f6f595d97b..473294d325 100644 --- a/packages/dashboard/app/components/GrokCliProviderCard.css +++ b/packages/dashboard/app/components/GrokCliProviderCard.css @@ -34,8 +34,12 @@ notch quieter than the primary status line via reduced opacity on the shared `.s margin-top: var(--space-sm); } +/* +FNXC:GrokCli 2026-07-15-00:00: +FN-8043 uses --text-muted for secondary labels because --text-secondary is an undefined deprecated alias in both themes. +*/ .grok-cli-binary-path-label { - color: var(--text-secondary); + color: var(--text-muted); font-size: 0.78rem; font-weight: 600; } diff --git a/packages/dashboard/app/components/OmpCliProviderCard.css b/packages/dashboard/app/components/OmpCliProviderCard.css index c43102d4c5..0201c2607a 100644 --- a/packages/dashboard/app/components/OmpCliProviderCard.css +++ b/packages/dashboard/app/components/OmpCliProviderCard.css @@ -26,8 +26,12 @@ Compact card body inset to match auth-provider-header horizontal padding (same a margin-top: var(--space-sm); } +/* +FNXC:OmpAcp 2026-07-15-00:00: +FN-8043 uses --text-muted for secondary labels because --text-secondary is an undefined deprecated alias in both themes. +*/ .omp-cli-binary-path-label { - color: var(--text-secondary); + color: var(--text-muted); font-size: 0.78rem; font-weight: 600; } diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index 4230aec92a..ab931e3843 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -1742,12 +1742,16 @@ One-click fix affordances: a quiet pill per fixable warning row plus a line-height: 1.35; } +/* +FNXC:WorkflowNodeEditor 2026-07-15-00:00: +FN-8043 uses --text-muted for lifecycle warning chips because --text-secondary is an undefined deprecated alias in both themes. +*/ .wf-lifecycle-warning-code, .wf-lifecycle-warning-node { padding: 1px 5px; border-radius: var(--radius-sm); background: var(--bg-tertiary); - color: var(--text-secondary); + color: var(--text-muted); font-size: 0.7rem; font-family: var(--font-mono, monospace); }