mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
Fix: parse oklch in visual-contrast and neon-text (#592)
Bare parseRgb() dropped Tailwind v4 computed colors, so contrast sampling skipped and neon-text never fired. AI-assisted (Cursor agent). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -626,7 +626,7 @@ if (IS_BROWSER) {
|
|||||||
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
||||||
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
||||||
|
|
||||||
const solidBg = parseRgb(currentStyle.backgroundColor);
|
const solidBg = parseRgb(currentStyle.backgroundColor) || parseAnyColor(currentStyle.backgroundColor);
|
||||||
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
||||||
current = current.parentElement;
|
current = current.parentElement;
|
||||||
}
|
}
|
||||||
@@ -688,7 +688,7 @@ if (IS_BROWSER) {
|
|||||||
// starve the url()-backed texts this mode exists to sample.
|
// starve the url()-backed texts this mode exists to sample.
|
||||||
if (options.imageOnly && !reasons.includes('image background')) continue;
|
if (options.imageOnly && !reasons.includes('image background')) continue;
|
||||||
|
|
||||||
const textColor = parseRgb(style.color);
|
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||||
const fontSize = parseFloat(style.fontSize) || 16;
|
const fontSize = parseFloat(style.fontSize) || 16;
|
||||||
const fontWeight = parseInt(style.fontWeight) || 400;
|
const fontWeight = parseInt(style.fontWeight) || 400;
|
||||||
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
||||||
@@ -985,7 +985,7 @@ if (IS_BROWSER) {
|
|||||||
return sample;
|
return sample;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bg = parseRgb(style.backgroundColor);
|
const bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
|
||||||
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
||||||
return { status: 'unresolved', reason: 'no readable background' };
|
return { status: 'unresolved', reason: 'no readable background' };
|
||||||
}
|
}
|
||||||
@@ -1115,7 +1115,7 @@ if (IS_BROWSER) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
const textColor = parseRgb(style.color) || candidate.textColor;
|
const textColor = parseRgb(style.color) || parseAnyColor(style.color) || candidate.textColor;
|
||||||
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
||||||
|
|
||||||
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
||||||
|
|||||||
@@ -3986,7 +3986,7 @@ function checkElementAIPaletteDOM(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for neon text (vivid cyan/purple color on dark background)
|
// Check for neon text (vivid cyan/purple color on dark background)
|
||||||
const textColor = parseRgb(style.color);
|
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||||
if (textColor && hasChroma(textColor, 80)) {
|
if (textColor && hasChroma(textColor, 80)) {
|
||||||
const hue = getHue(textColor);
|
const hue = getHue(textColor);
|
||||||
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
||||||
@@ -7281,7 +7281,7 @@ if (IS_BROWSER) {
|
|||||||
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
if (currentStyle.filter && currentStyle.filter !== 'none') reasons.add('filter');
|
||||||
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
if (currentStyle.backdropFilter && currentStyle.backdropFilter !== 'none') reasons.add('backdrop filter');
|
||||||
|
|
||||||
const solidBg = parseRgb(currentStyle.backgroundColor);
|
const solidBg = parseRgb(currentStyle.backgroundColor) || parseAnyColor(currentStyle.backgroundColor);
|
||||||
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
if (solidBg && solidBg.a >= 0.95 && (!bgImage || bgImage === 'none')) break;
|
||||||
current = current.parentElement;
|
current = current.parentElement;
|
||||||
}
|
}
|
||||||
@@ -7343,7 +7343,7 @@ if (IS_BROWSER) {
|
|||||||
// starve the url()-backed texts this mode exists to sample.
|
// starve the url()-backed texts this mode exists to sample.
|
||||||
if (options.imageOnly && !reasons.includes('image background')) continue;
|
if (options.imageOnly && !reasons.includes('image background')) continue;
|
||||||
|
|
||||||
const textColor = parseRgb(style.color);
|
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||||
const fontSize = parseFloat(style.fontSize) || 16;
|
const fontSize = parseFloat(style.fontSize) || 16;
|
||||||
const fontWeight = parseInt(style.fontWeight) || 400;
|
const fontWeight = parseInt(style.fontWeight) || 400;
|
||||||
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
||||||
@@ -7640,7 +7640,7 @@ if (IS_BROWSER) {
|
|||||||
return sample;
|
return sample;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bg = parseRgb(style.backgroundColor);
|
const bg = parseRgb(style.backgroundColor) || parseAnyColor(style.backgroundColor);
|
||||||
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
if (bg && bg.a > 0.05) return { status: 'sampled', color: bg, method: 'solid-background' };
|
||||||
return { status: 'unresolved', reason: 'no readable background' };
|
return { status: 'unresolved', reason: 'no readable background' };
|
||||||
}
|
}
|
||||||
@@ -7770,7 +7770,7 @@ if (IS_BROWSER) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
const textColor = parseRgb(style.color) || candidate.textColor;
|
const textColor = parseRgb(style.color) || parseAnyColor(style.color) || candidate.textColor;
|
||||||
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
if (!textColor) return { ...candidate, status: 'unresolved', confidence: 'none', reason: 'unreadable text color' };
|
||||||
|
|
||||||
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
const rect = getDirectTextRect(el) || el.getBoundingClientRect();
|
||||||
|
|||||||
@@ -2752,7 +2752,7 @@ function checkElementAIPaletteDOM(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for neon text (vivid cyan/purple color on dark background)
|
// Check for neon text (vivid cyan/purple color on dark background)
|
||||||
const textColor = parseRgb(style.color);
|
const textColor = parseRgb(style.color) || parseAnyColor(style.color);
|
||||||
if (textColor && hasChroma(textColor, 80)) {
|
if (textColor && hasChroma(textColor, 80)) {
|
||||||
const hue = getHue(textColor);
|
const hue = getHue(textColor);
|
||||||
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
|
||||||
|
|||||||
@@ -221,6 +221,14 @@ describe('detectUrl — browser-only fixtures', () => {
|
|||||||
assert.equal(contrast.length, 3, `expected exactly the 3 flag-column cases, got ${contrast.length}:\n${snippets}`);
|
assert.equal(contrast.length, 3, `expected exactly the 3 flag-column cases, got ${contrast.length}:\n${snippets}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ai-color-palette: oklch neon text on a dark ground is flagged', async () => {
|
||||||
|
const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/oklch-neon-text.html`, { visualContrast: false });
|
||||||
|
assert.ok(
|
||||||
|
f.some(r => r.antipattern === 'ai-color-palette' && /Cyan neon text on dark background/i.test(r.snippet || '')),
|
||||||
|
`expected cyan neon-text finding from oklch color, got: ${JSON.stringify(f.map(r => r.snippet))}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('shadowed form.id: a <form> with <input name="id"> does not crash the scan (issue #407)', async () => {
|
it('shadowed form.id: a <form> with <input name="id"> does not crash the scan (issue #407)', async () => {
|
||||||
// HTMLFormElement named-property shadowing makes form.id / form.className
|
// HTMLFormElement named-property shadowing makes form.id / form.className
|
||||||
// return the child input element, whose .startsWith throws. Every Shopify
|
// return the child input element, whose .startsWith throws. Every Shopify
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OKLCH Neon Text Fixture</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--neon: oklch(0.85 0.2 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 32px;
|
||||||
|
background: #050505;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--neon);
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Cyan neon token</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+2
-1
@@ -9,6 +9,7 @@
|
|||||||
--paper: #f7f3ee;
|
--paper: #f7f3ee;
|
||||||
--ink: #171717;
|
--ink: #171717;
|
||||||
--muted: #566174;
|
--muted: #566174;
|
||||||
|
--flag-white: oklch(1 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
<h2>Should flag after pixel sampling</h2>
|
<h2>Should flag after pixel sampling</h2>
|
||||||
|
|
||||||
<article class="image-card light-image">
|
<article class="image-card light-image">
|
||||||
<p style="color: rgb(255, 255, 255);">White text on light image should be sampled by pixel contrast.</p>
|
<p style="color: var(--flag-white);">White text on light image should be sampled by pixel contrast.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="image-card dark-image">
|
<article class="image-card dark-image">
|
||||||
|
|||||||
Reference in New Issue
Block a user