flet-box 1.1.0 → 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 +276 -45
- package/bin/commands/create.js +4 -0
- 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/templates.js +78 -73
- package/docs/README.md +2 -0
- package/docs/cli/README.md +18 -1
- 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/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 +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
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**.
|
|
@@ -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
|
package/docs/guides/utilities.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
FletBox includes small helpers for common UI and application tasks. Import only the helpers you need from `flet-box`.
|
|
4
4
|
|
|
5
|
+
These 123 helpers are the **short form** of code you would otherwise write by hand —
|
|
6
|
+
a shadow, a gradient, a mock list, a retry, a relative date. The rule that goes with
|
|
7
|
+
them is a comparison, not a prohibition: reach for a helper when the code it replaces
|
|
8
|
+
is longer or less clear, which for the examples below it is.
|
|
9
|
+
|
|
10
|
+
The one trap: **a routed prop is already the short form**. `Container({ padding: 16 })`
|
|
11
|
+
is shorter and more correct than `padding: padding(16)`, because the number reaches the
|
|
12
|
+
rem contract and the helper returns a pixel string. Use the string builders for a CSS
|
|
13
|
+
value a prop cannot express, not instead of a number.
|
|
14
|
+
|
|
15
|
+
Two names that read as something else: `filter({ blur: 5 })` builds a CSS filter, it
|
|
16
|
+
does not filter a list (that is `mapList`, `sort`, `unique`), and `color(x).lighten(20)`
|
|
17
|
+
is a chain that only becomes a color at `.hex()`, `.rgb()` or `.rgba()`.
|
|
18
|
+
|
|
5
19
|
## Layout helpers
|
|
6
20
|
|
|
7
21
|
Use direct props for the simplest code:
|
|
@@ -90,20 +104,50 @@ Use random helpers for demos and sample data. Do not use generated demo values a
|
|
|
90
104
|
|
|
91
105
|
## Strings and dates
|
|
92
106
|
|
|
107
|
+
The short form of the text and calendar code you would otherwise write by hand.
|
|
108
|
+
|
|
93
109
|
```javascript
|
|
94
110
|
import {
|
|
111
|
+
addDays,
|
|
95
112
|
capitalizeWords,
|
|
113
|
+
dateDiff,
|
|
114
|
+
dayOfWeek,
|
|
96
115
|
formatDate,
|
|
116
|
+
formatDateLong,
|
|
117
|
+
isWeekend,
|
|
118
|
+
parseDate,
|
|
97
119
|
relativeTime,
|
|
120
|
+
seconds,
|
|
121
|
+
timeAgoLocale,
|
|
122
|
+
today,
|
|
123
|
+
trim,
|
|
98
124
|
truncate,
|
|
99
125
|
} from "flet-box";
|
|
100
126
|
|
|
101
|
-
capitalizeWords("flet box");
|
|
102
|
-
truncate("A very long sentence", 10
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
capitalizeWords("flet box"); // "Flet Box"
|
|
128
|
+
truncate("A very long sentence", 10); // "A very lon..."
|
|
129
|
+
trim(" flet-box "); // "flet-box"
|
|
130
|
+
|
|
131
|
+
formatDate(today(), "YYYY-MM-DD"); // today(), read back as itself
|
|
132
|
+
formatDateLong(addDays(new Date(), 7));
|
|
133
|
+
dayOfWeek("2024-01-15"); // "Monday"
|
|
134
|
+
isWeekend("2024-01-15"); // false
|
|
135
|
+
dateDiff(parseDate("15/01/2024", "DD/MM/YYYY"), new Date(), "days");
|
|
136
|
+
relativeTime(Date.now() - seconds(90)); // "1 minute ago"
|
|
137
|
+
timeAgoLocale(Date.now() - seconds(90), "es"); // "hace 1 minuto"
|
|
105
138
|
```
|
|
106
139
|
|
|
140
|
+
Two things the helpers get right that hand-rolled `new Date(...)` chains usually
|
|
141
|
+
don't. A bare `"YYYY-MM-DD"` is read as a **calendar day in the reader's zone**,
|
|
142
|
+
so `isToday(today())` is true on a machine in any timezone; and `addDays` counts
|
|
143
|
+
days on the calendar rather than adding `86400000` milliseconds, so it does not
|
|
144
|
+
slip a day across a DST transition. `months(n)` and `years(n)` are the honest
|
|
145
|
+
exceptions — they approximate 30 and 365 days, which is fine for a cache window
|
|
146
|
+
and wrong for a birthday. For the repeating half of the same module — `every()`,
|
|
147
|
+
`everySecond()`, `timer()`, `waitUntil()` — see
|
|
148
|
+
[Text & time](../tools/text-and-time.md); whatever registers an interval releases
|
|
149
|
+
it in `onUnmount`.
|
|
150
|
+
|
|
107
151
|
## Delays and retries
|
|
108
152
|
|
|
109
153
|
```javascript
|
|
@@ -115,6 +159,9 @@ await withMinDelay(fetchData(), 800);
|
|
|
115
159
|
```
|
|
116
160
|
|
|
117
161
|
Use these helpers for loading states, demos, and recoverable network operations.
|
|
162
|
+
`waitUntil(condition, timeout, interval)` and `waitForElement(selector, …)` cover
|
|
163
|
+
the polling loop, and both **throw** at their timeout — await them inside a
|
|
164
|
+
`try`, or answer them with a `.catch`.
|
|
118
165
|
|
|
119
166
|
## Device and dimensions
|
|
120
167
|
|
|
@@ -139,6 +186,62 @@ const removeListener = dimensions.addListener(({ width, height }) => {
|
|
|
139
186
|
removeListener();
|
|
140
187
|
```
|
|
141
188
|
|
|
189
|
+
`device` reads a user agent; the width is the fact a layout can trust. The full
|
|
190
|
+
reference — the breakpoint table, the subscription and font loading — is
|
|
191
|
+
[Device & environment](../tools/device.md).
|
|
192
|
+
|
|
193
|
+
## Breakpoints
|
|
194
|
+
|
|
195
|
+
One table, five steps, in CSS pixels: `xs` 0, `sm` 560, `md` 768, `lg` 1024,
|
|
196
|
+
`xl` 1280. `md` is the same line `AdaptiveScaffold` draws between a drawer and a
|
|
197
|
+
permanent sidebar, so a screen and its shell cannot disagree.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
import { atLeast, pick, currentBreakpoint } from "flet-box";
|
|
201
|
+
|
|
202
|
+
const twoColumns = atLeast("md");
|
|
203
|
+
const gutter = pick({ xs: 16, md: 24, lg: 32 });
|
|
204
|
+
const step = currentBreakpoint(); // "sm" at 600px, "md" at 800px
|
|
205
|
+
|
|
206
|
+
// A step that is not named inherits the nearest smaller one:
|
|
207
|
+
// pick({ xs: 8, md: 16 }) answers 8, 8, 16, 16, 16.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Rebuild only when the viewport *crosses* a step, not on every pixel of a resize.
|
|
211
|
+
`onBreakpointChange` returns the unsubscribe — hand it to the widget that owns the
|
|
212
|
+
subscription:
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
const [isWide, setIsWide] = useState("isWide", atLeast("md"));
|
|
216
|
+
|
|
217
|
+
const screen = Column({
|
|
218
|
+
padding: gutter,
|
|
219
|
+
children: [Text({ text: String(isWide) })],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
screen.onUnmount(onBreakpointChange(() => setIsWide(atLeast("md"))));
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Fonts
|
|
226
|
+
|
|
227
|
+
`fontFamily` is a name. If the bytes of that face are not on the machine, the
|
|
228
|
+
browser paints its own fallback and the app ships a typeface nobody chose.
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
import { loadFont, fontsReady } from "flet-box";
|
|
232
|
+
|
|
233
|
+
// A webfont is a network request: ask, wait, then name it.
|
|
234
|
+
const loaded = await loadFont("Inter", { weights: [400, 600] });
|
|
235
|
+
if (loaded) Text({ text: "Hello", fontFamily: "Inter" });
|
|
236
|
+
|
|
237
|
+
// Before a canvas paint or an export, where reflowing text is a visible bug.
|
|
238
|
+
await fontsReady();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`loadFont` resolves `false` when the face could not be had — it never fails a
|
|
242
|
+
screen. Pass `url` to load your own same-origin stylesheet instead of a
|
|
243
|
+
provider's, which is the answer an offline launch keeps.
|
|
244
|
+
|
|
142
245
|
## Themes and colors
|
|
143
246
|
|
|
144
247
|
```javascript
|
|
@@ -231,6 +334,15 @@ formatMediaProgress(30, 120);
|
|
|
231
334
|
- Need URL changes? Read [Routing](router.md).
|
|
232
335
|
- Need colors that follow the app theme? Use `colors` and `getColor`.
|
|
233
336
|
- Need browser information? Use `device`, `os`, and `dimensions`.
|
|
337
|
+
- Need a layout that changes with the window? Use `atLeast` and `pick`, and
|
|
338
|
+
subscribe with `onBreakpointChange` — never a user-agent guess.
|
|
339
|
+
- Need a typeface that is not on the machine? Load it with `loadFont` before you
|
|
340
|
+
name it in `fontFamily`.
|
|
341
|
+
- Need a date moved, compared or labelled? Use `addDays`, `dateDiff`, `formatDate`
|
|
342
|
+
and `timeAgo` — not a millisecond walk you have to re-derive each time.
|
|
343
|
+
- Need a period, a stopwatch or a poll loop? Use `seconds(30)` and `every(…)`,
|
|
344
|
+
`timer(label)`, `waitUntil(…)`; the id `every` hands back is what `clearInterval`
|
|
345
|
+
releases in `onUnmount`.
|
|
234
346
|
|
|
235
347
|
---
|
|
236
348
|
|
|
@@ -114,6 +114,32 @@ export default MyWidget;
|
|
|
114
114
|
pass CSS props directly (`backgroundColor`, `padding`, `flexDirection`,
|
|
115
115
|
`whiteSpace`, …) with aliases resolved by the factory. `style: { ... }` is
|
|
116
116
|
rejected with a warning (`processProps`), so it never silently half-applies.
|
|
117
|
+
12. **A shared helper has one home.** The first time a helper is written it is
|
|
118
|
+
local; the second time it is a module both callers import. Two copies pass
|
|
119
|
+
every test on the day they are written and diverge on the day one is fixed —
|
|
120
|
+
so the home is named and the gate enforces it:
|
|
121
|
+
|
|
122
|
+
| Behaviour | Home |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| Escaping text for HTML | [`src/helpers/html.js`](../../src/helpers/html.js) |
|
|
125
|
+
| The four-side box shorthand | [`src/helpers/boxEdges.js`](../../src/helpers/boxEdges.js), behind `margin()`/`padding()` |
|
|
126
|
+
| Injecting `@keyframes`, naming one | [`src/utils/styleInjector.js`](../../src/utils/styleInjector.js) |
|
|
127
|
+
| A number that becomes a length | [`src/utils/units.js`](../../src/utils/units.js) |
|
|
128
|
+
|
|
129
|
+
**`src/helpers/` is the folder for reusable code, and a module there imports
|
|
130
|
+
nothing at all** — no widget, no theme, no engine service. That is what makes
|
|
131
|
+
it safe to import from anywhere: `html.js` has no dependencies on purpose,
|
|
132
|
+
because importing the Markdown parser from a widget would pull six widgets
|
|
133
|
+
into that widget's graph. A helper that needs another module is a service and
|
|
134
|
+
belongs in `utils/`. `node scripts/check-shared-helpers.mjs` fails on a second
|
|
135
|
+
definition, an `@keyframes` rule written outside the injector, a helper that
|
|
136
|
+
grew an import, or a caller that stopped importing the home.
|
|
137
|
+
13. **Only the code the widget needs.** No prop nobody passes, no option kept
|
|
138
|
+
"for later", no abstraction with a single caller, no wrapper that only
|
|
139
|
+
forwards its arguments — that last one is code to delete, not to keep. The
|
|
140
|
+
second caller is when a helper appears (rule 12), not before. Three lines
|
|
141
|
+
repeated once reads better than a premature module; the same three lines
|
|
142
|
+
repeated three times do not.
|
|
117
143
|
|
|
118
144
|
## Controller widgets
|
|
119
145
|
|
|
@@ -158,6 +184,10 @@ Everything else returns an `HTMLElement`.
|
|
|
158
184
|
to the baseline is never the fix.
|
|
159
185
|
- `node scripts/check-widgets.mjs --write-baseline` — re-snapshot after a batch
|
|
160
186
|
of fixes (only to *remove* entries in practice).
|
|
187
|
+
- `node scripts/check-shared-helpers.mjs` — the single-home contract from rule
|
|
188
|
+
12: one definition per shared helper, the module named in the rule, and every
|
|
189
|
+
caller importing it. `--root <dir>` checks another checkout, which is how each
|
|
190
|
+
of its five rules was proven to fail when the rule is broken.
|
|
161
191
|
- `/tests/widget-contract.html` — runtime contract: every widget instantiates,
|
|
162
192
|
returns an element (or a documented controller), exposes `update()`, and
|
|
163
193
|
reports its real `_widgetName`.
|
package/docs/tools/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Each category is the next chapter of the reading path:
|
|
|
30
30
|
| [Visual effects](visual-effects.md) | `applyShimmer`, `applyGlow`, `applyPulse`, `injectKeyframes` |
|
|
31
31
|
| [Widget introspection](introspection.md) | `getWidgetProps`, `getWidgetProp`, `stringifyWidgetProps` |
|
|
32
32
|
| [Input validation](validation.md) | `TextInputValidator` rules |
|
|
33
|
-
| [Animation helpers](animation.md) | `animate`, `fadeIn`, `pulse` + `AnimatedBox` and friends |
|
|
33
|
+
| [Animation helpers](animation.md) | `slideIn`, `popIn`, `shake`, `animate`, `fadeIn`, `pulse`, springs and `EASING_NAMES`, `setMotionMode` + `AnimatedBox` and friends |
|
|
34
34
|
|
|
35
35
|
## Styling helpers (CSS in JS)
|
|
36
36
|
|
package/docs/tools/animation.md
CHANGED
|
@@ -1,38 +1,159 @@
|
|
|
1
1
|
# Animation helpers
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
|
-
|
|
4
|
+
Three ways to animate in FletBox: the **named transitions** (`slideIn`, `popIn`, `shake`, `slideOut`) for the gestures a screen keeps repeating, `animate`/`fadeIn`/`fadeOut`/`pulse` (and their `Async` variants) for one property of one element, and the widgets `AnimatedBox`, `AnimatedText`, `MatrixRain`, and `ParallaxBox` for animated UI out of the box. Every helper-driven animation is cancelable — through the function it returns or later with `stopAnimation(widget)` — and all three honour the app's [motion preference](#reduced-motion), so a screen never has to ask whether the user wants movement.
|
|
5
5
|
|
|
6
6
|
## When to use
|
|
7
|
-
-
|
|
7
|
+
- Bring a screen, sheet or card in: `slideIn(card)`, `popIn(badge)` — one call, one gesture.
|
|
8
|
+
- Say "that was wrong": `shake(field)` on a rejected form.
|
|
9
|
+
- Animate one property when something happens: fade a panel in, pulse a button, animate a value change.
|
|
8
10
|
- Stop an animation the element no longer needs: `onUnmount(() => stopAnimation(el))`.
|
|
9
11
|
- Add moving backgrounds (`MatrixRain`, `ParallaxBox`).
|
|
10
12
|
|
|
11
13
|
## Import
|
|
12
14
|
|
|
13
15
|
```javascript
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
animate, fadeIn, fadeOut, pulse, stopAnimation,
|
|
18
|
+
slideIn, slideOut, popIn, shake, animateSpring,
|
|
19
|
+
EASING_NAMES, cubicBezier, spring, stagger,
|
|
20
|
+
prefersReducedMotion, setMotionMode, getMotionMode, onMotionModeChange,
|
|
21
|
+
AnimatedBox,
|
|
22
|
+
} from "flet-box";
|
|
15
23
|
```
|
|
16
24
|
|
|
17
25
|
## Functions
|
|
18
26
|
|
|
19
27
|
| Function | Signature | Returns | Description |
|
|
20
28
|
| --- | --- | --- | --- |
|
|
21
|
-
| `animate` | `animate(widget, property, from, to, duration?, easing?)` | cancel `() => boolean` | Animate one
|
|
29
|
+
| `animate` | `animate(widget, property, from, to, duration?, easing?)` | cancel `() => boolean` | Animate one `property` of a widget from `from` to `to`. A number is a **framework unit** — the same unit every widget prop takes — so `translateY: 20` is `20 / base-font-size` rem and grows with the reader's font; a unit string keeps its unit (`"16px"`); and a color blends whichever way you write it (`#rrggbb`, `rgb(…)`, `rgba(…)`). `duration` is ms, `easing` defaults to `"linear"` — a name from [`EASING_NAMES`](#easings), a `cubicBezier()` or a `spring().easing`. Calling the returned function stops the loop and returns `true` if it was still running. |
|
|
22
30
|
| `animateAsync` | `animateAsync(widget, property, from, to, duration?, easing?)` | `Promise` | Same, resolves with the widget when the animation finishes — or as soon as it is cancelled or replaced, so `await` never hangs. |
|
|
31
|
+
| `animateSpring` | `animateSpring(widget, property, from, to, options?)` | cancel `() => boolean` | The same loop driven by a damped spring: there is no duration to pick, the animation runs until it settles. `options` are `stiffness`, `damping`, `mass`, `initialVelocity`. |
|
|
32
|
+
| `animateSpringAsync` | same, `Async` suffix | `Promise` | Promise variant of `animateSpring`. |
|
|
23
33
|
| `stopAnimation` | `stopAnimation(widget, property?)` | `boolean` | Stop what is animating `widget` — one `property`, or every animated property when it is omitted. Use it in `onUnmount` when you did not keep the cancel function. |
|
|
24
|
-
| `fadeIn` | `fadeIn(widget, duration?)` | cancel `() => boolean` | Fade the widget in (default 300 ms). |
|
|
25
|
-
| `fadeOut` | `fadeOut(widget, duration?)` | cancel `() => boolean` | Fade it out. |
|
|
26
|
-
| `pulse` | `pulse(widget, duration?)` | cancel `() => boolean` | A quick scale pulse:
|
|
27
|
-
| `fadeInAsync` / `fadeOutAsync` / `pulseAsync` | same, `Async` suffix | `Promise` |
|
|
34
|
+
| `fadeIn` | `fadeIn(widget, duration?)` | cancel `() => boolean` | Fade the widget in to full opacity (default 300 ms). |
|
|
35
|
+
| `fadeOut` | `fadeOut(widget, duration?)` | cancel `() => boolean` | Fade it out — from wherever the element actually is, so a card the theme set to `0.8` does not first jump to fully opaque. |
|
|
36
|
+
| `pulse` | `pulse(widget, duration?)` | cancel `() => boolean` | A quick scale pulse: one loop that swells to `1.1` at the halfway mark and is back at `1` when it ends. |
|
|
37
|
+
| `fadeInAsync` / `fadeOutAsync` / `pulseAsync` | same, `Async` suffix | `Promise` | The same single loop, awaitable, resolving with the widget — the sync and awaitable forms of a preset are one gesture, not two implementations. |
|
|
28
38
|
|
|
29
39
|
Starting a second animation on the same widget and property **replaces** the first one; two loops writing
|
|
30
40
|
one style would otherwise fight, and the older animation would resume after the newer had finished.
|
|
41
|
+
Transform functions are the exception, and deliberately: `translateY` and `scale` are separate
|
|
42
|
+
properties in the registry and are merged into one `transform` string, so an element can slide and
|
|
43
|
+
grow at the same time.
|
|
44
|
+
|
|
45
|
+
## Named transitions
|
|
46
|
+
|
|
47
|
+
The three gestures every product repeats, with the durations and the distances already chosen, so an
|
|
48
|
+
app does not hand-roll eleven slightly different entrances. Each returns a cancel function and has an
|
|
49
|
+
`Async` form that resolves with the widget. `distance` is a framework unit like every other length in
|
|
50
|
+
the framework — `16` is `1 rem` at the default base, not sixteen fixed pixels.
|
|
51
|
+
|
|
52
|
+
| Function | Signature | What it says |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `slideIn` | `slideIn(widget, { direction, distance, duration, easing, fade })` | "this appeared" — a short travel plus a fade. `direction` is the way the element travels (`"up"` starts below its resting place), `distance` defaults to `16`, `duration` to `240`, `easing` to `"easeOut"`. |
|
|
55
|
+
| `slideOut` | `slideOut(widget, { direction, distance, duration, easing, fade })` | "this is gone" — the mirror, one notch shorter (180 ms). What follows is your unmount: `await slideOutAsync(dialog).then(() => dialog.remove())`. |
|
|
56
|
+
| `popIn` | `popIn(widget, { from, to, duration, easing, fade })` | "this belongs here" — scale from `0.94` with a small overshoot (`"easeOutBack"`). Nothing enters from `scale: 0`. |
|
|
57
|
+
| `shake` | `shake(widget, { axis, distance, cycles, duration })` | "that was wrong" — one decaying sine on `translateX` (or `translateY`), 6 units, 3 swings, 320 ms, ending at rest. |
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
// A sheet that arrives, and the error that refuses it.
|
|
61
|
+
slideIn(panel, { direction: "up", distance: 24, duration: 220 });
|
|
62
|
+
|
|
63
|
+
if (!password.valid) {
|
|
64
|
+
shake(loginForm, { axis: "x" });
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Easings
|
|
69
|
+
|
|
70
|
+
`animate()` reads a **FletBox name**, not a CSS string: `"ease-out"` is not in this vocabulary and the
|
|
71
|
+
loop would run `linear` (with a warning) while `AnimatedBox({ timing })` hands the same word straight
|
|
72
|
+
to CSS. `EASING_NAMES` is the list the framework knows.
|
|
73
|
+
|
|
74
|
+
| Family | Names |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| The original four | `linear`, `easeIn`, `easeOut`, `easeInOut` |
|
|
77
|
+
| Powers | `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInQuart`, `easeOutQuart`, `easeInOutQuart`, `easeInExpo`, `easeOutExpo`, `easeInOutExpo`, `easeInCirc`, `easeOutCirc`, `easeInOutCirc` |
|
|
78
|
+
| Overshoot | `easeInBack`, `easeOutBack`, `easeInOutBack`, `easeInElastic`, `easeOutElastic`, `easeOutBounce` |
|
|
79
|
+
| Material shapes | `standard`, `emphasized`, `emphasizedDecelerate`, `emphasizedAccelerate` |
|
|
80
|
+
|
|
81
|
+
An entrance decelerates — `easeOut*`, `emphasizedDecelerate` or a spring. `easeIn*` starts slow, which
|
|
82
|
+
is why it belongs on something leaving the screen at speed and nowhere else in an interface.
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// Any CSS curve, on the imperative side.
|
|
86
|
+
animate(card, "translateY", 24, 0, 240, cubicBezier(0.2, 0, 0, 1));
|
|
87
|
+
|
|
88
|
+
// A gesture that continues into its resting place instead of stopping at it.
|
|
89
|
+
animateSpring(button, "scale", 0.96, 1, { stiffness: 260, damping: 20 });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`stagger()` gives an entrance its choreography without the arithmetic that turns 20 rows into a 1.2 s
|
|
93
|
+
wait: the delay stops growing after `max` items.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
list.forEach((row, index) => {
|
|
97
|
+
slideIn(row, { duration: 240 });
|
|
98
|
+
row.style.animationDelay = `${stagger(index, { step: 60, max: 8 })}ms`;
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Reduced motion
|
|
103
|
+
|
|
104
|
+
`prefers-reduced-motion` is not a style note: for readers with a vestibular disorder, parallax and
|
|
105
|
+
large movement are physically unpleasant. FletBox answers it in the engine, so an animation cannot
|
|
106
|
+
ship without the check.
|
|
107
|
+
|
|
108
|
+
| Function | Signature | Description |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `prefersReducedMotion` | `prefersReducedMotion()` | `true` when the app or the operating system asked for no movement. |
|
|
111
|
+
| `setMotionMode` | `setMotionMode("auto" \| "reduce" \| "full")` | The app's own switch. `"auto"` (the default) follows the OS; `"reduce"` never moves; `"full"` always animates. Wires to a settings toggle. |
|
|
112
|
+
| `getMotionMode` | `getMotionMode()` | The mode in effect. |
|
|
113
|
+
| `onMotionModeChange` | `onMotionModeChange(listener)` | Calls `listener(reduced)` when the mode — or the OS setting — changes. Returns the unsubscribe; call it from `onUnmount`. |
|
|
114
|
+
| `motionDuration` | `motionDuration(240)` | The duration a loop should actually run: `0` when motion is reduced. |
|
|
115
|
+
|
|
116
|
+
What each surface does with it:
|
|
117
|
+
|
|
118
|
+
- **`animate()` and the presets** collapse to a single frame that writes the end value. The state still
|
|
119
|
+
lands and an `await` still resolves — only the movement disappears.
|
|
120
|
+
- **`AnimatedBox`** runs its keyframes at `0.01 ms`, once, with no delay. `fillMode: "forwards"` leaves
|
|
121
|
+
the element on its `to` values, and a `loop` stops looping. An element already looping when the reader
|
|
122
|
+
changes the OS setting is caught by a `@media (prefers-reduced-motion: reduce)` rule the engine injects
|
|
123
|
+
once, which overrides every animated element — no listener to leak, nothing to rebuild.
|
|
124
|
+
- **Every `infinite` CSS animation the factory writes** is marked with that same hook, so the loading
|
|
125
|
+
loops belong to the widget author: `Skeleton`, `ProgressBar`, `Slider` (striped or glowing), the
|
|
126
|
+
`ListView` refresh spinner and the `Modal` spinner all stop on their own.
|
|
127
|
+
- **`ParallaxBox`** parks its layer at the origin and stops answering scroll or pointer input.
|
|
128
|
+
- **`MatrixRain`** paints one static field and releases the `requestAnimationFrame`.
|
|
129
|
+
- **`applyPulse`** does nothing at all.
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
// A settings screen that hands the decision to the reader.
|
|
133
|
+
Switch({
|
|
134
|
+
label: "Reduce motion",
|
|
135
|
+
value: getMotionMode() === "reduce",
|
|
136
|
+
onToggle: (on) => setMotionMode(on ? "reduce" : "auto"),
|
|
137
|
+
});
|
|
138
|
+
```
|
|
31
139
|
|
|
32
140
|
## Widgets
|
|
33
141
|
|
|
34
|
-
- `AnimatedBox({ animations, timing, delay, fillMode, child, top, right, bottom, left })` — animates a child widget.
|
|
35
|
-
|
|
142
|
+
- `AnimatedBox({ animations, timing, delay, fillMode, child, top, right, bottom, left })` — animates a child widget.
|
|
143
|
+
`animations` is an array of `{ effect, from, to }` steps, and each step may carry its own `duration`, `delay`,
|
|
144
|
+
`loop` and `reverse` — every one of them is read off **that step**, so a 600 ms `scale` and a 400 ms `opacity`
|
|
145
|
+
run on two clocks inside one rule. Times are milliseconds as a number (`duration: 400`) or a CSS time as a
|
|
146
|
+
string (`delay: "0.4s"`); the widget-level `delay` is the default for the steps that do not name one. A step
|
|
147
|
+
plays once and holds `to` at the end (`reverse: true` swings it back to `from`, `loop: true` repeats it).
|
|
148
|
+
Effects are `opacity`, `scale`, `scaleX`, `scaleY`, `rotate`, `rotateX`, `rotateY`, `rotateZ`, `skewX`,
|
|
149
|
+
`skewY`, `translateX`, `translateY`, `translateZ`, `backgroundColor`, `color`, `borderColor`, `borderWidth`,
|
|
150
|
+
`borderRadius`, `width`, `height`, `boxShadow`, `background`, `textGradient` and the per-side margin/padding
|
|
151
|
+
steps. A looping step and a one-shot step generate two `@keyframes` rules — one rule per timing group — and
|
|
152
|
+
since two rules cannot compose one `transform`, keep the transform steps in one group. `timing` is a **CSS**
|
|
153
|
+
string (`"ease-out"`, `"cubic-bezier(…)"`).
|
|
154
|
+
- `AnimatedText({ child, animations, sameTime, delayBetween, orientation })` — animates a `Text` widget, one step
|
|
155
|
+
per character. `delayBetween` is in **seconds** (`0.05` is a 50 ms stagger); the letters share one generated
|
|
156
|
+
rule and each carries its own delay.
|
|
36
157
|
- `MatrixRain({ chars, fontSize, speed, fadeAmount, resetProbability, useDynamicColor, position, zIndex })` — the falling-characters canvas effect (returns a `canvas`).
|
|
37
158
|
- `ParallaxBox({ type, speed, direction, maxOffset, reverse, child })` — content that moves; `type` is `"scroll"`, `"mouse"`, or `"hover"`, `direction` is `"vertical"`, `"horizontal"`, or `"both"`.
|
|
38
159
|
|
|
@@ -67,7 +188,9 @@ AnimatedBox({
|
|
|
67
188
|
|
|
68
189
|
## Notes
|
|
69
190
|
|
|
70
|
-
-
|
|
191
|
+
- Two vocabularies of time, and each is honest about itself: `duration`/`delay` on a step or a preset are
|
|
192
|
+
milliseconds (a number, or a CSS time string the engine parses), `stagger()` returns milliseconds for
|
|
193
|
+
`animationDelay`, and `AnimatedText`'s `delayBetween` is seconds.
|
|
71
194
|
- The `Async` variants are handy for sequences: `await fadeInAsync(el); await fadeOutAsync(el);`
|
|
72
195
|
- For CSS-string animations use the `animation()` styling helper from [Styling helpers](styling.md).
|
|
73
196
|
|