
Html
- 8 installs
- 7 repo stars
- Updated July 6, 2026
- julianoczkowski/html
Generate interactive HTML artifacts for planning, comparisons, and throwaway tools.
About
Teaches agents to produce self-contained HTML files instead of markdown walls. Useful for plans, status reports, design tokens, and interactive triage boards.
- Single-file portable artifacts
- Information has spatial and interactive shape
Html by the numbers
- 8 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,733 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianoczkowski/html --skill htmlAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 7 |
| Last updated | July 6, 2026 |
| Repository | julianoczkowski/html ↗ |
What it does
Generate interactive HTML artifacts for planning, comparisons, and throwaway tools.
Files
html
You are producing a single self-contained `.html` file, not a markdown document. The reader will open it directly in a browser. No build step, no framework, no install. Treat the file as the deliverable.
Based on Thariq Shihipar's *The unreasonable effectiveness of HTML* — the observation that markdown flattens information that is inherently spatial, comparative, or interactive, and a small HTML file fixes this.
What this skill is for (read this first)
Agent-generated artifacts for a human reader. Throwaway or reference, not production.
Concretely, the things this skill produces are:
- Working documents the reader will read once and act on (a plan, an options comparison, a PR writeup, a status report).
- Reference pages the reader will return to (a design-tokens page, a codemap, an explainer).
- Throwaway tools the reader uses for a few minutes to do a piece of work, then exports out of (a triage board, a feature-flag toggler, a motion tuner).
What this skill is not for:
- Production websites or apps shipped to end users. The conventions here (single file, no build step, no framework, no server, vanilla JS, in-memory state) exist for fast disposability, not for production quality. If the user is building something they ship to customers, this is the wrong tool — use a real frontend skill.
- Long-form prose with no shape (an email, a paragraph of analysis, a conversational reply). HTML doesn't earn its place when the content is just sentences. Use markdown.
- Things that need persistence, multi-user state, or a backend. State here lives in memory and dies on refresh. The export button is how work survives.
If you're not sure which side a request falls on, ask yourself: would a human be happy reading this once in a browser tab and then closing it? If yes, this skill applies. If they need to bookmark it, share it with twenty colleagues, and have it still work in six months — that's a different skill.
When this skill applies (signals)
If the user invoked /html or a pattern slash command directly, just produce HTML. They've already chosen.
Otherwise, the strong signals are:
- Comparison — "show me my options", "compare X vs Y", "trade-offs of A, B, C"
- Code review or PR — annotate a diff, write the PR description, map a codebase
- Design — tokens, component variants, contact sheet
- Motion — animation, transition, click-through where feel matters
- Diagram — flowchart, architecture, data flow, illustrative figures
- Deck — a short presentation you'd arrow-key through
- Explainer — concept or feature, with collapsibles and tabs
- Recurring report — weekly status, incident timeline, post-mortem
- Editor — a throwaway UI for the specific thing the user is wrestling with
- Table — rows × columns the reader needs to sort, filter, or scan
If the request is plain prose without shape, use markdown. HTML is not a universal hammer; it earns its place when the content has structure.
What every output looks like
Every artifact this skill produces obeys these conventions. Patterns assume you've followed them.
1. Single file. One .html. No external CSS, no external JS, no build step. Inline everything. 2. No network at render time. No CDN, no Google Fonts, no remote images. The artifact must render offline. 3. Locked look — Claude editorial system. Start from templates/base.html. Do not redesign tokens per artifact. The look (cream canvas #faf9f5, coral accent #cc785c, dark navy #181715, EB Garamond serif display at weight 400 with negative tracking, Inter / system sans body, JetBrains Mono code) is fixed across every artifact this skill produces so the reader recognizes the genre on sight. If you need to tune a token, change base.html, not the per-artifact <style>. 4. Mobile-readable. Viewport meta is in the template. Multi-column layouts collapse below ~720px. 5. Semantic HTML. <section>, <article>, <details>/<summary>, <figure>, <table>. Headings nest properly. 6. Information has shape. Don't pour markdown into a <div>. Side-by-side things go in a grid. Sequences go on a timeline. Risks go in a table. If the content has no shape, you're producing the wrong artifact — use markdown. 7. A "Prompt:" block near the top. Small italic callout with the user's request, near-verbatim. Makes the artifact legible later and makes it forkable. Already styled in the template as .prompt. 8. Inline SVG over images. The agent has a real pen — use it. Diagrams are <svg>, not <img>. 9. Vanilla JS only, sparingly. One <script> tag at the bottom. State in a single in-memory object. No React, no Vue, no jQuery, no framework. No localStorage unless explicitly requested. 10. Close the loop: export back to text. Any artifact the user can do something to (toggle, reorder, tune, edit) MUST end with a button that emits the result as something pasteable — markdown, JSON, a diff, a prompt. The artifact is throwaway; the export is the durable output. This is the single most important convention. 11. Export-first design. Decide what text the user will copy out before designing the UI. The data model serves that export; the UI mutates the data model; the data model must always be derivable into the export string. Applies to any interactive pattern — editor, motion, tokens, table, and interactive variants of plan or pr. Detailed in patterns/editor.md. 12. Live tuning uses primitives from the gallery. When the user asks to tune values live (sliders, toggles, drag-and-drop, sort, filter, swatch pickers), lift the primitive from gallery/interactions.html rather than writing one from scratch. Keeps interaction style consistent across artifacts.
How to start
In this order, every time:
1. Identify the pattern(s). Skim the table below. Pick the closest one. If a request spans two patterns (planning + comparison, explainer + diagram), pick a primary and borrow structure from the secondary — don't refuse to combine. 2. Read the relevant pattern playbook(s). patterns/<name>.md, ~70–150 lines each, opinionated and prescriptive. The playbook owns the shape of the artifact: sections, order, what the export emits. 3. Read the shared template. templates/base.html — boilerplate, CSS variables, utility classes. Start from this rather than rewriting it. Do not redefine the tokens. 4. Decide the export format before the UI (if interactive). What text would the user paste somewhere after they're done? Markdown? A diff? A JSON blob? A re-issuable prompt? The data model serves that export. 5. Write the file. One pass: head → header (title + prompt block) → main content → export button + script.
Reference files (don't copy structure from these)
The gallery/ folder is a look + interaction reference, not a shape reference. Each file shows primitives in isolation; structure for the artifact you're producing comes from the pattern playbook, not from gallery files.
- `gallery/look.html` — color swatches, type ramp, callout / chip / card / glance / timeline variants. After producing an artifact, mentally compare its surfaces, type, and chips to this file. If they don't match, your tokens drifted — fix them.
- `gallery/interactions.html` — sliders, toggles, switches, drag-and-drop columns, sortable lists, filter bars, segmented controls, swatch pickers, number steppers. Pull the primitive you need when the user wants to tune values live.
- `gallery/navigation.html` — tabs, arrow-key deck, sticky side-nav, anchor row, collapsible details, sticky export bar. Pull from here when the artifact needs to move the reader around.
Where the file goes
- If a working directory or output path is implied, write there.
- Otherwise: current working directory, kebab-case filename from the topic.
debounced-search-options.html,cycle-14-triage.html,comment-threads-plan.html. - After writing, tell the user the path and suggest they open it.
Patterns
Each lives at patterns/<name>.md. Read the one you need.
Universal patterns (work for any domain — software, marketing, research, ops, life admin):
| Pattern | When to use |
|---|---|
options | "Show me 2–4 ways to do X with trade-offs" — any decision before commitment |
plan | A milestoned plan with a timeline, mockups or sketches, and a risk table |
diagram | Hand-rolled SVG diagrams — flowcharts, architecture, illustrative figures |
deck | A short presentation, arrow-key navigable, single file |
explainer | Layered explanation — TL;DR, collapsibles, tabs, glossary |
status | Recurring report — weekly status, project update, post-mortem, retro |
editor | Throwaway UI for editing the specific thing at hand, with export-back-to-text |
table | Sortable, filterable tabular data — incidents, customers, line-items, anything with rows and columns |
Software-specific patterns (assume the user is working with code; descriptions reflect this):
| Pattern | When to use |
|---|---|
pr | Code-review writeup — annotated diff OR author's PR description |
codemap | Onboarding to an unfamiliar codebase — boxes, arrows, hot path, entry points |
tokens | Render a design system — live swatches, type ramp, component contact sheet |
motion | UI animation or click-through prototype where feel can't be described in prose |
Combining patterns
Real requests cross pattern boundaries. That's normal — combine, don't refuse.
Common combinations:
- plan + options. "Plan the feature, but show me three ways to do the realtime part." → Use
planas the spine, drop anoptionsblock inside section 02 (data flow) showing the alternatives for the contested decision. - explainer + diagram. "Explain how X works." Most good explainers have one or two diagrams. Use
explaineras the spine, embeddiagram-pattern SVG figures inline. - status + diagram. A post-mortem with an architecture diagram showing where the failure was. Use
statusas the spine, embed adiagramin the root-cause section. - editor + options. "Help me pick which of these features to cut, and show me what the cut and remaining feature lists look like." → An
editor(drag-to-cut) whose export emits two markdown lists.
When combining, pick the primary pattern by which one most determines the overall shape of the file. The secondary pattern contributes a section or a component, not the whole structure. Read both playbooks; resolve conflicts in favor of the primary.
Anti-patterns
- Markdown-in-a-`<div>`. If your
<body>is one big<div>of paragraphs and headers, you've produced HTML but not a useful artifact. The content has no shape — use markdown. - Generic-LLM aesthetics. Avoid the gradient hero, the floating shadowed card, rainbow emoji bullets. These telegraph "AI made this" and undermine authority. Use the base template's restrained tokens; don't reach for decoration.
- No export on an editor. An interactive artifact without a "copy as ..." button is half-built. The user can't get the work back out.
- Frameworks. No React, no Vue, no Tailwind, no Bootstrap. The point is no build step. (Exception: if the user explicitly asks for "a React component", that's a different request — not this skill.)
- External assets. System fonts, inline SVG, no
<link rel="stylesheet">, no<img src="https://...">. Render offline. - Overlong files. A good artifact in any of these patterns is 200–700 lines of HTML. Past 1000, you're building a website when the user wanted a page.
Self-check before declaring done
1. Does it open standalone with no network? 2. Is there a "Prompt:" block near the top? 3. Does the content have shape — grid, timeline, table, diagram — not stacked paragraphs? 4. If interactive: working export button? Export format chosen before the UI? 5. Mobile-readable (columns collapse, type scales)? 6. Kebab-case filename, descriptive of the topic? 7. Did you read the pattern playbook(s)? If no, go back. 8. Did you keep the locked look — cream canvas, coral accent, EB Garamond display, Inter body — straight from base.html? Compare visually against gallery/look.html if unsure.
.DS_Store
.idea/
.vscode/
*.swp
*.swo
.claude/
node_modules/
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gallery · Interaction primitives</title>
<style>
:root {
--canvas: #faf9f5;
--surface-soft: #f5f0e8;
--surface-card: #efe9de;
--surface-cream-strong: #e8e0d2;
--surface-dark: #181715;
--coral: #cc785c;
--coral-active: #a9583e;
--coral-soft: #f3e2d8;
--ink: #141413;
--body: #3d3d3a;
--muted: #6c6a64;
--muted-soft: #8e8b82;
--on-primary: #ffffff;
--on-dark: #faf9f5;
--hairline: #e6dfd8;
--good: #5db872;
--good-soft: #dff1e1;
--warn: #d4a017;
--bad: #c64545;
--bad-soft: #f7dee1;
--r-md: 8px;
--r-lg: 12px;
--r-pill: 9999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font: 16px/1.55 "Inter", -apple-system, sans-serif;
color: var(--body);
background: var(--canvas);
}
.wrap { max-width: 980px; margin: 0 auto; padding: 64px 24px 96px; }
h1, h2, h3 { font-family: "EB Garamond", Garamond, serif; font-weight: 400; color: var(--ink); margin: 0 0 0.4em; line-height: 1.1; }
h1 { font-size: 3rem; letter-spacing: -0.025em; }
h2 { font-size: 1.7rem; letter-spacing: -0.02em; margin-top: 2.8em; padding-top: 1.6em; border-top: 1px solid var(--hairline); }
h3 { font-size: 1.2rem; letter-spacing: -0.012em; margin-top: 1.6em; }
p { margin: 0 0 1em; }
code { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.85em; background: var(--surface-card); padding: 0.1em 0.4em; border-radius: 4px; color: var(--ink); }
pre {
background: var(--surface-dark);
color: var(--on-dark);
padding: 16px 18px;
border-radius: var(--r-md);
font-family: "JetBrains Mono", monospace;
font-size: 0.82rem;
line-height: 1.55;
overflow-x: auto;
margin: 1em 0 1.6em;
}
pre code { background: transparent; color: inherit; padding: 0; }
.kicker { font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.8em; font-weight: 500; }
.sub { color: var(--muted); font-size: 1.05rem; max-width: 640px; margin: 0 0 3em; }
.demo {
background: var(--surface-card);
border-radius: var(--r-lg);
padding: 24px;
margin: 1em 0 0;
}
.demo-label {
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 12px;
font-weight: 500;
}
.demo-out {
margin-top: 14px;
font-family: "JetBrains Mono", monospace;
font-size: 0.85rem;
color: var(--ink);
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
padding: 10px 14px;
min-height: 38px;
}
/* Range slider */
.slider-row {
display: grid;
grid-template-columns: 140px 1fr 64px;
align-items: center;
gap: 16px;
}
.slider-row label { font-weight: 500; color: var(--ink); font-size: 0.95rem; }
.slider-row .val {
font-family: "JetBrains Mono", monospace;
font-size: 0.92rem;
color: var(--coral-active);
text-align: right;
}
input[type="range"] {
appearance: none;
width: 100%;
height: 4px;
background: var(--hairline);
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 20px; height: 20px;
background: var(--coral);
border-radius: 50%;
cursor: grab;
border: 2px solid var(--canvas);
box-shadow: 0 0 0 1px var(--coral);
}
input[type="range"]::-webkit-slider-thumb:active { cursor: grabbing; background: var(--coral-active); }
input[type="range"]::-moz-range-thumb {
width: 20px; height: 20px;
background: var(--coral);
border-radius: 50%;
cursor: grab;
border: 2px solid var(--canvas);
box-shadow: 0 0 0 1px var(--coral);
}
/* Switch (toggle) */
.switch-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
padding: 12px 16px;
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
margin-bottom: 8px;
}
.switch-row .name { font-weight: 500; color: var(--ink); }
.switch-row .meta { font-size: 0.85rem; color: var(--muted); }
.switch {
position: relative;
display: inline-block;
width: 40px; height: 22px;
flex-shrink: 0;
}
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider-knob {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--muted-soft);
border-radius: 22px;
transition: 0.2s;
}
.switch .slider-knob::before {
position: absolute;
content: "";
height: 18px; width: 18px;
left: 2px; top: 2px;
background: white;
border-radius: 50%;
transition: 0.2s;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.switch input:checked + .slider-knob { background: var(--coral); }
.switch input:checked + .slider-knob::before { transform: translateX(18px); }
/* Segmented control */
.segmented {
display: inline-flex;
background: var(--surface-card);
border-radius: var(--r-md);
padding: 4px;
gap: 2px;
}
.segmented button {
appearance: none;
border: 0;
background: transparent;
color: var(--muted);
font: inherit;
font-family: "Inter", sans-serif;
font-weight: 500;
font-size: 0.88rem;
padding: 8px 14px;
border-radius: 6px;
cursor: pointer;
}
.segmented button.active {
background: var(--canvas);
color: var(--ink);
}
/* Color picker / swatch picker */
.swatch-picker {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.swatch-picker button {
appearance: none;
width: 36px; height: 36px;
border-radius: 50%;
border: 2px solid var(--canvas);
box-shadow: 0 0 0 1px var(--hairline);
cursor: pointer;
padding: 0;
}
.swatch-picker button.selected {
box-shadow: 0 0 0 2px var(--ink);
}
/* Drag-and-drop columns */
.columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
}
.column {
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
padding: 14px;
min-height: 140px;
}
.column h4 {
margin: 0 0 12px;
font-family: "Inter", sans-serif;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
display: flex;
justify-content: space-between;
}
.column h4 .count {
background: var(--surface-card);
color: var(--ink);
padding: 1px 8px;
border-radius: var(--r-pill);
font-size: 0.7rem;
font-weight: 600;
}
.card-item {
background: var(--surface-card);
border-radius: var(--r-md);
padding: 10px 12px;
margin-bottom: 8px;
cursor: grab;
font-size: 0.9rem;
color: var(--ink);
user-select: none;
}
.card-item:active { cursor: grabbing; }
.card-item.dragging { opacity: 0.4; }
.column.drop-target {
background: var(--coral-soft);
border-color: var(--coral);
outline: 2px dashed var(--coral);
outline-offset: -2px;
}
/* Sortable list */
.sortable-list { display: grid; gap: 8px; }
.sort-item {
display: grid;
grid-template-columns: 32px 1fr 80px;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
cursor: grab;
font-size: 0.92rem;
}
.sort-item:active { cursor: grabbing; }
.sort-item .rank {
font-family: "EB Garamond", Garamond, serif;
font-size: 1.1rem;
color: var(--muted);
text-align: center;
}
.sort-item .handle {
font-family: "JetBrains Mono", monospace;
color: var(--muted-soft);
text-align: right;
font-size: 0.85rem;
}
/* Filter input + chips */
.filter-bar {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 14px;
flex-wrap: wrap;
}
.filter-bar input[type="search"] {
flex: 1;
min-width: 200px;
background: var(--canvas);
color: var(--ink);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
padding: 10px 14px;
font: inherit;
font-family: "Inter", sans-serif;
}
.filter-bar input:focus {
outline: none;
border-color: var(--coral);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--coral) 15%, transparent);
}
.filter-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.filter-chip {
appearance: none;
background: var(--canvas);
border: 1px solid var(--hairline);
color: var(--muted);
font: inherit;
font-family: "Inter", sans-serif;
font-size: 0.82rem;
font-weight: 500;
padding: 5px 12px;
border-radius: var(--r-pill);
cursor: pointer;
}
.filter-chip.active { background: var(--coral); color: var(--on-primary); border-color: var(--coral); }
.filter-list { display: grid; gap: 6px; margin-top: 8px; }
.filter-row {
padding: 8px 12px;
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
font-size: 0.9rem;
display: flex;
justify-content: space-between;
}
.filter-row .tag {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 500;
}
/* Number stepper */
.stepper {
display: inline-flex;
align-items: center;
gap: 0;
border: 1px solid var(--hairline);
border-radius: var(--r-md);
background: var(--canvas);
overflow: hidden;
}
.stepper button {
appearance: none;
background: transparent;
border: 0;
color: var(--ink);
width: 36px; height: 36px;
font-size: 1.1rem;
cursor: pointer;
}
.stepper button:hover { background: var(--surface-card); }
.stepper .stepper-val {
width: 50px;
text-align: center;
font-family: "JetBrains Mono", monospace;
font-size: 0.95rem;
color: var(--ink);
}
/* Sticky export bar */
.export-bar {
position: sticky;
bottom: 0;
margin: 2.4em -24px -96px;
padding: 18px 24px;
background: color-mix(in srgb, var(--canvas) 92%, transparent);
backdrop-filter: blur(8px);
border-top: 1px solid var(--hairline);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.button-primary {
appearance: none; border: 0;
background: var(--coral);
color: var(--on-primary);
font: inherit;
font-family: "Inter", sans-serif;
font-weight: 500;
font-size: 0.92rem;
padding: 12px 22px;
border-radius: var(--r-md);
cursor: pointer;
}
.reset-link {
font-size: 0.88rem;
color: var(--muted);
background: none;
border: 0;
cursor: pointer;
padding: 0;
border-bottom: 1px solid var(--hairline);
}
@media (max-width: 720px) {
.wrap { padding: 32px 18px 64px; }
h1 { font-size: 2.1rem; }
.slider-row { grid-template-columns: 1fr; gap: 6px; }
.slider-row .val { text-align: left; }
}
</style>
</head>
<body>
<main class="wrap">
<div class="kicker">Gallery · Interaction primitives</div>
<h1>Interaction primitives</h1>
<p class="sub">Copy-paste primitives for any interactive artifact. Each demo is followed by the minimal HTML+JS needed to reproduce it. Lift only what you need. Don't include this entire file in your output — pull the relevant block.</p>
<h2>Range slider</h2>
<p>Use when the user wants to feel a continuous value — opacity, easing, font size, debounce ms, threshold.</p>
<div class="demo">
<div class="demo-label">Live</div>
<div class="slider-row">
<label for="s1">Opacity</label>
<input type="range" id="s1" min="0" max="100" value="60">
<span class="val" id="s1v">60%</span>
</div>
<div class="slider-row" style="margin-top:12px">
<label for="s2">Font size</label>
<input type="range" id="s2" min="12" max="48" value="20">
<span class="val" id="s2v">20px</span>
</div>
<div class="demo-out" id="s-out">opacity 60% · font 20px</div>
</div>
<pre><code><input type="range" id="s1" min="0" max="100" value="60">
<span id="s1v">60%</span>
<script>
const s1 = document.getElementById('s1');
const s1v = document.getElementById('s1v');
s1.addEventListener('input', () => { s1v.textContent = s1.value + '%'; });
</script></code></pre>
<h2>Switch (toggle)</h2>
<p>Use for boolean state. One per row. When meaning of one depends on another, use the dependency warning convention from <code>patterns/editor.md</code>.</p>
<div class="demo">
<div class="switch-row">
<div><div class="name">Auto-retry on failure</div><div class="meta">Retries up to 3 times with exponential backoff.</div></div>
<label class="switch"><input type="checkbox" checked><span class="slider-knob"></span></label>
</div>
<div class="switch-row">
<div><div class="name">Verbose logging</div><div class="meta">Emits each step to stdout.</div></div>
<label class="switch"><input type="checkbox"><span class="slider-knob"></span></label>
</div>
</div>
<pre><code><label class="switch">
<input type="checkbox" checked>
<span class="slider-knob"></span>
</label></code></pre>
<h2>Segmented control</h2>
<p>Use for mutually-exclusive choice of 2–5 options. Replaces a select dropdown when the values are short and the user benefits from seeing them all.</p>
<div class="demo">
<div class="segmented" id="seg1">
<button data-v="all" class="active">All</button>
<button data-v="open">Open</button>
<button data-v="closed">Closed</button>
</div>
<div class="demo-out" id="seg-out">filter = all</div>
</div>
<pre><code><div class="segmented">
<button data-v="all" class="active">All</button>
<button data-v="open">Open</button>
<button data-v="closed">Closed</button>
</div>
<script>
document.querySelectorAll('.segmented button').forEach(b => {
b.addEventListener('click', () => {
b.parentElement.querySelectorAll('button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
// state.filter = b.dataset.v; render();
});
});
</script></code></pre>
<h2>Swatch / color picker</h2>
<p>Use when the value is one of a small palette — theme color, severity, category.</p>
<div class="demo">
<div class="swatch-picker" id="sw">
<button data-c="#cc785c" class="selected" style="background:#cc785c"></button>
<button data-c="#5db872" style="background:#5db872"></button>
<button data-c="#d4a017" style="background:#d4a017"></button>
<button data-c="#c64545" style="background:#c64545"></button>
<button data-c="#181715" style="background:#181715"></button>
</div>
<div class="demo-out" id="sw-out">selected #cc785c</div>
</div>
<h2>Number stepper</h2>
<p>Use when the value is small-integer (1–20) and the user benefits from one-click increment.</p>
<div class="demo">
<div class="stepper">
<button id="step-dec">−</button>
<span class="stepper-val" id="step-v">3</span>
<button id="step-inc">+</button>
</div>
</div>
<h2>Drag-and-drop columns</h2>
<p>The bread and butter of <code>editor</code>-pattern artifacts. Real HTML5 drag API. Never fake with click-to-cycle.</p>
<div class="demo">
<div class="columns" id="board">
<div class="column" data-col="now">
<h4>Now <span class="count">2</span></h4>
<div class="card-item" data-id="t1" draggable="true">Migrate auth service</div>
<div class="card-item" data-id="t2" draggable="true">Backfill user table</div>
</div>
<div class="column" data-col="next">
<h4>Next <span class="count">1</span></h4>
<div class="card-item" data-id="t3" draggable="true">Rate-limit middleware</div>
</div>
<div class="column" data-col="later">
<h4>Later <span class="count">1</span></h4>
<div class="card-item" data-id="t4" draggable="true">Multi-region failover</div>
</div>
<div class="column" data-col="cut">
<h4>Cut <span class="count">0</span></h4>
</div>
</div>
</div>
<pre><code><script>
let dragId = null;
document.querySelectorAll('.card-item').forEach(card => {
card.addEventListener('dragstart', () => {
dragId = card.dataset.id;
card.classList.add('dragging');
});
card.addEventListener('dragend', () => card.classList.remove('dragging'));
});
document.querySelectorAll('.column').forEach(col => {
col.addEventListener('dragover', e => { e.preventDefault(); col.classList.add('drop-target'); });
col.addEventListener('dragleave', () => col.classList.remove('drop-target'));
col.addEventListener('drop', e => {
e.preventDefault();
col.classList.remove('drop-target');
const card = document.querySelector(`.card-item[data-id="${dragId}"]`);
if (card) col.appendChild(card);
// update state.items[dragId].column = col.dataset.col;
// recount + render();
});
});
</script></code></pre>
<h2>Sortable list (reorder)</h2>
<p>Drag items vertically to reorder. Use for ranking, prioritization, agenda.</p>
<div class="demo">
<div class="sortable-list" id="sortable">
<div class="sort-item" draggable="true" data-id="r1"><span class="rank">1</span><span>Performance regression in feed renderer</span><span class="handle">drag ↕</span></div>
<div class="sort-item" draggable="true" data-id="r2"><span class="rank">2</span><span>Stale cache on profile load</span><span class="handle">drag ↕</span></div>
<div class="sort-item" draggable="true" data-id="r3"><span class="rank">3</span><span>Login captcha not localized</span><span class="handle">drag ↕</span></div>
<div class="sort-item" draggable="true" data-id="r4"><span class="rank">4</span><span>Sidebar collapse animation jitter</span><span class="handle">drag ↕</span></div>
</div>
</div>
<h2>Filter (search + chips)</h2>
<p>Use when the user is scanning a list and needs to slice it. Combine free-text search with category chips.</p>
<div class="demo">
<div class="filter-bar">
<input type="search" id="q" placeholder="Search...">
<div class="filter-chips" id="chips">
<button class="filter-chip active" data-f="all">All</button>
<button class="filter-chip" data-f="bug">Bug</button>
<button class="filter-chip" data-f="feature">Feature</button>
<button class="filter-chip" data-f="chore">Chore</button>
</div>
</div>
<div class="filter-list" id="filtered">
<div class="filter-row" data-tag="bug"><span>Login form double-submits</span><span class="tag">bug</span></div>
<div class="filter-row" data-tag="feature"><span>Add dark mode toggle</span><span class="tag">feature</span></div>
<div class="filter-row" data-tag="chore"><span>Bump axios to 1.7</span><span class="tag">chore</span></div>
<div class="filter-row" data-tag="bug"><span>Off-by-one in pagination</span><span class="tag">bug</span></div>
</div>
</div>
<h2>Sticky export bar</h2>
<p>The bottom of every interactive artifact. Reset link on the left (small + quiet), primary CTA on the right (big + coral). One primary CTA per page.</p>
<p>Scroll to the bottom of this file to see it in action.</p>
<h2>How to use this file</h2>
<p>When you need an interaction, find the closest demo above, copy its HTML + JS into your artifact, and adapt. Don't write interactions from scratch — every artifact then drifts in its own direction. Match the look (cream + coral + hairline) by reusing the tokens.</p>
<p>This file shows <em>interactions</em>. Shape is per-pattern. Look is in <code>gallery/look.html</code>.</p>
</main>
<div class="export-bar">
<button class="reset-link" type="button">Reset</button>
<button class="button-primary" id="copyBtn">Copy as markdown</button>
</div>
<script>
// Slider
const s1 = document.getElementById('s1');
const s1v = document.getElementById('s1v');
const s2 = document.getElementById('s2');
const s2v = document.getElementById('s2v');
const sOut = document.getElementById('s-out');
function renderSlider() {
s1v.textContent = s1.value + '%';
s2v.textContent = s2.value + 'px';
sOut.textContent = `opacity ${s1.value}% · font ${s2.value}px`;
}
s1.addEventListener('input', renderSlider);
s2.addEventListener('input', renderSlider);
// Segmented
document.querySelectorAll('#seg1 button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#seg1 button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
document.getElementById('seg-out').textContent = 'filter = ' + b.dataset.v;
});
});
// Swatches
document.querySelectorAll('#sw button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#sw button').forEach(x => x.classList.remove('selected'));
b.classList.add('selected');
document.getElementById('sw-out').textContent = 'selected ' + b.dataset.c;
});
});
// Stepper
let step = 3;
const stepV = document.getElementById('step-v');
document.getElementById('step-inc').addEventListener('click', () => { step++; stepV.textContent = step; });
document.getElementById('step-dec').addEventListener('click', () => { if (step > 0) { step--; stepV.textContent = step; } });
// Drag columns
let dragId = null;
function bindCard(card) {
card.addEventListener('dragstart', () => { dragId = card.dataset.id; card.classList.add('dragging'); });
card.addEventListener('dragend', () => card.classList.remove('dragging'));
}
document.querySelectorAll('.card-item').forEach(bindCard);
document.querySelectorAll('.column').forEach(col => {
col.addEventListener('dragover', e => { e.preventDefault(); col.classList.add('drop-target'); });
col.addEventListener('dragleave', () => col.classList.remove('drop-target'));
col.addEventListener('drop', e => {
e.preventDefault();
col.classList.remove('drop-target');
const card = document.querySelector(`.card-item[data-id="${dragId}"]`);
if (card) col.appendChild(card);
document.querySelectorAll('.column').forEach(c => {
c.querySelector('.count').textContent = c.querySelectorAll('.card-item').length;
});
});
});
// Sortable
let sortDragId = null;
function bindSortItem(item) {
item.addEventListener('dragstart', () => { sortDragId = item.dataset.id; item.classList.add('dragging'); });
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
renderRanks();
});
item.addEventListener('dragover', e => {
e.preventDefault();
const dragging = document.querySelector(`.sort-item[data-id="${sortDragId}"]`);
if (dragging && dragging !== item) {
const rect = item.getBoundingClientRect();
const after = e.clientY > rect.top + rect.height / 2;
item.parentElement.insertBefore(dragging, after ? item.nextSibling : item);
}
});
}
function renderRanks() {
document.querySelectorAll('#sortable .sort-item').forEach((it, i) => {
it.querySelector('.rank').textContent = i + 1;
});
}
document.querySelectorAll('#sortable .sort-item').forEach(bindSortItem);
// Filter
let activeFilter = 'all';
const q = document.getElementById('q');
function applyFilter() {
const query = q.value.toLowerCase();
document.querySelectorAll('.filter-row').forEach(r => {
const matchTag = activeFilter === 'all' || r.dataset.tag === activeFilter;
const matchText = r.textContent.toLowerCase().includes(query);
r.style.display = (matchTag && matchText) ? '' : 'none';
});
}
q.addEventListener('input', applyFilter);
document.querySelectorAll('#chips .filter-chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#chips .filter-chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
activeFilter = c.dataset.f;
applyFilter();
});
});
// Export demo
document.getElementById('copyBtn').addEventListener('click', async (e) => {
const txt = `# Interaction state\n\nopacity: ${s1.value}%\nfont: ${s2.value}px\n`;
await navigator.clipboard.writeText(txt);
const label = e.target.textContent;
e.target.textContent = 'Copied';
e.target.dataset.copied = '1';
setTimeout(() => { e.target.textContent = label; e.target.dataset.copied = ''; }, 1500);
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gallery · Look reference</title>
<style>
:root {
--canvas: #faf9f5;
--surface-soft: #f5f0e8;
--surface-card: #efe9de;
--surface-cream-strong: #e8e0d2;
--surface-dark: #181715;
--surface-dark-elevated: #252320;
--surface-dark-soft: #1f1e1b;
--coral: #cc785c;
--coral-active: #a9583e;
--coral-soft: #f3e2d8;
--ink: #141413;
--body-strong: #252523;
--body: #3d3d3a;
--muted: #6c6a64;
--muted-soft: #8e8b82;
--on-primary: #ffffff;
--on-dark: #faf9f5;
--on-dark-soft: #a09d96;
--hairline: #e6dfd8;
--hairline-soft: #ebe6df;
--good: #5db872;
--good-soft: #dff1e1;
--warn: #d4a017;
--warn-soft: #fbeed4;
--bad: #c64545;
--bad-soft: #f7dee1;
--r-md: 8px;
--r-lg: 12px;
--r-pill: 9999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font: 16px/1.55 "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: var(--body);
background: var(--canvas);
}
.wrap { max-width: 1040px; margin: 0 auto; padding: 64px 24px 96px; }
h1, h2, h3 {
font-family: "EB Garamond", "Cormorant Garamond", "Tiempos Headline", Garamond, serif;
font-weight: 400;
color: var(--ink);
margin: 0 0 0.4em;
line-height: 1.1;
}
h1 { font-size: 3rem; letter-spacing: -0.025em; }
h2 { font-size: 1.8rem; letter-spacing: -0.02em; margin-top: 3em; padding-top: 1.6em; border-top: 1px solid var(--hairline); }
h3 { font-size: 1.25rem; letter-spacing: -0.012em; margin-top: 1.8em; }
p { margin: 0 0 1em; }
code { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.85em; background: var(--surface-card); padding: 0.1em 0.4em; border-radius: 4px; color: var(--ink); }
.kicker {
font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--muted); margin-bottom: 0.8em; font-weight: 500;
}
.sub {
color: var(--muted); font-size: 1.05rem;
max-width: 640px; margin: 0 0 3em;
}
/* Swatch grid */
.swatch-grid {
display: grid; gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
margin: 1em 0 2em;
}
.swatch {
border: 1px solid var(--hairline);
border-radius: var(--r-md);
overflow: hidden;
background: var(--canvas);
}
.swatch .fill {
display: block;
width: 100%;
height: 64px;
border-bottom: 1px solid var(--hairline);
}
.swatch .meta { padding: 10px 12px; }
.swatch .name { font-size: 0.88rem; font-weight: 500; color: var(--ink); }
.swatch .hex {
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
color: var(--muted);
margin-top: 2px;
}
/* Type ramp */
.ramp-row {
display: grid;
grid-template-columns: 100px 1fr;
align-items: baseline;
gap: 24px;
padding: 14px 0;
border-bottom: 1px solid var(--hairline-soft);
}
.ramp-label {
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.ramp-sample-h1 { font-family: "EB Garamond", Garamond, serif; font-weight: 400; font-size: 3rem; letter-spacing: -0.025em; line-height: 1.05; color: var(--ink); }
.ramp-sample-h2 { font-family: "EB Garamond", Garamond, serif; font-weight: 400; font-size: 2.1rem; letter-spacing: -0.02em; line-height: 1.1; color: var(--ink); }
.ramp-sample-h3 { font-family: "EB Garamond", Garamond, serif; font-weight: 400; font-size: 1.45rem; letter-spacing: -0.012em; line-height: 1.2; color: var(--ink); }
.ramp-sample-body { font-family: "Inter", sans-serif; font-weight: 400; font-size: 1rem; color: var(--body); }
.ramp-sample-small { font-family: "Inter", sans-serif; font-weight: 400; font-size: 0.88rem; color: var(--muted); }
.ramp-sample-caps { font-family: "Inter", sans-serif; font-weight: 500; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); }
.ramp-sample-mono { font-family: "JetBrains Mono", monospace; font-weight: 400; font-size: 0.92rem; color: var(--ink); }
/* Surfaces strip */
.surfaces { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.surface-box {
height: 110px;
border-radius: var(--r-md);
border: 1px solid var(--hairline);
display: flex;
align-items: flex-end;
padding: 10px 12px;
font-size: 0.78rem;
color: var(--muted);
font-family: "JetBrains Mono", monospace;
}
.surface-box.canvas { background: var(--canvas); }
.surface-box.soft { background: var(--surface-soft); }
.surface-box.card { background: var(--surface-card); }
.surface-box.cream-strong { background: var(--surface-cream-strong); }
.surface-box.coral { background: var(--coral); color: var(--on-primary); border-color: var(--coral-active); }
.surface-box.dark { background: var(--surface-dark); color: var(--on-dark-soft); border-color: var(--surface-dark); }
.surface-box.dark-elev { background: var(--surface-dark-elevated); color: var(--on-dark-soft); border-color: var(--surface-dark-elevated); }
/* Callouts */
.callout {
padding: 16px 20px;
border-radius: 0 var(--r-md) var(--r-md) 0;
margin: 12px 0;
border-left: 3px solid;
background: var(--surface-card);
}
.callout > strong:first-child {
display: block; font-size: 0.68rem; font-weight: 600;
letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 4px;
}
.callout.info { background: var(--coral-soft); border-color: var(--coral); }
.callout.info > strong:first-child { color: var(--coral); }
.callout.warn { background: var(--warn-soft); border-color: var(--warn); }
.callout.warn > strong:first-child { color: var(--warn); }
.callout.bad { background: var(--bad-soft); border-color: var(--bad); }
.callout.bad > strong:first-child { color: var(--bad); }
.callout.good { background: var(--good-soft); border-color: var(--good); }
.callout.good > strong:first-child { color: var(--good); }
/* Coral callout */
.callout-coral {
background: var(--coral);
color: var(--on-primary);
border-radius: var(--r-lg);
padding: 48px;
margin: 1.6em 0;
}
.callout-coral h3 { color: var(--on-primary); margin-top: 0; }
.callout-coral p { color: var(--on-primary); opacity: 0.95; }
/* Dark surface */
.surface-dark-block {
background: var(--surface-dark);
color: var(--on-dark);
border-radius: var(--r-lg);
padding: 32px;
margin: 1.6em 0;
}
.surface-dark-block h3 { color: var(--on-dark); margin-top: 0; }
.surface-dark-block p { color: var(--on-dark-soft); }
.surface-dark-block code { background: var(--surface-dark-soft); color: var(--on-dark); }
.surface-dark-block pre {
background: var(--surface-dark-soft);
color: var(--on-dark);
padding: 16px 18px;
border-radius: var(--r-md);
font-family: "JetBrains Mono", monospace;
font-size: 0.88rem;
line-height: 1.55;
overflow-x: auto;
margin: 12px 0 0;
}
/* Chips & badges */
.chip {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: var(--r-pill);
vertical-align: middle;
background: var(--surface-card);
color: var(--muted);
margin: 4px 4px 4px 0;
}
.chip.coral { background: var(--coral); color: var(--on-primary); }
.chip.info { background: var(--coral-soft); color: var(--coral-active); }
.chip.warn { background: var(--warn-soft); color: var(--warn); }
.chip.bad { background: var(--bad-soft); color: var(--bad); }
.chip.good { background: var(--good-soft); color: var(--good); }
/* Cards */
.card-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin: 1em 0 2em; }
.card {
border: 1px solid var(--hairline);
border-radius: var(--r-lg);
padding: 24px;
background: var(--surface-card);
}
.card .num { font-family: "EB Garamond", Garamond, serif; font-size: 1rem; color: var(--muted); margin-right: 8px; }
.card h4 { font-family: "EB Garamond", Garamond, serif; font-weight: 400; font-size: 1.25rem; letter-spacing: -0.012em; color: var(--ink); margin: 0 0 0.4em; }
.card .summary { color: var(--body); font-size: 0.95rem; margin: 0.4em 0 1em; }
.card.featured { background: var(--surface-dark); border: 0; }
.card.featured h4 { color: var(--on-dark); }
.card.featured .summary { color: var(--on-dark-soft); }
.card.featured .num { color: var(--on-dark-soft); }
/* Glance row */
.glance {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 16px;
margin: 1.2em 0 2em;
padding: 24px;
background: var(--surface-card);
border-radius: var(--r-lg);
}
.glance .stat .label { font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; font-weight: 500; }
.glance .stat .val { font-family: "EB Garamond", Garamond, serif; font-size: 1.7rem; font-weight: 400; color: var(--ink); letter-spacing: -0.01em; }
/* Buttons */
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin: 1em 0 2em; }
.button-primary, .button-secondary, .button-text {
font: inherit; font-family: "Inter", sans-serif; font-weight: 500; font-size: 0.92rem;
cursor: pointer; border-radius: var(--r-md);
}
.button-primary {
background: var(--coral); color: var(--on-primary); border: 0;
padding: 12px 22px;
}
.button-primary:hover { background: var(--coral-active); }
.button-secondary {
background: var(--canvas); color: var(--ink);
border: 1px solid var(--hairline);
padding: 11px 20px;
}
.button-secondary:hover { border-color: var(--muted); }
.button-text {
background: none; border: 0; color: var(--coral);
padding: 4px 0;
border-bottom: 1px solid color-mix(in srgb, var(--coral) 30%, transparent);
border-radius: 0;
}
/* Timeline */
.timeline { position: relative; padding-left: 32px; margin: 1.4em 0 2em; }
.timeline::before { content: ""; position: absolute; left: 8px; top: 0; bottom: 0; width: 2px; background: var(--hairline); }
.timeline .event { position: relative; padding: 6px 0 14px; }
.timeline .event::before {
content: ""; position: absolute; left: -32px; top: 12px;
width: 12px; height: 12px; border-radius: 50%;
background: var(--muted-soft);
border: 2px solid var(--canvas);
margin-left: 2px;
}
.timeline .event.info::before { background: var(--coral); }
.timeline .event.good::before { background: var(--good); }
.timeline .event.warn::before { background: var(--warn); }
.timeline .event .when {
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
color: var(--muted-soft);
}
.timeline .event h4 {
margin: 4px 0;
font-family: "Inter", sans-serif;
font-weight: 500;
font-size: 1rem;
color: var(--ink);
}
/* Prompt block sample */
.prompt {
font-size: 0.95rem;
font-style: italic;
color: var(--body);
background: var(--surface-card);
border-left: 3px solid var(--coral);
padding: 14px 18px;
border-radius: 0 var(--r-md) var(--r-md) 0;
margin: 0 0 2em;
}
.prompt::before {
content: "Prompt";
display: block;
font-style: normal;
font-weight: 500;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--coral);
margin-bottom: 4px;
}
@media (max-width: 720px) {
.wrap { padding: 32px 18px 64px; }
h1 { font-size: 2.1rem; }
h2 { font-size: 1.4rem; }
.callout-coral { padding: 28px; }
.surface-dark-block { padding: 22px; }
}
</style>
</head>
<body>
<main class="wrap">
<div class="kicker">Gallery · Look reference</div>
<h1>Look reference</h1>
<p class="sub">Every visual primitive in the skill. No narrative content. Use this file to verify that an artifact you produced looks like an artifact this skill produces. Don't copy the structure here — the structure of your artifact comes from the pattern playbook.</p>
<p class="prompt">This is what a prompt block looks like at the top of every artifact. Verbatim user request, near-verbatim.</p>
<h2>Surfaces</h2>
<p>Cream canvas is the default floor. Coral is scarce, used on primary CTAs and on one full-bleed callout per artifact. Dark navy is the pacing partner — code blocks, mockups, post-mortem panels.</p>
<div class="surfaces">
<div class="surface-box canvas">canvas #faf9f5</div>
<div class="surface-box soft">surface-soft #f5f0e8</div>
<div class="surface-box card">surface-card #efe9de</div>
<div class="surface-box cream-strong">surface-cream-strong #e8e0d2</div>
<div class="surface-box coral">coral #cc785c</div>
<div class="surface-box dark">surface-dark #181715</div>
<div class="surface-box dark-elev">surface-dark-elev #252320</div>
</div>
<h2>Color tokens</h2>
<h3>Brand & ink</h3>
<div class="swatch-grid">
<div class="swatch"><div class="fill" style="background:#cc785c"></div><div class="meta"><div class="name">coral</div><div class="hex">#cc785c</div></div></div>
<div class="swatch"><div class="fill" style="background:#a9583e"></div><div class="meta"><div class="name">coral-active</div><div class="hex">#a9583e</div></div></div>
<div class="swatch"><div class="fill" style="background:#f3e2d8"></div><div class="meta"><div class="name">coral-soft</div><div class="hex">#f3e2d8</div></div></div>
<div class="swatch"><div class="fill" style="background:#141413"></div><div class="meta"><div class="name">ink</div><div class="hex">#141413</div></div></div>
<div class="swatch"><div class="fill" style="background:#3d3d3a"></div><div class="meta"><div class="name">body</div><div class="hex">#3d3d3a</div></div></div>
<div class="swatch"><div class="fill" style="background:#6c6a64"></div><div class="meta"><div class="name">muted</div><div class="hex">#6c6a64</div></div></div>
<div class="swatch"><div class="fill" style="background:#8e8b82"></div><div class="meta"><div class="name">muted-soft</div><div class="hex">#8e8b82</div></div></div>
<div class="swatch"><div class="fill" style="background:#e6dfd8"></div><div class="meta"><div class="name">hairline</div><div class="hex">#e6dfd8</div></div></div>
</div>
<h3>Semantic</h3>
<div class="swatch-grid">
<div class="swatch"><div class="fill" style="background:#5db872"></div><div class="meta"><div class="name">good</div><div class="hex">#5db872</div></div></div>
<div class="swatch"><div class="fill" style="background:#dff1e1"></div><div class="meta"><div class="name">good-soft</div><div class="hex">#dff1e1</div></div></div>
<div class="swatch"><div class="fill" style="background:#d4a017"></div><div class="meta"><div class="name">warn</div><div class="hex">#d4a017</div></div></div>
<div class="swatch"><div class="fill" style="background:#fbeed4"></div><div class="meta"><div class="name">warn-soft</div><div class="hex">#fbeed4</div></div></div>
<div class="swatch"><div class="fill" style="background:#c64545"></div><div class="meta"><div class="name">bad</div><div class="hex">#c64545</div></div></div>
<div class="swatch"><div class="fill" style="background:#f7dee1"></div><div class="meta"><div class="name">bad-soft</div><div class="hex">#f7dee1</div></div></div>
</div>
<h2>Type ramp</h2>
<p>Display = EB Garamond (Copernicus stand-in) at weight 400 with negative letter-spacing. Body = Inter / system humanist sans. Mono = JetBrains Mono / ui-monospace. Display weight stays 400; never bold.</p>
<div class="ramp-row"><div class="ramp-label">h1</div><div class="ramp-sample-h1">Meet your thinking partner</div></div>
<div class="ramp-row"><div class="ramp-label">h2</div><div class="ramp-sample-h2">The unreasonable effectiveness of HTML</div></div>
<div class="ramp-row"><div class="ramp-label">h3</div><div class="ramp-sample-h3">Spatial information markdown flattens</div></div>
<div class="ramp-row"><div class="ramp-label">body</div><div class="ramp-sample-body">Default running text in Inter at 16px, line-height 1.55, weight 400. This is the workhorse paragraph style.</div></div>
<div class="ramp-row"><div class="ramp-label">small</div><div class="ramp-sample-small">Fine print, secondary text, captions. Muted ink color.</div></div>
<div class="ramp-row"><div class="ramp-label">caps</div><div class="ramp-sample-caps">Section labels · kicker · category tags</div></div>
<div class="ramp-row"><div class="ramp-label">mono</div><div class="ramp-sample-mono">const example = "JetBrains Mono"</div></div>
<h2>Callouts</h2>
<div class="callout info"><strong>Info</strong>Short directive content lives in callouts. Coral-soft for info, the brand-toned voice.</div>
<div class="callout good"><strong>Decision</strong>Resolved item. The green tint signals "settled, no longer a risk."</div>
<div class="callout warn"><strong>Watch</strong>Yellow callouts are open questions or known risks — not yet bad, but not yet OK.</div>
<div class="callout bad"><strong>Blocker</strong>Red callouts are blockers. Use scarcely; if everything is red, nothing is.</div>
<h2>Coral callout (the voltage moment)</h2>
<p>One per artifact, max. Full-bleed coral. The CTA inside flips to a cream-tinted button so it reads against the coral fill.</p>
<div class="callout-coral">
<h3>Ship the artifact, not the markdown</h3>
<p>This is the brand-voltage callout. Use it for the headline takeaway, the single sentence the reader should leave with. If your artifact has two of these, you have one too many.</p>
<button class="button-secondary">Read the essay</button>
</div>
<h2>Dark surface (pacing partner)</h2>
<p>Use dark for code mockups, model showcases, post-mortem root-cause panels — anywhere the reader is looking at product chrome or raw output. Don't put two dark blocks back-to-back; alternate with cream.</p>
<div class="surface-dark-block">
<h3>Stack trace</h3>
<p>Real product chrome belongs on dark.</p>
<pre><code>TypeError: cannot read property 'foo' of undefined
at handleRequest (server.js:142:18)
at process.nextTick (internal/timers.js:438:13)</code></pre>
</div>
<h2>Chips & badges</h2>
<div>
<span class="chip">default</span>
<span class="chip coral">primary</span>
<span class="chip info">info</span>
<span class="chip good">shipped</span>
<span class="chip warn">at risk</span>
<span class="chip bad">blocked</span>
</div>
<h2>Cards (cream & featured)</h2>
<div class="card-row">
<div class="card">
<h4><span class="num">01</span>Cream card</h4>
<div class="summary">Default card surface. Used for feature blocks, option columns, content sections.</div>
</div>
<div class="card">
<h4><span class="num">02</span>Second cream card</h4>
<div class="summary">Always pair with at least one sibling. Single card in a grid is a code smell — use a callout instead.</div>
</div>
<div class="card featured">
<h4><span class="num">03</span>Featured (dark)</h4>
<div class="summary">Dark card variant — use for the recommended option, the chosen plan, the live model.</div>
</div>
</div>
<h2>At-a-glance row</h2>
<div class="glance">
<div class="stat"><div class="label">Owner</div><div class="val">A. Lin</div></div>
<div class="stat"><div class="label">Target</div><div class="val">Q3</div></div>
<div class="stat"><div class="label">Status</div><div class="val">On track</div></div>
<div class="stat"><div class="label">Risk</div><div class="val">Low</div></div>
</div>
<h2>Timeline</h2>
<div class="timeline">
<div class="event info"><div class="when">Apr 12</div><h4>Kickoff</h4><p>Scoping complete; team aligned on three candidate approaches.</p></div>
<div class="event"><div class="when">May 03</div><h4>Prototype</h4><p>First working slice. Internal demo on 04/30.</p></div>
<div class="event warn"><div class="when">May 19</div><h4>Cross-team review</h4><p>Open question on data-retention policy — see callout above.</p></div>
<div class="event good"><div class="when">Jun 07</div><h4>Beta launch</h4><p>Shipped to internal beta cohort. Metrics dashboard up.</p></div>
</div>
<h2>Buttons</h2>
<div class="btn-row">
<button class="button-primary">Copy as markdown</button>
<button class="button-secondary">Reset</button>
<button class="button-text">View source</button>
</div>
<h2>How to use this file</h2>
<p>Before producing an artifact, open this file mentally as a reference. After producing it, ask: do the surfaces, type, and chips look like the ones in this file? If yes, the look is right. If not, your tokens have drifted — rewrite them to match <code>templates/base.html</code>.</p>
<p>This file shows <em>look</em>, not <em>shape</em>. Shape — what sections to include, what order, what the export emits — lives in the pattern playbooks.</p>
</main>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gallery · Navigation primitives</title>
<style>
:root {
--canvas: #faf9f5;
--surface-soft: #f5f0e8;
--surface-card: #efe9de;
--surface-cream-strong: #e8e0d2;
--surface-dark: #181715;
--coral: #cc785c;
--coral-active: #a9583e;
--coral-soft: #f3e2d8;
--ink: #141413;
--body: #3d3d3a;
--muted: #6c6a64;
--muted-soft: #8e8b82;
--on-primary: #ffffff;
--on-dark: #faf9f5;
--hairline: #e6dfd8;
--good: #5db872;
--r-md: 8px;
--r-lg: 12px;
--r-pill: 9999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font: 16px/1.55 "Inter", -apple-system, sans-serif; color: var(--body); background: var(--canvas); }
.wrap { max-width: 980px; margin: 0 auto; padding: 64px 24px 96px; }
h1, h2, h3 { font-family: "EB Garamond", Garamond, serif; font-weight: 400; color: var(--ink); margin: 0 0 0.4em; line-height: 1.1; }
h1 { font-size: 3rem; letter-spacing: -0.025em; }
h2 { font-size: 1.7rem; letter-spacing: -0.02em; margin-top: 2.8em; padding-top: 1.6em; border-top: 1px solid var(--hairline); }
h3 { font-size: 1.2rem; letter-spacing: -0.012em; margin-top: 1.6em; }
p { margin: 0 0 1em; }
code { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.85em; background: var(--surface-card); padding: 0.1em 0.4em; border-radius: 4px; color: var(--ink); }
pre {
background: var(--surface-dark);
color: var(--on-dark);
padding: 16px 18px;
border-radius: var(--r-md);
font-family: "JetBrains Mono", monospace;
font-size: 0.82rem;
line-height: 1.55;
overflow-x: auto;
margin: 1em 0 1.6em;
}
pre code { background: transparent; color: inherit; padding: 0; }
.kicker { font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.8em; font-weight: 500; }
.sub { color: var(--muted); font-size: 1.05rem; max-width: 640px; margin: 0 0 3em; }
.demo {
background: var(--surface-card);
border-radius: var(--r-lg);
padding: 24px;
margin: 1em 0 0;
}
/* ---- Tabs ---- */
.tabs {
display: flex;
gap: 4px;
border-bottom: 1px solid var(--hairline);
margin-bottom: 18px;
flex-wrap: wrap;
}
.tabs button {
appearance: none;
background: transparent;
border: 0;
color: var(--muted);
font: inherit;
font-family: "Inter", sans-serif;
font-weight: 500;
font-size: 0.92rem;
padding: 10px 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tabs button:hover { color: var(--ink); }
.tabs button.active {
color: var(--ink);
border-bottom-color: var(--coral);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ---- Deck (slides) ---- */
.deck {
background: var(--canvas);
border: 1px solid var(--hairline);
border-radius: var(--r-md);
min-height: 240px;
padding: 36px 32px;
position: relative;
}
.slide { display: none; }
.slide.active { display: block; animation: fadein 0.2s ease; }
@keyframes fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.slide h3 {
font-family: "EB Garamond", Garamond, serif;
font-weight: 400;
font-size: 1.8rem;
letter-spacing: -0.02em;
color: var(--ink);
margin: 0 0 0.4em;
}
.slide p { color: var(--body); font-size: 1.05rem; }
.deck-meta {
position: absolute;
bottom: 14px;
right: 18px;
font-family: "JetBrains Mono", monospace;
font-size: 0.8rem;
color: var(--muted-soft);
}
.deck-nav {
position: absolute;
bottom: 12px;
left: 18px;
display: flex;
gap: 8px;
}
.deck-nav button {
appearance: none;
background: var(--canvas);
border: 1px solid var(--hairline);
color: var(--ink);
width: 32px; height: 32px;
border-radius: 50%;
font-size: 1rem;
cursor: pointer;
}
.deck-nav button:hover { border-color: var(--coral); color: var(--coral); }
.deck-hint {
margin-top: 12px;
font-size: 0.82rem;
color: var(--muted);
}
.deck-hint kbd {
font-family: "JetBrains Mono", monospace;
font-size: 0.78rem;
background: var(--surface-card);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--hairline);
}
/* ---- Side nav ---- */
.with-side {
display: grid;
grid-template-columns: 200px 1fr;
gap: 24px;
}
.side-nav {
position: sticky;
top: 24px;
align-self: start;
font-size: 0.9rem;
}
.side-nav .label {
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
font-weight: 500;
}
.side-nav a {
display: block;
padding: 6px 12px;
color: var(--muted);
text-decoration: none;
border-left: 2px solid var(--hairline);
border-bottom: 0;
}
.side-nav a.active {
color: var(--ink);
border-left-color: var(--coral);
background: var(--coral-soft);
}
.side-nav a:hover { color: var(--ink); }
.side-content section { padding: 12px 0; }
.side-content section h4 {
font-family: "EB Garamond", Garamond, serif;
font-weight: 400;
font-size: 1.3rem;
letter-spacing: -0.012em;
color: var(--ink);
margin: 0 0 0.4em;
}
/* ---- Collapsible details ---- */
details {
border: 1px solid var(--hairline);
border-radius: var(--r-md);
padding: 14px 18px;
margin: 8px 0;
background: var(--canvas);
}
details > summary {
cursor: pointer;
font-weight: 500;
color: var(--ink);
list-style: none;
}
details > summary::before {
content: "›";
display: inline-block;
margin-right: 10px;
color: var(--muted);
transition: transform 0.15s;
}
details[open] > summary::before { transform: rotate(90deg); }
details > *:not(summary) { margin-top: 0.6em; }
/* ---- Anchor scroll-spy ---- */
.anchor-row {
display: flex;
gap: 8px;
padding: 12px 16px;
background: var(--surface-card);
border-radius: var(--r-md);
margin-bottom: 16px;
flex-wrap: wrap;
}
.anchor-row a {
color: var(--muted);
text-decoration: none;
font-size: 0.88rem;
font-weight: 500;
padding: 4px 12px;
border-radius: var(--r-pill);
border: 0;
}
.anchor-row a:hover { color: var(--ink); background: var(--canvas); }
/* ---- Sticky export bar (cross-pattern) ---- */
.export-bar {
position: sticky;
bottom: 0;
margin: 2.4em -24px -96px;
padding: 18px 24px;
background: color-mix(in srgb, var(--canvas) 92%, transparent);
backdrop-filter: blur(8px);
border-top: 1px solid var(--hairline);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.button-primary {
appearance: none; border: 0;
background: var(--coral);
color: var(--on-primary);
font: inherit;
font-family: "Inter", sans-serif;
font-weight: 500;
font-size: 0.92rem;
padding: 12px 22px;
border-radius: var(--r-md);
cursor: pointer;
}
.reset-link {
font-size: 0.88rem;
color: var(--muted);
background: none;
border: 0;
cursor: pointer;
padding: 0;
border-bottom: 1px solid var(--hairline);
}
@media (max-width: 720px) {
.wrap { padding: 32px 18px 64px; }
h1 { font-size: 2.1rem; }
.with-side { grid-template-columns: 1fr; }
.side-nav { position: static; }
}
</style>
</head>
<body>
<main class="wrap">
<div class="kicker">Gallery · Navigation primitives</div>
<h1>Navigation primitives</h1>
<p class="sub">How the reader moves through an artifact. Tabs for parallel views, deck for sequential read-through, side-nav for long reference, anchors for short jumps, collapsible details for progressive disclosure.</p>
<h2>Tabs</h2>
<p>Use when the same domain has 2–5 parallel views (e.g., explainer with "How", "Why", "When"; options pattern with "Side-by-side", "Detail per option"). Don't use for sequence — use a deck.</p>
<div class="demo">
<div class="tabs" id="tabs1">
<button class="active" data-tab="t1">Overview</button>
<button data-tab="t2">Detail</button>
<button data-tab="t3">Trade-offs</button>
</div>
<div class="tab-panel active" id="t1"><p><strong>Overview.</strong> Top-level summary lives here. Tabs are non-modal — switch freely; nothing is "submitted."</p></div>
<div class="tab-panel" id="t2"><p><strong>Detail.</strong> Deeper material the reader may not want on first scan. Worth tabbing rather than scrolling because the reader chooses.</p></div>
<div class="tab-panel" id="t3"><p><strong>Trade-offs.</strong> The "compare" view. If your options exceed 3, prefer a side-by-side grid instead of tabs.</p></div>
</div>
<pre><code><div class="tabs">
<button class="active" data-tab="t1">Overview</button>
<button data-tab="t2">Detail</button>
</div>
<div class="tab-panel active" id="t1">...</div>
<div class="tab-panel" id="t2">...</div>
<script>
document.querySelectorAll('.tabs button').forEach(b => {
b.addEventListener('click', () => {
b.parentElement.querySelectorAll('button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById(b.dataset.tab).classList.add('active');
});
});
</script></code></pre>
<h2>Deck (arrow-key slides)</h2>
<p>The <code>deck</code> pattern's core nav. Left/right arrow keys, click prev/next, slide counter. Use for short sequential reads (5–15 slides), never for parallel views.</p>
<div class="demo">
<div class="deck" id="deck">
<div class="slide active" data-i="1">
<h3>Slide 1 — Premise</h3>
<p>Markdown flattens spatial information. HTML restores it.</p>
</div>
<div class="slide" data-i="2">
<h3>Slide 2 — Evidence</h3>
<p>Diffs are spatial. Call graphs are spatial. Risks-vs-impact is spatial. None of these survive a markdown wall.</p>
</div>
<div class="slide" data-i="3">
<h3>Slide 3 — Implication</h3>
<p>When the content has shape, ship an HTML artifact instead.</p>
</div>
<div class="deck-nav">
<button id="prev">‹</button>
<button id="next">›</button>
</div>
<div class="deck-meta"><span id="deck-num">1</span> / 3</div>
</div>
<div class="deck-hint">Use <kbd>←</kbd> <kbd>→</kbd> arrows or click prev/next.</div>
</div>
<pre><code><script>
let i = 0;
const slides = document.querySelectorAll('.slide');
function show(n) {
i = Math.max(0, Math.min(slides.length - 1, n));
slides.forEach((s, k) => s.classList.toggle('active', k === i));
document.getElementById('deck-num').textContent = i + 1;
}
document.getElementById('prev').addEventListener('click', () => show(i - 1));
document.getElementById('next').addEventListener('click', () => show(i + 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft') show(i - 1);
if (e.key === 'ArrowRight') show(i + 1);
});
</script></code></pre>
<h2>Side nav (sticky)</h2>
<p>Use for long reference pages (codemap, tokens, explainer with many sections). The nav highlights as the reader scrolls.</p>
<div class="demo">
<div class="with-side">
<nav class="side-nav" id="sideNav">
<div class="label">On this page</div>
<a href="#sec-a" class="active">Auth flow</a>
<a href="#sec-b">Token issuance</a>
<a href="#sec-c">Refresh strategy</a>
<a href="#sec-d">Failure modes</a>
</nav>
<div class="side-content">
<section id="sec-a"><h4>Auth flow</h4><p>Top-level entry: <code>POST /auth/login</code>. Handlers in <code>src/auth/login.ts</code>.</p></section>
<section id="sec-b"><h4>Token issuance</h4><p>Signed JWT issued from <code>src/auth/tokens.ts:issue()</code>. Expires after 1h.</p></section>
<section id="sec-c"><h4>Refresh strategy</h4><p>Rotating refresh tokens stored in <code>refresh_tokens</code> table; one-time-use.</p></section>
<section id="sec-d"><h4>Failure modes</h4><p>Common: clock skew, revoked refresh, missing scope. See <code>tests/auth/edges.test.ts</code>.</p></section>
</div>
</div>
</div>
<h2>Anchor row</h2>
<p>Lightweight alternative to a sidebar. Pin a row of jump-to links at the top of long content. Cheaper than a sticky sidebar; works fine for 4–8 sections.</p>
<div class="demo">
<div class="anchor-row">
<a href="#a1">Premise</a>
<a href="#a2">Evidence</a>
<a href="#a3">Counter-arguments</a>
<a href="#a4">Implications</a>
<a href="#a5">Next steps</a>
</div>
<p style="font-size: 0.88rem; color: var(--muted);">(Demo only — links inside this card are decorative.)</p>
</div>
<h2>Collapsible details</h2>
<p>Use for progressive disclosure inside an explainer or a long plan — keep the reader on one page; let them open just the pieces they want.</p>
<div class="demo">
<details open>
<summary>What is the unreasonable effectiveness of HTML?</summary>
<p>The observation that markdown — the default agent output format — flattens information that is inherently spatial, comparative, or interactive. A small self-contained HTML file fixes this.</p>
</details>
<details>
<summary>Why not Markdown?</summary>
<p>Markdown is linear. Side-by-side comparisons, call graphs, diffs, motion, and toggles can't live there.</p>
</details>
<details>
<summary>Why not a React app?</summary>
<p>Because the artifact is disposable. Build steps, dependencies, and frameworks are overhead the reader pays for nothing.</p>
</details>
</div>
<h2>Sticky export bar</h2>
<p>Universal end-of-artifact element for any interactive page. Left: quiet reset link. Right: primary coral CTA. One per page. Scroll to bottom to see it.</p>
<h2>How to use this file</h2>
<p>When the artifact you're producing needs to move the reader (between views, through a sequence, around a long doc), pick the closest primitive above and adapt. Don't invent new navigation per artifact — the reader's recognition is the point.</p>
<p>This file shows <em>navigation</em>. Look is in <code>gallery/look.html</code>. Interactions are in <code>gallery/interactions.html</code>.</p>
</main>
<div class="export-bar">
<button class="reset-link" type="button">Reset</button>
<button class="button-primary" id="copyBtn">Copy as markdown</button>
</div>
<script>
// Tabs
document.querySelectorAll('#tabs1 button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#tabs1 button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById(b.dataset.tab).classList.add('active');
});
});
// Deck
let i = 0;
const slides = document.querySelectorAll('.slide');
function show(n) {
i = Math.max(0, Math.min(slides.length - 1, n));
slides.forEach((s, k) => s.classList.toggle('active', k === i));
document.getElementById('deck-num').textContent = i + 1;
}
document.getElementById('prev').addEventListener('click', () => show(i - 1));
document.getElementById('next').addEventListener('click', () => show(i + 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft') show(i - 1);
if (e.key === 'ArrowRight') show(i + 1);
});
// Side nav scroll-spy
const sideLinks = document.querySelectorAll('#sideNav a');
const sections = ['sec-a','sec-b','sec-c','sec-d'].map(id => document.getElementById(id));
const setActive = id => {
sideLinks.forEach(a => a.classList.toggle('active', a.getAttribute('href') === '#' + id));
};
sideLinks.forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const id = a.getAttribute('href').slice(1);
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
setActive(id);
});
});
// Export
document.getElementById('copyBtn').addEventListener('click', async (e) => {
await navigator.clipboard.writeText('# Navigation gallery snapshot\n\n(No data to export — this file is a reference.)\n');
const label = e.target.textContent;
e.target.textContent = 'Copied';
setTimeout(() => { e.target.textContent = label; }, 1500);
});
</script>
</body>
</html>
MIT License
Copyright (c) 2026 Julian Oczkowski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
codemap
The reader is new to this code. Their question is the same one everyone has on day one: "where do I start?" A map answers better than a README does.
Read first
Open ../templates/base.html. Use its .callout.info for the TL;DR. Read patterns/diagram.md for SVG conventions — this pattern's diagram follows them.
Structure
<kicker>Codemap · <package>
<h1><Package name in plain words>
<prompt block>
<TL;DR callout> ← what this package does, who calls into it
<svg diagram> ← boxes (modules) and arrows (calls/data flow)
with the HOT PATH highlighted in accent color
<entry points list> ← the 3-5 functions/exports a newcomer should read first,
with file:line and a one-sentence "this is where X happens"
<glossary> ← 5-10 domain terms with one-line definitions,
for the words that recur in the codeRules
1. Diagram is hand-rolled SVG, never an ASCII box. The reader is looking at this to see structure; markdown art defeats the purpose. Read diagram.md for the conventions. 2. Highlight one path through the diagram in accent color. The "hot path" — the most common request flow. New readers anchor on it; everything else is variation. 3. Boxes are modules/files; arrows are calls. Label arrows with the operation ("creates", "validates", "publishes") when not obvious. Dashed arrows = async or fire-and-forget. 4. Entry points are real `file:line` references. Not "see the auth module" — packages/api/src/auth/login.ts:42. The reader will cmd-click; make that work. 5. *Glossary entries are for this codebase's vocabulary. Don't define "request" or "controller." Do define the project-specific nouns ("workspace", "thread", "channel") because newcomers conflate them with their generic meaning. 6. Keep it under one screen of diagram.* If the module is too big for one diagram, draw the top-level map and link to sub-maps. Don't cram 40 boxes into one SVG.
Pattern-specific CSS (extends base utility kit)
.diagram-wrap { margin: 2em 0; padding: 24px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.diagram-wrap svg { display: block; max-width: 100%; height: auto; }
.entry { display: grid; grid-template-columns: minmax(160px, auto) 1fr; gap: 8px 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.entry code { font-size: 0.88rem; color: var(--accent); }
.entry .desc { color: var(--ink-muted); }
dl.glossary { display: grid; grid-template-columns: minmax(120px, auto) 1fr; gap: 6px 16px; margin: 1em 0; }
dl.glossary dt { font-weight: 600; }
dl.glossary dd { margin: 0; color: var(--ink-muted); }Export
No export — this is a reading artifact, not an editing one.
Anti-patterns
- A diagram that's just the directory tree. Directory structure is rarely the same as call structure; draw the calls.
- Highlighting too many things. "Hot path" means one path. If multiple paths feel hot, pick the most common request.
- Entry points list with no file paths. The reader can't navigate from prose.
- Generic glossary entries. Only define terms that mean something specific in this codebase.
deck
A handful of <section> tags and twenty lines of JS is a slide deck. The user points at a Slack thread or a doc and gets something they can arrow-key through in a meeting. No Keynote, no PowerPoint, no Reveal.js install.
Read first
Open ../templates/base.html. The deck overrides much of the base's layout (body becomes full-viewport, no wrap), but keeps the color tokens and typography.
Structure
<head> ← title, no scroll on body
<body>
<main>
<section class="slide" data-slide="1"> ← one slide per section
<h2> ← slide title (large)
content ← keep it sparse: 3-5 bullets max, or one big idea
</section>
<section class="slide" data-slide="2">...</section>
...
</main>
<nav class="deck-nav"> ← prev / counter / next at the bottom
<script> ← arrow-key handlers, slide swap, overview grid
</body>Rules
1. One idea per slide. If a slide has more than 5 bullets or more than a paragraph of body text, split it. The reader is in a meeting and can only absorb one thing at a time. 2. Big type. Title is 3–4rem, body is 1.4rem. The slide is on a screen, possibly a TV, possibly to people not in front of the laptop. Default body text is wrong for a deck. 3. Centered, generous whitespace. Each slide is its own viewport. Don't fill edge-to-edge. 4. Arrow keys, space, and J/K all advance. Page Up / Page Down go back. ESC shows an overview grid. Home/End jump to first/last. These are the conventions users have from other deck tools. 5. Slide counter visible. "3 / 12" in the nav bar. The reader needs to know how much is left. 6. Speaker notes in `<aside hidden>`. Toggle with N. Don't print them on the slide. 7. Real HTML elements per slide. A code-walkthrough slide has a real <pre>. A diagram slide has a real <svg>. A quote slide has a real <blockquote>. The slide system is just navigation; the content underneath is HTML.
Pattern-specific CSS (overrides base layout)
body { overflow: hidden; height: 100vh; }
main.wrap { max-width: none; margin: 0; padding: 0; height: 100vh; }
.slide {
position: absolute; inset: 0;
display: flex; flex-direction: column; justify-content: center; align-items: center;
padding: 60px 80px;
opacity: 0; pointer-events: none;
transition: opacity 180ms ease;
text-align: center;
}
.slide.active { opacity: 1; pointer-events: auto; }
.slide h2 { font-size: 3.2rem; line-height: 1.15; margin: 0 0 0.6em; max-width: 18ch; }
.slide ul, .slide ol { font-size: 1.4rem; line-height: 1.5; text-align: left; max-width: 32ch; }
.slide ul li, .slide ol li { margin: 0.5em 0; }
.slide pre { font-size: 1.1rem; max-width: 80ch; text-align: left; }
.slide blockquote { font-size: 1.8rem; font-style: italic; max-width: 28ch; border-left: 4px solid var(--accent); padding-left: 24px; text-align: left; }
.slide .kicker { font-size: 0.85rem; margin-bottom: 1em; }
.deck-nav {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 16px;
background: color-mix(in srgb, var(--surface) 80%, transparent);
backdrop-filter: blur(8px);
padding: 8px 16px; border-radius: 999px; border: 1px solid var(--line);
font-size: 0.85rem;
}
.deck-nav button { appearance: none; border: 0; background: transparent; cursor: pointer; color: var(--ink); font: inherit; padding: 4px 8px; }
.deck-nav .counter { font-variant-numeric: tabular-nums; color: var(--ink-muted); }
.overview {
position: fixed; inset: 0; background: color-mix(in srgb, var(--bg) 95%, transparent);
backdrop-filter: blur(12px);
display: none; padding: 40px; overflow-y: auto;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;
}
.overview.active { display: grid; }
.overview .thumb { aspect-ratio: 16/10; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); padding: 16px; font-size: 0.8rem; cursor: pointer; }
.overview .thumb h3 { font-size: 0.95rem; margin: 0 0 6px; }
.overview .thumb.current { border-color: var(--accent); border-width: 2px; }JS skeleton (paste verbatim, adjust as needed)
<script>
const slides = [...document.querySelectorAll('.slide')];
let i = 0;
function go(n) {
i = Math.max(0, Math.min(slides.length - 1, n));
slides.forEach((s, idx) => s.classList.toggle('active', idx === i));
document.querySelector('.counter').textContent = `${i + 1} / ${slides.length}`;
}
document.addEventListener('keydown', (e) => {
if (['ArrowRight', ' ', 'j', 'PageDown'].includes(e.key)) go(i + 1);
if (['ArrowLeft', 'k', 'PageUp'].includes(e.key)) go(i - 1);
if (e.key === 'Escape') document.querySelector('.overview').classList.toggle('active');
if (e.key === 'Home') go(0);
if (e.key === 'End') go(slides.length - 1);
});
go(0);
</script>Cross-domain fit
- Engineering review: title slide, problem slide, options slide (3 columns), recommendation slide, implementation slide, risks slide. ~8 slides.
- Sales pitch: title slide, the customer's pain, our solution (one big visual), proof points (3 slides), pricing, call to action. ~10 slides.
- Conference talk: title, the question that motivates the talk, three sections of body (with section-divider slides between), conclusion, contact info. ~15 slides.
- All-hands update: title, the headline number, what shipped (3 slides), what's next (1 slide), open Q&A.
Export
No export — the file is the deck. The user opens it and presents. Optional: a print stylesheet that puts one slide per page so it can be saved as PDF via the browser's print dialog.
Anti-patterns
- Slides full of bullets at body-text size. That's a doc with page breaks, not a deck.
- Tiny code samples no one can read from the back of the room.
- Decorative slide transitions. A 180ms fade is enough; sliding/zooming pulls focus from the content.
- Two ideas on one slide because you have lots of content. Add a slide; they're free.
- No section dividers in a long deck. A 20-slide deck needs visual breath every 5–6 slides.
diagram
Inline SVG gives the agent a real pen. Use it instead of reaching for Mermaid or PlantUML. Mermaid is fine for a quick sketch but produces generic-looking output, and the user can't tweak it without rerunning a build. Hand-rolled SVG is a few extra lines and the result is a real figure.
Read first
Open ../templates/base.html. Use its color tokens — var(--ink), var(--line-strong), var(--accent) — so the diagram matches the page.
Two sub-modes
A. Figure sheet — a set of illustrative diagrams (e.g. for a blog post, lecture, report), each in its own <figure> with a caption.
B. Single annotated diagram — one large diagram (flowchart, architecture, process map) with click-to-reveal annotations on each node.
Structure — Figure sheet
<kicker>Figures · <post or doc>
<h1><Post title or topic>
<prompt block>
<figure> ← each SVG with a caption beneath
<figure>
<figure>
...Structure — Single annotated diagram
<kicker>Diagram · <system or process>
<h1><What this diagram shows>
<prompt block>
<svg> ← one large SVG, nodes are clickable
<annotation panel> ← updates when a node is clicked, showing detailsRules — drawing
1. Sensible viewBox, scale to fit. viewBox="0 0 800 500" then style="width: 100%; height: auto". Never fixed pixel dimensions — the page must work on mobile. 2. Boxes have rounded corners (4–8px) and 12–16px padding. Sharp corners read as harsh; over-rounded reads as toy. Aim for "engineering drawing", not "kindergarten." 3. Arrows via `<marker>`. Define an arrowhead marker once at the top, reuse it. Don't draw triangles by hand. 4. Solid vs dashed has meaning. Solid = sync / synchronous / direct. Dashed = async / fire-and-forget / indirect. State this in a small legend below the diagram. 5. Label every arrow. A box-and-arrow diagram with unlabeled arrows is half-done. The verb on the arrow is what makes the diagram readable ("creates", "validates", "publishes to", "reviews", "approves"). 6. Use the base palette. var(--ink) for text, var(--line-strong) for box strokes, var(--accent) to highlight the hot path or current focus. Don't introduce new colors unless the diagram has more than one semantic axis (e.g. one color for "owned by team A", another for "owned by team B"). 7. Text inside SVG uses `font-family: inherit`. So it picks up the page font and the diagram looks like part of the document, not pasted in.
Rules — annotated mode interactivity
- Clickable nodes have
cursor: pointerandtabindex="0". - Use
aria-controlsto point at the annotation panel so it's keyboard-accessible. - Annotation panel updates via a small JS event listener; no framework.
Pattern-specific CSS (extends base utility kit)
figure { margin: 2em 0; padding: 20px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
figure svg { display: block; width: 100%; height: auto; }
figcaption { margin-top: 12px; font-size: 0.9rem; color: var(--ink-muted); font-style: italic; }
.legend { font-size: 0.82rem; color: var(--ink-muted); margin-top: 12px; display: flex; flex-wrap: wrap; gap: 16px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend i { display: inline-block; width: 18px; height: 2px; }
.annotated { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; margin: 2em 0; }
@media (max-width: 720px) { .annotated { grid-template-columns: 1fr; } }
.annotated .diagram { padding: 20px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.annotated .panel { padding: 20px; background: var(--code-bg); border-radius: var(--radius); min-height: 200px; }
.annotated .panel h4 { margin-top: 0; }
.annotated .panel .empty { color: var(--ink-faint); font-style: italic; }
.annotated svg .node { cursor: pointer; }
.annotated svg .node:hover rect, .annotated svg .node:focus rect { stroke-width: 2; }
.annotated svg .node.active rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 2; }SVG arrowhead marker (copy verbatim)
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"/>
</marker>
</defs>Use marker-end="url(#arrow)" on <line> or <path>, and set stroke="currentColor" so the arrowhead matches the line color.
Cross-domain fit
- Software architecture: boxes are services, arrows are RPC calls; solid = request/response, dashed = async events.
- Business process: boxes are roles or stages, arrows are handoffs; solid = required, dashed = optional fast-path.
- Scientific concept: boxes are entities or states, arrows are transformations; solid = forward reaction, dashed = reverse.
- Org chart: boxes are roles, arrows are reporting lines; solid = direct, dashed = dotted-line.
- Customer journey: boxes are touchpoints, arrows are transitions; solid = primary path, dashed = drop-off recovery.
Export
Figure sheet mode: each <figure> has a small "Copy SVG" button (use base's .export style) that emits just that SVG's source — so the user can paste it into a blog post or doc.
Annotated mode: no export; the page is the artifact.
Anti-patterns
- Mermaid blocks. The whole point of this pattern is not Mermaid.
- SVG with fixed pixel dimensions that overflow on mobile.
- A 40-node spaghetti diagram. If you need 40 nodes, you need two diagrams.
- Decorative gradients and drop shadows. Engineering drawings, not landing-page hero art.
- Unlabeled arrows. Always state what flows along the arrow.
- Color-coding with no legend. If you used three colors, three colors are explained in the legend.
editor
This is the most distinctive pattern in the bundle and the one that fundamentally rewires how the user works with the agent. Instead of describing changes in prose ("move A to Next, move B to Cut, demote C..."), the user gets a small UI built for the thing they're doing, makes the changes by hand, and clicks a button that emits the result as text they can paste back into the conversation.
The artifact is throwaway. The export is the durable output. The export button is the whole point. Without it, you've built a toy.
Read first
Open ../templates/base.html. Use its .export-bar for the sticky bottom bar, .button.export-primary for the CTA, and the standard color tokens.
When the pattern fits
- The user has 10+ items and needs to reorder, categorize, or filter them. (Triage board, kanban, ranking UI, shortlist.)
- The user has a set of toggles or values to tune, where the meaning of one depends on others. (Feature flags with dependencies, config builder, prompt tuner, RSVP tracker.)
- The user will do the work themselves and just needs the right surface to do it on.
- The result is text the user will paste somewhere — a planning doc, a config file, a follow-up prompt, a colleague's inbox.
When it does NOT fit
- The work is just deciding, not editing. Use
optionsinstead. - The user wants the agent to do the work and just tell them the answer.
- There's no clean "result text" the user would paste anywhere afterward.
Structure
<kicker>Editor · <project> · <what's being edited>
<h1><What this editor does, in plain words>
<prompt block>
<one-line instruction> ← "Drag tickets across columns. Click a tag to filter."
<the editor itself> ← the actual UI: board, toggle list, ranking grid, etc.
<sticky export bar> ← reset link (small, quiet) + export button (big, primary)
Emits markdown / JSON / diff / prompt.Rules
1. Start with the export format, work backwards. Before any UI: decide what text the user will copy out. That decides the data model. The UI manipulates the data model; it must always be derivable into the export string. 2. Initial state comes from the user's input. If the user pasted a list of items, those items seed the board. If they shared a config, those values seed the toggles. The editor opens with their data already loaded, not empty. 3. Real interactions, not fake ones. Drag-and-drop uses the HTML5 drag API. Sortable lists really sort. Toggles really toggle. Don't fake interactions with click-to-cycle when drag-and-drop is what the user expects. 4. State lives in a single in-memory object. No localStorage unless explicitly requested. One state = {...} at the top of the script; one render() function that re-renders from it; mutations go through helpers that update state and call render(). 5. Export button is the primary CTA. Use .export-primary from the base. Label precisely: "Copy as markdown", "Copy as JSON", "Copy diff", "Copy as prompt" — never just "Export." 6. Reset link is small and quiet. A text-button or underline-link, not a big red button. Reset is rare; export is common. 7. No save, no submit, no server. The artifact is local. The export is the save.
Pattern-specific CSS (extends base utility kit)
.editor-instr { color: var(--ink-muted); font-size: 0.92rem; margin: 0 0 1.4em; }
.editor-area { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; margin: 1.4em 0; }
.reset-link {
font-size: 0.88rem; color: var(--ink-muted);
background: none; border: 0; cursor: pointer; padding: 0;
border-bottom: 1px solid var(--line-strong);
}
.reset-link:hover { color: var(--ink); }
/* Drag-and-drop columns (triage / kanban style) */
.columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
.column { background: var(--code-bg); border-radius: var(--radius); padding: 14px; min-height: 120px; }
.column h3 { margin: 0 0 12px; font-size: 0.85rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); display: flex; justify-content: space-between; }
.column h3 .count { background: var(--surface); color: var(--ink); padding: 1px 8px; border-radius: 999px; font-size: 0.78rem; }
.column .card-item { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 8px; cursor: grab; font-size: 0.92rem; }
.column .card-item:active { cursor: grabbing; }
.column .card-item.dragging { opacity: 0.4; }
.column.drop-target { background: var(--accent-soft); outline: 2px dashed var(--accent); outline-offset: -2px; }
/* Toggle list (config / feature-flag style) */
.toggle-list { display: grid; gap: 8px; }
.toggle-row { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; padding: 10px 14px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.toggle-row .name { font-weight: 500; }
.toggle-row .meta { font-size: 0.82rem; color: var(--ink-muted); }
.toggle-row.warn { border-color: var(--warn); }
.toggle-row.warn::after { content: "⚠ depends on a disabled item"; display: block; font-size: 0.8rem; color: var(--warn); grid-column: 1 / -1; }
.switch { position: relative; display: inline-block; width: 38px; height: 22px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider { position: absolute; cursor: pointer; inset: 0; background: var(--line-strong); border-radius: 22px; transition: 0.2s; }
.switch .slider::before { position: absolute; content: ""; height: 18px; width: 18px; left: 2px; top: 2px; background: white; border-radius: 50%; transition: 0.2s; }
.switch input:checked + .slider { background: var(--accent); }
.switch input:checked + .slider::before { transform: translateX(16px); }Drag-and-drop JS skeleton (paste verbatim)
<script>
let dragId = null;
document.querySelectorAll('.card-item').forEach(card => {
card.draggable = true;
card.addEventListener('dragstart', () => {
dragId = card.dataset.id;
card.classList.add('dragging');
});
card.addEventListener('dragend', () => card.classList.remove('dragging'));
});
document.querySelectorAll('.column').forEach(col => {
col.addEventListener('dragover', e => { e.preventDefault(); col.classList.add('drop-target'); });
col.addEventListener('dragleave', () => col.classList.remove('drop-target'));
col.addEventListener('drop', e => {
e.preventDefault();
col.classList.remove('drop-target');
const card = document.querySelector(`.card-item[data-id="${dragId}"]`);
if (card) {
col.appendChild(card);
// update state: state.items[dragId].column = col.dataset.col;
}
});
});
</script>Export pattern
function buildExport() {
// Walk the current state and produce the text the user will paste.
// For a triage board: markdown grouped by column.
// For toggles: a key=value block, or a JSON diff against initial state.
// For an ordering: a numbered markdown list.
}
document.querySelector('.export-primary').addEventListener('click', async (e) => {
await navigator.clipboard.writeText(buildExport());
e.target.dataset.copied = '1';
const label = e.target.textContent;
e.target.textContent = 'Copied';
setTimeout(() => { e.target.dataset.copied = ''; e.target.textContent = label; }, 1500);
});Cross-domain fit
- Engineering ticket triage: 30 issues drag across Now / Next / Later / Cut → export grouped markdown.
- Conference talk shortlist: 50 submissions drag across Accept / Maybe / Reject → export the accept list as a markdown table.
- Wedding RSVP tracker: 80 invitees toggle across Yes / No / Maybe / Dietary-note → export as CSV.
- Feature-flag config: 25 flags with dependencies, toggle on/off → export as the changed-only diff.
- Reading list prioritization: 40 books drag-to-reorder → export as numbered markdown list.
Same pattern, same export-first thinking, completely different domains.
Anti-patterns
- No export button. The single most common way to get this pattern wrong. The artifact is half-built.
- Export emits the whole page as HTML. No — emit the structured result. Markdown, JSON, a diff, a config block. Something pastable into the next surface.
- localStorage persistence by default. State should live in memory. The user can refresh to start over; the export is how they keep work.
- Faking interactions. ("Click to cycle through statuses" when the user wanted drag-and-drop.) If you can't build the real interaction, switch patterns.
- Empty initial state. The user gave you data; load it. An empty editor is a generic tool, not a custom one.
- Multiple primary CTAs. One export button per page. If multiple exports are needed (markdown and JSON), one is primary and others are secondary text-buttons next to it.
explainer
An explainer with collapsible sections, tabs, and a glossary in the margin reads completely differently from the same words dumped linearly. The scaffolding is the value — it lets the reader skim, drill in where curious, and skip what they already know.
Read first
Open ../templates/base.html. Use its .callout.info for the TL;DR, native <details> styled by the base for collapsibles, and the tab pattern below.
Structure
<kicker>Explainer · <topic or domain>
<h1><The concept or feature>
<prompt block>
<TL;DR callout> ← 2-3 sentences. The reader who reads nothing else gets this.
<sections> ← each section is a layered explanation:
<h2>step or concept name
<p>the surface explanation
<details> ← "Why this works" / "Edge cases" / "When this breaks"
<tabbed content> ← parallel content: same idea in different forms
<comparison table> ← if the concept has variants, contrast them
<FAQ> ← real questions, in <details> elements
<glossary> ← domain terms, with hover-links from the bodyRules
1. TL;DR is mandatory and first. Three sentences max. Use the base's .callout.info. The reader with 30 seconds leaves the page knowing the core idea. 2. Collapsibles use `<details>`. Native HTML, accessible, no JS. Default-closed for "going deeper"; default-open for "you must read this to continue." 3. Tabs use radio-buttons + CSS, no JS. Pattern below. The HTML stays semantic. 4. Glossary terms hover-link from the body. <span class="gloss" data-def="...">term</span>. On hover, the definition floats above. The glossary section at the bottom is the canonical list. 5. Examples are real and short. A 50-line code block is a tutorial, not an explainer. Keep examples focused on the single idea being explained — 3 to 15 lines of code, or 2–3 sentences of example prose. 6. An interactive demo earns its place only if it teaches something static can't. A consistent-hashing ring you can add/remove nodes from is worth building. A button that toggles a class isn't. 7. FAQ entries are real questions, not made-up. Generate them from the parts of the explanation that felt fragile or hand-wavy. If you wrote "we'll discuss this later" anywhere, the FAQ is where "later" happens.
Pattern-specific CSS (extends base utility kit)
.tabs { margin: 1.4em 0; }
.tabs input[type="radio"] { display: none; }
.tabs .tablist { display: flex; gap: 4px; border-bottom: 1px solid var(--line); }
.tabs .tablist label { padding: 8px 14px; cursor: pointer; font-size: 0.88rem; color: var(--ink-muted); border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tabs .tablist label:hover { color: var(--ink); }
.tabs .panel { display: none; padding: 16px 0; }
.tabs input[type="radio"]:checked + label { color: var(--ink); border-bottom-color: var(--accent); }
.gloss { border-bottom: 1px dotted var(--ink-faint); cursor: help; position: relative; }
.gloss:hover::after, .gloss:focus::after {
content: attr(data-def);
position: absolute;
bottom: 100%; left: 0;
background: var(--ink); color: var(--bg);
padding: 8px 12px; border-radius: var(--radius);
font-size: 0.85rem; white-space: normal; width: 280px; z-index: 10;
margin-bottom: 6px;
}
dl.glossary { display: grid; grid-template-columns: minmax(120px, auto) 1fr; gap: 8px 16px; }
dl.glossary dt { font-weight: 600; }
dl.glossary dd { margin: 0; color: var(--ink-muted); }(.callout.info and <details> styling come from the base.)
Tabs HTML pattern (no JS)
<div class="tabs">
<div class="tablist">
<input type="radio" name="t" id="t-a" checked>
<label for="t-a">YAML</label>
<input type="radio" name="t" id="t-b">
<label for="t-b">TOML</label>
</div>
<div class="panel" data-for="t-a">...YAML example...</div>
<div class="panel" data-for="t-b">...TOML example...</div>
</div>
<style>
#t-a:checked ~ .panel[data-for="t-a"],
#t-b:checked ~ .panel[data-for="t-b"] { display: block; }
</style>(The inputs sit as siblings of the panels so the :checked ~ selector matches without JS.)
Cross-domain fit
- Technical concept: "How does rate limiting work in this repo" — request-path collapsibles, tabs for nginx/redis/app-level config, glossary of project-specific terms.
- Scientific idea: "Explain how mRNA vaccines work" — collapsibles for each stage (entry, translation, response), tabs comparing mRNA vs viral-vector, glossary of immunology terms.
- Financial instrument: "Explain how convertible notes work" — collapsibles for each clause type, tabs for SAFE vs convertible vs equity, glossary of cap-table terms.
- Policy / procedure: "How does our expense-approval flow work" — collapsibles for each step, tabs for self-serve vs manager-approval, glossary of system names.
Same scaffolding; the depth of the layers and the choice of tabs adapt to the domain.
Export
No export — this is a reading artifact. Optional: a "Copy as markdown" button using the base's button.export style, for someone who wants to paste a flattened version into a wiki.
Anti-patterns
- Linear wall of text with no structural layering. If the explainer reads the same as the markdown version would, you've wasted the medium.
- TL;DR longer than the explanation. The TL;DR is for the skimmer; if they need more, they read on. 3 sentences is plenty.
- Tabs with three tabs where one is empty or trivially different. Use a tab only when there are real alternatives.
- Glossary defining words anyone reading already knows. Define the project- or domain-specific vocabulary only.
- "Interactive" demo that's a button toggling a class. The demo should teach something static can't, or it's decoration.
- No FAQ. Means you haven't asked yourself "what would the reader still wonder about?"
motion
Motion can't be described, only felt. A static screenshot or a paragraph of prose loses the thing that actually matters: how it feels at 250ms vs 400ms, whether the easing reads as snappy or sluggish. A throwaway page with the real animation answers the question in five seconds.
Read first
Open ../templates/base.html. The motion page uses much of the base for typography but adds a custom stage area.
Two sub-modes
A. Animation sandbox — one transition in isolation, with controls.
B. Clickable flow — 3–6 screens linked together by buttons that simulate the real interaction.
Structure — Animation sandbox
<kicker>Motion · <project>
<h1><Animation name>
<prompt block>
<stage> ← the animation itself, large and centered
<controls> ← sliders for duration, easing dropdown, "play" button to retrigger
<spec readout> ← the current values as CSS, copyable
(transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1))Structure — Clickable flow
<kicker>Motion · <project>
<h1><Flow name>
<prompt block>
<flow nav> ← step indicator: 1 → 2 → 3 → 4
<viewport> ← single visible screen at real device dimensions
screens swap in with real transition timing
<actions> ← the buttons inside the viewport advance the flow
<flow controls> ← prev / next at the bottom, "reset" linkRules
1. Real CSS transitions, not GIFs or screenshots. This is the entire point — the reader needs to scrub a slider and see the change. 2. Sandbox mode: spec readout is copyable. The user is tuning so they can paste the final values into the codebase. Make that paste one click. 3. Flow mode: respect the platform's real dimensions. A phone flow renders in a phone-sized frame (375×812). A desktop flow renders at desktop sizes. Don't scale; the geometry is part of the feel. 4. Don't overbuild. Three screens linked by click → fade → swap is enough to feel a flow. Eight screens with full content is a different project. 5. One transition per page. If the user wants to compare two easings, render them side-by-side as two stages on one page. Don't bury comparison in a dropdown. 6. Default to `prefers-reduced-motion`. Wrap the animations in @media (prefers-reduced-motion: no-preference). The user tuning the motion can disable this temporarily.
Pattern-specific CSS (extends base utility kit)
.stage { padding: 60px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; min-height: 280px; margin: 2em 0; }
.controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px 24px; padding: 20px; background: var(--code-bg); border-radius: var(--radius); margin: 1em 0; }
.controls label { display: flex; flex-direction: column; gap: 6px; font-size: 0.85rem; color: var(--ink-muted); }
.controls input[type="range"], .controls select { width: 100%; }
.spec { font-family: ui-monospace, monospace; font-size: 0.88rem; background: var(--code-bg); padding: 14px 16px; border-radius: var(--radius); position: relative; }
.spec button.export { position: absolute; top: 8px; right: 8px; padding: 4px 10px; font-size: 0.8rem; }
.flow-nav { display: flex; align-items: center; gap: 8px; margin: 1em 0; font-size: 0.85rem; color: var(--ink-muted); }
.flow-nav .step { width: 24px; height: 24px; border-radius: 50%; border: 1px solid var(--line-strong); display: inline-flex; align-items: center; justify-content: center; font-weight: 600; }
.flow-nav .step.active { background: var(--accent); border-color: var(--accent); color: white; }
.viewport { width: 375px; max-width: 100%; height: 600px; margin: 1em auto; border: 1px solid var(--line-strong); border-radius: 16px; overflow: hidden; background: white; position: relative; }
.viewport .screen { position: absolute; inset: 0; transition: opacity 240ms ease, transform 240ms ease; }
.viewport .screen[hidden] { display: block; opacity: 0; transform: translateX(20px); pointer-events: none; }Export
Sandbox mode: a "Copy CSS" button on the spec readout that emits the exact transition / animation declaration with the current tuned values.
Flow mode: no export — the artifact is the demo. The user shares the file or screencasts from it.
Anti-patterns
- A CSS animation defined with no way to retrigger. Add a "play" button — the reviewer wants to watch it three times.
- Sliders that don't update the running animation live. The whole point is real-time tuning.
- A "flow" that's static screenshots with prev/next and no transitions. That's a PDF.
- Hardcoded values everywhere. Use CSS custom properties so the controls can rewrite them via JS in one line.
options
Two to four candidates. The same problem solved different ways. Trade-offs paired side-by-side. A recommendation at the bottom. The reader scans, points at one, and you commit.
This is the highest-leverage pattern in the bundle. The wall-of-text alternative — three sequential markdown sections — is worse, because the reader can't hold all three in their head at once.
Read first
Open ../templates/base.html. Use its .grid.cols-2 or .grid.cols-3 layout, its .card class for each option, its .callout.info for the recommendation.
Structure
<kicker>Options · <topic>
<h1>Three ways to <do the thing>
<prompt block>
<grid of N cards> ← each card is one approach
approach 01
name + 1-line summary
body content (real, specific — code, plan, design, whatever fits)
pros table | cons table (two-column, paired)
footer chips: the consistent set of axes you're comparing on
approach 02
approach 03
<recommendation callout> ← which one and why, 2-3 sentences maxRules
1. 2 to 4 cards. Three is the sweet spot. Two feels thin; five is too many to compare at a glance. 2. Same axes for every option. If option 01 lists cost, every option lists cost. The reader is comparing, and comparing requires identical axes. Pick the axes that matter for the decision, declare them once, fill them in for every option. 3. Real, specific content. The body of each card should be concrete enough to evaluate — actual code, an actual venue with an address, the actual study design, the actual budget split. Not "approach A is the modern one." If you'd need a follow-up question to evaluate it, the option isn't specific enough yet. 4. Pros and cons in a two-column grid, paired by row. First pro paired with first con on the same row. This forces honesty (each pro implies a trade-off) and reads faster than two separate lists. 5. The recommendation is mandatory. Don't punt with "it depends" or "any of these would work." Pick one, name it, give 2–3 sentences of why. The reader can override your call — they need a default to push against. 6. Numbered headings: 01, 02, 03. Small grey number left of the option name. Mimics Thariq's house style and scans well.
Pattern-specific CSS (extends base utility kit)
.tradeoffs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; margin: 1em 0; font-size: 0.92rem; }
.tradeoffs .head { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
.tradeoffs .pro::before { content: "✓ "; color: var(--good); }
.tradeoffs .con::before { content: "✗ "; color: var(--bad); }
.axes { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.82rem; color: var(--ink-muted); margin-top: 1em; padding-top: 1em; border-top: 1px solid var(--line); }
.axes b { color: var(--ink); font-weight: 600; }The base template already provides .grid.cols-3, .card, .card .num, .card .summary, and .callout.info — use those rather than redefining.
What "axes" means (the consistent comparison row)
The axes are the named values you tag at the bottom of every card. They're the 3–5 things the reader cares about across all the options. Examples — invent your own per decision:
- Code: bundle impact · testability · reuse · SSR-safe
- Vendor: monthly cost · setup time · vendor lock-in · SLA
- Venue: capacity · price/person · distance from office · catering included
- Marketing channel: CPM · audience match · creative effort · measurability
- Study design: sample size · time to results · cost · external validity
If you can't think of 3 axes that vary meaningfully across the options, you don't actually have distinct options. Go back to one.
Export
No export — this artifact exists for choosing, not editing. The next prompt the user issues ("let's go with option 02") is the export.
Anti-patterns
- Four options where one is a strawman. If you wouldn't actually recommend an option under any conditions, drop it. The reader knows when an option is filler.
- Hedged recommendation ("any of these would work"). The whole point of the artifact is to enable a decision.
- Different shapes per card. If card 01 shows the implementation and card 02 shows the cost analysis, the reader can't compare. Use the same content shape across cards.
- Trade-offs that aren't trade-offs ("Pro: it works"). Each pro should imply a specific con elsewhere on the same card or on a different card.
- Generic axes that don't matter for this decision. "Quality: high" tells the reader nothing. Pick axes the options actually differ on.
Example openers across domains
To make the cross-domain fit concrete:
- "Three ways to implement debounced search in our React codebase" → cards have code snippets; axes are bundle/testability/reuse.
- "Four venues for the team offsite" → cards have address, capacity, price; axes are capacity/price/distance/catering.
- "Two study designs for the retention question" → cards have method, sample, duration; axes are sample size/time/cost/validity.
- "Three positioning options for the launch" → cards have headline, audience, channel mix; axes are audience match/cost/risk/measurability.
Same structure, same playbook, different content.
pr
Two related artifacts, same playbook. Pick a mode at the top.
Reviewer mode: annotated diff with margin notes, severity tags, jump links. Easier to scan than scrolling a terminal.
Author mode: motivation, before/after, file-by-file tour with the why, where to focus the review.
Read first
Open ../templates/base.html. Use its .chip, .callout, and table styles.
Structure — Reviewer mode
<kicker>Code review · <repo or branch>
<h1><PR title>
<prompt block>
<jump nav> ← bulleted file list, each a link to its diff section
<summary callout> ← does it do what it says; anything blocking
<diff sections> ← one per file: header, diff lines, anchored margin notes
<verdict callout> ← approve / request-changes / comment, with gating itemsStructure — Author mode
<kicker>Pull request · <repo or branch>
<h1><PR title>
<prompt block>
<motivation> ← why this change exists (the problem, in plain words)
<before/after grid> ← two cards: how it works today vs after this PR
<file tour> ← for each non-trivial file: 1-2 sentences on what changed & why
<review guidance callout> ← "focus your review on…", "skip the renames in X.ts"
<test plan> ← what to run locally, what CI covers, what's manualRules
1. Pick one mode at the top. The kicker text declares it ("Code review" vs "Pull request"). Don't blend. 2. Diffs are real monospace blocks with red/green line highlighting — not screenshots, not images. Line numbers in a gutter so margin notes can reference them. 3. Margin notes have severity. blocker (bad), nit (muted), question (info), praise (good). Use the base's .chip color classes plus the mode-specific names below. Color-code them so a reviewer can scan. 4. Jump links work. <a href="#file-foo-ts"> and matching ids on each section. 5. The verdict / review guidance is not optional. A review without a verdict is unfinished. An author writeup without "focus here" wastes the reviewer's time.
Pattern-specific CSS (extends base utility kit)
.diff { background: var(--code-bg); border: 1px solid var(--line); border-radius: var(--radius); overflow-x: auto; margin: 1.4em 0; }
.diff-file { display: flex; justify-content: space-between; align-items: center; padding: 8px 14px; background: color-mix(in srgb, var(--ink) 6%, transparent); border-bottom: 1px solid var(--line); font-family: ui-monospace, monospace; font-size: 0.88rem; }
.diff pre { margin: 0; border: 0; border-radius: 0; background: transparent; }
.diff .line { display: grid; grid-template-columns: 48px 1fr; font-family: ui-monospace, monospace; font-size: 0.88rem; line-height: 1.6; }
.diff .line .ln { color: var(--ink-faint); text-align: right; padding-right: 12px; user-select: none; }
.diff .line.add { background: color-mix(in srgb, var(--good) 12%, transparent); }
.diff .line.add .ln { color: var(--good); }
.diff .line.del { background: color-mix(in srgb, var(--bad) 12%, transparent); }
.diff .line.del .ln { color: var(--bad); }
.margin-note { display: grid; grid-template-columns: 100px 1fr; gap: 12px; padding: 10px 14px; border-top: 1px dashed var(--line); font-size: 0.92rem; }
.margin-note .chip { align-self: start; }
.chip.blocker { background: var(--bad-soft); color: var(--bad); }
.chip.nit { background: var(--code-bg); color: var(--ink-muted); }
.chip.question { background: var(--accent-soft); color: var(--accent); }
.chip.praise { background: var(--good-soft); color: var(--good); }For before/after, use .grid.cols-2 and two .card elements from the base.
Export
Reviewer mode: a "Copy as GitHub review" button that emits the comments as path:line — note lines, grouped by severity, suitable for pasting into a PR review.
Author mode: a "Copy as PR description" button that emits the page as markdown (## Motivation, before/after as a table, file tour, test plan) ready to paste into GitHub's PR body.
Use the base's button.export (or .export-primary if the export is the primary action).
Anti-patterns
- Diff as a `<pre>` block with no line numbers and no highlighting. The whole point is to make it scannable.
- Margin notes without severity. The reviewer can't tell what's blocking and what's a nit.
- Author writeup that's the commit messages concatenated. The commit log is the what; this artifact is the why and where to look.
- "LGTM" verdict on a 1000-line PR with no actual review. If the artifact is empty, don't ship it.