@zakkster/lite-ui-fx 1.4.0 → 1.6.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,101 @@ 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.6.0] -- 2026-09-07
9
+
10
+ Decorate mode (U4b, the second half of roadmap U4). A second public mount mode
11
+ alongside the hijack `mountUIFX`: `decorateUIFX` positions a canvas AROUND an
12
+ existing visible element instead of hijacking it. Additive: a bare `mountUIFX`
13
+ mount is byte-identical to 1.5.0; 53 -> 56 recipes.
14
+
15
+ ### Added
16
+
17
+ - `decorateUIFX(el, recipeFactory, options)`: a canvas overlay AROUND a live
18
+ element -- no `opacity:0`, no reparent. The overlay is a sibling placed from the
19
+ host's offset box and removed on `destroy()`, so the host is byte-identical
20
+ before and after (additive-only). Recipe `state` is wired from the host's own
21
+ events; for a form-control host `state.text` and `state.valid` mirror `el.value`
22
+ and `el.validity`, read at event time (never per frame). `setValue`/`setChecked`
23
+ are hijack-only and throw. Options are a subset (`padding`, `seed`, `colors`,
24
+ `theme`, `text`, `font`); the hijack-only keys throw in decorate mode.
25
+ - Three decorate recipes, born themed and t3-gated: `FocusHalo`, `ErrorShake`,
26
+ `SuccessBloom` (generic form feedback, reading `state.focused` / `state.valid`).
27
+ Registered in `RECIPES` / `RECIPE_META` (type `'decorate'`) / the default export
28
+ / a new `UIFXRecipes5` barrel.
29
+ - `'decorate'` as a `RECIPE_META.type` routing tag: `mountRecipe(el, id)` routes a
30
+ decorate recipe to `decorateUIFX`. `VALID_META_TYPES` gains exactly this one
31
+ non-`UIType` tag; `mountUIFX` still rejects it (the two paths cannot cross).
32
+ - Optional `state.text` / `state.valid` fields (decorate mode only). TypeScript
33
+ `DecorateOptions`, `DecorateInstance`, and `decorateUIFX` in the d.ts.
34
+ - `decisions/0004-decorate-mode.md`; decorate coverage across the suite: t0 host
35
+ byte-identical DOM diff + a t9 `decorate-host-mutation` control, t1 fail-closed +
36
+ degenerate sweep, t2 A14-A16 (state wiring, host untouched, non-input host), t3
37
+ zero-alloc churn, t5 decorate on the shared ticker. 164 -> 177 node:test tests.
38
+
39
+ ### Changed
40
+
41
+ - `PasswordStrength` and `TypewriterField` re-homed from their U4a-era fake types
42
+ (slider / toggle) onto decorate mode (`RECIPE_META.type` `'decorate'`). Unlike
43
+ the U4a re-homes these are behaviour ports: they now read the live host input --
44
+ PasswordStrength derives strength from `state.text` (zero-alloc `charCodeAt`
45
+ scan, recomputed only on change); TypewriterField animates an underline that
46
+ grows with the typed text (no `measureText`). Both stay `themeable`.
47
+ - RECIPE_META covers 56 recipes; `themeable` true for all 56, `motionSafe` false
48
+ for all 56. All 56 stay zero-GC under the t3 frame-alloc gate (default AND
49
+ themed). `mountUIFX` and every hijack mount are unchanged.
50
+
51
+ ## [1.5.0] -- 2026-09-07
52
+
53
+ New native element types (U4a, the first half of roadmap U4). Vol.3 faked
54
+ checkboxes as `role=switch` toggles and knobs/progress meters as sliders; U4a
55
+ promotes them to their true native elements (law 1). Additive: a bare mount of
56
+ any existing recipe is unchanged; 50 -> 53 recipes. Decorate mode is U4b.
57
+
58
+ ### Added
59
+
60
+ - Three `UIType`s, each wrapping the correct native element: `CHECKBOX`
61
+ (`<input type=checkbox>`, no `role=switch`; indeterminate via `setValue(null)`,
62
+ exposed as `state.indeterminate`), `PROGRESS` (native `<progress>`,
63
+ non-interactive, value written by `setValue`; opt-in `announce` adds a
64
+ visually-hidden `aria-live=polite` region updated at 10% steps), and `KNOB`
65
+ (`<input type=range>`, arrow keys native, canvas-side `knobMode`
66
+ `'rotate' | 'vertical'` pointer mapping).
67
+ - `instance.setValue(v)` / `instance.setChecked(b)`: one call syncs the native
68
+ element, `state`, any PROGRESS announcer, and fires the recipe hook
69
+ (`onDrag`/`onToggle`) exactly once (a programmatic write emits no native event).
70
+ - Options `knobMode` (KNOB-only) and `announce` (PROGRESS-only), both validated
71
+ fail-closed (presence on the wrong type throws).
72
+ - Three recipes, born themed and t3-gated: `TickDraw`, `IndeterminateScan`
73
+ (CHECKBOX, honouring `state.indeterminate`), `LiquidFill` (PROGRESS). Registered
74
+ in `RECIPES` / `RECIPE_META` / the default export / a new `UIFXRecipes4` barrel.
75
+ - `decisions/0003-element-types.md`; controller `npm test` coverage for the new
76
+ types; t2 gains the CHECKBOX-no-switch, PROGRESS-value, KNOB-arrows, and
77
+ `setValue`/`setChecked`-once contracts (A10-A13).
78
+
79
+ ### Changed
80
+
81
+ - Eight Vol.3 recipes re-homed onto their true types (rippleCheck/morphCheck ->
82
+ checkbox; volumeKnob/compassKnob -> knob; ringProgress/batteryGauge/signalMeter/
83
+ uploadProgress -> progress). Re-home is a `RECIPE_META.type` string change only
84
+ -- no recipe body touched -- so each renders byte-identical to 1.4.0 (proven by
85
+ `git diff`); new types keep the donor's default geometry (checkbox 64x36,
86
+ knob/progress 200x28).
87
+ - The mount type guard and `registerRecipe` both derive their valid-type set from
88
+ `UIType`, so the controller, the registry, and the d.ts cannot drift as types
89
+ are added; an unknown type still throws (fail closed).
90
+ - Torture: `makeChurn` drives the new types (checkbox like toggle + sweeps
91
+ indeterminate, knob like slider, progress sweeps value with no hook); t0/t5
92
+ synthetic batches iterate all six types; the t3 tier now gates 53 recipes
93
+ (default AND themed). `npm test` 164 pass; torture `gc major=0`, `alloc=0 B/op`.
94
+ - Docs (`llms.txt`, `README.md`, both `.d.ts`) updated to 53 recipes and the new
95
+ types / options / methods.
96
+
97
+ ### Fixed
98
+
99
+ - The Vol.3 semantic mis-mounts: a checkbox is no longer announced as a switch
100
+ (WCAG role match), and progress meters are non-interactive rather than
101
+ user-draggable sliders.
102
+
8
103
  ## [1.4.0] -- 2026-09-07
