FN-6190: replace remaining raw rgba styles with color-mix tokens

Tokenize the last non-token dashboard rgba() styles and guard them with regression coverage.

- replace remaining modal, sidebar, tooltip, dependency dropdown, and refine menu raw rgba() usages in styles.css with color-mix() design tokens
- align overlay, background, and shadow treatments with existing semantic CSS variables instead of hard-coded rgba() values
- add a CSS fixture regression test that bans non-:root raw rgba() usage and verifies the converted selectors stay tokenized

Files changed:
 .../__tests__/styles-css-rgba-tokenization.test.ts | 122 +++++++++++++++++++++
 packages/dashboard/app/styles.css                  |  14 +--
 2 files changed, 129 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-6190

Fusion-Task-Lineage: 478465a7-8ec4-4482-8e1c-478d86c1ac08
This commit is contained in:
gsxdsm
2026-06-10 06:48:56 -07:00
parent f79adafcc1
commit 49fdd8f41f
2 changed files with 129 additions and 7 deletions

View File

@@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import { loadStylesCss } from "../test/cssFixture";
interface SelectorExpectation {
selector: string;
expectedColorMix: string;
}
const convertedSelectors: SelectorExpectation[] = [
{
selector: ".modal-overlay",
expectedColorMix: "color-mix(in srgb, var(--text) 60%, transparent)",
},
{
selector: ".modal-header",
expectedColorMix: "color-mix(in srgb, var(--text) 10%, transparent)",
},
{
selector: ".modal-actions",
expectedColorMix: "color-mix(in srgb, var(--text) 5%, transparent)",
},
{
selector: ".settings-sidebar",
expectedColorMix: "color-mix(in srgb, var(--text) 10%, transparent)",
},
{
selector: ".step-progress-segment[data-tooltip]:hover::after",
expectedColorMix: "color-mix(in srgb, var(--text) 20%, transparent)",
},
{
selector: ".dep-dropdown-item.selected",
expectedColorMix: "color-mix(in srgb, var(--todo) 15%, transparent)",
},
{
selector: ".refine-menu",
expectedColorMix: "color-mix(in srgb, var(--text) 15%, transparent)",
},
];
function braceDelta(line: string): number {
return (line.match(/\{/g) ?? []).length - (line.match(/}/g) ?? []).length;
}
function isTokenDefinitionBlockStart(line: string): boolean {
return /^\s*:root(?:\[data-theme=[^\]]+\])?\s*\{/.test(line);
}
function nonTokenRgbaLines(css: string): string[] {
const violations: string[] = [];
const lines = css.split(/\r?\n/);
let tokenBlockDepth = 0;
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const entersTokenBlock = tokenBlockDepth === 0 && isTokenDefinitionBlockStart(line);
if (entersTokenBlock) {
tokenBlockDepth = braceDelta(line);
}
const insideTokenBlock = tokenBlockDepth > 0 || entersTokenBlock;
if (!insideTokenBlock && line.includes("rgba(")) {
violations.push(`${index + 1}:${line.trim()}`);
}
if (!entersTokenBlock && tokenBlockDepth > 0) {
tokenBlockDepth += braceDelta(line);
}
}
return violations;
}
function extractSelectorBlocks(css: string, selector: string): string[] {
const blocks: string[] = [];
let searchStart = 0;
while (searchStart < css.length) {
const selectorStart = css.indexOf(`${selector} {`, searchStart);
if (selectorStart === -1) break;
const blockStart = css.indexOf("{", selectorStart);
let depth = 0;
for (let index = blockStart; index < css.length; index += 1) {
if (css[index] === "{") {
depth += 1;
} else if (css[index] === "}") {
depth -= 1;
if (depth === 0) {
blocks.push(css.slice(selectorStart, index + 1));
searchStart = index + 1;
break;
}
}
}
if (blockStart === -1 || searchStart <= selectorStart) {
throw new Error(`Unterminated block for selector ${selector}`);
}
}
return blocks;
}
describe("styles.css rgba tokenization", () => {
it("does not use raw rgba() outside :root token definition blocks", () => {
expect(nonTokenRgbaLines(loadStylesCss())).toEqual([]);
});
it("keeps converted selectors on tokenized color-mix() values", () => {
const css = loadStylesCss();
for (const { selector, expectedColorMix } of convertedSelectors) {
const blocks = extractSelectorBlocks(css, selector);
expect(blocks, `Missing selector ${selector}`).not.toHaveLength(0);
const matchingBlock = blocks.find((block) => block.includes(expectedColorMix));
expect(matchingBlock, `Missing expected tokenized value for ${selector}`).toBeDefined();
expect(matchingBlock).toContain("color-mix(in srgb, var(--");
expect(matchingBlock).not.toContain("rgba(");
}
});
});

View File

@@ -1130,7 +1130,7 @@ body {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
background: color-mix(in srgb, var(--text) 60%, transparent);
backdrop-filter: blur(4px);
/* Must remain above sticky top banners (e.g. onboarding/session resume cards). */
z-index: 100;
@@ -1166,7 +1166,7 @@ body {
align-items: center;
padding: var(--modal-padding);
border-bottom: 1px solid var(--border);
background: rgba(0, 0, 0, 0.1);
background: color-mix(in srgb, var(--text) 10%, transparent);
}
.modal-header h3 {
font-size: 15px;
@@ -1220,7 +1220,7 @@ body {
gap: 10px;
padding: var(--modal-padding);
border-top: 1px solid var(--border);
background: rgba(0, 0, 0, 0.05);
background: color-mix(in srgb, var(--text) 5%, transparent);
}
.modal-actions-left {
@@ -1511,7 +1511,7 @@ input[type="range"]:focus-visible {
overflow-y: auto;
padding: 10px 8px;
gap: 2px;
background: rgba(0, 0, 0, 0.1);
background: color-mix(in srgb, var(--text) 10%, transparent);
scrollbar-color: var(--border) transparent;
scrollbar-width: thin;
}
@@ -2548,7 +2548,7 @@ input[type="range"]:focus-visible {
z-index: 10;
pointer-events: none;
margin-bottom: var(--space-xs);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 8px color-mix(in srgb, var(--text) 20%, transparent);
}
.step-progress-label {
@@ -2858,7 +2858,7 @@ input[type="range"]:focus-visible {
background: var(--card-hover);
}
.dep-dropdown-item.selected {
background: rgba(88, 166, 255, 0.15);
background: color-mix(in srgb, var(--todo) 15%, transparent);
}
.dep-dropdown-id {
@@ -2888,7 +2888,7 @@ input[type="range"]:focus-visible {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px color-mix(in srgb, var(--text) 15%, transparent);
min-width: 200px;
max-width: 280px;
z-index: 100;