Merge pull request 'Thesisly 研迹:论文阅读与共享账户接入 [skip ci]' (#1) from codex/thesisly into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-11-07 08:23:47 +00:00
145 changed files with 18090 additions and 0 deletions
+450
View File
@@ -0,0 +1,450 @@
# Beautiful Article Skill — Turn any source into a beautiful article
> A skill for AI agents to **edit and design** any source material (URL / PDF / DOCX / Markdown / plain text / screenshots / pasted notes) into a **beautiful, share-ready article** that is easier to read, archive, and pass around than the original.
[中文文档](./README.zh-CN.md) · [Back to collection root](../../README.md)
![Beautiful Article Skill](../../dist/imgs/covers/beautiful-article.webp)
---
### Powered by [ReActicle](https://github.com/ConardLi/reacticle)
`beautiful-article` is the editorial **harness** (methodology, checkpoints, theme picker, sub-agent reviewers); [`reacticle`](https://github.com/ConardLi/reacticle) is the underlying **runtime component protocol** the skill writes against — prose-first React components plus a token-based `Raw` escape hatch, all wired into the same theme system.
```
beautiful-article (this skill · methodology + harness)
│ composes
▼
reacticle (npm package · components / themes / Raw / export)
```
| Layer | What it owns | Where it lives |
|---|---|---|
| `beautiful-article` (this skill) | **How** the agent plans, writes, reviews and delivers an article from any source — six numbered phases, three hard checkpoints, theme picker, sub-agent reviewers | This directory |
| `reacticle` | The component vocabulary + 11 authoring themes the skill compiles into — `Article` / `Hero` / `Lead` / `Section` / `Quote` / `Image` / `Formula` / `CodeBlock` / `Raw` …, each theme a `.css` token bundle + `.md` authoring profile | [`ConardLi/reacticle`](https://github.com/ConardLi/reacticle) · [npm `reacticle`](https://www.npmjs.com/package/reacticle) · [docs](https://rearticle.mmh1.top/) |
The two pair very well but are independently useful: the skill works because it has ReActicle to target, and ReActicle is a perfectly usable React library on its own.
---
### [Showcase](https://mmh1.top/#/ai-article) — articles built with `beautiful-article` + ReActicle
Real long-form articles, each authored end-to-end by an AI agent running this skill against the [`reacticle`](https://www.npmjs.com/package/reacticle) component protocol. Click any cover to open the live, single-file HTML article.
<table>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/tools">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/tools.webp" alt="Agent Tools 设计的最佳实践" width="320">
<br><b>Agent Tools 设计的最佳实践</b>
</a>
<br><sub>Theme · Freddie · 长文 · 21 min</sub>
<br><sup>Anthropic 工程团队关于 Tools 的五条原则,与一套评测驱动的方法。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/skill">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/skill.webp" alt="Agent Skill 是如何进化的?" width="320">
<br><b>Agent Skill 是如何进化的?</b>
</a>
<br><sub>Theme · Freddie · 解释文 · 8 min</sub>
<br><sup>把 Skill 文档当成被训练的对象,而不是被复制粘贴的 prompt。</sup>
</td>
</tr>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/harness">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/harness.webp" alt="Agent Harness 的解剖图" width="320">
<br><b>Agent Harness 的解剖图</b>
</a>
<br><sub>Theme · Vignelli · 长文 · 12 min</sub>
<br><sup>智能在模型里;让智能变得有用的,是它周围的那套系统。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/prompt-cache">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/prompt-cache.webp" alt="提示词缓存对 Agent 有多重要?" width="320">
<br><b>提示词缓存对 Agent 有多重要?</b>
</a>
<br><sub>Theme · Bayer · 长文 · 15 min</sub>
<br><sup>缓存命中率是 Agent 的 SLO,Claude Code 团队的反直觉经验。</sup>
</td>
</tr>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/context">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/context.webp" alt="面向 Agent 的高效上下文工程" width="320">
<br><b>面向 Agent 的高效上下文工程</b>
</a>
<br><sub>Theme · Tufte · 长文 · 16 min</sub>
<br><sup>本文探讨如何高效地筛选与管理驱动 AI Agent 运转的上下文。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/transformer">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/transformer.webp" alt="Attention Is All You Need" width="320">
<br><b>Attention Is All You Need</b>
</a>
<br><sub>Theme · Tufte · 长文 · 30 min</sub>
<br><sup>一篇重塑现代 AI 的论文,逐层拆给你看。</sup>
</td>
</tr>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/agent-eval">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/agent-eval.webp" alt="把 AI Agent 的评测讲清楚" width="320">
<br><b>把 AI Agent 的评测讲清楚</b>
</a>
<br><sub>Theme · Tufte · 长文 · 25 min</sub>
<br><sup>让 Agent 有用的那些能力,恰恰让它难以评测 — 来自 Anthropic 的指南。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/agent-loop-codex">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/agent-loop-codex.webp" alt="Codex 的 Agent Loop 是怎么做的?" width="320">
<br><b>Codex 的 Agent Loop 是怎么做的?</b>
</a>
<br><sub>Theme · Sottsass · 长文 · 18 min</sub>
<br><sup>OpenAI 官方分享:在 Responses API 之上,一条对话是如何被反复"展开"的。</sup>
</td>
</tr>
</table>
---
### [Theme Gallery](https://rearticle.mmh1.top/#/gallery) — one specimen article per theme
> 11 themes shipped. Full theme contracts (`.css` token bundle + `.md` authoring profile, anti-patterns, code/media style) are documented at [Theming](https://rearticle.mmh1.top/#/theming).
Every theme ships with a long-form **specimen article** that lives the theme end-to-end — typography, photography, code, formulas, Raw blocks, the works. Click any cover to read the live article; click the theme name to jump to that theme's section in the docs.
<table>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/caffeine-half-life">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-tufte.webp" alt="Tufte · Data-Ink" width="260">
<br><b>Tufte</b> · Data-Ink
</a>
<br><sub>咖啡因与睡眠 · 数据笔记</sub>
<br><sup>Edward Tufte 数据墨水,证据优先,发丝级图表与最朴素的版式。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/movable-type">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-press.webp" alt="Press · 书卷" width="260">
<br><b>Press</b> · 书卷
</a>
<br><sub>活字之后 · 随笔</sub>
<br><sup>Stripe Press 式书卷长读物:会落定的标题、氧化血红首字母、纯正文之美。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/pool-exhaustion">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-shannon.webp" alt="Shannon · 工程暗色" width="260">
<br><b>Shannon</b> · 工程暗色
</a>
<br><sub>连接池耗尽 · 故障复盘</sub>
<br><sup>贝尔实验室技术论文血统,暗底黄金信号、回压依赖、夜间作战气质。</sup>
</td>
</tr>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/orbit-spec">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-vignelli.webp" alt="Vignelli · 瑞士" width="260">
<br><b>Vignelli</b> · 瑞士网格
</a>
<br><sub>Orbit 设计系统规格 · 规格</sub>
<br><sup>Massimo Vignelli 网格至上、grotesque 字族、瑞士红只承载结构。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/linear-attention">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/knuth.png" alt="Knuth · 学术" width="260">
<br><b>Knuth</b> · 学术
</a>
<br><sub>线性化自注意力 · 预印本</sub>
<br><sup>Donald Knuth / Computer Modern,编号小节、命题与证明、arXiv 草稿气质。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/first-newsletter">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-freddie.webp" alt="Freddie · 暖黄" width="260">
<br><b>Freddie</b> · 暖黄
</a>
<br><sub>第一封 Newsletter · 上手指南</sub>
<br><sup>Mailchimp Freddie 黑字荧光,亲和插画 + 不端着的产品上手语气。</sup>
</td>
</tr>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/slow-breathing">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-andy.webp" alt="Andy · 静谧" width="260">
<br><b>Andy</b> · 静谧
</a>
<br><sub>把呼吸放慢 · 练习</sub>
<br><sup>柔软圆润、呼吸-神经跷跷板,让人慢下来的练习气质。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/front-page">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-bodoni.webp" alt="Bodoni · 报刊" width="260">
<br><b>Bodoni</b> · 报刊
</a>
<br><sub>头版的消亡 · 特稿</sub>
<br><sup>高对比 Didone 报刊气质,黑白大报、对折线之上的分量。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/geometry-of-meaning">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-bayer.webp" alt="Bayer · 包豪斯" width="260">
<br><b>Bayer</b> · 包豪斯
</a>
<br><sub>形、色、网格 · 教学</sub>
<br><sup>Herbert Bayer 包豪斯三原色几何,形有性格、色有重量。</sup>
</td>
</tr>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/rate-limiter-spec">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-fuller.webp" alt="Fuller · 蓝图" width="260">
<br><b>Fuller</b> · 蓝图
</a>
<br><sub>限流器设计规格 · 系统设计</sub>
<br><sup>Buckminster Fuller 工程蓝图,方格纸拓扑、令牌桶模拟,可照着实现。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/color-clash">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-sottsass.webp" alt="Sottsass · 孟菲斯" width="260">
<br><b>Sottsass</b> · 孟菲斯
</a>
<br><sub>撞色不翻车 · 设计随笔</sub>
<br><sup>Memphis 80s 撞色,黑描边、硬投影、轻微旋转的不正经语法。</sup>
</td>
<td width="33%" valign="top" align="center">
&nbsp;
</td>
</tr>
</table>
> Browse all specimens with theme switching, search and filters at the live gallery: <https://rearticle.mmh1.top/#/gallery>.
---
## What it does
`beautiful-article` turns dry, linear, hard-to-digest source material into a polished, visually clear, share-ready article. It is **not** a web-app builder — the focus is always the *article*: better reading, better pacing, better aesthetics. The article is delivered as a self-contained file that opens offline (with optional companion PDF), but that's a delivery detail, not the goal.
It is designed for:
- Repackaging long URLs / PDFs / DOCX / Markdown into a beautiful HTML long-form
- Producing briefings, explainers, tutorials, post-mortems, design / proposal reviews
- Visual essays, dialogue / interview transcripts, interactive learning explainers
- Any time you want a **better reading medium** than raw Markdown — with tables, SVG, code, formulas, copy / export buttons baked in
The skill is primarily a **methodology + collaboration harness**. It ships a Vite + React + TypeScript scaffold built around the [`reacticle`](https://www.npmjs.com/package/reacticle) component protocol — the agent does not hand-write naked HTML / CSS, it composes prose-first semantic components plus a theme-constrained `Raw` free layer.
---
## Core ideas
- **Article first, not app** — the focus is the article. Raw layers, SVGs, mini-tools must serve reading / explanation / pacing / aesthetics, not stand on their own.
- **Source → Plan → Build → Review harness** — every project flows through six numbered phases with three hard checkpoints in between.
- **Component protocol via `reacticle`** — semantic prose components (Hero, Lead, Section, Quote, Callout, Image, Formula, CodeBlock, Table, …) plus a `Raw` escape hatch that must use theme tokens (`--ra-*`).
- **Theme-driven design** — pick from a registry of authoring profiles (`tufte`, `press`, `bayer`, `bodoni`, `vignelli`, `sottsass`, `freddie`, `andy`, `fuller`, `knuth`, `shannon`) — each profile is a Markdown contract for the agent, not a CSS file.
- **100% information retention by default** — the article type carries a recommended retention ratio (longform `~100%`, briefing `~50%`, visual-essay `~40%`, …) which the user can override.
- **Hard collaboration checkpoints** — the agent pauses for the plan, the first-spread proof, and the final delivery decision; **every decision must be confirmed item-by-item, never silently bundled**.
- **Cover by default** — a 3:4 book-style cover sits above the TOC, locked container, theme-token only, no remote images.
- **Optional PDF export** — the main deliverable is a self-contained HTML file; PDF is opt-in via a zero-npm `html-to-pdf.sh` headless-browser script.
---
## Workflow
```text
Phase 0 Intake
│
Phase 1 Source → Markdown (URL / PDF / DOCX / MD / text → source.md)
│
Phase 2 Editorial Planning (one plan.md: Brief / Outline / Theme / Assets)
│
★ Checkpoint 1 Plan (5 independent decisions confirmed)
│
Phase 4 First Spread (cover + hero + first section + one signature visual)
│
★ Checkpoint 2 First Spread (acceptance + dev mode A/B)
│
Phase 5 Full Article Build (single-agent sequential or multi-agent parallel)
│
Phase 6 Final Review (Editorial / Visual / Technical)
│
Phase 7 Repair (minimal-slice fixes only)
│
★ Checkpoint 3 Delivery (HTML, or HTML + PDF, or pause to revise)
│
Phase 8 Delivery (article.html, optional article.pdf)
```
The agent owns a per-project workspace directory that is its **long-term memory**:
```text
<workspace>/
source/ original.* source.md source.<lang>.md (if translated) extraction-notes.md
plan/ plan.md
article/ Cover.tsx Article.tsx sections/ raw-blocks/ assets/ article.html (output)
review/ first-spread-review.md final-review.md
(source-review.md only on complex sources, repair-log.md only when there are repairs)
index.html package.json vite.config.ts tsconfig*.json (build harness)
```
---
## Skill structure
```text
skills/beautiful-article/
├── SKILL.md Main skill (frontmatter name: beautiful-article)
├── manifest.json Release manifest
├── README.md / README.zh-CN.md This document
├── references/
│ ├── article-types.md Article-type router
│ ├── article-types/ briefing / dialogue / essay / explainer / full-report
│ │ interactive-explainer / longform / review / tutorial / visual-essay
│ ├── information-density.md Retention ratios vs. component / visual mix
│ ├── plan-template.md Single plan.md template (Brief / Outline / Theme / Assets)
│ ├── theme-selection.md Theme picker, density / theme decoupling rules
│ ├── layout.md Width modes, TOC defaults
│ ├── cover.md 3:4 book-style cover guide (5 self-checks, 5 layouts)
│ ├── asset-policy.md Image strategy (none / user-assets / placeholders / ai-generated)
│ ├── component-policy.md Reacticle component contract, prose-first
│ ├── raw-policy.md Raw allow / deny list, token-driven, self-checks
│ ├── section-build.md One-section-one-file rule, sub-agent prompt templates
│ ├── source-to-markdown.md Per-format extraction rules + 5-item self-check
│ ├── scaffold.md Scaffold script behaviour, workspace layout
│ ├── html-output.md dev / build / single-file commands
│ ├── pdf-output.md html-to-pdf.sh usage + print CSS overrides
│ ├── review-checklist.md Per-phase reviewer checklists & sub-agent prompts
│ ├── repair-policy.md Minimal-slice repair table
│ └── harness.md Skill-as-harness perspective
├── theme-profiles/
│ ├── index.json Theme registry
│ └── andy / bayer / bodoni / freddie / fuller / knuth / press / shannon / sottsass / tufte / vignelli
├── scripts/
│ ├── scaffold.sh One-shot workspace bootstrap
│ ├── html-to-pdf.sh Optional HTML → PDF (headless browser, zero npm deps)
│ ├── pdf-print-overrides.css @media print overrides injected into the HTML
│ ├── source-to-markdown-markitdown.py Main extraction path (PDF / DOCX / HTML)
│ └── source-to-markdown.py Lightweight fallback (Markdown / TXT / simple HTML)
└── assets/
└── scaffold-template/ Vite + React + TS template the scaffold script copies
```
---
## How it works (highlights)
### 1. Quality protocol per node
Different phases use different quality-checking approaches — over-using sub-agents and over-writing review files is the #1 perf trap, so the skill makes the rules explicit:
| Node | How it's checked | Artifact |
|---|---|---|
| Phase 1 Source (default) | Main agent inline 5-item checklist | none |
| Phase 1 Source (complex / low-confidence only) | Source Reviewer SubAgent (diff against `original.*`) | `review/source-review.md` |
| Phase 2 Plan / before Checkpoint 1 | **Main agent inline self-check (no SubAgent, no file)** | none |
| Phase 4 First Spread / before Checkpoint 2 | First Spread Reviewer SubAgent | `review/first-spread-review.md` |
| Phase 5 Per Section | Section Reviewer SubAgent — returns pass/fail by message | none (no per-section files) |
| Phase 6 Final / before Checkpoint 3 | Editorial + Visual + Technical Reviewer SubAgents | `review/final-review.md` |
### 2. No silent default decisions
At every checkpoint each decision is asked **independently** — the agent may recommend, but never sneak through "I went ahead with X, tell me if it's wrong". Five independent decisions on Plan Checkpoint: article type (with recommended retention), theme, width, image mode, cover on/off.
### 3. Article type → retention bundles
The 10 article types all carry a recommended retention ratio: `longform · ~100%`, `tutorial · ~90%`, `full-report · ~80%`, `explainer · ~80%`, `dialogue · ~80%`, `review · ~70%`, `essay · ~70%`, `briefing · ~50%`, `visual-essay · ~40%`, `interactive-explainer · ~25% excerpt + 75% AI-rebuild`. Users can override with a single sentence.
### 4. One-section-one-file rule
Every Section is its own component file in `article/sections/NN-*.tsx`. `Article.tsx` is just the assembler — owned by the main agent, who imports & orders sections, runs typecheck / build, and resolves theme drift. This is the precondition for sub-agent parallelism in dev-mode B.
### 5. Theme tokens everywhere
`Raw` blocks must consume `--ra-*` theme tokens — no wild colors / fonts. Switching theme rewires every Raw block in one place. Each theme ships a Markdown authoring profile telling the agent how to write & style content under that theme.
---
## Setting up a project
This skill **does not pre-create** a workspace — every project gets its own. From the SKILL's Phase 4:
```bash
# Default: cover on
bash <path-to-skill>/scripts/scaffold.sh ./my-article --theme=tufte
# Cover off
bash <path-to-skill>/scripts/scaffold.sh ./my-article --theme=press --no-cover
# List available themes
bash <path-to-skill>/scripts/scaffold.sh --list-themes
```
The scaffold spins up a Vite + React + TS workspace, installs the latest published `reacticle` from npm, and seeds `source/ plan/ review/` plus `article/Article.tsx`, `article/Cover.tsx`, and `article/sections/01-opening.tsx` so the agent has a known starting shape.
After Phase 5 the agent runs the build to produce a single inlined HTML:
```bash
npm run build # → article/article.html (CSS + JS inlined)
```
Optional PDF export (only if Checkpoint 3 selects HTML + PDF):
```bash
bash <path-to-skill>/scripts/html-to-pdf.sh
```
---
## Best practices
### Recommended
1. **Pick the article type before anything else** — its retention ratio anchors the whole plan.
2. **Trust the harness phases** — don't skip Plan checkpoint just because you can guess the answer.
3. **Use theme tokens for every Raw block** — `--ra-*` only. No raw hex / font names.
4. **One section per file** — even if a section is small, isolate it. It pays off in review and repair.
5. **Cover should reflect theme + article gist** — not just a placeholder gradient.
### Avoid
1. ❌ Treating the skill as "make me an HTML page" — the deliverable is an *article*.
2. ❌ Bundling multiple checkpoint decisions into one yes/no.
3. ❌ Letting Raw blocks bring their own colors / typography (theme drift).
4. ❌ Writing all sections inside `Article.tsx` (kills sub-agent parallelism).
5. ❌ Removing the colophon / cover container (they are part of the contract).
---
## FAQ
**Q1: When should I *not* use this skill?**
When the user actually wants a web app, dashboard, form, prototype, or generic landing page — those go to `web-design-engineer`, not here. If in doubt, the skill stops and asks rather than silently producing the wrong artifact.
**Q2: Does it always produce 100% information retention?**
No — that is just the `longform` default. The article type sets the ratio, and the user can override at Checkpoint 1.
**Q3: Can the article be in a different language than the source?**
Yes. If the user specifies a target language different from the source, Phase 1 produces an idiomatic translation `source/source.<lang>.md` first, and Phase 2+ writes from that file.
**Q4: What if my agent runtime has no SubAgent / Task tool?**
The skill notes this case explicitly: the main agent backfills the SubAgent's job and writes "no SubAgent environment, main-agent fallback" at the top of the resulting review file.
**Q5: Why React + Vite + reacticle instead of plain HTML?**
Because the agent needs a stable, prose-first component contract that survives multi-section parallel work, theme switching, and Raw escape hatches. The `npm run build` step always inlines everything back into a single HTML for delivery.
---
## Tool requirements
The skill assumes the agent runtime can:
- Spawn shell commands (for `scaffold.sh`, `html-to-pdf.sh`, `npm` builds)
- Read / write files in a project workspace
- (Optionally) launch sub-agents for First Spread / Section / Final review nodes
- (Optionally) run `MarkItDown` (Python) for high-fidelity PDF / DOCX / HTML extraction; otherwise the lightweight fallback script handles Markdown / TXT / simple HTML
---
## License
MIT
@@ -0,0 +1,450 @@
# Beautiful Article Skill —— 把任意素材编辑成一篇精美的文章
> 一个面向 AI Agent 的 Skill:把任意素材(URL / PDF / DOCX / Markdown / 纯文本 / 截图 / 粘贴材料)**编辑、设计**成一篇**比原文更易读、更便于分享和归档的精美文章**。
[English](./README.md) · [返回集合首页](../../README.zh-CN.md)
![Beautiful Article Skill](../../dist/imgs/covers/beautiful-article.webp)
---
### 由 [ReActicle](https://github.com/ConardLi/reacticle) 驱动
`beautiful-article` 是编辑型 **harness**(方法论、checkpoint、主题选型、sub-agent reviewer);[`reacticle`](https://github.com/ConardLi/reacticle) 是 Skill 在运行时调用的**底层组件协议** —— prose-first 的 React 语义组件 + 基于主题 token 的 `Raw` 自由层,统一接到同一套主题系统上。
```
beautiful-article (本 Skill · 方法论 + harness)
│ 调用
▼
reacticle (npm 包 · 组件 / 主题 / Raw / 导出)
```
| 层级 | 负责什么 | 仓库 / 文档 |
|---|---|---|
| `beautiful-article`(本 Skill) | **怎么** 让 Agent 从任意素材出发,规划、撰写、审阅、交付一篇文章 —— 6 阶段流程、3 个硬 checkpoint、主题选型、sub-agent reviewer | 当前目录 |
| `reacticle` | Skill 实际拼装出的组件词表 + 11 套 authoring 主题 —— `Article` / `Hero` / `Lead` / `Section` / `Quote` / `Image` / `Formula` / `CodeBlock` / `Raw` …,每套主题 = 一份 `.css` token 包 + 一份 `.md` authoring profile | [`ConardLi/reacticle`](https://github.com/ConardLi/reacticle) · [npm `reacticle`](https://www.npmjs.com/package/reacticle) · [文档站](https://rearticle.mmh1.top/) |
二者搭配最佳但相互独立:Skill 因为有 ReActicle 这个稳定目标层才能跑通;ReActicle 单独作为 React 组件库使用也完全立得住。
---
### [文章实例](https://mmh1.top/#/ai-article) —— 用 `beautiful-article` + ReActicle 写出来的真实文章
每一篇都是 AI Agent 用本 Skill 调用 [`reacticle`](https://www.npmjs.com/package/reacticle) 组件协议端到端写完的真实长文。点封面即可在线打开单文件 HTML 版本。
<table>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/tools">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/tools.webp" alt="Agent Tools 设计的最佳实践" width="320">
<br><b>Agent Tools 设计的最佳实践</b>
</a>
<br><sub>Theme · Freddie · 长文 · 21 min</sub>
<br><sup>Anthropic 工程团队关于 Tools 的五条原则,与一套评测驱动的方法。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/skill">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/skill.webp" alt="Agent Skill 是如何进化的?" width="320">
<br><b>Agent Skill 是如何进化的?</b>
</a>
<br><sub>Theme · Freddie · 解释文 · 8 min</sub>
<br><sup>把 Skill 文档当成被训练的对象,而不是被复制粘贴的 prompt。</sup>
</td>
</tr>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/harness">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/harness.webp" alt="Agent Harness 的解剖图" width="320">
<br><b>Agent Harness 的解剖图</b>
</a>
<br><sub>Theme · Vignelli · 长文 · 12 min</sub>
<br><sup>智能在模型里;让智能变得有用的,是它周围的那套系统。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/prompt-cache">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/prompt-cache.webp" alt="提示词缓存对 Agent 有多重要?" width="320">
<br><b>提示词缓存对 Agent 有多重要?</b>
</a>
<br><sub>Theme · Bayer · 长文 · 15 min</sub>
<br><sup>缓存命中率是 Agent 的 SLO,Claude Code 团队的反直觉经验。</sup>
</td>
</tr>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/context">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/context.webp" alt="面向 Agent 的高效上下文工程" width="320">
<br><b>面向 Agent 的高效上下文工程</b>
</a>
<br><sub>Theme · Tufte · 长文 · 16 min</sub>
<br><sup>本文探讨如何高效地筛选与管理驱动 AI Agent 运转的上下文。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/transformer">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/transformer.webp" alt="Attention Is All You Need" width="320">
<br><b>Attention Is All You Need</b>
</a>
<br><sub>Theme · Tufte · 长文 · 30 min</sub>
<br><sup>一篇重塑现代 AI 的论文,逐层拆给你看。</sup>
</td>
</tr>
<tr>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/agent-eval">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/agent-eval.webp" alt="把 AI Agent 的评测讲清楚" width="320">
<br><b>把 AI Agent 的评测讲清楚</b>
</a>
<br><sub>Theme · Tufte · 长文 · 25 min</sub>
<br><sup>让 Agent 有用的那些能力,恰恰让它难以评测 —— 来自 Anthropic 的指南。</sup>
</td>
<td width="50%" valign="top" align="center">
<a href="https://mmh1.top/#/ai-article/agent-loop-codex">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/agent-loop-codex.webp" alt="Codex 的 Agent Loop 是怎么做的?" width="320">
<br><b>Codex 的 Agent Loop 是怎么做的?</b>
</a>
<br><sub>Theme · Sottsass · 长文 · 18 min</sub>
<br><sup>OpenAI 官方分享:在 Responses API 之上,一条对话是如何被反复"展开"的。</sup>
</td>
</tr>
</table>
---
### [主题概览](https://rearticle.mmh1.top/#/gallery) —— 每套主题一篇样品文章
> 11 套主题已上架。每套主题的完整契约(`.css` token 包 + `.md` authoring profile、anti-patterns、code/media style)见 [Theming](https://rearticle.mmh1.top/#/theming)。
每套主题都附一篇**样品长文**,从字体到摄影、代码、公式、Raw 块通通走一遍。点封面在线阅读,点主题名跳到该主题在文档站的章节。
<table>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/caffeine-half-life">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-tufte.webp" alt="Tufte · Data-Ink" width="260">
<br><b>Tufte</b> · Data-Ink
</a>
<br><sub>咖啡因与睡眠 · 数据笔记</sub>
<br><sup>Edward Tufte 数据墨水,证据优先,发丝级图表与最朴素的版式。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/movable-type">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-press.webp" alt="Press · 书卷" width="260">
<br><b>Press</b> · 书卷
</a>
<br><sub>活字之后 · 随笔</sub>
<br><sup>Stripe Press 式书卷长读物:会落定的标题、氧化血红首字母、纯正文之美。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/pool-exhaustion">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-shannon.webp" alt="Shannon · 工程暗色" width="260">
<br><b>Shannon</b> · 工程暗色
</a>
<br><sub>连接池耗尽 · 故障复盘</sub>
<br><sup>贝尔实验室技术论文血统,暗底黄金信号、回压依赖、夜间作战气质。</sup>
</td>
</tr>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/orbit-spec">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-vignelli.webp" alt="Vignelli · 瑞士" width="260">
<br><b>Vignelli</b> · 瑞士网格
</a>
<br><sub>Orbit 设计系统规格 · 规格</sub>
<br><sup>Massimo Vignelli 网格至上、grotesque 字族、瑞士红只承载结构。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/linear-attention">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/knuth.png" alt="Knuth · 学术" width="260">
<br><b>Knuth</b> · 学术
</a>
<br><sub>线性化自注意力 · 预印本</sub>
<br><sup>Donald Knuth / Computer Modern,编号小节、命题与证明、arXiv 草稿气质。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/first-newsletter">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-freddie.webp" alt="Freddie · 暖黄" width="260">
<br><b>Freddie</b> · 暖黄
</a>
<br><sub>第一封 Newsletter · 上手指南</sub>
<br><sup>Mailchimp Freddie 黑字荧光,亲和插画 + 不端着的产品上手语气。</sup>
</td>
</tr>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/slow-breathing">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-andy.webp" alt="Andy · 静谧" width="260">
<br><b>Andy</b> · 静谧
</a>
<br><sub>把呼吸放慢 · 练习</sub>
<br><sup>柔软圆润、呼吸-神经跷跷板,让人慢下来的练习气质。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/front-page">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-bodoni.webp" alt="Bodoni · 报刊" width="260">
<br><b>Bodoni</b> · 报刊
</a>
<br><sub>头版的消亡 · 特稿</sub>
<br><sup>高对比 Didone 报刊气质,黑白大报、对折线之上的分量。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/geometry-of-meaning">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-bayer.webp" alt="Bayer · 包豪斯" width="260">
<br><b>Bayer</b> · 包豪斯
</a>
<br><sub>形、色、网格 · 教学</sub>
<br><sup>Herbert Bayer 包豪斯三原色几何,形有性格、色有重量。</sup>
</td>
</tr>
<tr>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/rate-limiter-spec">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-fuller.webp" alt="Fuller · 蓝图" width="260">
<br><b>Fuller</b> · 蓝图
</a>
<br><sub>限流器设计规格 · 系统设计</sub>
<br><sup>Buckminster Fuller 工程蓝图,方格纸拓扑、令牌桶模拟,可照着实现。</sup>
</td>
<td width="33%" valign="top" align="center">
<a href="https://rearticle.mmh1.top/#/gallery/color-clash">
<img src="https://raw.githubusercontent.com/ConardLi/assets/main/imgs/article/theam-sottsass.webp" alt="Sottsass · 孟菲斯" width="260">
<br><b>Sottsass</b> · 孟菲斯
</a>
<br><sub>撞色不翻车 · 设计随笔</sub>
<br><sup>Memphis 80s 撞色,黑描边、硬投影、轻微旋转的不正经语法。</sup>
</td>
<td width="33%" valign="top" align="center">
&nbsp;
</td>
</tr>
</table>
> 完整的 11 套主题样品(含主题切换、搜索与筛选)在线 gallery:<https://rearticle.mmh1.top/#/gallery>。
---
## 这个 Skill 干什么
`beautiful-article` 把原本枯燥、线性、难以消化的文字材料变成视觉体验更漂亮、阅读节奏更清晰、也更便于审阅和分享的**文章**。它**不是**网页应用生成器 —— 注意力永远在"文章"本身:更好的阅读、更好的节奏、更好的美学。最终交付物是一份自包含、可离线打开的文件(可选随附 PDF),但那是交付细节、不是目标。
适合的场景:
- 把一篇长 URL / PDF / DOCX / Markdown 编辑成一篇**网页长文**
- 决策摘要 briefing、概念解释 explainer、教学步骤 tutorial、复盘 review、方案分析
- 视觉随笔 visual essay、对话 / 访谈 / 播客转写、交互式学习解释器
- 任何时候你希望**比 Markdown 更好的阅读载体** —— 表格、SVG、代码、公式、复制 / 导出按钮一条龙
Skill 本质上是一个**方法论 + 协作 harness**。它附带一个 Vite + React + TypeScript 脚手架,基于 [`reacticle`](https://www.npmjs.com/package/reacticle) 组件协议 —— Agent 不手写裸 HTML / CSS,而是用 prose-first 的语义组件 + 主题约束的 `Raw` 自由层来组合。
---
## 核心思想
- **首先是一篇文章,不是应用** —— 注意力永远在文章。Raw 自由层、SVG、小工具必须服务阅读 / 解释 / 节奏 / 审美,不能喧宾夺主。
- **Source → Plan → Build → Review 小型 harness** —— 每个项目走 6 个有编号的 phase,中间有 3 个硬 checkpoint。
- **`reacticle` 组件协议** —— 语义化的 prose 组件(Hero / Lead / Section / Quote / Callout / Image / Formula / CodeBlock / Table …)+ 一个必须用主题 token(`--ra-*`)的 `Raw` 自由层。
- **主题驱动设计** —— 内置 11 套 authoring profile(`tufte` / `press` / `bayer` / `bodoni` / `vignelli` / `sottsass` / `freddie` / `andy` / `fuller` / `knuth` / `shannon`),每套是给 Agent 看的 Markdown 契约,不是 CSS 文件。
- **默认 100% 信息保留** —— 文章类型自带推荐保留比例(longform `~100%` / briefing `~50%` / visual-essay `~40%` …),用户可一句话覆盖。
- **硬协作 checkpoint** —— Plan、首屏样张、最终交付前 Agent 必须停下来;**每个决策必须逐项独立确认,不允许打包一个"全部 OK 吗"**。
- **默认带封面** —— 一个 3:4 书封式题图位于 TOC 之上,外壳锁死、只能用主题 token、不允许远程图片。
- **PDF 导出可选** —— 主交付物是一份自包含的 HTML 文件;PDF 只在用户在 Checkpoint 3 主动选择时通过零依赖的 `html-to-pdf.sh` 生成。
---
## 工作流
```text
Phase 0 Intake
│
Phase 1 Source → Markdown (URL / PDF / DOCX / MD / 文本 → source.md)
│
Phase 2 Editorial Planning (一份 plan.md:Brief / Outline / Theme / Assets)
│
★ Checkpoint 1 Plan (5 个独立决策逐项确认)
│
Phase 4 First Spread (封面 + Hero + 第一节 + 一个代表性视觉块)
│
★ Checkpoint 2 First Spread (验收 + 后续开发模式 A/B)
│
Phase 5 Full Article Build (单 Agent 顺序 / 多 Agent 并行)
│
Phase 6 Final Review (Editorial / Visual / Technical 三视角)
│
Phase 7 Repair (只允许最小切片修复)
│
★ Checkpoint 3 Delivery (HTML,或 HTML + PDF,或暂停修订)
│
Phase 8 Delivery (article.html,可选 article.pdf)
```
每个项目都有自己的工作区目录,作为 Agent 的**长期记忆**:
```text
<workspace>/
source/ original.* source.md source.<lang>.md(需翻译时) extraction-notes.md
plan/ plan.md
article/ Cover.tsx Article.tsx sections/ raw-blocks/ assets/ article.html(产物)
review/ first-spread-review.md final-review.md
(source-review.md 仅复杂源;repair-log.md 仅有修复时)
index.html package.json vite.config.ts tsconfig*.json (构建工装)
```
---
## Skill 目录结构
```text
skills/beautiful-article/
├── SKILL.md 主 Skill 文件(frontmatter name: beautiful-article)
├── manifest.json 发布清单
├── README.md / README.zh-CN.md 本文档
├── references/
│ ├── article-types.md 文章类型路由
│ ├── article-types/ briefing / dialogue / essay / explainer / full-report
│ │ interactive-explainer / longform / review / tutorial / visual-essay
│ ├── information-density.md 保留比例与组件 / 视觉比例的关系
│ ├── plan-template.md 单一 plan.md 模板(Brief / Outline / Theme / Assets)
│ ├── theme-selection.md 主题选择,density 与 theme 解耦
│ ├── layout.md 版式宽度、TOC 默认值
│ ├── cover.md 3:4 书封封面指南(5 条自检 + 5 个构图模板)
│ ├── asset-policy.md 配图策略(none / user-assets / placeholders / ai-generated)
│ ├── component-policy.md Reacticle 组件契约、prose-first
│ ├── raw-policy.md Raw 允许 / 禁止表、token 驱动、自检
│ ├── section-build.md 一节一文件铁律、subagent prompt 模板
│ ├── source-to-markdown.md 各类输入抽取规则 + 5 条自检
│ ├── scaffold.md 脚手架行为、工作区结构
│ ├── html-output.md dev / build / 单文件 HTML 命令
│ ├── pdf-output.md html-to-pdf.sh 用法 + print CSS 覆盖
│ ├── review-checklist.md 各阶段 reviewer 清单与 sub-agent prompt
│ ├── repair-policy.md 最小切片修复对照表
│ └── harness.md Skill-as-harness 视角
├── theme-profiles/
│ ├── index.json 主题注册表
│ └── andy / bayer / bodoni / freddie / fuller / knuth / press / shannon / sottsass / tufte / vignelli
├── scripts/
│ ├── scaffold.sh 一键创建工作区
│ ├── html-to-pdf.sh 可选 HTML → PDF(headless 浏览器,零 npm 依赖)
│ ├── pdf-print-overrides.css 注入 HTML 的 @media print 覆盖
│ ├── source-to-markdown-markitdown.py 主路径抽取(PDF / DOCX / HTML)
│ └── source-to-markdown.py 轻量 fallback(Markdown / TXT / 简单 HTML)
└── assets/
└── scaffold-template/ 脚手架脚本拷贝的 Vite + React + TS 模板
```
---
## 它是怎么工作的(要点)
### 1. 各节点的质检协议
不同 phase 用不同的质检方式 —— 滥开 SubAgent、滥写 review 文件是首要性能问题,所以 Skill 把规则写明:
| 节点 | 怎么检 | 产物 |
|---|---|---|
| Phase 1 Source(默认) | 主 Agent 内联 5 条 checklist | 无文件 |
| Phase 1 Source(仅复杂 / 低置信源) | Source Reviewer SubAgent(对照 `original.*` diff) | `review/source-review.md` |
| Phase 2 Plan / Checkpoint 1 前 | **主 Agent 内联自查(禁开 SubAgent、禁写文件)** | 无文件 |
| Phase 4 First Spread / Checkpoint 2 前 | First Spread Reviewer SubAgent | `review/first-spread-review.md` |
| Phase 5 每个 Section | Section Reviewer SubAgent —— 以消息返回 pass/fail | 无(不写每节文件) |
| Phase 6 终审 / Checkpoint 3 前 | Editorial + Visual + Technical Reviewer SubAgent | `review/final-review.md` |
### 2. 禁止静默替用户选择
每个 Checkpoint 的每一项决策必须**独立**问 —— Agent 可以推荐,但不能"我已经替你定了 X,不对再说"。Plan Checkpoint 5 项独立决策:文章类型(含推荐保留比例)/ 主题 / 版式宽度 / 配图模式 / 封面开关。
### 3. 文章类型 → 保留比例打包
10 种文章类型都自带推荐保留比例:`longform · ~100%` / `tutorial · ~90%` / `full-report · ~80%` / `explainer · ~80%` / `dialogue · ~80%` / `review · ~70%` / `essay · ~70%` / `briefing · ~50%` / `visual-essay · ~40%` / `interactive-explainer · ~25% 摘录 + 75% AI 重构`。用户可一句话覆盖。
### 4. 一节一文件铁律
每个 Section 都是 `article/sections/NN-*.tsx` 单文件组件。`Article.tsx` 只是 assembler —— 由主 Agent 拥有,负责 import 与排序、跑 typecheck / build、解决主题漂移。这是后续多 Agent 并行的前提。
### 5. 全程主题 token
`Raw` 块必须只用 `--ra-*` 主题 token —— 不允许野生颜色 / 字体。换主题时所有 Raw 块一处生效。每个主题都有一份 Markdown authoring profile,告诉 Agent 在该主题下怎么写、怎么排版。
---
## 创建一个项目
Skill 不预先创建工作区 —— 每个项目自己开。来自 Phase 4 的命令:
```bash
# 默认开封面
bash <path-to-skill>/scripts/scaffold.sh ./my-article --theme=tufte
# 关闭封面
bash <path-to-skill>/scripts/scaffold.sh ./my-article --theme=press --no-cover
# 查看可用主题
bash <path-to-skill>/scripts/scaffold.sh --list-themes
```
脚手架会拉起 Vite + React + TS 工作区,从 npm 装最新 `reacticle`,并放好 `source/ plan/ review/` 三个目录 + `article/Article.tsx` / `article/Cover.tsx` / `article/sections/01-opening.tsx` 三份起点文件。
Phase 5 完成后 Agent 跑构建产出单文件 HTML:
```bash
npm run build # → article/article.html(CSS + JS 全内联)
```
可选 PDF 导出(仅当 Checkpoint 3 用户选了 HTML + PDF):
```bash
bash <path-to-skill>/scripts/html-to-pdf.sh
```
---
## 最佳实践
### 推荐
1. **先定文章类型** —— 它的保留比例锚定整个 plan。
2. **信任 harness 的 phase** —— 不要因为"答案显而易见"就跳过 Plan checkpoint。
3. **Raw 块只用主题 token** —— `--ra-*`,禁止裸颜色 / 字体名。
4. **一节一文件** —— 哪怕这一节很短,也要隔离。review 与修复阶段会回报这点投入。
5. **封面要呼应主题 + 文章主旨** —— 不是一个占位渐变。
### 避免
1. ❌ 把 Skill 当成"帮我做个 HTML 页面" —— 交付物是**文章**。
2. ❌ 把多个 Checkpoint 决策打包成一个 yes/no。
3. ❌ Raw 块自带颜色 / 字体(主题漂移)。
4. ❌ 把所有 Section 都写进 `Article.tsx`(杀死 sub-agent 并行)。
5. ❌ 删除 colophon / 封面外壳(这些是契约的一部分)。
---
## 常见问题
**Q1:什么时候不应该用这个 Skill?**
当用户其实想要的是网页应用 / dashboard / 表单 / 原型 / 通用 landing page —— 这些去找 `web-design-engineer`,不是这里。不确定时 Skill 会停下来澄清,而不是默默生成错的产物。
**Q2:是不是总是 100% 信息保留?**
不是 —— 那只是 `longform` 类型的默认值。文章类型决定推荐比例,用户可以在 Checkpoint 1 覆盖。
**Q3:文章语言可以与源材料不同吗?**
可以。如果用户指定的目标语言与源不一致,Phase 1 会先产出地道翻译版 `source/source.<lang>.md`,Phase 2+ 据此编写。
**Q4:如果 Agent 运行时没有 SubAgent / Task 工具怎么办?**
Skill 显式照顾了这种情况:主 Agent 兜底承担 SubAgent 的工作,并在 review 文件首注明"无 SubAgent 环境,主 Agent 兜底"。
**Q5:为什么用 React + Vite + reacticle 而不是裸 HTML?**
因为 Agent 需要一个稳定的、prose-first 的组件契约 —— 它要扛住多 Section 并行写作、主题切换、Raw 自由层这些场景。`npm run build` 时所有依赖会全部内联回单文件 HTML 交付。
---
## 工具要求
Skill 假设 Agent 运行时可以:
- 启动 shell 命令(用于 `scaffold.sh` / `html-to-pdf.sh` / `npm` 构建)
- 在工作区读写文件
- (可选)开启 sub-agent 来跑 First Spread / Section / Final review
- (可选)调用 `MarkItDown`(Python)做高保真 PDF / DOCX / HTML 抽取;不可用时由轻量 fallback 脚本处理 Markdown / TXT / 简单 HTML
---
## 许可证
MIT
+466
View File
@@ -0,0 +1,466 @@
---
name: beautiful-article
description: "把用户提供的素材(网页 URL / PDF / DOCX / Markdown / 纯文本 / 截图 / 粘贴材料)编辑、设计成一篇美丽的、可离线打开和分享的**单文件 HTML 网页文章**。基于 reacticle 组件协议:不手写裸 HTML/CSS,而用语义组件 + 受主题约束的 Raw 自由层;按 source→规划→双确认→生成→终审→修复的小型 harness 流程推进,默认 100% 信息保留的长文。触发场景:把 URL/PDF/DOCX/文章做成网页文章 / 长文 / briefing / 解释文 / 视觉文章 / 教程 / 审阅复盘 / 方案分析,'render this as a beautiful web article / 把这篇做成网页文章 / 生成一篇可分享的 HTML 长文 / reacticle 文章'。只生成文章,不生成后台、表单、dashboard、产品原型或通用 Web App。"
---
# Beautiful Article
## 背景原则
AI 生成内容越复杂,输出媒介越重要。HTML 的价值在于同时提升信息密度、视觉清晰度、分享便利性和交互能力:表格、SVG、CSS、代码片段、可调控件、复制与导出按钮,可以让读者不只是“看完”,而是能比较、定位、调整、复查和继续使用。Beautiful Article 的目的,是把原本枯燥、线性、难以消化的文字材料,转换成视觉体验更漂亮、阅读节奏更清晰、也更容易审阅和分享的单文件网页文章。
## 边界(先判断要不要进这个 Skill)
- 最终主产物是 **single HTML 文章**,不是网页应用。
- 文章可以有 `Raw` 自由层(任意 HTML / CSS / JS / React:交互、布局排版、动效、小工具、
按需的 SVG / canvas 图解),但**必须服务阅读、解释、论证、节奏或审美**。
- **不**生成:后台、表单、拖拽工作台、完整 dashboard、产品原型、通用 Web App。
- 信息密度由用户确认;**默认保留 100% 信息**,生成长文式网页文章。
如果用户要的是应用而不是文章,停下来澄清,不要进入本 Skill。
---
## 工作流总览
```
Phase 0 Intake 判断是否进入本 Skill + 初步文章类型
▼
Phase 1 Source → Markdown URL/PDF/DOCX/MD/文本 → source.md + extraction-notes.md
└ 主 Agent 内联 5 条 checklist 自查(仅复杂/低置信源升级 SubAgent)
▼
Phase 2 Editorial Planning 一份 plan.md(Brief / Outline / Theme / Assets 四段)
└ 主 Agent 内联自查(无 SubAgent、无 review 文件)
▼
Phase 3 Plan Checkpoint ★Checkpoint 1 必须停。逐项确认 5 件事:文章类型(含标配保留比例)/ 主题 / 版式 / 配图模式 / 封面
▼
Phase 4 First Spread 首屏 + 第一节 + 一个代表性视觉块(脚手架在此创建)
└ First Spread Reviewer SubAgent(写 review/first-spread-review.md)
└ ★Checkpoint 2 必须停。逐项确认 2 件事:验收结论 / 开发模式 A/B
▼
Phase 5 Full Article Build 生成完整网页文章(默认单 Agent,超长可按 Section 隔离)
└ Section Reviewer SubAgent(以消息返回 pass/fail,无须写 review 文件)
▼
Phase 6 Final Review Editorial / Visual / Technical 三视角终审(写 review/final-review.md)
▼
Phase 7 Repair 最小切片修复,有修复才写 repair-log.md
▼
Phase 8 Delivery ★Checkpoint 3 必须停。逐项确认交付决策 → 交付 article.html + 简短编辑说明
```
工作区结构(脚手架创建;这些文件是 Skill 的长期记忆,**不要只依赖聊天上下文记决策**):
```text
<workspace>/
source/ original.* source.md source.<lang>.md(需翻译时) extraction-notes.md
plan/ plan.md # 单一规划文件:Brief / Outline / Theme / Assets 四段
article/ Cover.tsx(默认) Article.tsx sections/ raw-blocks/ assets/ article.html(产物)
review/ first-spread-review.md final-review.md # 仅这两份是常规产物
source-review.md(仅复杂源) repair-log.md(仅有修复时)
index.html package.json vite.config.ts tsconfig*.json (构建工装)
```
---
## 硬性质检协议(贯穿整个 Skill)
**质检方式按节点区分 —— 不是所有质检都要开 SubAgent,也不是所有质检都要写文件。**
误开 SubAgent / 误写文件是首要性能问题,按下表严格执行:
| 节点 | 质检方式 | 产物 | 为什么 |
|---|---|---|---|
| **Phase 1 Source(默认)** | 主 Agent 内联 5 条 checklist | 无文件 | 主 Agent 反正要通读 source.md |
| Phase 1 Source(仅复杂/低置信源) | Source Reviewer SubAgent(对照 `original.*` diff) | `review/source-review.md` | 静默丢失只能 diff 抓到 |
| **Phase 2 Plan / Checkpoint 1 前** | **主 Agent 内联自查(禁止开 SubAgent)** | **无文件** | plan 是文字决策且 200-400 行,上下文是热的,SubAgent 冷启反而更慢 |
| **Phase 4 First Spread / Checkpoint 2 前** | First Spread Reviewer SubAgent | `review/first-spread-review.md` | 首屏定调,多一道独立眼睛更稳 |
| **Phase 5 每个 Section** | Section Reviewer SubAgent | **以消息返回 pass/fail + 修复点(不写文件)** | 一篇可能 5-15 节,N 份 review 文件无人再读 |
| **Phase 6 终审 / Checkpoint 3 前** | Editorial + Visual + Technical Reviewer SubAgent | `review/final-review.md` | 交付物的一部分,留档有价值 |
**铁律:**
1. **Plan Checkpoint(Phase 2 → Checkpoint 1)严禁开 SubAgent 做质检**。主 Agent 写完 plan.md
后**就地**对照 5 条清单(见 `references/review-checklist.md` 的 Plan 自查段)核查、按结论
改完 `plan/plan.md`,**不要写任何 review 文件**,然后进入 Checkpoint 1。
2. First Spread / Final 必须用 SubAgent(这两个节点 SubAgent 价值 > 开销);只有探测不到
SubAgent 环境才由主 Agent 兜底,并在文件首注明"无 SubAgent 环境,主 Agent 兜底"。
3. Section Reviewer 用 SubAgent,但**返回值是消息**(pass / fail + 修复点);fail 项主 Agent
收到后直接修,**不要让 SubAgent 写 `review/section-NN-review.md` 文件**。
4. 拿到任何质检结论 —— **先按 fail 项把产出改完,再汇报"做完了 + 自检结论 + 改了什么"**。
直接拿原始结论汇报但不修复 = 违规。
5. **决策收集铁律 · 禁止静默替用户选择**:在每个 Checkpoint(1 / 2 / 3),所有需要用户确认的
决策项**必须每项独立列出 + 等用户答复**。Agent **可以推荐**("我推荐 X,因为 …"),但
**不能"已经替你定了 X,如果不对再说"** —— 这等于剥夺选择机会。
- **优先**:如果环境有 `AskQuestion` 工具,每个决策项作为一个独立 question(一次调用可
传多个 question),用户能用选择卡逐项确认。
- **否则**:停下来在消息里把所有问题**编号列出**(每个问题独占一段、写清推荐项 + 理由 +
备选项),明确说"我等你逐项答复后再继续",**不要继续做任何后续工作**。
- **绝不**:把多项决策打包成一个"全选我推荐的 / 全部 OK 吗?"yes/no 问题;也不要在
"推荐一句话"后默认直接进下一步。
各节点的 checklist 与 SubAgent prompt 模板见 `references/review-checklist.md`。
---
## 各阶段文件读取指南(渐进加载,别一次全读)
| 阶段 | 必读 | 按需查 |
|---|---|---|
| Phase 0 Intake | `references/harness.md` | —— |
| Phase 1 Source→MD | `references/source-to-markdown.md` | `scripts/source-to-markdown-markitdown.py` · `scripts/source-to-markdown.py` |
| Phase 2 Planning | `references/article-types.md` · `references/information-density.md` · `references/plan-template.md` · `references/theme-selection.md` · `references/layout.md` · `references/asset-policy.md` · `references/cover.md`(封面构图想法) | `references/article-types/<type>.md` · `theme-profiles/*.md` |
| Phase 4 First Spread / Phase 5 Build(每节回看) | `references/section-build.md` · `references/component-policy.md` · `references/raw-policy.md` · 选定主题 `theme-profiles/<id>.md` · **封面:`references/cover.md`** | `references/scaffold.md`(建项目时一次)· `references/html-output.md` |
| Phase 6/7 Review & Repair | `references/review-checklist.md` · `references/repair-policy.md` | —— |
| Phase 8 Delivery | `references/html-output.md` | `references/pdf-output.md`(仅当用户选 PDF 导出) |
> **长会话里 agent 容易遗忘原则** —— Phase 5 会重复实现 N 个 Section,**每次开工
> 前回看** `component-policy.md` + `raw-policy.md` + 当前主题 `theme-profiles/<id>.md`。
---
## Phase 0 —— Intake
判断是否进入本 Skill,给出初步文章类型与输出模式(默认 single HTML)。
| 用户给的东西 | 该做的 |
|---|---|
| 一个或多个素材(URL/PDF/DOCX/MD/文本/截图) | 进入 Phase 1 |
| 只说"帮我做篇 X 文章"但没素材 | **反问**:先要素材或大纲。Skill 不替用户凭空构思内容 |
| 明显要的是应用 / 工具 / dashboard | 停下来澄清,不进入本 Skill |
**捕获目标语言**:开场就记录用户**期望的最终文章语言**(如用户提到"用中文/做成英文版"等)。
- **用户指定了语言** → 记进 `plan/plan.md` Brief 段的"目标语言"。若与源材料语言不一致,Phase 1
需先产出一份**地道翻译版**源文,后续基于翻译版编写(见 Phase 1)。
- **用户未指定** → 默认**最终文章语言跟随源材料语言**,不做翻译。
自检:用户要的是**文章**还是**网页应用**?是否需要完整信息?是否要先索取更多素材?
**用户有没有指定最终语言?与源语言是否一致?**
---
## Phase 1 —— Source → Markdown
把任意输入统一成 `source/source.md`,把不确定项写进 `source/extraction-notes.md`。
规则与各类输入处理见 `references/source-to-markdown.md`;可借助
MarkItDown 主路径或轻量 fallback 脚本做 PDF/DOCX/HTML 抽取。
落盘后由**主 Agent 内联自查**(`references/source-to-markdown.md` 的 5 条 checklist),按结论修复
再进入 Phase 2;**仅当 `extraction-notes.md` 标记低置信 / 复杂源**时,才升级为独立 Source Reviewer
SubAgent 并对照 `original.*` 做 diff 式核查(写 `review/source-review.md`)。
**语言处理(紧接抽取之后)**:判断 `source.md` 的语言。
- 用户**未指定**目标语言,或目标语言**与源一致** → 不翻译,后续直接基于 `source.md` 编写,
最终文章语言 = 源语言。
- 用户**指定**了目标语言且**与源不一致** → 先产出**地道翻译版** `source/source.<lang>.md`
(如 `source.zh.md` / `source.en.md`),作为后续 Phase 2+ 的**事实底座**;原文 `source.md` 保留
备查。翻译要求:**用地道的目标语言、去除翻译腔**(按目标语言的表达习惯重组句子,不逐字直译,
不留生硬的外语语序 / 被动堆叠 / 异国标点),术语 / 数字 / 代码 / 公式 / 引用保持准确,结构与
信息保留比例不变。翻译说明写进 `extraction-notes.md`。
---
## Phase 2 —— Editorial Planning
形成编辑方案,**不直接写 HTML**。**只产出一份 `plan/plan.md`**(四段:Brief / Outline /
Theme / Assets),模板见 `references/plan-template.md`:
- **Brief**:目标读者 / 文章类型 / 信息保留比例 / 必须保留 / 可删减 / 语气 / 主要观点 /
阅读目标 / 目标语言 / 版式宽度 / TOC / 配图策略。
- **Outline**:Hero / Lead / Summary / Section 列表 / 每节保留哪些信息 / 每节是否需要
Raw·Table·CodeBlock·Formula·Image / 结尾方式。
- **Theme**:选定主题 + 理由 + 冲突说明(见 `references/theme-selection.md`)。
- **Assets**:配图策略与逐图计划(见 `references/asset-policy.md`;`none` 模式下本段
写一句话即可)。
文章类型路由见 `references/article-types.md`;信息密度与组件比例见
`references/information-density.md`。
**自检方式 · 强约束**:写完 `plan/plan.md` 后由**主 Agent 内联**对照 5 条 Plan 自查清单核查
(见 `references/review-checklist.md` 的 Plan 自查段),按结论改完 `plan/plan.md`,**直接进入
Checkpoint 1,禁止开 SubAgent,禁止写 `review/plan-review.md`**。
---
## Phase 3 —— Plan Checkpoint(★硬节点 · Checkpoint 1,必须停)
**铁律:禁止静默替用户选择。每个决策项必须独立列出、独立等用户答复。**
可以推荐("我推荐 X,因为 …"),**不能**说"已经替你定了 X,如果不对告诉我"——后者等于把
默认值偷渡过去、剥夺选择机会。
**收集方式(按环境二选一):**
- **优先 `AskQuestion` 工具**:每项作为一个独立 question 传入(一次调用可传多个 question),
用户用选择卡逐项确认。
- **无 `AskQuestion` 工具**:停下来在消息里把每个问题**编号列出 + 独占一段 + 写清推荐项 + 理由
+ 备选项**,明确说"我等你逐项答复后再继续",**不要继续做任何后续工作**。
无论哪种方式:每个**独立决策**对应**一个独立问题**,**不要打包成"全部 OK 吗?" yes/no**。
**必须独立确认的 5 项**(缺一不可):
| # | 决策项 | 选项(语义化标签 · 含标配信息保留比例) | 备注 |
|---|---|---|---|
| 1 | **文章类型**(信息保留比例打包在内) | 完整长文 / 归档 `longform · ~100%` / 研究报告 / 正式分析 `full-report · ~80%` / 教学步骤 / 上手指南 `tutorial · ~90%` / 概念 / 系统解释 `explainer · ~80%` / 对话 / 访谈 / 播客 `dialogue · ~80%` / PR / 方案 / 事故审阅 `review · ~70%` / 观点 / 评论 / 叙事 `essay · ~70%` / 交互式学习 / 玩明白一个概念 `interactive-explainer · ~25% 原文摘录 + 75% AI 重构` / 决策摘要 / 给忙人看 `briefing · ~50%` / 图文为主 / 传播展示 `visual-essay · ~40%` | AI 推荐一个并写一句理由。**比例已绑进类型选项**,不再单独成题(否则会出现 `longform + 20%` 这种伪组合)。用户想偏离标配,用自由文本一句话覆盖("我要 longform + 60%"),见下方"如何偏离标配" |
| 2 | **主题** | tufte / press / 其它已注册主题(读 `theme-profiles/index.json`) | AI 推荐一个并写一句理由 |
| 3 | **版式宽度** | narrow / regular / wide / full | AI 推荐一个;默认 `regular` |
| 4 | **配图模式**(必选 · 不允许"默认通过") | none / user-assets / placeholders / ai-generated | 一句话"只决定是否使用外部 `Image`;`Raw` 不受影响" |
| 5 | **封面**(3:4 书封式题图,位于 TOC + 正文之上) | 开(默认) / 关 | AI 推荐"开",并给一句构图想法(哪种主视觉 + 选哪个封面模板 A/B/C/D/E)。`briefing` / `dialogue` 可推荐"关"。详见 `references/cover.md` |
**TOC 默认开**:因为它只有一个开关 + 几乎所有文章都该开,可以在 Plan Checkpoint 开场说明
里以"默认 TOC 开,要关告诉我"一句话带过,**不必单独成题**。
**已经走默认值、不必单独问的事项**(仍然要在开场说明里明示"如要改请告诉我",给用户机会
反悔,不能完全藏起来):
- 最终文章语言:跟随源语言(除非用户已经在前文指定 / 已经翻译完成)。
- 是否允许编辑删减、重组、改写语气:默认允许(按上面的信息保留比例执行)。
- 是否要先看首屏样张:默认会先做(这就是 Phase 4)。
- TOC:默认开。
主题用户说"你定" → 取你推荐的第一个,**在选项里仍要把它和其它候选并列**,标"默认 · AI
推荐",留反悔余地,不能直接跳过主题问题。
**如何偏离信息保留比例的"标配"**:每个文章类型都自带一个推荐保留比例(见上表)。绝大
多数情况走标配即可。如果用户想精修(比如 "longform 但只要 60%" → 一篇被深度编辑过的长文),
让用户**在开场说明后的自由文本里写一句**"我要 <类型> + <X%>" 覆盖。AI 收到覆盖后要在
`plan/plan.md` 的 Brief 段同时记下"类型 / 标配保留 / 用户覆盖到 X%",并提醒用户这是"非标配
组合"——这类组合需要主 Agent 在写每节时手动调整正文/视觉比例。
**Plan Checkpoint 开场消息模板(在收集决策之前先发一条简短说明):**
```
plan/plan.md 已经写好(自检通过)。我会逐项跟你确认 5 件事:文章类型 / 主题 / 版式宽度 /
配图模式 / 封面。
我的推荐先放在这里供参考(不会替你选):
- 类型:<X>(含标配信息保留 <Y%>。理由:…)
- 主题:<theme>(理由:…)
- 版式宽度:<width>(理由:…)
- 配图模式:<策略>(理由:…)
- 封面:开 / 关(理由:…;若开,构图想法:…)
默认走但你可以推翻:语言跟随源语言;允许编辑删减重组;TOC 开;接下来会先做首屏样张。
信息保留比例如要偏离类型标配,下面回答完直接告诉我具体百分比(如 "longform 但只要 60%")。
下面逐项请你确认。
```
发完上面这条说明后,**立刻**用 AskQuestion 传 5 个 question(或在无工具环境下编号列出 5
个问题、停下等答复)。**5 项全部收齐答复才能进 Phase 4**;若用户在自由文本里给了"非标配保留
比例",先确认 AI 已经记进 `plan/plan.md` 再进 Phase 4。
---
## Phase 4 —— First Spread(文章版"第一章验收")
先做"封面(若开) + 首屏 + 第一节 + 一个代表性视觉块"。**脚手架在这里创建工作区**:
```bash
# 默认开封面
bash <path-to-beautiful-article>/scripts/scaffold.sh ./my-article --theme=<id>
# Checkpoint 1 用户选了"封面 · 关"
bash <path-to-beautiful-article>/scripts/scaffold.sh ./my-article --theme=<id> --no-cover
bash <path-to-beautiful-article>/scripts/scaffold.sh --list-themes
```
它创建 Vite + React + TS 工作区(从 npm 安装 `reacticle` 最新发布版)+ `source/ plan/
review/` 记忆目录 + assembler `article/Article.tsx` + 一个示例 section 组件
(+ 默认 `article/Cover.tsx`,除非 `--no-cover`)。详见 `references/scaffold.md`。
首屏(Hero / Lead)写进 assembler `article/Article.tsx`;**第一个 Section 必须写成独立组件**
`article/sections/01-*.tsx`(这是后续并行的代码锚点,见 `references/section-build.md`)。
**封面**(若开)替换 `article/Cover.tsx` 里的 `<CoverPlaceholder />` 为按主题 + 文章主旨
定制的图文构图,**外壳(3:4 容器 + 打印分页)不要动**。封面设计指南见 `references/cover.md`。
`npm run dev` 预览。它决定标题气质 / 字号 / 内容密度 / Raw 风格 / 配图方式 / 主题是否合适。
**第一个 Section 完成后,按硬性质检协议创建 First Spread Reviewer SubAgent**,写
`review/first-spread-review.md`(**含封面 5 条自检**,见 `references/cover.md`),改完
再进 Checkpoint 2。
---
## Checkpoint 2 · First Spread(★硬节点,必须停)
让用户验收首屏 + 第一个 Section,**并选定后续开发模式**。同样适用 Checkpoint 1 的决策收
集铁律:**两项独立确认,禁止打包;优先 AskQuestion,无工具则编号列出、停下等答复**。
先发一条简短消息:
```
首屏 + 第一个 Section 做好了,npm run dev 在 localhost 预览。
质检结论见 review/first-spread-review.md(已按 fail 项改完,列出修了哪些)。
下面两件事请你独立确认:1) 验收结论 2) 后续开发模式。
```
然后用 AskQuestion 传**两个独立 question**(或编号列出两个问题,停下等答复):
1. **验收结论** —— 选项:`通过 · 进入完整生成` / `局部修改 · 我会另起一条说改哪里` /
`主题或版式不合适 · 回到 Checkpoint 1`。
2. **后续开发模式** —— 选项:`A · 单 Agent 顺序(默认 · 最稳 · 风格最统一)` /
`B · 多 Agent 并行(最快 · 风格轻微差异)`。
**不要把这两件事打包成"通过 + A,OK 吗?"** —— 用户可能"通过验收但想用 B"或反之。
两题都收齐答复后进入 Phase 5。
---
## Phase 5 —— Full Article Build
按 Checkpoint 2 选定的开发模式生成完整文章。详见 `references/section-build.md` +
`references/component-policy.md` + `references/raw-policy.md`。
**铁律 · 每个 Section 必须是独立组件文件**(`article/sections/NN-*.tsx`),**坚决不允许把
多个 Section 直接写进一个组件**。`article/Article.tsx` 只是 **assembler**:import 并排序各
Section,由**主 Agent 拥有**。大型 Raw 同样隔离到 `article/raw-blocks/NN-*.tsx`。文件级隔离
是多 Agent 并行的前提。
开发模式(Checkpoint 2 选定):
- **A · 单 Agent 顺序(默认)**:主 Agent 顺序写每个 `sections/NN-*.tsx`,最稳、风格最统一。
- **B · 多 Agent 并行**:subagent 各**拥有一个** `sections/NN-*.tsx` 文件并行开发;**主 Agent
负责合并与稳定性** —— 维护 `Article.tsx` 的 import 与顺序、跑 `npm run typecheck` / `build`、
兜底主题与风格一致、解决冲突。subagent prompt 模板见 `references/section-build.md`。
其余原则:正文是主体;所有 Raw 用 `--ra-*` 主题 token,禁止野生样式;100% 信息保留以长文
结构为主、Raw / 配图做增强;低信息密度可提高视觉块比例,但仍必须是**文章形态**。
每个 Section 完成后**必须**按硬性质检协议走 **Section Reviewer SubAgent**:是否完成 outline
任务 / 是否符合信息保留比例 / 是否与前后衔接 / 是否过度组件化 / 是否有足够正文 / Raw 与
配图是否有明确目的 / 本节序号自洽。
**SubAgent 以消息返回 pass/fail + 修复点**(pass 则一行 OK;fail 则列出修复点),**不要写
`review/section-NN-review.md` 文件**。主 Agent 收到 fail 项后**直接修对应 section 文件**,
然后再汇报本节交付。
---
## Phase 6 —— Final Review(三视角终审)
从读者 / 主题 / 技术三个视角验收,产出 `review/final-review.md` + 修复列表。
完整硬性清单见 `references/review-checklist.md`。推荐三个 Reviewer(无 Teams 时至少
一个独立 SubAgent):
1. **Editorial Reviewer**:文章性、信息取舍、结构。
2. **Visual Reviewer**:主题、Raw、配图、移动端。
3. **Technical Reviewer**:构建、控制台、代码 / 公式、可访问性。
核心红线:它仍是一篇文章(不是应用)· 信息保留比例符合 Plan · 必须保留的信息没丢 ·
主题气质统一 · Raw 无野生样式 · 没有明显 AI 味 · 桌面 + 移动端可读 · HTML 可构建可打开可分享。
---
## Phase 7 —— Repair(最小切片)
按最小单位修复,规则见 `references/repair-policy.md`。**禁止**:只反馈一处就重写整篇 /
为修视觉改动已确认的文章结构 / 为压缩信息删掉用户指定必须保留的内容。**有修复才写**
`review/repair-log.md`(无修复 / 一次过则不写)。
---
## Checkpoint 3 · Final(★交付确认)
终审改完后,**停下来**让用户独立确认交付决策(不要"我打算导出 HTML 了,没问题就这样"
直接跳过)。优先 AskQuestion,无工具则在消息里编号列出问题、停下等答复。
- **交付决策** —— 选项:`通过 · 导出 HTML 交付` / `通过 · 同时导出 HTML + PDF` /
`还有局部修复 · 我会列出具体修哪里` / `先停一停 · 我要再看看`。
只有这一项决策,但**仍要主动停下来问**,不要静默走默认导出 HTML。
---
## Phase 8 —— Delivery
构建并交付(命令见 `references/html-output.md`):
- `article/article.html`(自包含单页,CSS + JS 内联,断网可打开)—— **主交付物**。
- **可选** `article/article.pdf`:仅当 Checkpoint 3 用户选了"通过 · 同时导出 HTML + PDF"
时才生成。命令:
```bash
bash <path-to-beautiful-article>/scripts/html-to-pdf.sh
```
脚本探测系统已装的 chromium-family 浏览器,注入 `@media print` 覆盖(TOC 从左右栅格塌
成上下排布、TOC 独占首页),headless 打印。零 npm 依赖。详细原理 / 故障排除见
`references/pdf-output.md`。
- 简短编辑说明:文章类型 / 信息保留比例 / 主题 / 配图策略 / 主要编辑取舍。
---
## 默认策略
- 输出 single HTML;文章类型 `longform`;信息保留 100%。
- 语言:用户**未指定**则**跟随源材料语言**;**指定且与源不一致**则先产出地道翻译版
`source/source.<lang>.md` 再据此编写(去翻译腔,见 Phase 1)。
- 主题:技术 / 证据优先 `tufte`,叙事 / 评论优先 `press`(按源材料推荐)。
- 版式:宽度默认 `regular`、**TOC 默认开**(与主题解耦,见 `references/layout.md`,均在 Checkpoint 确认)。
- 配图:**配图模式是 Checkpoint 1 必选项**(`none` / `user-assets` / `placeholders` /
`ai-generated`),只决定是否使用外部 `Image`,**不主动生成 AI 图片**。
- Raw:**与配图正交、始终默认存在**,鼓励多用,但必须服务具体段落、用主题 token。选 `none`
不影响 Raw。
- 自检:**Plan 内联自查(无 SubAgent、无文件)**;**First Spread 与 Final 用 SubAgent + 写文件**;
**Section 用 SubAgent + 消息返回(不写文件)**。详见"硬性质检协议"段。
- 决策收集:Checkpoint 1 / 2 / 3 **每项独立确认 · 禁止静默替用户选择**。可推荐,不能跳过。
优先 `AskQuestion` 工具(每项一个独立 question);无工具则停下、编号列出问题等用户答复。
- 修复:最小切片,有修复才写 `review/repair-log.md`。
- **Colophon · 不可移除**:scaffold 在 `article/Article.tsx` 末尾自带 colophon Raw 块
(`Made with [beautiful-article](github 仓库) · <主题> theme`,低对比小字、theme token 自适应)。
**每篇文章必须保留**,禁止删除、禁止移到 Hero 旁边或浮动到角落。切换主题时同步更新 colophon
里的主题名 + `main.tsx` 的 `<ThemeProvider theme="...">` 两处。
- **封面 · 默认开 · 必须图文并茂**:scaffold 默认在 `article/Cover.tsx` 创建**屏幕 3:4 +
PDF 独占首页**的书封式题图外壳 + 占位(`--no-cover` 关闭)。封面位于 TOC + Hero + 正文之上,
独立存在。Phase 4 First Spread 时主 Agent 把 `<CoverPlaceholder />` 替换为按 **主题 +
文章主旨** 定制的图 + 字构图。**硬约束**:外壳比例 / 打印分页不可动、必须有视觉元素
+ 文字、只用 `--ra-*` token、不要远程图片、不要重复 Hero 内容。**视觉技术全开放**:
SVG / CSS / Canvas / 复杂 React 组件 / 任意混搭由 Agent 自选,效果好就行。详见
`references/cover.md`(含 5 条自检 + 5 个构图模板 + 各主题封面起手)。PDF 导出会自动让
封面独占首页、TOC 从第二页开始。
- **PDF 导出 · 可选**:主交付物始终是 `article/article.html`。**仅当** Checkpoint 3 用户选了
"通过 · 同时导出 HTML + PDF",才跑 `bash <skill>/scripts/html-to-pdf.sh` 生成
`article/article.pdf`;不选则不动。不要替用户默认导。详见 `references/pdf-output.md`。
---
## 成功标准
- 它**首先是一篇文章**。
- 最终文章语言符合用户意图(未指定=跟随源语言;指定=全文统一为目标语言,地道、无翻译腔、
无残留源语言片段)。
- 用户确认的信息密度被尊重;源材料关键内容没有意外丢失。
- 主题气质统一;配图和 Raw 都服务阅读。
- 页面比 Markdown 更值得读;HTML 可直接打开和分享。
- 40% 信息时读起来像被编辑过的文章,而非缩水摘要;100% 信息时像被精修过的长文,
而非原文搬运。
---
## 相关资源(按"何时读"标注)
| 文件 | 何时读 | 内容 |
|---|---|---|
| `references/harness.md` | Phase 0 | Skill 的 harness 视角、六问、状态文件约定 |
| `references/source-to-markdown.md` | Phase 1 | 各类输入 → source.md 规则、抽取自检、脚本用法 |
| `references/article-types.md` | Phase 2 | 文章类型路由总览(含逐类型链接) |
| `references/article-types/<type>.md` | Phase 2 选定类型后 | 单类型结构 / 组件 / Raw 边界 / 配图倾向 / 自检 |
| `references/information-density.md` | Phase 2 | 信息密度等级、与组件 / 视觉比例的关系 |
| `references/plan-template.md` | Phase 2 | 单一 `plan/plan.md` 模板(Brief / Outline / Theme / Assets 四段)与写法 |
| `references/theme-selection.md` | Phase 2 | 主题选择、density 与 theme 解耦、新增主题约束 |
| `references/layout.md` | Phase 2 / Checkpoint | 版式:宽度模式(与主题解耦)+ TOC,确认与用法 |
| `references/asset-policy.md` | Phase 2 | 配图四种来源、AI 配图提示词原则、图片自检 |
| `references/cover.md` | Phase 2 / Phase 4 写封面时 | 书封式封面设计指南(屏幕 3:4 / PDF 独占首页):硬约束、视觉技术全开放、构图模板、各主题封面起手、5 条自检 |
| `references/section-build.md` | Phase 4/5 | 一节一文件铁律、单/多 Agent 模式、并行 subagent prompt、主 Agent 合并 |
| `references/component-policy.md` | Phase 4/5 每节 | reacticle 组件协议、prose-first、信息密度与组件比例 |
| `references/raw-policy.md` | Phase 4/5 每节 | Raw 允许 / 禁止、token 驱动、Raw 自检 |
| `references/html-output.md` | 构建 / 交付时 | dev / build / 单文件 HTML 命令与产物 |
| `references/pdf-output.md` | Phase 8 Delivery 当用户选 PDF 导出时 | `html-to-pdf.sh` 用法、TOC 排版原理、Raw 在 PDF 的表现、故障排除 |
| `references/review-checklist.md` | Phase 6 | 各阶段 Reviewer 清单与 prompt 模板 |
| `references/repair-policy.md` | Phase 7 | 最小切片修复对照表 |
| `references/scaffold.md` | Phase 4 建项目时 | 脚手架做什么、用法、工作区结构、切主题 |
| `theme-profiles/index.json` + `*.md` | Phase 2 选主题 / Phase 5 写作 | 主题 authoring profile(给 AI 读,非 CSS) |
| `scripts/scaffold.sh` | Phase 4 跑一次 | 一键创建文章工作区 |
| `scripts/html-to-pdf.sh` | Phase 8 Delivery 仅当用户选 PDF | HTML → PDF(headless 浏览器 + 注入 print CSS,零 npm 依赖) |
| `scripts/pdf-print-overrides.css` | 改 PDF 样式时 | `html-to-pdf.sh` 注入到 `<head>` 的 `@media print` 覆盖:A) TOC 塌成上下排布;B) 分页行为(撤销 `.ra-section` 原子化、标题不孤儿、寡行控制等);C) 封面独占首页 |
| `scripts/source-to-markdown-markitdown.py` | Phase 1 | MarkItDown 主路径,适合复杂 PDF / DOCX / HTML |
| `scripts/source-to-markdown.py` | Phase 1 | 轻量 fallback,适合 Markdown / TXT / 简单 HTML 或 MarkItDown 不可用时 |
@@ -0,0 +1 @@
registry=https://registry.npmjs.org/
@@ -0,0 +1,69 @@
import { Article, Hero, Lead, Raw } from "reacticle";
import { SectionOpening } from "./sections/01-opening";
// Article.tsx is the ASSEMBLER, owned by the main agent. It imports and orders
// Section components — it must NOT contain Section bodies inline.
//
// 铁律:每个 Section 是独立组件文件(article/sections/NN-*.tsx),坚决不允许把
// 多个 Section 直接写进这里。这样多个 Agent 才能并行各写一个 section 文件,主 Agent
// 在这里负责合并与稳定性。详见 references/section-build.md。
//
// width (narrow/regular/wide/full) + toc 在 Plan Checkpoint 确认,与主题解耦
// (见 references/layout.md)。
export function ArticleDoc() {
return (
<Article toc width="regular">
<Hero
title="文章标题"
subtitle="副标题:一句话框定这篇要解决什么"
meta={[{ label: "日期", value: "2026-06-08" }]}
/>
<Lead>导语:用一两句话框定主题与读者要带走的判断。</Lead>
<SectionOpening />
{/* 在此按顺序加入更多 section 组件:<SectionContext /> <SectionMechanism /> … */}
{/*
─── Colophon ───
每篇 Beautiful Article 必须保留这一段,位置在 </Article> 之前、所有 Section /
Conclusion 之后。它是文章的"印记",告诉读者文章是用什么工作流生成的。
约束:
• 不要删除。不要移到 Hero 旁边或浮动到角落。
• 文本格式固定:Made with beautiful-article(带链接到 github 仓库)· <主题> theme
• 主题名(下方 __THEME__ 占位)由 scaffold 写入;切换主题时同步更新这里和
main.tsx 的 <ThemeProvider theme="...">。
• 样式只能用 --ra-* token,跟随主题自适应;保持低对比、小字、居中。
*/}
<Raw title="">
<footer
style={{
marginTop: "var(--ra-space-7, 3rem)",
paddingTop: "var(--ra-space-4, 1rem)",
borderTop: "1px solid var(--ra-color-border, currentColor)",
color: "var(--ra-color-muted, inherit)",
fontSize: "var(--ra-text-xs, 0.78rem)",
textAlign: "center",
letterSpacing: "0.02em",
opacity: 0.85,
}}
>
Made with{" "}
<a
href="https://github.com/ConardLi/garden-skills"
target="_blank"
rel="noopener noreferrer"
style={{
color: "inherit",
textDecoration: "underline",
textUnderlineOffset: "0.2em",
}}
>
beautiful-article
</a>{" "}
· __THEME__ theme
</footer>
</Raw>
</Article>
);
}
@@ -0,0 +1,172 @@
// Cover.tsx —— 文章封面(独立于 Article,位于 TOC + 正文 + colophon 之上)
//
// 这一文件是 article-specific 的(跟 Article.tsx / sections/*.tsx 同等地位)。
// 主 Agent 在 Phase 4 First Spread 把下面的【封面内容区】替换成按 **主题 + 文章主旨**
// 定制的设计。**外壳(3:4 比例、定位、PDF 分页)不要动**。
//
// 硬约束(详见 references/cover.md):
// 1. **3:4 比例固定(屏幕 + PDF)**:不要改 aspectRatio;打印时 .ra-cover 会自动
// 独占首页,保持屏幕版 3:4 构图以避免 Chromium print 裁切内部布局。
// 让内部元素用百分比 / aspect-ratio / inset 自适应,不要写绝对 px 高度。
// 2. **图文并茂**:必须有视觉元素 + 简短文字(标题 + 可选副题 / 小标签)。
// **禁止纯文字封面**。视觉用什么技术由你选(见约束 5)。
// 3. **主题忠实**:颜色 / 字号 / 字重 / 边框 / 律动**只能用 `--ra-*` token**。
// 切主题时封面要跟随刷新;不要写死颜色 / 字体名 / 像素字号。
// 4. **内容忠实**:封面的视觉主图与文字要呼应正文主旨(看一眼能猜出文章是讲什么的)。
// 5. **技术自由**:内联 SVG / CSS 几何 / Canvas / 复杂 React 组件 / 字体艺术 / 多层
// gradient / mask / clip-path / 任意组合 —— 任选,最终效果好就行。**唯一禁止**:
// 远程图片(offline-first);base64 raster 仅当 Plan Checkpoint "配图模式" 是
// user-assets / ai-generated 才允许。
// 6. **封面不承担正文**:不要把 Lead 第一段、TOC、阅读时间塞进来 —— 封面只承担
// "识别 + 风格信号 + 引起阅读欲望",正文从下面的 Article 开始。
export function Cover() {
return (
<section
className="ra-cover"
aria-label="文章封面"
data-ra-cover=""
style={{
// ── 外壳(请不要动) ──
position: "relative",
width: "100%",
// 屏幕上像"一本立着的书":限宽 48rem(768px);同时**从视口高度反推宽度**
// (100vh - 8rem) * 3/4,确保整个 3:4 封面**一屏看全、不用下拉**。8rem
// (128px) 给顶栏 / 边距 / site nav 等留出充足呼吸(典型场景 site nav 60 +
// 容器顶 padding 32 + 边框 1 ≈ 93px,仍有 35px 余量)。
// 3:4 比例由 aspect-ratio 保证不破。
maxWidth: "min(100%, 48rem, calc((100vh - 8rem) * 3 / 4))",
margin: "0 auto var(--ra-space-7, 3rem) auto",
aspectRatio: "3 / 4",
overflow: "hidden",
// 背景透明:让外层 .ra-root / .gx-reader 的 --ra-color-bg "纸面色" 直接透上来。
// 整片视觉是一张连续的纸,封面不再"自带一块色"。封面的辨识度由内部插画 + 边框
// + 内容排版承担。如果你的封面**确实需要**整体着色(如孟菲斯主题大色块覆盖),
// 可以改成 surface / surface-2 / accent-soft 等任意主题 token。
background: "transparent",
color: "var(--ra-color-fg, inherit)",
borderRadius: "var(--ra-radius-md, 0)",
border: "1px solid var(--ra-color-border, currentColor)",
// 让 ::before 之类的几何装饰可以铺满
isolation: "isolate",
}}
>
{/*
─── 封面内容区 · 在这里写 ───
默认占位长这样:
• 一层主题感的几何装饰(SVG 网格 + 一个 accent 圆 + 描边斜线)
• 居中的占位标题、副题、小标签
构建时**替换为按文章 + 主题定制的封面**。占位是为了
"即使忘了替换,也不会渲染出一团乱",但**不能交付出去**。
*/}
<CoverPlaceholder />
</section>
);
}
// ────────────────────────────────────────────────────────────────────
// 占位实现 —— 主 Agent 把 <CoverPlaceholder /> 替换成本文真正的封面。
// 删掉这个 function 也行;保留它能让占位回退更友好。
// ────────────────────────────────────────────────────────────────────
function CoverPlaceholder() {
return (
<>
{/* 默认占位用了 SVG 是图省事,**不代表"应该用 SVG"** —— 你完全可以删掉这个
* <svg>,换成 CSS 渐变层、Canvas、复杂 React 组件、字体艺术拼贴等任何能产生
* 漂亮视觉的方式。视觉技术由你选,效果好就行。 */}
<svg
viewBox="0 0 1200 1600"
preserveAspectRatio="xMidYMid slice"
aria-hidden="true"
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
color: "var(--ra-color-border, currentColor)",
opacity: 0.55,
zIndex: 0,
}}
>
<defs>
<pattern id="ra-cover-grid" width="80" height="80" patternUnits="userSpaceOnUse">
<path
d="M 80 0 L 0 0 0 80"
fill="none"
stroke="currentColor"
strokeWidth="0.6"
/>
</pattern>
</defs>
<rect width="1200" height="1600" fill="url(#ra-cover-grid)" />
<circle
cx="900"
cy="1180"
r="220"
fill="var(--ra-color-accent, currentColor)"
opacity="0.18"
/>
<line
x1="80"
y1="1400"
x2="560"
y2="1400"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
{/* 文字层 */}
<div
style={{
position: "absolute",
inset: 0,
zIndex: 1,
display: "grid",
alignContent: "center",
justifyItems: "start",
padding:
"var(--ra-space-7, 3rem) var(--ra-space-8, 4rem) var(--ra-space-7, 3rem) var(--ra-space-8, 4rem)",
gap: "var(--ra-space-3, 0.75rem)",
}}
>
<span
style={{
fontSize: "var(--ra-text-xs, 0.75rem)",
letterSpacing: "0.22em",
textTransform: "uppercase",
color: "var(--ra-color-muted, inherit)",
opacity: 0.85,
}}
>
COVER · 3 : 4 · 占位
</span>
<h1
style={{
margin: 0,
fontSize: "clamp(1.6rem, 4.6vw, var(--ra-text-4xl, 3rem))",
lineHeight: 1.05,
fontWeight: "var(--ra-font-weight-bold, 700)",
color: "var(--ra-color-fg, inherit)",
maxWidth: "70%",
}}
>
按文章主旨 + 主题,在此处设计封面
</h1>
<p
style={{
margin: 0,
fontSize: "var(--ra-text-sm, 0.95rem)",
color: "var(--ra-color-muted, inherit)",
maxWidth: "70%",
lineHeight: 1.4,
}}
>
先读 <code>references/cover.md</code> 与选定主题的 <code>theme-profiles/&lt;id&gt;.md</code>,
再替换 <code>CoverPlaceholder</code> 为本文专属的图文构图。视觉用什么技术(SVG /
CSS / Canvas / 复杂 React 组件 / 任意混搭)由你选,效果好就行;唯一禁止远程图片。
</p>
</div>
</>
);
}
@@ -0,0 +1,26 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "reacticle";
import "reacticle/styles.css";
// __COVER_IMPORT_BEGIN__ (scaffold.sh 在 --no-cover 时剥掉这一段,连同标记)
import { Cover } from "./Cover";
// __COVER_IMPORT_END__
import { ArticleDoc } from "./Article";
// Entry for the self-contained single-file HTML build.
// Theme is fixed here — change `theme` (must be a registered reacticle theme id:
// "tufte" | "press" | …) to switch the whole look.
//
// 渲染顺序:Cover(封面,可选)→ ArticleDoc(含 TOC + 正文 + colophon)。
// Cover 故意**不**塞进 <Article> 内部(那样会被挤到正文栏旁边),而是和 <ArticleDoc/>
// 在 ThemeProvider 下做兄弟,DOM 顺序天然就是「封面 → TOC → 正文 → colophon」。
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeProvider theme="__THEME__">
{/* __COVER_RENDER_BEGIN__ (scaffold.sh 在 --no-cover 时剥掉这一段,连同标记) */}
<Cover />
{/* __COVER_RENDER_END__ */}
<ArticleDoc />
</ThemeProvider>
</StrictMode>
);
@@ -0,0 +1,30 @@
import { Section, Aside, Raw } from "reacticle";
// One Section per file. In parallel builds a single subagent owns this file and
// must not touch Article.tsx or other section files. See references/section-build.md.
//
// Rules of thumb (references/component-policy.md + raw-policy.md):
// - Prose is the body. Write paragraphs as <Section> children.
// - Semantic components (Aside / Quote / Table / RiskList ...) are accents.
// - Raw is freely used but hand-authored for THIS section, token-driven.
export function SectionOpening() {
return (
<Section index="01" title="第一节">
<p>正文段落用 children —— 这应是文章主体,尽量多写正文,把背景、推理、结论讲清楚。</p>
<p>再写一段,保持阅读节奏。语义组件只在内容确实"是"那个结构时才用。</p>
<Aside tone="principle" label="核心判断">一句话的核心判断,给本节点睛。</Aside>
<Raw title="为本段现写的内联 SVG(用主题 token 取色)">
<svg viewBox="0 0 240 60" width="100%">
<polyline
points="0,50 40,42 80,46 120,20 160,28 200,8 240,14"
fill="none"
stroke="var(--ra-color-accent)"
strokeWidth="2"
/>
</svg>
</Raw>
</Section>
);
}
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Beautiful Article</title>
<!-- Theme fonts. tufte falls back to Georgia; press uses Newsreader. -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500&family=Source+Serif+4:opsz,wght@8..60,400&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/article/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,26 @@
{
"name": "beautiful-article-workspace",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"html": "npm run build && node -e \"require('fs').mkdirSync('article',{recursive:true});require('fs').copyFileSync('dist/index.html','article/article.html');console.log('built article/article.html')\"",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reacticle": "latest"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^5.4.11",
"vite-plugin-singlefile": "^2.0.3"
}
}
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["article"]
}
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}
@@ -0,0 +1,18 @@
import { resolve } from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
// reacticle is consumed from the published npm package (see package.json).
// Builds a self-contained single-page HTML (CSS + JS inlined, opens offline)
// to dist/index.html. `npm run html` then copies it to article/article.html.
export default defineConfig({
plugins: [react(), viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: true,
rollupOptions: {
input: resolve(__dirname, "index.html"),
},
},
});
@@ -0,0 +1,15 @@
{
"name": "beautiful-article",
"version": "0.1.0",
"category": "Editorial · Any source → beautiful article",
"description": "Edit and design any source material (URL / PDF / DOCX / Markdown / plain text / screenshots / pasted notes) into a beautiful, share-ready article. Built on the reacticle component protocol with a theme-constrained Raw layer; runs a small source -> plan -> double-confirmation -> build -> final review -> repair harness, defaulting to 100% information retention long-form articles.",
"homepage": "https://github.com/ConardLi/garden-skills/tree/main/skills/beautiful-article",
"compat": [
"claude-code",
"claude-ai",
"cursor",
"codex-cli",
"gemini-cli",
"opencode"
]
}
@@ -0,0 +1,42 @@
# 文章类型路由
文章类型是**结构决策**,主题是**审美决策**(见 `theme-selection.md`)—— 两者完全解耦。
**文章类型与信息保留比例的关系**(重要):理论上"内容保留是独立决策",**但实践中两者
绑定很紧**。每个类型都自带一个标配保留比例(见下表"推荐信息保留"列)。`longform + 20%` /
`tutorial + 20%` / `briefing + 100%` 这类组合是**伪选项**:要么类型变形(briefing+100%≈full-report),
要么内容空洞(longform 写出 8 章每章 2 段)。所以 Plan Checkpoint 1 把"保留比例"打包进"文章
类型"的语义化选项里(见 SKILL.md Phase 3),**只在用户明确想精修**(如"longform 但只要 60%"
= 一篇被深度编辑的长文)才作为非标配组合记入 plan.md。
Phase 2 选定类型后,读对应 `article-types/<type>.md` 拿结构 / 组件 / Raw 边界 / 配图倾向 /
自检。**非标配组合**要在 `plan/plan.md` Brief 段同时记下"标配 X% → 用户覆盖 Y%",并让主 Agent
在写每节时手动调整正文/视觉比例(不能照搬 article-types/<type>.md 的默认建议)。
| 类型 | 推荐信息保留 | 用途 | 典型结构 | 详情 |
|---|---|---|---|---|
| `longform` | 100% | 完整长文、归档、深度阅读 | Hero / Lead / Summary / 多 Section / Raw 增强 / Conclusion | `article-types/longform.md` |
| `full-report` | 80% | 研究报告、正式分析 | 执行摘要 / 背景 / 证据 / 数据 / 风险 / 结论 | `article-types/full-report.md` |
| `tutorial` | 80-100% | 教学、步骤、上手 | 目标 / 步骤 / 示例 / 练习 / 总结 | `article-types/tutorial.md` |
| `explainer` | 80% | 解释技术、系统、概念 | 问题 / 机制 / 图解 / 示例 / 常见误区 | `article-types/explainer.md` |
| `dialogue` | 80% | 对话 / Q&A / 访谈 / 播客 / AMA | Hero / 嘉宾 / 多话题 Section / 关键观点摘要 | `article-types/dialogue.md` |
| `review` | 60-80% | PR / 方案 / 事故 / 设计审阅 | 背景 / 发现 / 影响 / 建议 / 行动 | `article-types/review.md` |
| `essay` | 60-80% | 观点、评论、叙事 | 开场 / 论点 / 例证 / 转折 / 收束 | `article-types/essay.md` |
| `briefing` | 40-60% | 给忙人快速判断 | 结论先行 / 关键证据 / 取舍 / 下一步 | `article-types/briefing.md` |
| `interactive-explainer` | ~25%(原文摘录占比,**非删 75%**) | **Raw 交互为主载体的"会用了再走"式学习页**(参考 3blue1brown / distill.pub / ciechanow.ski)。本质是**内容重构**:只摘核心知识点,其余 AI 围绕它们全新创作 | 每知识点:定义 / 交互演示 / 自己试 / 验证理解 | `article-types/interactive-explainer.md` |
| `visual-essay` | 20-60% | 展示、传播、图文主导 | 少文字 / 大视觉 / 强节奏 / 章节短 | `article-types/visual-essay.md` |
## 选型提示
- 源材料信息密度高、要完整归档 → `longform`。
- 源材料是消化后的报告 / 正式分析(执行摘要 + 数据 + 风险 + 建议四件套)→ `full-report`。
- 要把一个机制 / 概念讲清楚(正文为主)→ `explainer`。
- **要让读者"玩明白"一个概念**(Raw 交互为主,正文为辅,每知识点配交互演示)→ `interactive-explainer`。
- 对话 / Q&A / 访谈 / 播客转录 / AMA → `dialogue`。
- 评审某个 **工程** PR / 方案 / 事故 / 设计 → `review`。
- 给决策者快速判断 → `briefing`。
- 观点输出 / 评论 / 产品 / 书 / 论文评测 → `essay`。
- 教别人上手做事(步骤 + 跑通)→ `tutorial`。
- 传播 / 展示、图文主导 → `visual-essay`。
类型只定**结构倾向**,不锁死信息密度和主题;用户可在 Plan Checkpoint 覆盖。
@@ -0,0 +1,32 @@
# Article Type · briefing
给忙人(决策者 / 同事 / 投资人)**快速判断**用。读者**不会读完全文**,所以每段都得帮决策。
- **推荐信息保留**:40-60%(只留重点结论、关键证据、关键取舍、下一步;删一切铺垫 / 推导 /
背景 / 历史)。
- **典型结构**:`Hero`(标题 = 一句话结论)→ `Summary`(**结论先行 + 3-5 个关键事实**,最重要
的一块)→ `Section`:关键证据 → 取舍 / 风险 → 下一步 / 行动项。**短而紧凑,整体阅读时间
控制在 3-5 分钟**。不必有 `Conclusion`(`Summary` 已经把结论说了)。
- **组件选择**:仍 **prose-first**,正文短句直给(每段最好不超过 3 句)。`Summary` 是核心 ——
这是 briefing 的灵魂。其余按需:
- `Table`:确需 N 项并排对比时。
- `Decision` / `Tradeoff`:确有"X vs Y"取舍要展示时。
- `ActionList`:确有下一步要列时(谁做 / 何时做)。
- **不要因为是 briefing 就堆组件** —— briefing 贵在精简,组件多了反而显冗。
- **Raw 边界**:少量但有力 —— 一个**关键对比图** / 趋势 / 选项矩阵 / 决策树;不堆视觉。
一篇 briefing 通常 0-2 块 Raw。
- **配图倾向**:`none` 或**单张关键图**(数据图 / 关键截图);偏 `tufte` 证据感或 `press`
简洁编辑感。
- **主题倾向**:`tufte`(数据型 briefing)、`vignelli`(中性规格)、`press`(编辑节奏)。
- **自检**:
- 30 秒内(只读 Hero + Summary)能抓到结论吗?
- 每段是否都在**帮决策**(这段删掉决策会受影响吗)?
- 有没有冗余展开 / 解释来由 / 历史背景 / "之所以这样"段落?
- 行动项是否可执行(谁做 / 何时做 / 怎么验收)?
- 整体阅读时间是否真的在 3-5 分钟?超过就不是 briefing 了。
> **何时不要用 briefing**:
>
> - 要让读者**完整理解**而非快速判断 → `longform` / `full-report` / `explainer`。
> - 评审某个具体产物 → `review`。
> - 给读者**学懂**一个概念 → `explainer` / `interactive-explainer`。
@@ -0,0 +1,30 @@
# Article Type · dialogue
对话 / Q&A / 访谈 / 播客转录 / AMA / 圆桌。任何"多个声音轮流说话"的内容形式。
- **推荐信息保留**:80%(删冗余口语 / 重复 / 寒暄,保留实质内容;不删观点也不改语气)。
- **典型结构**:`Hero`(话题 + 嘉宾 + 主持 + 日期 / 来源)→ `Lead`(背景 / 为什么聊这个)→
多个 `Section`,每节一个话题或问题,正文用 **发言者明示 + 大量 `Quote`** 体现对话节奏 →
可选 `Summary` 放"关键观点摘要"在文章开头或结尾 → `Conclusion`(要点 / 延伸阅读)。
- **组件选择**:
- 正文中**每段开头明示发言者**(如 `**A**:…` / `**主持**:…`),避免读者搞混;
- `Quote` 大量使用 —— 嘉宾的金句、原话、关键定义都用 Quote 抬出来;
- `Aside tone="principle"` 标定义 / 数据 / 概念解释;
- `Summary` 在开头放"3-5 个关键观点",让快速读者也能拿到精华;
- 少用 `Table` / `CodeBlock`(对话很少有结构化技术内容;有时一律按嘉宾原话放代码段);
- `Detail` 折叠可选的延伸 / 注释。
- **Raw 边界**:可用 Raw 做**话题地图**(章节导航 / 时间线)、**关键概念可视化**(嘉宾解释了
什么机制就配一张 Raw 图解)、**金句卡片**;不要装饰性弹幕 / 动画头像。
- **配图倾向**:`user-assets`(嘉宾头像 / 现场照片 / 提到的截图)或 `none`(纯文本对话);
少用 `ai-generated` 氛围图。
- **主题倾向**:`press`(出版编辑感,适合长对话 / 访谈)、`bodoni`(杂志感专访)、`freddie`
(活泼播客感)。
- **自检**:
- 发言者归属清晰,不会让读者搞错"是谁说的"?
- 对话节奏保留?没有把它压成"单声音综述"?
- 关键观点能在 30 秒内从 Summary 抓到?
- 删减后嘉宾原意没有被改写 / 误传?
- 移动端发言者标记仍然清晰?
> **未来扩展**:若 reacticle 组件库新增 `Dialogue` / `Speaker` 等专用对话组件,本类型应优先使用。
> 当前实现用 `Quote` + 正文显式标注即可。
@@ -0,0 +1,31 @@
# Article Type · essay
观点、评论、叙事、评测(产品 / 书 / 论文 / 电影)、随笔、专栏、宣言。**观点驱动**的非
虚构写作。
- **推荐信息保留**:60-80%(保留论证链与最有力的例证;删旁枝、删冗长引用、删重复的换句话)。
- **典型结构**:`Hero`(标题气质强)→ `Lead`(**抛出张力 / 矛盾 / 反直觉问题**,钩住读者)
→ `Section`:开场 → 论点 → 例证 → 转折 → 收束。结构**服务叙事节奏,不必规整对称** ——
一篇好 essay 可以是 3 节也可以是 7 节,看节奏。
- **组件选择**:正文是**绝对主体**;`Quote` 引名言 / 原话 / 反方观点;`Aside tone="aside"`
放旁注 / 个人评论 / 反方立场;**少用** `Table` / `RiskList` 等数据组件(会破坏叙事感);
`Summary` 仅在长 essay 时用。
- **Raw 边界**:偶尔一个有表现力的视觉停顿(节奏图 / 概念对比 / 情绪曲线),服务叙事而非
解释机制;密度低 —— 一篇 essay 通常 1-3 块 Raw 就够了。
- **配图倾向**:`press` 暖色编辑摄影 / 插图 / 手稿;服务气氛但**不喧宾夺主**。评测型 essay
可用 `user-assets`(产品 / 书 / 电影截图)。
- **主题倾向**:`press`(出版编辑型)、`bodoni`(专栏 / feature)、`andy`(柔软叙事)、
`sottsass`(活泼文化评论)。
- **自检**:
- 是否有**一条清晰的论证 / 叙事线**?读者能否一句话复述这篇的核心观点?
- 例证是否有力?是否最少 2 个具体例子(而不是空泛论断)?
- 语气是否一致、像**人写的**(不是 AI 味、不是套路化展开)?
- Lead 是否真的"抛出张力",而不是温吞地铺垫背景?
- 收束是否给读者留下点什么(不一定结论,可以是新问题 / 新视角)?
> **何时不要用 essay**:
>
> - 评审**工程产物**(PR / 方案 / 事故)→ `review`。
> - 给读者做决策 → `briefing`。
> - 要把一个机制讲清楚 → `explainer`。
> - 是对话 / 访谈整理 → `dialogue`。
@@ -0,0 +1,32 @@
# Article Type · explainer
解释一个机制 / 系统 / 概念 / 算法 / 协议。读者读完**真懂**这个东西怎么回事、为什么这样、
何时用 / 不用。
- **推荐信息保留**:80%(保留主要机制和关键细节,删重复 / 旁枝 / 历史背景;保留所有"关键
直觉"和"易错点")。
- **典型结构**:`Hero` → `Lead`(为什么要懂这个 / 不懂的代价)→ `Section`:问题 → 机制 →
图解 → 示例 → 常见误区 → 何时用 / 不用 → `Conclusion`。
- **组件选择**:正文讲机制(**正文仍是主体**,不要拆成卡片堆);`Aside tone="principle"`
点出"关键直觉 / 一句话本质 / 易错点";`CodeBlock` 给具体示例;`Table` 对比方案 / 对比变种;
`Detail` / `Tabs` 折叠次要细节和深入推导;`Quote` 引原论文 / 规范原文。
- **Raw 边界**:**鼓励多用解释性视觉** —— 机制流程、状态变化、数据流向、概念关系可用 Raw
自由层(HTML 布局 / 轻交互 / 动效 / 按需 SVG),让抽象概念可视、可对照;每块**服务一个
具体机制点**。Raw 是辅助而非主体 —— 如果你发现 Raw 承载了主要信息、正文变成了图注,那应
该考虑 `interactive-explainer`。
- **配图倾向**:解释性视觉优先(Raw —— 交互 / 布局 / 动效 / SVG 均可);需要真实界面 / 系统
截图时用 `user-assets`;少用氛围图。
- **主题倾向**:`tufte`(技术 / 数据型)、`shannon`(系统 / 工程型)、`knuth`(学术型)、
`freddie` / `bayer`(面向新手的活泼型)、`fuller`(系统设计 / 协议型)。
- **自检**:
- 读者读完是否**真懂**这个机制(不只是"知道有这个东西")?
- 图解是否服务理解还是装饰?删掉是否影响理解?
- 常见误区是否覆盖(最容易踩的 2-3 个坑)?
- "何时用 / 不用"是否清楚?这是 explainer 与 longform 的关键差别。
- 长技术文章如果可以删 20% 仍能讲清,优先用 explainer;要原文归档用 `longform`。
> **何时不要用 explainer**:
>
> - 要让读者**操作着学懂**(Raw 是主载体)→ `interactive-explainer`。
> - 要让读者**跟着做出来一个东西** → `tutorial`。
> - 源材料是论文 / 报告级别的完整论证 → `longform` / `full-report`。
@@ -0,0 +1,32 @@
# Article Type · full-report
研究报告、正式分析、技术评估、年度回顾、调研报告。源材料具备"执行摘要 + 关键发现 +
数据 + 风险 + 建议"骨架,或可以被重组成这个骨架。
- **推荐信息保留**:80%(消化原始材料后的报告化呈现;删冗余推导、扩展阅读、附录细节,
保留**执行摘要 / 关键发现 / 数据 / 风险 / 建议**四件套不动)。若用户的源材料就是报告原文
且要 100% 归档,覆盖为 100% 并按"非标配组合"在 `plan/plan.md` Brief 段记下。
- **典型结构**:`Hero`(标题 + 报告期 / 范围 / 作者 / 单位)→ `Summary`(**执行摘要 / 关键
结论先行,必备**)→ `Section`:背景与方法 → 关键发现 → 数据与证据 → 风险与限制 → 建议与
下一步 → `Conclusion`(核心结论复述 + 决策建议)。**`TOC` 必开**。
- **组件选择**:`Summary` 放"关键结论 + 关键数据"(让快读者 30 秒能拿到核心);`Table` 承载
数据 / 对比;`RiskList` / `Decision` / `Tradeoff` 承载风险与取舍;`CodeBlock` / `Formula`
承载技术证据;`ActionList` 承载建议清单;正文承载论证过程。
- **Raw 边界**:数据图、趋势、对比矩阵、风险热度图、依赖关系等用 Raw 自由层(HTML / CSS
图表与矩阵、轻交互、按需 SVG / canvas);**保持证据感、低装饰**——避免动效抢戏,避免氛围
渐变色。
- **配图倾向**:`none` 或真实数据图 / 报告截图(`tufte` 风);**避免氛围图 / 配图打断阅读**。
如有产品截图,要服务一个具体结论。
- **主题倾向**:`tufte`(数据型)、`knuth`(学术型)、`vignelli`(中性规格 / 标准化报告)、
`fuller`(系统设计 / RFC 风格)。
- **自检**:
- 结论是否在文章开头 30 秒内能抓到?
- 数据 / 风险 / 建议是否齐全且有据可循(每条建议指向哪些发现)?
- 是否像一份**正式报告**而非营销页或长文随笔?
- `Table` / `RiskList` / `Decision` 等是因为内容是这样才用,还是为了"显得专业"硬塞?
> **何时不要用 full-report**:
>
> - 源材料是连贯论证 / 叙事长文 → `longform`。
> - 用户要的是"给老板看"的决策摘要而非完整报告 → `briefing`。
> - 评审某个具体 PR / 方案 / 事故 → `review`。
@@ -0,0 +1,66 @@
# Article Type · interactive-explainer
把一篇长技术文章 / 论文 / 系统设计稿,**重构为以交互动画为主载体的"会用了再走"式学习页**。
参考路径:3blue1brown 视觉化、distill.pub 的可调参数式机器学习解释、Bartosz Ciechanowski
(ciechanow.ski)的"硬件原理可玩页"。
> **与 explainer 的关键区别**:
>
> - `explainer`:**正文是主体**,Raw 是辅助插图。读者读完"知道了"。
> - `interactive-explainer`:**Raw 是主体**,正文是简短引导和定义。读者**操作完**"会用了"。
- **推荐信息保留**:~25%(**注意:这里的百分比不是"原文删了 75%",而是"成品里直接来自
原文的句子 / 段落只占约 25%"**。本类型的本质是**内容重构** —— 从原文里**只摘**核心知识
点的关键定义、公式、数据、约束、易错点;其余 75% 由 AI 围绕这些知识点**全新创作**:引导
文字、直觉解释、交互演示、自己试、验证理解。原文的叙事铺垫、历史背景、案例展开、扩展
讨论**全部舍弃**,由交互替代。**不要把它当"删 50% 的 explainer"做**,那样既学不透也失去
本类型存在的意义。
- **典型结构**:
- `Hero`(一句话定位:这页让你学懂什么 / 玩什么)
- `Lead`(前置:1-2 句话提示你需要哪些基础,玩完能做到什么)
- 可选 `Summary`(列出 N 个核心知识点作为路线图)
- 多个 **Concept Section**,每个一个核心知识点,节内三段式:
1. **定义 / 直觉**:1-3 段正文 + 一个 `Aside tone="principle"` 抬出一句话直觉。
2. **交互演示**(Raw 主体):动画 / 滑块 / 拖拽 / 状态切换 / 参数变化的实时可视化。
3. **自己试 / 验证理解**(Raw 交互):让读者动手试一个边界 / 反例 / 应用场景;可选一个
"答案揭晓"的折叠。
- `Conclusion`(知识点串联回顾 + 何时用 / 不用 + 延伸阅读)。
- **组件选择**:
- 正文**短**:每节正文加起来通常不超过 200-400 字;
- `Aside tone="principle"` 标定义、关键直觉、易错点;
- `Detail` / `Tabs` 折叠次要细节、答案、推导;
- `Quote` 引原文 / 论文金句;
- 少用 `Table` / `CodeBlock`(如有,必须服务一个具体交互演示,不放整页代码);
- `Raw` 是主角。
- **Raw 边界(核心 · 必读)**:
- **每个 Raw 必须服务一个具体知识点**,不允许装饰性炫技。
- **操作性优先**:滑块 / 拖拽 / 切换 / 输入框 / 步进按钮;让用户**改变某个量**并**实时看到
结果**。比"看一段动画"更深。
- **状态可见**:当前参数、当前数值、当前阶段都要显式可见,不要藏在动画里。
- **可重置**:每个交互配"reset"或"恢复初值",鼓励反复试。
- **样式走 token**:颜色 / 字体 / 间距用 `--ra-*`,不写野生 CSS。
- **错误示例同样有价值**:让用户拖到"会出错"的位置,配一行说明"为什么这里崩"。
- **配图倾向**:`none` 优先(交互比图片更有信息量);少量 `placeholders`(理论图 / 截图);
避免 `ai-generated`(氛围图打断学习节奏)。
- **主题倾向**:`tufte`(克制 + 数据感,适合 ML / 算法可视化)、`shannon`(暗底工程感,
适合系统 / 硬件交互演示)、`knuth`(学术克制,适合论文重构);避免 `freddie` / `sottsass`
这类活泼配色(会让交互显得像游戏而非学习)。
- **自检**:
- 读者玩完是否**真正会用**这个概念,而不只是"听过 / 看过"?
- 每个交互是否服务一个具体知识点?有没有炫技但学不到东西的 Raw?
- 没有交互的章节,是真的不需要交互,还是偷懒了?(这是判断本类型是否走样的关键问题)
- 移动端能不能操作?很多滑块 / 拖拽 / 复杂 SVG 在 mobile 上不能用 —— 必须在 Plan 阶段就
决定"是否放弃 mobile 交互" 还是"提供 mobile 替代展示"。
- 删掉所有 Raw 后,剩下的正文是不是太薄?太薄说明 Raw 没把信息装进去 —— 应该在 Raw 内或
紧邻位置补足。
- 成品里"直接来自原文的句子 / 段落"比例**确实在 ~25% 这个量级**吗?太高(>40%)说明你
在"删 explainer"而不是重构;太低(<10%)说明核心知识点没说清。
- **核心知识点筛选有据**:能否一句话说出"这页保留的 N 个知识点为什么是这 N 个"?随便挑
几个是这个类型走样的开端。
> **何时不要用 interactive-explainer**:
>
> - 源材料是叙事 / 观点 / 评论(→ essay)。
> - 源材料是步骤型操作(→ tutorial,步骤之间有先后依赖,不是知识点的并列)。
> - 源材料是数据报告(→ full-report,结论比交互重要)。
> - 你不打算写真正可操作的 Raw(→ explainer 即可,别假装 interactive)。
@@ -0,0 +1,32 @@
# Article Type · longform
完整长文、归档、深度阅读。这是**默认类型**。源材料是连贯论证 / 叙事 / 综述,且用户要原
文级保留。
- **推荐信息保留**:100%(原文关键内容不丢;只允许删除明显的重复段落和无信息的过场句)。
- **典型结构**:`Hero` → `Lead`(导语,框定主题)→ 可选 `Summary`(TL;DR / 结论先行)→
多个 `Section`(必要时 `Subsection`)→ 关键概念处 `Raw` 增强 → `Conclusion`。长文(>10
小节或预估阅读 >15 分钟)开启 `TOC`。
- **组件选择**:正文段落为**绝对主体**,应占文章绝大部分篇幅;`Aside` 点出关键直觉 /
历史注 / 反方观点;`Quote` 引用名言或原话;`Table` 承载二维数据;技术内容用 `CodeBlock` /
`Formula`。**不要把连贯段落拆成卡片堆** —— 卡片堆是 longform 最常见的走样形态。
- **Raw 边界**:在关键概念、数据趋势、机制处插入 Raw 自由层(轻交互 / 自定义排版 / 动效 /
按需 SVG),给长文节奏与呼吸;每块服务具体段落,用 `--ra-*` token。Raw **是增强而非主体**
—— 如果开始让 Raw 承载主要信息,说明你应该考虑 `explainer` 或 `interactive-explainer`。
- **配图倾向**:`none` / `placeholders` 优先;技术 / 证据型可用真实数据图(`tufte` 风);
叙事型可加少量 `press` 风氛围图。
- **主题倾向**:`tufte`(技术 / 证据型)、`knuth`(学术 / 论文)、`press`(叙事 / 综述)、
`bodoni`(专栏 / feature)。
- **自检**:
- 正文是否仍是绝对主体?没有把段落拆成卡片堆?
- 章节衔接是否自然?读者读完一节会自然想读下一节?
- Raw 是点亮关键概念还是打断阅读节奏?
- 100% 信息是否读起来像被精修过的长文(而非原文搬运)?
- 长文有没有 `TOC` + `Summary` 帮助读者定位?
> **何时不要用 longform**:
>
> - 源材料是消化后的报告(执行摘要 + 风险 + 建议四件套)→ `full-report`。
> - 源材料是要解释一个机制 / 概念,可以删 20% → `explainer`。
> - 源材料是论文 / 长文但你想做成交互学习页 → `interactive-explainer`。
> - 给忙人看 / 要决策 → `briefing`。
@@ -0,0 +1,32 @@
# Article Type · review
**工程审阅**:PR / 方案设计 / 事故复盘 / 架构 / API / 安全审计。起点是审阅一份**具体产物**
并产出意见与行动。
- **推荐信息保留**:60-80%(保留关键发现 + 证据 + 行动;省略无关细节 / 上下文铺垫)。
- **典型结构**:`Hero`(被评对象 + 评审范围 + 评审日期 / 评审人)→ `Summary`(**结论 /
评审意见先行**)→ `Section`:背景与目标 → 发现(逐条) → 影响评估 → 建议 → 行动项 →
`Conclusion`(核心判断 + 通过 / 待修 / 否决)。
- **组件选择**:仍 **prose-first** —— 结论与发现先用正文 + `Summary` 讲清楚。下面是**领域
特例组件**,**只在内容确实是该结构时**按需取用,不要因为是 review 就全堆上:
- `RiskList`:确有一组需要分级的风险时。
- `DiffReview`:确有代码改动要逐行评时;代码一律用 `CodeBlock`。
- `Decision` / `Tradeoff`:确有"X vs Y"的取舍要展示时。
- `Incident`:事故复盘时的时间线。
- `ActionList` / `Checkpoint`:行动项 / 验收点确需结构化时。
- **Raw 边界**:影响范围图、依赖关系、风险热度矩阵、调用链、监控曲线等用 Raw 自由层
(HTML / CSS 矩阵 + 热度、轻交互、按需 SVG);**克制、证据优先、不抢正文**。
- **配图倾向**:`user-assets` 真实截图 / diff / 监控图 / dashboard 截图(`tufte` 风)。
- **主题倾向**:`tufte`(证据型 review)、`shannon`(暗底工程 / 事故复盘)、`fuller`(系统设计
/ RFC review)、`vignelli`(中性规格型)。
- **自检**:
- 评审意见是否在文章开头先行清楚(通过 / 待修 / 否决)?
- 每条发现是否有**证据支撑**(代码片段 / 截图 / 数据 / 日志),不是凭感觉?
- 建议与行动项是否**可执行**(谁做 / 何时做 / 怎么验收)?
- 起点是审阅一份**具体产物**?如果起点是"读者要做决策",应该用 `briefing`。
> **何时不要用 review**:
>
> - 评测**产品 / 书 / 论文 / 电影**(观点驱动,没有"通过 / 否决"二元判断)→ `essay`。
> - 给老板做一份"该不该做 X"的决策摘要 → `briefing`。
> - 完整的事后调研报告 → `full-report`。
@@ -0,0 +1,30 @@
# Article Type · tutorial
教学、步骤、上手指南、安装配置、迁移手册。读者**跟着做**就能跑通的内容。
- **推荐信息保留**:80-100%(**步骤不能丢**,否则跟不下来;可删的只有冗长背景、可选的深入
扩展、不影响跑通的"为什么"段落)。
- **典型结构**:`Hero` → `Lead`(学完能做什么 / 前置条件 / 预计用时)→ 可选 `Summary`
(整体路径鸟瞰)→ `Section`:每个阶段一节,节内 = 目标 → 步骤(逐步)→ 示例 → 验收点 →
常见坑 → 可选 `Section`:练习 / 扩展 → `Conclusion`(总结 + 下一步学什么)。
- **组件选择**:`CodeBlock` 给每一步代码(**保真,可复制运行**);`ActionList` /
`Checkpoint` 列步骤与验收点;`Aside tone="warning"` 标坑 / 注意事项;`Detail` 折叠可选的
深入解释 / 替代方案;`Table` 列参数 / 选项 / 配置项;`Tabs` 给多平台 / 多语言并列示例。
- **Raw 边界**:流程图、状态变化(before / after)、UI 步骤示意、命令前后对照用 Raw 自由层
(HTML 步骤布局 / 轻交互 / 按需 SVG);让步骤可视、可对照。**不必为每步都加 Raw** ——
代码 + 截图能讲清楚的就不加。
- **配图倾向**:`user-assets` 优先(**真实截图**每个关键步骤);纯命令行类教程可用 `none`。
- **主题倾向**:`vignelli`(中性规格 / 文档感)、`freddie`(活泼上手)、`fuller`(系统配置 /
RFC 风格)、`tufte`(密度高的技术教程)。
- **自检**:
- 照着做能否**真的跑通**?没有跳步 / 没有"省略号 …"假装代码?
- 步骤是否完整有序?每步都有验收点(怎么知道这步成了)?
- 代码是否可**直接复制运行**?不缺 import / 不缺环境说明?
- 常见坑是否标注(版本不匹配 / 权限不够 / 平台差异)?
- 移动端代码块能横滚不溢出?
> **何时不要用 tutorial**:
>
> - 源材料是讲机制不是讲操作 → `explainer`。
> - 想让读者**玩懂**而不是**做出来** → `interactive-explainer`。
> - 源材料是 API 文档 / 完整规格 → 用 `longform` + 强 TOC,tutorial 不适合做参考手册。
@@ -0,0 +1,33 @@
# Article Type · visual-essay
展示、传播、图文主导、品牌叙事、年终回顾、文化评论。**视觉是主角**,文字短而精,节奏强。
- **推荐信息保留**:20-60%(只留核心观点和最有力量的材料;大量删减叙事 / 推导 / 数据细节,
把保留下来的内容用视觉重新组织)。
- **典型结构**:`Hero`(强气质 / 满版视觉 / 大字标题)→ 短 `Section` 串联,每节 = 少文字 +
大视觉 + 一句金句 → 节与节之间有**强节奏对比**(疏 / 密 / 明 / 暗)→ `Conclusion`(留白
收束 / 一句话点题)。**章节短** —— 每节阅读时间 30 秒到 1 分钟。
- **组件选择**:正文用**短句、断行**;`Quote` 做金句停顿(不引名言,而是把文章里的"狠话"抬
出来当节标记);`Image` / `Raw` 是主角;**少用密集数据组件**(`Table` / `RiskList` 会破
坏视觉节奏)。
- **Raw 边界**:**视觉块占比最高** —— 大尺度版面、节奏图、概念可视化、动效、轻交互(HTML
/ CSS / React,按需用 SVG / canvas);可以做**满版背景**、**滚动触发动效**、**多列错位
排版**等。但**仍必须是文章形态** —— 不是 landing page、不是产品 dashboard。
- **配图倾向**:`ai-generated` / `user-assets` **大图**;构图 / 留白 / 排版本身就是表达;
避免 stock photo 和"商务握手"类俗图。
- **主题倾向**:`bodoni`(黑白大刊)、`press`(暖色编辑)、`sottsass`(活泼彩色)、`bayer`
(现代主义 / 海报感)、`andy`(柔软叙事)。气质强的主题在这个类型里效果最好。
- **自检**:
- 是否仍是一篇**文章**而非海报 / landing page / dashboard?(删掉所有视觉后,剩下的文字
能否串成一个观点?能 = 文章;不能 = landing page)
- 视觉是否服务**核心观点**而非纯装饰 / 炫技?
- 节奏是否有呼吸(疏密对比 / 明暗对比 / 大小对比)?
- 删减后是否仍像"被编辑过的文章"而非"缩水的摘要"?
- 移动端是否仍能传达核心观点(很多大版面在 mobile 上会塌)?
> **何时不要用 visual-essay**:
>
> - 给读者做决策 → `briefing`。
> - 让读者完整理解 → `longform` / `explainer`。
> - 让读者**操作着学懂** → `interactive-explainer`(也是视觉主导,但读者要动手)。
> - 要传达完整论证 / 数据 → `essay` / `full-report`。
@@ -0,0 +1,71 @@
# 配图与素材策略
配图必须服务文章,不是装饰。Phase 2 把配图策略与逐图计划写进 `plan/plan.md` 的
**Assets** 段(见 `plan-template.md`),**不再产出独立的 `asset-plan.md` 文件**。
## 与 Raw 正交:配图策略只管 Image,不管 Raw(铁律)
**配图策略 = 是否使用外部 `Image`,以及用哪种来源。它与 `Raw` 完全正交,不是二选一:**
- **`Raw` 始终存在**,是每篇文章默认的表现力层(任意 HTML / CSS / JS / React:交互、
自定义布局排版、动效、小工具,以及按需的 SVG / canvas 图解),不受配图策略影响,也永远
不需要用户"开启"。
- **`Image` 是独立的可选叠加层**,由配图策略决定是否使用、用哪种来源。
- 选 `none` **不等于**"用 Raw 替代 Image" —— 它只表示"不使用外部图片",`Raw` 照常使用。
> 别把它框成 "Image vs Raw"。正确心智是:"Raw 一定有;Image 要不要、用哪种来源,由用户在
> Plan Checkpoint 明确选定。"
## 四种来源模式(只针对 Image)
| 模式 | 说明 | 适合情况 |
|---|---|---|
| `user-assets` | 用户提供截图 / 照片 / 图表 / 素材目录 | 产品文章、代码审阅、真实报告 |
| `placeholders` | 先用占位图或图片位说明 | 用户稍后补素材 |
| `ai-generated` | AI 按文章和主题生成图片提示词 | 视觉文章、概念解释、封面图 |
| `none` | 不使用外部图片(`Raw` 自由层 / 表格不受影响,照常使用) | tufte 风、技术分析、证据型文章 |
**不主动生成 AI 图片**:`ai-generated` 必须用户显式选择。但即便选 `none`,也不影响 `Raw`。
## Asset Checkpoint 必问(Plan Checkpoint 内 · 必须让用户选,不能默认通过)
配图模式是**必选项**,不允许用一个默认值一笔带过。先一句话说明"Raw 照常使用",再让用户
从四种 **Image** 来源里明确选一种:
```text
配图模式(这一项只决定是否使用外部 Image;Raw 自由层 —— 交互 / 布局 / 动效 / 图解 —— 不受影响,照常使用)。
请从以下四种里选一种:
- none:不使用外部图片,靠正文 + Raw + 表格表达(推荐给技术 / 证据型文章)。
- user-assets:你提供素材目录或截图,我据此排版。
- placeholders:先放占位图,我在 plan.md 的 Assets 段标注每张图应替换成什么。
- ai-generated:我先生成配图提示词,等你确认后再生成图片。
我的推荐:<策略>,原因:<一句话>。你确认或改成别的?
```
## ai-generated 提示词原则
选 `ai-generated` 时**不直接随意生成图片**,先在 `plan/plan.md` 的 Assets 段列出每张图:
位置 · 服务的段落 / 论点 · 目的 · 主题风格 · 构图 · 禁止项 · 提示词 · 备选提示词。
示例:
```text
Image 01
位置:Hero 背景
目的:建立"技术出版物"气质,不解释具体机制
主题:press
风格:warm editorial still life, paper texture, low saturation
禁止:3D icon, neon gradient, SaaS stock photo, smiling office people
Prompt: Warm editorial photograph of a desk with annotated technical notes,
printed code snippets, a graphite pencil, soft morning light, low saturation,
refined book-publishing mood, no screens, no logos.
```
## 图片自检(每张图)
- 是否服务文章中的具体位置?是否符合选定主题 `theme-profiles/<id>.md` 的媒体风格?
- 是否不是纯装饰?是否不会抢正文?是否没有和 Raw 表达重复?
- 是否有 caption / source / alt 文本?
配图自查并入 Plan 自查(5 条之一:"Raw / 图片有目的")—— 由主 Agent 内联完成,
**不再单独开 Asset Reviewer SubAgent**。详见 `review-checklist.md` 的 Plan 自查段。
@@ -0,0 +1,103 @@
# 组件使用政策(reacticle 协议)
文章用 `reacticle` 组件协议写:**不手写裸 `div` / `className` / 行内 `style` / CSS**。
结构走语义组件,正文走段落,自定义视觉走 `Raw`。从包入口导入:
```tsx
import { ThemeProvider, Article, Hero, Lead, Section, Aside, Table, Raw } from "reacticle";
```
## 核心规则(始终适用)
1. **Prose-first,组件按需,Raw 自由。**
- **正文是主体**:普通段落写成 `Section` 的 children,应占文章绝大部分。
- **语义组件是点睛,只在内容确实"是"那个结构时才用**:`Summary` / `Aside` /
`Quote` / `Table` / `RiskList` / `Decision` 等。不要堆叠当装饰 —— 堆卡片会让
文章显得做作、零碎。litmus test:若一句话 / 一个列表 / 一张表 / 一块 Raw 读起来
更好,就用那个。
- **Raw 相反:鼓励多用。** 交互、动画、自定义可视化、新颖排版 —— Markdown 做不到的
东西,才是把文章从"能读"变成"值得读"的关键。Raw 不打断阅读,它给密集文本节奏与
呼吸。
2. **表达语义,不表达布局。** 说"这是 insight / risk / decision",别说"蓝卡片、24px 边距"。
3. **Props 即协议。** 填齐必填字段;确实没有就留空 —— 组件会渲染显式的 `⚠ 未指定xxx`
标记,让缺口可见。**绝不为掩盖缺口而编造。**
4. **主题,不写样式。** 作者只通过 `ThemeProvider` 选一个主题,从不写组件样式。
5. **始终用 `<ThemeProvider theme="...">` 包住 `<Article>`。**
6. **版式与主题解耦。** `Article` 的 `width`(`narrow` / `regular` / `wide` / `full`,默认
`regular`)决定阅读列宽,`toc`(默认在本 Skill 开启)决定是否有左侧目录 —— 都按内容选、
经 Plan Checkpoint 确认,**不由主题决定**。详见 `references/layout.md`。
7. **一个 Section = 一个文件。** 每个 Section 必须是独立组件(`article/sections/NN-*.tsx`),
**坚决不允许**把多个 Section 写进一个组件;`Article.tsx` 只做组装(assembler)。这是
多 Agent 并行开发的前提,详见 `references/section-build.md`。
## 组件分两层:默认核心 + 领域特例
**默认核心组件(绝大多数文章只用这些 + 正文 + Raw):**
- 结构:`Article`、`Hero`、`Lead`、`Section`、`Subsection`、`Conclusion`、`TOC`
- 观点:`Summary`、`Aside`、`Quote`
- 数据:`Table`
- 技术:`CodeBlock`(写代码**只用它**)、`Formula`
- 媒体:`Image`
- 自由层:`Raw`
**领域特例组件(仅当内容"确实是"该结构时按需取用,不要因为属于某类文章就全用上):**
- 决策 / 审阅:`RiskList`、`Decision`、`ActionList`、`Checkpoint`、`Tradeoff`、`Incident`
- 代码评审:`DiffReview`
- 交互壳:`Detail`、`Tabs`
- 富媒体:`Video`、`Audio`
> **不要暴露给作者**:`HighlightedCode` 是 `CodeBlock` 的内部底层,写代码一律用
> `CodeBlock`,不直接使用 `HighlightedCode`。
选组件前先过 litmus test(规则 1):若正文 / 列表 / 表格 / Raw 读起来更好,就不要为了
用组件而用组件。领域特例组件每多用一个,都要能回答"这块内容本质上就是它"。
完整组件 API(属性、用法)见组件库本身的 reference:
`skill/references/{structure,insight,structured,decision,technical}.md`(reacticle-authoring
skill),按需读取,不要一次全读。
## 使用原则
- 正文是主体。组件是语义,不是装饰。
- Raw 是文章表现力,不是应用开发入口(边界见 `raw-policy.md`)。
- `Table` 负责二维信息;`Image` 负责真实或生成配图;`Raw` 是自由的 Web 层 —— 任意
HTML / CSS / JS / React:交互、自定义布局排版、动效、嵌入小工具,以及按需的图解(SVG /
canvas),SVG 只是其中一种手段,不是默认。
- **`Raw` 与 `Image` 正交,不是二选一**:`Raw` 始终默认存在、照常使用;`Image` 是否使用、
用哪种来源由 Plan Checkpoint 的配图策略决定(见 `references/asset-policy.md`)。选配图
`none` 只表示不用外部图片,`Raw` 不受影响。
## 信息密度与组件比例
见 `information-density.md`:100% 时长文优先、Raw 点亮关键概念;密度越低,视觉块比例
越高,但**仍必须保持文章形态**。
## 最小骨架(注意比例:大量正文 + 一个点睛组件 + 一块 Raw)
```tsx
import { ThemeProvider, Article, Hero, Lead, Section, Aside, Raw } from "reacticle";
export function Article_() {
return (
<ThemeProvider theme="tufte">
<Article>
<Hero title="标题" subtitle="副标题" meta={[{ label: "日期", value: "2026-06-08" }]} />
<Lead>导语,框定主题。</Lead>
<Section index="01" title="第一节">
<p>正文段落用 children —— 这应是文章主体,尽量多写正文。</p>
<p>再写一段,把背景、推理、结论用文字讲清楚。</p>
<Aside tone="principle" label="核心判断">一句话的核心判断。</Aside>
<Raw title="为本段现写的内联 SVG">
<svg viewBox="0 0 240 60" width="100%">
<polyline points="0,50 40,42 80,46 120,20 160,28 200,8 240,14"
fill="none" stroke="var(--ra-color-accent)" strokeWidth="2" />
</svg>
</Raw>
</Section>
</Article>
</ThemeProvider>
);
}
```
@@ -0,0 +1,208 @@
# 文章封面(Cover)—— 设计指南
## 这是什么
每篇 Beautiful Article 在 TOC + 正文之上有一块**像书的封面**的题图,独占顶部。
它是 HTML 文章"出版物感"的开篇 —— 类似书封 / 杂志封面 / 唱片封套:
一眼传达 "**这篇讲什么 + 长什么气质**",决定读者会不会往下看。
封面**不**是 Hero:
| 角色 | Hero | Cover |
|---|---|---|
| 位置 | `<Article>` 内、TOC 旁 | `<Article>` **之外**、TOC **之上** |
| 形态 | 标题 + 副题 + meta(文字栏) | 3:4 图文构图(图 + 字) |
| 职责 | 框定主题 + 读者收获 | 视觉钩子 + 风格定调 |
| 信息 | 文字为主 | **图主字辅** |
两者**互补**:封面引人,Hero 锚定。**不要把它们做成同一件事**。
---
## 尺寸 · 屏幕 3:4 一屏看全 / PDF 独占首页
- **屏幕**:`aspect-ratio: 3 / 4`,宽度同时受**两条上限**约束(取较小者),保证
3:4 完整封面**一屏看全、不用下拉**:
1. `48rem`(768px)—— 硬上限,再大就像广告牌而不是书封;
2. `calc((100vh - 8rem) * 3 / 4)` —— 从视口高度反推的宽度,给顶栏 / 容器
边距 / 上下呼吸留 8rem (128px)。
也就是 `max-width: min(100%, 48rem, calc((100vh - 8rem) * 3 / 4))`。在矮屏幕上
封面自动缩小(仍是 3:4),在 1024px 以上的高屏幕上保持 768×1024。
- **PDF**:`@media print` 默认保持屏幕版 3:4 构图,并在封面之后分页,让封面独占
PDF 首页。不要依赖通用 `height: 100vh` 把封面强行拉成整页;Chromium print 对
复杂封面内部布局的裁切行为不够稳定。
为什么是 3:4 而不是 A4 比例(1:√2 ≈ 0.707):
| 比例 | 数值 | 感觉 |
|---|---|---|
| 16:9 | 1.78 | 太宽,像横幅 / banner |
| A4 (1:√2) | 0.707 | 偏瘦,像报告内页 |
| **3:4** | **0.75** | **像书封 · A4 和 Letter 的中间值** |
| 2:3 | 0.667 | 像小说封面 · 偏窄 |
3:4 在 A4 PDF 上下方有约 4% 白边;在 Letter PDF 上下方约 3% 白边。默认保留这
些比例差异,换取 PDF 输出稳定。
**给设计者的影响**:默认 PDF 不会改变封面比例,但内部布局仍应自适应:用百分比 /
`aspect-ratio` / `inset: 0` / `grid` / `flex` 撑起元素,**不要把任何元素的位置写死
成绝对像素**,否则不同视口和打印缩放下仍可能错位。
---
## 硬约束(5 条 · 不可妥协)
1. **3:4 屏幕 + PDF 独占首页(外壳不要动)**。`Cover.tsx` 的 `aspectRatio: "3 / 4"` 和
max-width / margin / border 不要改 —— `pdf-print-overrides.css` C 段只负责让封面
之后分页。**内部元素一律用百分比 / 相对单位**,不要写绝对 px 高度。
2. **图文并茂**。**禁止纯文字封面**。必须同时具备:
- **视觉主体**(什么技术都行,见下);
- **文字层**:至少一个标题,可加一行副题、一个小标签(type / date / kicker)。
3. **主题忠实 · 只能用 `--ra-*` token**。颜色 / 字号 / 字重 / 边框 / 圆角 / 间距全部通过
`var(--ra-color-fg)` `var(--ra-color-accent)` `var(--ra-text-3xl)` 等取值。
**禁止**:写死 hex 颜色、写死字体名、写死像素字号 —— 切主题封面就废。
4. **内容忠实**。封面的视觉主体要呼应**正文主旨**(不是泛泛装饰)。读完封面,
读者要能猜出文章在讲哪个领域 / 哪种判断。比如:
- 文章讲"提示词缓存就是一切" → 封面可以是**缓存命中率曲线 / 重复 token 的高亮带**;
- 文章讲"Codex 智能体循环" → 封面可以是**带箭头的循环图(USER → MODEL → TOOL)**;
- 文章讲"色彩冲撞" → 封面可以是**两个互补色块的几何拼贴**。
5. **offline-first**。**唯一被硬禁的事**:远程图片(`<img src="https://...">`、
Google Fonts 动态加载、跨域 CSS background-url 等)—— 离线打不开。
**base64 raster** 仅当 Plan Checkpoint "配图模式" 是 `user-assets` / `ai-generated`
才允许,且必须内联。
---
## 视觉技术 · 模型自己选,效果好就行
封面的视觉主体**用什么技术由你(模型)决定** —— SVG / CSS / Canvas / WebGL / 字体 / 表情符号
/ 复杂 React 组件 / mask / clip-path / filter / 多层合成 / 任意混搭。**没有"首选"**,只
有"对这篇文章 + 这个主题,哪种最对味"。
可选技术(不全,能想到的都可用):
- **内联 SVG**:网格 / 曲线 / 节点-边图 / 流程箭头 / 矢量插画 / pattern fill / mask;
优势是任意尺寸清晰、`currentColor` 自动跟主题。
- **CSS 几何 / gradient / clip-path / backdrop-filter**:分屏色块、玻璃感、光感、
抽象排版;适合海报感 / 平面设计感的封面。
- **`<canvas>` + JS**:粒子 / 流体 / 噪声 / 程序化纹理 / 字符 ASCII art;
适合数据 / 科技 / 生成艺术气质。注意:Canvas 在 PDF 里只会渲染**初始帧**,所以
动画类要保证"第一帧本身就是好看的最终态"。
- **复杂 React 组件**:完全自定义的布局,比如用 grid + 条件渲染做一面"目录式封面"、
用 React 重排标题字符做字体艺术。
- **字体 / 排版本身就是图**:超大字号、字距 / 行距实验、字符叠加、emoji 拼贴、
Unicode 几何字符 (`◐ ▲ ◆ ╳`)、引号 / 章节号放大到布满整页。
- **多层合成**:背景层(gradient) + 中层(SVG) + 前景层(文字) + 装饰层(图标 / 标签)。
- **混搭**:上面任意几种叠在一起。封面是单次创作,没必要拘泥单一技术栈。
**唯一不允许**:远程图片(见硬约束 5)。其它**全开放**。
**判定标准**:眯眼看 3 秒(图 OK 吗?气质对吗?切主题不会废吗?打印不会错位吗?)。
通过这 4 关,技术怎么实现都行。
---
## 构图模板(按主题感选一个起手)
| 模板 | 视觉布局 | 适合主题感 |
|---|---|---|
| **A · 上字下图** | 标题区在上 1/3,视觉主体占下 2/3(书封最经典的"片名 + 主画面") | 教学 / 报告 / 多数场景 |
| **B · 大字盖图** | 视觉铺满整个 3:4,超大字标题压在中段或下段 | bodoni / press · 印刷 / 叙事 |
| **C · 上下分屏** | 上半色块(含标题) + 下半视觉主体;中间一条分割 | tufte / shannon · 数据 / 严谨 |
| **D · 满屏拼贴** | 视觉是若干色块 / 形状 / 图层拼接铺满整页,文字嵌在某个块里 | sottsass / bayer · 当代 / 视觉感 |
| **E · 极简框** | 大留白、细线框、标题居中、一个极小的视觉锚点(一个圆 / 一个图标 / 一段曲线) | 极简主题 / 严肃报告 / 哲思 |
**不要混搭** —— 一篇文章一个模板。模板只是"起手",具体怎么实现(用 SVG / CSS /
Canvas / React 还是别的)由你定。
---
## 主题倾向(速查)
读 `theme-profiles/<id>.md` 获得权威风格指南;下面是"封面起手"提示(**视觉手法只是
启发,不是规定** —— 你可以用任何技术做出对的气质):
| 主题 | 封面感觉 | 推荐模板 | 视觉手法举例 |
|---|---|---|---|
| tufte | 学术 / 克制 / 数据 | C 或 E | 极细线网格 + 一个小型 sparkline / 数据点;颜色低饱和 |
| press | 报刊 / 叙事 / 凝重 | B 或 E | 大字标题 + 横分割线 + 印章式 kicker;可加铜版画感纹理 |
| shannon | 信息论 / 工程 / 蓝调 | A 或 C | 节点-边图 / 香农式信道图 / 概率分布 |
| bodoni | 古典 / 优雅 / 印刷 | B | 高对比 serif 大标题 + 极细 hairline 装饰 + 留白 |
| bayer | 包豪斯 / 几何 / 排版 | D | 三原色块拼贴 + 圆 / 方 / 三角组合 |
| sottsass | 后现代 / 玩味 / 明亮 | D | 撞色色块 + 装饰图案 + 大胆字体 |
| fuller | 测地 / 科技 / 结构 | A 或 D | 三角网格 / 等距投影 / 工程图样 |
没列到的主题 → 读它的 `theme-profiles/*.md` 决定模板。
---
## 反面案例(**禁止**)
- **纯文字封面**(只有标题居中,没有视觉主体)。
- **使用远程图片**(`<img src="https://...">`、`background-image: url(https://…)`)—— 离线打不开。
- **写死颜色 / 字体 / 像素值**(`color: #ff0066` / `font: 24px Helvetica`)—— 切主题废。
- **位置写死成 3:4 时的绝对像素**(`top: 384px`)—— 换视口或打印缩放就错位。
- **复制 Hero 内容到封面**(标题、副题、日期、作者全堆封面里)—— 与 Hero 重复。
- **塞过多元素**(封面里同时塞下:标题 + 副题 + 三个小标签 + meta + Lead + TOC 预览 +
大插画 + 二维码)—— 信息密度爆炸,不像封面像 dashboard。
- **内部元素溢出容器**(让 absolute 子元素跑出 3:4 边界)—— PDF 会被裁切。
- **封面承担正文**(把第一段干货塞封面里)—— 封面是钩子不是内容。
- **Canvas 动画依赖时间才出现内容**(PDF 只截第一帧,黑屏)—— 保证第一帧自身就好看。
---
## 自检(**必过 5 条**)
写完封面,对照下面 5 项;任何一项 fail → 改完再交付:
1. **图文并茂**:截掉文字层后还剩视觉主体?截掉视觉层后还剩文字?两者都要有。
2. **主题忠实**:切到 `theme-profiles/index.json` 里另一个主题(改 `main.tsx` 一行),
封面**自动跟随**变色 / 变字、不破相?如果有写死值就不算过。
3. **内容忠实**:盯着封面看 5 秒钟,能不能猜出文章在讲什么?如果只能看到"一个漂亮
图形"但跟正文关系不大,不算过。
4. **比例自适应**:把容器从 3:4(屏幕)拉成 ~3:4.2(A4)/ ~3:3.9(Letter),内部元素
没溢出 / 没错位 / 没出现大块空白?(用 `position:absolute; inset:0` + `grid`
/ `flex` 撑起元素,而不是写死像素位置,就自动通过)。
5. **不与 Hero 重复**:封面文字 ≠ Hero 文字(一个是钩子,一个是锚点)。
---
## PDF 表现
`scripts/pdf-print-overrides.css` 的 C 段会把封面:
- **保持封面的 3:4 外壳不变** —— 避免 Chromium print 在强拉伸时裁切内部布局;
- **`break-after: always`** —— TOC 从第二页开始。
效果:PDF 第一页 = 3:4 封面独占首页;第二页起 = TOC + 正文。
> **想做满页封面**:可以在单篇文章里为 `.ra-cover` 加专门的 print 适配,但必须导出
> PDF 目检。不要把满页拉伸作为通用默认值。
---
## 何时关闭封面(`--no-cover`)
99% 的场景都该开。少数关闭的情况:
- **briefing**(决策摘要 / 给忙人看):用户希望"打开就是干货",封面反而是阻力。
- **dialogue**(对话 / 访谈):内容是对话流,封面价值不大;可关。
- **用户明确要关**:尊重用户。
关闭方法:
- 脚手架阶段:`bash scripts/scaffold.sh <dir> --theme=<id> --no-cover`。
- 已脚手架:删 `article/main.tsx` 里 `<Cover />` 引入和渲染,可顺手删 `Cover.tsx`。
---
## 写作流程(在 Skill 里的位置)
| 阶段 | 跟封面相关的事 |
|---|---|
| Phase 2 Plan | `plan/plan.md` Brief 段里加一行"封面:开/关 + 一句构图想法 + 主题模板(A/B/C/D/E)" |
| Phase 3 Checkpoint 1 | 第 5 项独立确认"封面 · 开 / 关",AI 推荐通常是"开" |
| Phase 4 First Spread | **替换 `article/Cover.tsx` 里的 `<CoverPlaceholder />`** 为本文专属设计;首屏验收必看封面 |
| Phase 4 First Spread Review | Reviewer 用本文档自检 5 条核对 |
| Phase 6 Final Review | Visual Reviewer 复查封面与主题一致性 |
| Phase 8 Delivery | PDF 导出时封面自动独占首页(不需要额外操作) |
@@ -0,0 +1,35 @@
# Harness 视角
本 Skill 的重点不是"提示词",而是一个小型 harness。它要回答六个问题:
| Harness 部分 | 本 Skill 解决的问题 | 设计手段 |
|---|---|---|
| 上下文管理 | 模型到底看到了什么 | 原始源统一转 `source.md`,阶段化读取 reference |
| 工具系统 | 能处理什么输入 / 输出 | URL / PDF / DOCX / Markdown / 截图 / 图片素材 / 本地构建 / 浏览器检查 |
| 执行编排 | 下一步该做什么 | 分 Phase、检查点、首屏样张、完整生成、验收修复 |
| 状态与记忆 | 决策如何跨步骤保持 | `source.md`、`plan/plan.md`(Brief/Outline/Theme/Assets 四段合一)、`review/first-spread-review.md`、`review/final-review.md` |
| 评估与观测 | 怎么知道文章好不好 | Plan 主 Agent 内联自查;First Spread / Final 用 SubAgent + 写文件;Section 用 SubAgent + 消息返回 |
| 约束与恢复 | 跑偏后怎么修 | 最小切片修复,禁止整篇无脑重写 |
## 状态文件是长期记忆
Agent **不应依赖聊天上下文记住关键决策**。跨阶段决策落盘到下列文件 —— 比原先精简了 ~5 份:
```text
source/source.md # 统一后的源材料(原始语言,事实底座)
source/source.<lang>.md # 仅当需翻译:地道翻译版,作为后续编写的事实底座
source/extraction-notes.md # 抽取风险 / 丢失 / 待补充 / 语言与翻译说明
plan/plan.md # 唯一规划文件:Brief / Outline / Theme / Assets 四段合一
review/first-spread-review.md # First Spread SubAgent 结论(首屏验收依据)
review/final-review.md # 终审三视角结论(交付物的一部分)
review/source-review.md # 仅复杂/低置信源时
review/repair-log.md # 仅有修复时
```
长会话中如果不确定某个已确认的决策,**回读这些文件**,不要凭记忆重新发明。
## 一句话定位
> Beautiful Article 把素材编辑、设计成一篇美丽的网页文章;它首先是一篇**文章**,
> 不是网页应用。交互、Raw、配图都服务阅读。
@@ -0,0 +1,42 @@
# HTML 输出与构建
工作区是一个 Vite + React + TS 项目,从 npm 消费 `reacticle`(最新发布版)。文章源在
`article/Article.tsx`(由 `article/main.tsx` 挂载,主题在此固定)。
## 命令(在工作区根目录)
| 命令 | 作用 |
|---|---|
| `npm run dev` | 启动预览(Phase 4 / 5 边写边看)。 |
| `npm run build` | `tsc --noEmit` 类型检查 **+** 构建自包含单页 HTML 到 `dist/index.html`(CSS + JS 内联)。TS 报错会让构建失败,避免错误漏进交付物。 |
| `npm run html` | 复用 `npm run build`(含类型检查),再把单页 HTML 复制到 `article/article.html`(**交付物**)。 |
| `npm run typecheck` | 仅类型检查。 |
单文件由 `vite-plugin-singlefile` 产出:CSS + JS 全部内联,**断网可打开、可分享**。
## 切换主题
主题在 `article/main.tsx` 的 `<ThemeProvider theme="...">` 改一个字即可(必须是组件库
已注册的 runtime theme id:`tufte` / `press`)。
## PDF(可选 · 由 Checkpoint 3 触发)
详见 `references/pdf-output.md`。一句话用法:
```bash
npm run html # 先有 article/article.html
bash <path-to-beautiful-article>/scripts/html-to-pdf.sh # → article/article.pdf
```
脚本会自动探测系统的 chromium-family 浏览器,在 HTML 头部注入 `@media print` 覆盖(把
TOC 从左右栅格塌成上下排布,TOC 独占首页),再 headless 打印为 PDF。无 npm 依赖、无 Node。
> Raw 交互在 PDF 里只能渲染为初始态。`interactive-explainer` 类型 PDF 价值有限,用户可在
> Checkpoint 3 自行决定要不要导。
如需"复制为提示词 / 行动项"按钮,可在文章里挂 `ExportBar`(与 PDF 无关)。
## 交付自检
- `npm run html` 成功,`article/article.html` 能在浏览器离线打开。
- 控制台无报错;桌面与移动端都可读,无文字溢出 / 遮挡 / 空白异常。
@@ -0,0 +1,50 @@
# 信息密度
信息密度(信息保留比例)决定**正文与视觉块的配比、内容取舍的尺度、章节深度**。
**与文章类型的关系**:理论上两者独立,实践中绑定很紧 —— 每个文章类型都自带一个标配保留
比例(见下表)。Plan Checkpoint 1 把"保留比例"打包进"文章类型"的语义化选项里收集,避免
`longform + 20%` 这种伪组合(详见 `article-types.md` 顶部说明)。**只在用户明确精修**(如
"longform 但只要 60%")时作为非标配组合记入 `plan/plan.md` Brief 段。
默认值:**100% 信息保留**(跟随默认类型 `longform`)。
## 信息密度等级
| 信息保留 | 推荐文章类型 | 适合场景 | 表达特征 |
|---|---|---|---|
| 100% | longform | 完整归档、原文级深度阅读 | 长文为主,Raw 增强,完整章节和细节 |
| 80-90% | tutorial / full-report / explainer / dialogue | 教学步骤 / 消化后的报告 / 概念解释 / 对话整理 | 删冗余但保留主要论证、步骤、对话 |
| 60-70% | review / essay | 工程审阅 / 观点评论 | 留核心证据、论证、关键发现 |
| ~50% | briefing | 给决策者看 | 留结论 + 关键证据 + 行动;视觉比例升高 |
| 40% | visual-essay | 分享、传播、图文展示 | 视觉块占比最高,文字更短,强调节奏 |
| **~25%(原文摘录)** | interactive-explainer | 玩懂一个概念(Raw 交互为主载体) | **特例:百分比含义不同** —— 不是"删 75%",而是"成品里直接来自原文的句子 / 段落约占 25%";其余 75% 是 AI 围绕核心知识点全新创作的引导文字 + 交互演示 + 自己试 + 验证理解 |
| 20% | one-page teaser / 封面式表达 | 引导阅读、封面 | 只留核心观点和最有力量的材料(任何注册类型 + 20% 都属"非标配组合",要在 plan.md 标注) |
## 信息密度与组件 / 视觉比例
| 信息保留 | 正文比例 | Raw / 图片比例 | 组件策略 |
|---|---|---|---|
| 100% | 高 | 中低 | 长文优先,Raw 点亮关键概念 |
| 80% | 中高 | 中 | 每节可有一个视觉增强 |
| 60% | 中 | 中高 | 重点结构化,视觉辅助理解 |
| 40% | 中低 | 高 | 图文节奏更强,但仍是文章 |
| 20% | 低 | 高 | 接近视觉文章,不保留细节 |
## 三个维度的关系
```text
文章类型 = 结构决策
信息密度 = 内容保留决策 ← 与类型实际绑定(每类有标配比例),仅在用户精修时解耦
主题 = 审美气质决策 ← 与前两者完全解耦
```
- 主题与类型 / 密度**完全解耦**:任意主题都能在任意类型 + 密度下成立(表现策略不同而已)。
- 类型与密度**实践绑定**:见 SKILL.md Phase 3 的合并问题;只有"non-standard"组合时才显式解耦。
## Plan Checkpoint 必问
1. 文章类型(含标配信息保留比例)—— 见 SKILL.md Phase 3,4 个必问题之一。
2. 是否允许编辑删减、重组、改写语气?—— 默认允许,开场说明里明示。
3. 哪些信息必须 100% 保留?—— 写进 Brief 段"必须保留的信息"列表。
4. 哪些内容可以压缩或移除?—— 写进 Brief 段"可删减的信息"列表。
@@ -0,0 +1,46 @@
# 版式:宽度模式与 TOC
版式是**与主题解耦的独立决策**(主题只管审美气质,宽度/目录管阅读版式),就像信息密度
与主题解耦一样。在 Plan Checkpoint(Phase 3)由用户确认,落盘到 `plan/plan.md` 的 Brief 段。
## 宽度模式(`Article` 的 `width`,需 reacticle ≥ 0.2.0)
宽度由 `<Article width="...">` 控制,**不再由主题决定**。四种常见模式:
| 模式 | 阅读列宽 | 适合 |
|---|---|---|
| `narrow` | ~34rem | 聚焦短文、`essay`、`briefing`、金句节奏强的文章 |
| `regular`(默认) | ~46rem | `longform` / `explainer` 等常规长文阅读 |
| `wide` | ~58rem | 表格 / 代码 / 数据密集(`full-report`、`review`、`tutorial`) |
| `full` | ~78rem | 图文主导、宽幅媒体(`visual-essay`) |
任意主题都能配任意宽度(解耦)。例:`tufte + wide` 适合大量数据表;`press + narrow`
适合从容随笔。
## TOC(`Article` 的 `toc`)
`<Article toc>` 渲染左侧目录(从 `Section` / `Subsection` 自动派生,最多三级,带滚动高亮)。
- **本 Skill 默认开启 TOC**(长文有导航更易读),但**必须在 Plan Checkpoint 让用户确认**。
- 很短的文章(`briefing` / 短 `visual-essay`)可以关掉,避免目录比正文还显眼。
- TOC 开启会变成"左目录 + 正文"两栏;窄视口(<1000px)自动回落为单栏。
## 用法
```tsx
// 默认:常规宽度 + 开 TOC
<Article toc width="regular"> ... </Article>
// 数据密集报告:更宽 + TOC
<Article toc width="wide"> ... </Article>
// 短随笔:窄列、不要目录
<Article width="narrow"> ... </Article>
```
## 自检
- 宽度是否匹配内容?(表格 / 代码多 → 至少 `wide`;纯叙事 → `regular` / `narrow`)
- 宽度是按内容选的,而不是被主题决定的?
- TOC 的开关是否经用户确认?短文是否误开了喧宾夺主的目录?
- 移动端两栏是否正常回落为单栏?
@@ -0,0 +1,208 @@
# PDF 输出(可选)
把交付的单页 HTML(`article/article.html`)转成 PDF。**这是 Phase 8 Delivery 里的可选步骤**,
由 Checkpoint 3 用户选 "通过 · 同时导出 HTML + PDF" 时触发;不选则不动。
> HTML 仍然是主交付物:它能离线打开、可分享、可在浏览器里完整体验 Raw 交互。PDF 是给"需要
> 归档 / 打印 / 邮件附件 / 不联网阅读"场景的补充,**Raw 交互在 PDF 里只能渲染为初始态**。
---
## 快速用法
```bash
# 工作区根目录
npm run html # 先确保有 article/article.html
bash <path-to-beautiful-article>/scripts/html-to-pdf.sh # 默认 article/article.html → article/article.pdf
bash <path-to-beautiful-article>/scripts/html-to-pdf.sh in.html out.pdf # 自定义路径
```
也可以把这个 bash 调用放到工作区 `package.json` 的 scripts 里,让用户 `npm run pdf` 就跑
(路径是用户机器上 Skill 的绝对路径,**不在脚手架模板里固化**,避免硬编码)。
---
## 前提条件
本机已装 chromium-family 浏览器之一(脚本会自动探测,按下列顺序):
```text
chromium / chromium-browser / google-chrome / google-chrome-stable / chrome
brave-browser / microsoft-edge
/Applications/Google Chrome.app/...
/Applications/Chromium.app/...
/Applications/Microsoft Edge.app/...
/Applications/Brave Browser.app/...
/Applications/Arc.app/...
/usr/bin/chromium / /snap/bin/chromium
```
**找不到任何浏览器** → 脚本不会爆,会给出回退指引:注入了打印 CSS 的临时 HTML 路径会被
打印出来,用户可以用任意浏览器手动 `Cmd+P` / `Ctrl+P` → "另存为 PDF"。
不依赖 Node / npm 包 / puppeteer / playwright / weasyprint —— 故意只用系统已装的浏览器,
零环境配置。
---
## 设计原理(重要 · 调样式前先读)
### 1. 为什么要注入 print CSS(不是改 reacticle)
reacticle 的 TOC 在桌面是**左右栅格**(`.ra-article-layout--with-toc` 是 `display: grid`,
两列:TOC | 文章),在 mobile(≤999px)才塌成单列 `display: block`。
PDF 阅读习惯是**上下排布**(TOC 先 / 正文后),跟 mobile 体验对齐。最干净的实现:在 PDF
生成时**注入一段 `@media print` CSS**,强制 `display: block`,等价于复用 mobile 分支。
这样:
- **不动 reacticle**:任何版本的 reacticle 都能用这个脚本生成合理 PDF。
- **不动用户的 Article.tsx**:用户的源码完全不受影响。
- **CSS 只在打印生效**:浏览器里看 HTML 还是左右栅格。
### 2. 注入了哪几条规则
CSS 抽到了独立文件 **`scripts/pdf-print-overrides.css`**(与脚本同目录),分三组:
```text
A · TOC 排版(让 TOC 上 / 正文下)
A1) .ra-article-layout--with-toc { display: block } → 塌成单列
A2) .ra-toc { position: static; page-break-after: always } → 解 sticky + 独占首页
A3) .ra-toc__list { column-count: 2 } → 长 TOC 双列省纸
A4) .ra-toc__item { break-inside: avoid } → TOC 项不被列间撕开
A5) .ra-article-layout--with-toc > .ra-article → 正文在 TOC 后自然流
A6) .ra-toc a / .ra-article a { text-decoration: none } → 关闭打印链接下划线
B · 分页行为(修长文章的大块空白页)
B1) .ra-section / .ra-subsection { break-inside: auto !important }
→ 撤销 reacticle print.css 里的 break-inside: avoid-page。
对长 Section 来说,那条规则会把整节推到下一页、留前一页大半空白。
B2) h1-h4 + .ra-section__head + .ra-subsection__head { break-after: avoid }
→ 标题不被孤儿化(不会孤零零卡在页底,下面是空白)
B3) .ra-hero / .ra-lead / .ra-conclusion { break-inside: avoid }
→ 短的开场 / 收束块原子化,不撕开
B4) p / li / blockquote { orphans: 3; widows: 3 }
→ 段落不留 1-2 行寡行
B5) figure / .ra-table / .ra-codeblock / .ra-formula / .ra-image / .ra-raw
{ break-inside: avoid }
→ 图表、表格、代码块、Raw 块尽量整块;过高时浏览器仍会自动 fallback 分页
C · 封面(若文章有 3:4 封面,让它独占 PDF 首页)
C1) .ra-cover { break-after: always; break-inside: avoid }
→ 屏幕和 PDF 都保持 3:4 书封;打印时 TOC 从第二页开始
→ 详见 references/cover.md
```
完整带注释的 CSS 直接看 `scripts/pdf-print-overrides.css`。要调样式(比如换页面 break 策略
/ 改双列阈值 / 加打印水印),**直接改这个文件就行**,不用动 bash 脚本。
> **为什么 CSS 是独立文件**:macOS 自带 BSD awk 对 `-v inject="$INLINE_CSS"` 这种多行字符串
> 报 `newline in string` 错(GNU awk 没事)。把 CSS 抽成文件,awk 用 `getline < file` 读取,
> 两个 awk 都吃得下。顺带也让 CSS 变成可独立编辑 / lint / diff 的真正资源。
reacticle 自带的 `print.css` 仍然生效(白底黑字 / 隐藏 export bar / `break-inside: avoid`
等),本脚本只补 TOC 排版那一块。
### 3. 渲染流程
```
article.html ──── awk 注入 print CSS ──── /tmp/article-print.html
│
▼
探测的浏览器 --headless --print-to-pdf
│
▼
article.pdf
```
Chrome 标志:
- `--headless=new`(旧版 fallback 到 `--headless`):无窗口模式。
- `--no-pdf-header-footer` / `--print-to-pdf-no-header`:去掉浏览器自带的 URL / 日期 / 页码
(颠倒了 colophon 的角色)。
- `--virtual-time-budget=5000`:给页面 JS 5 秒初始化时间,让 Raw 组件、KaTeX、Prism 等
渲染完再截屏。
- `--hide-scrollbars` / `--disable-gpu` / `--no-sandbox`:清洁渲染。
### 4. Raw 交互在 PDF 里会怎样?
PDF 是**静态文档**,所有 Raw 交互(滑块、按钮、动画、canvas、视频)只能渲染**初始状态**。
设计建议:
- **interactive-explainer 类型**:PDF 价值有限(核心是"操作"),用户可在 Plan 阶段不开
PDF 导出。
- **其它类型**:Raw 通常是辅助图解(流程图 / SVG / 趋势图),初始态已经够看,PDF 没问题。
- **Raw 内的内容若依赖 hover / click 才显示**:在写 Raw 时考虑"是否打印友好",如默认露出
关键内容、用 `print:` 风格让 hover 状态在打印时强制展开。
---
## 故障排除
### 找不到浏览器
脚本会打印临时 HTML 路径(已经注入打印 CSS),手动 Cmd+P 即可。
### PDF 里 TOC 没分页 / 跟正文挤在一起
某些 Chromium 旧版本对 `page-break-after: always` 的支持有差异。可以:
- 升级 Chrome 到当前主版本。
- 或手动 Cmd+P 时在打印对话框选"两面 / 缩放 / 自定义边距"。
### PDF 分页奇怪 / 出现大块空白页 / 标题孤零零卡在页底
通常是某些"原子块"被强制不分页导致整块被推到下一页。检查 `pdf-print-overrides.css`:
- **B1** 是否生效(看 DevTools Print Preview 里 `.ra-section` 的 `break-inside` 值)。
reacticle `print.css` 那条没 `!important`,理论上我们的 `!important` 一定能赢。
- 自己写的 **Raw 块**里有没有 inline `style={{ breakInside: 'avoid' }}` 或类似 CSS —— 删掉。
- 如果还想"标题永远不卡页底",把 B2 里 `break-after: avoid` 改成
`break-before: avoid; break-after: avoid`(更激进,但偶尔会牺牲一些纸面利用率)。
### PDF 里 Raw 没渲染完整 / 图表是空白
- 增加 `--virtual-time-budget`(编辑脚本,从 5000 改到 10000+)。
- 检查 Raw 的 JS 是否在 DOMContentLoaded 内同步渲染(异步加载的远程图片 / 数据可能没等到
截屏就开拍)。
- 改造 Raw:用 SSR-friendly 的初始态 + 客户端 hydrate 增强,不要"完全靠 JS 后才能看"。
### PDF 字体跟浏览器看不一样
主题用了 `@font-face` 远程字体?headless Chrome 默认不等远程字体加载完。可以:
- 用 system font fallback(推荐,多数主题已经做了)。
- 或在 HTML 里把字体 `data:` 内联(vite-plugin-singlefile 已经处理静态资源,但 woff 字体
要看主题怎么写的)。
### 想自定义页面留白 / 纸张大小
Chromium 的 `--print-to-pdf` 不支持命令行页面尺寸 / 边距参数。当前 CSS 用
`@page { margin: 0 }` 让主题纸色铺满整页,再用 `.ra-root { padding: 0.45in }`
提供内容留白。如果需要改留白,优先调整 `scripts/pdf-print-overrides.css` 里的
`.ra-root` print padding。
如果用户真的要自定义,建议:
1. 用浏览器 GUI 打印(Cmd+P)—— 那里有边距 / 纸张 / 缩放选项。
2. 或者派生一个 `html-to-pdf-puppeteer.sh` 脚本,单独支持自定义。当前脚本默认按 Chrome
默认页面尺寸(Letter 美区 / A4 其它区)输出。
---
## 与 Skill 流程的关系
| 阶段 | 触发 | 动作 |
|---|---|---|
| Phase 8 Delivery | 用户在 Checkpoint 3 选 "通过 · 同时导出 HTML + PDF" | 跑 `npm run html` → 跑 `bash <skill>/scripts/html-to-pdf.sh` → 交付 `article.html` + `article.pdf` |
| Checkpoint 3 其它选项 | 用户选 "通过 · 导出 HTML 交付" | 不跑 PDF |
| 用户事后想补 PDF | 任何时刻 | 在工作区根目录手动跑 `bash <skill>/scripts/html-to-pdf.sh` |
---
## 不做的事
- ❌ 不在脚手架强行装任何 PDF 相关 npm 包(保持脚手架轻量)。
- ❌ 不在 Checkpoint 3 默认勾选 "导出 PDF"(PDF 不是主交付物)。
- ❌ 不替用户判断"要不要 PDF" —— 这是 Checkpoint 3 用户独立选择项。
- ❌ 不改 reacticle 来支持 PDF(CSS 注入更轻量、跟 reacticle 版本解耦)。
@@ -0,0 +1,109 @@
# plan.md 模板(单一规划文件)
Phase 2 **只产出一份** `plan/plan.md`,四段:**Brief / Outline / Theme / Assets**。
**不直接写 HTML**。写完后由主 Agent 内联跑 5 条自查(见 `review-checklist.md` 的 "Plan
自查"),按结论改 `plan/plan.md` 本身,**不开 SubAgent、不写任何 review 文件**,然后进入
Checkpoint 1。
> 为什么合并:原先四份文件(editorial-brief / outline / theme-decision / asset-plan)真正
> 跨阶段被读取的只有"信息保留比例 / 目标语言 / 章节锚点 / 主题 id"。合并成一份后,主 Agent
> 维护更省心,Section subagent 只需打开 plan.md 找到"Outline"段里自己的章节即可。
---
## `plan/plan.md` 完整模板
```markdown
# Plan
## Brief
- 目标读者:<谁会读,带着什么问题>
- 目标语言:<跟随源语言(默认) / 指定语言>。若指定且与源不一致:源语言 <X> → 目标 <Y>,
事实底座用翻译版 `source/source.<lang>.md`(地道、去翻译腔)
- 文章类型:<longform / full-report / tutorial / explainer / dialogue / review / essay / interactive-explainer / briefing / visual-essay>
- 信息保留比例:<X%>(默认走类型标配:longform=100% · tutorial=90% · full-report=80% ·
explainer=80% · dialogue=80% · review=70% · essay=70% · briefing=50% · visual-essay=40% ·
**interactive-explainer=~25%(特例 · 见下)**;用户偏离标配则写实际值并加一行"非标配组合
· 注意事项",提醒主 Agent 在写每节时手动调整正文/视觉比例)
- **interactive-explainer 特例说明**:这个比例的含义和其它类型不同 —— 不是"原文删了 75%",而是
"成品里直接来自原文的句子 / 段落约占 25%";其余 75% 是 AI 围绕核心知识点全新创作的引导文字
+ 交互演示 + 自己试 + 验证理解。本质是**内容重构**而非内容压缩。
- 必须保留的信息:<章节 / 表格 / 代码 / 数据 / 引用,逐条列;指向 source.md 的具体位置>
- 可删减的信息:<重复 / 旁枝 / 过时内容;每条带理由>
- 语气:<克制分析 / 出版叙事 / 决策汇报 / 教学>
- 主要观点:<这篇要让读者记住的 1-3 个判断>
- 阅读目标:<读完能做什么 / 知道什么>
- 版式宽度:<narrow / regular / wide / full>(默认 regular,见 layout.md)
- TOC:<开 / 关>(默认开)
- 配图策略:<none / user-assets / placeholders / ai-generated>
- 封面:<开(默认) / 关>。若开,写一句构图想法 + 选定的封面模板(A 左字右图 / B 大字盖图 /
C 上字下图 / D 几何拼贴 / E 极简框)+ 主视觉用什么(如"SVG 缓存命中率曲线"/"包豪斯三色块")。
详见 `references/cover.md`。Brief 阶段一句话即可,正式视觉在 Phase 4 First Spread 替换
`article/Cover.tsx` 时落定。
## Outline
- Hero:<标题气质 / 副标题 / meta:日期·来源·作者>
- Lead:<导语,框定主题,1-2 句>
- Summary:<是否需要;放结论先行 / TL;DR>
### Sections
1. <编号 NN> <标题>
- 保留信息:<从 source.md 哪几段,要保留到什么程度>
- 需要的组件:<Section 正文 + 是否 Aside/Quote/Table/CodeBlock/Formula/Image>
- 是否需要 Raw:<是/否;若是,服务哪个论点、表达目的>
2. ...
- 结尾方式:<Conclusion / 行动项 / 留白收束>
## Theme
- 选定主题:<tufte / press / ...>
- 理由:<为什么是它,结合源材料类型 / 语气 / 配图策略>
- 与源材料的冲突:<若有,如何处理;若无,写"无">
- 当前信息密度下的表现建议:<正文 / Raw / 图片比例如何调整;可引用 theme-profiles/<id>.md>
## Assets
> 这一段配合"Brief / 配图策略"使用。`none` 模式下写一句话即可。
- 策略:<none / user-assets / placeholders / ai-generated>
- 一句话说明:<为什么是这个策略;Raw 始终存在、不在本段讨论>
### 逐图计划(仅 user-assets / placeholders / ai-generated 模式需要)
每张图列:
- 位置:<Hero 背景 / Section 02 之后 / ...>
- 服务的段落或论点:<...>
- 目的:<建立气质 / 解释机制 / 提供证据 / ...>
- 主题:<选定主题>
- 风格 / 构图:<...>
- 禁止项:<3D icon / neon gradient / SaaS stock photo / 笑脸办公室人 / ...>
- 来源:<user-assets 文件路径 / placeholders 描述 / ai-generated 提示词>
- 备选提示词(ai-generated 模式):<...>
```
---
## 模板使用要点
- **Outline 是章节锚点**:Section subagent(开发模式 B 下)会读这一段找到自己负责的章节。
每节用 `<编号 NN> <标题>` 起头,编号要和最终 `article/sections/NN-*.tsx` 一致。
- **必须保留 / 可删减**列出来不是装饰,**Section Reviewer 会核对**信息保留比例是否兑现。
- **Theme 段不必长**:通常 3-5 句话足够。冲突的处理才是这段的真正价值。
- **Assets 段在 `none` 模式下极短**:一句话说明"不使用外部图片,靠正文 + Raw + 表格表达"
即可,不需要逐图计划。
## 与其它 reference 的关系
- 文章类型选择:见 `article-types.md` → `article-types/<type>.md`。
- 信息保留比例:见 `information-density.md`。
- 主题选择:见 `theme-selection.md`,结论落到本模板的 **Theme** 段。
- 版式宽度 / TOC:见 `layout.md`,结论落到本模板的 **Brief** 段。
- 配图四种来源 / ai-generated 提示词原则:见 `asset-policy.md`,结论落到 **Assets** 段。
- 封面(书封式题图:屏幕 3:4 / PDF 独占首页):见 `cover.md`,结论落到 **Brief** 段的
"封面"一行;正式视觉在 Phase 4 First Spread 写 `article/Cover.tsx` 时定稿。
- 自查清单:见 `review-checklist.md` 的"Plan 自查(5 条)"。
@@ -0,0 +1,86 @@
# Raw 政策
Raw 是 Beautiful Article 的关键表现力,但必须受**主题**和**文章性**约束。写 Raw 前先读
选定主题的 `theme-profiles/<id>.md` 的 Raw 风格。
## Raw 是完整的 Web 平台,不是"画 SVG"
**Raw 里可以写任意 HTML / CSS / JS / React 组件 —— 整个 Web 平台都在你手里。** SVG 只是
**其中一种**手段,绝不是默认或唯一。别把 Raw 想成"内联画图",那会严重限制网页的想象力。
按"哪种媒介最能讲清这一段"自由选择,例如:
- **交互**:拖动条 / 切换 / 折叠 / 步进器 / 计算器 / 小型可调模型 / 假设演算。
- **布局排版**:并排对比、时间线、卡片网格、分栏、引文大字、特殊标题节奏(用 HTML + CSS)。
- **动效**:CSS transition / `@keyframes` / 滚动揭示 / 状态切换的动画。
- **数据可视**:HTML/CSS 条形与热度、`<canvas>`、需要时才用 `<svg>` 折线 / slopegraph。
- **嵌入与组合**:表格 + 控件 + 文本拼成的一次性小工具、可复制片段、对照面板。
判断只问一句:**哪种实现最能服务这一段的理解 / 论证 / 节奏?** 用那个,而不是反射性地画 SVG。
## 核心心法
- **为 THIS 篇文章手写,不是 widget 库。** 每块 Raw 都应为它旁边的段落即时发明:写 token
成本?现写一个小拖动条;写两套方案?拼一个并排对照面板;写体积趋势?才考虑一条内联折线。
**绝不**把 Raw 做成一套固定小组件在文章间复用(同一条 pipeline / 同一套配色到处出现)——
那等于把自由层退化成又一组受限组件。
- **自由但一致:用 token。** Raw 内部随便写 —— 任意 HTML / React 组件、`<style>` 与
`@keyframes`、行内样式、`<canvas>`、按需的 `<svg>`、一次性小交互 —— 但颜色 / 字体 / 间距
必须取自主题变量(`var(--ra-color-accent)`、`var(--ra-font-body)`、`var(--ra-space-4)` …),
这样每块都独一无二却又随主题切换。
## 允许
- 任意服务段落的 HTML / CSS / JS / React:轻量交互解释、可调小工具、自定义布局与排版、
并排对比、概念动效、阅读节奏中的视觉停顿,以及按需的 SVG / canvas 图解 —— 都是
为当前段落定制的一次性实现。
## 禁止
- 复杂表单、拖拽工作台、完整 dashboard、产品原型、和文章无关的动画、独立于主题的配色、
复用固定小组件冒充自由表达。
## Raw by example(每次现写,别复用固定 widget)
> 下面只是几种常见手段(SVG / CSS 动画 / React 交互),**不是穷举也不是优先级**。布局、
> 排版、对照面板、`<canvas>`、嵌入式小工具同样都行 —— 按"最能讲清这一段"来选。
```tsx
// 1) 需要曲线时,才为这个数据点手画一条内联 SVG(不是默认手段)
<Raw title="构建体积走势">
<svg viewBox="0 0 300 80" width="100%">
<polyline points={pts} fill="none" stroke="var(--ra-color-accent)" strokeWidth="2" />
</svg>
</Raw>
// 2) 带一次性 CSS / @keyframes 的 HTML 字符串(内联进产物)
<Raw html={`
<style>@keyframes ra-rise{from{height:0}to{height:var(--h)}}</style>
<div style="display:flex;gap:8px;align-items:flex-end;height:80px">
<i style="--h:60%;flex:1;background:var(--ra-color-accent);animation:ra-rise .6s ease"></i>
<i style="--h:90%;flex:1;background:var(--ra-color-accent);animation:ra-rise .8s ease"></i>
</div>
`} />
// 3) 只为这篇文章定义的一个小交互组件
function TokenScale() {
const [n, setN] = useState(50);
return (
<div>
<input type="range" value={n} onChange={(e) => setN(+e.target.value)} />
<span style={{ color: "var(--ra-color-accent)" }}>{n}%</span>
</div>
);
}
<Raw title="拖动感受差距"><TokenScale /></Raw>
```
整篇文章里变化这些 —— 不同媒介、不同布局、不同交互 —— 让没有两块 Raw 看起来一样。变化是好的,违反
主题气质不行:`tufte` 的 Raw 不该变成发亮营销 dashboard,`press` 的 Raw 不该变成冷霓虹终端
(除非主题 md 明确允许)。
## Raw 自检
- 这块 Raw 删掉后,文章理解是否会变差?
- 它服务哪一个段落 / 论点?
- 它是否使用 `--ra-*` token?是否符合主题 md?
- 它是否让文章更像应用?(如果是,砍掉或收敛成服务阅读的解释性视觉 / 排版)
@@ -0,0 +1,35 @@
# 修复政策(最小切片)
按最小单位修复。**有修复才写** `review/repair-log.md`(一次过 / 无修复则不写)。
## 禁止
- 用户只反馈一处问题就**重写整篇**。
- 为了修视觉而改动**已确认的文章结构**。
- 为了压缩信息而删除用户**指定必须保留**的内容。
## 最小切片对照
| 问题 | 最小修复单位 |
|---|---|
| 信息缺失 | 对应 Section / Table / CodeBlock |
| 信息太密 | 对应 Section 的段落和局部 Raw |
| 主题不对 | `plan/plan.md` 的 Theme 段 + 局部 token / Raw |
| 图片不对 | 对应图片和 `plan/plan.md` 的 Assets 段 |
| 首屏不对 | Hero / Lead / Summary |
| Raw 跑偏 | 单个 Raw block |
| 移动端问题 | 对应 CSS / 组件布局 |
| 构建错误 | 具体文件和行 |
## repair-log.md 格式
```markdown
## <日期> <谁反馈 / 哪个 Reviewer>
- 问题:<一句话>
- 定位层:<节奏 / 视觉 / 内容 / 构建>
- 最小修复单位:<Section 03 / raw-blocks/02 / main.tsx 主题 ...>
- 改动:<改了什么>
- 验证:<dev 预览 / npm run html 通过 / 控制台无错>
```
先定位是哪一层(内容 / 结构 / 视觉 / 构建),再改最小切片,**不要重做整篇**。
@@ -0,0 +1,133 @@
# 评审清单与 Reviewer
> 本文件的核心目的:**让每个节点用对的方式做对的事**,不要错开 SubAgent、不要错写文件。
> 误开 SubAgent / 错写 review 文件是首要性能问题。完整规则见 SKILL.md「硬性质检协议」段。
## 各阶段质检方式(铁律)
| 阶段 | 质检方式 | 产物 |
|---|---|---|
| **Phase 1 Source(默认)** | 主 Agent 内联 5 条 checklist(见 `source-to-markdown.md`) | 无文件 |
| Phase 1 Source(仅复杂/低置信源) | Source Reviewer SubAgent(对照 `original.*` diff) | `review/source-review.md` |
| **Phase 2 Plan / Checkpoint 1 前** | **主 Agent 内联自查(禁开 SubAgent)** | **无文件** |
| **Phase 4 First Spread / Checkpoint 2 前** | First Spread Reviewer SubAgent | `review/first-spread-review.md` |
| **Phase 5 每个 Section** | Section Reviewer SubAgent | **以消息返回 pass/fail + 修复点,不写文件** |
| **Phase 6 终审 / Checkpoint 3 前** | Editorial + Visual + Technical Reviewer SubAgent | `review/final-review.md` |
拿到结论后**先按 fail 项把产出改完,再向用户汇报**。直接拿结论汇报但不修复 = 违规。
---
## Plan 自查(Phase 2 → Checkpoint 1 · 主 Agent 内联 · 5 条)
写完 `plan/plan.md` 后**就地**核查这 5 条,按结论改 `plan/plan.md` 本身(不要写新文件):
1. **Brief / Outline 自洽**:信息保留比例与每节"保留信息"加起来对得上;Outline 没有偷偷
塞 Brief 没承诺的内容,也没有遗漏 Brief 必须保留的内容。
2. **信息取舍有据**:每个"可删减"项都给出了理由(重复 / 旁枝 / 过时);每个"必须保留"项
都指向 source.md 的具体段落 / 表格 / 代码。
3. **没有过度组件化**:每节的"需要组件"不是把所有内容都框成 Aside / Quote / Table;正文仍
是主体(prose-first,见 `component-policy.md`)。
4. **Raw / 图片有目的**:Outline 里每个标注"需要 Raw"或"需要 Image"的位置,都能用一句话
说出它服务的论点 / 表达目的,不是装饰。
5. **章节序号合理**:Outline 章节连续单调(01 / 02 / 03 …),子节序号前缀对齐父章节(第 08
章下只能是 8.1 / 8.2,不要出现 5.1)。
> 这一步**绝不开 SubAgent**:内容量小、上下文是热的,SubAgent 冷启动反而慢。
---
## First Spread 自检清单(Phase 4 → Checkpoint 2 前 · SubAgent)
SubAgent 读 `article/Article.tsx`、`article/sections/01-*.tsx`、`article/Cover.tsx`(若有)、
`plan/plan.md`、选定主题 `theme-profiles/<id>.md`,按清单核查,写 `review/first-spread-review.md`:
- **封面**(若开 · 见 `cover.md` 5 条自检):图文并茂、主题忠实(只用 `--ra-*` token)、
内容忠实(封面视觉跟正文主旨对得上)、比例自适应(屏幕 3:4 + PDF 独占首页都不错位)、
不与 Hero 重复内容?
- 首屏像文章,不像 landing page?读者是否立刻知道文章要解决什么?
- 第一节有阅读节奏?Raw 服务理解?图片服务表达?移动端能读?
- 主题气质对不对?版式宽度合适吗?
- 代码可构建(`npm run dev` 无报错),浏览器控制台无红字?
prompt 模板:
```text
请作为 First Spread Reviewer。读取 article/Cover.tsx(若有)、article/Article.tsx、
article/sections/01-*.tsx、plan/plan.md、theme-profiles/<id>.md、references/cover.md
(若有封面),对照 First Spread 自检清单逐项核查。把结论写进
review/first-spread-review.md(pass / fail + 证据 + 必须修复项 + 改写建议)。
不要替我改文件,也不要泛泛夸奖。
```
主 Agent 收到结论后**先按 fail 项改完**,再进 Checkpoint 2。
---
## Section 自检清单(Phase 5 每个 Section · SubAgent · 消息返回)
SubAgent 读对应 `sections/<NN>-*.tsx`、`plan/plan.md` 的本节段落、`source/source.md` 本节
对应内容,按清单核查,**以消息形式返回结论**:
- 完成本节 outline 任务?
- 符合 Brief 的信息保留比例?必须保留的信息没丢?
- 与前后节衔接?没有重复或矛盾?
- 没有过度组件化?正文充足?
- Raw 与配图有明确目的?
- **本节序号自洽**:`Section index` 等于主 Agent 指定的 `<NN>`;每个 `Subsection` 序号
前缀等于 `<NN>`(如 `<NN>=08` 则小节是 8.1 / 8.2,**不要**写成 5.1)。
prompt 模板:
```text
请作为 Section Reviewer。读取 article/sections/<NN>-*.tsx、plan/plan.md 本节段落、
source/source.md 本节对应内容、theme-profiles/<id>.md。
对照 Section 自检清单逐项核查,**直接以消息形式返回**:
- 第一行:pass / fail
- 若 fail:列出修复点(带行号 / 代码片段证据)
不要写任何 review 文件。不要替我改文件。不要泛泛夸奖。
```
主 Agent 收到 fail 项后**直接修对应 section 文件**,再汇报本节交付。
---
## Phase 6 终审三视角(SubAgent · 写 `review/final-review.md`)
**Editorial Reviewer**(文章性、信息取舍、结构)
- 它仍然是一篇文章,不是网页应用。
- 信息保留比例符合 Brief;必须保留的信息没有丢。
- 语言符合 Brief:全文统一为目标语言,地道、无翻译腔、无残留源语言片段(图注 / 引用 / 术语也算)。
- 没有空泛标题、堆卡片、过度总结。
**Visual Reviewer**(主题、Raw、图片、移动端)
- 主题气质统一;Raw 没有野生样式(都用 `--ra-*` token)。
- 图片符合主题和上下文,不抢正文,不与 Raw 重复。
- 没有明显 AI 味:装饰性视觉、紫粉渐变、圆角彩卡、假插画、emoji 装饰。
- 桌面和移动端都可读,无文字溢出 / 遮挡 / 空白异常。
**Technical Reviewer**(构建、控制台、代码 / 公式、可访问性、序号)
- `npm run html` 可构建,`article/article.html` 可打开、可分享。
- 浏览器控制台无报错;代码 / 公式高亮和主题一致。
- 图片有 alt,链接可用,标题层级合理。
- **章节序号全篇自洽**(`index` 是手写字符串,组件不自动编号也不校验):
- `Section` 序号连续单调:`01 / 02 / 03 …`,无跳号、无重复、无错序。
- 每个 `Subsection` 序号前缀等于其父 `Section` 编号:第 08 章下是 `8.1 / 8.2`,**绝不**出现 `5.1`。
- 序号与左侧 TOC、`plan/plan.md` Outline 章节顺序三者一致。
- 逐个 `Section` / `Subsection` 把渲染出来(或 TOC 里)的序号抄下来比对,**不要只看代码顺序** ——
并行模式(开发模式 B)下 subagent 看不到自己在全篇的位置,最容易在这里写错。
prompt 模板:
```text
请作为 <Editorial / Visual / Technical> Reviewer。读取 plan/plan.md、source/source.md、
article/Article.tsx 和所有 article/sections/*.tsx、theme-profiles/<id>.md。
对照本视角的终审清单逐项核查,把结论追加到 review/final-review.md 的
"<视角>"段(pass / fail + 证据 + 必须修复项 + 改写建议)。
不要替我改文件,不要泛泛夸奖。
```
三个视角可并行起 SubAgent,主 Agent 收齐后按 fail 项最小切片修复(见 `repair-policy.md`)。
@@ -0,0 +1,77 @@
# 脚手架
脚手架在 Phase 4 创建文章工作区,**不把工程代码塞进 SKILL.md**。工程模板是 Skill
assets(`assets/scaffold-template/`),由 `scripts/scaffold.sh` 复制并接线。
## 用法
```bash
bash <path-to-beautiful-article>/scripts/scaffold.sh ./my-article --theme=tufte
bash <path-to-beautiful-article>/scripts/scaffold.sh ./brief --theme=press --no-cover
bash <path-to-beautiful-article>/scripts/scaffold.sh --list-themes
```
`--theme` 必须是 `theme-profiles/index.json` 里的 id(当前 `tufte` / `press` / …)。工作区可建在
**任意目录**,不需要在 reacticle 仓库内。
`--no-cover` 禁用书封式文章封面(默认开 · 屏幕 3:4 / PDF 独占首页)。Checkpoint 1 用户
选了"封面 · 关"时传这个。详见 `references/cover.md`。
## 脚手架做什么
- 创建工作区目录 + 复制 Vite / React / TS 模板。
- **从 npm 安装最新发布版的 `reacticle`**:`package.json` 里 `reacticle: "latest"`,脚手架
装完依赖后再 `npm install reacticle@latest` 强制刷新到当下最新,并打印实际版本。
- 在 `article/main.tsx` 写入所选 runtime theme id,并 `import "reacticle/styles.css"`。
- 创建默认 `article/Article.tsx` + `article/sections/`、`article/raw-blocks/`、
`article/assets/`。`Article.tsx` 末尾自带 **colophon Raw 块**(`Made with
[beautiful-article](github 仓库) · <主题> theme`),样式低对比小字、走 `--ra-*` token,
**不可删除**(见 SKILL.md「默认策略」)。
- 默认创建 **`article/Cover.tsx`**(书封式封面外壳 + 占位:屏幕 3:4 / PDF 独占首页)并在
`main.tsx` 里渲染 `<Cover />` 在 `<ArticleDoc />` 之上。`--no-cover` 时跳过这一步:
不复制 Cover.tsx,并从 `main.tsx` 剥掉 `__COVER_IMPORT_*__` / `__COVER_RENDER_*__`
标记包裹的两段。封面设计见 `references/cover.md`。
- 创建工作记忆目录 `source/`、`plan/`、`review/`。
- `katex` / `prismjs` 作为 `reacticle` 的依赖会被自动带下来,无需在工作区单独声明。
## 升级组件库
工作区随时可升级到最新组件库:
```bash
npm install reacticle@latest
```
## 工作区结构
```text
my-article/
package.json vite.config.ts tsconfig.json tsconfig.node.json index.html
source/ plan/ review/
article/
main.tsx # 入口:<ThemeProvider theme="..."> + <Cover/> + <ArticleDoc/>
Cover.tsx # 书封式文章封面:屏幕 3:4 / PDF 独占首页(默认;--no-cover 时不生成)
Article.tsx # assembler(主 Agent 拥有):import + 排序各 Section,不写 Section 正文
sections/ # 一节一文件(铁律):NN-*.tsx,每个导出一个 Section 组件
01-opening.tsx
raw-blocks/ # 大型 Raw 隔离:NN-*.tsx
assets/ # 配图素材
.theme # 记录起步主题
```
> **一个 Section = 一个文件**(`sections/NN-*.tsx`),坚决不允许把多个 Section 写进
> `Article.tsx`。`Article.tsx` 只做组装。这是多 Agent 并行(开发模式由 Checkpoint 2 选定)
> 的前提,详见 `references/section-build.md`。
## 切主题
改两处(脚手架默认会把主题名注入到这两个位置):
1. `article/main.tsx` 里 `<ThemeProvider theme="...">`(控制运行时主题)。
2. `article/Article.tsx` 末尾的 colophon `· <主题> theme`(控制印记里显示的主题名)。
两处保持一致。详见 `html-output.md`。
## 构建 / 预览
见 `references/html-output.md`(`npm run dev` / `build` / `html`)。
@@ -0,0 +1,100 @@
# Section 构建与多 Agent 并行
## 铁律:一个 Section = 一个组件文件
每个 Section **必须**是独立组件文件,**坚决不允许**把多个 Section 直接写进一个组件。
```text
article/
Article.tsx # assembler(主 Agent 拥有):import + 排序各 Section
sections/
01-opening.tsx # export function SectionOpening() { return <Section .../> }
02-context.tsx
03-mechanism.tsx
raw-blocks/
01-token-flow.tsx # 大型 / 复用的 Raw 隔离到这里,被对应 section import
```
- 每个 `sections/NN-*.tsx` 导出一个组件,内部用 `<Section index="NN" title="...">…</Section>`。
- `Article.tsx` 只做组装:
```tsx
import { Article, Hero, Lead, Conclusion } from "reacticle";
import { SectionOpening } from "./sections/01-opening";
import { SectionContext } from "./sections/02-context";
export function ArticleDoc() {
return (
<Article toc width="regular">
<Hero ... /><Lead>…</Lead>
<SectionOpening />
<SectionContext />
<Conclusion>…</Conclusion>
</Article>
);
}
```
- 文件级隔离 + reacticle 无裸 CSS(样式全走主题 token)→ 多个 Agent 改不同 section 文件
**不会互相破坏**。
## 章节序号由主 Agent 统一编(避免序号错乱)
`Section` / `Subsection` 的 `index` 是**手写字符串**,组件既不自动编号、也不校验——它只会原样显示。
所以一旦哪个 section 文件写错了序号(典型:并行模式里 subagent 看不到自己在全篇的位置,凭空写出
"第 8 章下挂一个 5.1"),就会一路漏到成品里。规则:
- **全局序号归主 Agent(assembler)所有。** 在 `Article.tsx` 排定最终顺序后,主 Agent 据此把每个
`Section` 的 `index` 校准成 `01 / 02 / 03 …`,并把每个 `Subsection` 的序号前缀对齐到所属
`Section`(第 08 章下只能是 `8.1 / 8.2 …`)。
- **subagent 不自编全局序号。** 主 Agent 在派活时直接告诉它"你是第 `<NN>` 章",subagent 用这个
`<NN>` 写 `Section index` 和本节 `Subsection` 的前缀;拿不准就留 outline 给的占位,最终由主
Agent 统一过一遍。
- **组装后必校验**:对照 TOC 显示与 `outline.md` 顺序,确认序号连续单调、子节前缀正确(并入终审
Technical Reviewer 的"章节序号全篇自洽"清单)。
## 两种开发模式(Checkpoint 2 由用户选定)
第一个 Section 无论哪种模式都先由**主 Agent 完成并验收**(风格锚点)。差异在第 2 个 Section 起:
### A · 单 Agent 顺序(默认,最稳)
主 Agent 顺序写 `02 → 03 → …`,风格最统一,随时验收。
### B · 多 Agent 并行(最快)
subagent 各**拥有一个** `sections/NN-*.tsx` 并行开发。**主 Agent 负责合并与稳定性**:
- 维护 `Article.tsx` 的 import 与 Section 顺序(唯一组装点,避免冲突),并据最终顺序**统一校准每个
Section 的 `index` 与各 Subsection 的序号前缀**(见上节"章节序号由主 Agent 统一编")。
- 每轮并行结束跑 `npm run typecheck` + `npm run build`,修构建错误。
- 兜底主题与风格一致(颜色 / 字体 / 间距走 token,气质不跑偏)。
- 解决重复 / 衔接问题(相邻 section 论点是否承接)。
风格在并行下会有轻微差异(这是预期,主题 token 兜底视觉统一)。
### 并行 subagent 的 prompt 必须包含
```text
你负责文件 article/sections/<NN>-<id>.tsx,只改这一个文件,导出一个 Section 组件。
你是全篇第 <NN> 章(这个编号由主 Agent 指定,你看不到自己在全篇的位置,不要自己另编)。
读取:plan/plan.md 的 Outline 段本节段落 + Brief 段(信息保留比例)+
source/source.md 本节对应内容 + 选定主题 theme-profiles/<id>.md +
references/component-policy.md + references/raw-policy.md +
第一个 section 文件作为“代码风格”参考(不是抄袭对象)。
硬规则:
- 一个文件 = 一个 Section 组件;不要碰 Article.tsx 或别的 section 文件。
- 正文为主体,组件按需(默认核心组件优先),Raw 用 --ra-* token 现写。
- 符合本节 outline 任务与信息保留比例;与前后节衔接。
- 序号:<Section index="<NN>">;本节所有 <Subsection> 的序号前缀必须等于 <NN>
(如 <NN>=08 则小节是 8.1 / 8.2 …),不要从别的章节复制序号。
- 完工自检对照 references/review-checklist.md 的 Section 清单。
不要修改 Article.tsx(主 Agent 统一组装与序号校准),不要改主题。
```
## 每个 Section 完工(必走质检 · SubAgent · 消息返回)
按硬性质检协议创建 **Section Reviewer** SubAgent,对照清单核查:完成 outline 任务 / 符合
信息保留比例 / 与前后衔接 / 不过度组件化 / 正文充足 / Raw 与配图有明确目的 / **本节序号
自洽**(`Section index` 等于 `<NN>`,各 `Subsection` 序号前缀等于 `<NN>`)。
**SubAgent 以消息形式返回 pass/fail + 修复点**(pass 一行 OK,fail 列出修复点),**不要写
`review/section-NN-review.md` 文件**。主 Agent 收到 fail 项后直接修对应 section 文件,再
汇报本节交付。完整 prompt 模板见 `references/review-checklist.md` 的 Section 段。
@@ -0,0 +1,118 @@
# Source → Markdown
无论输入是什么,Phase 1 都先转成统一的 `source/source.md`,并把风险写进
`source/extraction-notes.md`。
## 产物
`source.md` 要包含:标题 · 来源 · 作者 / 时间 / 链接(若可得)· 正文 · 表格 ·
图片占位 · 代码块 · 引用 · 附录 / 脚注。
`extraction-notes.md` 要记录:输入类型 · 提取方式 · 可能丢失的信息 · PDF/DOCX 中
无法可靠还原的版式 · 图片 / 表格 / 脚注 / 代码是否完整 · 需要用户补充的素材或上下文 ·
**源语言、是否需要翻译、目标语言、翻译版文件名与翻译说明**。
## 语言与翻译
抽取后判断 `source.md` 的语言,并按 Phase 0 记录的目标语言决定:
- 未指定目标语言,或目标语言与源一致 → **不翻译**,最终文章语言 = 源语言。
- 指定了目标语言且与源不一致 → 产出 `source/source.<lang>.md`(如 `source.zh.md` / `source.en.md`),
作为 Phase 2+ 的**事实底座**;原 `source.md` 保留备查。
- 翻译要求:**地道、去翻译腔** —— 按目标语言的表达习惯重组句子,不逐字直译,不留生硬外语语序 /
被动堆叠 / 异国标点;术语 / 数字 / 代码 / 公式 / 引用保持准确;标题层级、结构、信息保留比例不变。
- 翻译只在源文层做一次;后续编辑、改写语气、组件化都基于翻译版进行。
## 不同输入的处理原则
| 输入 | 处理方式 | 自检重点 |
|---|---|---|
| URL | 抓网页正文,清理导航 / 广告 / 推荐 | 是否抓到主体,链接和图片是否保留 |
| PDF | 提取文本 / 章节 / 表格 / 图片占位 | 断行错乱、页眉页脚混入、表格丢失 |
| DOCX | 提取标题层级 / 段落 / 表格 / 图片占位 | 样式不重要,结构和内容完整性重要 |
| Markdown | 保持原有标题 / 代码块 / 表格 | 不要过度改写源文 |
| 纯文本 | 识别结构并标注不确定处 | 不要擅自编造层级 |
| 截图 / 图片 | 转成图片占位 + 说明,记录用途 | 记录到 extraction-notes,等用户确认 |
## 抽取脚本选择
Skill 提供两条抽取路径:MarkItDown 主路径 + 轻量 fallback。Agent 应在 Phase 1 先判断
输入类型、信息保留要求和本机环境,再选择脚本。
### 1. MarkItDown 主路径
对 PDF / DOCX / PPTX / HTML / 复杂文档,尤其是用户要求 80-100% 信息保留时,优先使用
`scripts/source-to-markdown-markitdown.py`:
```bash
python3.10 <path-to-beautiful-article>/scripts/source-to-markdown-markitdown.py <input> -o source/source.md
```
MarkItDown 需要 Python 3.10+。它是可选增强依赖,不随组件库或脚手架强制安装。
若未安装,按需提示用户安装:
```bash
python3.10 -m pip install "markitdown[pdf,docx]"
```
如果本机没有 `python3.10` 命令但有 `uv`,可临时拉起带 MarkItDown 的环境:
```bash
uv run --python 3.12 --with "markitdown[pdf,docx]" python \
<path-to-beautiful-article>/scripts/source-to-markdown-markitdown.py <input> -o source/source.md
```
不要默认安装 `markitdown[all]`,除非用户明确需要 PPTX / XLSX / 音频 / YouTube / Azure 等
额外格式。全量安装更重,也更容易引入环境问题。
### 2. 轻量 fallback
如果 MarkItDown 不可用、Python 版本不足、转换失败,或输入只是 Markdown / TXT / 简单 HTML,
使用 `scripts/source-to-markdown.py`:
```bash
python3 <path-to-beautiful-article>/scripts/source-to-markdown.py <input> -o source/source.md
```
脚本会探测可用的解析库(pdfminer / pdfplumber / python-docx / BeautifulSoup),缺库时
打印安装建议并优雅降级。脚本只做**机械抽取**;正文清理、占位标注、风险记录仍由 agent 完成。
URL 也可直接用 agent 的网页抓取能力获取正文,再清理。
### 3. Agent 决策规则
- PDF / DOCX / PPTX / 复杂 HTML:先尝试 MarkItDown。
- Markdown / TXT:直接用 fallback,保持原文结构,不要过度处理。
- URL:可先用 Agent 的网页抓取能力获取正文;若已保存为 HTML 文件,再按复杂度选择
MarkItDown 或 fallback。
- 100% 信息保留:抽取后必须更严格自检,必要时同时跑 MarkItDown 与 fallback,对比是否
有表格、代码、脚注、图片占位遗漏。
- 任一脚本产物都只是 `source.md` 草稿;Agent 必须继续清理噪音、补图片占位,并写
`source/extraction-notes.md`。
## Source Phase 自检(主 Agent 内联 · 5 条 checklist)
源材料质检**不是硬性 SubAgent 质检点**。主 Agent 进 Phase 2 前反正要通读 `source.md`,
就地按这 5 条核查、按结论修复即可(无需单独的 review 文件):
1. **完整性**:是否被截断?体量是否和原文相称(长 PDF / 长文没有只抽到一半 / 只抓到首屏)?
2. **结构**:标题层级是否保留?还是被压平成一片正文(后面没法分章)?
3. **关键载体**:表格 / 代码 / 公式 / 引用 / 脚注是否保留且没损坏(表格没挤成一行、代码缩进还在、
公式没变乱字符)?
4. **噪声**:是否误把导航 / 广告 / Cookie 横幅 / 推荐阅读 / 页眉页脚页码写进正文?编码有没有伤
(乱码、连字 `fi`、软连字符断词、两栏 PDF 阅读顺序错乱)?
5. **不确定项**:图片是否以占位标注?拿不准的都写进 `extraction-notes.md` 了吗?
> ⚠️ 上面 1/4 能孤读 markdown 抓到,但 **2/3 里"静默丢失"的表/段**(被悄悄删掉、吞掉)
> 光看 markdown 看不出来——必须对照 `original.*`。
## 升级为独立 Source Reviewer(仅限复杂/低置信源)
**只有**当 `extraction-notes.md` 标记了"低置信 / 复杂 PDF/DOCX / 转换吃不准 / 要求 100% 保留的关键源"
时,才升级为独立 SubAgent,并**强制对照原件做 diff 式核查**,写 `review/source-review.md`:
```text
请作为 Source Reviewer。同时读取 source/original.*(原件)和 source/source.md(转换产物)。
逐项做 diff 式核查:对照原件,source.md 是否漏掉了表格 / 段落 / 脚注 / 代码 / 图,
是否混入噪音,是否有结构塌陷或编码损坏。
只输出"按出现顺序的差异清单 + 必须修复项",不评价文章好不好看,不要替我改文件。
```
@@ -0,0 +1,48 @@
# 主题选择
主题负责**审美气质、排版语言、图片风格、Raw 风格、代码 / 公式风格**。它不是 CSS 皮肤,
也不是信息密度规则。
> CSS 给浏览器读,theme profile 给 AI 读。
- **组件库拥有运行时主题**:CSS token、`ThemeProvider` 注册、实际渲染。
注册的 runtime theme id 见 `src/theme/ThemeProvider.tsx`(当前:`tufte`、`press`)。
- **Skill 拥有主题 authoring profile**:`theme-profiles/index.json` + `<id>.md`,
指导 AI 如何选择和使用主题。
## 选择流程
1. 读 `theme-profiles/index.json`,拿每个主题的 `bestFor` / `mood`。
2. 按 `source.md` 的内容类型 / 语气,从 `bestFor` 命中里挑 1-2 个推荐:
- 技术 / 证据 / 数据型 → `tufte`。
- 叙事 / 评论 / 出版 / 产品手记 → `press`。
3. 读选定主题的 `theme-profiles/<id>.md`(写作 / 配图 / Raw / 代码前的权威)。
4. 把选择 + 理由写进 `plan/plan.md` 的 **Theme** 段(见 `plan-template.md`)。
## Density 与 Theme 解耦
theme profile 里可以写"不同信息密度下的表现建议",但**不能写成限制**。例:
- `tufte + 100% longform`:克制长文、数据证据、Raw 点亮关键概念。
- `tufte + 40% visual-essay`:仍成立,Raw 偏图解 / 证据、低装饰。
- `press + 100% longform`:可做深度出版文章。
- `press + 40% briefing`:更强编辑节奏与图文留白。
## 主题选择自检
- 主题是否匹配文章类型?
- 当前信息密度下,正文 / Raw / 图片比例该如何调整?
- Raw 是否能在该主题下自然发生?配图策略是否符合主题?
- 是否存在主题与源材料冲突?(冲突要在 `plan/plan.md` 的 Theme 段解释)
- runtime theme id 是否真实存在于组件库?Skill profile 是否存在?
## 新增主题的约束
新增主题必须**同时**满足:
1. 组件库有 runtime theme CSS 与 `ThemeProvider` 注册(`src/theme/themes/<id>/`)。
2. Skill 有对应 `theme-profiles/<id>.md`。
3. `theme-profiles/index.json` 绑定到正确 runtime theme id。
- 只有 Skill profile、没有组件库 runtime theme → 只能作候选,不能用于正式生成。
- 只有组件库 runtime theme、没有 Skill profile → Agent 不能主动推荐。
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# html-to-pdf.sh —— 把 Beautiful Article 的单页 HTML 转成 PDF
#
# 用法:
# bash <skill>/scripts/html-to-pdf.sh [input.html] [output.pdf]
# bash <skill>/scripts/html-to-pdf.sh # 默认 article/article.html → article/article.pdf
# bash <skill>/scripts/html-to-pdf.sh --help
#
# 前提:本机已装 Chromium / Google Chrome / Brave / Microsoft Edge 之一
# (脚本会自动探测)。无需 npm 包、无需 Node。
#
# 设计要点(详见 references/pdf-output.md):
# 1. reacticle 默认 TOC 在桌面是左右栅格;PDF 需要 TOC 上、正文下的上下排布。
# 2. 脚本在 HTML 头部注入一段 @media print CSS 覆盖:把 TOC 塌成一列、解除
# sticky、长 TOC 双列省纸、TOC 后强制分页、隐藏 colophon 之外的页眉页脚等。
# 3. 用 headless 浏览器 --print-to-pdf 渲染(执行页面 JS,Raw 交互渲染为初始
# 态);输出标准 A4 / 主题纸色满版背景 + 0.45in 内容留白,无浏览器自带页眉页脚。
# 4. 失败回退:打印"用浏览器手动 Cmd+P → 另存为 PDF"指引,并把注入后的 HTML
# 留在临时目录方便用户自己打印。
# ─────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CSS_FILE="$SCRIPT_DIR/pdf-print-overrides.css"
INPUT="${1:-article/article.html}"
OUTPUT="${2:-article/article.pdf}"
if [[ "$INPUT" == "--help" || "$INPUT" == "-h" ]]; then
sed -n '2,21p' "$0"
exit 0
fi
if [[ ! -f "$INPUT" ]]; then
echo "✗ 输入文件不存在:$INPUT" >&2
echo " 先在工作区跑 npm run html 产出 article/article.html。" >&2
exit 1
fi
mkdir -p "$(dirname "$OUTPUT")"
# ── 探测可用的 chromium-family 浏览器 ─────────────────────
find_browser() {
local candidates=(
chromium
chromium-browser
google-chrome
google-chrome-stable
chrome
brave-browser
microsoft-edge
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
"/Applications/Chromium.app/Contents/MacOS/Chromium"
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
"/Applications/Arc.app/Contents/MacOS/Arc"
"/usr/bin/chromium"
"/usr/bin/google-chrome"
"/snap/bin/chromium"
)
for c in "${candidates[@]}"; do
if command -v "$c" >/dev/null 2>&1; then
echo "$c"; return 0
fi
if [[ -x "$c" ]]; then
echo "$c"; return 0
fi
done
return 1
}
# ── 注入 @media print 覆盖到一个临时 HTML ─────────────────
# 设计:CSS 抽到 scripts/pdf-print-overrides.css(详见该文件顶部注释),这里只
# 负责"把它的内容包在 <style> 里、塞到 </head> 之前"。这样:
# • macOS BSD awk 不接受 -v 传多行字符串("newline in string"),用 awk
# getline 从文件读则两边都吃得下。
# • CSS 文件可独立编辑 / lint / 复用,不被 shell 转义吃掉。
TMP_DIR="$(mktemp -d -t beautiful-article-pdf.XXXXXX)"
TMP_HTML="$TMP_DIR/article-print.html"
if [[ ! -f "$CSS_FILE" ]]; then
echo "✗ 找不到打印覆盖 CSS:$CSS_FILE" >&2
echo " 这个文件应跟脚本同目录(scripts/pdf-print-overrides.css)。" >&2
exit 2
fi
awk -v css_file="$CSS_FILE" '
/<\/head>/ && !done {
print "<style id=\"ra-pdf-overrides\">"
while ((getline line < css_file) > 0) print line
close(css_file)
print "</style>"
done = 1
}
{ print }
' "$INPUT" > "$TMP_HTML"
if ! grep -q 'ra-pdf-overrides' "$TMP_HTML"; then
echo "✗ 注入失败:未在输入 HTML 找到 </head>。" >&2
echo " 你的 article.html 可能不是 Vite + reacticle 单页产物。" >&2
exit 3
fi
# ── 找浏览器 ──────────────────────────────────────────────
BROWSER="$(find_browser || true)"
if [[ -z "$BROWSER" ]]; then
echo "⚠ 未找到任何 chromium-family 浏览器(chromium / google-chrome / brave / edge)。"
echo
echo " 回退方案:注入了打印 CSS 的 HTML 已经放在:"
echo " $TMP_HTML"
echo
echo " 请用浏览器打开它 → Cmd+P / Ctrl+P → 目标改为'另存为 PDF' → 保存。"
echo " 注入的 print 样式会让 TOC 在上、正文在下,与 PDF 阅读习惯对齐。"
exit 3
fi
echo "▸ 用浏览器:$BROWSER"
echo "▸ 输入:$INPUT"
echo "▸ 输出:$OUTPUT"
# ── 渲染 ──────────────────────────────────────────────────
# Chromium 系都支持 --headless --print-to-pdf。
# --no-pdf-header-footer:去掉浏览器自带的 URL / 日期 / 页码(colophon 已有)。
# --virtual-time-budget:给页面 JS 一点时间初始化 Raw 组件(5s 兜底)。
# --hide-scrollbars:避免在 PDF 里看到滚动条残影。
"$BROWSER" \
--headless=new \
--disable-gpu \
--no-sandbox \
--hide-scrollbars \
--no-pdf-header-footer \
--virtual-time-budget=5000 \
--print-to-pdf-no-header \
--print-to-pdf="$OUTPUT" \
"file://$TMP_HTML" 2>/dev/null || {
# 旧版 Chrome 不认 --headless=new,回退老 flag
"$BROWSER" \
--headless \
--disable-gpu \
--no-sandbox \
--hide-scrollbars \
--print-to-pdf-no-header \
--print-to-pdf="$OUTPUT" \
"file://$TMP_HTML" 2>/dev/null
}
# 清理临时(HTML 注入留作回退证据,最后再清)
rm -rf "$TMP_DIR"
if [[ -f "$OUTPUT" ]]; then
SIZE="$(du -h "$OUTPUT" | cut -f1)"
echo "✓ PDF 输出:${OUTPUT} (${SIZE})"
echo
echo " 如果 TOC / 分页不理想,看 references/pdf-output.md 故障排除段。"
else
echo "✗ 浏览器返回成功但输出文件不存在:$OUTPUT" >&2
exit 4
fi
@@ -0,0 +1,192 @@
/*
* PDF print overrides for Beautiful Article.
*
* Injected by scripts/html-to-pdf.sh into article.html's <head> right before
* Chromium headless prints it. reacticle's own print.css already handles
* hiding export bars and break-inside on cards.
*
* This file adds four groups on top of that:
* 0) Theme surface — keep the article theme background / text colors in PDF.
* A) TOC layout — force the sidebar TOC to stack above the article and
* page-break after it, so the article body starts on a fresh page.
* B) Page break behavior — undo reacticle's `.ra-section { break-inside:
* avoid-page }` (which causes huge empty pages for multi-page sections),
* glue headings to the following content, keep Hero / Lead / figures
* atomic, and apply widow/orphan control to paragraphs.
* C) Cover — if the article uses the 3:4 Cover component (default), keep
* its authored geometry intact and only force a page break after it.
* See references/cover.md.
*
* Why a separate file:
* - macOS BSD awk rejects multi-line strings via -v; reading from a file
* with getline sidesteps that.
* - CSS is independently editable / lintable / diff-friendly here.
* - Easy to swap or extend without touching the shell script.
*/
@media print {
@page {
margin: 0;
}
/* ============================================================
* 0 · Theme surface (preserve paper color in PDF)
* ============================================================ */
.ra-root {
background: var(--ra-color-bg, #ffffff) !important;
color: var(--ra-color-text, #111111) !important;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
position: relative;
z-index: 0;
box-sizing: border-box;
min-height: 100vh;
padding: 0.45in !important;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.ra-root::before {
content: "";
position: fixed;
inset: 0;
background: var(--ra-color-bg, #ffffff);
z-index: -1;
pointer-events: none;
}
/* ============================================================
* A · TOC layout (TOC above article, not beside it)
* ============================================================ */
/* A1) reacticle's TOC layout is a 2-col grid on desktop and `display: block`
* on mobile (<= 999px). Force the mobile branch for print so the TOC
* sits ABOVE the article column instead of beside it. */
.ra-article-layout--with-toc {
display: block !important;
max-width: none !important;
padding: 0 !important;
}
/* A2) TOC: kill sticky (only paints once on print), add breathing room, and
* push the article to its own pages by breaking after the TOC. */
.ra-toc {
position: static !important;
margin-bottom: 1.5rem !important;
page-break-after: always;
break-after: page;
}
/* A3) Long TOCs save paper as a 2-column layout; short ones collapse
* naturally back to one column. */
.ra-toc__list {
column-count: 2;
column-gap: 1.5rem;
column-fill: balance;
}
/* A4) Never split a TOC item across columns / pages. */
.ra-toc__item {
break-inside: avoid;
page-break-inside: avoid;
}
/* A5) Let the article column flow naturally on the page after the TOC. */
.ra-article-layout--with-toc > .ra-article {
break-before: auto;
}
/* A6) Strip underlines from TOC + body links in print (chrome already shows
* them as link-blue; the colophon footer keeps its underline because it
* sets its own text-decoration inline). */
.ra-toc a,
.ra-article a {
color: inherit;
text-decoration: none;
}
/* ============================================================
* B · Page break behavior (fixes huge empty pages in long sections)
* ============================================================ */
/* B1) reacticle's print.css aggressively sets `.ra-section { break-inside:
* avoid-page }`. For multi-page sections that rule backfires badly:
* the browser pushes the whole oversized section to the next page,
* leaving the previous page nearly empty, then the section overflows
* anyway. UNDO IT — let long sections break naturally across pages. */
.ra-section,
.ra-subsection,
.ra-section__body,
.ra-subsection__body {
break-inside: auto !important;
page-break-inside: auto !important;
}
/* B2) But never strand a heading at the bottom of a page. Glue Section /
* Subsection headings to whatever follows, and keep the heading row
* (index + title) intact. */
.ra-section__head,
.ra-subsection__head,
.ra-hero__title,
.ra-hero__subtitle,
h1,
h2,
h3,
h4 {
break-after: avoid;
page-break-after: avoid;
break-inside: avoid;
page-break-inside: avoid;
}
/* B3) Hero / Lead / Conclusion are short, atomic blocks — never split them
* across pages. (Hero often holds title + subtitle + meta; ugly when
* subtitle ends up alone on next page.) */
.ra-hero,
.ra-lead,
.ra-conclusion {
break-inside: avoid;
page-break-inside: avoid;
}
/* B4) Widows / orphans — never leave 1–2 stranded lines of a paragraph at
* the top / bottom of a page. */
p,
li,
blockquote,
.ra-aside,
.ra-quote {
orphans: 3;
widows: 3;
}
/* B5) Atomic visual blocks: figures, tables, code blocks. Keep them whole
* when reasonable; very long ones still split (the browser falls back). */
figure,
.ra-table,
.ra-codeblock,
.ra-formula,
.ra-image,
.ra-raw {
break-inside: avoid;
page-break-inside: avoid;
}
/* ============================================================
* C · Cover (the 3:4 cover above TOC, see references/cover.md)
* ============================================================ */
/* C1) Keep the cover's screen-authored 3:4 geometry in print. Earlier
* versions stretched .ra-cover to height:100vh, but Chromium print can
* clip absolutely positioned / grid-based cover internals after that
* resize. The stable default is: preserve the cover and start the TOC on
* the next page. Article-specific covers may opt into full-page print
* sizing only after visual PDF verification. */
.ra-cover {
break-inside: avoid;
page-break-inside: avoid;
break-after: page;
page-break-after: always;
}
}
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# scaffold.sh —— 一键创建一个 Beautiful Article 工作区。
#
# 用法:
# bash scripts/scaffold.sh <target-dir> [--theme=<id>] [--no-cover]
# bash scripts/scaffold.sh --list-themes
#
# 例子:
# bash <path-to-beautiful-article>/scripts/scaffold.sh ./my-article --theme=tufte
# bash <path-to-beautiful-article>/scripts/scaffold.sh ./brief --theme=press --no-cover
# bash <path-to-beautiful-article>/scripts/scaffold.sh --list-themes
#
# --no-cover:禁用文章封面(默认开 · 屏幕 3:4 / PDF 独占首页)。详见 references/cover.md。
#
# 工作区从 npm 安装**最新发布版的 reacticle**(package.json 里 reacticle: "latest",
# 每次 fresh scaffold 都会取当下最新)。无需本地 reacticle 仓库。
#
# 跑完后看 SKILL.md「Phase 4 First Spread」+ references/component-policy.md /
# raw-policy.md / 选定主题 theme-profiles/<id>.md。
# ─────────────────────────────────────────────────────────────
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="$SKILL_DIR/assets/scaffold-template"
PROFILES="$SKILL_DIR/theme-profiles/index.json"
DEFAULT_THEME="tufte"
list_themes() {
echo "可用主题(来自 ${PROFILES}):"
echo
# 没有 jq,用 grep + sed 提字段
grep -E '"id"|"label"|"mood"' "$PROFILES" | sed -E \
-e 's/.*"id":[[:space:]]*"([^"]+)".*/ • \1/' \
-e 's/.*"label":[[:space:]]*"([^"]+)".*/ \1/' \
-e 's/.*"mood":[[:space:]]*"([^"]+)".*/ \1/'
echo
echo "用 --theme=<id> 选定一个。默认:${DEFAULT_THEME}。"
}
# 校验主题 id 是否在 theme-profiles/index.json 里
theme_exists() {
grep -Eq "\"id\"[[:space:]]*:[[:space:]]*\"$1\"" "$PROFILES"
}
# ── 解析参数 ──
TARGET=""
THEME="$DEFAULT_THEME"
COVER=1
for arg in "$@"; do
case "$arg" in
--list-themes) list_themes; exit 0 ;;
--theme=*) THEME="${arg#--theme=}" ;;
--no-cover) COVER=0 ;;
--cover) COVER=1 ;;
--*) echo "✗ 未知参数: $arg" >&2; exit 1 ;;
*) [[ -z "$TARGET" ]] && TARGET="$arg" ;;
esac
done
TARGET="${TARGET:-my-article}"
# ── 校验主题 ──
if ! theme_exists "$THEME"; then
echo "✗ 未知主题 '$THEME'。可用主题:" >&2
echo >&2
list_themes >&2
exit 1
fi
# ── 目标目录检查 ──
if [[ -d "$TARGET" && -n "$(ls -A "$TARGET" 2>/dev/null || true)" ]]; then
echo "✗ 目标目录 '$TARGET' 已存在且非空,已中止。" >&2
exit 1
fi
if ! command -v npm >/dev/null; then
echo "✗ 需要 npm,但在 PATH 里没找到。" >&2
exit 1
fi
echo "▸ 在 $TARGET 创建 Beautiful Article 工作区"
echo "▸ 主题:$THEME"
echo "▸ 封面:$([[ "$COVER" == "1" ]] && echo "开(屏幕 3:4 / PDF 独占首页,详见 references/cover.md)" || echo "关")"
echo "▸ reacticle:从 npm 安装最新发布版"
mkdir -p "$TARGET"
# 复制工程模板
cp "$TEMPLATE/package.json" "$TARGET/package.json"
cp "$TEMPLATE/vite.config.ts" "$TARGET/vite.config.ts"
cp "$TEMPLATE/tsconfig.json" "$TARGET/tsconfig.json"
cp "$TEMPLATE/tsconfig.node.json" "$TARGET/tsconfig.node.json"
cp "$TEMPLATE/index.html" "$TARGET/index.html"
# 工作记忆目录 + 文章源目录
mkdir -p "$TARGET/source" "$TARGET/plan" "$TARGET/review" \
"$TARGET/article/sections" "$TARGET/article/raw-blocks" "$TARGET/article/assets"
cp "$TEMPLATE/article/main.tsx" "$TARGET/article/main.tsx"
cp "$TEMPLATE/article/Article.tsx" "$TARGET/article/Article.tsx"
# 一节一文件:assembler + 第一个 section 组件(多 Agent 并行的代码锚点)
cp "$TEMPLATE/article/sections/01-opening.tsx" "$TARGET/article/sections/01-opening.tsx"
# 封面:默认开。--no-cover 时跳过 Cover.tsx 并从 main.tsx 剥掉 __COVER_*__ 段。
if [[ "$COVER" == "1" ]]; then
cp "$TEMPLATE/article/Cover.tsx" "$TARGET/article/Cover.tsx"
fi
# 留住空目录(git 友好)
touch "$TARGET/article/raw-blocks/.gitkeep" "$TARGET/article/assets/.gitkeep"
# ── 注入主题 id(用 perl 避免转义问题)──
# main.tsx: <ThemeProvider theme="__THEME__">
# Article.tsx: colophon "· __THEME__ theme"
export RA_THEME="$THEME"
perl -pi -e 's/__THEME__/$ENV{RA_THEME}/g' "$TARGET/article/main.tsx"
perl -pi -e 's/__THEME__/$ENV{RA_THEME}/g' "$TARGET/article/Article.tsx"
# ── 封面开关:处理 main.tsx 里 __COVER_*__ 标记包裹的区段 ──
# COVER=1 → 去掉两行 __COVER_*_BEGIN__ / __COVER_*_END__ 标记(保留中间的 import 和 <Cover/>)
# COVER=0 → 连标记带中间内容一起剥掉(封面不参与构建)
if [[ "$COVER" == "1" ]]; then
# 删除标记行本身,保留 Cover 引入与渲染
perl -i -ne 'print unless /__COVER_(IMPORT|RENDER)_(BEGIN|END)__/' "$TARGET/article/main.tsx"
else
# 把 BEGIN..END 之间(含两端标记行)整段删掉
perl -i -0pe 's{[^\n]*__COVER_IMPORT_BEGIN__.*?__COVER_IMPORT_END__[^\n]*\n}{}gs' "$TARGET/article/main.tsx"
perl -i -0pe 's{[^\n]*__COVER_RENDER_BEGIN__.*?__COVER_RENDER_END__[^\n]*\n}{}gs' "$TARGET/article/main.tsx"
fi
# 标记起步主题
echo "$THEME" > "$TARGET/.theme"
cd "$TARGET"
echo "▸ 安装依赖(含 reacticle 最新版,可能要等一会)..."
npm install >/dev/null 2>&1
# 确保拿到当下最新(即使将来模板带了 lockfile 也强制刷新到最新)
npm install reacticle@latest >/dev/null 2>&1
INSTALLED_REACTICLE="$(node -e "console.log(JSON.parse(require('fs').readFileSync('node_modules/reacticle/package.json','utf8')).version)" 2>/dev/null || echo '?')"
echo "▸ reacticle 版本:$INSTALLED_REACTICLE"
echo "▸ 跑一次 typecheck 确认接线 OK ..."
if npx tsc --noEmit; then
echo "✓ typecheck 通过"
else
echo "⚠ typecheck 有问题(见上),dev / build 仍可能正常 —— 请人工确认。" >&2
fi
cat <<EOF
✓ 完成。工作区:$TARGET(主题 $THEME,见 .theme;reacticle $INSTALLED_REACTICLE)
下一步:
1. cd $TARGET
2. npm run dev # 预览(Phase 4 先写首屏 + 第一个 Section)
3. 首屏(Hero/Lead)写进 article/Article.tsx(assembler);
第一个 Section 写进 article/sections/01-opening.tsx
—— 铁律:一个 Section 一个文件,坚决不要写进 Article.tsx(多 Agent 并行前提)。
4. $([[ "$COVER" == "1" ]] && echo "封面:替换 article/Cover.tsx 里的 <CoverPlaceholder />,按文章 + 主题做定制(读 references/cover.md)。" || echo "封面:已关闭。如需打开,重新跑 scaffold 时去掉 --no-cover,或手动复制 Cover.tsx 模板。")
5. 把决策落盘到 source/ plan/ review/(Skill 的长期记忆)
构建交付(Phase 8):
• npm run build # 类型检查 + 单页 HTML → dist/index.html(CSS+JS 内联)
• npm run html # 复用 build,再复制为交付物 article/article.html
切主题:改 article/main.tsx 的 <ThemeProvider theme="..."> 一个字(tufte / press)。
升级组件库:npm install reacticle@latest
写作必读(路径在 Skill 仓库内):
• $SKILL_DIR/references/component-policy.md
• $SKILL_DIR/references/raw-policy.md
• $SKILL_DIR/theme-profiles/$THEME.md
EOF
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""MarkItDown-backed Source -> Markdown helper for Beautiful Article.
This script intentionally depends on MarkItDown and fails clearly when it is not
available. Use source-to-markdown.py as the lightweight fallback.
Usage:
python3 source-to-markdown-markitdown.py <file-or-url> -o source/source.md
Optional dependency:
python3.10 -m pip install "markitdown[pdf,docx]"
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from urllib.parse import urlparse
INSTALL_HINT = 'python3.10 -m pip install "markitdown[pdf,docx]"'
def load_markitdown():
if sys.version_info < (3, 10):
print(
"✗ MarkItDown requires Python 3.10+; current Python is "
f"{sys.version_info.major}.{sys.version_info.minor}.",
file=sys.stderr,
)
print(f" Install with: {INSTALL_HINT}", file=sys.stderr)
raise SystemExit(2)
try:
from markitdown import MarkItDown # type: ignore
except Exception as exc:
print("✗ MarkItDown is not installed in this Python environment.", file=sys.stderr)
print(f" Install with: {INSTALL_HINT}", file=sys.stderr)
print(" Or use scripts/source-to-markdown.py as the fallback.", file=sys.stderr)
print(f" Import error: {exc}", file=sys.stderr)
raise SystemExit(2)
return MarkItDown
def result_text(result) -> str:
for attr in ("text_content", "markdown"):
value = getattr(result, attr, None)
if isinstance(value, str) and value.strip():
return value
if isinstance(result, str):
return result
raise RuntimeError("MarkItDown returned no text_content/markdown output.")
def is_url(src: str) -> bool:
return urlparse(src).scheme in ("http", "https")
def main() -> None:
parser = argparse.ArgumentParser(description="Convert a source file or URL to Markdown via MarkItDown")
parser.add_argument("input", help="PDF / DOCX / PPTX / HTML / TXT / MD file or URL")
parser.add_argument("-o", "--output", help="write Markdown here (default: stdout)")
parser.add_argument(
"--use-plugins",
action="store_true",
help="enable installed MarkItDown plugins; disabled by default",
)
args = parser.parse_args()
src = args.input
if not is_url(src) and not Path(src).exists():
print(f"✗ 文件不存在:{src}", file=sys.stderr)
raise SystemExit(1)
MarkItDown = load_markitdown()
converter = MarkItDown(enable_plugins=args.use_plugins)
try:
markdown = result_text(converter.convert(src)).strip() + "\n"
except Exception as exc:
print(f"✗ MarkItDown 转换失败:{exc}", file=sys.stderr)
print(" 可改用 scripts/source-to-markdown.py 做轻量 fallback。", file=sys.stderr)
raise SystemExit(2)
if args.output:
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(markdown, "utf-8")
print(
f"✓ MarkItDown 写入 {out}({len(markdown)} 字符)。"
"请继续清理噪音并补 extraction-notes.md。",
file=sys.stderr,
)
else:
sys.stdout.write(markdown)
if __name__ == "__main__":
main()
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Source → Markdown extraction helper for the Beautiful Article skill.
Mechanically extracts text from a PDF / DOCX / HTML file (or URL) into rough
Markdown. It does NOT clean editorial noise, mark image placeholders, or record
extraction risk — that judgement stays with the agent (see Phase 1 +
references/source-to-markdown.md). The agent should review and refine the output
into source/source.md and write source/extraction-notes.md.
Usage:
python3 source-to-markdown.py <input.pdf|.docx|.html|.htm|.txt|.md|URL> [-o out.md]
Dependencies are probed at runtime and optional. If a parser is missing the
script prints an install hint and degrades gracefully.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from urllib.parse import urlparse
def _hint(pkg: str) -> str:
return f" (缺少 {pkg},可安装:pip install {pkg})"
def from_pdf(path: str) -> str:
try:
import pdfplumber # type: ignore
except Exception:
try:
from pdfminer.high_level import extract_text # type: ignore
return extract_text(path)
except Exception:
print(_hint("pdfplumber 或 pdfminer.six"), file=sys.stderr)
raise SystemExit(2)
out = []
with pdfplumber.open(path) as pdf:
for i, page in enumerate(pdf.pages, 1):
out.append(f"\n<!-- page {i} -->\n")
out.append(page.extract_text() or "")
for t in page.extract_tables() or []:
out.append("\n" + _table_to_md(t) + "\n")
return "\n".join(out)
def from_docx(path: str) -> str:
try:
import docx # type: ignore
except Exception:
print(_hint("python-docx"), file=sys.stderr)
raise SystemExit(2)
doc = docx.Document(path)
out = []
for p in doc.paragraphs:
text = p.text.strip()
if not text:
out.append("")
continue
style = (p.style.name or "").lower()
if style.startswith("heading"):
level = "".join(c for c in style if c.isdigit()) or "1"
out.append("#" * min(int(level), 6) + " " + text)
else:
out.append(text)
for table in doc.tables:
rows = [[c.text.strip() for c in r.cells] for r in table.rows]
if rows:
out.append("\n" + _table_to_md(rows) + "\n")
return "\n".join(out)
def from_html(html: str) -> str:
try:
from bs4 import BeautifulSoup # type: ignore
except Exception:
print(_hint("beautifulsoup4"), file=sys.stderr)
# crude fallback: strip tags
import re
return re.sub(r"<[^>]+>", "", html)
soup = BeautifulSoup(html, "html.parser")
for tag in soup(["script", "style", "nav", "footer", "aside", "header"]):
tag.decompose()
main = soup.find("article") or soup.find("main") or soup.body or soup
lines = []
for el in main.find_all(
["h1", "h2", "h3", "h4", "p", "li", "pre", "blockquote"]
):
text = el.get_text(" ", strip=True)
if not text:
continue
name = el.name
if name.startswith("h") and name[1:].isdigit():
lines.append("#" * int(name[1:]) + " " + text)
elif name == "li":
lines.append("- " + text)
elif name == "pre":
lines.append("```\n" + text + "\n```")
elif name == "blockquote":
lines.append("> " + text)
else:
lines.append(text)
return "\n\n".join(lines)
def from_url(url: str) -> str:
try:
import urllib.request
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
html = resp.read().decode("utf-8", "replace")
except Exception as e: # pragma: no cover
print(f"✗ 抓取失败:{e}", file=sys.stderr)
print(" 也可以用 agent 的网页抓取能力获取正文后再清理。", file=sys.stderr)
raise SystemExit(2)
return from_html(html)
def _table_to_md(rows) -> str:
rows = [[("" if c is None else str(c)).replace("\n", " ").strip() for c in r] for r in rows]
if not rows:
return ""
width = max(len(r) for r in rows)
rows = [r + [""] * (width - len(r)) for r in rows]
head = "| " + " | ".join(rows[0]) + " |"
sep = "| " + " | ".join(["---"] * width) + " |"
body = ["| " + " | ".join(r) + " |" for r in rows[1:]]
return "\n".join([head, sep, *body])
def main() -> None:
ap = argparse.ArgumentParser(description="Source → Markdown extraction helper")
ap.add_argument("input", help="PDF / DOCX / HTML / TXT / MD file or URL")
ap.add_argument("-o", "--output", help="write Markdown here (default: stdout)")
args = ap.parse_args()
src = args.input
parsed = urlparse(src)
if parsed.scheme in ("http", "https"):
md = from_url(src)
else:
p = Path(src)
if not p.exists():
print(f"✗ 文件不存在:{src}", file=sys.stderr)
raise SystemExit(1)
ext = p.suffix.lower()
if ext == ".pdf":
md = from_pdf(str(p))
elif ext == ".docx":
md = from_docx(str(p))
elif ext in (".html", ".htm"):
md = from_html(p.read_text("utf-8", "replace"))
elif ext in (".md", ".markdown", ".txt"):
md = p.read_text("utf-8", "replace")
else:
print(f"✗ 不支持的输入类型:{ext}", file=sys.stderr)
raise SystemExit(1)
md = (md or "").strip() + "\n"
if args.output:
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(md, "utf-8")
print(f"✓ 写入 {out}({len(md)} 字符)。请人工清理噪音并补 extraction-notes.md。", file=sys.stderr)
else:
sys.stdout.write(md)
if __name__ == "__main__":
main()
@@ -0,0 +1,57 @@
# Theme Profile · andy(Headspace 静谧 / 温柔)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="andy"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/andy/andy.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`andy`(`<ThemeProvider theme="andy">`)
- **气质**:温暖人文派(Headspace)。暖奶油纸、友好南瓜橙、暖灰墨(非纯黑)、通体
圆体无衬线(Quicksand + Nunito)。**七套里唯一同时用大圆角 + 柔和暖阴影**的主题。
柔软、平静、治愈,重在让人安心。完整继承语义化组件契约。
## 适合 / 不适合的文章类型
- **适合**:健康 / 心理 / 生活方式、引导式 `tutorial`、温柔的上手与安抚、产品价值观叙事、
需要"柔软 UI"质感与亲和力的 `explainer`。面向普通人、降低压力感。
- **不适合**:学术论文(`knuth`);冷调规格(`vignelli`);暗底工程(`shannon`);
密集数据报告(`tufte`);俏皮黄黑(`freddie`);需要锋利严肃感的内容;幻灯片。
## 排版气质
- 标题 / 标签用 Quicksand(几何圆体),正文用 Nunito(人文圆体);呼应 Headspace 微笑曲线。
- 正文 ~17px,行距 1.7(舒展);标题 `display: 700`,圆体偏粗最 friendly。强调不用斜体(协议级禁用)。
- **亮橙作填充、加深橙作文字**:圆圈章节号 / pill 用亮南瓜橙;链接 / 结构用加深烧橙保证可读。
## Raw 风格
像一页柔软、圆润、令人放松的插图。
- 约束的是**气质**(暖橙系、大圆角、柔阴影、大留白、平静),不是**媒介**。
- 典型:圆角步骤卡、柔和进度 / 呼吸动画、圆形数据图、温柔问答、blob / imperfect-circle 装饰。
- 构图:大留白、大圆角、柔阴影;颜色只用 `--ra-*`(橙用 `--hs-orange`)。
- 动效:平缓顺滑、可有舒缓循环(如呼吸引导),但不喧闹。
## 媒体(图片 / 视频 / 音频)风格
- 适合:圆润吉祥物 / blob 插画、暖色生活摄影、暖橙柔和渐变背景、友好圆角图标。
- 构图留白充足、圆角裁切、主体平静;caption 温柔;必须配 alt。
- 色彩暖橙为主,辅柔蓝 / 柔绿情绪色,低饱和柔和。
## 代码 / 公式风格
- `CodeBlock` 像友好 App 文档里的代码卡片:暖桃 surface + 大圆角 + 可有极轻阴影,不用暗色终端。
- Prism token:标签 / 函数用加深橙 accent(克制),关键字用 risk 红,字符串用绿,其余走暖墨 / muted。
- `Formula` 像温柔标注:克制、对齐,可有圆角 surface。
## 禁止项
- 纯黑墨、硬直角、冷色——立刻破坏"柔软治愈"识别。
- 亮南瓜橙作正文 / 小字(对比不足,用加深 accent)。
- 柔阴影 + 大圆角滥用成浮夸卡片堆叠;紫粉霓虹、Tailwind 默认味;用斜体强调。
## 不同信息密度下的表现建议(建议,非限制)
- `100% explainer / tutorial`:柔软长文 + 圆角步骤卡 + 枕头 callout,正文为主体。
- `60-80% 引导内容`:保留关键步骤 + 温柔插画,圆圈章节号点睛。
- `40% briefing`:Raw 偏柔和图解 / 呼吸动画,文字更短,仍是治愈形态。
@@ -0,0 +1,57 @@
# Theme Profile · bayer(包豪斯 / 三原色几何)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="bayer"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/bayer/bayer.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`bayer`(`<ThemeProvider theme="bayer">`)
- **气质**:包豪斯 / 构成主义。暖纸、近黑墨,三原色(红 / 蓝 / 黄)当**结构色**而非装饰。
识别度来自几何:蓝色实心圆里的章节数字、红黄蓝三色刊头条、小写无衬线导语、硬边方块。
几何无衬线(Josefin / Poppins)。响亮但有纪律。
## 适合 / 不适合的文章类型
- **适合**:教学 / 科普解释、产品介绍、宣言、品牌叙事;"专业但有性格、彩色但不浮夸",
适合配几何信息图 / 流程图 / 构成式插画。
- **不适合**:学术论文(`knuth`)、黑白大刊(`bodoni`)、暗底工程(`shannon` / `fuller`)、
柔软治愈(`andy`)、极致中性规格(`vignelli`)、幻灯片。
## 排版气质
- 标题 / 标签用 Josefin Sans(几何),正文用 Poppins(几何人文)。
- 正文 ~17px,行距 1.65;display 字重 700;强调不用斜体(协议级禁用)。
- **蓝是结构、红是风险、黄是填充**:链接 / 章节圆用蓝,警示用红,黄只作色块(绝不作文字色)。
- 导语 / kicker 走小写(Bauhaus 单一字母表;仅作用于拉丁字母)。
## Raw 风格
像一页构成主义信息图。
- 约束的是**气质**(红蓝黄三色、强网格、硬边、圆形点睛),不是**媒介**。
- 典型:几何流程图、圆 / 方 / 三角构成、原色柱状 / 比例图、网格示意、带蓝圆编号的步骤。
- 构图:强网格、硬边、三色 + 黑白;无圆角矩形卡。
- 动效:干脆位移 / 显隐,无回弹、无循环装饰。
## 媒体(图片 / 视频 / 音频)风格
- 适合:几何构成插画、原色海报、强对比黑白 / 原色摄影、网格 / 圆方三角示意。
- 构图强网格、果断留白、原色块面;caption 简洁;必配 alt。
- 色彩红 / 蓝 / 黄 + 黑白;不引入紫粉冷渐变。
## 代码 / 公式风格
- `CodeBlock` 像构成主义文档的代码块:暖浅 surface + 发丝线,无圆角、无暗窗。
- Prism token:标签 / 函数走蓝 accent,关键字走红 risk,字符串走绿;**黄不作语法色**。
- `Formula` 居中克制,可有极淡 surface。
## 禁止项
- 把包豪斯黄当文字 / 链接 / 语法色(对比不足)。
- 圆角矩形卡 + 投影堆叠;紫粉渐变、霓虹、Tailwind 默认味、emoji 当装饰。用斜体强调(协议级禁用)。
## 不同信息密度下的表现建议(建议,非限制)
- `100% explainer / 教学`:长文 + 蓝圆编号 + 几何信息图 + 黄底原则块,正文为主体。
- `60-80% 内容`:保留关键步骤 + 构成示意,三色条点睛。
- `40% briefing`:Raw 偏几何图解 / 比例图,文字更短,仍是构成形态。
@@ -0,0 +1,57 @@
# Theme Profile · bodoni(报刊 / Didone 高反差)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="bodoni"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/bodoni/bodoni.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`bodoni`(`<ThemeProvider theme="bodoni">`)
- **气质**:印刷殿堂派(Didone broadsheet / 时装刊头)。纯白纸、真黑墨、发丝栏线,
极端粗细对比的 Playfair Display 刊头大字压在从容老式衬线正文上。识别度来自印刷惯例
——刊头粗细双线、首字下沉、小型大写导语、栏线分节——颜色几乎只留给风险。
## 适合 / 不适合的文章类型
- **适合**:宣言、深度特稿、有分量的随笔与长评、文化 / 时政 / 时装报道;需要权威、
戏剧、仪式感的开篇与封面式长文。
- **不适合**:暖调亲和(`press` / `freddie`)、柔软治愈(`andy`)、冷调规格(`vignelli`)、
暗底工程(`shannon` / `fuller`)、学术论文(`knuth`)、彩色活泼(`bayer` / `sottsass`)、幻灯片。
## 排版气质
- 标题 / 刊头用 Playfair Display(Didone 高反差),正文用 Source Serif 4 / Newsreader(可读老式衬线)。
- 正文 ~17px,行距 1.62;display 字重 900、字号巨大,与发丝正文形成大报落差。
- 导语 / 表头 / TOC 标题 / 结语标签统一走**小型大写 + 字距**;强调不用斜体(协议级禁用)。
- **层级靠字号 / 字重 / 发丝线,不靠颜色**;整篇近黑白,红只在真正风险处出现。
## Raw 风格
像大报版面里亲手排的图版或抽言。
- 约束的是**气质**(黑白、发丝线、Didone 大字、印刷克制),不是**媒介**。
- 典型:黑白数据 / 折线图、版式抽言、时间线、对照栏、刊头式标题块、首字下沉开篇。
- 构图:发丝线 + 黑白 + 大字标题;填色极少,红点睛;无圆角、无阴影。
- 动效:克制、近静态;避免循环装饰与回弹。
## 媒体(图片 / 视频 / 音频)风格
- 适合:黑白 / 低饱和摄影、高质感人像 / 静物、纪实照片、单色信息图、版画质感。
- 构图果断留白、可满版 / 裁切;caption 小型大写或衬线小字;必配 alt。
- 色彩以黑白灰为主,慎用一抹编辑红点睛。
## 代码 / 公式风格
- `CodeBlock` 像书页里克制的代码清单:暖浅 surface + 发丝线,无圆角、无暗窗。
- Prism token:墨色 / 暖编辑红 / 低饱和绿;不彩虹。
- `Formula` 居中克制、衬线数字。
## 禁止项
- 引入除编辑红以外的颜色作装饰;圆角 / 阴影 / 卡片化 / 渐变 / 霓虹。
- 把 Didone 当正文长段(正文用老式衬线)。用斜体强调(协议级禁用)。
## 不同信息密度下的表现建议(建议,非限制)
- `100% longform / 特稿`:首字下沉开篇 + 栏线分节 + 居中铅字抽言,正文为主体。
- `60-80% 评论`:保留关键抽言与小型大写导语,黑白图版点睛。
- `40% briefing`:Raw 偏黑白数据图 / 时间线,文字更短,仍是大报形态。
@@ -0,0 +1,57 @@
# Theme Profile · freddie(Mailchimp 暖黄 / 友善)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="freddie"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/freddie/freddie.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`freddie`(`<ThemeProvider theme="freddie">`)
- **气质**:温暖人文派(Mailchimp)。纯白纸、近黑 Peppercorn 墨、Cavendish 明黄
只作**荧光笔 / 色块**(绝不作文字色)。俏皮柔和的衬线标题(Fraunces)压在干净
grotesque 正文(Hanken)之上。机灵、亲切、有人味,但仍专业。完整继承结构纪律。
## 适合 / 不适合的文章类型
- **适合**:产品介绍、`tutorial` / 上手指南、changelog 叙事、功能 `explainer`、FAQ、
亲和力强的营销味长文。面向普通用户、需要温度与幽默的内容。
- **不适合**:学术论文(`knuth`);冷调系统规格(`vignelli`);暗底工程现场(`shannon`);
极密集数据报告(`tufte`);柔软治愈向(`andy`);幻灯片。
## 排版气质
- 标题用 Fraunces(仿 Cooper / Means 的柔软俏皮),正文 / 标签用 Hanken Grotesk(仿 Graphik)。
- 正文 ~17px,行距 1.65;标题靠 `display: 600` + 柔轴,不靠超粗。强调不用斜体(协议级禁用)。
- **黄是荧光不是墨**:链接 = 黑字 + 黄 highlight(hover 填满);章节号 = 黑字 + 黄贴纸(略旋转)。
## Raw 风格
像一页友好、带手作感的产品说明插图。
- 约束的是**气质**(黑 / 白 / 黄、适度圆角、舒展留白、一点人情味),不是**媒介**。
- 典型:步骤 / 流程图、对比示意、带黄色 highlight 的标注、友好小数据图、可点开 FAQ。
- 构图:留白舒展、黄只作强调、适度圆角;颜色只用 `--ra-*`(黄用 `--mc-yellow`)。
- 动效:允许短促、带一丝回弹的过渡;避免无限循环装饰。
## 媒体(图片 / 视频 / 音频)风格
- 适合:精修产品截图、温暖摄影、scruffy / 手绘风插画、流程示意、带人物的友好配图。
- 构图留白舒展、主体清楚、可有一点不规整的人味;caption 简洁;必须配 alt。
- 色彩贴近黑 / 白 / 黄,强调用黄色块承载,不引入紫粉冷渐变。
## 代码 / 公式风格
- `CodeBlock` 像友好的代码片段:暖浅 surface + 发丝线 + 适度圆角,不用暗色编辑器窗口。
- Prism token:标签 / 函数走墨色或暖红,关键字用 risk 红,字符串用绿;**黄不作语法色**。
- `Formula` 像正文里的友好标注:克制、对齐,可有极淡圆角 surface。
## 禁止项
- 把 Cavendish 黄当文字 / 链接 / 语法色(不可读,破坏识别)。
- 紫粉渐变、霓虹、Tailwind 默认味、把 emoji / 图标当装饰。
- 大圆角 + 大投影的卡片堆叠(那是 `andy` 的柔软领域);用斜体强调。
## 不同信息密度下的表现建议(建议,非限制)
- `100% explainer / tutorial`:友好长文 + 步骤图 + 黄色 callout,正文为主体。
- `60-80% 产品介绍`:保留关键步骤 + 截图,黄色强调点睛。
- `40% briefing`:Raw 偏友好图解,文字更短,仍是亲切产品形态。
@@ -0,0 +1,56 @@
# Theme Profile · fuller(蓝图 / 工程制图)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="fuller"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/fuller/fuller.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`fuller`(`<ThemeProvider theme="fuller">`)
- **气质**:工程制图 / 蓝图。深蓝图底 + 青白墨 + 一束制图青,外加淡方格网。识别度来自
绘图板:方格纸上的标题块 Hero、等宽尺寸标注、虚线标注分节、青色发丝线。是 `shannon`
(暖琥珀终端)的冷色对位——同为暗底,气质是制图桌而非命令行。
## 适合 / 不适合的文章类型
- **适合**:技术规格、系统 / 架构设计、协议 / 接口文档、RFC、硬件 / 机械说明;需要精密、
冷静、工程质感的暗底长文,配示意 / 标注 / 拓扑图。
- **不适合**:需打印的文档(暗底费墨)、温暖叙事(`press` / `freddie`)、柔软治愈(`andy`)、
彩色活泼(`bayer` / `sottsass`)、学术论文(`knuth`)、黑白大刊(`bodoni`)、幻灯片。
## 排版气质
- 正文 / 标题用 IBM Plex Sans,标签 / 尺寸 / 章节号 / 元数据用 IBM Plex Mono(等宽承载标注)。
- 正文 ~17px,行距 1.62;display 字重 600(工程克制);强调不用斜体(协议级禁用)。
- **青是唯一结构色,风险用暖橙**以便在满屏青中辨识;暗底,所有 soft 是深色面板。
## Raw 风格
像图纸上亲手画的标注示意。
- 约束的是**气质**(青发丝线、等宽标注、方格网、暗面板、无发光),不是**媒介**。
- 典型:线框 / 拓扑 / 时序图、带尺寸线的标注、方格纸坐标、等宽数据表、结构剖面。
- 构图:青色发丝线 + 等宽标注 + 可叠方格;无圆角、无发光。
- 动效:克制位移 / 描边动画;无霓虹循环。
## 媒体(图片 / 视频 / 音频)风格
- 适合:线框 / 结构 / 拓扑图、尺寸标注图、蓝晒图、CAD / 示意截图、暗底数据可视化。
- 构图以线为主、青色描边、可叠网格;caption 等宽冷静;必配 alt。
- 色彩蓝青为主,风险用暖橙点睛;低饱和。
## 代码 / 公式风格
- `CodeBlock` 是暗底第一公民但仍是工程文档:暗面板 + 暗青发丝线 + 退后 mono 行号,无圆角、无发光。
- Prism token 从青 accent / 风险橙 / 薄荷绿派生;克制不彩虹。
- `Formula` 像规格里的公式:等宽标注、对齐、克制。
## 禁止项
- 把暗底当"赛博朋克霓虹"——本主题是冷静蓝图,无发光、无故障风。
- 圆角 / 阴影 / 亮底卡片 / 暖色生活化插画;风险用青色(风险固定暖橙)。用斜体强调(协议级禁用)。
## 不同信息密度下的表现建议(建议,非限制)
- `100% spec / 系统设计`:标题块 Hero + 虚线分节 + 规格表 + 拓扑示意,文字与图并重。
- `60-80% 文档`:保留关键标注图,等宽元数据点睛。
- `40% briefing`:Raw 偏线框 / 时序图,文字更短,仍是蓝图形态。
@@ -0,0 +1,112 @@
[
{
"id": "tufte",
"runtimeTheme": "tufte",
"label": "Tufte · Data-Ink",
"mood": "证据、数据、克制、低装饰;让读者凑近去读",
"bestFor": ["longform", "full-report", "explainer", "review", "tutorial"],
"notFor": ["十米外观看的演示幻灯片", "移动端优先且需要大量边注的场景"],
"profile": "tufte.md",
"canonicalRuntimeMd": "src/theme/themes/tufte/tufte.md"
},
{
"id": "press",
"runtimeTheme": "press",
"label": "Press · 书卷 / 编辑",
"mood": "出版、叙事、温暖、编辑感;请你从一臂之外读一本书",
"bestFor": ["essay", "briefing", "visual-essay", "longform", "explainer"],
"notFor": ["极度密集、需大量边注的数据型报告(那是 tufte 主场)", "演示幻灯片"],
"profile": "press.md",
"canonicalRuntimeMd": "src/theme/themes/press/press.md"
},
{
"id": "shannon",
"runtimeTheme": "shannon",
"label": "Shannon · 暗色工程证据",
"mood": "暗底工程现场、仪表信号、克制、低装饰;在暗纸上读工程证据",
"bestFor": ["postmortem", "system-design", "benchmark", "explainer", "review", "tutorial"],
"notFor": ["需打印的正式文档(暗底费墨)", "温暖叙事 / 出版随笔", "演示幻灯片"],
"profile": "shannon.md",
"canonicalRuntimeMd": "src/theme/themes/shannon/shannon.md"
},
{
"id": "vignelli",
"runtimeTheme": "vignelli",
"label": "Vignelli · 瑞士国际主义文档",
"mood": "冷中性、sans、网格、系统化;清晰可扫读的中性文档",
"bestFor": ["docs", "spec", "changelog", "reference", "explainer", "tutorial"],
"notFor": ["温暖叙事(press)", "正式学术论文(knuth)", "暗底工程现场(shannon)", "演示幻灯片"],
"profile": "vignelli.md",
"canonicalRuntimeMd": "src/theme/themes/vignelli/vignelli.md"
},
{
"id": "knuth",
"runtimeTheme": "knuth",
"label": "Knuth · 学术预印本",
"mood": "正式、严谨、公式与编号优先;像读一篇 arXiv 论文",
"bestFor": ["paper", "preprint", "research", "literature-review", "explainer", "full-report"],
"notFor": ["温暖叙事(press)", "中性产品文档(vignelli)", "暗底工程现场(shannon)", "演示幻灯片"],
"profile": "knuth.md",
"canonicalRuntimeMd": "src/theme/themes/knuth/knuth.md"
},
{
"id": "freddie",
"runtimeTheme": "freddie",
"label": "Freddie · 暖黄 / 友善",
"mood": "暖白 + 明黄、机灵有人味;黑字荧光、俏皮衬线,专业但不端着",
"bestFor": ["explainer", "tutorial", "product-intro", "changelog", "faq"],
"notFor": ["正式学术论文(knuth)", "冷调系统规格(vignelli)", "暗底工程现场(shannon)", "柔软治愈向(andy)", "演示幻灯片"],
"profile": "freddie.md",
"canonicalRuntimeMd": "src/theme/themes/freddie/freddie.md"
},
{
"id": "andy",
"runtimeTheme": "andy",
"label": "Andy · 静谧 / 温柔",
"mood": "暖奶油 + 暖橙、柔软平静治愈;大圆角 + 柔阴影、通体圆体,让人安心",
"bestFor": ["explainer", "tutorial", "wellness", "onboarding", "lifestyle"],
"notFor": ["正式学术论文(knuth)", "冷调系统规格(vignelli)", "暗底工程现场(shannon)", "俏皮黄黑(freddie)", "需要锋利严肃感的内容", "演示幻灯片"],
"profile": "andy.md",
"canonicalRuntimeMd": "src/theme/themes/andy/andy.md"
},
{
"id": "bodoni",
"runtimeTheme": "bodoni",
"label": "Bodoni · 报刊 / Didone 高反差",
"mood": "极致黑白、戏剧高反差;像一份精心排过的大报 / 时装刊头",
"bestFor": ["longform", "essay", "manifesto", "feature", "review"],
"notFor": ["暖调亲和(press / freddie)", "柔软治愈(andy)", "冷调规格(vignelli)", "暗底工程(shannon / fuller)", "学术论文(knuth)", "彩色活泼(bayer / sottsass)", "演示幻灯片"],
"profile": "bodoni.md",
"canonicalRuntimeMd": "src/theme/themes/bodoni/bodoni.md"
},
{
"id": "bayer",
"runtimeTheme": "bayer",
"label": "Bayer · 包豪斯 / 三原色几何",
"mood": "响亮而理性;三原色当结构色、蓝圆章节号、三色刊头条、几何构成",
"bestFor": ["explainer", "tutorial", "manifesto", "product-intro", "brand"],
"notFor": ["学术论文(knuth)", "黑白大刊(bodoni)", "暗底工程(shannon / fuller)", "柔软治愈(andy)", "极致中性规格(vignelli)", "演示幻灯片"],
"profile": "bayer.md",
"canonicalRuntimeMd": "src/theme/themes/bayer/bayer.md"
},
{
"id": "fuller",
"runtimeTheme": "fuller",
"label": "Fuller · 蓝图 / 工程制图",
"mood": "冷峻技术暗色;蓝图底 + 制图青 + 方格纸标题块、等宽尺寸标注",
"bestFor": ["spec", "system-design", "rfc", "reference", "explainer"],
"notFor": ["需打印的文档(暗底费墨)", "温暖叙事(press / freddie)", "柔软治愈(andy)", "彩色活泼(bayer / sottsass)", "学术论文(knuth)", "黑白大刊(bodoni)", "演示幻灯片"],
"profile": "fuller.md",
"canonicalRuntimeMd": "src/theme/themes/fuller/fuller.md"
},
{
"id": "sottsass",
"runtimeTheme": "sottsass",
"label": "Sottsass · 孟菲斯 / 80s 撞色",
"mood": "最叛逆好玩;撞色粉彩 + 无模糊硬投影 + 旋转药丸 + 波浪下划线 + 彩屑",
"bestFor": ["explainer", "tutorial", "culture", "launch", "design-writing"],
"notFor": ["学术(knuth)", "黑白大刊(bodoni)", "冷调规格(vignelli)", "暗底工程(shannon / fuller)", "严肃克制内容", "安静治愈(andy)", "演示幻灯片"],
"profile": "sottsass.md",
"canonicalRuntimeMd": "src/theme/themes/sottsass/sottsass.md"
}
]
@@ -0,0 +1,60 @@
# Theme Profile · knuth(学术预印本)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="knuth"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/knuth/knuth.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`knuth`(`<ThemeProvider theme="knuth">`)
- **气质**:学术 / 科学排版。给报告穿上期刊 / arXiv 预印本的外衣:Computer Modern
衬线、编号小节、图 / 表 / 公式编号、两端对齐正文、引用密集、公式优先。完整继承
data-ink 纪律。与 `tufte` 的随笔式边注分明:`knuth` 是"读一篇正式论文"。
## 适合 / 不适合的文章类型
- **适合**:`paper` / `preprint`、`research` / 科研调研、`literature-review` / 文献综述、
技术白皮书、形式化分析、公式 / 定理 / 引用密集的 `explainer` / `full-report`。
- **不适合**:温暖叙事(`press`);中性产品文档(`vignelli`);暗底工程现场(`shannon`);幻灯片。
## 排版气质
- 正文与标题用 Computer Modern / Latin Modern 衬线(→ Source Serif 4 / Georgia / 宋体回退)。
- 标签 / 图注 / 表头用 CMU Sans 一脉小字号;数学用 KaTeX(`Formula`)。
- 正文 ~16.5px,行距 1.58;标题用粗衬线(weight 700)取论文标题分量。强调不用斜体(协议级禁用)。
- **倾向两端对齐**(justify),营造印刷论文的整齐块面。
## Raw 风格
像论文中作者亲手排的一张 Figure。
- 约束的是**气质**(正式、克制、公式 / 编号优先、可核查),不是**媒介**。
- 典型:内联 SVG 图表、坐标轴 / 函数曲线 / 推导示意、可交互参数演示、定理 / 证明结构图、
带 "Figure N." 题注的图版。
- 构图:克制留白、清晰坐标与标注、学术蓝作结构线索;每条线有含义。颜色只用 `--ra-*`。
- 动效:默认无;必须交互只用即时响应轻过渡,不用循环装饰。
## 媒体(图片 / 视频 / 音频)风格
- 适合:数据图 / 实验结果图、示意图 / 流程图 / 架构图、论文截图、表格、低饱和摄影、手稿 / 推导。
- 每张图配 "Figure N." 式 caption / source / alt;构图留白充足、主体清楚。
- 色彩低饱和贴近纸墨,强调只用学术蓝或警示红承载信息。
## 代码 / 公式风格
- `CodeBlock` 像专著里的代码清单:浅纸 surface + 发丝线,不用暗色编辑器窗口。行号退后、从容。
- Prism token 从主题派生:标签 / 函数用学术蓝 accent,关键字 / 风险用 risk 红,字符串用绿,
注释用 muted。
- `Formula` 是本主题主角:块级公式从容上下留白、编号靠右((1)(2)(3)),行内与正文无缝。
## 禁止项
- 卡片、面板、填色块、投影、圆角;比 `#C7C6BB` 更深的网格线。
- 把学术蓝当装饰;第二个红色用于"警示"以外;emoji / 图标当装饰。
- 库存 hero 图、3D 插画、渐变背景、装饰光斑、霓虹、高对比终端黑底代码。
- Raw / 媒体变成营销素材或 SaaS 首页,而非论文图版。
## 不同信息密度下的表现建议(建议,非限制)
- `100% paper / full-report`:正式长文 + 编号小节 + 公式 / 图表编号,引用密集。
- `60-80% research / review`:保留核心推导 + 关键图表,正文为主体。
- `40% explainer`:Raw 偏公式 / 函数图解,文字更短,仍保持论文气质。
@@ -0,0 +1,61 @@
# Theme Profile · press(书卷 / 编辑)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="press"`)。详尽版本见组件库 canonical md:
> `src/theme/themes/press/press.md`(写代码 / 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`press`(`<ThemeProvider theme="press">`)
- **气质**:出版物级长读物(参照 Stripe Press)。与 tufte 同宗、共享 Data-Ink 的
结构纪律(以线代框、内联注、去垃圾表格,绝不用卡片与投影),但人格更外放:把报告
当成一本被认真设计过的书来排。tufte 让你"凑近读",press 请你"从一臂之外读一本书"。
## 适合 / 不适合的文章类型
- **适合**:`essay`、`briefing`、`visual-essay`、`longform`(叙事 / 出版型)、
`explainer`(偏人文)。需要从容阅读的长文、随笔、白皮书、产品手记。
- **不适合**:极度密集、需大量边注的数据型报告(那是 tufte 主场);演示幻灯片。
## 排版气质
- 正文与标题用当代过渡式衬线(Newsreader → Source Serif 4 / Spectral / Georgia / 宋体),
气质比 tufte 更现代、更"成书"。
- 正文偏大(~17px),行距 1.7;标题字号拉开(h1≈3rem),靠体量而非黑体取得分量。
- 一个浓郁主色(氧化血红)串起全篇:章节号、强调线、Hero、目录高亮。禁用斜体。
## Raw 风格
可以比 tufte 更有版式感,但仍像为文章专门设计的一页内插图。
- 首选:出版物式分栏示意、暖色信息图、小型交互标尺、章节间节奏图、带精细 caption 的
SVG、可轻微展开 / 切换的解释控件。
- 构图:更大留白、更强标题层级,氧化血红作识别线索;填色可比 tufte 多一点但克制。
- 动效:允许短促、柔和、一次性过渡;避免无限循环装饰动画。
- 自定义 CSS / SVG / React 仍必须用 `--ra-*` token,不引入新品牌色或冷色渐变。
## 媒体(图片 / 视频 / 音频)风格
- 适合:暖色真实摄影、书籍 / 纸张 / 工作台细节、产品界面截图、低饱和 editorial 插图、
精修信息图、手稿 / 笔记 / 版面草图。
- 构图主体清楚、留白充足、色温偏暖;可比 tufte 更完整保留产品上下文,但要裁掉噪声。
- 图注像出版物 caption,简洁说明来源与上下文,不写营销口号。
## 代码 / 公式风格
- `CodeBlock` 像一本技术书里的代码页:温暖、清晰、有精修感,但不变成 IDE 截图。
奶油纸上的浅 code surface + 线边界,行号比 tufte 更从容。
- Prism token 色从 press token 派生:标签 / 函数用氧化血红 accent,字符串用低饱和绿,
关键字 / 风险用更热的 risk。
- `Formula` 像书页里的排版公式,块级公式允许更从容留白,caption 像出版物题注。
## 禁止项
- 卡片、面板、填色块、投影、圆角。
- 把主色当装饰滥用(它只承载结构与品牌识别);第二个红色用于"警示"以外的用途。
- emoji / 图标当装饰。
- 图片或 Raw 变成营销落地页 / SaaS 首页 / 仪表盘大屏,而非出版物里的图版。
- 冷蓝紫 SaaS stock 图、3D 渲染图标、廉价渐变背景、无信息量 hero 氛围图。
## 不同信息密度下的表现建议(建议,非限制)
- `100% longform`:深度出版文章,体量即层级,Raw 作精修插图。
- `40-60% briefing / visual-essay`:更强编辑节奏与图文留白,视觉块占比更高,仍是文章。
@@ -0,0 +1,64 @@
# Theme Profile · shannon(暗色工程证据)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="shannon"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/shannon/shannon.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`shannon`(`<ThemeProvider theme="shannon">`)
- **气质**:`tufte` 的"夜间工程版"。温暖石墨暗底、暖白墨、唯一一束琥珀信号色。
完整继承 data-ink 纪律(以线代框、内联注、去垃圾表格、色彩承载含义),只是把
场景搬到暗底的工程现场。**非纯黑、非 #0D1117 赛博暗、无霓虹、无发光。**
## 适合 / 不适合的文章类型
- **适合**:`postmortem` / 故障复盘、`system-design` / 架构决策、`benchmark` / 性能分析、
技术 / AI / 算法 `explainer`、`review`、`tutorial`。`CodeBlock` `DiffReview`
`Incident` `RiskList` `Decision` 密集的内容尤其契合。
- **不适合**:需打印的正式文档(暗底费墨,打印会被强制白底);温暖叙事 / 出版随笔
(`press` / `knuth`);10 米外观看的幻灯片。
## 排版气质
- 正文与标题用技术人文 sans(IBM Plex Sans → Söhne → system-ui),字重 400,靠字号取分量。
- **等宽体被刻意抬升**(IBM Plex Mono / Berkeley Mono):章节号、时间戳、指标、键值
一律 mono,营造工程日志感。
- 正文 ~16px,行距 1.6;强调靠字重 / 颜色 / 间距,不用斜体(协议级禁用),不用厚黑体。
## Raw 风格
像作者在暗底为当前段落手绘的一张仪表小图。
- 约束的是**气质**(暗底、线条化、信号克制、工程证据),不是**媒介**。
- 典型:暗底内联 SVG、细线折线 / 散点 / 火焰图 / 时序图、可拖动阈值线、mono 标注、
微型 sparkline、终端式状态条。
- 构图:少填充、多线条;每条线都有含义。颜色只用 `--ra-*`,强调用琥珀 accent 或警示红。
- 动效:默认无;必须交互只用即时响应或极轻过渡(≤120ms),不用循环装饰。
## 媒体(图片 / 视频 / 音频)风格
- 适合:真实监控图 / 火焰图 / trace、终端截图、架构 / 时序 / 拓扑细线图、暗底数据可视化、
低饱和产品界面局部。
- 构图暗底留白充足、主体清楚;必须配清晰 caption / source / alt;截图裁掉无关 chrome。
- 色彩贴近暗纸,强调只用琥珀或警示红承载信息。
## 代码 / 公式风格
- `CodeBlock` 是暗底第一公民但仍是工程证据:暗纸轻 surface + 暗发丝线,不用发光边框 /
彩色标题栏 / 玻璃拟态。行号可见但退后。
- Prism token 从主题派生:结构 / 标签 / 函数用琥珀 accent,关键字 / 风险用 risk 红橙,
字符串 / 成功路径用低饱和绿。
- `Formula` 像仪表标注,块级只用发丝线与留白承载。
## 禁止项
- 卡片、面板、填色块、投影、圆角、发光边框;比 `#45423A` 更亮的网格线。
- 纯黑 / `#0D1117` 赛博暗、霓虹高亮、彩虹语法、过饱和科技蓝紫。
- 把琥珀信号色当装饰;第二个红色用于"警示"以外;emoji / 图标当装饰。
- 媒体 / Raw 变成赛博朋克氛围视觉而非工程证据(仪表盘大屏拟物、粒子背景、玻璃拟态)。
## 不同信息密度下的表现建议(建议,非限制)
- `100% longform / postmortem`:克制长文 + 工程证据,Raw 点亮关键信号 / 时序。
- `60-80% system-design / explainer`:保留核心架构图 + 代码证据,正文仍是主体。
- `40% briefing`:Raw 偏暗底图解 / 指标,文字更短,仍是文章形态。
@@ -0,0 +1,57 @@
# Theme Profile · sottsass(孟菲斯 / 80s 撞色)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="sottsass"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/sottsass/sottsass.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`sottsass`(`<ThemeProvider theme="sottsass">`)
- **气质**:后现代 / 孟菲斯(米兰 1981)。暖奶油纸、黑墨,故意撞色的孟菲斯粉彩
(艳粉 / 青绿 / 阳光黄 / 钴蓝)。识别度来自 80s 游戏感:无模糊硬投影、轻微旋转、
圆角彩色药丸、波浪下划线、几何彩屑。响亮欢快但仍可读。
## 适合 / 不适合的文章类型
- **适合**:好玩的科普 / 解释、设计 / 文化 / 潮流写作、发布与活动稿、轻松上手;
"个性大于精致"、敢撞色不端着的内容。
- **不适合**:学术(`knuth`)、黑白大刊(`bodoni`)、冷调规格(`vignelli`)、
暗底工程(`shannon` / `fuller`)、严肃克制内容、安静治愈(`andy` 更柔)、幻灯片。
## 排版气质
- 标题 / 标签用 Space Grotesk(顽皮几何),正文用 Hanken Grotesk(干净 grotesque)。
- 正文 ~17px,行距 1.65;display 字重 700;强调不用斜体(协议级禁用),用色块 / 高亮。
- **撞色是表达、钴蓝是结构**:链接 / 序号固定钴蓝(可读);粉 / 青 / 黄只作大胆填充,绝不作小字。
## Raw 风格
像一页孟菲斯海报式的图解。
- 约束的是**气质**(撞色块、无模糊硬投影、混合圆角、轻旋转、彩屑 / 波纹),不是**媒介**。
- 典型:硬投影卡片、撞色几何图、彩屑 / 波纹装饰、旋转标签、明快的比例 / 步骤图。
- 构图:黑描边 + 偏移硬投影 + 撞色 + 混合圆角;可轻微旋转。
- 动效:带回弹的弹入 / 位移;避免无限霓虹循环。
## 媒体(图片 / 视频 / 音频)风格
- 适合:几何彩屑插画、撞色海报、波纹 / 水磨石纹理、硬投影拼贴、明快人物 / 静物。
- 构图大胆撞色、可旋转 / 不对称、黑描边 + 硬投影;caption 简洁俏皮;必配 alt。
- 色彩粉 / 青 / 黄 / 蓝撞色 + 黑白;不用柔光渐变与霓虹。
## 代码 / 公式风格
- `CodeBlock` 像潮流杂志的代码卡片:暖浅 surface + 黑边 + 适度圆角,可有硬投影;不用暗终端。
- Prism token:标签 / 函数走钴蓝,关键字走红玫 risk,字符串走青 success;撞色在边框 / 高亮,不在语法色彩虹。
- `Formula` 可有圆角 surface 与一抹色,克制对齐。
## 禁止项
- 把粉 / 青 / 黄当正文 / 小字 / 链接色(可读结构固定钴蓝)。
- 柔光模糊阴影(本主题用无模糊硬投影)、廉价渐变、霓虹。
- "撞色 + 硬投影"用力过猛到读不下去(仍要保证正文从容可读)。用斜体强调(协议级禁用)。
## 不同信息密度下的表现建议(建议,非限制)
- `100% 好玩 explainer`:撞色长文 + 旋转章节块 + 硬投影 callout + 彩屑点缀,正文为主体。
- `60-80% 内容`:保留关键硬投影卡 + 撞色图解,波浪下划线点睛。
- `40% briefing`:Raw 偏撞色比例 / 步骤图,文字更短,仍是孟菲斯形态。
@@ -0,0 +1,64 @@
# Theme Profile · tufte(Data-Ink)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="tufte"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/tufte/tufte.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`tufte`(`<ThemeProvider theme="tufte">`)
- **气质**:Edward Tufte 数据墨水比。页面上每一滴墨水都承载信息。去除卡片、
填色、阴影、圆角、装饰色。留下文字、发丝级参考线、充裕页边、由排版承载的含义。
## 适合 / 不适合的文章类型
- **适合**:`longform`、`full-report`、`explainer`、`review`(技术 / 数据型)、
`tutorial`。以论点与证据为主角的长文阅读。
- **不适合**:需要 10 米外观看的幻灯片(正文字号刻意偏小);移动端优先且需要大量
边注的场景。
## 排版气质
- 正文与标题用老式(old-style)衬线(et-book → Palatino / Georgia / 宋体),字重 400。
- 标签 / 注释 / 表头用人文 sans,小字号。
- 正文刻意偏小(~16px),行距 1.6;标题层级克制(h1≈2.5rem,h2≈1.7rem)。
- 强调靠字重 / 颜色 / 间距,不靠斜体(协议级禁用斜体),不靠厚黑体。
## Raw 风格
像作者为当前段落手画的一张小图,不是营销组件。
- 约束的是**气质**(克制、线条化、数据密集、低装饰),不是**媒介** —— 可用任意
HTML / CSS / React,按需才用 SVG / canvas。
- 典型:细线折线 / 散点 / slopegraph、小坐标轴、可拖动阈值线、局部高亮、文本旁注、
微型 sparkline、轻量可调参数控件、紧凑对照排版。
- 构图:少填充、多线条;信息密度可以高,但每条线 / 每个元素都要有含义。
- 动效:默认无;必须交互时只用即时响应或极轻过渡,不用弹跳 / 漂浮 / 循环装饰。
- 颜色 / 字体 / 间距必须取自 `--ra-*`,不自造 palette。
## 媒体(图片 / 视频 / 音频)风格
- 适合:真实数据图、论文 / 报告截图、表格截屏、产品界面局部、低饱和摄影、黑白或
暖纸感线稿、地图 / 时间轴 / 细线示意图。
- 构图留白充足、主体清楚、边缘干净;必须配清晰 caption / source / alt。
- 截图优先裁掉浏览器 chrome。
## 代码 / 公式风格
- `CodeBlock` 像正文里的精密标本:纸面轻 surface + 发丝线,不用深色编辑器壳 / 发光边框 /
彩色标题栏。行号可见但退后。
- Prism token 色从主题派生:结构 / 标签用青灰 accent,关键字 / 警示用暖红 risk,
字符串用低饱和绿。
- `Formula` 像正文里的数学标注,块级公式只用发丝线与留白承载。
## 禁止项
- 卡片、面板、填色块、投影、圆角;比 `#D8D2C2` 更深的网格线。
- emoji / 图标当装饰;色彩用于"承载含义"以外的任何用途。
- 库存感 hero 图、3D 插画、渐变背景、装饰光斑、强饱和科技蓝紫、卡通人物。
- 在 Raw 里写出与 Data-Ink 相反的装饰性组件(仪表盘大屏、粒子背景、玻璃拟态)。
## 不同信息密度下的表现建议(建议,非限制)
- `100% longform / full-report`:克制长文为主,Raw 点亮关键概念,数据证据优先。
- `60-80% explainer / review`:保留核心证据 + 图解,正文仍是主体。
- `40% visual-essay`:仍成立,但 Raw 应偏图解 / 证据、低装饰,文字更短。
@@ -0,0 +1,63 @@
# Theme Profile · vignelli(瑞士国际主义文档)
> 这是给 **AI 写作时读** 的 authoring profile,不是 CSS。CSS token 由组件库
> 运行时主题持有(`data-theme="vignelli"`)。本文件是"如何选择和使用这个主题"。
> 详尽版本见组件库 canonical md:`src/theme/themes/vignelli/vignelli.md`(写代码 /
> 公式 / 媒体 / Raw 前请读它)。
- **runtime theme id**:`vignelli`(`<ThemeProvider theme="vignelli">`)
- **气质**:瑞士国际主义排版。ReActicle **唯一的 sans 正文 + 冷中性**主题。冷中性纸、
一个 grotesque 字族在不同字号上建立层级、发丝网格线、一抹瑞士红、等宽体承载元数据。
完整继承结构纪律(以线代框、去垃圾表格、色彩承载含义)。
## 适合 / 不适合的文章类型
- **适合**:`docs` / 产品文档、`spec` / 技术规格、`changelog` / release notes、
`reference` / SDK·API 参考、AI 工具 / 平台文档、`explainer`、`tutorial`。
需要强结构、可扫读的中性内容。
- **不适合**:温暖叙事(`press`);正式学术论文(`knuth`);暗底工程现场(`shannon`);幻灯片。
## 排版气质
- 正文与标题用单一 grotesque 字族(Söhne → Aktiv Grotesk → Helvetica Neue → Arial),
**靠字号与留白、而非堆字重**建立层级。
- 标签 / 表头用同一字族小字号;元数据 / 代码用等宽体作 metadata "chip"(以线与字重呈现,
绝不做成彩色胶囊或卡片)。
- 正文 ~17px,行距 1.6;大标题带负字距保持紧致。强调不用斜体(协议级禁用)。
## Raw 风格
像一页严格按网格设计的系统文档插图。
- 约束的是**气质**(冷中性、强网格、系统化、可扫读),不是**媒介**。
- 典型:网格示意、流程 / 状态图、规格对照表、键盘 / 快捷键图、可切换参数说明、
带 mono 标注的 SVG。
- 构图:强网格对齐、清晰层级、瑞士红作识别线索;填色克制、服务理解。颜色只用 `--ra-*`。
- 动效:允许短促干脆的一次性过渡(~120ms);避免无限循环装饰。
## 媒体(图片 / 视频 / 音频)风格
- 适合:界面截图(裁净 chrome)、信息图 / 流程图 / 网格示意、图标系统说明、线框 / 规格图、
低饱和中性摄影。
- 构图强网格对齐、留白克制、主体清楚;caption 简洁说明来源;必须配 alt。
- 色彩贴近冷中性体系,强调只用瑞士红承载信息。
## 代码 / 公式风格
- `CodeBlock` 像排版严谨的技术规格:冷纸浅 surface + 发丝线,不用暗色编辑器窗口。行号克制。
- Prism token 从主题派生:标签 / 函数用瑞士红 accent(克制),关键字 / 风险用 risk 深红,
字符串用绿,其余走墨色 / muted。
- `Formula` 像规格里的公式:克制、对齐、发丝线与留白承载。
## 禁止项
- 卡片、面板、填色块、投影、圆角、**彩色左边框强调卡**(被点名的 slop)。
- 把瑞士红当装饰;第二个红色用于"警示"以外;用堆字重代替字号层级。
- 紫粉渐变 SaaS hero、霓虹、Tailwind 默认味、emoji / 图标当装饰、3D 渲染图标。
- Raw / 媒体变成营销落地页或仪表盘大屏。
## 不同信息密度下的表现建议(建议,非限制)
- `100% docs / reference`:系统化长文 + 规格表 + 网格图,正文为主体。
- `60-80% spec / changelog`:保留关键规格 + 流程图,mono 元数据带扫读友好。
- `40% briefing`:Raw 偏网格图解,文字更短,仍是文档形态。
@@ -0,0 +1,98 @@
---
name: ui-craft-editorial
description: "Editorial / magazine / long-form / Medium / Substack / content-heavy UIs. Locked knobs: CRAFT=9, MOTION=4, DENSITY=3. Serif display + humanist body, wide reading column, drop caps, OpenType. Trigger on: editorial, magazine, long-form, blog, Medium-like, Substack-like."
argument-hint: "[action: build|review|polish] [target]"
---
# UI Craft — Editorial
Pre-committed editorial style: magazine, long-form, content-first. Typography carries the hierarchy; layout serves the reading experience.
## Knobs (locked)
- **CRAFT_LEVEL = 9** — editorial typography only works at high refinement; Polish Pass mandatory.
- **MOTION_INTENSITY = 4** — reading-aware: fade on reveal, link hovers, no entrance stagger.
- **VISUAL_DENSITY = 3** — wide reading column, generous vertical rhythm, 1 item per row for features.
Do not re-ask these in Discovery. Confirm accent + serif/sans pairing only.
## Style anchors
- Serif display for headings + humanist body sans/serif for reading.
- Baseline grid feel. `leading-relaxed` (1.7) on body.
- Wide reading column `max-w-[65ch]`. Generous vertical rhythm.
- Accent used for emphasis — pull quotes, drop caps, link underlines — not decoration.
- OpenType features ON: small caps, ligatures, stylistic sets, tabular figures where relevant.
- Asymmetric, text-heavy heroes. Featured image large.
- Motion respects reading: fades only, never pulls the eye away.
- Black text, warm/ivory canvas, one accent. Color is a voice, not a decoration.
## Base rules (inherited)
All rules in `../ui-craft/SKILL.md` apply. This file overrides knob defaults and adds style-specific guidance below. The anti-slop and craft tests still apply in full.
## Style-specific overrides
**Typography (the whole point)**
- Display: Playfair Display, Fraunces, Lora, or Source Serif 4 for headings and pull quotes.
- Body: Alegreya, Source Sans 3, ITC Charter, or a humanist pairing. Do NOT use Inter as primary body — it reads as product UI, not editorial.
- Max two families: one display + one body.
- Headings: `text-wrap: balance`, `font-optical-sizing: auto`, tracking near 0 or slightly negative on large sizes.
- Body: `text-wrap: pretty`, `line-height: 1.7`, `font-feature-settings: "liga", "kern"`.
- Drop caps via `p:first-of-type::first-letter { float: left; font-size: 4.5em; line-height: 0.85; padding: 6px 10px 0 0; font-family: <display>; }`.
- Small caps via `font-variant-caps: small-caps` for section labels, bylines.
- Opt-in OpenType: `font-feature-settings: "ss01", "cv11"` when the chosen font supports them.
- Pull quotes: serif, large (1.75-2.25em), one accent color, short rule above.
**Color**
- Canvas: ivory or warm white — `oklch(98% 0.01 85)` — not pure `#fff`.
- Text: `oklch(18% 0.01 40)` warm near-black.
- Restricted palette: black, ivory/warm white, ONE accent. That's it.
- Accent applied to: drop cap, pull quote, inline link underline, section rule. Not buttons that don't need it.
**Spacing & layout**
- Reading column: `max-w-[65ch]` for body; hero and featured image can break out to `max-w-5xl`.
- Vertical rhythm on body: paragraph spacing = line-height × 0.6 minimum.
- Asymmetric heroes: left-aligned display headline + byline + featured image below or offset.
- Section breaks: centered ornament or a short rule, not a full-width divider.
**Motion**
- Fade on scroll reveal for hero/featured image only, 300-400ms ease-out.
- Inline link hover: underline-offset grows 1px, color shifts to accent, 120ms.
- No entrance stagger, no parallax, no page transitions that fight the reading flow.
- Honor `prefers-reduced-motion` — cut all reveals.
**Content components**
- Byline: small caps, tracking-wide, secondary text color.
- Dateline + read time: tabular-nums, separated by a bullet `·`.
- Inline images: full-width or breakout beyond column, with italic serif caption underneath.
- Blockquote: left border in accent, serif italic, `max-w-[55ch]`.
## Reference files to read first
Load these from `../ui-craft/references/`:
- `typography.md` — the primary reference for this style
- `layout.md` — reading column, rhythm, hierarchy
- `color.md` — warm palette, accent discipline
- `copy.md` — editorial voice, headings, empty states
Skip `dashboard.md`, `sound.md` unless relevant.
## Anti-patterns for THIS style
- Inter, Geist, DM Sans as primary body. They signal product UI, not editorial.
- Three font families. Always one display + one body.
- Pure `#fff` canvas. Warm it.
- Tight `leading-tight` on body copy. Reading needs 1.6-1.8.
- Colored pull quote backgrounds, gradient text on headlines, emoji in section rules.
- Scroll-triggered stagger or parallax — breaks reading flow.
- Card grids for articles — use stacked list with thumbnail + dek.
- Sans-serif display headlines by default. If you need sans display, the user probably wants minimal variant.
- Uppercase headlines. Sentence case, always (small caps for bylines is the exception).
- CTAs with heavy pill backgrounds — prefer inline underlined links in the serif voice.
+393
View File
@@ -0,0 +1,393 @@
---
name: ui-craft
description: "Use for UI design and implementation work to avoid generic AI-looking interfaces. Provides anti-slop rules, a required discovery phase before coding, and guidance for layout, typography, color, motion, accessibility, dashboards, tables, landing pages, theming, and polish. Trigger when editing UI code or reviewing and refining components, pages, screens, layouts, animations, responsive behavior, or design systems."
argument-hint: "[action: build|animate|review|polish|audit] [target]"
---
# UI Craft
You are a design engineer. Every decision below is one you make deliberately and can defend — never a default you inherited.
## The Ladder (use this when explaining ui-craft to the user)
One progression, four rungs. Never describe ui-craft as "layers" or "modes" — use these rung names, and name the rung the user is on before suggesting a command.
| Rung | User wants | They do | They get | Effort |
|------|-----------|---------|----------|--------|
| **0 · Ask** | better UI, zero effort | ask for UI as always | taste by default: real hierarchy, system tokens, no slop | none |
| **1 · Direct** | control one pass | `/craft`, `/critique`, `/polish`, `/animate`, … | a focused pass on one surface | one command |
| **2 · Persist** | consistency across sessions | `/brief`, `/tokens`, `/remember` | durable design context every future session reads | write once |
| **3 · Enforce** | it can't regress | `/finalize`, review agents, MCP gates, score, `ui-craft-detect` | gates in review/CI + a 0-100 number | wire once |
`/sddesign` is **not** a rung — it is the express lane that walks rungs 1 to 3 for one big surface. When a pass finishes, name the natural next step (`/craft` → `/finalize`, `/brief` → `/tokens`, `/audit` → `/harden`).
## Knobs (ask during Discovery, 1-10)
Knobs are **fallback defaults applied only when the user declines to specify**. When the user gives explicit guidance during Discovery — "make it dense", "minimal motion", "ship-fast" — those override the defaults. Knobs are not a starting position; they are a graceful fallback.
- **CRAFT_LEVEL** (default 7) — refinement depth. 3 ships fast, 9 is pixel-perfect.
- **MOTION_INTENSITY** (default 5) — 1 = hover only, 10 = scroll-triggered, magnetic, page transitions.
- **VISUAL_DENSITY** (default 5) — 1 = whitespace-heavy editorial, 10 = dashboard-dense.
- **DESIGN_VARIANCE** (default by surface — see [craft-intent.md](references/craft-intent.md)) — 1 = symmetric/safe layout, 10 = experimental composition. Dashboards default 4; landings 7; portfolios 8. Gates layout risk separately from density.
Behavior: **CRAFT_LEVEL 8+** → run Polish Pass ([review.md](references/review.md)). **≤4** → skip it. **MOTION_INTENSITY ≤3** → hover only, no entrance/stagger/scroll animations. **4-7** → standard entrances + hover, one scroll reveal max per section. **8+** → scroll-linked, page transitions, magnetic cursor OK (still honor `prefers-reduced-motion`); load [stack.md](references/stack.md) if user opts in. **VISUAL_DENSITY ≤3** → wide spacing, 1-2 items/row. **8+** → dashboard-dense ([dashboard.md](references/dashboard.md)). **DESIGN_VARIANCE ≤4** → symmetric grids, safe product layouts. **5-7** → split heroes, alternating rows, one layout break. **8+** → display-scale drama, asymmetric marketing compositions; **9-10** only when user asks for experimental or brief demands it ([craft-intent.md](references/craft-intent.md)).
## Quick Start: Top 12
The rules that make the biggest difference between "AI-generated" and "designed by a human":
0. **Ask before assuming** — never default accent, font, or style. Analyze project, then ask. Use Knob defaults only when the user explicitly declines to specify.
1. **Sentence case by default** — uppercase = template. Exception: 11-13px category labels with wide tracking — an eyebrow above every heading is template grammar; budget formula in [recipe-landing.md](references/recipe-landing.md) (Eyebrow budget).
2. **90%+ neutral, one accent** — mostly black/white/gray; single brand color. NEVER *default* to blue — if your brand is blue, that's different.
3. **Vary border-radius** — 6px inputs, 10px cards, 14px modals (steps from the radius token scale in [tokens.md](references/tokens.md)); uniform radii look stamped out.
4. **Real SVG icons, not emoji** — use the project's existing icon set first; if none, pick one consistent SVG library (Lucide, Heroicons, Phosphor) and never mix two.
5. **Tight letter-spacing on large headings** — `tracking-tight` or `-0.02em`+ above 24px.
6. **One body font, optionally a second for display** — never mix three by accident. Inter/Geist/DM Sans are safe fallbacks when no brand font exists.
7. **Layered shadows over flat borders** — ambient + direct light.
8. **Exit faster than enter** — ~75% of entrance duration.
9. **Plain secondary text for comparisons** — "+12.5% from last month", not a colored pill.
10. **Accent budget: one accent color, 3-5 placements of it per above-the-fold viewport** — CTA, one key metric, active states. **Why:** Hick's Law — every accent placement competes for attention budget; >5 dilutes the focal point. Modals and overlays count as their own viewport.
11. **Every section earns its space** — if it doesn't answer a question or drive action, cut it.
12. **One signature detail per UI** — subtle motif, layout break, custom markers, distinctive hover. On `/craft`, pick and **build** it in the first pass ([craft-intent.md](references/craft-intent.md)) — not only at polish.
> **Before writing ANY code:** For non-trivial projects, run `/brief` and `/tokens` first — durable artifacts beat per-session re-derivation. Then run Stack Detection + Discovery Phase. Use existing tokens if any token system is present. If none exists, establish a minimal token set before writing components — at minimum: spacing scale, neutral ramp, one accent, two type sizes for body and display (see [layout.md](references/layout.md) and [color.md](references/color.md)). If preferences are missing, ask.
## Routing
**If the ui-craft MCP server is connected, call `route_task` with the user's own words before reading anything below.** It returns the ranked references, commands and tools that cover the task plus the first move, and it resolves vocabulary this table cannot: "an analytics panel" reaches `recipe-dashboard.md`, "pricing block" reaches `recipe-landing.md`. **Why:** a table only fires when the user's words match our filenames, and they usually don't. The table below is the fallback when no MCP is available — and it stays authoritative for what each entry *is*, since `route_task` returns pointers only.
| Intent | Pass / Reference |
|--------|------------------|
| New here / unsure where to begin | Run `/start` → reads the project, reports what's available now, routes you to the right next step |
| Pre-build: write the project's design brief | Run `/brief` → see [brief.md](references/brief.md) |
| Pre-build: establish or audit token spine | Run `/tokens` → see [tokens.md](references/tokens.md) |
| Build a surface end-to-end with the full spec-driven pipeline (brief → tokens → shape → craft → converge → ship) | Run `/sddesign` → walks all gates, writes `.ui-craft/spec.md`, orchestrates existing phase commands |
| Build a surface in one shot (known composition, no pipeline needed) | Run `/craft <surface>` → outcome recipes: [recipe-dashboard.md](references/recipe-dashboard.md), [recipe-landing.md](references/recipe-landing.md), [recipe-auth.md](references/recipe-auth.md) |
| Pick a ready-made theme (no token system exists) | [themes.md](references/themes.md) — 4 production token presets |
| Building new UI | **Build pass** (rung 0/1) — this file + relevant references |
| Adding/fixing animations | **Motion pass** — [motion.md](references/motion.md) |
| Reviewing existing UI | **Review pass** — [review.md](references/review.md) — ends with a Craft Report |
| Polishing existing UI | **Polish pass** — this file + [review.md](references/review.md) Polish Pass — ends with a Craft Report |
| Multi-stage animations | [animation-storyboard.md](../../examples/animation-storyboard.md) |
| Layout / spacing | [layout.md](references/layout.md) |
| Typography (focused pass: `/typeset`) | [typography.md](references/typography.md) |
| Color / theming / dark mode (focused pass: `/colorize`) | [color.md](references/color.md) |
| Accessibility / a11y audit (technical audit: `/audit`) | [accessibility.md](references/accessibility.md) |
| UX critique, no code changes | Run `/critique` — [review.md](references/review.md) + [inspiration.md](references/inspiration.md) |
| Production hardening (states, i18n, edge cases) | Run `/harden` — [state-design.md](references/state-design.md) + [coverage.md](references/coverage.md) |
| "What's missing from this screen?" / completeness check on a table, settings, checkout, pricing, docs, invite, delete-confirm, onboarding | Call `ux_coverage` (MCP) or read [coverage.md](references/coverage.md) — the **completeness** axis, reported beside distinction, never folded into a score |
| Cut noise / simplify an over-built surface | Run `/distill` |
| Redesign / modernize an existing site without losing brand, IA, or SEO | Run `/redesign` — audit first, preserve list, refresh/reskin/rebuild scope |
| Amplify personality / "make it bolder" | Run `/bolder` — [craft-intent.md](references/craft-intent.md) |
| Tone down / "quieter", "more restrained" | Run `/quieter` — [craft-intent.md](references/craft-intent.md) |
| Extract repeated patterns into components/tokens | Run `/extract` — [layout.md](references/layout.md), [typography.md](references/typography.md), [color.md](references/color.md) |
| Purposeful micro-interactions | Run `/delight` — [motion.md](references/motion.md) |
| Animation performance | [motion.md](references/motion.md) — Rendering Performance section |
| Advanced CSS / View Transitions | [modern-css.md](references/modern-css.md) |
| Sound design | [sound.md](references/sound.md) |
| UX copy / voice / tone / microcopy (focused pass: `/clarify`) | [copy.md](references/copy.md) — errors, empty states, CTAs, voice matrix, reading level, locale, inclusive language |
| Responsive (focused pass: `/adapt`) | [responsive.md](references/responsive.md) |
| Page metadata correctness (title/description/canonical, social cards, structured data, favicons) | [metadata.md](references/metadata.md) |
| Three.js / GSAP / Motion | [stack.md](references/stack.md) — **OPT-IN ONLY — do not load unless user chose Motion/GSAP/Three.js in Discovery Step 2** |
| Scored critique / PM-ready audit | [heuristics.md](references/heuristics.md) + [personas.md](references/personas.md) — load for `/heuristic` |
| State-first design (before happy path) | [state-design.md](references/state-design.md) — load for `/unhappy` |
| Data visualization / charts / dashboards | [dataviz.md](references/dataviz.md) — Cleveland-McGill, color for data, Tufte |
| Motion system / tokens / choreography | [motion.md](references/motion.md) — duration + easing scale, motion budget |
| Wireframe-first / shape a new screen | Run `/shape` before coding; see state lattice + content inventory |
| AI / chat / streaming surfaces | [ai-chat.md](references/ai-chat.md) — streaming contract, tool traces, citations, feedback |
| Forms (multi-step, validation timing, autosave) | [forms.md](references/forms.md) — holistic form system design |
| Component anatomy (buttons, menus, modals, search, cards, nav) | [components.md](references/components.md) — contracts below the surface level |
| Pre-ship: finalize gate (full bar before merge) | Run `/finalize` → see [finish-bar.md](references/finish-bar.md) |
| Iterate a surface until a quality bar passes (converge, not one-shot) | [loops.md](references/loops.md) — loop engine + presets; wired into /finalize, /unhappy, /tokens |
| Remember a design correction (record as a learned constraint) | Run `/remember` → [brief.md](references/brief.md) |
| Parallel design + a11y verify (fresh-context, read-only, run both simultaneously on a diff/file) | Delegate `ui-craft:design-reviewer` + `ui-craft:a11y-auditor` together → [agents.md](references/agents.md). Agents = fresh-context parallel delegation; `/critique` + `/audit` = inline commands in the caller's context. Use agents for dedicated review passes and PR audits; use commands for interactive build sessions. |
| Ambiguous | Ask which mode |
**Overlap with other skills:** defer marketing copy to a copywriting skill; defer SEO strategy to an SEO skill — UI Craft covers the correctness of metadata already being emitted ([metadata.md](references/metadata.md)), not keyword or ranking strategy. UI Craft is the visual and interaction layer.
**Out of scope.** These are surface classes the recipes do not help with. Say so, name the right tool, and still apply UI Craft to the web surfaces around them — a brief containing one of these is rarely only that.
| Not this | Use instead |
|---|---|
| Code editor surfaces (syntax, gutters, diff views) | Monaco or CodeMirror with their own theming API |
| Native mobile apps | Apple HIG or Material directly — UI Craft covers web |
| Realtime collaboration UI (presence, live cursors, conflict states) | Liveblocks, or Yjs / Automerge if you own the sync layer — the recipes assume a single actor |
| HTML email | MJML or a dedicated email framework — the CSS rules here are void in mail clients |
Refusing with a pointer beats confident bad output. Silence produces the second one.
---
## Stack Detection (Always Run First)
Detect the styling approach from signals: Tailwind (`tailwind.config.*`, `@tailwind`), CSS Modules (`*.module.css`), styled-components/Emotion (`styled(...)`, `css\`...\``), CSS-in-JS (`*.styles.ts`, vanilla-extract, Stitches), SFC (`<style scoped>` in Vue/Svelte/Astro), or Vanilla CSS.
**Rules:** never fight the project's stack; never mix approaches. The design rules hold across stacks — only the syntax changes. (Context can still invert a rule — that's [When Rules Break](#when-rules-break), and it's about the design context, never the stack.) Reference files are CSS-first with Tailwind translations. When in doubt, match existing patterns.
### Tailwind Translations (common)
`tracking-tighter` / `tabular-nums` / `text-balance` / `motion-reduce:` / `focus-visible:ring-2` / `touch-manipulation` / `min-h-11` (44px). Use `ease-[cubic-bezier(...)]` for custom easing.
**Tailwind anti-slop:** avoid `bg-gradient-to-r from-purple-500 to-cyan-500`, `animate-bounce`, heavy glow shadows. Tailwind makes it easier to ship slop faster.
---
## Discovery Phase (Always Run First)
Before applying any design decisions, discover what the project has and what the user wants. Never *default* to blue, Inter, or any style without checking — if the brand calls for blue, that's different.
### Step 1: Project Analysis
**Design Memory (`.ui-craft/` directory).** This is the project's typed design context. It replaces the single `brief.md` with a structured directory — all files are plain markdown, committable to git.
**Always-load on every UI task** (small, define project taste/tokens):
- `.ui-craft/brief.md` — product identity, design intent, audience, voice, constraints. See [references/brief.md](references/brief.md) for the format guide.
- `.ui-craft/tokens.md` — the project's actual token decisions (colors, type, spacing, radius, shadows).
**Lazy-load only when the task needs them** (growing logs — always loading bloats context unnecessarily):
- `.ui-craft/decisions.md` — append-only date-stamped design decision log. Load when the user asks to reference prior rationale or past decisions.
- `.ui-craft/patterns.md` — validated component/layout compositions. Load when the task references a known pattern or the user asks to reuse one.
- `.ui-craft/surfaces/<name>.md` — per-surface notes (layout, components, edge cases). Load only the surface file matching the current task; do NOT load all surface files eagerly.
**If `.ui-craft/` is absent:** proceed without error — no design memory files are loaded. Recommend `ui-craft install` to scaffold the directory when the user wants to establish project-level design context.
The brief includes **Learned constraints** — corrections the user made on this project, each a binding design fact. Apply them like principles: they override skill defaults, never the a11y/correctness floor.
Scan for existing tokens: CSS variables (`--color-*`, `--font-*`, `--accent-*`), Tailwind config (`theme.extend.*`), globals.css, font imports, next/font, component library theme (shadcn, MUI), design-tokens files. Build an inventory (accent, fonts, radius, shadows). If the project has an intentional system, respect it. Don't override.
If a token system is present but incomplete (no semantic layer, no intentional dark mode, missing categories), recommend `/tokens` to audit and fill gaps. Cross-ref [tokens.md](references/tokens.md) for the 3-layer contract.
### Step 2: Ask the User (Quick Ask)
If tokens are missing or ambiguous, ask in one compact prompt:
> "Before I build: (1) Design style — minimal, soft modern, sharp geometric, editorial, dark premium, or playful? (2) Accent color preference? (3) Font — clean sans-serif, geometric, humanist, monospace, or system? (4) Animation stack — Motion / GSAP / Three.js / none? (I'll load `references/stack.md` only if you opt in.)"
Style choices (brief): **Minimal Clean** (whitespace-heavy, monochrome + one accent, hairline borders, tight type), **Soft Modern** (rounded cards, generous spacing, gradient-tinted neutrals, soft shadows), **Sharp Geometric** (precise grids, mono numbers, hard edges, semantic palette), **Rich Editorial** (serif display + humanist body, wide reading column, deliberate asymmetry), **Dark Premium** (deep neutrals, restrained accent, surface elevation via tint over shadow), **Playful Bold** (saturated palette, asymmetric layouts, expressive type, custom illustration). Style is independent of color scheme — default to light unless user asks for dark.
### Step 3: Apply Decisions
The project's own code becomes the source of truth — no external config file. **Shortcut:** if user provides accent + font + style in the prompt, skip Discovery. See style-to-CSS mapping in [layout.md](references/layout.md).
### Craft Read (full surfaces)
When building a complete surface (dashboard, landing, auth, settings shell, portfolio page) — including `/craft` — output the **Craft Read** before writing code, in exactly this form:
> **Craft Read:** *[surface kind] for [audience], [product | marketing] language, [theme/accent hint], variance [N], signature bet: [choice].*
The template is here rather than only in [craft-intent.md](references/craft-intent.md) on purpose. **Why:** an instruction to emit a form, with the form in another file, produces the right *elements* in an improvised shape whenever that file is not loaded — a planning paragraph instead of the line the user can react to. A pointer to a form is not the form.
Then load the recipe for the surface **before writing code**, not after: dashboard → [recipe-dashboard.md](references/recipe-dashboard.md), landing → [recipe-landing.md](references/recipe-landing.md), auth → [recipe-auth.md](references/recipe-auth.md). **Why:** every numeric limit that keeps a surface from reading as a template lives in its recipe (hero subtext ≤20 words, eyebrow budget, form column width, acceptance bar). Skipping the recipe does not soften those limits — it removes them, and the build breaches them without ever seeing them. If the MCP server is connected, `route_task` names the recipe for you.
Pick **DESIGN_VARIANCE** and a **signature bet** in that line; full rationale, variance defaults and worked examples in [craft-intent.md](references/craft-intent.md). The user steers in plain language ("more like X", "bolder", "quieter") — no design vocabulary required.
---
## Core Rules (Always Apply)
### The Anti-Slop Test
Before shipping any UI, ask: "If someone said AI made this, would they believe it immediately?" If yes, start over.
**Critical (immediately reads as AI):**
- Identical card grids (icon + heading + text, 3-6x repeated)
- ALL CAPS on headings, labels, tables, nav, buttons (exception: 11-13px category labels)
- Purple/cyan gradient everything
- Emoji as feature icons
- Bounce/elastic easing curves
- Glassmorphism on dark + neon accents
**Major (designers notice):**
- Colored pills on trend percentages — use plain secondary text
- Thick colored left/top borders on cards — use elevation or bg tint
- Uniform border-radius on everything — vary by element
- Gradient text on hero metrics
- Vertical bar charts for time-series — use area/line (horizontal bars OK for categorical)
- `transition: all` — list specific properties
- Decorative glow as primary affordance
- Soft blurry gradient blobs/orbs
- Generic CTAs ("Learn more", "Click here") — be specific
- Walls of text — no landing section > 2-3 sentences
- "OR" divider in caps between auth options — lowercase it: "or with email"
- Full-bleed saturated brand panel beside a sign-in form — tinted neutral surface with one proof asset ([recipe-auth.md](references/recipe-auth.md))
- Uppercase tracked eyebrow above every section heading — ration to max 1 per 3 sections; one deliberate kicker is voice, one per section is template grammar
- Numbered section eyebrows ("01 · About", "02 / Process") — numbers earn their place only when content is a real ordered sequence
- Scroll cues ("Scroll to explore", ↓ arrows) — the fold composition should imply continuation, not label it
- Two CTA labels with the same intent on one page ("Get in touch" + "Let's talk") — one label per intent, reused everywhere
- Fake product screenshots built from styled `<div>` rectangles — use a real screenshot, a real mini component, or editorial imagery; never a div mockup
- Logo walls as plain text wordmarks — use real SVG marks; for invented brands, a simple monogram mark, never a styled `<span>`
- Carousels without narrative purpose — a carousel earns its place only when order tells a story (steps, timeline); as a "fit more stuff" device it hides content and reads as template
- App UI built from stacked cards instead of a real layout — cards are for peer items in a collection; wrapping every section in a rounded card is avoidance of layout decisions
- Em-dash flood in UI strings — 3+ em dashes in visible copy is prose grammar leaking into interface grammar; restructure with periods, colons, or separate elements
**Minor** (polish that separates good from great — full list in [review.md](references/review.md) Polish Pass): no `tabular-nums` on data, missing `text-wrap: balance`, straight quotes, no `&nbsp;` in brand names, testimonial star ratings, hero metric without adjacent context.
### The Craft Test (What TO Do)
Anti-slop says what to avoid. Craft says what to aim for.
**General craft:**
- One accent, 3-5 placements per above-the-fold viewport. Never two competing accents at the same chroma + saturation — the eye reads them as a tie and stalls. Two accent hues are acceptable when one is clearly subordinate (lower chroma, smaller surface).
- White backgrounds with barely-there borders or whitespace. Numbers large, undecorated, `tabular-nums`.
- Comparisons plain secondary text. One chart color at different opacities. Area fill fades ~15% → 0%.
- Functional color only — dots for status, flags for countries. Real content, not placeholders.
**Landing pages** (detail in [inspiration.md](references/inspiration.md)):
- Hero — center is fine if asymmetric supporting elements break the symmetry (offset badges, staggered social proof, side-weighted graphics). Avoid is center-everything with every row perfectly symmetrical — that reads as template. One headline (48-72px, tight tracking), one paragraph, dual CTAs, social proof below.
- Features: 2-3 asymmetric rows with real visuals (chart, timeline, funnel). NEVER uniform 3-column icon grids.
- Sections breathe: 80-160px between majors, varied for rhythm (dense products sit low, editorial high — production range in [inspiration.md](references/inspiration.md)). Every section answers one question.
- Prefer specific metrics over vague praise ("Build times 7m → 40s" beats "trusted by thousands").
**Dashboards** (detail in [dashboard.md](references/dashboard.md)):
- Sidebar: subtle bg tint, NOT full dark (common AI pattern).
- Metric cards: primary gets accent tint; others neutral. Sparklines on all. NEVER identical colored top borders.
- At least 3 content types per dashboard viewport (e.g., chart + table + metric). **Why:** uniform grids of identical cards trigger the AI-template tell; variety signals editorial decision. Chart type matches data story (area/horizontal bar/sparkline). Never pie or 3D.
### When Rules Break
Every rule above has a context where it inverts. Stating the rule is half the work; knowing when it doesn't apply is the other half.
- **"Never ALL CAPS on headings"** — small category labels (10-13px) with positive tracking are an exception in editorial layouts. The size shift removes the shouty-bigness; the tracking compensates for descender loss.
- **"One accent only"** — multi-tenant dashboards (where each tenant has its own brand) and editorial sites with explicit color systems are exceptions. The rule is "one accent per consistent design surface", not "one accent ever".
- **"Avoid pie charts"** — for two-segment proportional comparisons (e.g., used vs. free storage on a single device), a donut with center label is acceptable. The rule covers multi-segment pies, which fail Cleveland-McGill perceptual ordering.
- **"No emoji as feature icons"** — affordance contexts where emoji are user content (reactions, message-thread emoji rosters) are not slop, they are content. The rule covers decorative emoji standing in for designed icons.
- **"Never gradient text on metrics"** — branded marketing pages can use gradient on a single hero metric where the gradient is the brand expression, not decoration. Inside-the-product metrics still follow the rule.
- **"Never mix three typefaces"** — deliberate three-family type systems (display serif + body sans + mono for data/code) are standard in editorial and data-heavy products. The rule targets accidental font accumulation, not a designed hierarchy where each family has a named role.
**The general principle:** every rule encodes a default that prevents the most common failure mode. When the context inverts the failure mode, the rule may invert too. The work is recognizing the inversion, not memorizing exceptions.
### Convergence
Commands can iterate until their quality gate passes rather than producing a single-shot output. The engine never fakes a render and always reports honest confidence. See [loops.md](references/loops.md).
### Self-Correction
When the user corrects design output — "not like that", "I don't like this", "always do X here", "never Z", or a reversal that reads as a standing preference — record it as a **learned constraint** in the brief (section 6; run `/remember`). Capture the **why**, not just the what, so it generalizes; confirm in one line where it landed; don't re-litigate a correction already recorded. Learned constraints rank with the principles: they override skill defaults but never the a11y/correctness floor — if a correction would breach the floor, apply the closest compliant interpretation and say so.
This is project-scoped and lives in the brief by design — ui-craft is a UI skill, not a general memory engine. Cross-project corrections ("in all my projects") are general memory: mirror them to an external memory service if one is available, else note that cross-project recall needs one. Full behavior → [brief.md](references/brief.md) (Self-Correction).
### Animation Decision Ladder
> **Should this animate?** → High-frequency? No. Not communicating hierarchy/state/space? Cut it. Otherwise: ≤400ms (most UI 150-300ms; 400ms only for page transitions/drawers), GPU-only, `prefers-reduced-motion` honored.
Full ladder, easing, springs, stagger, interaction contract → [motion.md](references/motion.md) (**Decision Ladder**).
### Design Rules (core)
Layered shadows (ambient + direct). Semi-transparent borders + shadows for crisp edges. Hue-consistent borders/shadows/text on colored surfaces. APCA over WCAG 2. Interactions increase contrast. `color-scheme` + `theme-color` match theme. OKLCH for scales. Full detail in [layout.md](references/layout.md) and [color.md](references/color.md).
---
## Review Format (Required)
When reviewing UI code, use a markdown table. Never use "Before:"/"After:" on separate lines.
| Before | After | Why |
| --- | --- | --- |
| `transition: all 300ms` | `transition: opacity 200ms ease-out` | `all` animates unintended things |
| No focus-visible style | `focus-visible:ring-2 ring-offset-2` | Keyboard users need visible focus |
| `color: gray` for disabled | `opacity: 0.5` + `cursor: not-allowed` | Multiple signals, not just color |
Prioritize findings by impact:
1. **Critical** — blocks usability/a11y (missing focus, broken keyboard nav, no reduced-motion)
2. **High-impact** — immediately noticeable (wrong font, default blue, identical card grids, no hover states)
3. **Quick wins** — big polish (tabular-nums, letter-spacing, curly quotes, `&nbsp;`)
---
## Quick Decision Frameworks
### Should This Animate?
| Frequency | Decision |
|-----------|----------|
| High (keyboard, toggles, typing) | No animation. Speed is the feature. |
| Medium (hover, list nav) | Minimal — under 150ms or remove |
| Low (modals, page transitions) | Standard — 200-300ms, clear purpose |
| One-time (onboarding) | Can be expressive — tell a story |
### Motion Budget
| Element | Budget |
|---------|--------|
| Color/opacity | 100-150ms |
| Small UI (tooltips, dropdowns) | 150-200ms |
| Medium UI (modals, panels) | 200-300ms |
| Large UI (page transitions, drawers) | 300-400ms |
Full easing curves, spring configs, stagger rules, and interaction rules → [motion.md](references/motion.md).
---
## Reference Files
Tiered by signal. Tier 1 is required reading before writing any UI; lower tiers load on context.
### Tier 1 — Always load before writing UI
Two entries, because two is what a build actually loads. This list used to hold seven; build evals showed a passing dashboard opened one of them and a passing landing opened two, while both reached for the surface recipe that was filed a tier below. A required list that nobody reads is not a standard, it is a wish — so the list now names what carries the build, and everything else below states the trigger that pulls it in.
| Reference | When to Read |
|-----------|--------------|
| [craft-intent.md](references/craft-intent.md) | Craft Read, DESIGN_VARIANCE, signature bets, product + marketing build patterns. The one reference every full-surface build needs. |
| **The surface recipe** | [recipe-dashboard.md](references/recipe-dashboard.md) · [recipe-landing.md](references/recipe-landing.md) · [recipe-auth.md](references/recipe-auth.md) — whichever matches what you are building. Every numeric limit that keeps a surface off template grammar lives here (hero subtext ≤20 words, eyebrow budget, form column width, acceptance bar). Skipping it does not soften those limits, it removes them. |
### Tier 1b — Load on trigger
Same references as before, same weight when their trigger fires. Only the claim changed: these are conditional, and pretending otherwise made the whole Tier-1 label unreliable — including for the two above.
| Reference | Trigger |
|-----------|---------|
| [brief.md](references/brief.md) | `.ui-craft/brief.md` exists — then read it first, it anchors every decision and its learned constraints override skill defaults. Absent: run `/brief` or proceed from Discovery. |
| [tokens.md](references/tokens.md) | The project has a token system to respect or extend, or you are establishing one. With neither, [themes.md](references/themes.md) gives a production preset in one step. |
| [accessibility.md](references/accessibility.md) | Any form, any custom interactive control, any focus or keyboard work. The a11y floor in Core Rules is the minimum; this is the detail. |
| [color.md](references/color.md) | Choosing or changing a palette, building dark mode, or auditing accent budget. Not needed to apply a preset. |
| [layout.md](references/layout.md) | Composing a surface from scratch, or a spacing/hierarchy pass. Not needed when a recipe already prescribes the composition. |
| [inspiration.md](references/inspiration.md) | Highest-signal reference in the skill. Read it when the build needs an archetype or a signature detail, and whenever the result feels generic. |
> **What this measured, and what it did not.** The evidence is greenfield builds — an empty
> sandbox with no brief and no tokens, so `brief` and `tokens` could not have been read
> whatever the label said. It shows the label was not causing loads. It does **not** show the
> references are unnecessary: `layout` and `color` govern craft that the deterministic scorers
> barely test, so a build can pass every check with mediocre spacing rhythm. Re-measure before
> trimming anything further, and re-measure on a project that already has a design system.
### Tier 2 — Surface-specific (read when building this surface)
| Reference | When to Read |
|-----------|--------------|
| [spec.md](references/spec.md) | Durable composition spec at `.ui-craft/spec.md` — the "what". Written by `/shape` Step 6, walked by `/sddesign`. Read after `brief.md` when a spec exists for the surface being built. |
| **Outcome recipes** | Promoted to Tier 1 — see *Always load*. Listed there and not here so the recipe has one home; two descriptions of the same requirement is how one of them goes stale. |
| [themes.md](references/themes.md) | 4 named production token presets (Graphite, Porcelain, Carbon, Signal). Load when no token system exists. |
| [dashboard.md](references/dashboard.md) | Dashboards, metric cards, charts, tables, sidebar, filters. |
| [forms.md](references/forms.md) | Validation timing, progressive disclosure, multi-step wizards, autosave, optimistic submit. |
| [components.md](references/components.md) | Component anatomy contracts: buttons (padding ratio, icon-side semantics), menus (5-option threshold, scroll affordance), modals (verb labels, ways out), search, content cards, nav bar. |
| [ai-chat.md](references/ai-chat.md) | Streaming contract, 7-state affordance model for AI surfaces, tool traces, citations, generative UI. |
| [review.md](references/review.md) | Critique methodology, Polish Pass, common issues, component craft. Load when reviewing or refining. |
| [finish-bar.md](references/finish-bar.md) | 10-pass finishing protocol. Load on `/finalize` or CRAFT_LEVEL ≥ 8. |
| [loops.md](references/loops.md) | Loop engine: read→evaluate→fix-one→re-evaluate→stop contract + 3 presets. Load when converging /finalize, /unhappy, or /tokens audit. |
| [principles-catalog.md](references/principles-catalog.md) | 42 example design principles across 8 product categories. Load during `/brief` principles workshop branch as conversation seed. |
### Tier 3 — Foundations (read for the relevant discipline)
| Reference | When to Read |
|-----------|--------------|
| [typography.md](references/typography.md) | Scale, font choice, readability, weight — scoped per script and role. |
| [motion.md](references/motion.md) | Decision ladder, duration + easing scales with perceptual grounding, interaction rules, motion-gap audit. |
| [modern-css.md](references/modern-css.md) | View Transitions, scroll timelines, container queries, `@starting-style`. |
| [responsive.md](references/responsive.md) | Mobile/tablet/desktop, breakpoints, touch zones. |
| [metadata.md](references/metadata.md) | Title/description/canonical consistency, deterministic metadata, social cards, noindex on staging, structured data honesty, favicons. |
| [copy.md](references/copy.md) | Voice/tone matrix, reading level (Flesch ≥70), terminology, inclusive language, errors, empty states, CTAs. |
| [sound.md](references/sound.md) | Web Audio, UI sound, appropriateness matrix. Rare — load when explicitly building audio feedback. |
### Tier 4 — Opt-in (only on explicit request or specific commands)
| Reference | When to Read |
|-----------|--------------|
| [stack.md](references/stack.md) | Three.js / GSAP / Motion — **opt-in only — do not load unless user chose Motion/GSAP/Three.js in Discovery Step 2.** |
| [heuristics.md](references/heuristics.md) | Nielsen's 10 + 6 design laws (Fitts, Hick, Doherty, Cleveland-McGill, Miller, Tesler) + 1-5 rubric. Load for `/heuristic`. |
| [personas.md](references/personas.md) | 5 persona walkthroughs (first-timer / power / low-bandwidth / screen-reader / one-thumb). Load for `/heuristic --persona=<name>`. |
| [state-design.md](references/state-design.md) | State lattice — idle / loading / empty / error / partial / conflict / offline. Load for `/unhappy`. |
| [coverage.md](references/coverage.md) | UX coverage — the parts 12 screen archetypes need to be complete (data table, settings, search, detail view, first-run, billing, pricing, docs, checkout, onboarding, destructive confirm, invite). The **completeness** axis, not distinction. Load for `/harden`, or when asked what a screen is missing. **Prefer the `ux_coverage` MCP tool** — it returns one archetype instead of all twelve. Generated from `mcp/src/coverage-data.mjs`; do not edit by hand. |
| [dataviz.md](references/dataviz.md) | Cleveland-McGill perceptual hierarchy, chart selection matrix, ColorBrewer/Okabe-Ito palettes, Tufte, direct labeling. Load when designing charts. |
| [agents.md](references/agents.md) | Agent pack overview: `design-reviewer` + `a11y-auditor` roles, agent-vs-command guidance, and parallel verify-team usage pattern. Load when setting up or describing the verify team. |
@@ -0,0 +1,182 @@
# Accessibility
WCAG compliance, keyboard navigation, focus management, ARIA, and forms.
---
## Priority Categories
| Priority | Category | Impact |
|----------|----------|--------|
| 1 | Accessible names | Critical |
| 2 | Keyboard access | Critical |
| 3 | Focus & dialogs | Critical |
| 4 | Semantics | High |
| 5 | Forms & errors | High |
| 6 | Announcements | Medium-high |
| 7 | Contrast & states | Medium |
| 8 | Media & motion | Low-medium |
---
## 1. Accessible Names (Critical)
- Every interactive control MUST have an accessible name
- Icon-only buttons: `aria-label` or `aria-labelledby`
- Every input/select/textarea: associated `<label>`
- Links: meaningful text (never "click here")
- Decorative icons: `aria-hidden="true"`
```html
<!-- Icon-only button -->
<button aria-label="Close"><svg aria-hidden="true">...</svg></button>
<!-- Labeled input -->
<label for="email">Email</label>
<input id="email" type="email" />
```
## 2. Keyboard Access (Critical)
- **Never `<div>` or `<span>` as buttons** without full keyboard support — use `<button>`
- All interactive elements reachable by Tab
- Focus visible for keyboard users (`:focus-visible`)
- Never `tabindex > 0`
- Escape closes dialogs/overlays
- Full keyboard support per [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/)
## 3. Focus & Dialogs (Critical)
- Modals MUST trap focus while open
- Restore focus to trigger on close
- Set initial focus inside dialogs
- Opening dialog must not scroll page unexpectedly
- Never `outline: none` without visible focus replacement
## 4. Semantics (High)
- Prefer native elements (`button`, `a`, `input`, `label`, `table`) before ARIA
- If role used, required aria attributes must be present
- Lists use `ul`/`ol` with `li`
- Don't skip heading levels; hierarchical `<h1>`–`<h6>`
- "Skip to content" link
- Tables use `<th>` for headers
## 5. Forms & Errors (High)
- Errors linked to fields via `aria-describedby`
- Required fields announced
- Invalid fields use `aria-invalid="true"`
- Helper text associated with inputs
- On submit, focus first error
- Never block paste
- Disabled states explain why (not just grayed out)
```html
<input id="email" aria-describedby="email-err" aria-invalid="true" />
<span id="email-err">Please enter a valid email address</span>
```
## 6. Announcements (Medium-high)
- Use `aria-live="polite"` for toasts and inline validation
- Loading states use `aria-busy` or status text
- Toasts must not be the only way to convey critical information
- Expandable controls use `aria-expanded` and `aria-controls`
## 7. Contrast & States (Medium)
- APCA contrast preferred over WCAG 2
- Hover-only interactions MUST have keyboard equivalents
- Disabled states don't rely on color alone
- Interactions (`:hover`, `:active`, `:focus`) have MORE contrast than rest state
- Never remove focus outlines without visible replacement
## 8. Media & Motion (Low-medium)
- Images: correct alt text (meaningful or empty `alt=""`)
- Videos with speech: provide captions
- **`prefers-reduced-motion`** on every animation:
```css
@media (prefers-reduced-motion: reduce) {
.animated { animation: none; transition: none; }
}
```
```jsx
const shouldReduceMotion = useReducedMotion();
const initial = shouldReduceMotion ? false : { opacity: 0, y: 20 };
```
- Gate hover animations:
```css
@media (hover: hover) and (pointer: fine) {
.element:hover { transform: scale(1.05); }
}
```
- No autoplaying media with sound
---
## Touch Targets
- Minimum: **44px** (Apple/WCAG recommendation)
- If visual element < 24px, expand hit area with pseudo-element:
```css
.small-button {
position: relative;
}
.small-button::before {
content: "";
position: absolute;
inset: -8px -12px;
/* Expands clickable area without changing visual size */
}
```
---
## Color-Blind Safe
- Never rely on color alone for status — include icons/text labels
- Test red/green combinations specifically
- Use color-blind-friendly palettes for charts
- Redundant status cues always
---
## Quick Checklist
Every UI you build or review must pass these:
- [ ] `prefers-reduced-motion` respected on every animation — with meaningful fallbacks:
- Fade+slide entrance → just appear instantly (`opacity: 1`, no transform)
- Spring/bounce → simple opacity fade (`200ms ease`)
- Parallax scroll → static positioning
- Color and opacity transitions are fine to keep — they don't cause motion sickness
- [ ] `@media (hover: hover) and (pointer: fine)` gates hover animations
- [ ] All interactive elements keyboard-reachable with visible focus
- [ ] Icon-only buttons have `aria-label`; decorative icons are `aria-hidden`
- [ ] Focus trapped in modals; restored to trigger on close
- [ ] Color is never the sole status indicator
- [ ] Touch targets ≥ 44px (use pseudo-element expansion)
- [ ] Native elements (`button`, `a`, `label`) before ARIA roles
- [ ] Form errors linked via `aria-describedby`, invalid fields use `aria-invalid`
- [ ] Skip-to-content link; hierarchical `<h1>`–`<h6>`
---
## Forms (Non-negotiable)
- **Never block paste** in inputs
- **Enter submits** focused input; ⌘/Ctrl+Enter in multi-line fields
- **Keep submit enabled** until request starts; then disable with spinner and keep label
- **Accept free text, validate after** — don't block typing
- **Errors inline next to fields**; on submit, focus first error
- **Set `autocomplete` + meaningful `name`**; correct `type` and `inputmode`
- **Warn on unsaved changes** before navigation
- **Trim values** to handle trailing whitespace from text expansion
- **Mobile input font-size ≥ 16px** to prevent iOS zoom-on-focus — canonical rule and the viewport-meta trap to avoid live in `forms.md` (Input Basics)
@@ -0,0 +1,63 @@
# Agent Pack
Two read-only agents form the ui-craft **parallel verify team**. They complement the existing slash commands — they do not replace them.
---
## The agents
### `ui-craft:design-reviewer`
Adversarial design critique. Loads [review.md](review.md), the Anti-Slop and Craft Test sections from [../SKILL.md](../SKILL.md), and [heuristics.md](heuristics.md). Returns a severity-tagged findings table (Critical / Warning / Suggestion with `file:line`). Read-only — no edits, no code changes.
**Triggers on:** "review UI/design quality", "audit a diff or PR for design issues", "adversarial design critique".
### `ui-craft:a11y-auditor`
Accessibility audit. Loads [accessibility.md](accessibility.md). Covers keyboard navigation, focus-visible, APCA contrast, ARIA roles and labels, touch targets, and reduced-motion. Returns a severity-tagged findings table. Read-only — no edits, no code changes.
**Triggers on:** "accessibility audit of UI", "keyboard/focus/contrast/ARIA/touch targets check".
---
## Agents vs. commands — when to use which
Both agents and commands surface quality findings, but they operate differently:
| | Commands (`/critique`, `/audit`, …) | Agents (`design-reviewer`, `a11y-auditor`) |
|---|---|---|
| **Context** | Inline — run inside the caller's active context window | Fresh — each agent starts with a clean context |
| **Parallelism** | Sequential (one lens at a time) | Parallel — both can run simultaneously on the same target |
| **Tool scope** | Full skill tool scope | Restricted to Read, Grep, Glob (read-only) |
| **Best for** | Interactive iteration, mixed build+review sessions | Dedicated review passes, PR audits, CI-style verification |
Use commands when you are already in a build session and want a quick inline lens.
Use agents when you want an independent, fresh-context review — especially on a completed diff or PR where you do not want the reviewer to carry any build-session bias.
---
## Parallel verify team — how to invoke
Delegate both agents simultaneously on the same diff or file. They run in independent contexts and each returns its own report. The caller combines the two reports; neither agent depends on the other.
**Pattern — parallel design + a11y verify on a PR diff:**
> Delegate `ui-craft:design-reviewer` and `ui-craft:a11y-auditor` together on [target diff or file path]. Run both simultaneously. Each returns an independent severity-tagged findings table.
The caller (orchestrator or user) receives two reports and decides which findings to act on. This is the recommended final-pass workflow before merging a UI change.
---
## Delegate-only rule
Both agents are **delegate-only entry points**. Their system prompts load the relevant references and apply the role — they do not restate the rules. All judgment lives in the references ([review.md](review.md), [accessibility.md](accessibility.md), [heuristics.md](heuristics.md), [../SKILL.md](../SKILL.md)). This mirrors how `/sddesign` delegates phase commands rather than re-implementing their logic inline.
---
## Related references
- [review.md](review.md) — systematic critique methodology, anti-slop detection, Polish Pass
- [accessibility.md](accessibility.md) — WAI-ARIA, keyboard, focus, APCA contrast, touch targets, reduced-motion
- [heuristics.md](heuristics.md) — Nielsen's 10 + 6 design laws, scoring rubric, impact framing
- [../SKILL.md](../SKILL.md) — Anti-Slop Test, Craft Test, routing table
@@ -0,0 +1,158 @@
# AI Chat — Interaction Patterns for AI-Native Surfaces
Framework-agnostic patterns for UIs that stream tokens, render tool traces, show citations, or render AI-generated content alongside user chrome. The rules sit on top of `state-design.md` — AI surfaces extend the base lattice with streaming-specific states, caret semantics, and trust affordances. No assumption of Vercel AI SDK, LangChain, or CopilotKit — patterns apply to any streaming runtime.
---
## When to Read This
Read when building anything that streams tokens to the user, renders function-call traces, shows citations or source chips, or presents AI-generated content alongside human-authored UI. If the surface is just a "submit + await + render" form with no streaming and no tool use, skip this and stay in `state-design.md` + `copy.md`.
---
## The Streaming Contract
What the user expects the moment they hit send. Every bullet is a hard rule, not a preference.
- **First visible pixel < 400ms** (Doherty threshold). If the model is slow, paint the message shell, the thinking indicator, and the stop button before the first token. Never a blank screen waiting on the first chunk.
- **Caret or cursor indicator while tokens arrive.** No caret = "is it broken?" panic. A blinking block, a pulsing dot at the tail of the last token, or a subtle gradient sweep — one of these is always visible during an active stream.
- **Progressive markdown rendering.** Parse headings, lists, code blocks, and inline formatting as chunks arrive. Never buffer the full response and render once — users perceive that as a freeze even if the total time is identical.
- **Token pacing matches reality.** If the model is fast, don't artificially slow the render to feel "human." If it's slow, show incremental progress, never a spinner that sits for 15s.
- **Interruptible.** Always a stop button while streaming. After stop, a regenerate button. Never strand the user watching a response they don't want.
- **Scroll discipline.** Auto-scroll to follow new tokens, but pause auto-scroll the moment the user scrolls up. Resume only on user send or explicit "scroll to bottom."
---
## Status Affordances
Seven states AI UIs need beyond the base `state-design.md` lattice. Every state has a distinct visual signal — the user never has to guess which one they're in.
| State | Signal |
|---|---|
| Idle | Empty input, placeholder copy, blinking cursor on focus. Optionally: starter prompt carousels below input — only when the surface has tested the carousel against a control (no surface should ship "what AI thinks users want to ask" — that pattern reads as condescending and misses real intents). |
| Composing | User typing; no AI activity shown; send button enabled |
| Thinking | Sent, model hasn't started; typing-dots or "thinking…" label; max 2s before escalating to progress |
| Streaming | Caret visible; tokens appearing; stop button active; auto-scroll following tail |
| Tool-calling | Labeled tool trace ("Searching web for…" / "Reading file X.ts") with collapsible detail and running spinner |
| Complete | Caret gone; all actions available — regenerate, copy, feedback, branch |
| Error | Specific cause + retry + copy-error-id; never a toast that vanishes |
The thinking → streaming transition is the most-botched handoff. If thinking lasts > 2s without a progress signal, users refresh. Escalate to "Still thinking — this sometimes takes up to 10s" after 2s.
---
## Tool Traces
When the model calls a function (web search, file read, code execution), render it inline. Hiding tool calls is the fastest way to lose user trust.
**Required parts:**
- **Title + target.** "Search web: 'Anchor Positioning browser support'" — not "Tool call: search".
- **Status icon.** Running (spinner), complete (check), failed (warning).
- **Collapsible detail.** Expand to show request args + response. Collapsed by default for readable flow; expanded by user choice persists through the session.
- **Duration once done.** "Search web: 'Anchor Positioning' · 1.2s" — cheap honesty.
- **Chained traces render in order.** If the model calls three tools, show three traces in sequence as they happen, not a single "tools" umbrella.
**Never** hide tool calls behind a toggle that defaults to off. If the model did work, the user sees it happened. Transparency is the trust primitive for AI UIs.
---
## Citation Chips
When the model cites sources, make them first-class content, not footnotes no one reads.
- **Inline superscript** — `⟨¹⟩` or `[1]` at the end of the cited clause.
- **Hover/tap reveals source card** — title, URL, excerpt (2-3 lines of context), favicon.
- **Sources section at the bottom** — ordered by first-appearance citation number, not alphabetical.
- **Click a chip scrolls to that quote** — the stream becomes a deep-link map. Use View Transitions if navigating between a source detail page and the chat.
- **Broken citation = find-and-replace.** If a source 404s at chip-render time, mark the chip as unverified with a warning icon; don't silently drop it.
---
## Feedback Affordances
Every AI response gets visible feedback controls (thumbs / star / "did this help?" affordance). User interaction with them is optional — the system never gates progression on feedback. The control's presence signals the team values feedback; the optional interaction respects user attention.
- **Thumb up / thumb down.** Pick up/down OR agree/disagree wording — never mix. Persist choice; allow undo.
- **On thumb down: inline quick chips** — "factually incorrect", "harmful", "refused reasonable request", "bad format", "other". User can skip chips and just send the thumb.
- **Acknowledgement copy** — "Thanks — this helps us improve." Not "Your feedback has been recorded successfully!" Match `copy.md` restraint.
- **Never block the UI on feedback.** No "you must rate before continuing" modals. Ever.
---
## Retry / Regenerate / Continue
Three distinct verbs for three distinct operations. Mixing them is a common AI-UI tell.
| Verb | Meaning | Effect on history |
|------|---------|-------------------|
| Retry | Same prompt, same context, same model | Replaces prior response |
| Regenerate | Same prompt, variation (temperature bump or alt model) | Creates alternative; original stays visible |
| Continue | Response was cut off; extend from last token | Appends to prior response |
Label the buttons with the actual verb. "Try again" is ambiguous — is it retry or regenerate? Pick one and be explicit.
---
## Inline Actions on Response
What lives on each assistant message, reachable by hover (desktop) or tap-and-hold (mobile):
- **Copy.** Always. Clean text — no invisible whitespace, no markdown-to-plain conversion surprises. If the response is code, copy without the surrounding prose.
- **Edit prompt.** Jump to the user message that produced this response, with the original text pre-filled. Edits trigger retry.
- **Branch conversation.** Fork from this point; the original thread stays intact. Useful for "what if I asked differently?" exploration.
- **Share link.** Generates a read-only URL to this response (or the full thread up to this point).
- **Export markdown.** Clean markdown of the response, ready to paste into a doc.
- **Flag.** For destructive or concerning content. Sends to your review queue.
Actions appear on hover; persist on the last message (most commonly actioned); collapse into a `⋯` menu on mobile.
---
## Generative UI Patterns
When the model generates UI — charts, cards, forms, interactive tools — the render has rules the model doesn't know about. Enforce them in the renderer.
- **Loading skeleton matches component shape.** A generated chart gets a chart-shaped skeleton; a generated table gets table rows. Generic gray box is the AI-slop fallback.
- **Fallback to plain text if structured render fails.** If the schema is malformed, render the raw text and log the parse error — never show a broken component.
- **Interactive elements keep keyboard accessibility.** Generated buttons, selects, and forms are real `<button>`, `<select>`, `<form>` — not divs. The model doesn't get to skip a11y.
- **"Copy as code" always available.** For any generated UI, the user can see and copy the underlying code/props. Debuggability is trust.
- **Streaming components render progressively.** A generated table fills row by row as data arrives, not all-at-once at the end.
---
## Conversation Surface Layout
Body rules that make long threads readable.
- **Input always visible.** Pinned to viewport bottom on mobile (above the home indicator); sticky on desktop when the thread scrolls past its height. Never behind a scroll gate.
- **Scroll-to-bottom on send.** Force-scroll when the user sends a new message. During streaming, auto-scroll follows the tail UNTIL the user scrolls up — then pause until they scroll down or send again.
- **Max-width on message content.** ~65-75ch reading line. Full-width on tool traces and generative UI (they need room).
- **Alternating visual treatment for user vs assistant.** Right-aligned vs left-aligned, or tinted bubble vs plain, or avatar on one side only. Subtle — not a heavy rhythm that fights the content.
- **Thread virtualization at > 100 messages.** Long sessions compound DOM cost; render only the visible window + some buffer.
---
## Anti-Patterns
Ten sins that immediately read as "AI UI built in a weekend":
- Streaming without a caret — user can't tell if it's alive.
- Spinner > 3s without a progress signal or expected-duration copy.
- Auto-scrolling while the user is reading earlier content.
- Hiding tool calls behind a default-off toggle.
- Forced feedback ("rate before continuing").
- Removing the stop button during an active stream.
- Awaiting the full response then rendering once (no progressive parse).
- Copy button that copies with invisible whitespace, zero-width chars, or broken markdown.
- "Thinking…" label for 30s with no escalation, no progress, no tool trace.
- Fake typewriter pacing slower than the actual stream — makes a fast model feel slow.
---
## Cross-References
- `state-design.md` — base state lattice (idle / loading / empty / error / partial / conflict / offline). AI surfaces extend it with thinking / streaming / tool-calling.
- `motion.md` — caret animation tokens, streaming shimmer, tool-trace expand/collapse. Use the existing duration scale; never bespoke.
- `accessibility.md` — live-region announcements when content streams (`aria-live="polite"` on the message container; `aria-busy` during tool calls).
- `heuristics.md` — visibility-of-system-status applied to AI; the stream is the system, and it needs to be visible at every moment.
- `copy.md` — error and acknowledgement copy for AI responses; tone is restrained, not effusive.
+186
View File
@@ -0,0 +1,186 @@
# Design Brief
Defines the format of `.ui-craft/brief.md` — the durable artifact that anchors every design decision in this project. Read it first when working on any UI task. If it doesn't exist, run `/brief` to generate one. Every decision you make should be traceable to a principle in the brief.
---
## The Brief Format
The brief is a single markdown file at `.ui-craft/brief.md`. Five required sections, all short, plus an optional sixth that grows over time. No preamble. No mission statements.
---
### 1. Product purpose (1 sentence)
What this product does, in one sentence. Not a tagline. Specific. The kind of sentence a stranger could repeat after reading once.
> **Strong:** "Schedules end-of-day cash reconciliation reports for retail finance teams."
> **Weak:** "An AI-powered platform for modern teams." — generic, interchangeable with any SaaS product, tells the agent nothing about what trade-offs to make.
The test: could two different products use this sentence? If yes, rewrite.
---
### 2. Primary user (1 sentence)
Who the product is for, by role and context. Specific enough that a UI decision could differ from a hypothetical secondary user.
> **Strong:** "A finance ops lead at a 200-store retail chain, working in the office between 6am and 9am, on a 13" laptop."
> **Weak:** "Anyone who manages money." — too broad to constrain a single layout decision.
Role + context + device is the minimum. Frequency of use and session length are useful additions.
---
### 3. Three to five principles (the operating beliefs)
Opinionated statements about how this product behaves. Not aspirations — beliefs. Beliefs that, when two conflict, one wins.
Each principle must clear five bars:
- **Opinionated** — takes a stance someone could disagree with
- **Actionable** — resolves a real design debate when applied
- **Memorable** — short enough to recall without reading the file
- **Distinctive** — reflects this product, not generic SaaS values
- **Testable** — a proposed design can be evaluated against it
List them in conflict-resolution order. When two principles apply to the same decision, the higher one wins.
**Example pairs — strong vs. weak:**
| Strong | Weak | Why the weak one fails |
|--------|------|----------------------|
| "Wrong is worse than late." | "Accuracy matters." | The strong one resolves the confirmation modal debate. The weak one is a platitude every product claims. |
| "The empty page is the customer." | "Good onboarding is important." | The strong one makes a specific architectural call: blank canvas over wizard. The weak one is a category. |
| "Show the data, not the design." | "Keep it simple." | The strong one tells the agent what to do with color, chrome, and decoration. The weak one means nothing actionable. |
---
### 4. Success metric for the surface (1-2 sentences)
What "the user succeeded" looks like, in observable behavior. Not a business metric — a usage signal that surface design directly influences.
> **Strong:** "User identifies the day's outlier stores within 30 seconds of landing on the dashboard."
> **Weak:** "Increase MRR by 12%." — a business outcome; the surface has no direct lever on it.
The metric constrains every information hierarchy decision on that surface. Write it before designing the layout.
---
### 5. Out of scope (bullet list)
Three to five things this surface deliberately does NOT do. Constrain by exclusion. Prevents scope creep mid-design.
```markdown
- Does not display historical trends beyond 30 days
- Does not support bulk edits from this surface
- Does not expose raw transaction data
```
Short. Declarative. Each item should prevent at least one future feature request from landing in the wrong place.
---
### 6. Learned constraints (optional, append-only)
Corrections the user has made to design output on this project — the brief teaching itself. Starts empty; grows whenever the user rejects or redirects a choice. Each entry is dated and pins the rule plus the reason, so it generalizes instead of pattern-matching one literal case.
```markdown
- **2026-06-23** — No gradient backgrounds on hero/landing surfaces. *Why:* brand reads restrained; flashy undercuts trust.
- **2026-06-20** — Button press feedback caps at `scale(0.97)`. *Why:* user found 0.92 exaggerated.
```
These are project-scoped design facts, not a general memory store — they live in the brief because they *are* design decisions, just learned rather than stated up front. They rank with the principles (section 3): a learned constraint overrides a skill default but never the accessibility/correctness floor.
### 7. Fold classes used (append-only, written by `/craft`)
Which composition class each landing fold in this project committed to. Append-only; `/craft` reads it to draw a class this project has not spent yet.
```markdown
- **2026-07-29** — `type-only` — pricing page. Sacrificed any product demonstration above the fold.
- **2026-07-14** — `product-dominant` — home. Sacrificed narrative; the fold shows, it does not explain.
```
This section is not documentation, it is the mechanism. Asking a model for variety returns its default every time — ten blind builds of this skill produced the same fold ten times out of ten, across two unrelated products, with the fold prescribed, then permitted, then explicitly forbidden. Nothing written in prose moved it. What moves it is drawing from what is left, and that requires remembering what was taken. Delete this section and the project converges again on the next surface.
---
## How the Brief Gets Used
Read `.ui-craft/brief.md` first when working on any UI. It is Discovery Phase Step 1.
When making a design decision, cite the principle it applies. Write it as: *"Density reduced per principle 2: 'Show the data, not the design.'"* If a decision isn't covered by any principle, flag it — the brief is incomplete, not the decision wrong. Surface the gap so the brief can be updated.
If two principles conflict, the higher-ranked one wins. Document the override.
---
## Principles Workshop
Run this when the user is starting fresh and has no principles yet.
**Step 1: Start from product values, not visual style.**
Visual style is a consequence of values. "Clean and minimal" is not a principle — it's an aesthetic preference. Ask: what does this product believe about its users' time, expertise, errors, and goals?
**Step 2: Apply the "would anyone disagree?" test.**
If every reasonable person would agree with the principle, it's a platitude. "Be accessible to all users" fails this test. "Speed over completeness — truncate everything, reveal on demand" passes it because some would argue against it.
**Step 3: Test against past decisions.**
Take three real design calls that were debated. Would the candidate principle have produced the same outcome? If it would have flipped a decision you're happy with, it's the wrong principle.
**Step 4: Rank for conflict.**
When "speed" and "completeness" both apply to the same decision, which wins? Write the ranking explicitly. Unranked principles defer every close call back to a human — which defeats the purpose.
**Step 5: Trim to three to five.**
More than five means none of them are load-bearing. If you have eight, find the two that subsume the others and keep those. A principle that never resolves a debate is decorative.
**Examples by product archetype:**
- Finance reconciliation tool: "Wrong is worse than late." Makes confirmation modals universal. Makes fast-paths expensive to justify. Prevents destructive actions from hiding behind affordance.
- Creative tool: "The empty page is the customer." Makes loading states cheaper than first-run wizards. Makes templates less prominent than blank-canvas affordance. Deprioritizes onboarding over invitations.
- Developer dashboard: "Show the data, not the design." Retreats chrome. Reserves color for status signals. Blocks marketing-page patterns from leaking into the product.
---
## When to Update the Brief
Update when any of these shift: product direction, target user definition, surface scope, or a principle is found to resolve decisions in the wrong direction.
Date each addition. Never delete past principles — if a principle is superseded, mark it deprecated with the date and a reason:
```markdown
~~3. "Speed over completeness."~~ — deprecated 2025-11, product shifted to compliance use case where error cost > latency cost.
4. "Correctness over speed."
```
The brief is append-mostly. The team should be able to trace why a principle changed.
---
## Self-Correction
When the user corrects design output — "not like that", "I don't like this", "always do X here", "never Z", or reverses a non-default choice in a way that reads as a standing preference — append it to section 6 (run `/remember`). Capture the **why**, not just the what; phrase it so a future build can apply it without this conversation's context; confirm in one line where it landed. Don't re-litigate a correction already recorded. If a correction would breach the accessibility/correctness floor, apply the closest compliant interpretation and say so — never store an unsafe rule.
This is project-scoped, and it lives in the brief by design — ui-craft is a UI skill, not a general memory engine.
**Optional cross-project bridge.** If the user wants a correction applied across *all* their projects (not just this one), that's general memory, outside the brief's scope. When an external memory service is available to the agent, mirror the constraint there so other projects inherit it; otherwise note that cross-project recall needs such a service. The brief remains the canonical project store either way.
---
## When the Brief Doesn't Apply
**Marketing pages** need a different brief than the product itself. The primary user, success metric, and principles are different. Write a separate brief scoped to the marketing surface.
**First-run experiences** often have a distinct goal (activation, not task completion) and may warrant their own brief — or an addendum to the product brief.
**Multi-brand products** need a brief per brand, not per product. The format is the same; the content differs per brand context.
The brief format is universal. Its content is always scoped to a single surface and audience. If one brief is trying to serve three different contexts, split it.
---
## File Location and Git
Commit `.ui-craft/brief.md` to the repo. The brief is documentation, not configuration. Ignoring it in `.gitignore` means losing the team's collective design memory between sessions and collaborators.
The `.ui-craft/` directory is the project's design decision store. It belongs in version control alongside the code it governs.
+216
View File
@@ -0,0 +1,216 @@
# Color & Theming
Strategic color application, palette building, dark mode, and design tokens.
---
## Color Strategy
More color ≠ better. Strategic color beats rainbow vomit. Every color needs a purpose.
### Application UI Color Balance
The 60-30-10 rule is for interior design and marketing sites — not application UI. Mature SaaS products follow a different pattern:
- **90%+ neutral tones** — grays, whites, subtle warm/cool tints for surfaces and text
- **One accent color, used sparingly** — buttons, links, focus rings, emphasis states
- **Secondary colors only for semantic meaning** — success green, warning amber, error red
### Commitment Axis (marketing surfaces)
Product UI stays restrained; marketing surfaces have permission to commit further. Pick a strategy **before** picking colors, gated by DESIGN_VARIANCE ([craft-intent.md](craft-intent.md)):
| Strategy | What it means | Variance gate |
|----------|---------------|---------------|
| **Restrained** | 90%+ neutral, one accent ≤10% of surface | any — the product default |
| **Committed** | one saturated color carries 30-60% of the surface (a drenched hero, a full-color section) | ≥7 |
| **Full palette** | 3-4 named color roles, each used deliberately | ≥8, brand campaign or portfolio |
| **Drenched** | the surface IS the color; neutrals are the exception | 9-10, explicit brief only |
Commit or don't — a saturated hero hedged with timid neutrals around the edges reads as indecision. A beige-and-muted landing page on a brand brief ignores the register: on marketing surfaces, restraint without intent reads as mediocre, not refined. Name what the strategy is doing in the Craft Read so the user can push back.
### Color Purposes
- **Semantic**: success (green), error (red/rose), warning (amber), info (blue)
- **Hierarchy**: drawing attention to important elements
- **Categorization**: different sections, types, or states
- **Emotional tone**: warmth, energy, trust, creativity
- **Wayfinding**: helping users navigate structure
---
## Use OKLCH
OKLCH is perceptually uniform — equal steps in lightness *look* equal. Best for generating harmonious scales.
```css
/* Warm neutral (not pure gray) */
--surface: oklch(97% 0.01 60);
/* Cool tint */
--surface-cool: oklch(97% 0.01 250);
/* Generate a consistent scale */
--blue-50: oklch(97% 0.02 250);
--blue-100: oklch(93% 0.04 250);
--blue-200: oklch(87% 0.08 250);
--blue-500: oklch(60% 0.15 250);
--blue-900: oklch(30% 0.10 250);
```
---
## Gamut & P3
Max displayable chroma is not a flat ceiling — it varies strongly by hue and lightness. Purples and blues reach far higher chroma at mid lightness than yellows and cyans do at the same lightness; the OKLCH gamut is an uneven shape, not a cylinder. When generating a multi-hue palette (categorical chart colors, semantic status colors across hues), hold **chroma as a percentage of each hue's own max** rather than one absolute chroma value for every hue — a fixed `C 0.15` looks vivid on a purple and clipped/dull on a cyan at the same lightness, because the cyan's ceiling is lower.
**A P3 screen should get more color, not a different color.** Ship the sRGB-safe value as the default and let capable displays step up to a richer chroma at the same hue and lightness — the two versions should read as the same accent, just less clipped on hardware that can show it:
```css
:root {
--accent: oklch(60% 0.15 250); /* sRGB-safe default */
}
@media (color-gamut: p3) {
:root {
--accent: oklch(60% 0.19 250); /* richer on P3 displays */
}
}
```
If the project also needs to support browsers without `oklch()` at all, wrap the enhanced rule in `@supports (color: oklch(0 0 0))` — inside that block, declare the plain-color fallback first and the OKLCH value second, so a supporting browser applies the fallback and then immediately overrides it with the richer color, while a non-supporting browser skips the whole block and keeps whatever fallback was set outside it.
---
## Palette Building
### One-Hue Palette Recipe
A complete UI palette from a single brand hue, five roles: **text strong**, **text weak**, **stroke strong**, **stroke weak**, **fill** — generated by shifting only saturation and brightness of the base hue (in OKLCH: lightness and chroma; hold the hue). Each role has a contrast job (text strong ≥ Lc 75-90, strokes ≥ non-text minimums). **Why:** one hue at five intensities is cohesive by construction and gives every tone a functional meaning; five unrelated grays give you neither. Add a second hue only when a role genuinely can't be served by the first.
**QA the scale before shipping it.** A scale generated in HSL keeps the same numeric hue at every step but doesn't hold that hue perceptually constant — the light and dark ends visibly lean toward a neighboring hue even though the number didn't move. Spot-check by reading the OKLCH hue angle at each step of the generated scale; if it wanders more than roughly a dozen degrees from one end to the other, the drift is visible to the eye, not just on paper, and the fix is regenerating in OKLCH with hue held fixed while only lightness and chroma vary.
### Tinted Neutrals
Default to tinted neutrals over pure gray — a subtle hue tint reads as intentional. (Exception: deliberately achromatic systems — Sharp Geometric at zero chroma, print-derived palettes — where pure gray IS the statement; see the Signal preset in `themes.md`.)
- **Warm**: `oklch(L 0.01 60)` (slight warm tint)
- **Cool**: `oklch(L 0.01 250)` (slight blue tint)
- Match tint to your brand's primary hue
### Hue Consistency
On non-neutral backgrounds, tint borders/shadows/text toward the same hue:
```css
/* Card on blue background */
.card {
border: 1px solid oklch(70% 0.05 250); /* blue-tinted border */
box-shadow: 0 4px 12px oklch(30% 0.03 250 / 0.15); /* blue-tinted shadow */
}
```
### Never
- Gray text on colored backgrounds — looks washed out; use darker shade of bg color or transparency
- Pure black (`#000`) or pure white (`#fff`) for large areas
- Purple/cyan/blue gradient everything (AI slop)
- Gradients as decoration without purpose
---
## Dark Mode
### Required Setup
```html
<html style="color-scheme: dark">
<meta name="theme-color" content="#000000">
```
### Rules
- `color-scheme: dark` on `<html>` — ensures scrollbars, form controls, etc. have proper contrast
- `<meta name="theme-color">` matches page background
- Explicit `background-color` and `color` on native `<select>` (Windows fix)
- Reduce shadow intensity in dark mode (shadows are less visible on dark)
- **Elevation via transparency, not fixed grays:** define raised surfaces as white at stepped opacities (e.g. 6% / 8% / 12% over the base) instead of hand-picked lighter grays. **Why:** a transparent fill blends with whatever sits beneath it, so the same token reads consistently at every elevation — and light mode reuses the trick with black at 4-9%. Solid grays only match the one background they were picked against.
- Increase surface differentiation (more elevation steps)
- Watch gradient banding — fading to dark colors can cause banding; use background images when needed
---
## Semantic Color Tokens
```css
/* Surfaces */
--surface-primary: oklch(...);
--surface-secondary: oklch(...);
--surface-elevated: oklch(...);
/* Text */
--text-primary: oklch(...);
--text-secondary: oklch(...);
--text-tertiary: oklch(...);
/* Status */
--status-success: oklch(65% 0.18 145);
--status-error: oklch(60% 0.20 25);
--status-warning: oklch(75% 0.15 70);
--status-info: oklch(60% 0.15 250);
/* Interactive */
--interactive-primary: oklch(...);
--interactive-hover: oklch(...); /* more contrast than rest */
--interactive-active: oklch(...); /* even more contrast */
--interactive-focus: oklch(...);
```
**State overlays scale better than per-component state colors:** define hover/press as two reusable overlay tokens (e.g. hover = the fill tone, press = the weak stroke tone, or translucent black/white steps) and apply them over ANY component surface. Every new component inherits correct states for free instead of minting its own hover/press variations.
---
## Contrast
- **Prefer APCA** over WCAG 2 for perceptual accuracy
- **Text contrast**: minimum 4.5:1 (WCAG AA), prefer 7:1 (AAA)
- **UI components**: minimum 3:1
- **Think in the gap, not the absolute value.** What determines whether body text reads is the ΔL between foreground and background in OKLCH — roughly 0.55 or more of separation is the target. That gap is cheap near the extremes: a background at L 0.95 or L 0.1 has plenty of room to pair with a strongly contrasting foreground. It gets expensive as background L drifts toward the middle of the range — somewhere around L 0.4-0.7, there simply isn't 0.55 of room left on either side without pushing the foreground all the way to near-black or near-white. Treat any surface landing in that middle band as a contrast red flag and resolve it before the text color gets locked in
- **Interactions increase contrast**: `:hover`/`:active`/`:focus` have more contrast than rest
- **Never rely on color alone** — include text labels, icons, patterns
- **Color-blind-friendly palettes** for charts and data visualization
---
## Application Patterns
### Accent Application
- Primary actions (CTA buttons)
- Links (maintain accessibility)
- Key icons for recognition
- Section headers
- Hover states (introduce color on interaction)
- Focus rings matching brand
### Background & Surfaces
- Tinted backgrounds (warm/cool neutrals, not pure gray)
- Colored sections to separate areas
- Subtle gradient backgrounds (intentional, not generic)
- Tinted cards for warmth
### Borders & Accents
- Accent borders on cards (left/top)
- Colored underlines for active states
- Subtle colored dividers
- Colored focus indicators
### Image Outlines
Give `<img>` a subtle `1px` inset outline so images sit at the same depth as bordered/shadowed surfaces — and the outline color is **non-negotiable**:
- **Light mode:** pure black at low opacity — `rgba(0,0,0,0.1)` (Tailwind `outline-black/10`).
- **Dark mode:** pure white — `rgba(255,255,255,0.1)` (Tailwind `dark:outline-white/10`).
- **Never** a tinted near-black/near-white from the palette (slate/zinc/neutral, `#0a0a0a`, `#f5f5f7`) and never the accent/ink hue. A tinted outline picks up the surface underneath and reads as dirt on the image edge — this is the one place the hue-consistency rule above does NOT apply.
- Use `outline` + `outline-offset: -1px` (inset), not `border` — keeps the image its intended size and adds nothing to layout.
```css
img { outline: 1px solid rgba(0,0,0,0.1); outline-offset: -1px; }
/* dark: rgba(255,255,255,0.1) */
```
---
## Design Rules
- **Limit accent color to one per view**
- Use existing theme tokens before introducing new ones
- Never gradients unless explicitly requested
- Never glow effects as primary affordances
- Layered shadows: ambient + direct light
- Crisp edges: semi-transparent borders + shadows
@@ -0,0 +1,91 @@
# Component Contracts
Anatomy-level rules for the workhorse components: buttons, menus, modals, search, cards, nav. Surface-level composition lives in the recipes; form fields live in [forms.md](forms.md); metric cards and tables in [dashboard.md](dashboard.md). This file is the layer below: what makes an individual component read as designed.
---
## Buttons
**Anatomy:**
- Horizontal padding ≈ 2× vertical padding. Floors: 16px horizontal, 10px vertical. Equal padding on all sides makes a button read as a tile, not an action.
- Heights: 44-48px on touch, 32-40px acceptable on desktop-only surfaces. **Why:** pointer precision — a mouse hits a 32px target reliably; a thumb doesn't. **When it breaks:** any desktop surface that also ships to touch (most do) — then 44px is the floor everywhere. Hit-area expansion for smaller visuals: [accessibility.md](accessibility.md).
- Label 14-17px. Check the height/font pair for clean vertical centering: a 32px button with 17px text can't center on whole pixels — pick value pairs whose difference is even, or the label sits visibly off-center.
- Inline groups order left→right by importance, and every state (default, hover, pressed, loading, disabled) is distinguishable without being a costume change — one property shift per state transition is usually enough.
**Icon placement carries meaning:**
- Icon **left** of label = aids scanning, communicates the action type at a glance (add, edit, download).
- Icon **right** of label = consequence or destination ("Log out →", "Continue →", external-link marker).
- Don't decorate with an icon that repeats the label without adding either.
**Depth:** flat or one subtle shadow pair ([layout.md](layout.md)). Gradient fills and inner shadows on buttons are expressive opt-ins for branded marketing surfaces — inside the product they read as 2015. Anti-slop rules apply.
**Tiers and their contracts:**
- One primary per context — two solid CTAs in one view is a tie, and ties stall. Secondary = outline at ≥3:1 contrast (a light-gray outline button is indistinguishable from disabled). Tertiary = text + underline — the underline is an accessibility requirement for color-blind recognition, not decoration.
- **Avoid disabled buttons.** They can't explain themselves, give nothing on press, and assistive tech often skips them. Alternatives, in order: keep enabled and validate on press with errors; remove the action and say why; lock icon + explanation on press. If one survives anyway, pair it with visible inline text saying what unlocks it.
- Icon weight matches text weight, icon size matches text size; if an icon overpowers its label, fix it with a lighter stroke, not a washed-out color.
**Destructive actions — friction proportional to blast radius:**
- Low (recoverable, undo exists): act immediately + undo toast. Friction here is rude.
- Medium (recoverable with effort): confirmation dialog with verb buttons + the affected items named and highlighted.
- High (irreversible, shared, or bulk): confirmation + explicit acknowledgment — type the resource name or tick a checkbox before the destructive button activates. A misclick must not be able to complete the flow.
- Destructive buttons get LOW visual prominence (tertiary/outline) in the normal UI — prominence belongs to the actions you want repeated. Inside the confirm dialog the destructive verb is the filled one, in the danger color.
## Text Links
- **The link affordance is exclusive.** Underline + link color mean "this navigates" — nothing else gets that costume. A colored, underlined heading that isn't a link is a broken promise; an icon styled like a button that does nothing is the same lie. Decoration must never impersonate function.
- Inline links inside body text need the underline (color alone fails color-blind users); standalone links in chrome (nav, footers) may drop it when context already signals interactivity.
## Icon Labels
- **Prefer a visible text label next to icons.** Only universally-learned glyphs survive alone (search, close, play); everything else is a guess that costs a tap to verify. Icon-only buttons require `aria-label` + tooltip as the floor ([accessibility.md](accessibility.md)) — but visible labels beat both.
## Menus & Dropdowns
- **≤5 options: don't use a dropdown.** Radios or a visible segmented control — one less click, options scannable without interaction. The dropdown earns its collapse only past ~5 (6-10 still favors radios when vertical space allows; >10 wants type-ahead — full selection ladder in [forms.md](forms.md) Field Layout).
- **Long lists get a scroll container with an affordance:** visible scrollbar plus the last visible item partially cut by the container edge or faded. A list that ends exactly at the container edge looks complete — users never scroll. Cutting an item mid-row is the strongest "there's more" signal available.
- **Very long lists (countries, currencies, assignees) escalate to a searchable sheet/popover** — type-ahead beats scrolling past ~20 items, especially on mobile.
- Multi-select: each selected option visibly marked in the list + a one-step "clear all". Nested options: signal children before expansion (chevron, count).
- Power-user menus show keyboard shortcuts inline, right-aligned, in muted mono — see the Command composition in [recipe-dashboard.md](recipe-dashboard.md).
- On low-contrast minimal surfaces, an open menu needs a hairline stroke and/or shadow pair to separate from the page — whitespace alone can't establish the overlay plane.
## Modals
- **A modal is for decisions that interrupt for good reason** — destructive confirmations, flows that must not lose context. Minor errors and notices stay inline. If putting a task in a modal adds steps versus doing it in-flow, it doesn't belong in a modal.
- **Three ways out, always:** visible close button, an explicit cancel action, and overlay click (escape key too — [accessibility.md](accessibility.md) focus-trap rules).
- **Confirmation buttons are verbs, never Yes/No.** "Delete project" / "Cancel" — the button restates the consequence. Yes/No forces re-reading the question, and a negatively-phrased question ("Don't you want to keep…?") plus Yes/No produces wrong clicks under time pressure.
- Multi-step flows inside a modal show progress (step count). If the flow grows past ~3 steps, question the modal — promote to a page.
## Search
- Input ≥44px tall with a defined boundary (border or fill) — a search field users can't find is a navigation failure.
- Placeholder shows example queries or the dominant content type ("Search projects, people, docs…"), not the word "Search" the icon already says.
- Type-ahead suggestions as the user types; recent searches with individual remove + clear-all when the product is revisit-heavy.
- **The no-results state is a fork, not a wall:** show spelling-adjacent alternatives, popular queries, or a scoped-search escape ("search all workspaces instead"). A bare "no results found" ends the session — design it like any other empty state ([state-design.md](state-design.md)).
## Cards (content cards)
Metric cards have their own contract in [dashboard.md](dashboard.md). For content/preview cards in grids:
- **Variable-length content must not break grid rhythm.** Either clamp text (line-clamp + optional tooltip for the full string) or set a shared min-height per row. Mixed card heights in one grid read as unfinished.
- Identical image aspect ratio across every card in the grid — one off-ratio image breaks the set.
- Card CTA at the bottom (the reading endpoint), with hover + active states on desktop and a full-card tap target on touch.
- Internal padding 16-24px; grid gap 16-40px scaled by [VISUAL_DENSITY]. Multi-column grids want short content; single-column (mobile) tolerates longer — the card is the whole viewport there.
## Navigation (top bar)
- Logo top-left links home. Universal recovery convention — breaking it strands lost users.
- Active page visibly marked; hover states on every link.
- One CTA in the nav, visually distinct from both the plain links and the page's own primary CTA — three levels that must not tie.
- **Sticky nav needs a plane separator once content scrolls under it:** 1px hairline at low contrast, or backdrop blur (20-32px) over a 50-80% opacity fill. Without it the nav and content occupy the same visual plane and collide.
- **Transparent/blurred navs must be contrast-checked against every background they'll scroll over** — the hero may pass and the white section below fail. Provide a scrolled variant (solid fill) that activates past the hero.
- Overflow: collapse low-priority links into a "More" menu or push them to the footer; never shrink the type to fit.
---
## Cross-Refs
[forms.md](forms.md) inputs and validation · [accessibility.md](accessibility.md) hit areas, focus, keyboard · [dashboard.md](dashboard.md) metric cards, tables, toolbars · [state-design.md](state-design.md) empty/error states · [motion.md](motion.md) state-transition timing
+300
View File
@@ -0,0 +1,300 @@
# UX Copy & Writing
Voice, tone, reading level, terminology, inclusive language, locale handling, and tactical microcopy (errors, empty states, CTAs). Clear, human interface writing.
---
## Voice vs Tone
**Voice is constant. Tone varies.** Voice is the product's personality — how it sounds whether you're reading onboarding, an error, or a paywall. Tone is how that voice adjusts to context: warmer in onboarding, restrained in success, explicit in destructive confirms. A product with a consistent voice but responsive tone reads as designed; a product with shifting voice reads as written by 12 people.
### Voice Matrix
Pick three axes. Lock a position on each. Deviations are deliberate (brand campaigns, April Fools), never accidental.
| Axis | Endpoints | What to lock down |
|------|-----------|-------------------|
| Formality | Formal ↔ Casual | Pick a position; document with one sample sentence at the chosen point and one at each extreme |
| Reverence | Reverent ↔ Irreverent | Reverent treats the user task as serious; irreverent allows wit and acknowledges absurdity. Pick one. |
| Expertise | Expert ↔ Beginner-friendly | Expert assumes vocabulary; beginner-friendly defines on first use. Mixed within a product is acceptable when the surfaces serve different audiences. |
Document the positions. "Casual-but-respectful, beginner-friendly, mildly irreverent" is a brief a writer can execute against. "Friendly" is not.
### Tone by Context
Voice is fixed; tone moves along these contexts. Each row below assumes a default voice of "casual-professional, beginner-friendly, restrained."
| Context | Tone | Example |
|---------|------|---------|
| Onboarding | Warm + specific | "Let's get your first project in. Takes 60 seconds." |
| Error — user-recoverable | Honest + actionable | "We couldn't save your draft. Retry, or copy the error for support: `abc-123`." |
| Error — system outage | Honest + transparent | "Search is down. We're investigating. Your data is safe." |
| Success | Restrained | "Changes saved." — not "Amazing work!" |
| Destructive confirm | Explicit about consequences | "Delete 3 items? You'll have 30 days to restore them from Trash." |
| Empty state | Action-oriented | "No integrations yet. Connect Slack, GitHub, or Linear to get started." |
| Paywall / limit | Informational, not shaming | "You're at 5/5 free projects. Upgrade to create more." |
| Danger zone (settings) | Neutral + precise | "Deleting the workspace removes all 47 projects and invalidates all API keys." |
| Waiting / latency | Honest about duration | "Generating report. Usually 20-40 seconds." |
**Never:** exclamation points on errors. Emoji in enterprise contexts. Celebratory copy on routine saves.
---
## Reading Level
Target ~8th-grade reading level (Flesch-Kincaid ~70+, grade ~8). Rules of thumb:
- Sentences ≤ 20 words.
- Paragraphs ≤ 3 sentences.
- Concrete nouns over abstractions ("invoice" beats "billing artifact").
- Active voice ("You signed in" beats "Sign-in was completed").
- One idea per sentence.
- **Front-load.** The key word or fact opens the sentence, heading, and notification — readers and screen-reader users decide whether to continue from the first words. Inverted pyramid for blocks: conclusion → support → background.
- **Abbreviations are a tax.** Spell terms out; if an acronym must repeat, define it on first use ("estimated arrival (ETA)") and stay consistent after.
**Example rewrite:**
| Before (grade 14) | After (grade 7) |
|-------------------|-----------------|
| "Upon successful authentication, users will be redirected to their personalized dashboard interface." | "You'll see your dashboard after you sign in." |
| "In the event that the operation is unsuccessful, please re-attempt the transmission." | "If it fails, try again." |
| "Configuration modifications will be persisted upon form submission." | "Your changes save when you click Save." |
Automate: [hemingwayapp.com](https://hemingwayapp.com/) for spot checks; the [`flesch` npm package](https://www.npmjs.com/package/flesch) for build-time linting. `write-good` (npm) flags weasel words, passive voice, lexical illusions. `alex` (npm) is an inclusive-language linter.
---
## Terminology Consistency
Every product needs a glossary. Same concept → same word, everywhere.
**Common pitfalls:**
- `user` / `member` / `account` / `seat` — pick one per concept and stick.
- `project` / `workspace` / `board` — pick one per level of hierarchy.
- `delete` / `remove` / `archive` / `trash` — these mean different things; don't alias.
- `sign in` / `log in` — both are standard; pick one and never mix.
- `save` / `publish` / `update` — same action under three names in three surfaces is a finding.
**Red flag:** in a review, grep the codebase for every verb on every button. If `Save` / `Update` / `Apply` appear on forms that do the same thing, file it.
---
## Inclusive Language
Swap the loaded term for the neutral one. Keep the list tight — don't moralize, just fix.
| Replace | With |
|---------|------|
| blacklist / whitelist | blocklist / allowlist |
| master / slave | primary / replica |
| sanity check | quick check |
| grandfathered | legacy |
| he / she (generic) | they |
| manhours / manpower | work hours / team capacity |
| man-in-the-middle | on-path attack |
| dummy value | placeholder value |
| first-class citizen | fully supported |
Don't soften to the point of vagueness — "on-path attack" is still precise; "bad thing happens in network" is not.
---
## Locale-Aware Strings
Rules agents routinely ignore. Every one is shippable to production the day the product adds a second locale.
- **Dates.** `Intl.DateTimeFormat(locale, options)`. Never hardcode `MM/DD/YYYY` unless the audience is exclusively US. Use `new Date().toLocaleDateString(locale, { dateStyle: 'medium' })` for user-facing dates.
- **Numbers and currency.** `Intl.NumberFormat(locale, { style: 'currency', currency })`. Decimal and thousands separators differ: `1,234.56` (US) vs `1.234,56` (DE) vs `1 234,56` (FR).
- **Plurals.** `Intl.PluralRules(locale)`. English has 2 forms; Russian has 3; Arabic has 6. Never `n === 1 ? "1 item" : n + " items"` — breaks in Polish, Arabic, Welsh.
- **Length tolerance.** German ≈ 1.3× English; Russian ≈ 1.2×; Chinese ≈ 0.5×. Don't fix widths to English copy. Test with `de-DE` pseudo-locale.
- **RTL readiness.** Use logical CSS properties (`margin-inline-start`, `padding-inline-end`, `border-inline-start`), not `margin-left`. `dir="rtl"` at the root should flip the UI without additional CSS.
- **Time zones.** Store UTC, display in the user's zone. Never assume server time is user time. Use `Intl.DateTimeFormat(locale, { timeZone })`.
- **Sorting.** `Intl.Collator(locale)` — alphabetical order is locale-dependent (Swedish sorts `å` after `z`, German doesn't).
---
## Core Principles (Tactical)
1. **Be specific**: "Enter email" not "Enter value"
2. **Be concise**: Cut unnecessary words (don't sacrifice clarity)
3. **Be active**: "Save changes" not "Changes will be saved"
4. **Be human**: "Something went wrong" not "System error encountered"
5. **Be helpful**: Tell users what to do, not just what happened
6. **Be consistent**: Same terms throughout — don't vary for variety
---
## CTAs & Buttons
**Verb + noun.** Never `Submit`, `OK`, `Click here`, `Continue`.
| Generic | Specific |
|---------|----------|
| Submit | Create project |
| OK | Got it |
| Continue | Next: billing |
| Click here | Download invoice (PDF, 84KB) |
| Delete | Delete workspace |
- Describe the action specifically (verb + noun). Active voice.
- **Destructive actions include the object:** "Delete `project-alpha`" — user confirms the thing, not a generic verb. Typing the name for irreversible operations is the gold standard.
- **Action reveals consequence.** "Send invite" tells the user exactly what happens. "Submit form" does not.
- **One primary per view section** — solid for primary, outline/ghost for secondary.
- **Disabled states explain why** — not just grayed out, never rely on color alone.
- **One label per intent, page-wide.** "Get in touch", "Contact us", and "Let's talk" on the same page are one action wearing three names — each variant dilutes the conversion path. Pick one label per intent (contact, signup, view-work) and reuse it in nav, hero, and footer. Same for "Try free" / "Get started" / "Sign up free".
- **CTA labels fit one line at desktop.** A primary CTA that wraps to two lines is broken — shorten the label (≤3 words for primaries) or widen the button; never constrain a CTA's width artificially.
- **An action keeps its name through the whole flow.** The button that says "Publish" produces a toast that says "Published" — not "Success!" or "Your post is live". Confirmation dialogs, progress states, and completion messages all conjugate the same verb the trigger used. Interface vocabulary is signposting; renaming an action mid-flow makes the user re-verify they did the right thing.
---
## Numbers & Control Labels
- Numerals, never spelled out: "245", not "two hundred forty-five" — numerals have distinct shapes and scan faster. Group thousands ("12,400"); huge numbers go hybrid ("1 billion", "2.4M") so nobody counts zeros.
- Form labels drop possessives: "Email", not "My email" or "Your email" — the possessive adds a word and an ambiguity (whose?).
- Button labels are verb + noun ("Save post", "Delete message"): readable out of context, which is exactly how screen-reader users jumping between controls hear them.
- **Fake-precise numbers are a tell.** "92%", "4.1×", "13.4 lb" either come from real data (brief, brand, public metrics), are explicitly labeled as sample data, or get cut. Invented engineering precision the brand never claimed reads as fabricated the moment anyone checks.
---
## Copy Self-Audit (before any surface ships)
Re-read **every visible string** — headlines, eyebrows, button labels, body copy, captions, alt text, footer, error messages. Flag and rewrite anything that is:
- **Grammatically broken** or has an unclear referent ("we plan to stay that way" with no antecedent).
- **Forced cleverness** — wordplay that doesn't track, mock-poetic labels ("Field notes", "On our desks", "Loose plates" for a testimonials section), performative humility ("Quietly trusted by…"). Use the plain functional label: "Testimonials", "Trusted by", "Latest writing".
- **Generic step scaffolding** — "Step 1 / Step 2 / Step 3", "Phase 01". The step's verb is the label: "Install", "Configure", "Ship".
- **Separator flooding** — ration the middle dot to one per metadata line ("foo · bar · baz · qux" is a tell; use line breaks or columns). Avoid the em-dash in UI strings (labels, buttons, captions) — restructure with a period, comma, or colon instead.
If a string might not make sense, replace it with a plain functional sentence. Boring copy beats broken-clever copy every time.
---
## Error Messages
Three parts, in order: **what happened** + **why** + **what to do**.
- "Upload failed. The file is 62MB; the limit is 50MB. Try compressing it or splitting into parts."
- "We couldn't reach the server. Your internet looks offline. We'll retry automatically when you're back online."
- "Can't create the project. The name `beta` is already taken in this workspace. Try another name."
**Missing pieces = bad error.** "Something went wrong" has none. "Invalid input" has only the first.
### Rules
- Explain what went wrong in plain language
- Suggest how to fix it
- Don't blame the user
- Include examples when helpful
- Link to help/support if applicable
**Bad**: "Error 403: Forbidden" → **Good**: "You don't have permission to view this page. Contact your admin for access."
**Bad**: "Invalid input" → **Good**: "Email addresses need an @ symbol. Try: name@example.com"
---
## Empty States
**Bad**: "No items" → **Good**: "No projects yet. Create your first project to get started."
- Explain why it's empty (if not obvious)
- Show next action clearly
- Make it welcoming, not a dead-end
---
## Loading & Success States
**Loading** — Bad: "Loading..." (for 30+ seconds). Good: "Analyzing your data... this usually takes 30-60 seconds."
- Set expectations, explain what's happening, show progress, offer "Cancel".
**Success** — Bad: "Success". Good: "Settings saved! Your changes will take effect immediately."
- Confirm what happened, explain what's next, be brief but complete.
---
## Confirmation Dialogs
**Bad**: "Are you sure?" → **Good**: "Delete 'Project Alpha'? This can't be undone."
- State the specific action
- Explain consequences (especially destructive)
- Clear button labels ("Delete project" not "Yes")
- Only for risky actions
---
## Form Labels & Microcopy
- Clear, specific labels (not generic placeholders)
- Placeholders are examples, not labels — labels exist separately, placeholders end with `…`
- Explain why you're asking (when not obvious)
- Instructions before the field, not after
- Never use placeholders as the only labels
**Tooltips** — Bad: "This is the username field". Good: "Choose a username. You can change this later in Settings."
- Add value (don't repeat the label), answer "What is this?" or "Why?", inline help first, tooltips as last resort.
---
## Formatting Rules
- **Ellipsis for loading/follow-ups**: "Loading…", "Rename…"
- **Use `…` character** not three dots `...`
- **Curly quotes**: " " not " "
- **Non-breaking spaces**: `10&nbsp;MB`, `⌘&nbsp;K`
- **Numerals for counts**: "8 deployments" not "eight deployments"
- **Active voice**: "Install the CLI" not "The CLI will be installed"
- **Default to positive language**: encourage, don't blame
- **`<title>` reflects current context**
---
## Content & States
Every UI must design for the full range of content and states, not just the happy path:
- **Design all states**: empty, sparse, dense, error, loading, success
- **Skeletons mirror final content** exactly — prevent layout shift
- **No dead ends** — always offer next step or recovery
- **Empty states** guide toward action, not just "nothing here"
- **Accessible names** exist even when visuals omit labels
- **Resilient to user-generated content**: short, average, very long
---
## Banned Patterns (Dark UX)
UX sins, not style choices.
- **Confirmshaming.** "No thanks, I don't care about security." Ship: "No thanks" as a plain link.
- **Fake urgency.** "Only 2 seats left!" when it's always 2. Ship: real counters or no counter.
- **Fake scarcity.** "87% of users upgraded" with no source. Ship: named, verifiable stats or omit.
- **Roach motel.** Easy signup, impossible cancellation. Ship: cancel in the same number of clicks as signup.
- **Dark toggles.** Opt-in checkboxes pre-checked for marketing emails. Ship: unchecked by default, unambiguous label.
- **Bait-and-switch pricing.** "$9/mo" that's actually "$9/mo, billed $108 annually, with a $29 setup fee." Ship: the total visible at decision time.
---
## Never
- Use jargon without explanation
- Blame users
- Be vague without specifics
- Use passive voice unnecessarily
- Use humor for errors (be empathetic)
- Assume technical knowledge
- Vary terminology (pick one term, stick with it)
- Repeat information (headers restating intros)
- Use placeholders as the only labels
- Exclamation points on errors
- Celebratory copy on routine saves
---
## Sources
- Nielsen, J. "How Users Read on the Web." Nielsen Norman Group.
- Mailchimp Content Style Guide — [styleguide.mailchimp.com](https://styleguide.mailchimp.com/).
- Shopify Polaris — Content Guidelines.
- Atlassian Design System — Writing Style.
- Flesch, R. "A New Readability Yardstick." *Journal of Applied Psychology*, 1948.
- Unicode CLDR — locale data for dates, numbers, plurals.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,160 @@
# Craft Intent — Read, Variance, Signature
Load on every `/craft`, default Build mode, and when the user asks for a full surface without naming a command. This file is the **generative front door** — declare intent before writing code.
---
## 1. Craft Read (mandatory before code)
Before tokens or layout, output **one line** the user can react to:
> **Craft Read:** *[surface kind] for [audience], [product | marketing] language, [theme/accent hint], variance [N], signature bet: [choice].*
**Infer from:** page/app kind, vibe words ("minimal", "editorial", "dense", "playful"), references the user named, existing brand tokens, audience.
**Examples:**
- *Craft Read: B2B ops dashboard for finance admins, product language, Graphite + teal accent, variance 4, signature bet: hero metric card with sparkline.*
- *Craft Read: devtool launch page for technical buyers, marketing language, Porcelain + indigo, variance 7, signature bet: product screenshot cropped at fold with floating proof card.*
- *Craft Read: designer portfolio for hiring managers, marketing language, editorial type, variance 8, signature bet: kinetic display headline with italic emphasis in same family.*
If the brief is ambiguous, ask **one** clarifying question — never a questionnaire. If inferable, declare the Craft Read and proceed.
**Anti-default discipline:** Generated design without a committed read clusters into three defaults — (1) warm cream background (~`#F4F1EA`) + serif display + terracotta/brass accent; (2) near-black background + one bright acid-green or vermilion accent; (3) broadsheet layout — hairline rules, zero border-radius, dense columns. Each is legitimate only *when the brief asks for it*, never as a default answer to "no brand direction" — same goes for purple mesh gradients, symmetric three-icon-card heroes, full-black sidebars, and Inter-on-everything without a reason.
**Second-order check (marketing surfaces):** name the aesthetic lane before building ("editorial type", "terminal-dark devtool", "warm-craft artisan"), then ask if someone could guess it from the domain alone — "devtool → dark mode + mono" is the first reflex, "AI tool that's not-purple → editorial serif restraint" is the same reflex one tier deeper. If the lane, or any default above, is guessable from the category, rework it until it isn't — the brand's specifics should pick the lane, not the category. Self-test: "would a similar brief for a *different* brand in this category produce this same plan?" If yes for any axis, that's a reflex, not a decision — revise it and state what changed.
---
## 2. DESIGN_VARIANCE knob (1–10)
Separate from **VISUAL_DENSITY** (how much fits) and **CRAFT_LEVEL** (how refined). Variance controls **layout risk and creative amplitude**.
| Variance | Product surfaces | Marketing surfaces |
|----------|------------------|-------------------|
| **1–3** | Symmetric grids, safe hierarchy | Centered hero OK only with asymmetric support |
| **4–6** | Split layouts, metric hierarchy, one layout break | Split/bento heroes, alternating feature rows |
| **7–8** | Strong typographic moments in shell (empty state, onboarding) | Asymmetric offsets, scroll-linked logo strip, display scale drama |
| **9–10** | Rare — one experimental shell moment only | Kinetic type, scroll choreography — **explicit brief or user asks "experimental"** |
**Defaults when user doesn't specify:**
| Surface | Default variance |
|---------|------------------|
| Dashboard, settings, auth, tables | **4** |
| Landing (live product) | **7** |
| Landing (waitlist / pre-launch) | **6** |
| Portfolio, case-study, launch event | **8** |
`/bolder` raises variance (+1–2, cap 10) and motion (+1, cap 10). `/quieter` lowers both. Honor `prefers-reduced-motion` always.
---
## 3. Signature bet (mandatory — not polish-gated)
Every `/craft` ships **exactly one** memorable decision, chosen at plan time and built in Step 3 — not deferred to `/polish`.
### Product surfaces (pick one)
1. Custom empty-state motif (simple SVG tied to domain)
2. Distinctive nav active indicator (left bar, pill, or dot) — system-wide
3. Welcome / first-run panel — dismissible, value-focused
4. Primary table row hover (subtle lift + shadow on the main table only)
5. Command palette or keyboard hint (`⌘K`) when search/actions warrant it
### Auth surfaces (pick one — load [recipe-auth.md](recipe-auth.md))
1. Panel proof asset — one believable metric + sparkline **or** one attributed quote (not both)
2. Trust footer line — compliance / data residency in tertiary text at panel foot
3. Domain-specific welcome line — verb tied to product ("Pick up where you left off"), not generic filler
4. Real provider marks on SSO row — equal-weight neutral outlines, no fake icons
5. Subtle panel motif — simple SVG tied to domain on the tinted panel only
### Marketing surfaces (pick one)
1. Hero crop — product visual cut mid-element at fold edge
2. Floating proof card over screenshot (metric, notification)
3. Custom section marker (FIG label, domain icon motif)
4. Distinctive CTA copy — specific verb, not "Get started"
5. One scroll moment — logo strip or restrained headline entrance (variance ≥7)
Name the bet in the Craft Read. Build it in the first pass.
---
## 4. Product surface patterns (apps, dashboards, settings)
Apply on dashboard, auth, settings, onboarding, admin — load with [recipe-dashboard.md](recipe-dashboard.md).
**Shell:** Tinted sidebar (not full black). Active nav = accent/10 + accent text. Page header row: title + context + primary action aligned.
**Data:** One hero metric (larger, accent tint); secondaries neutral. Sparklines in KPI cards where trends matter. Table rows with context (avatar, status dot, proportion bar) — not plain grids. Ghost buttons in filter toolbars; primary CTA lives outside the toolbar.
**Type & color:** `tabular-nums` on numbers. Sentence case on nav and headers. Tinted surfaces (`accent/5` on primary card). Semantic green/red only when the domain expects it.
**Motion:** Optimistic UI where saves matter. Skeletons match final layout. `:active` micro-push (`scale(0.98)`). One staggered entrance per section max, `once: true`.
**States:** Empty states with icon + one sentence + CTA. Errors with retry/copy path. Onboarding checklists 3–5 steps, dismissible.
**Avoid in product shells:** scroll pinning, kinetic body copy, mesh backgrounds behind tables, glass on dense forms, decorative carousels.
**Ecosystem briefs use the official system.** When the brief names a platform with a mandated or expected design system, install and use the official package and its tokens instead of hand-rolling a lookalike — recreating a known system's CSS by hand is both more work and instantly off. One system per project; don't mix component libraries in the same tree.
Naming the platform is not enough to act on. Install what the row says:
| Brief reads as… | Install | Ships as |
|---|---|---|
| Microsoft 365 / Fluent-flavoured | `@fluentui/react-components` | React |
| Atlassian-suite companion | `@atlaskit/button`, `@atlaskit/textfield`, … | React, one package per component |
| Shopify app or admin surface | `@shopify/polaris` | React |
| Material-flavoured product | `@material/web` | Web components — any framework |
| IBM-style B2B / enterprise analytics | `@carbon/react` | React (`@carbon/styles` for the CSS alone) |
| Salesforce / Lightning | `@salesforce-ux/design-system` | CSS — any framework |
| Adobe-flavoured | `@adobe/react-spectrum` | React |
| GitHub-flavoured developer tooling | `@primer/react` | React (`@primer/primitives` for tokens alone) |
| UK public sector | `govuk-frontend` | Plain CSS + JS |
| US federal / state service | `@uswds/uswds` | Plain CSS + JS |
Check the third column against the project before adding a dependency. Most of these ship as React and installing one into a Vue, Svelte or Angular codebase is worse than hand-rolling: take the system's tokens and CSS instead, which is the part that makes it recognisable. Atlassian has no meta-package — install the components the surface actually uses.
No row matches → this rule does not apply. Proceed with the normal token flow rather than reaching for the nearest big component library.
---
## 5. Marketing surface patterns (landings, portfolios)
Apply on landing, portfolio, waitlist — load with [recipe-landing.md](recipe-landing.md) and [inspiration.md](inspiration.md) hero archetypes.
**Composition:** Pick one hero archetype (product-forward, message-forward, proof-forward, bento, editorial figure, social proof strip). **No two adjacent sections share the same layout.**
**Type:** Display 48–80px H1, tight tracking, sentence case. Font pairing from theme preset or brief — body + optional display. Emphasize with italic/bold **same family**, not random serif injection.
**Color:** Pick a commitment strategy first ([color.md](color.md) Commitment Axis — restrained / committed / full palette / drenched, gated by variance); default restrained: 90% neutral + one accent; rotate accent family when no brand (indigo, teal, rose, amber, forest). No default warm-beige + brass "premium" palette unless the brief names it.
**Imagery:** marketing surfaces are visual products, not text-only pages — asset priority order, the div-built-screenshot ban, and the logo-wall rule live in [recipe-landing.md](recipe-landing.md) (Imagery is not optional).
**Motion:** Hero stagger 50–80ms; one scroll reveal per section; multi-property hover on **one** featured block. No bounce, confetti, or idle float. Every animation must answer "what does this communicate?" in one sentence (hierarchy, sequence, feedback, state change) — "it looked cool" fails.
**Portfolio:** Variable grid aspects; one hero project above fold; asymmetric about/contact — not centered template.
---
## 6. Rotation (when no brand direction)
Rotate **one axis per build** so outputs don't converge:
| Axis | Pool (pick 1) |
|------|----------------|
| Theme preset | Graphite, Porcelain, Signal, Carbon — [themes.md](themes.md) |
| Accent family | indigo, teal, rose, amber, forest (OKLCH-safe) |
| Signature bet | from §3 lists |
Declare the axis in the Craft Read so the user can say "warmer accent" on iteration.
**Premium/artisan briefs must rotate palette families** — the default warm-beige + brass + espresso "premium" look is a convergence tell the moment the brief doesn't name those colors. Full family list and the never-twice-in-a-row rule: [themes.md](themes.md) Premium palette families.
---
## Cross-refs
[recipe-dashboard.md](recipe-dashboard.md) · [recipe-landing.md](recipe-landing.md) · [recipe-auth.md](recipe-auth.md) · [inspiration.md](inspiration.md) · [themes.md](themes.md) · [dashboard.md](dashboard.md) · [review.md](review.md)
@@ -0,0 +1,131 @@
# Dashboard Patterns
Detailed guidance for building data-heavy dashboards that feel designed, not generated.
## Layout Structure
A dashboard needs a sidebar + main content area. The sidebar is the navigation spine.
**Sidebar navigation:**
- Subtle background tint (e.g., `background: #f8fafc` / Tailwind: `bg-slate-50`, or `background: #030712` / Tailwind: `bg-gray-950`) — NOT full black unless Dark Premium style. A dark sidebar is a common AI pattern; prefer a subtle tint that complements the content area.
- Muted nav text that brightens on hover/active. Active item gets accent background at low opacity (`background: oklch(var(--accent) / 0.1)` / Tailwind: `bg-accent/10`) + accent text.
- Brand/logo at top, user profile at bottom.
- `aria-label="Main navigation"` on `<nav>`, `aria-current="page"` on active item.
- `overscroll-behavior: contain` on the sidebar if it scrolls independently.
**Main content area:**
- Filter/toolbar row at the top: ghost buttons for filters, active state uses accent bg at low opacity. Always include a date range selector for any time-series surface. **Why:** time-series data without an interactive range silently encodes the assumption that the default window is correct — when it isn't (and it usually isn't for power users), the dashboard becomes a screenshot. **When it breaks:** real-time monitoring surfaces with a fixed last-N-minutes window — the range is the affordance, not the picker.
- Content grid below filters: metric cards → charts → tables/lists.
- Minimum 3 different content types visible per viewport (e.g., metric cards + chart + table).
## Metric Card Hierarchy
Never show 4+ identical metric cards. Differentiate the primary metric. **Why:** a uniform grid of equal-weight cards triggers the AI-template tell — variety signals editorial decision; uniformity signals defaulted-out. It also fails the squint test: the eye can't lock on a primary at first glance. **How to differentiate:** primary card gets accent tint, slightly larger number, optional sparkline; secondaries are neutral with smaller type. See Signal-to-Noise Hierarchy section below.
**Primary metric card:**
- Accent-tinted background (`background: oklch(var(--accent) / 0.05)` / Tailwind: `bg-accent/5`) with accent-colored number, OR solid accent background with white text.
- Slightly larger number (36px vs 28px for others).
**Secondary metric cards:**
- White background, subtle border (`1px solid oklch(92% 0.005 250)`).
- Black number, secondary-color label.
**All metric cards should include:**
- Sparklines: 32px tall, polyline SVG, accent color with faded fill underneath.
- Change text: "+2,149 from last month" in `var(--text-tertiary)`. **Never green arrows for positive, red for negative.** **Why:** color implies judgment that may not match user context — a 30% increase in costs is "positive" by sign and "bad" by goal; the green arrow encodes the wrong story. Render the magnitude in neutral and let the user's interpretation supply meaning. **When it breaks:** financial trading surfaces where positive/negative is universally tied to goal (gains green, losses red is the domain convention) — match the user's domain, don't fight it.
- Label: sentence case, 12-13px, `font-weight: 500`, secondary color.
- Value: 28-36px, `font-weight: 700`, `font-variant-numeric: tabular-nums`, `letter-spacing: -0.02em`.
- NO colored top/left borders. NO colored change text. NO arrow icons next to percentages.
## Chart Type Decision Matrix
| Data story | Best chart | Why | Avoid |
|-----------|-----------|-----|-------|
| Trend over time | Area chart with gradient fill | Shows direction + volume | Vertical bar chart |
| Comparing categories | Horizontal bar chart | Labels are readable, easy to scan | Vertical bar with rotated labels |
| Comparing discrete values | Vertical bar chart | Natural for small sets (3-7 items) | Too many bars (>8) |
| Part-of-whole | Donut/ring chart (use sparingly) | Center text shows total | Pie chart — harder to compare |
| Inline trend in a card | Sparkline (32px polyline) | Minimal, contextual | Full chart crammed into a card |
| Conversion/funnel | Progressive bars with stage labels | Shows drop-off clearly | Donut chart |
| Never use | — | — | Pie charts, 3D charts of any kind |
**Why never pie charts:** Cleveland-McGill perceptual hierarchy ranks angle (pie) below position (bar). Humans compare bar lengths to within a few percent; pie wedges fail at 4+ slices. **Why never 3D:** depth occludes data points and foreshortens position-based comparison. No 2D chart benefits from a third dimension. **When a two-segment donut is acceptable:** a donut with center label for binary proportions (used vs free, paid vs free) — only one comparison to make.
## Chart Styling
- **Single accent hue at varying opacities** for multi-series: `accent/100`, `accent/60`, `accent/30`. **Never rainbow colors.** **Why:** hue does not encode ordering — readers can't rank red vs green by magnitude. Single-hue opacity ramps preserve perceptual ordering and remain colorblind-safe. See `dataviz.md` for full palette guidance.
- **Gradient fill underneath area lines**: line at full opacity, fill fades from ~15% at line to 0% at bottom.
- **Label placement**: axis labels in secondary text, 11-12px. Data point labels only on hover (tooltip), not permanently displayed.
- **Grid lines**: horizontal only, very subtle (`border-color: oklch(95% 0 0)` / Tailwind: `border-gray-100`). No vertical grid lines.
- **No chart chrome**: skip legends when there's only one series. Put context in the card title instead.
## Data Tables
Tables are the workhorse of dashboards. Make them earn their space.
- **Row context**: tiny avatars (24px), colored status dots (6px, not badges), flag icons, or proportion bars. Plain text tables feel like spreadsheets.
- **Status indicators**: small colored dots (6-8px) inline with text. "Active" with a green dot, not a green badge/pill. The dot conveys status; the text provides the label. Badges add visual noise and are an AI-slop pattern.
- **Proportion bars**: show relative values as width-proportional bars within cells. More visual than raw numbers.
- **Row hover**: subtle background highlight (`background: #f9fafb` / Tailwind: `hover:bg-gray-50`).
- **Headers**: sentence case, `font-weight: 500`, secondary color. **Never uppercase table headers.** **Why:** uppercase removes the lowercase letterforms that aid scan-pattern recognition; readers process uppercase ~13-20% slower than sentence case (Tinker 1969). Uppercase also reads as decorative-template, not data-functional.
- **Alignment**: text left, numbers right, status center.
- **Wrap every table in `overflow-x: auto`.** Horizontal, not `overflow-y` and not `overflow-hidden`. **Why:** a table is the one layout that cannot reflow — columns have minimum content widths, so at ~320px it either clips at the viewport edge or forces the whole page to scroll sideways. Vertical overflow solves a different problem and leaves this one intact.
- **Put `position: sticky; top: 0` on `thead` past ~15 rows.** **Why:** once the header scrolls out of view every cell below it is an unlabelled string, and the user scrolls back up to remember which column is which. On an operator surface the table *is* the product, so this is not polish.
- **Virtualize past 200 rows.** **Why:** every row is DOM the browser lays out, styles, and paints on each scroll frame — a 2,000-row table blows the 16ms budget before any of your CSS is at fault, and the fix is never "optimize the row component". Render a window (TanStack Virtual, `react-window`) or paginate. **When it breaks:** the table must be Cmd+F-searchable or printable in full — then paginate instead of virtualizing, since a virtual window hides rows from find-in-page.
- **`scrollbar-gutter: stable` on every scroll container.** **Why:** on overlay-scrollbar platforms the track appears only on scroll, and the content reflows by its width at that moment — column edges jump the instant the user touches the wheel. Reserving the gutter costs nothing and removes the shift.
## Filter & Toolbar Patterns
- Ghost buttons for all filter controls — **never solid primary buttons in a toolbar.** **Why:** a toolbar holds 5-15 tertiary actions (filter, sort, group, export, refresh). Each solid primary button competes for the user's primary-action attention budget; the dashboard's actual primary action (drill-in, edit, alert config) gets buried. Hick's Law applied to visual weight.
- Active filter state: accent background at low opacity + accent text, or subtle border change.
- Group related filters visually. Date range selector deserves more prominence (slightly larger or separated).
- "Reset filters" appears only when filters are active, as a text link — not a button.
## Content Density
Dashboards value density over breathing room (opposite of landing pages).
- Metric cards: tight padding (16-20px), not the generous 32-48px of landing page cards.
- Charts: minimize whitespace around the chart area. The data should fill the container.
- Tables: compact row height (40-48px). Dense but scannable.
- A dashboard should show enough data that a user can make a decision without scrolling. If the first viewport is just 4 large metric cards, you've wasted space.
- Aim for: 3-4 metric cards + at least one chart + start of a table/list — all above the fold.
---
## Signal-to-Noise Hierarchy
The most common AI-generated dashboard failure isn't any single bad choice — it's that every KPI, every chart, every card looks equally important. The user doesn't know where to look. Signal-to-noise hierarchy is the fix.
### Tiers
**Tier 1 — The hero metric.** One per dashboard. The number that answers "am I winning today?". Largest visual weight. Accent color is allowed here, and here only. Trend indicator adjacent (up/down + delta). Context micro-text below: "vs last 7 days" or "of monthly quota."
**Tier 2 — Supporting metrics (2-4).** The breakdown of the hero — if the hero is revenue, supports are revenue by channel, by region, by segment. Neutral colors. Smaller type. Grouped visually with the hero (shared container, aligned baseline).
**Tier 3 — Context (0-3 charts).** Trends, time series, comparisons. Chart type matches data shape (see `dataviz.md` and the Chart Type Decision Matrix above). Sparklines for micro-context inside cards. No large charts competing with the hero's visual weight.
**Tier 4 — Deep-dive.** Tables, filters, raw data. Accessible but not crowding the viewport. Often collapsed by default, or lives on a separate tab / route. The user drills in when they need it.
### The anti-pattern: "grid of 8 KPI cards with equal weight"
Every card the same size, same color, same layout → the user's eye has nowhere to land → the user scrolls looking for meaning → the user leaves. This is the single most recognizable AI-generated dashboard shape.
### The rule
One hero. 2-4 supports. Context behind. Deep-dive one click away. Test by squinting at the dashboard: your eye should land on ONE thing first. If it lands everywhere, the hierarchy is broken.
### Ranking decisions to document per dashboard
Before building, answer these:
- **What's the hero metric for this surface?** Different for exec view (revenue) vs operator view (tickets open) vs customer view (usage vs limit). See `personas.md` to ground this in concrete archetypes.
- **What supporting metrics contextualize the hero?** The 2-4 that make the hero actionable, not the 8 you could show.
- **What's deep-dive, not surface?** Everything the user needs once a week, not once a minute.
- **Who's the user and what decision are they making in the next 60 seconds?** If you can't name the decision, the dashboard has no job.
### Cross-references
- `dataviz.md` — chart selection by data shape; Cleveland-McGill perceptual hierarchy (position-on-common-scale is most accurate — use it for the hero).
- `heuristics.md` — Cleveland-McGill PASS/FAIL, Hick's Law for filter/nav choices.
- `personas.md` — concrete user archetypes to ground "who's the user" in reality, not abstraction.
@@ -0,0 +1,197 @@
# Dataviz — Perceptual Accuracy
Most AI-generated charts are rainbow 3-D pies on grid backgrounds with shouting legends. That's not dataviz — that's decoration. Dataviz is a science with names attached: Cleveland, McGill, Tufte, Bostock, Brewer. This file is the perceptual layer — `dashboard.md` handles dashboard chrome (sidebar, metric cards, toolbar); this handles what actually encodes information accurately.
---
## Cleveland-McGill Perceptual Hierarchy
Source: Cleveland, W. S. & McGill, R. (1984). "Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods." JASA.
Humans decode visual encodings with predictable, measurable accuracy. Ordered from most → least accurate:
1. **Position on a common scale** — e.g., dot plot, aligned bar chart. Highest precision.
2. **Position on unaligned scales** — e.g., small multiples with different baselines.
3. **Length** — e.g., bar chart, progress bar.
4. **Angle / slope** — e.g., line chart slopes, pie wedges.
5. **Area** — e.g., bubble chart, treemap.
6. **Volume** — e.g., 3-D bar (and yes, this is already worse than area).
7. **Color saturation / lightness** — e.g., heatmap.
8. **Color hue** — e.g., categorical colors. Lowest precision for quantities.
**Implication:** if a numeric comparison is load-bearing, prefer bar over pie, line over stacked area, dot plot over heatmap. Color hue is for categories, not quantities.
---
## Chart Selection Matrix
Match the data shape and analytical question to a chart. Fallbacks listed when the default isn't possible.
| Data shape + question | Default | Fallback | Avoid |
|----------------------|---------|----------|-------|
| Time series, one metric | Line | Area (sparingly, single series) | Vertical bars per day |
| Time series, categorical breakdown, totals | Stacked bar or stacked area | — | 100% stacked (hides totals) |
| Time series, categorical breakdown, individual trends | Small multiples (one line per panel) | Multi-line (≤ 4 series) | Spaghetti line chart (> 5 series) |
| Categorical, one metric | Horizontal bar (sorted) | Dot plot | Pie, unless n ≤ 3 and parts-of-whole is the story |
| Parts of a whole | Stacked bar (100%) with labels | Donut (n ≤ 5) | Pie with n > 5; donut without center total |
| Distribution of one variable | Histogram | Density / violin | Mean-only bar |
| Distribution across groups | Box plot or violin | Strip plot with jitter | Mean + error bar alone |
| Correlation of two variables | Scatter + trend line | Hex-bin (high n) | A correlation coefficient printed as a single number |
| Ranking | Horizontal bar sorted desc | Dot plot with reference line | Vertical bar with rotated labels |
| Geographic (regional) | Choropleth, **per-capita normalized** | Symbol map | Raw-count choropleth (shows population, not signal) |
| Flow / transition | Sankey (sparingly) | Cohort table, small multiples | Chord diagram for > 8 categories |
| Hierarchy | Tree / indented list | Treemap (area ≫ precision) | Sunburst for > 3 levels |
**Rule of thumb:** if you can't name the question the chart answers, remove it.
---
## Color for Data
Three families. Choose by the data's semantics, not by taste.
### Sequential — one direction (low → high)
For ordinal or continuous magnitude: revenue, count, score. Use perceptually uniform palettes.
- **viridis, cividis** (perceptually uniform sequential — available in every charting stack) — colorblind-safe, print-safe.
- **YlGnBu, YlOrRd** (ColorBrewer) — classic sequential, works on light backgrounds.
Never use a rainbow (hue-only) palette for sequential data. Hue is not ordered — the eye cannot rank red vs green by magnitude. Ordered magnitude needs a luminance ramp (viridis, cividis, YlOrRd). Hue-only is also colorblind-hostile: red-green deficiency affects approximately 8% of men and 0.5% of women (Birch 2012; Sharpe et al. 1999) — exactly the mid-spectrum of a rainbow.
### Diverging — centered on a meaningful midpoint
For values with a neutral zero: surplus/deficit, above/below average, net promoter score.
- **RdBu, PiYG, BrBG** (ColorBrewer).
- The midpoint must be semantic, not arbitrary. Don't use diverging for "low to high" — that's sequential.
### Categorical — nominal classes
For discrete groups with no order: regions, departments, products.
- **Okabe-Ito** (Okabe & Ito 2008, colorblind-safe, 8 hues). Values, in series order:
`#E69F00` orange, `#56B4E9` sky blue, `#009E73` bluish green, `#F0E442` yellow, `#0072B2` blue, `#D55E00` vermillion, `#CC79A7` reddish purple, `#000000` black.
- **Cap at 7 hues.** After that, hues become indistinguishable. Options: (a) group remainder into "Other", (b) repeat hues with patterns, (c) direct-label each series and drop the legend.
### Never-defaults
- **Pie chart with > 5 slices** — Cleveland-McGill: angle is the fourth-lowest encoding in the perceptual hierarchy; humans cannot accurately compare more than 4–5 angular wedges. Use a horizontal bar chart sorted by magnitude instead.
- **Rainbow / HSL-rotation palette for sequential or ordinal data** — hue carries no perceptual ordering; the eye cannot rank hues by magnitude without trial-and-error. Diverging data needs a two-direction luminance ramp (RdBu, PiYG); sequential data needs a single-direction luminance ramp (viridis, YlOrRd). Hue-only is also colorblind-hostile: red-green deficiency affects approximately 8% of men and 0.5% of women (Birch 2012; Sharpe et al. 1999). Test every palette with a colorblind simulator (Color Oracle, Coblis) — non-negotiable for status colors.
- **Red and green as the only signal** — pair color with shape, icon, or text label; color alone fails for red-green deficient users (same citation above).
- **3-D charts of any kind** — depth occludes data points; foreshortening distorts position-based comparison (Cleveland-McGill: volume is second-lowest in the hierarchy). No 2-D chart benefits from a third dimension.
- **Chart library defaults without a palette override** — built-in palettes (Chart.js, Recharts) are designed for demos, not perceptual accuracy; always override with a named, colorblind-safe palette.
- **Default grid at every tick** — Tufte's data-ink ratio: a dense grid is chartjunk. Use sparse grid lines (every 2nd or 3rd tick), or baseline + target lines only.
---
## Tufte Principles, Distilled
Source: Tufte, E. R. (1983, 2001). *The Visual Display of Quantitative Information*.
- **Data-ink ratio** — maximize ink spent on data, minimize ink spent on chrome. Every gridline, tick, border, legend box must earn its space.
- **No chartjunk** — no 3-D, no gradients as decoration, no drop shadows on bars, no textures.
- **Small multiples over animated dashboards** — the eye compares adjacent panels faster than it compares across a timeline.
- **Annotation beats legend** — label the line, not a color swatch 200px away (see "Direct labeling" below).
- **Bar charts start at zero.** Length encodes magnitude; truncating the axis lies.
- **Line charts do NOT have to start at zero.** Position encodes change; a zero baseline can obscure the signal. Use judgment — when the story is "rose from 98 to 102", starting at zero hides it.
---
## Direct Labeling — the Quiet Superpower
A legend forces: read color, look 200px away, match, remember, come back. Direct labels collapse that into one saccade.
```jsx
// Bad — legend + color lookup + cognitive hop
<LineChart data={data}>
<Line dataKey="revenue" stroke="#1f77b4" />
<Line dataKey="costs" stroke="#ff7f0e" />
<Legend />
</LineChart>
// Good — label at the end of each line, legend gone
<LineChart data={data}>
<Line dataKey="revenue" stroke="#1f77b4">
<LabelList dataKey="revenue" position="right" />
</Line>
<Line dataKey="costs" stroke="#ff7f0e">
<LabelList dataKey="costs" position="right" />
</Line>
</LineChart>
```
Works for: bar charts (label inside or at end of bar), scatter (label the outlier dots), small multiples (title per panel replaces legend).
---
## Small Multiples
Use when comparing the same metric across a dimension — 8 regions, 6 products, 12 months. The eye compares adjacent panels instantly; a single overlaid chart with 8 lines is a mess.
**Layout rules:**
- **Consistent axes across panels** — same x-scale, same y-scale. If scales differ, the comparison breaks.
- **Consistent chart type** — all lines, all bars; never mix.
- **One title explaining the one thing that varies** ("Revenue by region, Jan-Dec"), plus a small subtitle per panel naming that panel's dimension value.
- **3-6 columns** typically. 10+ panels = let it wrap; keep row heights equal.
- **Sort panels** by a meaningful order (magnitude, geography, alphabetical) — not by insertion order.
---
## Animated Transitions for Dataset Changes
Animate to reveal change, not for decoration. The D3 enter / update / exit pattern — now standard in Recharts, VisX, Framer Motion, and Observable Plot:
- **Enter** — new data points fade in and slide to position (200-300ms, `ease-out`).
- **Update** — existing points tween to new values (300-500ms, `ease-in-out`). The tween is the insight — the user's eye tracks which point moved where.
- **Exit** — removed points fade out and collapse (150-200ms, `ease-in`).
**Rules:**
- Tween values, not colors — category color stays stable so the eye can track a specific series.
- Respect `prefers-reduced-motion` — collapse to instant transitions.
- Never animate on mount just for the entrance. A dashboard loading is not a moment for choreography.
See `references/motion.md` for easing curves and the duration scale.
---
## Anti-Slop Checklist (Dataviz)
Ten items a reviewer will flag immediately. Fix before shipping.
- [ ] 3-D charts of any kind. Zero exceptions.
- [ ] Pie chart with > 5 slices, or any pie where parts-of-whole isn't the story.
- [ ] Rainbow / HSL-rotation palette for sequential or numeric data.
- [ ] Red and green as the only differentiator (color-blind failure — add shape or label).
- [ ] Truncated y-axis on a bar chart (bars lie about magnitude).
- [ ] Dense gridlines at every tick. Prefer sparse grid (every 2nd or 3rd), or baseline + target lines only.
- [ ] Axis labels in scientific notation (`1.2e6`) when `1.2M` or `1,200,000` is readable.
- [ ] Missing units on axes ("Revenue" vs "Revenue (USD, thousands)").
- [ ] Legend with > 7 entries without direct labeling.
- [ ] Choropleth of raw counts instead of per-capita / rate normalization.
---
## When `ui-craft-detect` Flags `dataviz/*` Rules
The detector rules map directly onto this file:
- `dataviz/no-pie-gt-5` → § Cleveland-McGill + § Chart Selection Matrix
- `dataviz/no-3d` → § Anti-Slop Checklist
- `dataviz/sequential-not-rainbow` → § Color for Data → Sequential
- `dataviz/direct-label-over-legend` → § Direct Labeling
- `dataviz/bar-starts-at-zero` → § Tufte Principles
Cross-reference when triaging a flagged chart.
---
## Sources
- Cleveland, W. S. & McGill, R. (1984). "Graphical Perception." *JASA*.
- Tufte, E. R. (2001). *The Visual Display of Quantitative Information*, 2nd ed.
- Bostock, M. et al. (2011). "D3: Data-Driven Documents." *IEEE TVCG*.
- Brewer, C. A. ColorBrewer 2.0 — [colorbrewer2.org](https://colorbrewer2.org/).
- Okabe, M. & Ito, K. (2008). "Color Universal Design."
- Wilke, C. O. (2019). *Fundamentals of Data Visualization*. O'Reilly.
- Birch, J. (2012). "Worldwide prevalence of red-green color deficiency." *Journal of the Optical Society of America A*.
- Sharpe, L. T. et al. (1999). "A new and popular explanation of colour blindness." In *Colour Vision: From Genes to Perception*. Cambridge University Press.
@@ -0,0 +1,243 @@
# Finish Bar
Ten finishing passes, each with measurable criteria. Run before shipping. Gated to `CRAFT_LEVEL ≥ 8` or `/finalize`. The point is not more rules — each pass surfaces a class of failure the others can't catch. Passes share no blind spots by design.
---
## How to Run the Bar
Run passes in order. Earlier passes (hierarchy, type system, surface stack) are foundational — changing them after later passes would invalidate findings downstream. Sequence is not arbitrary.
For each pass: collect findings, assign severity (Critical / Major / Minor), then fix or defer with a written reason. "Defer" without a reason is not a defer, it's a skip.
**Done** means each pass returns zero Critical and zero Major findings. Minor findings are explicitly accepted — write them down and ship anyway, or address them. Unwritten Minor findings are ignored findings.
Time budget: one screen with a single primary action takes 15–30 minutes. A full multi-screen flow takes hours. Don't try to run the bar on the whole app at once — run it on a vertical slice, ship, repeat.
The bar is opinionated about what matters. When a pass conflicts with a recorded project decision (e.g., the project deliberately uses dense data tables that violate Pass 4's spacing rhythm), the pass yields. Cite the principle in `brief.md` that overrides. If no brief principle covers it, the conflict is a finding, not a defer.
---
## Pass 1 — Hierarchy
**Goal:** the surface resolves to one primary thing on first glance. The rest reads as supporting.
**Criteria:**
- Squint test passes: blur the screen or squint for 200ms. One element dominates. (Perceptual basis in `layout.md` — Squint Test section.)
- Primary / Secondary / Tertiary / Quaternary named explicitly for this surface before touching pixels. Write them down.
- Adjacent hierarchy levels differ by ≥1.5× in at least one signal: size, weight, contrast, surface area, or position.
- One focal point per viewport. Two elements at the same visual weight means one is wrong.
- Navigation, footers, and sidebars sit below content in perceived weight — they are infrastructure, not content.
**How to verify:** apply `filter: blur(8px)` in DevTools or squint physically. If the layout reads as a flat field of similar shapes, hierarchy failed. If two things fight for dominance, name which one wins and why; the loser gets demoted.
**When it doesn't apply:** purely transactional confirmations (a single action + summary) legitimately collapse to two levels — the action versus everything else. That's correct.
---
## Pass 2 — Type System
**Goal:** typography is a system, not a collection of styles that happen to coexist.
**Criteria:**
- ≤3 font weights in the visible viewport. More signals indecision, not richness.
- Tabular numerals on every cell, badge, or display element containing numerical data (`font-variant-numeric: tabular-nums`).
- OpenType features active where the font supports them: discretionary ligatures off in UI, contextual alternates on for serif headlines, kerning always on.
- Prose body line-length: 50–75 characters. UI labels, badges, and tables are exempt.
- Letter-spacing scoped per role: Latin sans-serif display tightens at large sizes; small utility labels widen. Never adjust CJK or Arabic spacing — those scripts have their own optical metrics.
- One body face, optionally one display face, optionally one mono. No fourth face.
**How to verify:** grep rendered `font-weight` declarations on the surface — count distinct values. Inspect numbers in DevTools computed styles for `font-variant-numeric`. Measure a prose container by pasting its text into a character-count tool.
**When it doesn't apply:** single-screen experiences with no prose (a confirmation dialog, a product card) skip the line-length criterion. The weight and numeral criteria still apply.
---
## Pass 3 — Surface Stack
**Goal:** at least three distinguishable elevation levels, both modes intentional.
**Criteria:**
- Canvas, raised, and overlay surfaces are visually distinct: ≥2% luminance delta in light mode, ≥4% in dark mode.
- Semantic naming in tokens: `--surface-canvas`, `--surface-raised`, `--surface-overlay` (or equivalent). No unnamed magic values.
- Dark mode surfaces are not inverted light primitives. Canvas in dark sits at a near-black with a hue tint — not `#000000`. Raised surfaces add luminance, not just reduce it.
- `color-scheme` declared so native form controls follow the mode.
- Shadows fall back to border tints in dark mode where shadow-on-dark loses effect. A 10% opacity drop shadow on a near-black background is invisible — replace it.
**How to verify:** render side-by-side in both modes. Toggle rapidly. Dark should not look like a photo-inverted version of the light surface — it should look like a deliberate reinterpretation. Cross-ref the intentional-dark test in `tokens.md`.
**When it doesn't apply:** single-mode embedded UIs (a terminal panel, an e-ink reader context) where dark mode is not a product requirement.
---
## Pass 4 — Spacing Rhythm
**Goal:** within < between < section, at every nesting level without exception.
**Criteria:**
- Every spacing value comes from the token scale. No arbitrary px values. (`layout.md` — Spacing Rhythm section.)
- The rhythm invariant holds at every nesting level: label 4px above its input, inputs 16px apart within the form, form 48px from the next section. Check three nesting levels minimum.
- Section breaks are ≥2× the inter-block spacing. If they are not, sections blur into blocks.
- Optical adjustments noted explicitly: icon vertical nudge to optical center, modal vertical offset for visual weight. Document these as intentional, not accidents.
**How to verify:** scroll the surface slowly. At each nested group, check that the surrounding space is visibly larger than the internal space. If you cannot tell which level you are in from spacing alone, the rhythm broke.
**When it doesn't apply:** dense data tables and command palettes deliberately compress. The proportion still holds — the absolute values shrink. Never collapse two adjacent levels to the same value regardless of density.
---
## Pass 5 — Iconography
**Goal:** one icon family, stroke weight matched to type, geometry coherent across the surface.
**Criteria:**
- Single icon family throughout the surface. No mixing families from different systems.
- Icon stroke weight visually matches body type weight: 1.5px stroke for `font-weight: 400`, 2px for 500. Mismatched weights read as elements from different UIs.
- Icon-to-type size ratio is consistent: if a 24px icon appears beside a 16px label, that ratio holds throughout. A 20px icon beside a 16px label elsewhere is a finding.
- Container shape is one of: circle, rounded square, squircle. Chosen once, held throughout. No surface where some icons have circle containers and others have rounded squares.
- No non-icon characters standing in for designed icons anywhere outside user-generated content contexts.
**How to verify:** collect every icon instance on the surface in a contact sheet (screenshot + arrange). They should look like siblings from one family. Any that don't match immediately is a finding. Cross-ref `inspiration.md` — "What Mature Interfaces Never Do" section.
**When it doesn't apply:** emoji as user content (chat reactions, message-thread rosters) are content, not icons — they are exempt from family and stroke criteria.
---
## Pass 6 — State Coverage
**Goal:** every state designed, not defaulted or missing.
**Criteria — all eight states must have explicit designs:**
- **Idle** — the resting state is intentional, not just the absence of everything else.
- **Loading** — skeleton matches the final layout geometry; shown after ~200ms to avoid flash on fast connections.
- **Empty** — one line explaining why it's empty + one primary action. Never "No items found" with no next step.
- **Error** — inline, actionable: what failed + what the user does next. Never a modal for a recoverable field error.
- **Success** — visible beyond color change: icon swap, position shift, or explicit confirmation text.
- **Partial** — unknown values rendered as em-dash; never `null`, `N/A`, or `0` for a value that isn't actually zero.
- **Conflict** — concurrent-edit or stale-data scenarios handled with explicit UI, not silent overwrites.
- **Offline** — skeleton persists past the loading timeout with an affordance that explains the surface is stale.
**How to verify:** write the eight state names on paper. For each interactive surface, mark which states have explicit designs. Unmarked states are Critical findings. Cross-ref `state-design.md`.
**When it doesn't apply:** read-only static surfaces (an embedded widget, a printed report) genuinely have only idle and loading. Conflict and offline states are not applicable.
---
## Pass 7 — Motion Tuning
**Goal:** every transition is purposeful, sub-400ms for UI, and motion-gap-clean.
**Criteria:**
- UI transitions use the duration scale's transition band: 100–400ms. Nothing outside this range for interactive state changes.
- Easings have perceptual basis: no `linear` for spatial motion, no symmetric `ease-in-out` on hover — spatial motion should ease out. (Cross-ref `motion.md` — Duration and Easing Scale.)
- Motion-gap audit clean: no interactive state changes that snap without a transition where the change is visually significant (appearance, disappearance, repositioning).
- Reduced-motion contract honored: `prefers-reduced-motion: reduce` collapses durations to ≤80ms or removes entrance animations entirely. The surface must be fully usable without motion.
- Custom curves where the surface demands character. The four named CSS easing keywords are a starting point, not a destination.
**How to verify:** trigger every interactive state in sequence and watch the transitions. Anything that snaps where a transition is expected is a Pass 7 Critical. Verify `prefers-reduced-motion` in DevTools by emulating the media feature.
**When it doesn't apply:** static surfaces with no state changes have no motion to audit. Skip the motion-gap pass, but the reduced-motion contract still applies if any CSS animation exists.
---
## Pass 8 — Microcopy Voice
**Goal:** every string has voice, specificity, and a single author.
**Criteria:**
- Verbs consistent across primary actions throughout the surface. Pick one form and hold it.
- No placeholder copy in production paths: no lorem ipsum, no generic names, no TODO comments, no XXX strings.
- No generic CTAs — every call to action names the specific outcome of the action.
- Error messages name what operation failed and offer a concrete next step. "Something went wrong" is not an error message.
- Empty states contain exactly one primary action — not three suggestions, not a paragraph, not a link list.
- Voice axes locked per the brief: formality, expertise level, and relationship position documented in `brief.md`. All strings on this surface should sound like the same product.
**How to verify:** extract every visible string from the surface. Read each one out of context — stripped of layout and visual hierarchy. Each should still communicate clearly and sound like the same product. Cross-ref `copy.md` voice matrix and `brief.md`.
**When it doesn't apply:** legal text (terms, privacy notices, regulatory copy) is locked to compliance language and exempt from product voice criteria.
---
## Pass 9 — Pixel Honesty
**Goal:** small details match the resolution intent of the design.
**Criteria:**
- Borders use sub-pixel approaches where a hairline is intended: `border: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent)` reads as a separator; a solid `neutral-200` border reads as a structural edge. Use the right one.
- Shadow stacks use 2–3 layers for depth perception. A single shadow at one blur value looks flat and unconvincing.
- Corner radii vary by element role: buttons, cards, and inputs each have a distinct radius. Uniform radius on every element is an AI-template tell — the single most recognizable signal of generated UI. (Cross-ref `inspiration.md` — Reference Token Values section.)
- Overflow handled on every text container: truncation includes a `title` tooltip, no `text-overflow: ellipsis` without a `max-width` set.
- Icon containers use squircles where the design language calls for it — circle and rounded square are not interchangeable.
**How to verify:** zoom to 200%+ and inspect details. Anything that "looks slightly off" at that zoom is a finding worth naming. The test is not whether it looks perfect at 100% — it's whether the details hold up under scrutiny.
**When it doesn't apply:** utility surfaces (internal tooling, admin dashboards) where the brief explicitly records that polish is deferred in favor of completeness. The decision must be in `brief.md`, not assumed.
---
## Pass 10 — Data Formatting
**Goal:** every number, date, and currency communicates the right thing at the right precision.
**Criteria:**
- Tabular numerals on all data display: cells, KPIs, badges, inline metrics (`font-variant-numeric: tabular-nums`).
- Counts abbreviated where the absolute value isn't the point: `1.2k`, `3.4M`, `12.5h`. Unabbreviated where the exact value matters.
- Relative time where the user cares about recency (`2m ago`, `yesterday`). Absolute time where provenance matters (`Mar 4, 14:32 UTC`). Never use one format everywhere — they answer different questions.
- Currency formatted to locale. Negative values distinguished by at least two signals: sign + color, or sign + parentheses for accounting contexts.
- Precision matches the decision granularity the user needs. A percentage to five decimal places is noise. A stock price rounded to the dollar is misleading. Match the precision to the question the number answers.
**How to verify:** inspect every numeric value on the surface. For each: what question does this number answer, and is the format right for that question? A count that could be `1,248` but shows `1.2k` when the user needs the exact number is a finding. Cross-ref `dataviz.md` for number formatting in chart contexts.
**When it doesn't apply:** integer-only surfaces (step counts, indices, item counts) skip currency and decimal precision criteria. Tabular numerals still apply.
---
## How Findings Map to Severity
**Critical — block ship:**
- Pass 1: no discernible hierarchy (flat field of equal-weight elements)
- Pass 6: states missing explicit designs (loading, empty, or error undefined)
- Pass 8: placeholder copy in production paths (lorem ipsum, TODO strings, generic names)
**Major — fix before ship when time allows; document if not:**
- Pass 3: surface stack incomplete or dark mode is an inverted light layer
- Pass 7: motion-gap failures on visible state changes; `prefers-reduced-motion` contract not honored
- Pass 9: uniform border-radius across all element types
- Pass 2: proportional numerals on tabular data (misaligned columns, impossible to compare)
**Minor — polish; ship-okay if explicitly accepted:**
- Pass 2: single weight-count violation (four weights, not three)
- Pass 5: icon stroke weight off by a half-unit
- Pass 10: currency format missing locale
- Pass 4: one spacing value that skips a token (an arbitrary 20px in an otherwise clean scale)
---
## When to Skip the Bar
- Internal tools where `brief.md` explicitly defers polish to product completeness. The deferral must be written, not assumed.
- Throwaway prototypes that will be discarded after a user test. If there is no brief, there is no bar.
- Single-screen confirmation flows where only 2–3 passes have anything to evaluate. Run those passes only, note the others as N/A.
The bar is a tool, not a ritual. Skipping it without recording why is the only wrong answer.
---
## Convergence mode
When invoked under a loop preset (e.g., `visual-anti-slop` from `loops.md`), the bar re-runs from **Pass 1** after each single highest-impact fix — full, not partial (passes are order-dependent). Repeat until Done (zero Critical, zero Major) or the loop budget is exhausted. The loop controls iteration count and stop conditions; the bar supplies the evaluative lens only.
---
## Cross-References
- `brief.md` — which passes can be deferred and which are non-negotiable for this product; voice axes for Pass 8.
- `tokens.md` — Passes 3, 4, 7, and 9 require token discipline; intentional-dark test in Pass 3.
- `layout.md` — Pass 1 (hierarchy and squint test) and Pass 4 (spacing rhythm) primary references.
- `typography.md` — Pass 2 (type system) primary reference.
- `motion.md` — Pass 7 (motion tuning) primary reference; duration and easing scale.
- `state-design.md` — Pass 6 (state coverage) primary reference; state lattice.
- `copy.md` — Pass 8 (microcopy) primary reference; voice matrix, error copy, empty states.
- `dataviz.md` — Pass 10 (data formatting) primary reference for numeric display in chart contexts.
- `inspiration.md` — Pass 5 (iconography) and Pass 9 (pixel honesty); "What Mature Interfaces Never Do" and Reference Token Values sections.
- `accessibility.md` — every pass assumes a11y is satisfied at the floor. The finish bar is the ceiling, not the floor. Run `accessibility.md` first.
+192
View File
@@ -0,0 +1,192 @@
# Forms — System Design
Holistic form design: timing, placement, progressive disclosure, multi-step flows, autosave, optimistic submit, keyboard contract, and field-specific patterns. Input labels and ARIA requirements live in `accessibility.md`; error message tone lives in `copy.md`. This file is the system that ties those tactical pieces together.
---
## When to Read This
Read when building any form longer than 2 fields, any multi-step flow, any form with conditional logic, any surface with autosave, or any destructive confirmation. A single search input doesn't need this file; a checkout, onboarding, settings page, or record editor does.
---
## Input Basics
Two rules that belong at the input level, before any validation or layout decision:
- **Never block paste.** Full rule and rationale live in `accessibility.md`.
- **Mobile input text sits at 16px minimum on small viewports.** Below that, Safari on iOS zooms the whole page in the moment the input gets focus, and getting back to the prior zoom level is on the user, not the page. Set 16px as the floor and size down from `sm:` breakpoints upward, never the reverse. Do not "fix" this by capping zoom in the viewport meta (`maximum-scale=1`, `user-scalable=no`) — most mobile browsers actually honor that cap, so the workaround takes the user's own pinch-to-zoom with it. That swaps a minor iOS quirk for a real WCAG 1.4.4 (Resize Text) failure.
---
## Validation Timing
The most-botched decision in form UX. Wrong timing feels punitive; right timing feels invisible.
| Trigger | When to use | When not to |
|---------|-------------|-------------|
| On blur | Format-checkable fields (email, URL, credit card, phone) | Required-only check — don't punish leaving a field |
| On submit | Required fields, inter-field dependencies, server-side uniqueness | Format checks that could fire earlier |
| Inline (as-you-type) | Password strength, character count, username availability | Email format (too noisy); most other fields |
| On mount | Never | Ever |
**Rules:**
- Validate on blur only after the user has interacted with the field at least once. Never validate on mount — nothing is wrong yet, and red borders on a fresh form read as hostile.
- Inline validation: debounce 300ms before showing error states. **Why:** without debounce, users see flickering error→success cycles as they type. Jittery feedback breaks the typing flow and reduces trust. 300ms is the threshold where users perceive feedback as "after I stopped" rather than "while I'm typing". (See Debounce Timings below.)
- Server-side uniqueness checks (username, slug) show a tiny spinner in the field during the check, then a green check or red error on resolve.
- On submit, surface ALL errors at once, not one at a time. Users want to fix everything in one pass.
---
## Error Placement
Where error messages live on the page. Details on copy tone are in `copy.md`.
- **Errors below the input; hints and help text ABOVE it.** Errors appear after typing, so below-field placement follows reading order. Hints are needed *while* typing — below the field they get covered by autofill menus and the mobile keyboard; above the field they stay visible.
- **Red border on the input itself**, plus message, plus icon (`⚠` or field-specific). Color alone fails WCAG and color-blind users — pair color with shape/text.
- **Scroll to first error on submit** if not in view. Focus it so screen readers announce it immediately.
- **Summary at the top is additive, not a replacement.** For long forms, an aria-live summary box ("3 fields need attention") helps — but every field still has its own inline error.
---
## Field Layout
- **Single column.** Multi-column forms break downward momentum, create fill-order ambiguity, and screen-magnifier users miss the second column entirely. Exception: short, intrinsically paired fields (MM/YY + CVC) sit side by side.
- **Labels above inputs, within ~16px.** Top labels eliminate the zig-zag eye path of side labels; more distance than that and the label detaches from its field.
- **Selection widgets by option count:** 2-5 → radios or segmented control, stacked vertically (horizontal invites mis-taps); 6-10 → radios if vertical space allows, dropdown if tight; >10 → autocomplete/type-ahead; huge taxonomies (1000+ occupations) → cascade two fields (category → item) to avoid choice paralysis. Numeric nudges → stepper with horizontal +/- and 48px targets, never a dropdown of numbers.
- **Checkbox vs toggle:** checkbox = applies on submit; toggle = applies immediately. Label checkboxes positively ("Allow automatic updates", never "Don't allow…") — test by replacing the checked box with "yes": the sentence must still make sense.
- **Submit button aligns with the fields' left edge** in stacked forms — the eye travels straight down the column and lands on it. Exception: single inline field (search, newsletter) → button attached to the field's right.
- **Never remove an error until the user changes the field.** Clearing it on re-focus without re-validation hides the problem.
---
## Progressive Disclosure
Show fields only when relevant. A form with 20 visible fields reads as homework; the same form with 6 visible and 14 revealed-on-relevance feels considerate.
- **Conditional fields appear smoothly** — use a 200ms height/opacity transition (`interpolate-size: allow-keywords` enables height-to-auto), not a pop-in. See `modern-css.md`.
- **Group related fields under a shared section header** — billing address separate from shipping address separate from payment.
- **Optional fields behind a "More options" toggle** if there are more than 2. Most users don't need them; showing them upfront adds cognitive load for no gain.
- **Never make users guess which fields are required.** Two valid schools: mark only the optional minority with "(optional)" (lighter), or mark both — asterisk on required plus "(optional)" (most explicit; favors scanners and screen-reader users). Pick per audience and form length — but never rely on intro text ("all fields required unless noted"): users scan forms, they don't read preambles. If you use asterisks, they're black/neutral, never red — red is reserved for error states. Skip marking entirely in single-field familiar forms (login, newsletter).
- **Field width hints at expected content length** — postal code and CVC narrow, full name and address wide. Uniform full-width fields discard a free affordance.
- **Inputs sit on solid surfaces** — never semi-translucent fills over imagery; legibility collapses for low-vision users and in bright ambient light.
- **Mobile multi-step forms: max 5-7 fields per step.** **Why:** Hick's Law — decision time grows logarithmically with options on-screen, and mobile reading width amplifies the effect. Forms longer than 7 fields exceed the page-scanning time (~7 seconds) before the user begins typing. Desktop tolerates higher density when the form has a clear scan structure (left-aligned labels, grouped fieldsets).
- **Conditional reveal is one-way** until submit — don't hide a field the user has filled, even if the condition changes. Instead, disable it with a note.
---
## Multi-Step / Wizard Patterns
When a form is too long for one screen, or the steps represent logical phases (account → plan → payment → confirm).
- **Progress indicator.** Numbered steps + current/total + short label per step. Not just "Step 3 of 5" — "Step 3 of 5: Payment."
- **State the cost upfront.** "3 steps, about 2 minutes, you'll need your ID" before step 1 — unknown length is why users abandon at step 2.
- **Order steps easy → hard.** Name and email before payment details: early wins build completion momentum; opening with the hardest ask maximizes abandonment.
- **Back button always available.** Moving back preserves all state; never re-fetch or reset.
- **Prefer an always-enabled "Next" that validates on press.** A disabled button can't say why it's disabled, gives no feedback when pressed, and is invisible to some assistive tech. If you do gate, the explanation must be visible inline — never tooltip-only, never silent.
- **Save draft automatically** between steps. See Autosave below.
- **Resume on return.** If the user leaves and comes back, restore to the last completed step, not step 1.
- **Review step before final submit.** Summary of all entries grouped by section, with an "Edit" link per section that jumps back to that step and returns. Users catch their own errors cheaper than servers do.
- **Never more than ~5 steps.** See Miller's Law in `heuristics.md` — beyond 5, the user loses the map.
---
## Save-Draft / Autosave
For any form that takes longer than ~30 seconds to fill.
- **Autosave debounce: 1-2s** after the last keystroke across the form, not per keystroke. (See Debounce Timings below.)
- **Visible "Saved" indicator with timestamp** — "Saved 2s ago" near the fields, quiet typography. Match `copy.md` restraint: "Saved" beats "Your draft has been saved successfully!"
- **Network loss.** Show "Reconnecting… Your draft is safe locally" and queue the save. When back online, sync and update the indicator.
- **Conflict resolution.** If another tab or user edited the same resource, offer "Keep mine / Keep theirs / Merge." See `state-design.md` for the conflict-state pattern.
- **Never show a spinner for autosave.** The whole point is invisibility. A check mark or timestamp is enough.
---
## Debounce Timings
Two debounces, two purposes:
- **Validation debounce: 300ms.** Triggers after the user pauses typing in a field. Prevents flicker; lets the user see the field's final state before judging it.
- **Autosave debounce: 1-2s.** Triggers after the user pauses across the form. Reduces network cost; lets the user settle on a value before persisting.
These never share the same timer. Validation runs per-field; autosave runs per-form.
---
## Optimistic Submit
Forms that look instant. Use for low-risk writes (likes, toggles, comment posts, small field edits).
- **Show success state immediately on submit** — don't wait for server ack.
- **Roll back with a clear undo prompt if the server rejects.** "Couldn't save — undo or retry?" Never silently revert.
- **Queue on offline; sync on reconnect** with a visible indicator.
- **Never double-submit.** Disable the button OR swap its label to "Sending…" with a spinner inside. Both — belt and suspenders.
- **Non-optimistic submits** (payments, destructive actions, high-stakes writes) — always wait for server confirmation before showing success.
---
## Keyboard Affordances
Basics most forms miss. The keyboard contract is non-negotiable on any form a user will fill more than once.
- **Enter submits from any single-line input.** Textareas get `Cmd/Ctrl + Enter` for submit; plain Enter inserts a newline.
- **Tab order matches visual order.** No DOM-order surprises. Never `tabindex > 0` to hack the order — fix the DOM.
- **Submit button is focus-reachable** without `tabindex` tricks.
- **Escape closes form modals** and prompts a "Discard changes?" if the form is dirty.
- **Autofocus the first field on form open** — but with caveats on mobile (may trigger the keyboard to pop up before the user sees the form). Test on real devices; consider autofocus only on desktop.
- **Cmd/Ctrl + S saves** on long-form editors. Common expectation from native apps.
---
## Field-Specific Patterns
The fields that AI-generated forms get wrong.
| Field | Pattern |
|-------|---------|
| Phone | Country code picker + mask + formatted display. Store E.164 internally; display local format ("(415) 555-1234") |
| Date | Prefer native `<input type="date">` where UX is tolerable. For ranges or complex pickers, a dedicated accessible component |
| Time zone | Pre-fill from browser (`Intl.DateTimeFormat().resolvedOptions().timeZone`); let user override; display user-facing name ("9:00 AM PT") not IANA ID |
| Credit card | Autoformat with a space every 4 digits; detect brand from first digits; move focus to expiry when card number is valid |
| Currency | Format per locale via `Intl.NumberFormat`; never let users type the currency symbol — render it as a prefix affordance |
| Password | Show/hide toggle (eye icon + `aria-label`). Strength indicator relative, not absolute. Skip the "1 uppercase, 1 number, 1 special" dance unless compliance requires it — long is better than complex |
| Magic link | "Check your email" state with explicit "Didn't receive? Resend in 30s" countdown; cooldown visible |
| File upload | Drag-and-drop with dashed border indicator on drag-over; per-file progress (bar + filename + size + cancel); per-file retry on failure; `accept` hints + descriptive copy ("PDF, PNG, JPG up to 10MB") |
---
## Destructive Actions Inside Forms
Deleting accounts, subscriptions, workspaces, or data from within a settings form.
- **Separate section** — a "Danger zone" block, visually distinct, at the bottom. Never primary-button-style for the delete action; Cancel is the primary.
- **Type-to-confirm** for irreversible deletions: require the user to type the resource name.
- Copy tone and confirmshaming rules: see `copy.md` — Banned Patterns (Dark UX).
---
## Anti-Patterns
Form sins that invalidate the rest of the work:
- **Placeholder as the only label.** Disappears on focus → user forgets what the field was. Accessibility and memory fail.
- **Red required asterisks, or marking convention explained nowhere.** Red is the error color — a field can't be "in error" before the user touches it. Asterisks are black; the marking scheme is self-evident or legended. See Progressive Disclosure for the optional-vs-required marking decision.
- **Red validation on mount.** Nothing is wrong yet — don't punish arrival.
- **Disabled submit button with no explanation.** User can't fix what they can't see. Tooltip or inline hint explains what's missing.
- **Clearing the whole form on one error.** Preserve every valid field; only reset the invalid one.
- **Captcha before form submission.** Captcha runs after the user expressed intent, not before.
- **"Are you sure?" after every step** in a wizard. Users desensitize and click through; save the confirm for the last, destructive moment.
- **Reset button next to submit.** Users accidentally click it. Almost never worth shipping.
- **Two password fields** ("confirm password") without a show/hide toggle. Everyone types the same typo twice.
- **Blocking paste** on any field — especially password confirm, order IDs, credit cards. See `accessibility.md`.
---
## Cross-References
- `accessibility.md` — ARIA, label requirements, keyboard non-negotiables, `aria-describedby` for errors.
- `copy.md` — voice/tone across form states, reading level, locale-aware strings, inclusive language, error message tone, empty states, CTA discipline.
- `state-design.md` — form states (idle / submitting / success / error / partial / offline); autosave conflict state.
- `heuristics.md` — Error Prevention heuristic, Miller's Law for step counts, Tesler's Law for progressive disclosure.
- `modern-css.md` — `interpolate-size: allow-keywords` for revealed fields, `<dialog>` for confirm modals, Anchor Positioning for inline help popovers.
@@ -0,0 +1,403 @@
# Heuristics — Scored Critique Methodology
Any skill can list anti-patterns. A scored critique is a defensible artifact a PM can action. Every finding gets a heuristic, a score (1-5), and an impact estimate. This file is the source of truth for the `/ui-craft:heuristic` command — the rubric, the laws, the output shape.
---
## How to Use
Run the full pass: Nielsen's 10 (score 1-5 each) → 6 Design Laws (PASS/FAIL each) → rank findings by impact tag. Output is a machine-parseable scorecard PMs can paste into any issue tracker.
Usability is not a knob. `CRAFT_LEVEL` does not relax these scores — a 2 is a 2 at every craft level.
---
## Scoring Rubric (applies to every heuristic)
| Score | Meaning |
|-------|---------|
| **1** | Blocks users. Core task cannot complete. |
| **2** | Severe friction. Users abandon or error repeatedly. |
| **3** | Works but confusing. Users complete with hesitation. |
| **4** | Works, minor polish left. No user impact. |
| **5** | Best-in-class. Nothing to add. |
Every finding is recorded in a row: `| Heuristic | Score | Finding | Impact |`.
---
## Nielsen's 10 Usability Heuristics
Source: Nielsen, J. (1994). "10 Usability Heuristics for User Interface Design." Nielsen Norman Group.
### 1. Visibility of System Status
The system should always keep users informed about what is going on, through appropriate feedback within reasonable time.
**What to look for:**
- [ ] Every async action has a loading state (spinner, progress, skeleton)
- [ ] Submit buttons disable + show progress while in flight
- [ ] Optimistic UI reconciles on server response (no silent failure)
- [ ] Navigation state is visible (active tab, breadcrumb, selected row)
- [ ] Long operations (>5s) escalate to progress percentage or estimate
**Common failures in AI-generated UIs:**
- Form submit has no loading state — user clicks 3-4x thinking nothing happened
- Toast appears then disappears before the user can read it
- No indication that a save succeeded ("did it work?")
### 2. Match Between System and the Real World
Speak the user's language — words, phrases, concepts they know. Follow real-world conventions.
**What to look for:**
- [ ] No system jargon in user-facing copy ("exception thrown", "null ref")
- [ ] Icons match cultural convention (trash = delete, not a cog)
- [ ] Dates are formatted for the user's locale, not ISO
- [ ] Error messages speak human, not stack trace
- [ ] Metaphors are consistent (folder, file, workspace)
**Common failures in AI-generated UIs:**
- "Submit payload" instead of "Save changes"
- "Entity not found" instead of "We couldn't find that project"
- Time displayed as `1713369600` (Unix) instead of `2 hours ago`
### 3. User Control and Freedom
Users often perform actions by mistake. They need a clearly marked emergency exit.
**What to look for:**
- [ ] Every destructive action has Undo (preferred) or a confirm dialog
- [ ] Modals close on Esc and on backdrop click
- [ ] Multi-step flows have Back without losing state
- [ ] Autosave preserves in-progress work on nav away
- [ ] URL reflects state so the back button works
**Common failures in AI-generated UIs:**
- Delete is instant and irreversible — no undo toast
- Modal traps the user; no way out without completing the form
- Browser back loses all form input
### 4. Consistency and Standards
Users should not have to wonder whether different words, situations, or actions mean the same thing. Follow platform conventions.
**What to look for:**
- [ ] Primary action placement consistent across views (right side, solid fill)
- [ ] Same verb for same operation throughout ("Delete" always, not "Delete"/"Remove"/"Trash")
- [ ] Icon meanings stable across screens
- [ ] Form patterns match platform (e.g., Cmd+Enter submits on macOS)
**Common failures in AI-generated UIs:**
- Buttons swap position between modals (Cancel left here, right there)
- Three different words for "delete" in the same product
- Non-standard scroll hijacking on a standard content page
### 5. Error Prevention
Better than good error messages is a design that prevents the problem from occurring in the first place.
**What to look for:**
- [ ] Destructive actions require confirmation typed with the resource name
- [ ] Disabled states prevent invalid submissions (with explanation on hover)
- [ ] Input constraints enforced client-side (date pickers, typed inputs)
- [ ] Autosave + version history on content-creation surfaces
- [ ] Warn before leaving unsaved changes
**Common failures in AI-generated UIs:**
- Freeform "Are you sure?" confirms that users mindlessly click
- No validation until submit; user fills 12 fields then sees 4 errors
- Paste blocked on fields (password confirm, order IDs)
### 6. Recognition Rather Than Recall
Minimize the user's memory load by making objects, actions, and options visible.
**What to look for:**
- [ ] Recently used / favorited items surfaced
- [ ] Autocomplete on any field with a known set of answers
- [ ] Keyboard shortcuts listed in tooltips (not hidden in a help menu)
- [ ] Current selection persists visibly
- [ ] Empty states suggest next actions
**Common failures in AI-generated UIs:**
- Power-user shortcuts (Cmd+K) exist but are undiscoverable
- User must remember which tab they were on after a refresh
- Dropdown with 200 options and no search
### 7. Flexibility and Efficiency of Use
Accelerators — unseen by novice — may speed up the interaction for the expert.
**What to look for:**
- [ ] Keyboard shortcuts for frequent actions (Cmd+K, j/k nav)
- [ ] Bulk actions on lists (multi-select + batch operate)
- [ ] Saved views / filters / segments
- [ ] Customization for heavy users (column config, hotkeys)
- [ ] Paste-to-action patterns (URL paste auto-parses)
**Common failures in AI-generated UIs:**
- Forces the same flow for novice and expert
- No way to multi-select in lists of 100+
- Command palette missing on tools users will spend hours in
### 8. Aesthetic and Minimalist Design
Dialogues should not contain information that is irrelevant or rarely needed.
**What to look for:**
- [ ] Every section answers one question; no filler
- [ ] Visual weight proportional to importance
- [ ] One accent, 3-5 placements per viewport (see SKILL.md anti-slop)
- [ ] White space is used, not avoided
- [ ] No decorative graphics competing with content
**Common failures in AI-generated UIs:**
- Hero has 4 CTAs of equal weight
- Every card has a colored top border
- Gradient backgrounds on every section
### 9. Help Users Recognize, Diagnose, and Recover From Errors
Error messages in plain language, precisely indicate the problem, and constructively suggest a solution.
**What to look for:**
- [ ] Errors appear inline, at the field, not as a global banner
- [ ] Messages specify what went wrong AND how to fix it
- [ ] Server errors include a copy-to-clipboard ID for support
- [ ] Recovery is one click (retry, undo, contact support)
- [ ] Network errors differ from validation errors
**Common failures in AI-generated UIs:**
- "Something went wrong" with no detail, no retry, no support path
- Red toast that disappears before the user reads it
- Error blames the user ("Invalid input") instead of explaining
### 10. Help and Documentation
Even though it is better if the system can be used without documentation, help may be necessary.
**What to look for:**
- [ ] Help is contextual, not a separate site the user must leave to
- [ ] Empty states double as onboarding ("Create your first project")
- [ ] Tooltips on non-obvious icons
- [ ] Inline help links in complex forms
- [ ] Searchable docs for deep features
**Common failures in AI-generated UIs:**
- No tooltips on icon-only buttons
- Help buried behind a "?" in the corner with a 404 behind it
- Onboarding is a 10-slide lightbox the user skips
---
## The 6 Design Laws
Each is scored PASS / FAIL with a specific detail. These are physics + psychology, not preferences — failing them has measurable UX impact.
### Fitts's Law
Source: Fitts, P. M. (1954). "The information capacity of the human motor system in controlling the amplitude of movement."
**Statement:** Time to acquire a target is a function of its size and distance.
**When load-bearing:** Every tappable / clickable surface. Critical on mobile.
**How to score:**
- **PASS** — touch targets ≥ 44px on mobile, ≥ 24px on desktop; primary CTA in the natural eye/thumb path; related actions grouped
- **FAIL** — touch targets under 44px mobile; destructive actions adjacent to primary actions; CTA placed far from where the user just looked
### Hick's Law
Source: Hick, W. E. (1952). "On the rate of gain of information."
**Statement:** Decision time grows logarithmically with the number of choices.
**When load-bearing:** Menus, navigation, plan selectors, form fields, dashboards.
**How to score:**
- **PASS** — nav ≤ 7 items at any level; no field offers 20+ unfiltered options without search
- **FAIL** — 12+ top-nav items; a single select with 200 entries and no type-ahead; dashboard with 18 metrics at equal weight
### Doherty Threshold
Source: Doherty, W. J. & Thadani, A. J. (1982). "The Economic Value of Rapid Response Time." IBM.
**Statement:** Productivity soars when response time is under 400ms.
**When load-bearing:** Every user-initiated action. Critical on data-heavy views and transitions.
**How to score:**
- **PASS** — perceived response < 400ms; optimistic UI for saves, likes, toggles; skeletons within 200ms
- **FAIL** — form submit shows a spinner for 2s with no optimistic state; nav click waits on full server round-trip before painting anything
### Cleveland-McGill Hierarchy
Source: Cleveland, W. S. & McGill, R. (1984). "Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods." JASA.
**Statement:** Humans decode visual encodings with different accuracy — position > length > angle > area > color-hue.
**When load-bearing:** Every chart, metric card, comparative dataviz.
**How to score:**
- **PASS** — quantitative comparisons use position or length (bar, line, dot); color reserved for category; no pie or 3D
- **FAIL** — pie chart used for comparison; donut with ≥ 5 slices; color-gradient used to encode magnitude where length would work
### Miller's Law
Source: Miller, G. A. (1956). "The Magical Number Seven, Plus or Minus Two."
**Statement:** Humans hold ~7 (±2) items in working memory. Chunking extends this.
**When load-bearing:** Nav depth, form section counts, onboarding flows, step counts.
**How to score:**
- **PASS** — navigation ≤ 7 items per level; forms chunked into ≤ 5 sections; multi-step flows ≤ 5 steps or with clear progress. (Wizard ceiling is 5, not 7: each step holds multiple fields, and each field consumes working-memory slots of its own.)
- **FAIL** — 14-item top nav; a single-page form with 23 ungrouped fields; a checkout with 9 steps and no progress indicator
### Tesler's Law
Also called the Law of Conservation of Complexity. Attributed to Larry Tesler.
**Statement:** Every application has an irreducible amount of complexity. The only question is who deals with it — the user, the UI, or the backend.
**When load-bearing:** Feature-rich tools. Configuration-heavy flows. Anywhere you're tempted to "expose a toggle."
**How to score:**
- **PASS** — complexity absorbed by sensible defaults, automation, or backend inference; advanced controls behind progressive disclosure
- **FAIL** — UI exposes every backend knob; user must configure 8 things before they can accomplish the primary task; no default state, only empty forms
---
## Impact Tags
Every finding carries exactly one impact tag. This is how a PM prioritizes:
| Tag | Meaning | Example |
|-----|---------|---------|
| `blocks-conversion` | User cannot complete the core journey | Signup form submit silently fails on network error |
| `adds-friction` | User completes but abandonment rate rises | Submit button has no loading state; users click 3-4x |
| `reduces-trust` | Quality signal — affects retention | Typos in error messages; inconsistent button styles |
| `minor-polish` | Craft layer only; no measurable user impact | Missing `tabular-nums` on a metric |
Rank findings in the final output by impact tag, not by heuristic order: `blocks-conversion > adds-friction > reduces-trust > minor-polish`.
---
## Scoring Output Format
The `/ui-craft:heuristic` command outputs **exactly** this structure. Do not invent new sections. Do not merge tables. Do not drop the ranking at the end.
```markdown
## Heuristic Scorecard
| Heuristic | Score | Finding | Impact |
|-----------|-------|---------|--------|
| Visibility of system status | 2 | Form submit has no loading state; users click 3-4x thinking nothing happened | adds-friction |
| Match system and real world | 4 | Dates formatted for locale; one copy says "entity" instead of "project" | minor-polish |
| User control and freedom | 3 | Delete has confirm but no undo after it lands | reduces-trust |
| Consistency and standards | 4 | Primary CTA consistent; one modal has Cancel on right, rest on left | minor-polish |
| Error prevention | 4 | Delete confirms with the resource name — good | minor-polish |
| Recognition over recall | 3 | Cmd+K exists but not surfaced in tooltips | adds-friction |
| Flexibility and efficiency | 2 | No bulk-select on list of 200+ items | adds-friction |
| Aesthetic and minimalist | 3 | Hero has two competing accents | reduces-trust |
| Error recovery | 1 | "Something went wrong" with no ID, no retry | blocks-conversion |
| Help and documentation | 3 | Tooltips on most icons; empty state has no CTA | reduces-trust |
## Design Law Audit
| Law | Pass/Fail | Detail |
|-----|-----------|--------|
| Fitts's Law | FAIL | Primary CTA is 32px tall on mobile; expected ≥ 44px |
| Hick's Law | PASS | 5 top-nav items — within 7±2 |
| Doherty Threshold | FAIL | Save shows spinner for ~1.8s with no optimistic state |
| Cleveland-McGill | PASS | Bar charts for comparison, color for category only |
| Miller's Law | PASS | Form chunked into 4 logical sections |
| Tesler's Law | FAIL | New user sees 8 required config fields before first action |
## Top findings (ranked by impact)
1. Error recovery (score 1, blocks-conversion) — "Something went wrong" message strands users; no ID, no retry, no support path
2. Fitts's Law (FAIL, adds-friction) — primary CTA 32px on mobile; raise to ≥ 44px
3. Doherty Threshold (FAIL, adds-friction) — save feels broken; add optimistic UI
4. Flexibility (score 2, adds-friction) — bulk select missing on 200+ item list
5. Visibility (score 2, adds-friction) — submit has no loading state
```
**Notes on output:**
- The first row of each heuristic table is the short-name version (not the verbose Nielsen title). The heuristic is unambiguous from context.
- The `Top findings` list is a rank, not a re-listing. Include only findings at `reduces-trust` or worse. Cap at 5.
- Scores of 4-5 don't need a finding row unless the grader wants to surface what's working. Blank findings for 4-5 rows are fine; the Score column carries the signal.
---
## UsabilityScore (0-100) — the judged companion to UICraftScore
The scorecard above is qualitative. **UsabilityScore** rolls it into one 0-100 number + letter grade so it composes with the deterministic **UICraftScore** (`evals/quality/score.mjs`) and so a PM or CI run has a single figure to track.
> **Judged, not deterministic.** UsabilityScore is computed by the host agent from this rubric — it reflects judgment and *may vary run to run*. It is **not** reproducible the way UICraftScore is. Never gate CI on UsabilityScore; gate on the deterministic UICraftScore and use UsabilityScore for review depth. Always label it `(judged)` when reporting.
### Formula
Deterministic *given the scores* — the same scorecard always yields the same number. Only the act of scoring is judgment.
```
heuristic_base = round( ((mean(nielsen_scores) − 1) / 4) × 100 ) # 10 Nielsen scores, each 1–5
law_penalty = (number of FAILED design laws) × 5 # 6 laws, −5 each (max −30)
UsabilityScore = clamp( heuristic_base − law_penalty , 0 , 100 )
```
Grade bands are **identical to UICraftScore** for cross-comparison: **A ≥ 90 · B ≥ 80 · C ≥ 70 · D ≥ 60 · F < 60**.
Properties: all-5s with all laws passing → 100 (A). All-3s, all laws pass → 50 (F). The base maps the 1–5 mean linearly onto 0–100 (1→0, 3→50, 5→100); each failed law is a flat −5 because laws are physics/psychology violations with measurable cost.
### Worked example (the scorecard above)
Nielsen scores `2,4,3,4,4,3,2,3,1,3` → mean `2.9` → base `round(((2.9−1)/4)×100)` = **48**.
Failed laws: Fitts, Doherty, Tesler = 3 → penalty `15`.
`UsabilityScore = clamp(48 − 15, 0, 100)` = **33 / F**. (A genuinely broken UI scores in the F band — as it should.)
### Output
Append this block to the `/ui-craft:heuristic` output, after Top findings:
```markdown
## UsabilityScore
**33 / F** (judged) · heuristic base 48 − law penalty 15
| Component | Value |
|-----------|-------|
| Nielsen mean (1–5) | 2.9 |
| Heuristic base (0–100) | 48 |
| Failed design laws | 3 (Fitts, Doherty, Tesler) |
| Law penalty | −15 |
| **UsabilityScore** | **33 / F** |
```
Machine-readable form (emit when a caller asks for `--json`, or when invoked by `/sddesign` / an extended report):
```json
{
"usability": { "score": 33, "grade": "F", "judged": true },
"components": { "nielsen_mean": 2.9, "heuristic_base": 48, "failed_laws": 3, "law_penalty": 15 },
"version": "usability-1.0"
}
```
### Extended quality report (optional combine)
When the user wants the full quality picture, place the judged UsabilityScore beside the deterministic UICraftScore. Get the deterministic score with `node scripts/eval.mjs <path> --json` or the `score_ui` MCP tool, then render:
```markdown
## Extended quality report
| Score | Value | Reproducible? |
|-------|-------|---------------|
| UICraftScore | 82 / B | ✅ deterministic — identical every run |
| UsabilityScore | 33 / F | ❌ judged — LLM judgment, may vary |
Gate CI on UICraftScore. Use UsabilityScore to prioritize review depth — a high deterministic score with a low UsabilityScore means the code is clean but the *experience* has friction the static rules can't see.
```
The two scores are intentionally **not** averaged into one figure: one is reproducible and one is judged, and collapsing them would hide that distinction — the exact property the deterministic score exists to protect.
@@ -0,0 +1,287 @@
# Patterns Observed in Mature SaaS Interfaces
Observational pattern analysis from production-grade SaaS landing pages, abstracted from source. All values extracted from real CSS. Section 6 ("What Mature Interfaces Never Do") is the highest-signal section — read it first.
---
## 1. Cross-Surface Patterns
Universal rules observed across the full sample. Treat as proven conventions.
| Pattern | Detail |
|---------|--------|
| **Product screenshots with real data** | Never mockups, illustrations, or abstract graphics. Show the actual product with realistic numbers and content. |
| **Specific metrics in social proof** | "Build times went from 7m to 40s" beats "Trusted by thousands." Quantify the transformation. |
| **Tabs or interactive sections for features** | Feature areas use tabs, toggles, or interactive demos — never identical 3-column card grids. |
| **Dual CTAs in the hero** | Primary action (solid) + secondary action (outline/ghost). "Start for free" + "Get a demo" pattern. |
| **One positioning statement** | Hero headline is a single sentence that defines what the product is. No paragraphs. |
| **Logo strips of real companies** | "Trusted by" followed by 5-8 recognizable, specific company logos. |
| **Minimal decoration** | Content IS the design. No ornamental gradients, blobs, or abstract shapes competing with the product. |
| **Changelog/velocity proof** | At least one section proves the product ships fast — changelogs, release notes, version timelines. |
| **Every section has a distinct layout** | No two adjacent sections share the same visual structure. Variety signals intentional design. |
| **Centered hero alignment** | Center-aligned heroes dominate mature SaaS. Left-aligned is less common. |
---
## 2. Hero Section Archetypes
Six recurring hero patterns. Each is named by structure, not by source.
### Archetype A — Minimal text-only
One large headline + subhead + 1-2 CTAs. No product visual in the viewport. Relies entirely on copy to communicate value. Works when the product name alone carries recognition or when the headline is provocative enough to pull the scroll.
- Headlines: 5-10 words, sentence case
- Subhead: 1-2 lines max, 18-20px, neutral-500
- CTAs: dual (solid primary + ghost secondary) or single strong action
- Below hero: immediate product proof (no filler sections)
**When it breaks:** products without brand recognition or a strong category-defining statement — the missing visual leaves new users with nothing to anchor on.
### Archetype B — Asymmetric headline + product screenshot
Left-aligned headline block, right-aligned product UI or screenshot. Gives a document/editorial feel. Communicates "the product is the proof" without centering the hero on pure copy.
- Layout: ~50/50 split or 40/60 text/visual
- Visual: high-fidelity product screenshot, not illustration
- Alignment: text left, visual right (rarely reversed)
**When it breaks:** products with complex UIs that don't read quickly at a glance — the visual confuses instead of proving.
### Archetype C — Product-first (full-width UI)
Minimal headline above or beside a massive full-width product screenshot or embedded interactive demo. The product IS the hero. Copy is secondary.
- Headline: minimal, often a single phrase or word
- Visual: takes 60-80% of above-the-fold height
- Can include real interactive elements, not just static images
### Archetype D — Bento grid hero
Positioning statement centered above a bento grid of product surfaces. Each card shows a different feature or UI. Communicates product breadth.
- Grid: mixed card sizes — one large card + 2-3 smaller
- Each card shows a distinct product surface
- Typography: headline centered above the grid
### Archetype E — Editorial figure
Value propositions presented as numbered editorial figures ("FIG 0.2", "FIG 0.3"). Gives a technical publication feel. Strong for developer-focused products where "serious" positioning outweighs "approachable."
- Section labels: uppercase monospace or small-caps
- Layout: each figure gets a distinct visual treatment
- No uniform card grid
### Archetype F — Scrolling social proof strip
Hero text centered, immediately followed by a horizontally scrolling logo strip (continuous loop, not static). Makes "trusted by" feel dynamic and high-volume.
- Logos: monochrome or muted, not full-color
- Motion: CSS scroll animation, pauses on hover
- Placement: immediately below hero CTA block
**When it breaks:** early-stage products without recognizable logos — an empty or weak strip signals the opposite of trust.
---
## 3. Feature Presentation Patterns
No mature SaaS product uses identical card grids for features.
### Tab-based feature sections
- Horizontal tabs or pills at the top of a section
- Each tab reveals completely different content (screenshot, demo, description)
- Prevents the monotonous 3-column icon+heading+text grid
**When it breaks:** more than 5-6 tabs — cognitive load exceeds the benefit of the interactive reveal.
### Bento grid (varied card sizes)
- Mixed card dimensions: one large card + 2-3 smaller cards
- Each card shows a different product surface
- Unequal sizing creates visual hierarchy within the grid
### Editorial figures
- Value propositions presented as numbered figures
- Each figure gets a distinct layout treatment
- Feels like a technical publication, not a marketing template
### Product-first (screenshots as features)
- Instead of describing features, embed the actual product
- Interactive demos, live UI, or high-fidelity screenshots
- The product IS the feature presentation
### Alternating text + visual rows
- Text on one side, visual (screenshot/demo) on the other
- Sides alternate per row to create rhythm
- Each visual is unique — chart, screenshot, code block
### Key principle
Every feature section should feel designed individually for that feature. If two feature blocks can be swapped without anyone noticing, the design is too uniform.
---
## 4. Social Proof Patterns
| Pattern | Implementation |
|---------|----------------|
| **Logo strip** | Horizontal row of 5-8 company logos, monochrome or muted. "Trusted by" or "Used by teams at." |
| **Specific metrics** | Metric + customer name: "7m to 40s build times" next to company logo. |
| **Testimonial with identity** | Photo + name + title + company logo. Quote is specific, references the product feature. |
| **Customer count** | "Join 50,000+ teams" or similar, placed near CTA. |
| **Changelog as proof** | Release dates and features listed to show shipping velocity. |
### What makes social proof work
- **Specificity over volume** — one concrete metric beats "trusted by thousands"
- **Recognizable logos** — users infer quality from the companies they recognize
- **Real quotes with attribution** — name, role, company, photo. Generic praise without identity is worthless
- **Proximity to CTAs** — social proof placed directly before or after the call to action
---
## 5. Signature Details by Pattern Type
Concrete patterns observed in production. Each stands as a reusable technique.
### Card and surface treatment
- **Grid-line border aesthetic**: horizontal and vertical borders (`border-x`, `border-y` in Tailwind) with a custom grid-border color — creates a technical grid feel without background fills
- **Inset shadow for sunken surfaces**: `box-shadow: 0 2px 6px 0 rgba(0,0,0,0.2) inset` — signals an embedded or input-like container
- **Hairline card borders**: 1px `neutral-200` with a bottom-only `rgba(0,0,0,0.04)` shadow — separation without visual weight, common in card-heavy dashboard layouts
- **Bento card sizing ratio**: ~2:1 dominant card to minor cards; never all cards equal size
### Typography signatures
- **Display italics**: italicized hero headline as a typographic accent — distinctive without added color
- **Negative letter-spacing on all headings** (see Section 7 for exact values)
- **Font weight distribution skewed to medium**: `font-weight: 500` as the dominant weight across UI, `600` for emphasis only, `700` reserved for hero scale
- **Editorial section labels**: uppercase, tracked-out, small — "FIG 0.2" or "01 / OVERVIEW" style
### Color signatures
- **One accent, used sparingly**: accent color appears in CTAs and key interactive elements only — not in decorative elements
- **Nearly colorless base UI**: neutral-first palette; color reserved for status and brand accent
- **Dark mode via alpha overlays**: `color: rgba(255,255,255,0.9)`, `background: rgba(255,255,255,0.1)` on hover — avoids hardcoded dark-mode color values
- **Tinted neutral grays**: never pure `#000` or `#fff` as text; always cool-toned (see Section 7)
### Microinteraction signatures
- **Ring-halo hover**: `box-shadow: 0 0 0 4px var(--neutral-200)` instead of background color change — reads as "glowing" selection
- **Slide-up-fade entrance**: translate Y offset + opacity fade, 50ms staggered per item, 500ms duration
- **Hover lift**: `translateY(-2px)` + shadow growth at `150-200ms ease-out`
- **No `transition: all`**: always target specific properties — `transition: box-shadow, transform, opacity`
### Spacing signatures
- **Section rhythm**: 80-120px between sections
- **Card internal padding**: 24-40px
- **Card gaps**: 24-32px
- **Nav height**: 64px
- **Max content width**: 1080-1200px
---
## 6. What Mature Interfaces Never Do
Confirmed absent across the full observed sample:
| Anti-Pattern | Why It Fails |
|-------------|-------------|
| **Emoji icons in feature lists** | Looks unfinished. Production UIs use custom SVG icons or product screenshots. |
| **Colored gradient text** | None use rainbow or multi-color gradient text. Even dark-themed interfaces avoid it. |
| **Star ratings in testimonials** | Zero instances. All use quote + name + role + company. |
| **Identical 3-column feature cards** | No section repeats the same card layout for features. Each section is bespoke. |
| **Placeholder or generic text** | Every piece of text is specific to the product. No "lorem ipsum" energy. |
| **Bouncy/elastic animations** | Motion is subtle and functional. No bounce, no wiggle, no playful easing. |
| **Colored pill badges for metrics** | Metrics are displayed as plain text with clear hierarchy, not decorated badges. |
| **ALL CAPS headings** | Sentence case or title case throughout. No uppercase headings. |
| **Glassmorphism** | Not a single frosted-glass panel. Even dark-themed interfaces avoid it. |
| **Abstract blob/orb decorations** | Intentional branded gradients exist. Generic decorative blobs do not. |
| **Multiple competing accent colors** | One accent color, used consistently and sparingly. |
| **"Powered by" or tool attribution** | No marketing page advertises its own stack in the footer. |
| **Generic CTAs** | No "Learn more" or "Click here." Every CTA is specific: "Start for free", "Download", "Start Deploying". |
| **Walls of text** | No section has more than 2-3 sentences. Copy is ruthlessly concise. |
| **Pure black text** | None use `#000000`. Darkest text is cool gray — `#414552` or `#18181b` (zinc-900). |
---
## 7. Reference Token Values
Values extracted from production CSS. Use as calibration for "what premium feels like in code." Where two observed systems differ significantly, both are shown with labels.
### Type scale ranges observed
```
Hero (76-94px): weight 700, line-height 1.05, letter-spacing -0.04em
Section (48-56px): weight 700, line-height 1.1, letter-spacing -0.03em
Card (24-32px): weight 600, line-height 1.2, letter-spacing -0.02em
Body (16-17px): weight 400, line-height 1.5, letter-spacing normal
```
**Pattern A (medium-dominant)**: `font-weight: 500` as primary UI weight (120+ instances), `600` for emphasis only, `700` avoided entirely.
**Pattern B (bold-dominant)**: `700` reserved for hero and section headings only; `400` for all body copy.
### Spacing scales observed
```
Section gaps: 80-120px
Card gaps: 24-32px
Card padding: 24-40px
Nav height: 64px
Max content width: 1080-1200px
Base unit: 4px
```
### Shadow stack examples
**Blue-gray tinted shadows** (never pure black):
```css
/* Resting */ box-shadow: rgb(64 68 82 / 8%) 0px 2px 5px 0px;
/* Hover */ box-shadow: rgb(64 68 82 / 8%) 0px 2px 5px 0px, rgb(64 68 82 / 8%) 0px 3px 9px 0px;
/* Focus */ box-shadow: 0 0 0 4px rgb(1 150 237 / 36%);
```
**Ring-halo hover** (neutral, no blue tint):
```css
box-shadow: 0 0 0 4px var(--neutral-200, #e5e5e5);
/* Tailwind: hover:ring-4 hover:ring-neutral-200 */
```
**Inset sunken surface**:
```css
box-shadow: 0 2px 6px 0 rgba(0,0,0,0.2) inset;
```
### Border-radius observed
**Pattern A** (restrained):
```
Inputs: 4px | Buttons: 8px | Cards: 8-10px | Pills/badges: 999em
```
**Pattern B** (more generous):
```
Buttons: 8px (rounded-lg) | Cards: 12-16px (rounded-xl / rounded-2xl) | Badges: 9999px (rounded-full)
```
The difference teaches: card radius scales up proportionally with card size. Small cards (dashboard widgets) at 8px, large cards (feature showcase) at 12-16px.
### Color tinted-neutral examples
```
Text primary: #414552 (cool gray-700, NOT #000)
Text secondary: #687385 (cool gray-500)
Borders: #d5dbe1 (cool gray-150)
Surface: #f6f8fa (cool gray-50)
Brand accent: #625afa (purple-500 — one example; substitute project accent)
```
**Dark mode via alpha** (avoids hardcoded dark-mode palette):
```css
color: rgba(255,255,255,0.9); /* Tailwind: text-white/90 */
background: rgba(255,255,255,0.1); /* Tailwind: hover:bg-white/10 */
```
### Motion timing observed
```
Hover transitions: 150-200ms ease-out
Entrance animations: 500ms duration, 50ms staggered delay per item
Transform: translateY(-2px) on hover lift
Property targeting: box-shadow, transform, opacity — never transition: all
```
**Font pairing pattern** (display + system + mono):
```
Display headings: Satoshi or equivalent geometric sans
Body: Inter or equivalent neutral sans
Code/technical: Geist Mono or equivalent monospace
```
@@ -0,0 +1,236 @@
# Layout & Composition
Spatial reasoning, grouping principles, and hierarchy discipline. Tools (Flex, Grid) come last.
---
## Gestalt Grouping Principles
Perception organizes visual fields before conscious thought. Five laws govern how elements are read as groups:
**Proximity** — Elements closer together are perceived as related. Spacing is the primary grouping signal. Use it before borders, cards, or background fills.
**Similarity** — Elements that share shape, color, or size are perceived as the same kind of thing. Color-coding a column in a table exploits this; inconsistent icon weights break it.
**Common Region** — Elements inside a shared boundary (card, background, border) form a group. Use sparingly — every card is a visual tax. Ask: does this content need isolation, or does proximity alone suffice?
**Continuity** — The eye follows a line or curve through a layout. Aligning elements along an axis creates an implied path that guides the scan.
**Closure** — The eye completes incomplete shapes. Use this to reduce clutter: a partially-visible element (card cropped at viewport edge) signals "more exists" without a label.
### The primary rule: use spacing before structure
Before reaching for a card, background swatch, or divider line, try creating the group with spacing alone. If it reads, stop there. Structure adds visual weight and complexity — earn it.
---
## Spacing Rhythm
The most violated rule in UI. State it as an invariant:
> **Space within a group < space between groups < space between sections.**
Concrete: a form label 4px above its input, inputs in the same form 16px apart, the form itself 48px from the next section. If any of these relationships inverts, the grouping breaks.
### Scale
Use tokens from a defined set. Values come from the scale — never arbitrary.
```css
--space-xs: 0.25rem; /* 4px — label-to-input, icon-to-label */
--space-sm: 0.5rem; /* 8px — items within a group */
--space-md: 1rem; /* 16px — related components */
--space-lg: 1.5rem; /* 24px — sibling sections */
--space-xl: 2rem; /* 32px — component-to-component */
--space-2xl: 3rem; /* 48px — section separations */
--space-3xl: 4rem; /* 64px — major page regions */
--space-4xl: 6rem; /* 96px — hero-to-content, above-fold breathing room */
```
Use `gap` for sibling spacing — eliminates margin collapse and is the correct mental model.
Use `clamp()` for fluid spacing: `clamp(var(--space-lg), 4vw, var(--space-3xl))` scales without breakpoints.
**WHEN IT BREAKS:** Very dense data tables and compact UI patterns (command palettes, menus) compress this rhythm deliberately. At 100% zoom the hierarchy still holds — the scale values shrink proportionally. Never collapse two adjacent levels to the same value.
---
## Visual Hierarchy
### The Squint Test
Stand back, defocus your eyes, or apply a blur filter to the design. The image resolves to blobs of light and dark, size and contrast — text becomes unreadable. What you see is the page at the perceptual level of a first glance (~200ms).
At that level:
1. One thing should dominate — the primary action or headline.
2. A secondary cluster should appear next — supporting information.
3. The page structure (navigation, footer, sidebar) should be lowest in perceived weight.
Why it works: the human visual system applies a low-pass spatial frequency filter before conscious reading. The squint test simulates that filter. If hierarchy fails the squint test, users experience visual noise even if they cannot articulate why.
### Measurable Ratios
Four levels is the practical maximum for most UI. Beyond that, distinctions collapse.
| Level | Role | Signal |
|-------|------|--------|
| Primary | Page-defining element (hero headline, main CTA) | Largest, highest contrast, most visual weight |
| Secondary | Supporting structure (subheadings, section labels) | Minimum 1.5x smaller or lighter than primary |
| Tertiary | Body content, field labels, card titles | Readable, not competing |
| Quaternary | Metadata, timestamps, captions | Muted, clearly subordinate |
**Minimum 1.5x difference between adjacent levels** to create a clear distinction. A 1.2x difference reads as an accident, not a decision.
**WHEN IT BREAKS:** Typography pairs can carry hierarchy at identical size using weight + tracking — a 400/700 weight pair at 14px is a clear two-level system. In that case the size ratio rule yields to the type system. See `typography.md`.
### Hierarchy Tools (use in this order)
1. **Space** — generous whitespace isolates the primary element before you touch anything else
2. **Size** — scale differences above 1.5x register immediately
3. **Weight** — font-weight, border-weight, shadow depth
4. **Color** — last resort; adds semantic load and accessibility risk
Using all four at once creates noise. Mature interfaces use one or two.
---
### Serial Position Effect
First and last items in a sequence are remembered best (primacy + recency). Put the critical item first or last in nav lists, onboarding steps, and feature rows — never buried mid-sequence. Mid-list is where options go to be forgotten.
## Composition Strategies
### Symmetry vs. Asymmetry
Symmetry signals stability, formality, and confidence. Use it for hero sections, authentication flows, landing page headers.
Asymmetry signals energy, modernity, and movement. Use it for editorial layouts, feature showcases, dashboards with a dominant primary panel.
Avoid the default of centered-everything — it reads as unsophisticated because it makes no compositional decision. Left-aligned layouts with deliberate asymmetric weight distribution feel more considered.
### Focal Point
Every layout needs exactly one focal point — the element the eye arrives at first. If you have two competing elements at the same visual weight, one of them is wrong. Remove hierarchy from one or subordinate it spatially.
### Optical Center vs. Geometric Center
The geometric center of a rectangle is at 50% vertical. The optical center is 5–8% above that. Content positioned at the optical center feels balanced; content at true geometric center reads as slightly low.
This matters most for:
- Modal dialogs — vertically centered modals in a viewport look sunken; use `align-items: center` with a slight upward offset or top-biased margins
- Icon placement within a bounding box — a 24×24 icon with a visually tall glyph needs a −1px vertical nudge
- Above-the-fold hero sections — weight the primary element in the upper optical zone
### Rule of Thirds (use only with hero imagery)
The rule of thirds creates dynamic tension by placing subjects at grid intersections rather than center. In interfaces without strong photographic content it adds complexity without benefit. Reserve it for editorial layouts and landing pages with full-bleed images.
---
## Alignment Discipline
- **Max 1-2 alignment types per section.** Every additional alignment axis is another anchor the eye must re-find; mixed left/center/right in one view reads as unplanned.
- **Mixed text sizes on one line align to the shared baseline**, not to vertical centers — center-aligning a 24px number next to a 13px label makes the label float.
- **Truncate the middle, not the end**, when items share long prefixes ("Quarterly report — … — Q3 2026"): the differentiating ends are what users scan for.
---
## Choosing Layout Tools
**Flexbox — 1D.** Rows of items, nav bars, button groups, component internals. Default choice for the majority of components.
**Grid — 2D.** Page structure, dashboards, anything requiring row AND column coordination. Reach for it when flex with `flex-wrap` forces workarounds.
```css
/* Responsive without breakpoints — flex */
.group { display: flex; flex-wrap: wrap; gap: var(--space-md); }
/* Responsive without breakpoints — grid */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-lg); }
```
Don't default to Grid when Flex would be simpler. Don't reach for JS measurement when CSS layout solves it.
---
## Breaking Monotony
- Don't default to card grids for everything — spacing creates grouping naturally
- Cards only when content is truly distinct and actionable — never nest cards inside cards
- Vary card sizes, span columns, or mix cards with non-card content
- Asymmetric compositions break the centered-content default when the content supports it
- Never template the "big metric + small label + stat row + gradient" pattern — it's a design cliché
---
## Depth & Elevation
### Z-Index Scale (Semantic)
```css
--z-dropdown: 10;
--z-sticky: 20;
--z-modal-backdrop: 30;
--z-modal: 40;
--z-toast: 50;
--z-tooltip: 60;
```
Never use arbitrary values (999, 9999). Use elevation to reinforce hierarchy, not as decoration.
### Shadow Scale
```css
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);
--shadow-xl: 0 20px 25px rgba(0,0,0,0.1), 0 8px 10px rgba(0,0,0,0.04);
```
Always layered (ambient + direct light). Higher elevation = larger spread + lower opacity, not just a bigger number.
**Dark mode breaks layered shadows.** A dark shadow cast on a dark surface has nowhere to show up — the layered ambient+direct recipe above effectively disappears. Don't stack darker shadows to compensate; collapse elevation to a single subtle light ring instead — a 1px, low-alpha white inset or outset border (`0 0 0 1px rgba(255,255,255,0.08)`-ish) reads as "raised" against a dark surface the way a shadow does against a light one.
---
## Optical Adjustments
- **±1px nudges** when geometric centering looks visually off-center — common with icons inside buttons, badges, avatars
- **Nested radii:** child ≤ parent. Use `calc(var(--radius-parent) - var(--padding))` to keep concentric curves
- **Icon/text lockups:** adjust weight, size, spacing, or color — one lever at a time until they feel unified
### Concentric Radius Arithmetic
The relationship that keeps nested corners visually parallel: **outer radius = inner radius + the gap between the two edges.** Worked example on the token scale — an inner surface using `--radius-xl` (14px) sitting inside 6px of padding needs an outer radius of `14 + 6 = 20px`, which lands exactly on `--radius-2xl`. Skip the arithmetic and the outer curve either pinches tighter than the inner one (looks accidental) or bows out looser (looks sloppy) — concentric only reads as intentional when the two curves share a center point.
**Exception:** past roughly 24px of padding, the two surfaces read far enough apart that concentric math stops mattering — at that distance, treat inner and outer as separate surfaces with independently chosen radii rather than chasing an arithmetic relationship the eye can no longer perceive.
### Optical Alignment Recipes
- **Icon-leading buttons need asymmetric padding** — trim roughly 2px off the icon side of the button's padding. An icon's own whitespace (the space inside its bounding box between the glyph and its edge) reads as extra padding the eye already counts, so equal padding on both sides looks lopsided toward the icon.
- **Triangular and asymmetric glyphs need a nudge toward their visual mass.** Play icons, carets, and similar shapes have most of their ink weight off-center within their bounding box — a 1-2px nudge in the direction the shape "leans" corrects the optical imbalance a perfectly centered bounding box leaves behind.
- **When an SVG is persistently off-balance across every context it's used in**, fix the path's `viewBox` centering at the source rather than compensating with margin on every instance. Per-instance margin hacks drift out of sync the next time the icon is resized or reused elsewhere; a corrected `viewBox` fixes it everywhere at once.
---
## Style-to-CSS Reference
| Property | Minimal Clean | Soft Modern | Sharp Geometric | Rich Editorial | Dark Premium | Playful Bold |
|----------|---------------|-------------|-----------------|----------------|--------------|--------------|
| `border-radius` | 2-4px | 8-16px | 0px | 2-4px | 4-8px | 12-20px |
| Shadows | None or barely-there | Layered (ambient + direct) | None — use borders | Subtle, warm | Subtle glow, inset | Bold, offset |
| Borders | `rgba(0,0,0,0.06)` 1px | Soft, same hue as shadow | 1px crisp, high-contrast | Thin, warm-tinted | `rgba(255,255,255,0.08)` | Thick, colored |
| Spacing | Generous whitespace | Comfortable, padded | Tight, precise | Generous, editorial | Moderate | Loose, breathing |
| Weight range | 400-600 | 400-600 | 400-800 | 300-700 | 400-700 | 500-800 |
---
## Never
- Arbitrary spacing outside your defined scale
- All spacing equal across a section — variety creates hierarchy
- Wrap everything in cards — not everything needs a container
- Nest cards inside cards
- Center everything — left-aligned asymmetry reads as more intentional
- Default to Grid when Flex would be simpler
- Arbitrary z-index values (999, 9999)
- Two adjacent hierarchy levels at the same visual weight — decide which leads
+155
View File
@@ -0,0 +1,155 @@
# Loop Engine
Run→evaluate→fix one→re-evaluate→stop. A loop is not a review pass — it iterates until a binary gate passes or the budget runs out. A loop NEVER hard-depends on a renderer, NEVER auto-installs anything, and ALWAYS degrades with an explicit notice when visual evaluation is unavailable. The budget is non-negotiable: when it is exhausted the engine stops and reports, even if the gate has not passed.
---
## Engine Contract
Every preset declares all five fields. None is optional.
| Field | Allowed values | Description |
|-------|---------------|-------------|
| `target` | `new-build` \| `existing-surface` \| `user-screenshot` | The artifact being evaluated |
| `lens` | one or more reference file paths / sections | Reference content that drives evaluation |
| `gate` | `checklist-all-green` \| `zero-critical` \| `zero-off-system` | Binary pass condition |
| `renderer` | `required` \| `none` | Renderer dependency level — `required` degrades to code-only if unavailable; `none` means static analysis only (always available) |
| `budget` | integer 1–5 (default 3, hard cap 5) | Maximum iterations allowed |
---
## The Loop
The agent executes this numbered procedure for every iteration:
1. **Read** the target AND `.ui-craft/brief.md` §6 (learned constraints = override registry). Brief §6 overrides preset gates; the a11y/correctness floor is never overridden.
2. **Detect renderer** (see Renderer Detection Ladder below) and emit the pre-flight cost notice — preset name, budget, "will run up to N iterations; each iteration runs one full evaluate → fix → re-evaluate cycle." MUST emit before any iteration begins.
3. **Evaluate** the target through the preset's lens → produce a severity-ranked findings list.
4. **Select ONE highest-impact fix.** Ranking: Critical before Major; tiebreaker within the same tier = earlier-numbered finish-bar pass / earlier-listed state in the state-design lattice / earlier-cited finding in the brief; for token-consistency: within the same tier, apply layer order — primitive token violations before semantic, semantic before component. MUST NOT apply more than one fix per iteration.
5. **Apply** the selected fix.
6. **Re-evaluate** — run the gate check on the updated state.
7. **Stop** when the gate passes OR the iteration count reaches the budget, whichever comes first. On budget exhaustion: STOP, print final gate state, and list all open findings as "Unresolved within budget:". MUST NOT silently extend the budget.
**Anti-thrash rule:** a fix that introduces a new violation is caught in the NEXT iteration — no inline re-validation. It counts against the budget.
---
## Renderer Detection Ladder
Probe in this order; stop at the first match:
1. `playwright` / `@playwright/test` / `puppeteer` in `package.json` devDependencies → use it.
2. Existing e2e / visual harness file (`playwright.config.*`, `cypress.config.*`, `cypress/`, `.storybook/test-runner*`) → use it.
3. Agent-browser tool present in the current agent session → **announce before launch** ("I will invoke agent-browser for visual evaluation — proceeding unless you say stop") then use it.
4. `npx playwright screenshot` — **OPT-IN ONLY.** Present as a choice; NEVER auto-run. If the user declines → rung 5.
5. **NONE** → degrade to code-only reasoning. Emit verbatim:
> "renderer: none — evaluating code only; visual rendering unverified. Findings cover structure, tokens, and code-level anti-slop signals. Confidence: partial."
---
## Honesty / Confidence Contract
Every loop report MUST state:
- **Renderer tier** used (rung number from the detection ladder above).
- **Whether visual evaluation occurred.**
- **Confidence level** — `full` (renderer present), `partial` (code-only or renderer degraded).
- **Iteration count vs budget** — e.g., "2 of 3 iterations used."
- **Unresolved findings** — any finding still open when the loop stops.
**`target = user-screenshot` → EVALUATE-ONLY mode:**
- Produce a severity-ranked findings report.
- MUST NOT apply any fixes.
- MUST NOT run fix iterations.
- MUST NOT claim the gate passed or failed.
- MUST state: "Target is a screenshot — findings reported; no fix iterations run."
**`visual-anti-slop` with renderer=none** is the SAME preset degraded — not a separate preset. Report the gate as "partial — visual rendering unverified."
---
## Anti-thrash + Budget Rules
- **One fix per iteration.** The ranking above is mandatory — it prevents oscillation.
- The iteration counter is tracked in the report ("iteration 2 of 3").
- Budget exhausted → STOP, print final gate state + "Unresolved within budget:" list. The user must explicitly raise the budget; the engine MUST NOT auto-extend.
- Brief §6 constraints override preset gates. The a11y/correctness floor is never overridden by brief constraints.
---
## Preset Catalog
| Preset | target | lens | gate | renderer | maps to |
|--------|--------|------|------|----------|---------|
| `visual-anti-slop` | new-build \| existing-surface | SKILL.md Anti-Slop Test + finish-bar.md Pass 1 + Pass 9 | zero-critical (Major: absent or explicitly user-confirmed — no self-accept, silence ≠ accepted) | required — degrades to code-only | `/finalize` |
| `state-coverage` | new-build \| existing-surface | state-design.md lattice, knob-gated to CRAFT_LEVEL | checklist-all-green (all required states present) | none | `/unhappy` |
| `token-consistency` | new-build \| existing-surface | tokens.md 3-layer contract + SKILL.md Anti-Slop Minor items | zero-off-system | none (static scan) | `/tokens audit` |
---
## visual-anti-slop — eliminate AI-template signals until the gate passes
**Lens:** SKILL.md Anti-Slop Test (Critical + Major items) + `finish-bar.md` Pass 1 (hierarchy) + Pass 9 (pixel honesty).
**Gate:** all Critical Anti-Slop items absent AND all Major items absent or explicitly user-confirmed. A Major is "accepted" ONLY when the user explicitly confirms it (during pre-flight or mid-loop); absent explicit confirmation it stays OPEN and counts against the gate. No self-accept, no "not found on re-eval = accepted".
**Renderer:** required — degrades to code-only (partial confidence) if unavailable. The gate is reported as "partial — visual rendering unverified" when renderer=none. MUST NOT claim full pass on visual criteria without a renderer.
**Highest-impact ranking:** Critical Anti-Slop findings first; within Major, earlier-numbered finish-bar pass wins.
**Example run:**
- Pre-flight: "visual-anti-slop, budget 3, will run up to 3 iterations; each iteration runs one full evaluate → fix → re-evaluate cycle."
- Iter 1: Critical — identical card grid detected → refactor to asymmetric layout. Re-evaluate.
- Iter 2: Major — uniform border-radius on all elements → vary by element role. Re-evaluate.
- Gate passes (0 Critical, 0 unaccepted Major). Stop at 2/3 iterations.
---
## state-coverage — iterate until all required states are present
**Lens:** `state-design.md` state lattice, knob-gated to CRAFT_LEVEL (per state-design.md lattice — see the CRAFT_LEVEL table there for the full threshold definition).
**Gate:** all required states (per the CRAFT_LEVEL knob, as defined in state-design.md) present as explicit designs or stubs.
**Renderer:** none — code analysis only; always available.
**Highest-impact ranking:** missing required states ordered by position in the state-design lattice (idle → loading → empty → error → partial → success → conflict → offline).
**Example run:**
- Pre-flight: "state-coverage, budget 3, will run up to 3 iterations; each iteration runs one full evaluate → fix → re-evaluate cycle."
- Iter 1: CRAFT_LEVEL 6 → required: idle/loading/empty/error/success. Missing: empty, error. Highest-impact = empty (lattice position). Stub empty state with copy + CTA. Re-evaluate.
- Iter 2: Missing: error. Stub error state with cause + retry. Re-evaluate.
- Gate passes (all required states present). Stop at 2/3 iterations.
---
## token-consistency — iterate until zero off-system values remain
**Lens:** `tokens.md` 3-layer contract (per tokens.md 3-layer contract — see that file for canonical layer definitions) + SKILL.md Anti-Slop Minor items for hardcoded values.
**Gate:** zero off-system values — any color, radius, or spacing value not drawn from the project's token system.
**Renderer:** none — static code scan; always available.
**Highest-impact ranking:** Critical token violations (raw hex colors) before Major (arbitrary radii, magic spacing); tiebreaker = order of appearance in the file.
**Example run:**
- Pre-flight: "token-consistency, budget 3, will run up to 3 iterations; each iteration runs one full evaluate → fix → re-evaluate cycle."
- Iter 1: Off-system values found: `#3B82F6` (hex color), `border-radius: 6px` (magic value). Highest-impact = hex color → replace with `var(--accent)`. Re-evaluate.
- Iter 2: Remaining: `border-radius: 6px` → replace with `var(--radius-sm)`. Re-evaluate.
- Gate passes (zero off-system values). Stop at 2/3 iterations.
---
## Choosing / Adding a Preset
| Use case | Preset |
|----------|--------|
| Surface looks AI-generated; fix visual slop | `visual-anti-slop` |
| Edge-case states are missing (loading, error, empty) | `state-coverage` |
| Hardcoded colors, radii, or spacing bypass the token system | `token-consistency` |
**Preset block template** (drop-in for future presets — mirror this file's style):
```
## <preset-name> — <one-line intent>
**Lens:** <reference file(s) + exact section(s)>
**Gate:** <binary pass condition>
**Renderer:** <none | required | gated — degrades to code-only at lower confidence>
**Highest-impact ranking:** <tier order + tiebreaker>
**Example run:** <3-5 line trace: iter 1 finding→fix, iter 2 …, stop reason>
```
Add the new preset row to the Preset Catalog table above. All five template fields are required.
@@ -0,0 +1,98 @@
# Metadata
Page metadata quality — correctness of what's already being emitted, not SEO strategy. For keyword strategy, content structure, or ranking tactics, defer to an SEO skill; this file is about the mechanical contract between what a page claims about itself and what actually ships.
---
## When to Read This
Read when a task touches `<title>`, `<meta>` tags, canonical URLs, Open Graph / Twitter Card tags, structured data (JSON-LD), or favicons — building a new page template, auditing an existing site, or wiring up a metadata API in a framework (Next.js `generateMetadata`, Remix `meta`, etc.).
---
## Social Sharing Tags
Any page that could get pasted into Slack, X, LinkedIn, or iMessage needs `og:title`, `og:image`, and `twitter:card` present at minimum. Drop one and the platform improvises — usually a generic card, sometimes a broken one.
```html
<meta property="og:title" content="Page title" />
<meta property="og:description" content="One or two sentences." />
<meta property="og:image" content="https://example.com/og/page-slug.png" />
<meta property="og:url" content="https://example.com/page-slug" />
<meta name="twitter:card" content="summary_large_image" />
```
Tags existing in the page source proves nothing about how a card actually renders. Crawlers for these platforms can't resolve `localhost`, so the only real test is pasting a live deployed link into the destination (or its dedicated preview debugger) and reading what comes back.
---
## Deterministic Metadata
Nothing feeding a title, description, or JSON-LD block should trace back to an unstable input — no `Math.random()`, no `Date.now()`, no client-only state read before first paint.
Two distinct failures follow from ignoring this:
- **Hydration mismatch** — the server-rendered value and the client's first pass disagree, and the framework flags it.
- **A stuck preview cache** — social platforms fetch a URL's metadata once and cache the result. If that single fetch happened to capture a randomized or time-sensitive value, every future share of that link shows the same wrong preview, with no external way to force a refresh.
Pull metadata only from route params, fetched content, or build-time constants.
---
## Identity Consistency
Title, description, canonical URL, and `og:url` all describe one page — a contradiction between any two of them (a title selling one thing, a description written for another) is a bug, not a stylistic quirk.
Settle on the page's single canonical form up front — trailing slash or not, `www` or not, query params stripped or kept — and reuse that exact string in the canonical tag, the sitemap, internal links, and the Open Graph URL. Letting any of these drift from the others leaves crawlers guessing which version is authoritative, which can split ranking signal across what should be one page.
---
## Non-Production Indexing
Preview deployments, staging environments, and branch builds ship with search indexing turned off:
```html
<meta name="robots" content="noindex, nofollow" />
```
Wire this to the environment itself — a build-time flag, not a checkbox a person remembers to tick — because the one deploy someone forgets is exactly the one that matters. An indexed staging URL then competes with the real page in search results and can surface content before it's meant to be public.
---
## Structured Data (JSON-LD)
Structured data should restate facts that already exist elsewhere on the page, not add new ones for the crawler's benefit.
```json
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Actual product name",
"offers": {
"@type": "Offer",
"price": "49.00",
"priceCurrency": "USD"
}
}
```
Don't fabricate organization details that aren't published anywhere, a price the product doesn't actually charge, a review count nobody left, or a `ratingValue` backed by zero reviews. Search engines diff structured data against the visible page and penalize the mismatch — and beyond the penalty, a made-up rating is a false claim presented to users as fact. No reviews yet means the `AggregateRating` block gets omitted, not filled with placeholder numbers.
---
## Favicons & Touch Icons
A single `favicon.ico` doesn't cover the surfaces that request icons — browsers, home-screen bookmarks, and OS task switchers each ask for a different size, and any size that isn't supplied falls back to an unstyled default glyph.
```html
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
<link rel="manifest" href="/site.webmanifest" />
```
---
## Cross-References
- `accessibility.md` — semantic structure that structured data and screen readers both depend on.
- `responsive.md` — `og:image` dimensions and safe-area concerns for share-preview screenshots.
@@ -0,0 +1,448 @@
# Modern CSS Capabilities
CSS capabilities that unlock interaction patterns otherwise requiring JS: View Transitions, CSS Scroll Timelines, Anchor Positioning, Container Queries, `@starting-style`, allow-keywords. Each section: when to reach for the API, what principle it encodes, and the rules that follow from the principle.
---
## View Transitions API
View Transitions preserve scroll position and focus across navigations because the browser captures both before and after states as snapshots, eliminating the layout thrashing and focus loss that hand-rolled JS transitions cause. The browser handles the morph; you name the elements that should persist.
**Use View Transitions when:** DOM identity is preserved across views — App Router page transitions, list-to-detail flows where the same element exists on both sides of the navigation.
**Don't use View Transitions for:** modal overlays (use `@starting-style` + `display: block` allow-keywords instead), in-page state transitions (use CSS Scroll Timelines or simple transitions), tab-switching within the same page (no view boundary).
### Basic Usage
```js
document.startViewTransition(() => {
// Update DOM here
updatePage();
});
```
### Named Transitions
```css
/* Source element */
.card-image { view-transition-name: card-hero; }
/* Style the transition */
::view-transition-group(card-hero) {
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
```
### Rules
Each `view-transition-name` must be unique at the moment the transition fires — two elements sharing the same name will cause the transition to fail silently. Names are a handshake between the before and after state; clean them up so stale names don't leak across navigations.
- Each `view-transition-name` must be **unique during transition**
- **Clean up names** after transition completes:
```js
sourceImg.style.viewTransitionName = "card";
document.startViewTransition(() => {
sourceImg.style.viewTransitionName = "";
targetImg.style.viewTransitionName = "card";
});
```
- Use **only for navigation-level changes** — avoid for rapid interactions
- Interruptibility is limited — avoid for interaction-heavy UI
- Prefer over JS animation libraries for page transitions
---
## @starting-style
`@starting-style` defines the element's style on first paint, before the browser computes the transition. Without it there is no "from" state for a newly mounted element — the browser skips the transition entirely. This replaces the `useEffect` → `setMounted(true)` → `data-mounted` pattern with a single CSS block.
```css
.toast {
opacity: 1;
transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
```
Baseline 2024. Pair with `transition-behavior: allow-discrete` when the element toggles `display`.
---
## CSS Scroll Timelines
Scroll Timelines drive animations from scroll position rather than time, running off the main thread with zero scroll event listeners. The timeline replaces the clock — the progress of the scroll IS the animation progress.
### Scroll-Linked
```css
.progress-bar {
animation: grow linear;
animation-timeline: scroll();
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
```
### View-Linked (element entering viewport)
```css
.reveal {
animation: fade-in linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
```
### Rules
Scroll Timelines exist because polling `scrollY` or attaching scroll event listeners for continuous animation causes layout/paint work on every frame — a Scroll Timeline runs on the compositor thread and costs nothing. Reserve IntersectionObserver for one-shot visibility triggers (wider support, no continuous callback overhead).
- Prefer Scroll/View Timelines over JS for scroll-linked motion
- Never poll scroll position for animation
- Never use `scroll` event listeners for continuous animation
- Use IntersectionObserver for visibility/pausing (wider support)
---
## Container Queries
A component's layout should respond to the space it occupies, not the viewport. Viewport media queries break reusable components because the same component in a sidebar and in main content hits the same breakpoints even though the available space is completely different. Container queries fix the unit of measure.
```css
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card { display: grid; grid-template-columns: 1fr 2fr; }
}
@container card (max-width: 399px) {
.card { display: flex; flex-direction: column; }
}
```
---
## Pseudo-Elements
`::before` and `::after` add a layer of styling surface without extra DOM nodes. The most useful application is interactive state overlays — placing a pseudo-element between the element's background and content lets hover/focus states change the overlay without affecting surrounding elements. Hit target expansion is the other dominant use case: extend the interactive area without changing the visible element.
### Decorative Content (::before / ::after)
```css
/* Background effect without extra DOM */
.button {
position: relative;
z-index: 1;
}
.button::before {
content: "";
position: absolute;
inset: 0;
background: var(--gray-3);
z-index: -1;
border-radius: inherit;
transition: background 150ms ease;
}
.button:hover::before {
background: var(--gray-4);
}
```
### Hit Target Expansion
```css
.small-icon-button {
position: relative;
}
.small-icon-button::before {
content: "";
position: absolute;
inset: -8px -12px; /* Expand clickable area */
}
```
### Rules
- `::before`/`::after` **require `content` property** to render
- Parent must have `position: relative` for absolute pseudo-elements
- Pseudo-elements need `z-index` for proper layering
- **Use pseudo-elements for decoration** — don't add extra DOM nodes
- Native variants: `::backdrop` (dialog/popover backgrounds), `::placeholder` (input text), `::selection` (highlighted text)
---
## clip-path for Animation
`clip-path: inset()` is the cleaner alternative to animating `width` or `height` for reveal effects — it composites, doesn't reflow, and accepts fractional values. Four inset values map to top/right/bottom/left clipping edges; animate between them for directional reveals.
```css
.hidden { clip-path: inset(0 100% 0 0); } /* clipped from right */
.visible { clip-path: inset(0 0 0 0); } /* fully visible */
.element { transition: clip-path 200ms ease-out; }
```
Patterns: hold-to-delete (2s linear on `:active`), tab transitions, image reveals on scroll, comparison sliders. In all cases the visible-state value is `inset(0 0 0 0)` — vary the hidden-state edge to control direction.
---
## Useful Modern Properties
| Property | Use |
|----------|-----|
| `text-wrap: balance` | Balanced line lengths for headings |
| `text-wrap: pretty` | Better line breaks for body text |
| `font-variant-numeric: tabular-nums` | Aligned numbers in tables/data |
| `overscroll-behavior: contain` | Prevent scroll chaining in modals |
| `scroll-margin-top` | Offset for anchor links with sticky headers |
| `content-visibility: auto` | Lazy-render off-screen content |
| `color-scheme: dark` | Native dark mode for scrollbars/forms |
| `accent-color` | Style form controls (checkboxes, radios) |
| `scrollbar-gutter: stable` | Reserve the scrollbar track so content never shifts when it appears |
---
## Edge Fades: `mask-image`, Never a Gradient Overlay
A fade to the edge of a scroll area or an image is a **transparency** effect, not a color effect. Build it with `mask-image`, never with a `linear-gradient` overlay from `transparent` to the background color.
```css
/* Wrong — a gradient overlay hardcodes the background */
.fade { background: linear-gradient(to bottom, transparent, var(--bg)); }
/* Right — the mask fades the element itself */
.fade {
mask-image: linear-gradient(to bottom, black 60%, transparent);
}
```
**Why:** the overlay is painted in one specific color, so it only reads correctly on that exact background. It breaks the moment the element sits on a tinted surface, inside a card, in dark mode, or over an image — a visible band appears where the fake background meets the real one. A mask removes pixels instead of painting over them, so it is correct on every background by construction. It also survives a theme flip with no dark-mode override.
### Rules
- **Never fade over a scroll area's leading edge.** A mask at the bottom of a list hides exactly the content the user is scrolling toward. Fade the trailing edge only, or drop the effect.
- **Ease the stops on any fade over ~200px.** A two-stop gradient bands on wide surfaces; add intermediate stops (`black 60%, rgba(0,0,0,0.6) 80%, transparent`) so the ramp reads smooth.
- **Prefer masks for image edges and text truncation too** — same reasoning: independent of the surface below.
---
## Anchor Positioning
Anchor Positioning replaces JS-driven popover positioning — no `getBoundingClientRect()`, no resize observers, no coordinate math. The browser owns the placement and handles viewport overflow through declarative fallback chains. Baseline 2026 (Firefox 147, Safari 26, Chrome 125+). Kills the Floating UI dependency for most cases; pair with the Popover API for a fully declarative menu.
```css
.button {
anchor-name: --trigger;
}
.popover {
position: absolute;
position-anchor: --trigger;
position-area: bottom center; /* one-line placement */
margin-top: 8px;
}
/* Fallback if there's no room below */
@position-try --top {
position-area: top center;
margin-top: 0;
margin-bottom: 8px;
}
.popover {
position-try-fallbacks: --top;
}
```
### Rules
Anchor names are scoped to their containing block by default — explicitly declare `position-anchor` in nested contexts to avoid resolution ambiguity. Always define a fallback so viewport-edge popovers don't clip rather than flip.
- Anchor names are scoped by default; use `position-anchor` explicitly in nested cases
- Always define a `position-try-fallbacks` for viewport-edge cases
- Pair with `<div popover>` for automatic focus and dismiss handling
- Progressive enhancement: feature-detect with `@supports (anchor-name: --x)`, fall back to JS only for older browsers
---
## Popover API + `<dialog>`
The browser already implements focus trapping, ESC handling, and backdrop rendering for modal and non-modal overlays. Building these in JS means reimplementing browser behavior with more code and more failure modes. Use the platform.
```html
<!-- Popover: lightweight menu, tooltip, dropdown -->
<button popovertarget="menu">Open menu</button>
<div id="menu" popover>
<button>Profile</button>
<button>Settings</button>
<button>Sign out</button>
</div>
<!-- Dialog: modal with backdrop -->
<dialog id="confirm">
<form method="dialog">
<p>Delete this workspace?</p>
<button value="cancel">Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
```
```js
// Open modal dialog (ESC closes, focus trapped)
document.getElementById('confirm').showModal();
```
### Rules
Native `<dialog>` and `[popover]` enforce correct semantics and ARIA roles automatically — a custom `<div role="dialog">` with manual focus management is always more code for a worse result.
- Use `<dialog>` with `showModal()` for modals — get focus trap + backdrop + ESC for free
- Use `popover` attribute for non-modal overlays (menus, tooltips, combobox panels)
- Pair Popover with Anchor Positioning for placement instead of JS math
- Style `::backdrop` for dim/blur behind modals
- **Anti-pattern:** custom `<div role="dialog">` with manual focus management when `<dialog>` works
---
## `interpolate-size: allow-keywords`
Browsers historically refused to animate to intrinsic sizes (`height: auto`, `width: max-content`) because the value isn't known until layout. `interpolate-size: allow-keywords` opts in to browser-side interpolation of these values, enabling accordion and disclosure patterns without JS measurement. Baseline 2025 — supported in all current major browsers; keep the JS fallback only if you support older releases.
```css
:root {
interpolate-size: allow-keywords;
}
.accordion {
height: 0;
overflow: hidden;
transition: height 300ms var(--ease-out);
}
.accordion[open] {
height: auto; /* now animatable */
}
```
### Rules
Set at `:root` so every element in the document inherits the opt-in — scoping it narrower creates inconsistency. The fallback for non-supporting browsers is to measure `scrollHeight` in JS and animate to a pixel value.
- Set `interpolate-size: allow-keywords` at `:root` to opt-in globally
- Works with `height`, `width`, `block-size`, `inline-size` to/from `auto`, `min-content`, `max-content`, `fit-content`
- Fallback for non-supporting browsers: JS measures scroll height and animates to a pixel value
- Respect `prefers-reduced-motion` — snap to final size without transition
---
## `color-mix()` for Theming Derivations
Hand-tuned shade ladders break every time the base token changes. `color-mix()` derives hover, pressed, muted, and ring variants from a single base token — one change propagates through every derived value. Mix in `oklch` for perceptual uniformity; sRGB mixing produces inconsistent lightness shifts across hues.
```css
:root {
--accent: oklch(62% 0.15 250);
--surface: oklch(98% 0.005 250);
--accent-hover: color-mix(in oklch, var(--accent) 85%, white);
--accent-pressed: color-mix(in oklch, var(--accent) 85%, black);
--accent-muted: color-mix(in oklch, var(--accent) 20%, var(--surface));
--accent-ring: color-mix(in oklch, var(--accent) 40%, transparent);
}
```
### Rules
- Mix in `oklch` or `lch` for perceptual uniformity — never `srgb` unless the base is already sRGB
- Same derivation pattern works for semantic palettes: `--success`, `--warning`, `--danger` each get their own hover/pressed/muted
- One base change cascades through every derived token — no ladder to re-tune
- Feature support: Baseline 2024, safe to ship
---
## `transition-behavior: allow-discrete`
CSS transitions skip `display: none` by default because `none` is not a numeric value — there is no midpoint to interpolate. `allow-discrete` tells the browser to hold the element visible through the exit transition before applying `display: none`, enabling fade-out on elements that unmount from the DOM.
```css
.menu {
display: none;
opacity: 0;
transition:
opacity 200ms var(--ease-out),
display 200ms allow-discrete;
@starting-style {
opacity: 0;
}
}
.menu:popover-open {
display: block;
opacity: 1;
}
```
### Rules
Without `allow-discrete`, popovers and dialogs snap in and out — there is no mechanism to fade them. Without `@starting-style`, the enter keyframe has no "from" state and the element jumps directly to its final state on first paint.
- `allow-discrete` enables transitions on `display`, `visibility`, and custom-property changes
- Combine with `@starting-style` for the enter keyframe — without it, the element jumps to its final state on first paint
- Essential for `<dialog>` and `[popover]` fade/slide animations
- Without it, popovers snap in/out — no way to fade them
---
## Container Queries — Deeper Patterns
Beyond the basic size query. Use for component-level responsiveness where viewport media queries are the wrong tool.
### When to prefer container over media queries
- Component reused across sidebar + main + modal at different widths
- Layout must respond to its slot, not the viewport
- Design system components meant to be dropped into arbitrary layouts
**Anti-pattern:** using `@media (max-width: 768px)` to change a card's layout when the card lives in both a narrow sidebar and a wide main region — the breakpoint fires on both even though only one needs the narrow layout.
### Style queries
```css
.card {
container-name: card;
container-type: inline-size;
}
@container card style(--theme: dark) {
.card__title { color: oklch(95% 0 0); }
}
```
Style queries read custom properties from the container (custom-property style queries are Baseline 2025 — supported in all current major browsers).
### Named containers for predictable scoping
```css
.sidebar { container-name: sidebar; container-type: inline-size; }
.main { container-name: main; container-type: inline-size; }
@container sidebar (min-width: 240px) {
.nav-item { padding-inline: 12px; }
}
@container main (min-width: 720px) {
.nav-item { padding-inline: 20px; }
}
```
Same component, different behavior per parent. Viewport media queries can't express this.
@@ -0,0 +1,434 @@
# Motion
Unified motion reference — decision ladder, token scales, interaction rules, choreography, motion budget, reduced-motion contract, anti-patterns.
---
## When to Read This
Any motion work: hover, enter/exit, modals, drawers, page transitions, scroll reveals, stagger choreography, spring vs tween decisions, reduced-motion, or bespoke durations showing up in a review.
---
## Decision Ladder
1. **Justify every animation** — motion must communicate something (hierarchy, state, spatial relationship). Decorative motion is noise.
2. **Frequency determines budget** — 100+ times/day gets zero animation. Occasional (modals, drawers) get standard. First-time experiences can delight.
| Frequency | Animation |
|-----------|-----------|
| 100+ times/day (keyboard shortcuts, command palette) | **None. Ever.** |
| Tens of times/day (hover, list nav, typing) | Remove or drastically reduce — under 150ms or none |
| Occasional (modals, drawers, toasts) | Standard animation, 200-300ms, clear purpose |
| Rare / first-time (onboarding, celebrations) | Can add delight — tell a story |
3. **Speed communicates confidence** — under 200ms feels instant. 300ms+ starts feeling sluggish.
4. **Respect the user's system** — `prefers-reduced-motion` is not optional. Provide meaningful fallbacks, not just `animation: none`.
5. **GPU-only properties** — stick to `transform` and `opacity`. Animating `width`, `height`, `top`, `left` causes layout thrashing.
6. **List properties explicitly** — `transition: all` animates things you didn't intend.
7. **Degrade before removing.** When an animation is causing jank, the fix ladder is: shrink duration, shrink distance, swap to a cheaper property, reduce the number of animated elements — in that order. Cutting the animation entirely is the last rung, not the first response to a performance complaint.
**Asymmetric enter/exit.** Pressing can be slow when deliberate (hold-to-delete: 2s linear), but release is always snappy (200ms ease-out). Slow where user decides, fast where system responds.
---
## Duration Scale
Perception bands that govern every token below:
- **Under 100ms** — registers as instant. Use for acknowledgment-only feedback (button press ripple, toggle snap). Anything longer in this slot feels laggy.
- **100–250ms** — registers as a transition but doesn't slow the user. Correct for UI state changes, hover effects, and small reveals.
- **250–400ms** — noticeable. The user expects a moment of change. Reserve for screen-level events: modal open, drawer slide, page transition.
- **400ms+** — deliberate and slow. Productive UI has no business here. Reserve for storytelling moments (onboarding, hero animations) used at most once per session.
Five tokens cover ~95% of UI. Pick from this scale; do not invent.
```css
:root {
--motion-fast: 120ms; /* color / opacity, tooltip, hover */
--motion-base: 200ms; /* small UI — dropdown, toggle, tab, select */
--motion-medium: 280ms; /* medium UI — modal open, drawer, popover */
--motion-slow: 400ms; /* large UI — page transition, full sheet */
--motion-slower: 600ms; /* decorative / onboarding — sparingly */
}
```
| Token | Range | Use for |
|-------|-------|---------|
| `--motion-fast` | 120ms | Opacity, color, hover, focus ring, tooltip show/hide |
| `--motion-base` | 200ms | Dropdowns, toggles, tabs, selects, chips, accordion |
| `--motion-medium` | 280ms | Modals, popovers, drawers (desktop), snackbars |
| `--motion-slow` | 400ms | Page transitions, drawers (mobile), full-screen sheets |
| `--motion-slower` | 600ms | Hero animations, onboarding choreography, first-run only |
Never `transition: 153ms`. Never `200ms` on one button and `220ms` on another for the same state. A bespoke duration is a finding.
---
## Easing Scale
Four tokens cover ~95% of UI easing. System defaults — framework libraries have their own defaults (see `stack.md` for Motion/GSAP mapping).
```css
:root {
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* entrances, most UI */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* same-layer transitions */
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1); /* hero, attention-grabbing */
--ease-soft: cubic-bezier(0.4, 0, 0.2, 1); /* Material-like, general purpose */
}
```
| Token | Perceptual basis | When to use |
|-------|-----------------|-------------|
| `--ease-out` | Decelerates to rest — matches how real objects behave under gravity. Feels natural for anything arriving. | Default for almost everything. Entrances, hover, dropdown, modal open. |
| `--ease-in-out` | Symmetric ramp — deliberate, reads as intentional but can feel sluggish if overused. | Elements already on screen that move or morph (layout changes, repositioning). |
| `--ease-emphasized` | Aggressive ease-out — arrives quickly and settles hard. Signals importance and speed of response. | One element per viewport — hero reveal, primary CTA emphasis. |
| `--ease-soft` | Gentle ease-out with softer deceleration tail — less snappy, more comfortable for dense UI. | Gentler alternative for general UI; Material-style products. |
**Never:**
- `ease-in` for UI — acceleration without deceleration means the element speeds up as it arrives, which reads as a collision, not a landing. A dropdown with `ease-in` at 300ms *feels* slower than `ease-out` at the same duration.
- `linear` for spatial motion — machine-like and robotic; use only for opacity-only fades, loading bars, marquees, scroll-linked progress, and continuous rotation.
- Bespoke easings named "smooth" with `cubic-bezier(0.5, 0.5, 0.5, 0.5)` — that's a straight line.
- Bounce / elastic easing on functional UI — reads dated and draws attention to the animation itself.
- Different easings on entrance vs exit of the same element.
---
## Interaction Rules
- **Interaction feedback ≤ 100ms** — if it takes longer, it stops feeling like a reaction.
- **Hover contract** — transform + color + shadow move together, never just one property.
- **Focus contract** — a focus ring still needs a perceptible transition (fast, no easing drama) so keyboard users see the state change.
- **Never change metrics on interaction** — `font-weight`, `font-size`, `letter-spacing`, `padding`, `border-width`. **Why:** these are layout properties, so the text re-flows and every glyph shifts under the cursor; the element also can't be animated on the compositor, so the change lands as a jump instead of a transition. Signal hover with color, background, shadow, or `transform` — all of which leave the box untouched. When a bolder hover is the design, render the bold weight from the start in a hidden layer and cross-fade opacity, or pair a `border` with a same-width transparent `border` in the resting state so the box never resizes.
- **Disable interactions on exiting elements** — visually present, logically gone.
- **Never block interaction while a stagger plays** — decorative motion must not gate input.
- **Transitions vs. keyframes — pick by interruptibility.** A CSS transition retargets smoothly toward whatever the newest end state is, even mid-flight — interrupt a hover-out with a hover-in and it reverses cleanly from its current position. A keyframe animation restarts from its `0%` frame every time it's re-triggered, discarding wherever it currently was. Interactive state changes (hover, press, toggle, open/close) are transitions for exactly this reason; keyframes are for one-shot choreography that never needs to be interrupted mid-play (a one-time celebration, a loading spinner).
General interaction rules (keyboard timing, touch targets, `overscroll-behavior`, `touch-action`, optimistic UI, destructive confirmations) live in `accessibility.md` — honor them on every animated control.
---
## Choreography Rules
**Exit runs at ~75% of entrance duration.** This is the one rule that matters most. If entrance is `--motion-medium` (280ms), exit is ~200ms with `--ease-out` (or a flatter tail like `cubic-bezier(0.4, 0, 1, 1)`).
1. **Hierarchy first.** Parent/container animates before child/content. Context arrives, then detail. Modal backdrop fades in (`--motion-fast`), then the modal panel slides (`--motion-medium`), then form fields enter (staggered).
2. **Stagger is 30-80ms between siblings.** Never uniform at zero (feels pasted-on), never more than 80ms per item (list feels slow beyond the 6th item). Marketing pages can push to 100-150ms for dramatic entrances.
3. **Co-located properties share timing.** Multiple properties on one element (transform + opacity + color on hover) use ONE duration + easing, not three competing. `transition: transform 200ms var(--ease-out), opacity 200ms var(--ease-out);` — not three different durations.
4. **Shared elements use layout animations.** When the "same" element appears in two places (list → detail), use `layoutId` (Motion), View Transitions API, or FLIP — not discrete enter/exit pairs that visually unmount and remount.
5. **Exit mirrors initial** for symmetry: if entering from `opacity: 0, y: 20`, exit to the same.
6. **Unique keys** (not index) for dynamic lists — enables smooth add/remove.
7. **List reordering** needs layout animation mode, not sequential mode.
8. **Wait modes nearly double perceived duration** — use shorter durations when sequencing enter/exit.
9. **Suppress enter animation on first paint.** Elements already in their default state on page load must not animate in — only on later state changes. With Motion, set `initial={false}` on `AnimatePresence` (icon swaps, tabs, toggles, segmented controls). Exception: a deliberate first-time entrance (staggered hero, loading sequence) relies on `initial` — don't kill it there. Verify on a hard refresh. This is the fix for the "entrance on every page load" anti-pattern below.
Stagger example:
```css
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 50ms; }
.item:nth-child(3) { animation-delay: 100ms; }
```
For multi-stage storyboards with named timing + config objects + stage-driven sequencing, see `../examples/animation-storyboard.md`.
---
## Motion Budget per Surface
A ceiling on simultaneous entrance animations. This is the tool against AI's "animate everything on mount" slop.
| Surface | Budget |
|---------|--------|
| Landing hero | Up to 3 staggered entrances (headline / subhead / CTA). One scroll-linked element max. |
| Feature section | 1 reveal-on-scroll per card, stagger 40ms, triggers once. |
| Dashboard | **Micro-interactions only.** No entrance animations on metric cards, charts, tables. |
| Forms | 1 focus ring transition per field. No field-by-field staggered entrance. |
| Modals | Backdrop fade + panel transform. Nothing inside the modal animates on open. |
| Settings / admin | Zero entrance animations. High-frequency tool — motion wastes time. |
| Onboarding (first-run) | Larger budget — the one moment it pays off. |
If a viewport has more than its budget, cut. Every "let's add one more" compounds.
---
## Spring vs Tween
> **Pick spring OR tween globally, not per-component.** Mixing in the same app reads as inconsistent. Document the choice in the project's design doc.
Springs feel natural because they simulate real physics — no fixed duration, they settle based on physical parameters. Stiffness controls resistance to displacement: high stiffness means the spring snaps back hard and fast. Damping controls resistance to velocity: low damping lets the element overshoot and oscillate; high damping absorbs the energy and settles immediately. The practical result: high stiffness + high damping = quick, settled, confident. High stiffness + low damping = bouncy and playful. Low stiffness + high damping = slow and heavy. Never tune by feel alone — fix a target feel, then dial parameters to match it.
**Use springs when:**
- Drag interactions with momentum
- Elements that should feel "alive" (Dynamic Island)
- Gestures that can be interrupted mid-animation (springs maintain velocity; CSS animations restart from zero)
- Mouse-tracking decorative interactions (`useSpring` to interpolate)
**Use tween when:**
- The project has strict duration budgets (200-300ms UI rule)
- Deterministic motion matches the brand
- Motion is purely functional (no "alive" personality)
**Spring configuration (Motion / Framer):**
```js
// Apple-style (recommended — easier to reason about)
{ type: "spring", duration: 0.5, bounce: 0.2 }
// Traditional physics (more control)
{ type: "spring", mass: 1, stiffness: 100, damping: 10 }
```
**Spring rules:**
- Bounce in functional UI is an anti-pattern. The only acceptable use is single-element micro-feedback (heart toggle, badge appearance) where bounce amplitude stays at 0.1-0.3 of base.
- Balanced parameters: `stiffness: 500, damping: 30` settles quickly; `stiffness: 1000, damping: 5` is too bouncy.
- Drag release: `{ type: "spring", velocity: info.velocity.x }` preserves input energy.
- Springs have no fixed duration → constrain via `bounce ≤ 0.25` and `visualDuration`.
**Spring presets:**
| Use case | Config | Feel |
|----------|--------|------|
| Cards / containers | `stiffness: 300, damping: 30` | Smooth settle — no overshoot, reads as composed |
| Pop-ins / badges | `stiffness: 500, damping: 25` | Snappy with a very slight bounce — draws the eye without distracting |
| Slides / entrances | `stiffness: 350, damping: 28` | Balanced — lands confidently, slight energy without playfulness |
| Drag release | `stiffness: 500, damping: 30` + velocity | Fast return that preserves the momentum the user imparted |
---
## Swapping One Icon for Another In Place
A sun that becomes a moon on theme toggle, a checkmark that replaces a copy icon after a click — same slot, different glyph, no layout shift. Done wrong it either reads as a flicker (plain crossfade, nothing else) or as the icon resizing itself rather than being replaced (entering scale starts too large).
- **Both icons cross-fade opacity together** — outgoing down, incoming up, one shared duration.
- **The incoming icon grows in from roughly 0.25-0.35 of its full size, not from a near-full scale.** The point isn't the exact number — it's that entering should read as *arriving*, not as *resizing*. Starting closer to 1 makes the icon look like it's stretching into place; starting small enough makes it unambiguous that a new element appeared.
- **A few pixels of blur clearing to zero** as the incoming icon settles takes the edge off the transition further, so the swap feels like one continuous event rather than a hard cut.
- **No overshoot.** Springs here want to land and stop — a wobbling checkmark after a save reads as a glitch, not a flourish, because nothing about "your click registered" calls for bounce.
DOM-only fallback when a motion library isn't in play — stack both icons in the same grid cell and toggle a class from parent state:
```css
.swap-slot {
position: relative;
display: inline-grid;
}
.swap-slot > svg {
grid-area: 1 / 1;
opacity: 0;
transform: scale(0.3);
filter: blur(2px);
transition: opacity var(--motion-fast) var(--ease-out),
transform var(--motion-fast) var(--ease-out),
filter var(--motion-fast) var(--ease-out);
}
.swap-slot > svg[data-visible="true"] {
opacity: 1;
transform: scale(1);
filter: blur(0);
}
```
---
## Reduced-Motion Contract
`prefers-reduced-motion` is not optional. The system must degrade to zero-motion gracefully — not break, not disappear, not skip states.
```css
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
**Two exceptions to "disable everything":**
- **Loading indicators keep animating.** Reduced motion removes gratuitous motion, not signal. A frozen spinner reads as "broken."
- **Essential feedback stays.** A focus ring still needs a perceptible transition (fast, no easing drama) so keyboard users see the state change.
Test: macOS → System Preferences → Accessibility → Display → "Reduce motion." Windows → Settings → Accessibility → Visual effects → "Animation effects" off. Every entrance and exit should still deliver the user to the right visual state, instantly.
---
## Rendering Performance
### Pipeline
```
Layout (expensive) → Paint (moderate) → Composite (cheap)
```
- **Composite only** (`transform`, `opacity`) — GPU-accelerated, off main thread. Default choice.
- **Paint** (`color`, `borders`, `gradients`, `filters`) — repaints the layer, moderate cost.
- **Layout** (`width`, `height`, `margin`, `padding`, `flex`, `grid`) — forces full reflow. Never animate continuously.
Prefer CSS transitions/animations → WAAPI → JS libraries, in that order. Some JS libraries use shorthand props (`x`, `y`, `scale`) that run on the main thread via `requestAnimationFrame` — verify compositor promotion. When in doubt, use explicit `transform` strings.
```js
// May drop frames (shorthand — main thread)
animate({ x: 100 })
// Hardware-accelerated (explicit)
animate({ transform: "translateX(100px)" })
```
### FLIP Pattern
Measure once, animate via transform — never animate layout properties directly:
```js
const first = el.getBoundingClientRect(); // 1. First: measure
el.classList.add('moved');
const last = el.getBoundingClientRect(); // 2. Last: apply final state
el.style.transform = `translateX(${first.left - last.left}px)`; // 3. Invert
requestAnimationFrame(() => { // 4. Play
el.style.transition = 'transform 0.3s';
el.style.transform = '';
});
```
### Scroll-Linked Motion
Prefer CSS Scroll Timelines — runs off main thread:
```css
.reveal {
animation: fade-in linear;
animation-timeline: view();
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
```
Prefer `IntersectionObserver` for visibility triggers. Never poll `scrollY` or use `scroll` event listeners for continuous animation — causes layout/paint on every frame.
**A `requestAnimationFrame` loop needs an owner.** Something in the code must be responsible for calling `cancelAnimationFrame` once the element unmounts or the animation reaches its resting state — otherwise the loop just keeps scheduling itself against a target that no longer needs it, spending frame budget for no visible benefit.
### Layers & Promotion
`will-change` is surgical and temporary — add it before the animation starts, remove it after:
```css
.animating { will-change: transform; }
.done { will-change: auto; }
```
Avoid many or large promoted layers — each consumes GPU memory. Validate with DevTools Layers panel when performance matters.
### Blur & Filters
Keep blur ≤ 8px when animating (≤ 20px static). Never on large surfaces — extremely expensive, especially Safari. `backdrop-filter` is costly; never animate it. Reach for `opacity` + `translate` before blur.
### CSS Variables Gotcha
Changing a CSS variable on a parent recalculates all children. Update `transform` directly on the element:
```js
// Bad: triggers recalc on all children
element.style.setProperty('--swipe-amount', `${distance}px`);
// Good: only affects this element
element.style.transform = `translateY(${distance}px)`;
```
Never animate inherited CSS variables. Scope animated variables locally.
---
## Anti-Patterns
- [ ] Bespoke durations (`transition-[153ms]`, `duration: 0.23`). Pick a token.
- [ ] `transition: all` — list specific properties.
- [ ] Easings named "smooth" with symmetric `cubic-bezier(0.5, 0.5, 0.5, 0.5)`. That's linear.
- [ ] Bounce / elastic easing on functional UI. Bounce is for celebration, not buttons.
- [ ] `ease-in` on UI — slow start reads as sluggish.
- [ ] Parallax on body content (kills scroll performance; hostile to reduced-motion).
- [ ] Scroll-jacking (hijacking the wheel). Users have a mental model for scroll. Don't break it.
- [ ] Motion on every hover (death by a thousand animations). Hover affordance is enough; not every row needs to move.
- [ ] Entrance animations on every page load. First visit is special; the 47th is not.
- [ ] Different easings on entrance vs exit of the same element. Asymmetry feels broken.
- [ ] Exaggerated scale on hover (`1.08+` on CTAs). Keep ≤ 1.02 for functional UI; 1.02-1.05 for cards.
- [ ] Animating `width` / `height` / `top` / `left` — layout thrash. Use `transform` / `opacity`.
---
## Motion Gap Audit
The most common motion failure is not bad easing — it is missing motion entirely: UI state changes that snap with no transition, modals that appear at full opacity in one frame, lists that teleport items in on mount.
**Audit pattern.** Search the codebase for conditional renders without enter/exit, modal mounts without entry, and list items appearing without stagger. These are the sites most likely to have a motion gap.
Grep patterns for React:
```bash
# Conditional renders — element may appear/disappear with no transition
grep -rn '&&\s*(<' src/
# Modals / dialogs — check whether AnimatePresence or CSSTransition wraps the mount
grep -rn 'useState.*open\|isOpen.*useState' src/
# Lists — check whether items have stagger or enter animation
grep -rn '\.map(' src/ | grep -v 'test\|spec\|story'
```
Not every state change deserves motion. The audit surfaces candidates; the **Decision Ladder** (frequency × justification) filters them. A settings panel that opens 50 times a day should snap — that gap is intentional. A first-run modal that appears once deserves an entrance. Apply the ladder before adding motion to every hit.
---
## Figma JSON Export
Shareable tokens for Figma variables, Style Dictionary, or tokens.studio:
```json
{
"motion": {
"duration": {
"fast": { "value": 120, "type": "duration", "unit": "ms" },
"base": { "value": 200, "type": "duration", "unit": "ms" },
"medium": { "value": 280, "type": "duration", "unit": "ms" },
"slow": { "value": 400, "type": "duration", "unit": "ms" },
"slower": { "value": 600, "type": "duration", "unit": "ms" }
},
"ease": {
"out": { "value": [0.22, 1, 0.36, 1], "type": "cubicBezier" },
"inOut": { "value": [0.65, 0, 0.35, 1], "type": "cubicBezier" },
"emphasized": { "value": [0.2, 0, 0, 1], "type": "cubicBezier" },
"soft": { "value": [0.4, 0, 0.2, 1], "type": "cubicBezier" }
}
}
}
```
Figma handles `cubicBezier` via plugins; Style Dictionary compiles to CSS / JS / iOS / Android.
**Framework mapping:**
| Framework | Duration | Easing |
|-----------|----------|--------|
| Vanilla CSS | `var(--motion-base)` | `var(--ease-out)` |
| Tailwind (extend) | `theme.transitionDuration: { base: '200ms' }` | `theme.transitionTimingFunction: { out: 'cubic-bezier(0.22, 1, 0.36, 1)' }` |
| Motion (framer-motion) | `transition={{ duration: 0.2 }}` | `transition={{ ease: [0.22, 1, 0.36, 1] }}` |
| GSAP | `gsap.to(el, { duration: 0.2 })` | `ease: 'power2.out'` (≈ ease-out) |
Map the project's animation library to the token names, not raw values — when the scale changes, one file updates.
---
## Cross-Refs
- `stack.md` — Motion / GSAP / Three.js library specifics (opt-in during Discovery).
- `modern-css.md` — `@starting-style` for enter-on-mount, View Transitions API.
- `accessibility.md` — reduced-motion + full interaction rules (keyboard, focus, touch).
- `../examples/animation-storyboard.md` — multi-stage sequenced animations pattern.
---
## Sources
- W3C — CSS Media Queries Level 5: `prefers-reduced-motion`.
- WCAG 2.3.3 (AAA) — Animation from Interactions.
@@ -0,0 +1,206 @@
# Personas — Walkthrough Checklists
Five personas, five journeys. A heuristic score says what's wrong in the abstract. A persona walkthrough says *who* it breaks for. Use these as overlays on top of `heuristics.md`: same UI, five different lenses.
Each persona has a checklist. Every un-ticked box is a finding. Rank findings using the same impact tags from `heuristics.md` (`blocks-conversion`, `adds-friction`, `reduces-trust`, `minor-polish`).
---
## 1. First-timer Priya
**Tagline:** Never used this product category. Doesn't know what's possible.
**Profile:** 28, mobile-only, moderate English literacy, small-business owner evaluating the product for the first time. Comes in cold from a social ad. Has 90 seconds before she bounces. Doesn't know industry jargon and won't search for it.
**Journey:** Land → understand what the product does → decide whether to sign up → complete signup → reach the "first value" moment (whatever that is for this app).
### Walkthrough checklist
- [ ] Above the fold, I understand what this product does in 5 seconds
- [ ] No jargon on the landing page ("synergy", "orchestrate", "leverage")
- [ ] One primary CTA is obvious — I don't have to hunt for "sign up"
- [ ] Signup asks for the minimum; I'm not giving 8 fields before I see the product
- [ ] Email / Google / Apple auth all work on mobile (no desktop-only flow)
- [ ] After signup, there's a clear next action — not a blank dashboard
- [ ] Empty states tell me what to do: "Create your first X"
- [ ] Terms I don't know have tooltips or inline explanations
- [ ] Error messages explain the problem in plain language
- [ ] If I tap something and nothing happens, I know whether it's loading or broken
- [ ] I can undo my first destructive action
- [ ] The primary value moment is reachable in ≤ 3 steps from signup
- [ ] Help/contact is reachable without leaving the app
- [ ] Keyboard autocorrect doesn't fight the form (autocomplete attrs set)
- [ ] I can pinch-zoom if text is too small
### Red flags
- Landing page uses phrases like "AI-powered orchestration platform" without saying what it does
- Signup form asks for company size / industry / role before account creation
- Dashboard is empty with no "Create your first" CTA
- Tooltips missing on icon-only buttons — she doesn't know what the cog vs wrench means
- Email verification required before she sees any product value
---
## 2. Power User Jordan
**Tagline:** Lives in this tool 4+ hours a day. Expects to fly.
**Profile:** 34, senior ops at a scaleup. Keyboard-first, multiple-monitor, deep domain expertise. Uses the product as a core part of their job. Metrics their own speed — any friction compounds. Will build a userscript or quit the product if workflows feel slow.
**Journey:** Log in → jump straight to a specific record via shortcut or search → perform bulk operations → export/report → jump to the next task.
### Walkthrough checklist
- [ ] Cmd+K (or /) command palette exists and is fast (< 100ms to open)
- [ ] Every common action has a keyboard shortcut, listed in tooltips
- [ ] Tab order is sensible; Shift+Tab reverses; Escape closes
- [ ] Bulk select works on lists of 100+ with Shift-click and Cmd+A
- [ ] Common bulk actions (delete, archive, assign) execute in < 400ms
- [ ] URL state reflects filters / sort / selection so I can bookmark and share
- [ ] I can paginate 1000+ items without virtualized list jank
- [ ] Saved views / segments / filters persist across sessions
- [ ] The app never traps me in a modal I can't Escape out of
- [ ] Destructive actions are undoable, not confirm-blocking (confirms slow me down)
- [ ] Forms submit on Cmd+Enter
- [ ] I can customize columns, hotkeys, or density
- [ ] Paste-to-action works (paste a URL, paste a list of IDs)
- [ ] Export works for the current view, not "everything"
- [ ] No scroll hijacking, no unasked page transitions
### Red flags
- No command palette on a tool users will spend hours in
- Modal-confirm on every delete instead of undo toast
- Lists of 200+ items without bulk select
- Filters reset on navigation — state lives in local React, not URL
- Shortcuts exist but are only discoverable in a hidden help page
---
## 3. Low-bandwidth Adaeze
**Tagline:** 2G cellular, older Android, often offline.
**Profile:** 31, field worker in a region with intermittent connectivity. Budget Android device (4GB RAM, Chrome). Tethers off a phone. Comes in and out of signal throughout the day. Data costs real money per MB.
**Journey:** Open the app on a flaky connection → read or edit a record → queue work while offline → have it sync when reconnected.
### Walkthrough checklist
- [ ] First paint < 3s on simulated 2G (check Lighthouse mobile)
- [ ] Critical CSS inlined; no blocking font load
- [ ] Images have explicit dimensions to prevent layout shift
- [ ] Images are lazy below the fold and use modern formats (WebP/AVIF)
- [ ] Bundle doesn't ship megabytes of unused code
- [ ] Skeletons appear within 200ms so the screen isn't blank
- [ ] Offline state is detected and communicated ("You're offline — changes will sync")
- [ ] Writes are queued locally and reconciled on reconnect
- [ ] Optimistic UI so she doesn't wait for round-trips
- [ ] Error message distinguishes network failure from server failure
- [ ] Retry is one tap, not "refresh the page"
- [ ] Text remains readable without images loaded
- [ ] The app doesn't poll aggressively on cellular
- [ ] Large uploads resumeable or chunked
- [ ] Service worker caches the shell so return visits are instant
### Red flags
- Blank screen for 6+ seconds on 2G
- "Network error" toast that disappears; no retry, no queue
- Data lost on tab close because it wasn't persisted locally
- Every interaction requires a server round-trip before the UI updates
- App auto-plays hero video over cellular
---
## 4. Screen-reader user Kwame
**Tagline:** Fully blind. NVDA + Chrome. Keyboard-only. Needs semantic HTML.
**Profile:** 40, accessibility consultant, experienced screen-reader user. Runs NVDA on Windows + Chrome. Never uses a mouse. Relies on heading structure, ARIA landmarks, and predictable focus order.
**Journey:** Land on the page → use headings / landmarks to skim structure → tab through interactive elements → complete the same task any sighted user can.
### Walkthrough checklist
- [ ] Page has exactly one `<h1>` and a sensible heading outline
- [ ] Landmarks present: `<main>`, `<nav>`, `<header>`, `<footer>`, `<aside>`
- [ ] Every interactive element is reachable via Tab in logical order
- [ ] Focus is always visible (`:focus-visible` ring on every control)
- [ ] Buttons are `<button>`; links are `<a>`; no `<div onClick>`
- [ ] Icon-only buttons have `aria-label` or visually-hidden text
- [ ] Form fields have associated `<label>` (not just placeholder)
- [ ] Errors use `aria-invalid` and `aria-describedby` to link the message
- [ ] Modals trap focus, restore on close, and have `aria-labelledby`
- [ ] Live regions (`aria-live`) announce loading / success / error states
- [ ] Dynamic content changes are announced — not silent
- [ ] Images have `alt`; decorative images have `alt=""`
- [ ] Tables have `<th scope>` and captions where relevant
- [ ] Custom components (combobox, tabs, menu) follow WAI-ARIA APG
- [ ] Skip-to-content link exists and works
- [ ] No positive `tabindex` values; no focus traps outside modals
- [ ] Color is not the only signal for error / success / state
### Red flags
- `<div>` with onClick used as a button — not keyboard-reachable
- Custom select built on `<div>` with no ARIA combobox pattern
- Focus ring removed globally (`*:focus { outline: none }`)
- Error toast shown with no `aria-live` — screen reader never hears it
- Modal opens but focus stays on the triggering button
---
## 5. One-thumb Margo
**Tagline:** On the subway. Holding coffee. 30 seconds of attention.
**Profile:** 29, commuter, Mobile Safari, iPhone 14. Holding a coffee in one hand, phone in the other. Checking the app for a quick status glance. If she can't get what she needs in 30 seconds, she closes the tab.
**Journey:** Open link from a notification → get the single answer she came for → maybe take one quick action → leave.
### Walkthrough checklist
- [ ] Primary info is above the fold on a 390px-wide screen
- [ ] Everything she needs to tap sits in the thumb zone (bottom 2/3 of screen)
- [ ] Touch targets ≥ 44×44px
- [ ] No reliance on hover — every affordance works on tap
- [ ] Sticky header/footer doesn't eat 40% of the viewport
- [ ] Safe areas respected (notch, home indicator, keyboard)
- [ ] Forms use correct `inputmode` and `autocomplete` so iOS keyboard cooperates
- [ ] Submit buttons are large and reachable with one thumb
- [ ] Confirmations are visible without scrolling
- [ ] No horizontal scroll unless explicitly intended
- [ ] Back-swipe gesture works; browser back doesn't lose state
- [ ] Modals are dismissible by swipe-down or tap-outside
- [ ] Copy doesn't assume desktop ("click the button on the right")
- [ ] Text is readable without zoom (min 16px body)
- [ ] Sharing respects the system share sheet
### Red flags
- Primary CTA at the top of the viewport — she can't reach it
- Iframe / captcha that doesn't scale to mobile
- Pinch-zoom disabled
- Tooltip-only explanations (no hover on touch)
- Cancel / submit placed 8px apart — fat-finger tapping wrong one
---
## How to Use
Invoked by `/ui-craft:heuristic <target> --persona=<name>`:
| Arg | Persona | Loads |
|-----|---------|-------|
| `--persona=priya` | First-timer Priya | Section 1 |
| `--persona=jordan` | Power user Jordan | Section 2 |
| `--persona=adaeze` | Low-bandwidth Adaeze | Section 3 |
| `--persona=kwame` | Screen-reader user Kwame | Section 4 |
| `--persona=margo` | One-thumb Margo | Section 5 |
| `--persona=all` | All five | All sections |
Output format: same scorecard shape as `heuristics.md` (rows of `| Checklist item | Pass/Fail | Finding | Impact |`). End with top 3-5 findings ranked by impact tag.
Persona walkthroughs do not replace the Nielsen + Design Laws pass — they supplement it. A UI can score well on Nielsen and still fail Kwame.
@@ -0,0 +1,299 @@
# Principles Catalog
A worked-example bank of opinionated design principles, organized by product category. Use as seed material for the principles workshop in `/brief`. Do not adopt these literally — adapt to your specific product, user, and constraints. The point is to see what opinionated, actionable, distinctive principles look like in practice. Many entries are product-specific manifestations of the heuristics in `heuristics.md` — they complement the scored methodology, they don't replace it.
---
## How to Read This Catalog
Each principle has four parts: a title (4-7 words), a statement (one line), the design implication it produces (what changes in the product because of it), and an opposing principle it rules out (the contrast that proves it's opinionated).
Categories are starting points, not silos. A finance reconciliation tool may need a "creative tools" principle if its power users have strong workflow preferences. Mix and match across categories — the category label is scaffolding, not a fence.
Strong principles take a stand. If you can flip the principle and the opposite sounds reasonable, the principle is doing work. If the opposite sounds insane, the principle is a platitude — drop it.
Trim to 3-5. More than 5 means none of them are load-bearing. If you have eight, find the two that subsume the others and keep those.
Test against past decisions. Take three real design calls that were debated. Would the candidate principle have resolved them? If it would have flipped a decision you stand behind, it is the wrong principle.
---
## Categories
### 1. Developer Tools (CLIs, dashboards, internal tools)
#### "Show the data, not the design"
**Statement:** chrome retreats; data and state are the entire surface.
**Implication:** color reserved for status (success / warn / error), accent used on the primary action only, typography defaults to monospace for data and humanist sans for chrome. Animations are sub-200ms or absent. Decorative illustrations, hero gradients, and marketing-page polish have no place here.
**Opposite ruled out:** "internal tools deserve the same visual richness as consumer products" — a developer dashboard with gradient headers, large illustrations, and motion choreography signals decoration over function and erodes trust with its audience.
#### "The keyboard is the primary input"
**Statement:** every action is reachable without a pointer.
**Implication:** every modal, panel, and dropdown has a keyboard shortcut. Tab order matches visual order. Command palette covers all global actions. Hover-only affordances do not exist. Mouse-first interactions (drag-to-reorder, right-click context menus without keyboard equivalents) are redesigned or dropped.
**Opposite ruled out:** "power users will learn the UI" — assuming that heavy users will mouse through menus every session trades their time for your engineering convenience.
#### "Error state is a first-class screen"
**Statement:** failure and partial states are designed before the happy path ships.
**Implication:** every async operation has an explicit error state with a recovery action (retry, copy error, open docs). No "Something went wrong" strings without a path forward. Skeleton loaders are paired with error recovery. Empty states are not blank pages.
**Opposite ruled out:** "ship the happy path first, handle errors later" — deferring error states means they ship broken or never; this category's users lose the most time when errors are opaque.
#### "Density by default, space by request"
**Statement:** the default layout is dense; whitespace is earned by user preference.
**Implication:** default row height is compact (32-36px), table padding is minimal, sidebars collapse by default on narrow viewports. A spacious view is available as a user preference, not the default. Every pixel of padding is justified by legibility, not aesthetics.
**Opposite ruled out:** "generous whitespace reads as quality" — in a tool used for hours daily, whitespace that forces scrolling is a cost, not a signal.
#### "State is always visible"
**Statement:** the current view, filter, and scope are always in the viewport.
**Implication:** active filters shown as removable chips above the data, not hidden in a sidebar. Breadcrumbs or scope indicators in the nav. Connection / sync status visible in a persistent corner. "Showing 1-50 of 2,340" is always present on data tables.
**Opposite ruled out:** "minimize chrome to maximize content area" — hiding active state to save pixels means the user constantly re-orients, which costs more than the pixels saved.
#### "Logs beat summaries"
**Statement:** give users the raw output alongside the derived insight.
**Implication:** aggregated metrics always have a "view raw" or drill-down path. Pipeline outputs show the full log, collapsible to summary. No computed status that hides the underlying signal. "3 errors" links to the 3 errors, not a modal that restates the count.
**Opposite ruled out:** "surface insights, not raw data" — for a developer audience, hiding the raw signal behind a summary reads as paternalism and blocks debugging.
---
### 2. Consumer Apps (lifestyle, social, content)
#### "The empty page is the customer"
**Statement:** the user's first action defines the product; the empty state is the most important screen.
**Implication:** loading and empty states receive the same design care as filled states. CTAs in empty states are specific to the likely first task, not generic ("Start your first list" not "Get started"). Templates are visible but never the default — the blank canvas wins by being immediate. Onboarding flows do not gate the empty state.
**Opposite ruled out:** "pre-populate with sample content to show value" — demo data tells the user the product was designed for someone else, not for them.
#### "Speed of the first action"
**Statement:** time from open to first meaningful interaction is the north-star metric.
**Implication:** zero required sign-in gates before the user touches the product. No mandatory tutorial. Profile setup is deferred until after first value. App open → core action is one tap or one keystroke. Onboarding is invisible or async.
**Opposite ruled out:** "complete profile upfront for personalization" — high-friction onboarding before proven value loses users at the first friction point, regardless of the personalization payoff.
#### "Social context compounds value"
**Statement:** every surface becomes more useful when others are present.
**Implication:** sharing affordances are built into the primary action flow, not a separate share sheet. Activity feeds are part of the core loop, not a tab buried in the nav. Reactions, replies, and attributions are near the content, not in a detail view. The single-user experience is the degraded form; the social form is the default.
**Opposite ruled out:** "sharing is a secondary feature" — treating social features as addons rather than core creates a product that is average alone and invisible together.
#### "Personalization shows immediately"
**Statement:** the product adapts visibly to the user from the first session.
**Implication:** recent content, saved preferences, and history are in the primary view on return. No "start fresh" blank slate on every open. The product remembers the last state (scroll position, last viewed item, active draft). First-time experience and return experience are explicitly different designs.
**Opposite ruled out:** "consistent state across sessions ensures predictability" — predictability for its own sake, without personalization, reads as indifferent.
#### "Friction sized to the commitment"
**Statement:** the UI's friction must match what the user is actually committing to.
**Implication:** liking, saving, sharing — zero confirmation. Following someone — one tap, no modal. Deleting an account — modal with explicit consequence. Friction scales with irreversibility and social consequence. No confirmation dialogs on low-stakes actions.
**Opposite ruled out:** "confirm everything to prevent accidental actions" — treating every action as high-stakes makes the product feel bureaucratic and trains users to dismiss confirmations without reading them.
#### "Delight once, then step aside"
**Statement:** expressive moments earn their place at first run; after that, they disappear.
**Implication:** confetti on first completion, celebration animation on first streak — then suppressed on repeat. Onboarding animations do not repeat after session one. Empty states have character for new users; returning-user empty states are functional. The product does not perform joy for users who have moved past the new-user phase.
**Opposite ruled out:** "consistent animation keeps the product feeling alive" — animations that were delightful at first run become noise by week two. Respecting the user's familiarity is its own form of polish.
---
### 3. Finance / Regulated (compliance-first, audit trails)
#### "Wrong is worse than late"
**Statement:** confirm everything; recovering from a mistake is more expensive than friction.
**Implication:** confirmation modals on every irreversible action, no fast-paths around them, detailed receipts after every transaction. Undo windows exist where the underlying API allows; where they do not, the action is labeled explicitly. Loading states linger over feedback states — the user needs to know the server confirmed before proceeding.
**Opposite ruled out:** "delight via speed" — instant-feeling interactions in finance contexts read as careless. Speed at the cost of confirmation is a liability, not a feature.
#### "Everything has an author and a timestamp"
**Statement:** every state change is attributed and dated.
**Implication:** audit logs are a first-class surface, not a compliance afterthought. Inline attribution ("Last edited by J. Reyes, Mar 4 14:22") is visible without drilling into a log. Deletions are soft-deletions with a visible record. No anonymous mutations in shared records.
**Opposite ruled out:** "users don't want to see attribution clutter" — regulated contexts require it, and even where they don't, attribution is the primary trust signal between collaborators handling money.
#### "The number is the hero"
**Statement:** the key figure dominates every surface that contains it.
**Implication:** primary amounts are the largest element on the card or row. Supporting context (date, category, status) is secondary — smaller, lower-weight, lower-saturation. No icon grids, feature lists, or decorative elements share visual weight with a financial figure. The hierarchy is always number first.
**Opposite ruled out:** "balanced information hierarchy for readability" — in finance, burying the primary number in a visually balanced layout slows the task and erodes trust.
#### "Export is a primary action"
**Statement:** data that cannot leave the product is a liability in regulated contexts.
**Implication:** CSV, PDF, and audit-format exports are accessible from every data surface without navigating away. Export is in the primary toolbar, not buried in a settings menu. Export respects the current filter and scope. Scheduled export is a feature, not a power-user workaround.
**Opposite ruled out:** "minimize exports to keep users in the product" — in compliance and finance contexts, data portability is a trust signal. Restricting it reads as controlling, not sticky.
#### "Status is never ambiguous"
**Statement:** every record has an explicit, visible state — pending, confirmed, failed, reconciled.
**Implication:** status badges are always present and high-contrast, never inferred from context. Pending and processing states are distinct (not both "loading"). Failed states have a reason, not just a label. Filtering by status is a first-class affordance, not a power-user feature.
**Opposite ruled out:** "reduce visual noise by hiding status for healthy records" — hiding status until something goes wrong means users only interact with the system in crisis mode, which erodes confidence.
---
### 4. Creative Tools (design, writing, video)
#### "The canvas is sacred"
**Statement:** chrome serves the canvas; the canvas never serves chrome.
**Implication:** every panel, toolbar, and overlay is dismissable, collapsible, or fades on inactivity. The active workspace gets the largest available area. Tool palettes can be repositioned. Keyboard shortcuts cover every chrome action. The canvas area does not shrink for notification banners, update prompts, or upsell overlays.
**Opposite ruled out:** "discoverable UI via persistent surfaces" — keeping toolbars permanently visible signals distrust of the user's ability to learn. It also treats every pixel of chrome as more valuable than the user's work.
#### "Undo is unlimited"
**Statement:** the user is never punished for experimenting.
**Implication:** undo history is per-session and deep (50+ steps). Destructive actions have a visible undo window, not just a warning. Version history is a first-class feature, not a recovery tool of last resort. The product does not ask "are you sure?" — it asks "do you want to undo?" after the fact.
**Opposite ruled out:** "confirmation before destructive actions prevents mistakes" — pre-action friction slows creative work. Post-action undo restores the same safety with none of the interruption.
#### "Presets are a starting point, not a destination"
**Statement:** every preset is editable; no workflow ends at a template.
**Implication:** templates and presets always resolve to the underlying editable state, not a locked copy. "Customize" is always available after applying a preset. The preset library is visible and searchable, but the blank-canvas affordance is equal or greater in visual weight. Users who start from a preset should not feel their work is "derivative."
**Opposite ruled out:** "guide users through templates for better outcomes" — prescriptive guidance in a creative tool signals that the product does not trust the user's judgment.
#### "History is visible, not just recoverable"
**Statement:** the user can see what changed, not just revert to before it changed.
**Implication:** version history shows a diff or thumbnail, not just a timestamp. Side-by-side comparison of two versions is possible. Change annotations ("changed from 24px to 18px") are in the undo stack, not just in abstract state. Power users can branch from any point in history.
**Opposite ruled out:** "save storage by keeping only the last N versions" — capped history limits creative risk-taking; users self-censor experiments when they know they cannot reach back far enough.
#### "Collaboration is opt-in, not default"
**Statement:** the single-user experience is complete; sharing is additive.
**Implication:** the product loads immediately without a sign-in gate. Collaboration features (comments, live cursors, sharing) are available when the user is ready for them, not surfaced on first open. A solo creator who never shares their work has the same tier of product quality as a team user.
**Opposite ruled out:** "build for collaboration first; solo use is a subset" — treating solo use as a downgrade punishes independent creators and makes the product feel socially coercive.
---
### 5. Data Analytics (BI, dashboards, monitoring)
#### "One number per screen"
**Statement:** every dashboard surface has a hero metric; everything else supports it.
**Implication:** the primary metric is largest, has the most contrast, sits in the optical center of the above-the-fold zone. Supporting metrics are explicitly subordinate (smaller, lower-weight, lower-saturation). No grid of equal-weight cards — hierarchy is visible at a glance from across the room.
**Opposite ruled out:** "let users decide what's important" — the absence of hierarchy is itself a design choice, and it always reads as the team did not decide. Equal-weight grids push every judgment onto the user and slow time-to-insight.
#### "Comparison is the unit of understanding"
**Statement:** a number without context is noise; every metric shows its comparison.
**Implication:** primary metrics always show period-over-period change (vs. prior week, vs. target). Sparklines on every metric card. Trend direction is signaled by color and icon, not just a number. Tables have inline comparison columns. No standalone counts without a denominator or a delta.
**Opposite ruled out:** "show current state; let users query for comparison" — requiring users to manually request comparison on every visit is an analysis tax. Comparison belongs in the default view.
#### "Drill-down is always one click"
**Statement:** every aggregated view resolves to its component data in one action.
**Implication:** clicking a chart segment opens the underlying records. Clicking a metric opens the contributing events. Breadcrumbs track the drill path. Back navigation returns to the aggregated view with state preserved. No aggregated view is a dead end.
**Opposite ruled out:** "keep the dashboard clean by removing drill-through affordances" — a dashboard that cannot answer "why" is an alert surface, not an analytics surface.
#### "Filters are first-class state"
**Statement:** the active filter configuration is a shareable, bookmarkable artifact.
**Implication:** filter state lives in the URL. Active filters are visible as removable chips above the data. "Share this view" produces a link that preserves filter state. Default filters are explicit, not implicit. Saving a filter configuration is a primary action, not a power-user feature.
**Opposite ruled out:** "filters are a temporary interaction, not state" — ephemeral filters mean users recreate the same analysis on every visit and cannot share findings with colleagues.
#### "Anomalies surface themselves"
**Statement:** the user should not have to look for outliers; the surface finds them.
**Implication:** statistical outliers are visually distinct (different color, flagged row, annotated data point) without requiring the user to configure a threshold. Alert states are visible in the default view. The absence of anomalies is also communicated explicitly ("all metrics within normal range"). Silence is a state, not the default.
**Opposite ruled out:** "show all data equally; users will spot patterns" — presenting uniform data without anomaly detection turns the analyst into a manual filter, adding no value over raw exports.
---
### 6. Collaborative Tools (real-time, multi-user)
#### "Presence is content"
**Statement:** who is on the surface matters as much as what is on the surface.
**Implication:** active users are always visible (avatars in nav, cursors in canvas, edits attributed inline). Conflict states are first-class designs, not hidden errors. Optimistic updates with reconciliation are the default; loading states are exceptional. The product communicates "you are not alone here" at every moment.
**Opposite ruled out:** "single-user UX with multiplayer features added later" — when single-user is the assumed base, collaboration feels grafted on. Presence needs to be architectural, not a feature flag.
#### "Conflict is expected, not exceptional"
**Statement:** simultaneous edits are the happy path; sequential editing is the edge case.
**Implication:** merge conflict UI is fully designed before launch, not handled by a generic error. Conflict resolution has a clear owner (last-write-wins, or explicit picker, or merge view) and it is documented in the product. The user is never dropped into an undefined state because two people edited the same field.
**Opposite ruled out:** "prevent conflicts via locking" — locking blocks work. Collaborative products that serialize writes to prevent conflicts have misunderstood their category.
#### "Attribution is visible, not interrogated"
**Statement:** the origin of every change is visible without opening a history panel.
**Implication:** inline attribution ("J. Reyes" next to an edited cell, an avatar on a comment) is standard in the default view. History is for audit, not for understanding current state. New content from other users is visually differentiated until dismissed. "Who did this?" has an answer in the main view.
**Opposite ruled out:** "attribution belongs in activity logs, not the main surface" — burying authorship in a panel means users lose context about what is new and what is theirs, slowing trust-building in shared workspaces.
#### "Async and sync are the same surface"
**Statement:** the product works for people in different timezones using the same interface.
**Implication:** unread changes are surfaced at the top of the affected view on return, not in a notification inbox. Comments thread in context, not in a sidebar. Status indicators (online now / last seen) are present but not the primary affordance. A user returning after 8 hours sees the same product as a user who was online during those 8 hours.
**Opposite ruled out:** "optimize for real-time; async users use notifications" — bifurcating async users into a notifications inbox creates a two-tier experience and signals the product was designed for a single timezone team.
#### "Undo survives other users"
**Statement:** a user can revert their own changes even after others have built on top of them.
**Implication:** per-user undo history is maintained separately from the shared document state. Reverting a change produces a new change, not a silent overwrite. The user is informed when their undo would affect content another user has seen or built on. Undo is never blocked because someone else has since edited the same record.
**Opposite ruled out:** "shared document state supersedes individual undo" — stripping undo after a collaborator touches a record makes users afraid to experiment in shared spaces.
---
### 7. AI / Streaming Surfaces
#### "Streaming is a state, not a transition"
**Statement:** mid-stream is a first-class UI state with its own affordances.
**Implication:** designed token-by-token streaming with a visible caret, partial citations rendered as they arrive, a mid-response abort affordance always present. Stream-complete is its own distinct state with a summary, feedback controls, and copy/share affordances. No skeleton loaders during streaming — they communicate "wait" when the system is communicating "I am thinking."
**Opposite ruled out:** "show a spinner until the full response arrives" — collapsing the streaming experience into a loading state discards the primary trust signal (visible progress) and makes the product feel slower than it is.
#### "Tool use is observable"
**Statement:** when the model calls a tool or searches a source, the user sees it.
**Implication:** tool calls are rendered as trace items inline with the response, not hidden behind the output. Web searches, code executions, and API calls show the query and result summary, not just the derived answer. Power users can expand traces to full detail. The product never presents a derived answer without attributing the source.
**Opposite ruled out:** "hide tool calls to keep the experience clean" — hiding the reasoning behind AI outputs reduces trust and prevents the user from catching errors in the sourcing step.
#### "Regeneration is always available"
**Statement:** every AI response has a retry path that the user can reach without retyping.
**Implication:** regenerate is a persistent affordance on every response, not just the most recent one. Editing a prompt to regenerate is one click, not a delete-and-retype flow. Variant generation (same prompt, different result) is available. The user is never trapped with a single output.
**Opposite ruled out:** "one response per prompt keeps the interaction clean" — without regeneration, the user loses trust in the system when the first output is wrong, because they perceive no recourse.
#### "Citations are load-bearing, not decorative"
**Statement:** every factual claim has a traceable source; citations are interactive, not footnotes.
**Implication:** cited sources are inline, hoverable or clickable, and open the source in context. Citations appear during streaming, not only after completion. The UI distinguishes between high-confidence and low-confidence citations (e.g., direct quote vs. paraphrase). A response with no citable sources is visually distinct from one with citations.
**Opposite ruled out:** "citations clutter the reading experience" — removing citations to improve readability trades user trust for surface aesthetics. In AI surfaces, the citation is the proof, not the decoration.
#### "Feedback closes the loop"
**Statement:** the user can signal quality, and the signal is visible as confirmed.
**Implication:** thumbs up/down or equivalent is always present on completed responses. Submitting feedback produces visible confirmation (not a silent network request). Negative feedback opens a specific follow-up (what was wrong?), not a generic rating. Feedback state persists on the response after page reload.
**Opposite ruled out:** "feedback is a product-side signal; users don't need to see it confirmed" — invisible feedback signals read as dismissed. Confirmation closes the trust loop and increases feedback volume.
---
### 8. Public-facing Forms (sign-up, checkout, applications)
#### "Friction sized to commitment"
**Statement:** the form's resistance must match what the user is actually agreeing to.
**Implication:** account creation requires email only (no profile, no avatar, no phone on the first step). Checkout uses smart defaults — saved address, autofill, no required account creation. Application forms show progress and allow resume-anywhere. Friction increases only where the commitment justifies it (2FA on first payment, ID verification on regulated actions).
**Opposite ruled out:** "collect all necessary information upfront to avoid re-engagement" — high-friction sign-ups for unproven value lose users at the first required field. Collecting data early optimizes for the product's convenience, not the user's.
#### "Validation never surprises"
**Statement:** errors appear where the user is, not where they are going.
**Implication:** inline validation fires on blur, not on submit. Error messages name the field and state the fix, not the rule violated ("Enter a valid email address" not "Invalid format"). Password rules are visible before the user types, not after they fail. Submit-time errors scroll to the first error; the user does not have to find it.
**Opposite ruled out:** "validate on submit to avoid interrupting the flow" — submit-time validation collapses all errors into one moment of failure, maximizing frustration and minimizing the information density needed to fix each error.
#### "Progress is always visible"
**Statement:** the user knows where they are, how far is left, and what they have completed.
**Implication:** multi-step forms show a step indicator that counts completed and remaining steps. Step labels are specific ("Payment details" not "Step 3"). Completed steps are accessible for review without losing current state. The last step is labeled "Submit" or equivalent — never "Next."
**Opposite ruled out:** "hide progress to avoid anchoring users to effort" — hiding progress trades completion anxiety (I don't know how long this will take) for commitment bias (I've come this far). Most users benefit from knowing the end is near.
#### "Defaults are decisions"
**Statement:** every pre-filled value or pre-selected option was chosen deliberately, not as a placeholder.
**Implication:** pre-selected checkboxes are opted-in by default only when the user would genuinely expect them (save address for future orders — reasonable; opt into marketing email — not reasonable). Auto-selected tiers or plans are the one that matches the most common user, not the most profitable. Pre-filled dates use the most likely answer. No field has a default that benefits the product at the user's expense.
**Opposite ruled out:** "use defaults that maximize conversion or revenue" — dark-pattern defaults are a one-time conversion gain traded for long-term trust erosion and churn.
#### "Submit is one button"
**Statement:** the primary action is always singular and unambiguous.
**Implication:** no forms have two competing primary actions at the bottom ("Save" + "Submit" at equal weight). Secondary actions (save draft, cancel) are visually subordinate. The primary button label says what happens after the click ("Create account", "Place order", "Send application"). No CTAs labeled "Continue," "Next," or "Finish" on the final step.
**Opposite ruled out:** "multiple actions give users flexibility" — equal-weight competing actions on a form are the fastest path to the wrong action. Flexibility in form submission is a failure of design confidence.
---
## Anti-principles (what NOT to write down as a principle)
These are platitudes that masquerade as principles. They look opinionated but rule nothing out. If your candidate principle appears on this list, rephrase it or discard it.
- **"Be user-friendly"** — opposite ("be user-hostile") is insane. Not opinionated. Every product claims this.
- **"Design with empathy"** — opposite ("design without empathy") is incoherent. Too vague to produce a design implication.
- **"Make it beautiful"** — beautiful is undefined and context-dependent. No design decision follows from it.
- **"Mobile-first"** — this is consensus in 2026, not a stance. It belongs in your stack defaults, not your principles.
- **"Accessibility matters"** — yes. This is the floor, not a principle. Every surface must be accessible; treating it as a principle lets non-accessible surfaces exist by default.
- **"Keep it simple"** — "simple" is undefined. This is a preference, not a constraint. "Show the data, not the design" is what "keep it simple" actually means for a developer tool.
- **"Modern design"** — modern compared to what, when? This is an aesthetic preference with no design resolution power.
- **"We care about our users"** — a mission statement, not a principle. Put it in the README, not the brief.
A principle that produces no design implication and rules out no alternative is a slogan. Slogans go in marketing copy, not in `.ui-craft/brief.md`.
---
## When to Skip Categories
The catalog covers archetypes. Many products span multiple categories and should borrow from each.
A developer tool with a consumer-facing onboarding flow needs Developer Tools principles for the product surfaces and Consumer Apps principles for the sign-up and first-run experience. Write the brief in two scoped sections rather than forcing all principles to cover both.
Internal tools where the design language is intentionally platform-native (system UI, operating-system controls) can adopt the platform's implicit principles rather than inventing product-specific ones. Codify only the places where the product diverges from platform defaults — those are the only decisions that need an explicit principle.
Pre-launch products may lack enough product reality to write distinctive principles. The "would anyone disagree?" test from `brief.md` is the gate: if every candidate principle passes it trivially (the opposite sounds insane), defer the principles workshop until you have user reality to anchor against. Provisional principles based on hypotheses are acceptable — mark them as provisional and revisit after first usability sessions.
Products built on a design system (shadcn, MUI, platform-native) inherit a set of implicit principles from that system. Those inherited principles do not need to be restated. Write only the principles where this product diverges from or extends the system's defaults.
---
## Cross-References
- `brief.md` — the parent reference; this catalog is invoked from the principles workshop section. The brief format that consumes these principles is defined there.
- The `inspiration.md` "what mature interfaces never do" section is the empirical complement to this catalog. Those are observed patterns from production products; these are intentional commitments. Both are needed: observation shows what the standard is; principles show where you differ from it.
@@ -0,0 +1,88 @@
# Recipe: Auth (Sign in / Sign up)
Outcome recipe for authentication surfaces. Small surface, zero tolerance: auth is used by 100% of users at their least patient moment, and it's where AI-generated UIs are most recognizable (full-bleed saturated brand panel, italic quote, "OR" divider). Field rules live in [forms.md](forms.md), component contracts in [components.md](components.md).
**Load [craft-intent.md](craft-intent.md)** — apply product-surface patterns (§4), default **DESIGN_VARIANCE 4**, pick one **signature bet** from craft-intent §3 auth list in the Craft Read before Step 1.
## Step 0 — Four inputs (defaults if unanswered)
| Input | Options | Default |
|---|---|---|
| Brand surface | product has marketing presence worth showing / pure utility app | marketing presence |
| Auth methods | SSO-first (Google/GitHub/SAML) / email-first / both | both, SSO on top |
| Theme | preset from [themes.md](themes.md) or existing brand tokens | brand tokens if present, else **Graphite** |
| Variance | DESIGN_VARIANCE 1-10 | 4 |
| Signature bet | one from [craft-intent.md](craft-intent.md) §3 auth list | panel proof asset (metric + sparkline) or trust footer line |
## Step 1 — Pick the composition
### Split panel (default — brand presence)
```
┌──────────────────────────┬────────────────────┐
│ logo │ │
│ │ Welcome back │
│ ONE proof asset: │ sub, one line │
│ metric + sparkline │ [SSO] [SSO] │
│ OR attributed quote │ ── or with email ─│
│ (pick one, not a deck) │ Email │
│ │ Password Forgot? │
│ │ [Sign in] │
│ trust footer (compliance)│ New here? link │
└──────────────────────────┴────────────────────┘
```
Panel is a **tinted neutral surface** (gray-1 of the theme), NEVER a full-saturation brand flood — a wall of pure accent is the single loudest AI tell on auth screens, and it blows the entire accent budget on decoration. Panel content: one believable proof asset with real numbers or a real attribution. Trust line at the panel's foot (compliance, data residency) — sign-in is where users think about trust.
### Centered card (utility / in-product re-auth)
```
┌───────────────────────────────────────────────┐
│ │
│ logo (small, centered) │
│ Welcome back │
│ [SSO buttons] │
│ ── or with email ── │
│ form card, 360-400px │
│ footer links │
│ │
└───────────────────────────────────────────────┘
```
No panel, no story — speed is the design. Card 360-400px, generous vertical rhythm, page background gray-1 with the card on white (or theme surfaces in dark).
## Step 2 — Form contract (non-negotiable)
- Form column **360-400px**. Wider reads as a document; narrower cramps the SSO row.
- **SSO buttons above email** when SSO is the dominant path — order by actual usage, not aesthetics. Equal visual weight between SSO providers (neutral outline buttons, real provider marks).
- Divider is lowercase, quiet: "or with email" — not "OR" shouting in caps.
- Labels above fields ([forms.md](forms.md)); "Forgot password?" inline with the password label, right-aligned — not buried under the button.
- **No required asterisks** — every field on an auth form is required; single-purpose familiar forms skip marking entirely ([forms.md](forms.md)).
- Password: show/hide toggle with `aria-label`. On sign-UP: live strength hint, validate-as-you-type with debounce; never a wall of "must contain" rules before the user types.
- Submit: full-width primary, verb label ("Sign in", "Create account") — the page's ONE accent-filled element. Stays enabled; validates on press ([forms.md](forms.md)).
- Error state: message below the offending field + border, focus moved to it. Auth errors stay honest but unspecific where security demands ("Email or password doesn't match") — and actionable where it doesn't (rate-limit: say when to retry).
- Cross-link the sibling flow ("New here? Start a free trial") — one line, after the submit.
## Step 3 — Sign-up deltas
- Ask the minimum: email + password (+ name only if the product needs it day one). Every extra field measurably increases drop-off — enrich the profile later, in-product.
- State what happens next under the CTA ("We'll send a verification link").
- Legal line (terms/privacy) in tertiary text under the CTA — present, quiet, real links.
- Multi-step signup (workspace, invites): apply the multi-step contract from [forms.md](forms.md) — cost upfront, easy→hard, progress.
## Acceptance bar
- [ ] Craft Read declared; variance 4 unless brief says otherwise; signature bet built in first pass
- [ ] Squint test: eye lands on the submit button; accent appears ONLY on submit + links
- [ ] No saturated full-bleed brand panel; panel (if any) is tinted neutral with one proof asset
- [ ] Form column 360-400px, labels above, no asterisks, divider lowercase
- [ ] "Forgot password?" findable without scanning; sibling flow cross-linked
- [ ] Submit enabled + validates on press; errors below fields, focus managed
- [ ] Keyboard: tab order top-to-bottom, Enter submits, focus visible on every control
- [ ] Trust signals present (compliance line or equivalent) and honest
- [ ] Works at 320px wide: panel drops, form survives alone
- [ ] Product patterns from craft-intent §4: tinted panel surface, one accent-filled element, honest error copy
## Cross-refs
[craft-intent.md](craft-intent.md) Craft Read, variance, auth signature bets · [forms.md](forms.md) field layout, validation, marking · [components.md](components.md) buttons, links · [themes.md](themes.md) surface tokens · [state-design.md](state-design.md) loading/error states for the submit roundtrip
@@ -0,0 +1,130 @@
# Recipe: SaaS Dashboard
An outcome recipe is a build contract, not a rule list: follow it top to bottom and the result is shippable without a design retouch pass. Rules live in [dashboard.md](dashboard.md), [dataviz.md](dataviz.md), and [tokens.md](tokens.md) — this file decides **composition**: which layout, which defaults, in what order, and what "done" means.
**Who this serves:** the zero-questions path produces a strong default for users who don't design. Every default below is overridable — users who do design treat the compositions as skeletons and go from there.
**Load [craft-intent.md](craft-intent.md)** — apply product-surface patterns (§4), default **DESIGN_VARIANCE 4**, pick one **signature bet** from craft-intent §3 (product list) in the Craft Read before Step 1.
## Step 0 — Three inputs (defaults if unanswered)
Ask once, in one compact prompt. If the user declines or says "you decide", apply the defaults — never block.
| Input | Options | Default |
|---|---|---|
| Primary user + 60-second decision | exec ("are we winning?") / operator ("what needs action now?") / analyst ("why did X change?") | operator |
| Theme | one of the four presets in [themes.md](themes.md), or project's existing tokens | existing tokens if present, else **Graphite** |
| Density | VISUAL_DENSITY knob 1-10 | 7 |
| Variance | DESIGN_VARIANCE knob 1-10 | 4 |
| Signature bet | one from [craft-intent.md](craft-intent.md) §3 product list | nav active indicator or hero metric sparkline |
The first answer selects the composition. The persona is not cosmetic — it decides what the hero metric is and what gets buried ([dashboard.md](dashboard.md) "Ranking decisions").
## Step 1 — Pick the composition
Three named compositions. Don't invent a fourth unless the user's brief demands it — these cover the decision shapes that recur in production SaaS.
### Overview (exec)
```
┌──────┬──────────────────────────────────────┐
│ │ topbar: title · date range · avatar │
│ nav ├───────────────┬──────┬──────┬────────┤
│ │ HERO metric │ sup1 │ sup2 │ sup3 │
│ ├───────────────┴──────┴──────┴────────┤
│ │ trend area chart (2/3) │ breakdown │
│ │ │ h-bars 1/3 │
│ ├─────────────────────────┴────────────┤
│ │ table (top movers, 5-8 rows) ▼fold │
└──────┴──────────────────────────────────────┘
```
One hero answering "are we winning", 3 supports decomposing it, one trend, one breakdown. Table is below the fold on purpose — execs drill rarely. Charts large, tables small.
### Command (operator) — default
```
┌──────┬──────────────────────────────────────┐
│ │ topbar: title · global filter · ⌘K │
│ nav ├──────┬──────┬──────┬─────────────────┤
│ │ hero │ sup1 │ sup2 │ status strip │
│ ├──────┴──────┴──────┴─────────────────┤
│ │ filter row: ghost buttons · reset │
│ ├──────────────────────────────────────┤
│ │ WORK QUEUE table — dominant, 60%+ │
│ │ of viewport height ▼fold │
└──────┴──────────────────────────────────────┘
```
The table IS the product: the operator acts on rows. Metric strip is compact (single row, small cards). Row actions visible on hover, keyboard navigable (j/k or arrows), status as 6px dots. Charts only as sparklines inside cards — a big trend chart steals space from the queue.
### Analytics (analyst)
```
┌──────┬──────────────────────────────────────┐
│ │ topbar: title · saved views · export │
│ nav ├──────────────┬───────────────────────┤
│ │ filter rail │ primary chart canvas │
│ │ (dimensions, │ (area or h-bars, │
│ │ segments, │ direct-labeled) │
│ │ compare) ├───────────────────────┤
│ │ │ segmented table ▼fold│
└──────┴──────────────┴───────────────────────┘
```
Filters are first-class (left rail, not a toolbar row) because the analyst's job is slicing. Chart canvas re-renders on every filter change — show a 150ms opacity transition, never a spinner for operations under 400ms. Direct-label series, drop the legend ([dataviz.md](dataviz.md)).
## Step 2 — Shell spec (exact values)
- **Sidebar:** 240px (range 220-260). Subtle bg tint per [dashboard.md](dashboard.md) — never full dark outside Dark Premium. Collapses to 56px icon rail below 1024px, off-canvas below 768px.
- **Topbar:** 56px (range 48-64), same surface as content (no border-bottom heavier than 1px hairline).
- **Content:** padding 24px (density ≤4: 32px; ≥8: 16px), max-width none — dashboards use the viewport. 12-col grid, gap 16px (24px at density ≤4).
- **Card padding:** 16-20px. **Table rows:** 40-48px. Both from [dashboard.md](dashboard.md) Content Density.
- **Above the fold at 1440×900:** hero + supports + one more content type minimum. Three content types visible — non-negotiable.
## Step 3 — Component inventory
Build exactly these, to spec — each links to its contract:
1. Nav item (active = accent bg at 10% + accent text; `aria-current="page"`) — [dashboard.md](dashboard.md)
2. Metric card, two variants: primary (accent-tinted, 36px number) / secondary (neutral, 28px). Sparkline 32px. Change text plain tertiary — [dashboard.md](dashboard.md)
3. Area chart with gradient fill 15%→0, horizontal-only gridlines — [dashboard.md](dashboard.md) Chart Styling
4. Data table: status dots, right-aligned `tabular-nums` numbers, sentence-case headers, row hover — [dashboard.md](dashboard.md) Data Tables
5. Filter bar: ghost buttons, conditional "Reset" text link — [dashboard.md](dashboard.md)
6. Empty / loading / error states for EVERY data region — [state-design.md](state-design.md). Skeletons mirror final layout geometry, never generic gray boxes of arbitrary size.
**Libraries** (tools, not design authority — match the project's stack first): charts via Recharts/visx (React) or ECharts/Chart.js (otherwise); sparklines as inline SVG polylines, no library. Tables: headless (TanStack Table) + own markup; never a themed datagrid that fights the token system. Icons per SKILL.md rule 4.
## Step 4 — Build order
Token spine first, states before polish. Skipping ahead is how slop happens.
1. Tokens: apply theme preset or map existing tokens ([themes.md](themes.md), [tokens.md](tokens.md))
2. Shell: sidebar + topbar + grid, responsive collapse wired
3. Hero tier + supports (the squint test must already pass here)
4. Primary chart or work-queue table (per composition)
5. Remaining tiers
6. States: loading / empty / error / partial for every data region
7. Keyboard pass: focus-visible everywhere, table navigable, ⌘K if Command composition
8. Finish: run [finish-bar.md](finish-bar.md) passes 1-4 + 7 minimum (full bar at CRAFT_LEVEL ≥ 8)
## Acceptance bar — "would a designer retouch this?"
Ship only when every box checks. One unchecked = not done, no exceptions:
- [ ] Squint test: eye lands on exactly ONE thing first (the hero)
- [ ] One accent color, ≤5 placements in the viewport; everything else neutral
- [ ] ≥3 content types above the fold; NO equal-weight card grid anywhere
- [ ] Every number `tabular-nums`; every comparison plain secondary text (no pills, no arrows)
- [ ] Empty, loading, and error states exist and match layout geometry
- [ ] Date range or equivalent time control present on time-series surfaces
- [ ] Keyboard: tab order logical, focus visible, table rows reachable
- [ ] Sidebar tinted (not black), headers sentence case, no uppercase anywhere except 11-13px tracked labels
- [ ] Charts: correct type per data story, single-hue opacity ramp, no legend for single series
- [ ] Signature bet from Craft Read is built and visible (not deferred to polish)
- [ ] Product patterns from craft-intent §4: tinted sidebar, metric hierarchy, row context or sparkline on hero metric
- [ ] `prefers-reduced-motion` honored; all transitions ≤400ms
## Cross-refs
[dashboard.md](dashboard.md) component contracts · [dataviz.md](dataviz.md) chart science · [themes.md](themes.md) token presets · [state-design.md](state-design.md) state lattice · [finish-bar.md](finish-bar.md) finishing passes · [personas.md](personas.md) grounding the persona choice
@@ -0,0 +1,122 @@
# Recipe: Landing Page
Outcome recipe for marketing landing pages — composition, section grammar, and the bar that makes the result publishable without a design retouch. Component rules live in [components.md](components.md), copy rules in [copy.md](copy.md), pattern evidence in [inspiration.md](inspiration.md). This file decides what to build and in what order.
**Who this serves:** zero-questions path → strong default. Designers treat the compositions as skeletons.
**Portfolios / case studies:** use this recipe with **DESIGN_VARIANCE 8** (craft-intent default) — variable grid aspects, one hero project above fold, asymmetric about/contact. Marketing patterns (§5) apply; default composition is often message-forward or proof-forward with editorial type.
**Load [craft-intent.md](craft-intent.md)** and [inspiration.md](inspiration.md) hero archetypes — apply marketing patterns (§5), default **DESIGN_VARIANCE 7** (6 for waitlist-only), pick **signature bet** from craft-intent §3 marketing list in the Craft Read.
## Step 0 — Three inputs (defaults if unanswered)
| Input | Options | Default |
|---|---|---|
| What exists to show | live product (screenshots possible) / pre-launch (no product yet) / sales-led service | live product |
| Theme | a preset from [themes.md](themes.md) or existing brand tokens | brand tokens if present, else **Graphite** |
| One conversion action | trial signup / demo request / waitlist / purchase | trial signup |
| Variance | DESIGN_VARIANCE 1-10 | 7 (6 if pre-launch / waitlist only) |
| Signature bet | one from [craft-intent.md](craft-intent.md) §3 marketing list | hero crop or floating proof card |
The first answer selects the composition. One page, ONE conversion action — every section either advances it or gets cut.
## Step 1 — Decide what the fold argues, then draw its geometry
Two decisions, in this order, and they are not the same decision. **What the fold argues** is yours to choose from the brief. **How it is laid out** is drawn, not chosen.
Until 2026-08-13 this section shipped three ASCII diagrams, one per strategy, and the first was labelled *default*. Its geometry was text one side and a visual the other — which is the `split` composition class, the one `fold_candidates` deliberately draws last because *"it is the fold every generator reaches for unprompted."* Measured across seven landing builds, every fold that got built was one of the three geometries those diagrams prescribed, and none was the class the draw had offered. A drawn class cannot introduce variety while this file ships the answer with a picture, so the pictures are gone and the content grammar stayed.
### 1a — What it argues
| Strategy | Lead with | Fold carries |
|---|---|---|
| **Product-forward** — live product to show | the product doing its job | badge (what's new, or none) · H1 2-3 lines · sub ≤2 sentences · primary + ghost CTA · one micro-trust line · a real product shot, cropped at the fold **and** an edge · at most one floating mini-card over it (live metric, notification) for depth |
| **Message-forward** — pre-launch, waitlist, no product to show | the claim, in language | H1 as the largest thing on the page · sub one sentence · email input with the join CTA attached to it · proof placed off-centre on purpose (avatars + count one side, badge the other) · an abstract motif rather than a screenshot |
| **Proof-forward** — sales-led, B2B service | evidence before argument | compact H1 + sub + demo CTA · an outcome strip of exactly 3 specific metrics in large numerals · a case block: quote + attributed face + numbers · capability rows alternating beneath |
No strategy is a default. A brief with a live product picks product-forward; a brief without one cannot.
### 1b — What it looks like
Call **`fold_candidates`** and commit to one of the classes it returns. Do not pick by taste: asking a model for a composition returns its default every time, which is how eight builds in a row produced the same hero. The draw is seeded per project, so two projects start somewhere different and one project stays reproducible.
The six classes are geometric and orthogonal to 1a — `type-only`, `full-bleed-overlay`, `split`, `stacked`, `product-dominant`, `band`. Most strategies can be built as most classes. The exclusions are the ones where the geometry cannot carry the argument:
- **product-forward** cannot be `type-only` — no visual means no product
- **message-forward** cannot be `product-dominant` — nothing to dominate with
- **proof-forward** cannot be `full-bleed-overlay` — one dominating image cannot hold three metrics and a case block
Anything else the draw offers is legitimate. If it draws a class that fights the brief for a reason not listed above, say which reason in the Craft Read and draw again — do not silently substitute the shape you had in mind.
### 1c — Verify what you actually built
Render it and call **`check_fold` with `expected_class`** set to the class you committed to. Without that argument it reports what the fold *is* and cannot tell you whether it is what you intended — and it will say so rather than imply agreement.
If it reports drift, the geometry moved while you were writing. Fix the build to match the class, or record in the brief that the class changed and why. A declared class the code does not honour is worse than no declaration: it reads as compliance to anyone auditing the transcript.
Record the class in the brief's spent list ([brief.md](brief.md)) so the next landing in this project draws something else.
### Hero discipline (all compositions)
The hero is a single moment, not a feature list. Hard limits:
- **Max 4 text elements:** one eyebrow/badge (or none), headline (≤2 lines desktop), subtext (≤20 words, ≤4 lines), CTAs (1 primary + ≤1 secondary). If the value prop needs more than 20 words of subtext, the value prop is unclear — fix the copy, not the limit.
- **Plan font scale and asset together.** Headline >6 words with a large hero shot → start at 48-60px, not 72-80px. A 4-line hero headline is always a font-size error.
- **Nothing else rides the hero:** trust micro-strips, pricing teasers, feature bullets, avatar rows, and the logo wall all live in dedicated sections **below** the fold line. One micro-trust line under the CTAs is the ceiling.
- **The headline starts 18-30% down the fold, not at the top.** This used to read "top padding cap ~96px", and that cap was wrong: on a 900px viewport it puts the headline at 11%, which is where a dense app starts its content, not a landing. Measured on four surfaces whose design is the reason people copy them, the first headline pixel lands at **19%, 20%, 24% and 64%** of the fold. 18-30% is the working band; above 30% is a deliberate, centred composition and needs the rest of the fold to be nearly empty to earn it. On 900px that is **160-270px of clear space** above the eyebrow — an amount that feels wrong while you are writing the CSS and correct the moment you look at the render.
- **Ink density in the fold: 15-35 text nodes.** The same four measured 16, 34, 34 and 88 — and the 88 is a page whose fold is 68% one product screenshot, so its text is concentrated inside that image. A fold with 50 separate text nodes is a dashboard. Count them: if the fold has more than ~35, something in it belongs to the section below.
- **The hero asset is 30-70% of the fold, or it is absent.** Measured: 68%, 32%, 31%, and one page with no hero image at all. What does not work is the middle: a 4:1 letterbox band or a small framed card reads as a placeholder for a real asset. Either give the product most of the fold and crop it at the edge, or commit to type alone.
- **No decorative cues:** no "Scroll to explore", no ↓ arrows, no version badges (BETA, v0.6) unless the brief is literally a launch announcement, no mono-caps decoration strips at the hero foot.
Order, each answering ONE question; spacing 80-160px between majors, varied:
1. **Proof strip** — "do people like me use this?" Logos at low contrast + one specific stat ("teams cut X from 6h to 20min" beats "trusted by thousands").
2. **Feature rows × 2-3** — "what does it do for me?" Asymmetric alternating rows with REAL visuals (chart, flow, screenshot detail). NEVER a uniform 3-column icon grid — that's the #1 template tell.
3. **How it works / depth section** — "is it credible?" 3 steps max, or one technical diagram. Cut it if the product is self-evident.
4. **Pricing teaser or full pricing** — "can I afford it?" See pricing block below.
5. **Final CTA** — "ok, how do I start?" Restate the primary action + the micro-trust line. One section, not a wall.
6. **Footer** — boring on purpose. Sitemap, legal, socials. Footers that try to be clever bury the links people need. No fake version stamps (`v1.4.2`, `Build 0048`) — those are devtool fixtures, not marketing content.
**Layout-family budget.** A layout family (3-column cards, split text+image, full-width quote, bento, marquee) appears **at most once per page**; a page with 8 sections needs ≥4 distinct families. Max 2 consecutive image+text splits — the third consecutive zigzag is a fail; break it with a full-width section, a vertical stack, or a bento. "Selected work" must not look like "What we do".
**Eyebrow budget.** Max 1 uppercase tracked micro-label per 3 sections (hero counts as one). Mechanical check: count `uppercase` + wide-tracking labels above headings; if count > ceil(sections / 3), delete eyebrows until it passes — the headline alone is enough.
**Imagery is not optional.** Marketing pages are visual products; a text-only page is incomplete work, not minimalism. Priority order: (1) real product shots / brand assets, (2) image-generation tool if available in the environment, (3) clearly-labeled placeholder slots plus a list for the user of what's needed — never div-built fake screenshots or hand-drawn decorative SVG scenes. Logo walls use real SVG marks (both themes); invented brands get a simple monogram mark, never styled text wordmarks. Logos only — no industry labels printed under each logo.
**Pricing block rules** (when present): highlight the recommended plan (border/badge/size) without making siblings look irrelevant; sticky column headers on long comparison tables; tooltips on hover for feature jargon; discounts shown as % under $100 and absolute amounts above (perceived size); charm pricing where the brand tolerates it; scarcity only if genuinely true — faked urgency reads instantly and burns trust.
## Step 3 — Craft constraints (the ones landings break most)
- **CTA hierarchy is three levels that must not tie:** nav CTA ≠ hero primary ≠ section CTAs. Hero primary is the most prominent interactive element on the page ([components.md](components.md)).
- **Headline carries a dual benefit** where honest — immediate + long-term ("Answers today, confidence at month-end"). Front-load the key noun. No jargon: a stranger gets the value in one read.
- **One signature detail** — drawn underline on the key word, a custom marker, a motif from the brand mark. Exactly one.
- **Specific beats vague, everywhere:** metrics with units, named customers, real UI. Every "world-class/powerful/seamless" is a slot where evidence should be.
- **Gradients**: if used, adjacent hues only, plus subtle grain/noise to kill the flat plastic look. Never the purple-cyan template wash.
- **Copy budget:** no section over 2-3 sentences; CTAs are verb + outcome ("Start free trial", not "Get started" twice in different colors).
## Step 4 — Build order
1. Tokens (preset or brand) → 2. Nav + hero (squint test must pass with hero alone; then check the two numbers: headline top at 18-30% of the fold, and 4-6 distinct font sizes on the page with a 2.5x+ jump to display — see [typography.md](typography.md)) → 3. Proof strip → 4. Feature rows → 5. Pricing → 6. Final CTA + footer → 7. Responsive pass ([responsive.md](responsive.md) — hero stacks text-first on mobile, shot below, still cropped) → 8. Motion: entrances subtle, one scroll reveal per section max ([motion.md](motion.md)) → 9. Finish: [finish-bar.md](finish-bar.md) passes 1-4 + 8.
## Acceptance bar — publishable without retouching?
- [ ] Squint test on the hero: H1 → primary CTA, in that order; nothing competes
- [ ] One conversion action; every section advances it
- [ ] Product/visual cropped at fold or edge (scroll tease); no visual floating in dead air
- [ ] CTA hierarchy: 3 distinct levels, no ties
- [ ] At least one specific, attributed proof point; zero unattributed superlatives
- [ ] No uniform icon-card grid anywhere
- [ ] Section spacing 80-160px, varied; every section answers one question
- [ ] One signature detail, exactly one
- [ ] Mobile: hero readable without zoom, CTAs thumb-reachable, no horizontal scroll
- [ ] Craft Read declared; variance and signature bet match the built page
- [ ] No two adjacent sections share the same layout structure; no layout family repeats; ≤2 consecutive image+text splits
- [ ] Hero discipline: ≤4 text elements, subtext ≤20 words, headline ≤2 lines, logo wall below the hero
- [ ] Eyebrow count ≤ ceil(sections / 3); no numbered section eyebrows; no scroll cues
- [ ] One CTA label per intent across the page (nav, hero, footer reuse the same words); every CTA fits one line at desktop and passes AA contrast on its own background
- [ ] Real imagery where the composition calls for it; no div-built fake screenshots; logo wall is real SVG marks
- [ ] `prefers-reduced-motion` honored; entrances ≤400ms; no scroll-jacking
## Cross-refs
[inspiration.md](inspiration.md) observed patterns · [components.md](components.md) buttons, links, nav · [copy.md](copy.md) voice, CTAs, numbers · [themes.md](themes.md) presets · [recipe-dashboard.md](recipe-dashboard.md) when the hero shot needs a product UI worth showing
@@ -0,0 +1,168 @@
# Responsive & Adaptive Design
Adaptation is not just scaling — it's rethinking for each context.
---
## Breakpoint Strategy: Content-Driven (Preferred)
Add breakpoints where the content breaks, not at conventional device widths. Container queries are the preferred mechanism — components respond to their container, not to the viewport.
```css
.card-container { container-type: inline-size; }
@container (min-width: 400px) {
.card { grid-template-columns: 1fr 2fr; }
}
```
Use fluid sizing to eliminate most breakpoints entirely:
```css
/* Fluid font: 16px at 320px → 20px at 1200px */
font-size: clamp(1rem, 0.5rem + 1.5vw, 1.25rem);
/* Fluid spacing */
padding: clamp(1rem, 2vw, 3rem);
```
For full container query patterns — style queries, named containers, when to prefer container over media queries — see [modern-css.md](modern-css.md).
---
## Breakpoint Strategy: Device Reference (Fallback)
When the project's existing system uses device breakpoints, match it. The conventional ranges:
- Mobile: < 768px
- Tablet: 768–1023px
- Desktop: 1024px+
Use these only when integrating with an existing system. New code should prefer container queries (see above).
### Verify Coverage
- Mobile, tablet, laptop, ultra-wide
- Portrait AND landscape
- Different browsers (Safari, Chrome, Firefox)
- iOS Low Power Mode, macOS Safari
---
## Mobile Adaptation (Desktop → Mobile)
### Layout
- Single column (not multi-column)
- Vertical stacking
- Full-width components
- Bottom navigation (not top/side)
### Interaction
- Touch targets **44x44px minimum**
- Swipe gestures only for reversible, spatially-implied actions — dismiss a sheet, page a carousel, reveal a row action. **Why:** a swipe leaves no visible affordance, so anything discovered only by swiping is undiscoverable to a first-time user, and anything destructive behind it is irreversible-by-accident. Every swipe needs a visible equivalent: a button, a menu item, or a drag handle.
- Bottom sheets instead of dropdowns
- **Thumbs-first design** — primary and navigational actions inside the bottom-center thumb zone; destructive or cancel actions deliberately OUTSIDE easy reach. **Why:** the thumb zone optimizes for the action you want repeated; placing "Delete" there optimizes for accidents.
- More spacing between interactive elements
### Content
- **Progressive disclosure** — don't show everything at once
- Secondary content in tabs/accordions
- Shorter, more concise text
- Minimum **16px font size** (prevents iOS zoom)
### Navigation
- Hamburger or bottom nav
- Reduce complexity
- Sticky headers for context
- Back button in flows
---
## Tablet Adaptation
- Two-column layouts (not single or three)
- Master-detail views (list + detail)
- Adaptive per orientation (portrait vs landscape)
- Support both touch and pointer
- Touch targets 44px but allow denser layouts than phone
---
## Desktop Adaptation (Mobile → Desktop)
### Layout
- Multi-column (use horizontal space)
- Side navigation: always visible on desktop (1024px+). On tablet (768–1023px): collapse to icon rail or persistent narrow sidebar. On mobile: collapse to a drawer or bottom-nav pattern. **Why:** primary navigation must remain discoverable; visible-by-default at desktop trades screen real estate for discoverability, while mobile trades discoverability for content room — both are correct in their context.
- Multiple information panels
- Max-width constraints (don't stretch to 4K)
### Interaction
- Hover states for additional info
- Keyboard shortcuts
- Right-click context menus
- Drag and drop, multi-select with Shift/Cmd
### Content
- Show more info upfront (less progressive disclosure)
- Data tables with many columns
- Richer visualizations
---
## Responsive Grid (No Breakpoints)
```css
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-lg);
}
```
---
## Responsive Images
```html
<picture>
<source media="(min-width: 768px)" srcset="large.webp">
<img src="small.webp" alt="..." width="600" height="400" loading="lazy">
</picture>
```
---
## Viewport Units
Use `dvh` instead of `100vh` or Tailwind's `h-screen` for any surface meant to fill the viewport. Mobile browsers change the visible viewport height as the URL bar shows and hides — `100vh` measures the *largest* possible viewport and locks to it, so a full-height hero or modal ends up taller than what's actually visible and the bottom gets clipped behind the browser chrome. `dvh` (dynamic viewport height) tracks the real, current viewport instead.
---
## Safe Areas
Any fixed or sticky element must respect `env(safe-area-inset-*)` on notched devices — a bottom nav bar or sticky CTA that ignores the inset sits under the home indicator or gets clipped by it.
```css
.fixed-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
```
---
## Print Adaptation
- Remove navigation, footer, interactive elements
- Page breaks at logical points
- Expand shortened content (show full URLs)
- Black and white or limited color
- Add page numbers, headers, print date
---
## Never
- Don't hide core functionality on mobile — feature parity is the floor; if a feature is too complex for mobile, redesign the feature, don't hide it.
- Don't assume touch only on mobile — tablets and 2-in-1 laptops blur the line; design pointer-agnostic.
- Don't forget landscape orientation — mobile in landscape and tablet in portrait often have similar dimensions and need similar treatment.
- Don't ship horizontal scroll at any breakpoint without an explicit scroll affordance — accidental horizontal scroll is the most common AI-template responsive failure.
@@ -0,0 +1,340 @@
# UI Review & Critique
Systematic methodology for reviewing interface quality — visual design, interaction, accessibility, and performance.
---
## Feedback Hierarchy
Evaluate in order: Value → Ease of Use → Delight. Aesthetic feedback that arrives before Value and Ease is feedback misallocation — it tells the team to polish a thing that may not be solving the right problem, or polish a thing the user can't actually use.
### 1. Value — does this solve the problem the user came for?
- Confirm the surface answers the user's question (cite the brief.md success metric if available).
- Test against the primary user (also from brief.md) — does the path from arrival to outcome exist, and is it the shortest path the design allows?
- Findings here block all subsequent feedback. If Value fails, the visual review is not yet useful.
- Examples of Value findings:
- "User cannot find the report they came to read — search is hidden in a settings menu."
- "The empty state is decorative but doesn't explain how to add the first item."
- "Primary CTA leads somewhere unexpected — clicked Save, taken to a list view, no confirmation."
### 2. Ease of Use — can the user accomplish the task?
- Path length: count clicks/taps from arrival to outcome. Compare against the user's expectation.
- State coverage: every state the user encounters has explicit affordance (loading, empty, error, partial, success).
- Friction points: where does the user need to think, type, or wait? Each friction point earns a finding.
- Accessibility floor: if a path is unusable for a keyboard or screen-reader user, it's an Ease of Use failure (not a Delight failure — defer this in your head before promoting).
- Cross-ref: `accessibility.md`, `state-design.md`, `forms.md`.
### 3. Delight — does it feel polished?
- This is the layer most reviews start with — and most reviews stay here, missing the failures above.
- Delight findings are real and valuable, but they are **the last 20% of leverage**, not the first.
- The full Delight pass is the Finish Bar (`finish-bar.md` — 10 passes).
- Common Delight findings: hierarchy ratios off, type weights inconsistent, motion missing, microcopy generic, surface stack flat.
### How to triage
- Start by reading the surface against the brief (Value).
- If Value findings exist, write them as the first section of the report. Recommend the team fix Value before considering Delight feedback. **Do not include Delight findings in a report where Value is failing — it dilutes the signal.**
- If Value passes, evaluate Ease of Use. Same rule: surface Ease findings before Delight.
- If Value and Ease both pass, run the Finish Bar.
- The brief's principles can downgrade or defer findings (e.g., a brief that records "speed over completeness" defers some Ease findings about progressive disclosure).
### When to skip Value-first
- Pure visual review at the request of design (e.g., "review this hero animation") — the user has explicitly scoped to Delight. Honor it; note in the report header that Value was not assessed.
- Library/component reviews where the component is generic and Value depends on consumer code. Run Ease and Delight only.
- Brand/marketing surfaces where conversion is the metric — Value is "did the user do the conversion action?" — apply the hierarchy but the bar shifts.
---
## How to Use
- **`/ui-craft:critique <file>`** — UX critique, flag issues, no code changes
- **`/ui-craft:audit <file>`** — Technical audit (a11y, perf, responsive), severity-ranked
- **`/ui-craft:polish <file>`** — Apply the Polish Pass compound details directly
---
## Review Output Format
For file reviews, use terse findings:
```
file:line - [category] description of issue → fix
```
Example:
```
components/modal.tsx:45 - [animation] Exit 400ms exceeds 300ms limit → reduce to 200ms
components/button.css:12 - [interaction] Missing :active transform → add scale(0.97)
```
---
## Critique Methodology (for screenshots or deep review)
Follow this sequence. Each section is a separate lens.
### Step 0: Context
- **What is this?** (app type, screen purpose, target user)
- **Emotional context?** (stressful? casual? high-stakes? routine?)
### Step 1: Anti-Slop Detection (CRITICAL)
Run the Anti-Slop Test from SKILL.md (Critical → Major → Minor). Every item present is a finding.
**The test**: "If someone said AI made this, would they believe it immediately?"
### Step 2: First Impressions
One paragraph on gut reaction. Be honest and direct. This is the "noticing" step — seeing what's actually there, not what you expect.
### Step 3: Visual Design
| Dimension | What to Look For |
|-----------|-----------------|
| Color intentionality | Every color purposeful? Too many competing backgrounds/accents? |
| Typographic hierarchy | Clear scale from most to least important? Count distinct sizes/weights |
| Shadow & stroke quality | Crisp or muddy? Borders competing with content? |
| Visual weight vs importance | Heaviest elements = most important? Decorative stealing attention? |
| Spacing & alignment | Consistent? Clear grid? Excess padding? |
| Icon consistency | Same family, weight, stroke width, optical size? |
For each issue:
> **[Issue name]** — [Specific factual observation]. [Impact on user]. [What it could be instead.]
Be precise. Count things. Quote text. Name colors. Measure relative sizes.
### Step 4: Interface Design
| Dimension | What to Look For |
|-----------|-----------------|
| Focusing mechanism | Clear where to look first? Visual entry point? |
| Progressive disclosure | Complexity revealed gradually? 40 things when 5 would suffice? |
| Information density | Appropriate for context? |
| Expectation setting | User knows what happens next? Progress communicated? |
| Feedback & reward | Actions acknowledged? Completed items celebrated? |
| Redundancy | Labels repeating known information? Can anything be removed? |
Frame as missed opportunities:
> "We're missing an opportunity to [reward progress / reduce cognitive load / etc.]"
### Step 5: Consistency & Conventions
| Dimension | What to Look For |
|-----------|-----------------|
| Pattern consistency | Similar actions handled the same way? |
| Platform conventions | Follows established patterns? Deviations intentional? |
| Component reuse | Elements that should be same component but aren't? |
| Visual language cohesion | Feels like one designer or assembled from different kits? |
### Step 6: User Context
- **How does this make the user feel?** Name the emotion.
- **User's likely state of mind?** Anxious? Focused? Under pressure?
- **Does the interface respect that state?**
- **What would "uncommon care" look like here?**
---
## Evidence Gate
A finding only counts once it clears three bars. Anything missing one of these is a **candidate**, not a finding — drop it or keep investigating, but don't write it into the report as-is.
1. **Cite the rule.** Name the specific rule or decision the issue violates — a line from this file, from `brief.md`, from the token spine, from an explicit accessibility requirement. "This feels off" is not a citation.
2. **Prove it reaches the rendered surface.** Trace the real path — file:line through the actual render, import, or token-resolution chain — that gets you from the code you're pointing at to the pixels the user sees. Two components sharing a name, or a token that merely looks related, is not proof; grep proximity is not proof. If you can't walk the chain, you don't have a finding yet.
3. **One concrete fix.** Exactly one, specific enough to apply directly — not a menu of options, not "consider revisiting."
**Falsification pass.** Before the report goes out, re-open the evidence for every candidate and actively try to disprove it — check whether the cited rule actually applies in this context, whether the traced path really executes, whether the fix could break something the candidate didn't account for. Only candidates that survive an honest attempt to kill them get written up.
**Signal cap.** Rank surviving findings by impact × confidence and report the ones that matter — roughly the top 3-5, not an exhaustive list. Say explicitly what else was observed and cut, so the reader knows the noise was filtered, not missed. A short, high-confidence report beats an exhaustive, noisy one.
**Scope discipline.** A review or audit pass fixes what it found — it does not refactor unrelated code, swap libraries or frameworks, or add ARIA and abstraction layers where native semantics already solve the problem. The smallest change that resolves the finding is the correct one.
---
## Craft Report
Every review, polish, audit, or refine pass ends with a Craft Report — a receipt of the run. This is non-negotiable: **a pass that changes nothing still produces the report.** "Everything checked out" is a finding, and the most important one to surface — a silent no-op reads as the tool doing nothing, while a receipt turns the same no-op into validation the user can act on.
Sections may be omitted when empty, except **Checked** and **Verdict** — those two always appear, even on a pure pass.
- **Checked** — which dimensions or rule sets actually ran (anti-slop scan, hierarchy, type, color/contrast, motion, a11y, states…) and over what scope (which files or surfaces). Never imply coverage broader than what actually ran.
- **Passed** — what was inspected and found sound, with a word on the evidence behind it (e.g. "focus states present on all interactive elements in nav + forms," not "looks fine"). This is the payload when little or nothing changed — treat it with the same rigor as a finding, not as filler.
- **Changed** — each change, one line, with a file reference and the rule or decision it serves. Every entry here already cleared the Evidence Gate above — no changes ship without a citation and a fix.
- **Left alone** — deliberate non-changes: things that look unusual but are intentional or brand, things technically correct so untouched, things out of scope or that need a user decision. One line each with the why — this is what separates "didn't notice" from "noticed, chose not to touch."
- **Verdict** — one sentence: overall state, plus the single highest-leverage next step if one exists (or "ship it" if none does).
Keep it honest and short. The report reflects what this run actually inspected — no padding, no invented coverage, no claiming a dimension was checked because it usually is.
---
## Comprehensive Audit Checklist
### Animation Audit
- [ ] No `transition: all` — properties listed explicitly
- [ ] No `scale(0)` entry — starts from `scale(0.95)` with `opacity: 0`
- [ ] No `ease-in` on UI elements — use `ease-out` or custom curve
- [ ] No `transform-origin: center` on popovers — set to trigger location (modals exempt)
- [ ] No animation on keyboard actions
- [ ] No duration > 300ms on UI elements
- [ ] No hover animation without `@media (hover: hover) and (pointer: fine)`
- [ ] No keyframes on rapidly-triggered elements — use CSS transitions
- [ ] Animation library uses compositor-promoted properties under load
- [ ] Exit faster than enter
- [ ] Stagger delays ≤ 50ms per item
- [ ] `prefers-reduced-motion` respected
- [ ] No entrance animation on first paint for default-state elements (`initial={false}` on `AnimatePresence`)
### Interaction Audit
Full checklist lives in `accessibility.md` (**Quick Checklist** + **Forms**). Fast pass:
- [ ] Full keyboard navigation (WAI-ARIA APG)
- [ ] Visible focus rings (`:focus-visible`)
- [ ] Hit targets ≥ 24px (44px mobile)
- [ ] URL reflects state (filters, tabs, pagination)
- [ ] Links use `<a>`/`<Link>`, not `<div onClick>`
- [ ] Destructive actions confirmed or Undo provided
- [ ] Loading buttons show spinner + keep label
- [ ] Forms: paste not blocked, errors inline, autocomplete set
- [ ] `overscroll-behavior: contain` in modals/drawers
- [ ] `touch-action: manipulation` on controls
### Layout Audit
- [ ] Optical alignment (±1px adjustments where needed)
- [ ] Deliberate grid/baseline/edge alignment
- [ ] Responsive: mobile, laptop, ultra-wide verified
- [ ] Safe areas respected
- [ ] No unwanted scrollbars
- [ ] Flex children have `min-w-0` for truncation
- [ ] Text containers handle long content
- [ ] Empty states handled
### Design Audit
- [ ] Layered shadows (ambient + direct)
- [ ] Nested radii: child ≤ parent
- [ ] Hue-consistent borders/shadows on colored backgrounds
- [ ] APCA contrast met
- [ ] Interactions increase contrast
- [ ] `color-scheme: dark` on html in dark themes
- [ ] `theme-color` matches background
- [ ] `tabular-nums` for number comparisons
- [ ] `text-wrap: balance` on headings
- [ ] Images have inset outline `black/10` (light) / `white/10` (dark) — never tinted
- [ ] `-webkit-font-smoothing: antialiased` on root (macOS)
### Performance Audit
- [ ] Only compositor props animated (`transform`, `opacity`)
- [ ] No layout thrashing (interleaved reads/writes)
- [ ] Large lists virtualized (>50 items)
- [ ] Images: preload above-fold, lazy rest, explicit dimensions
- [ ] Re-renders minimized
- [ ] CSS variables not animated on deep trees
- [ ] `will-change` used temporarily and surgically
- [ ] Blur ≤ 8px, never continuous, never on large surfaces
---
## Output Format for Critiques
```
## Context
[1-2 sentences]
## Anti-Slop Verdict
[Pass/fail with specific tells]
## First Impressions
[1 paragraph, direct]
## Visual Design
[Issues as: **Issue Name** — observation. Impact. Opportunity.]
## Interface Design
[Issues framed as missed opportunities]
## Consistency & Conventions
[Pattern issues]
## User Context
[Empathy-driven observations]
## Top Opportunities
[Ranked 3-5 highest-impact changes, one sentence each]
```
---
## Voice Rules
### BE:
- **Specific** — "There are six columns per row" not "a lot of data"
- **Decisive** — "This is overwhelming" not "might feel overwhelming"
- **Factual first** — State what you see before judging
- **Impact-aware** — Connect observation to user impact
- **Constructive** — Every problem paired with opportunity
- **Quantitative** — Count elements, name colors, measure sizes
### DO NOT:
- Hedge ("maybe", "perhaps")
- Apologize ("unfortunately")
- Be vague ("feels off" without specifics)
- Add praise padding (no manufactured positivity)
- Prescribe without reasoning
### Severity Priority
1. **Structural** — information architecture, mental model, missing functionality
2. **Behavioral** — response, flow, communication
3. **Visual** — color, type, spacing, shadows
---
## Polish Pass (Compound Details)
The difference between "correct" and "crafted" is 20 small things done right. Run this checklist after the UI works (required when CRAFT_LEVEL is 8+):
- **Brand names use `&nbsp;`** — prevent "UI" on one line and "Craft" on the next
- **Confirmation feedback is visual, not just color** — swap icon to checkmark, not just green tint
- **One visual anchor per text-heavy page** — a code block, screenshot, or diagram breaks monotony
- **Headings sit closer to their content than to the previous section** — unequal spacing creates grouping
- **Footer earns its space or disappears** — generic "Built by Name" adds nothing
- **Test on mobile before declaring done** — drag interactions, overflow, label overlap, touch targets
- **Secondary actions don't compete with primary** — outline/ghost for secondary, solid for primary
- **Data-heavy content uses monospace or tabular-nums** — even in casual contexts
- **Images inside interactive containers can't be natively dragged** — kills slider/carousel UX
- **Every `<section>` on a landing page answers one question** — if it answers two, split it
---
## Common Issues (What We See in Real Projects)
When reviewing or polishing existing UI, these are the most frequent problems:
| Issue | How to spot it | Fix |
|-------|---------------|-----|
| Everything is the same shade of gray | Squint test — no visual hierarchy | Darken headings to 900, lighten secondary to 500, add one accent |
| Cards all look identical | 4+ cards with same border, radius, shadow | Differentiate primary card, vary content types, break the grid |
| Hover states missing or default | Buttons/cards don't respond to cursor | Add translateY(-1px) + shadow on cards, bg darken on buttons |
| Spacing is uniform everywhere | Same gap between all sections | Vary: tighter within groups, looser between sections |
| No loading/empty/error states | Only the happy path is designed | Add skeleton, empty state with CTA, inline errors |
---
## Component Craft
- **Interactive elements have three states minimum** — rest, hover/focus, active/pressed. If it looks the same when you click it, it feels broken.
- **Button hierarchy guides action** — primary = solid, secondary = outline/ghost, destructive = red outline. Only ONE primary per view section.
- **Feedback must be visual, not just color** — swap icon to checkmark, show inline confirmation, animate the state change. Color alone fails colorblind users.
- **Metric cards vary treatment** — if 3+ cards share structure, differentiate the primary (heavier weight, slightly larger number, subtle bg tint — never thick colored borders).
- **Price typography splits weight** — dollar sign lighter/smaller than the number, period label ("/month") in secondary color.
- **Prevent native image drag** on interactive overlays — `user-drag: none; -webkit-user-drag: none; pointer-events: none` on images inside sliders/carousels.
- **Placeholders are visual, not text** — skeleton bars, subtle grid lines, muted pattern. "Chart would render here" looks unfinished.
**Hover & interaction micro-details** (gate behind `@media (hover: hover) and (pointer: fine)`):
- Cards: `transform: translateY(-1px)` + shadow increase — `transition: transform 200ms ease-out, box-shadow 200ms ease-out` / Tailwind: `hover:-translate-y-px hover:shadow-md transition-[transform,box-shadow] duration-200`
- Buttons: slight bg darkening, not a full color swap — `transition: background 150ms ease-out` / Tailwind: `hover:bg-accent/90 transition-colors duration-150`
- Table rows: subtle bg — `hover:bg-gray-50` / Tailwind: `hover:bg-slate-50/50`
- Links: `text-underline-offset: 2px; text-decoration-skip-ink: auto` / Tailwind: `underline-offset-2 decoration-skip-ink-auto`
- Active/pressed: `transform: scale(0.98)` on buttons for tactile feedback
+150
View File
@@ -0,0 +1,150 @@
# Sound Design for UI
Web Audio API best practices, UI sound appropriateness, and implementation.
---
## When Sound is Appropriate
| Interaction | Sound? | Reason |
|-------------|--------|--------|
| Payment success | Yes | Significant confirmation |
| Form submission | Yes | User needs assurance |
| Error state | Yes | Can't be overlooked |
| Notification | Yes | User may not be looking |
| Significant button | Maybe | Only for important actions |
| Typing | **No** | Too frequent |
| Hover | **No** | Decorative only |
| Scroll | **No** | Too frequent |
| Navigation | **No** | Keyboard nav would be noisy |
---
## Accessibility Rules
- Every audio cue MUST have a visual equivalent — sound never replaces visual feedback
- Provide explicit toggle to disable sounds in settings
- Respect `prefers-reduced-motion` as a default proxy for sound sensitivity, AND provide an explicit independent sound toggle. **Why caveat:** the proxy is imperfect — some users tolerate visual motion but not audio (vestibular conditions, hearing aids), some users tolerate audio but not motion. Always offer a sound toggle separate from motion preference; reduced-motion is the default-off, not the only signal.
- Allow volume adjustment independent of system volume
- Never add sound to high-frequency interactions
- Sound should inform, not punish — avoid harsh sounds for mistakes
---
## Web Audio API Implementation
### Single AudioContext
Single AudioContext per page. Never create new AudioContext per sound. **Why critical:** browsers limit AudioContext instances (typically 6 per page); exhausting them silently fails new sounds. A shared AudioContext also synchronizes timing across rapid-fire UI feedback (typing keystrokes, button clicks within 50ms) — independent contexts desync.
```ts
let audioContext: AudioContext | null = null;
function getAudioContext(): AudioContext {
if (!audioContext) audioContext = new AudioContext();
return audioContext;
}
```
### Resume Suspended Context
```ts
if (ctx.state === "suspended") ctx.resume();
```
### Clean Up Nodes
```ts
source.onended = () => { source.disconnect(); gain.disconnect(); };
```
---
## Sound Design Patterns
### Clicks/Taps (Percussion) — Use Filtered Noise
```ts
const buffer = ctx.createBuffer(1, ctx.sampleRate * 0.008, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / 50);
}
const filter = ctx.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.value = 4000; // 3000-6000Hz range
filter.Q.value = 3; // 2-5 for focused but not harsh
source.connect(filter).connect(gain).connect(ctx.destination);
```
### Confirmations/Pops (Tonal) — Use Oscillators
```ts
const osc = ctx.createOscillator();
osc.frequency.setValueAtTime(400, t);
osc.frequency.exponentialRampToValueAtTime(600, t + 0.04);
```
### Envelope
Use exponential decay (not linear) for sound envelopes. **Why:** linear decay produces an audible click as the envelope hits zero — the abrupt stop creates a high-frequency artifact at the cutoff. Exponential decay mimics natural acoustic decay (room reverberation, string damping) — it reaches zero asymptotically, no click.
```ts
gain.gain.setValueAtTime(0.3, t); // set initial value first
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.05); // never target 0
```
---
## Parameters
| Parameter | Range | Notes |
|-----------|-------|-------|
| Click duration | 5-15ms | Quick and percussive |
| Filter frequency | 3000-6000Hz | For click sounds |
| Filter Q | 2-5 | Focused but not harsh |
| Gain | ≤ 1.0 | Prevent clipping |
| Default volume | 0.3 | Subtle, not loud |
| "too harsh" | → lower filter frequency, reduce Q | |
| "too muffled" | → higher filter frequency | |
| "too long" | → shorter duration, faster decay | |
| "cuts off" | → use exponential decay | |
| "more mechanical" | → higher Q, faster decay | |
| "softer" | → lower gain, triangle wave | |
---
## Weight Matching
Sound weight should match action importance:
- **Subtle click** for toggle switches
- **Soft pop** for adding items
- **Rich confirmation** for purchases/payments
- **Brief alert** for errors
Sound duration should match action duration:
- **50ms** for clicks/taps
- **100-200ms** for confirmations
- **Longer** for uploads/completions
---
## Preloading
```ts
const sounds = {
success: new Audio("/sounds/success.mp3"),
error: new Audio("/sounds/error.mp3"),
};
Object.values(sounds).forEach(audio => audio.load());
// Reset before replay for rapid triggering
function play(sound: HTMLAudioElement) {
sound.currentTime = 0;
sound.play();
}
```
---
## Cross-Refs
- `accessibility.md` — the full reduced-motion contract; `prefers-reduced-motion` as a sound-sensitivity proxy lives there.
- `motion.md` — sound duration should match the duration of the action it accompanies, the same pairing rule as interaction timing.
- `state-design.md` — sound cues for state transitions (error, success) must never be the only signal; pair with the visual state.
+123
View File
@@ -0,0 +1,123 @@
# Composition Spec
Defines `.ui-craft/spec.md` — the durable "what" between brief (why) and build (how). Created by `/shape` Step 6, consumed by `/sddesign` and `/craft`. Sits beside `brief.md` in `.ui-craft/` and is committed to the repo.
---
## The Spec Format
One file. Per-surface `## Surface: <name>` sections. Append-mostly — new surfaces add a new section; existing sections are only edited when a composition decision changes. No JSX, no component code — this is a design document, not implementation.
Multiple surfaces coexist in a single file, just as a brief covers one project with multiple concerns in one place.
---
## Per-Surface Template
Copy this block for each new surface. Fill every subsection — leave none empty or the acceptance bar is undefined.
```markdown
## Surface: <name>
### Composition
**Recipe:** `recipe-<type>.md` → <Variant name>
**Why:** <one line citing the brief principle or constraint that drove this choice>
### Layout skeleton
Desktop
┌──────────────────────────────────────────────┐
│ [region] [region] [region] │
├──────────────────────────────────────────────┤
│ ┌────────────────┐ ┌───────────────────┐ │
│ │ <content area> │ │ <content area> │ │
│ └────────────────┘ └───────────────────┘ │
└──────────────────────────────────────────────┘
Mobile
┌──────────────────┐
│ [region] [☰] │
├──────────────────┤
│ <content area> │
└──────────────────┘
### Component inventory
| Region | Component | Contract ref |
|--------|-----------|--------------|
| header | Nav bar | components.md → Nav bar |
| hero | Headline + CTA | components.md → Buttons |
| … | … | … |
### State lattice
| State | Required / Optional / N-A | Notes |
|-------|--------------------------|-------|
| idle | Required | default, data present |
| loading | Required | skeletons mirror final layout |
| empty | Required | first-run or no data |
| error | Required | cause + recovery action |
| partial | Optional — only if widgets can fail independently | |
| conflict | N-A | no collaborative editing on this surface |
| offline | Optional — only if writes are queued | |
| success | Required | confirmation after primary action |
See [state-design.md](state-design.md) for state contracts and CRAFT_LEVEL thresholds.
### Acceptance bar
Drawn from the recipe's acceptance criteria. All items must be green before build reports done.
- [ ] <recipe checklist item 1>
- [ ] <recipe checklist item 2>
- [ ] <recipe checklist item 3>
```
---
## How the Spec Gets Used
**Discovery phase** — read `.ui-craft/spec.md` after `brief.md`. If the surface already has a spec section, it is the composition ground truth; do not re-derive from scratch.
**Build phase** — `/craft <surface>` builds against the acceptance bar in the surface's spec section. Every item in the acceptance bar must be green before the build reports done.
**Finalize phase** — `/finalize` verifies against the acceptance bar and the brief's learned constraints (§6). The acceptance bar is the surface-level definition of done; the brief's §6 constraints are the project-level floor.
---
## When to Update
Update a spec section when any of these change:
- **Composition changes** — different recipe variant selected.
- **New surface** — append a new `## Surface: <name>` section. Never overwrite an existing section.
- **Recipe swap** — the recipe file changed and the acceptance bar items shift.
- **Learned constraint overrides a composition choice** — note it in the Composition subsection, citing the brief §6 entry.
Append-mostly. When deprecating a composition choice within a surface, mark the old entry as:
```markdown
~~**Recipe:** [recipe-landing.md](recipe-landing.md) → Product-forward~~ — deprecated 2026-06-24, swapped to Message-forward per brief §6 constraint (no hero visuals).
```
Never delete past composition records — the spec is a log, not a config file.
---
## File Location and Git
Commit `.ui-craft/spec.md` to the repo. It belongs in version control alongside the code it governs — losing it between sessions means losing the composition rationale.
The `.ui-craft/` directory is the project's design decision store:
```
.ui-craft/
├── brief.md # why — principles, user, success metric
├── spec.md # what — chosen composition, layout, inventory, acceptance bar
└── (tokens live in the project's own token files)
```
`brief.md` and `spec.md` are companions: the brief anchors principles; the spec records how those principles translated into a specific surface composition. When they conflict, the brief wins — a spec choice that violates a brief §6 constraint must be updated to comply.
See `../SKILL.md` for routing and when to run `/sddesign` vs `/craft`.
+522
View File
@@ -0,0 +1,522 @@
# Stack: Motion / GSAP / Three.js
Load this reference **only when the user opts in during Discovery**. Every rule in `motion.md` still applies — this file covers the library-specific how.
## Decision tree — which library?
| Need | Pick |
|------|------|
| Single hover, fade, or enter | **CSS transition / `@keyframes`** (0KB, always wins) |
| Modern CSS scroll-linked reveal | **`animation-timeline: view()`** before any JS library |
| React layout/exit animations, gestures, scroll progress tied to state | **Motion** |
| Complex timelines with labels/overlaps, ScrollTrigger pin+scrub, SplitText, SVG paths, non-React | **GSAP** |
| 3D scenes, shaders, product viewers, particle fields | **Three.js + R3F** |
**Never install two animation libraries that animate the same property** — they will fight and one will silently lose. Pick one per surface.
---
## Motion (framer-motion)
Package renamed to `motion` on npm (legacy `framer-motion` still works).
### When to reach for it
- React component needs **exit** animation — `AnimatePresence` is the only clean way to animate unmounts.
- **Shared element transition** (tabs underline, modal-from-card) — `layoutId`.
- **Scroll-linked** progress bars, parallax tied to component state — `useScroll` + `useTransform`.
- **Gesture-driven** interactions (drag, pan).
### When NOT
- A single hover/fade/enter — CSS wins, no library needed.
- A one-off `@keyframes` spinner or loader.
- Non-React surfaces — use GSAP.
### Install
```bash
npm install motion
```
```tsx
import {
motion,
AnimatePresence,
useScroll,
useTransform,
useReducedMotion,
MotionConfig,
LazyMotion,
domAnimation,
} from "motion/react"
```
### Top patterns
**1. Exit animation with `AnimatePresence`**
```tsx
<AnimatePresence>
{open && (
<motion.div
key="modal"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
/>
)}
</AnimatePresence>
```
Every child needs a unique `key`. Without `AnimatePresence`, `exit` is silently ignored.
**2. Shared element transition with `layoutId`**
```tsx
{tabs.map((tab) => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{active === tab.id && (
<motion.div layoutId="tab-underline" className="underline" />
)}
</button>
))}
```
Animates position between elements sharing the same `layoutId`. FLIP-based, GPU-safe.
**3. Scroll progress (smoothed with `useSpring`)**
```tsx
const { scrollYProgress } = useScroll()
const smooth = useSpring(scrollYProgress, { stiffness: 120, damping: 30, mass: 0.3 })
return (
<motion.div
style={{ scaleX: smooth, transformOrigin: "0%" }}
className="fixed top-0 inset-x-0 h-[2px] bg-foreground"
/>
)
```
Always bind scroll-linked values via `style`, **never** via `animate` (would trigger React renders per frame). `useSpring` smooths without a RAF loop.
**3b. Viewport-triggered reveal**
```tsx
<motion.section
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
/>
```
`once: true` prevents re-triggering on scroll back. Negative `margin` fires before the element is fully in view — feels natural, not delayed.
**4. Spring vs tween — pick ONE per project**
```tsx
// Spring (Motion default — physical, no fixed duration)
transition={{ type: "spring", bounce: 0.25, visualDuration: 0.3 }}
// Tween (deterministic, matches our 200–300ms rule)
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
```
Mixing both within the same app reads as inconsistent. Document the choice.
**5. Reduced-motion, once for the whole app**
```tsx
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
```
Or per-component with `useReducedMotion()` and branch the animation.
### Clashes with ui-craft rules
- Motion defaults to spring (good) but specs often call `{ duration, ease }`. **Pick lane per project**, don't mix.
- Springs have no fixed duration → our 200–300ms budget doesn't apply directly; constrain via `bounce ≤ 0.25` and `visualDuration`.
- `layout` animates via `transform` only → matches "GPU-accelerated transforms only".
- `whileHover={{ scale: 1.05 }}` on clickable cards feels toy-like — keep ≤ 1.02 for functional UI.
### Perf gotchas
- Full `motion` import ≈ 50KB gz. On marketing pages use **`LazyMotion` + `domAnimation`** for code-split:
```tsx
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
```
- `layout` without stable `key`/`layoutId` on list items → every item reflows on any change. Use `layoutId` or `LayoutGroup`.
- Animating `width`/`height`/`top` triggers layout. Use `scale`, `x`, `y`.
- `useScroll({ container })` with `trackContentSize: true` has cost — only enable when content genuinely resizes.
- **Next.js App Router**: any file using `motion` hooks (`useScroll`, `useTransform`, `useSpring`, `useReducedMotion`) must be `'use client'`. Silent hydration mismatches otherwise.
- Inline `style={{ … }}` objects on `motion.div` create a new object every render → memoization busted. Hoist or `useMemo`.
- **Motion v12 animates `oklch()` / `oklab()` / `color-mix()` directly** — useful for dynamic theming. Example: `animate={{ backgroundColor: "oklch(70% 0.15 250)" }}`. Hardware-accelerated, no RGB interpolation artifacts.
### Anti-patterns (flag in review)
- `motion.div` wrapping every element "just in case" — each one adds an observer.
- `exit` prop without `AnimatePresence` — silently does nothing.
- `whileHover={{ scale: 1.08 }}` on buttons — fine for cards, too much for CTAs.
- `layout` on virtualized lists — kills FPS.
- `transition={{ duration: 2 }}` on functional UI — 300ms ceiling; longer only for decorative.
- Binding `scrollYProgress` to `animate={{ … }}` instead of `style={{ … }}` — one causes per-frame React renders, the other runs on the compositor.
- Permanent `style={{ willChange: "transform" }}` on motion elements — leaks compositor layers. Only just-in-time, via `whileHover` / `whileTap` or transient state.
---
## GSAP
All GSAP plugins (including ScrollTrigger, SplitText, MorphSVG) are MIT since late 2024.
### When to reach for it
- **Timelines with labels/overlaps** — sequence shots, re-use, rewind.
- **ScrollTrigger** — pin sections, scrub cinematic scenes, snap to waypoints.
- **SplitText** — per-line/word/char reveal on headlines.
- **SVG morphing/drawing** — `MorphSVG`, `DrawSVG`.
- **Non-React surfaces** — vanilla, canvas/WebGL tweening.
### When NOT
- A hover state or enter animation in React — Motion is more ergonomic.
- Anything CSS `animation-timeline: view()` can do natively in evergreens.
- Text splitting on body copy — huge DOM cost + a11y risk. Headlines only.
### Install
```bash
npm install gsap @gsap/react
```
```ts
import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import { SplitText } from "gsap/SplitText"
import { useGSAP } from "@gsap/react"
gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP)
```
Always `registerPlugin()` — in production bundlers will otherwise tree-shake plugins to nothing (silent failure).
### Top patterns
**1. `useGSAP` hook (React) — auto-cleanup, StrictMode safe**
```tsx
const ref = useRef<HTMLDivElement>(null)
useGSAP(
() => {
gsap.from(".card", { y: 40, opacity: 0, stagger: 0.08, duration: 0.3 })
},
{ scope: ref }
)
return <div ref={ref}>…</div>
```
Wraps `gsap.context()` so everything reverts on unmount. Never use raw `useEffect` for GSAP in React.
**2. Timeline with labels**
```ts
const tl = gsap.timeline({
defaults: { ease: "power2.out", duration: 0.3 },
})
tl.from(".title", { y: 40, opacity: 0 })
.from(".sub", { opacity: 0 }, "-=0.2") // overlap 200ms
.addLabel("hero-done")
```
Overlap with relative offsets keeps sequences tight.
**3. ScrollTrigger pin + scrub**
```ts
gsap.to(".panel", {
xPercent: -300,
scrollTrigger: {
trigger: ".panels",
pin: true,
scrub: 1, // smooth with 1s catch-up, never `true`
end: "+=2000",
invalidateOnRefresh: true,
},
})
```
Use `scrub: 1` (or higher), not `true` — `true` welds the animation to the wheel and jitters.
**3a. Sticky-stack (cards pin and stack on scroll) — canonical skeleton**
The #1 sticky-stack bug: the trigger fires halfway down the viewport instead of pinning at the top. The fix is always `start: "top top"`.
```tsx
useGSAP(() => {
const cards = gsap.utils.toArray<HTMLElement>(".stack-card")
cards.forEach((card, i) => {
if (i === cards.length - 1) return
ScrollTrigger.create({
trigger: card,
start: "top top", // pin at viewport top — never "top center"
endTrigger: cards[cards.length - 1],
end: "top top",
pin: true,
pinSpacing: false,
})
// previous card recedes as the NEXT card arrives
gsap.to(card, {
scale: 0.94, autoAlpha: 0.6, ease: "none",
scrollTrigger: { trigger: cards[i + 1], start: "top bottom", end: "top top", scrub: 1 },
})
})
}, { scope: ref })
```
Every card except the last is pinned; the recede tween is driven by the *next* card's trigger. Cards themselves are `min-height: 100dvh` flex-centered wrappers.
**3b. Horizontal pan (vertical scroll drives horizontal travel) — canonical skeleton**
The #1 horizontal-pan bug: the pan starts before the section is pinned, so the user sees half a slide. Same fix: `start: "top top"`, pin the wrapper, scrub the inner track.
```tsx
useGSAP(() => {
const distance = track.current!.scrollWidth - window.innerWidth
gsap.to(track.current, {
x: -distance, ease: "none",
scrollTrigger: {
trigger: wrap.current,
start: "top top",
end: () => `+=${distance}`, // scroll length = horizontal travel
pin: true,
scrub: 1,
invalidateOnRefresh: true,
},
})
}, { scope: wrap })
```
Wrapper is `overflow: hidden`; the track is a `100dvh` flex row. Both skeletons collapse to plain stacked sections under `prefers-reduced-motion` (wrap in `gsap.matchMedia`, pattern 5 below).
**4. SplitText (headlines only)**
```ts
const split = SplitText.create(".headline", { type: "lines,words" })
gsap.from(split.words, {
y: 24,
opacity: 0,
stagger: 0.02,
duration: 0.4,
ease: "power3.out",
})
```
Revert with `split.revert()` in cleanup. `@gsap/react` handles it if scoped.
**5. `matchMedia` for responsive + reduced-motion**
```ts
const mm = gsap.matchMedia()
mm.add("(prefers-reduced-motion: no-preference)", () => {
// animations here
})
mm.add("(min-width: 768px)", () => {
// desktop-only pins
})
```
This is the only sane way to handle reduced-motion and breakpoints together in GSAP.
**6. `quickTo` for high-frequency updates (mouse follower, pointer-linked)**
```ts
const xTo = gsap.quickTo("#dot", "x", { duration: 0.4, ease: "power3" })
const yTo = gsap.quickTo("#dot", "y", { duration: 0.4, ease: "power3" })
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY) })
```
Reuses one tween instead of creating a new tween per event — crucial for pointer/scroll handlers that fire 60+ times/sec. Never `gsap.to()` inside a frequent event handler.
**7. `autoAlpha` over `opacity`**
```ts
gsap.to(".menu", { autoAlpha: 0, duration: 0.2 })
```
`autoAlpha` animates opacity **and** toggles `visibility: hidden` at 0 — hidden elements stop catching pointer events and get skipped by screen readers. `opacity: 0` alone does neither.
### Clashes with ui-craft rules
- Default ease is `power1.out` — flat. For UI use `power2.out` or `power3.out`; for springiness `back.out(1.4)` sparingly. **Avoid `elastic` / `bounce` on functional UI** — reads as demo.
- Default `duration: 0.5` — trim to **0.2–0.3 for UI**; keep 0.5–1.2 for hero scroll scenes only.
- GSAP animates any property — easy to accidentally animate `left` / `top` / `width` (layout thrash). Stick to `x`, `y`, `scale`, `rotation`, `opacity`, `autoAlpha`.
- No default reduced-motion — wrap in `gsap.matchMedia` or early-return on `matchMedia("(prefers-reduced-motion: reduce)").matches`.
### Perf gotchas
- Core ≈ 25KB gz, +7KB ScrollTrigger, +11KB SplitText. Import named plugins only.
- Forgetting `registerPlugin()` → tree-shaken in production → silent failure.
- Timelines not killed on unmount (outside `useGSAP`) → orphan RAF ticks. Always `ctx.revert()` in cleanup.
- More than ~50 independent `ScrollTrigger`s with `scrub` → RAF stalls. Batch into one timeline.
- **`immediateRender` trap**: a second `from()` / `fromTo()` targeting the same property on the same element will snap to its start value immediately, overwriting earlier tweens. Pass `immediateRender: false` on all but the first.
- `ScrollTrigger.refresh()` is expensive — don't call it on every `resize`. Debounce, or only on orientation change / content load. `invalidateOnRefresh: true` on the trigger handles most recalcs automatically.
- Use `clearProps: "all"` at the end of a tween to remove the inline styles GSAP wrote, preventing conflicts with CSS states (hover, `:active`) afterwards.
- **Pause animations off-screen with `IntersectionObserver`**. On long pages with multiple timelines, idle sections still burn RAF cycles. Pause them:
```ts
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) tl.play()
else tl.pause()
})
io.observe(container)
```
Or use ScrollTrigger's `toggleActions: "play pause resume pause"` which handles this natively.
### Anti-patterns
- Reaching for GSAP for what one `transition: transform 200ms ease-out` would do.
- Pinning every section — "scroll jacking". Pin 1-2 hero sections, max.
- `scrub: true` — jittery. Use `scrub: 1`.
- SplitText on body copy — huge DOM cost, a11y nightmare. Headlines only.
- Mixing `gsap.to` with Motion or CSS transitions on the same property — they fight.
---
## Three.js + React Three Fiber
R3F v9 pairs with React 19; v8 with React 18. `drei` is the swiss-army helper set — always install alongside.
### When to reach for it
- **3D product viewers** the user rotates.
- **Interactive hero scenes** where the scene reacts to scroll or cursor.
- **Data viz in 3D** (rare — prefer 2D unless the extra dimension is load-bearing).
- **Shaders** tied to React state.
### When NOT
- A marketing page that could be a **WebP/MP4** — users on phones pay 400KB + GPU for nothing.
- Static logo rotation — CSS 3D transforms.
- Decorative 2D canvas — `<canvas>` + Motion.
### Install
```bash
npm install three @react-three/fiber @react-three/drei
```
```tsx
import { Canvas, useFrame, useThree } from "@react-three/fiber"
import {
Environment,
OrbitControls,
useGLTF,
PerformanceMonitor,
} from "@react-three/drei"
```
### Top patterns
**1. Canvas with perf caps (always)**
```tsx
<Canvas
dpr={[1, 2]} // cap retina cost
frameloop="demand" // render only when props change
camera={{ position: [0, 0, 5], fov: 45 }}
gl={{ antialias: true, powerPreference: "high-performance" }}
>
<ambientLight intensity={0.5} />
<Environment preset="city" />
<Model />
</Canvas>
```
`dpr={[1, 2]}` + `frameloop="demand"` is the responsible default. Most "heavy" R3F demos are heavy because they omit these.
**2. `useFrame` with delta, never fixed increments**
```tsx
useFrame((_, dt) => {
ref.current.rotation.y += dt * 0.5 // speed in rad/sec, framerate-independent
})
```
Never `+= 0.01` — ties speed to framerate (feels fast on 120Hz, slow on 60Hz).
**3. `<Environment />` + `<Lightformer />` for lighting**
```tsx
<Environment preset="studio" />
```
Presets (`studio`, `city`, `sunset`, `warehouse`, `apartment`) cover 90% of cases. Beats five hand-placed lights.
**4. Instancing for repeats**
```tsx
<instancedMesh args={[null, null, 10000]}>
<boxGeometry args={[0.1, 0.1, 0.1]} />
<meshStandardMaterial />
</instancedMesh>
```
One draw call for 10k objects. Use when the same mesh repeats more than ~50 times.
**5. Auto-degrade with `PerformanceMonitor` (drei)**
```tsx
const [dpr, setDpr] = useState(1.5)
return (
<Canvas dpr={dpr}>
<PerformanceMonitor
onDecline={() => setDpr(1)}
onIncline={() => setDpr(2)}
/>
…
</Canvas>
)
```
Drops `dpr` automatically on low FPS. Ship this on any production scene.
**6. `<Suspense>` with nested progressive loading**
```tsx
<Canvas>
<Suspense fallback={<Model url="/model-low.glb" />}>
<Model url="/model-high.glb" />
</Suspense>
</Canvas>
```
Every `useLoader` / `useGLTF` / `useTexture` **must** live under a Suspense boundary. Nesting lets you show a low-res placeholder while the high-res loads, then swap — zero blank frames.
**6b. DOM overlays inside the scene with `<Html>` (drei)**
```tsx
import { Html } from "@react-three/drei"
<mesh position={[0, 1, 0]}>
<Html distanceFactor={8} occlude="blending">
<div className="rounded-md bg-background/90 px-2 py-1 text-xs">Label</div>
</Html>
</mesh>
```
`distanceFactor` scales with camera distance (so labels don't balloon on zoom). `occlude` hides the overlay when geometry blocks it — respects depth without feeling cheap.
**6c. Axis-specific prop notation**
```tsx
// Instead of <mesh position={[x, 0, 0]}> on every render:
<mesh position-x={x} />
```
Avoids recreating the `[x, y, z]` tuple each frame and reads cleaner when animating a single axis via state/ref.
**7. Auto-fit camera with `<Bounds>` + `<Center>`**
```tsx
import { Bounds, Center } from "@react-three/drei"
<Canvas>
<Bounds fit clip observe margin={1.2}>
<Center><Model /></Center>
</Bounds>
</Canvas>
```
Stops the "model invisible because the camera is inside it" footgun. `observe` re-fits on model change.
### Clashes with ui-craft rules
- Three.js runs **its own RAF** — independent of CSS `prefers-reduced-motion`. Read it in React and cut autorotate / camera drift:
```tsx
const reduced = useReducedMotion()
useFrame((_, dt) => {
if (!reduced) ref.current.rotation.y += dt * 0.2
})
```
- Heavy lighting/shadows violate the spirit of "GPU-accelerated transforms only" by burning GPU on everything. Budget draw calls and shadow resolution.
- Default `Canvas` runs 60fps forever — battery killer. **`frameloop="demand"` is the default**, not the optimization.
### Perf gotchas
- **No `dpr` cap** → 3× pixel cost on retina. Always `dpr={[1, 2]}`.
- Mutating position/rotation via React state → re-render per frame. Mutate via `ref` inside `useFrame`.
- Loading `.glb` in component body → re-fetches on remount. Use `useGLTF("/model.glb")` (cached) + `useGLTF.preload("/model.glb")` at module scope.
- **Compress assets**: GLB via DRACO (`useGLTF("/model.glb", "/draco/")`) + textures via KTX2 (`useKTX2Loader`). Routine 60-80% size reduction. Uncompressed GLBs are the #1 cause of "my 3D page is 8MB".
- `frameloop="demand"` + external state change (window resize, parent re-render) → scene goes stale. Call `invalidate()` from `useThree()` to trigger one render frame manually.
- Draw-call budget: **~200 meshes mobile, ~1000 desktop**. Beyond → instance, merge, or LOD (`<Detailed />` from drei).
- Custom shaders / textures need explicit `.dispose()` on unmount — R3F handles standard cases but external refs leak.
### Anti-patterns
- `<Canvas>` with no `dpr` cap and `frameloop="always"` on a mostly-static hero.
- `new THREE.Vector3()` / `new THREE.Color()` inside `useFrame` — GC pressure every frame. Hoist to `useMemo` or module scope.
- `OrbitControls` on a marketing page — confuses users. `autoRotate` only, or omit.
- 10k individual `<mesh>` instead of `<instancedMesh>`.
- Post-processing stack (bloom + DoF + SSAO) shipped to phones — feature-flag by device or user preference.
- A scene that would be a 60KB WebP rendered server-side.
---
## Cross-stack rules
1. **One library per property.** Never let Motion and GSAP animate the same `transform`. Never let Three.js drive DOM transforms that CSS is also animating.
2. **Reduced-motion is non-negotiable across all three.** Motion: `MotionConfig reducedMotion="user"`. GSAP: `matchMedia("(prefers-reduced-motion: no-preference)")`. Three.js: read `useReducedMotion()` and cut autorotate / idle drift.
3. **Spring vs tween is a project-level choice**, not a per-component one. Document it.
4. **GPU-accelerated transforms only.** Motion's `layout` and GSAP's `x`/`y`/`scale` honor this; animating `width`/`top` does not. Three.js runs on the GPU, but lighting/shadows/post-processing are their own budget.
5. **Budget, then build.** Bundle: Motion ≈ 50KB, GSAP core ≈ 25KB, Three.js ≈ 150KB, R3F + drei another 30-80KB. If you're shipping all three to one page, stop — one of them is wrong.
6. **`will-change` discipline.** Set `will-change: transform` only **just before** the animation fires (e.g. on `:hover`, in a `whileHover`, before a `gsap.to` call) and remove it after. Persistent `will-change` leaks compositor layers and eats GPU memory. Never set it globally on `*`.
7. **No smooth-scroll libraries (Lenis, Locomotive, ScrollSmoother) unless the design absolutely demands it.** They hijack native scroll, break touch momentum on iOS, fight `prefers-reduced-motion`, and make Cmd-F scroll-to-match janky. Prefer native scroll + `scroll-behavior: smooth` on specific anchors + `animation-timeline: scroll()` for reveal effects.
8. **Strip debug artifacts before ship.** GSAP `ScrollTrigger` `markers: true`, R3F `<Stats />`, drei `<Perf />`, `<OrbitControls />` on marketing pages, Leva panels, `console.log` in `useFrame`. Grep the diff before merging.
@@ -0,0 +1,188 @@
# State Design — Design the Unhappy Path First
Every interactive surface has more states than "it works." The happy path is the easy one. Idle, loading, empty, error, partial, conflict, and offline are where real UX lives — and where AI-generated UIs fall apart.
Design every state before writing any JSX. Stub the ones the knob level doesn't require — don't skip them.
---
## The State Lattice
Every component-that-touches-data has this set. For each, design the visual AND the copy before coding.
This table is the evaluation source for the `state-coverage` loop preset (see `../SKILL.md` → loops.md).
| State | What to design | Common mistake |
|-------|---------------|----------------|
| **Idle** | Default resting state. Primary action obvious. | Treating "empty form" as idle — it's actually Empty. |
| **Loading** | Skeleton matching final layout. Appears after 200ms. | Centered spinner on a blank screen (no layout match, no cue). |
| **Empty** | Explain why empty + CTA to fix it. | "No data" with no illustration, no explanation, no next step. |
| **Error** | Specific cause + one-click recovery + support ID. | "Something went wrong" with no detail and no retry. |
| **Partial** | Some data loaded, some failing. Show what succeeded. | Hide everything because one field failed — user sees blank. |
| **Success** | Positive feedback, brief, visual + textual. | Toast that vanishes in 1s; user misses it. |
| **Conflict** | Two actors edited. Show both, let user resolve. | Last write wins silently; prior user's work is gone. |
| **Offline** | Connection lost. Queue writes. Communicate. | Pretend everything is fine; fail on next sync. |
---
## Design the Unhappy Path First
Designing loading / empty / error states first produces a better happy path. When you start from "the list is empty and we need to onboard the user," the happy-path card has a job it has to fit into, not the other way around. When you start from "this API returns an error," your loading skeleton and retry copy are baked in, not bolted on.
Teams that design happy-first ship demos that crumble on real data. Teams that design unhappy-first ship products.
---
## Loading States
**Skeleton rules:**
- Match the final layout — same grid, same rough box sizes. Skeletons that don't match cause cumulative layout shift (CLS) when real content arrives.
- Show after **200ms**, not immediately. A flash of skeleton for fast responses looks broken.
- Set an upper bound of **5s**. Past that, escalate to a progress indicator or a timeout message with a retry.
- Use a subtle pulse or shimmer — not a spinner on top of a skeleton. One signal is enough.
- Preserve stable layout for known elements (avatar, title) while skeletonizing variable ones (body, list).
**Button loading:**
- Keep the label; don't swap to just a spinner. Users lose context.
- Disable but don't hide — users click it trying to cancel.
- Spinner goes *inside* the button, not next to it.
**Navigation loading:**
- Optimistic route change — paint the new layout immediately; stream data into it.
- A progress bar at the top of the viewport is better than a blank screen.
---
## Empty States
Empty states are the most skipped state and the most valuable. A first-time user sees an empty state before any happy path.
**Required:**
- Explain **why** it's empty — "You haven't created any projects yet" beats "No data"
- Offer **a next action** — a CTA to populate the state. Every empty state is a call to onboard.
- Visual or illustration — even a subtle icon. Prevents the "this page is broken" read.
- Secondary info if useful — "Projects help you organize your work. Learn more."
**Empty state types:**
- **First-run empty** (user just signed up) — onboarding. Heavy CTA.
- **Filtered empty** (user applied filters that match nothing) — "No results — try removing filters" with a clear-filters button.
- **Cleared empty** (user archived / deleted everything) — celebratory or contextual; may not need a CTA.
---
## Error States
Specific beats generic. Actionable beats dead-end. Recoverable beats fatal.
**Field-level errors:**
- Inline, at the field. Not a toast, not a global banner.
- Describe the problem AND the fix — "Password must include a number" beats "Invalid password."
- Red is a signal, not the whole message. Pair color with icon and text.
- `aria-invalid` + `aria-describedby` so screen readers announce.
**Form-level errors:**
- Scroll to the first invalid field, focus it.
- Summary at the top is fine in addition to inline, not instead of.
**Server errors:**
- Specific cause if you know it ("File too large — max 10MB").
- Copy-paste support ID — `Error ID: xyz-123` — so the user can send it to support.
- One-click retry where possible.
- Never blame the user for network failures.
**Network errors:**
- Distinguish offline from 500 from 403. Different causes, different fixes.
- Keep the in-flight state — don't throw away the form the user just filled.
---
## Conflict, Partial, and Offline States
These are the forgotten states. Designing them separates products from demos.
### Conflict
Two users edited the same resource at the same time. Someone's about to lose work if you don't handle this.
- Detect via version / etag / updated_at.
- Present both versions. Let the user pick or merge.
- Never silently overwrite. The "last write wins" pattern looks like a bug to the losing user.
- For high-frequency collaborative surfaces, consider CRDT or operational transform rather than explicit conflict UI.
### Partial
Some data loaded, some failed. Common with multi-source dashboards or parallel fetches.
- Show what succeeded.
- Mark what failed with a per-tile error state, not a page-level one.
- Let the user retry the failing piece without reloading the rest.
- Example: dashboard with 6 metrics, one metric API is down — render 5 metrics + one inline error tile with retry.
### Offline
Connection is gone. Anticipate this on mobile, field apps, travel, and flaky wifi.
- Detect with `navigator.onLine` + a heartbeat (more reliable).
- Communicate at the app shell: "You're offline — changes will sync when reconnected."
- Queue writes locally (IndexedDB, localStorage). Optimistic UI locally.
- Reconcile on reconnect: apply queued writes, handle conflicts, surface any rejections.
- Disable actions that *require* connectivity (payments) — explain why.
---
## State Machine Reference
Don't ship a state library for this. But think in state machines. Pseudocode:
```
states:
idle -> on FETCH -> loading
loading -> on SUCCESS(data=empty) -> empty
-> on SUCCESS(data) -> success
-> on SUCCESS(data=partial) -> partial
-> on ERROR(network) -> offline
-> on ERROR(server) -> error
-> on TIMEOUT -> error
empty -> on CREATE -> loading
error -> on RETRY -> loading
partial -> on RETRY -> loading (retains successful data)
offline -> on ONLINE -> loading (retries + drains queue)
success -> on EDIT -> loading (optimistic)
-> on CONFLICT -> conflict
conflict -> on RESOLVE -> loading
```
The mental model is the value. Use `useReducer`, XState, Zustand slices, or plain discriminated unions — whatever the project uses. Just don't model states as disconnected booleans (`isLoading && !error && !data`) — that's how impossible states appear.
---
## Checklist
Before any interactive surface ships, every item on this list is designed (not necessarily implemented — design can be "stubbed as an empty component"):
- [ ] Idle state has a clear primary action
- [ ] Loading state matches final layout; appears after 200ms
- [ ] Empty state has explanation + CTA + visual
- [ ] Error state is specific, actionable, and recoverable
- [ ] Partial state handled if data has multiple sources
- [ ] Success state provides visual feedback (not color-only)
- [ ] Conflict state exists if the resource is collaborative
- [ ] Offline state exists if the user-journey is likely on mobile / flaky networks
**Knob gating (used by `/ui-craft:unhappy`):**
| CRAFT_LEVEL | Required states |
|-------------|----------------|
| ≤ 4 | idle, loading, error |
| 5-7 | idle, loading, empty, error, success |
| 8+ | all of the above plus partial, conflict, offline |
Missing a required state is a finding. Fix before declaring the happy path "done."
---
## Cross-Refs
- `heuristics.md` — visibility of system status (Nielsen #1) is the principle behind the loading/partial states; use it to argue severity.
- `forms.md` — validation timing and error-state placement for form surfaces.
- `recipe-dashboard.md` — states are a build step (step 6), not polish; skeletons mirror final layout geometry.
@@ -0,0 +1,229 @@
# Theme Presets
Four named, production-ready token stacks. Each is a complete primitive layer for the 3-layer contract in [tokens.md](tokens.md) — map these into your semantic tokens, don't reference them raw from components. Pick one when the project has no token system; if the project has tokens, those win ([SKILL.md](../SKILL.md) Discovery).
These are not "styles to sprinkle". A preset is all-or-nothing: mixing Porcelain's radii with Carbon's shadows produces incoherence that reads as generated.
**Contrast rule for all presets:** before shipping, verify text pairs with APCA — Lc ≥ 75 for body text, Lc ≥ 60 for large text (≥24px), Lc ≥ 45 for non-text UI. The pairs below are chosen to clear these; re-verify after any tweak.
**Default usage:** light mode unless the user asks for dark. Every preset defines both; both must be intentional ([color.md](color.md) dark-mode rules).
**Motion tokens:** each preset's `--motion-*` values intentionally override the canonical scale in [motion.md](motion.md) — pace is part of the aesthetic (Signal is fast, Porcelain unhurried). When a preset is active, its values win; the motion.md scale is the default when building without a preset. All values stay within the ≤400ms ceiling.
---
## Graphite — minimal clean (default)
Cool neutrals, one deep accent, hairline borders, tight type. The safe choice for SaaS tools, admin surfaces, and the Command/Overview dashboard compositions. Disappears behind the data — which is the point.
```css
:root {
/* neutrals — cool, hue 255 */
--gray-0: oklch(99% 0.002 255); /* page */
--gray-1: oklch(97% 0.003 255); /* raised surface, sidebar tint */
--gray-2: oklch(93% 0.004 255); /* hairlines, dividers */
--gray-3: oklch(86% 0.005 255); /* disabled fills */
--gray-6: oklch(55% 0.012 255); /* secondary text */
--gray-7: oklch(42% 0.014 255); /* tertiary headings */
--gray-9: oklch(18% 0.012 255); /* primary text */
/* accent — deep indigo */
--accent: oklch(46% 0.17 275);
--accent-hover: oklch(41% 0.17 275);
--accent-tint: oklch(46% 0.17 275 / 0.08);
--on-accent: oklch(99% 0.002 255);
/* semantic */
--success: oklch(52% 0.13 155);
--warning: oklch(62% 0.14 75);
--danger: oklch(52% 0.18 25);
/* type — one grotesque sans for everything */
--font-body: var(--font-sans); /* Inter-class; system-ui acceptable */
--text-body: 14px / 1.5;
--text-display: 28px; /* metrics; 36px for hero */
--tracking-display: -0.02em;
/* shape + depth */
--radius-input: 0.375rem; --radius-card: 0.625rem; --radius-modal: 0.875rem;
--shadow-card: 0 0 0 1px oklch(0% 0 0 / 0.04), 0 1px 2px oklch(0% 0 0 / 0.05);
--shadow-overlay: 0 0 0 1px oklch(0% 0 0 / 0.05), 0 8px 24px oklch(0% 0 0 / 0.12);
/* motion */
--motion-fast: 120ms; --motion-base: 200ms; --motion-slow: 320ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
```
**Dark mapping:** page `oklch(15% 0.012 255)`, raised `oklch(19% 0.012 255)`, hairline `oklch(26% 0.012 255)`, text `oklch(93% 0.004 255)`, accent lifts to `oklch(62% 0.15 275)`, shadows replaced by surface lightness steps.
---
## Porcelain — soft modern
Warm neutrals, terracotta accent, generous radius, soft layered shadows. For products that want approachable over technical: consumer SaaS, onboarding-heavy tools, health/finance for humans. Pairs with VISUAL_DENSITY ≤ 5.
```css
:root {
--gray-0: oklch(98.5% 0.004 80);
--gray-1: oklch(96.5% 0.006 80);
--gray-2: oklch(92% 0.008 80);
--gray-3: oklch(85% 0.009 80);
--gray-6: oklch(54% 0.015 70);
--gray-9: oklch(22% 0.015 60);
--accent: oklch(60% 0.13 40);
--accent-hover: oklch(55% 0.13 40);
--accent-tint: oklch(60% 0.13 40 / 0.09);
--on-accent: oklch(98.5% 0.004 80);
--success: oklch(56% 0.12 150);
--warning: oklch(66% 0.13 80);
--danger: oklch(54% 0.17 28);
/* type — humanist sans body, optional rounded display */
--text-body: 15px / 1.55;
--text-display: 30px;
--tracking-display: -0.015em;
--radius-input: 0.625rem; --radius-card: 0.875rem; --radius-modal: 1.25rem;
--shadow-card: 0 1px 2px oklch(30% 0.02 60 / 0.06), 0 4px 12px oklch(30% 0.02 60 / 0.06);
--shadow-overlay: 0 2px 4px oklch(30% 0.02 60 / 0.08), 0 16px 40px oklch(30% 0.02 60 / 0.14);
--motion-fast: 140ms; --motion-base: 220ms; --motion-slow: 360ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}
```
**Dark mapping:** keep the warm hue — page `oklch(17% 0.012 70)`, raised `oklch(21% 0.012 70)`; accent lifts to `oklch(70% 0.12 45)`. Warm dark is this preset's signature; switching to cool dark breaks it.
---
## Carbon — dark premium
Dark-first. Deep cool neutrals, elevation by surface tint (not shadow), one restrained luminous accent. For developer tools, monitoring, pro audio/video — products used for hours in the dark. Light mode exists but dark is primary.
```css
:root { /* dark IS the root */
--surface-0: oklch(14% 0.012 260); /* page */
--surface-1: oklch(17.5% 0.012 260); /* card */
--surface-2: oklch(21% 0.012 260); /* raised: popover, hover */
--surface-3: oklch(25% 0.012 260); /* highest: modal */
--line: oklch(28% 0.012 260); /* hairlines */
--text-1: oklch(93% 0.005 260);
--text-2: oklch(70% 0.01 260);
--text-3: oklch(52% 0.012 260);
--accent: oklch(78% 0.12 170); /* luminous mint — reads as light, not paint */
--accent-hover: oklch(84% 0.12 170);
--accent-tint: oklch(78% 0.12 170 / 0.10);
--on-accent: oklch(8% 0.008 260); /* near-true black — the 14% surface tone fails Lc 60 here */
--success: oklch(74% 0.12 155);
--warning: oklch(78% 0.13 85);
--danger: oklch(68% 0.16 25);
--text-body: 14px / 1.5;
--text-display: 28px;
--tracking-display: -0.02em;
/* numbers and code in mono; tabular-nums everywhere */
--radius-input: 0.375rem; --radius-card: 0.625rem; --radius-modal: 0.875rem;
/* NO drop shadows — elevation = surface step + 1px line. Glow only on the
single primary CTA, subtle: 0 0 16px var(--accent-tint) */
--motion-fast: 120ms; --motion-base: 180ms; --motion-slow: 300ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
```
**Light mapping (secondary):** invert the surface ladder onto Graphite's neutrals; accent darkens to `oklch(50% 0.13 170)`. Don't ship light-Carbon as default — if light is the primary need, pick Graphite.
---
## Signal — sharp geometric
High-contrast, near-mono neutrals, semantic color doing real work, minimal radius, zero decorative depth. For dense data products — the Analytics composition, trading-adjacent surfaces, ops consoles at VISUAL_DENSITY ≥ 8.
```css
:root {
--gray-0: oklch(100% 0 0);
--gray-1: oklch(97% 0 0);
--gray-2: oklch(91% 0 0);
--gray-6: oklch(50% 0 0);
--gray-9: oklch(12% 0 0);
--accent: oklch(40% 0.16 264); /* one decisive blue-violet — used ONLY for selection + primary action */
--accent-hover: oklch(34% 0.16 264);
--accent-tint: oklch(40% 0.16 264 / 0.08);
--on-accent: oklch(100% 0 0);
/* semantic carries the information load — held to dots, deltas-on-demand, alerts */
--success: oklch(50% 0.15 150);
--warning: oklch(60% 0.15 70);
--danger: oklch(50% 0.20 25);
/* type — body sans, ALL numerals mono */
--text-body: 13px / 1.45;
--font-data: var(--font-mono);
--text-display: 26px;
--tracking-display: -0.01em;
--radius-input: 0.125rem; --radius-card: 0.375rem; --radius-modal: 0.625rem;
--shadow-card: none; /* 1px hairline only */
--shadow-overlay: 0 0 0 1px oklch(0% 0 0 / 0.08), 0 6px 20px oklch(0% 0 0 / 0.10);
--motion-fast: 100ms; --motion-base: 160ms; --motion-slow: 240ms; /* speed IS the aesthetic */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
```
**Dark mapping:** pure-neutral dark — page `oklch(13% 0 0)`, card `oklch(16.5% 0 0)`, hairline `oklch(25% 0 0)`; accent lifts to `oklch(70% 0.13 264)`; semantic hues lift ~20% lightness. The mono-numeral rule does not relax in dark.
---
## Choosing
| Signal in the brief | Preset |
|---|---|
| "clean", "professional", internal tool, no strong brand | **Graphite** |
| "friendly", "approachable", consumer-facing, onboarding matters | **Porcelain** |
| "developer tool", "pro", users live in it at night | **Carbon** |
| "dense", "fast", data is the product, power users | **Signal** |
Brand color exists? Replace `--accent` with the brand hue at the preset's lightness/chroma discipline — the preset survives an accent swap; it does not survive a radius or shadow swap.
---
## Accent rotation (no brand yet)
When the brief has no brand color, **rotate one accent family per build** so outputs don't all land on indigo. Declare the choice in the Craft Read ([craft-intent.md](craft-intent.md) §6). Swap only `--accent`, `--accent-hover`, `--accent-tint`, and `--on-accent` on the chosen preset — keep neutrals, radii, and shadows intact.
| Family | Light `--accent` (OKLCH) | Best on preset | Avoid pairing with |
|---|---|---|---|
| **Indigo** | `oklch(46% 0.17 275)` | Graphite, Signal | — (default; rotate away often) |
| **Teal** | `oklch(48% 0.12 195)` | Graphite, Carbon | Porcelain terracotta clash |
| **Rose** | `oklch(52% 0.14 15)` | Porcelain, Graphite | Signal high-contrast grids |
| **Amber** | `oklch(58% 0.14 75)` | Porcelain, Graphite | Carbon mint accent |
| **Forest** | `oklch(46% 0.11 155)` | Porcelain, Graphite | Signal blue-violet default |
**Hover / tint discipline:** `--accent-hover` ≈ 5% darker on the same hue; `--accent-tint` ≈ 8–10% alpha of accent; `--on-accent` must clear APCA Lc ≥ 60 against the filled button.
**Dark mode:** lift accent lightness ~15–20% on dark surfaces (see each preset's dark mapping). Re-verify contrast after swap.
**Marketing vs product:** landings and portfolios may rotate accent **and** theme preset; dashboards and auth rotate accent only unless the user asks for a full re-skin.
## Premium palette families (artisan / luxury / wellness briefs)
"Premium" briefs converge on one palette — warm cream background, brass/clay accent, espresso text — until every generated artisan site is interchangeable. Unless the brief names those colors, rotate through distinct families (one per build, never the same family twice in a row):
| Family | Recipe | Reads as |
|---|---|---|
| **Cold luxury** | silver-grey neutrals + chrome + smoke dark | precision, engineering premium |
| **Forest** | deep green + bone + amber accent | outdoor heritage, built-to-last |
| **Black & tan** | true off-black + warm tan, hard contrast, no beige | leather-goods, deliberate |
| **Cobalt + cream** | one saturated blue against a single neutral | confident, modern craft |
| **Terracotta + slate** | warm rust against cool grey | earthy without the beige wash |
| **Mono + pop** | off-white + off-black + one bright accent | gallery, editorial-modern |
Warmth can come from typography, imagery, and copy — it doesn't have to live in the background color. Verify AA contrast after picking; dark-mode mapping per the preset rules above.
@@ -0,0 +1,350 @@
# Token Spine
Every design decision in a project references a three-layer token system established before any component is written. The layers are: **primitive** (raw values named for what they are), **semantic** (contextual meaning, switches between themes), and **component** (specific usage, created on demand). The scope is projects: throwaway prototypes and single standalone components may run on primitives alone — but the moment a second surface appears, build the spine.
This 3-layer contract and the off-system-value definition (any color, radius, or spacing value not drawn from the token system) are the evaluation source for the `token-consistency` loop preset (see `../SKILL.md` → loops.md).
---
## The Three Layers
### Layer 1: Primitive tokens
Raw values. Named for what they ARE, not how they're used. Reusable across themes.
**Color — neutral ramp + accent ramp + semantic bases.**
Use OKLCH for perceptually uniform steps. See `color.md` for palette strategy and tinted-neutral rationale.
```css
/* Neutral ramp — warm tint, not pure gray */
--gray-50: oklch(98% 0.008 60);
--gray-100: oklch(95% 0.010 60);
--gray-200: oklch(90% 0.012 60);
--gray-300: oklch(82% 0.014 60);
--gray-400: oklch(70% 0.016 60);
--gray-500: oklch(58% 0.016 60);
--gray-600: oklch(46% 0.015 60);
--gray-700: oklch(36% 0.013 60);
--gray-800: oklch(26% 0.010 60);
--gray-900: oklch(18% 0.008 60);
--gray-950: oklch(12% 0.006 60);
/* Accent ramp — replace hue with your brand value */
--accent-50: oklch(97% 0.03 var(--accent-hue));
--accent-100: oklch(93% 0.06 var(--accent-hue));
--accent-200: oklch(86% 0.10 var(--accent-hue));
--accent-300: oklch(78% 0.14 var(--accent-hue));
--accent-400: oklch(68% 0.18 var(--accent-hue));
--accent-500: oklch(58% 0.20 var(--accent-hue));
--accent-600: oklch(48% 0.19 var(--accent-hue));
--accent-700: oklch(38% 0.16 var(--accent-hue));
--accent-800: oklch(28% 0.12 var(--accent-hue));
--accent-900: oklch(20% 0.08 var(--accent-hue));
--accent-950: oklch(13% 0.05 var(--accent-hue));
/* Semantic color bases — for success / warning / error / info */
--green-400: oklch(72% 0.18 145);
--green-600: oklch(52% 0.18 145);
--amber-400: oklch(78% 0.16 70);
--amber-600: oklch(58% 0.16 70);
--red-400: oklch(65% 0.20 25);
--red-600: oklch(48% 0.20 25);
--blue-400: oklch(66% 0.16 250);
--blue-600: oklch(50% 0.16 250);
```
**Spacing — 8pt scale.** Full rhythm rules and when to compress → see `layout.md`.
```css
--space-xs: 0.25rem; /* 4px */
--space-sm: 0.5rem; /* 8px */
--space-md: 1rem; /* 16px */
--space-lg: 1.5rem; /* 24px */
--space-xl: 2rem; /* 32px */
--space-2xl: 3rem; /* 48px */
--space-3xl: 4rem; /* 64px */
--space-4xl: 6rem; /* 96px */
```
**Type — size, weight, line-height, letter-spacing.** Full scale and font selection → see `typography.md`.
```css
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
--text-4xl: 2.25rem; /* 36px */
--text-5xl: 3rem; /* 48px */
--font-light: 300;
--font-regular: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
--leading-none: 1;
--leading-tight: 1.1;
--leading-snug: 1.3;
--leading-normal: 1.5;
--leading-relaxed: 1.65;
--tracking-tight: -0.03em;
--tracking-normal: 0;
--tracking-wide: 0.01em;
--tracking-wider: 0.05em;
```
**Radii.**
```css
--radius-none: 0;
--radius-sm: 0.125rem; /* 2px — minimal, sharp */
--radius-md: 0.375rem; /* 6px — inputs, badges */
--radius-lg: 0.625rem; /* 10px — cards */
--radius-xl: 0.875rem; /* 14px — modals, large panels */
--radius-2xl: 1.25rem; /* 20px — expressive cards, popovers */
--radius-full: 9999px; /* pills, avatars */
```
**Shadows — layered ambient + direct.** Always stack two layers minimum. See `layout.md` for the scale and the "never flat" rule.
```css
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.05);
--shadow-md: 0 4px 6px oklch(0% 0 0 / 0.07),
0 1px 3px oklch(0% 0 0 / 0.06);
--shadow-lg: 0 10px 15px oklch(0% 0 0 / 0.09),
0 4px 6px oklch(0% 0 0 / 0.05);
--shadow-xl: 0 20px 25px oklch(0% 0 0 / 0.10),
0 8px 10px oklch(0% 0 0 / 0.04);
```
**Motion — durations and easings.** Perceptual bands and choreography rules → see `motion.md`.
```css
--duration-instant: 80ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 400ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
```
**Z-index — semantic layers, never arbitrary numbers.**
```css
--z-base: 0;
--z-raised: 1;
--z-dropdown: 10;
--z-sticky: 20;
--z-modal-backdrop: 30;
--z-modal: 40;
--z-toast: 50;
--z-tooltip: 60;
```
> **The naming rule:** primitives are nouns about the value — `--gray-500`, `--space-md`, `--radius-lg`. Never role-named at this layer. `--button-bg` is a component token. `--text-primary` is a semantic token. Both are wrong here.
---
### Layer 2: Semantic tokens
Contextual meaning. References primitives. This is the layer that switches between light and dark mode — primitives never change; semantics remap.
```css
:root {
/* Surface stack — 5 levels of elevation */
--surface-canvas: var(--gray-50); /* page background */
--surface-raised: var(--gray-100); /* cards, panels */
--surface-overlay: #fff; /* modals, sheets */
--surface-sunken: var(--gray-200); /* inset areas, code blocks */
--surface-inverse: var(--gray-900); /* inverted blocks, tooltips */
/* Text */
--text-primary: var(--gray-900);
--text-secondary: var(--gray-600);
--text-tertiary: var(--gray-400);
--text-on-accent: #fff;
--text-on-inverse: var(--gray-50);
--text-link: var(--accent-600);
--text-success: var(--green-600);
--text-warning: var(--amber-600);
--text-error: var(--red-600);
/* Border */
--border-subtle: oklch(0% 0 0 / 0.06);
--border-default: oklch(0% 0 0 / 0.12);
--border-strong: oklch(0% 0 0 / 0.24);
--border-focus: var(--accent-500);
--border-error: var(--red-400);
/* Accent */
--accent-bg: var(--accent-500);
--accent-bg-hover: var(--accent-600);
--accent-bg-active: var(--accent-700);
--accent-text: #fff;
--accent-border: var(--accent-400);
/* Status — bg / text / border per state */
--success-bg: oklch(96% 0.04 145);
--success-text: var(--green-600);
--success-border: var(--green-400);
--warning-bg: oklch(97% 0.04 70);
--warning-text: var(--amber-600);
--warning-border: var(--amber-400);
--error-bg: oklch(97% 0.04 25);
--error-text: var(--red-600);
--error-border: var(--red-400);
--info-bg: oklch(96% 0.04 250);
--info-text: var(--blue-600);
--info-border: var(--blue-400);
}
```
Dark mode remaps semantics only — primitives are untouched:
```css
[data-theme="dark"] {
--surface-canvas: var(--gray-950);
--surface-raised: var(--gray-900);
--surface-overlay: var(--gray-800);
--surface-sunken: oklch(9% 0.008 60); /* darker than canvas */
--surface-inverse: var(--gray-50);
--text-primary: var(--gray-50);
--text-secondary: var(--gray-400);
--text-tertiary: var(--gray-600);
--text-on-inverse: var(--gray-900);
--border-subtle: oklch(100% 0 0 / 0.06);
--border-default: oklch(100% 0 0 / 0.12);
--border-strong: oklch(100% 0 0 / 0.24);
/* Accent desaturates ~12% in dark to prevent burn */
--accent-bg: oklch(58% 0.18 var(--accent-hue));
--accent-bg-hover: oklch(65% 0.18 var(--accent-hue));
--accent-bg-active: oklch(70% 0.18 var(--accent-hue));
}
```
---
### Layer 3: Component tokens
Specific usage. References semantics. Created on demand per component — not pre-built as a blanket catalog.
```css
/* Example: button */
--button-primary-bg: var(--accent-bg);
--button-primary-bg-hover: var(--accent-bg-hover);
--button-primary-bg-active: var(--accent-bg-active);
--button-primary-text: var(--accent-text);
--button-primary-border-radius: var(--radius-md);
--button-primary-padding-x: var(--space-md);
--button-primary-padding-y: var(--space-sm);
/* Example: input */
--input-bg: var(--surface-canvas);
--input-border: var(--border-default);
--input-border-focus: var(--border-focus);
--input-border-error: var(--border-error);
--input-text: var(--text-primary);
--input-placeholder: var(--text-tertiary);
--input-border-radius: var(--radius-md);
```
> **When to create component tokens:** only when a component has multiple states or themes. A simple component referencing semantic tokens directly is fine — don't add a layer to prove you understand the system.
---
## Light + Dark: Both Intentional
Dark mode is not the negation of light mode. Both are designed; neither is a fallback.
> **The intentional-dark test:** if your dark mode maps `--text-primary` from `gray-900` to `gray-100` and stops there, you inverted — you didn't design. Real dark mode rebalances the entire surface stack.
**The canvas rule.** `--surface-canvas` in dark sits between `gray-950` and `gray-900` with a slight hue tint. Pure `#000` reads as broken on any monitor with backlight bleed — it's also a cold dead black that strips warmth from everything above it.
**The accent rule.** Saturated accent colors burn on dark surfaces. Drop chroma 10-15% in OKLCH when switching to dark. The hue stays constant; only `C` shifts. This preserves brand recognition while reducing visual strain.
**The shadow rule.** Shadows are invisible on dark surfaces — black-on-near-black has no contrast. In dark mode: replace `box-shadow` depth with a `1px` border ring using `--border-default`, or a subtle top-edge highlight (`inset 0 1px 0 oklch(100% 0 0 / 0.08)`). Reserve box-shadow for colored glows only.
**The text rule.** Pure white (`#fff`) on dark backgrounds creates excessive halation on OLED screens. Use `--gray-50` or a warmer `oklch(97% 0.01 60)` as your lightest text value.
**The transition rule.** Always declare `color-scheme: light dark` for native form controls to adopt the right system rendering. Provide `prefers-color-scheme` for users without a toggle; respect the user's explicit toggle when present — never override it with system preference once the user has made a choice.
```css
/* Both modes in one block */
:root {
color-scheme: light dark;
/* Light defaults above */
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
/* Paste dark remaps here — same keys as light */
}
}
[data-theme="dark"] {
/* Explicit toggle — overrides media query */
}
```
---
## Token Categories Required
Every non-trivial UI needs all seven. Missing categories produce ad-hoc inline values that break theming.
| Category | Primitive examples | Semantic examples | Notes |
|---|---|---|---|
| Color | `--gray-50` → `--gray-950`, `--accent-50` → `--accent-950` | `--text-primary`, `--surface-raised`, `--accent-bg` | Light + dark both required |
| Spacing | `--space-xs` → `--space-4xl` | (typically used directly) | See `layout.md` for scale and rhythm rules |
| Type | `--text-xs` → `--text-5xl`, `--leading-tight`, `--font-medium` | `--font-display`, `--font-body`, `--font-mono` | See `typography.md` for scale and letter-spacing rules |
| Radii | `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-full` | `--radius-card`, `--radius-button`, `--radius-input` | Vary by element — uniform radius is the AI tell |
| Shadows | `--shadow-sm` → `--shadow-xl` | `--elevation-raised`, `--elevation-overlay`, `--elevation-modal` | Stack 2+ layers per shadow; dark mode requires border-ring replacement |
| Motion | `--duration-instant` → `--duration-slow`, `--ease-out`, `--ease-spring` | `--motion-hover`, `--motion-modal-in` | See `motion.md` for perceptual bands and choreography |
| Z-index | `--z-dropdown`, `--z-modal`, `--z-toast` | (typically used directly) | Semantic labels, never arbitrary numbers |
---
## When to Extend vs. Replace
**If the project has a token system:**
- **Extend** — add the missing layer or category. The most common gap is missing semantics: primitives exist, but nothing switches for dark mode and no surface stack is defined.
- **Audit** — check dark mode is intentional (run the test above), semantic naming exists, all 7 categories are present.
- **Don't replace** unless the user explicitly asks. Token replacement is a multi-week refactor. Propose additions as patches, not rewrites.
**If the project has nothing:**
- **Establish primitives first** — color (neutral ramp + one accent), spacing (8pt scale), type (5 sizes + 3 weights). These three cover the majority of any UI.
- **Add semantics on demand** as components get built. Surface stack + text roles are almost always next.
- **Defer component tokens** until variant explosion — more than 2-3 states or themes — forces them.
---
## When the Token Spine Doesn't Apply
- **Single-page experiences** (landing pages, one-off campaign pages) — primitives + inline use is fine when nothing will be themed or reused. Semantics add no value if there's one theme and no components.
- **Adopted upstream systems** (Material, shadcn, Ant Design) — use the upstream tokens wholesale. Don't reinvent a parallel system alongside an existing one; that's two token systems to maintain.
- **Native iOS / Android** — platform tokens (`systemBackground`, `label`, `tint`) govern rendering. Custom tokens run parallel and cause drift.
---
## Cross-References
- Spacing scale: see `layout.md` for the full table and spacing rhythm rule.
- Type scale: see `typography.md` for sizes, weights, scoped letter-spacing, and line-height by script.
- Color strategy: see `color.md` for OKLCH rationale, tinted neutrals, accent budget, and semantic palette structure.
- Motion durations: see `motion.md` for the duration scale, perceptual bands, and reduced-motion contract.
The spine ties these together. Each scale is its own discipline; the spine defines how they layer.
@@ -0,0 +1,246 @@
# Typography
Type scale, font selection, readability, and weight systems.
---
## Essentials
Apply these in all standard Latin-script web UI unless a specific context overrides:
- **`text-wrap: balance`** for headings; **`text-wrap: pretty`** for body
- **`font-variant-numeric: tabular-nums`** for data/numbers
- **Truncation handling** for dense UI; flex children need `min-w-0`
- **Smart punctuation**: curly quotes (`&ldquo;` `&rdquo;`), apostrophes (`&rsquo;`), ellipsis (`&hellip;`), em-dash (`&mdash;`)
- **Non-breaking spaces**: `10&nbsp;MB`, `⌘&nbsp;K`, brand names, `$&nbsp;79/month`
**Font recommendations** — pick one family for body, optionally a second for display. In a single product surface, don't mix more than two typefaces; editorial layouts intentionally mix three or more for hierarchy.
| Category | Safe choices |
|----------|-------------|
| Sans-serif | Inter, Geist, DM Sans, Plus Jakarta Sans |
| Monospace | Geist Mono, JetBrains Mono, IBM Plex Mono |
| System stack | `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` |
**Safe choices are product-shell defaults, not marketing display picks.** On landings, portfolios, and brand surfaces the display face carries voice — reaching for the same safe sans every build produces monoculture. Selection procedure for marketing display type:
1. Write three concrete brand-voice words from the brief ("warm, mechanical, opinionated" — not "modern, clean").
2. Note the font you'd reach for by reflex; if it's the same face your last marketing build used, reject it and rotate.
3. Pick for the voice words, then cross-check: "creative brief = serif" is itself a reflex — a distinctive sans display (Cabinet Grotesk, Satoshi, GT Walsheim class) is the right default for most creative/premium briefs. Serif display only when the brief is genuinely editorial, luxury, or publication-shaped, and rotate which serif.
4. Emphasize within a headline using italic or bold of the **same family** — never inject a one-word serif into a sans headline for "visual interest".
**Italic descender clearance:** italic display words containing `y g j p q` clip under `line-height: 1`. Use ≥1.1 and reserve bottom padding on the wrapper.
---
## Type Hierarchy
Create a clear scale from most important to least. Two weights (regular + one of
semibold/bold) cover most products; every added weight is another hierarchy level readers
must decode. For long-form reading (articles, docs) the body floor is 18px — 14-16px serves
UI labels and controls, not sustained reading.
### The ladder is what you use, not what you define
A nine-step scale is a palette. The **ladder** is the four to six steps that actually appear
in the built page, and it is the ladder a reader sees. Emitting most of a modular scale
produces a gradient of sizes where every step is 1.2x the last — legible, forgettable, and
the single loudest tell of a surface built by an agent rather than designed.
Measured on four surfaces whose typography is the reason people copy them (a project tool, a
deploy platform, a voice API, a commerce framework), at 2120x1143:
| | A | B | C | D |
|---|---|---|---|---|
| distinct sizes rendered in the fold | 9 | **4** | 5 | **4** |
| sizes below 14px | 4 | **0** | 2 | 2 |
| display size | 64 | 64 | 48 | 64 |
| next size used below it | 20 | 24 | 16 | 16 |
| **the jump** | **3.2x** | **2.7x** | **3.0x** | **4.0x** |
| weight of the display type | 510 | 450-500 | 400 | 500 |
| distinct weights | 3 | 3 | 3 | **2** |
Three rules fall out of that table, and all three are checkable:
1. **Four to six steps, not nine.** One label size, one body size, one intermediate, one
section head, one display. Collapse everything else onto the nearest step. Half-pixel
neighbours (13px and 13.5px) are never a distinction — they are indecision.
Enforced by `type/crowded-ladder`.
2. **The display step sits 2.5x to 4x above the step below it, with nothing in between.**
Display type is made by the **gap**, not by the size: 56px directly above 44px reads as
slightly bigger text, while 76px above 30px reads as a headline. If the jump is small,
either raise the display size or delete the step under it.
Enforced by `type/display-not-separated`.
3. **Display type is not bold.** None of the four sets its largest type above 510. Big and
bold reads cheap — a poster made in a hurry; big and medium reads like the size was
already enough. Reserve 600-700 for 13-16px labels and buttons, where weight is the only
emphasis available.
Enforced by `type/bold-display`.
Corollary on small type: seven sizes under 14px is how a landing page ends up reading as a
dashboard. Two is plenty, and one of those should be the mono label size.
### Scale (a palette to choose from, not a set to emit)
```css
--text-xs: 0.75rem; /* 12px - mono labels, captions */
--text-sm: 0.875rem; /* 14px - secondary text */
--text-base: 1rem; /* 16px - body text */
--text-lg: 1.25rem; /* 20px - lead text */
--text-xl: 1.875rem; /* 30px - section heads */
--text-2xl: 2.75rem; /* 44px - the one big number, on data surfaces */
--text-3xl: 4.75rem; /* 76px - display */
```
Pick four to six of these per surface and use nothing else. A dense console legitimately
sits low on the ladder (11 / 13 / 16 / 22 / 64); a restaurant page legitimately skips the
middle entirely (12 / 15 / 30 / 88). Both are ladders. Twelve evenly-spaced steps is not.
Match the *floor* to the product type — dense apps start at 11-13px, marketing at 13-16px —
but the jump to display stays large in both. A console with a 2.9x jump to its one headline
number is right; a console with twelve steps is not.
---
## Font Selection
### Principles
- **Display fonts** for headlines — distinctive, personality-rich. A font excellent for display (high contrast, tight spacing) is wrong for body text at 16px.
- **Body fonts** for running text — readable, neutral, well-hinted at small sizes. A workhorse body font (Charter, IBM Plex Sans) often looks bland at hero scale — that's fine, just don't ask it to do both jobs.
- **UI fonts** for labels and dense interfaces — optimized x-height, open apertures. Inter and Geist are UI fonts first; using them at 48px display is fine, but don't expect typographic personality.
- **Monospace** as intentional accent — not as lazy "dev tool" default
- **Variable fonts** for flexibility — one file, many weights
- **Subset fonts** — ship only code points/scripts you use
### Loading
```html
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
```
```css
@font-face {
font-family: 'Main';
src: url('/fonts/main.woff2') format('woff2');
font-display: swap;
unicode-range: U+0000-007F; /* Basic Latin only */
}
```
**`font-synthesis: none`** on any family missing a weight or style file. Without it, the browser fakes bold by smearing the outline and fakes italic by shearing it — synthesized glyphs that ship silently to production. Setting `font-synthesis: none` makes the gap fail visibly in development (regular weight renders where bold was requested) so it gets caught before ship, instead of shipping fake glyphs users never notice as wrong but perceive as slightly off.
### Feature Control: Properties Over Raw Tags
When a high-level CSS property and a raw OpenType feature tag both produce the same visual result, take the property. **Why it matters more than it looks:** a property degrades gracefully — a fallback font that lacks the matching axis or feature just ignores the property and falls back to its own default rendering, no error, nothing visibly broken. A raw feature tag has no such fallback path; it silently stops applying, and the gap stays invisible until someone diffs the rendered output against the design.
Where this shows up in practice: `font-variant-numeric: tabular-nums slashed-zero` instead of a hand-written feature-tag equivalent, for the tabular figures this skill already asks for anywhere numbers stack in a column; `font-weight` instead of `font-variation-settings: 'wght' 600` on variable fonts; `font-variant-caps` instead of a raw `font-feature-settings` string for small caps.
### Letter-Spacing by Size
These rules apply to **Latin script, sans-serif and display faces** unless stated otherwise. Do not adjust tracking on CJK, Arabic, Devanagari, or other non-Latin scripts unless you deeply understand their typographic conventions — the font's built-in metrics handle it. Serif faces are designed with tracking already optimized across sizes; tightening them at display sizes damages their rhythm.
- **Display (≥24px), sans-serif Latin**: tighten to `-0.02em` to `-0.04em` — large sizes optically appear loose at default tracking. `tracking-tight` in Tailwind.
> **When it breaks:** serif faces (Playfair, Freight, Garamond) — leave at default. The built-in sidebearings were set for the size.
- **Body text (14-18px)**: leave `letter-spacing` alone — the font designer optimized it. This holds for all scripts and classifications.
- **Small text/labels (11-13px), Latin**: slight positive tracking `+0.01em` to `+0.02em` improves readability at tight optical sizes. `tracking-wide` in Tailwind.
> **When it breaks:** CJK labels — already have generous inter-glyph spacing; adding more creates uneven color.
- **ALL CAPS labels, Latin**: `+0.05em` to `+0.1em` — all-caps removes descender space and needs tracking to stay readable. `tracking-widest` in Tailwind.
> **When it breaks:** Not a universal Never. ALL CAPS is acceptable for small category labels (10-13px), regulatory text, and utilitarian aesthetics — just always add tracking when you do it.
### Never
- Use system fonts when personality matters — system stacks are fine for utilitarian UI but they signal "no design intent" in brand-facing contexts
- Mix more than 2-3 typefaces **in a single product surface** — editorial layouts (magazines, marketing pages) intentionally exceed this for typographic hierarchy
---
## Readability
### Line Length
- **Prose body text**: 45-75 characters per line (ideal: 66). Use `max-width: 65ch` on text containers.
> **When it breaks:** UI labels, buttons, table cells, and dense data layouts tolerate much shorter or longer lines. The 65ch rule is for continuous reading, not UI components.
### Line Height
Line height is script- and x-height-dependent — no single value works universally.
- **Body text (14-18px), Latin**: 1.5 to 1.65. Tall x-height fonts (Inter, Geist) tolerate the lower end; short x-height faces (Garamond, Freight) may need slightly tighter — test visually.
- **Display headlines (≥24px)**: 1.05 to 1.2. Headlines need to feel tight; a 1.5 line-height on a 48px heading looks airy and unintentional.
- **UI labels and dense interfaces**: 1.3 to 1.4. Enough air to avoid collision, not so much that the interface feels sparse.
- **CJK scripts**: 1.7 to 1.85 — CJK glyphs have different vertical metrics and need more inter-line clearance than Latin at the same size.
> **When it breaks:** Applying 1.5 body line-height to display text; applying CJK ratios to Latin body text (creates too much vertical space).
### Text Wrapping
```css
h1, h2, h3, h4 { text-wrap: balance; }
p, li, dd { text-wrap: pretty; }
```
- **`text-wrap: balance`** — even line lengths for headings
- **`text-wrap: pretty`** — avoids widows/orphans in body text
---
## Data Typography
```css
/* Tabular numbers for data alignment */
.data-value { font-variant-numeric: tabular-nums; }
/* Or use a monospace font for data-heavy tables */
.data-table { font-family: var(--font-mono, monospace); }
```
- **`tabular-nums`** for any numbers that align vertically in columns (tables, prices, stats) — this is a near-universal rule for data UI; the exception is decorative numerals in display headings where proportional figures look better
- **Truncate dense UI**: `truncate` or `line-clamp-*`
- **Flex children need `min-w-0`** to allow truncation
---
## Text Handling
### Content Overflow
```css
/* Single line truncation */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Multi-line clamp */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Flex children MUST have min-w-0 */
.flex-child {
min-width: 0; /* allows text to truncate */
}
```
### Special Characters
- **Curly quotes**: " " not " "
- **Ellipsis character**: … not ...
- **Non-breaking spaces**: `10&nbsp;MB`, `⌘&nbsp;K`, brand names
- **`scroll-margin-top`** on headings for anchor links
### Resilience
- Layouts handle short, average, AND very long content
- Handle empty strings without broken UI
- Locale-aware: `Intl.DateTimeFormat`, `Intl.NumberFormat`
---
## Cap-Height Trimming & Underlines
- **`text-box: trim-both cap alphabetic`** strips out the half-leading a browser adds above and below the glyph outlines — that reserved sliver is what makes a single-line label inside a pill, chip, or button look like it's drifted below true center instead of sitting on it. Until support is universal, the old manual counter-measure still works: nudge the text node with a small negative margin or padding tuned to the font's own metrics.
- **`text-underline-position: from-font`** and **`text-decoration-thickness: from-font`** pull the underline's offset and weight from the font file's own metrics instead of the browser's generic guess — closer underlines on script-adjacent faces, correctly-weighted underlines on heavier display fonts.
- **Reserve dotted underlines for "more info" affordances** — glossary terms, `<abbr>`, inline definitions — not for links. A dotted underline reads as "hover for detail," and putting it on a normal link signals the wrong interaction.
---
## Anti-aliasing
- **Root smoothing (macOS):** apply `-webkit-font-smoothing: antialiased` + `-moz-osx-font-smoothing: grayscale` once on `html` (Tailwind: `antialiased` on the root element). macOS renders text heavier than intended by default; this thins it for crisper text. Set it ONCE at the root — never per-element, or smoothed and unsmoothed text end up with visibly different weights. No-op on other platforms, safe to apply universally.
- Scaling text via `transform` can change smoothing
- Prefer animating a wrapper instead of text node directly
- If artifacts persist: `translateZ(0)` or `will-change: transform` to promote layer
+8
View File
@@ -0,0 +1,8 @@
# Confirm project slug and register this callback in FOUNDRY_PROJECTS first.
FOUNDRY_PROJECT=thesisly
FOUNDRY_API_ORIGIN=https://api.foundry.entroia.com
APP_ORIGIN=http://localhost:17322
# Enable after the Foundry research PR is merged, deployed and configured.
# PAPER_API_ORIGIN=https://api.foundry.entroia.com/projects/thesisly/research/
# Blank = explicitly labelled sample mode.
PAPER_API_ORIGIN=
+10
View File
@@ -0,0 +1,10 @@
node_modules/
.next/
out/
.env*
!.env.example
*.tsbuildinfo
playwright-report/
test-results/
.session-test-*/
.DS_Store
+2
View File
@@ -0,0 +1,2 @@
# Product brief
Researchers browse a paper index, search title/abstract/authors, inspect source-preserving abstracts and metadata, and sign in through shared Foundry Core. Modern editorial product: paper list instead of dashboard, quiet monochrome, readable measure. Variance 3. Signature: one consistent research index with three reading themes. English source titles/abstracts preserved. UI Chinese. No invented summaries or code links. Real 20-row SQLite preview is a clearly labelled bounded dataset until an HTTP paper API is supplied.
+20
View File
@@ -0,0 +1,20 @@
# Thesisly 研迹 working agreements
- 本项目使用 Next.js App Router、TypeScript、Tailwind CSS、shadcn/ui(Radix primitives)、Lucide 和 Sonner。用户明确授权本地 dev,随后仅撤回本地 build:允许按需启动 pnpm dev,禁止 pnpm build。不配置前端 CI/CD,不部署。用户最新要求本阶段暂不需要 CI:不新增或触发 CI。交付使用已核对的 Gitea [skip ci] 提交及 PR 标题标记跳过推送和 PR 同步事件,并核对本次 commit 未新增 Actions 运行;不删除或禁用共享工作流。
- 仅通过 Foundry Core 共享后端管理身份。复用 Endoleap 的 PKCE、HttpOnly Cookie、闲置续期、退出撤销与同源校验;禁止把 token 发到客户端或 localStorage。
- 论文读取模式必须显式区分真实数据库样本与远程 API。不得把本地20条样本称为全库、最新或热门数据,不虚构摘要、代码仓库、会议决定或指标。
- 源数据只读;不复制整库,不从浏览器直连 SQLite 或持有数据库凭据。
- 阅读与文章列表优先,无 KPI 仪表盘。默认 Vignelli;Editorial、Tufte 可切换。UI Craft Editorial 的衬线默认按用户现代简洁偏好及选定主题调整。
- 项目级技能在 .agents/skills;beautiful-article 内含 Vignelli/Tufte authoring profiles,两者不是独立上游 skill。Beautiful Article 只在另行请求单篇 HTML 文章时进入其完整 harness。
- 英文品牌 Thesisly、中文名研迹;远程仓库为 `ssh://git@gitea.entroia.com:22222/Zayden-Jung/Thesisly.git`。用户已在 FOUNDRY_PROJECTS 新增 thesisly 回调,并复用当前正式 Core 二进制完成仅配置重载;新旧项目路由、Google/Slack 跳转和取消回调通过,Google/Slack真实授权登录及用户侧刷新、新标签保持、退出均已确认,Google首次授权已完成注册;邮件投递与验证未实测。main 仅允许本次已授权的空初始化提交,其余代码走工作分支与人工 PR 合并。
- 论文库已由「调查云盘占用原因」任务完成 PostgreSQL 迁移、验收与切换;Foundry Core 的只读论文 API 正在按已发布 schema 联调。服务器已配置专用 SELECT 账号,连接串仅保存在服务器受保护文件中。不部署已撤回的 SQLite 方案,也不要求添加 FOUNDRY_RESEARCH_DATABASE。
<!-- BEGIN:nextjs-agent-rules -->
## This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+61
View File
@@ -0,0 +1,61 @@
# Thesisly 研迹
论文索引与阅读前端:Next.js App Router + TypeScript + Tailwind CSS;shadcn/ui Button组合模式基于Radix Slot与CVA,Lucide提供图标,Sonner提供复制/退出等反馈。不配置CI/CD,不部署,不创建新的业务后端。
## 本地开发
用户允许本地 dev,暂不执行本地 build。开发预览已启动并完成桌面/手机浏览器检查:
```sh
pnpm install
cp .env.example .env.local
pnpm dev
```
本地地址:`http://localhost:17322`。可匿名浏览论文;真实登录前先按[身份接入](docs/auth-integration.md)注册项目。没有账户时auth/me本地返回401,不向后端发送空token。
```sh
pnpm typecheck
pnpm test
# 自动启动并清理隔离的样本dev,使用系统Chrome,不改预览配置
pnpm test:browser
# 自动启动并清理隔离 dev 副本及模拟 Core,使用系统 Chrome
pnpm test:session
```
## 页面
- `/`:论文列表、标题/作者/摘要搜索、领域过滤、收录顺序/标题排序与分页。
- `/papers/{id}`:原始摘要、作者、日期、来源元数据、引用复制与原文链接。
- `/login`:Google/Slack OAuth入口及邮箱注册登录。
- `/auth/callback`:一次性凭据兑换与邮箱验证。
- `/about`:数据范围与主题说明。
- 空结果、404、加载中与数据服务错误均有对应状态。
默认Vignelli,顶部可切换Editorial/Tufte;仅主题偏好存于localStorage,绝不保存认证token。英文品牌已确定为 Thesisly;中文名为研迹,Gitea 仓库为 Zayden-Jung/Thesisly,尚未注册域名或商标。
## 数据与后端
`src/data/papers.json`来自既有Paper Atlas的2026-11-06T13:58:30Z各表20行导出,按paper_id关联来源,保留标题、作者、摘要和metadata。首页显式标注“样本”;20条都是历史记录,不冒充热门或最新论文。
`PAPER_API_ORIGIN`为空使用样本;配置后只访问远程HTTP接口,错误会明确显示,不能静默退回样本。正在实现的共享后端接口见[论文API契约](docs/paper-api.md)。本机和浏览器不直连服务器SQLite、不保存数据库密码。
## 项目级技能
`.agents/skills/ui-craft-editorial`与其依赖`ui-craft`来自educlopez/ui-craft;`.agents/skills/beautiful-article`来自ConardLi/garden-skills。后者已包含`theme-profiles/vignelli.md`和`tufte.md`,不是两个单独上游skill。阅读页主题按这些authoring profiles在应用CSS中适配,不声称是ReActicle运行时。完整Beautiful Article harness仅在另行生成单篇HTML时应用。
## 当前验证边界
类型检查与16项测试已通过:除了Cookie/样本策略,测试直接调用生产认证代理,覆盖兑换、登录/邮件验证、续期、退出、错误、凭据隔离与请求限制。它们使用进程内模拟的Core响应,不代表真实OAuth成功,也不需要启动Next.js或编译。
12项隔离样本模式桌面/手机浏览器测试已通过,覆盖搜索、分页、空结果、文章摘要与引用复制、三种主题持久化、登录/注册表单和404恢复;截图用于人工核对布局;样本dev运行在17323,不占用17322预览。另有7项浏览器会话测试,使用隔离的实际Next dev副本和生产代理连接模拟Core,验证刷新/新标签恢复、HttpOnly Cookie、跨标签退出、故障重试、失效清理、Google/Slack模拟PKCE和邮箱验证的一次性兑换。测试不复制.env,不访问真实账户,结束后清理副本;新增2项隔离浏览器回归验证搜索错误恢复及异常数据处理,合计9项隔离浏览器测试通过;这些测试的Core响应仍为合成数据。另已在真实库上检查桌面/手机首页、简介、领域和全文搜索、超时后缩小搜索恢复。Google和Slack真实登录均已到达后端,用户分别确认刷新、新标签保持及退出正常;邮件实际投递与验证未实测。生产构建按用户要求未执行。前端目录为 `/Users/zzyang/thesisly`,远程已连接 Zayden-Jung/Thesisly,main 仅有用户授权的空初始化提交;论文 HTTP API 已纳入本目标,由代理在 Foundry Core 的 codex/research-api 分支实现。
## 当前后端依赖
论文库已完成 PostgreSQL 迁移及独立验收。当前本地忽略配置通过 SSH 回环隧道连接 ECS 上的临时 Core(真实库已验证3,526,999篇);浏览器不持有数据库连接串。临时 Core 仅启用真实论文只读连接,账户仍访问正式 Core。宽泛全文计数可能超时,页面会保留搜索表单并提示缩小范围,不显示伪造总数或退回样本。
用户已配置 Thesisly 的 FOUNDRY_PROJECTS,正式 Core 已通过仅配置重载生效,原有项目保留;Google/Slack 跳转与取消回调通过,Google/Slack真实身份及会话检查已由服务端观察和用户实测确认;邮件投递未实测。研究 API 的新代码尚未合入或部署正式后端。本阶段不触发CI,交付使用Gitea [skip ci]提交与PR标记并核对运行记录;本地build未执行,不采用SQLite sidecar,也不需要FOUNDRY_RESEARCH_DATABASE。
## Endoleap 参考
已核对 [Endoleap 主线 README](https://gitea.entroia.com/Zayden-Jung/Endoleap/src/branch/main/README.md)(blob `358824763e09811a85f8986526e93dda406fdb93`):沿用 App Router、同源身份代理、PKCE、HttpOnly Cookie、Core 控制的30天闲置/90天绝对会话期限与共享第三方回调。Thesisly 使用独立端口17322、项目键thesisly及服务端环境变量;不复制 Endoleap 的品牌、Canvas 功能或部署流程。浏览器表单测试不代表真实 Google/Slack 登录成功。
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

Some files were not shown because too many files have changed in this diff Show More