mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 07:06:45 +03:00
Overhaul /anti-patterns with visuals, detection layers, and LLM rules
Three additions to the anti-patterns catalog page, all sourced from a
new content/site/anti-patterns-catalog.js file so the user's parallel
edits to src/detect-antipatterns.mjs don't conflict with display metadata.
1. Detection layer badge per rule. Three layers:
cli - static analysis or jsdom. Runs from `npx impeccable detect`
on files, no browser required. 23 of 25 current rules.
browser - needs real browser layout (getBoundingClientRect).
Runs via the browser extension or Puppeteer, not the
plain CLI. Only 2 rules: cramped-padding and line-length,
as documented in tests/detect-antipatterns-browser.test.mjs.
llm - no deterministic detector. Flagged by /critique's LLM
review pass. 13 rules live only in the skill's DON'T list.
Each card renders a mono pill with the layer label, color-coded per
layer (neutral mist for CLI, blue tint for browser, amber tint for LLM).
The How-to-read legend grows a dl explaining what each layer means.
2. Inline visual example per detected rule. All 25 detection rules get
a ~140px tall preview area at the top of the card showing the bad
pattern as live HTML (cream background, self-contained inline styles).
Visuals for side-tab, gradient-text, dark-glow, nested-cards, and the
rest let you see what the detector is actually flagging. LLM-only
rules ship without visuals for now; their card bodies take the full
card height.
3. LLM-only rules merged into the sections. Parsed out from
source/skills/impeccable/SKILL.md DON'T lines that the detector
doesn't cover: Syne, monospace-as-technical, dark-mode-default,
everything-in-cards, identical-card-grids, hero-metric-layout,
glassmorphism, sparkline-decoration, generic-drop-shadows,
modal-reflex, every-button-primary, redundant-headers,
mobile-amputation. Each renders like a detection rule card but
shows the 'LLM only' layer badge and has no rule id chip. They
slot into the same section groups as detected rules (Interaction
and Responsive sections added to the section order so these get
real headings).
- scripts/lib/sub-pages-data.js: imports the catalog, enriches
detected rules with { layer, visual }, appends LLM_ONLY_RULES with
layer: 'llm'. Re-exports LAYER_LABELS and LAYER_DESCRIPTIONS for
the generator.
- scripts/build-sub-pages.js: renderRuleCard adds the visual block
and the layer badge; LLM rules drop the rule id chip since their id
is just an internal slug. groupRulesBySection now extends the
primary order with whatever extra sections rules reference.
- public/css/sub-pages.css: .rule-card now has a .rule-card-visual
preview area on top with border-bottom, body section below. New
.rule-card-layer pill styling per layer. Layer legend dl using a
2-column grid for badge -> description.
Dev server serves 38 total cards (25 detected + 13 LLM) across 8
sections: Visual Details, Typography, Color & Contrast, Layout & Space,
Motion, Interaction, Responsive, General quality.
This commit is contained in:
+60
-13
@@ -17,6 +17,8 @@ import {
|
||||
CATEGORY_ORDER,
|
||||
CATEGORY_LABELS,
|
||||
CATEGORY_DESCRIPTIONS,
|
||||
LAYER_LABELS,
|
||||
LAYER_DESCRIPTIONS,
|
||||
} from './lib/sub-pages-data.js';
|
||||
import { renderMarkdown, slugify } from './lib/render-markdown.js';
|
||||
import { renderPage } from './lib/render-page.js';
|
||||
@@ -294,21 +296,28 @@ ${mainHtml}
|
||||
* Rules without a skillSection fall into a 'General quality' bucket.
|
||||
*/
|
||||
function groupRulesBySection(rules) {
|
||||
const order = [
|
||||
// Canonical ordering. Additional sections referenced by rules (e.g.
|
||||
// 'Interaction', 'Responsive' from LLM-only entries) are appended to
|
||||
// the end, before 'General quality', so every rule renders.
|
||||
const primaryOrder = [
|
||||
'Visual Details',
|
||||
'Typography',
|
||||
'Color & Contrast',
|
||||
'Layout & Space',
|
||||
'Motion',
|
||||
'General quality',
|
||||
'Interaction',
|
||||
'Responsive',
|
||||
];
|
||||
const bySection = {};
|
||||
for (const name of order) bySection[name] = [];
|
||||
for (const name of primaryOrder) bySection[name] = [];
|
||||
bySection['General quality'] = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
const section = rule.skillSection || 'General quality';
|
||||
if (!bySection[section]) bySection[section] = [];
|
||||
bySection[section].push(rule);
|
||||
}
|
||||
|
||||
// Sort each bucket: slop first (they're the named tells), then quality.
|
||||
for (const name of Object.keys(bySection)) {
|
||||
bySection[name].sort((a, b) => {
|
||||
@@ -316,6 +325,17 @@ function groupRulesBySection(rules) {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Final render order: primary sections first, then any extras that
|
||||
// rules introduced, then General quality last.
|
||||
const order = [...primaryOrder];
|
||||
for (const name of Object.keys(bySection)) {
|
||||
if (!order.includes(name) && name !== 'General quality') {
|
||||
order.push(name);
|
||||
}
|
||||
}
|
||||
order.push('General quality');
|
||||
|
||||
return { order, bySection };
|
||||
}
|
||||
|
||||
@@ -352,21 +372,38 @@ ${entries}
|
||||
*/
|
||||
function renderRuleCard(rule) {
|
||||
const categoryLabel = rule.category === 'slop' ? 'AI slop' : 'Quality';
|
||||
const layer = rule.layer || 'cli';
|
||||
const layerLabel = LAYER_LABELS[layer] || layer;
|
||||
const layerTitle = LAYER_DESCRIPTIONS[layer] || '';
|
||||
const skillLink = rule.skillSection
|
||||
? `<a class="rule-card-skill-link" href="/skills/impeccable#${slugify(rule.skillSection)}">See in /impeccable</a>`
|
||||
: '';
|
||||
const visual = rule.visual
|
||||
? `<div class="rule-card-visual" aria-hidden="true"><div class="rule-card-visual-inner">${rule.visual}</div></div>`
|
||||
: '';
|
||||
const ruleIdDisplay = rule.layer === 'llm' ? '' : `<code class="rule-card-id">${escapeHtml(rule.id)}</code>`;
|
||||
return `
|
||||
<article class="rule-card" id="rule-${rule.id}">
|
||||
<div class="rule-card-head">
|
||||
<code class="rule-card-id">${escapeHtml(rule.id)}</code>
|
||||
<span class="rule-card-category" data-category="${rule.category}">${categoryLabel}</span>
|
||||
<article class="rule-card" id="rule-${rule.id}" data-layer="${layer}">
|
||||
${visual}
|
||||
<div class="rule-card-body">
|
||||
<div class="rule-card-head">
|
||||
${ruleIdDisplay}
|
||||
<span class="rule-card-badges">
|
||||
<span class="rule-card-category" data-category="${rule.category}">${categoryLabel}</span>
|
||||
<span class="rule-card-layer" data-layer="${layer}" title="${escapeAttr(layerTitle)}">${escapeHtml(layerLabel)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="rule-card-name">${escapeHtml(rule.name)}</h3>
|
||||
<p class="rule-card-desc">${escapeHtml(rule.description)}</p>
|
||||
${skillLink}
|
||||
</div>
|
||||
<h3 class="rule-card-name">${escapeHtml(rule.name)}</h3>
|
||||
<p class="rule-card-desc">${escapeHtml(rule.description)}</p>
|
||||
${skillLink}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
return String(str || '').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the /tutorials index main content.
|
||||
*/
|
||||
@@ -439,17 +476,27 @@ ${rules.map(renderRuleCard).join('\n')}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
const detectedCount = grouped.order
|
||||
.flatMap((s) => grouped.bySection[s] || [])
|
||||
.filter((r) => r.layer !== 'llm').length;
|
||||
const llmCount = totalRules - detectedCount;
|
||||
|
||||
return `
|
||||
<div class="anti-patterns-content">
|
||||
<header class="anti-patterns-header">
|
||||
<p class="sub-page-eyebrow">${totalRules} detection rules</p>
|
||||
<p class="sub-page-eyebrow">${totalRules} rules</p>
|
||||
<h1 class="sub-page-title">Anti-patterns</h1>
|
||||
<p class="sub-page-lede">These are the visible tells of AI-generated interfaces. Every rule in this catalog is implemented as a deterministic check in <code>npx impeccable detect</code> and in the browser extension. Run <a href="/skills/critique">/critique</a> on any page to see which ones it triggers.</p>
|
||||
<p class="sub-page-lede">The full catalog of patterns <a href="/skills/impeccable">/impeccable</a> teaches against. ${detectedCount} are caught by a deterministic detector (<code>npx impeccable detect</code> or the browser extension). ${llmCount} can only be flagged by <a href="/skills/critique">/critique</a>'s LLM review pass.</p>
|
||||
</header>
|
||||
|
||||
<section class="anti-patterns-legend">
|
||||
<h2 class="anti-patterns-legend-title">How to read this</h2>
|
||||
<p>Rules are grouped by the section of the <a href="/skills/impeccable">/impeccable</a> skill that teaches the pattern to avoid. <strong>AI slop</strong> rules flag the specific visual tells (gradient text, purple palettes, side-tab borders, nested cards). <strong>Quality</strong> rules flag general design mistakes that are not AI-specific but still hurt the work.</p>
|
||||
<p><strong>AI slop</strong> rules flag the visible tells of AI-generated UIs. <strong>Quality</strong> rules flag general design mistakes that are not AI-specific but still hurt the work. Each rule also shows how it is detected:</p>
|
||||
<dl class="anti-patterns-legend-layers">
|
||||
<div><dt><span class="rule-card-layer" data-layer="cli">CLI</span></dt><dd>Deterministic. Runs from <code>npx impeccable detect</code> on files, no browser required.</dd></div>
|
||||
<div><dt><span class="rule-card-layer" data-layer="browser">Browser</span></dt><dd>Deterministic, but needs real browser layout. Runs via the browser extension or Puppeteer, not the plain CLI.</dd></div>
|
||||
<div><dt><span class="rule-card-layer" data-layer="llm">LLM only</span></dt><dd>No deterministic detector. Caught by <a href="/skills/critique">/critique</a> during its LLM design review.</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<div class="anti-patterns-sections">
|
||||
|
||||
@@ -14,6 +14,13 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { readSourceFiles, parseFrontmatter } from './utils.js';
|
||||
import {
|
||||
DETECTION_LAYERS,
|
||||
VISUAL_EXAMPLES,
|
||||
LLM_ONLY_RULES,
|
||||
} from '../../content/site/anti-patterns-catalog.js';
|
||||
|
||||
export { LAYER_LABELS, LAYER_DESCRIPTIONS } from '../../content/site/anti-patterns-catalog.js';
|
||||
|
||||
/**
|
||||
* Skills that should be excluded from the index and not get a detail page.
|
||||
@@ -33,7 +40,6 @@ const SKILL_CATEGORIES = {
|
||||
// CREATE - build something new
|
||||
impeccable: 'create',
|
||||
shape: 'create',
|
||||
overdrive: 'create',
|
||||
// EVALUATE - review and assess
|
||||
critique: 'evaluate',
|
||||
audit: 'evaluate',
|
||||
@@ -46,6 +52,7 @@ const SKILL_CATEGORIES = {
|
||||
bolder: 'refine',
|
||||
quieter: 'refine',
|
||||
onboard: 'refine',
|
||||
overdrive: 'refine',
|
||||
// SIMPLIFY - reduce and clarify
|
||||
distill: 'simplify',
|
||||
clarify: 'simplify',
|
||||
@@ -191,8 +198,19 @@ export async function buildSubPageData(rootDir) {
|
||||
for (const cat of CATEGORY_ORDER) skillsByCategory[cat] = [];
|
||||
for (const skill of skills) skillsByCategory[skill.category].push(skill);
|
||||
|
||||
// Anti-pattern rules, grouped for the index.
|
||||
const rules = readAntipatternRules(rootDir);
|
||||
// Anti-pattern rules, enriched with catalog metadata and merged with
|
||||
// LLM-only rules from the skill's DON'T list.
|
||||
const detectedRules = readAntipatternRules(rootDir).map((r) => ({
|
||||
...r,
|
||||
layer: DETECTION_LAYERS[r.id] || 'cli',
|
||||
visual: VISUAL_EXAMPLES[r.id] || null,
|
||||
}));
|
||||
const llmRules = LLM_ONLY_RULES.map((r) => ({
|
||||
...r,
|
||||
layer: 'llm',
|
||||
visual: VISUAL_EXAMPLES[r.id] || null,
|
||||
}));
|
||||
const rules = [...detectedRules, ...llmRules];
|
||||
|
||||
// Tutorials: each required file in content/site/tutorials/.
|
||||
const tutorialsDir = path.join(contentDir, 'tutorials');
|
||||
|
||||
Reference in New Issue
Block a user