@zakkster/lite-ui-fx 1.5.0 → 1.7.0
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/CHANGELOG.md +86 -0
- package/README.md +45 -7
- package/UIFX-RECIPE-GUIDE.md +91 -3
- package/UIFXController.d.ts +99 -1
- package/UIFXController.js +481 -13
- package/UIFXRecipes.d.ts +27 -4
- package/UIFXRecipes.js +239 -44
- package/llms.txt +68 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,92 @@ All notable changes to `@zakkster/lite-ui-fx` are documented here.
|
|
|
5
5
|
The format follows Keep a Changelog; this project adheres to Semantic
|
|
6
6
|
Versioning.
|
|
7
7
|
|
|
8
|
+
## [1.7.0] -- 2026-09-07
|
|
9
|
+
|
|
10
|
+
Host integration (roadmap U5). Two host-clock modes plus reduced-motion and a
|
|
11
|
+
frame-budget signal, applied to BOTH mount modes (`mountUIFX` and `decorateUIFX`).
|
|
12
|
+
Additive: a default mount is byte-identical to 1.6.0.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `{ ticker }` mount option (both modes): a caller-supplied ticker
|
|
17
|
+
(`{ add(fn) -> removeFn }`, e.g. `@zakkster/lite-ticker`) drives the component
|
|
18
|
+
instead of the shared ref-counted ticker. `destroy()` unregisters the
|
|
19
|
+
component's frame but never destroys the caller's ticker (ownership stays with
|
|
20
|
+
the caller).
|
|
21
|
+
- `{ driven: true }` mount option (both modes): no ticker and no RAF; the host
|
|
22
|
+
drives each frame via `instance.tick(dtMs)`. `instance.tick` is the internal
|
|
23
|
+
frame body in driven mode and throws otherwise. `{ ticker }` and `{ driven }`
|
|
24
|
+
are mutually exclusive; a non-boolean `driven` or a ticker without `.add()`
|
|
25
|
+
throws at mount.
|
|
26
|
+
- `state.reducedMotion` (both modes): read from
|
|
27
|
+
`matchMedia('(prefers-reduced-motion: reduce)')` before `recipe.init` and
|
|
28
|
+
watched via the AbortController; absent `matchMedia` is a no-op (stays `false`).
|
|
29
|
+
Calm paths for six recipes -- `SwarmToggle`, `PasswordStrength`,
|
|
30
|
+
`TypewriterField`, `FocusHalo`, `ErrorShake`, `SuccessBloom`: under reduced
|
|
31
|
+
motion `ErrorShake` stops displacing, `SuccessBloom` spawns no particles, and
|
|
32
|
+
`SwarmToggle` rests its particles at formation.
|
|
33
|
+
- `state.budget` (0..1, both modes): a per-frame frame-budget number (1 at
|
|
34
|
+
~60fps, lower as frames lengthen), computed in place with no allocation, for
|
|
35
|
+
budget-aware recipes to shed work.
|
|
36
|
+
- `mountRecipe` emits a `console.warn` (not a throw) when mounting a
|
|
37
|
+
`motionSafe:false` recipe while the user prefers reduced motion.
|
|
38
|
+
- TypeScript: `HostTicker`, `HostClockOptions`, `state.reducedMotion` /
|
|
39
|
+
`state.budget`, and `instance.tick(dtMs)` on both instance types.
|
|
40
|
+
- `decisions/0005-host-clock.md`. U5 coverage: t5 caller-ticker ownership +
|
|
41
|
+
driven determinism, t3 reduced-motion churn, and two t9 controls (`fake-calm`,
|
|
42
|
+
`ticker-ownership`). 177 -> 196 node:test tests; 5 -> 7 torture controls.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- The per-mount frame loop is one named function shared by all three clock modes;
|
|
47
|
+
the default (shared-ticker) path is byte-identical to 1.6.0.
|
|
48
|
+
- `RECIPE_META.motionSafe` is now `true` for six recipes (previously `false` for
|
|
49
|
+
all 56): it marks exactly the recipes that ship a reduced-motion calm path.
|
|
50
|
+
|
|
51
|
+
## [1.6.0] -- 2026-09-07
|
|
52
|
+
|
|
53
|
+
Decorate mode (U4b, the second half of roadmap U4). A second public mount mode
|
|
54
|
+
alongside the hijack `mountUIFX`: `decorateUIFX` positions a canvas AROUND an
|
|
55
|
+
existing visible element instead of hijacking it. Additive: a bare `mountUIFX`
|
|
56
|
+
mount is byte-identical to 1.5.0; 53 -> 56 recipes.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- `decorateUIFX(el, recipeFactory, options)`: a canvas overlay AROUND a live
|
|
61
|
+
element -- no `opacity:0`, no reparent. The overlay is a sibling placed from the
|
|
62
|
+
host's offset box and removed on `destroy()`, so the host is byte-identical
|
|
63
|
+
before and after (additive-only). Recipe `state` is wired from the host's own
|
|
64
|
+
events; for a form-control host `state.text` and `state.valid` mirror `el.value`
|
|
65
|
+
and `el.validity`, read at event time (never per frame). `setValue`/`setChecked`
|
|
66
|
+
are hijack-only and throw. Options are a subset (`padding`, `seed`, `colors`,
|
|
67
|
+
`theme`, `text`, `font`); the hijack-only keys throw in decorate mode.
|
|
68
|
+
- Three decorate recipes, born themed and t3-gated: `FocusHalo`, `ErrorShake`,
|
|
69
|
+
`SuccessBloom` (generic form feedback, reading `state.focused` / `state.valid`).
|
|
70
|
+
Registered in `RECIPES` / `RECIPE_META` (type `'decorate'`) / the default export
|
|
71
|
+
/ a new `UIFXRecipes5` barrel.
|
|
72
|
+
- `'decorate'` as a `RECIPE_META.type` routing tag: `mountRecipe(el, id)` routes a
|
|
73
|
+
decorate recipe to `decorateUIFX`. `VALID_META_TYPES` gains exactly this one
|
|
74
|
+
non-`UIType` tag; `mountUIFX` still rejects it (the two paths cannot cross).
|
|
75
|
+
- Optional `state.text` / `state.valid` fields (decorate mode only). TypeScript
|
|
76
|
+
`DecorateOptions`, `DecorateInstance`, and `decorateUIFX` in the d.ts.
|
|
77
|
+
- `decisions/0004-decorate-mode.md`; decorate coverage across the suite: t0 host
|
|
78
|
+
byte-identical DOM diff + a t9 `decorate-host-mutation` control, t1 fail-closed +
|
|
79
|
+
degenerate sweep, t2 A14-A16 (state wiring, host untouched, non-input host), t3
|
|
80
|
+
zero-alloc churn, t5 decorate on the shared ticker. 164 -> 177 node:test tests.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- `PasswordStrength` and `TypewriterField` re-homed from their U4a-era fake types
|
|
85
|
+
(slider / toggle) onto decorate mode (`RECIPE_META.type` `'decorate'`). Unlike
|
|
86
|
+
the U4a re-homes these are behaviour ports: they now read the live host input --
|
|
87
|
+
PasswordStrength derives strength from `state.text` (zero-alloc `charCodeAt`
|
|
88
|
+
scan, recomputed only on change); TypewriterField animates an underline that
|
|
89
|
+
grows with the typed text (no `measureText`). Both stay `themeable`.
|
|
90
|
+
- RECIPE_META covers 56 recipes; `themeable` true for all 56, `motionSafe` false
|
|
91
|
+
for all 56. All 56 stay zero-GC under the t3 frame-alloc gate (default AND
|
|
92
|
+
themed). `mountUIFX` and every hijack mount are unchanged.
|
|
93
|
+
|
|
8
94
|
## [1.5.0] -- 2026-09-07
|
|
9
95
|
|
|
10
96
|
New native element types (U4a, the first half of roadmap U4). Vol.3 faked
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ https://cdpn.io/pen/debug/yyaPKpB
|
|
|
21
21
|
## Live Demo (UI-FX vol3.)
|
|
22
22
|
https://cdpn.io/pen/debug/YPGEaYY
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
**56 recipes** across UI element categories:
|
|
25
25
|
|
|
26
26
|
- **Toggles** -- Swarm, Liquid, Neon Pulse, Pendulum, Circuit, Lightning, DNA
|
|
27
27
|
- **Buttons** -- Magnetic, Shatter, Confetti, Glitch, Heartbeat, Breathing, Ink Splash, Pixel Dissolve, Firework
|
|
@@ -29,18 +29,19 @@ https://cdpn.io/pen/debug/YPGEaYY
|
|
|
29
29
|
- **Knobs** -- Volume dial, Compass needle
|
|
30
30
|
- **Progress** -- Ring, Battery, Signal meter, Liquid Fill
|
|
31
31
|
- **Controls** -- Pill tabs, Stepper, Radio orbit
|
|
32
|
-
- **Indicators** --
|
|
32
|
+
- **Indicators** -- Water level, Heat map
|
|
33
33
|
- **Mood** -- Day/night, Reaction picker, Notification bell
|
|
34
|
-
- **Feedback** --
|
|
34
|
+
- **Feedback** -- Sound wave, Upload progress
|
|
35
35
|
- **Fun** -- Scratch reveal, Timer countdown, Pull refresh
|
|
36
36
|
- **Checkboxes** -- Ripple, Morph (X to check), Tick Draw, Indeterminate Scan
|
|
37
37
|
- **Loaders** -- Orbit planets, DNA helix
|
|
38
38
|
- **Counters** -- Flame heat, Glitch signal
|
|
39
39
|
- **Rating** -- Bubble inflate
|
|
40
|
+
- **Form (decorate)** -- Focus halo, Error shake, Success bloom, Password strength, Typewriter field (mounted via `decorateUIFX`, around a live element)
|
|
40
41
|
|
|
41
42
|
Every recipe is zero-GC, uses `dt`-based animation, and includes accessibility indicators (focus rings, state labels).
|
|
42
43
|
|
|
43
|
-
All
|
|
44
|
+
All 56 recipes ship in the package on the `./recipes` subpath -- versioned,
|
|
44
45
|
typed, and tree-shakeable. With `sideEffects: false`, importing one recipe pulls
|
|
45
46
|
in only that recipe, so a controller-only install stays tiny.
|
|
46
47
|
|
|
@@ -65,7 +66,7 @@ Part of the [@zakkster/lite-*](https://www.npmjs.com/org/zakkster) ecosystem.
|
|
|
65
66
|
npm i @zakkster/lite-ui-fx
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
> The
|
|
69
|
+
> The 56 recipes ship in the same package on the `./recipes` subpath and
|
|
69
70
|
> tree-shake, so importing one adds only that one.
|
|
70
71
|
|
|
71
72
|
|
|
@@ -97,7 +98,7 @@ instance.destroy();
|
|
|
97
98
|
// Controller (always needed)
|
|
98
99
|
import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
|
|
99
100
|
|
|
100
|
-
// All
|
|
101
|
+
// All 56 recipes ship on the ./recipes subpath (tree-shakeable) -- import by name:
|
|
101
102
|
import { SwarmToggle, MagneticButton, SparkSlider } from '@zakkster/lite-ui-fx/recipes';
|
|
102
103
|
import { PendulumToggle, HeartbeatButton, RippleCheck } from '@zakkster/lite-ui-fx/recipes';
|
|
103
104
|
import { VolumeKnob, WaterLevel, TimerCountdown } from '@zakkster/lite-ui-fx/recipes';
|
|
@@ -158,6 +159,36 @@ Returns `{ el, canvas, wrapper, state, setValue(v), setChecked(b), destroy() }`.
|
|
|
158
159
|
- `setChecked(b)` -- TOGGLE/CHECKBOX: set checked (updates the element +
|
|
159
160
|
`state.toggled`, fires `onToggle` once).
|
|
160
161
|
|
|
162
|
+
### `decorateUIFX(el, recipeFactory, options?)` -- the second mount mode
|
|
163
|
+
|
|
164
|
+
Where `mountUIFX` **hijacks** (creates a hidden native element under a canvas),
|
|
165
|
+
`decorateUIFX` **decorates**: it positions a canvas *around* an existing, visible
|
|
166
|
+
element without hijacking it -- no `opacity:0`, no reparenting. The overlay is a
|
|
167
|
+
sibling placed from the host's offset box and removed on `destroy()`, so the host
|
|
168
|
+
is byte-identical before and after. Recipe `state` is wired from the host's own
|
|
169
|
+
events; for a form-control host, `state.text` and `state.valid` mirror `el.value`
|
|
170
|
+
and `el.validity` (read at event time, never per frame). This is the honest home
|
|
171
|
+
for a decoration over a real input.
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import { decorateUIFX } from '@zakkster/lite-ui-fx';
|
|
175
|
+
import { PasswordStrength } from '@zakkster/lite-ui-fx/recipes';
|
|
176
|
+
|
|
177
|
+
const input = document.querySelector('#password');
|
|
178
|
+
const deco = decorateUIFX(input, PasswordStrength, { theme });
|
|
179
|
+
// ... input stays fully usable; the meter tracks what the user types ...
|
|
180
|
+
deco.destroy(); // removes ONLY the overlay; the input is untouched
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`options` is a subset: `padding`, `seed`, `colors`, `theme`, `text`, `font`. The
|
|
184
|
+
hijack-only keys (`width`/`height`/`value`/`checked`/`disabled`/`knobMode`/
|
|
185
|
+
`announce`/`label`) throw in decorate mode. Returns
|
|
186
|
+
`{ el, canvas, state, setValue, setChecked, destroy() }`, where `setValue` and
|
|
187
|
+
`setChecked` are hijack-only and throw (a decoration reflects the host; it does
|
|
188
|
+
not drive it). Built-in decorate recipes: `FocusHalo`, `ErrorShake`,
|
|
189
|
+
`SuccessBloom`, `PasswordStrength`, `TypewriterField` (`RECIPE_META.type` =
|
|
190
|
+
`'decorate'`, so `mountRecipe(el, id)` routes them here automatically).
|
|
191
|
+
|
|
161
192
|
### Element Types
|
|
162
193
|
|
|
163
194
|
| Type | Native Element | Recipe Hooks | Key State |
|
|
@@ -168,6 +199,10 @@ Returns `{ el, canvas, wrapper, state, setValue(v), setChecked(b), destroy() }`.
|
|
|
168
199
|
| `UIType.CHECKBOX` | `<input type="checkbox">` (no `role=switch`) | `onToggle(checked)` | `state.toggled`, `state.indeterminate` |
|
|
169
200
|
| `UIType.PROGRESS` | `<progress>` (non-interactive) | (driven by `setValue`) | `state.val` (0-1) |
|
|
170
201
|
| `UIType.KNOB` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0-1) |
|
|
202
|
+
| *(decorate)* | none -- a canvas AROUND a live host (`decorateUIFX`) | host events -> state | `state.focused`, `state.text`, `state.valid` |
|
|
203
|
+
|
|
204
|
+
*(decorate)* is a mount mode, not a `UIType`: it creates no native element. A recipe
|
|
205
|
+
with `RECIPE_META.type === 'decorate'` is mounted via `decorateUIFX`.
|
|
171
206
|
|
|
172
207
|
### State Object (provided to `tick()` every frame)
|
|
173
208
|
|
|
@@ -184,6 +219,8 @@ Returns `{ el, canvas, wrapper, state, setValue(v), setChecked(b), destroy() }`.
|
|
|
184
219
|
h: number; // Element height
|
|
185
220
|
padding: number; // Canvas padding
|
|
186
221
|
dpr: number; // Device pixel ratio
|
|
222
|
+
text?: string; // decorate mode only: the host value string
|
|
223
|
+
valid?: boolean; // decorate mode only: the host validity
|
|
187
224
|
}
|
|
188
225
|
```
|
|
189
226
|
|
|
@@ -194,7 +231,7 @@ Returns `{ el, canvas, wrapper, state, setValue(v), setChecked(b), destroy() }`.
|
|
|
194
231
|
| Framer Motion | ~45 KB | React HOC | 0 | Via React | `npm i framer-motion` |
|
|
195
232
|
| GSAP | ~25 KB | Timeline | 0 | Manual | `npm i gsap` |
|
|
196
233
|
| Lottie | ~55 KB | JSON animation | After Effects | Manual | `npm i lottie-web` |
|
|
197
|
-
| **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **
|
|
234
|
+
| **lite-ui-fx** | **< 5 KB** | **Canvas hijack + decorate** | **56 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
|
|
198
235
|
|
|
199
236
|
## Writing Custom Recipes
|
|
200
237
|
|
|
@@ -227,6 +264,7 @@ export function MyButton() {
|
|
|
227
264
|
Full TypeScript declarations are included for:
|
|
228
265
|
|
|
229
266
|
- `mountUIFX`
|
|
267
|
+
- `decorateUIFX` (+ `DecorateOptions`, `DecorateInstance`)
|
|
230
268
|
- `UIType`
|
|
231
269
|
- `UIFXState`
|
|
232
270
|
- `UIFXPointer`
|
package/UIFX-RECIPE-GUIDE.md
CHANGED
|
@@ -63,15 +63,51 @@ The controller provides this every frame:
|
|
|
63
63
|
hover: boolean, // Pointer is inside the element
|
|
64
64
|
active: boolean, // Pointer is pressed down
|
|
65
65
|
focused: boolean, // Element has keyboard focus
|
|
66
|
-
toggled: boolean, // Checkbox checked state (toggles)
|
|
67
|
-
|
|
66
|
+
toggled: boolean, // Checkbox checked state (toggles/checkboxes)
|
|
67
|
+
indeterminate: boolean, // CHECKBOX only: native indeterminate (setValue(null))
|
|
68
|
+
val: number, // 0--1 value (sliders/knobs/progress)
|
|
68
69
|
w: number, // Element width in CSS pixels
|
|
69
70
|
h: number, // Element height
|
|
70
71
|
padding: number, // Canvas overflow padding
|
|
71
72
|
dpr: number, // Device pixel ratio
|
|
73
|
+
reducedMotion: boolean, // U5: user prefers reduced motion (see below)
|
|
74
|
+
budget: number, // U5: 0--1 frame budget (1 at ~60fps, lower under load)
|
|
75
|
+
// Decorate mode only (decorateUIFX): the live host's value + validity.
|
|
76
|
+
text: string, // the host form-control's value string ('' if none)
|
|
77
|
+
valid: boolean, // the host's validity (el.validity.valid, else true)
|
|
72
78
|
}
|
|
73
79
|
```
|
|
74
80
|
|
|
81
|
+
## Reduced Motion & Frame Budget (U5)
|
|
82
|
+
|
|
83
|
+
Two state fields let a recipe be a good citizen without changing the interface.
|
|
84
|
+
|
|
85
|
+
- **`state.reducedMotion`** is `true` when the user has set
|
|
86
|
+
`prefers-reduced-motion: reduce`. A recipe that animates should read it and
|
|
87
|
+
render a **static** alternative -- no continuous motion, no bursts, no shakes.
|
|
88
|
+
Fades and instant state changes are fine; sustained or positional motion is not.
|
|
89
|
+
When your recipe ships such a calm path, set its `RECIPE_META.motionSafe: true`;
|
|
90
|
+
that flag is a promise the calm path exists, so keep them in sync.
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
tick(c, dt, now, st) {
|
|
94
|
+
// full motion vs. a steady, motion-free render
|
|
95
|
+
const wobble = st.reducedMotion ? 0 : Math.sin(now / 200) * 4;
|
|
96
|
+
// ... draw using `wobble` (0 = no motion) ...
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **`state.budget`** is `1` when frames are healthy and drops toward `0` as they
|
|
101
|
+
lengthen. A budget-aware recipe scales expensive work by it (fewer particles,
|
|
102
|
+
less glow) so it degrades before the host drops frames. Consuming it is optional.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
const live = (this.count = Math.floor(MAX_PARTICLES * st.budget));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Both are read-only per-frame numbers -- never write them, never allocate to honour
|
|
109
|
+
them (a branch on a boolean/number is free; a new array per frame is not).
|
|
110
|
+
|
|
75
111
|
## The Pointer Object
|
|
76
112
|
|
|
77
113
|
```javascript
|
|
@@ -121,13 +157,65 @@ const instance = mountUIFX(
|
|
|
121
157
|
instance.destroy();
|
|
122
158
|
```
|
|
123
159
|
|
|
124
|
-
##
|
|
160
|
+
## Element Types & Mount Modes
|
|
161
|
+
|
|
162
|
+
`mountUIFX` HIJACKS -- it creates one of six native elements (opacity:0) under the
|
|
163
|
+
canvas:
|
|
125
164
|
|
|
126
165
|
| Type | Native Element | Recipe Gets | Key State |
|
|
127
166
|
|------|----------------|-------------|-----------|
|
|
128
167
|
| `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
|
|
129
168
|
| `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
|
|
130
169
|
| `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0--1) |
|
|
170
|
+
| `UIType.CHECKBOX` | `<input type="checkbox">` (no `role=switch`) | `onToggle(checked)` | `state.toggled`, `state.indeterminate` |
|
|
171
|
+
| `UIType.PROGRESS` | `<progress>` (non-interactive) | (driven by `setValue`) | `state.val` |
|
|
172
|
+
| `UIType.KNOB` | `<input type="range">` | `onDrag(val, velocity)` | `state.val`, `knobMode` |
|
|
173
|
+
|
|
174
|
+
## Decorate-Mode Recipes (a canvas AROUND a live element)
|
|
175
|
+
|
|
176
|
+
`decorateUIFX(el, factory, options)` is the SECOND mount mode: instead of creating
|
|
177
|
+
a hidden element, it positions a canvas around an EXISTING, visible element (a real
|
|
178
|
+
`<input>`, a button, any element). Same recipe interface, same coordinate system --
|
|
179
|
+
`(0,0)` is the host's top-left, `state.w/h` are the host's size -- but three rules
|
|
180
|
+
differ, and breaking them is caught by the torture t0 decorate DOM-diff:
|
|
181
|
+
|
|
182
|
+
1. **Never touch the host.** A decoration paints ONLY its overlay canvas. Do not
|
|
183
|
+
write `el.style`, set an attribute, or read/move the host in `init`/`tick`. The
|
|
184
|
+
host must be byte-identical after `destroy()` (additive-only). The controller
|
|
185
|
+
never sets `opacity:0` and never reparents the host -- neither may your recipe.
|
|
186
|
+
2. **Read host content from `state`, at frame time, allocation-free.** For a
|
|
187
|
+
form-control host, `state.text` (the value string) and `state.valid` (validity)
|
|
188
|
+
are updated by the controller at EVENT time (input/change/invalid) -- never per
|
|
189
|
+
frame. Your `tick` reads them; scan `state.text` with `charCodeAt` (no allocating
|
|
190
|
+
string ops), and edge-detect `state.valid` against a closure-cached previous.
|
|
191
|
+
There is NO new hook: a decoration reacts by polling `state` in `tick`.
|
|
192
|
+
3. **`setValue`/`setChecked` are hijack-only.** A decoration reflects the host; it
|
|
193
|
+
does not drive it. Both throw in decorate mode.
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
import { decorateUIFX } from './UIFXController.js';
|
|
197
|
+
// A decoration reads state.focused / state.valid / state.text; it never draws the
|
|
198
|
+
// host's own text (the real element already shows it).
|
|
199
|
+
export function Underline() {
|
|
200
|
+
let fill = 0;
|
|
201
|
+
return {
|
|
202
|
+
tick(ctx, dt, now, state) {
|
|
203
|
+
const len = (state.text || '').length; // number, no alloc
|
|
204
|
+
fill += ((len ? Math.min(len / 24, 1) : 0) - fill) * dt * 8;
|
|
205
|
+
ctx.strokeStyle = state.focused ? '#6ee7b6' : '#556';
|
|
206
|
+
ctx.lineWidth = 2;
|
|
207
|
+
ctx.beginPath();
|
|
208
|
+
ctx.moveTo(2, state.h - 3);
|
|
209
|
+
ctx.lineTo(2 + (state.w - 4) * fill, state.h - 3);
|
|
210
|
+
ctx.stroke();
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const deco = decorateUIFX(document.querySelector('#field'), Underline);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Register a decorate recipe with `RECIPE_META.type: 'decorate'` -- then
|
|
218
|
+
`mountRecipe(el, id)` routes it to `decorateUIFX` automatically.
|
|
131
219
|
|
|
132
220
|
## Using @zakkster Libraries in Recipes
|
|
133
221
|
|
package/UIFXController.d.ts
CHANGED
|
@@ -27,6 +27,17 @@ export interface UIFXState {
|
|
|
27
27
|
h: number;
|
|
28
28
|
padding: number;
|
|
29
29
|
dpr: number;
|
|
30
|
+
/** U5: true when the user prefers reduced motion (matchMedia). Calm-path
|
|
31
|
+
* recipes render statically when set; recipes that ignore it animate. */
|
|
32
|
+
reducedMotion: boolean;
|
|
33
|
+
/** U5: frame budget in 0..1 -- 1 at ~60fps, lower as frames lengthen.
|
|
34
|
+
* Budget-aware recipes shed work (particles/glow) when it drops. */
|
|
35
|
+
budget: number;
|
|
36
|
+
/** Decorate mode only (decorateUIFX): the host form-control's current value.
|
|
37
|
+
* Absent for hijack mounts and for a non-form host (then ''). */
|
|
38
|
+
text?: string;
|
|
39
|
+
/** Decorate mode only: the host's validity (el.validity.valid, else true). */
|
|
40
|
+
valid?: boolean;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
export interface UIFXPointer {
|
|
@@ -56,7 +67,30 @@ export interface UIFXRecipe {
|
|
|
56
67
|
|
|
57
68
|
export type RecipeFactory = () => UIFXRecipe;
|
|
58
69
|
|
|
59
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A caller-supplied clock for the { ticker } host-clock mode (U5). Duck-typed to
|
|
72
|
+
* @zakkster/lite-ticker: it must expose add(fn) returning a remove function. The
|
|
73
|
+
* component registers its frame on it and, on destroy, removes that frame but
|
|
74
|
+
* NEVER destroys the ticker -- ownership stays with the caller.
|
|
75
|
+
*/
|
|
76
|
+
export interface HostTicker {
|
|
77
|
+
add(fn: (dtMs: number) => void): () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Host-clock options (U5, decisions/0005), shared by both mount modes. Three
|
|
82
|
+
* mutually-exclusive modes: omit both for the shared ref-counted ticker (default);
|
|
83
|
+
* `ticker` to ride a caller-supplied clock; `driven: true` for no clock at all
|
|
84
|
+
* (the host calls instance.tick(dtMs)). Passing both throws.
|
|
85
|
+
*/
|
|
86
|
+
export interface HostClockOptions {
|
|
87
|
+
/** Ride a caller-supplied ticker instead of the shared one. Mutually exclusive with `driven`. */
|
|
88
|
+
ticker?: HostTicker;
|
|
89
|
+
/** No ticker/RAF: the host drives frames via instance.tick(dtMs). Mutually exclusive with `ticker`. */
|
|
90
|
+
driven?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface MountOptions extends HostClockOptions {
|
|
60
94
|
width?: number;
|
|
61
95
|
height?: number;
|
|
62
96
|
padding?: number;
|
|
@@ -86,12 +120,36 @@ export interface MountOptions {
|
|
|
86
120
|
announce?: boolean;
|
|
87
121
|
}
|
|
88
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Options accepted by decorateUIFX. A subset of MountOptions: a decoration
|
|
125
|
+
* inherits the host's geometry (offset box) and value (read from the host), so
|
|
126
|
+
* the hijack-only options (width/height/value/checked/disabled/knobMode/announce/
|
|
127
|
+
* label) are rejected -- passing one throws (fail closed).
|
|
128
|
+
*/
|
|
129
|
+
export interface DecorateOptions extends HostClockOptions {
|
|
130
|
+
/** Overlay padding around the host, in px (default 40). */
|
|
131
|
+
padding?: number;
|
|
132
|
+
seed?: number;
|
|
133
|
+
colors?: string[];
|
|
134
|
+
theme?: { light: string; mid: string; dark: string };
|
|
135
|
+
text?: string;
|
|
136
|
+
font?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
89
139
|
export interface UIFXInstance {
|
|
90
140
|
el: HTMLElement;
|
|
91
141
|
canvas: HTMLCanvasElement;
|
|
92
142
|
wrapper: HTMLDivElement;
|
|
93
143
|
state: UIFXState;
|
|
94
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Drive one frame by hand (U5). Callable ONLY when mounted with { driven: true }
|
|
147
|
+
* -- it is the internal frame body, so a driven host pays exactly the internal
|
|
148
|
+
* per-frame cost. On a ticker-driven component it throws (that component owns
|
|
149
|
+
* its own clock).
|
|
150
|
+
*/
|
|
151
|
+
tick(dtMs: number): void;
|
|
152
|
+
|
|
95
153
|
/**
|
|
96
154
|
* Set a valued control (SLIDER/KNOB/PROGRESS) to v in [0,1]: updates the
|
|
97
155
|
* native element, state.val, any PROGRESS announcer, and fires onDrag once.
|
|
@@ -117,4 +175,44 @@ export declare function mountUIFX(
|
|
|
117
175
|
options?: MountOptions
|
|
118
176
|
): UIFXInstance;
|
|
119
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The instance returned by decorateUIFX. Like UIFXInstance but WITHOUT `wrapper`
|
|
180
|
+
* (there is none -- the overlay is a sibling of the host, not a wrapper around
|
|
181
|
+
* it), and setValue/setChecked are hijack-only: a decoration reflects the host,
|
|
182
|
+
* it does not drive it, so both throw.
|
|
183
|
+
*/
|
|
184
|
+
export interface DecorateInstance {
|
|
185
|
+
/** The decorated host element (unchanged -- decorate never mutates it). */
|
|
186
|
+
el: HTMLElement;
|
|
187
|
+
/** The overlay canvas (the only DOM node decorate adds). */
|
|
188
|
+
canvas: HTMLCanvasElement;
|
|
189
|
+
state: UIFXState;
|
|
190
|
+
/** Drive one frame by hand (U5). Callable ONLY with { driven: true }; a
|
|
191
|
+
* ticker-driven decoration throws. */
|
|
192
|
+
tick(dtMs: number): void;
|
|
193
|
+
/** Hijack-only. Throws in decorate mode. */
|
|
194
|
+
setValue(v?: number | null): void;
|
|
195
|
+
/** Hijack-only. Throws in decorate mode. */
|
|
196
|
+
setChecked(b?: boolean): void;
|
|
197
|
+
/** Remove the overlay + every listener decorate added; the host is left
|
|
198
|
+
* byte-identical to before decorate. Idempotent. */
|
|
199
|
+
destroy(): void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Decorate an EXISTING visible element with a canvas recipe WITHOUT hijacking it:
|
|
204
|
+
* no native element is created, opacity is never set, and the host is never
|
|
205
|
+
* reparented. An overlay canvas is added as a sibling and removed on destroy, so
|
|
206
|
+
* the host is byte-identical before and after. Recipe state is wired from the
|
|
207
|
+
* host's own events; for a form-control host, state.text/state.valid mirror
|
|
208
|
+
* el.value/el.validity (read at event time, never per frame). This is the honest
|
|
209
|
+
* home for a decoration over a real input (PasswordStrength, TypewriterField) and
|
|
210
|
+
* for generic form feedback (FocusHalo, ErrorShake, SuccessBloom). See 0004.
|
|
211
|
+
*/
|
|
212
|
+
export declare function decorateUIFX(
|
|
213
|
+
el: HTMLElement,
|
|
214
|
+
recipeFactory: RecipeFactory,
|
|
215
|
+
options?: DecorateOptions
|
|
216
|
+
): DecorateInstance;
|
|
217
|
+
|
|
120
218
|
export default mountUIFX;
|