@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 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
- **53 recipes** across UI element categories:
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** -- Password strength, Water level, Heat map
32
+ - **Indicators** -- Water level, Heat map
33
33
  - **Mood** -- Day/night, Reaction picker, Notification bell
34
- - **Feedback** -- Typewriter, Sound wave, Upload progress
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 53 recipes ship in the package on the `./recipes` subpath -- versioned,
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 53 recipes ship in the same package on the `./recipes` subpath and
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 53 recipes ship on the ./recipes subpath (tree-shakeable) -- import by name:
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** | **53 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
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`
@@ -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
- val: number, // 0--1 slider value (sliders)
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
- ## Three Element Types
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
 
@@ -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
- export interface MountOptions {
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;