diff --git a/README.md b/README.md index cc486ccc6..d554bf0d1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Impeccable -Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 53 deterministic detector rules for AI-generated frontend design. +Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 56 deterministic detector rules for AI-generated frontend design. > **Quick start:** From your project root, run `npx impeccable install`, then run `/impeccable init` inside your AI coding tool. Full docs: [impeccable.style](https://impeccable.style). @@ -13,7 +13,7 @@ Every model trained on the same SaaS templates. Skip the guidance and you get th Impeccable adds: - **One setup flow.** `/impeccable init` writes `PRODUCT.md` and offers `DESIGN.md`, so later commands know the audience, brand/product lane, voice, anti-references, colors, type, and components. - **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more. -- **53 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key. +- **56 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key. ## What's Included diff --git a/README.npm.md b/README.npm.md index 67ba9d276..030651f0c 100644 --- a/README.npm.md +++ b/README.npm.md @@ -1,6 +1,6 @@ # Impeccable CLI -Detect UI anti-patterns and design quality issues from the command line. Scans HTML, CSS, JSX, TSX, Vue, and Svelte files for 53 deterministic rules, including AI-generated UI tells, accessibility violations, and general design quality problems. +Detect UI anti-patterns and design quality issues from the command line. Scans HTML, CSS, JSX, TSX, Vue, and Svelte files for 56 deterministic rules, including AI-generated UI tells, accessibility violations, and general design quality problems. ## Quick Start @@ -56,7 +56,7 @@ npx impeccable detect --fast src/ **Quality**: tiny body text, cramped padding, long line lengths, small touch targets -53 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop). +56 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop). ## Exit Codes diff --git a/cli/engine/browser/injected/index.mjs b/cli/engine/browser/injected/index.mjs index 2ace5650b..a3d2ff8a3 100644 --- a/cli/engine/browser/injected/index.mjs +++ b/cli/engine/browser/injected/index.mjs @@ -1556,6 +1556,13 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]); } + // Edge-flush cards in horizontal scrollers (browser-only: needs real + // layout for the scroller clip box vs card rect math) + const edgeFlushFindings = checkEdgeFlushCardsDOM().filter(f => _ruleOk(f.type)); + for (const f of edgeFlushFindings) { + addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]); + } + // Page-level quality checks (headings, etc.) const qualityFindings = checkPageQualityDOM().filter(f => _ruleOk(f.type)); if (qualityFindings.length > 0) { @@ -1955,6 +1962,9 @@ if (IS_BROWSER) { window.impeccableDetectAsync = detectAsync; window.impeccableScan = scan; window.impeccableScanAsync = scanAsync; + // Raw measurement for the URL engine's content-hidden-at-rest pass: it + // drives a reveal sweep from Node and thresholds the result itself. + window.impeccableMeasureHiddenText = measureHiddenTextDOM; window.impeccableCollectVisualContrastCandidates = collectVisualContrastCandidates; window.impeccableAnalyzeVisualContrast = analyzeVisualContrast; window.impeccableGetLastVisualContrastAnalyses = () => lastVisualContrastAnalyses.slice(); diff --git a/cli/engine/cli/main.mjs b/cli/engine/cli/main.mjs index 1f046257a..db3e76a8c 100644 --- a/cli/engine/cli/main.mjs +++ b/cli/engine/cli/main.mjs @@ -96,6 +96,8 @@ Options: --gemini Also report Gemini-specific provider tells (off by default) --scope Only report rules in the given design domain (type, layout). Comma-separated. + --viewport Browser viewport for URL scans (default 1280x800), + e.g. --viewport 390x844 for a mobile-width pass --no-config Do not apply project config, detector ignores, inline ignore comments, or DESIGN.md --no-inline-ignores Do not honor in-file impeccable-disable* ignore comments @@ -175,6 +177,20 @@ async function detectCli() { args.splice(i, inline ? 1 : 2); i -= 1; } + let viewport = null; + for (let i = 0; i < args.length; i++) { + if (args[i] !== '--viewport' && !args[i].startsWith('--viewport=')) continue; + const inline = args[i].startsWith('--viewport='); + const value = inline ? args[i].slice('--viewport='.length) : args[i + 1]; + const match = /^(\d{2,5})x(\d{2,5})$/i.exec(value || ''); + if (!match) { + process.stderr.write('Error: --viewport requires a WxH value, e.g. --viewport 390x844\n'); + process.exit(1); + } + viewport = { width: Number(match[1]), height: Number(match[2]) }; + args.splice(i, inline ? 1 : 2); + i -= 1; + } const unknownScopes = scopes.filter(s => !RULE_SCOPES.has(s)); if (unknownScopes.length > 0) { process.stderr.write( @@ -190,6 +206,7 @@ async function detectCli() { const inlineIgnoresEnabled = configEnabled && !args.includes('--no-inline-ignores'); const scanOptions = { providers, inlineIgnores: inlineIgnoresEnabled }; if (designSystem) scanOptions.designSystem = designSystem; + if (viewport) scanOptions.viewport = viewport; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index 3aff11077..d29463790 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -372,6 +372,31 @@ const ANTIPATTERNS = [ }, // ── Quality: general design and accessibility issues ── + { + id: 'script-error', + category: 'quality', + severity: 'error', + name: 'Uncaught script error on load', + description: + 'A script threw an uncaught exception or failed to parse while the page loaded. Broken JavaScript silently kills reveals, interactions, and dynamic content, and can leave most of a page invisible. Fix the error before judging anything else.', + }, + { + id: 'content-hidden-at-rest', + category: 'quality', + severity: 'error', + scopes: ['layout'], + name: 'Content invisible at rest', + description: + 'A large share of the page text sits at opacity 0 or visibility hidden even after every reveal handler had a chance to run. This is the failed-reveal signature: the content shipped but never becomes visible. Make content visible by default and let JavaScript enhance its entrance instead of gating its existence.', + }, + { + id: 'edge-flush-cards', + category: 'quality', + scopes: ['layout'], + name: 'Cards flush against the scroller edge', + description: + 'Cards inside a horizontal scroller or tab panel sit flush against the container edge at rest while keeping a gutter on the other side, so their edges and rounded corners get cut off. Usually the panel is sized wider than its clip box. Keep a consistent inset on both sides.', + }, { id: 'gray-on-color', category: 'quality', @@ -817,16 +842,20 @@ function isEmojiOnlyText(text) { function checkColors(opts) { const { tag, textColor, bgColor, effectiveBg, effectiveBgStops, fontSize, fontWeight, hasDirectText, isEmojiOnly, bgClip, bgImage, classList } = opts; if (SAFE_TAGS.has(tag)) { - // Exception for and +

Text-bearing chip with low contrast

+ + SEV-2 +

Tailwind color anti-patterns

bg-black — pure black bg

@@ -151,6 +166,21 @@ let the contrast check run, but this case must clearly pass. --> High contrast pill +

Chip with good contrast (must not flag)

+ HEALTHY + +

Sub-9px decorative chip (below the font floor, must not flag)

+ tick + +

background: none resets an earlier background-color

+ +
+
light text over the dark panel, not the light code surface
+
+

Emoji on light backgrounds

diff --git a/tests/fixtures/antipatterns/edge-flush-cards.html b/tests/fixtures/antipatterns/edge-flush-cards.html new file mode 100644 index 000000000..4c67feae0 --- /dev/null +++ b/tests/fixtures/antipatterns/edge-flush-cards.html @@ -0,0 +1,67 @@ + + + + + Edge-flush cards fixture (browser-only) + + + +

Flag: first panel wider than its clip box

+
+
+
Card cut off at the right clip edge while keeping its left gutter.
+
Second card in the oversized panel, also flush against the edge.
+
+
+
Sibling panel card with the intended inset on both sides.
+
+
+ +

Pass: symmetric insets

+
+
Card with an even gutter on both sides of the clip box.
+
Second panel card, also properly inset.
+
+ +

Pass: deliberate peek

+
+
First peek card fully visible.
+
Second peek card fully visible.
+
Third card cut mid-card as a scroll affordance.
+
Fourth card entirely off-screen.
+
+ +

Pass: plain content, not cards

+
+
Unstyled text block flush against the edge is not a card.
+
Second plain block.
+
+ + diff --git a/tests/fixtures/antipatterns/reveal-working.html b/tests/fixtures/antipatterns/reveal-working.html new file mode 100644 index 000000000..a1f23643a --- /dev/null +++ b/tests/fixtures/antipatterns/reveal-working.html @@ -0,0 +1,62 @@ + + + + + Working reveal-on-scroll fixture (browser-only pass case) + + + +
+

Visible hero headline stays readable

+

Identical markup to the broken fixture, but the script parses and runs, so every + reveal section becomes visible once the detector's reveal sweep scrolls it into view. + Nothing on this page may produce a script-error or content-hidden finding.

+ + + + + + + +
+

First revealed section

+

This block starts at opacity zero, exactly like a broken page would, but its + IntersectionObserver is alive. During the sweep it gains the in-view class and + transitions to full opacity, so the post-sweep measurement sees it as visible.

+
+
+

Second revealed section

+

More reveal-on-scroll copy that carries a meaningful share of the page's + character count. If the sweep or the exclusion rules regress, this fixture is the + page that starts producing a false content-hidden-at-rest finding.

+
+
+

Third revealed section

+

The smooth scroll-behavior on the root element is deliberate: the sweep must + use instant scrolling or it never actually reaches these sections before it + measures, which was the first false-positive mode found while calibrating.

+
+
+ + + + diff --git a/tests/fixtures/antipatterns/script-error.html b/tests/fixtures/antipatterns/script-error.html new file mode 100644 index 000000000..596befb3a --- /dev/null +++ b/tests/fixtures/antipatterns/script-error.html @@ -0,0 +1,54 @@ + + + + + Script error + content hidden fixture (browser-only) + + + +
+

Visible hero headline stays readable

+

The opening paragraph renders normally, exactly like the real broken sample: the + first viewport looks fine while everything below it never appears.

+ +
+

First hidden section

+

This block waits for an IntersectionObserver that will never construct, because + the single script tag below fails to parse. Its text ships in the document but the + reveal class never lands, so the reader scrolls through blank space.

+
+
+

Second hidden section

+

More shipped-but-invisible copy. Together the hidden sections carry well over + thirty percent of the character count of the page, which is the failed-reveal + signature this fixture exists to reproduce for the detector tests.

+
+
+

Third hidden section

+

A final block of body copy that stays at opacity zero forever. A healthy page + with this exact markup reveals every one of these sections during the detector's + reveal sweep; this one cannot, because its JavaScript died at parse time.

+
+
+ + + + diff --git a/tests/fixtures/antipatterns/text-overflow.html b/tests/fixtures/antipatterns/text-overflow.html index 887c42dfd..21b109a22 100644 --- a/tests/fixtures/antipatterns/text-overflow.html +++ b/tests/fixtures/antipatterns/text-overflow.html @@ -46,6 +46,9 @@
A single long line of running text that refuses to wrap and clearly spills past its fixed-width box.
Supercalifragilisticexpialidocioussupercalifragilisticexpialidocious
+ +
IOC-QARTOD-ARGO-PROFILES-SPILLING-WELL-PAST-THE-CELL
@@ -61,6 +64,8 @@
An inactive carousel slide can contain long text that overflows while hidden.
+
Fits fine
+
An ordinary inline span with wrapping text that stays inside its container across several lines.