mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
Fix glow/gradient detection, fix test performance (280s -> 5s)
Detection improvements: - Remove SAFE_TAGS from glow check (buttons/links with glows are valid) - Add gradient color parsing (parseGradientColors) for AI palette detection on gradient backgrounds including buttons - Detect cyan neon text on dark backgrounds as AI palette - Resolve gradient backgrounds as dark for glow detection - Fix pure-black false positive on semi-transparent overlays (a >= 0.9) - Skip low-contrast/gray-on-color when background is a gradient - Fix "Only font:" double-colon in browser labels Test performance: - Split jsdom fixture tests to Node's test runner (bun + jsdom hangs after ~13 instances due to resource leak) - bun test for unit/regex/CLI tests (94 tests, 4s) - node --test for jsdom fixtures (15 tests, 1.3s) - Total: 109 tests in ~5s (was 280s+) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0574bd0be2
commit
4d2ef64935
@@ -244,8 +244,8 @@ function checkColors(opts) {
|
||||
if (SAFE_TAGS.has(tag)) return [];
|
||||
const findings = [];
|
||||
|
||||
// Pure black background
|
||||
if (bgColor && bgColor.a > 0.1 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) {
|
||||
// Pure black background (only solid or near-solid, not semi-transparent overlays)
|
||||
if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) {
|
||||
findings.push({ id: 'pure-black-white', snippet: '#000000 background' });
|
||||
}
|
||||
|
||||
@@ -363,8 +363,7 @@ function checkMotion(opts) {
|
||||
}
|
||||
|
||||
function checkGlow(opts) {
|
||||
const { tag, boxShadow, effectiveBg } = opts;
|
||||
if (SAFE_TAGS.has(tag)) return [];
|
||||
const { boxShadow, effectiveBg } = opts;
|
||||
if (!boxShadow || boxShadow === 'none') return [];
|
||||
if (!effectiveBg) return [];
|
||||
|
||||
@@ -626,29 +625,84 @@ function checkElementMotionDOM(el) {
|
||||
|
||||
function checkElementGlowDOM(el) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (SAFE_TAGS.has(tag)) return [];
|
||||
const style = getComputedStyle(el);
|
||||
if (!style.boxShadow || style.boxShadow === 'none') return [];
|
||||
// Use parent's background — glow radiates outward, so the surrounding context matters
|
||||
const parentBg = el.parentElement ? resolveBackground(el.parentElement) : resolveBackground(el);
|
||||
// If resolveBackground returns null (gradient), try to infer from the gradient colors
|
||||
let parentBg = el.parentElement ? resolveBackground(el.parentElement) : resolveBackground(el);
|
||||
if (!parentBg) {
|
||||
// Gradient background — sample its colors to determine if it's dark
|
||||
let cur = el.parentElement;
|
||||
while (cur && cur.nodeType === 1) {
|
||||
const bgImage = getComputedStyle(cur).backgroundImage || '';
|
||||
const gradColors = parseGradientColors(bgImage);
|
||||
if (gradColors.length > 0) {
|
||||
// Average the gradient colors
|
||||
const avg = { r: 0, g: 0, b: 0 };
|
||||
for (const c of gradColors) { avg.r += c.r; avg.g += c.g; avg.b += c.b; }
|
||||
avg.r = Math.round(avg.r / gradColors.length);
|
||||
avg.g = Math.round(avg.g / gradColors.length);
|
||||
avg.b = Math.round(avg.b / gradColors.length);
|
||||
parentBg = avg;
|
||||
break;
|
||||
}
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
}
|
||||
return checkGlow({ tag, boxShadow: style.boxShadow, effectiveBg: parentBg });
|
||||
}
|
||||
|
||||
function checkElementGradientDOM(el) {
|
||||
function checkElementAIPaletteDOM(el) {
|
||||
const style = getComputedStyle(el);
|
||||
const bgImage = style.backgroundImage || '';
|
||||
const colors = parseGradientColors(bgImage);
|
||||
if (colors.length === 0) return [];
|
||||
const findings = [];
|
||||
|
||||
// AI palette: purple/violet gradient
|
||||
for (const c of colors) {
|
||||
// Check gradient backgrounds for purple/violet or cyan
|
||||
const bgImage = style.backgroundImage || '';
|
||||
const gradColors = parseGradientColors(bgImage);
|
||||
for (const c of gradColors) {
|
||||
if (hasChroma(c, 50)) {
|
||||
const hue = getHue(c);
|
||||
if (hue >= 260 && hue <= 310) {
|
||||
findings.push({ id: 'ai-color-palette', snippet: `Purple/violet gradient background` });
|
||||
findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet gradient background' });
|
||||
break;
|
||||
}
|
||||
if (hue >= 160 && hue <= 200) {
|
||||
findings.push({ id: 'ai-color-palette', snippet: 'Cyan gradient background' });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for neon text (vivid cyan/purple color on dark background)
|
||||
const textColor = parseRgb(style.color);
|
||||
if (textColor && hasChroma(textColor, 80)) {
|
||||
const hue = getHue(textColor);
|
||||
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
||||
if (isAIPalette) {
|
||||
const parentBg = el.parentElement ? resolveBackground(el.parentElement) : null;
|
||||
// Also check gradient parents
|
||||
let effectiveBg = parentBg;
|
||||
if (!effectiveBg) {
|
||||
let cur = el.parentElement;
|
||||
while (cur && cur.nodeType === 1) {
|
||||
const gi = getComputedStyle(cur).backgroundImage || '';
|
||||
const gc = parseGradientColors(gi);
|
||||
if (gc.length > 0) {
|
||||
const avg = { r: 0, g: 0, b: 0 };
|
||||
for (const c of gc) { avg.r += c.r; avg.g += c.g; avg.b += c.b; }
|
||||
avg.r = Math.round(avg.r / gc.length);
|
||||
avg.g = Math.round(avg.g / gc.length);
|
||||
avg.b = Math.round(avg.b / gc.length);
|
||||
effectiveBg = avg;
|
||||
break;
|
||||
}
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
}
|
||||
if (effectiveBg && relativeLuminance(effectiveBg) < 0.1) {
|
||||
const label = hue >= 260 ? 'Purple/violet' : 'Cyan';
|
||||
findings.push({ id: 'ai-color-palette', snippet: `${label} neon text on dark background` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1108,7 +1162,7 @@ if (IS_BROWSER) {
|
||||
...checkElementColorsDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementGradientDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
||||
];
|
||||
|
||||
if (findings.length > 0) {
|
||||
@@ -1208,13 +1262,10 @@ async function detectHtml(filePath) {
|
||||
|
||||
const dom = new JSDOM(processedHtml, {
|
||||
url: `file://${resolvedPath}`,
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
const { window } = dom;
|
||||
const { document } = window;
|
||||
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
|
||||
const findings = [];
|
||||
|
||||
// Element-level checks (borders + colors + motion)
|
||||
|
||||
Reference in New Issue
Block a user