flet-box 1.0.9 → 1.1.1
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/bin/commands/checkUi.js +327 -46
- package/bin/commands/create.js +48 -19
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/skills.js +59 -0
- package/bin/utils/templates.js +196 -575
- package/docs/README.md +3 -1
- package/docs/cli/README.md +18 -1
- package/docs/guides/README.md +6 -1
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +84 -21
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +4 -2
- package/skills/fletbox-design/SKILL.md +949 -0
- package/skills/fletbox-memory/SKILL.md +134 -0
- package/skills/fletbox-motion/SKILL.md +266 -0
- package/skills/fletbox-rules/SKILL.md +293 -0
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +31 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
|
@@ -0,0 +1,949 @@
|
|
|
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, and Part II, the visual atlas: six style archetypes with their surface ramps, accent budget, number-as-hero typography, nested radii, photo-and-sheet composition, control shapes, data widgets and per-genre recipes. Use when creating, editing or reviewing any screen, form, list, dialog, hero or action control, or when choosing how a screen should look.
|
|
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 · 20 · 24 · 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 15 beside the 16, the 18 beside a 24 — is the same drift on the
|
|
96
|
+
corner (`OFF_SCALE_RADIUS`), and it is what makes a rounded picture sit next to a
|
|
97
|
+
square button like two products. The scale reaches 24 because `Drawer`,
|
|
98
|
+
`DrawerItem` and the `Tabs` slider already default there: an app matching its own
|
|
99
|
+
widgets cannot be off-scale.
|
|
100
|
+
- Never type a hex or `rgb()` outside `src/database/themes.js`
|
|
101
|
+
(`RAW_COLOR`). If a token is missing, add it there.
|
|
102
|
+
- Never build a color by appending alpha to a token — `colors.primary + "15"` is
|
|
103
|
+
a one-off shade the rest of the app cannot reuse (`HAND_MIXED_COLOR`). The
|
|
104
|
+
tints exist: `colors.primaryBg`, `colors.errorBg`, `colors.successBg`,
|
|
105
|
+
`colors.warningBg`, `colors.infoBg`.
|
|
106
|
+
- A frosted surface is `colors.glass` with `colors.glassBorder` and a
|
|
107
|
+
`backdropFilter`; it is translucent on purpose, so put it over imagery,
|
|
108
|
+
not over body text. A hand-written `rgba()` is a `RAW_COLOR` like any other.
|
|
109
|
+
- Text on a brand fill is `colors.onPrimary`, never a literal white.
|
|
110
|
+
- Tap targets are at least 40 on a side (`SMALL_TAP_TARGET`). Reach it with
|
|
111
|
+
`height`, not with padding. The books argue this qualitatively (Fitts, p.099);
|
|
112
|
+
40 is the floor this system enforces and 44 is what a phone primary action
|
|
113
|
+
gets.
|
|
114
|
+
- `size: "small"` on a `Button` is for dense secondary actions, never for the one
|
|
115
|
+
thing a phone screen asks for — a preset that lands under the 40 floor is
|
|
116
|
+
reported as `SMALL_PRESET` even though no number was typed: the preset is the
|
|
117
|
+
decision, and it was made by the framework, so it has to be overridden with a
|
|
118
|
+
`height`.
|
|
119
|
+
|
|
120
|
+
## 04 Hierarchy must be obvious in three seconds
|
|
121
|
+
|
|
122
|
+
A person must answer three questions without reading: what is this, what
|
|
123
|
+
matters, what happens next. There are exactly four levers — size, color, weight,
|
|
124
|
+
position (Playbook p.007). If everything is emphasized, nothing is.
|
|
125
|
+
|
|
126
|
+
- One primary action per screen. One secondary. Everything else is tertiary.
|
|
127
|
+
Never two filled buttons on one view (`MULTIPLE_PRIMARY_ACTIONS`).
|
|
128
|
+
- Actions rank by emphasis, information ranks by size: the secondary button keeps
|
|
129
|
+
the same `height` as the primary and loses color (p.008); the data gets the
|
|
130
|
+
size ladder (p.088). That is how p.008 and p.088 stop contradicting each other.
|
|
131
|
+
- In a stat card the value is the largest and darkest text and the label is
|
|
132
|
+
smaller and muted. Never the inverse (p.010).
|
|
133
|
+
- Darkness means importance: heading darker than body, body muted. Never let a
|
|
134
|
+
thumbnail outsize the price (p.009, p.011).
|
|
135
|
+
- Never render a detail panel as uniform label:value rows — it has no hierarchy
|
|
136
|
+
and cannot be scanned. Promote the one value that matters, demote the rest,
|
|
137
|
+
add an icon per row (p.012).
|
|
138
|
+
- Before building, list the elements and rank them by user importance; then map
|
|
139
|
+
size, color, weight and position onto that ranking (p.010).
|
|
140
|
+
|
|
141
|
+
## 05 One purpose per screen
|
|
142
|
+
|
|
143
|
+
Write the sentence first: "this screen lets <who> do <what>". If it needs an
|
|
144
|
+
"and", split the screen (NBG pp.40-43).
|
|
145
|
+
|
|
146
|
+
- Conflicting goals reduce usability. A task screen carries nothing that does not
|
|
147
|
+
serve the task: no promo panel, no second call to action (Playbook p.127).
|
|
148
|
+
- Decorative imagery next to a form is allowed only when it is action-free — no
|
|
149
|
+
text overlay, no button — and it still has to be about this product, not merely
|
|
150
|
+
pretty (p.128).
|
|
151
|
+
- Navigation exposes what the job needs and nothing else. Every extra link is a
|
|
152
|
+
decision taken before the user made theirs.
|
|
153
|
+
|
|
154
|
+
## 06 Reduce cognitive load
|
|
155
|
+
|
|
156
|
+
People recognize; they must never memorize, guess or interpret.
|
|
157
|
+
|
|
158
|
+
- Chunk to about four items per group and cap a visible group at 7; past that,
|
|
159
|
+
truncate and expose one action — "Show all 21 amenities" (Playbook p.101).
|
|
160
|
+
The 7±2 figure is a myth in its loosest form; plan on four and the group still
|
|
161
|
+
holds when someone adds a fifth.
|
|
162
|
+
- Recognition over recall: common icons, common words, state visible on screen
|
|
163
|
+
rather than in memory (p.104). "Continue watching" beats "remember where you
|
|
164
|
+
stopped".
|
|
165
|
+
- Do not hide choices behind a control that must be opened to be seen: variants
|
|
166
|
+
as swatches, quantity as a stepper, not a nested menu (p.107-108).
|
|
167
|
+
- Never oversimplify into vagueness either: a switch with an icon and no words is
|
|
168
|
+
not clean, it is unreadable (p.014).
|
|
169
|
+
- If no user has shown they need a feature, remove it rather than park it behind
|
|
170
|
+
a toggle (NBG p.136).
|
|
171
|
+
- A file that builds a dozen fields at once is a wall, not a step. The gate counts
|
|
172
|
+
them (`CROWDED_FORM`); the fix is grouping or splitting the task (rule 13), not
|
|
173
|
+
smaller text.
|
|
174
|
+
|
|
175
|
+
## 07 Spacing communicates the relationship
|
|
176
|
+
|
|
177
|
+
Whitespace is functional, not decorative. Related things sit closer than
|
|
178
|
+
unrelated things, and that difference is the whole message (Playbook p.013, p.029).
|
|
179
|
+
|
|
180
|
+
- Field pattern: label to its own input `12`, that field to the next label `32`.
|
|
181
|
+
Equal spacing at both levels makes label ownership ambiguous (p.013). A number
|
|
182
|
+
between two steps of the spacing scale is the same mistake written smaller, and
|
|
183
|
+
the gate names it (`OFF_SCALE_SPACING`) wherever it appears — including inside a
|
|
184
|
+
`"12px 20px"` shorthand, because a shorthand is four decisions in one string.
|
|
185
|
+
- Card padding is `16`; the rhythm between cards is `16` or `24`; sections
|
|
186
|
+
separate at `24` or `32`; page blocks at `48`. Never mix two rhythms in one list.
|
|
187
|
+
- Start a layout with more room than you think you need and tighten it; clutter
|
|
188
|
+
is easier to see than cramp is to undo (p.025-027).
|
|
189
|
+
- Do not stretch a form or a checkout to the viewport. 1440 is a canvas size, not
|
|
190
|
+
a target; give the card the width it needs and let the margin carry the rest
|
|
191
|
+
(p.129-130). `Container({ maxWidth: 480 })` around a form, `fullWidth: true`
|
|
192
|
+
only on the button inside it.
|
|
193
|
+
- Layout is `useState` bindings, `flex`, `gap` and `wrap`. An absolute offset is
|
|
194
|
+
for overlays, and then through `stackPosition`, not raw coordinates.
|
|
195
|
+
|
|
196
|
+
## 08 Typography is structure
|
|
197
|
+
|
|
198
|
+
- Body is `size: 16` (Playbook p.090). Nothing that must be read goes below 12;
|
|
199
|
+
the book contradicts itself between 12 (p.090) and 10 (p.096) — 12 is the floor
|
|
200
|
+
here. A size between two steps of the type scale is reported as
|
|
201
|
+
`OFF_SCALE_TYPE`: the 15 beside the 16 does not read as hierarchy, it reads as
|
|
202
|
+
a mistake — two steps per jump, one role per size.
|
|
203
|
+
- Heading: bigger, heavier, darker, tighter. Body: smaller, regular, muted,
|
|
204
|
+
looser. Never the same weight for both, never extra-light for body (p.084-087).
|
|
205
|
+
- Line-height: 1.5-1.6 on body, 1.2-1.3 on headings; a 32px heading sits at about
|
|
206
|
+
1.25 (p.091-092). A heading never gets more air than a paragraph.
|
|
207
|
+
- Measure: 45-75 characters per line on desktop, 30-40 on mobile. A paragraph
|
|
208
|
+
that runs the full width is unreadable at 1440 (p.076-078).
|
|
209
|
+
- One typeface; two at the absolute maximum, and then headings against body
|
|
210
|
+
(p.075-076). Declare it once — `loadFont("Inter", { weights: [400, 600] })` and
|
|
211
|
+
one `fontFamily` on the screen's root — never per `Text`. There is no font on
|
|
212
|
+
the theme: `fontFamily` is a routed prop, and the loader is what puts the bytes
|
|
213
|
+
behind the name.
|
|
214
|
+
- `align: "left"` for every run of text and every list. `right` for numbers and
|
|
215
|
+
prices, so magnitudes compare. `center` only for a short title or a lone CTA,
|
|
216
|
+
never for a paragraph (p.018, p.094) — a centered paragraph is reported as
|
|
217
|
+
`CENTERED_BODY_TEXT`, because an eye that has to find the left edge again on
|
|
218
|
+
every line is doing the layout's work for it.
|
|
219
|
+
- Long copy becomes scannable copy: a short lead, a heading, then one item per
|
|
220
|
+
feature with an icon, a bold name and two lines of body; four or more items go
|
|
221
|
+
in a two-column grid (p.123-125).
|
|
222
|
+
|
|
223
|
+
## 09 Color carries meaning
|
|
224
|
+
|
|
225
|
+
- One brand color per screen, and it is the color of the one action the screen
|
|
226
|
+
asks for. A second accent on the same view means one of them is wrong.
|
|
227
|
+
- Reserve the brand for things you can click. Headers, labels and icons stay
|
|
228
|
+
neutral — brand color on a decorative surface teaches people that color means
|
|
229
|
+
nothing (Playbook p.066-067).
|
|
230
|
+
- Backgrounds are neutral (`colors.background`, `colors.surface`, `colors.card`);
|
|
231
|
+
color belongs in the action, not the wall (p.067-068). A dark theme is equally
|
|
232
|
+
valid; offer both, because dark mode is expected, not a bonus (p.070).
|
|
233
|
+
- Feedback colors are fixed and never redefined: `colors.success`,
|
|
234
|
+
`colors.warning`, `colors.danger`, `colors.info`. Red is error, green is
|
|
235
|
+
success, amber is warning; never pink for success, never green for delete.
|
|
236
|
+
- The signal red and the readable red are different tokens. `colors.danger`
|
|
237
|
+
(#ef4444) is for a badge, a border, an icon: as words on a white page it
|
|
238
|
+
measures 3.76:1 and that is not AA. `colors.dangerStrong` (#b91c1c) is the red
|
|
239
|
+
you may read and press — 6.47:1 as words on white, 6.47:1 with white written on
|
|
240
|
+
it, 5.42:1 inside `colors.errorBg`. Same for the others: `colors.warning`
|
|
241
|
+
(2.15:1 with white on it) and `colors.success` (2.54:1) are plates and dots,
|
|
242
|
+
never a sentence — `colors.warningStrong` (5.02:1) and `colors.successStrong`
|
|
243
|
+
(5.48:1) are the readable ones, in either direction. Measure any pair you are
|
|
244
|
+
about to ship: `npm run ui -- --tokens`.
|
|
245
|
+
- Never carry meaning in color alone — a color-blind person and a phone in
|
|
246
|
+
sunlight both lose it. Pair the color with an icon and a word (p.070): a status
|
|
247
|
+
chip is `Badge` plus text, a field error is `error` plus `showValidationMessage`.
|
|
248
|
+
- Hierarchy inside a fill comes from opacity, not from a second hue.
|
|
249
|
+
|
|
250
|
+
## 10 Depth is a scale
|
|
251
|
+
|
|
252
|
+
- `elevation: 0-5` is the shadow scale the framework ships (`SHADOW_LEVELS`), and
|
|
253
|
+
these widgets read it: `Accordion`, `AppBar`, `BottomNavigation`, `Button`,
|
|
254
|
+
`Card`, `Chip`, `Drawer`, `FloatingActionButton`, `ListTile`, `Modal`, `SnackBar`.
|
|
255
|
+
Elsewhere — `Container` and `ListView` included — the number reaches
|
|
256
|
+
`style.boxShadow` as a bare number, CSS drops it, and the box stays flat
|
|
257
|
+
without saying so: pass a `shadow: "0 12px 32px rgba(15,23,42,0.12)"` string
|
|
258
|
+
there instead (`DEAD_ELEVATION`).
|
|
259
|
+
- Anything that floats gets depth: a dropdown, a menu, a modal, a card on a map.
|
|
260
|
+
A shadowless layer merges with the page and the user cannot tell what is on top
|
|
261
|
+
(p.050-051).
|
|
262
|
+
- Soft, not harsh: depth comes from a wide blur at low opacity, and the shadow
|
|
263
|
+
is tinted toward the background hue, never pure grey or black (p.052-054).
|
|
264
|
+
- Do not put a white card on a white page — the interface goes flat. Page
|
|
265
|
+
`colors.background`, card `colors.card` (p.056-057).
|
|
266
|
+
- Borders are thin and rare. Separation comes from space and surface, not from a
|
|
267
|
+
rule under every row; a bordered list of everything reads as busy and dated
|
|
268
|
+
(p.059-061).
|
|
269
|
+
- Glass and blur are a brand decision, and they never survive a light background:
|
|
270
|
+
text on frosted glass over white loses the contrast war (p.063-065).
|
|
271
|
+
|
|
272
|
+
## 11 Accessibility is a floor, not a feature
|
|
273
|
+
|
|
274
|
+
When accessibility and aesthetics conflict, accessibility wins. Every time.
|
|
275
|
+
|
|
276
|
+
- Text needs 4.5:1 against what it sits on; large text (24px, or 18px bold) and
|
|
277
|
+
meaningful graphics need 3:1. Disabled text is exempt. The book agrees: its
|
|
278
|
+
passing examples measure 4.8-6.54:1 and its failures 1.81-2.84:1 (p.021), and
|
|
279
|
+
#626262 is the lightest grey that survives on white (p.086).
|
|
280
|
+
- Measure instead of arguing: `npm run ui -- --tokens`. On this palette `text` is
|
|
281
|
+
17.85:1, `textSecondary` 4.76:1, `textDisabled` 2.56:1 (decoration only), and
|
|
282
|
+
the brand pair `onPrimary` on `primary` is 4.55:1 — which is why `primary` may
|
|
283
|
+
carry a button label and not a paragraph. By hand:
|
|
284
|
+
`color("#0f172a").contrastRatio("#ffffff")` answers 17.85.
|
|
285
|
+
- The gate's four contrast codes are four different sentences, and treating them
|
|
286
|
+
as one noise is how a real one gets dismissed. `LOW_CONTRAST` is under 3:1 — no
|
|
287
|
+
size, no weight and no theme rescues those words. `LOW_CONTRAST_SOFT` is
|
|
288
|
+
between 3:1 and 4.5:1: legal as large text only (24px, or 18px bold), so either
|
|
289
|
+
promote the run or darken the token. `LOW_CONTRAST_ON_SURFACE` is a run with no
|
|
290
|
+
fill of its own measured against `background` and `surface` — the color you
|
|
291
|
+
wrote is a plate, not an ink, or the words are sitting on a fill the call never
|
|
292
|
+
named. `LOW_CONTRAST_UI` is a graphic that carries meaning under 3:1; an icon
|
|
293
|
+
next to a label that already says the same thing is decoration and can be kept.
|
|
294
|
+
- Text over a photograph needs a scrim: a dark overlay or a blurred plate under
|
|
295
|
+
the words. Light text over one dark photo proves nothing about the next photo
|
|
296
|
+
(p.022).
|
|
297
|
+
- Every field has a visible `label`; a placeholder is a format hint, never a
|
|
298
|
+
substitute (p.115). Every switch and checkbox has one too (`UNLABELED_TOGGLE`).
|
|
299
|
+
- Every control that is only an icon gets a name: `aria-label` on the control
|
|
300
|
+
itself, or a visible label beside it (`ICON_ONLY_CONTROL`). A `Tooltip` is a
|
|
301
|
+
description — it explains the control, it does not name it.
|
|
302
|
+
- Every `Image` that carries information has `alt`; `alt: ""` is the correct
|
|
303
|
+
answer for a decoration (`IMAGE_NO_ALT`).
|
|
304
|
+
- Focus must be visible, keyboard must reach everything, and `Esc` must close
|
|
305
|
+
whatever opened (`Modal({ closeOnEsc: true, showCloseButton: true })`).
|
|
306
|
+
- Motion is optional content: respect a reduced-motion preference; never
|
|
307
|
+
auto-play a carousel or a banner. FletBox listens (`prefersReducedMotion()`,
|
|
308
|
+
`setMotionMode('reduce')`) and the engine obeys it, so a hand-written
|
|
309
|
+
`requestAnimationFrame` is the only way to ignore it — see `fletbox-motion`.
|
|
310
|
+
|
|
311
|
+
## 12 Every interaction has four states
|
|
312
|
+
|
|
313
|
+
A control that handles only the happy path is unfinished (NBG pp.94-97). Write
|
|
314
|
+
all four, in the same component, before the screen counts as done.
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
Column({ gap: 16, children: [
|
|
318
|
+
loading ? Skeleton({ width: "100%", height: 48, count: 3 }) : null,
|
|
319
|
+
!loading && rows.length === 0
|
|
320
|
+
? Column({ gap: 16, alignItems: "center", children: [
|
|
321
|
+
Icon({ name: "inbox", size: 48, color: colors.textSecondary }),
|
|
322
|
+
Text({ text: "No projects yet", size: 18, weight: "bold" }),
|
|
323
|
+
Text({ text: "Create one to see it here.", size: 14,
|
|
324
|
+
color: colors.textSecondary, align: "center" }),
|
|
325
|
+
Button({ text: "New project", onPress: create }),
|
|
326
|
+
]}) // headline + illustration + tip + CTA
|
|
327
|
+
: null,
|
|
328
|
+
failed
|
|
329
|
+
? SnackBar({ message: "Could not load the list.", action: "Retry", onAction: retry })
|
|
330
|
+
: null,
|
|
331
|
+
!loading && rows.length > 0 ? ListView({ children: rows }) : null,
|
|
332
|
+
]})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- Empty has four parts: a solution-oriented headline, an illustration, two
|
|
336
|
+
concrete tips, one primary action. "You have no projects" is a dead end, not a
|
|
337
|
+
state (Playbook p.135-136).
|
|
338
|
+
- Loading appears before the wait, not after it: a `Skeleton` at once beats a
|
|
339
|
+
spinner two seconds later (NBG p.134, Doherty).
|
|
340
|
+
- Error does exactly three things: says what went wrong in the user's words,
|
|
341
|
+
gives the way back, keeps the visual style of the app. Show the code, keep the
|
|
342
|
+
navigation, offer links (p.137-138). Never an error code alone, never a shrug.
|
|
343
|
+
- Success confirms in place — a `SnackBar`, not a new screen — and offers the
|
|
344
|
+
next step while it is there.
|
|
345
|
+
- Progress is honest. Show real progress or show a `Skeleton`; a bar that lies
|
|
346
|
+
buys one visit and costs the next one (NBG defends inflating them; do not).
|
|
347
|
+
- The gate knows this rule: a file that renders a list and names none of the
|
|
348
|
+
other three states is reported as `LIST_WITHOUT_STATE`. Silence is the bug it
|
|
349
|
+
is looking for, so do not hush it with a comment — add the state.
|
|
350
|
+
|
|
351
|
+
## 13 Forms: prevent the error, do not report it
|
|
352
|
+
|
|
353
|
+
- Ask for 3 to 7 things at once. More than that is more than one step, and a step
|
|
354
|
+
the user can see: `Stepper({ steps, activeStep, onFinish })` (NBG pp.40-43).
|
|
355
|
+
- Width fits the data: CVC is 3 or 4 characters, so the field is short; card
|
|
356
|
+
number and name run full width; pair Expiry with CVC on one row (p.115-116).
|
|
357
|
+
- A column of three or more fields lives inside a cap — `Container({ maxWidth:
|
|
358
|
+
480 })`. A field as wide as the window answers "type anything" and the eye
|
|
359
|
+
loses the next line (`UNCONSTRAINED_FORM_WIDTH`).
|
|
360
|
+
- A fixed-length code gets one box per character in enlarged type, not one long
|
|
361
|
+
field (p.114).
|
|
362
|
+
- `Input({ label, placeholder, required, maxLength, validation, type })`:
|
|
363
|
+
placeholder carries the format ("MM/YY", "12345"), `maxLength` and `type`
|
|
364
|
+
constrain the input, and validation runs inline as they type, not on submit
|
|
365
|
+
(p.119). A field that has neither is `UNLABELED_FIELD`, and the label is also
|
|
366
|
+
the name a screen reader says out loud.
|
|
367
|
+
- No "Confirm password" field. It adds cost, repeats the same typo twice, and
|
|
368
|
+
buys a false sense of safety. One field, `passwordToggle: true`, and a strength
|
|
369
|
+
line under it (p.117-119).
|
|
370
|
+
- Good defaults and autofill remove keystrokes: prefill the country, the saved
|
|
371
|
+
address, the last card (p.105).
|
|
372
|
+
- Required fields are marked, and the label of the submit button names the
|
|
373
|
+
outcome — "Pay $65", not "Submit" (p.115).
|
|
374
|
+
- Never clear a form because one field failed. Keep what they typed.
|
|
375
|
+
|
|
376
|
+
## 14 Destructive actions are designed last, and carefully
|
|
377
|
+
|
|
378
|
+
- The button carries the verb: "Delete 3 files", never "OK" or "Yes"
|
|
379
|
+
(`GENERIC_ACTION_LABEL`) (Playbook p.121).
|
|
380
|
+
- Destructive is red — `colors.dangerStrong` for the words, `colors.danger` for
|
|
381
|
+
the dot beside them — because green reads safe and a person presses it twice
|
|
382
|
+
(`DESTRUCTIVE_COLOR`) (p.120). `colors.danger` alone is 3.76:1 as a label.
|
|
383
|
+
- Order in a two-button row: Cancel on the left, destructive on the right. Left
|
|
384
|
+
is what a reading eye hits first (`CONFIRM_ORDER`) (p.121-122).
|
|
385
|
+
- The dialog states reversibility: "This action cannot be undone." Say it even
|
|
386
|
+
when it can, and then offer the way back.
|
|
387
|
+
- `AlertDialog({ title, message, confirmText, cancelText, showCancel, onCancel, onConfirm, onClose })`
|
|
388
|
+
always leaves an escape: a Cancel, and an X on any layer that can be opened.
|
|
389
|
+
- Undo is cheaper than a warning. If the action is reversible, let people undo it
|
|
390
|
+
instead of interrupting them to confirm it (NBG pp.116-118).
|
|
391
|
+
- On a phone, a destructive action is not in the thumb zone; the thing you do
|
|
392
|
+
often is (p.133-135).
|
|
393
|
+
|
|
394
|
+
## 15 Reduce interaction cost
|
|
395
|
+
|
|
396
|
+
Interaction cost is cognitive effort + physical effort + time (Playbook p.098).
|
|
397
|
+
Count it, then cut it: reading, scrolling, searching, deciding, clicking, typing,
|
|
398
|
+
waiting, remembering (p.099).
|
|
399
|
+
|
|
400
|
+
- Count the steps in the main flow and delete every one that does not change the
|
|
401
|
+
outcome (p.105). Three is a target; one is better.
|
|
402
|
+
- Put the action next to the thing it acts on: the CTA beside the quantity
|
|
403
|
+
stepper, the row action inside the row (p.108, Fitts).
|
|
404
|
+
- Keep the control that runs all the time in a fixed, reachable place — a player
|
|
405
|
+
bar at the bottom, a primary tab at the thumb (p.100, p.133-135).
|
|
406
|
+
- Show the content instead of a door to the content: three real cards beat a
|
|
407
|
+
banner that says "Explore now" (p.111-113).
|
|
408
|
+
- One click where a confirmation is not needed. Two where it is.
|
|
409
|
+
|
|
410
|
+
## 16 Respect the platform
|
|
411
|
+
|
|
412
|
+
- Use the words, icons and gestures people already brought with them (NBG p.131,
|
|
413
|
+
Jakob's law). An invented gesture is a bug report waiting to be written.
|
|
414
|
+
- iOS, Android and web each have conventions; meet them instead of overriding
|
|
415
|
+
them with a custom pattern. A back that lands where the user expects, a sheet
|
|
416
|
+
that dismisses down, a form that advances on Enter.
|
|
417
|
+
- Break a convention only when the measurement says the convention is wrong here,
|
|
418
|
+
and write that measurement down.
|
|
419
|
+
|
|
420
|
+
## 17 Composition starts as a sentence
|
|
421
|
+
|
|
422
|
+
Before the first widget, write four lines: what this screen decides for the
|
|
423
|
+
user, the three pieces of information that carry it, and the one action. Then
|
|
424
|
+
lay out those four things. A screen that starts as "a hero, then three cards,
|
|
425
|
+
then a footer" is a template, and it gets filled with whatever is handy.
|
|
426
|
+
|
|
427
|
+
- The type scale caps at 48, so a headline cannot buy all of its presence from
|
|
428
|
+
size. Take the rest from the two levers that are free: `weight: "bold"` on the
|
|
429
|
+
title against `weight: "normal"` on the body, and `colors.text` against
|
|
430
|
+
`colors.textSecondary`. That one step is what makes a screen look designed at
|
|
431
|
+
375px, where 48 is already the ceiling.
|
|
432
|
+
- At most five sizes per screen, and at most one role per size.
|
|
433
|
+
- The accent is under 5% of the pixels. If a first-glance photo shows it twice,
|
|
434
|
+
one of the two is not an accent (rule 09).
|
|
435
|
+
- Squint at the screen. What you can still read is the hierarchy. If what
|
|
436
|
+
survives is a badge or an accent, rule 04 is not done.
|
|
437
|
+
|
|
438
|
+
## 18 The framework traps
|
|
439
|
+
|
|
440
|
+
Real FletBox behaviors, not opinions. Each one has already produced a "the UI is
|
|
441
|
+
broken" report, and none of them is something `npm run ui` can see:
|
|
442
|
+
|
|
443
|
+
- **`Container` is a column and it paints a surface.** Children stack
|
|
444
|
+
vertically unless you pass `direction: "row"`, and it has a background — a
|
|
445
|
+
`Container` around your content is a visible plate, not an invisible wrapper.
|
|
446
|
+
- **Widths default to 100%.** A `Button` inside a `Column` fills the screen
|
|
447
|
+
unless it sits in a `Row` or gets a `width`. Use `fullWidth: true`
|
|
448
|
+
deliberately on the one action a phone screen asks for; let the rest be sized
|
|
449
|
+
by their content.
|
|
450
|
+
- **`flex: 1` needs a sibling that wants the space.** One flexible child in a
|
|
451
|
+
fixed-height parent stretches; `justifyContent: "flex-start"` starves the
|
|
452
|
+
`flex` you meant on the other axis.
|
|
453
|
+
- **`alignItems: "center"` centers the block, not the text.** A paragraph that
|
|
454
|
+
wraps inside a centered column becomes a ragged stripe: center the heading,
|
|
455
|
+
left-align the copy (rule 08).
|
|
456
|
+
- **Fluid first, breakpoint second.** `flex`, `wrap: true`, `maxWidth` and the rem
|
|
457
|
+
contract carry one screen from phone to desktop with no decision at all, and
|
|
458
|
+
that is the default. There is no breakpoint *prop* — when a shape genuinely
|
|
459
|
+
changes (one column into two, a rail into a drawer), the change is made in the
|
|
460
|
+
code that builds the tree, with the one table: `atLeast("md")` for the boolean,
|
|
461
|
+
`currentBreakpoint()` for the name, a per-step value function for a number that
|
|
462
|
+
really wants to be different at 400px and 1400px. `md` is 768, the same line
|
|
463
|
+
`AdaptiveScaffold` crosses, so a screen and its shell cannot disagree. Read the
|
|
464
|
+
width, never the user agent: `device.isMobile()` answers a platform question
|
|
465
|
+
(long-press, a native back gesture), not a layout one. A screen built from two
|
|
466
|
+
hard-coded layouts is two screens to maintain; a screen with one per-step value
|
|
467
|
+
where the shape truly changes is one.
|
|
468
|
+
- Overlays go in a `Stack` with `stackPosition`, not in absolute coordinates on
|
|
469
|
+
`Container({ pos: "absolute", top, left })`. The first survives a content
|
|
470
|
+
change; the second is a pixel bomb in any language longer than English.
|
|
471
|
+
|
|
472
|
+
## 19 The dated tells
|
|
473
|
+
|
|
474
|
+
Each of these says "assembled from a template". Banned outright, with the reason
|
|
475
|
+
once so an edge case can be judged instead of guessed:
|
|
476
|
+
|
|
477
|
+
- **An eyebrow label** — a small uppercase word above a heading ("OVERVIEW",
|
|
478
|
+
"GET STARTED"). Hierarchy cosplay: it names a section nobody asked to have
|
|
479
|
+
named.
|
|
480
|
+
- **Numbered markers** `01 · 02 · 03` — they imply an order a feature list does
|
|
481
|
+
not have.
|
|
482
|
+
- **Monospace outside code.** `Text({ styles: ["code"] })` means code. Using it
|
|
483
|
+
to look technical is a costume.
|
|
484
|
+
- **A gradient headline, or a gradient behind everything.** `gradient` exists on
|
|
485
|
+
`Button` and `AppBar`: it is one element's surface, never a text treatment.
|
|
486
|
+
- **Glass everywhere.** `colors.glass` with `colors.glassBorder` is for the one
|
|
487
|
+
layer that really floats above scrolling content. Frosted cards on a solid page
|
|
488
|
+
are a texture, not a hierarchy (rule 10).
|
|
489
|
+
- **A row of identical cards** — three plates with an icon, a title, two lines.
|
|
490
|
+
If the content has no shape a list would reveal, it does not need four cards.
|
|
491
|
+
- **Resting shadows.** Depth answers "what is above what", never "make it pop"
|
|
492
|
+
(rule 10).
|
|
493
|
+
- **A wall of centered text** (rule 08), and a hairline `Divider` under every
|
|
494
|
+
row: a line that separates nothing is the fastest way a page looks dated.
|
|
495
|
+
- **Copy clichés** — "unlock", "elevate", "seamless", "empower", "leverage", a
|
|
496
|
+
sentence that starts with "Whether you're…". Say the specific thing the product
|
|
497
|
+
does. If you cannot, that is a content gap, not a font gap.
|
|
498
|
+
|
|
499
|
+
Deleting a tell is not a design. Every ban above needs the element that carries
|
|
500
|
+
the job instead: hierarchy from rule 17, separation from rule 07 or 10, emphasis
|
|
501
|
+
from rule 09, grouping from a `ListView` or a `gap`.
|
|
502
|
+
|
|
503
|
+
## 20 Density is one dial
|
|
504
|
+
|
|
505
|
+
Pick one row and hold it for the whole screen. Three numbers set it: a `gap`, a
|
|
506
|
+
`padding`, and a body `size`.
|
|
507
|
+
|
|
508
|
+
| Screen | gap | padding | body |
|
|
509
|
+
| --- | --- | --- | --- |
|
|
510
|
+
| Reading / marketing | 32 | 32 | 16 |
|
|
511
|
+
| Form | 16 | 24 | 16 |
|
|
512
|
+
| Working list / dashboard | 8 | 16 | 14 |
|
|
513
|
+
|
|
514
|
+
A dense screen is not a cramped one: the 40 tap floor and the 14 label floor
|
|
515
|
+
(rule 03, rule 11) still apply, and they are why "compact" has a hard limit.
|
|
516
|
+
|
|
517
|
+
## 21 The review
|
|
518
|
+
|
|
519
|
+
When asked to review a screen, answer in this order, and say why every time:
|
|
520
|
+
|
|
521
|
+
1 Goal — what the screen is for, and whether it is one goal.
|
|
522
|
+
2 UX problems — cost, states, recall, flow.
|
|
523
|
+
3 UI problems — hierarchy, spacing, type, color, depth.
|
|
524
|
+
4 System violations — the values that are not tokens, the component that is an
|
|
525
|
+
exception.
|
|
526
|
+
5 Accessibility violations — measured ratios, targets, labels, focus.
|
|
527
|
+
6 Consistency violations — what this screen does differently for no reason.
|
|
528
|
+
7 Scalability risk — what breaks when this screen has eleven siblings.
|
|
529
|
+
8 Fix — the call, not the mood. Then the rule it establishes.
|
|
530
|
+
9 Score — see below.
|
|
531
|
+
|
|
532
|
+
Score the seven axes 0-100, but only against evidence, never against taste:
|
|
533
|
+
an axis starts at 100 and loses 10 for every open finding of its own codes, so
|
|
534
|
+
the number is a count and not an opinion. Below 85 on any axis means the fix is
|
|
535
|
+
a rule change, not a pixel move. An axis with no findings and no evidence is
|
|
536
|
+
unmeasured, not perfect — say so.
|
|
537
|
+
|
|
538
|
+
| Axis | Its codes (from `npm run ui`) |
|
|
539
|
+
| --- | --- |
|
|
540
|
+
| Consistency | OFF_SCALE_SPACING · OFF_SCALE_RADIUS · OFF_SCALE_TYPE · RAW_COLOR · HAND_MIXED_COLOR · DEAD_ELEVATION |
|
|
541
|
+
| Accessibility | LOW_CONTRAST · LOW_CONTRAST_UI · SMALL_TAP_TARGET · UNLABELED_FIELD · UNLABELED_TOGGLE · ICON_ONLY_CONTROL · IMAGE_NO_ALT |
|
|
542
|
+
| Usability | CROWDED_FORM · LIST_WITHOUT_STATE · UNCONSTRAINED_FORM_WIDTH |
|
|
543
|
+
| Hierarchy | MULTIPLE_PRIMARY_ACTIONS · CENTERED_BODY_TEXT · GENERIC_ACTION_LABEL · DESTRUCTIVE_COLOR · CONFIRM_ORDER |
|
|
544
|
+
| Scalability | every token that had to be added to make the screen pass |
|
|
545
|
+
| Conversion | the label of the primary action, counted in words and in steps |
|
|
546
|
+
| Visual quality | measured only by the axes above; state what you looked at |
|
|
547
|
+
|
|
548
|
+
## 22 Where these books are wrong
|
|
549
|
+
|
|
550
|
+
Both are worth more read than copied. The parts that do not survive measurement:
|
|
551
|
+
|
|
552
|
+
- The Playbook bans pure black on white and pure white on dark (p.083). This
|
|
553
|
+
system puts white text on deep brand fills, and the ratio is what decides —
|
|
554
|
+
4.55:1 there, not the rule of thumb.
|
|
555
|
+
- p.008 says a secondary button keeps the primary's size; p.088 says size is
|
|
556
|
+
the hierarchy lever. Both are right about different things: emphasis ranks
|
|
557
|
+
actions, size ranks information.
|
|
558
|
+
- p.079 marks center-aligned body as good; p.094 says center is for titles only.
|
|
559
|
+
The later rule is the readable one.
|
|
560
|
+
- p.103 bans pop-ups as distractions; p.120 requires one to confirm a deletion.
|
|
561
|
+
Reconciled: never unsolicited, always on destructive.
|
|
562
|
+
- p.019 (gutter 70, margin 79, overlay 17%) and p.052-054 (Y:12, blur:48,
|
|
563
|
+
#E9E9E9) are one designer's Figma fields in a screenshot, not a scale.
|
|
564
|
+
Do not harvest them as thresholds.
|
|
565
|
+
- The Playbook's own sign-up example (p.127) is white on a pale blue fill that
|
|
566
|
+
fails its own contrast chapter. Copy the rule, never the screenshot.
|
|
567
|
+
- "7 ± 2" (p.101) is Miller's number quoted loosely; chunk size is a design
|
|
568
|
+
choice, and four is the one that survives a fifth item.
|
|
569
|
+
- NBG defends inflating progress bars (its "little lie"). Do not.
|
|
570
|
+
- Neither book states a tap-target size in px. Fitts gives the direction; 40 is
|
|
571
|
+
this system's floor, and the number to defend if someone asks.
|
|
572
|
+
|
|
573
|
+
## 23 What no checker can see
|
|
574
|
+
|
|
575
|
+
`npm run ui` cannot tell you whether the screen is understood. For that:
|
|
576
|
+
|
|
577
|
+
- Ask one person to do one task and stay quiet while they do it. Their first three
|
|
578
|
+
sentences are the specification.
|
|
579
|
+
- Watch where the pointer hesitates and where it misclicks: that is where the
|
|
580
|
+
layout is lying about what is tappable.
|
|
581
|
+
- Five people find most of what is broken. Two rounds of five beat one round of
|
|
582
|
+
ten.
|
|
583
|
+
- Write what you saw into `.fletbox/memory/`, or the next session designs this
|
|
584
|
+
screen again from taste.
|
|
585
|
+
|
|
586
|
+
## 24 The loop
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
npm run check
|
|
590
|
+
npm run ui
|
|
591
|
+
npm run dev # look at the screen, at 375px and at 1440px
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
Fix the code, not the checker. Then make the rule reusable: if the fix was right
|
|
595
|
+
for this screen, it is a rule for every screen, and it belongs in this file.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
# PART II — THE VISUAL ATLAS
|
|
600
|
+
|
|
601
|
+
Rules 01-24 say what is legal. They do not say what to choose, and a screen that
|
|
602
|
+
is merely legal looks like nothing. Part II is the menu: the looks that hold up
|
|
603
|
+
for their own reasons, each reduced to the calls that produce it in this
|
|
604
|
+
framework. It is read after the rules, never instead of them — every recipe below
|
|
605
|
+
still has to pass `npm run check`, `npm run ui` and rules 01-24.
|
|
606
|
+
|
|
607
|
+
Nothing here cites a trend, a screenshot or a mood board. Each rule states its
|
|
608
|
+
mechanism — what the eye is actually doing, or what the CSS engine will and will
|
|
609
|
+
not accept — so an edge case can be judged from the reason instead of from the
|
|
610
|
+
precedent. Where a number appears it is either arithmetic (30), a measured ratio
|
|
611
|
+
from this palette (03, 11, 35), or a size the framework ships.
|
|
612
|
+
|
|
613
|
+
## 25 Three laws
|
|
614
|
+
|
|
615
|
+
One accent, one surface family, one hero. These three hold across mobile and web,
|
|
616
|
+
dark and light, and every genre; a screen that breaks any of them is not
|
|
617
|
+
stylistically bold, it is unreadable in a new way.
|
|
618
|
+
|
|
619
|
+
1. **One accent.** A neutral base plus exactly one saturated hue, spent on the
|
|
620
|
+
action and the selected state and nowhere else (26, 28). Two accents force the
|
|
621
|
+
eye to rank them, and it has no basis to rank them with.
|
|
622
|
+
2. **One surface family.** Either a tinted dark ramp or a tinted paper; never a
|
|
623
|
+
black page with white cards, never a grey page with white cards (27).
|
|
624
|
+
3. **One hero.** The screen has one element that is 3-5× the rest — usually a
|
|
625
|
+
number, once an image, rarely a word. Everything else is at reading size (29).
|
|
626
|
+
|
|
627
|
+
Squint test for the three: if you can still tell which element is the hero, and
|
|
628
|
+
how many colored things there are, the screen obeys them.
|
|
629
|
+
|
|
630
|
+
## 26 Pick an archetype, then obey it
|
|
631
|
+
|
|
632
|
+
Name the archetype in the commit or in `.fletbox/memory/` before the first
|
|
633
|
+
widget. A screen that mixes two of them is what rule 02 calls an exception.
|
|
634
|
+
|
|
635
|
+
| Archetype | Use it when | The tell you must get right |
|
|
636
|
+
| --- | --- | --- |
|
|
637
|
+
| Acid-on-black | finance, media, fitness, tools, anything glanceable | the accent is on one element only (28) |
|
|
638
|
+
| Tinted paper | commerce, food, wellness, content | the page is a wash of the accent, not grey (27) |
|
|
639
|
+
| Photo-bleed + sheet | travel, product detail, real estate, recipes | the sheet overlaps the photo, it does not sit under it (31) |
|
|
640
|
+
| Bento dashboard | dashboards, smart-home, admin | one tile is double-size and it is the hero (33) |
|
|
641
|
+
| Editorial luxury | watches, perfume, hotel, long-read | one serif, used for the name of the thing only (29) |
|
|
642
|
+
| Single-hue field | a brand moment: splash, campaign, empty state | the whole page is one hue, the text is its darker plate (27) |
|
|
643
|
+
|
|
644
|
+
The six are exhaustive of what survives review here. The look they are usually
|
|
645
|
+
reached for instead — violet mesh gradient, glass on every card, a 3D blob behind
|
|
646
|
+
the phone — is not a seventh archetype; it is in 36 as a dated tell, and it is the
|
|
647
|
+
one most often mistaken for a style.
|
|
648
|
+
|
|
649
|
+
## 27 Surfaces are never the extremes
|
|
650
|
+
|
|
651
|
+
Pure black under pure white is the one pair that always loses: `#000000` against
|
|
652
|
+
`#ffffff` measures 21:1, and an eye moving across a list at that ratio keeps
|
|
653
|
+
catching on the edges of every glyph. A large bright surface glows for the same
|
|
654
|
+
reason. So the extremes get replaced by steps, and the steps are where the
|
|
655
|
+
hierarchy lives. The shipped dark palette already obeys this — `background`
|
|
656
|
+
`#09090b`, `card` and `surface` `#18181b`, `border` `#27272a` — a three-step ramp
|
|
657
|
+
of one near-black hue. Use the steps, do not invent a fourth.
|
|
658
|
+
|
|
659
|
+
- **Dark.** Depth is a lighter plate, not a shadow: a shadow on a dark page is
|
|
660
|
+
invisible, so the only signal available is value. Two steps maximum between the
|
|
661
|
+
page and the topmost card; a third step is a bug, because nobody can tell step
|
|
662
|
+
three from step two. A hairline `colors.border` rim replaces the shadow on an
|
|
663
|
+
elevated card (rule 10: `Container` ignores `elevation` — pass `shadow` or
|
|
664
|
+
`borderColor` there, never a bare number).
|
|
665
|
+
- **Paper.** The page is a low-chroma wash of the accent (`colors.primaryBg`
|
|
666
|
+
exists for exactly this), the card is `colors.card`, and the tile inside the
|
|
667
|
+
card is one step back. Grey page under white card is the wrong version of this
|
|
668
|
+
look and reads as a wireframe: grey is a neutral, so it separates the card from
|
|
669
|
+
nothing.
|
|
670
|
+
- **Single-hue field.** One hue carries the screen and the text is a darker or
|
|
671
|
+
lighter plate of the same hue — never a second hue for contrast, because a
|
|
672
|
+
second hue is a second accent and 25 allows one. In this framework that means a
|
|
673
|
+
new token in `src/database/themes.js` per theme, not an inline mix:
|
|
674
|
+
`colors.primary + "22"` is `HAND_MIXED_COLOR` (rule 03).
|
|
675
|
+
- **Never** a pure-`#fff` page under pure-`#000` text with nothing between: that
|
|
676
|
+
pair is the failure this section exists to prevent, and the shipped light page
|
|
677
|
+
is `#ffffff` only because `surface` is `#f8fafc` and does the separating.
|
|
678
|
+
|
|
679
|
+
## 28 The accent budget
|
|
680
|
+
|
|
681
|
+
The accent is a budget, not a color. It may appear on, and only on:
|
|
682
|
+
|
|
683
|
+
1. the one primary action;
|
|
684
|
+
2. the selected state of whatever chose it (chip, tab, nav icon, swatch);
|
|
685
|
+
3. the hero number, when the number is the point.
|
|
686
|
+
|
|
687
|
+
Everything else — headers, labels, icons, dividers, decorative plates — stays
|
|
688
|
+
neutral. This is rule 09's "reserve the brand for things you can click" with the
|
|
689
|
+
count added: three placements, and a fourth means one of them is not an accent.
|
|
690
|
+
The mechanism is learning: a color that appears on non-clickable things stops
|
|
691
|
+
being a cue, and the user stops reading it — so the CTA loses the very property
|
|
692
|
+
that made it findable.
|
|
693
|
+
|
|
694
|
+
- Secondary is the same ink, hollow: `variant: "outlined"` naming its own
|
|
695
|
+
`color:`, or `variant: "text"`. Never a second hue for the second button.
|
|
696
|
+
- A gradient on the primary is legal only as **two shades of one hue** — a violet
|
|
697
|
+
button running `#9D70F6 → #5520FF`, both declared as tokens, is still one
|
|
698
|
+
accent. Cap any gradient at two hues: three is no longer a surface, it is an
|
|
699
|
+
event, and it competes with the CTA for the same attention (36).
|
|
700
|
+
- Glow is the accent's only other permitted spend, and only on dark, because a
|
|
701
|
+
bloom over a light page is indistinguishable from a smudge:
|
|
702
|
+
`CircularBar({ glow: true, glowColor: colors.primary })`, or a tinted `shadow`
|
|
703
|
+
string on the CTA. One glowing element per screen.
|
|
704
|
+
|
|
705
|
+
## 29 The number is the hero
|
|
706
|
+
|
|
707
|
+
The composition every genre that shows a quantity should reach for: one oversized
|
|
708
|
+
tabular numeral, its label above it in muted text, and nothing else on the screen
|
|
709
|
+
competing. A magnitude read as a shape is understood faster than the same
|
|
710
|
+
magnitude read digit by digit, which is why the number gets the size and not the
|
|
711
|
+
heading.
|
|
712
|
+
|
|
713
|
+
```js
|
|
714
|
+
Column({ gap: 4, children: [
|
|
715
|
+
Text({ text: "Total balance", size: 12, weight: 500, color: colors.textSecondary, letterSpacing: 0.4 }),
|
|
716
|
+
Row({ alignItems: "flex-end", gap: 8, children: [
|
|
717
|
+
Text({ text: "26072", size: 40, weight: 700, color: colors.text, letterSpacing: -0.8, lineHeight: 1.1 }),
|
|
718
|
+
Text({ text: ".00", size: 18, weight: 500, color: colors.textSecondary }),
|
|
719
|
+
Text({ text: "+4.2%", size: 14, weight: 600, color: colors.successStrong }),
|
|
720
|
+
]}),
|
|
721
|
+
]})
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
- The jump is 3-5× the body size, and the numeral is the only element at that
|
|
725
|
+
size on the screen. Two heroes is zero heroes (rule 04).
|
|
726
|
+
- Decimals and currency marks one step down and muted: the eye is meant to read
|
|
727
|
+
the magnitude first. `letterSpacing` goes slightly negative on big digits and
|
|
728
|
+
positive on small caps labels — never both the same.
|
|
729
|
+
- Delta text is `colors.successStrong` / `colors.dangerStrong`, never the plate
|
|
730
|
+
tokens (rule 09: `colors.success` is 2.54:1 with white on it and is a dot).
|
|
731
|
+
- Serif is a brand voice, not a font choice: one serif for the name of the thing
|
|
732
|
+
(product, hotel, edition), sans for everything readable, and never a serif
|
|
733
|
+
paragraph. Declare it once with `loadFont` and one `fontFamily` on the root,
|
|
734
|
+
not per `Text` (rule 08).
|
|
735
|
+
- Uppercase with wide tracking is for a two-word label, never for a sentence,
|
|
736
|
+
and never for the body: tracking destroys the word shape that a reader
|
|
737
|
+
recognizes, so it costs speed everywhere it is not a single glance (rule 19's
|
|
738
|
+
eyebrow ban still applies to the label above a heading).
|
|
739
|
+
|
|
740
|
+
## 30 Radius, nesting and the pill
|
|
741
|
+
|
|
742
|
+
- Controls are pills: `borderRadius` = half the height. Cards are 16-24. Inner
|
|
743
|
+
thumbnails and tiles are 12-16, one step below their card.
|
|
744
|
+
- **The nesting rule**, as arithmetic: `outer = inner + padding`. A 16-radius
|
|
745
|
+
tile inside 8 of padding needs a 24-radius card. Equal radii at both levels is
|
|
746
|
+
the failure — the inner corner and the outer corner curve on the same radius,
|
|
747
|
+
so the gap between them reads as a second box rather than as one plate.
|
|
748
|
+
- A radius between two steps is `OFF_SCALE_RADIUS` (rule 03), and the reason is
|
|
749
|
+
perceptual, not bookkeeping: the corner is what people match by eye, so a 15
|
|
750
|
+
beside a 16 looks like a mistake even when nobody can say why. 20 and 24 are
|
|
751
|
+
steps, not neighbours — they are where the nesting rule lands, and where the
|
|
752
|
+
framework's own drawers and tab indicators sit.
|
|
753
|
+
- One radius language per screen (rule 02). A pill button inside a 16-radius card
|
|
754
|
+
on a 24-radius sheet is three languages; pick the card radius and let the
|
|
755
|
+
controls be pills, since a pill is the one shape that reads as a control at any
|
|
756
|
+
card radius.
|
|
757
|
+
|
|
758
|
+
```js
|
|
759
|
+
Card({ borderRadius: 24, padding: 16, children:
|
|
760
|
+
Row({ gap: 12, alignItems: "center", children: [
|
|
761
|
+
Image({ src: thumb, width: 56, height: 56, borderRadius: 12, fit: "cover", alt: "" }),
|
|
762
|
+
Column({ gap: 2, children: [
|
|
763
|
+
Text({ text: title, size: 16, weight: 600, color: colors.text }),
|
|
764
|
+
Text({ text: subtitle, size: 12, color: colors.textSecondary }),
|
|
765
|
+
]}),
|
|
766
|
+
Button({ text: "Add", variant: "filled", height: 36, borderRadius: 999 }),
|
|
767
|
+
]}),
|
|
768
|
+
})
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
## 31 Photo, scrim, sheet
|
|
772
|
+
|
|
773
|
+
The composition of every photo-led genre — travel, product detail, real estate,
|
|
774
|
+
recipes — and the one place a `Stack` earns its keep:
|
|
775
|
+
|
|
776
|
+
```js
|
|
777
|
+
Stack({ alignment: "bottom-left", height: 420, children: [
|
|
778
|
+
Image({ src: hero, fit: "cover", alt: "The room as booked", full: true,
|
|
779
|
+
maskImage: "linear-gradient(to top, rgba(0,0,0,0.75) 0%, transparent 55%)" }),
|
|
780
|
+
Card({ borderRadius: 24, padding: 20, margin: 16, child:
|
|
781
|
+
Text({ text: "From $128 a night", size: 18, weight: 600, color: colors.text }) }),
|
|
782
|
+
]})
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- The sheet **overlaps** the photograph's bottom edge; a card placed under a
|
|
786
|
+
photo is a caption, and a caption says the photo is the subject of the screen.
|
|
787
|
+
- Words over a photograph need a scrim, and the scrim belongs on the image
|
|
788
|
+
(`maskImage`) or as its own plate (`colors.overlay`) — light text over one
|
|
789
|
+
unscrimmed photo is the finding rule 11 already names.
|
|
790
|
+
- **Color harvest**: the page tint comes from the photograph's dominant hue, so
|
|
791
|
+
the image and the interface are the same color event instead of two arguments.
|
|
792
|
+
It is a token decision, so it goes into `src/database/themes.js` as a named
|
|
793
|
+
tint, not an inline hex.
|
|
794
|
+
- **Glass is for the layer that floats over scrolling content** — the nav over a
|
|
795
|
+
feed, the sheet over a photo. `colors.glass` with `colors.glassBorder` and
|
|
796
|
+
`backdropFilter` on a solid page is a texture, not a hierarchy (rules 10, 19):
|
|
797
|
+
translucency only means anything when there is something moving behind it.
|
|
798
|
+
Translucency only means something when there is something moving behind it: a
|
|
799
|
+
glass panel over a photograph reads as a layer, the same panel over a gradient
|
|
800
|
+
reads as a smudge.
|
|
801
|
+
|
|
802
|
+
## 32 The shape of each control
|
|
803
|
+
|
|
804
|
+
| Job | The shape | The call |
|
|
805
|
+
| --- | --- | --- |
|
|
806
|
+
| choose 1 of 3-6 | pill row, one filled, rest hollow | `Chip({ variant: "filled" \| "outlined" })` in a `Row({ gap: 8, wrap: true })` |
|
|
807
|
+
| switch 2 views of one object | segmented pill with a sliding plate | `Tabs({ variant: "pills", tabs, activeIndex, activeTextColor, buttonColor })` |
|
|
808
|
+
| choose 1 of N with a preview | selection **tile**, not a radio list | `ListTile({ selected, selectedBgColor })`, or a 2-up `Row` of `Card`s |
|
|
809
|
+
| pick a color/size | dot row, selected gets a ring | `Avatar({ shape: "circle", size: 28 })` row, ring via `borderColor` + `borderWidth` |
|
|
810
|
+
| count then buy | stepper merged into the CTA | one `Row` of `Button`s inside the add-to-cart bar, not two actions |
|
|
811
|
+
| navigate the app | bottom bar, active = accent + label | `BottomNavigation({ items, currentIndex, selectedColor, unselectedColor, showLabels: true })` |
|
|
812
|
+
| a badge on an icon | dot, never a word | `Badge({ value, size, offset, borderColor: colors.background })` |
|
|
813
|
+
|
|
814
|
+
- Selected state is **inverted**, not tinted: on dark, the selected chip goes
|
|
815
|
+
white-with-black-text among hollow dark chips. An inversion is seen before it
|
|
816
|
+
is read, and it survives a color-blind user; a slightly lighter grey is the
|
|
817
|
+
same hue at a value nobody can compare across a row.
|
|
818
|
+
- Disabled is a desaturated gray token, not the same color at 40% opacity,
|
|
819
|
+
because opacity fades the text into whatever is behind it and changes its
|
|
820
|
+
contrast per surface. `colors.textDisabled` is already a different hue rather
|
|
821
|
+
than a fade for exactly that reason (rule 03).
|
|
822
|
+
- Quantity, variant and price live in the same row as the action that spends them
|
|
823
|
+
(rule 15, Fitts). The same rule written as a label is cheaper still:
|
|
824
|
+
`Button({ text: "Add to cart · $34.21" })` — the decision and its cost are one
|
|
825
|
+
read instead of two.
|
|
826
|
+
|
|
827
|
+
## 33 Data: the dial, the bento, the axis-less chart
|
|
828
|
+
|
|
829
|
+
- **Dial as hero.** One KPI gets a ring, not a chart:
|
|
830
|
+
`CircularBar({ value, max, size: 180, strokeWidth: 14, lineCap: "round", label, subtitle, valueSize: 32, showValue: true })`.
|
|
831
|
+
The number sits inside the ring, so the gauge and the hero are one object and
|
|
832
|
+
one read. One per screen; a second ring is a decoration.
|
|
833
|
+
- **Bento.** Two columns, mixed heights, uniform radius, one tile double-size and
|
|
834
|
+
that tile is the hero: `GridView({ columns: 2, spacing: 12 })` with tiles of
|
|
835
|
+
different `height`, or a `Row` + `Column` pair when the spans get awkward.
|
|
836
|
+
Gaps stay on the 4-scale (rule 03); dense is not the same as tight.
|
|
837
|
+
- **Zero-label charts.** A sparkline with one number beats an axis:
|
|
838
|
+
`Chart({ type: "area", data, smooth: true, areaGradient: true, showGrid: false, showLabels: false, lineColor: colors.primary })`.
|
|
839
|
+
Gridlines ask the reader to measure; the delta text tells them the answer, and
|
|
840
|
+
the shape of the line carries the trend.
|
|
841
|
+
- A chart's series color is the accent; its second series is a neutral, not a
|
|
842
|
+
second hue (rule 09). `Chart({ barColor })` and `lineColor` take tokens.
|
|
843
|
+
- Skeletons, not spinners, and the shape of the skeleton is the shape of the
|
|
844
|
+
thing: `Skeleton({ count: 3, height: 72, borderRadius: 16, gap: 12 })` under a
|
|
845
|
+
list of 72-tall rows (rule 12).
|
|
846
|
+
|
|
847
|
+
## 34 Genre recipes
|
|
848
|
+
|
|
849
|
+
The moves each genre is made of. Each is a starting composition, not a
|
|
850
|
+
permission slip: rules 01-24 still decide what ships.
|
|
851
|
+
|
|
852
|
+
| Genre | The five moves |
|
|
853
|
+
| --- | --- |
|
|
854
|
+
| Onboarding | full-bleed image or one-hue field · one line at display size · dots or a ghost skip · one pill CTA at the bottom · nothing tappable else |
|
|
855
|
+
| Dashboard | greeting + avatar · one hero number or card · 2-up tinted stat tiles · one chart, no axes · `BottomNavigation` |
|
|
856
|
+
| Commerce list | pill search · horizontal `Chip` rail · 2-col `GridView` of image-first cards · price bold, strike-through muted · a circular add on the card |
|
|
857
|
+
| Product detail | photo top half · sheet overlapping it with 24 top radius · dot swatch + size pills · sticky bar with price inside the CTA |
|
|
858
|
+
| Wallet | dark ramp · card plate as the hero · signed rows, `+` in `successStrong`, `-` in `dangerStrong` · tabular amounts right-aligned |
|
|
859
|
+
| Media player | artwork blurred as the page tint · square art · thin `ProgressBar` · circular transport row · no other chrome |
|
|
860
|
+
| Social feed | story rail of `Avatar`s · full-bleed post cards · actions under the image, centered as a group · one accent for the like state only |
|
|
861
|
+
| Travel | destination photo with scrim · date/guest pill row · horizontal card carousel with a peek of the next · price chip on the image |
|
|
862
|
+
| Health | one or three rings · a 7-bar week strip · mint or lime accent on dark · the number inside the ring |
|
|
863
|
+
| Settings | grouped rows, `ListTile` · leading icon in a tinted plate · trailing chevron or `Switch` · one section cap per group, no hairline between rows |
|
|
864
|
+
| Web landing | transparent `AppBar` · display headline with one word in the accent · pill CTA + ghost pair · a three-number stat strip · card row below |
|
|
865
|
+
|
|
866
|
+
## 35 Deriving a palette
|
|
867
|
+
|
|
868
|
+
A theme is five roles, not five colors:
|
|
869
|
+
|
|
870
|
+
```
|
|
871
|
+
anchor the darkest surface, never #000
|
|
872
|
+
primary the one hue the screen spends
|
|
873
|
+
accent a warm counter-hue, used at most once per screen
|
|
874
|
+
wash the tinted paper the page sits on
|
|
875
|
+
ink white or near-white, and its opacity tiers
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
- **Ramp, not swatch.** A family is 5-7 stops (base → tint → accent → shadow).
|
|
879
|
+
Build it as a ramp so a card, a tile and a plate can be three steps of one hue
|
|
880
|
+
instead of three hues — which is the only way 27's surface discipline is
|
|
881
|
+
possible without breaking 25's one-accent law.
|
|
882
|
+
- **The light/dark pair rule**: for the same hue in both palettes, lift the
|
|
883
|
+
lightness and drop the saturation going dark. That is why this framework's dark
|
|
884
|
+
`dangerStrong` is `#fca5a5` where the light one is `#b91c1c` — the readable red
|
|
885
|
+
is the one that measures 4.5:1 *on that page*, which is rule 09's whole point.
|
|
886
|
+
- **Shadows are tinted, never grey.** A real shadow is the page's own hue darkened
|
|
887
|
+
and made translucent, so the ambient shadow is a large blur at low alpha in that
|
|
888
|
+
hue (`0 5px 50px rgba(0,67,101,0.08)`). In FletBox that is a
|
|
889
|
+
`shadow: "0 20px 50px rgba(…)"` string on a `Container`/`Card` —
|
|
890
|
+
`colors.shadow` is the neutral default and is the flat version of the same idea
|
|
891
|
+
(rule 10).
|
|
892
|
+
- Add the tokens to `src/database/themes.js` in **both** palettes. The gate that
|
|
893
|
+
checks the two palettes expose the same token names exists because a light-only
|
|
894
|
+
tint once deleted `colors.errorBg` out from under a running app; a token added
|
|
895
|
+
to one theme is a runtime `undefined` in the other.
|
|
896
|
+
- Contrast is still the veto: `npm run ui -- --tokens` measures every pair. A
|
|
897
|
+
palette that looks right and measures 3.2:1 is a palette with a bug in it.
|
|
898
|
+
|
|
899
|
+
## 36 The dated visual tells
|
|
900
|
+
|
|
901
|
+
Rule 19 bans the template tells of copy and hierarchy. These are the *visual*
|
|
902
|
+
ones, and each is named with the mechanism that dates it, so an edge case can be
|
|
903
|
+
judged rather than obeyed.
|
|
904
|
+
|
|
905
|
+
- **Violet mesh gradient behind the phone**, and the blurred 3D blob or sphere
|
|
906
|
+
floating in front of it. A decoration that carries no information reads as
|
|
907
|
+
stock the moment the eye finishes with it. There is no gradient page surface in
|
|
908
|
+
this framework anyway — `gradient` exists on `Button` and `AppBar`, which is the
|
|
909
|
+
system saying it is one element's surface (rule 19).
|
|
910
|
+
- **Neumorphism**: extruded dials and pressed-in tiles. Its shadows are the only
|
|
911
|
+
depth cue, so the moment the surface changes the affordance disappears —
|
|
912
|
+
a control whose visibility depends on a light source is not a control.
|
|
913
|
+
- **Glass over a solid page**, or glass on every card. See 31: translucency needs
|
|
914
|
+
something moving behind it, and a page of glass has no layer left to signal.
|
|
915
|
+
- **Gradient-filled headline text.** `Text` has no gradient, and color running
|
|
916
|
+
across a word destroys the letter edges the reader is using. Emphasis comes
|
|
917
|
+
from weight and ink (rule 17).
|
|
918
|
+
- **Hairline border + 6-8px radius + drop shadow on every card**, and a
|
|
919
|
+
`Divider` under every row: the three-piece card that separates everything and
|
|
920
|
+
therefore organizes nothing (rules 10, 19).
|
|
921
|
+
- **Skeuomorphic props** — a rendered credit card as the hero of a wallet, a
|
|
922
|
+
photoreal watch bezel, isometric people, emoji as feature icons. They depict an
|
|
923
|
+
object instead of the user's own data, which is the one thing the screen has
|
|
924
|
+
that no stock art can supply.
|
|
925
|
+
- **Default-blue SaaS**: a blue→indigo button, a grey page, white cards, no
|
|
926
|
+
accent discipline. It is not a color problem, it is a missing archetype
|
|
927
|
+
decision (26) — the palette was never tied to anything this product does.
|
|
928
|
+
- **Three CTAs in a hero row**, and the floating-phone-on-a-plinth presentation:
|
|
929
|
+
both are portfolio staging, not interface.
|
|
930
|
+
|
|
931
|
+
Deleting a tell is not a design: each one still owes its job to a rule —
|
|
932
|
+
atmosphere to 27, hierarchy to 29, separation to 30, depth to 31, emphasis to 28.
|
|
933
|
+
|
|
934
|
+
## 37 Choosing, in five questions
|
|
935
|
+
|
|
936
|
+
Answer them before the first widget, in the commit message or the memory note.
|
|
937
|
+
If two answers are "I don't mind", the screen is not being designed, it is being
|
|
938
|
+
filled.
|
|
939
|
+
|
|
940
|
+
1. Which archetype (26), and why does this product's genre point there?
|
|
941
|
+
2. Which surface family, and which three steps of its ramp does this screen use (27)?
|
|
942
|
+
3. Where is the accent spent — name the three placements (28)?
|
|
943
|
+
4. What is the hero, and is it 3-5× everything else (25, 29)?
|
|
944
|
+
5. Which tell from 36 is the one this screen is most likely to fall into, given
|
|
945
|
+
its genre and its imagery?
|
|
946
|
+
|
|
947
|
+
Then run the loop in 24. Part II never overrides a rule from Part I: the atlas is
|
|
948
|
+
the menu, the rules are the contract, and the checker has the last word on
|
|
949
|
+
legality and the user has it on whether the screen is understood (23).
|