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.
Files changed (132) hide show
  1. package/bin/commands/checkUi.js +327 -46
  2. package/bin/commands/create.js +48 -19
  3. package/bin/commands/createScreen.js +1 -1
  4. package/bin/commands/runBundle.js +1 -1
  5. package/bin/utils/colors.js +0 -8
  6. package/bin/utils/helpers.js +0 -5
  7. package/bin/utils/images/jpeg.js +2 -3
  8. package/bin/utils/skills.js +59 -0
  9. package/bin/utils/templates.js +196 -575
  10. package/docs/README.md +3 -1
  11. package/docs/cli/README.md +18 -1
  12. package/docs/guides/README.md +6 -1
  13. package/docs/guides/accessibility.md +282 -0
  14. package/docs/guides/clean-ui.md +84 -21
  15. package/docs/guides/minification-and-protection.md +1 -1
  16. package/docs/guides/mobile-and-platforms.md +51 -0
  17. package/docs/guides/utilities.md +116 -4
  18. package/docs/guides/widget-structure.md +30 -0
  19. package/docs/tools/README.md +1 -1
  20. package/docs/tools/animation.md +134 -11
  21. package/docs/tools/device.md +75 -3
  22. package/docs/tools/text-and-time.md +116 -6
  23. package/docs/widget/Accordion.md +2 -0
  24. package/docs/widget/AnimatedBox.md +46 -20
  25. package/docs/widget/AnimatedText.md +13 -7
  26. package/docs/widget/Badge.md +1 -0
  27. package/docs/widget/COMMON_PROPS.md +26 -3
  28. package/docs/widget/Chart.md +1 -0
  29. package/docs/widget/Checkbox.md +7 -1
  30. package/docs/widget/CircularChart.md +2 -0
  31. package/docs/widget/Drawer.md +1 -0
  32. package/docs/widget/GridView.md +4 -1
  33. package/docs/widget/Input.md +2 -0
  34. package/docs/widget/InstallButton.md +1 -0
  35. package/docs/widget/Markdown.md +4 -0
  36. package/docs/widget/Rating.md +2 -0
  37. package/docs/widget/Slider.md +2 -0
  38. package/docs/widget/Stack.md +6 -5
  39. package/docs/widget/Stepper.md +2 -0
  40. package/docs/widget/Tooltip.md +3 -1
  41. package/package.json +4 -2
  42. package/skills/fletbox-design/SKILL.md +949 -0
  43. package/skills/fletbox-memory/SKILL.md +134 -0
  44. package/skills/fletbox-motion/SKILL.md +266 -0
  45. package/skills/fletbox-rules/SKILL.md +293 -0
  46. package/src/animations/AnimatedBox.js +321 -226
  47. package/src/animations/MatrixRain.js +7 -1
  48. package/src/animations/ParallaxBox.js +13 -10
  49. package/src/animations/animate.js +269 -106
  50. package/src/animations/color.js +91 -0
  51. package/src/animations/easings.js +275 -0
  52. package/src/animations/index.js +21 -0
  53. package/src/animations/motion.js +204 -0
  54. package/src/animations/transitions.js +327 -0
  55. package/src/core/runApp.js +0 -6
  56. package/src/helpers/boxEdges.js +69 -0
  57. package/src/helpers/html.js +39 -0
  58. package/src/index.d.ts +377 -25
  59. package/src/index.js +77 -0
  60. package/src/navigations/AdaptiveScaffold.js +6 -2
  61. package/src/navigations/AppBar.js +16 -7
  62. package/src/navigations/BottomNavigation.js +24 -1
  63. package/src/navigations/CollapsibleSideBar.js +0 -1
  64. package/src/navigations/Drawer.js +32 -1
  65. package/src/navigations/DrawerItem.js +1 -1
  66. package/src/navigations/Router.js +0 -1
  67. package/src/navigations/Scaffold.js +9 -3
  68. package/src/navigations/Tabs.js +104 -31
  69. package/src/services/RamStore.js +1 -2
  70. package/src/tools/breakpoints.js +113 -0
  71. package/src/tools/createList.js +0 -2
  72. package/src/tools/fonts.js +99 -0
  73. package/src/tools/index.js +74 -1
  74. package/src/tools/margin.js +5 -63
  75. package/src/tools/padding.js +5 -63
  76. package/src/tools/print.js +0 -8
  77. package/src/tools/scale.js +9 -2
  78. package/src/tools/time.js +56 -15
  79. package/src/tools/useState.js +1 -54
  80. package/src/utils/fieldFont.js +77 -0
  81. package/src/utils/focusRing.js +73 -0
  82. package/src/utils/focusScope.js +113 -0
  83. package/src/utils/markdownParser.js +7 -9
  84. package/src/utils/readableInk.js +7 -1
  85. package/src/utils/stackLayout.js +55 -0
  86. package/src/utils/styleInjector.js +25 -0
  87. package/src/utils/syntaxHighlight.js +1 -17
  88. package/src/utils/themes.js +31 -0
  89. package/src/utils/visualEffects.js +49 -47
  90. package/src/widget-factory/createWidget.js +16 -3
  91. package/src/widget-factory/effects.js +76 -3
  92. package/src/widget-factory/reactiveValue.js +0 -7
  93. package/src/widget-factory/tools.js +26 -143
  94. package/src/widget-factory/translateProps.js +5 -0
  95. package/src/widget-factory/widgetFactory.js +1 -1
  96. package/src/widgets/Accordion.js +70 -12
  97. package/src/widgets/AdMob.js +1 -1
  98. package/src/widgets/AdSense.js +1 -1
  99. package/src/widgets/AlertDialog.js +11 -9
  100. package/src/widgets/Avatar.js +10 -1
  101. package/src/widgets/Badge.js +7 -1
  102. package/src/widgets/BottomSheet.js +39 -5
  103. package/src/widgets/Chart.js +41 -9
  104. package/src/widgets/Checkbox.js +59 -9
  105. package/src/widgets/Chip.js +13 -1
  106. package/src/widgets/CircularBar.js +17 -5
  107. package/src/widgets/CircularChart.js +24 -2
  108. package/src/widgets/CodeViewer.js +5 -3
  109. package/src/widgets/Dropdown.js +223 -20
  110. package/src/widgets/GridView.js +17 -1
  111. package/src/widgets/Icon.js +21 -0
  112. package/src/widgets/Input.js +75 -44
  113. package/src/widgets/InstallButton.js +8 -2
  114. package/src/widgets/ListTile.js +6 -5
  115. package/src/widgets/ListView.js +21 -7
  116. package/src/widgets/Markdown.js +25 -44
  117. package/src/widgets/Modal.js +43 -6
  118. package/src/widgets/Pagination.js +35 -3
  119. package/src/widgets/ProgressBar.js +18 -0
  120. package/src/widgets/Radio.js +7 -0
  121. package/src/widgets/Rating.js +104 -16
  122. package/src/widgets/Skeleton.js +9 -5
  123. package/src/widgets/Slider.js +87 -7
  124. package/src/widgets/SnackBar.js +30 -1
  125. package/src/widgets/Stack.js +25 -13
  126. package/src/widgets/Stepper.js +112 -64
  127. package/src/widgets/Switch.js +10 -1
  128. package/src/widgets/Tooltip.js +93 -10
  129. package/src/widgets/TreeView.js +5 -5
  130. package/src/create-index.sh +0 -237
  131. package/src/widget-factory/pixTool.js +0 -40
  132. package/src/widget-factory/stringTool.js +0 -26
