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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fletbox-memory
|
|
3
|
+
description: The project journal that lets any AI pick up where the last one stopped - read .fletbox/memory/MEMORY.md before working, write an entry the moment something is learned, keep a running status note so a screen, a fix or a half-finished flow survives a change of model. Use at the start and the end of every session, before a risky change, and whenever the user says "continue" or "what is the state".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory — the handover between sessions
|
|
7
|
+
|
|
8
|
+
An app is built across many sessions and, more often than not, by different
|
|
9
|
+
models. None of them remembers the last one. The only thing that carries over is
|
|
10
|
+
what is written down, so writing it is part of the work, not a courtesy at the end.
|
|
11
|
+
|
|
12
|
+
A session that learned something and did not write it is a session that will have
|
|
13
|
+
to be repeated.
|
|
14
|
+
|
|
15
|
+
## MEM1 Read before writing code
|
|
16
|
+
|
|
17
|
+
At the start of a session, in this order:
|
|
18
|
+
|
|
19
|
+
1. `AGENTS.md` — the contract.
|
|
20
|
+
2. `.fletbox/memory/MEMORY.md` — one line per entry, with what it is about.
|
|
21
|
+
3. The entries the task touches: the current status note, and any entry naming the
|
|
22
|
+
screen, widget or flow you are about to change.
|
|
23
|
+
|
|
24
|
+
If the index points at a file that is not there, that is itself a finding: fix the
|
|
25
|
+
index before you trust it, and say so.
|
|
26
|
+
|
|
27
|
+
## MEM2 One entry per fact, never one entry per day
|
|
28
|
+
|
|
29
|
+
`.fletbox/memory/notes/<slug>.md`, kebab-case, named after the **fact**, not the
|
|
30
|
+
session: `checkout-form.md`, `listview-itemsize.md`, `dark-contrast.md`. Ten files
|
|
31
|
+
about real things beat one `notes/2026-10-24.md` nobody can search.
|
|
32
|
+
|
|
33
|
+
Every entry has the same shape, and the last two lines are what make it usable a
|
|
34
|
+
month from now:
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
---
|
|
38
|
+
name: listview-itemsize
|
|
39
|
+
description: ListView virtualises — itemSize must match the row it renders
|
|
40
|
+
metadata:
|
|
41
|
+
type: project
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
A `ListView` row taller than its `itemSize` is clipped, and the scroll math drifts
|
|
45
|
+
with it. Set `itemSize` to the real row height, or leave the list unvirtualised by
|
|
46
|
+
passing `children` instead of `data`.
|
|
47
|
+
|
|
48
|
+
**Why:** the row looked fine at the top of the list; only after 30 items did the
|
|
49
|
+
bottom of each one disappear. Nothing in `npm run ui` can see it.
|
|
50
|
+
|
|
51
|
+
**How to apply:** whenever a `ListView({ data, renderItem })` is added or a row's
|
|
52
|
+
content changes, re-measure the row and update `itemSize`.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`type` is `project` (what this app is and where it is going), `decision` (a choice
|
|
56
|
+
with a reason behind it), `finding` (something the framework or a screen does that
|
|
57
|
+
surprised you), or `task` (open work someone has to finish).
|
|
58
|
+
|
|
59
|
+
Then add the pointer to `MEMORY.md` — one line, under 150 characters, with a hook:
|
|
60
|
+
|
|
61
|
+
```markdown
|
|
62
|
+
- [ListView itemSize](notes/listview-itemsize.md) — a row taller than itemSize is clipped; re-measure after any row change
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## MEM3 Keep one status note, and update it as you work
|
|
66
|
+
|
|
67
|
+
`notes/current-status.md` is the handover document: the next model reads it and
|
|
68
|
+
knows where it is standing. Keep four sections, short, and rewrite them instead of
|
|
69
|
+
appending to them.
|
|
70
|
+
|
|
71
|
+
```markdown
|
|
72
|
+
# Status
|
|
73
|
+
|
|
74
|
+
**Doing:** the checkout form — step 2 (payment) is built, step 3 is stubbed.
|
|
75
|
+
**Done and verified:** `npm run check` clean, `npm run ui` clean, 375px and 1440px
|
|
76
|
+
looked at in both themes.
|
|
77
|
+
**Not done / half-done:** the "Apply coupon" action has no loading state; the empty
|
|
78
|
+
cart is a blank panel.
|
|
79
|
+
**Next:** add the two missing states (rule 12), then wire `onEndReached` on the
|
|
80
|
+
product list.
|
|
81
|
+
**Blocked:** the coupon endpoint is not decided — ask, do not guess.
|
|
82
|
+
**Last touched:** src/screens/Checkout.js, 2026-10-24.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Honesty here is the whole value of the file. A note that says "done" when the empty
|
|
86
|
+
state is missing is worse than no note: the next session builds on top of it.
|
|
87
|
+
|
|
88
|
+
## MEM4 What belongs in memory, and what does not
|
|
89
|
+
|
|
90
|
+
Write it when it passes one of these tests:
|
|
91
|
+
|
|
92
|
+
- **You would get it wrong again.** A prop that silently does nothing, a widget that
|
|
93
|
+
ignores `elevation`, a default that surprises — re-deriving these costs a session.
|
|
94
|
+
- **It explains a rule you broke.** The one-sentence justification for a kept
|
|
95
|
+
`npm run ui` finding belongs next to the finding.
|
|
96
|
+
- **It is a decision with a reason.** "The list is unvirtualised because rows have
|
|
97
|
+
variable height" tells the next session not to "optimise" it back.
|
|
98
|
+
- **It is user preference.** Tone, language, what they call the feature, what they
|
|
99
|
+
refused. This is the most valuable kind and the first thing a new model loses.
|
|
100
|
+
|
|
101
|
+
Do not write:
|
|
102
|
+
|
|
103
|
+
- Anything the code or `.fletbox/api/api.json` already says. Memory that restates
|
|
104
|
+
the API goes stale the next release; the generated files do not.
|
|
105
|
+
- A log of what you edited. `git log` knows; a journal of diffs is noise that pushes
|
|
106
|
+
the useful entries out of view.
|
|
107
|
+
- The scratch plan for a task you are about to finish. Delete it when the work lands,
|
|
108
|
+
or keep the decision and drop the plan.
|
|
109
|
+
|
|
110
|
+
## MEM5 Correct the record instead of arguing with it
|
|
111
|
+
|
|
112
|
+
Memory is a claim about a point in time, not a law. Before acting on an entry that
|
|
113
|
+
names a file, a prop or a behavior, check that it is still true:
|
|
114
|
+
|
|
115
|
+
- Still accurate → use it.
|
|
116
|
+
- Contradicted by the code, by `npm run check` or by what the screen does → **the
|
|
117
|
+
code wins.** Fix the entry, or delete it, in the same change that fixes the screen.
|
|
118
|
+
- About work that is now finished → remove the `task` entry and fold whatever was
|
|
119
|
+
learned into a `finding` or `decision` entry.
|
|
120
|
+
|
|
121
|
+
Never leave a wrong entry because deleting it feels like losing history. A stale
|
|
122
|
+
entry is the mechanism by which one model's mistake becomes the next model's
|
|
123
|
+
"existing pattern".
|
|
124
|
+
|
|
125
|
+
## MEM6 Write before the session ends, not after
|
|
126
|
+
|
|
127
|
+
Update the status note at each natural boundary — a screen that passes both gates, a
|
|
128
|
+
fix confirmed in the browser, a decision taken, a blocker found. Do not plan to write
|
|
129
|
+
it "at the end": the end of a session is the one moment that reliably does not
|
|
130
|
+
happen.
|
|
131
|
+
|
|
132
|
+
One line in `MEMORY.md`, one file in `notes/`, and the status note rewritten is the
|
|
133
|
+
whole cost. If the change is big enough to commit, the memory change goes in the same
|
|
134
|
+
commit — the code and the reason for it should not be able to drift apart.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fletbox-motion
|
|
3
|
+
description: When a FletBox screen may move and with which API - the named transitions (slideIn, slideOut, popIn, shake), AnimatedBox, AnimatedText, ParallaxBox, animate/fadeIn/pulse, springs and the easing vocabulary, reduced motion with setMotionMode, the frequency gate, and what must never animate. Use when adding any transition, entrance, error feedback, loading pulse, hover lift, parallax or hero motion.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Motion — the only animation contract this app has
|
|
7
|
+
|
|
8
|
+
Motion is a sentence, not a coat of paint. One screen says one thing with it:
|
|
9
|
+
"this appeared", "this is loading", "this is where you are". Every other movement
|
|
10
|
+
is noise that costs the frame budget and the user's attention.
|
|
11
|
+
|
|
12
|
+
## M0 The frequency gate, first
|
|
13
|
+
|
|
14
|
+
Ask how often the user sees this interaction. That number decides everything else:
|
|
15
|
+
|
|
16
|
+
| Seen | Verdict |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| 100+ times a day (a button, a form field, a list row, a tab) | **No animation.** Feedback is a color or a state, not a movement. |
|
|
19
|
+
| 10–100 times (a modal, a drawer, a filter applied) | 120–250ms, one property, no choreography. |
|
|
20
|
+
| First time only (an onboarding screen, a hero, a landing page) | This is where the budget lives: entrance, stagger, one parallax. |
|
|
21
|
+
|
|
22
|
+
A screen the user works in all day gets *less* motion than a page they visit once.
|
|
23
|
+
If you cannot say which row of this table the interaction is in, it gets no
|
|
24
|
+
animation.
|
|
25
|
+
|
|
26
|
+
## M1 Pick the shortest API that says it
|
|
27
|
+
|
|
28
|
+
- **The named transitions** — the three gestures a product repeats, with the
|
|
29
|
+
distance and the duration already chosen:
|
|
30
|
+
`slideIn(widget, { direction, distance, duration, easing, fade })`,
|
|
31
|
+
`slideOut(…)` (the mirror, and the unmount follows it),
|
|
32
|
+
`popIn(widget, { from, to, duration, easing })`,
|
|
33
|
+
`shake(widget, { axis, distance, cycles, duration })`. Each returns a cancel
|
|
34
|
+
function; each has an `…Async` form that resolves with the widget. Use these
|
|
35
|
+
before reaching for `animate`: they are one call instead of three, and they
|
|
36
|
+
already obey M7.
|
|
37
|
+
- **The imperative helpers** — `animate(widget, property, from, to, duration, easing)`
|
|
38
|
+
for one property, `animateAsync` when the next step has to wait, `fadeIn` /
|
|
39
|
+
`fadeOut` / `pulse` (+ the `…Async` forms), `applyPulse`, and
|
|
40
|
+
`stopAnimation(widget, property)` for the cleanup you did not keep a handle for.
|
|
41
|
+
`animateSpring(widget, property, from, to, { stiffness, damping })` when the
|
|
42
|
+
movement should settle like an object instead of stopping like a timer.
|
|
43
|
+
- **`AnimatedBox`** — declarative keyframes on one child:
|
|
44
|
+
`AnimatedBox({ child, animations: [{ effect, from, to, duration, reverse, loop, delay }], timing, delay, fillMode })`.
|
|
45
|
+
Each step carries its own `duration`, `delay` and `loop` and each one is read off
|
|
46
|
+
that step: a 600 ms `scale` and a 400 ms `opacity` in the same entrance are one
|
|
47
|
+
rule that ends at 600 ms with the fade holding from 400 ms on. Times are
|
|
48
|
+
milliseconds as a number (`duration: 400`) or a CSS time as a string
|
|
49
|
+
(`delay: "0.4s"`); the widget-level `delay` is the default for the steps that do
|
|
50
|
+
not name one. A step that loops and a step that plays once cannot share a rule —
|
|
51
|
+
repeating the rule repeats the entrance too — so they become two `@keyframes`, and
|
|
52
|
+
two rules cannot compose one `transform`: keep the transform steps in one group.
|
|
53
|
+
- **`AnimatedText`** — the same steps across the characters of a text, with
|
|
54
|
+
`delayBetween` in **seconds** (`0.05` is a 50 ms stagger; `40` is forty seconds)
|
|
55
|
+
and `sameTime: true` to fire it at once.
|
|
56
|
+
- **`ParallaxBox`** — offset driven by input: `type: 'scroll' | 'mouse' | 'hover'`,
|
|
57
|
+
`speed`, `direction: 'vertical' | 'horizontal' | 'both'`, `maxOffset`, `reverse`,
|
|
58
|
+
`disabled`, `onParallaxMove`.
|
|
59
|
+
|
|
60
|
+
Anything else — a hand-written `transition: all`, a CSS library, a `requestAnimationFrame`
|
|
61
|
+
loop in a widget — is a second animation system, and this app already has one. A
|
|
62
|
+
hand-rolled loop is also the one way to ship motion that ignores reduced motion,
|
|
63
|
+
because the engine is what reads the preference.
|
|
64
|
+
|
|
65
|
+
**Effects `AnimatedBox` knows:** `scale`, `scaleX`, `scaleY`, `rotate`, `rotateX`,
|
|
66
|
+
`rotateY`, `rotateZ`, `skewX`, `skewY`, `translateX`, `translateY`, `translateZ`,
|
|
67
|
+
`opacity`, `color`, `backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`,
|
|
68
|
+
`width`, `height`, `boxShadow`, `background`, `textGradient`. Anything outside that list
|
|
69
|
+
does nothing, silently. The spacing effects (margin and padding, per side) work as
|
|
70
|
+
well — they are routed by prefix, so the gate cannot see them by name.
|
|
71
|
+
|
|
72
|
+
**Properties `animate()` moves:** the same names, plus any real CSS property
|
|
73
|
+
(`width`, `opacity`, `borderRadius`, `color`…). A bare number is a **framework unit**,
|
|
74
|
+
exactly as it is in a widget: `translateY: 20` is `20 / base-font-size` rem, so the
|
|
75
|
+
movement scales with the reader's font instead of freezing at 20 pixels. A unit
|
|
76
|
+
string keeps its unit (`"16px"`), a color blends whether you write `#rrggbb`,
|
|
77
|
+
`rgb(…)` or `rgba(…)`, and a pair the framework cannot blend (`"red"` → `"blue"`)
|
|
78
|
+
holds the start and lands on the end rather than writing `NaN`. `translateX`,
|
|
79
|
+
`translateY` and `scale` are separate loops whose outputs are merged into one
|
|
80
|
+
`transform` string, so an element can slide and grow at the same time.
|
|
81
|
+
|
|
82
|
+
## M2 Duration follows the distance, and the interaction
|
|
83
|
+
|
|
84
|
+
Milliseconds, in the ranges that read as "instant" instead of "smooth" — smooth is
|
|
85
|
+
what slow feels like after the third repetition:
|
|
86
|
+
|
|
87
|
+
| Interaction | duration | What moves |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| Press feedback | 100–160 | `scale` 1 → 0.97 |
|
|
90
|
+
| Hover / focus state | 120–180 | `opacity`, `borderColor` |
|
|
91
|
+
| Tooltip, chip, badge | 125–200 | `opacity` |
|
|
92
|
+
| Menu, dropdown, sheet | 150–250 | `opacity` + `translateY` 8 → 0 |
|
|
93
|
+
| Modal, drawer | 200–400 | `translateX`/`translateY`, never scale |
|
|
94
|
+
| Screen entrance, hero | 300–500 | `opacity` + `translateY` 16 → 0 |
|
|
95
|
+
| Rejected input | 300–360 | `shake`, never a color flash alone |
|
|
96
|
+
| Loading | loop | `Skeleton({ animation: 'wave' })` |
|
|
97
|
+
|
|
98
|
+
Nothing in the UI exceeds 500ms. A 300ms number on every element is not a design
|
|
99
|
+
decision — it is the default.
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
// A sheet entering: the whole gesture is one call.
|
|
103
|
+
slideIn(panel, { direction: 'up', distance: 24, duration: 220 });
|
|
104
|
+
|
|
105
|
+
// …and leaving is the same sentence in the other direction, then the unmount.
|
|
106
|
+
await slideOutAsync(panel, { direction: 'down' });
|
|
107
|
+
panel.remove();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## M3 Two easing vocabularies — do not mix them
|
|
111
|
+
|
|
112
|
+
This is a real trap, and it fails silently:
|
|
113
|
+
|
|
114
|
+
- **`animate(..., easing)`** takes a *framework name* — one of `EASING_NAMES` — or
|
|
115
|
+
a curve (`cubicBezier(0.2, 0, 0, 1)`, `spring().easing`). An unrecognized string
|
|
116
|
+
warns and falls back to `linear`.
|
|
117
|
+
- **`AnimatedBox({ timing })` and `ParallaxBox({ easing })`** are passed straight to
|
|
118
|
+
CSS: `'ease' | 'ease-out' | 'ease-in' | 'linear' | 'cubic-bezier(…)'`. `easeIn` in
|
|
119
|
+
a `timing` string is not a curve CSS knows, and the step runs linear. `ease-out`
|
|
120
|
+
in `animate()` is not a framework name, and it runs linear too.
|
|
121
|
+
|
|
122
|
+
The names the engine knows: `linear`, `easeIn`, `easeOut`, `easeInOut`, the powers
|
|
123
|
+
(`easeOutCubic`, `easeInOutQuart`, `easeOutExpo`, `easeOutCirc` and their `In` /
|
|
124
|
+
`InOut` siblings), the overshoot family (`easeOutBack`, `easeInOutBack`,
|
|
125
|
+
`easeOutElastic`, `easeOutBounce`), and the Material shapes (`standard`,
|
|
126
|
+
`emphasized`, `emphasizedDecelerate`, `emphasizedAccelerate`).
|
|
127
|
+
|
|
128
|
+
Rules that hold for both:
|
|
129
|
+
|
|
130
|
+
- Entering and exiting UI use an **out** curve (`easeOut` / `ease-out`) — it starts
|
|
131
|
+
fast, which reads as responsive.
|
|
132
|
+
- **Never** `easeIn` on an interface element: it starts slow, so the first 100ms of
|
|
133
|
+
a press feels broken. Save it for something leaving the screen at speed.
|
|
134
|
+
- A loop that has to feel like a machine uses `linear`.
|
|
135
|
+
- Use the overshoot family for one thing per screen at most. `easeOutElastic` on a
|
|
136
|
+
modal is a toy; on a badge that just arrived it is a highlight.
|
|
137
|
+
- Never `transition: all`, and never animate `width`, `height`, `top`, `left` or
|
|
138
|
+
`boxShadow` for something the user presses repeatedly: they reflow or repaint the
|
|
139
|
+
page. `AnimatedBox` *can* animate them, which is exactly why this rule needs to be
|
|
140
|
+
written down. Transform and opacity are the two properties that cost nothing:
|
|
141
|
+
`scale`, `translateX/Y`, `rotate`, `opacity`.
|
|
142
|
+
- Nothing enters from `scale: 0`, and nothing from full scale on a big element: use
|
|
143
|
+
**0.9–0.97**. From zero, the element is a dot that pops. That is `popIn`'s default.
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// The curve a stylesheet would use, on the imperative side of the same screen.
|
|
147
|
+
animate(card, 'translateY', 24, 0, 240, cubicBezier(0.2, 0, 0, 1));
|
|
148
|
+
|
|
149
|
+
// Something the pointer just flicked finishes like an object, not like a timer.
|
|
150
|
+
animateSpring(button, 'scale', 0.96, 1, { stiffness: 260, damping: 20 });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## M4 Press feedback, done right
|
|
154
|
+
|
|
155
|
+
A press is 100+ times a day (M0): the answer is a state change, not a show.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
// The whole of it: 140ms, 3% of scale, back on release.
|
|
159
|
+
onPress: (widget) => animate(widget, 'scale', 1, 0.97, 140, 'easeOut');
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`Button` already changes color on press. Adding a lift, a glow and a scale on top
|
|
163
|
+
of that is three simultaneous answers to one question. Pick one — and if the color
|
|
164
|
+
change is enough, which it usually is, ship it and move on.
|
|
165
|
+
|
|
166
|
+
`shake` is not press feedback. It is the answer to "the server said no": 320ms,
|
|
167
|
+
three decaying swings, and it ends at rest so the field does not stay off its
|
|
168
|
+
column. Call it once per rejection, not on every keystroke.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
shake(loginForm, { axis: 'x', distance: 6, cycles: 3 });
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## M5 Entrance and stagger
|
|
177
|
+
|
|
178
|
+
Motion on first paint is the one place a longer budget is fair, with two limits:
|
|
179
|
+
|
|
180
|
+
- Stagger **30–80ms** between siblings, and cap the total. `AnimatedText` gives this
|
|
181
|
+
for free with `delayBetween: 0.05` — that prop is in **seconds**, the unit the
|
|
182
|
+
framework writes into its `animation-delay`, so `40` would not be a fast stagger
|
|
183
|
+
but forty seconds. A hand-built list uses `stagger(index)` —
|
|
184
|
+
`stagger(3, { step: 60, max: 8 })` stops growing after 8 items, which is the
|
|
185
|
+
difference between a screen arriving and a screen loading: a list of 20 rows at
|
|
186
|
+
60ms each is a 1.2s wait before the content is readable.
|
|
187
|
+
- One direction per screen. Elements arriving from the left *and* the bottom *and*
|
|
188
|
+
scaled is three ideas. `slideIn` is one direction plus opacity, on purpose.
|
|
189
|
+
|
|
190
|
+
## M6 Loops, ambient motion and `ParallaxBox`
|
|
191
|
+
|
|
192
|
+
- A loop is a status. `loop: true`, `pulse`, `applyPulse` and `MatrixRain` say
|
|
193
|
+
"waiting" or "this is the one". If it never stops and means nothing, turn it off.
|
|
194
|
+
- Loading uses `Skeleton({ animation: 'wave', count, gap, height })` in the shape of
|
|
195
|
+
the content that will replace it — not a spinner in the middle of a blank page.
|
|
196
|
+
- `ParallaxBox` belongs to a hero or a single decorative layer, never inside a
|
|
197
|
+
working screen, and `type: 'mouse'` never on a touch-first app. Keep `maxOffset`
|
|
198
|
+
small (8–24). A background that moves 120px is not depth, it is a screensaver.
|
|
199
|
+
- Ambient motion gets its own budget: one per screen, at most.
|
|
200
|
+
|
|
201
|
+
## M7 Reduced motion is decided once, in the engine
|
|
202
|
+
|
|
203
|
+
The user can ask for no movement, and FletBox listens:
|
|
204
|
+
|
|
205
|
+
- `prefersReducedMotion()` — `true` when they have.
|
|
206
|
+
- `setMotionMode('auto' | 'reduce' | 'full')` — the app's own switch, wired to a
|
|
207
|
+
settings control; `'auto'` (the default) follows the operating system.
|
|
208
|
+
- `getMotionMode()`, and `onMotionModeChange(reduced => …)` for a widget holding a
|
|
209
|
+
live loop — it returns the unsubscribe, so release it in `onUnmount`.
|
|
210
|
+
|
|
211
|
+
What it does to each surface, and why none of it needs a per-screen `if`:
|
|
212
|
+
|
|
213
|
+
- `animate()` and the named transitions run one frame that **writes the end value**,
|
|
214
|
+
so the state still lands and an `await` still resolves. Only the movement goes.
|
|
215
|
+
- `AnimatedBox` plays its keyframes once at `0.01ms` with no delay, and `fillMode:
|
|
216
|
+
'forwards'` leaves the element on its `to` values — content never stays hidden
|
|
217
|
+
waiting for an animation that was not allowed to run. A `loop` stops looping.
|
|
218
|
+
The reader can flip the OS setting *while the app is open*: the engine injects one
|
|
219
|
+
`@media (prefers-reduced-motion: reduce)` rule that overrides every element it
|
|
220
|
+
animates, so a loop already running stops without a rebuild and without a listener
|
|
221
|
+
to leak. The factory marks any element it gives an `infinite` animation — a
|
|
222
|
+
`Skeleton`, a striped `ProgressBar`, a `ListView` spinner — so a loop you write
|
|
223
|
+
yourself through the `animation` prop is covered too. Nothing in the app has to do
|
|
224
|
+
anything about it.
|
|
225
|
+
- `ParallaxBox` parks its layer at the origin. `MatrixRain` paints one static field
|
|
226
|
+
and releases its `requestAnimationFrame`. `applyPulse` does nothing.
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
const chooseMotion = (on) => {
|
|
230
|
+
if (on) return setMotionMode('reduce');
|
|
231
|
+
return setMotionMode('auto');
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// A settings screen hands the decision to the reader, and the engine applies it.
|
|
235
|
+
Switch({ label: 'Reduce motion', value: getMotionMode() === 'reduce', onToggle: chooseMotion });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Build the screen so it reads with zero animation anyway. Motion is a decision you
|
|
239
|
+
can point at, and if it is only decoration the honest answer is to not add it —
|
|
240
|
+
reduced motion is not a second design to maintain, it is the same design with the
|
|
241
|
+
movable parts still.
|
|
242
|
+
|
|
243
|
+
## M8 Clean up
|
|
244
|
+
|
|
245
|
+
An animation is a resource. `animate()` and the presets return a cancel function —
|
|
246
|
+
hold it and call it when the widget unmounts, or call `stopAnimation(widget, property)`.
|
|
247
|
+
A loop that outlives its widget keeps a `requestAnimationFrame` alive and animates a
|
|
248
|
+
detached node forever. A widget that registers timers, listeners or observers releases
|
|
249
|
+
them in its `onUnmount` disposer.
|
|
250
|
+
|
|
251
|
+
Starting a second animation on the same widget and property replaces the first — that
|
|
252
|
+
is the engine, not a bug to work around. Two gestures on one element (`translateY` plus
|
|
253
|
+
`scale`) are two loops and are merged into one `transform`, so they do not fight.
|
|
254
|
+
|
|
255
|
+
## M9 The check
|
|
256
|
+
|
|
257
|
+
1. `npm run check` — every prop named exists (`flet-box unpack AnimatedBox`).
|
|
258
|
+
2. `npm run ui`.
|
|
259
|
+
3. Watch it, twice, at 375px. Then answer: how many times a day does a real user
|
|
260
|
+
see this? If the answer is "a lot", the correct fix is usually to delete the
|
|
261
|
+
animation, not to tune it.
|
|
262
|
+
4. Check the frame, not the curve: press a control while something else moves. If
|
|
263
|
+
the press feels delayed, the motion is the cause.
|
|
264
|
+
5. Turn the mode to `'reduce'` and walk the screen. Everything must still arrive,
|
|
265
|
+
still be readable, still be operable — with nothing moving. That is the test
|
|
266
|
+
reduced motion asks for, and it is five minutes.
|