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.
Files changed (132) hide show
  1. package/bin/commands/checkUi.js +327 -46
  2. package/bin/commands/create.js +48 -19
  3. package/bin/commands/createScreen.js +1 -1
  4. package/bin/commands/runBundle.js +1 -1
  5. package/bin/utils/colors.js +0 -8
  6. package/bin/utils/helpers.js +0 -5
  7. package/bin/utils/images/jpeg.js +2 -3
  8. package/bin/utils/skills.js +59 -0
  9. package/bin/utils/templates.js +196 -575
  10. package/docs/README.md +3 -1
  11. package/docs/cli/README.md +18 -1
  12. package/docs/guides/README.md +6 -1
  13. package/docs/guides/accessibility.md +282 -0
  14. package/docs/guides/clean-ui.md +84 -21
  15. package/docs/guides/minification-and-protection.md +1 -1
  16. package/docs/guides/mobile-and-platforms.md +51 -0
  17. package/docs/guides/utilities.md +116 -4
  18. package/docs/guides/widget-structure.md +30 -0
  19. package/docs/tools/README.md +1 -1
  20. package/docs/tools/animation.md +134 -11
  21. package/docs/tools/device.md +75 -3
  22. package/docs/tools/text-and-time.md +116 -6
  23. package/docs/widget/Accordion.md +2 -0
  24. package/docs/widget/AnimatedBox.md +46 -20
  25. package/docs/widget/AnimatedText.md +13 -7
  26. package/docs/widget/Badge.md +1 -0
  27. package/docs/widget/COMMON_PROPS.md +26 -3
  28. package/docs/widget/Chart.md +1 -0
  29. package/docs/widget/Checkbox.md +7 -1
  30. package/docs/widget/CircularChart.md +2 -0
  31. package/docs/widget/Drawer.md +1 -0
  32. package/docs/widget/GridView.md +4 -1
  33. package/docs/widget/Input.md +2 -0
  34. package/docs/widget/InstallButton.md +1 -0
  35. package/docs/widget/Markdown.md +4 -0
  36. package/docs/widget/Rating.md +2 -0
  37. package/docs/widget/Slider.md +2 -0
  38. package/docs/widget/Stack.md +6 -5
  39. package/docs/widget/Stepper.md +2 -0
  40. package/docs/widget/Tooltip.md +3 -1
  41. package/package.json +4 -2
  42. package/skills/fletbox-design/SKILL.md +949 -0
  43. package/skills/fletbox-memory/SKILL.md +134 -0
  44. package/skills/fletbox-motion/SKILL.md +266 -0
  45. package/skills/fletbox-rules/SKILL.md +293 -0
  46. package/src/animations/AnimatedBox.js +321 -226
  47. package/src/animations/MatrixRain.js +7 -1
  48. package/src/animations/ParallaxBox.js +13 -10
  49. package/src/animations/animate.js +269 -106
  50. package/src/animations/color.js +91 -0
  51. package/src/animations/easings.js +275 -0
  52. package/src/animations/index.js +21 -0
  53. package/src/animations/motion.js +204 -0
  54. package/src/animations/transitions.js +327 -0
  55. package/src/core/runApp.js +0 -6
  56. package/src/helpers/boxEdges.js +69 -0
  57. package/src/helpers/html.js +39 -0
  58. package/src/index.d.ts +377 -25
  59. package/src/index.js +77 -0
  60. package/src/navigations/AdaptiveScaffold.js +6 -2
  61. package/src/navigations/AppBar.js +16 -7
  62. package/src/navigations/BottomNavigation.js +24 -1
  63. package/src/navigations/CollapsibleSideBar.js +0 -1
  64. package/src/navigations/Drawer.js +32 -1
  65. package/src/navigations/DrawerItem.js +1 -1
  66. package/src/navigations/Router.js +0 -1
  67. package/src/navigations/Scaffold.js +9 -3
  68. package/src/navigations/Tabs.js +104 -31
  69. package/src/services/RamStore.js +1 -2
  70. package/src/tools/breakpoints.js +113 -0
  71. package/src/tools/createList.js +0 -2
  72. package/src/tools/fonts.js +99 -0
  73. package/src/tools/index.js +74 -1
  74. package/src/tools/margin.js +5 -63
  75. package/src/tools/padding.js +5 -63
  76. package/src/tools/print.js +0 -8
  77. package/src/tools/scale.js +9 -2
  78. package/src/tools/time.js +56 -15
  79. package/src/tools/useState.js +1 -54
  80. package/src/utils/fieldFont.js +77 -0
  81. package/src/utils/focusRing.js +73 -0
  82. package/src/utils/focusScope.js +113 -0
  83. package/src/utils/markdownParser.js +7 -9
  84. package/src/utils/readableInk.js +7 -1
  85. package/src/utils/stackLayout.js +55 -0
  86. package/src/utils/styleInjector.js +25 -0
  87. package/src/utils/syntaxHighlight.js +1 -17
  88. package/src/utils/themes.js +31 -0
  89. package/src/utils/visualEffects.js +49 -47
  90. package/src/widget-factory/createWidget.js +16 -3
  91. package/src/widget-factory/effects.js +76 -3
  92. package/src/widget-factory/reactiveValue.js +0 -7
  93. package/src/widget-factory/tools.js +26 -143
  94. package/src/widget-factory/translateProps.js +5 -0
  95. package/src/widget-factory/widgetFactory.js +1 -1
  96. package/src/widgets/Accordion.js +70 -12
  97. package/src/widgets/AdMob.js +1 -1
  98. package/src/widgets/AdSense.js +1 -1
  99. package/src/widgets/AlertDialog.js +11 -9
  100. package/src/widgets/Avatar.js +10 -1
  101. package/src/widgets/Badge.js +7 -1
  102. package/src/widgets/BottomSheet.js +39 -5
  103. package/src/widgets/Chart.js +41 -9
  104. package/src/widgets/Checkbox.js +59 -9
  105. package/src/widgets/Chip.js +13 -1
  106. package/src/widgets/CircularBar.js +17 -5
  107. package/src/widgets/CircularChart.js +24 -2
  108. package/src/widgets/CodeViewer.js +5 -3
  109. package/src/widgets/Dropdown.js +223 -20
  110. package/src/widgets/GridView.js +17 -1
  111. package/src/widgets/Icon.js +21 -0
  112. package/src/widgets/Input.js +75 -44
  113. package/src/widgets/InstallButton.js +8 -2
  114. package/src/widgets/ListTile.js +6 -5
  115. package/src/widgets/ListView.js +21 -7
  116. package/src/widgets/Markdown.js +25 -44
  117. package/src/widgets/Modal.js +43 -6
  118. package/src/widgets/Pagination.js +35 -3
  119. package/src/widgets/ProgressBar.js +18 -0
  120. package/src/widgets/Radio.js +7 -0
  121. package/src/widgets/Rating.js +104 -16
  122. package/src/widgets/Skeleton.js +9 -5
  123. package/src/widgets/Slider.js +87 -7
  124. package/src/widgets/SnackBar.js +30 -1
  125. package/src/widgets/Stack.js +25 -13
  126. package/src/widgets/Stepper.js +112 -64
  127. package/src/widgets/Switch.js +10 -1
  128. package/src/widgets/Tooltip.js +93 -10
  129. package/src/widgets/TreeView.js +5 -5
  130. package/src/create-index.sh +0 -237
  131. package/src/widget-factory/pixTool.js +0 -40
  132. 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.