mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
* Stop assuming white when a background cannot be read Dark themes came back from a scan buried in low-contrast findings that all claimed the light text sat on #ffffff. Two live runs against impeccable.style produced 102 and 95 of them. Two causes, both fixed here. Parsing. Browsers keep the authored color space in getComputedStyle output: oklch() stayed oklch, but color-mix results come back as color(srgb 1.04 0.72 -0.21), wide-gamut authors get color(display-p3 ...), and lch()/lab() survive verbatim. The parser read none of those, so those surfaces registered as unset. parseGradientColors was worse: it matched only rgba() and #hex, so a ground painted as linear-gradient(oklch(...), oklch(...)) counted as a gradient with no stops at all. Guessing. When the ancestor walk ran out of readable color it returned white, and on a body-level gradient it returned white without even looking. Light copy on a lacquer-black page then measured 1.3:1 against a canvas the visitor never sees. resolveBackgroundInfo now separates three outcomes: a resolved surface, a gradient the caller should fall back to stops for, and an unreadable layer. The last one makes both color adapters skip their contrast checks entirely. White survives in exactly one case, the one that earns it: every layer up to the document root was genuinely transparent. Color conversions moved to cli/engine/shared/color.mjs and gained lab, lch, and color() for srgb, srgb-linear, and display-p3. Spaces outside that set return null, which now routes to abstention rather than to a color nobody painted. Every conversion is pinned against what Chrome itself paints for the same string. Rescanning impeccable.style: 102 low-contrast findings down to 30, none of them on an invented white ground. Assisted-by: Claude Code * fix: address PR review bot findings on background resolution - Treat a url() image layer stacked above a gradient as an occluding, unreadable surface: resolveBackgroundInfo now returns unresolved so the gradient-stop fallback never measures stops the image hides (greptile-apps finding, reproduced in Chrome). - Route the glow and AI-palette DOM adapters through resolveBackgroundInfo so an unresolved surface makes them abstain instead of hunting gradient ancestors past an unreadable layer (Cursor Bugbot finding). - Resolve background-color keywords jsdom hands through verbatim: inherit now reads as no-paint (the ancestor walk IS its resolution) and currentcolor substitutes the element's own computed text color instead of forcing an abstention (Copilot finding). - Regression coverage in the dark-theme fixture for all three, asserted in both the jsdom and real-Chrome suites; browser detector regenerated. AI-assisted: prepared with Claude Code at the maintainer's direction. Co-Authored-By: Claude <noreply@anthropic.com> * fix: keep zero-offset glow findings when the surface is unreadable The browser glow adapter abstained from the whole element when resolveBackgroundInfo reported an unreadable surface, which also dropped zero-offset chromatic halo findings that do not depend on the background at all. It now skips only the gradient hunt past the unreadable layer and scores the halo tell against a null surface, matching what the static loop already did. Fixture cases pin both sides: the halo over a url() image ancestor flags in both engines, and an offset chromatic shadow on the same unknown surface stays abstained. Also hardens the currentcolor background substitution with the parseColorResolved fallback used by the text-color path, and adds fixture coverage proving tokenized currentcolor surfaces already resolve through the static cascade (flag when knowable, abstain when the token is undefined). Addresses Cursor Bugbot review findings on PR #541. AI-assisted-by: Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * fix: abstain on translucent gradients over images, drop phantom color-mix stops Two follow-up review findings on the merge with main. A gradient leading a url() layer was treated as a resolvable surface even when its stops are translucent, so the glow and AI-palette hunts averaged wash stops (a 20% black wash reads as pure black) while the real surface blends with image pixels the engine cannot read. resolveBackgroundInfo now marks gradient-over-image unresolved unless every readable stop of the leading gradient is opaque, in which case the gradient provably covers the image and remains the scorable surface. parseGradientColorsModern predated this branch's parseGradientColors rewrite: its second regex pass re-extracted color tokens nested inside color-mix() stops that the shared parser already captures whole via balanced-paren tokens, appending ingredient colors that are never painted. The worst-case stop ratio then invented low-contrast findings against a color nobody sees. The helper is removed; all callers use the shared parser, which covers the modern syntaxes it existed for. Fixture coverage pins both: the translucent-wash-over-image glow abstains in both engines, an opaque gradient over an image still flags in the browser, and the color-mix wash case stays clean in the static engine. Each new assertion was verified to fail against the previous engine. Addresses Greptile and Cursor Bugbot review findings on PR #541. AI-assisted-by: Claude Code Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
284 lines
14 KiB
HTML
284 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dark Glow Anti-Patterns — Should Flag vs Should Pass</title>
|
|
<style>
|
|
/* Two-column fixture: left = should flag, right = should pass.
|
|
The dark-glow rule fires when a colored box-shadow appears on a
|
|
child whose ancestor has a dark background. Each column wraps
|
|
its dark-context cases in its own .dark-context section so the
|
|
parent-bg lookup behaves predictably regardless of layout. */
|
|
body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 24px; margin: 0; }
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1200px; margin: 0 auto; }
|
|
.col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }
|
|
.col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }
|
|
.dark-context { background: #111827; padding: 16px; border-radius: 12px; }
|
|
.dark-context + .dark-context { margin-top: 12px; }
|
|
/* Card variants: explicit dark vs light pairings, both WCAG AA compliant */
|
|
.card { padding: 14px 16px; border-radius: 10px; margin-bottom: 10px; }
|
|
.card h4 { font-weight: 600; font-size: 13px; margin: 0; }
|
|
.card p { font-size: 12px; margin: 4px 0 0; }
|
|
.card-dark { background: #1f2937; }
|
|
.card-dark h4 { color: #f9fafb; }
|
|
.card-dark p { color: #cbd5e1; }
|
|
.card-light { background: white; }
|
|
.card-light h4 { color: #0f172a; }
|
|
.card-light p { color: #475569; }
|
|
|
|
/* ── FLAG: dark background + colored glow ── */
|
|
.glow-blue { box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); }
|
|
.glow-purple { box-shadow: 0 0 25px rgba(139, 92, 246, 0.35); }
|
|
.glow-cyan { box-shadow: 0 0 15px rgba(6, 182, 212, 0.5); }
|
|
.glow-multi { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 0 30px rgba(168, 85, 247, 0.3); }
|
|
|
|
/* ── FLAG: modern color formats, var() refs, text-shadow ── */
|
|
:root { --fixture-glow: oklch(0.78 0.14 155); }
|
|
.glow-oklch { box-shadow: 0 0 18px oklch(0.72 0.15 195 / 0.5); }
|
|
.glow-hex { box-shadow: 0 0 16px #3b82f6; }
|
|
.glow-hsl { box-shadow: 0 0 22px hsl(280, 80%, 60%); }
|
|
.glow-var { box-shadow: 0 0 10px var(--fixture-glow); }
|
|
.glow-text h4 { text-shadow: 0 0 12px #22d3ee; }
|
|
/* Zero-offset chromatic halo is slop on light backgrounds too */
|
|
.glow-light-oklch { box-shadow: 0 0 20px oklch(0.65 0.2 300 / 0.45); }
|
|
/* Unreadable surface: url() image ancestor. The zero-offset halo tell
|
|
holds on ANY background, so an unknown surface must not suppress it.
|
|
Both element loops must agree here (the browser adapter once returned
|
|
[] for the whole element on an unresolved surface). */
|
|
.photo-context { background-image: url('/fixtures/antipatterns/missing-photo.png'); padding: 16px; border-radius: 12px; }
|
|
.glow-photo-halo { box-shadow: 0 0 26px rgba(217, 70, 239, 0.5); }
|
|
/* Opaque gradient atop a url() layer: every stop is opaque, so the
|
|
gradient provably covers the image — the surface IS the dark
|
|
gradient, and the dark-background glow tell may score against it
|
|
(browser hunt; the static loop has no gradient hunt). */
|
|
.photo-opaque-grad { background: linear-gradient(#111827, #0b1220), url('/fixtures/antipatterns/missing-photo.png'); padding: 16px; border-radius: 12px; }
|
|
.glow-photo-opaque { box-shadow: 0 6px 22px rgba(249, 115, 22, 0.5); }
|
|
|
|
/* ── PASS: same dark/light backgrounds, but neutral or no glow ── */
|
|
.light-colored-shadow { box-shadow: 0 2px 4px rgba(59, 130, 246, 0.15); }
|
|
.dark-normal-shadow { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); }
|
|
.dark-focus-ring { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); }
|
|
.dark-subtle-shadow { box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2); }
|
|
.dark-elevated-sm { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); }
|
|
.dark-elevated-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4); }
|
|
.dark-elevated-lg { box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5); }
|
|
.dark-elevated-inset { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); }
|
|
.dark-elevated-multi { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2); }
|
|
.elevated-sm-light { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
|
|
.elevated-md-light { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
|
|
.elevated-warm-light { box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.15); }
|
|
/* Medium-gray card: lighter than dark mode, used to demonstrate "not dark enough" for glow detection. White text for AA contrast. */
|
|
.card-medium { background: #6b7280; }
|
|
.card-medium h4 { color: #ffffff; }
|
|
.card-medium p { color: #ffffff; }
|
|
.medium-gray-shadow { box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); }
|
|
/* Offset chromatic drop shadow on a light background: a real drop
|
|
shadow, not a halo — must NOT flag under the zero-offset rule.
|
|
(Element path; the page-level text scan caps at one finding.) */
|
|
.light-offset-colored { box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25); }
|
|
/* Achromatic zero-offset shadow: soft ambient elevation, stays legal */
|
|
.light-soft-neutral { box-shadow: 0 0 24px rgba(0, 0, 0, 0.15); }
|
|
/* Offset chromatic shadow under the same unreadable url() surface:
|
|
the dark-background glow tell needs a surface we can read — abstain. */
|
|
.photo-offset-colored { box-shadow: 0 6px 20px rgba(16, 185, 129, 0.35); }
|
|
/* Translucent gradient atop a url() layer: the image shows through the
|
|
20% wash, so the real surface is a blend with unknowable pixels (a
|
|
white photo composites to ~#cccccc, not black). Averaging the wash
|
|
stops as if they were the surface scored this "dark" — abstain. */
|
|
.photo-translucent-grad { background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('/fixtures/antipatterns/missing-photo.png'); padding: 16px; border-radius: 12px; }
|
|
.photo-translucent-offset { box-shadow: 0 6px 22px rgba(244, 63, 94, 0.45); }
|
|
/* Offset neutral text-shadow on dark card: legibility aid, not a glow */
|
|
.dark-text-offset h4 { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="grid">
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
LEFT COLUMN: should flag (dark page + colored glows)
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="col" data-col="flag">
|
|
<h2>Should flag</h2>
|
|
|
|
<h3>CSS colored glows on dark background</h3>
|
|
<div class="dark-context">
|
|
<div class="card card-dark glow-blue">
|
|
<h4>Blue glow</h4>
|
|
<p>box-shadow: 0 0 20px rgba(59, 130, 246, 0.4)</p>
|
|
</div>
|
|
<div class="card card-dark glow-purple">
|
|
<h4>Purple glow</h4>
|
|
<p>box-shadow: 0 0 25px rgba(139, 92, 246, 0.35)</p>
|
|
</div>
|
|
<div class="card card-dark glow-cyan">
|
|
<h4>Cyan glow</h4>
|
|
<p>box-shadow: 0 0 15px rgba(6, 182, 212, 0.5)</p>
|
|
</div>
|
|
<div class="card card-dark glow-multi">
|
|
<h4>Multi-shadow with colored glow</h4>
|
|
<p>Normal shadow + purple glow combined.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Inline-style glow (regex path)</h3>
|
|
<div class="dark-context">
|
|
<div class="card card-dark" style="box-shadow: 0 0 20px rgba(236, 72, 153, 0.5);">
|
|
<h4>Inline pink glow</h4>
|
|
<p>Inline style for regex detection path.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Modern color formats, var() refs, text-shadow</h3>
|
|
<div class="dark-context">
|
|
<div class="card card-dark glow-oklch">
|
|
<h4>OKLCH glow</h4>
|
|
<p>box-shadow: 0 0 18px oklch(0.72 0.15 195 / 0.5)</p>
|
|
</div>
|
|
<div class="card card-dark glow-hex">
|
|
<h4>Hex glow</h4>
|
|
<p>box-shadow: 0 0 16px #3b82f6</p>
|
|
</div>
|
|
<div class="card card-dark glow-hsl">
|
|
<h4>HSL glow</h4>
|
|
<p>box-shadow: 0 0 22px hsl(280, 80%, 60%)</p>
|
|
</div>
|
|
<div class="card card-dark glow-var">
|
|
<h4>Glow via var()</h4>
|
|
<p>box-shadow: 0 0 10px var(--fixture-glow)</p>
|
|
</div>
|
|
<div class="card card-dark glow-text">
|
|
<h4>Glowing text</h4>
|
|
<p>text-shadow: 0 0 12px #22d3ee on the heading.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Zero-offset halo on LIGHT background</h3>
|
|
<div class="card card-light glow-light-oklch">
|
|
<h4>Chromatic halo on light page</h4>
|
|
<p>box-shadow: 0 0 20px oklch(0.65 0.2 300 / 0.45)</p>
|
|
</div>
|
|
|
|
<h3>Zero-offset halo on unreadable (image) surface</h3>
|
|
<div class="photo-context">
|
|
<div class="card card-light glow-photo-halo">
|
|
<h4>Halo over photo background</h4>
|
|
<p>box-shadow: 0 0 26px rgba(217, 70, 239, 0.5)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Colored glow under an opaque gradient over an image (browser)</h3>
|
|
<div class="photo-opaque-grad">
|
|
<div class="card card-dark glow-photo-opaque">
|
|
<h4>Opaque dark gradient covers the photo</h4>
|
|
<p>box-shadow: 0 6px 22px rgba(249, 115, 22, 0.5)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
RIGHT COLUMN: should pass
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="col" data-col="pass">
|
|
<h2>Should pass</h2>
|
|
|
|
<h3>Light page + colored shadow</h3>
|
|
<div class="card card-light light-colored-shadow">
|
|
<h4>Colored shadow on light background</h4>
|
|
<p>Not dark mode, colored shadow is fine.</p>
|
|
</div>
|
|
|
|
<h3>Dark cards with neutral shadows</h3>
|
|
<div class="dark-context">
|
|
<div class="card card-dark dark-normal-shadow">
|
|
<h4>Normal gray shadow on dark card</h4>
|
|
<p>Gray/black shadow is not a glow.</p>
|
|
</div>
|
|
<div class="card card-dark dark-focus-ring">
|
|
<h4>Focus ring (spread, no blur)</h4>
|
|
<p>Functional ring, not decorative glow.</p>
|
|
</div>
|
|
<div class="card card-dark dark-subtle-shadow">
|
|
<h4>Tiny blur (<5px)</h4>
|
|
<p>Too subtle to be "glowing".</p>
|
|
</div>
|
|
<div class="card card-dark dark-elevated-sm">
|
|
<h4>Small dark elevation</h4>
|
|
<p>Subtle shadow on dark card.</p>
|
|
</div>
|
|
<div class="card card-dark dark-elevated-md">
|
|
<h4>Medium dark elevation</h4>
|
|
<p>Standard dark card shadow.</p>
|
|
</div>
|
|
<div class="card card-dark dark-elevated-lg">
|
|
<h4>Large dark elevation</h4>
|
|
<p>Prominent shadow on dark card.</p>
|
|
</div>
|
|
<div class="card card-dark dark-elevated-inset">
|
|
<h4>Inset shadow</h4>
|
|
<p>Inner shadow, not a glow.</p>
|
|
</div>
|
|
<div class="card card-dark dark-elevated-multi">
|
|
<h4>Multi-shadow dark card</h4>
|
|
<p>Layered gray shadows, no color glow.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Medium-gray background (not dark enough)</h3>
|
|
<div class="card card-medium medium-gray-shadow">
|
|
<h4>Not dark enough background</h4>
|
|
<p>Medium gray doesn't qualify as "dark mode".</p>
|
|
</div>
|
|
|
|
<h3>Colored drop shadow / neutral halo on light background</h3>
|
|
<div class="card card-light light-offset-colored">
|
|
<h4>Offset colored drop shadow</h4>
|
|
<p>Real drop shadow with a color tint — not a halo.</p>
|
|
</div>
|
|
<div class="card card-light light-soft-neutral">
|
|
<h4>Soft neutral halo</h4>
|
|
<p>Achromatic zero-offset shadow is ambient elevation.</p>
|
|
</div>
|
|
|
|
<h3>Offset chromatic shadow on unreadable (image) surface</h3>
|
|
<div class="photo-context">
|
|
<div class="card card-light photo-offset-colored">
|
|
<h4>Offset colored shadow over photo background</h4>
|
|
<p>Surface unknown, offset shadow — abstain, not flag.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Offset chromatic shadow under a translucent gradient over an image</h3>
|
|
<div class="photo-translucent-grad">
|
|
<div class="card card-light photo-translucent-offset">
|
|
<h4>The wash blends with unknowable image pixels</h4>
|
|
<p>Surface unknown — abstain, never score the wash stops as dark.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Neutral text-shadow on dark card</h3>
|
|
<div class="dark-context">
|
|
<div class="card card-dark dark-text-offset">
|
|
<h4>Offset neutral text-shadow</h4>
|
|
<p>Legibility aid, not a glow.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Light elevated cards</h3>
|
|
<div class="card card-light elevated-sm-light">
|
|
<h4>Small elevation (Tailwind shadow-sm)</h4>
|
|
<p>Standard subtle card shadow.</p>
|
|
</div>
|
|
<div class="card card-light elevated-md-light">
|
|
<h4>Medium elevation (Tailwind shadow-md)</h4>
|
|
<p>Standard card shadow.</p>
|
|
</div>
|
|
<div class="card card-light elevated-warm-light">
|
|
<h4>Warm deep shadow</h4>
|
|
<p>Large spread neutral shadow.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/js/detect-antipatterns-browser.js"></script>
|
|
</body>
|
|
</html>
|