fix(01-design-tokens-rams-foundation): WR-03 audit named colors, modern color functions, text-shadow
This commit is contained in:
+87
-3
@@ -57,9 +57,12 @@
|
|||||||
* minified >=1e30px form) is sanctioned; every other non-zero
|
* minified >=1e30px form) is sanctioned; every other non-zero
|
||||||
* radius — including 50% — is flagged.
|
* radius — including 50% — is flagged.
|
||||||
*
|
*
|
||||||
* 5. rgb()/rgba()/hsl()/hsl() literals are checked too (hex is not
|
* 5. rgb()/rgba()/hsl()/hsla() literals are checked too (hex is not
|
||||||
* the only escape hatch for off-token colors). var() references
|
* the only escape hatch for off-token colors) — as are named color
|
||||||
* are always token paths and pass by construction.
|
* keywords (on color-bearing properties; currentcolor/inherit/
|
||||||
|
* transparent stay sanctioned) and modern color functions —
|
||||||
|
* oklch()/oklab()/lab()/lch()/hwb()/color()/color-mix(). var()
|
||||||
|
* references are always token paths and pass by construction.
|
||||||
*
|
*
|
||||||
* Reuse (D-14 lineage): later UI phases extend IMPLEMENTATION_PAGES
|
* Reuse (D-14 lineage): later UI phases extend IMPLEMENTATION_PAGES
|
||||||
* with their pages and keep running `npm run audit` in CI/local
|
* with their pages and keep running `npm run audit` in CI/local
|
||||||
@@ -361,6 +364,84 @@ function splitDecl(text) {
|
|||||||
|
|
||||||
/* ————— declaration checks ————— */
|
/* ————— declaration checks ————— */
|
||||||
|
|
||||||
|
/* CSS named colors (the full CSS Color keyword list, WR-03): a named
|
||||||
|
* keyword on a color-bearing property is an off-token color — the
|
||||||
|
* palette lives in tokens.css (D-07). currentcolor/transparent and
|
||||||
|
* the css-wide keywords (inherit/initial/unset/revert) are not in
|
||||||
|
* the list and stay sanctioned. */
|
||||||
|
const NAMED_COLORS = new Set([
|
||||||
|
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige',
|
||||||
|
'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown',
|
||||||
|
'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral',
|
||||||
|
'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan',
|
||||||
|
'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki',
|
||||||
|
'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
|
||||||
|
'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray',
|
||||||
|
'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue',
|
||||||
|
'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite',
|
||||||
|
'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod',
|
||||||
|
'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred',
|
||||||
|
'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen',
|
||||||
|
'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
|
||||||
|
'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink',
|
||||||
|
'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray',
|
||||||
|
'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen',
|
||||||
|
'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue',
|
||||||
|
'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue',
|
||||||
|
'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue',
|
||||||
|
'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace',
|
||||||
|
'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod',
|
||||||
|
'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff',
|
||||||
|
'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red',
|
||||||
|
'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown',
|
||||||
|
'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue',
|
||||||
|
'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan',
|
||||||
|
'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white',
|
||||||
|
'whitesmoke', 'yellow', 'yellowgreen',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* Color-bearing properties, shorthands included (WR-03): a bare named
|
||||||
|
* keyword in any of these values is a color choice. box-shadow and
|
||||||
|
* text-shadow are not listed — any non-none value is already forbidden
|
||||||
|
* outright by shadowViolations. */
|
||||||
|
const COLORISH_PROPS = new Set([
|
||||||
|
'color', 'background', 'background-color', 'background-image', 'fill',
|
||||||
|
'stroke', 'caret-color', 'outline', 'outline-color', 'border',
|
||||||
|
'border-top', 'border-right', 'border-bottom', 'border-left',
|
||||||
|
'border-x', 'border-y', 'border-block-start', 'border-block-end',
|
||||||
|
'border-inline-start', 'border-inline-end', 'border-color',
|
||||||
|
'border-top-color', 'border-right-color', 'border-bottom-color',
|
||||||
|
'border-left-color', 'border-block-color', 'border-block-start-color',
|
||||||
|
'border-block-end-color', 'border-inline-color',
|
||||||
|
'border-inline-start-color', 'border-inline-end-color',
|
||||||
|
'text-decoration', 'text-decoration-color', 'accent-color',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function namedColorViolations(prop, value) {
|
||||||
|
if (!COLORISH_PROPS.has(prop)) return [];
|
||||||
|
const bad = [];
|
||||||
|
for (const part of splitTopLevel(value, ',')) {
|
||||||
|
for (const tok of splitTopLevel(part, ' ')) {
|
||||||
|
const t = tok.trim().toLowerCase().replace(/[(),]+$/g, '');
|
||||||
|
if (NAMED_COLORS.has(t)) bad.push(`named color keyword ${tok.trim()} — use var(--color-*) (Rams palette, D-07)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern color functions (CSS Color 4, WR-03): unambiguous color
|
||||||
|
* literals on any property — the same surface the hex check covers.
|
||||||
|
* var()-bearing values pass by construction (Tailwind's own color-mix
|
||||||
|
* emissions are var-driven). */
|
||||||
|
function colorFunctionViolations(value) {
|
||||||
|
if (/var\(/i.test(value)) return [];
|
||||||
|
const bad = [];
|
||||||
|
for (const m of value.matchAll(/\b(?:oklch|oklab|lab|lch|hwb|color-mix|color)\(/gi)) {
|
||||||
|
bad.push(`off-token color function ${m[0].slice(0, -1)}(…) — color values derive from var(--color-*) (Rams palette, D-07)`);
|
||||||
|
}
|
||||||
|
return bad;
|
||||||
|
}
|
||||||
|
|
||||||
function literalHexViolations(value, tokens) {
|
function literalHexViolations(value, tokens) {
|
||||||
const found = [];
|
const found = [];
|
||||||
const hexRe = /#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])/g;
|
const hexRe = /#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])/g;
|
||||||
@@ -422,6 +503,7 @@ function radiusAllowed(value) {
|
|||||||
function shadowViolations(prop, value) {
|
function shadowViolations(prop, value) {
|
||||||
const v = value.trim().toLowerCase();
|
const v = value.trim().toLowerCase();
|
||||||
if (prop === 'box-shadow') return v === 'none' ? [] : ['box-shadow forbidden (Rams Shapes: no shadows)'];
|
if (prop === 'box-shadow') return v === 'none' ? [] : ['box-shadow forbidden (Rams Shapes: no shadows)'];
|
||||||
|
if (prop === 'text-shadow') return v === 'none' ? [] : ['text-shadow forbidden (Rams Shapes: no shadows)'];
|
||||||
if (prop === 'backdrop-filter') return v === 'none' ? [] : ['backdrop-filter forbidden (Rams Shapes: no glass)'];
|
if (prop === 'backdrop-filter') return v === 'none' ? [] : ['backdrop-filter forbidden (Rams Shapes: no glass)'];
|
||||||
if (prop === 'filter') {
|
if (prop === 'filter') {
|
||||||
return /blur\(|drop-shadow\(|var\(--tw-blur|var\(--tw-drop-shadow/i.test(value)
|
return /blur\(|drop-shadow\(|var\(--tw-blur|var\(--tw-drop-shadow/i.test(value)
|
||||||
@@ -437,7 +519,9 @@ function checkDeclaration(prop, value, tokens) {
|
|||||||
const violations = [];
|
const violations = [];
|
||||||
if (prop.startsWith('--tw-')) return violations; // framework plumbing registration
|
if (prop.startsWith('--tw-')) return violations; // framework plumbing registration
|
||||||
violations.push(...literalHexViolations(value, tokens));
|
violations.push(...literalHexViolations(value, tokens));
|
||||||
|
violations.push(...colorFunctionViolations(value));
|
||||||
if (prop.startsWith('--')) return violations; // custom property: color literals above are the checkable surface
|
if (prop.startsWith('--')) return violations; // custom property: color literals above are the checkable surface
|
||||||
|
violations.push(...namedColorViolations(prop, value));
|
||||||
violations.push(...shadowViolations(prop, value));
|
violations.push(...shadowViolations(prop, value));
|
||||||
if (prop === 'font') {
|
if (prop === 'font') {
|
||||||
/* WR-02: a literal font: shorthand embeds unvalidated weight/
|
/* WR-02: a literal font: shorthand embeds unvalidated weight/
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
* family (convention: font: var(--type-*)); var()
|
* family (convention: font: var(--type-*)); var()
|
||||||
* shorthands pass; familiesFromShorthand extracts
|
* shorthands pass; familiesFromShorthand extracts
|
||||||
* the family for the message → flagged (exit 1)
|
* the family for the message → flagged (exit 1)
|
||||||
|
* T11 named color keywords, oklch()/oklab()/lab()/lch()/
|
||||||
|
* hwb()/color()/color-mix(), text-shadow → flagged
|
||||||
|
* (exit 1); currentcolor/inherit/transparent stay
|
||||||
|
* sanctioned
|
||||||
*/
|
*/
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||||
@@ -179,6 +183,23 @@ writeFileSync(fontShorthand, `/* literal font: shorthand — flagged regardless
|
|||||||
.c-font-shorthand-var { font: var(--type-body-md); }
|
.c-font-shorthand-var { font: var(--type-body-md); }
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
/* T11 — color-evasion gaps (WR-03): named keywords, modern color
|
||||||
|
* functions, text-shadow — all previously passed the audit unflagged. */
|
||||||
|
const colorEvasions = join(FIX, 'color-evasions.css');
|
||||||
|
writeFileSync(colorEvasions, `/* every flagged declaration must be listed; sanctioned keywords pass */
|
||||||
|
.k-named { color: red; }
|
||||||
|
.l-oklch { background: oklch(0.6 0.2 30); }
|
||||||
|
.m-colorfn { fill: color(display-p3 1 0 0); }
|
||||||
|
.n-hwb { stroke: hwb(90 10% 0); }
|
||||||
|
.o-lab { border-color: lab(50% 40 60); }
|
||||||
|
.p-lch { background-color: lch(50% 40 60); }
|
||||||
|
.q-text-shadow { text-shadow: 0 1px 0 red; }
|
||||||
|
/* sanctioned: css-wide + currentcolor/transparent stay clean */
|
||||||
|
.r-ok { color: currentcolor; }
|
||||||
|
.s-ok { background: transparent; }
|
||||||
|
.t-ok { outline: 2px solid inherit; }
|
||||||
|
`);
|
||||||
|
|
||||||
/* ————— tests ————— */
|
/* ————— tests ————— */
|
||||||
|
|
||||||
// T1 — clean stylesheet exits 0
|
// T1 — clean stylesheet exits 0
|
||||||
@@ -319,6 +340,21 @@ writeFileSync(fontShorthand, `/* literal font: shorthand — flagged regardless
|
|||||||
`out=${r.out.slice(0, 300)}`);
|
`out=${r.out.slice(0, 300)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T11 — named colors, modern color functions, text-shadow flagged
|
||||||
|
{
|
||||||
|
const r = runAudit([colorEvasions]);
|
||||||
|
const markers = ['color: red', 'oklch(0.6 0.2 30)', 'color(display-p3 1 0 0)',
|
||||||
|
'hwb(90 10% 0)', 'lab(50% 40 60)', 'lch(50% 40 60)', 'text-shadow: 0 1px 0 red'];
|
||||||
|
const missing = markers.filter((m) => !r.out.includes(m));
|
||||||
|
check('T11 color evasions → exit 1', r.status === 1,
|
||||||
|
`status=${r.status} error=${r.error}`);
|
||||||
|
check('T11 every evasion listed', missing.length === 0,
|
||||||
|
`missing markers: ${missing.join(', ')}`);
|
||||||
|
check('T11 sanctioned keywords pass (currentcolor/transparent/inherit)',
|
||||||
|
!/currentcolor/.test(r.out) && !/transparent/.test(r.out) && !/outline: 2px solid inherit/.test(r.out),
|
||||||
|
`out=${r.out.slice(0, 300)}`);
|
||||||
|
}
|
||||||
|
|
||||||
/* ————— report ————— */
|
/* ————— report ————— */
|
||||||
console.log(`audit.test: ${passes.length} passed, ${failures.length} failed`);
|
console.log(`audit.test: ${passes.length} passed, ${failures.length} failed`);
|
||||||
passes.forEach((p) => console.log(` PASS ${p}`));
|
passes.forEach((p) => console.log(` PASS ${p}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user