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
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
|
|
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
|
```
|
package/docs/cli/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
package/docs/guides/README.md
CHANGED
|
@@ -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
|
|
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**.
|
package/docs/guides/clean-ui.md
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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 |
|
|
108
|
-
| 18 |
|
|
109
|
-
| 19 |
|
|
110
|
-
| 20 |
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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 |
|
|
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
|
|
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:** [
|
|
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:** [
|
|
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
|