mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
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:
co-authored by
Claude Opus 4.6
parent
83330a66cb
commit
e1032b7285
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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: // 1–2 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 3–6 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>
|
||||
<!-- 4–6 cases that should be flagged, each with a unique <h3> text -->
|
||||
</div>
|
||||
<div class="col" data-col="pass">
|
||||
<h2>Should pass</h2>
|
||||
<!-- 5–8 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, 1–2 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.
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|---------|--------------|
|
||||
|
||||
@@ -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">← Back to impeccable.style</a>
|
||||
</header>
|
||||
|
||||
|
||||
+10
-10
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -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!');
|
||||
}
|
||||
|
||||
@@ -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 65–75ch so the eye can track easily
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
|
||||
@@ -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 32–128px on both axes (not too small, not a hero image)
|
||||
// • aspect ratio 0.7–1.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: 32–128px 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));
|
||||
|
||||
|
||||
@@ -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 32–128px on both axes (not too small, not a hero image)
|
||||
// • aspect ratio 0.7–1.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: 32–128px 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
@@ -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 32–128 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>
|
||||
Reference in New Issue
Block a user