Two unrelated breakages were stacking on the v2.0 PR:
1. The static site build crashed because the generated /visual-mode page
referenced images via root-absolute paths (/antipattern-images/*.png).
Bun's HTML loader resolves <img src> at build time relative to the
source HTML file and treats a leading slash as filesystem-absolute, so
it could not find the images. Use a relative path so Bun bundles and
hashes them the same way the homepage already does.
2. The Puppeteer-backed fixture tests crashed in GitHub Actions because
the Ubuntu runners block unprivileged user namespaces, so Chrome's
sandbox cannot initialize. Pass --no-sandbox / --disable-setuid-sandbox
only when process.env.CI is set, so local users keep the hardened
default launch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jsdom's CSSOM silently drops any border shorthand containing var(),
leaving the computed style empty — which hid the canonical real-world
side-tab pattern (border-left: Npx solid var(--brand)) from the Node
detector path. Real browsers resolve var() natively, so this only
affected the jsdom path.
Add a pre-pass that walks the stylesheets, reads border shorthands off
rule.style (jsdom preserves them there even when it drops them from
cssText), resolves var() against :root custom properties via the
documentElement's computed style, and attaches the result to a per-
element override map. checkElementBorders consults the map whenever
jsdom returned an empty width, or substitutes a resolved color when
jsdom kept a literal var() string. Hex and named colors are normalized
to rgb() so isNeutralColor can classify them correctly — without that,
--line:#e5e7eb slipped through as non-neutral.
Adds four flag cases and three pass cases to modern-color-borders.html
covering shorthand, mixed neutral+colored, border-right, card-shaped
label, neutral-resolving var, thin var, and uniform all-sides var.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Visual Details DON'T entry in source/skills/impeccable/SKILL.md
was rewritten to cover border-left/border-right specifically ('colored
accent stripe') and no longer contains the old 'thick colored border
on one side' substring. Update the side-tab and border-accent-on-rounded
rules so the build-time validator passes again.
- Regenerate src/detect-antipatterns-browser.js via bun run build:browser
- Fix isNeutralColor to handle oklch, oklab, lch, lab, hsl, and hwb
with format-specific chroma/saturation thresholds. jsdom returns
these formats literally, so the previous rgb-only regex caused every
modern-format border color to be silently treated as neutral and
skipped by checkBorders.
- Flip the unknown-format fallback from neutral to colored, so
unrecognized color strings err on the side of detection.
- Introduce a narrower BORDER_SAFE_TAGS set (SAFE_TAGS minus 'label')
used only by the border checks. Card-shaped clickable labels with
thick colored side borders are now detected, while colors, motion,
and nested-card checks continue to skip labels to avoid false
positives on real form labels.
- Add tests/fixtures/antipatterns/modern-color-borders.html with 8
flag cases (oklch x3, oklab, lch, lab, plus 2 label cards) and
10 pass cases (neutrals across formats, plain inline labels,
thin/neutral-bordered labels, colored-on-all-sides).
Reproducer (preop-portal demo): side-tab findings rise from 0 to 12.
Emojis render as multicolor glyphs regardless of CSS \`color\`, so the
text color is irrelevant for contrast calculations. The detector was
flagging emoji icons as low-contrast whenever the surrounding bg/text
colors were close (e.g. an emoji card with text-color set to match
the bg). Adds an isEmojiOnlyText() helper that returns true when the
direct text consists entirely of emoji characters (and zero-width
joiners, variation selectors, skin-tone modifiers, regional
indicators), and skips both gray-on-color and low-contrast checks
when it's true.
Same insight fixes a missed icon-tile-stack detection: many AI-
generated cards use \`<div class="card-icon">⚡</div>\` where the
tile contains the emoji directly as text, not an <svg>/<i> child.
The detector now also recognizes these "inline emoji icon" tiles.
Both fixes are TDD'd: new test cases in color.html (two emoji cards
with matching text/bg colors) and icon-tile-stack.html (the inline
emoji tile pattern). The test suite went from green → red → green
across both rules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Overused-font check now measures actual computed-style usage across
text-bearing DOM elements instead of scanning CSS rules. A font is
only flagged as "primary" when it's used by ≥15% of text elements,
so demo/example classes that exist in the stylesheet but apply to
one tiny element no longer trigger false positives. The detail
message also includes the actual usage percentage.
- Same approach for the single-font check.
- Skipped-heading detail now includes the heading text on both sides
of the skip (e.g. <h1> "Title" followed by <h3> "Subtitle"), making
the offending element trivial to locate.
- Removes the 26-character truncation in TYPE_LABELS that cut long
anti-pattern names mid-word in overlay labels.
- Selector generator now uses class names + tag names and stops
walking up the DOM as soon as the partial selector uniquely matches
the target. Filters out CSS-in-JS hashed class names. Replaces ugly
6-level :nth-child chains with readable selectors like
`code.detection-cmd` or `#section > div > .card`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The old rule used a fixed 8px floor on minPad, which produced false
positives on small inline pills (like the homepage's .detection-cmd
at 6px vertical / 14px horizontal on 13px font) and false negatives
on large text (a 24px heading with 8px padding all around passed
the floor but is genuinely too tight for the text size).
The new rule uses two independent axis thresholds that scale with
font-size:
vertical: max(4px, fontSize × 0.3)
horizontal: max(8px, fontSize × 0.5)
The asymmetry reflects typographic reality: line-height already
provides built-in vertical breathing room (the line box is taller
than the cap height), so vertical padding can be tighter than
horizontal. Both thresholds scale with font-size — bigger text
demands proportionally more padding.
Behavior changes
- Small inline pills with line-height-aware padding now pass
(.detection-cmd: V 6 ≥ 4, H 14 ≥ 8). The homepage CSS is unchanged.
- Cramped large text now flags (24px heading with 8px padding fails
H 8 < 12). The old rule missed this entirely.
- All original 8px-floor flag cases still flag — 4px on 14px text
is still 4 < 4.2 vertical, 2px is still cramped, etc.
- Snippet now indicates which axis failed and the specific threshold
for the font-size: "6px vertical padding (need ≥4.8px for 16px text)"
instead of the old "6px padding (need >=8px)".
Fixture
- tests/fixtures/antipatterns/cramped-padding.html is a new
comprehensive side-by-side fixture with 8 flag cases and 12 pass
cases spanning small pills, cards, code blocks, interactive
elements, and big text. Replaces the prior 3-case version.
Test
- tests/detect-antipatterns-browser.test.mjs asserts exactly 8
cramped-padding findings with detailed comments listing each
expected case and which axis fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The quality detection rules (line-length, cramped-padding, tight-leading,
tiny-text, justified-text, all-caps-body, wide-tracking, skipped-heading)
were originally added as browser-only and wired only into the overlay
loop. The CLI's jsdom path silently skipped all of them.
Two of the eight rules genuinely need real browser layout
(line-length reads rect.width for chars-per-line; cramped-padding reads
rect.width/height to filter small badges). The other six only need
computed CSS values and pure DOM walks — they can run in jsdom too.
Refactor
- Extract a pure checkQuality(opts) from checkElementQualityDOM, taking
pre-resolved lineHeightPx and letterSpacingPx so each adapter handles
its own unit resolution.
- Add resolveFontSizePx(el, win) — walks the parent chain to compute
effective font-size in pixels, handling px / rem / em / % through
inheritance. Browsers do this automatically in getComputedStyle, but
jsdom returns "0.875rem" verbatim, which broke naive parseFloat math.
- Add resolveLengthPx(value, fontSizePx) — generic CSS length → px
helper used for line-height and letter-spacing in the Node adapter.
- Extract checkPageQualityFromDoc(doc) and add a Node call site so
skipped-heading fires from the CLI too.
- Add checkElementQuality(el, style, tag, window) Node adapter and wire
it into detectHtml's element loop.
Tests
- New tests/detect-antipatterns-browser.test.mjs — Puppeteer-backed
runner that spins up a temporary static server (port 8765, mirrors
the dev server's /fixtures/* and /js/* routes) and uses detectUrl()
to load fixtures in headless Chrome. Asserts the two browser-only
rules (cramped-padding, line-length) that need real layout.
- New tests/fixtures/antipatterns/cramped-padding.html — focused
side-by-side fixture for the cramped-padding rule. Pass column
includes a faithful replica of .detection-cmd from the homepage
(the disputed "small inline pill" case the user is deciding what
to do with). Test asserts 3 findings: 2 from the obvious flag
column + 1 from the disputed pill.
- New tests/fixtures/antipatterns/quality.html — merged side-by-side
replacement for the orphaned quality-should-flag/pass.html files.
Covers all 7 typography-quality rules. The 6 jsdom-compatible rules
are asserted in the jsdom test; line-length stays in the Puppeteer
test.
- Delete the orphaned quality-should-flag.html / quality-should-pass.html.
- Wire the new browser test into bun run test (~2.6s overhead).
Coverage win: the CLI now catches tight-leading, tiny-text,
justified-text, all-caps-body, wide-tracking, and skipped-heading on
real projects, where it previously missed all six.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the 26-character truncation in TYPE_LABELS that cut off long
anti-pattern names mid-word (e.g. "icon tile stacked above heading"
became "icon tile stacked above he"). The label is sized by content
via white-space: nowrap so it grows to fit, and multi-finding overlays
that exceed the outline width already fall back to the cycling UI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A new icon-tile-stack detection (the canonical AI feature-card with a
small rounded-square icon container above a heading), backed by a
two-column TDD fixture, plus a single-source-of-truth design that ties
the engine to the impeccable skill so they can no longer drift silently.
Detection
- New icon-tile-stack rule (slop): heading's previousElementSibling is
a 32–128px rounded-square element with a non-transparent background
or border, contains an svg/icon-i child, and sits above (not next to)
the heading. Excludes round avatars, wide thumbnails, side-by-side
layouts, tiny icons, and hero images.
- Two-column fixture convention: a single icon-tile-stack.html with a
flag column (4 cases) and pass column (6 cases), with snippet-text
matching used by the fixture test.
Single source of truth
- Each ANTIPATTERNS entry can now declare skillSection + skillGuideline.
18 of 25 rules carry these fields; the build's new
validateAntipatternRules() in scripts/build.js fails if any declared
skillGuideline isn't found verbatim in the right SKILL.md section.
- scripts/build-extension.js now includes the description field in
extension/detector/antipatterns.json (it was previously dropped).
- The existing count validator was promoted from warn to error so
command count drift fails the build the same way detection drift does.
Impeccable skill DON'Ts
- Added 4 new top-level DON'Ts that target real default AI behavior:
single-font, flat-type-hierarchy, all-caps-body, line-length.
- Cut 7 new DON'Ts I had drafted (tight-leading, tiny-text, wide-tracking,
justified-text, low-contrast, cramped-padding, skipped-heading) because
they teach things every model already knows from CSS/a11y basics. The
detector still catches all of them.
Stale count cleanup
- 22 commands → 21 across 17 references in HTML, README, NOTICE, AGENTS,
plugin.json, marketplace.json (left over from the validate skill removal).
- Dropped the hand-coded "212 design guidelines" marketing copy on the
homepage, which never mapped to any real count.
Sub-agent
- New private .claude/agents/anti-patterns.md captures the full TDD
recipe, schema, plug-in points, jsdom constraints, and pre-commit
checklist so future sessions can add rules end-to-end without
re-investigating the wiring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously checkColors bailed out whenever an ancestor used a gradient
background, since resolveBackground returned null. As a result, gray or
low-contrast text inside any gradient container was completely invisible
to both rules — e.g. the gray heading on bad-contrast.html.
Add a resolveGradientStops fallback that walks parents for gradient
stops and runs contrast against the worst-case stop, plus gray-on-color
when every stop is chromatic. parseGradientColors now also accepts hex
so jsdom fixtures with raw inline gradients work too. Extended the
color-should-flag fixture and tests to cover the gradient case.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactors the extension for on-demand injection (no static content_scripts
entry — content script and detector are loaded only when the user actively
opens the Impeccable panel, sidebar pane, or popup). Adds a new "Auto-scan"
preference (default: scan when the Impeccable panel opens, opt-in: scan
when DevTools opens) plus configurable line length (strict/lax) and
highlight blur on/off settings. Adds an Elements panel sidebar that shows
findings for the currently selected element.
Includes substantial overlay UX work: page-pixel-perfect spotlight mask
via clip-path, refined hover/dim states, instant transitions for snappier
feel, copy buttons for findings, hover-from-panel highlighting, and a
brand-aware exception list so the font check no longer flags Roboto on
Google's own properties.
Robustness fixes for the MV3 service worker lifecycle: heartbeat keepalive
plus auto-reconnecting ports across panel/sidebar/devtools so transient
SW restarts don't break the panel UI, and immediate teardown on DevTools
close (replacing an unreliable setTimeout-based defer that didn't survive
SW termination).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a Manifest V3 Chrome extension that injects the detector when
DevTools opens, with a dedicated panel for browsing findings, a toolbar
popup for quick scan/toggle, and per-rule settings synced via
chrome.storage. Categorizes anti-patterns into AI slop vs quality
issues with visual differentiation (sparkle prefix, panel grouping).
Overlay labels are polished with flush positioning, cycling for
multi-finding elements, and synchronized hover darkening.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merges the impeccable-detect CLI repo (pbakaus/impeccable-cli@831a6cc)
into this repo. The BSL-1.1 license that motivated the split is gone;
everything is now Apache 2.0.
- Add bin/, src/, detection tests and fixtures from CLI repo
- Merge package.json: name → "impeccable", add bin/exports/files fields
- Internal refs now read from local src/ instead of node_modules/
- Update SPDX headers, NOTICE.md, CLAUDE.md, FAQ, npm README
- Add prepack/postpack scripts for CLI-focused README on npm
- Remove terminal license labels (no longer needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>