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,293 @@
|
|
|
1
|
+
---
|
|
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, 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
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rules — how to work inside this framework
|
|
7
|
+
|
|
8
|
+
AGENTS.md is the short version of this file. It exists because the models that
|
|
9
|
+
work here keep making the same five mistakes, and every one of them produces a
|
|
10
|
+
screen that renders and is quietly wrong.
|
|
11
|
+
|
|
12
|
+
## R1 FletBox only — no other framework
|
|
13
|
+
|
|
14
|
+
A widget is a **function call that returns an element**. That is the whole model.
|
|
15
|
+
Nothing from another ecosystem is valid here, however close it looks:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// Correct: a function call with a props object.
|
|
19
|
+
Text({ text: 'Hello', size: 18, color: colors.text });
|
|
20
|
+
|
|
21
|
+
// Wrong: JSX. <Text size={18}>Hello</Text>
|
|
22
|
+
// Wrong: positional args. Text('Hello', 18);
|
|
23
|
+
// Wrong: a foreign import. import { Button } from 'react-native';
|
|
24
|
+
// Wrong: a framework that is not this one. <div class="…">, Svelte markup, Vue SFC.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Every file is ESM (`"type": "module"`), imports come from `'flet-box'`, and there
|
|
28
|
+
is no build step to learn and no compiler to blame.
|
|
29
|
+
|
|
30
|
+
## R2 Never invent an API — look it up
|
|
31
|
+
|
|
32
|
+
This is the rule the rest of the file exists to protect. A prop that silently does
|
|
33
|
+
nothing is worse than a compile error: it renders as a broken layout and nobody
|
|
34
|
+
finds it for weeks.
|
|
35
|
+
|
|
36
|
+
| I want to know… | Look here |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| Which widgets exist and which props each accepts | `.fletbox/api/cheatsheet.md` |
|
|
39
|
+
| Everything about one widget, with proof | `flet-box unpack <Widget>` |
|
|
40
|
+
| A call that is safe to paste | `flet-box unpack <Widget> --example` |
|
|
41
|
+
| Which props are documented but dead | `flet-box unpack <Widget> --diff` |
|
|
42
|
+
| The whole surface as data, to build calls from | `.fletbox/api/api.json` |
|
|
43
|
+
| Whether a name is exported at all | `npm run check -- --list` |
|
|
44
|
+
|
|
45
|
+
**If a name is not in those files, it does not exist.** The four places a prop can
|
|
46
|
+
live — `src/index.d.ts`, the prop registry, the widget's own source, its doc page —
|
|
47
|
+
do not always agree, and `unpack` labels every disagreement: `DEAD` (typed, routed
|
|
48
|
+
by nothing, read by nothing), `INVENTED` (read by the widget, declared nowhere),
|
|
49
|
+
`UNDOCUMENTED`. Design against what reaches the DOM.
|
|
50
|
+
|
|
51
|
+
- Do not edit `node_modules/flet-box`. It is a dependency, not source. A limitation
|
|
52
|
+
you hit there is fixed upstream, in the framework repo, or not at all.
|
|
53
|
+
- Regenerate the API files (`npm run api`) after upgrading the package; never
|
|
54
|
+
hand-edit them.
|
|
55
|
+
- When the API genuinely has no answer, choose a real composition or say the gap
|
|
56
|
+
out loud. Do not add a prop and hope the engine reads it — it drops unlisted
|
|
57
|
+
props.
|
|
58
|
+
|
|
59
|
+
## R3 Aliases are the API, not a mistake
|
|
60
|
+
|
|
61
|
+
`bgColor` ≡ `backgroundColor`, `onPress` ≡ `onClick`, and the rest. They are
|
|
62
|
+
deliberate: each alias has exactly one meaning, resolved by the factory. Pick one
|
|
63
|
+
spelling per screen and keep it — but never "clean up" an alias out of the
|
|
64
|
+
framework, and never assume a second spelling is dead code.
|
|
65
|
+
|
|
66
|
+
## R4 A number is not a pixel
|
|
67
|
+
|
|
68
|
+
Any length you hand a rem-routed prop (`padding`, `margin`, `gap`, `width`,
|
|
69
|
+
`size`, `borderRadius`, …) is divided by the base font size and emitted as `rem`,
|
|
70
|
+
so the whole interface grows when the reader raises their browser font. That is
|
|
71
|
+
the feature: one build fits phone, tablet and desktop.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
Container({ padding: 16 }); // 1rem — 16px at the default base, more at a bigger one
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Pixels stay where pixels are honest: a canvas backing store, a value measured back
|
|
78
|
+
out of the DOM (`offsetWidth`, `getBoundingClientRect`, scroll offsets), the
|
|
79
|
+
hairline `borderWidth`, and a virtualised row's math in px space. Never write a
|
|
80
|
+
`'16px'` string for something that should scale.
|
|
81
|
+
|
|
82
|
+
## R5 State, and one way to re-render
|
|
83
|
+
|
|
84
|
+
State is key-based RAM with subscribers: `useState('cart', 0)` returns the value
|
|
85
|
+
and a setter; the widgets bound to the key update themselves. Do not build a second
|
|
86
|
+
render system, do not reach into the DOM to change what a widget shows, and do not
|
|
87
|
+
manually rebuild a tree that a setter could update. Setters on a widget
|
|
88
|
+
(`widget.text = …`) are reactive for the same reason.
|
|
89
|
+
|
|
90
|
+
## R6 Every resource you register, you release
|
|
91
|
+
|
|
92
|
+
A widget or a screen that adds a `window`/`document` listener, a timer, a
|
|
93
|
+
`requestAnimationFrame`, an observer, a router or storage subscription must release
|
|
94
|
+
it when it unmounts — one idempotent disposer in `onUnmount`. A leaked loop keeps
|
|
95
|
+
animating a detached node and outlives the screen that started it.
|
|
96
|
+
|
|
97
|
+
## R7 One rendering engine
|
|
98
|
+
|
|
99
|
+
`WidgetFactory` turns props into an element (style, events, children, lifecycle,
|
|
100
|
+
reactivity). It is the only renderer. A custom component is still a function call
|
|
101
|
+
returning widgets; never add a parallel way to paint, and never monkey-patch
|
|
102
|
+
`container.update` — compose the update.
|
|
103
|
+
|
|
104
|
+
## R8 The two gates, after every edit
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run check # does this API exist? exits 1 on an invented widget, prop,
|
|
108
|
+
# positional call, foreign import or broken module import
|
|
109
|
+
npm run ui # is this screen clean? tokens, the scale, tap targets, labels,
|
|
110
|
+
# contrast — and it cites the rule it broke
|
|
111
|
+
npm run fix # rewrites what the checker can prove: a wrong path, a default
|
|
112
|
+
# import of a named export, a missing import line, a typo in a
|
|
113
|
+
# prop key. Anything ambiguous is printed, never touched
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then look at the screen at 375px and at 1440px, in both themes: **the checker has no
|
|
117
|
+
eyes.** A clean `npm run ui` says the values are legal, not that the screen is
|
|
118
|
+
understood.
|
|
119
|
+
|
|
120
|
+
A finding you keep needs one sentence of justification, written where the next
|
|
121
|
+
session will read it (R10). "The design wants it" is not a justification — it is a
|
|
122
|
+
rule that has not been written down yet.
|
|
123
|
+
|
|
124
|
+
## R9 Do not trust your memory of this framework
|
|
125
|
+
|
|
126
|
+
FletBox has no `.d.ts` you have read in full, the prop set grows between versions,
|
|
127
|
+
and the installed version is the truth for this app. Three habits follow:
|
|
128
|
+
|
|
129
|
+
- Look up before using, even for `Text`.
|
|
130
|
+
- After a dependency bump, re-run `npm run check` across the app — it also reports
|
|
131
|
+
names the package stopped exporting.
|
|
132
|
+
- What is deployed is built from the **published** package, not from your working
|
|
133
|
+
tree. A fix you made in a local checkout does not exist for an installed app
|
|
134
|
+
until it is released.
|
|
135
|
+
|
|
136
|
+
## R10 Write down what only you learned
|
|
137
|
+
|
|
138
|
+
The project keeps its memory in `.fletbox/memory/`. Read `MEMORY.md` before you
|
|
139
|
+
start, and record what the next session would otherwise rediscover the hard way.
|
|
140
|
+
`fletbox-memory` is the format and the rules; it is not optional — a session that
|
|
141
|
+
learns something and does not write it is a session that has to be repeated.
|
|
142
|
+
|
|
143
|
+
## R11 The order the skills are read in
|
|
144
|
+
|
|
145
|
+
| # | Skill | Read it when |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| 1 | `fletbox-rules` (this file) | always, before writing code |
|
|
148
|
+
| 2 | `fletbox-design` | creating, editing or reviewing any screen, form, list or dialog |
|
|
149
|
+
| 3 | `fletbox-motion` | adding any transition, entrance, pulse, parallax or hover lift |
|
|
150
|
+
| 4 | `fletbox-memory` | at the start and at the end of every session, and whenever progress changes |
|
|
151
|
+
|
|
152
|
+
They cover the same screen from three angles and do not repeat each other. When
|
|
153
|
+
they disagree, this file wins about the API, `fletbox-design` wins about the
|
|
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
|
+
|