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,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
+