9
104
 
10
105
  The theming pass (U3b), completing U3's third finding (U-06). One option
package/README.md CHANGED
@@ -21,26 +21,27 @@ https://cdpn.io/pen/debug/yyaPKpB
21
21
  ## Live Demo (UI-FX vol3.)
22
22
  https://cdpn.io/pen/debug/YPGEaYY
23
23
 
24
- **50 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
28
28
  - **Sliders** -- Spark, Cosmic Void, Laser, Aurora, Wave, Elastic Band, Gravity
29
29
  - **Knobs** -- Volume dial, Compass needle
30
- - **Progress** -- Ring, Battery, Signal meter
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
- - **Checkboxes** -- Ripple, Morph (X to check)
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 50 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 50 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 50 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';
@@ -136,7 +137,7 @@ import { RECIPES, RECIPE_META, RECIPE_NAMES, registerRecipe, mountRecipe } from
136
137
  | Parameter | Type | Description |
137
138
  |-----------|------|-------------|
138
139
  | `container` | `HTMLElement` | Parent to mount into |
139
- | `type` | `'button' \| 'toggle' \| 'slider'` | Determines native element type |
140
+ | `type` | `'button' \| 'toggle' \| 'slider' \| 'checkbox' \| 'progress' \| 'knob'` | Determines native element type |
140
141
  | `recipeFactory` | `() => Recipe` | Factory function (controller calls it) |
141
142
  | `options.width` | `number` | Element width (auto from type if omitted) |
142
143
  | `options.height` | `number` | Element height |
@@ -145,8 +146,48 @@ import { RECIPES, RECIPE_META, RECIPE_NAMES, registerRecipe, mountRecipe } from
145
146
  | `options.value` | `number` | Slider initial value, 0..1 (default 0.5); out-of-range throws |
146
147
  | `options.checked` | `boolean` | Toggle initial state (default false) |
147
148
  | `options.disabled` | `boolean` | Disables the native element; sets `state.disabled` |
