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/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
|
|
package/docs/tools/device.md
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
# Device & environment
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
|
-
Detect what your app is running on: screen, orientation, operating system, and clipboard access. These helpers read the browser and stay in sync when the window changes.
|
|
4
|
+
Detect what your app is running on: screen, orientation, operating system, and clipboard access. These helpers read the browser and stay in sync when the window changes. The same page carries the two halves a responsive screen needs — the **breakpoint** vocabulary (`BREAKPOINTS`, `currentBreakpoint`, `atLeast`, `pick`, `onBreakpointChange`), which says *which layout this viewport wants*, and `loadFont`, which makes a `fontFamily` prop real.
|
|
5
5
|
|
|
6
6
|
## When to use
|
|
7
7
|
- Layout that changes between phone and desktop (`device`, `dimensions`).
|
|
8
|
+
- One value per viewport (`pick({ xs: 8, md: 16 })`), or a decision that has to agree with `AdaptiveScaffold` (`atLeast("md")`).
|
|
9
|
+
- Rebuild a screen when the viewport *crosses a step*, not when it moves a pixel (`onBreakpointChange`).
|
|
8
10
|
- Show platform-specific UI (`os`).
|
|
9
11
|
- Copy and paste text with one call (`clipboard`).
|
|
10
12
|
- Walk the widget tree (`addNavigation`).
|
|
13
|
+
- Load a webfont before you name it in `fontFamily` (`loadFont`, `fontsReady`).
|
|
11
14
|
|
|
12
15
|
## Import
|
|
13
16
|
|
|
14
17
|
```javascript
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
device, os, dimensions, width, height, clipboard,
|
|
20
|
+
BREAKPOINTS, BREAKPOINT_NAMES, breakpointForWidth,
|
|
21
|
+
currentBreakpoint, atLeast, pick, onBreakpointChange,
|
|
22
|
+
loadFont, fontsReady,
|
|
23
|
+
} from "flet-box";
|
|
16
24
|
```
|
|
17
25
|
|
|
18
26
|
## Objects
|
|
@@ -42,6 +50,67 @@ import { device, os, dimensions, width, height, clipboard } from "flet-box";
|
|
|
42
50
|
|
|
43
51
|
`width` and `height` are also exported directly as numbers.
|
|
44
52
|
|
|
53
|
+
### Breakpoints
|
|
54
|
+
|
|
55
|
+
`dimensions` answers "how wide is the window"; a screen almost always wants the
|
|
56
|
+
next question, "which layout does that width ask for". One table answers it, and
|
|
57
|
+
`md` is 768 — the same line `AdaptiveScaffold` draws between a drawer and a
|
|
58
|
+
permanent sidebar, so a screen and its shell cannot disagree.
|
|
59
|
+
|
|
60
|
+
| Function | Signature | Description |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `BREAKPOINTS` | `{ xs, sm, md, lg, xl }` | The widths in **CSS pixels** (0 · 560 · 768 · 1024 · 1280). A viewport is not a framework unit: it does not scale with the reader's font, and pretending otherwise puts a phone layout on a desktop. |
|
|
63
|
+
| `BREAKPOINT_NAMES` | `['xs' … 'xl']` | The same names in width order. |
|
|
64
|
+
| `currentBreakpoint()` | `() => Breakpoint` | The step the live window is in. `"xs"` where there is no window, so a Node import never throws. |
|
|
65
|
+
| `breakpointForWidth(px)` | `(px: number) => Breakpoint` | The pure read behind it — a test, or a width that is not the viewport (an embed's container). |
|
|
66
|
+
| `atLeast(name)` | `(name: Breakpoint) => boolean` | `atLeast("md")` is the sentence a two-column layout needs. |
|
|
67
|
+
| `pick(values, fallback?)` | `(Record<Breakpoint, T>, T) => T` | A value per step. An unnamed step inherits the nearest smaller one: `pick({ xs: 8, md: 16 })` reads 8, 8, 16, 16, 16. |
|
|
68
|
+
| `onBreakpointChange(listener)` | `(cb) => () => void` | Fires when the *name* changes, not on every pixel of a resize. Returns the unsubscribe; hand it to the widget's `onUnmount` so the subscription dies with the screen. |
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// One screen, two shapes, no second codebase.
|
|
72
|
+
const gutter = pick({ xs: 16, md: 24, lg: 32 });
|
|
73
|
+
|
|
74
|
+
const items = atLeast("md")
|
|
75
|
+
? [listPane, detailPane]
|
|
76
|
+
: [currentPane];
|
|
77
|
+
|
|
78
|
+
const [name, setName] = useState("bp", currentBreakpoint());
|
|
79
|
+
const header = Column({ padding: gutter, children: [Text({ text: name })] });
|
|
80
|
+
header.onUnmount(onBreakpointChange(setName));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A breakpoint is a *decision*, not a device. `device.isMobile()` reads a user
|
|
84
|
+
agent, which a desktop in a narrow window cannot change and a tablet on desktop
|
|
85
|
+
mode will not admit; `currentBreakpoint()` reads the width the app actually has.
|
|
86
|
+
Use the width for layout and the UA only for a genuinely platform question (touch
|
|
87
|
+
long-press, a native back gesture).
|
|
88
|
+
|
|
89
|
+
### Fonts
|
|
90
|
+
|
|
91
|
+
`fontFamily` is a name. If the bytes of that face are not on the machine, the
|
|
92
|
+
browser paints its fallback and the app ships a typeface nobody chose.
|
|
93
|
+
|
|
94
|
+
| Function | Signature | Description |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `loadFont(family, options?)` | `(string, { weights?, url?, display? }) => Promise<boolean>` | Ask for a family. `weights` is required for a Google family if you want anything but 400 — a heading needs its weight asked for by name. `url` loads your own stylesheet instead of a provider's, which is the same-origin answer an offline launch keeps. Resolves `false` when the face could not be had; the screen keeps its fallback instead of failing. |
|
|
97
|
+
| `fontsReady()` | `() => Promise<boolean>` | Wait for the faces already started. Use it before a canvas paint or an export, where text that reflows *after* the drawing is a visible bug. |
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
import { Text, loadFont } from "flet-box";
|
|
101
|
+
|
|
102
|
+
const body = Text({ text: "Reading", fontFamily: "Georgia, serif" });
|
|
103
|
+
|
|
104
|
+
// One request per family, whoever asks first.
|
|
105
|
+
loadFont("Inter", { weights: [400, 600] }).then((ok) => {
|
|
106
|
+
if (ok) body.style.fontFamily = "Inter, Georgia, serif";
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> A webfont is a network request. A self-hosted `@font-face` in the app's own CSS
|
|
111
|
+
> is free of that and is the right answer for a product; a provider URL is fine
|
|
112
|
+
> for a demo and is what a tunnel, a firewall or an offline launch breaks on.
|
|
113
|
+
|
|
45
114
|
### `os`
|
|
46
115
|
|
|
47
116
|
| Method | Returns | Description |
|
|
@@ -111,7 +180,10 @@ dimensions.addListener(({ width, height }) => {
|
|
|
111
180
|
|
|
112
181
|
## Notes
|
|
113
182
|
|
|
114
|
-
- `dimensions` subscribes to window resize internally; `addListener` returns an **unsubscribe function** — call it when the feature is destroyed to avoid leaks. Same for `device.onResize`/`onOrientationChange`.
|
|
183
|
+
- `dimensions` subscribes to window resize internally; `addListener` returns an **unsubscribe function** — call it when the feature is destroyed to avoid leaks. Same for `device.onResize`/`onOrientationChange` and `onBreakpointChange`.
|
|
184
|
+
- `onBreakpointChange` keeps **one** window listener for the whole app and fires only when the step name changes. Subscribing per card would still be one listener, but each callback would run on every pixel — a `Row` that re-renders on a pixel can change the scrollbar and resize the window again.
|
|
185
|
+
- `BREAKPOINTS` are pixels by design. Everything else about a FletBox screen is a framework unit that grows with the reader's font; the viewport is the browser's own number and the layout switch has to be in the same currency as the window.
|
|
186
|
+
- `loadFont` is idempotent per family and never rejects: a blocked request resolves `false`, and the app keeps its fallback. It writes a `<link>` into `document.head`, so a page with a strict CSP needs that host allowed — a self-hosted `url` avoids the question entirely.
|
|
115
187
|
- `clipboard.read()` requires a user gesture in most browsers (it uses the same rules as `navigator.clipboard`).
|
|
116
188
|
- `addNavigation` is idempotent — calling it twice on a widget is safe, and widgets already expose these members when they are built by the factory.
|
|
117
189
|
- `os` also detects mobile/desktop families, which `device` derives for touch+screen decisions.
|