diff --git a/public/css/sub-pages.css b/public/css/sub-pages.css index ac5019c65..e0fd0e030 100644 --- a/public/css/sub-pages.css +++ b/public/css/sub-pages.css @@ -570,6 +570,217 @@ main#main { background: var(--color-cream); } +/* ============================================ + ANTI-PATTERNS INDEX + ============================================ */ + +.anti-patterns-sidebar-list a { + display: flex !important; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.anti-patterns-sidebar-count { + font-size: 0.6875rem; + font-weight: 500; + color: var(--color-ash); + font-variant-numeric: tabular-nums; +} + +.anti-patterns-content { + max-width: 820px; +} + +.anti-patterns-header { + margin-bottom: clamp(2.5rem, 5vw, 4rem); +} + +.anti-patterns-header .sub-page-lede code { + font-family: var(--font-mono); + font-size: 0.875em; + color: var(--color-ink); + background: var(--color-cream); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid var(--color-mist); +} + +.anti-patterns-header .sub-page-lede a { + color: var(--color-ink); + text-decoration: underline; + text-decoration-color: var(--color-accent); + text-decoration-thickness: 1px; + text-underline-offset: 4px; + font-family: var(--font-mono); + font-weight: 500; + font-size: 0.9375em; +} + +.anti-patterns-legend { + padding: var(--spacing-lg); + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 10px; + margin-bottom: clamp(2.5rem, 5vw, 4rem); + max-width: 720px; +} + +.anti-patterns-legend-title { + font-family: var(--font-display); + font-size: 1.25rem; + font-style: italic; + font-weight: 500; + color: var(--color-ink); + margin-bottom: var(--spacing-sm); +} + +.anti-patterns-legend p { + font-size: 0.9375rem; + line-height: 1.7; + color: var(--color-charcoal); +} + +.anti-patterns-legend a { + color: var(--color-ink); + text-decoration: none; + border-bottom: 1px solid var(--color-accent); + font-family: var(--font-mono); + font-size: 0.875em; + font-weight: 500; +} + +.anti-patterns-legend a:hover { + color: var(--color-accent); +} + +.anti-patterns-sections { + display: flex; + flex-direction: column; + gap: clamp(3rem, 6vw, 4.5rem); +} + +.anti-patterns-section-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-md); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--color-mist); + margin-bottom: var(--spacing-lg); +} + +.anti-patterns-section-title { + font-family: var(--font-display); + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 500; + font-style: italic; + color: var(--color-ink); + letter-spacing: -0.01em; +} + +.anti-patterns-section-count { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ash); +} + +.rule-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: var(--spacing-md); +} + +.rule-card { + padding: var(--spacing-md); + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 8px; + transition: border-color var(--duration-fast) var(--ease-out); +} + +.rule-card:hover { + border-color: var(--color-ash); +} + +.rule-card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-sm); + margin-bottom: 2px; +} + +.rule-card-id { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + color: var(--color-ash); + background: transparent; + padding: 0; + border: none; +} + +.rule-card-category { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 3px 8px; + border-radius: 99px; +} + +.rule-card-category[data-category="slop"] { + color: var(--color-accent); + background: var(--color-accent-dim); +} + +.rule-card-category[data-category="quality"] { + color: var(--color-charcoal); + background: var(--color-mist); +} + +.rule-card-name { + font-family: var(--font-body); + font-size: 1rem; + font-weight: 600; + color: var(--color-ink); + line-height: 1.35; +} + +.rule-card-desc { + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-charcoal); + flex: 1; +} + +.rule-card-skill-link { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + color: var(--color-charcoal); + text-decoration: none; + align-self: flex-start; + padding-top: 4px; + transition: color var(--duration-fast) var(--ease-out); +} + +.rule-card-skill-link:hover { + color: var(--color-accent); +} + +.rule-card-skill-link::after { + content: " →"; +} + /* ============================================ MOBILE: collapse sidebar into inline block ============================================ */ diff --git a/scripts/build-sub-pages.js b/scripts/build-sub-pages.js index 5111b5eb0..4a937036c 100644 --- a/scripts/build-sub-pages.js +++ b/scripts/build-sub-pages.js @@ -212,6 +212,124 @@ ${mainHtml} `; } +/** + * Group anti-pattern rules by skill section. + * Rules without a skillSection fall into a 'General quality' bucket. + */ +function groupRulesBySection(rules) { + const order = [ + 'Visual Details', + 'Typography', + 'Color & Contrast', + 'Layout & Space', + 'Motion', + 'General quality', + ]; + const bySection = {}; + for (const name of order) bySection[name] = []; + 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) => { + if (a.category !== b.category) return a.category === 'slop' ? -1 : 1; + return a.name.localeCompare(b.name); + }); + } + return { order, bySection }; +} + +/** + * Render the anti-patterns sidebar: a table of contents of rule sections + * with per-section rule counts. Every entry anchor-jumps to the section + * in the main column. + */ +function renderAntiPatternsSidebar(grouped) { + const entries = grouped.order + .filter((section) => grouped.bySection[section]?.length > 0) + .map((section) => { + const slug = slugify(section); + const count = grouped.bySection[section].length; + return `
  • ${escapeHtml(section)}${count}
  • `; + }) + .join('\n'); + + return ` +`; +} + +/** + * Render one rule card inside the anti-patterns main column. + */ +function renderRuleCard(rule) { + const categoryLabel = rule.category === 'slop' ? 'AI slop' : 'Quality'; + const skillLink = rule.skillSection + ? `See in /impeccable` + : ''; + return ` +
    +
    + ${escapeHtml(rule.id)} + ${categoryLabel} +
    +

    ${escapeHtml(rule.name)}

    +

    ${escapeHtml(rule.description)}

    + ${skillLink} +
    `; +} + +/** + * Render the /anti-patterns main column content. + */ +function renderAntiPatternsMain(grouped, totalRules) { + let sectionsHtml = ''; + for (const section of grouped.order) { + const rules = grouped.bySection[section] || []; + if (rules.length === 0) continue; + const slug = slugify(section); + sectionsHtml += ` +
    +
    +

    ${escapeHtml(section)}

    +

    ${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}

    +
    +
    +${rules.map(renderRuleCard).join('\n')} +
    +
    `; + } + + return ` +
    +
    +

    ${totalRules} detection rules

    +

    Anti-patterns

    +

    These are the visible tells of AI-generated interfaces. Every rule in this catalog is implemented as a deterministic check in npx impeccable detect and in the browser extension. Run /critique on any page to see which ones it triggers.

    +
    + +
    +

    How to read this

    +

    Rules are grouped by the section of the /impeccable skill that teaches the pattern to avoid. AI slop rules flag the specific visual tells (gradient text, purple palettes, side-tab borders, nested cards). Quality rules flag general design mistakes that are not AI-specific but still hurt the work.

    +
    + +
    +${sectionsHtml} +
    +
    `; +} + /** * Entry point. Generates all sub-page HTML files. * @@ -271,5 +389,23 @@ export async function generateSubPages(rootDir) { generated.push(out); } + // Anti-patterns index: single page, docs-browser shell with TOC sidebar. + { + const grouped = groupRulesBySection(data.rules); + const sidebar = renderAntiPatternsSidebar(grouped); + const main = renderAntiPatternsMain(grouped, data.rules.length); + const html = renderPage({ + title: 'Anti-patterns | Impeccable', + description: `${data.rules.length} deterministic detection rules that flag the visible tells of AI-generated interfaces and common quality issues. Used by npx impeccable detect and the browser extension.`, + bodyHtml: wrapInDocsLayout(sidebar, main), + activeNav: 'anti-patterns', + canonicalPath: '/anti-patterns', + bodyClass: 'sub-page skills-layout-page anti-patterns-page', + }); + const out = path.join(outDirs.antiPatterns, 'index.html'); + fs.writeFileSync(out, html, 'utf-8'); + generated.push(out); + } + return { files: generated }; }