149
+ | `options.knobMode` | `'rotate' \| 'vertical'` | KNOB only: pointer-to-value mapping (default `'rotate'`); wrong type throws |
150
+ | `options.announce` | `boolean` | PROGRESS only: opt-in `aria-live` announcements at 10% steps; wrong type throws |
148
151
 
149
- Returns `{ el, canvas, wrapper, state, destroy() }`.
152
+ Recipe theming options (`seed`, `colors`, `theme`, `text`, `font`) are also
153
+ accepted and forwarded to the recipe -- see `llms.txt` for the full option surface.
154
+
155
+ Returns `{ el, canvas, wrapper, state, setValue(v), setChecked(b), destroy() }`.
156
+
157
+ - `setValue(v)` -- SLIDER/KNOB/PROGRESS: set `v` in `0..1` (updates the element +
158
+ `state.val`, fires `onDrag` once). CHECKBOX: `setValue(null)` sets indeterminate.
159
+ - `setChecked(b)` -- TOGGLE/CHECKBOX: set checked (updates the element +
160
+ `state.toggled`, fires `onToggle` once).
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).
150
191
 
151
192
  ### Element Types
152
193
 
@@ -155,6 +196,13 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
155
196
  | `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
156
197
  | `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
157
198
  | `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0-1) |
199
+ | `UIType.CHECKBOX` | `<input type="checkbox">` (no `role=switch`) | `onToggle(checked)` | `state.toggled`, `state.indeterminate` |
200
+ | `UIType.PROGRESS` | `<progress>` (non-interactive) | (driven by `setValue`) | `state.val` (0-1) |
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`.
158
206
 
159
207
  ### State Object (provided to `tick()` every frame)
160
208
 
@@ -163,13 +211,16 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
163
211
  hover: boolean; // Pointer inside element
164
212
  active: boolean; // Pointer pressed
165
213
  focused: boolean; // Keyboard focus
166
- toggled: boolean; // Checkbox state
214
+ toggled: boolean; // Checkbox/toggle state
215
+ indeterminate: boolean; // CHECKBOX only: native indeterminate (setValue(null))
167
216
  disabled: boolean; // Disabled via options.disabled
168
217
  val: number; // Slider value (0-1)
169
218
  w: number; // Element width
170
219
  h: number; // Element height
171
220
  padding: number; // Canvas padding
172
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
173
224
  }
174
225
  ```
175
226
 
@@ -180,7 +231,7 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
180
231
  | Framer Motion | ~45 KB | React HOC | 0 | Via React | `npm i framer-motion` |
181
232
  | GSAP | ~25 KB | Timeline | 0 | Manual | `npm i gsap` |
182
233
  | Lottie | ~55 KB | JSON animation | After Effects | Manual | `npm i lottie-web` |
183
- | **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **50 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`** |
184
235
 
185
236
  ## Writing Custom Recipes
186
237
 
@@ -213,6 +264,7 @@ export function MyButton() {
213
264
  Full TypeScript declarations are included for:
214
265
 
215
266
  - `mountUIFX`
267
+ - `decorateUIFX` (+ `DecorateOptions`, `DecorateInstance`)
216
268
  - `UIType`
217
269
  - `UIFXState`
218
270
  - `UIFXPointer`
@@ -63,12 +63,16 @@ 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
+ // Decorate mode only (decorateUIFX): the live host's value + validity.
74
+ text: string, // the host form-control's value string ('' if none)
75
+ valid: boolean, // the host's validity (el.validity.valid, else true)
72
76
  }
73
77
  ```
74
78
 
@@ -121,13 +125,65 @@ const instance = mountUIFX(
121
125
  instance.destroy();
122
126
  ```
123
127
 
124
- ## Three Element Types
128
+ ## Element Types & Mount Modes
129
+
130
+ `mountUIFX` HIJACKS -- it creates one of six native elements (opacity:0) under the
131
+ canvas:
125
132
 
126
133
  | Type | Native Element | Recipe Gets | Key State |
127
134
  |------|----------------|-------------|-----------|
128
135
  | `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
129
136
  | `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
