Add icon-tile-stack rule and cross-validate engine against skill

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>
This commit is contained in:
Paul Bakaus
2026-04-06 22:58:13 -07:00
co-authored by Claude Opus 4.6
parent 83330a66cb
commit e1032b7285
27 changed files with 1026 additions and 30 deletions
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+2 -2
View File
@@ -2,7 +2,7 @@
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "impeccable",
"metadata": {
"description": "Design fluency for AI harnesses. 1 skill, 22 commands, and curated anti-patterns for impeccable frontend design."
"description": "Design fluency for AI harnesses. 1 skill, 21 commands, and curated anti-patterns for impeccable frontend design."
},
"owner": {
"name": "Paul Bakaus",
@@ -11,7 +11,7 @@
"plugins": [
{
"name": "impeccable",
"description": "Design vocabulary and skills for frontend development. Includes 22 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced impeccable skill with curated anti-patterns.",
"description": "Design vocabulary and skills for frontend development. Includes 21 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced impeccable skill with curated anti-patterns.",
"version": "2.0.6",
"author": {
"name": "Paul Bakaus",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"description": "Design vocabulary and skills for frontend development. Includes 22 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced impeccable skill with curated anti-patterns.",
"description": "Design vocabulary and skills for frontend development. Includes 21 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced impeccable skill with curated anti-patterns.",
"version": "2.0.6",
"author": {
"name": "Paul Bakaus",
+278
View File
@@ -0,0 +1,278 @@
---
name: anti-patterns
description: Use when adding, modifying, or debugging an anti-pattern detection rule in this repo. Walks through the TDD recipe, the rule schema, all five plug-in points, jsdom constraints, the cross-validation step against the impeccable skill, and the post-implementation checklist. Trigger this for any work touching src/detect-antipatterns.mjs, tests/fixtures/antipatterns/, or extension/detector/.
tools: Read, Edit, Write, Glob, Grep, Bash, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp
---
# Anti-Pattern Engine Maintenance
This agent handles every step of adding or modifying an anti-pattern detection rule in the impeccable repo. The rule engine is wired into many places and a single source-of-truth design ties them together. Skip any step at your peril — the build's cross-validator will fail loudly if drift slips in.
## The five things that need to stay in sync
When you add a rule, all of these update or get regenerated:
| Where | What | How it stays in sync |
|---|---|---|
| `src/detect-antipatterns.mjs` `ANTIPATTERNS` | Rule metadata (id, category, name, description, skillSection, skillGuideline) and the detection logic (`checkXxx`) | **Hand-edited.** Source of truth. |
| `src/detect-antipatterns-browser.js` | Browser-bundled engine for the public site overlay | Generated by `bun run build:browser` |
| `extension/detector/detect.js` | Browser-bundled engine for the Chrome extension | Generated by `bun run build:extension` |
| `extension/detector/antipatterns.json` | Rule list (id, name, category, description) for the extension's devtools panel — drives rule toggles UI | Generated by `bun run build:extension` |
| `public/js/generated/counts.js` | `DETECTION_COUNT` integer for homepage display | Generated by `bun run build` |
| `source/skills/impeccable/SKILL.md` | Per-rule **DON'T** line in the right `### Section` — taught to users via the impeccable skill | **Hand-edited.** Validator catches drift. |
The CLI (`bin/cli.js`) imports `ANTIPATTERNS` directly from `src/detect-antipatterns.mjs` — no separate sync needed.
## Rule schema
Each entry in the `ANTIPATTERNS` array (around src/detect-antipatterns.mjs:77) looks like this:
```js
{
id: 'icon-tile-stack', // kebab-case, unique, stable
category: 'slop', // 'slop' or 'quality' (see below)
name: 'Icon tile stacked above heading', // human-readable, used in extension UI
description: // 12 sentences. Used in CLI output, extension tooltips, web overlay labels
'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.',
skillSection: 'Typography', // OPTIONAL but strongly recommended. Must be one of the parser's allowed sections (see below)
skillGuideline: 'large icons with rounded corners above every heading', // OPTIONAL but strongly recommended. Substring that must appear in some **DON'T**: line of the named section in source/skills/impeccable/SKILL.md
}
```
### Categories
- **`slop`** = "AI tells". Patterns that scream *AI generated this*. Things like purple gradients, gradient text, dark glow accents, thick side borders, icon-tile-stacks. Flagging these is about taste and freshness, not correctness.
- **`quality`** = real design or accessibility issues regardless of who wrote the code. WCAG contrast, line length, padding, line height, justified text, skipped headings, etc.
If you're not sure, ask: *"would a human designer who's careful and tasteful still ship this?"* If no, it's `quality`. If they would (because it works fine, it just looks templated), it's `slop`.
### `skillSection` allowed values
These are the section names the `readPatterns()` parser at scripts/lib/utils.js:221 understands. Use **exactly** these strings (note the parser normalizes `Color & Theme``Color & Contrast`):
```
Typography
Color & Contrast
Layout & Space
Visual Details
Motion
Interaction
Responsive
UX Writing
```
### `skillGuideline` substring
A 36 word substring that appears verbatim in some `**DON'T**:` line of the named section in `source/skills/impeccable/SKILL.md`. The build validator checks this with `String.includes()`. Pick a substring that's:
- Short enough that benign rewordings of the DON'T won't break it
- Specific enough that it can't accidentally collide with an unrelated DON'T
Examples: `'AI color palette'`, `'large icons with rounded corners above every heading'`, `'WCAG AA contrast'`.
If a rule genuinely doesn't deserve a skill DON'T (rare — only the most niche a11y-only rules), omit both `skillSection` and `skillGuideline`. The validator skips rules without `skillGuideline`.
## The TDD recipe (always do it in this order)
This order is non-negotiable. Fixture and failing test before implementation. The full suite must run between the rule going in and you committing.
### 1. Write the fixture (two-column convention)
A single HTML file at `tests/fixtures/antipatterns/{rule-id}.html` with two columns: left = should-flag, right = should-pass. Each test case carries a unique heading text so the test can match snippets back to expectations.
Convention skeleton:
```html
<!DOCTYPE html>
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 960px; margin: 0 auto; padding: 24px; }
.col h2 { font-size: 14px; text-transform: uppercase; }
/* ... per-case styles with EXPLICIT pixel dimensions (jsdom can't lay out) ... */
</style>
</head>
<body>
<div class="grid">
<div class="col" data-col="flag">
<h2>Should flag</h2>
<!-- 46 cases that should be flagged, each with a unique <h3> text -->
</div>
<div class="col" data-col="pass">
<h2>Should pass</h2>
<!-- 58 cases that should NOT be flagged: cover every false-positive shape you can think of -->
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>
</body>
</html>
```
The script tag at the bottom is critical — it lets you load the fixture in the browser via `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` (served by `server/index.js:62` route for `/fixtures/*`).
**Should-pass cases must cover the false-positive shapes you can think of in advance.** A good fixture has 5+ pass cases. The icon-tile-stack fixture covers: round avatar, wide thumbnail, side-by-side, no-icon, too-tiny, too-huge.
### 2. Write the failing test
Add to `tests/detect-antipatterns-fixtures.test.mjs` in its own `describe` block. Use the snippet-substring matching pattern — the test parses heading text out of each finding's snippet and asserts membership against expected lists:
```js
describe('detectHtml — {rule-id}', () => {
const SHOULD_FLAG = ['Heading One', 'Heading Two', /* ... */];
const SHOULD_PASS = ['Pass Heading One', /* ... */];
it('{rule-id}: flags only the should-flag column', async () => {
const f = await detectHtml(path.join(FIXTURES, '{rule-id}.html'));
const flagged = new Set();
for (const r of f) {
if (r.antipattern !== '{rule-id}') continue;
const m = (r.snippet || '').match(/"([^"]+)"/);
if (m) flagged.add(m[1]);
}
for (const text of SHOULD_FLAG) {
assert.ok(flagged.has(text), `expected "${text}" to be flagged`);
}
for (const text of SHOULD_PASS) {
assert.ok(!flagged.has(text), `"${text}" should NOT be flagged`);
}
});
});
```
For this to work, the rule's snippet **must include the heading text in quotes**. See "Snippet conventions" below.
Run `node --test tests/detect-antipatterns-fixtures.test.mjs` and **watch it fail**. If it doesn't fail, your test is wrong.
### 3. Add the rule definition
Add a new entry to the `ANTIPATTERNS` array in `src/detect-antipatterns.mjs`. Place it in the right category section (slop or quality). Fill in all fields including `skillSection` and `skillGuideline`.
### 4. Implement the pure check function
Add a `checkXxx(opts)` function alongside the others (`checkColors`, `checkBorders`, `checkMotion`, `checkGlow`, `checkIconTile`, etc.). The pure function takes a plain options object — no DOM access — and returns an array of `{ id, snippet }`. This makes it testable and reusable across the browser/Node adapters.
Example shape (see `checkIconTile` in src/detect-antipatterns.mjs for a real one):
```js
function checkXxx(opts) {
const { tag, /* whatever fields the rule needs */ } = opts;
if (SAFE_TAGS.has(tag)) return [];
// ... your detection logic ...
if (matches) {
return [{ id: 'rule-id', snippet: `... "${headingText}"` }];
}
return [];
}
```
### 5. Add the two adapters
Two adapters wrap the pure function with environment-specific input gathering:
- **`checkElementXxxDOM(el)`** — for the browser. Uses `getComputedStyle(el)` and `el.getBoundingClientRect()`.
- **`checkElementXxx(el, tag, window)`** — for jsdom (Node). Uses `window.getComputedStyle(el)` and **must read explicit pixel dimensions from `parseFloat(style.width)`** instead of bounding rects, because **jsdom does not lay out**`getBoundingClientRect()` returns 0×0 for everything.
If your rule needs vertical positioning info (e.g. "icon must be above heading"), that check is browser-only — gate it behind `if (headingTop && siblingBottom)` so the Node path skips it. The structural checks alone (sizes, sibling identity, classes) are enough for the fixture.
### 6. Wire into both element-iteration loops
Two loops iterate every element on the page. You need to add your DOM-adapter call to **both**:
- **Browser loop** at src/detect-antipatterns.mjs:1837 (`for (const el of document.querySelectorAll('*'))` with the `findings` spread). Add a line like:
```js
...checkElementXxxDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
```
- **Node (jsdom) loop** at src/detect-antipatterns.mjs:2058 (in `detectHtml`). Add a block like:
```js
for (const f of checkElementXxx(el, tag, window)) {
findings.push(finding(f.id, filePath, f.snippet));
}
```
Forgetting one of these is the most common mistake — the test passes but the live page doesn't show anything (or vice versa).
### 7. Add the SKILL.md DON'T line if the rule doesn't reuse an existing one
Open `source/skills/impeccable/SKILL.md`, find the right `### Section`, and add a **DON'T** line that contains your `skillGuideline` substring verbatim. Match the style of existing DON'Ts (terse, prescriptive, 12 sentences max).
If your rule reuses an existing DON'T (e.g. multiple engine rules can map to the same skill guidance, like `side-tab` and `border-accent-on-rounded` both pointing to `'thick colored border on one side'`), no skill edit is needed.
### 8. Run the build (this regenerates everything and validates)
```bash
bun run build && bun run build:browser && bun run build:extension
```
This regenerates:
- `src/detect-antipatterns-browser.js` (public-site detector)
- `extension/detector/detect.js` (extension detector)
- `extension/detector/antipatterns.json` (extension rule list, includes description)
- `public/js/generated/counts.js` (DETECTION_COUNT)
And validates:
- Cross-checks every rule with `skillGuideline` against `source/skills/impeccable/SKILL.md` via `validateAntipatternRules()` in scripts/build.js. **Build fails if drift exists.**
### 9. Run the test suite
```bash
bun run test
```
166 unit tests + N fixture tests, including your new one. All should be green.
### 10. Verify on a live page in the browser
Don't skip this. The jsdom path uses `parseFloat(style.width)` and the browser path uses `getBoundingClientRect()` — they can disagree. The fixture test catches one path; manual browser verification catches the other.
```
http://localhost:3000/fixtures/antipatterns/{rule-id}.html
http://localhost:3000/antipattern-examples/{your-example}.html (if relevant)
http://localhost:3000/ (no false positives on real pages)
```
Use the chrome MCP tools (`mcp__claude-in-chrome__navigate` + `mcp__claude-in-chrome__javascript_tool`) to inject `window.impeccableScan()` and read `.impeccable-overlay` / `.impeccable-label` from the DOM to verify. Don't try to screenshot — the overlays are decorative; read them programmatically.
## Snippet conventions
The fixture-test convention extracts the heading text from a finding's snippet using regex `/"([^"]+)"/` — so **wrap the identifying heading text in straight double quotes** in your snippet. Examples:
- `'80x80px icon tile above h3 "Lightning Fast"'`
- `'4.5:1 (need 4.5:1) — text #808080 on #3b82f6'` ← uses element identifiers instead, since this rule isn't anchored to a heading
If your rule isn't naturally anchored to a heading, pick another stable identifier (a class name, the parent element's text, etc.) and document the test pattern in the test itself.
## jsdom constraints (the most common gotcha)
- **No layout.** `getBoundingClientRect()` returns `0×0` always. Read `parseFloat(style.width)` and `parseFloat(style.height)` instead — jsdom does honor explicit pixel widths in `<style>` and inline styles.
- **`background:` shorthand isn't decomposed.** `style.backgroundColor` and `style.backgroundImage` may be empty even when `style="background: ..."` is set. The existing `resolveBackground()` and `resolveGradientStops()` helpers (src/detect-antipatterns.mjs:631 and src/detect-antipatterns.mjs:670) handle this — use them.
- **Computed colors are normalized in real browsers, not in jsdom.** A browser returns `rgb(59, 130, 246)`; jsdom may return the original hex. The `parseGradientColors()` helper handles both.
- **No SAFE_TAGS skipping for parent walks.** When walking ancestors, you don't get the `SAFE_TAGS` filter the main loop applies — be explicit.
## Where to find concrete example rules to learn from
- Simplest border check: **`side-tab`** — `checkBorders()` at src/detect-antipatterns.mjs:312
- Color/contrast with gradient handling: **`low-contrast`** — `checkColors()` at src/detect-antipatterns.mjs:339
- Element-relationship check (siblings): **`icon-tile-stack`** — `checkIconTile()` at src/detect-antipatterns.mjs:425
- Page-level / cross-element: **`flat-type-hierarchy`** — `checkPageTypography()` at src/detect-antipatterns.mjs:1080
- Motion/animation: **`bounce-easing`** — `checkMotion()` at src/detect-antipatterns.mjs:425
## Pre-commit checklist
Before you commit a new rule, all of these MUST be true. The first three are non-negotiable — if any is missing, the engine, extension, public site, and skill will silently drift apart.
- [ ] Test passes: `bun run test` is green
- [ ] Build passes: `bun run build && bun run build:browser && bun run build:extension` is green (validator says `✓ Validated N/N anti-pattern rules`)
- [ ] Live verification: rule fires on a real page and produces zero false positives on the homepage `http://localhost:3000/`
- [ ] Both element loops were updated (browser DOM at line ~1846 + Node jsdom at line ~2058)
- [ ] Rule has a corresponding SKILL.md DON'T (or explicitly omitted `skillGuideline`)
- [ ] Snippet format matches the test's extraction regex
- [ ] Fixture covers ≥4 should-flag and ≥5 should-pass cases
- [ ] Commit only the relevant files — `git status` will show many unrelated stale skill builds; do not stage them
## Things that have bitten previous sessions
- **Forgot to run `bun run build:extension`** — extension JSON went stale, missing the new rule. Symptom: extension panel doesn't show toggle for new rule. Fix: always run all three build commands.
- **Forgot to update both loops** — test passed in jsdom but live browser was silent (or vice versa). Fix: grep for an existing rule's adapter call and copy its placement.
- **Used a `skillGuideline` substring that doesn't appear in SKILL.md** — validator fails. Fix: the substring must appear verbatim in some `**DON'T**:` line of the named section.
- **Used the wrong `skillSection` name** — `Color & Theme` vs `Color & Contrast` (parser normalizes the former to the latter, so use `Color & Contrast`).
- **Wrote the fixture without explicit pixel dimensions** — jsdom returned 0×0 and the rule never matched. Fix: always set `width: Npx; height: Npx` in CSS for fixture elements, or use inline style attributes.
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -55,6 +55,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -83,6 +86,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -54,6 +54,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -82,6 +85,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -53,6 +53,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -81,6 +84,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -54,6 +54,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -82,6 +85,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -54,6 +54,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -82,6 +85,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+1 -1
View File
@@ -1,6 +1,6 @@
# Impeccable
The vocabulary you didn't know you needed. 1 skill, 22 commands, and curated anti-patterns for impeccable style. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.
The vocabulary you didn't know you needed. 1 skill, 21 commands, and curated anti-patterns for impeccable style. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.
## Repository Purpose
+1 -1
View File
@@ -13,5 +13,5 @@ The `impeccable` skill in this project builds on Anthropic's original frontend-d
This project extends the original with:
- 7 domain-specific reference files (typography, color-and-contrast, spatial-design, motion-design, interaction-design, responsive-design, ux-writing)
- 22 steering commands
- 21 steering commands
- Expanded patterns and anti-patterns
+3 -3
View File
@@ -1,6 +1,6 @@
# Impeccable
The vocabulary you didn't know you needed. 1 skill, 22 commands, and curated anti-patterns for impeccable frontend design.
The vocabulary you didn't know you needed. 1 skill, 21 commands, and curated anti-patterns for impeccable frontend design.
> **Quick start:** Visit [impeccable.style](https://impeccable.style) to download ready-to-use bundles.
@@ -12,7 +12,7 @@ Every LLM learned from the same generic templates. Without guidance, you get the
Impeccable fights that bias with:
- **An expanded skill** with 7 domain-specific reference files ([view source](source/skills/impeccable/))
- **22 steering commands** to audit, review, polish, distill, animate, and more
- **21 steering commands** to audit, review, polish, distill, animate, and more
- **Curated anti-patterns** that explicitly tell the AI what NOT to do
## What's Included
@@ -31,7 +31,7 @@ A comprehensive design skill with 7 domain-specific references ([view skill](sou
| [responsive-design](source/skills/impeccable/reference/responsive-design.md) | Mobile-first, fluid design, container queries |
| [ux-writing](source/skills/impeccable/reference/ux-writing.md) | Button labels, error messages, empty states |
### 22 Commands
### 21 Commands
| Command | What it does |
|---------|--------------|
+1 -1
View File
@@ -169,7 +169,7 @@
<body>
<header>
<h1>Impeccable Commands</h1>
<p class="subtitle">Quick reference for all 22 design commands</p>
<p class="subtitle">Quick reference for all 21 design commands</p>
<a href="/" class="back-link">&larr; Back to impeccable.style</a>
</header>
+10 -10
View File
@@ -13,7 +13,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impeccable: The missing upgrade to Anthropic's impeccable skill</title>
<meta name="description" content="1 skill, 22 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta name="description" content="1 skill, 21 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style">
@@ -21,7 +21,7 @@
<meta property="og:type" content="website">
<meta property="og:url" content="https://impeccable.style">
<meta property="og:title" content="Impeccable: Design skills for AI harnesses">
<meta property="og:description" content="1 skill, 22 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta property="og:description" content="1 skill, 21 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta property="og:image" content="https://impeccable.style/og-image.jpg">
<!-- Twitter -->
@@ -29,7 +29,7 @@
<meta name="twitter:site" content="@pbakaus">
<meta name="twitter:creator" content="@pbakaus">
<meta name="twitter:title" content="Impeccable: Design skills for AI harnesses">
<meta name="twitter:description" content="1 skill, 22 commands, and curated anti-patterns for impeccable frontend design.">
<meta name="twitter:description" content="1 skill, 21 commands, and curated anti-patterns for impeccable frontend design.">
<meta name="twitter:image" content="https://impeccable.style/og-image.jpg">
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
@@ -70,15 +70,15 @@
<h1 class="hero-title-combined">Impeccable</h1>
<p class="hero-tagline-combined">Design fluency for AI harnesses</p>
<p class="hero-hook-text hero-hook-text--full">Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI 212 design guidelines and gives you 22 commands to steer the result.</p>
<p class="hero-hook-text hero-hook-text--short">Impeccable teaches your AI real design and gives you 22 commands to steer the result.</p>
<p class="hero-hook-text hero-hook-text--full">Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI deep design knowledge and gives you 21 commands to steer the result.</p>
<p class="hero-hook-text hero-hook-text--short">Impeccable teaches your AI real design and gives you 21 commands to steer the result.</p>
<div class="hero-included-box">
<span class="hero-included-title">What's included</span>
<div class="hero-included-items">
<span>Enhanced <em>impeccable</em> skill + anti-patterns</span>
<span class="hero-included-sep">·</span>
<span>22 design commands: /polish, /audit, /typeset, /overdrive...</span>
<span>21 design commands: /polish, /audit, /typeset, /overdrive...</span>
</div>
</div>
@@ -165,7 +165,7 @@
<h2 class="section-title">The Foundation</h2>
</div>
<div class="foundation-content">
<p class="section-lead" data-reveal>Before commands, before detection, Impeccable teaches your AI real design. 212 guidelines across 7 dimensions, loaded as deep reference knowledge every time your AI writes code.</p>
<p class="section-lead" data-reveal>Before commands, before detection, Impeccable teaches your AI real design. Deep reference knowledge across 7 dimensions, loaded every time your AI writes code.</p>
<div class="foundation-grid" data-reveal>
<!-- 7 dimension cards, rendered by JS -->
@@ -185,7 +185,7 @@
<h2 class="section-title">The Language</h2>
</div>
<div class="language-content">
<p class="section-lead" data-reveal>22 commands form a shared vocabulary between you and your AI. Each one encodes a specific design discipline, so you can steer with precision.</p>
<p class="section-lead" data-reveal>21 commands form a shared vocabulary between you and your AI. Each one encodes a specific design discipline, so you can steer with precision.</p>
<div class="solution-visual-interactive" id="framework-viz-container" data-reveal>
<!-- Periodic table generated by JS -->
@@ -282,7 +282,7 @@
</div>
<div class="visual-mode-details">
<div class="visual-mode-feature">
<span class="visual-mode-feature-label">24 deterministic checks</span>
<span class="visual-mode-feature-label">25 deterministic checks</span>
<p>No LLM needed. Pattern matching catches purple gradients, overused fonts, nested cards, low contrast, and more.</p>
</div>
<div class="visual-mode-feature">
@@ -312,7 +312,7 @@
<div class="install-path install-path-primary">
<span class="install-path-label"><span class="install-path-step">1</span></span>
<h3 class="install-path-title">Install the skills <span class="install-path-badge">Recommended</span></h3>
<p class="install-path-desc">22 commands that steer your AI toward better design, in real time. The full Impeccable experience.</p>
<p class="install-path-desc">21 commands that steer your AI toward better design, in real time. The full Impeccable experience.</p>
<div class="install-path-terminal">
<div class="glass-terminal">
+1 -1
View File
@@ -1,3 +1,3 @@
// GENERATED by build.js — do not edit
export const COMMAND_COUNT = 21;
export const DETECTION_COUNT = 24;
export const DETECTION_COUNT = 25;
+9 -2
View File
@@ -57,9 +57,16 @@ console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length
const rawSource = fs.readFileSync(SOURCE, 'utf-8');
const apMatch = rawSource.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (apMatch) {
// Convert JS object literals to JSON
// Convert JS object literals to JSON. Include description so the
// devtools panel can show the full rule explanation in tooltips —
// previously this dropped description and the panel had nothing to display.
const antipatterns = new Function(`return [${apMatch[1]}]`)();
const apJson = antipatterns.map(({ id, name, category }) => ({ id, name, category: category || 'quality' }));
const apJson = antipatterns.map(({ id, name, category, description }) => ({
id,
name,
category: category || 'quality',
description: description || '',
}));
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${antipatterns.length} rules)`);
}
+72 -8
View File
@@ -64,7 +64,7 @@ function generateCounts(rootDir, skills, buildDir) {
'.claude-plugin/marketplace.json',
];
let warnings = 0;
let errors = 0;
for (const relPath of filesToCheck) {
const absPath = path.join(rootDir, relPath);
if (!fs.existsSync(absPath)) continue;
@@ -78,8 +78,8 @@ function generateCounts(rootDir, skills, buildDir) {
const num = parseInt(match[1]);
// Allow 1 (for "1 skill") and the correct count
if (num !== commandCount && num !== 1) {
console.warn(` ⚠️ ${relPath}: found "${match[0]}" but active command count is ${commandCount}`);
warnings++;
console.error(` ${relPath}: found "${match[0]}" but active command count is ${commandCount}`);
errors++;
}
}
@@ -88,17 +88,75 @@ function generateCounts(rootDir, skills, buildDir) {
for (const match of content.matchAll(detectPattern)) {
const num = parseInt(match[1]);
if (num !== detectionCount && num > 10) { // ignore small numbers like "3 patterns"
console.warn(` ⚠️ ${relPath}: found "${match[0]}" but detection count is ${detectionCount}`);
warnings++;
console.error(` ${relPath}: found "${match[0]}" but detection count is ${detectionCount}`);
errors++;
}
}
}
if (warnings > 0) {
console.warn(`\n⚠️ ${warnings} stale count reference(s) found. Update them to match source of truth.`);
if (errors > 0) {
console.error(`\n ${errors} stale count reference(s) found. Update them to match source of truth.`);
}
console.log(`✓ Generated counts: ${commandCount} commands, ${detectionCount} detection rules`);
return errors;
}
/**
* Cross-validate that every detection rule with a `skillGuideline` has a
* matching DON'T line in the right section of source/skills/impeccable/SKILL.md.
*
* This is the linchpin of the single-source-of-truth design: it catches drift
* between the engine's ANTIPATTERNS and the human-written DO/DON'T prose.
*
* Returns the number of validation errors. Build fails if > 0.
*/
function validateAntipatternRules(rootDir) {
const detectPath = path.join(rootDir, 'src/detect-antipatterns.mjs');
const src = fs.readFileSync(detectPath, 'utf-8');
const apMatch = src.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (!apMatch) {
console.error(' ❌ Could not extract ANTIPATTERNS from detect-antipatterns.mjs');
return 1;
}
const antipatterns = new Function(`return [${apMatch[1]}]`)();
const { antipatterns: skillSections } = readPatterns(rootDir);
// Build section -> joined-DON'T-text lookup for substring matching
const sectionText = {};
for (const section of skillSections) {
sectionText[section.name] = section.items.join('\n');
}
let errors = 0;
let validated = 0;
for (const rule of antipatterns) {
if (!rule.skillGuideline) continue;
if (!rule.skillSection) {
console.error(` ❌ Rule '${rule.id}' declares skillGuideline but no skillSection`);
errors++;
continue;
}
const text = sectionText[rule.skillSection];
if (!text) {
console.error(` ❌ Rule '${rule.id}': skillSection '${rule.skillSection}' has no DON'T lines in source/skills/impeccable/SKILL.md`);
errors++;
continue;
}
if (!text.includes(rule.skillGuideline)) {
console.error(` ❌ Rule '${rule.id}': skillGuideline '${rule.skillGuideline}' not found in any **DON'T** of section '${rule.skillSection}' in source/skills/impeccable/SKILL.md`);
errors++;
continue;
}
validated++;
}
if (errors > 0) {
console.error(`\n${errors} anti-pattern rule(s) drift between src/detect-antipatterns.mjs and source/skills/impeccable/SKILL.md`);
} else {
console.log(`✓ Validated ${validated}/${antipatterns.length} anti-pattern rules against impeccable SKILL.md`);
}
return errors;
}
/**
@@ -441,7 +499,13 @@ async function build() {
// Generate authoritative counts and validate references
generateCounts(ROOT_DIR, skills, buildDir);
const countErrors = generateCounts(ROOT_DIR, skills, buildDir);
// Cross-validate engine rules against impeccable SKILL.md DON'Ts
const validationErrors = validateAntipatternRules(ROOT_DIR);
if (countErrors > 0 || validationErrors > 0) {
process.exit(1);
}
console.log('\n✨ Build complete!');
}
+4
View File
@@ -56,6 +56,9 @@ Choose fonts that are beautiful, unique, and interesting. Pair a distinctive dis
**DON'T**: Use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading. They rarely add value and make sites look templated
**DON'T**: Use only one font family for the entire page. Pair a distinctive display font with a refined body font
**DON'T**: Use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps
**DON'T**: Set long body passages in uppercase. Reserve all-caps for short labels and headings
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
@@ -84,6 +87,7 @@ Create visual rhythm through varied spacing, not the same padding everywhere. Em
**DON'T**: Use the hero metric layout template (big number, small label, supporting stats, gradient accent)
**DON'T**: Center everything. Left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere. Without rhythm, layouts feel monotonous
**DON'T**: Let body text wrap beyond ~80 characters per line. Add a max-width like 6575ch so the eye can track easily
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
+164
View File
@@ -87,6 +87,8 @@ const ANTIPATTERNS = [
name: 'Side-tab accent border',
description:
'Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.',
skillSection: 'Visual Details',
skillGuideline: 'thick colored border on one side',
},
{
id: 'border-accent-on-rounded',
@@ -94,6 +96,8 @@ const ANTIPATTERNS = [
name: 'Border accent on rounded element',
description:
'Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.',
skillSection: 'Visual Details',
skillGuideline: 'thick colored border on one side',
},
{
id: 'overused-font',
@@ -101,6 +105,8 @@ const ANTIPATTERNS = [
name: 'Overused font',
description:
'Inter, Roboto, Open Sans, Lato, Montserrat, and Arial are used on millions of sites. Choose a distinctive font that gives your interface personality.',
skillSection: 'Typography',
skillGuideline: 'overused fonts like Inter',
},
{
id: 'single-font',
@@ -108,6 +114,8 @@ const ANTIPATTERNS = [
name: 'Single font for everything',
description:
'Only one font family is used for the entire page. Pair a distinctive display font with a refined body font to create typographic hierarchy.',
skillSection: 'Typography',
skillGuideline: 'only one font family for the entire page',
},
{
id: 'flat-type-hierarchy',
@@ -115,6 +123,8 @@ const ANTIPATTERNS = [
name: 'Flat type hierarchy',
description:
'Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).',
skillSection: 'Typography',
skillGuideline: 'flat type hierarchy',
},
{
id: 'gradient-text',
@@ -122,6 +132,8 @@ const ANTIPATTERNS = [
name: 'Gradient text',
description:
'Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.',
skillSection: 'Color & Contrast',
skillGuideline: 'gradient text for',
},
{
id: 'ai-color-palette',
@@ -129,6 +141,8 @@ const ANTIPATTERNS = [
name: 'AI color palette',
description:
'Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.',
skillSection: 'Color & Contrast',
skillGuideline: 'AI color palette',
},
{
id: 'nested-cards',
@@ -136,6 +150,8 @@ const ANTIPATTERNS = [
name: 'Nested cards',
description:
'Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.',
skillSection: 'Layout & Space',
skillGuideline: 'Nest cards inside cards',
},
{
id: 'monotonous-spacing',
@@ -143,6 +159,8 @@ const ANTIPATTERNS = [
name: 'Monotonous spacing',
description:
'The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.',
skillSection: 'Layout & Space',
skillGuideline: 'same spacing everywhere',
},
{
id: 'everything-centered',
@@ -150,6 +168,8 @@ const ANTIPATTERNS = [
name: 'Everything centered',
description:
'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.',
skillSection: 'Layout & Space',
skillGuideline: 'Center everything',
},
{
id: 'bounce-easing',
@@ -157,6 +177,8 @@ const ANTIPATTERNS = [
name: 'Bounce or elastic easing',
description:
'Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.',
skillSection: 'Motion',
skillGuideline: 'bounce or elastic easing',
},
{
id: 'dark-glow',
@@ -164,6 +186,17 @@ const ANTIPATTERNS = [
name: 'Dark mode with glowing accents',
description:
'Dark backgrounds with colored box-shadow glows are the default "cool" look of AI-generated UIs. Use subtle, purposeful lighting instead — or skip the dark theme entirely.',
skillSection: 'Color & Contrast',
skillGuideline: 'dark mode with glowing accents',
},
{
id: 'icon-tile-stack',
category: 'slop',
name: 'Icon tile stacked above heading',
description:
'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.',
skillSection: 'Typography',
skillGuideline: 'large icons with rounded corners above every heading',
},
// ── Quality: general design and accessibility issues ──
@@ -173,6 +206,8 @@ const ANTIPATTERNS = [
name: 'Pure black background',
description:
'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.',
skillSection: 'Color & Contrast',
skillGuideline: 'pure black (#000)',
},
{
id: 'gray-on-color',
@@ -180,6 +215,8 @@ const ANTIPATTERNS = [
name: 'Gray text on colored background',
description:
'Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.',
skillSection: 'Color & Contrast',
skillGuideline: 'gray text on colored backgrounds',
},
{
id: 'low-contrast',
@@ -194,6 +231,8 @@ const ANTIPATTERNS = [
name: 'Layout property animation',
description:
'Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.',
skillSection: 'Motion',
skillGuideline: 'Animate layout properties',
},
{
id: 'line-length',
@@ -201,6 +240,8 @@ const ANTIPATTERNS = [
name: 'Line length too long',
description:
'Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.',
skillSection: 'Layout & Space',
skillGuideline: 'wrap beyond ~80 characters',
},
{
id: 'cramped-padding',
@@ -243,6 +284,8 @@ const ANTIPATTERNS = [
name: 'All-caps body text',
description:
'Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.',
skillSection: 'Typography',
skillGuideline: 'long body passages in uppercase',
},
{
id: 'wide-tracking',
@@ -437,6 +480,62 @@ function isCardLikeFromProps(hasShadow, hasBorder, hasRadius, hasBg) {
return hasRadius || hasBg;
}
const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
// Pure check: given a heading and metrics about its previousElementSibling,
// decide if the sibling is the canonical "icon-tile-stacked-above-heading" shape.
//
// Triggers when ALL of the following hold for the sibling:
// • size 32128px on both axes (not too small, not a hero image)
// • aspect ratio 0.71.4 (squarish — excludes wide thumbnails / pill badges)
// • has a non-transparent background-color, background-image, OR a visible border
// (covers solid colors, white-with-border, gradients — anything that visually
// defines a tile)
// • border-radius < width/2 (excludes round avatars; rounded squares pass)
// • contains an <svg> or icon-class <i> element that's smaller than the tile
// • the tile sits above the heading (its bottom is above the heading's top)
function checkIconTile(opts) {
const { headingTag, headingText, headingTop,
siblingTag, siblingWidth, siblingHeight, siblingBottom,
siblingBgColor, siblingBgImage, siblingBorderWidth, siblingBorderRadius,
hasIconChild, iconChildWidth } = opts;
if (!HEADING_TAGS.has(headingTag)) return [];
if (!siblingTag) return [];
// Don't recurse into nested headings (e.g. h2 above h3 in a section header)
if (HEADING_TAGS.has(siblingTag)) return [];
// Size window: 32128px on each axis
if (!(siblingWidth >= 32 && siblingWidth <= 128)) return [];
if (!(siblingHeight >= 32 && siblingHeight <= 128)) return [];
// Squarish aspect ratio
const ratio = siblingWidth / siblingHeight;
if (ratio < 0.7 || ratio > 1.4) return [];
// Must have something that visually defines the tile
const bgVisible = (siblingBgColor && siblingBgColor.a > 0.1)
|| (siblingBgImage && siblingBgImage !== 'none' && siblingBgImage !== '');
const borderVisible = siblingBorderWidth > 0;
if (!bgVisible && !borderVisible) return [];
// Exclude circles (avatars). Rounded squares pass.
if (siblingBorderRadius >= siblingWidth / 2) return [];
// Must contain an icon element smaller than the tile
if (!hasIconChild) return [];
if (iconChildWidth && iconChildWidth >= siblingWidth * 0.95) return [];
// Vertical stacking: tile must end above where the heading starts.
// (Allow the check to skip when both top/bottom are 0 — jsdom layout case.)
if (headingTop && siblingBottom && siblingBottom > headingTop + 4) return [];
const text = (headingText || '').trim().slice(0, 60);
return [{
id: 'icon-tile-stack',
snippet: `${Math.round(siblingWidth)}x${Math.round(siblingHeight)}px icon tile above ${headingTag} "${text}"`,
}];
}
const LAYOUT_TRANSITION_PROPS = new Set([
'width', 'height', 'padding', 'margin',
'max-height', 'max-width', 'min-height', 'min-width',
@@ -758,6 +857,36 @@ function checkElementColorsDOM(el) {
});
}
function checkElementIconTileDOM(el) {
const tag = el.tagName.toLowerCase();
if (!HEADING_TAGS.has(tag)) return [];
const sibling = el.previousElementSibling;
if (!sibling) return [];
const sibRect = sibling.getBoundingClientRect();
const headRect = el.getBoundingClientRect();
const sibStyle = getComputedStyle(sibling);
const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
const iconRect = iconChild?.getBoundingClientRect();
return checkIconTile({
headingTag: tag,
headingText: el.textContent || '',
headingTop: headRect.top,
siblingTag: sibling.tagName.toLowerCase(),
siblingWidth: sibRect.width,
siblingHeight: sibRect.height,
siblingBottom: sibRect.bottom,
siblingBgColor: parseRgb(sibStyle.backgroundColor),
siblingBgImage: sibStyle.backgroundImage || '',
siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
hasIconChild: !!iconChild,
iconChildWidth: iconRect?.width || 0,
});
}
function checkElementMotionDOM(el) {
const tag = el.tagName.toLowerCase();
if (SAFE_TAGS.has(tag)) return [];
@@ -1010,6 +1139,40 @@ function checkElementColors(el, style, tag, window) {
});
}
function checkElementIconTile(el, tag, window) {
if (!HEADING_TAGS.has(tag)) return [];
const sibling = el.previousElementSibling;
if (!sibling) return [];
const sibStyle = window.getComputedStyle(sibling);
// jsdom doesn't lay out — read explicit pixel dimensions from CSS instead.
const sibWidth = parseFloat(sibStyle.width) || 0;
const sibHeight = parseFloat(sibStyle.height) || 0;
const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
let iconWidth = 0;
if (iconChild) {
const iconStyle = window.getComputedStyle(iconChild);
iconWidth = parseFloat(iconStyle.width) || parseFloat(iconChild.getAttribute('width')) || 0;
}
return checkIconTile({
headingTag: tag,
headingText: el.textContent || '',
headingTop: 0, // jsdom: no layout, skip vertical-stacking gate
siblingTag: sibling.tagName.toLowerCase(),
siblingWidth: sibWidth,
siblingHeight: sibHeight,
siblingBottom: 0,
siblingBgColor: parseRgb(sibStyle.backgroundColor),
siblingBgImage: sibStyle.backgroundImage || '',
siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
hasIconChild: !!iconChild,
iconChildWidth: iconWidth,
});
}
function checkElementMotion(tag, style) {
return checkMotion({
tag,
@@ -1854,6 +2017,7 @@ if (IS_BROWSER) {
...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
].filter(f => _ruleOk(f.type));
+167
View File
@@ -82,6 +82,8 @@ const ANTIPATTERNS = [
name: 'Side-tab accent border',
description:
'Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.',
skillSection: 'Visual Details',
skillGuideline: 'thick colored border on one side',
},
{
id: 'border-accent-on-rounded',
@@ -89,6 +91,8 @@ const ANTIPATTERNS = [
name: 'Border accent on rounded element',
description:
'Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.',
skillSection: 'Visual Details',
skillGuideline: 'thick colored border on one side',
},
{
id: 'overused-font',
@@ -96,6 +100,8 @@ const ANTIPATTERNS = [
name: 'Overused font',
description:
'Inter, Roboto, Open Sans, Lato, Montserrat, and Arial are used on millions of sites. Choose a distinctive font that gives your interface personality.',
skillSection: 'Typography',
skillGuideline: 'overused fonts like Inter',
},
{
id: 'single-font',
@@ -103,6 +109,8 @@ const ANTIPATTERNS = [
name: 'Single font for everything',
description:
'Only one font family is used for the entire page. Pair a distinctive display font with a refined body font to create typographic hierarchy.',
skillSection: 'Typography',
skillGuideline: 'only one font family for the entire page',
},
{
id: 'flat-type-hierarchy',
@@ -110,6 +118,8 @@ const ANTIPATTERNS = [
name: 'Flat type hierarchy',
description:
'Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).',
skillSection: 'Typography',
skillGuideline: 'flat type hierarchy',
},
{
id: 'gradient-text',
@@ -117,6 +127,8 @@ const ANTIPATTERNS = [
name: 'Gradient text',
description:
'Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.',
skillSection: 'Color & Contrast',
skillGuideline: 'gradient text for',
},
{
id: 'ai-color-palette',
@@ -124,6 +136,8 @@ const ANTIPATTERNS = [
name: 'AI color palette',
description:
'Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.',
skillSection: 'Color & Contrast',
skillGuideline: 'AI color palette',
},
{
id: 'nested-cards',
@@ -131,6 +145,8 @@ const ANTIPATTERNS = [
name: 'Nested cards',
description:
'Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.',
skillSection: 'Layout & Space',
skillGuideline: 'Nest cards inside cards',
},
{
id: 'monotonous-spacing',
@@ -138,6 +154,8 @@ const ANTIPATTERNS = [
name: 'Monotonous spacing',
description:
'The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.',
skillSection: 'Layout & Space',
skillGuideline: 'same spacing everywhere',
},
{
id: 'everything-centered',
@@ -145,6 +163,8 @@ const ANTIPATTERNS = [
name: 'Everything centered',
description:
'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.',
skillSection: 'Layout & Space',
skillGuideline: 'Center everything',
},
{
id: 'bounce-easing',
@@ -152,6 +172,8 @@ const ANTIPATTERNS = [
name: 'Bounce or elastic easing',
description:
'Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.',
skillSection: 'Motion',
skillGuideline: 'bounce or elastic easing',
},
{
id: 'dark-glow',
@@ -159,6 +181,17 @@ const ANTIPATTERNS = [
name: 'Dark mode with glowing accents',
description:
'Dark backgrounds with colored box-shadow glows are the default "cool" look of AI-generated UIs. Use subtle, purposeful lighting instead — or skip the dark theme entirely.',
skillSection: 'Color & Contrast',
skillGuideline: 'dark mode with glowing accents',
},
{
id: 'icon-tile-stack',
category: 'slop',
name: 'Icon tile stacked above heading',
description:
'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.',
skillSection: 'Typography',
skillGuideline: 'large icons with rounded corners above every heading',
},
// ── Quality: general design and accessibility issues ──
@@ -168,6 +201,8 @@ const ANTIPATTERNS = [
name: 'Pure black background',
description:
'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.',
skillSection: 'Color & Contrast',
skillGuideline: 'pure black (#000)',
},
{
id: 'gray-on-color',
@@ -175,6 +210,8 @@ const ANTIPATTERNS = [
name: 'Gray text on colored background',
description:
'Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.',
skillSection: 'Color & Contrast',
skillGuideline: 'gray text on colored backgrounds',
},
{
id: 'low-contrast',
@@ -189,6 +226,8 @@ const ANTIPATTERNS = [
name: 'Layout property animation',
description:
'Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.',
skillSection: 'Motion',
skillGuideline: 'Animate layout properties',
},
{
id: 'line-length',
@@ -196,6 +235,8 @@ const ANTIPATTERNS = [
name: 'Line length too long',
description:
'Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.',
skillSection: 'Layout & Space',
skillGuideline: 'wrap beyond ~80 characters',
},
{
id: 'cramped-padding',
@@ -238,6 +279,8 @@ const ANTIPATTERNS = [
name: 'All-caps body text',
description:
'Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.',
skillSection: 'Typography',
skillGuideline: 'long body passages in uppercase',
},
{
id: 'wide-tracking',
@@ -432,6 +475,62 @@ function isCardLikeFromProps(hasShadow, hasBorder, hasRadius, hasBg) {
return hasRadius || hasBg;
}
const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
// Pure check: given a heading and metrics about its previousElementSibling,
// decide if the sibling is the canonical "icon-tile-stacked-above-heading" shape.
//
// Triggers when ALL of the following hold for the sibling:
// • size 32128px on both axes (not too small, not a hero image)
// • aspect ratio 0.71.4 (squarish — excludes wide thumbnails / pill badges)
// • has a non-transparent background-color, background-image, OR a visible border
// (covers solid colors, white-with-border, gradients — anything that visually
// defines a tile)
// • border-radius < width/2 (excludes round avatars; rounded squares pass)
// • contains an <svg> or icon-class <i> element that's smaller than the tile
// • the tile sits above the heading (its bottom is above the heading's top)
function checkIconTile(opts) {
const { headingTag, headingText, headingTop,
siblingTag, siblingWidth, siblingHeight, siblingBottom,
siblingBgColor, siblingBgImage, siblingBorderWidth, siblingBorderRadius,
hasIconChild, iconChildWidth } = opts;
if (!HEADING_TAGS.has(headingTag)) return [];
if (!siblingTag) return [];
// Don't recurse into nested headings (e.g. h2 above h3 in a section header)
if (HEADING_TAGS.has(siblingTag)) return [];
// Size window: 32128px on each axis
if (!(siblingWidth >= 32 && siblingWidth <= 128)) return [];
if (!(siblingHeight >= 32 && siblingHeight <= 128)) return [];
// Squarish aspect ratio
const ratio = siblingWidth / siblingHeight;
if (ratio < 0.7 || ratio > 1.4) return [];
// Must have something that visually defines the tile
const bgVisible = (siblingBgColor && siblingBgColor.a > 0.1)
|| (siblingBgImage && siblingBgImage !== 'none' && siblingBgImage !== '');
const borderVisible = siblingBorderWidth > 0;
if (!bgVisible && !borderVisible) return [];
// Exclude circles (avatars). Rounded squares pass.
if (siblingBorderRadius >= siblingWidth / 2) return [];
// Must contain an icon element smaller than the tile
if (!hasIconChild) return [];
if (iconChildWidth && iconChildWidth >= siblingWidth * 0.95) return [];
// Vertical stacking: tile must end above where the heading starts.
// (Allow the check to skip when both top/bottom are 0 — jsdom layout case.)
if (headingTop && siblingBottom && siblingBottom > headingTop + 4) return [];
const text = (headingText || '').trim().slice(0, 60);
return [{
id: 'icon-tile-stack',
snippet: `${Math.round(siblingWidth)}x${Math.round(siblingHeight)}px icon tile above ${headingTag} "${text}"`,
}];
}
const LAYOUT_TRANSITION_PROPS = new Set([
'width', 'height', 'padding', 'margin',
'max-height', 'max-width', 'min-height', 'min-width',
@@ -753,6 +852,36 @@ function checkElementColorsDOM(el) {
});
}
function checkElementIconTileDOM(el) {
const tag = el.tagName.toLowerCase();
if (!HEADING_TAGS.has(tag)) return [];
const sibling = el.previousElementSibling;
if (!sibling) return [];
const sibRect = sibling.getBoundingClientRect();
const headRect = el.getBoundingClientRect();
const sibStyle = getComputedStyle(sibling);
const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
const iconRect = iconChild?.getBoundingClientRect();
return checkIconTile({
headingTag: tag,
headingText: el.textContent || '',
headingTop: headRect.top,
siblingTag: sibling.tagName.toLowerCase(),
siblingWidth: sibRect.width,
siblingHeight: sibRect.height,
siblingBottom: sibRect.bottom,
siblingBgColor: parseRgb(sibStyle.backgroundColor),
siblingBgImage: sibStyle.backgroundImage || '',
siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
hasIconChild: !!iconChild,
iconChildWidth: iconRect?.width || 0,
});
}
function checkElementMotionDOM(el) {
const tag = el.tagName.toLowerCase();
if (SAFE_TAGS.has(tag)) return [];
@@ -1005,6 +1134,40 @@ function checkElementColors(el, style, tag, window) {
});
}
function checkElementIconTile(el, tag, window) {
if (!HEADING_TAGS.has(tag)) return [];
const sibling = el.previousElementSibling;
if (!sibling) return [];
const sibStyle = window.getComputedStyle(sibling);
// jsdom doesn't lay out — read explicit pixel dimensions from CSS instead.
const sibWidth = parseFloat(sibStyle.width) || 0;
const sibHeight = parseFloat(sibStyle.height) || 0;
const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
let iconWidth = 0;
if (iconChild) {
const iconStyle = window.getComputedStyle(iconChild);
iconWidth = parseFloat(iconStyle.width) || parseFloat(iconChild.getAttribute('width')) || 0;
}
return checkIconTile({
headingTag: tag,
headingText: el.textContent || '',
headingTop: 0, // jsdom: no layout, skip vertical-stacking gate
siblingTag: sibling.tagName.toLowerCase(),
siblingWidth: sibWidth,
siblingHeight: sibHeight,
siblingBottom: 0,
siblingBgColor: parseRgb(sibStyle.backgroundColor),
siblingBgImage: sibStyle.backgroundImage || '',
siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
hasIconChild: !!iconChild,
iconChildWidth: iconWidth,
});
}
function checkElementMotion(tag, style) {
return checkMotion({
tag,
@@ -1849,6 +2012,7 @@ if (IS_BROWSER) {
...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
].filter(f => _ruleOk(f.type));
@@ -2066,6 +2230,9 @@ async function detectHtml(filePath) {
for (const f of checkElementMotion(tag, style)) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of checkElementIconTile(el, tag, window)) {
findings.push(finding(f.id, filePath, f.snippet));
}
}
// Page-level checks (only for full pages, not partials)
@@ -82,6 +82,45 @@ describe('detectHtml — jsdom fixtures', () => {
});
});
describe('detectHtml — icon-tile-stack', () => {
// Two-column fixture convention: left col = should-flag, right col = should-pass.
// The rule's snippet embeds the heading text in quotes, e.g.
// "80x80px icon tile above h3 \"Lightning Fast\"".
// The test extracts those quoted texts and matches them against the
// expected lists below.
const SHOULD_FLAG = [
'Lightning Fast',
'Secure Storage',
'Easy Setup',
'Powerful Analytics',
];
const SHOULD_PASS = [
'Sarah Chen',
'Article Headline',
'Inline Side By Side',
'Plain Heading No Icon',
'Tiny Icon Above Me',
'Huge Hero Image',
];
it('icon-tile-stack: flags only the should-flag column', async () => {
const f = await detectHtml(path.join(FIXTURES, 'icon-tile-stack.html'));
const flagged = new Set();
for (const r of f) {
if (r.antipattern !== 'icon-tile-stack') continue;
const m = (r.snippet || '').match(/"([^"]+)"/);
if (m) flagged.add(m[1]);
}
for (const text of SHOULD_FLAG) {
assert.ok(flagged.has(text), `expected "${text}" to be flagged as icon-tile-stack`);
}
for (const text of SHOULD_PASS) {
assert.ok(!flagged.has(text), `"${text}" should NOT be flagged as icon-tile-stack`);
}
});
});
describe('detectHtml — layout fixtures', () => {
it('layout-should-flag: detects nested cards', async () => {
const f = await detectHtml(path.join(FIXTURES, 'layout-should-flag.html'));
+229
View File
@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Icon-Tile-Stack — Should Flag vs Should Pass</title>
<style>
/* ─── Layout convention: two-column fixture ─────────────────────────
Left column = patterns the rule SHOULD flag.
Right column = legitimate patterns the rule should NOT flag.
Each test case is annotated with a unique <h3> text so the test
can match snippets back to expectations.
──────────────────────────────────────────────────────────────── */
body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 960px; margin: 0 auto; }
.col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }
.case { margin-bottom: 24px; padding: 16px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }
.case h3 { font-size: 18px; margin: 0 0 4px; color: #0f172a; }
.case p { font-size: 14px; margin: 0; color: #64748b; }
/* ── FLAG cases ── */
/* 1: pastel-gradient icon-tile (the canonical massive-icons.html shape) */
.icon-pastel {
width: 80px; height: 80px;
border-radius: 20px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
display: flex; align-items: center; justify-content: center;
margin-bottom: 16px;
}
/* 2: solid-color icon-tile, white svg inside (no gradient) */
.icon-solid {
width: 64px; height: 64px;
border-radius: 16px;
background: #3b82f6;
display: flex; align-items: center; justify-content: center;
margin-bottom: 16px;
}
/* 3: white-bg outlined icon-tile (no chromatic background at all) */
.icon-white {
width: 72px; height: 72px;
border-radius: 18px;
background: #ffffff;
border: 1px solid #e2e8f0;
display: flex; align-items: center; justify-content: center;
margin-bottom: 16px;
}
/* 4: larger square (still inside the 32128 range) */
.icon-large {
width: 96px; height: 96px;
border-radius: 24px;
background: #f1f5f9;
display: flex; align-items: center; justify-content: center;
margin-bottom: 16px;
}
/* ── PASS cases ── */
/* a: round avatar above name (border-radius: 50% — circle, not rounded square) */
.avatar-round {
width: 64px; height: 64px;
border-radius: 50%;
background: #f59e0b;
display: flex; align-items: center; justify-content: center;
margin-bottom: 16px;
}
/* b: wide thumbnail above article title (16:9, much wider than tall) */
.thumb-wide {
width: 200px; height: 112px;
border-radius: 8px;
background: #cbd5e1;
margin-bottom: 16px;
}
/* c: inline icon next to heading (icon and h3 on the same row, NOT stacked) */
.inline-row { display: flex; align-items: center; gap: 12px; }
.inline-row .icon-inline {
width: 80px; height: 80px;
border-radius: 20px;
background: #ddd6fe;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
/* d: nothing above the heading at all */
.no-icon h3 { margin-top: 0; }
/* e: icon-tile that's too small (under 32px) — out of size range */
.icon-tiny {
width: 24px; height: 24px;
border-radius: 6px;
background: #93c5fd;
display: flex; align-items: center; justify-content: center;
margin-bottom: 16px;
}
/* f: oversized image card / hero image (over 128px) — out of size range */
.img-huge {
width: 200px; height: 200px;
border-radius: 16px;
background: #cbd5e1;
margin-bottom: 16px;
}
</style>
</head>
<body>
<div class="grid">
<!-- ════════════════════════════════════════════════════════════════
LEFT COLUMN: should flag
═══════════════════════════════════════════════════════════════ -->
<div class="col" data-col="flag">
<h2>Should flag</h2>
<div class="case">
<div class="icon-pastel">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="1.5">
<polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>
</div>
<h3>Lightning Fast</h3>
<p>Built for speed and performance from the ground up.</p>
</div>
<div class="case">
<div class="icon-solid">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="1.5">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</div>
<h3>Secure Storage</h3>
<p>Enterprise-grade security for your peace of mind.</p>
</div>
<div class="case">
<div class="icon-white">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#0f172a" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
</div>
<h3>Easy Setup</h3>
<p>Get started in minutes with zero configuration.</p>
</div>
<div class="case">
<div class="icon-large">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#0f172a" stroke-width="1.5">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
</div>
<h3>Powerful Analytics</h3>
<p>Insights that drive smarter business decisions.</p>
</div>
</div>
<!-- ════════════════════════════════════════════════════════════════
RIGHT COLUMN: should pass
═══════════════════════════════════════════════════════════════ -->
<div class="col" data-col="pass">
<h2>Should pass</h2>
<!-- a: round avatar (circle, not rounded square) -->
<div class="case">
<div class="avatar-round">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2">
<circle cx="12" cy="8" r="4"/>
<path d="M4 22v-2a8 8 0 0116 0v2"/>
</svg>
</div>
<h3>Sarah Chen</h3>
<p>Product designer based in Berlin.</p>
</div>
<!-- b: wide 16:9 thumbnail (aspect ratio out of range) -->
<div class="case">
<div class="thumb-wide"></div>
<h3>Article Headline</h3>
<p>A summary of an interesting article.</p>
</div>
<!-- c: side-by-side icon and heading (not stacked vertically) -->
<div class="case">
<div class="inline-row">
<div class="icon-inline">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#7c3aed" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
</svg>
</div>
<div>
<h3>Inline Side By Side</h3>
<p>Icon sits next to the heading, not above it.</p>
</div>
</div>
</div>
<!-- d: no icon above the heading at all -->
<div class="case no-icon">
<h3>Plain Heading No Icon</h3>
<p>Just a heading with body text, nothing above.</p>
</div>
<!-- e: icon-tile too small (24px — under 32px floor) -->
<div class="case">
<div class="icon-tiny">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1e40af" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
</svg>
</div>
<h3>Tiny Icon Above Me</h3>
<p>The icon container is smaller than 32px.</p>
</div>
<!-- f: huge image / hero (200px — over 128px ceiling) -->
<div class="case">
<div class="img-huge"></div>
<h3>Huge Hero Image</h3>
<p>The container is larger than the size cap.</p>
</div>
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>
</body>
</html>