mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Fix Live side-tab validation gaps
Scan Astro style blocks for inset-shadow stripes, recognize semantically chromatic external tokens without flagging neutral unknowns, and make the polling generator run advisory detector checks before publication. Sync the affected detector bundles and add a paired regression fixture.\n\nAI-assisted: Codex analyzed the failed Live task, implemented the detector and generator changes, and ran the validation suites under maintainer direction.
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user