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:
Paul Bakaus
2026-07-15 16:23:49 -07:00
parent 0ac1ca6867
commit 8682c85c57
47 changed files with 1187 additions and 284 deletions
@@ -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;
+26 -8
View File
@@ -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;
+26 -4
View File
@@ -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) {
+26 -8
View File
@@ -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;
+3 -2
View File
@@ -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'));
+15
View File
@@ -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>