flet-box 1.0.9 → 1.1.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.
@@ -0,0 +1,573 @@
1
+ ---
2
+ name: fletbox-design
3
+ description: The design contract for a FletBox app - tokens only, one scale, three-tier hierarchy, a state for every interaction, measured contrast, composition that starts as a sentence, the dated visual tells that give away a template, destructive actions designed last. Use when creating, editing or reviewing any screen, form, list, dialog, hero or action control.
4
+ ---
5
+
6
+ # Design — the system rules for a FletBox screen
7
+
8
+ You are not decorating screens. You are extending a system that every later
9
+ screen will copy. A decision that cannot be restated as a rule is a defect, not
10
+ a style.
11
+
12
+ When two goals conflict, take them in this order:
13
+
14
+ 1 Consistency · 2 Clarity · 3 Accessibility · 4 Usability · 5 Scalability
15
+ · 6 Maintainability · 7 Conversion
16
+
17
+ Never optimize for aesthetics alone. Beauty is what is left when the system is
18
+ correct; it is an outcome, never a target you can hit directly.
19
+
20
+ ## 00 The two gates
21
+
22
+ ```bash
23
+ npm run check # the API is real: widgets, props, imports
24
+ npm run ui # the screen follows these rules (codes map to rule numbers)
25
+ npm run ui -- --tokens # measured contrast of every token, on both surfaces
26
+ npm run dev # then look at it: the checker has no eyes
27
+ ```
28
+
29
+ A finding you keep needs one sentence of justification, in the commit or in
30
+ `.fletbox/memory/`. If that sentence is "the design wants it", the design is
31
+ what needs writing down, because the next screen will copy it.
32
+
33
+ A file the gate cannot parse comes back `UNREADABLE` and nothing else in it is
34
+ judged — the screen is not clean, it is unmeasured. Fix the syntax before
35
+ reading the rest of the list.
36
+
37
+ Sources, cited as `NBG p.N` (No bullshit guide to UX) and `Playbook p.N`
38
+ (The UI/UX Playbook; the PDF page — the printed folio is one lower). Every
39
+ number below is either stated by one of them or measured on this palette.
40
+ Every prop in every call below exists in `.fletbox/api/api.json`.
41
+
42
+ ## 01 System before component
43
+
44
+ Define the token, then the component, then the screen. In that order, always.
45
+ Components are outputs; the system is the source of truth.
46
+
47
+ - A screen that needs a value the system does not have adds a token to
48
+ `src/database/themes.js`. It does not write the value inline.
49
+ - A component that needs a special exception: challenge the exception first, and
50
+ if it survives, change its siblings too. Two components solving one problem
51
+ differently get unified, not documented.
52
+ - If you cannot say which rule a decision follows, do not make it.
53
+
54
+ ## 02 Consistency is sacred
55
+
56
+ No arbitrary differences. If a person learns one pattern here, they must not
57
+ relearn it anywhere else (Playbook p.038).
58
+
59
+ - One button scheme for the whole app: the primary is `variant: "filled"`, the
60
+ secondary `variant: "outlined"`, the tertiary `variant: "text"`. Never a
61
+ per-card button color chosen to match the picture — people then wonder whether
62
+ each button does something different (p.040-041).
63
+ A filled `Button` defaults to `bgColor: colors.surface` with `colors.text` on
64
+ it, so the primary action must name the brand pair: `bgColor: colors.primary`
65
+ and `color: colors.onPrimary`. An outlined one must name its border (`color:`),
66
+ or it inherits the surface it sits on and disappears.
67
+ - One radius language per screen. A rounded picture next to a square button reads
68
+ as two products (p.039).
69
+ - One icon family, one weight, one size per screen. The one allowed mix is state:
70
+ a selected tab filled, unselected tabs outlined (p.045-046).
71
+ - Cards in the same row share a height. When the copy is uncontrollable, fix the
72
+ height and bottom-align the action; the empty space goes in the middle
73
+ (p.043-044).
74
+ - Same purpose, same shape: identical inputs get the same `height`,
75
+ `borderRadius`, states and validation behavior (rule 13).
76
+ - Images inside a repeated card sit in the same slot, same size, same crop
77
+ (`fit`, `shape`), whatever the picture is (p.042).
78
+
79
+ ## 03 Design tokens only
80
+
81
+ No arbitrary values, no hex, no `px` strings. Every value comes from the scale.
82
+
83
+ ```
84
+ space 4 · 8 · 12 · 16 · 18 · 24 · 32 · 40 · 48 · 64 · 96
85
+ radius 4 · 8 · 12 · 16 · 999 (pill)
86
+ type 12 caption · 14 body-small · 16 body · 18 subtitle · 24 title
87
+ 32 headline · 40 display-sm · 48 display
88
+ weight 400 body · 500 label · 600 title · 700 headline
89
+ line 1.5-1.6 body · 1.2-1.3 heading
90
+ depth elevation 0 · 1 · 2 · 3 · 4 · 5
91
+ ```
92
+
93
+ - The base unit is 4 and everything is a multiple of it (Playbook p.028). A value
94
+ like 13, 19, 21 or 31 is not a design decision, it is drift. A radius between
95
+ two steps — the 20 beside the 16 — is the same drift on the corner
96
+ (`OFF_SCALE_RADIUS`), and it is what makes a rounded picture sit next to a
97
+ square button like two products.
98
+ - Never type a hex or `rgb()` outside `src/database/themes.js`
99
+ (`RAW_COLOR`). If a token is missing, add it there.
100
+ - Never build a color by appending alpha to a token — `colors.primary + "15"` is
101
+ a one-off shade the rest of the app cannot reuse (`HAND_MIXED_COLOR`). The
102
+ tints exist: `colors.primaryBg`, `colors.errorBg`, `colors.successBg`,
103
+ `colors.warningBg`, `colors.infoBg`.
104
+ - A frosted surface is `colors.glass` with `colors.glassBorder` and a
105
+ `backdropFilter`; it is translucent on purpose, so put it over imagery,
106
+ not over body text. A hand-written `rgba()` is a `RAW_COLOR` like any other.
107
+ - Text on a brand fill is `colors.onPrimary`, never a literal white.
108
+ - Tap targets are at least 40 on a side (`SMALL_TAP_TARGET`). Reach it with
109
+ `height`, not with padding. The books argue this qualitatively (Fitts, p.099);
110
+ 40 is the floor this system enforces and 44 is what a phone primary action
111
+ gets.
112
+ - `size: "small"` on a `Button` is for dense secondary actions, never for the one
113
+ thing a phone screen asks for — a preset that lands under the 40 floor is
114
+ reported as `SMALL_PRESET` even though no number was typed: the preset is the
115
+ decision, and it was made by the framework, so it has to be overridden with a
116
+ `height`.
117
+
118
+ ## 04 Hierarchy must be obvious in three seconds
119
+
120
+ A person must answer three questions without reading: what is this, what
121
+ matters, what happens next. There are exactly four levers — size, color, weight,
122
+ position (Playbook p.007). If everything is emphasized, nothing is.
123
+
124
+ - One primary action per screen. One secondary. Everything else is tertiary.
125
+ Never two filled buttons on one view (`MULTIPLE_PRIMARY_ACTIONS`).
126
+ - Actions rank by emphasis, information ranks by size: the secondary button keeps
127
+ the same `height` as the primary and loses color (p.008); the data gets the
128
+ size ladder (p.088). That is how p.008 and p.088 stop contradicting each other.
129
+ - In a stat card the value is the largest and darkest text and the label is
130
+ smaller and muted. Never the inverse (p.010).
131
+ - Darkness means importance: heading darker than body, body muted. Never let a
132
+ thumbnail outsize the price (p.009, p.011).
133
+ - Never render a detail panel as uniform label:value rows — it has no hierarchy
134
+ and cannot be scanned. Promote the one value that matters, demote the rest,
135
+ add an icon per row (p.012).
136
+ - Before building, list the elements and rank them by user importance; then map
137
+ size, color, weight and position onto that ranking (p.010).
138
+
139
+ ## 05 One purpose per screen
140
+
141
+ Write the sentence first: "this screen lets <who> do <what>". If it needs an
142
+ "and", split the screen (NBG pp.40-43).
143
+
144
+ - Conflicting goals reduce usability. A task screen carries nothing that does not
145
+ serve the task: no promo panel, no second call to action (Playbook p.127).
146
+ - Decorative imagery next to a form is allowed only when it is action-free — no
147
+ text overlay, no button — and it still has to be about this product, not merely
148
+ pretty (p.128).
149
+ - Navigation exposes what the job needs and nothing else. Every extra link is a
150
+ decision taken before the user made theirs.
151
+
152
+ ## 06 Reduce cognitive load
153
+
154
+ People recognize; they must never memorize, guess or interpret.
155
+
156
+ - Chunk to about four items per group and cap a visible group at 7; past that,
157
+ truncate and expose one action — "Show all 21 amenities" (Playbook p.101).
158
+ The 7±2 figure is a myth in its loosest form; plan on four and the group still
159
+ holds when someone adds a fifth.
160
+ - Recognition over recall: common icons, common words, state visible on screen
161
+ rather than in memory (p.104). "Continue watching" beats "remember where you
162
+ stopped".
163
+ - Do not hide choices behind a control that must be opened to be seen: variants
164
+ as swatches, quantity as a stepper, not a nested menu (p.107-108).
165
+ - Never oversimplify into vagueness either: a switch with an icon and no words is
166
+ not clean, it is unreadable (p.014).
167
+ - If no user has shown they need a feature, remove it rather than park it behind
168
+ a toggle (NBG p.136).
169
+ - A file that builds a dozen fields at once is a wall, not a step. The gate counts
170
+ them (`CROWDED_FORM`); the fix is grouping or splitting the task (rule 13), not
171
+ smaller text.
172
+
173
+ ## 07 Spacing communicates the relationship
174
+
175
+ Whitespace is functional, not decorative. Related things sit closer than
176
+ unrelated things, and that difference is the whole message (Playbook p.013, p.029).
177
+
178
+ - Field pattern: label to its own input `12`, that field to the next label `32`.
179
+ Equal spacing at both levels makes label ownership ambiguous (p.013). A number
180
+ between two steps of the spacing scale is the same mistake written smaller, and
181
+ the gate names it (`OFF_SCALE_SPACING`) wherever it appears — including inside a
182
+ `"12px 20px"` shorthand, because a shorthand is four decisions in one string.
183
+ - Card padding is `16`; the rhythm between cards is `16` or `24`; sections
184
+ separate at `24` or `32`; page blocks at `48`. Never mix two rhythms in one list.
185
+ - Start a layout with more room than you think you need and tighten it; clutter
186
+ is easier to see than cramp is to undo (p.025-027).
187
+ - Do not stretch a form or a checkout to the viewport. 1440 is a canvas size, not
188
+ a target; give the card the width it needs and let the margin carry the rest
189
+ (p.129-130). `Container({ maxWidth: 480 })` around a form, `fullWidth: true`
190
+ only on the button inside it.
191
+ - Layout is `useState` bindings, `flex`, `gap` and `wrap`. An absolute offset is
192
+ for overlays, and then through `stackPosition`, not raw coordinates.
193
+
194
+ ## 08 Typography is structure
195
+
196
+ - Body is `size: 16` (Playbook p.090). Nothing that must be read goes below 12;
197
+ the book contradicts itself between 12 (p.090) and 10 (p.096) — 12 is the floor
198
+ here. A size between two steps of the type scale is reported as
199
+ `OFF_SCALE_TYPE`: the 15 beside the 16 does not read as hierarchy, it reads as
200
+ a mistake — two steps per jump, one role per size.
201
+ - Heading: bigger, heavier, darker, tighter. Body: smaller, regular, muted,
202
+ looser. Never the same weight for both, never extra-light for body (p.084-087).
203
+ - Line-height: 1.5-1.6 on body, 1.2-1.3 on headings; a 32px heading sits at about
204
+ 1.25 (p.091-092). A heading never gets more air than a paragraph.
205
+ - Measure: 45-75 characters per line on desktop, 30-40 on mobile. A paragraph
206
+ that runs the full width is unreadable at 1440 (p.076-078).
207
+ - One typeface; two at the absolute maximum, and then headings against body
208
+ (p.075-076). Declare it once in the theme, never per `Text`.
209
+ - `align: "left"` for every run of text and every list. `right` for numbers and
210
+ prices, so magnitudes compare. `center` only for a short title or a lone CTA,
211
+ never for a paragraph (p.018, p.094) — a centered paragraph is reported as
212
+ `CENTERED_BODY_TEXT`, because an eye that has to find the left edge again on
213
+ every line is doing the layout's work for it.
214
+ - Long copy becomes scannable copy: a short lead, a heading, then one item per
215
+ feature with an icon, a bold name and two lines of body; four or more items go
216
+ in a two-column grid (p.123-125).
217
+
218
+ ## 09 Color carries meaning
219
+
220
+ - One brand color per screen, and it is the color of the one action the screen
221
+ asks for. A second accent on the same view means one of them is wrong.
222
+ - Reserve the brand for things you can click. Headers, labels and icons stay
223
+ neutral — brand color on a decorative surface teaches people that color means
224
+ nothing (Playbook p.066-067).
225
+ - Backgrounds are neutral (`colors.background`, `colors.surface`, `colors.card`);
226
+ color belongs in the action, not the wall (p.067-068). A dark theme is equally
227
+ valid; offer both, because dark mode is expected, not a bonus (p.070).
228
+ - Feedback colors are fixed and never redefined: `colors.success`,
229
+ `colors.warning`, `colors.danger`, `colors.info`. Red is error, green is
230
+ success, amber is warning; never pink for success, never green for delete.
231
+ - The signal red and the readable red are different tokens. `colors.danger`
232
+ (#ef4444) is for a badge, a border, an icon: as words on a white page it
233
+ measures 3.76:1 and that is not AA. `colors.dangerStrong` (#b91c1c) is the red
234
+ you may read and press — 6.47:1 as words on white, 6.47:1 with white written on
235
+ it, 5.42:1 inside `colors.errorBg`. Same for the others: `colors.warning`
236
+ (2.15:1 with white on it) and `colors.success` (2.54:1) are plates and dots,
237
+ never a sentence — `colors.warningStrong` (5.02:1) and `colors.successStrong`
238
+ (5.48:1) are the readable ones, in either direction. Measure any pair you are
239
+ about to ship: `npm run ui -- --tokens`.
240
+ - Never carry meaning in color alone — a color-blind person and a phone in
241
+ sunlight both lose it. Pair the color with an icon and a word (p.070): a status
242
+ chip is `Badge` plus text, a field error is `error` plus `showValidationMessage`.
243
+ - Hierarchy inside a fill comes from opacity, not from a second hue.
244
+
245
+ ## 10 Depth is a scale
246
+
247
+ - `elevation: 0-5` is the shadow scale the framework ships (`SHADOW_LEVELS`), and
248
+ these widgets read it: `Accordion`, `AppBar`, `BottomNavigation`, `Button`,
249
+ `Card`, `Chip`, `Drawer`, `FloatingActionButton`, `ListTile`, `Modal`, `SnackBar`.
250
+ Elsewhere — `Container` and `ListView` included — the number reaches
251
+ `style.boxShadow` as a bare number, CSS drops it, and the box stays flat
252
+ without saying so: pass a `shadow: "0 12px 32px rgba(15,23,42,0.12)"` string
253
+ there instead (`DEAD_ELEVATION`).
254
+ - Anything that floats gets depth: a dropdown, a menu, a modal, a card on a map.
255
+ A shadowless layer merges with the page and the user cannot tell what is on top
256
+ (p.050-051).
257
+ - Soft, not harsh: depth comes from a wide blur at low opacity, and the shadow
258
+ is tinted toward the background hue, never pure grey or black (p.052-054).
259
+ - Do not put a white card on a white page — the interface goes flat. Page
260
+ `colors.background`, card `colors.card` (p.056-057).
261
+ - Borders are thin and rare. Separation comes from space and surface, not from a
262
+ rule under every row; a bordered list of everything reads as busy and dated
263
+ (p.059-061).
264
+ - Glass and blur are a brand decision, and they never survive a light background:
265
+ text on frosted glass over white loses the contrast war (p.063-065).
266
+
267
+ ## 11 Accessibility is a floor, not a feature
268
+
269
+ When accessibility and aesthetics conflict, accessibility wins. Every time.
270
+
271
+ - Text needs 4.5:1 against what it sits on; large text (24px, or 18px bold) and
272
+ meaningful graphics need 3:1. Disabled text is exempt. The book agrees: its
273
+ passing examples measure 4.8-6.54:1 and its failures 1.81-2.84:1 (p.021), and
274
+ #626262 is the lightest grey that survives on white (p.086).
275
+ - Measure instead of arguing: `npm run ui -- --tokens`. On this palette `text` is
276
+ 17.85:1, `textSecondary` 4.76:1, `textDisabled` 2.56:1 (decoration only), and
277
+ the brand pair `onPrimary` on `primary` is 4.55:1 — which is why `primary` may
278
+ carry a button label and not a paragraph. By hand:
279
+ `color("#0f172a").contrastRatio("#ffffff")` answers 17.85.
280
+ - The gate's four contrast codes are four different sentences, and treating them
281
+ as one noise is how a real one gets dismissed. `LOW_CONTRAST` is under 3:1 — no
282
+ size, no weight and no theme rescues those words. `LOW_CONTRAST_SOFT` is
283
+ between 3:1 and 4.5:1: legal as large text only (24px, or 18px bold), so either
284
+ promote the run or darken the token. `LOW_CONTRAST_ON_SURFACE` is a run with no
285
+ fill of its own measured against `background` and `surface` — the color you
286
+ wrote is a plate, not an ink, or the words are sitting on a fill the call never
287
+ named. `LOW_CONTRAST_UI` is a graphic that carries meaning under 3:1; an icon
288
+ next to a label that already says the same thing is decoration and can be kept.
289
+ - Text over a photograph needs a scrim: a dark overlay or a blurred plate under
290
+ the words. Light text over one dark photo proves nothing about the next photo
291
+ (p.022).
292
+ - Every field has a visible `label`; a placeholder is a format hint, never a
293
+ substitute (p.115). Every switch and checkbox has one too (`UNLABELED_TOGGLE`).
294
+ - Every control that is only an icon gets a name: wrap it in
295
+ `Tooltip({ text, child })` or give it a visible label (`ICON_ONLY_CONTROL`).
296
+ - Every `Image` that carries information has `alt`; `alt: ""` is the correct
297
+ answer for a decoration (`IMAGE_NO_ALT`).
298
+ - Focus must be visible, keyboard must reach everything, and `Esc` must close
299
+ whatever opened (`Modal({ closeOnEsc: true, showCloseButton: true })`).
300
+ - Motion is optional content: respect a reduced-motion preference; never
301
+ auto-play a carousel or a banner.
302
+
303
+ ## 12 Every interaction has four states
304
+
305
+ A control that handles only the happy path is unfinished (NBG pp.94-97). Write
306
+ all four, in the same component, before the screen counts as done.
307
+
308
+ ```js
309
+ Column({ gap: 16, children: [
310
+ loading ? Skeleton({ width: "100%", height: 48, count: 3 }) : null,
311
+ !loading && rows.length === 0
312
+ ? EmptyState() // headline + illustration + tip + CTA
313
+ : null,
314
+ failed
315
+ ? SnackBar({ message: "Could not load the list.", action: "Retry", onAction: retry })
316
+ : null,
317
+ !loading && rows.length > 0 ? ListView({ children: rows }) : null,
318
+ ]})
319
+ ```
320
+
321
+ - Empty has four parts: a solution-oriented headline, an illustration, two
322
+ concrete tips, one primary action. "You have no projects" is a dead end, not a
323
+ state (Playbook p.135-136).
324
+ - Loading appears before the wait, not after it: a `Skeleton` at once beats a
325
+ spinner two seconds later (NBG p.134, Doherty).
326
+ - Error does exactly three things: says what went wrong in the user's words,
327
+ gives the way back, keeps the visual style of the app. Show the code, keep the
328
+ navigation, offer links (p.137-138). Never an error code alone, never a shrug.
329
+ - Success confirms in place — a `SnackBar`, not a new screen — and offers the
330
+ next step while it is there.
331
+ - Progress is honest. Show real progress or show a `Skeleton`; a bar that lies
332
+ buys one visit and costs the next one (NBG defends inflating them; do not).
333
+ - The gate knows this rule: a file that renders a list and names none of the
334
+ other three states is reported as `LIST_WITHOUT_STATE`. Silence is the bug it
335
+ is looking for, so do not hush it with a comment — add the state.
336
+
337
+ ## 13 Forms: prevent the error, do not report it
338
+
339
+ - Ask for 3 to 7 things at once. More than that is more than one step, and a step
340
+ the user can see: `Stepper({ steps, activeStep, onFinish })` (NBG pp.40-43).
341
+ - Width fits the data: CVC is 3 or 4 characters, so the field is short; card
342
+ number and name run full width; pair Expiry with CVC on one row (p.115-116).
343
+ - A column of three or more fields lives inside a cap — `Container({ maxWidth:
344
+ 480 })`. A field as wide as the window answers "type anything" and the eye
345
+ loses the next line (`UNCONSTRAINED_FORM_WIDTH`).
346
+ - A fixed-length code gets one box per character in enlarged type, not one long
347
+ field (p.114).
348
+ - `Input({ label, placeholder, required, maxLength, validation, type })`:
349
+ placeholder carries the format ("MM/YY", "12345"), `maxLength` and `type`
350
+ constrain the input, and validation runs inline as they type, not on submit
351
+ (p.119). A field that has neither is `UNLABELED_FIELD`, and the label is also
352
+ the name a screen reader says out loud.
353
+ - No "Confirm password" field. It adds cost, repeats the same typo twice, and
354
+ buys a false sense of safety. One field, `passwordToggle: true`, and a strength
355
+ line under it (p.117-119).
356
+ - Good defaults and autofill remove keystrokes: prefill the country, the saved
357
+ address, the last card (p.105).
358
+ - Required fields are marked, and the label of the submit button names the
359
+ outcome — "Pay $65", not "Submit" (p.115).
360
+ - Never clear a form because one field failed. Keep what they typed.
361
+
362
+ ## 14 Destructive actions are designed last, and carefully
363
+
364
+ - The button carries the verb: "Delete 3 files", never "OK" or "Yes"
365
+ (`GENERIC_ACTION_LABEL`) (Playbook p.121).
366
+ - Destructive is red — `colors.dangerStrong` for the words, `colors.danger` for
367
+ the dot beside them — because green reads safe and a person presses it twice
368
+ (`DESTRUCTIVE_COLOR`) (p.120). `colors.danger` alone is 3.76:1 as a label.
369
+ - Order in a two-button row: Cancel on the left, destructive on the right. Left
370
+ is what a reading eye hits first (`CONFIRM_ORDER`) (p.121-122).
371
+ - The dialog states reversibility: "This action cannot be undone." Say it even
372
+ when it can, and then offer the way back.
373
+ - `AlertDialog({ title, message, confirmText, cancelText, showCancel, onCancel, onConfirm, onClose })`
374
+ always leaves an escape: a Cancel, and an X on any layer that can be opened.
375
+ - Undo is cheaper than a warning. If the action is reversible, let people undo it
376
+ instead of interrupting them to confirm it (NBG pp.116-118).
377
+ - On a phone, a destructive action is not in the thumb zone; the thing you do
378
+ often is (p.133-135).
379
+
380
+ ## 15 Reduce interaction cost
381
+
382
+ Interaction cost is cognitive effort + physical effort + time (Playbook p.098).
383
+ Count it, then cut it: reading, scrolling, searching, deciding, clicking, typing,
384
+ waiting, remembering (p.099).
385
+
386
+ - Count the steps in the main flow and delete every one that does not change the
387
+ outcome (p.105). Three is a target; one is better.
388
+ - Put the action next to the thing it acts on: the CTA beside the quantity
389
+ stepper, the row action inside the row (p.108, Fitts).
390
+ - Keep the control that runs all the time in a fixed, reachable place — a player
391
+ bar at the bottom, a primary tab at the thumb (p.100, p.133-135).
392
+ - Show the content instead of a door to the content: three real cards beat a
393
+ banner that says "Explore now" (p.111-113).
394
+ - One click where a confirmation is not needed. Two where it is.
395
+
396
+ ## 16 Respect the platform
397
+
398
+ - Use the words, icons and gestures people already brought with them (NBG p.131,
399
+ Jakob's law). An invented gesture is a bug report waiting to be written.
400
+ - iOS, Android and web each have conventions; meet them instead of overriding
401
+ them with a custom pattern. A back that lands where the user expects, a sheet
402
+ that dismisses down, a form that advances on Enter.
403
+ - Break a convention only when the measurement says the convention is wrong here,
404
+ and write that measurement down.
405
+
406
+ ## 17 Composition starts as a sentence
407
+
408
+ Before the first widget, write four lines: what this screen decides for the
409
+ user, the three pieces of information that carry it, and the one action. Then
410
+ lay out those four things. A screen that starts as "a hero, then three cards,
411
+ then a footer" is a template, and it gets filled with whatever is handy.
412
+
413
+ - The type scale caps at 48, so a headline cannot buy all of its presence from
414
+ size. Take the rest from the two levers that are free: `weight: "bold"` on the
415
+ title against `weight: "normal"` on the body, and `colors.text` against
416
+ `colors.textSecondary`. That one step is what makes a screen look designed at
417
+ 375px, where 48 is already the ceiling.
418
+ - At most five sizes per screen, and at most one role per size.
419
+ - The accent is under 5% of the pixels. If a first-glance photo shows it twice,
420
+ one of the two is not an accent (rule 09).
421
+ - Squint at the screen. What you can still read is the hierarchy. If what
422
+ survives is a badge or an accent, rule 04 is not done.
423
+
424
+ ## 18 The framework traps
425
+
426
+ Real FletBox behaviors, not opinions. Each one has already produced a "the UI is
427
+ broken" report, and none of them is something `npm run ui` can see:
428
+
429
+ - **`Container` is a column and it paints a surface.** Children stack
430
+ vertically unless you pass `direction: "row"`, and it has a background — a
431
+ `Container` around your content is a visible plate, not an invisible wrapper.
432
+ - **Widths default to 100%.** A `Button` inside a `Column` fills the screen
433
+ unless it sits in a `Row` or gets a `width`. Use `fullWidth: true`
434
+ deliberately on the one action a phone screen asks for; let the rest be sized
435
+ by their content.
436
+ - **`flex: 1` needs a sibling that wants the space.** One flexible child in a
437
+ fixed-height parent stretches; `justifyContent: "flex-start"` starves the
438
+ `flex` you meant on the other axis.
439
+ - **`alignItems: "center"` centers the block, not the text.** A paragraph that
440
+ wraps inside a centered column becomes a ragged stripe: center the heading,
441
+ left-align the copy (rule 08).
442
+ - **There is no breakpoint prop, and none is needed.** Responsiveness is
443
+ `flex`, `wrap: true` and `maxWidth` — plus the rem contract: a number is a
444
+ fraction of the reader's font, so one build fits a phone and a desktop. A
445
+ screen built from two hard-coded layouts is two screens to maintain.
446
+ - Overlays go in a `Stack` with `stackPosition`, not in absolute coordinates on
447
+ `Container({ pos: "absolute", top, left })`. The first survives a content
448
+ change; the second is a pixel bomb in any language longer than English.
449
+
450
+ ## 19 The dated tells
451
+
452
+ Each of these says "assembled from a template". Banned outright, with the reason
453
+ once so an edge case can be judged instead of guessed:
454
+
455
+ - **An eyebrow label** — a small uppercase word above a heading ("OVERVIEW",
456
+ "GET STARTED"). Hierarchy cosplay: it names a section nobody asked to have
457
+ named.
458
+ - **Numbered markers** `01 · 02 · 03` — they imply an order a feature list does
459
+ not have.
460
+ - **Monospace outside code.** `Text({ styles: ["code"] })` means code. Using it
461
+ to look technical is a costume.
462
+ - **A gradient headline, or a gradient behind everything.** `gradient` exists on
463
+ `Button` and `AppBar`: it is one element's surface, never a text treatment.
464
+ - **Glass everywhere.** `colors.glass` with `colors.glassBorder` is for the one
465
+ layer that really floats above scrolling content. Frosted cards on a solid page
466
+ are a texture, not a hierarchy (rule 10).
467
+ - **A row of identical cards** — three plates with an icon, a title, two lines.
468
+ If the content has no shape a list would reveal, it does not need four cards.
469
+ - **Resting shadows.** Depth answers "what is above what", never "make it pop"
470
+ (rule 10).
471
+ - **A wall of centered text** (rule 08), and a hairline `Divider` under every
472
+ row: a line that separates nothing is the fastest way a page looks dated.
473
+ - **Copy clichés** — "unlock", "elevate", "seamless", "empower", "leverage", a
474
+ sentence that starts with "Whether you're…". Say the specific thing the product
475
+ does. If you cannot, that is a content gap, not a font gap.
476
+
477
+ Deleting a tell is not a design. Every ban above needs the element that carries
478
+ the job instead: hierarchy from rule 17, separation from rule 07 or 10, emphasis
479
+ from rule 09, grouping from a `ListView` or a `gap`.
480
+
481
+ ## 20 Density is one dial
482
+
483
+ Pick one row and hold it for the whole screen. Three numbers set it: a `gap`, a
484
+ `padding`, and a body `size`.
485
+
486
+ | Screen | gap | padding | body |
487
+ | --- | --- | --- | --- |
488
+ | Reading / marketing | 32 | 32 | 16 |
489
+ | Form | 16 | 24 | 16 |
490
+ | Working list / dashboard | 8 | 16 | 14 |
491
+
492
+ A dense screen is not a cramped one: the 40 tap floor and the 14 label floor
493
+ (rule 03, rule 11) still apply, and they are why "compact" has a hard limit.
494
+
495
+ ## 21 The review
496
+
497
+ When asked to review a screen, answer in this order, and say why every time:
498
+
499
+ 1 Goal — what the screen is for, and whether it is one goal.
500
+ 2 UX problems — cost, states, recall, flow.
501
+ 3 UI problems — hierarchy, spacing, type, color, depth.
502
+ 4 System violations — the values that are not tokens, the component that is an
503
+ exception.
504
+ 5 Accessibility violations — measured ratios, targets, labels, focus.
505
+ 6 Consistency violations — what this screen does differently for no reason.
506
+ 7 Scalability risk — what breaks when this screen has eleven siblings.
507
+ 8 Fix — the call, not the mood. Then the rule it establishes.
508
+ 9 Score — see below.
509
+
510
+ Score the seven axes 0-100, but only against evidence, never against taste:
511
+ an axis starts at 100 and loses 10 for every open finding of its own codes, so
512
+ the number is a count and not an opinion. Below 85 on any axis means the fix is
513
+ a rule change, not a pixel move. An axis with no findings and no evidence is
514
+ unmeasured, not perfect — say so.
515
+
516
+ | Axis | Its codes (from `npm run ui`) |
517
+ | --- | --- |
518
+ | Consistency | OFF_SCALE_SPACING · OFF_SCALE_RADIUS · OFF_SCALE_TYPE · RAW_COLOR · HAND_MIXED_COLOR · DEAD_ELEVATION |
519
+ | Accessibility | LOW_CONTRAST · LOW_CONTRAST_UI · SMALL_TAP_TARGET · UNLABELED_FIELD · UNLABELED_TOGGLE · ICON_ONLY_CONTROL · IMAGE_NO_ALT |
520
+ | Usability | CROWDED_FORM · LIST_WITHOUT_STATE · UNCONSTRAINED_FORM_WIDTH |
521
+ | Hierarchy | MULTIPLE_PRIMARY_ACTIONS · CENTERED_BODY_TEXT · GENERIC_ACTION_LABEL · DESTRUCTIVE_COLOR · CONFIRM_ORDER |
522
+ | Scalability | every token that had to be added to make the screen pass |
523
+ | Conversion | the label of the primary action, counted in words and in steps |
524
+ | Visual quality | measured only by the axes above; state what you looked at |
525
+
526
+ ## 22 Where these books are wrong
527
+
528
+ Both are worth more read than copied. The parts that do not survive measurement:
529
+
530
+ - The Playbook bans pure black on white and pure white on dark (p.083). This
531
+ system puts white text on deep brand fills, and the ratio is what decides —
532
+ 4.55:1 there, not the rule of thumb.
533
+ - p.008 says a secondary button keeps the primary's size; p.088 says size is
534
+ the hierarchy lever. Both are right about different things: emphasis ranks
535
+ actions, size ranks information.
536
+ - p.079 marks center-aligned body as good; p.094 says center is for titles only.
537
+ The later rule is the readable one.
538
+ - p.103 bans pop-ups as distractions; p.120 requires one to confirm a deletion.
539
+ Reconciled: never unsolicited, always on destructive.
540
+ - p.019 (gutter 70, margin 79, overlay 17%) and p.052-054 (Y:12, blur:48,
541
+ #E9E9E9) are one designer's Figma fields in a screenshot, not a scale.
542
+ Do not harvest them as thresholds.
543
+ - The Playbook's own sign-up example (p.127) is white on a pale blue fill that
544
+ fails its own contrast chapter. Copy the rule, never the screenshot.
545
+ - "7 ± 2" (p.101) is Miller's number quoted loosely; chunk size is a design
546
+ choice, and four is the one that survives a fifth item.
547
+ - NBG defends inflating progress bars (its "little lie"). Do not.
548
+ - Neither book states a tap-target size in px. Fitts gives the direction; 40 is
549
+ this system's floor, and the number to defend if someone asks.
550
+
551
+ ## 23 What no checker can see
552
+
553
+ `npm run ui` cannot tell you whether the screen is understood. For that:
554
+
555
+ - Ask one person to do one task and stay quiet while they do it. Their first three
556
+ sentences are the specification.
557
+ - Watch where the pointer hesitates and where it misclicks: that is where the
558
+ layout is lying about what is tappable.
559
+ - Five people find most of what is broken. Two rounds of five beat one round of
560
+ ten.
561
+ - Write what you saw into `.fletbox/memory/`, or the next session designs this
562
+ screen again from taste.
563
+
564
+ ## 24 The loop
565
+
566
+ ```bash
567
+ npm run check
568
+ npm run ui
569
+ npm run dev # look at the screen, at 375px and at 1440px
570
+ ```
571
+
572
+ Fix the code, not the checker. Then make the rule reusable: if the fix was right
573
+ for this screen, it is a rule for every screen, and it belongs in this file.