@zakkster/lite-ui-fx 1.4.0 → 1.5.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 +52 -0
- package/README.md +24 -10
- package/UIFXController.d.ts +28 -1
- package/UIFXController.js +184 -17
- package/UIFXRecipes.d.ts +18 -3
- package/UIFXRecipes.js +167 -15
- package/llms.txt +24 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,58 @@ 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.5.0] -- 2026-09-07
|
|
9
|
+
|
|
10
|
+
New native element types (U4a, the first half of roadmap U4). Vol.3 faked
|
|
11
|
+
checkboxes as `role=switch` toggles and knobs/progress meters as sliders; U4a
|
|
12
|
+
promotes them to their true native elements (law 1). Additive: a bare mount of
|
|
13
|
+
any existing recipe is unchanged; 50 -> 53 recipes. Decorate mode is U4b.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Three `UIType`s, each wrapping the correct native element: `CHECKBOX`
|
|
18
|
+
(`<input type=checkbox>`, no `role=switch`; indeterminate via `setValue(null)`,
|
|
19
|
+
exposed as `state.indeterminate`), `PROGRESS` (native `<progress>`,
|
|
20
|
+
non-interactive, value written by `setValue`; opt-in `announce` adds a
|
|
21
|
+
visually-hidden `aria-live=polite` region updated at 10% steps), and `KNOB`
|
|
22
|
+
(`<input type=range>`, arrow keys native, canvas-side `knobMode`
|
|
23
|
+
`'rotate' | 'vertical'` pointer mapping).
|
|
24
|
+
- `instance.setValue(v)` / `instance.setChecked(b)`: one call syncs the native
|
|
25
|
+
element, `state`, any PROGRESS announcer, and fires the recipe hook
|
|
26
|
+
(`onDrag`/`onToggle`) exactly once (a programmatic write emits no native event).
|
|
27
|
+
- Options `knobMode` (KNOB-only) and `announce` (PROGRESS-only), both validated
|
|
28
|
+
fail-closed (presence on the wrong type throws).
|
|
29
|
+
- Three recipes, born themed and t3-gated: `TickDraw`, `IndeterminateScan`
|
|
30
|
+
(CHECKBOX, honouring `state.indeterminate`), `LiquidFill` (PROGRESS). Registered
|
|
31
|
+
in `RECIPES` / `RECIPE_META` / the default export / a new `UIFXRecipes4` barrel.
|
|
32
|
+
- `decisions/0003-element-types.md`; controller `npm test` coverage for the new
|
|
33
|
+
types; t2 gains the CHECKBOX-no-switch, PROGRESS-value, KNOB-arrows, and
|
|
34
|
+
`setValue`/`setChecked`-once contracts (A10-A13).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Eight Vol.3 recipes re-homed onto their true types (rippleCheck/morphCheck ->
|
|
39
|
+
checkbox; volumeKnob/compassKnob -> knob; ringProgress/batteryGauge/signalMeter/
|
|
40
|
+
uploadProgress -> progress). Re-home is a `RECIPE_META.type` string change only
|
|
41
|
+
-- no recipe body touched -- so each renders byte-identical to 1.4.0 (proven by
|
|
42
|
+
`git diff`); new types keep the donor's default geometry (checkbox 64x36,
|
|
43
|
+
knob/progress 200x28).
|
|
44
|
+
- The mount type guard and `registerRecipe` both derive their valid-type set from
|
|
45
|
+
`UIType`, so the controller, the registry, and the d.ts cannot drift as types
|
|
46
|
+
are added; an unknown type still throws (fail closed).
|
|
47
|
+
- Torture: `makeChurn` drives the new types (checkbox like toggle + sweeps
|
|
48
|
+
indeterminate, knob like slider, progress sweeps value with no hook); t0/t5
|
|
49
|
+
synthetic batches iterate all six types; the t3 tier now gates 53 recipes
|
|
50
|
+
(default AND themed). `npm test` 164 pass; torture `gc major=0`, `alloc=0 B/op`.
|
|
51
|
+
- Docs (`llms.txt`, `README.md`, both `.d.ts`) updated to 53 recipes and the new
|
|
52
|
+
types / options / methods.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- The Vol.3 semantic mis-mounts: a checkbox is no longer announced as a switch
|
|
57
|
+
(WCAG role match), and progress meters are non-interactive rather than
|
|
58
|
+
user-draggable sliders.
|
|
59
|
+
|
|
8
60
|
## [1.4.0] -- 2026-09-07
|
|
9
61
|
|
|
10
62
|
The theming pass (U3b), completing U3's third finding (U-06). One option
|
package/README.md
CHANGED
|
@@ -21,26 +21,26 @@ 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
|
+
**53 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
32
|
- **Indicators** -- Password strength, Water level, Heat map
|
|
33
33
|
- **Mood** -- Day/night, Reaction picker, Notification bell
|
|
34
34
|
- **Feedback** -- Typewriter, 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
40
|
|
|
41
41
|
Every recipe is zero-GC, uses `dt`-based animation, and includes accessibility indicators (focus rings, state labels).
|
|
42
42
|
|
|
43
|
-
All
|
|
43
|
+
All 53 recipes ship in the package on the `./recipes` subpath -- versioned,
|
|
44
44
|
typed, and tree-shakeable. With `sideEffects: false`, importing one recipe pulls
|
|
45
45
|
in only that recipe, so a controller-only install stays tiny.
|
|
46
46
|
|
|
@@ -65,7 +65,7 @@ Part of the [@zakkster/lite-*](https://www.npmjs.com/org/zakkster) ecosystem.
|
|
|
65
65
|
npm i @zakkster/lite-ui-fx
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
> The
|
|
68
|
+
> The 53 recipes ship in the same package on the `./recipes` subpath and
|
|
69
69
|
> tree-shake, so importing one adds only that one.
|
|
70
70
|
|
|
71
71
|
|
|
@@ -97,7 +97,7 @@ instance.destroy();
|
|
|
97
97
|
// Controller (always needed)
|
|
98
98
|
import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
|
|
99
99
|
|
|
100
|
-
// All
|
|
100
|
+
// All 53 recipes ship on the ./recipes subpath (tree-shakeable) -- import by name:
|
|
101
101
|
import { SwarmToggle, MagneticButton, SparkSlider } from '@zakkster/lite-ui-fx/recipes';
|
|
102
102
|
import { PendulumToggle, HeartbeatButton, RippleCheck } from '@zakkster/lite-ui-fx/recipes';
|
|
103
103
|
import { VolumeKnob, WaterLevel, TimerCountdown } from '@zakkster/lite-ui-fx/recipes';
|
|
@@ -136,7 +136,7 @@ import { RECIPES, RECIPE_META, RECIPE_NAMES, registerRecipe, mountRecipe } from
|
|
|
136
136
|
| Parameter | Type | Description |
|
|
137
137
|
|-----------|------|-------------|
|
|
138
138
|
| `container` | `HTMLElement` | Parent to mount into |
|
|
139
|
-
| `type` | `'button' \| 'toggle' \| 'slider'` | Determines native element type |
|
|
139
|
+
| `type` | `'button' \| 'toggle' \| 'slider' \| 'checkbox' \| 'progress' \| 'knob'` | Determines native element type |
|
|
140
140
|
| `recipeFactory` | `() => Recipe` | Factory function (controller calls it) |
|
|
141
141
|
| `options.width` | `number` | Element width (auto from type if omitted) |
|
|
142
142
|
| `options.height` | `number` | Element height |
|
|
@@ -145,8 +145,18 @@ import { RECIPES, RECIPE_META, RECIPE_NAMES, registerRecipe, mountRecipe } from
|
|
|
145
145
|
| `options.value` | `number` | Slider initial value, 0..1 (default 0.5); out-of-range throws |
|
|
146
146
|
| `options.checked` | `boolean` | Toggle initial state (default false) |
|
|
147
147
|
| `options.disabled` | `boolean` | Disables the native element; sets `state.disabled` |
|
|
148
|
+
| `options.knobMode` | `'rotate' \| 'vertical'` | KNOB only: pointer-to-value mapping (default `'rotate'`); wrong type throws |
|
|
149
|
+
| `options.announce` | `boolean` | PROGRESS only: opt-in `aria-live` announcements at 10% steps; wrong type throws |
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
Recipe theming options (`seed`, `colors`, `theme`, `text`, `font`) are also
|
|
152
|
+
accepted and forwarded to the recipe -- see `llms.txt` for the full option surface.
|
|
153
|
+
|
|
154
|
+
Returns `{ el, canvas, wrapper, state, setValue(v), setChecked(b), destroy() }`.
|
|
155
|
+
|
|
156
|
+
- `setValue(v)` -- SLIDER/KNOB/PROGRESS: set `v` in `0..1` (updates the element +
|
|
157
|
+
`state.val`, fires `onDrag` once). CHECKBOX: `setValue(null)` sets indeterminate.
|
|
158
|
+
- `setChecked(b)` -- TOGGLE/CHECKBOX: set checked (updates the element +
|
|
159
|
+
`state.toggled`, fires `onToggle` once).
|
|
150
160
|
|
|
151
161
|
### Element Types
|
|
152
162
|
|
|
@@ -155,6 +165,9 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
|
|
|
155
165
|
| `UIType.TOGGLE` | `<input type="checkbox" role="switch">` | `onToggle(checked)` | `state.toggled` |
|
|
156
166
|
| `UIType.BUTTON` | `<button>` | `onClick(x, y, state)` | `state.active` |
|
|
157
167
|
| `UIType.SLIDER` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0-1) |
|
|
168
|
+
| `UIType.CHECKBOX` | `<input type="checkbox">` (no `role=switch`) | `onToggle(checked)` | `state.toggled`, `state.indeterminate` |
|
|
169
|
+
| `UIType.PROGRESS` | `<progress>` (non-interactive) | (driven by `setValue`) | `state.val` (0-1) |
|
|
170
|
+
| `UIType.KNOB` | `<input type="range">` | `onDrag(val, velocity)` | `state.val` (0-1) |
|
|
158
171
|
|
|
159
172
|
### State Object (provided to `tick()` every frame)
|
|
160
173
|
|
|
@@ -163,7 +176,8 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
|
|
|
163
176
|
hover: boolean; // Pointer inside element
|
|
164
177
|
active: boolean; // Pointer pressed
|
|
165
178
|
focused: boolean; // Keyboard focus
|
|
166
|
-
toggled: boolean; // Checkbox state
|
|
179
|
+
toggled: boolean; // Checkbox/toggle state
|
|
180
|
+
indeterminate: boolean; // CHECKBOX only: native indeterminate (setValue(null))
|
|
167
181
|
disabled: boolean; // Disabled via options.disabled
|
|
168
182
|
val: number; // Slider value (0-1)
|
|
169
183
|
w: number; // Element width
|
|
@@ -180,7 +194,7 @@ Returns `{ el, canvas, wrapper, state, destroy() }`.
|
|
|
180
194
|
| Framer Motion | ~45 KB | React HOC | 0 | Via React | `npm i framer-motion` |
|
|
181
195
|
| GSAP | ~25 KB | Timeline | 0 | Manual | `npm i gsap` |
|
|
182
196
|
| Lottie | ~55 KB | JSON animation | After Effects | Manual | `npm i lottie-web` |
|
|
183
|
-
| **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **
|
|
197
|
+
| **lite-ui-fx** | **< 5 KB** | **Canvas hijack** | **53 built-in** | **Native + visual** | **`npm i @zakkster/lite-ui-fx`** |
|
|
184
198
|
|
|
185
199
|
## Writing Custom Recipes
|
|
186
200
|
|
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,6 +19,8 @@ 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;
|
|
@@ -72,6 +80,10 @@ export interface MountOptions {
|
|
|
72
80
|
text?: string;
|
|
73
81
|
/** Canvas font string; falls back to the recipe's historical font. */
|
|
74
82
|
font?: string;
|
|
83
|
+
/** KNOB only: pointer-to-value mapping (default 'rotate'). Throws on any other type. */
|
|
84
|
+
knobMode?: 'rotate' | 'vertical';
|
|
85
|
+
/** PROGRESS only: opt-in aria-live announcements at 10% steps. Throws on any other type. */
|
|
86
|
+
announce?: boolean;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
export interface UIFXInstance {
|
|
@@ -80,6 +92,21 @@ export interface UIFXInstance {
|
|
|
80
92
|
wrapper: HTMLDivElement;
|
|
81
93
|
state: UIFXState;
|
|
82
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Set a valued control (SLIDER/KNOB/PROGRESS) to v in [0,1]: updates the
|
|
97
|
+
* native element, state.val, any PROGRESS announcer, and fires onDrag once.
|
|
98
|
+
* For a CHECKBOX, setValue(null) sets the indeterminate state. Throws on the
|
|
99
|
+
* wrong element type or an out-of-range value.
|
|
100
|
+
*/
|
|
101
|
+
setValue(v: number | null): void;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Set a TOGGLE/CHECKBOX checked state: updates the native element,
|
|
105
|
+
* state.toggled, clears indeterminate, and fires onToggle exactly once.
|
|
106
|
+
* Throws on any other element type.
|
|
107
|
+
*/
|
|
108
|
+
setChecked(b: boolean): void;
|
|
109
|
+
|
|
83
110
|
destroy(): void;
|
|
84
111
|
}
|
|
85
112
|
|
package/UIFXController.js
CHANGED
|
@@ -22,7 +22,7 @@ import { Ticker } from '@zakkster/lite-ticker';
|
|
|
22
22
|
|
|
23
23
|
// Three-place version sync: this constant, package.json "version", and the
|
|
24
24
|
// VERSION line in llms.txt must always match. /release keeps them locked.
|
|
25
|
-
export const VERSION = '1.
|
|
25
|
+
export const VERSION = '1.5.0';
|
|
26
26
|
|
|
27
27
|
// ---------------------------------------------------------
|
|
28
28
|
// SHARED TICKER (ref-counted, one RAF for all UI components)
|
|
@@ -84,7 +84,8 @@ function releaseSliderStyle() {
|
|
|
84
84
|
// ---------------------------------------------------------
|
|
85
85
|
|
|
86
86
|
const KNOWN_HOOKS = ['init', 'tick', 'onHover', 'onLeave', 'onClick', 'onToggle', 'onDrag', 'destroy'];
|
|
87
|
-
const KNOWN_OPTIONS = ['width', 'height', 'padding', 'label', 'value', 'checked', 'disabled', 'seed', 'colors', 'theme', 'text', 'font'];
|
|
87
|
+
const KNOWN_OPTIONS = ['width', 'height', 'padding', 'label', 'value', 'checked', 'disabled', 'seed', 'colors', 'theme', 'text', 'font', 'knobMode', 'announce'];
|
|
88
|
+
const KNOB_MODES = ['rotate', 'vertical'];
|
|
88
89
|
|
|
89
90
|
// Levenshtein edit distance. Cold: only reached on the error path.
|
|
90
91
|
function _editDistance(a, b) {
|
|
@@ -143,8 +144,15 @@ export const UIType = Object.freeze({
|
|
|
143
144
|
BUTTON: 'button',
|
|
144
145
|
TOGGLE: 'toggle',
|
|
145
146
|
SLIDER: 'slider',
|
|
147
|
+
CHECKBOX: 'checkbox', // <input type=checkbox> WITHOUT role=switch (a check is not a switch)
|
|
148
|
+
PROGRESS: 'progress', // native <progress>, non-interactive, value driven programmatically
|
|
149
|
+
KNOB: 'knob', // <input type=range>, arrows native, canvas-side knobMode pointer map
|
|
146
150
|
});
|
|
147
151
|
|
|
152
|
+
// The valid mount types, derived once from UIType so the controller guard, the
|
|
153
|
+
// recipe registry, and the d.ts never drift apart. Cold: read only at mount.
|
|
154
|
+
const _KNOWN_TYPES = new Set(Object.values(UIType));
|
|
155
|
+
|
|
148
156
|
|
|
149
157
|
// =========================================================
|
|
150
158
|
// UIFXController -- The Canvas Hijacker
|
|
@@ -177,11 +185,11 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
177
185
|
throw new Error('mountUIFX: container must be a DOM element');
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
// 1b. type: exactly one of the
|
|
188
|
+
// 1b. type: exactly one of the known element types (UIType). An unknown or
|
|
181
189
|
// undefined type is an Error here, never a silent default to a button
|
|
182
190
|
// (fail closed -- the type selects the native element).
|
|
183
|
-
if (
|
|
184
|
-
throw new Error('mountUIFX: type must be UIType.BUTTON,
|
|
191
|
+
if (!_KNOWN_TYPES.has(type)) {
|
|
192
|
+
throw new Error('mountUIFX: type must be one of UIType.BUTTON, TOGGLE, SLIDER, CHECKBOX, PROGRESS, KNOB');
|
|
185
193
|
}
|
|
186
194
|
|
|
187
195
|
// 2. options: unknown keys -> did-you-mean; value/checked/disabled
|
|
@@ -205,6 +213,8 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
205
213
|
const height = options.height;
|
|
206
214
|
const padding = options.padding === undefined ? 40 : options.padding;
|
|
207
215
|
const label = options.label === undefined ? '' : options.label;
|
|
216
|
+
const knobMode = options.knobMode || 'rotate'; // KNOB pointer map; validated below
|
|
217
|
+
const announce = options.announce === true; // PROGRESS aria-live; validated below
|
|
208
218
|
|
|
209
219
|
// Reserved theming options (decisions/0002): all optional, validated fail
|
|
210
220
|
// closed here, then forwarded to the recipe factory which resolves them in
|
|
@@ -233,6 +243,26 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
233
243
|
throw new Error('mountUIFX: option "seed" must be a finite number');
|
|
234
244
|
}
|
|
235
245
|
|
|
246
|
+
// Type-scoped options (U4a). knobMode belongs only to a KNOB; announce only
|
|
247
|
+
// to a PROGRESS. Presence on the wrong type is a mistake, not a silent
|
|
248
|
+
// ignore (fail closed). Both validated here, before any element exists.
|
|
249
|
+
if (options.knobMode !== undefined) {
|
|
250
|
+
if (type !== UIType.KNOB) {
|
|
251
|
+
throw new Error('mountUIFX: option "knobMode" is only valid for UIType.KNOB');
|
|
252
|
+
}
|
|
253
|
+
if (KNOB_MODES.indexOf(options.knobMode) === -1) {
|
|
254
|
+
throw new Error('mountUIFX: option "knobMode" must be "rotate" or "vertical"');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (options.announce !== undefined) {
|
|
258
|
+
if (type !== UIType.PROGRESS) {
|
|
259
|
+
throw new Error('mountUIFX: option "announce" is only valid for UIType.PROGRESS');
|
|
260
|
+
}
|
|
261
|
+
if (typeof options.announce !== 'boolean') {
|
|
262
|
+
throw new Error('mountUIFX: option "announce" must be a boolean');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
236
266
|
// 3. recipeFactory
|
|
237
267
|
if (typeof recipeFactory !== 'function') {
|
|
238
268
|
throw new Error('mountUIFX: recipeFactory must be a function');
|
|
@@ -279,24 +309,37 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
279
309
|
|
|
280
310
|
try {
|
|
281
311
|
// -- Resolve dimensions --
|
|
282
|
-
|
|
283
|
-
|
|
312
|
+
// SLIDER/PROGRESS/KNOB share the 200x28 range geometry (so every vol.3
|
|
313
|
+
// re-home renders byte-identical to its slider era); CHECKBOX shares the
|
|
314
|
+
// 64x36 toggle geometry; BUTTON keeps 160x48.
|
|
315
|
+
const _rangeLike = type === UIType.SLIDER || type === UIType.PROGRESS || type === UIType.KNOB;
|
|
316
|
+
const w = width || (type === UIType.BUTTON ? 160 : _rangeLike ? 200 : 64);
|
|
317
|
+
const h = height || (type === UIType.BUTTON ? 48 : _rangeLike ? 28 : 36);
|
|
284
318
|
let dpr = window.devicePixelRatio || 1;
|
|
285
319
|
|
|
286
320
|
// -- Create native element (invisible, accessible, receives events) --
|
|
287
321
|
let el;
|
|
288
|
-
if (type === UIType.TOGGLE) {
|
|
322
|
+
if (type === UIType.TOGGLE || type === UIType.CHECKBOX) {
|
|
289
323
|
el = document.createElement('input');
|
|
290
324
|
el.type = 'checkbox';
|
|
291
|
-
|
|
325
|
+
// TOGGLE is a switch; CHECKBOX is a plain checkbox. A check is not a
|
|
326
|
+
// switch -- U4a drops the role for CHECKBOX (the vol.3 mis-mount fix).
|
|
327
|
+
if (type === UIType.TOGGLE) el.setAttribute('role', 'switch');
|
|
292
328
|
el.checked = checked; // coerced boolean; lands before frame 1
|
|
293
329
|
if (label) el.setAttribute('aria-label', label);
|
|
294
|
-
} else if (type === UIType.SLIDER) {
|
|
330
|
+
} else if (type === UIType.SLIDER || type === UIType.KNOB) {
|
|
295
331
|
el = document.createElement('input');
|
|
296
332
|
el.type = 'range';
|
|
297
333
|
el.min = '0'; el.max = '100';
|
|
298
334
|
el.value = value !== undefined ? String(value * 100) : '50'; // 0..1 -> 0..100
|
|
299
335
|
if (label) el.setAttribute('aria-label', label);
|
|
336
|
+
} else if (type === UIType.PROGRESS) {
|
|
337
|
+
// Non-interactive: value is written programmatically (setValue) only,
|
|
338
|
+
// and exposed to assistive tech by the native <progress> element.
|
|
339
|
+
el = document.createElement('progress');
|
|
340
|
+
el.max = 1;
|
|
341
|
+
el.value = value !== undefined ? value : 0; // 0..1
|
|
342
|
+
if (label) el.setAttribute('aria-label', label);
|
|
300
343
|
} else {
|
|
301
344
|
el = document.createElement('button');
|
|
302
345
|
el.textContent = label || 'Action';
|
|
@@ -316,7 +359,7 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
316
359
|
// Slider thumb needs explicit sizing for hit area. One shared, ref-counted
|
|
317
360
|
// <style> for all sliders (U-09): released in destroy() when the last slider
|
|
318
361
|
// goes -- head child count nets to zero across mount/destroy.
|
|
319
|
-
if (type === UIType.SLIDER) {
|
|
362
|
+
if (type === UIType.SLIDER || type === UIType.KNOB) {
|
|
320
363
|
acquireSliderStyle();
|
|
321
364
|
styleAcquired = true;
|
|
322
365
|
el.classList.add('uifx-slider');
|
|
@@ -349,14 +392,32 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
349
392
|
container.appendChild(wrapper);
|
|
350
393
|
wrapperAppended = true;
|
|
351
394
|
|
|
395
|
+
// -- Optional aria-live announcer for PROGRESS (U4a). Opt-in via
|
|
396
|
+
// { announce: true }: a visually-hidden polite region that setValue
|
|
397
|
+
// updates at 10% steps. It lives in the wrapper, so wrapper.remove() in
|
|
398
|
+
// destroy() takes it with everything else -- no separate teardown. --
|
|
399
|
+
let announceRegion = null;
|
|
400
|
+
if (type === UIType.PROGRESS && announce) {
|
|
401
|
+
announceRegion = document.createElement('span');
|
|
402
|
+
announceRegion.setAttribute('aria-live', 'polite');
|
|
403
|
+
Object.assign(announceRegion.style, {
|
|
404
|
+
position: 'absolute', width: '1px', height: '1px',
|
|
405
|
+
overflow: 'hidden', clipPath: 'inset(50%)',
|
|
406
|
+
whiteSpace: 'nowrap', border: '0', padding: '0', margin: '-1px',
|
|
407
|
+
});
|
|
408
|
+
wrapper.appendChild(announceRegion);
|
|
409
|
+
}
|
|
410
|
+
let _lastAnnouncePct = -1; // last announced 10% step (cold: only setValue writes)
|
|
411
|
+
|
|
352
412
|
// -- State (value/checked/disabled land here BEFORE frame 1) --
|
|
353
413
|
const state = {
|
|
354
414
|
hover: false,
|
|
355
415
|
active: false, // pointer is down
|
|
356
416
|
focused: false, // keyboard focus
|
|
357
417
|
toggled: checked, // coerced boolean; element + state AGREE
|
|
418
|
+
indeterminate: false, // CHECKBOX only; set via setValue(null)
|
|
358
419
|
disabled, // recipes can render a disabled look
|
|
359
|
-
val: value !== undefined ? value : (type === UIType.SLIDER ? 0.5 : 0), // 0-1
|
|
420
|
+
val: value !== undefined ? value : (type === UIType.SLIDER || type === UIType.KNOB ? 0.5 : 0), // 0-1
|
|
360
421
|
w, h, padding, dpr,
|
|
361
422
|
};
|
|
362
423
|
|
|
@@ -366,6 +427,28 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
366
427
|
// pointer event, then lazily filled once (see updatePointer).
|
|
367
428
|
let rect = null;
|
|
368
429
|
|
|
430
|
+
// Apply a numeric value to a valued control (SLIDER/KNOB/PROGRESS): reflect
|
|
431
|
+
// it to the native element, update state, announce (PROGRESS), and fire
|
|
432
|
+
// onDrag exactly once when asked. A programmatic el.value write fires NO
|
|
433
|
+
// native 'input', so this explicit hook call is the only one -- no double
|
|
434
|
+
// fire. Cold path (setValue + knob drag), never a per-frame body.
|
|
435
|
+
function _applyVal(v, fireHook) {
|
|
436
|
+
state.val = v;
|
|
437
|
+
if (type === UIType.PROGRESS) {
|
|
438
|
+
el.value = v; // 0..1, native max=1
|
|
439
|
+
if (announceRegion) {
|
|
440
|
+
const pct = Math.round(v * 10) * 10;
|
|
441
|
+
if (pct !== _lastAnnouncePct) {
|
|
442
|
+
_lastAnnouncePct = pct;
|
|
443
|
+
announceRegion.textContent = pct + '%';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
el.value = String(v * 100); // range 0..100
|
|
448
|
+
}
|
|
449
|
+
if (fireHook && recipe.onDrag) recipe.onDrag(v, pointer.vx, state);
|
|
450
|
+
}
|
|
451
|
+
|
|
369
452
|
// -- Initialize recipe (already validated in phase 1: object, tick fn,
|
|
370
453
|
// only known hooks). ctx exists now, so init can run. --
|
|
371
454
|
if (recipe.init) recipe.init(ctx, w, h, padding);
|
|
@@ -417,9 +500,12 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
417
500
|
el.addEventListener('focus', () => { state.focused = true; }, { signal });
|
|
418
501
|
el.addEventListener('blur', () => { state.focused = false; }, { signal });
|
|
419
502
|
|
|
420
|
-
// Toggle events
|
|
421
|
-
if (type === UIType.TOGGLE) {
|
|
503
|
+
// Toggle + checkbox events (both are a native <input type=checkbox>)
|
|
504
|
+
if (type === UIType.TOGGLE || type === UIType.CHECKBOX) {
|
|
422
505
|
el.addEventListener('change', () => {
|
|
506
|
+
// A user interaction resolves any indeterminate state (native does
|
|
507
|
+
// this too); keep state.indeterminate in agreement.
|
|
508
|
+
state.indeterminate = false;
|
|
423
509
|
state.toggled = el.checked;
|
|
424
510
|
if (recipe.onToggle) recipe.onToggle(state.toggled, state);
|
|
425
511
|
}, { signal });
|
|
@@ -431,14 +517,51 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
431
517
|
}, { signal });
|
|
432
518
|
}
|
|
433
519
|
|
|
434
|
-
// Slider events
|
|
435
|
-
|
|
520
|
+
// Slider + knob value events. A range input fires 'input' on native drag
|
|
521
|
+
// (slider) AND on arrow keys (both) -- one path for keyboard on either type.
|
|
522
|
+
if (type === UIType.SLIDER || type === UIType.KNOB) {
|
|
436
523
|
el.addEventListener('input', () => {
|
|
437
524
|
state.val = el.value / 100;
|
|
438
525
|
if (recipe.onDrag) recipe.onDrag(state.val, pointer.vx, state);
|
|
439
526
|
}, { signal });
|
|
440
527
|
}
|
|
441
528
|
|
|
529
|
+
// KNOB pointer remap (U4a). A range input maps value to horizontal thumb
|
|
530
|
+
// position; a knob maps a rotational or vertical drag instead. Arrow keys
|
|
531
|
+
// stay native (the 'input' handler above); for pointer we drive the value
|
|
532
|
+
// ourselves and preventDefault the native jump-to-pointer, restoring focus
|
|
533
|
+
// by hand. All cold: pointer handlers, no per-frame work.
|
|
534
|
+
if (type === UIType.KNOB) {
|
|
535
|
+
let knobActive = false;
|
|
536
|
+
let knobStartVal = 0;
|
|
537
|
+
let knobStartY = 0;
|
|
538
|
+
el.addEventListener('pointerdown', (e) => {
|
|
539
|
+
knobActive = true;
|
|
540
|
+
knobStartVal = state.val;
|
|
541
|
+
knobStartY = e.clientY;
|
|
542
|
+
refreshRect();
|
|
543
|
+
el.focus();
|
|
544
|
+
e.preventDefault(); // suppress the range's native jump-to-pointer
|
|
545
|
+
if (el.setPointerCapture) el.setPointerCapture(e.pointerId);
|
|
546
|
+
}, { signal });
|
|
547
|
+
el.addEventListener('pointermove', (e) => {
|
|
548
|
+
if (!knobActive) return;
|
|
549
|
+
let v;
|
|
550
|
+
if (knobMode === 'vertical') {
|
|
551
|
+
v = knobStartVal + (knobStartY - e.clientY) / 150; // 150px = full sweep
|
|
552
|
+
} else {
|
|
553
|
+
const cx = rect ? rect.left + rect.width / 2 : e.clientX;
|
|
554
|
+
const cy = rect ? rect.top + rect.height / 2 : e.clientY;
|
|
555
|
+
let a = Math.atan2(e.clientY - cy, e.clientX - cx); // -PI..PI
|
|
556
|
+
a = (a + Math.PI * 2.5) % (Math.PI * 2); // 0 at bottom, clockwise
|
|
557
|
+
v = a / (Math.PI * 2);
|
|
558
|
+
}
|
|
559
|
+
if (v < 0) v = 0; else if (v > 1) v = 1;
|
|
560
|
+
_applyVal(v, true); // reflect + fire onDrag once
|
|
561
|
+
}, { signal });
|
|
562
|
+
el.addEventListener('pointerup', () => { knobActive = false; }, { signal });
|
|
563
|
+
}
|
|
564
|
+
|
|
442
565
|
// -- DPR re-read on display change (cold, feature-detected). Absent
|
|
443
566
|
// matchMedia is a silent no-op: the canvas stays at mount DPR (fail
|
|
444
567
|
// closed, never throw). Listener bound to signal for teardown. --
|
|
@@ -497,6 +620,50 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
497
620
|
/** Current state (read-only reference). */
|
|
498
621
|
state,
|
|
499
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Programmatically set a valued control (SLIDER/KNOB/PROGRESS) to v in
|
|
625
|
+
* [0,1]: updates the native element, state.val, any PROGRESS announcer,
|
|
626
|
+
* and fires onDrag exactly once (a programmatic write emits no native
|
|
627
|
+
* event, so there is no second fire). For a CHECKBOX, setValue(null)
|
|
628
|
+
* sets the indeterminate state. Fail closed on wrong type / bad value.
|
|
629
|
+
*/
|
|
630
|
+
setValue(v) {
|
|
631
|
+
if (destroyed) return;
|
|
632
|
+
if (type === UIType.CHECKBOX) {
|
|
633
|
+
if (v === null) {
|
|
634
|
+
el.indeterminate = true;
|
|
635
|
+
state.indeterminate = true;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
throw new Error('setValue: a checkbox takes setChecked(bool), or setValue(null) for indeterminate');
|
|
639
|
+
}
|
|
640
|
+
if (type !== UIType.SLIDER && type !== UIType.KNOB && type !== UIType.PROGRESS) {
|
|
641
|
+
throw new Error('setValue: only SLIDER, KNOB, and PROGRESS carry a numeric value');
|
|
642
|
+
}
|
|
643
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0 || v > 1) {
|
|
644
|
+
throw new Error('setValue: v must be a number in [0,1]');
|
|
645
|
+
}
|
|
646
|
+
_applyVal(v, true);
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Programmatically set a TOGGLE/CHECKBOX checked state: updates the
|
|
651
|
+
* native element, state.toggled, clears indeterminate, and fires
|
|
652
|
+
* onToggle exactly once. Fail closed on the wrong type.
|
|
653
|
+
*/
|
|
654
|
+
setChecked(b) {
|
|
655
|
+
if (destroyed) return;
|
|
656
|
+
if (type !== UIType.TOGGLE && type !== UIType.CHECKBOX) {
|
|
657
|
+
throw new Error('setChecked: only TOGGLE and CHECKBOX carry a checked state');
|
|
658
|
+
}
|
|
659
|
+
const nb = !!b;
|
|
660
|
+
el.indeterminate = false;
|
|
661
|
+
el.checked = nb;
|
|
662
|
+
state.indeterminate = false;
|
|
663
|
+
state.toggled = nb;
|
|
664
|
+
if (recipe.onToggle) recipe.onToggle(nb, state);
|
|
665
|
+
},
|
|
666
|
+
|
|
500
667
|
/** Destroy everything. Idempotent. */
|
|
501
668
|
destroy() {
|
|
502
669
|
if (destroyed) return;
|
|
@@ -505,7 +672,7 @@ export function mountUIFX(container, type, recipeFactory, options = {}) {
|
|
|
505
672
|
removeTick();
|
|
506
673
|
if (recipe.destroy) recipe.destroy();
|
|
507
674
|
releaseTicker();
|
|
508
|
-
if (type === UIType.SLIDER) releaseSliderStyle();
|
|
675
|
+
if (type === UIType.SLIDER || type === UIType.KNOB) releaseSliderStyle();
|
|
509
676
|
wrapper.remove();
|
|
510
677
|
},
|
|
511
678
|
};
|
package/UIFXRecipes.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UIFXRecipe, UIFXInstance, MountOptions } from './UIFXController';
|
|
2
2
|
|
|
3
3
|
// ===========================================================
|
|
4
|
-
// RECIPE OPTIONS + FACTORIES (all
|
|
4
|
+
// RECIPE OPTIONS + FACTORIES (all 53)
|
|
5
5
|
// ===========================================================
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -72,6 +72,9 @@ export declare function HelixLoader(options?: RecipeOptions): UIFXRecipe;
|
|
|
72
72
|
// -- Vol.2: Checkboxes --
|
|
73
73
|
export declare function RippleCheck(options?: RecipeOptions): UIFXRecipe;
|
|
74
74
|
export declare function MorphCheck(options?: RecipeOptions): UIFXRecipe;
|
|
75
|
+
// -- U4a: CHECKBOX recipes (honour st.indeterminate) --
|
|
76
|
+
export declare function TickDraw(options?: RecipeOptions): UIFXRecipe;
|
|
77
|
+
export declare function IndeterminateScan(options?: RecipeOptions): UIFXRecipe;
|
|
75
78
|
|
|
76
79
|
// -- Vol.2: Counters --
|
|
77
80
|
export declare function FlameCounter(options?: RecipeOptions): UIFXRecipe;
|
|
@@ -88,6 +91,8 @@ export declare function CompassKnob(options?: RecipeOptions): UIFXRecipe;
|
|
|
88
91
|
export declare function RingProgress(options?: RecipeOptions): UIFXRecipe;
|
|
89
92
|
export declare function BatteryGauge(options?: RecipeOptions): UIFXRecipe;
|
|
90
93
|
export declare function SignalMeter(options?: RecipeOptions): UIFXRecipe;
|
|
94
|
+
// -- U4a: PROGRESS recipe --
|
|
95
|
+
export declare function LiquidFill(options?: RecipeOptions): UIFXRecipe;
|
|
91
96
|
|
|
92
97
|
// -- Vol.3: Controls --
|
|
93
98
|
export declare function PillTabs(options?: RecipeOptions): UIFXRecipe;
|
|
@@ -177,11 +182,18 @@ export declare const UIFXRecipes3: {
|
|
|
177
182
|
PullRefresh: typeof PullRefresh;
|
|
178
183
|
};
|
|
179
184
|
|
|
185
|
+
/** U4a additions -- new native element types (kept out of the Vol.1-3 snapshots). */
|
|
186
|
+
export declare const UIFXRecipes4: {
|
|
187
|
+
TickDraw: typeof TickDraw;
|
|
188
|
+
IndeterminateScan: typeof IndeterminateScan;
|
|
189
|
+
LiquidFill: typeof LiquidFill;
|
|
190
|
+
};
|
|
191
|
+
|
|
180
192
|
// ===========================================================
|
|
181
193
|
// RECIPE REGISTRY
|
|
182
194
|
// ===========================================================
|
|
183
195
|
|
|
184
|
-
export type RecipeType = 'toggle' | 'button' | 'slider';
|
|
196
|
+
export type RecipeType = 'toggle' | 'button' | 'slider' | 'checkbox' | 'progress' | 'knob';
|
|
185
197
|
|
|
186
198
|
export type RecipeFactory = (options?: Record<string, unknown>) => UIFXRecipe;
|
|
187
199
|
|
|
@@ -221,7 +233,7 @@ export declare function mountRecipe(
|
|
|
221
233
|
): UIFXInstance;
|
|
222
234
|
|
|
223
235
|
// ===========================================================
|
|
224
|
-
// DEFAULT EXPORT -- combined all-
|
|
236
|
+
// DEFAULT EXPORT -- combined all-53 namespace
|
|
225
237
|
// ===========================================================
|
|
226
238
|
|
|
227
239
|
declare const UIFXAllRecipes: {
|
|
@@ -252,6 +264,8 @@ declare const UIFXAllRecipes: {
|
|
|
252
264
|
HelixLoader: typeof HelixLoader;
|
|
253
265
|
RippleCheck: typeof RippleCheck;
|
|
254
266
|
MorphCheck: typeof MorphCheck;
|
|
267
|
+
TickDraw: typeof TickDraw;
|
|
268
|
+
IndeterminateScan: typeof IndeterminateScan;
|
|
255
269
|
FlameCounter: typeof FlameCounter;
|
|
256
270
|
GlitchCounter: typeof GlitchCounter;
|
|
257
271
|
BubbleRating: typeof BubbleRating;
|
|
@@ -260,6 +274,7 @@ declare const UIFXAllRecipes: {
|
|
|
260
274
|
RingProgress: typeof RingProgress;
|
|
261
275
|
BatteryGauge: typeof BatteryGauge;
|
|
262
276
|
SignalMeter: typeof SignalMeter;
|
|
277
|
+
LiquidFill: typeof LiquidFill;
|
|
263
278
|
PillTabs: typeof PillTabs;
|
|
264
279
|
Stepper: typeof Stepper;
|
|
265
280
|
RadioOrbit: typeof RadioOrbit;
|
package/UIFXRecipes.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-ui-fx -- Recipe Collection (all
|
|
2
|
+
* @zakkster/lite-ui-fx -- Recipe Collection (all 53)
|
|
3
3
|
*
|
|
4
4
|
* The three recipe volumes consolidated into one shipped, typed, versioned
|
|
5
|
-
* module, exposed as the ./recipes subpath export
|
|
6
|
-
* decisions/0001-recipes-position.md.
|
|
5
|
+
* module, exposed as the ./recipes subpath export, plus the U4a additions for
|
|
6
|
+
* the new native element types. See decisions/0001-recipes-position.md.
|
|
7
7
|
*
|
|
8
8
|
* Vol.1 (10): SwarmToggle, LiquidToggle, NeonPulseToggle, MagneticButton,
|
|
9
9
|
* ShatterButton, ConfettiButton, GlitchButton, SparkSlider,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* HeatMap, DayNightToggle, ReactionPicker, NotificationBell,
|
|
19
19
|
* TypewriterField, SoundWaveBtn, UploadProgress, ScratchReveal,
|
|
20
20
|
* TimerCountdown, PullRefresh
|
|
21
|
+
* U4a (3): TickDraw, IndeterminateScan (CHECKBOX), LiquidFill (PROGRESS)
|
|
21
22
|
*
|
|
22
23
|
* Registry: RECIPES (null-prototype), RECIPE_META (live), RECIPE_NAMES,
|
|
23
24
|
* registerRecipe(id, factory, meta?), mountRecipe(container, id, options?).
|
|
@@ -1588,6 +1589,84 @@ export function MorphCheck(o = {}) {
|
|
|
1588
1589
|
};
|
|
1589
1590
|
}
|
|
1590
1591
|
|
|
1592
|
+
/** Tick Draw (U4a CHECKBOX) -- the checkmark strokes itself on; a centred dash
|
|
1593
|
+
* when indeterminate; empty when off. Reads st.toggled + st.indeterminate. */
|
|
1594
|
+
export function TickDraw(o = {}) {
|
|
1595
|
+
const P = resolveTheme(o, { accent: '#34d399', surface: '#0a0a12', dim2: '#8888aa' });
|
|
1596
|
+
const boxOff = 'rgba(255,255,255,.06)';
|
|
1597
|
+
const FONT = pickFont(o, "500 9px 'JetBrains Mono',monospace");
|
|
1598
|
+
let drawT = 0; // 0..1 stroke progress of the check
|
|
1599
|
+
return {
|
|
1600
|
+
tick(ctx, dt, now, st) {
|
|
1601
|
+
// Indeterminate suppresses the check; a full check draws only when on.
|
|
1602
|
+
drawT = lerp(drawT, st.indeterminate ? 0 : (st.toggled ? 1 : 0), dt * 12);
|
|
1603
|
+
const sz = Math.min(st.w, st.h), cx = sz / 2, cy = sz / 2, r = 6;
|
|
1604
|
+
|
|
1605
|
+
ctx.fillStyle = (st.toggled || st.indeterminate) ? P.accent : boxOff;
|
|
1606
|
+
roundRect(ctx, 0, 0, sz, sz, r); ctx.fill();
|
|
1607
|
+
|
|
1608
|
+
ctx.strokeStyle = P.surface; ctx.lineWidth = 3; ctx.lineCap = 'round';
|
|
1609
|
+
if (st.indeterminate) {
|
|
1610
|
+
// Standard indeterminate glyph: one centred dash.
|
|
1611
|
+
ctx.beginPath(); ctx.moveTo(sz * 0.28, cy); ctx.lineTo(sz * 0.72, cy); ctx.stroke();
|
|
1612
|
+
} else if (drawT > 0.01) {
|
|
1613
|
+
const p1 = clamp(drawT * 2, 0, 1), p2 = clamp(drawT * 2 - 1, 0, 1);
|
|
1614
|
+
ctx.beginPath();
|
|
1615
|
+
ctx.moveTo(cx - 7, cy);
|
|
1616
|
+
ctx.lineTo(cx - 7 + 7 * p1, cy + 7 * p1);
|
|
1617
|
+
if (p2 > 0) ctx.lineTo(cx + 14 * p2, cy + 7 - 14 * p2);
|
|
1618
|
+
ctx.stroke();
|
|
1619
|
+
}
|
|
1620
|
+
ctx.lineCap = 'butt';
|
|
1621
|
+
|
|
1622
|
+
label(ctx, st.indeterminate ? '\u2212' : (st.toggled ? '\u2713' : '\u25cb'),
|
|
1623
|
+
cx, sz + 12, (st.toggled || st.indeterminate) ? P.accent : P.dim2, FONT);
|
|
1624
|
+
if (st.focused) focusRing(ctx, sz, sz, r);
|
|
1625
|
+
},
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/** Indeterminate Scan (U4a CHECKBOX) -- sweeps a scan line while indeterminate,
|
|
1630
|
+
* settles to a stroked check when toggled, empty when off. */
|
|
1631
|
+
export function IndeterminateScan(o = {}) {
|
|
1632
|
+
const P = resolveTheme(o, { accent: '#60a5fa', surface: '#0a0a12', dim2: '#8888aa' });
|
|
1633
|
+
const FONT = pickFont(o, "500 9px 'JetBrains Mono',monospace");
|
|
1634
|
+
let scan = 0; // 0..1 sweep phase
|
|
1635
|
+
let checkT = 0; // 0..1 check reveal
|
|
1636
|
+
return {
|
|
1637
|
+
tick(ctx, dt, now, st) {
|
|
1638
|
+
checkT = lerp(checkT, (!st.indeterminate && st.toggled) ? 1 : 0, dt * 12);
|
|
1639
|
+
const sz = Math.min(st.w, st.h), cx = sz / 2, cy = sz / 2, r = 6;
|
|
1640
|
+
|
|
1641
|
+
ctx.fillStyle = (st.toggled && !st.indeterminate) ? P.accent : 'rgba(255,255,255,.06)';
|
|
1642
|
+
roundRect(ctx, 0, 0, sz, sz, r); ctx.fill();
|
|
1643
|
+
|
|
1644
|
+
if (st.indeterminate) {
|
|
1645
|
+
// Scan line sweeping down, fading at the sweep ends (globalAlpha,
|
|
1646
|
+
// not a per-frame colour string).
|
|
1647
|
+
scan += dt * 1.6; if (scan > 1) scan -= 1;
|
|
1648
|
+
const y = 4 + scan * (sz - 8);
|
|
1649
|
+
ctx.strokeStyle = P.accent; ctx.lineWidth = 2; ctx.lineCap = 'round';
|
|
1650
|
+
ctx.globalAlpha = Math.sin(scan * Math.PI);
|
|
1651
|
+
ctx.beginPath(); ctx.moveTo(4, y); ctx.lineTo(sz - 4, y); ctx.stroke();
|
|
1652
|
+
ctx.globalAlpha = 1; ctx.lineCap = 'butt';
|
|
1653
|
+
} else if (checkT > 0.01) {
|
|
1654
|
+
const p1 = clamp(checkT * 2, 0, 1), p2 = clamp(checkT * 2 - 1, 0, 1);
|
|
1655
|
+
ctx.strokeStyle = P.surface; ctx.lineWidth = 3; ctx.lineCap = 'round';
|
|
1656
|
+
ctx.beginPath();
|
|
1657
|
+
ctx.moveTo(cx - 7, cy);
|
|
1658
|
+
ctx.lineTo(cx - 7 + 7 * p1, cy + 7 * p1);
|
|
1659
|
+
if (p2 > 0) ctx.lineTo(cx + 14 * p2, cy + 7 - 14 * p2);
|
|
1660
|
+
ctx.stroke(); ctx.lineCap = 'butt';
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
label(ctx, st.indeterminate ? '\u2212' : (st.toggled ? '\u2713' : '\u25cb'),
|
|
1664
|
+
cx, sz + 12, (st.toggled || st.indeterminate) ? P.accent : P.dim2, FONT);
|
|
1665
|
+
if (st.focused) focusRing(ctx, sz, sz, r);
|
|
1666
|
+
},
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1591
1670
|
|
|
1592
1671
|
// ===========================================================
|
|
1593
1672
|
// COUNTERS (use slider val as input)
|
|
@@ -2037,6 +2116,55 @@ export function SignalMeter(o = {}) {
|
|
|
2037
2116
|
// CONTROLS
|
|
2038
2117
|
// ===========================================================
|
|
2039
2118
|
|
|
2119
|
+
/** Liquid Fill (U4a PROGRESS) -- a container that fills with a waving liquid to
|
|
2120
|
+
* the progress value, with a percentage readout. Non-interactive. */
|
|
2121
|
+
export function LiquidFill(o = {}) {
|
|
2122
|
+
const P = resolveTheme(o, { accent: '#22d3ee', accent2: '#0ea5e9', dim: '#e2e2f0' });
|
|
2123
|
+
const FONT = pickFont(o, "700 14px 'JetBrains Mono',monospace");
|
|
2124
|
+
let displayVal = 0, phase = 0, liquid = null;
|
|
2125
|
+
return {
|
|
2126
|
+
init(c, w, h) {
|
|
2127
|
+
// Vertical liquid gradient, built ONCE (cold): crest -> base.
|
|
2128
|
+
const g = c.createLinearGradient(0, 0, 0, h);
|
|
2129
|
+
g.addColorStop(0, P.accent);
|
|
2130
|
+
g.addColorStop(1, P.accent2);
|
|
2131
|
+
liquid = g;
|
|
2132
|
+
},
|
|
2133
|
+
tick(c, dt, now, st) {
|
|
2134
|
+
displayVal = lerp(displayVal, st.val, dt * 5);
|
|
2135
|
+
phase += dt * 3;
|
|
2136
|
+
const w = st.w, h = st.h, r = Math.min(12, h / 2);
|
|
2137
|
+
|
|
2138
|
+
// Container outline
|
|
2139
|
+
c.strokeStyle = 'rgba(255,255,255,.14)'; c.lineWidth = 2;
|
|
2140
|
+
roundRect(c, 1, 1, w - 2, h - 2, r); c.stroke();
|
|
2141
|
+
|
|
2142
|
+
// Liquid body, clipped to the container, with a sine surface. Const
|
|
2143
|
+
// gradient fill + fixed-step sampling -> zero per-frame allocation.
|
|
2144
|
+
c.save();
|
|
2145
|
+
roundRect(c, 1, 1, w - 2, h - 2, r); c.clip();
|
|
2146
|
+
const surface = h - displayVal * h;
|
|
2147
|
+
const amp = displayVal > 0.01 && displayVal < 0.99 ? 3 : 0;
|
|
2148
|
+
c.fillStyle = liquid || P.accent;
|
|
2149
|
+
c.beginPath();
|
|
2150
|
+
c.moveTo(0, h);
|
|
2151
|
+
c.lineTo(0, surface);
|
|
2152
|
+
for (let x = 0; x <= w; x += 6) {
|
|
2153
|
+
c.lineTo(x, surface + Math.sin(x * 0.15 + phase) * amp);
|
|
2154
|
+
}
|
|
2155
|
+
c.lineTo(w, h);
|
|
2156
|
+
c.closePath();
|
|
2157
|
+
c.fill();
|
|
2158
|
+
c.restore();
|
|
2159
|
+
|
|
2160
|
+
// Percentage readout (PCT LUT -- no per-frame string build)
|
|
2161
|
+
c.fillStyle = P.dim; c.font = FONT; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
2162
|
+
c.fillText(PCT[Math.round(displayVal * 100)], w / 2, h / 2);
|
|
2163
|
+
if (st.focused) fr(c, w, h, r);
|
|
2164
|
+
},
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2040
2168
|
/** 6. Pill Tabs -- 3 segmented tabs with sliding indicator. */
|
|
2041
2169
|
export function PillTabs(o = {}) {
|
|
2042
2170
|
const P = resolveTheme(o, { accent: '#c4b5fd', accent2: '#a78bfa', dim: '#9999b8' });
|
|
@@ -2726,9 +2854,17 @@ export const UIFXRecipes3 = {
|
|
|
2726
2854
|
ScratchReveal, TimerCountdown, PullRefresh,
|
|
2727
2855
|
};
|
|
2728
2856
|
|
|
2857
|
+
// U4a additions -- new native element types (CHECKBOX, PROGRESS). Kept out of the
|
|
2858
|
+
// Vol.1-3 historical snapshots above so those stay accurate; all recipes remain
|
|
2859
|
+
// reachable via RECIPES / RECIPE_META and their named exports regardless.
|
|
2860
|
+
export const UIFXRecipes4 = {
|
|
2861
|
+
TickDraw, IndeterminateScan,
|
|
2862
|
+
LiquidFill,
|
|
2863
|
+
};
|
|
2864
|
+
|
|
2729
2865
|
|
|
2730
2866
|
// ===========================================================
|
|
2731
|
-
// DEFAULT EXPORT -- combined all-
|
|
2867
|
+
// DEFAULT EXPORT -- combined all-53 namespace
|
|
2732
2868
|
// ===========================================================
|
|
2733
2869
|
|
|
2734
2870
|
export default {
|
|
@@ -2759,6 +2895,8 @@ export default {
|
|
|
2759
2895
|
HelixLoader,
|
|
2760
2896
|
RippleCheck,
|
|
2761
2897
|
MorphCheck,
|
|
2898
|
+
TickDraw,
|
|
2899
|
+
IndeterminateScan,
|
|
2762
2900
|
FlameCounter,
|
|
2763
2901
|
GlitchCounter,
|
|
2764
2902
|
BubbleRating,
|
|
@@ -2767,6 +2905,7 @@ export default {
|
|
|
2767
2905
|
RingProgress,
|
|
2768
2906
|
BatteryGauge,
|
|
2769
2907
|
SignalMeter,
|
|
2908
|
+
LiquidFill,
|
|
2770
2909
|
PillTabs,
|
|
2771
2910
|
Stepper,
|
|
2772
2911
|
RadioOrbit,
|
|
@@ -2822,6 +2961,8 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
2822
2961
|
helixLoader: HelixLoader,
|
|
2823
2962
|
rippleCheck: RippleCheck,
|
|
2824
2963
|
morphCheck: MorphCheck,
|
|
2964
|
+
tickDraw: TickDraw,
|
|
2965
|
+
indeterminateScan: IndeterminateScan,
|
|
2825
2966
|
flameCounter: FlameCounter,
|
|
2826
2967
|
glitchCounter: GlitchCounter,
|
|
2827
2968
|
bubbleRating: BubbleRating,
|
|
@@ -2830,6 +2971,7 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
2830
2971
|
ringProgress: RingProgress,
|
|
2831
2972
|
batteryGauge: BatteryGauge,
|
|
2832
2973
|
signalMeter: SignalMeter,
|
|
2974
|
+
liquidFill: LiquidFill,
|
|
2833
2975
|
pillTabs: PillTabs,
|
|
2834
2976
|
stepper: Stepper,
|
|
2835
2977
|
radioOrbit: RadioOrbit,
|
|
@@ -2883,16 +3025,19 @@ export const RECIPE_META = [
|
|
|
2883
3025
|
{ id: 'gravitySlider', name: 'Gravity Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
2884
3026
|
{ id: 'orbitLoader', name: 'Orbit Loader', type: 'toggle', family: 'Loaders', themeable: true, motionSafe: false },
|
|
2885
3027
|
{ id: 'helixLoader', name: 'Helix Loader', type: 'toggle', family: 'Loaders', themeable: true, motionSafe: false },
|
|
2886
|
-
{ id: 'rippleCheck', name: 'Ripple Check', type: '
|
|
2887
|
-
{ id: 'morphCheck', name: 'Morph Check', type: '
|
|
3028
|
+
{ id: 'rippleCheck', name: 'Ripple Check', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3029
|
+
{ id: 'morphCheck', name: 'Morph Check', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3030
|
+
{ id: 'tickDraw', name: 'Tick Draw', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3031
|
+
{ id: 'indeterminateScan', name: 'Indeterminate Scan', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
2888
3032
|
{ id: 'flameCounter', name: 'Flame Counter', type: 'slider', family: 'Counters', themeable: true, motionSafe: false },
|
|
2889
3033
|
{ id: 'glitchCounter', name: 'Glitch Counter', type: 'slider', family: 'Counters', themeable: true, motionSafe: false },
|
|
2890
3034
|
{ id: 'bubbleRating', name: 'Bubble Rating', type: 'slider', family: 'Rating', themeable: true, motionSafe: false },
|
|
2891
|
-
{ id: 'volumeKnob', name: 'Volume Knob', type: '
|
|
2892
|
-
{ id: 'compassKnob', name: 'Compass Knob', type: '
|
|
2893
|
-
{ id: 'ringProgress', name: 'Ring Progress', type: '
|
|
2894
|
-
{ id: 'batteryGauge', name: 'Battery Gauge', type: '
|
|
2895
|
-
{ id: 'signalMeter', name: 'Signal Meter', type: '
|
|
3035
|
+
{ id: 'volumeKnob', name: 'Volume Knob', type: 'knob', family: 'Knobs', themeable: true, motionSafe: false },
|
|
3036
|
+
{ id: 'compassKnob', name: 'Compass Knob', type: 'knob', family: 'Knobs', themeable: true, motionSafe: false },
|
|
3037
|
+
{ id: 'ringProgress', name: 'Ring Progress', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3038
|
+
{ id: 'batteryGauge', name: 'Battery Gauge', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3039
|
+
{ id: 'signalMeter', name: 'Signal Meter', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3040
|
+
{ id: 'liquidFill', name: 'Liquid Fill', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
2896
3041
|
{ id: 'pillTabs', name: 'Pill Tabs', type: 'button', family: 'Controls', themeable: true, motionSafe: false },
|
|
2897
3042
|
{ id: 'stepper', name: 'Stepper', type: 'button', family: 'Controls', themeable: true, motionSafe: false },
|
|
2898
3043
|
{ id: 'radioOrbit', name: 'Radio Orbit', type: 'slider', family: 'Controls', themeable: true, motionSafe: false },
|
|
@@ -2904,7 +3049,7 @@ export const RECIPE_META = [
|
|
|
2904
3049
|
{ id: 'notificationBell', name: 'Notification Bell', type: 'button', family: 'Mood', themeable: true, motionSafe: false },
|
|
2905
3050
|
{ id: 'typewriterField', name: 'Typewriter Field', type: 'toggle', family: 'Feedback', themeable: true, motionSafe: false },
|
|
2906
3051
|
{ id: 'soundWaveBtn', name: 'Sound Wave Btn', type: 'button', family: 'Feedback', themeable: true, motionSafe: false },
|
|
2907
|
-
{ id: 'uploadProgress', name: 'Upload Progress', type: '
|
|
3052
|
+
{ id: 'uploadProgress', name: 'Upload Progress', type: 'progress', family: 'Feedback', themeable: true, motionSafe: false },
|
|
2908
3053
|
{ id: 'scratchReveal', name: 'Scratch Reveal', type: 'slider', family: 'Fun', themeable: true, motionSafe: false },
|
|
2909
3054
|
{ id: 'timerCountdown', name: 'Timer Countdown', type: 'toggle', family: 'Fun', themeable: true, motionSafe: false },
|
|
2910
3055
|
{ id: 'pullRefresh', name: 'Pull Refresh', type: 'slider', family: 'Fun', themeable: true, motionSafe: false },
|
|
@@ -2913,6 +3058,11 @@ export const RECIPE_META = [
|
|
|
2913
3058
|
/** Names of every built-in recipe (the keys of RECIPES at load time). */
|
|
2914
3059
|
export const RECIPE_NAMES = Object.freeze(Object.keys(RECIPES));
|
|
2915
3060
|
|
|
3061
|
+
// The valid recipe/mount types, taken from the controller's UIType so the
|
|
3062
|
+
// registry's fail-closed check and the controller's mount guard are one source
|
|
3063
|
+
// of truth (they cannot drift as U4 adds types). Built once at load (cold).
|
|
3064
|
+
const VALID_META_TYPES = new Set(Object.values(UIType));
|
|
3065
|
+
|
|
2916
3066
|
/**
|
|
2917
3067
|
* Register a custom recipe, or override a built-in. Instantly usable via
|
|
2918
3068
|
* RECIPES[id] and reflected in RECIPE_META so existing pickers keep working.
|
|
@@ -2937,9 +3087,11 @@ export function registerRecipe(id, factory, meta) {
|
|
|
2937
3087
|
const type = (meta && meta.type) || (prev && prev.type) || undefined;
|
|
2938
3088
|
// A recipe's type selects its native element; a typeless recipe cannot be
|
|
2939
3089
|
// mounted. Reject it at registration (fail closed) -- checked BEFORE any
|
|
2940
|
-
// mutation, so a rejected call leaves RECIPES/RECIPE_META untouched.
|
|
2941
|
-
|
|
2942
|
-
|
|
3090
|
+
// mutation, so a rejected call leaves RECIPES/RECIPE_META untouched. The
|
|
3091
|
+
// valid set is UIType (VALID_META_TYPES), so registry + controller never
|
|
3092
|
+
// disagree about what a type is.
|
|
3093
|
+
if (!VALID_META_TYPES.has(type)) {
|
|
3094
|
+
throw new TypeError('registerRecipe: type must be one of "button", "toggle", "slider", "checkbox", "progress", "knob"');
|
|
2943
3095
|
}
|
|
2944
3096
|
|
|
2945
3097
|
RECIPES[id] = factory;
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @zakkster/lite-ui-fx
|
|
2
|
-
> Canvas-hijacked UI components with pluggable recipe system.
|
|
2
|
+
> Canvas-hijacked UI components with pluggable recipe system. 53 built-in recipes.
|
|
3
3
|
|
|
4
|
-
VERSION 1.
|
|
4
|
+
VERSION 1.5.0
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
npm i @zakkster/lite-ui-fx
|
|
@@ -14,7 +14,7 @@ Recipe = { tick(), init?(), onHover?(), onClick?(), onToggle?(), onDrag?(), dest
|
|
|
14
14
|
## Import -- Controller
|
|
15
15
|
import { mountUIFX, UIType } from '@zakkster/lite-ui-fx';
|
|
16
16
|
|
|
17
|
-
## Import -- Recipes (one ./recipes subpath,
|
|
17
|
+
## Import -- Recipes (one ./recipes subpath, 53 total, tree-shakeable)
|
|
18
18
|
import { SwarmToggle, MagneticButton, SparkSlider } from '@zakkster/lite-ui-fx/recipes';
|
|
19
19
|
import { PendulumToggle, HeartbeatButton, AuroraSlider } from '@zakkster/lite-ui-fx/recipes';
|
|
20
20
|
import { VolumeKnob, WaterLevel, TimerCountdown } from '@zakkster/lite-ui-fx/recipes';
|
|
@@ -26,7 +26,9 @@ import { RECIPES, RECIPE_META, RECIPE_NAMES, registerRecipe, mountRecipe } from
|
|
|
26
26
|
|
|
27
27
|
## Mount
|
|
28
28
|
const instance = mountUIFX(container, UIType.TOGGLE, SwarmToggle, { label: 'Sound' });
|
|
29
|
-
instance.
|
|
29
|
+
instance.setValue(v); // SLIDER/KNOB/PROGRESS: v in 0..1 (fires onDrag once); CHECKBOX: setValue(null) = indeterminate
|
|
30
|
+
instance.setChecked(b); // TOGGLE/CHECKBOX: set checked (fires onToggle once)
|
|
31
|
+
instance.destroy(); // cleanup
|
|
30
32
|
|
|
31
33
|
## Options (4th arg; unknown option or recipe-hook keys throw a did-you-mean -- fail closed)
|
|
32
34
|
width, height, padding=40, label // geometry + accessible label
|
|
@@ -38,16 +40,21 @@ colors // string[] palette override; wins over theme (array-palette recipes:
|
|
|
38
40
|
theme // { light, mid, dark } -> accent/dim/surface; partial or wrong-shape throws
|
|
39
41
|
text // visible canvas label; falls back to label, then the recipe default (label-in-name)
|
|
40
42
|
font // canvas font string; falls back to the recipe's historical font
|
|
43
|
+
knobMode // KNOB only: 'rotate' | 'vertical' pointer mapping (default 'rotate'); wrong type throws
|
|
44
|
+
announce // PROGRESS only: opt-in aria-live announcements at 10% steps; wrong type throws
|
|
41
45
|
|
|
42
46
|
## Element Types
|
|
43
|
-
UIType.TOGGLE
|
|
44
|
-
UIType.BUTTON
|
|
45
|
-
UIType.SLIDER
|
|
47
|
+
UIType.TOGGLE -> <input type="checkbox" role="switch"> -> state.toggled, onToggle(checked)
|
|
48
|
+
UIType.BUTTON -> <button> -> state.active, onClick(x, y, state)
|
|
49
|
+
UIType.SLIDER -> <input type="range"> -> state.val (0-1), onDrag(val, velocity)
|
|
50
|
+
UIType.CHECKBOX -> <input type="checkbox"> (no role=switch) -> state.toggled + state.indeterminate, onToggle(checked)
|
|
51
|
+
UIType.PROGRESS -> <progress> (non-interactive) -> state.val, driven by instance.setValue
|
|
52
|
+
UIType.KNOB -> <input type="range"> -> state.val, arrows native + knobMode pointer map, onDrag(val, velocity)
|
|
46
53
|
|
|
47
54
|
## State Object (provided to tick every frame)
|
|
48
|
-
{ hover, active, focused, toggled, disabled, val, w, h, padding, dpr }
|
|
55
|
+
{ hover, active, focused, toggled, indeterminate, disabled, val, w, h, padding, dpr }
|
|
49
56
|
|
|
50
|
-
##
|
|
57
|
+
## 53 Built-in Recipes
|
|
51
58
|
|
|
52
59
|
### Vol. 1 -- 10 recipes
|
|
53
60
|
Toggles: SwarmToggle, LiquidToggle, NeonPulseToggle
|
|
@@ -72,6 +79,10 @@ Mood: DayNightToggle, ReactionPicker, NotificationBell
|
|
|
72
79
|
Feedback: TypewriterField, SoundWaveBtn, UploadProgress
|
|
73
80
|
Fun: ScratchReveal, TimerCountdown, PullRefresh
|
|
74
81
|
|
|
82
|
+
### U4a -- 3 recipes (new element types)
|
|
83
|
+
Checkboxes (UIType.CHECKBOX): TickDraw, IndeterminateScan
|
|
84
|
+
Progress (UIType.PROGRESS): LiquidFill
|
|
85
|
+
|
|
75
86
|
## Writing Custom Recipes
|
|
76
87
|
See UIFX-RECIPE-GUIDE.md (included in package).
|
|
77
88
|
|
|
@@ -87,8 +98,10 @@ See UIFX-RECIPE-GUIDE.md (included in package).
|
|
|
87
98
|
- Zero-GC in all built-in recipes: const colors + globalAlpha, precomputed
|
|
88
99
|
color/label LUTs, fixed preallocated particle pools, gradients built in init.
|
|
89
100
|
Gated per recipe by the t3-frame-alloc torture tier (default AND themed mount).
|
|
90
|
-
- Themeable: all
|
|
101
|
+
- Themeable: all 53 recipes honour { colors, theme:{light,mid,dark}, text, font },
|
|
91
102
|
resolved once in init (zero per-frame alloc). RECIPE_META.themeable is true for
|
|
92
|
-
all
|
|
103
|
+
all 53; motionSafe stays false (reduced motion is a later pass). A bare mount is
|
|
93
104
|
byte-identical to pre-theming. Shipped palettes + APCA contrast are authored with
|
|
94
105
|
@zakkster/lite-hueforge (a dev-only tool, never a runtime dependency).
|
|
106
|
+
- U4a element types: CHECKBOX (indeterminate), PROGRESS (setValue-driven, aria-live
|
|
107
|
+
opt-in), KNOB (knobMode pointer map); setValue/setChecked sync native+state+hook once.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-ui-fx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Canvas-hijacked UI components with a pluggable recipe system. 50 built-in recipes across toggles, buttons, sliders, knobs, loaders, checkboxes, counters, and ratings.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|