diff --git a/README.md b/README.md index d554bf0d1..621021161 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 56 deterministic detector rules for AI-generated frontend design. +Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 57 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. -- **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. +- **57 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 030651f0c..ece80d914 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 56 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 57 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 -56 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop). +57 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 a3d2ff8a3..6adf7c8fb 100644 --- a/cli/engine/browser/injected/index.mjs +++ b/cli/engine/browser/injected/index.mjs @@ -1222,7 +1222,7 @@ if (IS_BROWSER) { return { type: f.type || f.id, category: ap ? ap.category : 'quality', - severity: ap?.severity || 'warning', + severity: f.severity || ap?.severity || 'warning', detail: f.detail || f.snippet, ignoreValue: f.ignoreValue || f.value || '', name: ap ? ap.name : (f.type || f.id), @@ -1489,7 +1489,7 @@ if (IS_BROWSER) { ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementBlinkingCursorDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementBlinkingCursorDOM(el).map(f => ({ type: f.id, detail: f.snippet, ...(f.severity ? { severity: f.severity } : {}) })), ...checkElementDesignSystemDOM(el, designSystem, designSeen), ].filter(f => _ruleOk(f.type)); @@ -1588,7 +1588,25 @@ if (IS_BROWSER) { } const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML); if (htmlPatternFindings.length > 0) { - const mapped = htmlPatternFindings.map(f => ({ type: f.id, detail: f.snippet })).filter(f => _ruleOk(f.type)); + const mapped = htmlPatternFindings.map(f => { + const item = { type: f.id, detail: f.snippet }; + if (f.severity) { + item.severity = f.severity; + } else if (f.id === 'pulsing-dot' && f.selector) { + // The string scan promotes header/nav dots on its own; with a live + // layout also promote dots resting in the first ~900px of the page + // (the hero region), which the source scan cannot measure. + try { + const dotEl = document.querySelector(f.selector); + if (dotEl) { + const rect = dotEl.getBoundingClientRect(); + const pageTop = rect.top + (window.scrollY || 0); + if (pageTop <= 900) item.severity = 'error'; + } + } catch { /* unresolvable selector: keep registry severity */ } + } + return item; + }).filter(f => _ruleOk(f.type)); pageLevelFindings.push(...mapped); addBrowserFindings(groupMap, document.body, mapped); } diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index d29463790..453b260c8 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -224,6 +224,15 @@ const ANTIPATTERNS = [ 'A blinking text cursor animated into a hero or landing section simulates typing where no input exists. It borrows the dev-tool aesthetic as decoration. Real editable fields draw their own caret; anywhere else, let the composition hold attention without a fake prompt.', skillSection: 'Motion', }, + { + id: 'shape-assembled-illustration', + category: 'slop', + severity: 'advisory', + name: 'Shape-assembled illustration', + description: + 'A large inline SVG that builds a pictorial scene from a pile of primitive shapes reads as placeholder clip art, not illustration. Icons, logos, and data graphics are fine at their scale; a hero-sized visual deserves real artwork, a photograph, or a deliberately drawn graphic.', + skillSection: 'Imagery', + }, { id: 'dark-glow', category: 'slop', @@ -853,7 +862,13 @@ function checkColors(opts) { // 1.2:1; the old a/button-only exception never looked at it.) The 9px // font floor keeps sub-text decorations out. const isStyledControl = hasDirectText - && bgColor && bgColor.a > 0.5 + && ((bgColor && bgColor.a > 0.5) + // A gradient painted on the element itself is an own surface the + // same way a solid background is. Without this branch a nav CTA + // built as `` with `background: linear-gradient(…)` and a text + // color that fails against every stop sails through on the + // SAFE_TAGS suppression (the shipped escape). + || (bgImage && /gradient/i.test(bgImage))) && fontSize >= 9; if (!isStyledControl) return []; } @@ -1805,22 +1820,129 @@ function isRoundDotRadius(radiusValue, w, h) { return px >= 999 || px >= 0.4 * Math.min(w, h); } +// Remove @media blocks whose condition is prefers-reduced-motion: reduce. +// Those blocks describe the accessibility fallback, not the default +// experience that ships — an `animation: none` reset inside one must not +// mask the resting-state animation the page plays for everyone else. +function stripReducedMotionBlocks(content) { + const re = /@media[^{]*prefers-reduced-motion\s*:\s*reduce[^{]*\{/gi; + let out = ''; + let last = 0; + let m; + while ((m = re.exec(content)) !== null) { + let depth = 1; + let i = re.lastIndex; + while (i < content.length && depth > 0) { + const ch = content.charCodeAt(i); + if (ch === 0x7b /* { */) depth++; + else if (ch === 0x7d /* } */) depth--; + i++; + } + out += content.slice(last, m.index); + last = i; + re.lastIndex = i; + } + return out + content.slice(last); +} + +// Source-index ranges of
and