mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
* Add data-impeccable-ignore scoped waivers; fix occlusion and image-backed contrast FPs Three changes that let a page hosting deliberate anti-pattern exhibits scan clean without losing coverage, prepared with AI assistance (Claude Code) on maintainer instruction: - data-impeccable-ignore="rule-a rule-b" (or "*" / bare) on any element suppresses matching findings for its whole subtree, in the browser overlay, the extension, and the static engine. The DOM twin of the line-based impeccable-disable comments (which a live DOM cannot apply) and the generalization of data-impeccable-allow-kickers. Applied at the addBrowserFindings choke point, at the static element walk, and for regex findings that carry a live selector. - text-occlusion: an occluder whose effective opacity multiplies out to ~0 paints nothing. An opacity-0 range scrubber stretched over a before/after comparison produced 16 "100% covered by an opaque element" findings on one page because elementFromPoint returns it and its UA background-color read as opaque paint. Invisible-at-rest elements are also no longer probed as victims. - Analytic contrast now skips what it cannot measure: a url() image layer anywhere in the background stack ends the gradient-stops walk (dark ink on a bright gold-leaf image measured 2.6:1 against the wash composited over the wrong base), and elements that are invisible at rest (visibility hidden, effective opacity ~0 — hidden scene decks) are skipped by the color checks in both engines. The static cascade now tracks opacity to support this. Covered by a new scoped-ignore fixture (exact rule, star, comma list, nested depth, wrong-rule control) tested in both engines, a scrubber pass case in the occlusion fixture, and image-backed / photo-panel / hidden-scene pass cases in the gradient-ground fixture. Full suite passes; browser and extension bundles regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * CSS-scan findings carry their enclosing selector; browser pass resolves them Page-level CSS-text findings (marquee, dark-glow, radial-halo, repeating-stripes, codex-grid, ai-color-palette, image-hover-transform, pseudo/inset side-tab stripes) now attach the selector of the rule that matched, via a best-effort enclosingCssSelector() helper or the selector already in scope. The browser pass resolves that selector against the live DOM: pseudo segments are stripped, a selector that renders nowhere on the page drops the finding (the CSS ships there but the pattern never paints — the live DOM is ground truth in a browser scan), and matches under a data-impeccable-ignore ancestor are waived. Static scans are unchanged: partial documents keep the text-level findings. Applied with AI assistance (Claude Code). Covered in the scoped-ignore fixture: a live marquee under a marquee waiver is suppressed, and dead two-axis grid CSS matching no element is dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Attribute selectors on gradient-text and bounce-easing page emitters too Same mechanism as the previous commit, extended to the three page-level motion/text emitters that were still selector-less. Applied with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * text-overflow: skip SVG content; scrollWidth lies there Chrome reports arbitrary non-zero scrollWidth/clientWidth on SVG elements (a <text> gave 78/48 while its rendered length sat inside its box), so the box-metric delta is noise. SVG clips to its own viewport anyway. Pass case added to the quality fixture. Applied with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Overlay samples image-backed text at the pixel level by default Visual contrast gains a third mode. Explicit true still runs the full sampled pass, explicit false still disables everything (the mode the test suites use), and unset — the default overlay run — now samples ONLY image-backed text: the one class the analytic walk deliberately skips, because a url() layer's pixels are unknowable without looking. The cost is bounded and the method is precise: at most a 3x3 grid of sample points per candidate (degrading to 3 or 1 for small rects), the source image drawn once to a canvas with only those pixels read, and glyph ink never pollutes the samples because the image is drawn alone. A cross-origin image without CORS headers reports unresolved rather than guessing. Applied with AI assistance (Claude Code). Covered by a new fixture: white text on a near-white same-origin data-URI image background flags via sampled pixels under default options; dark ink on the same image passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Review fixes: root opacity, keyframe steps, static parity, attributed fixtures Applied with AI assistance (Claude Code), addressing all seven findings from the automated reviews: - effectiveOpacityDOM walks through body and html: a page-fade wrapper with body/html opacity 0 hides every descendant (Greptile executed a Chromium repro of the false positive). - enclosingCssSelector refuses `from`/`to` keyframe steps, which read as never-matching type selectors and got valid findings wrongly dropped by the zero-match rule (Bugbot, high). Regression case: an overshoot bezier inside a `to` step must survive as page-level. - The static cascade now inherits visibility, so descendants of a hidden container compute as hidden like the browser path; a declared visibility:visible still overrides. - The static engine applies scoped waivers to selector-backed html-pattern findings, mirroring the browser — but keeps findings whose selector matches nothing, since static scans see partial documents. - The scoped-ignore fixture grows to the mandated matrix: 4 flag cases (control, other-rule waiver, sibling waiver, misspelled rule id) and 5 waived shapes (exact rule, nested depth, star, comma list, self), each with a unique border width so every finding attributes to exactly one case in both engines' tests. - The image-backed contrast test pins its cases via the sampled finding's candidate text: the white-on-light specimen must flag and the dark-ink control must stay clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Review fixes: image-only starvation, selector rejection class, inset stripes Second review round, applied with AI assistance (Claude Code): - The image-only filter moves inside the candidate collector, before the cap: gradient/opacity/filter candidates earlier in DOM order no longer consume the 12-candidate budget and starve the url()-backed texts the mode exists to sample (Bugbot, high). The regression fixture packs 14 gradient decoys ahead of the photo panels, and the test now drives the overlay entry (impeccableDetectAsync, default options) rather than detectUrl's Node-side full fallback, which is where the image-only mode actually lives. - enclosingCssSelector no longer rejects the child combinator or quoted attribute selectors; only braces and angle brackets disqualify. - The inset box-shadow side-tab scanner attaches its selector like the pseudo-element scanner does, so those findings waive and dead-drop the same way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
253 lines
11 KiB
HTML
253 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Quality (Typography & Readability) — Should Flag vs Should Pass</title>
|
|
<style>
|
|
/* Two-column fixture: left = should flag, right = should pass.
|
|
Covers the typography-quality rules that need real browser layout
|
|
(line-length, tight-leading, tiny-text, justified-text, all-caps-body,
|
|
wide-tracking, skipped-heading). All are browser-only — see
|
|
tests/detect-antipatterns-browser.test.mjs for assertions. */
|
|
body { font-family: system-ui, sans-serif; background: #fafafa; padding: 24px; margin: 0; color: #0f172a; line-height: 1.6; }
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1280px; 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; }
|
|
.case { margin-bottom: 16px; padding: 16px; background: white; border: 1px solid #e2e8f0; border-radius: 8px; }
|
|
.case-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 6px; font-style: italic; }
|
|
|
|
/* ── FLAG: typography quality issues ── */
|
|
|
|
/* Line length: 12px body text fits >85 chars/line in a ~528px column.
|
|
12px is not below the tiny-text 12px floor (rule fires at <12px). */
|
|
.long-lines {
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Tight leading */
|
|
.tight-leading {
|
|
font-size: 16px;
|
|
line-height: 1.0;
|
|
max-width: 40em;
|
|
}
|
|
|
|
/* Tiny body text */
|
|
.tiny-text {
|
|
font-size: 10px;
|
|
line-height: 1.6;
|
|
max-width: 40em;
|
|
}
|
|
.tiny-text-11 {
|
|
font-size: 11px;
|
|
line-height: 1.6;
|
|
max-width: 40em;
|
|
}
|
|
|
|
/* Justified text without hyphens */
|
|
.justified-no-hyphens {
|
|
text-align: justify;
|
|
max-width: 30em;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* All-caps long body */
|
|
.all-caps-body {
|
|
text-transform: uppercase;
|
|
font-size: 16px;
|
|
max-width: 40em;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Wide letter spacing on body */
|
|
.wide-tracking-body {
|
|
letter-spacing: 0.15em;
|
|
font-size: 16px;
|
|
max-width: 40em;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* ── PASS: comfortable typography ── */
|
|
|
|
.good-measure {
|
|
max-width: 65ch;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.good-leading {
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
max-width: 40em;
|
|
}
|
|
|
|
.good-text-size {
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
max-width: 40em;
|
|
}
|
|
|
|
/* Justified text WITH hyphens — fine */
|
|
.justified-with-hyphens {
|
|
text-align: justify;
|
|
hyphens: auto;
|
|
-webkit-hyphens: auto;
|
|
max-width: 30em;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Short label in all-caps (passes — under 30 chars) */
|
|
.label-caps {
|
|
text-transform: uppercase;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.1em;
|
|
color: #475569;
|
|
}
|
|
|
|
/* Wide tracking on a short uppercase label (passes — both conditions excluded) */
|
|
.label-tracking {
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
}
|
|
|
|
.mock-terminal-title,
|
|
.mock-terminal-meta,
|
|
.mock-diff-line {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
color: #475569;
|
|
line-height: 1.5;
|
|
}
|
|
.mock-terminal-title { font-size: 11px; }
|
|
.mock-terminal-meta { font-size: 10.8px; }
|
|
.mock-diff-line { font-size: 11.84px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="grid">
|
|
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
LEFT COLUMN: should flag
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="col" data-col="flag">
|
|
<h2>Should flag</h2>
|
|
|
|
<h3>Line length too long</h3>
|
|
<div class="case">
|
|
<span class="case-label">no max-width on a paragraph</span>
|
|
<p class="long-lines">This paragraph has no max-width constraint at all, which means on a wide monitor or ultrawide display, each line of text can stretch to 150 or even 200 characters wide. Research consistently shows that line lengths beyond 75 characters significantly reduce reading speed and comprehension. The eye has to travel too far to find the beginning of the next line, causing readers to lose their place. A simple max-width of 65ch to 75ch on the paragraph or its container would fix this entirely.</p>
|
|
</div>
|
|
|
|
<h3>Tight line height</h3>
|
|
<div class="case">
|
|
<span class="case-label">line-height: 1.0</span>
|
|
<p class="tight-leading">This paragraph has a line-height of 1.0, which means the lines are touching. Multi-line body text needs breathing room between lines for readability. A line-height of 1.5 to 1.7 is generally recommended for body text.</p>
|
|
</div>
|
|
|
|
<h3>Tiny body text</h3>
|
|
<div class="case">
|
|
<span class="case-label">10px body text</span>
|
|
<p class="tiny-text">This body text is only 10px. While this might be fine for a disclaimer or legal footnote, it's too small for primary content that users need to actually read. Aim for at least 14px for body content, 16px is ideal.</p>
|
|
</div>
|
|
|
|
<h3>Justified text without hyphens</h3>
|
|
<div class="case">
|
|
<span class="case-label">text-align: justify, no hyphens: auto</span>
|
|
<p class="justified-no-hyphens">This paragraph uses text-align: justify, which forces each line to stretch to fill the full width. Without hyphenation support, this creates uneven gaps between words known as "rivers of white space" that flow vertically through the text. Left-aligned text is almost always more readable on the web.</p>
|
|
</div>
|
|
|
|
<h3>All-caps body text</h3>
|
|
<div class="case">
|
|
<span class="case-label">text-transform: uppercase on a long passage</span>
|
|
<p class="all-caps-body">This entire paragraph is in uppercase via text-transform. While all-caps works for short labels, headings, or navigation items, longer body text in uppercase is significantly harder to read because we lose the word shape cues that come from ascenders and descenders in mixed-case text.</p>
|
|
</div>
|
|
|
|
<h3>Wide letter spacing on body text</h3>
|
|
<div class="case">
|
|
<span class="case-label">letter-spacing: 0.15em on body text</span>
|
|
<p class="wide-tracking-body">This body text has letter-spacing: 0.15em applied to it. While subtle tracking adjustments can improve readability for headings or all-caps text, adding significant letter spacing to body text actually makes it harder to read by disrupting natural character groupings.</p>
|
|
</div>
|
|
|
|
<h3>Skipped heading levels</h3>
|
|
<div class="case">
|
|
<span class="case-label">h1 → h3 (missing h2)</span>
|
|
<h1 style="font-size: 20px; margin: 0 0 4px;">Top heading</h1>
|
|
<h3 style="font-size: 16px; margin: 0;">Skips straight to h3</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════════════════════════════
|
|
RIGHT COLUMN: should pass
|
|
═══════════════════════════════════════════════════════════ -->
|
|
<div class="col" data-col="pass">
|
|
<h2>Should pass</h2>
|
|
|
|
<h3>Comfortable line length</h3>
|
|
<div class="case">
|
|
<span class="case-label">max-width: 65ch on the paragraph</span>
|
|
<p class="good-measure">This paragraph has a max-width of 65ch, keeping the line length comfortable for reading. The eye can easily track from the end of one line to the beginning of the next without losing its place.</p>
|
|
</div>
|
|
|
|
<h3>Comfortable line height</h3>
|
|
<div class="case">
|
|
<span class="case-label">line-height: 1.6</span>
|
|
<p class="good-leading">This paragraph has a line-height of 1.6, which gives multi-line text plenty of room to breathe and improves the rhythm of the page.</p>
|
|
</div>
|
|
|
|
<h3>Comfortable body text size</h3>
|
|
<div class="case">
|
|
<span class="case-label">16px body text</span>
|
|
<p class="good-text-size">This is 16px body text — the recommended baseline for comfortable reading on modern displays.</p>
|
|
</div>
|
|
|
|
<h3>Justified text with hyphens</h3>
|
|
<div class="case">
|
|
<span class="case-label">text-align: justify + hyphens: auto</span>
|
|
<p class="justified-with-hyphens">When justified text is paired with hyphens: auto, the browser can break long words across lines, eliminating the rivers of white space and making the justification look intentional rather than awkward.</p>
|
|
</div>
|
|
|
|
<h3>Short label in all-caps</h3>
|
|
<div class="case">
|
|
<span class="case-label">short label, uppercase</span>
|
|
<span class="label-caps">Featured</span>
|
|
</div>
|
|
|
|
<h3>Wide tracking on a short uppercase label</h3>
|
|
<div class="case">
|
|
<span class="case-label">letter-spacing on a short uppercase label</span>
|
|
<span class="label-tracking">Beta</span>
|
|
</div>
|
|
|
|
<h3>Proper heading hierarchy</h3>
|
|
<div class="case">
|
|
<span class="case-label">h1 → h2 → h3</span>
|
|
<h1 style="font-size: 20px; margin: 0 0 4px;">Top heading</h1>
|
|
<h2 style="font-size: 18px; margin: 0 0 4px;">Second level</h2>
|
|
<h3 style="font-size: 16px; margin: 0;">Third level</h3>
|
|
</div>
|
|
|
|
<h3>Small mockup metadata</h3>
|
|
<div class="case">
|
|
<span class="case-label">terminal/diff metadata at tiny sizes</span>
|
|
<span class="mock-terminal-title">/impeccable polish : scanning codebase</span>
|
|
<div class="mock-terminal-meta">12 files inspected · 4 visual issues queued</div>
|
|
<div class="mock-diff-line">+ Replace generic button label with action-specific copy</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/js/detect-antipatterns-browser.js"></script>
|
|
<!-- SVG text: Chrome reports fake scrollWidth/clientWidth on SVG content;
|
|
the text-overflow rule must skip it (pass). -->
|
|
<svg class="svg-metrics-pass" viewBox="0 0 430 60" width="430" height="60" role="img" aria-label="diagram">
|
|
<rect x="6" y="10" width="96" height="40" rx="3" fill="none" stroke="#888"/>
|
|
<text x="54" y="35" text-anchor="middle" font-size="13">impeccable</text>
|
|
</svg>
|
|
</body>
|
|
</html>
|