package/docs/README.md CHANGED
@@ -16,7 +16,8 @@ This documentation is one continuous book. Start at **Chapter 0** and follow the
16
16
  | 9 | [State, Router & Services](guides/state.md) | Shared state, navigation routes, storage, and HTTP | Intermediate |
17
17
  | 10 | [Tools & Utilities](tools/README.md) | The helper functions that ship with the framework | Intermediate |
18
18
  | 11 | [The CLI](cli/README.md) | Create, run, and build projects with `flet-box` | Intermediate |
19
- | 12 | [Clean UI](guides/clean-ui.md) | The design system every app ships with and the gate that enforces it | Intermediate |
19
+ | 12 | [Clean UI](guides/clean-ui.md) | The four skills every app ships with, the design system they hold, and the gate that enforces it | Intermediate |
20
+ | 13 | [Accessibility](guides/accessibility.md) | The focus ring, the keyboard pass over every press, the roles the widgets declare, and the overlays that hold the keyboard | Intermediate |
20
21
  | A | [Appendices](guides/minification-and-protection.md) | Minification, mobile, compatibility, embedding, online updates (OTA) | Advanced |
21
22
 
22
23
  > **Levels.** *Beginner:* start here and read in order. *Intermediate:* continue in order — you are comfortable with widgets. *Advanced:* for when you ship, skip on a first read.
@@ -54,6 +55,7 @@ Front cover (this page)
54
55
  → Utilities guide · guides/utilities.md
55
56
  → The CLI · cli/README.md
56
57
  → Clean UI · guides/clean-ui.md
58
+ → Accessibility · guides/accessibility.md
57
59
  → Appendices (minification → mobile → compatibility → embedding → online updates)
58
60
  → The end of the book
59
61
  ```
@@ -275,6 +275,7 @@ A typical generated project looks like this:
275
275
  ```text
276
276
  my-app/
277
277
  ├── index.html
278
+ ├── jsconfig.json
278
279
  ├── package.json
279
280
  ├── capacitor.config.json
280
281
  ├── manifest.json
@@ -301,7 +302,23 @@ my-app/
301
302
 
302
303
  The exact folders depend on the selected template.
303
304
 
304
- ## A complete beginner workflow
305
+ ### Autocomplete without TypeScript
306
+
307
+ `jsconfig.json` is not a build step: it tells the editor's own JavaScript
308
+ language service to read `flet-box`'s bundled `src/index.d.ts`. With `checkJs`
309
+ on, VS Code, Cursor and any editor that understands `jsconfig.json` autocomplete
310
+ every widget, prop, colour token and helper — and underline a prop that does not
311
+ exist, which is the same error `npm run check` reports from the command line.
312
+
313
+ Nothing is installed and nothing is compiled. The framework keeps the promise
314
+ honest with `scripts/check-app-types.mjs`, which creates all five templates and
315
+ requires zero diagnostics over their `src/` — a starter that produces red
316
+ squiggles fails `npm test`.
317
+
318
+ ```bash
319
+ npm run check # the gate: real API, real props, real imports
320
+ ```
321
+
305
322
 
