@freetesh/canon 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -7
- package/cli/dist/api.d.ts +2 -2
- package/cli/dist/api.js +2 -2
- package/cli/dist/checks/{external.js → commands.js} +2 -0
- package/cli/dist/checks/designs.d.ts +5 -0
- package/cli/dist/checks/designs.js +33 -0
- package/cli/dist/checks/index.js +6 -3
- package/cli/dist/checks/stale-copies.d.ts +2 -0
- package/cli/dist/checks/stale-copies.js +18 -0
- package/cli/dist/checks/{copies.d.ts → standards-copies.d.ts} +0 -1
- package/cli/dist/checks/{copies.js → standards-copies.js} +0 -16
- package/cli/dist/commands/add.d.ts +1 -1
- package/cli/dist/commands/add.js +1 -1
- package/cli/dist/commands/check.d.ts +1 -1
- package/cli/dist/commands/check.js +2 -2
- package/cli/dist/commands/copies.d.ts +1 -1
- package/cli/dist/commands/hooks.d.ts +1 -1
- package/cli/dist/commands/hooks.js +2 -2
- package/cli/dist/commands/init.d.ts +1 -1
- package/cli/dist/commands/init.js +2 -2
- package/cli/dist/commands/learn.d.ts +1 -1
- package/cli/dist/commands/learn.js +2 -2
- package/cli/dist/commands/pipeline.d.ts +1 -1
- package/cli/dist/{search → commands}/search.js +1 -1
- package/cli/dist/commands/standards.d.ts +1 -1
- package/cli/dist/commands/status.d.ts +1 -1
- package/cli/dist/commands/sync.d.ts +1 -1
- package/cli/dist/commands/sync.js +1 -1
- package/cli/dist/commands/ui.d.ts +1 -1
- package/cli/dist/commands/ui.js +2 -2
- package/cli/dist/config/load.js +18 -6
- package/cli/dist/config/schema.d.ts +24 -3
- package/cli/dist/config/schema.js +17 -3
- package/cli/dist/format.d.ts +1 -1
- package/cli/dist/index.js +2 -2
- package/cli/dist/init.js +2 -2
- package/cli/dist/packs.d.ts +3 -1
- package/cli/dist/standards.d.ts +11 -6
- package/cli/dist/standards.js +37 -17
- package/cli/dist/status.d.ts +1 -1
- package/cli/dist/status.js +1 -1
- package/package.json +3 -3
- package/packs/expo/pack.yaml +1 -1
- package/packs/firebase/pack.yaml +1 -1
- package/packs/react/pack.yaml +1 -1
- package/packs/revenuecat/pack.yaml +1 -1
- package/packs/uniwind/pack.yaml +1 -1
- package/scripts/check-designs.py +709 -0
- package/standards/README.md +23 -6
- package/standards/agent-contract.md +21 -6
- package/standards/context-layout.md +41 -6
- package/standards/library/design-sets.md +857 -0
- package/standards/library/expo.md +54 -0
- package/standards/library/frontend.md +44 -0
- package/standards/library/performance.md +45 -0
- package/standards/library/pipeline.md +42 -0
- package/standards/library/secrets.md +37 -0
- package/standards/library/store-listing.md +122 -0
- /package/cli/dist/{run.d.ts → check.d.ts} +0 -0
- /package/cli/dist/{run.js → check.js} +0 -0
- /package/cli/dist/checks/{external.d.ts → commands.d.ts} +0 -0
- /package/cli/dist/checks/{metrics.d.ts → metrics-definitions.d.ts} +0 -0
- /package/cli/dist/checks/{metrics.js → metrics-definitions.js} +0 -0
- /package/cli/dist/{search → commands}/mcp.d.ts +0 -0
- /package/cli/dist/{search → commands}/mcp.js +0 -0
- /package/cli/dist/{search → commands}/search.d.ts +0 -0
- /package/cli/dist/{commands → lib}/env.d.ts +0 -0
- /package/cli/dist/{commands → lib}/env.js +0 -0
- /package/cli/dist/{search → lib}/qmd.d.ts +0 -0
- /package/cli/dist/{search → lib}/qmd.js +0 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
# Design sets
|
|
2
|
+
|
|
3
|
+
How a repo's HTML design set is built. Opt in with `standards: [design-sets]`.
|
|
4
|
+
|
|
5
|
+
Every rule below is a hard rule with a reason attached. The reason is there so
|
|
6
|
+
you can tell when a rule truly does not apply — not so you can argue past it.
|
|
7
|
+
|
|
8
|
+
The product's UX is its own. A game and a tax app should look nothing alike.
|
|
9
|
+
**Everything around the screens is fixed**, so that learning one set teaches you
|
|
10
|
+
all of them: where the gallery is, what the bar does, how a variant is named,
|
|
11
|
+
where the reasoning lives.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 0. The one-line version
|
|
16
|
+
|
|
17
|
+
A design set is a folder of static HTML screens, one screen per file, declared
|
|
18
|
+
in a single manifest, wrapped in an identical demo navbar, indexed by a flow
|
|
19
|
+
chart, and checked against the app it describes.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Layout on disk
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
<repo>/.context/designs/
|
|
27
|
+
brand/ # source art: icon.svg · splash-icon.svg · adaptive layers · wordmark. Not a surface.
|
|
28
|
+
architecture/ # diagram sources (<slug>.json) and their drawn pages. Not a surface — see §4
|
|
29
|
+
email/ # transactional email HTML. An email is a surface of its own, drawn in HTML
|
|
30
|
+
<surface>/ # `mobile` or `web` — the surfaces that carry the set machinery below
|
|
31
|
+
index.html # the gallery — a flow chart, nothing else
|
|
32
|
+
screenshots.html # every screen at once, as live iframes
|
|
33
|
+
routes.js # THE MANIFEST — the one place a screen is declared
|
|
34
|
+
_gallery.js # renders the flow chart from the model
|
|
35
|
+
_chrome.js # derives the model (window.WF), injects the navbar
|
|
36
|
+
shared.css # CORE, then PALETTE, then this product's components
|
|
37
|
+
tailwind.config.js # loaded AFTER the CDN; every value is var(--token)
|
|
38
|
+
<flow>/<screen>.html # one screen per file
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`brand/` is the only home of source art. Store images, press media and the
|
|
42
|
+
app's own `assets/` are exports of it.
|
|
43
|
+
|
|
44
|
+
Documentation lives in `.context/wiki/`, never beside the HTML. No top-level
|
|
45
|
+
`docs/` or `wireframes/` folder: that material belongs under `.context/`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. `routes.js` is the single source
|
|
50
|
+
|
|
51
|
+
`_chrome.js`, `index.html` and `screenshots.html` all read it. Before it existed
|
|
52
|
+
the screen order lived in three places — the gallery's table, each screen's
|
|
53
|
+
hand-written sibling strip, and nobody's idea of prev/next — and they disagreed,
|
|
54
|
+
because three copies of a list always do.
|
|
55
|
+
|
|
56
|
+
**`routes.js` is read exactly once, by `_chrome.js`**, which publishes the
|
|
57
|
+
derived model as `window.WF` and is loaded by every page. Nothing else may walk
|
|
58
|
+
the manifest itself. The three readers each used to rebuild their own maps from
|
|
59
|
+
it and came to disagree about what a state was: the chart collapsed
|
|
60
|
+
`Board · loading` onto Board, the contact sheet gave it a tile of its own, and
|
|
61
|
+
back/next stepped through a list that matched neither.
|
|
62
|
+
|
|
63
|
+
| `window.WF` | |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `flat` | every file, states inline, manifest order — what `back`/`next` walk |
|
|
66
|
+
| `meta` / `base` | one entry per screen; `base(file)` maps a state onto its screen |
|
|
67
|
+
| `links` / `col` | the graph collapsed onto screens, and each screen's rank |
|
|
68
|
+
| `mainPath` / `mainNodes` | the happy path, start to first end |
|
|
69
|
+
| `journey` | `[{ flow, screens }]` — flows and screens in the order reached |
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
window.ROUTES = {
|
|
73
|
+
product: "My Game",
|
|
74
|
+
surface: "mobile",
|
|
75
|
+
surfaces: ["mobile"], // every surface this product draws
|
|
76
|
+
themes: ["light", "dark"], // every theme this SET draws, not the app
|
|
77
|
+
frame: { w: 390, h: 844 },
|
|
78
|
+
architecture: "architecture/overview.html", // see §4; omit if none
|
|
79
|
+
|
|
80
|
+
/* Reserved order: states · auth · onboarding · home · * · settings · store · patterns */
|
|
81
|
+
flows: [
|
|
82
|
+
{ flow: "game", screens: [
|
|
83
|
+
{ file: "game/board.html", screen: "Board",
|
|
84
|
+
states: { loading: "game/board-loading.html" },
|
|
85
|
+
status: "built", // built | approved | review
|
|
86
|
+
notes: "board" }, // wiki slug, or null — see §10
|
|
87
|
+
]},
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
flow: { // the chart
|
|
91
|
+
start: "states/splash.html",
|
|
92
|
+
end: ["game/win.html"],
|
|
93
|
+
edges: [ /* see §6 */ ],
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
updated: "2026-09-22",
|
|
97
|
+
missing: [{ what: "Interstitial", why: "ships and has no wireframe" }],
|
|
98
|
+
states: { empty: { drawn: null, note: "why not" } },
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**A screen not in `routes.js` appears nowhere.** That is the point: adding one is
|
|
103
|
+
a single edit, and forgetting is visible immediately rather than months later.
|
|
104
|
+
|
|
105
|
+
Status (`built` / `approved` / `review`) and the five-state coverage live in
|
|
106
|
+
`routes.js`, where the bar reads them. They are data, not a page.
|
|
107
|
+
|
|
108
|
+
A screen file declares only its own path. Everything else is derived, and the
|
|
109
|
+
bar is injected from it — never hand-write chrome into a screen:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<body data-chrome data-file="game/board.html">
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 3. Tailwind, and only Tailwind
|
|
118
|
+
|
|
119
|
+
Static HTML plus Tailwind via the CDN. **No React, no build step, no other CSS
|
|
120
|
+
framework, no hand-written utility classes.**
|
|
121
|
+
|
|
122
|
+
Every screen loads exactly this, in this order, with **pinned** versions:
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
|
126
|
+
<script src="../tailwind.config.js"></script>
|
|
127
|
+
<script src="https://unpkg.com/lucide@0.544.0/dist/umd/lucide.js"></script>
|
|
128
|
+
<link rel="stylesheet" href="../shared.css" />
|
|
129
|
+
...
|
|
130
|
+
<script src="../routes.js"></script>
|
|
131
|
+
<script src="../_chrome.js"></script>
|
|
132
|
+
<script>lucide.createIcons();</script>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
An unpinned CDN can silently restyle every screen in every repo overnight.
|
|
136
|
+
|
|
137
|
+
`index.html` and `screenshots.html` load the lucide line too. They have no
|
|
138
|
+
screen on them, but they carry the same navbar, and the navbar is icons.
|
|
139
|
+
|
|
140
|
+
- **Icons are lucide**, the same set the Expo app uses via `lucide-react-native`,
|
|
141
|
+
so an icon never has to be re-chosen at build time. Use the *lucide* name of
|
|
142
|
+
the component the app imports: `CircleCheckBig` → `circle-check-big`.
|
|
143
|
+
- **One icon set, and nothing hand-copied.** No inline `<svg>` in a screen and
|
|
144
|
+
no path data in `_chrome.js`: ask lucide for the icon by name. A mark lucide
|
|
145
|
+
cannot supply — the product's own logo — is **one file under `brand/`**,
|
|
146
|
+
referenced by every screen that shows it. Pasted inline it exists twice and
|
|
147
|
+
drifts twice; one game's mark had two copies with two different sets of
|
|
148
|
+
gradient ids before it became `brand/mark.svg`.
|
|
149
|
+
- **A DRAWING is not an icon, and the rule above is about icons.** A progress
|
|
150
|
+
ring, a sparkline, a chart — something the app itself renders as SVG and whose
|
|
151
|
+
shape carries data — has no lucide name and no static file, because it changes
|
|
152
|
+
with the number. Draw it, but **declare the geometry once, as a component
|
|
153
|
+
class in `shared.css`**, and leave only the datum in the markup:
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<svg class="ring-svg [--arc:116]" viewBox="0 0 118 118">
|
|
157
|
+
<circle class="ring-track" /><circle class="ring-fill" />
|
|
158
|
+
</svg>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The test is the same one as for a mark: is there one source of truth? One
|
|
162
|
+
set's calorie ring was five copies of nine identical attributes differing in
|
|
163
|
+
one number — nine chances to drift, on the first thing on the home screen.
|
|
164
|
+
Prefer CSS where CSS can say it (a countdown timer can be a conic gradient),
|
|
165
|
+
and keep SVG where it cannot — round caps and a bottom gap are the shape of a
|
|
166
|
+
ring like that, and a conic gradient has neither.
|
|
167
|
+
- `style="..."` is for what a class truly cannot express — an exact type
|
|
168
|
+
ramp step, a one-off overlay alpha. Not for colour that a token exists for.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 4. The demo navbar
|
|
173
|
+
|
|
174
|
+
**One bar, identical in every product**, injected by `_chrome.js`. It renders on
|
|
175
|
+
a screen, on the flow chart and on the screenshots sheet, at the same height,
|
|
176
|
+
with every control in the same place. Navigation on the left, settings on the
|
|
177
|
+
right, and what you are looking at in between — so the two never trade places
|
|
178
|
+
and you stop hunting for the control you just used.
|
|
179
|
+
|
|
180
|
+
| Group | Contents |
|
|
181
|
+
|---|---|
|
|
182
|
+
| **Left** | all routes · screenshots · architecture · back · next — all icons |
|
|
183
|
+
| **Middle** | every screen in this flow, then this screen's states — words |
|
|
184
|
+
| **Right** | light / dark · mobile / web · notes — all icons |
|
|
185
|
+
|
|
186
|
+
- **Architecture** (`network`) opens how the product is built, between
|
|
187
|
+
screenshots and back. `routes.js` names the page in `architecture`, relative
|
|
188
|
+
to the surface folder; a set without one shows the chip struck out, so the
|
|
189
|
+
chips after it never move. See "The architecture pages" below.
|
|
190
|
+
- **Key screens.** With more than a handful of screens, every box at once is a
|
|
191
|
+
wall nobody can read. A set marks the few screens that explain the product
|
|
192
|
+
with `key` in `routes.js`: `key: "Editor"` puts a screen in a named row,
|
|
193
|
+
`key: true` in its flow's own row. Rows group related flows (for example
|
|
194
|
+
Start · Onboarding · Editor · Settings). Base screens only, never a state.
|
|
195
|
+
One to five a row, about 8–25 in all.
|
|
196
|
+
- The chart then opens on those rows, one row per lane, each read left to
|
|
197
|
+
right, arrows only between neighbours, and a label only on a direct link (a
|
|
198
|
+
link that steps over hidden screens would borrow a label the reader cannot
|
|
199
|
+
see). Rows come in the order their first screen appears in `routes.js`.
|
|
200
|
+
- The contact sheet opens on the key tiles, grouped by the same rows.
|
|
201
|
+
- Both have a switch to every screen. A set that marks none shows everything.
|
|
202
|
+
- **A control that means the same thing on every page is an icon.** All eight
|
|
203
|
+
of them — all routes, screenshots, architecture, back, next, theme, surface,
|
|
204
|
+
notes — never change meaning between products, so the word was only ever a
|
|
205
|
+
label for something you already knew. They are lucide, asked for by name, so
|
|
206
|
+
the bar carries no icon set of its own. Icons: `workflow`, `layout-grid`,
|
|
207
|
+
`network`, `chevron-left`, `chevron-right`, `sun`, `moon`, `smartphone`,
|
|
208
|
+
`monitor`, `info` — theme and surface are two-state, so they take two each.
|
|
209
|
+
- **The middle fits on one line.** The screen names scroll when they overflow,
|
|
210
|
+
and the state chips sit after them, so an overflowing flow hides its states.
|
|
211
|
+
Keep each flow small enough to fit the frame's width: split a long flow by
|
|
212
|
+
what it is about (twenty settings screens became product, company and account
|
|
213
|
+
settings), and **never repeat the flow in a screen's name** — in `pages`,
|
|
214
|
+
"Code", not "Pages · Code"; the flow already says it. A state key outside the
|
|
215
|
+
closed set (§5) is not shown at all, so anything else is a screen.
|
|
216
|
+
- **A word stays where the word IS the content.** The screen names in the
|
|
217
|
+
middle and the state names beside them are the only text left in the bar.
|
|
218
|
+
- **Every icon keeps its label** in `title` and `aria-label`, so hovering says
|
|
219
|
+
which control it is. An icon you have to click to understand is worse than
|
|
220
|
+
the word it replaced. If the lucide CDN fails, each icon falls back to
|
|
221
|
+
rendering its own label rather than an empty square.
|
|
222
|
+
- **There is no breadcrumb.** `mobile · /game ›` said what the surface toggle
|
|
223
|
+
on the right already says and what the flow tint in the chart already says,
|
|
224
|
+
and it sat between the nav chips and the screen chips pushing the two apart
|
|
225
|
+
for no information.
|
|
226
|
+
- **The bar has its own fixed colours**, `--chrome-*`, declared in the CORE
|
|
227
|
+
block of `shared.css`. **It must never read a PALETTE token.** Reading
|
|
228
|
+
`--brand-500` made the active chip blue in one set and navy in another, so the
|
|
229
|
+
same control looked like a different control, and the bar read as part of the
|
|
230
|
+
product rather than as the thing holding it. Other CORE tokens are fine and
|
|
231
|
+
are used — `--font-mono` is the same stack in every set, so it cannot drift.
|
|
232
|
+
- **Both halves of each right-hand control are always rendered.** A set with no
|
|
233
|
+
dark palette shows `dark` struck through; a product with no web surface shows
|
|
234
|
+
`web` struck through. A control that appears on some pages and not others
|
|
235
|
+
moves everything beside it — and a theme the app ships but nobody has drawn is
|
|
236
|
+
a gap, which has to *look* like a gap rather than like "there is no dark here".
|
|
237
|
+
- `back` / `next` step through the set in route order and are struck through at
|
|
238
|
+
the ends, so the bar does not reflow as you walk it.
|
|
239
|
+
- **The bar is measured, not assumed.** It wraps to two rows on a narrow window,
|
|
240
|
+
and `_chrome.js` publishes `--chrome-h` so the frame can clamp under it. A
|
|
241
|
+
guessed constant is wrong exactly when the window is small, which is when
|
|
242
|
+
fitting matters.
|
|
243
|
+
- **`?bare` renders a screen with no bar at all** — what the screenshots sheet
|
|
244
|
+
embeds, and what you want when capturing one screen. It also sets `data-bare`
|
|
245
|
+
on the root, which drops the stage gutter and the device rounding.
|
|
246
|
+
|
|
247
|
+
### The notes button
|
|
248
|
+
|
|
249
|
+
- **The `info` icon at the far right, past the display controls.** It is the
|
|
250
|
+
only control in the bar that opens something *over* the screen rather than
|
|
251
|
+
changing which screen you are looking at, so it sits apart from the ones that
|
|
252
|
+
navigate, and it is the only round single chip — the segmented pairs beside
|
|
253
|
+
it are pills, every other chip is a rounded rectangle. As a word among the
|
|
254
|
+
screen names it read as another screen.
|
|
255
|
+
- The drawer is collapsed by default. It opens on the right, has a **header
|
|
256
|
+
naming the screen and a close button**, and closes on Escape. A panel covering
|
|
257
|
+
a third of the window needs a way out that is not the button you opened it
|
|
258
|
+
with, which the bar may have scrolled past.
|
|
259
|
+
- **Every screen has a notes button.** A screen with no wiki page still renders
|
|
260
|
+
the button, struck out, for the same reason a missing theme is struck out.
|
|
261
|
+
|
|
262
|
+
### The architecture pages
|
|
263
|
+
|
|
264
|
+
Every set carries an `architecture` flow: how the product is built.
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
<repo>/.context/designs/
|
|
268
|
+
architecture/ # ASSETS, like brand/: <slug>.json (the source) + <slug>.html (drawn)
|
|
269
|
+
<surface>/architecture/ # SCREENS: overview.html + one screen per diagram, declared in routes.js
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- **Diagrams are small and answer one question each**: who uses it, how it is
|
|
273
|
+
built, the backend (only when there is a server), what it relies on. At most
|
|
274
|
+
six boxes and straight arrows. One big diagram of everything was unreadable.
|
|
275
|
+
- They are drawn by the **Archify** agent skill from their `.json`. Edit the
|
|
276
|
+
`.json`, then in `designs/architecture/` run
|
|
277
|
+
`ARCHIFY_UPDATE_CHECK_DISABLED=1 node <archify>/bin/archify.mjs finalize
|
|
278
|
+
architecture <slug>.json <slug>.html --quality showcase`. It must pass; delete
|
|
279
|
+
its receipts after.
|
|
280
|
+
- **The screens are generated** from a spec that names the set, the diagrams,
|
|
281
|
+
the stack tables (library, version, what it is for), one line on the
|
|
282
|
+
product's own code, and the monthly cost. The generator writes the screens
|
|
283
|
+
and their `routes.js` entries.
|
|
284
|
+
- A screen frames its diagram in Archify's embed mode (`?embed=1&theme=light`),
|
|
285
|
+
in a frame of the diagram's own shape, so nothing is cropped and the only
|
|
286
|
+
theme switch is the set's own bar. In a phone set the architecture screens
|
|
287
|
+
drop the 390px frame and fill the window under the bar.
|
|
288
|
+
- **The overview is architecture and cost only**: the diagrams, the stack by
|
|
289
|
+
layer with versions, the monthly cost. How a feature works belongs in the
|
|
290
|
+
wiki and the docs, not here. Facts come from the repo; a fact that is not in
|
|
291
|
+
it is left out or marked, never invented.
|
|
292
|
+
- Notes: `.context/wiki/surfaces/architecture.md`.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 5. One screen per file
|
|
297
|
+
|
|
298
|
+
A second frame in a file has no URL, cannot appear in `routes.js`, cannot be
|
|
299
|
+
linked to, and does not show up in the screenshots sheet. So:
|
|
300
|
+
|
|
301
|
+
- **A theme is not a second frame.** It is the toggle. One file, both themes.
|
|
302
|
+
- **A state is its own file**: `<screen>-<state>.html`, with the state drawn
|
|
303
|
+
from the closed set `empty · loading · error · offline · locked`.
|
|
304
|
+
- **Anything the closed set cannot name is its own screen, not a state.** "Draw"
|
|
305
|
+
and "Ads off" are screens with their own base names, not `-draw` and `-off`
|
|
306
|
+
variants. Stretching a state word to cover them is how a vocabulary stops
|
|
307
|
+
meaning anything.
|
|
308
|
+
- **No prose inside a screen file.** No annotation sidebar, no rationale column,
|
|
309
|
+
no explanatory paragraph. The screen is the thing being reviewed, and an essay
|
|
310
|
+
beside it competes for the eye. Comments in the markup are fine and expected.
|
|
311
|
+
|
|
312
|
+
Not every screen has every state; a loading state on an about page is a rule
|
|
313
|
+
people learn to ignore. Draw the real ones and let `routes.js` say in one line
|
|
314
|
+
why each of the others is not drawn.
|
|
315
|
+
|
|
316
|
+
### Flow names
|
|
317
|
+
|
|
318
|
+
One folder per flow, lowercase kebab-case. Seven names recur across products
|
|
319
|
+
and mean the same thing every time, so they are reserved and always sort to the
|
|
320
|
+
same place; everything else is the product's own domain and sorts
|
|
321
|
+
alphabetically in the `*` slot:
|
|
322
|
+
|
|
323
|
+
`states · auth · onboarding · home · * · settings · store · patterns`
|
|
324
|
+
|
|
325
|
+
**`states` sorts first because it holds `splash`** — the screen the product
|
|
326
|
+
starts on. `back`/`next` walk manifest order and the contact sheet breaks ties
|
|
327
|
+
with it, so a manifest that does not begin where the product begins puts the
|
|
328
|
+
entry screen near the end and makes the first `back` dead in the wrong place.
|
|
329
|
+
The rest of `states` — `404`, `offline`, `maintenance` — comes along with it,
|
|
330
|
+
which costs nothing: nobody browses to a 404 on purpose.
|
|
331
|
+
|
|
332
|
+
| Flow | Owns |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `auth` | sign-in, verify, profile — getting an identity |
|
|
335
|
+
| `onboarding` | first run only. A screen shown again later is not onboarding |
|
|
336
|
+
| `home` | the first screen after entry |
|
|
337
|
+
| `settings` | changing the thing, including how-to-play and about |
|
|
338
|
+
| `store` | paywall, packs, upgrade, ads — anything about money |
|
|
339
|
+
| `states` | app-wide: `404`, `500`, `offline`, `maintenance`, `splash` |
|
|
340
|
+
| `patterns` | reusable pieces shown alone: toasts, sheets, components |
|
|
341
|
+
|
|
342
|
+
**A `patterns` page is a screen file in every respect but one: the frame is
|
|
343
|
+
optional, and what it shows decides.** A modal, a sheet or a toast is a device
|
|
344
|
+
thing — it has a backdrop, it sits over a screen, and it only means anything at
|
|
345
|
+
390pt, so it belongs in the frame. A colour ramp, a type ramp or a component
|
|
346
|
+
inventory is not a device thing: nothing in the product renders it, it is wider
|
|
347
|
+
than 390pt, and squeezed into a phone it teaches less than the same ramp laid
|
|
348
|
+
out, so it goes frameless. Never more than one frame, the same as any other
|
|
349
|
+
file.
|
|
350
|
+
|
|
351
|
+
**Everything else about a patterns page is the ordinary rule**, including the
|
|
352
|
+
one most often broken here: **it carries no prose.** The specimens and their
|
|
353
|
+
legends are the content — a badge row that does not say what the badges mean is
|
|
354
|
+
useless, and a legend is inventory, not rationale. What a pattern is FOR goes in
|
|
355
|
+
its wiki page, which the notes button already opens. Pattern pages that open
|
|
356
|
+
with the same paragraph as their wiki page are two copies of one sentence —
|
|
357
|
+
exactly what §10 exists to prevent.
|
|
358
|
+
|
|
359
|
+
Screens are named for what the screen **is**. **No `index.html` inside a flow** —
|
|
360
|
+
at surface root it is the gallery, and one level down the same name meaning "the
|
|
361
|
+
main screen of this flow" is ambiguous and has no variant form.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 6. `index.html` is a flow chart
|
|
366
|
+
|
|
367
|
+
**A horizontal flow chart and nothing else.** No title, no description, no meta
|
|
368
|
+
line, no tables, no status legend. The chart already names every screen, and
|
|
369
|
+
anything that needs explaining is a wiki page. It runs start → … → end, and
|
|
370
|
+
every screen in `routes.js` appears in it.
|
|
371
|
+
|
|
372
|
+
Drawn with **React Flow v11**, from `WF` — never from `routes.js`. `_gallery.js`
|
|
373
|
+
draws; it derives nothing.
|
|
374
|
+
|
|
375
|
+
```html
|
|
376
|
+
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
|
377
|
+
<script src="tailwind.config.js"></script>
|
|
378
|
+
<script src="https://unpkg.com/lucide@0.544.0/dist/umd/lucide.js"></script>
|
|
379
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
|
|
380
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
|
|
381
|
+
<script src="https://unpkg.com/reactflow@11.11.4/dist/umd/index.js"></script>
|
|
382
|
+
<link rel="stylesheet" href="https://unpkg.com/reactflow@11.11.4/dist/style.css" />
|
|
383
|
+
<link rel="stylesheet" href="shared.css" />
|
|
384
|
+
...
|
|
385
|
+
<script src="routes.js"></script>
|
|
386
|
+
<script src="_chrome.js"></script>
|
|
387
|
+
<script src="_gallery.js"></script>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`_chrome.js` comes before `_gallery.js`: it publishes the model the chart reads.
|
|
391
|
+
|
|
392
|
+
v11 and not v12: the v12 UMD bundle expects `react/jsx-runtime` as a page global,
|
|
393
|
+
which React's own UMD build does not provide.
|
|
394
|
+
|
|
395
|
+
**No layout library.** React Flow wants explicit positions, and the graph is a
|
|
396
|
+
shallow DAG with a declared start, so longest-path depth gives the column
|
|
397
|
+
directly. dagre or elk would be a third dependency solving a problem that is
|
|
398
|
+
already solved — and their columns would line up with what the engine thought
|
|
399
|
+
looked balanced rather than with the journey.
|
|
400
|
+
|
|
401
|
+
### Declaring the graph
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
flow: {
|
|
405
|
+
start: "states/splash.html",
|
|
406
|
+
end: ["game/score.html"],
|
|
407
|
+
edges: [
|
|
408
|
+
{ from: "states/splash.html", to: "home/home.html" },
|
|
409
|
+
{ from: "home/home.html", to: "game/play.html", label: "Let's Play" },
|
|
410
|
+
{ from: "game/score.html", to: "store/bundle.html", label: "more packs", side: true },
|
|
411
|
+
{ from: "game/score.html", to: "home/home.html", label: "Home", back: true },
|
|
412
|
+
],
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Four kinds of edge**, and only the first defines the shape of the chart:
|
|
417
|
+
|
|
418
|
+
| | Meaning | Drawn |
|
|
419
|
+
|---|---|---|
|
|
420
|
+
| *(none)* | a step forward | solid; heavier and brand-coloured on the main path |
|
|
421
|
+
| `side: true` | a shortcut or an upsell — real navigation, no progress | dotted |
|
|
422
|
+
| `back: true` | a return: Home, Back, Play Again | dashed, **hidden by default** |
|
|
423
|
+
| `state: true` | **not a step in the journey** — it involves a state screen | never drawn — dropped in `_chrome.js` before anything is ranked |
|
|
424
|
+
|
|
425
|
+
`side` exists because counting a shortcut as progress drags its target right: an
|
|
426
|
+
upsell from the score put the whole shop *after* the score and stretched the
|
|
427
|
+
spine across the page. Returns are hidden behind a toggle because every screen
|
|
428
|
+
goes back to Home, so they all converge and cross everything on the way.
|
|
429
|
+
|
|
430
|
+
### `state: true` is a judgement, not a shape
|
|
431
|
+
|
|
432
|
+
It does **not** mean "an edge that touches a state file". It means *this is not
|
|
433
|
+
a step in the journey*. Two edges both touching a state screen can go opposite
|
|
434
|
+
ways:
|
|
435
|
+
|
|
436
|
+
| | |
|
|
437
|
+
|---|---|
|
|
438
|
+
| `balances-offline → add` | The offline variant of Balances offers the same navigation the base screen already offers. Drawn again from the variant it is a duplicate. **`state: true`.** |
|
|
439
|
+
| `board-loading → win` | The game progresses through the thinking state to an outcome, and no other edge says so. Drop it and Win has no way in. **A plain forward edge.** |
|
|
440
|
+
|
|
441
|
+
Ask whether the journey moves. If the same step is already declared from the
|
|
442
|
+
base screen, it is not a second step.
|
|
443
|
+
|
|
444
|
+
### The ranked graph must be a DAG
|
|
445
|
+
|
|
446
|
+
Collapse the states, drop `state`, `back` and `side`, and what is left **must be
|
|
447
|
+
acyclic**. Longest-path ranking is only defined on a DAG, and those three flags
|
|
448
|
+
are what keep it one — that is what they are *for*, not a side effect.
|
|
449
|
+
|
|
450
|
+
Get it wrong and the failure is silent and enormous: relaxation round a cycle
|
|
451
|
+
never settles, so every screen in the loop is ranked by the pass budget instead
|
|
452
|
+
of by the journey. One set once put 34 screens at column 727 and drew a chart
|
|
453
|
+
**185,385px wide**, which reads as "broken" rather than as "mis-declared".
|
|
454
|
+
|
|
455
|
+
`_chrome.js` detects the cycle instead of waiting it out, falls back to BFS
|
|
456
|
+
depth — defined on any graph, always terminating — and warns in the console
|
|
457
|
+
naming the three flags. The chart stays readable; the warning tells you which
|
|
458
|
+
declaration to go and fix.
|
|
459
|
+
|
|
460
|
+
### Rules the chart must obey
|
|
461
|
+
|
|
462
|
+
- **One box per screen.** A state is a variant, not a second screen — it
|
|
463
|
+
collapses onto the screen it is a state of, and edges are rewritten through
|
|
464
|
+
the same map so a step that passes through a state still connects the screens
|
|
465
|
+
either side of it. Drawing states as their own nodes put "Board" beside
|
|
466
|
+
"Board · loading" and read as two places.
|
|
467
|
+
- **A node is a labelled rectangle** — the screen's name and its flow. Never a
|
|
468
|
+
thumbnail; the screenshots sheet is where you go to look at screens.
|
|
469
|
+
- **Each node carries an arrow in its corner that opens the screen.** The box
|
|
470
|
+
itself is not a link.
|
|
471
|
+
- **Clicking a box shows how you reach it**: every route into it lights up, the
|
|
472
|
+
rest fades. Computed over the edges currently drawn, so turning returns on
|
|
473
|
+
widens the answer rather than having silently counted paths nobody could see.
|
|
474
|
+
- **The main path is found, not declared** — start to the first end node,
|
|
475
|
+
following forward edges — and pinned to row 0 in every column, so the happy
|
|
476
|
+
path is one straight horizontal line with everything else hanging below it.
|
|
477
|
+
Ordering rows by declaration made the spine zig-zag between columns, which is
|
|
478
|
+
exactly what a flow chart exists to stop.
|
|
479
|
+
- **The main path reads first.** Its edges are heavier and brand-coloured and
|
|
480
|
+
its nodes fully opaque; everything else is lighter and slightly faded.
|
|
481
|
+
- **Each node carries its flow's colour on its left edge**, so you can tell the
|
|
482
|
+
shop from the game without reading a word: `home` brand, `store` accent,
|
|
483
|
+
`states` and `settings` ink, the product's main domain flow success, anything
|
|
484
|
+
else info. A small key names them — one swatch per flow the set uses, four to
|
|
485
|
+
seven of them, not a paragraph.
|
|
486
|
+
- **A flow's colour falls back rather than resolving to nothing.** `store` is
|
|
487
|
+
`var(--accent-500, var(--brand-500))`: only a set with a second brand colour
|
|
488
|
+
declares accent, and an undeclared `var()` paints *nothing at all* — a
|
|
489
|
+
colourless node edge and a blank swatch, which reads as a bug rather than as
|
|
490
|
+
a colour choice.
|
|
491
|
+
- The start node is ringed in `brand`, the end nodes in `success`.
|
|
492
|
+
- **Only forward edges carry a label.** A return is dashed and arrowed, which
|
|
493
|
+
already says "goes back here"; labelling them put three "Back"s on top of each
|
|
494
|
+
other.
|
|
495
|
+
- **A label sits at the target's row**, on the run going into the screen it
|
|
496
|
+
names — that is the end that makes it mean something, and five exits from one
|
|
497
|
+
screen no longer stack in the gutter. Labels landing on the same screen stack
|
|
498
|
+
a line apart. The few that still land on a box (an edge that skips a column
|
|
499
|
+
passes over the node between) are measured once the chart has settled and
|
|
500
|
+
lifted clear, with a halo rather than a background box, which would detach
|
|
501
|
+
from the moved text.
|
|
502
|
+
- **Every edge leaving one node bends at a different distance.** They share a
|
|
503
|
+
bend by default, so five exits from Home ran down one corridor and became a
|
|
504
|
+
single thick line.
|
|
505
|
+
- **Nothing may re-fit the chart after load.** Memoise the node and edge arrays;
|
|
506
|
+
rebuilding them every render made React Flow re-measure and the chart shivered
|
|
507
|
+
continuously.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## 7. `screenshots.html`
|
|
512
|
+
|
|
513
|
+
Every screen at once, live, at half scale — the fastest way to spot the one that
|
|
514
|
+
drifted.
|
|
515
|
+
|
|
516
|
+
- **Live iframes, not stills.** A sheet of stills is stale the day after it is
|
|
517
|
+
made, and this page exists to catch the screen that drifted.
|
|
518
|
+
- Each tile embeds `<screen>?bare`, which is why bare mode must fill its tile
|
|
519
|
+
exactly: no stage gutter, no device rounding, no device shadow.
|
|
520
|
+
- Tiles are labelled with the screen name and link to the screen.
|
|
521
|
+
- It carries the same demo navbar as every other page.
|
|
522
|
+
- **One tile per screen. States get no tile of their own** — the same rule as
|
|
523
|
+
the chart, and for the same reason. "Board" beside "Board · loading" beside
|
|
524
|
+
"Game History · empty" is one screen listed twice, and on a page whose whole
|
|
525
|
+
job is *find the one that drifted* the duplicate is noise. The navbar's state
|
|
526
|
+
switcher is where the variants live: there you are already looking at the
|
|
527
|
+
screen they are variants of.
|
|
528
|
+
- **Grouped by flow, in journey order.** The flow is a heading, once, instead
|
|
529
|
+
of a line under every tile; the flows run in the order the product first
|
|
530
|
+
reaches one and the screens inside a flow run by rank — so the sheet opens on
|
|
531
|
+
Splash, then Home, then the game. Manifest order breaks every tie. Both the
|
|
532
|
+
grouping and the order are `WF.journey`, which the chart ranks from the same
|
|
533
|
+
numbers, so the sheet and the chart cannot tell different stories.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## 8. Tokens
|
|
538
|
+
|
|
539
|
+
`shared.css` is three blocks, in this order:
|
|
540
|
+
|
|
541
|
+
1. **CORE** — identical in every product. The frame, the stage, the bar, the
|
|
542
|
+
notes drawer, the chart, the type ramp, the radii, the shadow geometry, and
|
|
543
|
+
the token *names*. Copied byte for byte (§13).
|
|
544
|
+
2. **PALETTE** — this product's colours, and nothing else.
|
|
545
|
+
3. **Components** — this product's own classes.
|
|
546
|
+
|
|
547
|
+
The naming rule is the whole naming policy:
|
|
548
|
+
|
|
549
|
+
> **the CSS variable is the Tailwind token path with dots as dashes.**
|
|
550
|
+
> `line.strong` ↔ `--line-strong`. `ink.2` ↔ `--ink-2`. `brand.500` ↔ `--brand-500`.
|
|
551
|
+
|
|
552
|
+
Required in every palette: `page`, `frame`, `surface`, `muted`, `sunken`; `ink`,
|
|
553
|
+
`ink-2`, `ink-3`, `ink-4`, `ink-inverse`; `line`, `line-strong`;
|
|
554
|
+
`brand-50/100/200/400/500/700/900`; `accent-*` where the product has a second
|
|
555
|
+
brand colour; `success`/`warn`/`danger`/`info` each with `-bg` and `-fg`; and
|
|
556
|
+
`--shadow-tint` as an `r g b` triple.
|
|
557
|
+
|
|
558
|
+
- **Colours are named by role** (`brand`, `accent`, `line`), never by hue
|
|
559
|
+
(`navy`, `gold`, `green`), so a repalette is a one-file change.
|
|
560
|
+
- **Every value in `tailwind.config.js` is `var(--token)`, never a literal.** A
|
|
561
|
+
literal compiled into `bg-surface` paints the dark frame white — the utility
|
|
562
|
+
stops following the scope it lands in, and a theme remap can no longer reach
|
|
563
|
+
it.
|
|
564
|
+
- **A theme is a token remap** (`.phone--dark`), never a `dark:` prefix and never
|
|
565
|
+
a second set of utilities.
|
|
566
|
+
- **`.phone` sets `color: var(--ink)` itself.** A variable remap only reaches
|
|
567
|
+
declarations that read the variable; text with no colour class inherits the
|
|
568
|
+
*computed* colour from `body`, resolved in the light scope, and stays black in
|
|
569
|
+
a dark frame.
|
|
570
|
+
- **Each product declares its reserved colours** at the top of the palette block:
|
|
571
|
+
the one or two that carry product meaning and may not be reused. In a party
|
|
572
|
+
game green may mean one role and black another; in a two-player game blue is X
|
|
573
|
+
and rose is O; gold may mark a paid action and nothing else.
|
|
574
|
+
- **Adding a class means declaring it in `shared.css` in the same edit.** A class
|
|
575
|
+
that resolves to nothing does not throw — it quietly does not paint, which is
|
|
576
|
+
how `text-2xs` came to be used 186 times across three apps while declared in
|
|
577
|
+
exactly one. The `tokens` check (§12) catches it.
|
|
578
|
+
- **A component class may not take a name Tailwind generates.** `.ring` looks
|
|
579
|
+
free and is not: `ring` is Tailwind's focus-shadow utility, so a component
|
|
580
|
+
class called `.ring` loses the cascade and paints a blue box round whatever it
|
|
581
|
+
was meant to draw. The CDN build emits the utility whether or not you meant
|
|
582
|
+
it, and nothing warns. Check the name against Tailwind's own before using it,
|
|
583
|
+
or suffix it — `.ring-svg`.
|
|
584
|
+
- **One stylesheet per surface.** `shared.css` and nothing else — no
|
|
585
|
+
`<flow>/<flow>.css`. A second sheet has no slot in §1, is loaded by some
|
|
586
|
+
screens and not others, and puts one flow's rules where no token check will
|
|
587
|
+
look at them. Its contents are either component classes, which belong in the
|
|
588
|
+
third block, or Tailwind utilities that were never written as utilities.
|
|
589
|
+
- **Two tokens are published at runtime, not declared in CSS**: `--chrome-h` and
|
|
590
|
+
`--stage-pad`, both set on the root by `_chrome.js`. Every read of them carries
|
|
591
|
+
a fallback (`var(--chrome-h, 0px)`) so a page without the bar still lays out,
|
|
592
|
+
and a token check must not flag them as undeclared.
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 9. The frame
|
|
597
|
+
|
|
598
|
+
- **390 × 844**, the device radius on a normal screen page, square and
|
|
599
|
+
full-bleed under `?bare`.
|
|
600
|
+
- It **clamps to whatever is left under the bar** and scrolls its own content the
|
|
601
|
+
way the phone does:
|
|
602
|
+
`height: min(var(--frame-h), calc(100dvh - var(--chrome-h) - var(--stage-pad)))`.
|
|
603
|
+
Never `transform: scale()` — a transform does not change layout height, so the
|
|
604
|
+
frame still occupies 844px and the page scrolls to reveal a frame that was
|
|
605
|
+
already drawn smaller.
|
|
606
|
+
- The stage's gutter and the frame's height subtraction read **the same
|
|
607
|
+
variable**, or the two disagree and the frame is short by the gutter.
|
|
608
|
+
- **A screen with a modal over it carries `phone--modal`**, which stops the frame
|
|
609
|
+
scrolling. `position: absolute; inset: 0` resolves against the frame's padding
|
|
610
|
+
box, and in a scroll container that is the visible box pinned to the top of the
|
|
611
|
+
scrollable area — so scrolling slid the backdrop off and left the card floating
|
|
612
|
+
over nothing.
|
|
613
|
+
- **A modal card uses `bg-frame`, not `bg-surface`.** In a dark palette
|
|
614
|
+
`--surface` is a translucent white tint, which is right on a solid page and
|
|
615
|
+
wrong over a backdrop: the screen shows straight through the card.
|
|
616
|
+
- **A modal draws the screen underneath unchanged and inert.** A sheet does not
|
|
617
|
+
redraw the screen it opens over, and every link in the underlay is blocked in
|
|
618
|
+
the app, so it must not be a link in the wireframe either.
|
|
619
|
+
- **The device status bar is drawn.** The OS draws it, not the app — but the app
|
|
620
|
+
pads for it, so a wireframe that omits it puts every screen ~44px too high and
|
|
621
|
+
makes the first row look like it has more air than it will.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## 10. HTML or wiki — which holds what
|
|
626
|
+
|
|
627
|
+
- **HTML is the spec for anything visual** — layout, component inventory,
|
|
628
|
+
placement, size, state (see C-16 in `pipeline`). The screen inventory is the
|
|
629
|
+
`routes.js` route table, never a second list in Markdown.
|
|
630
|
+
- **A screen's notes live in the wiki, only where it has some** —
|
|
631
|
+
`.context/wiki/surfaces/<slug>.md`, named by `routes.js`; every other screen
|
|
632
|
+
has `notes: null`. The notes button renders it beside the screen on demand.
|
|
633
|
+
- The wiki also holds what a picture cannot: decisions and why, canonical values
|
|
634
|
+
(prices, tier names, copy strings, enums), research, ops.
|
|
635
|
+
- **Where they disagree**: the HTML wins on layout and flow; the canonical-values
|
|
636
|
+
page wins on prices, names and copy strings. Different columns, not a conflict.
|
|
637
|
+
|
|
638
|
+
### What a notes page contains
|
|
639
|
+
|
|
640
|
+
Decisions and constraints true of **that page** and invisible in its pixels.
|
|
641
|
+
Not a description of the screen — the screen is right there.
|
|
642
|
+
|
|
643
|
+
```markdown
|
|
644
|
+
---
|
|
645
|
+
last-verified: 2026-09-22
|
|
646
|
+
---
|
|
647
|
+
**A lead in bold, then the reason.** One paragraph per decision. Say what was
|
|
648
|
+
tried and why it lost, where that is what makes the decision stick.
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
No summary, no wireframe link, no related list: the screen and `routes.js`
|
|
652
|
+
already say those. The rules are canon's `wiki-schema` standard.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## 11. The designs describe the app that exists
|
|
657
|
+
|
|
658
|
+
This is the rule that matters most and is broken most often.
|
|
659
|
+
|
|
660
|
+
- **When a screen and its wireframe disagree, the wireframe is the spec** — for a
|
|
661
|
+
product still being designed. For a product that has shipped, the *app* is the
|
|
662
|
+
fact and the wireframe is the thing that is wrong. Fix the wireframe, or change
|
|
663
|
+
it deliberately and say so in its notes page.
|
|
664
|
+
- **Read the source, do not remember it.** Icons come from the component the app
|
|
665
|
+
imports. Names come from the data table. Copy comes from the i18n dictionary.
|
|
666
|
+
Colours come from the app's theme file. A wireframe drawn from memory invents
|
|
667
|
+
packs that do not exist, screens that were deleted, and copy nobody wrote.
|
|
668
|
+
- **A deliberate departure is allowed and must be recorded** in the screen's
|
|
669
|
+
notes page, saying what the code does and why the drawing differs.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 12. Checks
|
|
674
|
+
|
|
675
|
+
There is no lint and no build for a design set. **`canon check` runs the design
|
|
676
|
+
checks** for every surface under `.context/designs/<surface>/` that has a
|
|
677
|
+
`routes.js`, after every change:
|
|
678
|
+
|
|
679
|
+
| Check | Asks |
|
|
680
|
+
|---|---|
|
|
681
|
+
| **inventory** | is every file on disk declared in `routes.js`, and every declared file on disk? |
|
|
682
|
+
| **well-formed** | is every `<div>` closed exactly once, in order? |
|
|
683
|
+
| **links** | does every local `href` point at a file that exists? |
|
|
684
|
+
| **tokens** | does every class and `var(--…)` in the set resolve against `shared.css` + `tailwind.config.js`? |
|
|
685
|
+
| **click-through** | is every forward edge in `routes.js` a real link in the screen it leaves? |
|
|
686
|
+
| **flow-complete** | is every link between screens a declared edge? |
|
|
687
|
+
| **acyclic** | with states collapsed and `state`/`back`/`side` dropped, is the forward graph a DAG? |
|
|
688
|
+
| **routes** | is every app screen drawn, and does every drawing have an app screen? |
|
|
689
|
+
| **app-flow** | do the app and the chart navigate alike, in both directions? |
|
|
690
|
+
| **copy** | is every word a dialog or a toast puts on screen drawn somewhere? |
|
|
691
|
+
|
|
692
|
+
Two things are exempt from the link checks and from the chart: edges marked
|
|
693
|
+
`state: true`, which are not journey steps, and the links inside the *underlay*
|
|
694
|
+
of a modal screen, which the modal blocks in the app.
|
|
695
|
+
|
|
696
|
+
**acyclic** is the one that cannot be eyeballed at all, and the one whose
|
|
697
|
+
failure is worst — see §6.
|
|
698
|
+
|
|
699
|
+
click-through and flow-complete are a pair and a set needs both:
|
|
700
|
+
|
|
701
|
+
- **Every declared edge is a real link**, or the chart is a diagram of an
|
|
702
|
+
intention. A chart that claims Home leads to the store, on a Home whose pack
|
|
703
|
+
tile is a `<span>`, is exactly that — and the whole reason to draw screens in
|
|
704
|
+
HTML rather than a design tool is that you can walk the product.
|
|
705
|
+
- **Every link is a declared edge**, or the chart quietly under-reports the
|
|
706
|
+
product. This one found 24 missing transitions across two migrated sets: "Play
|
|
707
|
+
Again", "Continue playing", "Save & go home", the resume banner, both upsell
|
|
708
|
+
paths.
|
|
709
|
+
|
|
710
|
+
**Serve the set over http to look at it**, never `file://`. The notes drawer
|
|
711
|
+
fetches Markdown, which `file://` blocks; and a plain static server without
|
|
712
|
+
`Cache-Control: no-store` will serve you a stale stylesheet and make every visual
|
|
713
|
+
check a lie.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## 13. Where each part comes from
|
|
718
|
+
|
|
719
|
+
**The canonical set lives in the repo named in `copies.source`** in
|
|
720
|
+
`.claude/canon.yaml`. It should exercise nearly everything in this guide: the
|
|
721
|
+
reserved flows, a few domain flows, modal screens, a state variant, and at least
|
|
722
|
+
one edge of each of the four kinds. If it draws one theme only, the estate names
|
|
723
|
+
a second reference for a two-theme palette. **Copy from those and from nothing
|
|
724
|
+
else** — other sets may be on older versions of the shared files, and copying
|
|
725
|
+
from them silently reverts the bar, the chart, or both. A migration that copied
|
|
726
|
+
from the nearest set would look finished and be a version behind.
|
|
727
|
+
|
|
728
|
+
`canon check` holds the copied parts equal to the source, and
|
|
729
|
+
`canon copies --sync` repairs a drifted one.
|
|
730
|
+
|
|
731
|
+
| Part | Where it comes from |
|
|
732
|
+
|---|---|
|
|
733
|
+
| `_chrome.js` | copied **byte for byte**. Never edited per product |
|
|
734
|
+
| `_gallery.js` | copied **byte for byte**. Never edited per product |
|
|
735
|
+
| CORE block of `shared.css` | copied **byte for byte** — everything above the PALETTE banner |
|
|
736
|
+
| `index.html` | copied; change the `<title>` and nothing else |
|
|
737
|
+
| `screenshots.html` | copied; change the `<title>` and nothing else |
|
|
738
|
+
| PALETTE + components in `shared.css` | written for this product |
|
|
739
|
+
| `tailwind.config.js` | copied, then this product's own colour groups swapped in — most of the file is the shared token keys. Every value `var(--token)` |
|
|
740
|
+
| `routes.js` | written for this product |
|
|
741
|
+
| `<flow>/<screen>.html` | drawn |
|
|
742
|
+
| `brand/` | this product's artwork |
|
|
743
|
+
|
|
744
|
+
Those five copied files are the whole reason a set is legible at a glance. If a
|
|
745
|
+
product needs one of them to behave differently, change it in the source repo
|
|
746
|
+
and sync — that is a change to every set, not a local edit in one.
|
|
747
|
+
|
|
748
|
+
**A formatter is an edit, and it is the one that gets past everybody.** One
|
|
749
|
+
repo ran `prettier --write` on every `*.js` through lint-staged, so every commit
|
|
750
|
+
silently rewrote `_chrome.js`, `_gallery.js` and `routes.js` — quote style,
|
|
751
|
+
trailing commas, wrapped object literals. Behaviour identical; bytes not. That
|
|
752
|
+
is how a set ends up the only one whose bar and chart do not match the others,
|
|
753
|
+
with nobody having decided anything, and it survives being fixed because the
|
|
754
|
+
next commit does it again. **Add the shared parts to the repo's
|
|
755
|
+
`.prettierignore`** (or its equivalent) in any repo whose tooling formats
|
|
756
|
+
JavaScript:
|
|
757
|
+
|
|
758
|
+
```
|
|
759
|
+
.context/designs/*/_chrome.js
|
|
760
|
+
.context/designs/*/_gallery.js
|
|
761
|
+
.context/designs/*/routes.js
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Check for it before trusting a drift report: a set can be one `prettier --write`
|
|
765
|
+
away from looking like it has drifted a version when it has not, and one commit
|
|
766
|
+
away from drifting for real.
|
|
767
|
+
|
|
768
|
+
### Building a new set
|
|
769
|
+
|
|
770
|
+
1. Read the app's routes, components, theme file and i18n dictionary first. If
|
|
771
|
+
there is no app yet, the designs are the spec and this step is the brief.
|
|
772
|
+
2. Copy the five shared parts from the canonical set, per the table above.
|
|
773
|
+
3. Write the PALETTE block. Role names only. Declare the reserved colours.
|
|
774
|
+
4. Write `routes.js`: every screen, every state, every edge.
|
|
775
|
+
5. Draw the screens, one per file, no prose.
|
|
776
|
+
6. Write a notes page for every screen that has notes.
|
|
777
|
+
7. Run `canon check`. Open the set in a browser at ≥640px and walk it.
|
|
778
|
+
|
|
779
|
+
### Migrating a set that predates this guide
|
|
780
|
+
|
|
781
|
+
A set with no `routes.js` and no bar is not "an existing set to fix" — none of
|
|
782
|
+
the machinery it would be checked with is there yet. Do it in this order; each
|
|
783
|
+
step is checkable before the next one starts, and going out of order means
|
|
784
|
+
drawing screens against a manifest that is still moving.
|
|
785
|
+
|
|
786
|
+
1. **Inventory first, change nothing.** List every `.html` under the surface,
|
|
787
|
+
and for each one decide: a screen, a state of another screen, or a file that
|
|
788
|
+
should not exist. Two things are always the third case — an `index.html`
|
|
789
|
+
inside a flow (§5) and a second frame in one file (§5). Write the list down;
|
|
790
|
+
it becomes `routes.js`.
|
|
791
|
+
2. **Pin the CDNs.** A pre-standard set usually loads `cdn.tailwindcss.com` and
|
|
792
|
+
`lucide@latest` with no version. Pin every one to the versions in §3 in a
|
|
793
|
+
single mechanical pass, before anything visual is judged — an unpinned set
|
|
794
|
+
is not the set you are looking at tomorrow.
|
|
795
|
+
3. **Copy the five shared parts** from the canonical set, per the table above.
|
|
796
|
+
4. **Write the PALETTE and `tailwind.config.js`** from the colours the set
|
|
797
|
+
already uses. Role names, no hues, every config value a `var(--token)`.
|
|
798
|
+
This is where a pre-standard set usually has literals; §8 says why they
|
|
799
|
+
cannot stay.
|
|
800
|
+
5. **Write `routes.js` — flows and screens first, the graph second.** Order the
|
|
801
|
+
flows by the reserved order. Give every screen its `screen` name, `status`
|
|
802
|
+
and `notes` slug.
|
|
803
|
+
6. **Derive the edges; do not recall them.** Scan the real `href`s in every
|
|
804
|
+
screen and declare one edge per link — the `flow-complete` check of §12 run
|
|
805
|
+
as an authoring step, and on a 40-screen set the only tractable way. Then
|
|
806
|
+
classify each: plain forward, `side`, `back`, or `state`. A set migrated from
|
|
807
|
+
memory under-reports itself by about a quarter.
|
|
808
|
+
7. **Add `data-chrome` and `data-file` to every screen**, and the `routes.js` +
|
|
809
|
+
`_chrome.js` lines to its tail. A screen whose `data-file` disagrees with its
|
|
810
|
+
path logs a warning and appears nowhere.
|
|
811
|
+
8. **Fold stray per-flow CSS into `shared.css`.** A `<flow>/<flow>.css` has no
|
|
812
|
+
slot in §1 and no way to be checked. Its rules belong in the components
|
|
813
|
+
block, or they are Tailwind classes that were never written as classes.
|
|
814
|
+
9. **Deduplicate the brand artwork.** Any mark pasted inline into more than one
|
|
815
|
+
screen becomes one file under `brand/` (§3).
|
|
816
|
+
10. **Write the notes pages**, then run `canon check` and walk the set in a
|
|
817
|
+
browser.
|
|
818
|
+
|
|
819
|
+
**If the product has no splash screen**, `flow.start` is the first screen the
|
|
820
|
+
app actually opens on — Home, or the first onboarding step. `start` is required
|
|
821
|
+
and is what the column rank counts from; a chart with no declared start ranks
|
|
822
|
+
every screen at zero and draws one column.
|
|
823
|
+
|
|
824
|
+
### Fixing an existing set
|
|
825
|
+
|
|
826
|
+
1. Run `canon check` first — it tells you what is broken before you look.
|
|
827
|
+
2. Diff the set against the app: icon names, data tables, copy strings, screen
|
|
828
|
+
inventory, navigation. Most drift is here, and none of it throws.
|
|
829
|
+
3. Check the frame fits with no page scroll, and that the screenshots sheet has
|
|
830
|
+
no gaps or clipped corners.
|
|
831
|
+
4. Check the bar and CORE match the canonical set (`canon check` reports drift).
|
|
832
|
+
5. Re-run `canon check` and walk the set.
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## 14. Known-wrong patterns
|
|
837
|
+
|
|
838
|
+
Things that have already been done wrong once. Do not repeat them.
|
|
839
|
+
|
|
840
|
+
| Pattern | Why it fails |
|
|
841
|
+
|---|---|
|
|
842
|
+
| `transform: scale()` to fit the frame | does not change layout height; the page still scrolls |
|
|
843
|
+
| Several frames in one file | no URL, no route entry, no link, no tile |
|
|
844
|
+
| A prose sidebar beside the screen | competes with the thing under review |
|
|
845
|
+
| A literal hex in `tailwind.config.js` | the utility stops following the theme scope |
|
|
846
|
+
| Hue-named colours (`navy`, `gold`) | a repalette becomes a rename across 500 files |
|
|
847
|
+
| A class used but never declared | resolves to nothing, throws nothing, paints nothing |
|
|
848
|
+
| Unpinned CDN | every set restyles itself overnight |
|
|
849
|
+
| Per-tile semantic colours where the app tints by category | one shelf reads as four unrelated things |
|
|
850
|
+
| The bar reading a palette token | the same control looks like a different control per product |
|
|
851
|
+
| Drawing states as chart nodes | the same screen appears twice |
|
|
852
|
+
| Inventing a screen, a pack, or a string | the set stops being a reference and becomes fiction |
|
|
853
|
+
| A per-flow stylesheet beside the screens | loaded by some screens, checked by nothing |
|
|
854
|
+
| Copying shared files from whichever set is nearest | older sets are behind; the bar silently reverts |
|
|
855
|
+
| A data-driven SVG pasted into every screen that shows it | five copies of one geometry, and the datum is the only line that should differ |
|
|
856
|
+
| A component class named after a Tailwind utility | the utility wins the cascade and paints something nobody asked for |
|
|
857
|
+
| The same sentence in a screen file and in its wiki page | two copies of one decision, and §10 says there is one |
|