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:
@@ -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(");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user