Design
Rules and checks
Every design rule by name, and what holds it: the kit that’s its only implementation, the check that measures it, or the audit that looks at it.
On this page
Every rule in Principles is held in at least one of three ways (how a rule is held): the kit is its only implementation, a check measures it, or the design audit looks at it before a release. This chapter lists them all, by name.
Every rule, and what holds it#
“By eye” means the /design-audit skill’s look over every game’s screens (.claude/skills/design-audit/SKILL.md). A check kind in brackets is report-only for now: it’s printed, but doesn’t fail the run.
| Rule | The kit | The checks | By eye |
|---|---|---|---|
| Same place, same word | sheet(), result(), openOptions(), backArrow() |
check:house |
yes |
| Never stranded | result() fixes the actions for each ending |
check:house (each ending’s buttons and order) |
that every ending reaches one |
| Progress stays put | keep, prefs() |
each game’s own tests | yes |
| The way up | backArrow() |
check:house; check:layout (one 44px arrow, its name and where it goes) |
|
| Undo or ask | toast() with Undo; Slot.set({ confirm }) |
yes | |
| Helpers on | openOptions() |
check:house |
|
| Keys and focus | sheet(), shortcuts(), keyCap() |
check:house (Escape, focus, Tab, keys while typing); check:details: stray (a key cap showing for a hidden control) |
with a screen reader |
| A finger’s reach | .house-reach |
check:details: tap, taken |
|
| Every screen fits | the safe-area gutter | check:details: offscreen; check:layout (no sideways scroll) |
|
| Errors say what’s next | yes | ||
| Type roles | the scale in house.css |
check:tokens (every root’s sizes); check:layout (the sizes drawn) |
|
| Readable | check:tokens (soft ink and accent ink on their surfaces); check:room (each table style’s words and marks); check:details: [readable] |
||
| Words fit | check:details: clip, overflow |
||
| Room to breathe | check:details: [room] |
||
| Centered by ink | .house-glyph, closeIcon(), MID |
check:details: center, icon, and the painted pass; check:details:self |
|
| Rows in line | check:details: align |
||
| Plain words | the kit’s own words | yes | |
| The glance | .house-figure |
yes | |
| Never only color or motion | seat shapes; reducedMotion() |
check:house (reduced motion) |
yes |
| One fill | a sheet’s foot and the result sheet take one primary | check:details: alike (a fill from where something sits) |
yes |
| Three weights | configure() |
yes | |
| Choosing isn’t doing | seg() |
check:tokens (every root declares a pick) |
|
| Look follows meaning | check:details: alike |
yes | |
| Colors have jobs | check:tokens (every table game declares the jobs) |
yes | |
| One skeleton | the layout tokens in house.css |
check:tokens (the rhythm; no game sets a layout token); check:layout (gutter, bar, column, kicker) |
|
| One setting pattern | setting(), openOptions() |
check:house (Options’ order) |
|
| Nothing moves under you | slots that keep their space; presses ignored for 0.4s | check:house (a sheet’s first 400ms) |
yes |
| Nothing over anything | check:details: overlap |
||
| The quietest signal | toast(), banner(), announce(), celebrate() |
check:house (where each goes, and its queue) |
which one a game picks |
| Fair clocks | onSheets(), setSheetClock() |
check:house |
|
| Motion explains | MOTION and the --m-* tokens |
check:house (the two agree; reduced motion) |
yes |
| Your move | prompt(), beckon() |
check:house; check:room (the turn ring stands out in every table style) |
|
| See others’ moves | the room’s Cues |
check:room (beats and holds) |
|
| Done to you | announce(); the room’s onyours.js |
check:room |
yes |
| Celebrate a victory | celebrate(), result({ celebrate }) |
check:house |
|
| The game’s own look | check:tokens (what a game may set) |
yes |
The checks, and what they measure#
| Check | Needs | Measures |
|---|---|---|
npm run check:tokens |
nothing | Every stylesheet, read as text: each root’s tokens and their sizes, contrast on each surface, no colors in the structure files, nothing set that’s the house’s |
npm run check:house, npm run check:room |
Node | The kits’ behavior, on a fake page: sheets, keys, the result sheet, Options, motion, cues; and each table style’s marks against its felt |
npm run check:layout |
the site and Chrome | Every family’s screens at four sizes: gutters, the bar, the column, the type roles, buttons, panels and sheets |
npm run check:details |
the site and Chrome (or WebKit) | Every game’s screens at seven sizes: each kind in the table above |
npm run check:details:self |
Chrome and WebKit | That check:details still finds what it should, from planted mistakes |
How to run each one, and when, is in Testing and checks.
Every finding names its rule#
check:tokens, check:layout and check:details end each finding with the rule it breaks, by name, as [one-skeleton], so a finding leads straight to its principle (/guide/design/principles/#one-skeleton). The names come from tools/house/rulebook.mjs, which reads them from the Principles chapter’s headings, and a name that isn’t a rule throws:
tools/house/rulebook.mjs rule · lines 32–37// A finding's rule, by name, for a check's output: "[one-skeleton]". An
// unknown name throws, so a check can't cite a rule that isn't one.
export function rule(name) {
if (!NAMES.has(name)) throw new Error(`no design rule named ${name} in ${PRINCIPLES}`);
return `[${name}]`;
}
check:details maps each kind it finds to its rule:
tools/house/rulebook.mjs DETAILS · lines 39–45// Each kind check:details finds, and the rule it holds.
export const DETAILS = {
center: 'centered-by-ink', icon: 'centered-by-ink', overlap: 'nothing-over-anything',
clip: 'words-fit', overflow: 'words-fit', offscreen: 'every-screen-fits', align: 'rows-in-line',
tap: 'a-fingers-reach', taken: 'a-fingers-reach', stray: 'keys-and-focus',
alike: 'look-follows-meaning', room: 'room-to-breathe', readable: 'readable',
};
Nothing goes unheld. tools/house/rulebook.test.mjs, part of npm run check:house, fails when:
- a rule in Principles is missing from the table above, or the table names one that isn’t a rule;
- a rule’s row has nothing in it: no kit, no check and not by eye;
- a kind
check:detailsfinds has no rule, or its rule’s row doesn’t list it (in brackets while it’s report-only); check:tokensorcheck:layoutcites a rule that doesn’t exist.
So a new rule comes with what holds it, and renaming one means renaming it everywhere it’s cited.
Exceptions#
A finding that’s meant is listed with why, and every other finding is fixed. The lists are in the checks themselves:
ALLOWintools/house/details.mjs: a badge on a corner, a fanned hand under 44px, a seat’s name ellipsized, and the rest.EXCEPTIONSintools/house/layout.mjs: The Register’s masthead, the one title not on the scale.
An exception names the screen and the reason a player would recognize. “The code put it there” isn’t a reason.