design-grill
102 linesInterviews the user to settle every design system decision, in dependency order, writing a rendered HTML preview for each round so choices are made by looking rather than reading numbers. Produces .design/decisions.json. Use as phase 1 of design-system, after design-extract.
Design Grill
Settle every node of the design tree with the user. One round of questions at a time, in dependency order.
Each round writes an HTML file the user opens in a browser. Type scales, spacing and color cannot be judged from numbers in a terminal.
Prerequisite
test -f .design/drift.json || echo "MISSING"
Stop if missing and run design-extract first. Its absorption numbers are what make each recommendation checkable.
Loop
Read references/design-tree.md for the tree, the default for each node, and the traversal rules.
compute frontier -> pre-settle from drift.json -> render -> ask -> wait -> record -> repeat
1. Compute the frontier
The frontier is every unsettled node whose prerequisites are all settled. Round 1 is product character alone.
2. Pre-settle from the data
For each node in the frontier, check drift.json. If one value already accounts for 90% or more of usage on that axis, do not ask. Put it in the round as a settled line with the percentage, and move on.
Look up anything you can measure. Ask only for decisions the codebase cannot answer.
3. Render the round
Write .design/grill/round-N.html. Single file, no build step, opens directly in a browser.
Requirements:
- Use the fonts and colors from the project. Read them from the codebase.
- Show options side by side, labelled A, B, C.
- Use real strings from the app. Do not use lorem ipsum, which hides line-length problems.
- Mark the recommended option and put its absorption number on it.
- Include light and dark if the project has both.
- No horizontal scroll at 375px.
Use assets/round-shell.html as the starting file.
4. Ask in the terminal
Print the file path first, then the questions.
Round 2, rendered: .design/grill/round-2.html
SETTLED **Radius**: rounded-lg on 94% of surfaces. Adopting as base.
---
**Q1. Spacing base**: 4px or 8px grid. Options A and B in the preview show the same card at both.
> **A, 4px.** Absorbs 24 of your 31 current values. 8px absorbs 13 and requires 11 rewrites.
---
**Q2. Type ratio**: 1.200 or 1.250, rendered in Inter at your copy.
> **B, 1.250.** Absorbs 11 of your 14 sizes. Your current h1-to-body ratio is 1.24.
Number every question. Give every recommendation a number from drift.json. Separate questions with ---.
5. Wait
Wait for the user's answers before the next round. Do not proceed on silence and do not ask the next round early.
When an answer differs from your recommendation, accept it, then recompute the affected descendants and state what changed. For example: "8px base removes 6 and 10 from the spacing steps, so round 3 will offer 4 steps instead of 6."
Record
Append to .design/decisions.json after each round:
{
"character": { "value": "dense-productive", "source": "user", "round": 1 },
"radius.base": { "value": "0.5rem", "source": "pre-settled", "evidence": "94% of surfaces" },
"spacing.base": { "value": 4, "source": "user", "recommended": 4, "round": 2 }
}
design-ratify reads source to decide which decisions get a written record. Use user, pre-settled, or default.
Finish
The phase ends when the frontier is empty.
Print the full settled tree and wait for the user to confirm before handing off to design-ratify.
Reference references/design-tree.md 97 lines
The Design Tree
The order of decisions is fixed and the same for every project. Traverse it, do not derive it.
An edge is a hard prerequisite: the child cannot be answered until the parent is settled. The frontier is every unsettled node whose parents are settled. Ask the whole frontier in one round.
+-------------+
| PRODUCT | R1
| CHARACTER |
+------+------+
+------------+-------+--------+------------+
v v v v v
+-------+ +---------+ +------+ +------+ +--------+
|SPACING| |TYPE | |COLOR | |RADIUS| | MOTION | R2
| base | | ratio | | space| | base | | curve |
+---+---+ +----+----+ +--+---+ +--+---+ +---+----+
| | | | |
v v v | v
+-------+ +---------+ +--------+ | +--------+
| scale | | sizes + | |semantic| | |duration| R3
| steps | | leading | | roles | | | scale |
+---+---+ +----+----+ +---+----+ | +---+----+
+-----------+----------+-------+---------+
v
+-----------------+
| COMPONENT SHAPE | R4
| density,variants|
+--------+--------+
v
+-----------------+
| PATTERNS | R5
| empty, loading, |
| error, elevation|
+-----------------+
Nodes
R1. Product character
Decides: which adjective the system serves. One of: dense/productive, calm/editorial, playful/consumer, premium/restrained. Unblocks: density, motion duration and radius defaults all derive from it. Default: infer from the codebase and present the inference for confirmation. Tables and dense data views indicate productive. Large imagery and long prose indicate editorial. Note: this is the only node that cannot be answered from the code.
R2. Spacing base
Decides: 4px or 8px grid. Default: 4px. It absorbs more existing values and still permits an 8px rhythm. Recommend 8px only when the extraction shows the codebase already clusters on multiples of 8.
R2. Type ratio
Decides: the multiplier between adjacent steps. 1.125, 1.200, 1.250 or 1.333.
Default: 1.200 for dense interfaces, 1.250 for marketing pages. Quote the absorption number from drift.json.
R2. Color space
Decides: HSL or OKLCH for token definitions.
Default: OKLCH on Tailwind v4. HSL on v3, and always when components.json exists, to match shadcn's hsl(var(--x)) format.
R2. Radius base
Decides: one base value. sm and lg derive from it with a 2px offset.
Default: the value the extraction working set already centers on.
R2. Motion curve
Decides: the easing pair for entering and exiting.
Default: ease-out entering, ease-in exiting.
R3. Spacing steps
Decides: which multiples of the base are legal. Requires the base. Default: 6 to 8 steps.
R3. Type sizes and leading
Decides: how many steps, and the line-height rule. Requires the ratio. Default: 6 sizes. Line-height inverse to size: tighter as size increases.
R3. Semantic roles
Decides: what primary, muted, accent and destructive mean in this product. Requires the color space.
Default: when components.json exists, adopt shadcn's role names unchanged.
Note: this node determines whether color is maintainable. A raw palette class appears in every file that uses it; a role is changed in one place.
R3. Duration scale
Decides: two or three durations mapped to interaction classes. Requires the motion curve. Default: 150ms for hover and press, 250ms for dropdowns and tooltips, 400ms for modals and route changes.
R4. Component shape
Decides: control heights, density, and which variants exist. Requires all R2 and R3 nodes. Default: derive control heights from the spacing steps. Do not introduce a second scale.
R5. Patterns
Decides: empty, loading and error treatment, and what elevation encodes. Requires component shape. Default: elevation encodes distance from the page surface. Three levels.
Traversal rules
- Do not ask a node whose parent is unsettled.
- Recompute descendants after each answer. An answer can remove options from a later round.
- When
drift.jsonshows one value at 90% or more on an axis, treat that node as settled and present it for confirmation rather than asking. - The phase ends when the frontier is empty, not when enough is known to proceed.
Asset assets/round-shell.html 55 lines
Show source Hide source
<!doctype html>
<!-- Frame for a rendered grill round. Fill: TITLE, ROUND_N, OPTIONS.
Keep it single-file. Substitute the project's real font stack and palette. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Round ROUND_N: TITLE</title>
<style>
:root{
--bg:#fafafa; --fg:#0a0a0a; --muted:#71717a; --line:#e4e4e7;
--card:#fff; --pick:#2563eb; --pick-bg:#eff6ff;
/* SUBSTITUTE the project's real font stack here */
--font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
}
:root:not([data-theme=light]) @media (prefers-color-scheme:dark){}
@media (prefers-color-scheme:dark){ :root:not([data-theme=light]){
--bg:#09090b; --fg:#fafafa; --muted:#a1a1aa; --line:#27272a;
--card:#131316; --pick:#60a5fa; --pick-bg:#172554;
}}
:root[data-theme=dark]{ --bg:#09090b; --fg:#fafafa; --muted:#a1a1aa;
--line:#27272a; --card:#131316; --pick:#60a5fa; --pick-bg:#172554; }
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font);
padding:48px 16px 96px;line-height:1.5;-webkit-font-smoothing:antialiased}
.wrap{max-width:1100px;margin:0 auto}
.eyebrow{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:0 0 8px}
h1{font-size:clamp(24px,4vw,34px);margin:0 0 6px;letter-spacing:-.02em}
.sub{color:var(--muted);margin:0 0 40px;max-width:62ch}
.grid{display:grid;gap:20px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
.opt{background:var(--card);border:1px solid var(--line);border-radius:12px;
padding:24px;position:relative;overflow:hidden}
.opt[data-pick]{border-color:var(--pick);box-shadow:0 0 0 3px var(--pick-bg)}
.tag{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;
letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin-bottom:16px}
.opt[data-pick] .tag{color:var(--pick)}
.why{margin-top:20px;padding-top:16px;border-top:1px solid var(--line);
font-size:13px;color:var(--muted)}
.opt[data-pick] .why{color:var(--pick)}
/* specimen area, replace per round */
.specimen{display:flex;flex-direction:column;gap:10px}
</style>
</head>
<body>
<div class="wrap">
<p class="eyebrow">Round ROUND_N</p>
<h1>TITLE</h1>
<p class="sub">SUBTITLE. State what is being decided and what it unblocks.</p>
<div class="grid">
<!-- OPTIONS: one .opt per choice; add data-pick to the recommendation -->
</div>
</div>
</body>
</html>