@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 +95 -0
- package/README.md +64 -12
- package/UIFX-RECIPE-GUIDE.md +59 -3
- package/UIFXController.d.ts +86 -1
- package/UIFXController.js +513 -17
- package/UIFXRecipes.d.ts +42 -4
- package/UIFXRecipes.js +366 -46
- package/llms.txt +54 -13
- package/package.json +1 -1
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
|
-
**
|
|
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** --
|
|
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
|
-
- **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
|
|
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';
|
|
@@ -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
|
-
|
|
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** | **
|
|
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`
|
package/UIFX-RECIPE-GUIDE.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
##
|
|
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
|
|
package/UIFXController.d.ts
CHANGED
|
@@ -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;
|