130
137
  | `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0--1) |
138
+ | `UIType.CHECKBOX` | `<input type="checkbox">` (no `role=switch`) | `onToggle(checked)` | `state.toggled`, `state.indeterminate` |
139
+ | `UIType.PROGRESS` | `<progress>` (non-interactive) | (driven by `setValue`) | `state.val` |
140
+ | `UIType.KNOB` | `<input type="range">` | `onDrag(val, velocity)` | `state.val`, `knobMode` |
141
+
142
+ ## Decorate-Mode Recipes (a canvas AROUND a live element)
143
+
144
+ `decorateUIFX(el, factory, options)` is the SECOND mount mode: instead of creating
145
+ a hidden element, it positions a canvas around an EXISTING, visible element (a real
146
+ `<input>`, a button, any element). Same recipe interface, same coordinate system --
147
+ `(0,0)` is the host's top-left, `state.w/h` are the host's size -- but three rules
148
+ differ, and breaking them is caught by the torture t0 decorate DOM-diff:
149
+
150
+ 1. **Never touch the host.** A decoration paints ONLY its overlay canvas. Do not
151
+ write `el.style`, set an attribute, or read/move the host in `init`/`tick`. The
152
+ host must be byte-identical after `destroy()` (additive-only). The controller
153
+ never sets `opacity:0` and never reparents the host -- neither may your recipe.
154
+ 2. **Read host content from `state`, at frame time, allocation-free.** For a
155
+ form-control host, `state.text` (the value string) and `state.valid` (validity)
156
+ are updated by the controller at EVENT time (input/change/invalid) -- never per
157
+ frame. Your `tick` reads them; scan `state.text` with `charCodeAt` (no allocating
158
+ string ops), and edge-detect `state.valid` against a closure-cached previous.
159
+ There is NO new hook: a decoration reacts by polling `state` in `tick`.
160
+ 3. **`setValue`/`setChecked` are hijack-only.** A decoration reflects the host; it
161
+ does not drive it. Both throw in decorate mode.
162
+
163
+ ```javascript
164
+ import { decorateUIFX } from './UIFXController.js';
165
+ // A decoration reads state.focused / state.valid / state.text; it never draws the
166
+ // host's own text (the real element already shows it).
167
+ export function Underline() {
168
+ let fill = 0;
169
+ return {
170
+ tick(ctx, dt, now, state) {
171
+ const len = (state.text || '').length; // number, no alloc
172
+ fill += ((len ? Math.min(len / 24, 1) : 0) - fill) * dt * 8;
173
+ ctx.strokeStyle = state.focused ? '#6ee7b6' : '#556';
174
+ ctx.lineWidth = 2;
175
+ ctx.beginPath();
176
+ ctx.moveTo(2, state.h - 3);
177
+ ctx.lineTo(2 + (state.w - 4) * fill, state.h - 3);
178
+ ctx.stroke();
179
+ },
180
+ };
181
+ }
182
+ const deco = decorateUIFX(document.querySelector('#field'), Underline);
183
+ ```
184
+
185
+ Register a decorate recipe with `RECIPE_META.type: 'decorate'` -- then
186
+ `mountRecipe(el, id)` routes it to `decorateUIFX` automatically.
131
187
 
132
188
  ## Using @zakkster Libraries in Recipes
133
189
 
@@ -1,11 +1,17 @@
1
1
  export declare const VERSION: string;
2
2
 
3
- export type UITypeValue = 'button' | 'toggle' | 'slider';
3
+ export type UITypeValue = 'button' | 'toggle' | 'slider' | 'checkbox' | 'progress' | 'knob';
4
4
 
5
5
  export declare const UIType: Readonly<{
6
6
  BUTTON: 'button';
7
7
  TOGGLE: 'toggle';
8
8
  SLIDER: 'slider';
9
+ /** Plain checkbox (no role=switch); indeterminate via setValue(null). */
10
+ CHECKBOX: 'checkbox';
11
+ /** Native <progress>, non-interactive; value driven by setValue. */
12
+ PROGRESS: 'progress';
13
+ /** <input type=range>; arrows native, pointer mapped by knobMode. */
14
+ KNOB: 'knob';
9
15
  }>;
10
16
 
11
17
  export interface UIFXState {
@@ -13,12 +19,19 @@ export interface UIFXState {
13
19
  active: boolean;
14
20
  focused: boolean;
15
21
  toggled: boolean;
22
+ /** CHECKBOX only: the native indeterminate state (set via setValue(null)). */
23
+ indeterminate: boolean;
16
24
  disabled: boolean;
17
25
  val: number;
18
26
  w: number;
19
27
  h: number;
20
28
  padding: number;
21
29
  dpr: number;
30
+ /** Decorate mode only (decorateUIFX): the host form-control's current value.
31
+ * Absent for hijack mounts and for a non-form host (then ''). */
32
+ text?: string;
33
+ /** Decorate mode only: the host's validity (el.validity.valid, else true). */
34
+ valid?: boolean;
22
35
  }
23
36
 
24
37
  export interface UIFXPointer {
@@ -72,6 +85,26 @@ export interface MountOptions {
72
85
  text?: string;
73
86
  /** Canvas font string; falls back to the recipe's historical font. */
74
87
  font?: string;
88
+ /** KNOB only: pointer-to-value mapping (default 'rotate'). Throws on any other type. */
89
+ knobMode?: 'rotate' | 'vertical';
90
+ /** PROGRESS only: opt-in aria-live announcements at 10% steps. Throws on any other type. */
91
+ announce?: boolean;
92
+ }
93
+
94
+ /**
95
+ * Options accepted by decorateUIFX. A subset of MountOptions: a decoration
96
+ * inherits the host's geometry (offset box) and value (read from the host), so
97
+ * the hijack-only options (width/height/value/checked/disabled/knobMode/announce/
98
+ * label) are rejected -- passing one throws (fail closed).
99
+ */
100
+ export interface DecorateOptions {
101
+ /** Overlay padding around the host, in px (default 40). */
102
+ padding?: number;
103
+ seed?: number;
104
+ colors?: string[];
105
+ theme?: { light: string; mid: string; dark: string };
106
+ text?: string;
107
+ font?: string;
75
108
  }
76
109
 
77
110
  export interface UIFXInstance {
@@ -80,6 +113,21 @@ export interface UIFXInstance {
80
113
  wrapper: HTMLDivElement;
81
114
  state: UIFXState;
82
115
 
116
+ /**
117
+ * Set a valued control (SLIDER/KNOB/PROGRESS) to v in [0,1]: updates the
118
+ * native element, state.val, any PROGRESS announcer, and fires onDrag once.
119
+ * For a CHECKBOX, setValue(null) sets the indeterminate state. Throws on the
120
+ * wrong element type or an out-of-range value.
121
+ */
122
+ setValue(v: number | null): void;
123
+
124
+ /**
125
+ * Set a TOGGLE/CHECKBOX checked state: updates the native element,
126
+ * state.toggled, clears indeterminate, and fires onToggle exactly once.
127
+ * Throws on any other element type.
128
+ */
129
+ setChecked(b: boolean): void;
130
+
83
131
  destroy(): void;
84
132
  }
85
133
 
@@ -90,4 +138,41 @@ export declare function mountUIFX(
90
138
  options?: MountOptions
91
139
  ): UIFXInstance;
92
140
 
141
+ /**
142
+ * The instance returned by decorateUIFX. Like UIFXInstance but WITHOUT `wrapper`
143
+ * (there is none -- the overlay is a sibling of the host, not a wrapper around
144
+ * it), and setValue/setChecked are hijack-only: a decoration reflects the host,
145
+ * it does not drive it, so both throw.
146
+ */
147
+ export interface DecorateInstance {
148
+ /** The decorated host element (unchanged -- decorate never mutates it). */
149
+ el: HTMLElement;
150
+ /** The overlay canvas (the only DOM node decorate adds). */
151
+ canvas: HTMLCanvasElement;
152
+ state: UIFXState;
153
+ /** Hijack-only. Throws in decorate mode. */
154
+ setValue(v?: number | null): void;
155
+ /** Hijack-only. Throws in decorate mode. */
156
+ setChecked(b?: boolean): void;
157
+ /** Remove the overlay + every listener decorate added; the host is left
158
+ * byte-identical to before decorate. Idempotent. */
159
+ destroy(): void;
160
+ }
161
+
162
+ /**
163
+ * Decorate an EXISTING visible element with a canvas recipe WITHOUT hijacking it:
164
+ * no native element is created, opacity is never set, and the host is never
165
+ * reparented. An overlay canvas is added as a sibling and removed on destroy, so
166
+ * the host is byte-identical before and after. Recipe state is wired from the
167
+ * host's own events; for a form-control host, state.text/state.valid mirror
168
+ * el.value/el.validity (read at event time, never per frame). This is the honest
169
+ * home for a decoration over a real input (PasswordStrength, TypewriterField) and
170
+ * for generic form feedback (FocusHalo, ErrorShake, SuccessBloom). See 0004.
171
+ */
172
+ export declare function decorateUIFX(
173
+ el: HTMLElement,
174
+ recipeFactory: RecipeFactory,
175
+ options?: DecorateOptions
176
+ ): DecorateInstance;
177
+
93
178
  export default mountUIFX;