306
323
  ```bash
307
324
  # 1. Create the project
@@ -10,7 +10,12 @@ This directory is the **conceptual index** of FletBox: how to build apps, set up
10
10
  - **[Frontend services](frontend-services.md)** — Chapter 9 · the practical walkthrough of the three storage layers (`RamStore`, `Session`, `Storage`) and the HTTP client.
11
11
  - **[FletBox utilities](utilities.md)** — Chapter 10 · layout/theme/format helpers and validator tools.
12
12
  - **[The CLI](../cli/README.md)** — Chapter 11 · create, run, and build projects with `flet-box`.
13
- - **[Clean UI](clean-ui.md)** — Chapter 12 · the token scale, the 21 rules, and the `flet-box check-ui` gate that enforces them.
13
+ - **[Clean UI](clean-ui.md)** — Chapter 12 · the four skills `flet-box create`
14
+ installs, the token scale, the numbered rules (00–24 enforced, 25–37 the
15
+ visual atlas), and the `flet-box check-ui` gate that enforces them.
16
+ - **[Accessibility](accessibility.md)** — Chapter 13 · the focus ring, the
17
+ keyboard pass over every press, the roles and states the widgets declare, and
18
+ the overlays that hold the keyboard.
14
19
  - **[Widget structure](widget-structure.md)** — the canonical, modular shape every widget follows (and how new ones are added).
15
20
 
16
21
  ## Appendices (Advanced — for shipping)
@@ -0,0 +1,282 @@
1
+ # Accessibility: the engine's half and your screen's half
2
+
3
+ A FletBox control is a `div` that the framework teaches to behave like a
4
+ `button`. That is what makes the syntax pleasant — `Container({ onPress })`
5
+ instead of a stylesheet and three event listeners — and it is also where
6
+ accessibility is won or lost. Three things have to be true for someone with no
7
+ pointer: the control must be **reachable** (Tab), **operable** (Enter and
8
+ Space), **announced** (a role and a name), and the person must be able to see
9
+ **where they are** (a focus ring). An overlay owes a fourth: hold the keyboard
10
+ while it is open, and give it back when it closes.
11
+
12
+ The engine already does all four. This chapter is about the half it cannot do
13
+ for you, and about the two ways an app destroys the half it was given.
14
+
15
+ ## 1. The focus ring is a stylesheet, not a paint
16
+
17
+ A focus ring is not a decoration — it is a *state*, and a state can only live in
18
+ a stylesheet. `src/utils/focusRing.js` appends one `<style id="fletbox-focus-ring">`
19
+ to `<head>` the first time any widget is created, and never appends a second:
20
+
21
+ ```css
22
+ :where(a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])):focus-visible {
23
+ outline: 2px solid var(--color-focusRing, #1d4ed8);
24
+ outline-offset: 2px;
25
+ }
26
+ ```
27
+
28
+ Three deliberate choices, each worth knowing when you want something different:
29
+
30
+ - **`:focus-visible`, not `:focus`.** A click with a mouse does not ring; the
31
+ keyboard does. That is the browser's own distinction, not the framework's.
32
+ - **`:where(…)` keeps specificity at zero.** One class of yours overrides it,
33
+ and no `!important` is needed anywhere.
34
+ - **The color is a token.** `--color-focusRing` is written by `applyPalette` for
35
+ every theme, so the ring moves with the theme instead of being a literal that
36
+ only suits one background. The light palette's `#1d4ed8` is 6.4:1 on the page,
37
+ the dark one's `#93c5fd` is 11:1, and the literal after the comma in the rule
38
+ is the boot value a page shows before the first `setTheme`.
39
+
40
+ The widths are pixels on purpose, next to `SHADOW_LEVELS` and the hairline
41
+ `borderWidth`: a 1px ring is invisible and a 6px one is a blob, whatever the
42
+ reader's font size is.
43
+
44
+ ### The one thing an app must never do
45
+
46
+ ```javascript
47
+ // Never. An inline declaration outranks every stylesheet rule, including the
48
+ // engine's, so this is what "the focus ring does not work" looks like in code.
49
+ element.style.outline = "none";
50
+
51
+ // Never, for the same reason, anywhere in a global stylesheet:
52
+ // *:focus { outline: none; }
53
+ ```
54
+
55
+ If you do not like the ring's colour, change the token — or override the rule
56
+ with one class of your own, which `:where()` makes possible:
57
+
58
+ ```javascript
59
+ import { registerPalette } from "flet-box";
60
+
61
+ // The token is part of the palette, so both themes move it and nothing is
62
+ // overridden behind the framework's back. A registered palette repaints only
63
+ // when it is the one on screen.
64
+ registerPalette("light", { focusRing: "#0f766e" });
65
+ registerPalette("dark", { focusRing: "#5eead4" });
66
+ ```
67
+
68
+ ```css
69
+ /* or, for one component only: a class beats :where(...) whatever the order */
70
+ .chart:focus-visible { outline-color: var(--color-primary); }
71
+ ```
72
+
73
+ ### A field rings as a whole control
74
+
75
+ `Input` is a wrapper with a border, an icon and sometimes a reveal button, and
76
+ the native `<input>` inside it is only part of the control. A ring around the
77
+ text box alone draws a box *inside* the thing it is supposed to mark. So the
78
+ rule has a second pair:
79
+
80
+ ```css
81
+ :where([data-fb-field]):has(> input:focus-visible) {
82
+ outline: 2px solid var(--color-focusRing, #1d4ed8);
83
+ outline-offset: 2px;
84
+ }
85
+ :where([data-fb-field]) > input:focus-visible {
86
+ outline: none;
87
+ }
88
+ ```
89
+
90
+ The `outline: none` there is a *rule*, at zero specificity, which any app can
91
+ still override — that is the whole difference between silencing the inner box
92
+ and killing the ring. The marker (`data-fb-field`) is written by `Input`; if you
93
+ build your own composite field (a text box with a button inside), put the same
94
+ marker on the wrapper and the ring lands where the control is.
95
+
96
+ And keep the two jobs apart: **the border is validation, the ring is focus.**
97
+ A field that paints focus by recolouring its rim, or by adding a `box-shadow`,
98
+ puts two indicators where one belongs and loses the validation state the moment
99
+ someone types.
100
+
101
+ ## 2. A press is a press, however it arrives
102
+
103
+ Give any widget a click handler as a prop and the engine runs a keyboard pass
104
+ over it — `src/widget-factory/effects.js`:
105
+
106
+ - it sets `role="button"` **if you have not set a role**;
107
+ - it sets `tabIndex = 0` **if you have not set one** (an explicit `-1` is your
108
+ decision to keep the element off the Tab path, and it is respected);
109
+ - Enter and Space fire the same handler a pointer fires, with Space's default
110
+ (scrolling the page) cancelled and a held key counted as one press;
111
+ - a nested control keeps its own keys — Enter on a `Button` inside a pressable
112
+ `Card` presses the button and the card, not twice the card;
113
+ - native `button`/`a[href]` are left alone, and disabled controls are not made
114
+ Tab stops.
115
+
116
+ ```javascript
117
+ Container({ onPress: openDetails, children: [Text({ text: "Details" })] });
118
+ // role="button", tabIndex=0, Enter and Space → openDetails
119
+ ```
120
+
121
+ The one way an app defeats this is to attach the handler to the element after
122
+ the widget was built. The pass runs during construction; a handler assigned
123
+ afterwards leaves a control with a click, no role, no Tab stop and no keyboard:
124
+
125
+ ```javascript
126
+ const card = Container({ children: [Text({ text: "Details" })] });
127
+ card.onclick = openDetails; // mouse-only: the engine already ran
128
+ ```
129
+
130
+ Express the control with a prop instead — `onPress` (or `onClick`) on any
131
+ widget — and the engine sees it.
132
+
133
+ ### An icon is decoration until it is a control
134
+
135
+ `Icon` has two lives, and it picks one by itself:
136
+
137
+ - `Icon({ name: "search" })` — no name for a reader, so it is marked
138
+ `aria-hidden="true"` and kept off the Tab path. Correct inside a `Button`
139
+ that already has text.
140
+ - `Icon({ name: "search", onPress: focusSearch, "aria-label": "Search" })` — a
141
+ named, pressable icon keeps `role="button"` and is read as "Search, button".
142
+
143
+ An icon-only control with no `aria-label` is read as "button", which is the
144
+ worst of both worlds: reachable, operable, and anonymous. The places where the
145
+ framework wires a glyph up for you already name it — the `AppBar` back arrow,
146
+ `Input`'s `onIconPress` and reveal button, and the menu icon a `Scaffold`
147
+ connects to its drawer.
148
+
149
+ ## 3. Names, roles and states the widgets already declare
150
+
151
+ You can pass `role` and `tabIndex` as ordinary props, and any `aria-*`
152
+ attribute reaches the element verbatim (`"aria-label": "…"`). Everything below
153
+ is already in place; the table exists so you know what not to duplicate.
154
+
155
+ | Widget | Role | States it keeps in sync |
156
+ | --- | --- | --- |
157
+ | `Button`, `Container` with `onPress` | `button` | — (engine pass) |
158
+ | `Switch` | `switch` | `aria-checked` |
159
+ | `Checkbox` | `checkbox` | `aria-checked` |
160
+ | `Radio` / `Rating` | `radio` / `radiogroup` | `aria-checked` |
161
+ | `Slider` | `slider` | `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-orientation` |
162
+ | `ProgressBar`, `CircularBar` | `progressbar` | `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-busy` while indeterminate |
163
+ | `Dropdown` | `combobox` | `aria-expanded`, `aria-controls`, `aria-activedescendant`, `aria-invalid`, `aria-labelledby` |
164
+ | `Dropdown` list | `listbox` / `option` | `aria-selected` |
165
+ | `Tabs` | `tablist` / `tab` / `tabpanel` | `aria-selected`, roving `tabIndex`, `aria-controls` |
166
+ | `Accordion` | `button` / `region` | `aria-expanded`, `aria-controls` |
167
+ | `Pagination` | `navigation` | `aria-current="page"` |
168
+ | `Stepper` | — | `aria-current="step"` |
169
+ | `BottomNavigation` | — | `aria-current="page"` |
170
+ | `Modal`, `BottomSheet`, `Drawer` | `dialog` | `aria-modal="true"` |
171
+ | `SnackBar` | live region | `aria-live="polite"`, `"assertive"` for error and warning |
172
+ | `Tooltip` | `tooltip` | `aria-describedby` on the trigger while shown |
173
+ | `Maps` | `application` | — |
174
+ | `CircularChart` | `img` | give it an `aria-label`; it is a picture of data |
175
+
176
+ Composite widgets answer the keyboard the way their ARIA pattern says: `Tabs`,
177
+ `Dropdown`, `Rating` and `Slider` move with the arrow keys, take `Home` and
178
+ `End`, and keep a single Tab stop per group. The Dropdown's options are
179
+ deliberately *not* Tab stops — the selector holds focus, the arrows move the
180
+ highlight and `aria-activedescendant` names the row.
181
+
182
+ What is left for you is **naming**: a visible `label` where the widget has one
183
+ (`Input`, `Dropdown`), `text` for a button (`"Save"` is a name; `"OK"` is not —
184
+ see the clean-UI rule about generic action labels), `alt` on images, and
185
+ `aria-label` for anything that has no visible words of its own.
186
+
187
+ ## 4. An invalid field says so in two places
188
+
189
+ A red rim is the visual half. The other half is the state and the reason, and
190
+ `Input` keeps them together: while a message is showing, the field carries
191
+ `aria-invalid="true"` and an `aria-describedby` pointing at the message, which
192
+ is a `polite` live region; when the value becomes valid, all three leave
193
+ together. A described-by pointing at a `display: none` node would be a control
194
+ claiming a description it does not have.
195
+
196
+ ```javascript
197
+ const email = Input({ label: "Email", validation: "email", required: true });
198
+
199
+ // submit
200
+ const { valid, message } = email.validate(); // paints, links and announces
201
+ ```
202
+
203
+ Use `validate()` (or `setValue()`, which validates) rather than colouring a rim
204
+ by hand: `showValidationMessage` decides whether the reason is on screen, and a
205
+ ring with no message tells a reader "wrong" and nothing else.
206
+
207
+ ## 5. Overlays hold the keyboard, then give it back
208
+
209
+ `Modal`, `BottomSheet` and `Drawer` share one scope (`src/utils/focusScope.js`),
210
+ and one implementation on purpose:
211
+
212
+ - focus moves into the panel when it opens — the first control, or the panel
213
+ itself (`tabIndex="-1"`) when it has none, so an alert with two lines of text
214
+ still takes the keyboard;
215
+ - Tab and Shift+Tab cycle **inside** the panel instead of walking out behind
216
+ the scrim into a page the person can no longer see;
217
+ - Escape closes it (each overlay already listens);
218
+ - closing hands focus back to whatever was focused before, and skips that if the
219
+ element has left the page (a route change replaced it).
220
+
221
+ The trap and the ring read the same list of reachable elements — `a[href]`,
222
+ `button`, `input`, `select`, `textarea`, `summary` and anything on the Tab path
223
+ — so a stop you can Tab to is always a stop that shows a ring.
224
+
225
+ For an app, the contract is simply: open and close these through their props and
226
+ refs, and do not build a full-screen scrim by hand with its own Escape listener.
227
+
228
+ ## 6. Verify it with the keyboard
229
+
230
+ Five minutes, no screen reader needed:
231
+
232
+ 1. Press Tab from the top of the screen. Every control gets a ring; nothing is
233
+ skipped and nothing rings twice.
234
+ 2. Press Enter and Space on each control. The same thing happens as with a click.
235
+ 3. Focus a field and type. The rim stays put until there is a verdict.
236
+ 4. Submit an invalid form. The message is under the field, the field says
237
+ `aria-invalid`, and the message disappears with the fix.
238
+ 5. Open a modal. Tab to the last control and press Tab once more: focus is back
239
+ at the first, not behind the scrim.
240
+ 6. Press Escape. Focus is back on whatever opened it.
241
+ 7. Repeat 1–6 with a screen reader (VoiceOver: `Cmd+F5`; TalkBack on Android) and
242
+ listen for names: "Search, button", "Email, invalid entry, Invalid email
243
+ address".
244
+
245
+ Then let the gates do what they do:
246
+
247
+ ```bash
248
+ npm run check # the API is real: widgets, props, imports
249
+ npm run ui # names, labels, tap targets, contrast:
250
+ # UNLABELED_FIELD, UNLABELED_TOGGLE, ICON_ONLY_CONTROL,
251
+ # IMAGE_NO_ALT, SMALL_TAP_TARGET, LOW_CONTRAST*, UNREADABLE
252
+ npm run dev # then Tab through it; the checker has no keyboard
253
+ ```
254
+
255
+ In the framework repository itself, `node scripts/check-a11y.mjs` runs inside
256
+ `npm test` and holds the invariants of this chapter: nothing writes an inline
257
+ `outline`; the ring rule is injected and carries the token; both palettes define
258
+ `focusRing`; the pressable path sets role, Tab stop and Enter/Space and releases
259
+ its listener; every overlay that reads Escape also traps focus; the trap and the
260
+ ring agree on what is reachable; and no handler is attached after the factory
261
+ returned. Every one of those rules has been proven to fail on the bug it
262
+ forbids. The runtime half lives in `/tests/a11y.html`.
263
+
264
+ ## See also
265
+
266
+ - [Clean UI](clean-ui.md) — the design rules this chapter's checklist comes from.
267
+ - [Themes](../tools/theme.md) — `registerPalette`, the call that moves the ring.
268
+ - [Input](../widget/Input.md) — the props used above.
269
+ - [Modal](../widget/Modal.md) · [BottomSheet](../widget/BottomSheet.md) ·
270
+ [Drawer](../widget/Drawer.md) — the three overlays and what they return.
271
+ - [Widget structure](widget-structure.md) — where a widget's defaults and
272
+ disposers live.
273
+
274
+ ---
275
+
276
+ ## Continue reading
277
+
278
+ - **Previous:** [Clean UI](clean-ui.md) — Chapter 12
279
+ - **Next:** [Minify and protect a FletBox build](minification-and-protection.md) — Appendix A
280
+ - **Index:** [Guides index](README.md) · [The FletBox Book](../README.md)
281
+
282
+ You are reading **Chapter 13 · Accessibility**.
@@ -5,8 +5,9 @@ defaults, and every later screen copies the screen before it. This page is the
5
5
  contract that makes the copy look like one product — the numbers, the tokens,
6
6
  and the two commands that prove a screen obeys them.
7
7
 
8
- It is the published version of the rule file that ships inside every generated
9
- app, so the app's agent and the framework's documentation cannot disagree.
8
+ It is the published version of the `fletbox-design` skill — one of four files in
9
+ the framework's `skills/` folder that `flet-box create` copies into every app — so
10
+ the app's agent and the framework's documentation cannot disagree.
10
11
 
11
12
  ## The two gates
12
13
 
@@ -26,12 +27,24 @@ one replaces opening the page.
26
27
 
27
28
  ## Where the rules live
28
29
 
30
+ A generated app is handed four skills — one file per subject, in a visible
31
+ `skills/` folder at its root, mirrored byte for byte to `.claude/skills/` and
32
+ `.qoder/skills/` because that is where those assistants load a skill from, and
33
+ pointed at from one `.cursor/rules/<name>.mdc` each:
34
+
35
+ | Skill | What it owns |
36
+ | --- | --- |
37
+ | `fletbox-rules` | The working contract: FletBox syntax only, never invent an API and the four ways to look one up, the rem rule, aliases, cleanup, the two gates, the memory duty. |
38
+ | `fletbox-design` | The numbered rules below — tokens, the scale, hierarchy, the four states, measured contrast, composition, the framework traps, the dated tells, density (00–24), then the visual atlas of looks that hold up, each reduced to calls this framework makes (25–37). |
39
+ | `fletbox-motion` | Whether a thing may move at all, and through which of `AnimatedBox`, `AnimatedText`, `ParallaxBox` or `animate`. |
40
+ | `fletbox-memory` | The `.fletbox/memory/` journal that hands progress from one AI to the next. |
41
+
29
42
  | Home | What it holds |
30
43
  | --- | --- |
31
- | `.claude/skills/fletbox-clean-ui/SKILL.md` | The 21 numbered rules below, written for the agent that builds the screen. Mirrored to `.qoder/skills/…` and `.cursor/rules/fletbox-clean-ui.mdc`. |
44
+ | `skills/fletbox-design/SKILL.md` (framework) → `skills/…`, `.claude/skills/…`, `.qoder/skills/…` (app) | The rules as the agent reads them, in 38 numbered sections across two parts. The app's copies are generated; edit the framework file and regenerate, never the mirror. |
32
45
  | `AGENTS.md` §4 of the app | The short version: one brand color per screen, no hex literals, spacing and radius only from the scale. |
33
46
  | `src/tools/scale.js` in the framework | The tables themselves — `SPACING_SCALE`, `RADIUS_SCALE`, `TYPE_SCALE`, `TAP_MINIMUM`, `PILL`, `LABEL_TEXT`. |
34
- | `bin/commands/checkUi.js` | The gate. It re-exports those same tables instead of keeping its own copy. |
47
+ | `bin/commands/checkUi.js` | The gate. It re-exports those same tables instead of keeping its own copy, and `scripts/check-skills.mjs` fails when a skill and the gate stop naming the same codes. |
35
48
 
36
49
  That last line matters more than it looks. A gate that carries its own numbers
37
50
  drifts from the widgets, and the app then gets told to be clean by values the
@@ -42,7 +55,7 @@ components it uses do not follow. The defaults and the checker read one table.
42
55
  | Dimension | Allowed values |
43
56
  | --- | --- |
44
57
  | Spacing | `0 · 4 · 8 · 12 · 16 · 18 · 24 · 32 · 40 · 48 · 64 · 96` |
45
- | Radius | `0 · 4 · 8 · 12 · 16 · 999` — `999` is a pill, `0` a flush edge; both are decisions |
58
+ | Radius | `0 · 4 · 8 · 12 · 16 · 20 · 24 · 999` — `999` is a pill, `0` a flush edge; both are decisions. 20 and 24 are on the scale because the nesting rule (`outer = inner + padding`) lands there and `Drawer`, `DrawerItem` and the `Tabs` indicator default there |
46
59
  | Type | `12 · 14 · 16 · 18 · 24 · 32 · 40 · 48` |
47
60
  | Tap minimum | `40` — Fitts: the smallest pressable box a finger reliably hits |
48
61
  | Label text | `14` — the reading size a control's own label may not fall under |
@@ -80,9 +93,17 @@ The framework keeps this honest with its own gate, `scripts/check-widget-scale.m
80
93
  (part of `npm test`): a length written straight to `el.style.*` that is neither
81
94
  `toREM(...)` nor measured geometry fails the build.
82
95
 
83
- ## The 21 rules
96
+ ## The rules
97
+
98
+ The skill numbers its rules 00–37 and splits them in two parts. **00–24 are the
99
+ enforceable contract**: each one is cited by at least one `check-ui` code, so
100
+ `npm run ui` can fail a screen for breaking it, and `scripts/check-skills.mjs`
101
+ fails if a code loses its rule. **25–37 are the visual atlas** — the archetypes,
102
+ the accent budget, the surfaces, the dated tells — judgement about what to
103
+ choose, which no static pass can prove. The atlas lives only in the skill; this
104
+ page indexes it by number and does not copy it, so the two cannot drift.
84
105
 
85
- When two of them conflict, take them in this order: **consistency · clarity ·
106
+ When two rules conflict, take them in this order: **consistency · clarity ·
86
107
  accessibility · usability · scalability · maintainability · conversion**.
87
108
 
88
109
  | # | Rule | What it forbids |
@@ -104,10 +125,37 @@ accessibility · usability · scalability · maintainability · conversion**.
104
125
  | 14 | Destructive actions designed last | A delete that is the closest, fastest, most colorful thing on the screen. |
105
126
  | 15 | Reduce interaction cost | Taps, scrolls and keystrokes that carry no information. |
106
127
  | 16 | Respect the platform | Fighting the browser's own conventions — back, scroll, text selection. |
107
- | 17 | The review | Shipping before reading your own diff as the user. |
108
- | 18 | Where these books are wrong | Following a guideline that measurement disagrees with. |
109
- | 19 | What no checker can see | Taste, hierarchy in the wild, whether the screen solves the problem. |
110
- | 20 | The loop | Fixing the checker instead of the code, or leaving a rule you had to break undocumented. |
128
+ | 17 | Composition starts as a sentence | A screen that begins as "a hero, then three cards, then a footer" instead of a decision the page carries. |
129
+ | 18 | The framework traps | `Container` assumed invisible, a `flex: 1` with no sibling, `alignItems: "center"` mistaken for centered text, absolute coordinates where a `Stack` belongs. |
130
+ | 19 | The dated tells | An eyebrow label, `01 · 02 · 03`, glass everywhere, resting shadows, a hairline `Divider` under every row, copy that says "seamless". |
131
+ | 20 | Density is one dial | Two rhythms in one screen — a gap, a padding and a body size chosen three different times. |
132
+ | 21 | The review | Shipping before reading your own diff as the user. |
133
+ | 22 | Where these books are wrong | Following a guideline that measurement disagrees with. |
134
+ | 23 | What no checker can see | Taste, hierarchy in the wild, whether the screen solves the problem. |
135
+ | 24 | The loop | Fixing the checker instead of the code, or leaving a rule you had to break undocumented. |
136
+
137
+ ### Part II — the visual atlas, 25–37
138
+
139
+ Read these after the table, never instead of it: every recipe still has to pass
140
+ `npm run check` and `npm run ui`. They are indexed here so a rule number in a
141
+ review comment points somewhere, and they are written out in full in
142
+ `skills/fletbox-design/SKILL.md`.
143
+
144
+ | # | Rule | What it decides |
145
+ | --- | --- | --- |
146
+ | 25 | Three laws | One accent, one surface family, one hero — across mobile, web, both themes. |
147
+ | 26 | Pick an archetype | Acid-on-black, tinted paper, photo-bleed, bento, editorial, single-hue field — name one before the first widget. |
148
+ | 27 | Surfaces are never the extremes | No pure black under pure white: the steps between them are where hierarchy lives. |
149
+ | 28 | The accent budget | How much saturated hue a screen may spend, and on what. |
150
+ | 29 | The number is the hero | The one element at 3–5× the rest, and why it is usually a number. |
151
+ | 30 | Radius, nesting and the pill | `outer = inner + padding`, and when a pill is a decision. |
152
+ | 31 | Photo, scrim, sheet | The sheet overlaps the photo, the scrim earns the text over it. |
153
+ | 32 | The shape of each control | Which control carries which affordance, from the widgets that exist. |
154
+ | 33 | Data widgets | The dial, the bento tile, the axis-less chart. |
155
+ | 34 | Genre recipes | What a finance, commerce, travel or wellness screen actually uses. |
156
+ | 35 | Deriving a palette | Building a hue family from one accent without leaving contrast behind. |
157
+ | 36 | The dated visual tells | What reads as 2021, and what to choose instead. |
158
+ | 37 | Choosing, in five questions | How to land on an archetype and a palette for a brief. |
111
159
 
112
160
  ## What `flet-box check-ui` proves
113
161
 
@@ -123,12 +171,12 @@ judging yours. Each finding carries a code, and the code maps to a rule above.
123
171
  | `OFF_SCALE_SPACING` | 07 | A padding, margin, gap or size off the spacing scale — including inside a `"12px 20px"` shorthand. |
124
172
  | `OFF_SCALE_RADIUS` | 03 | A corner radius that is not a scale step. |
125
173
  | `OFF_SCALE_TYPE` | 08 | A font size off the type scale. |
126
- | `SMALL_TAP_TARGET` | 11 | A pressable widget under the Fitts floor, including a bare `Icon` with `onPress`. |
174
+ | `SMALL_TAP_TARGET` | 03 | A pressable widget under the Fitts floor, including a bare `Icon` with `onPress`. The floor is a scale value, so rule 03 owns it. |
127
175
  | `LOW_CONTRAST` | 11 | Text measured below AA against the fill it actually sits on. |
128
176
  | `UNLABELED_FIELD` | 13 | An `Input` or `Dropdown` with neither a `label` nor a `placeholder`. |
129
- | `UNLABELED_TOGGLE` | 13 | A `Switch`, `Checkbox` or `Radio` with nothing to say what it toggles. |
177
+ | `UNLABELED_TOGGLE` | 11 | A `Switch`, `Checkbox` or `Radio` with nothing to say what it toggles. |
130
178
  | `DESTRUCTIVE_COLOR` | 14 | A delete/remove verb rendered in the brand fill instead of the danger one. |
131
- | `UNREADABLE` | — | A file the scanner could not parse, reported rather than skipped. |
179
+ | `UNREADABLE` | 00 | A file the scanner could not parse, reported rather than skipped — and nothing else in that file was judged. |
132
180
 
133
181
  ### Warnings (fail with `--strict`)
134
182
 
@@ -140,20 +188,20 @@ judging yours. Each finding carries a code, and the code maps to a rule above.
140
188
  | `HAND_MIXED_COLOR` | 03 | `colors.primary + "22"` — a one-off shade no other screen can reuse. |
141
189
  | `DEAD_ELEVATION` | 10 | A numeric `elevation` on a widget that never reads it: CSS drops the bare number and the box stays flat in silence. |
142
190
  | `IMAGE_NO_ALT` | 11 | An `Image` with no `alt` — `alt: ""` is a decision, a missing prop is an oversight. |
143
- | `ICON_ONLY_CONTROL` | 15 | A pressable glyph with no name; wrap it in `Tooltip({ text, child })`. |
191
+ | `ICON_ONLY_CONTROL` | 11 | A pressable glyph with no accessible name; give it `aria-label` (a `Tooltip` is a description, not a name). |
144
192
  | `CENTERED_BODY_TEXT` | 08 | A paragraph centered; a paragraph needs a left edge to find the next line. |
145
- | `GENERIC_ACTION_LABEL` | 06 | "OK", "Submit", "Click here" — a label that says nothing about what pressing it does. |
193
+ | `GENERIC_ACTION_LABEL` | 14 | "OK", "Submit", "Click here" — a label that says nothing about what pressing it does. |
146
194
  | `CONFIRM_ORDER` | 14 | A destructive button placed before Cancel in the same row. |
147
- | `UNCONSTRAINED_FORM_WIDTH` | 08 | A form block whose fields run to the full width of a desktop window. |
195
+ | `UNCONSTRAINED_FORM_WIDTH` | 13 | A form block whose fields run to the full width of a desktop window. |
148
196
 
149
197
  ### Informational
150
198
 
151
199
  | Code | Rule | What it says |
152
200
  | --- | --- | --- |
153
- | `SMALL_PRESET` | 11 | `size: "small"` on a control that is the screen's main action (~30px). |
201
+ | `SMALL_PRESET` | 03 | `size: "small"` on a control that is the screen's main action (~30px). |
154
202
  | `CROWDED_FORM` | 06 | Many fields in one file: group them, or split the task. |
155
203
  | `LIST_WITHOUT_STATE` | 12 | A list with no loading, empty or failed state in this file. |
156
- | `MULTIPLE_PRIMARY_ACTIONS` | 04 | Several filled buttons on one screen. |
204
+ | `MULTIPLE_PRIMARY_ACTIONS` | 04 | More than one filled button in one view; keep one and make the rest outlined or text. |
157
205
 
158
206
  ## Running the gate
159
207
 
@@ -186,11 +234,26 @@ agent, will otherwise copy the exception as if it were the rule.
186
234
  And a rule you had to break is information about the rule: if the exception was
187
235
  right twice, it is a defect in the scale, not in the app.
188
236
 
237
+ ## The framework runs this gate on itself
238
+
239
+ A widget cannot ship a default that the gate would reject in your app, because
240
+ `npm test` runs the same checker over the framework's `src/`
241
+ (`scripts/check-framework-ui.mjs`). It found real defects the first time: a tab
242
+ badge wrote white on the signal red (2.77:1 in the dark theme), a `ListTile`
243
+ description borrowed the disabled grey (2.57:1), a dialog painted two filled
244
+ buttons at once, and eleven defaults sat off the scale.
245
+
246
+ The list of accepted findings lives in `scripts/framework-ui-baseline.json` and
247
+ is shrink-only: every entry carries the reason it is not a defect, and
248
+ `--write-baseline` exits `1` while any entry has no justification. A new finding
249
+ has to be fixed, not snapshotted.
250
+
189
251
  ## What no checker can see
190
252
 
191
253
  `check-ui` has no eyes. It cannot tell you that the screen is ugly, that the
192
254
  hierarchy reads wrong at a glance, or that the flow does not solve the user's
193
- problem. Those are rules 17 and 19, and they are the ones that need a person.
255
+ problem. Those are rules 17, 19, 21 and 23 — composition, the tells, the review
256
+ and this section — and they are the ones that need a person.
194
257
  Run the gate, then open the page.
195
258
 
196
259
  ## Related pages
@@ -205,7 +268,7 @@ Run the gate, then open the page.
205
268
  ## Continue reading
206
269
 
207
270
  - **Previous:** [The CLI](../cli/README.md) — Chapter 11
208
- - **Next:** [Minify and protect a FletBox build](minification-and-protection.md) — Appendix A
271
+ - **Next:** [Accessibility](accessibility.md) — Chapter 13
209
272
  - **Index:** [Guides index](README.md) · [The FletBox Book](../README.md)
210
273
 
211
274
  You are reading **Chapter 12 · Clean UI**.
@@ -255,6 +255,6 @@ Minification reduces the web payload inside the native application. It does not
255
255
 
256
256
  ## Continue reading
257
257
 
258
- - **Previous:** [Clean UI](clean-ui.md) — Chapter 12
258
+ - **Previous:** [Accessibility](accessibility.md) — Chapter 13
259
259
  - **Next:** [Build FletBox for mobile and other platforms](mobile-and-platforms.md) — Appendix B
260
260
  - **Index:** [Guides index](README.md) · [The FletBox Book](../README.md)
@@ -75,6 +75,44 @@ Your Capacitor configuration must point `webDir` to that output directory, commo
75
75
 
76
76
  The exact configuration file may be `capacitor.config.ts`, `capacitor.config.json`, or another format depending on how Capacitor was initialized.
77
77
 
78
+ ### The safe area
79
+
80
+ Every app `flet-box create` writes carries `viewport-fit=cover` in `index.html`,
81
+ which lets the web view paint behind the notch and the home indicator. That is
82
+ what a full-bleed app wants, but it only looks right if content steps back from
83
+ those edges, so the framework reads `env(safe-area-inset-*)` wherever a widget
84
+ touches a screen edge: the [AppBar](../widget/AppBar.md) adds the top inset to
85
+ its own padding, the [Drawer](../widget/Drawer.md) pads its panel and the
86
+ [Tooltip](../widget/Tooltip.md) measures the same box before it picks the side
87
+ its bubble goes on, so a header never sits under the status bar, a footer never
88
+ under the gesture bar and a tooltip never flips into either. The
89
+ value is `0` on a device with no inset, so nothing is configured per platform and
90
+ a desktop browser is untouched.
91
+
92
+ A screen that pins its own content to an edge — a hand-built tab bar, a floating
93
+ action row — has to ask for the same inset. A length prop accepts the expression
94
+ as it stands, because only a value made purely of pixels is converted to rem:
95
+
96
+ ```js
97
+ Container({
98
+ paddingBottom: "calc(16px + env(safe-area-inset-bottom, 0px))",
99
+ children: [/* tabs */],
100
+ });
101
+ ```
102
+
103
+ ### Hover does not exist here
104
+
105
+ A touch screen has no resting pointer, and iOS answers the first tap on an
106
+ element with a synthetic `mouseenter` that no `mouseleave` ever follows. So a
107
+ hover-only affordance on a phone is one that appears and then cannot be
108
+ dismissed, which is why [Tooltip](../widget/Tooltip.md) binds its hover path only
109
+ when `matchMedia("(hover: hover)")` matches and stays reachable through keyboard
110
+ focus everywhere else. The same test is the one to copy for a custom hover card,
111
+ peek row or mouse-following cursor: gate it on the media query, and give the
112
+ information a home the finger can reach — the control's own label, a
113
+ [SnackBar](../widget/SnackBar.md), or a
114
+ [BottomSheet](../widget/BottomSheet.md).
115
+
78
116
  ## Install Capacitor
79
117
 
80
118
  ```bash
@@ -225,6 +263,19 @@ Both platforms serve the app from a local HTTPS origin
225
263
  `capacitor.config.json`. A remote API has to allow that origin in its CORS
226
264
  configuration.
227
265
 
266
+ ### Text fields do not zoom the page
267
+
268
+ iOS magnifies the whole viewport when a text control whose computed font size is
269
+ under 16px receives focus, which reads as the screen growing and staying
270
+ deformed after you type. FletBox floors the field text at 16px on iOS only: a
271
+ `medium` or `small` field renders slightly larger there, while its box, label
272
+ and icons keep the design's sizes. A reader who raises the browser font still
273
+ gets a larger field, because the floor bounds the field's rem value instead of
274
+ replacing it.
275
+
276
+ Nothing has to be configured in the app, and the viewport is left alone: adding
277
+ `maximum-scale=1` would stop the zoom by also blocking pinch-to-zoom.
278
+
228
279
  ### Add iOS to the project
229
280
 
230
281
  ```bash