flet-box 1.1.0 → 1.1.2
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/README.md +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- 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/templates.js +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- 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 +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- 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 +15 -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,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**.
|
package/docs/guides/clean-ui.md
CHANGED
|
@@ -35,13 +35,13 @@ pointed at from one `.cursor/rules/<name>.mdc` each:
|
|
|
35
35
|
| Skill | What it owns |
|
|
36
36
|
| --- | --- |
|
|
37
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
|
|
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
39
|
| `fletbox-motion` | Whether a thing may move at all, and through which of `AnimatedBox`, `AnimatedText`, `ParallaxBox` or `animate`. |
|
|
40
40
|
| `fletbox-memory` | The `.fletbox/memory/` journal that hands progress from one AI to the next. |
|
|
41
41
|
|
|
42
42
|
| Home | What it holds |
|
|
43
43
|
| --- | --- |
|
|
44
|
-
| `skills/fletbox-design/SKILL.md` (framework) → `skills/…`, `.claude/skills/…`, `.qoder/skills/…` (app) | The rules as the agent reads them, in
|
|
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. |
|
|
45
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. |
|
|
46
46
|
| `src/tools/scale.js` in the framework | The tables themselves — `SPACING_SCALE`, `RADIUS_SCALE`, `TYPE_SCALE`, `TAP_MINIMUM`, `PILL`, `LABEL_TEXT`. |
|
|
47
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. |
|
|
@@ -55,7 +55,7 @@ components it uses do not follow. The defaults and the checker read one table.
|
|
|
55
55
|
| Dimension | Allowed values |
|
|
56
56
|
| --- | --- |
|
|
57
57
|
| Spacing | `0 · 4 · 8 · 12 · 16 · 18 · 24 · 32 · 40 · 48 · 64 · 96` |
|
|
58
|
-
| 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 |
|
|
59
59
|
| Type | `12 · 14 · 16 · 18 · 24 · 32 · 40 · 48` |
|
|
60
60
|
| Tap minimum | `40` — Fitts: the smallest pressable box a finger reliably hits |
|
|
61
61
|
| Label text | `14` — the reading size a control's own label may not fall under |
|
|
@@ -93,9 +93,17 @@ The framework keeps this honest with its own gate, `scripts/check-widget-scale.m
|
|
|
93
93
|
(part of `npm test`): a length written straight to `el.style.*` that is neither
|
|
94
94
|
`toREM(...)` nor measured geometry fails the build.
|
|
95
95
|
|
|
96
|
-
## The
|
|
96
|
+
## The rules
|
|
97
97
|
|
|
98
|
-
|
|
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.
|
|
105
|
+
|
|
106
|
+
When two rules conflict, take them in this order: **consistency · clarity ·
|
|
99
107
|
accessibility · usability · scalability · maintainability · conversion**.
|
|
100
108
|
|
|
101
109
|
| # | Rule | What it forbids |
|
|
@@ -126,6 +134,29 @@ accessibility · usability · scalability · maintainability · conversion**.
|
|
|
126
134
|
| 23 | What no checker can see | Taste, hierarchy in the wild, whether the screen solves the problem. |
|
|
127
135
|
| 24 | The loop | Fixing the checker instead of the code, or leaving a rule you had to break undocumented. |
|
|
128
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. |
|
|
159
|
+
|
|
129
160
|
## What `flet-box check-ui` proves
|
|
130
161
|
|
|
131
162
|
The gate reads your `src/` and reports what a static pass can prove. Prop names
|
|
@@ -157,7 +188,7 @@ judging yours. Each finding carries a code, and the code maps to a rule above.
|
|
|
157
188
|
| `HAND_MIXED_COLOR` | 03 | `colors.primary + "22"` — a one-off shade no other screen can reuse. |
|
|
158
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. |
|
|
159
190
|
| `IMAGE_NO_ALT` | 11 | An `Image` with no `alt` — `alt: ""` is a decision, a missing prop is an oversight. |
|
|
160
|
-
| `ICON_ONLY_CONTROL` | 11 | A pressable glyph with no name;
|
|
191
|
+
| `ICON_ONLY_CONTROL` | 11 | A pressable glyph with no accessible name; give it `aria-label` (a `Tooltip` is a description, not a name). |
|
|
161
192
|
| `CENTERED_BODY_TEXT` | 08 | A paragraph centered; a paragraph needs a left edge to find the next line. |
|
|
162
193
|
| `GENERIC_ACTION_LABEL` | 14 | "OK", "Submit", "Click here" — a label that says nothing about what pressing it does. |
|
|
163
194
|
| `CONFIRM_ORDER` | 14 | A destructive button placed before Cancel in the same row. |
|
|
@@ -170,7 +201,7 @@ judging yours. Each finding carries a code, and the code maps to a rule above.
|
|
|
170
201
|
| `SMALL_PRESET` | 03 | `size: "small"` on a control that is the screen's main action (~30px). |
|
|
171
202
|
| `CROWDED_FORM` | 06 | Many fields in one file: group them, or split the task. |
|
|
172
203
|
| `LIST_WITHOUT_STATE` | 12 | A list with no loading, empty or failed state in this file. |
|
|
173
|
-
| `MULTIPLE_PRIMARY_ACTIONS` | 04 |
|
|
204
|
+
| `MULTIPLE_PRIMARY_ACTIONS` | 04 | More than one filled button in one view; keep one and make the rest outlined or text. |
|
|
174
205
|
|
|
175
206
|
## Running the gate
|
|
176
207
|
|
|
@@ -203,6 +234,20 @@ agent, will otherwise copy the exception as if it were the rule.
|
|
|
203
234
|
And a rule you had to break is information about the rule: if the exception was
|
|
204
235
|
right twice, it is a defect in the scale, not in the app.
|
|
205
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
|
+
|
|
206
251
|
## What no checker can see
|
|
207
252
|
|
|
208
253
|
`check-ui` has no eyes. It cannot tell you that the screen is ugly, that the
|
|
@@ -223,7 +268,7 @@ Run the gate, then open the page.
|
|
|
223
268
|
## Continue reading
|
|
224
269
|
|
|
225
270
|
- **Previous:** [The CLI](../cli/README.md) — Chapter 11
|
|
226
|
-
- **Next:** [
|
|
271
|
+
- **Next:** [Accessibility](accessibility.md) — Chapter 13
|
|
227
272
|
- **Index:** [Guides index](README.md) · [The FletBox Book](../README.md)
|
|
228
273
|
|
|
229
274
|
You are reading **Chapter 12 · Clean UI**.
|
package/docs/guides/first-app.md
CHANGED
|
@@ -4,24 +4,23 @@ This page is for **absolute beginners**. You do not need to know any trick — y
|
|
|
4
4
|
|
|
5
5
|
## Before you start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You need Node.js 20.9 or newer and one command, because FletBox is published on npm:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
11
|
-
cd flet-box
|
|
12
|
-
npm link # link the flet-box CLI globally
|
|
10
|
+
npm install -g flet-box # the CLI, once
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
Then create an empty project,
|
|
13
|
+
Then create an empty project, install the framework into it, and start the dev server:
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
cd ..
|
|
19
16
|
flet-box create my-app
|
|
20
17
|
cd my-app
|
|
21
|
-
npm
|
|
18
|
+
npm install # flet-box itself, from the registry
|
|
22
19
|
npm run dev
|
|
23
20
|
```
|
|
24
21
|
|
|
22
|
+
If you would rather not install anything globally, `npx flet-box create my-app` does the same thing.
|
|
23
|
+
|
|
25
24
|
Open your browser at `http://localhost:8000`. You should see "Hello FletBox" (or a small welcome screen). If you do, everything is working.
|
|
26
25
|
|
|
27
26
|
## Step 1: the smallest app
|
|
@@ -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:** [
|
|
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
|
|
@@ -116,7 +116,7 @@ Every bundle ships a small manifest at `www/config.json`. This is what a running
|
|
|
116
116
|
|
|
117
117
|
`update.url` is resolved automatically, in this order:
|
|
118
118
|
|
|
119
|
-
1. `update.url` in `fletbox.config.json` at your project root (explicit wins).
|
|
119
|
+
1. `update.url` in `fletbox.config.json` at your project root (explicit wins) — unless it is still the scaffold placeholder, which is not a choice. See [The Pages address of a generated app](#the-pages-address-of-a-generated-app).
|
|
120
120
|
2. Derived from the `repository` field of `package.json`, mapped to GitHub Pages: `https://<user>.github.io/<repo>/`.
|
|
121
121
|
3. Derived from the **current project's** git remote `origin` (when it is a GitHub URL). This means `flet-box createBundle` always points at the project it is built in, never at the flet-box framework installed as a dependency.
|
|
122
122
|
4. If none of these exist, OTA is **disabled** and the bundle simply runs what shipped in it.
|
|
@@ -152,10 +152,22 @@ things at once:
|
|
|
152
152
|
- writes that address into the header of the generated `.github/workflows/build.yml`;
|
|
153
153
|
- prints it in the closing summary, with the one-time setting Pages needs.
|
|
154
154
|
|
|
155
|
-
Without `--repo` nothing is invented. `fletbox.config.json` keeps
|
|
156
|
-
placeholder `https://TU-USUARIO.github.io/TU-REPO/`, the workflow
|
|
157
|
-
guess built from the folder name, and the workflow says
|
|
158
|
-
|
|
155
|
+
Without `--repo` nothing is invented at scaffold time. `fletbox.config.json` keeps
|
|
156
|
+
the generic placeholder `https://TU-USUARIO.github.io/TU-REPO/`, the workflow
|
|
157
|
+
header names a guess built from the folder name, and the workflow says in its own
|
|
158
|
+
comment which part of that address still has to be checked by hand.
|
|
159
|
+
|
|
160
|
+
The build does not trust that placeholder. `TU-USUARIO` and `TU-REPO` mean "not
|
|
161
|
+
configured yet", so `flet-box createBundle` falls through to rules 2 and 3 and
|
|
162
|
+
writes the address it derived — and prints that it derived, because a manifest
|
|
163
|
+
pointing at a real site next to a file that still says `TU-REPO` is a half-filled
|
|
164
|
+
config you should either finish or leave alone. A project with no GitHub remote
|
|
165
|
+
yet gets `update.url: null`: OTA is off, the bundle runs what shipped in it, and
|
|
166
|
+
nothing polls a host that does not exist.
|
|
167
|
+
|
|
168
|
+
That matters because the failure used to be silent. The app compares bundle
|
|
169
|
+
**hashes**, and OTA is built never to break the app, so a placeholder URL that
|
|
170
|
+
reached a bundle produced no error, no dialog and no update — ever.
|
|
159
171
|
|
|
160
172
|
**The workflow log is the authority.** The header names the address you typed at
|
|
161
173
|
scaffold time; the `Where this lives` step prints the address the run *actually*
|
|
@@ -274,7 +286,7 @@ Requirements: a **public** GitHub repository, GitHub Pages enabled (Settings →
|
|
|
274
286
|
| Symptom | Likely cause |
|
|
275
287
|
| --- | --- |
|
|
276
288
|
| Build prints "THIS APP DOES NOT SUPPORT OTA UPDATES" | No public GitHub repo is reachable from the project (no `origin` remote, a non-GitHub remote, or nothing configured). The app still works — it runs the bundle it shipped with. Create the repo, push it, and rebuild. |
|
|
277
|
-
| No dialog, app runs old code | `update.url`
|
|
289
|
+
| No dialog, app runs old code | `update.url` wrong — a real address that is not where the bundle is (the scaffold placeholder is not one of those: the build derives the address and prints it). Or no `v*` tag was published — a normal push updates the site but deliberately leaves the manifest out, so there is nothing to compare against. |
|
|
278
290
|
| `hash mismatch` in console | The published `config.json` and bundle are from different builds. Re-run `flet-box release` so both are published together. |
|
|
279
291
|
| Dialog appears but download fails | The Pages site is not reachable/CORS-blocked from the app origin. Pages serves with permissive headers; check you are not behind a proxy that strips them. |
|
|
280
292
|
| Update applies but reverts on next launch | Cache Storage unavailable (private browsing). The bundle runs once but cannot persist. |
|