mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Fix Live side-tab validation gaps
Scan Astro style blocks for inset-shadow stripes, recognize semantically chromatic external tokens without flagging neutral unknowns, and make the polling generator run advisory detector checks before publication. Sync the affected detector bundles and add a paired regression fixture.\n\nAI-assisted: Codex analyzed the failed Live task, implemented the detector and generator changes, and ran the validation suites under maintainer direction.
This commit is contained in:
@@ -25,6 +25,7 @@ Do not request the full Live reference or repeat broad project discovery. Use th
|
||||
- Preserve the existing component contract, semantic tag, links, accessibility relationships, and functional descendants.
|
||||
- Reuse existing components, CSS custom properties, typography, spacing, radii, and color roles. Never invent raw colors or foreign fonts when tokens exist.
|
||||
- Do not add gradients, blur, glow, glass, neon, decorative shadows, emoji, or unrelated content unless the explicit user direction requires it.
|
||||
- Never decorate a card, label, row, tab, or container with a colored stripe on only one edge. This includes borders, inset box-shadows, gradients, and pseudo-elements; selection and focus indicators are the only exception.
|
||||
- Produce the requested number of materially different directions through hierarchy, layout, density, or existing color-role allocation. CSS-only no-ops and source-identical variants are invalid.
|
||||
- Keep temporary Live markers and preview CSS out of accepted project truth; the publisher/Accept pipeline owns cleanup.
|
||||
|
||||
@@ -34,10 +35,10 @@ Do not request the full Live reference or repeat broad project discovery. Use th
|
||||
2. If annotations exist, read the screenshot before designing. Treat pins and strokes as semantic constraints.
|
||||
3. Name all directions and their parameter axes before writing so the set stays coherent. Parameters are lazy: revision 1 carries no parameter manifest.
|
||||
4. Prepare revision 1 with `live-publish.mjs --prepare --id EVENT_ID --file SOURCE_FILE`. Edit only the returned artifact (or isolated component directory), never live project source.
|
||||
5. Write one complete, valid first variant plus only its CSS. Publish it immediately with the returned epoch, artifact path, expected source hash, `--arrived 1`, and the requested `--expected` count.
|
||||
5. Write one complete, valid first variant plus only its CSS. Run `detect.mjs --json` on the staged artifact before publishing. Fix genuine findings; when inspection shows a contextual false positive, use judgment and continue without changing persistent detector configuration. The detector is a review signal, not an automatic publication veto. Publish immediately with the returned epoch, artifact path, expected source hash, `--arrived 1`, and the requested `--expected` count.
|
||||
6. Prepare again from the published prefix, add the remaining validated directions, attach parameter manifests only with the complete set, and publish the largest ready prefix. Preserve every already-published variant byte-for-byte.
|
||||
7. On `stale_generation_epoch`, `source_changed`, or another fence rejection, stop. Do not retry against stale source or leave direct edits behind.
|
||||
8. Verify the final artifact/source parses. Reply exactly once with `live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH`. On a real failure, reply once with `error` and a short reason.
|
||||
8. Verify the final artifact/source parses and run the detector again before the final publication. Apply the same genuine-finding versus contextual-false-positive judgment. Reply exactly once with `live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH`. On a real failure, reply once with `error` and a short reason.
|
||||
|
||||
For Svelte or Vue component preview, write only `vN.svelte` / `vN.vue` in the isolated `componentDir` returned by prepare and update the isolated manifest. Never edit the live component directory. For JSX/TSX source previews, preserve JSX attribute syntax and wrap preview CSS as required by `scaffold.cssAuthoring`.
|
||||
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -1264,6 +1264,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -1273,6 +1278,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -1674,16 +1686,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
||||
import { isNeutralColor } from '../../shared/color.mjs';
|
||||
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
||||
import { checkSourceDesignSystem } from '../../design-system.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { scanCssTextForGlow, scanCssTextForInsetStripe, scanCssTextForMarquee, scanCssTextForRadialHalo } from '../../rules/checks.mjs';
|
||||
import { isFullPage } from '../../shared/page.mjs';
|
||||
import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
|
||||
import { finding } from '../../findings.mjs';
|
||||
@@ -340,12 +340,12 @@ const REGEX_ANALYZERS = [
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Style block extraction (Vue/Svelte <style> blocks)
|
||||
// Style block extraction (Astro/Vue/Svelte <style> blocks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function extractStyleBlocks(content, ext) {
|
||||
ext = ext.toLowerCase();
|
||||
if (ext !== '.vue' && ext !== '.svelte') return [];
|
||||
if (ext !== '.astro' && ext !== '.vue' && ext !== '.svelte') return [];
|
||||
const blocks = [];
|
||||
const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
||||
let m;
|
||||
@@ -472,8 +472,16 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'source',
|
||||
}));
|
||||
if (cssLike.has(ext)) {
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'source',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(content).map(hit => finding(hit.id, filePath, hit.snippet))));
|
||||
}
|
||||
|
||||
// Extract and scan <style> blocks from Vue/Svelte SFCs
|
||||
// Extract and scan <style> blocks from Astro/Vue/Svelte components.
|
||||
const styleBlocks = profile
|
||||
? profileStep(profile, {
|
||||
engine: 'regex',
|
||||
@@ -488,6 +496,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'style-block',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'style-block',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
// Extract and scan CSS-in-JS template literals
|
||||
@@ -505,6 +520,13 @@ function detectText(content, filePath, options = {}) {
|
||||
profile,
|
||||
phase: 'css-in-js',
|
||||
}));
|
||||
findings.push(...profileFindings(profile, {
|
||||
engine: 'regex',
|
||||
phase: 'css-in-js',
|
||||
ruleId: 'side-tab',
|
||||
target: filePath,
|
||||
}, () => scanCssTextForInsetStripe(block.content)
|
||||
.map(hit => finding(hit.id, filePath, hit.snippet, block.startLine))));
|
||||
}
|
||||
|
||||
if (options?.designSystem) {
|
||||
|
||||
@@ -475,6 +475,11 @@ function findShadowColor(layer) {
|
||||
if (fn) return { color: parseAnyColor(fn[0]), start: fn.index, end: fn.index + fn[0].length };
|
||||
const hex = layer.match(/#[0-9a-fA-F]{3,8}\b/);
|
||||
if (hex) return { color: parseAnyColor(hex[0]), start: hex.index, end: hex.index + hex[0].length };
|
||||
// Keep unresolved custom properties intact as one color token. Otherwise
|
||||
// names such as `--signal-blue` are accidentally parsed as the named color
|
||||
// `blue`, and digits in names such as `--accent-500` become shadow lengths.
|
||||
const variable = layer.match(/var\([^)]*\)/i);
|
||||
if (variable) return { color: null, start: variable.index, end: variable.index + variable[0].length };
|
||||
const wordRe = /[a-zA-Z][a-zA-Z]*/g;
|
||||
let m;
|
||||
while ((m = wordRe.exec(layer)) !== null) {
|
||||
@@ -484,6 +489,13 @@ function findShadowColor(layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const CHROMATIC_CUSTOM_PROPERTY_HINT_RE = /(?:^|-)(?:accent|kinpaku|patina|gold|red|orange|amber|yellow|lime|green|emerald|teal|cyan|blue|indigo|violet|purple|magenta|pink|rose|coral|aqua|mint|burgundy|crimson|scarlet)(?:-|$)/i;
|
||||
|
||||
function unresolvedShadowTokenLooksChromatic(layer) {
|
||||
const variable = layer.match(/var\(\s*(--[\w-]+)/i);
|
||||
return variable ? CHROMATIC_CUSTOM_PROPERTY_HINT_RE.test(variable[1]) : false;
|
||||
}
|
||||
|
||||
// Extract the length values of a shadow layer in declaration order, with the
|
||||
// color token removed so its components aren't misread as lengths. Handles
|
||||
// computed-style px values AND authored unitless zeros ("0 0 20px"); rem/em
|
||||
@@ -885,16 +897,22 @@ function scanCssTextForInsetStripe(content) {
|
||||
// menu items — are wider or leave width to layout.
|
||||
const declaredWidth = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', customProps));
|
||||
if (declaredWidth != null && declaredWidth <= 40) continue;
|
||||
const value = resolveVarRefs(shadow, customProps);
|
||||
for (const layer of value.split(/,(?![^(]*\))/)) {
|
||||
for (const authoredLayer of shadow.split(/,(?![^(]*\))/)) {
|
||||
const layer = resolveVarRefs(authoredLayer, customProps);
|
||||
if (!/\binset\b/i.test(layer)) continue;
|
||||
const colorInfo = findShadowColor(layer);
|
||||
// Unresolvable colors (currentColor, external vars): don't guess.
|
||||
if (!colorInfo || !colorInfo.color) continue;
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
if (!colorInfo) continue;
|
||||
if (colorInfo.color) {
|
||||
const c = colorInfo.color;
|
||||
if ((c.a ?? 1) < 0.1) continue;
|
||||
const chroma = Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b);
|
||||
if (chroma < 30) continue;
|
||||
} else if (!unresolvedShadowTokenLooksChromatic(authoredLayer)) {
|
||||
// External custom properties are unknowable in an isolated artifact.
|
||||
// Only explicit accent or hue semantics justify treating one as
|
||||
// chromatic; neutral shadow/divider tokens and currentColor stay legal.
|
||||
continue;
|
||||
}
|
||||
const vals = extractShadowLengths(layer, colorInfo.start, colorInfo.end);
|
||||
const x = vals[0] || 0, y = vals[1] || 0, blur = vals[2] || 0, sp = vals[3] || 0;
|
||||
if (blur !== 0 || sp !== 0) continue;
|
||||
|
||||
@@ -35,6 +35,7 @@ Do not request the full Live reference or repeat broad project discovery. Use th
|
||||
- Preserve the existing component contract, semantic tag, links, accessibility relationships, and functional descendants.
|
||||
- Reuse existing components, CSS custom properties, typography, spacing, radii, and color roles. Never invent raw colors or foreign fonts when tokens exist.
|
||||
- Do not add gradients, blur, glow, glass, neon, decorative shadows, emoji, or unrelated content unless the explicit user direction requires it.
|
||||
- Never decorate a card, label, row, tab, or container with a colored stripe on only one edge. This includes borders, inset box-shadows, gradients, and pseudo-elements; selection and focus indicators are the only exception.
|
||||
- Produce the requested number of materially different directions through hierarchy, layout, density, or existing color-role allocation. CSS-only no-ops and source-identical variants are invalid.
|
||||
- Keep temporary Live markers and preview CSS out of accepted project truth; the publisher/Accept pipeline owns cleanup.
|
||||
|
||||
@@ -44,10 +45,10 @@ Do not request the full Live reference or repeat broad project discovery. Use th
|
||||
2. If annotations exist, read the screenshot before designing. Treat pins and strokes as semantic constraints.
|
||||
3. Name all directions and their parameter axes before writing so the set stays coherent. Parameters are lazy: revision 1 carries no parameter manifest.
|
||||
4. Prepare revision 1 with `live-publish.mjs --prepare --id EVENT_ID --file SOURCE_FILE`. Edit only the returned artifact (or isolated component directory), never live project source.
|
||||
5. Write one complete, valid first variant plus only its CSS. Publish it immediately with the returned epoch, artifact path, expected source hash, `--arrived 1`, and the requested `--expected` count.
|
||||
5. Write one complete, valid first variant plus only its CSS. Run `detect.mjs --json` on the staged artifact before publishing. Fix genuine findings; when inspection shows a contextual false positive, use judgment and continue without changing persistent detector configuration. The detector is a review signal, not an automatic publication veto. Publish immediately with the returned epoch, artifact path, expected source hash, `--arrived 1`, and the requested `--expected` count.
|
||||
6. Prepare again from the published prefix, add the remaining validated directions, attach parameter manifests only with the complete set, and publish the largest ready prefix. Preserve every already-published variant byte-for-byte.
|
||||
7. On `stale_generation_epoch`, `source_changed`, or another fence rejection, stop. Do not retry against stale source or leave direct edits behind.
|
||||
8. Verify the final artifact/source parses. Reply exactly once with `live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH`. On a real failure, reply once with `error` and a short reason.
|
||||
8. Verify the final artifact/source parses and run the detector again before the final publication. Apply the same genuine-finding versus contextual-false-positive judgment. Reply exactly once with `live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH`. On a real failure, reply once with `error` and a short reason.
|
||||
|
||||
For Svelte or Vue component preview, write only `vN.svelte` / `vN.vue` in the isolated `componentDir` returned by prepare and update the isolated manifest. Never edit the live component directory. For JSX/TSX source previews, preserve JSX attribute syntax and wrap preview CSS as required by `scaffold.cssAuthoring`.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
@@ -17,6 +18,38 @@ import {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const FIXTURES = path.join(__dirname, 'fixtures', 'antipatterns');
|
||||
|
||||
describe('detectText - Astro structural CSS fixtures', () => {
|
||||
const SHOULD_FLAG = [
|
||||
'Kinpaku Edge',
|
||||
'Patina Edge',
|
||||
'Accent Edge',
|
||||
'Signal Blue Edge',
|
||||
];
|
||||
const SHOULD_PASS = [
|
||||
'Neutral Shadow Token',
|
||||
'Current Color Edge',
|
||||
'Selected State Edge',
|
||||
'Hairline Edge',
|
||||
'Thick Fill Edge',
|
||||
'Blurred Edge',
|
||||
'Narrow Artwork',
|
||||
];
|
||||
|
||||
it('Astro style blocks flag unresolved chromatic inset stripes only', () => {
|
||||
const filePath = path.join(FIXTURES, 'astro-inset-shadow-stripe.astro');
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
const findings = detectText(source, filePath).filter(r => r.antipattern === 'side-tab');
|
||||
const snippets = findings.map(r => r.snippet || '').join(' | ');
|
||||
|
||||
for (const heading of SHOULD_FLAG) {
|
||||
assert.match(snippets, new RegExp(`data-case=${JSON.stringify(heading)}`), `expected "${heading}" to flag`);
|
||||
}
|
||||
for (const heading of SHOULD_PASS) {
|
||||
assert.doesNotMatch(snippets, new RegExp(`data-case=${JSON.stringify(heading)}`), `"${heading}" should pass`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectHtml — static HTML/CSS fixtures', () => {
|
||||
it('should-flag: catches border anti-patterns', async () => {
|
||||
const f = await detectHtml(path.join(FIXTURES, 'should-flag.html'));
|
||||
|
||||
@@ -1405,6 +1405,21 @@ describe('inset box-shadow stripe', () => {
|
||||
expect(scanCssTextForInsetStripe(selectedOnly)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('flags semantically chromatic external tokens without guessing neutral tokens', () => {
|
||||
const css = `
|
||||
.kinpaku { box-shadow: inset 3px 0 0 var(--ks-kinpaku-deep); }
|
||||
.patina { box-shadow: inset 3px 0 0 var(--ks-patina-deep); }
|
||||
.accent { box-shadow: inset 3px 0 0 var(--brand-accent); }
|
||||
.signal { box-shadow: inset 3px 0 0 var(--signal-blue); }
|
||||
.neutral { box-shadow: inset 3px 0 0 var(--shadow-color); }
|
||||
.current { box-shadow: inset 3px 0 0 currentColor; }
|
||||
`;
|
||||
const findings = scanCssTextForInsetStripe(css);
|
||||
expect(findings).toHaveLength(4);
|
||||
expect(findings.map(f => f.snippet).join(' ')).not.toContain('.neutral');
|
||||
expect(findings.map(f => f.snippet).join(' ')).not.toContain('.current');
|
||||
});
|
||||
|
||||
test('static tab strip: chromatic border on every tab flags, selected-only underline stays silent', async () => {
|
||||
// All-tabs variant: every tab in the group carries the stripe.
|
||||
await withStaticFixture({
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
const title = 'Astro inset shadow stripe regression';
|
||||
---
|
||||
|
||||
<main>
|
||||
<h1>{title}</h1>
|
||||
|
||||
<section aria-labelledby="should-flag">
|
||||
<h2 id="should-flag">Should flag</h2>
|
||||
<article data-case="Kinpaku Edge"><h3>Kinpaku Edge</h3></article>
|
||||
<article data-case="Patina Edge"><h3>Patina Edge</h3></article>
|
||||
<article data-case="Accent Edge"><h3>Accent Edge</h3></article>
|
||||
<article data-case="Signal Blue Edge"><h3>Signal Blue Edge</h3></article>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="should-pass">
|
||||
<h2 id="should-pass">Should pass</h2>
|
||||
<article data-case="Neutral Shadow Token"><h3>Neutral Shadow Token</h3></article>
|
||||
<article data-case="Current Color Edge"><h3>Current Color Edge</h3></article>
|
||||
<article data-case="Selected State Edge" aria-current="page"><h3>Selected State Edge</h3></article>
|
||||
<article data-case="Hairline Edge"><h3>Hairline Edge</h3></article>
|
||||
<article data-case="Thick Fill Edge"><h3>Thick Fill Edge</h3></article>
|
||||
<article data-case="Blurred Edge"><h3>Blurred Edge</h3></article>
|
||||
<article data-case="Narrow Artwork"><h3>Narrow Artwork</h3></article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style is:inline>
|
||||
[data-case="Kinpaku Edge"] { box-shadow: inset 3px 0 0 var(--ks-kinpaku-deep); }
|
||||
[data-case="Patina Edge"] { box-shadow: inset 3px 0 0 var(--ks-patina-deep); }
|
||||
[data-case="Accent Edge"] { box-shadow: inset -4px 0 0 var(--brand-accent); }
|
||||
[data-case="Signal Blue Edge"] { box-shadow: inset 0 5px 0 var(--signal-blue); }
|
||||
|
||||
[data-case="Neutral Shadow Token"] { box-shadow: inset 3px 0 0 var(--shadow-color); }
|
||||
[data-case="Current Color Edge"] { box-shadow: inset 3px 0 0 currentColor; }
|
||||
[data-case="Selected State Edge"][aria-current="page"] { box-shadow: inset 3px 0 0 var(--brand-accent); }
|
||||
[data-case="Hairline Edge"] { box-shadow: inset 2px 0 0 var(--brand-accent); }
|
||||
[data-case="Thick Fill Edge"] { box-shadow: inset 14px 0 0 var(--brand-accent); }
|
||||
[data-case="Blurred Edge"] { box-shadow: inset 3px 0 5px var(--brand-accent); }
|
||||
[data-case="Narrow Artwork"] { width: 24px; box-shadow: inset 3px 0 0 var(--brand-accent); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user