flet-box 1.1.0 → 1.1.2
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/README.md +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/templates.js +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +15 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fletbox-motion
|
|
3
|
-
description: When a FletBox screen may move and with which API - AnimatedBox, AnimatedText, ParallaxBox, animate/fadeIn/pulse, the
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Motion — the only animation contract this app has
|
|
@@ -23,27 +23,61 @@ A screen the user works in all day gets *less* motion than a page they visit onc
|
|
|
23
23
|
If you cannot say which row of this table the interaction is in, it gets no
|
|
24
24
|
animation.
|
|
25
25
|
|
|
26
|
-
## M1
|
|
27
|
-
|
|
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.
|
|
28
43
|
- **`AnimatedBox`** — declarative keyframes on one child:
|
|
29
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.
|
|
30
53
|
- **`AnimatedText`** — the same steps across the characters of a text, with
|
|
31
|
-
`delayBetween` (a
|
|
54
|
+
`delayBetween` in **seconds** (`0.05` is a 50 ms stagger; `40` is forty seconds)
|
|
55
|
+
and `sameTime: true` to fire it at once.
|
|
32
56
|
- **`ParallaxBox`** — offset driven by input: `type: 'scroll' | 'mouse' | 'hover'`,
|
|
33
57
|
`speed`, `direction: 'vertical' | 'horizontal' | 'both'`, `maxOffset`, `reverse`,
|
|
34
58
|
`disabled`, `onParallaxMove`.
|
|
35
|
-
- **The imperative helpers** — `animate(widget, property, from, to, duration, easing)`
|
|
36
|
-
which returns a cancel function, `animateAsync` which returns a Promise,
|
|
37
|
-
`fadeIn` / `fadeOut` / `pulse` (+ the `…Async` forms), `applyPulse`,
|
|
38
|
-
`stopAnimation(widget, property)`.
|
|
39
59
|
|
|
40
60
|
Anything else — a hand-written `transition: all`, a CSS library, a `requestAnimationFrame`
|
|
41
|
-
loop in a widget — is a second animation system, and this app already has one.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`
|
|
46
|
-
`
|
|
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.
|
|
47
81
|
|
|
48
82
|
## M2 Duration follows the distance, and the interaction
|
|
49
83
|
|
|
@@ -58,31 +92,38 @@ what slow feels like after the third repetition:
|
|
|
58
92
|
| Menu, dropdown, sheet | 150–250 | `opacity` + `translateY` 8 → 0 |
|
|
59
93
|
| Modal, drawer | 200–400 | `translateX`/`translateY`, never scale |
|
|
60
94
|
| Screen entrance, hero | 300–500 | `opacity` + `translateY` 16 → 0 |
|
|
95
|
+
| Rejected input | 300–360 | `shake`, never a color flash alone |
|
|
61
96
|
| Loading | loop | `Skeleton({ animation: 'wave' })` |
|
|
62
97
|
|
|
63
98
|
Nothing in the UI exceeds 500ms. A 300ms number on every element is not a design
|
|
64
99
|
decision — it is the default.
|
|
65
100
|
|
|
66
101
|
```js
|
|
67
|
-
// A sheet entering:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
],
|
|
74
|
-
});
|
|
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();
|
|
75
108
|
```
|
|
76
109
|
|
|
77
110
|
## M3 Two easing vocabularies — do not mix them
|
|
78
111
|
|
|
79
112
|
This is a real trap, and it fails silently:
|
|
80
113
|
|
|
81
|
-
- **`animate(..., easing)`** takes a *framework name
|
|
82
|
-
|
|
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`.
|
|
83
117
|
- **`AnimatedBox({ timing })` and `ParallaxBox({ easing })`** are passed straight to
|
|
84
118
|
CSS: `'ease' | 'ease-out' | 'ease-in' | 'linear' | 'cubic-bezier(…)'`. `easeIn` in
|
|
85
|
-
a `timing` string is not a curve CSS knows, and the step runs linear.
|
|
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`).
|
|
86
127
|
|
|
87
128
|
Rules that hold for both:
|
|
88
129
|
|
|
@@ -91,13 +132,23 @@ Rules that hold for both:
|
|
|
91
132
|
- **Never** `easeIn` on an interface element: it starts slow, so the first 100ms of
|
|
92
133
|
a press feels broken. Save it for something leaving the screen at speed.
|
|
93
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.
|
|
94
137
|
- Never `transition: all`, and never animate `width`, `height`, `top`, `left` or
|
|
95
138
|
`boxShadow` for something the user presses repeatedly: they reflow or repaint the
|
|
96
139
|
page. `AnimatedBox` *can* animate them, which is exactly why this rule needs to be
|
|
97
140
|
written down. Transform and opacity are the two properties that cost nothing:
|
|
98
141
|
`scale`, `translateX/Y`, `rotate`, `opacity`.
|
|
99
142
|
- Nothing enters from `scale: 0`, and nothing from full scale on a big element: use
|
|
100
|
-
**0.9–0.97**. From zero, the element is a dot that pops.
|
|
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
|
+
```
|
|
101
152
|
|
|
102
153
|
## M4 Press feedback, done right
|
|
103
154
|
|
|
@@ -112,16 +163,29 @@ onPress: (widget) => animate(widget, 'scale', 1, 0.97, 140, 'easeOut');
|
|
|
112
163
|
of that is three simultaneous answers to one question. Pick one — and if the color
|
|
113
164
|
change is enough, which it usually is, ship it and move on.
|
|
114
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
|
+
|
|
115
176
|
## M5 Entrance and stagger
|
|
116
177
|
|
|
117
178
|
Motion on first paint is the one place a longer budget is fair, with two limits:
|
|
118
179
|
|
|
119
180
|
- Stagger **30–80ms** between siblings, and cap the total. `AnimatedText` gives this
|
|
120
|
-
for free with `delayBetween:
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
123
187
|
- One direction per screen. Elements arriving from the left *and* the bottom *and*
|
|
124
|
-
scaled is three ideas.
|
|
188
|
+
scaled is three ideas. `slideIn` is one direction plus opacity, on purpose.
|
|
125
189
|
|
|
126
190
|
## M6 Loops, ambient motion and `ParallaxBox`
|
|
127
191
|
|
|
@@ -134,28 +198,59 @@ Motion on first paint is the one place a longer budget is fair, with two limits:
|
|
|
134
198
|
small (8–24). A background that moves 120px is not depth, it is a screensaver.
|
|
135
199
|
- Ambient motion gets its own budget: one per screen, at most.
|
|
136
200
|
|
|
137
|
-
## M7
|
|
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.
|
|
138
227
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
```
|
|
146
237
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
motion is
|
|
150
|
-
|
|
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.
|
|
151
242
|
|
|
152
243
|
## M8 Clean up
|
|
153
244
|
|
|
154
|
-
An animation is a resource. `animate()`
|
|
155
|
-
it when the widget unmounts, or call `stopAnimation(widget, property)`.
|
|
156
|
-
outlives its widget keeps a `requestAnimationFrame` alive and animates a
|
|
157
|
-
node forever. A widget that registers timers, listeners or observers releases
|
|
158
|
-
its `onUnmount` disposer.
|
|
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.
|
|
159
254
|
|
|
160
255
|
## M9 The check
|
|
161
256
|
|
|
@@ -166,3 +261,6 @@ its `onUnmount` disposer.
|
|
|
166
261
|
animation, not to tune it.
|
|
167
262
|
4. Check the frame, not the curve: press a control while something else moves. If
|
|
168
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fletbox-rules
|
|
3
|
-
description: The working contract for a FletBox app - FletBox syntax only, never a widget or prop from React/Vue/Svelte/Flutter, look an API up before using it, the rem unit contract, prop aliases, key-based state, resource cleanup and the two gates that prove the work. Read first, before any other skill, and before writing or editing a single file in src/.
|
|
3
|
+
description: The working contract for a FletBox app - FletBox syntax only, never a widget or prop from React/Vue/Svelte/Flutter, look an API up before using it, the rem unit contract, prop aliases, key-based state, resource cleanup, one home for every shared helper, and the two gates that prove the work. Read first, before any other skill, and before writing or editing a single file in src/.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Rules — how to work inside this framework
|
|
@@ -152,3 +152,142 @@ learns something and does not write it is a session that has to be repeated.
|
|
|
152
152
|
They cover the same screen from three angles and do not repeat each other. When
|
|
153
153
|
they disagree, this file wins about the API, `fletbox-design` wins about the
|
|
154
154
|
screen, and the checker wins about a value.
|
|
155
|
+
|
|
156
|
+
## R12 The tools are the short form — use one before you hand-write it
|
|
157
|
+
|
|
158
|
+
`flet-box` exports 123 helpers alongside the widgets. They exist because the code
|
|
159
|
+
they replace is longer, and because the longer version is the one every model
|
|
160
|
+
writes from memory. The rule is not "wrap everything in a helper": it is that a
|
|
161
|
+
hand-written replacement has to be **shorter or clearer** than the tool, and for
|
|
162
|
+
the list below it is not.
|
|
163
|
+
|
|
164
|
+
| What you were about to write | Use instead |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `Array.from({ length: n }, (_, i) => …)`, and a second branch for an object | `mapList(source, fn)` — takes an array, a number or an object |
|
|
167
|
+
| a mock list built field by field | `createList(row, 12)` with `row = { name: () => random.fullName(), price: () => random.number(5, 40) }` |
|
|
168
|
+
| `list.map` + `sort` + `[...new Set(…)]` chains | `sort(rows, 'price', 'desc')`, `unique(xs)`, `shuffle`, `chunk`, `reverse` |
|
|
169
|
+
| a `for (let i = 0; …)` that only builds children | `range(0, 10)`, `repeat(3, Divider({}))` |
|
|
170
|
+
| `'0px 4px 12px 0px rgba(…)'` typed out | `shadow(0, 4, 12, 0, rgba(0, 0, 0, 0.12))` |
|
|
171
|
+
| `` `rgba(${r},${g},${b},${a})` `` and `` `linear-gradient(…)` `` | `rgba(37, 99, 235, 0.2)`, `gradient('linear', [a, b], 135)` |
|
|
172
|
+
| a `transform` string you have to keep in sync by hand | `transform({ translate: [10, 20], rotate: 45, scale: 1.5 })` |
|
|
173
|
+
| `'all 0.3s ease 0s'`, `'blur(5px) brightness(0.8)'` | `transition({ property: 'opacity', duration: 0.3 })`, `filter({ blur: 5, brightness: 0.8 })` |
|
|
174
|
+
| a hex you re-lighten by eye, or hand-picked tints | `color(colors.primary).darken(12).hex()` |
|
|
175
|
+
| `Math.floor(Math.random() * …)` and `Math.random().toString(36)` | `random.number(a, b)`, `random.choice(list)`, `shortId()`, `uuid()`, `timestampId()` |
|
|
176
|
+
| `new Date(d).toLocaleDateString()` chains, a hand-written `"… ago"` | `formatDate(d, 'YYYY-MM-DD')`, `formatDateLong(d)`, `formatTimeAmPm(d)`, `relativeTime(iso)`, `timeAgoLocale(d, 'es')` |
|
|
177
|
+
| `d.getTime() + 7 * 86400000` and a `(b - a) / 86400000` you have to re-derive | `addDays(d, 7)`, `subtractHours(d, 3)`, `dateDiff(a, b, 'days')`, and `days(7)` where you need the milliseconds |
|
|
178
|
+
| `new Date().getDay() === 0`, a weekday name built from an array | `dayOfWeek(d)`, `monthName(d)`, `isWeekend(d)`, `isToday(d)` |
|
|
179
|
+
| `new Promise(r => setTimeout(r, ms))`, a retry loop written twice, a poll loop written twice | `delay(ms)`, `withMinDelay(p, 600)` (a spinner that never flashes), `retry(fn, 3, 500)`, `waitUntil(() => ready, seconds(30))` |
|
|
180
|
+
| a timer loop plus a handle you have to remember to clear | `everySecond(fn)`, `every(minutes(5), fn)` — and per R6, release the id it hands back on unmount |
|
|
181
|
+
| a `const cache = {}` keyed by `JSON.stringify(args)` | `memo(fn)`, `memoWithKey(fn, keyFn)` |
|
|
182
|
+
| `obj[k] !== undefined ? obj[k] : d`, `Object.entries(o).map(…)` | `dict(obj).get(k, d)`, `.items()`, `.has(k)`, `fromJSON(text)` |
|
|
183
|
+
| `console.log('[' + label + '] ' + msg)` | `print('synced', 'success', { label: 'cart' })` |
|
|
184
|
+
| `let el = null` plus `ref: (v) => (el = v)` | `const display = ref()`, then `Text({ ref: display })` and `display.update({ text: '…' })` |
|
|
185
|
+
| `window.innerWidth >= 768`, and a resize listener that fires per pixel | `atLeast('md')`, `pick({ xs: 16, md: 24 })`, `onBreakpointChange(cb)` |
|
|
186
|
+
| `navigator.clipboard.writeText(x)` (which throws without a gesture fallback) | `clipboard.copy(x)` |
|
|
187
|
+
| injecting a `<link>` for a webfont | `loadFont('Inter', { weights: [400, 600] })`, then `fontsReady()` before a canvas paint |
|
|
188
|
+
| `String(arr).split(sep)`, `s.trim().startsWith(x)` written as three calls | `split(s, sep)`, `join(arr, sep)`, `trim(s)`, `startsWith(s, x)`, `endsWith(s, x)`, `includes(s, x)` |
|
|
189
|
+
| a `theme` object rebuilt per screen from hex literals | `colors.*`, `setTheme`, `subscribeTheme` |
|
|
190
|
+
|
|
191
|
+
Four traps, because the rule is only honest with them:
|
|
192
|
+
|
|
193
|
+
1. **A routed prop already *is* the short form.** `Container({ padding: 16 })` is
|
|
194
|
+
shorter and more correct than `padding: padding(16)` — the number reaches the
|
|
195
|
+
rem contract; the helper returns a **string** of pixels. Reach for
|
|
196
|
+
`padding(...)`/`margin(...)` only when you are writing a CSS value the props
|
|
197
|
+
cannot express (a `styles` object, a computed shorthand). Asymmetric sides have
|
|
198
|
+
props: `px: 20, py: 12`, `pt`, `pr`, `pb`, `pl`.
|
|
199
|
+
2. **`filter` is the CSS filter, not a list filter.** `mapList`, `sort`, `unique`
|
|
200
|
+
and `createList` do list work; `filter({ blur: 5 })` builds a string.
|
|
201
|
+
3. **`color()` is a chain, not a value.** `color(x).lighten(20)` is a `Color`; it
|
|
202
|
+
only becomes CSS at `.hex()`, `.rgb()` or `.rgba()`.
|
|
203
|
+
4. **`months()` and `years()` are approximations, and a day is not 24 hours.**
|
|
204
|
+
`months(1)` is 30 days and `years(1)` is 365 — fine for a cache window, wrong
|
|
205
|
+
for a birthday. Whole-day steps (`addDays`, `subtractDays`) count on the
|
|
206
|
+
calendar, so they hold across a DST night; `addHours` and below count in real
|
|
207
|
+
elapsed time, which is what a countdown needs and what a "same time tomorrow"
|
|
208
|
+
must not use.
|
|
209
|
+
|
|
210
|
+
R2 still outranks this one: when the exact signature is not in memory,
|
|
211
|
+
`npm run check -- --list` and `flet-box unpack` say what is real. A confident wrong
|
|
212
|
+
call is worse than the plain JavaScript it replaced — and `npm run check` will
|
|
213
|
+
catch it, which is the point of running it.
|
|
214
|
+
|
|
215
|
+
## R13 The keyboard is part of the screen, not an extra
|
|
216
|
+
|
|
217
|
+
The engine already carries the whole of it, and an app only has to not break it:
|
|
218
|
+
|
|
219
|
+
- **A handler given as a prop is a keyboard control.** `onPress` (or `onClick`)
|
|
220
|
+
on any widget makes the engine add `role="button"`, put it on the Tab path and
|
|
221
|
+
wire Enter and Space. A handler attached to the element *after* the widget was
|
|
222
|
+
built is a mouse-only control — the pass already ran. Express the control with
|
|
223
|
+
a prop.
|
|
224
|
+
- **Never write `outline: none`**, and never add a global `*:focus` rule that
|
|
225
|
+
removes it. The ring is one rule the framework injects, coloured by the
|
|
226
|
+
`colors.focusRing` token; an inline `outline` outranks it and leaves a screen
|
|
227
|
+
where nobody can see where they are. Change the token with
|
|
228
|
+
`registerPalette`, or override with a class — the rule is zero-specificity for
|
|
229
|
+
exactly that.
|
|
230
|
+
- **A control needs a name.** An icon with no `aria-label` and no visible text is
|
|
231
|
+
read as "button". An unnamed `Icon` is `aria-hidden` decoration by design, so
|
|
232
|
+
pressable icons take a label.
|
|
233
|
+
- **Fields keep two jobs apart:** the border is validation, the ring is focus.
|
|
234
|
+
`label` names the field, the widget's own `validate` method announces the
|
|
235
|
+
verdict, and the message is already a live region with `aria-invalid` on the
|
|
236
|
+
input.
|
|
237
|
+
- **Overlays trap focus and give it back.** `Modal`, `BottomSheet` and `Drawer`
|
|
238
|
+
do it; do not build a full-screen scrim with its own Escape listener beside
|
|
239
|
+
them.
|
|
240
|
+
|
|
241
|
+
Prove it the way the framework does: press Tab through the screen, then Enter and
|
|
242
|
+
Space on each control, then open an overlay and Tab past its last control.
|
|
243
|
+
`npm run ui` covers names, labels, tap targets and contrast, and no checker has a
|
|
244
|
+
keyboard — the last step is yours.
|
|
245
|
+
|
|
246
|
+
## R14 The second copy is a module
|
|
247
|
+
|
|
248
|
+
The first time you write a helper, it is yours. The second time the same code
|
|
249
|
+
appears — the same formatter, the same validation, the same escaped string, the
|
|
250
|
+
same card — it stops being a line you happen to share and becomes one module both
|
|
251
|
+
files import. A screen-specific variant is a prop on the shared component, never a
|
|
252
|
+
second component that starts as a copy.
|
|
253
|
+
|
|
254
|
+
**Why:** two identical copies pass every test on the day they are written. They
|
|
255
|
+
diverge on the day one of them is fixed — the fix lands in the copy that was in
|
|
256
|
+
front of you, the other keeps the bug, and nothing tells you which is which.
|
|
257
|
+
This is not hypothetical in this framework: the HTML escaper existed three times,
|
|
258
|
+
`margin()` and `padding()` were byte-identical files apart from one word, and a
|
|
259
|
+
keyframe injector lived in two modules, so the same animation name could be
|
|
260
|
+
appended twice and neither dedupe set knew about the other.
|
|
261
|
+
|
|
262
|
+
- Put a pure helper in **`src/helpers/`** — one file per behaviour, and it
|
|
263
|
+
**imports nothing at all**. That is the property that makes it reusable: a
|
|
264
|
+
widget can import it without dragging a theme, the parser or another widget
|
|
265
|
+
into its graph. If the helper needs another module it is a service, not a
|
|
266
|
+
helper, and it belongs in `src/utils/`. Anything that returns widgets goes in
|
|
267
|
+
`src/components/`. Name it for the behaviour, not for its first caller.
|
|
268
|
+
- Import it from its own path. Re-exporting through a barrel is fine; copying the
|
|
269
|
+
body "to avoid a dependency" is not — if the import drags in too much, move the
|
|
270
|
+
helper down into a leaf module instead, which is exactly why the framework's
|
|
271
|
+
escaper imports nothing at all.
|
|
272
|
+
- A helper that writes CSS state (`@keyframes`, an injected `<style>`) is owned by
|
|
273
|
+
one module for the whole document. A second injector is a second document-level
|
|
274
|
+
cache, and the same name is then defined twice.
|
|
275
|
+
|
|
276
|
+
## R15 Essential code only
|
|
277
|
+
|
|
278
|
+
Write the code the screen needs, in the smallest shape that says it. A feature
|
|
279
|
+
nobody asked for is not a head start — it is code that has to be read, tested and
|
|
280
|
+
maintained before it has a single caller, and it is the code most likely to be
|
|
281
|
+
wrong because nothing uses it yet.
|
|
282
|
+
|
|
283
|
+
- No option "for later", no prop nobody passes, no abstraction with one caller.
|
|
284
|
+
The second caller is when the helper appears (R14), not before.
|
|
285
|
+
- A wrapper that only forwards its arguments is code to delete, not to keep: call
|
|
286
|
+
the function it forwards to.
|
|
287
|
+
- Prefer removing a branch to adding a flag that selects between two branches.
|
|
288
|
+
- Before writing a helper, look for the one that already does it: `flet-box`
|
|
289
|
+
ships 123 (R12), and the framework's own three are `escapeHtml`, `boxEdges`
|
|
290
|
+
and `injectKeyframes`.
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|