@zakkster/lite-ui-fx 1.10.0 → 1.12.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 +93 -1
- package/README.md +16 -14
- package/UIFXController.js +1 -1
- package/UIFXHeadless.d.ts +7 -0
- package/UIFXHeadless.js +7 -0
- package/UIFXRecipes.d.ts +25 -1
- package/UIFXRecipes.js +540 -9
- package/llms.txt +46 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,94 @@ 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.12.0] -- 2026-09-08
|
|
9
|
+
|
|
10
|
+
Enrichment session E2 (decisions/0010): eight bespoke decorations on the U4b decorate
|
|
11
|
+
mode -- text-fx, card, and pointer micro-interactions painted AROUND a live element.
|
|
12
|
+
The first session since U4b to grow the MAIN recipe registry; no new dependency.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Eight `decorateUIFX` recipes on the `./recipes` subpath (RECIPE_META type
|
|
17
|
+
`decorate`), growing `RECIPE_NAMES` 57 -> 65 across three new families. Each is
|
|
18
|
+
themeable, `motionSafe` (a reduced-motion calm path), and allocates zero bytes per
|
|
19
|
+
frame, gated by the t3 torture tier (now gating 65 recipes, default AND themed):
|
|
20
|
+
- Text, painted OVER the host's own live glyphs: `TextShimmer` (a swept light band),
|
|
21
|
+
`SpotlightText` (a pointer-follow reading highlight), `UnderlineDraw` (a
|
|
22
|
+
focus-triggered underline wipe, distinct from TypewriterField).
|
|
23
|
+
- Card: `BorderBeam` (a comet around the perimeter), `TiltShine` (a pointer-driven
|
|
24
|
+
sheen), `CardSpotlight` (a pointer-follow radial glow).
|
|
25
|
+
- Pointer: `MagneticPull` (an accent ring eased toward the pointer; the host never
|
|
26
|
+
moves), `PointerRipple` (expanding rings from each press, a fixed Float32Array pool).
|
|
27
|
+
A recipe receives only `(ctx, dt, now, state, pointer)` -- never a DOM handle -- so a
|
|
28
|
+
decoration cannot read layout and is structurally reflow-safe. Shimmer/glow/beam use
|
|
29
|
+
const colours + `globalAlpha` with no gradient built in tick; no dependency was added
|
|
30
|
+
(`package.json` deps stay `lite-lerp` / `lite-random` / `lite-ticker`).
|
|
31
|
+
- `test/decorations-e2.test.mjs`: registry shape, additive-diff + exact-restore over
|
|
32
|
+
`<input>` / `<div>` / `<button>` hosts, and reduced-motion calm-path divergence. The
|
|
33
|
+
t3 decorate churn now sweeps the pointer and fires the click hook so the pointer
|
|
34
|
+
decorations' full draw paths are inside the alloc window.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `llms.txt` and `README.md`: the recipe catalog + counts move 57 -> 65 (new families
|
|
39
|
+
Text, Card, Pointer), the `motionSafe` roster 6 -> 14, and the catalog size figure is
|
|
40
|
+
re-measured (~29 KB min+gzip). `demo/index.html` auto-generates the eight scenes from
|
|
41
|
+
RECIPE_META (the card/pointer families decorate a visible card host). Documentation
|
|
42
|
+
and demo only; no shipped code path changed.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
none
|
|
47
|
+
|
|
48
|
+
### Removed
|
|
49
|
+
|
|
50
|
+
none
|
|
51
|
+
|
|
52
|
+
## [1.11.0] -- 2026-09-08
|
|
53
|
+
|
|
54
|
+
Enrichment session E1b (decisions/0009): seven bespoke headless skins continuing the
|
|
55
|
+
E1 adapter -- the select trigger (resolving the dropdown decision toward a V4 skin),
|
|
56
|
+
a tri-state checkbox + checkbox-group master, and meter/steps/accordion/skeleton.
|
|
57
|
+
Single-host, no new dependency; the 57-recipe registry is byte-identical.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- Seven headless skins on the `./headless` subpath, registered in the sibling
|
|
62
|
+
`HEADLESS_SKINS` / `SKIN_META` / `SKIN_NAMES` (4 -> 11 skins; RECIPES / RECIPE_META
|
|
63
|
+
and the 57-recipe count unchanged). Each is single-host, themeable, and allocates
|
|
64
|
+
zero bytes per frame, gated by the t6 torture tier (now gating 11 skins):
|
|
65
|
+
- `CheckboxSkin` -- tri-state from `aria-checked` (`true`|`false`|`mixed`) or the
|
|
66
|
+
`data-checked` / `data-indeterminate` presence pair, onto the existing `toggled`
|
|
67
|
+
/ `indeterminate` state slots.
|
|
68
|
+
- `CheckboxGroupSkin` -- the same body over a checkbox-group master (identical
|
|
69
|
+
3-state contract; "mixed" reads as partial members).
|
|
70
|
+
- `SelectSkin` -- the select TRIGGER: `aria-expanded` drives an open/close chevron,
|
|
71
|
+
the handle `value()` fast path drives a selected-value dot. The portaled listbox
|
|
72
|
+
open-state is a recorded follow-on (decisions/0009).
|
|
73
|
+
- `MeterSkin` -- `aria-valuenow`/`min`/`max` fill, zone-tinted from `data-zone`.
|
|
74
|
+
- `StepsSkin` -- a node rail from `data-step-count` / `data-current-index` /
|
|
75
|
+
`data-complete`.
|
|
76
|
+
- `AccordionSkin` -- a header chevron + underline from `aria-expanded` / `data-open`.
|
|
77
|
+
- `SkeletonSkin` -- a zero-allocation `globalAlpha` shimmer from `data-loading` /
|
|
78
|
+
`aria-busy`.
|
|
79
|
+
`@zakkster/lite-headless` is imported nowhere and is not a dependency; no dependency
|
|
80
|
+
was added.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- `llms.txt` and `README.md`: the `./headless` skin catalog lists all 11 skins, and
|
|
85
|
+
the sibling primitive count is corrected ("59" -> 62, the `@zakkster/lite-headless`
|
|
86
|
+
1.9.1 catalog). Documentation only; no code path changed.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
none
|
|
91
|
+
|
|
92
|
+
### Removed
|
|
93
|
+
|
|
94
|
+
none
|
|
95
|
+
|
|
8
96
|
## [1.10.0] -- 2026-09-08
|
|
9
97
|
|
|
10
98
|
Enrichment session E1: skinHeadless, a fourth mount adapter that paints a
|
|
@@ -41,7 +129,11 @@ byte-identical.
|
|
|
41
129
|
|
|
42
130
|
### Fixed
|
|
43
131
|
|
|
44
|
-
|
|
132
|
+
- ReactionPicker: paints a hairline ring around each disc and seeds the discs at
|
|
133
|
+
rest radius, so the row is visible on the first frame and where colour-emoji
|
|
134
|
+
glyphs do not render. Previously only the 4%-alpha discs and the emoji drew, so
|
|
135
|
+
an un-ticked or emoji-less environment showed an empty card. No added per-frame
|
|
136
|
+
allocation; the torture alloc baseline is unchanged.
|
|
45
137
|
|
|
46
138
|
### Removed
|
|
47
139
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-ui-fx
|
|
2
2
|
|
|
3
|
-
> Canvas microinteractions on real native controls. A DPR-aware canvas is hijacked over a hidden native element -- decorated around a live one -- or shared across a group of them -- and painted by a pluggable, zero-GC **recipe**. The native element owns focus, keyboard, and pointer events; the canvas owns the visuals.
|
|
3
|
+
> Canvas microinteractions on real native controls. A DPR-aware canvas is hijacked over a hidden native element -- decorated around a live one -- or shared across a group of them -- and painted by a pluggable, zero-GC **recipe**. The native element owns focus, keyboard, and pointer events; the canvas owns the visuals. 65 built-in recipes behind a tree-shakeable registry, one option convention for theming, one clock you can hand it, and reduced-motion built in.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@zakkster/lite-ui-fx)
|
|
6
6
|

|
|
@@ -81,9 +81,9 @@ The alternative is a hand-rolled canvas threshold loop (no a11y, allocates freel
|
|
|
81
81
|
- **`decorateUIFX(el, recipeFactory, options?)`** -- the decorate mount. Places a canvas *around* an existing visible element (a live `<input>`), reading `state.text`/`state.valid` from the host's own events. The host is byte-identical before and after; `destroy()` removes only the overlay.
|
|
82
82
|
- **`mountUIFXGroup(container, groupType, recipeFactory, options)`** -- the group mount. N native elements + one canvas + one recipe: `RADIO`/`RATING` (a fieldset radiogroup), `TABS` (an APG tablist with roving tabindex), `STEPPER` (a spinbutton). The recipe reads `state.index`/`state.count`; selection and keyboard are the native elements' own.
|
|
83
83
|
- **`skinHeadless(handle, recipeFactory, options)`** -- the headless-skin adapter (on the `./headless` subpath). Paints a `@zakkster/lite-headless` primitive by observing the state attributes it paints -- never importing lite-headless, so it stays a compose-target, not a dependency.
|
|
84
|
-
- **
|
|
84
|
+
- **65 built-in recipes** on the `./recipes` subpath, versioned, typed, and tree-shakeable. With `sideEffects: false`, importing one recipe drops the other 64. Families: Toggles (7), Buttons (9), Sliders (7), Knobs (2), Progress (4), Checkboxes (4), Loaders (2), Counters (2), Rating (1), Controls (4), Indicators (3), Mood (3), Feedback (3), Fun (3), Form decorations (3), Text (3), Card (3), Pointer (2).
|
|
85
85
|
- **A registry for data-driven UIs** -- `RECIPES` (id -> factory, null-prototype), `RECIPE_META` (`{ id, name, type, family, themeable, motionSafe }`), `RECIPE_NAMES`, `registerRecipe(id, factory, meta)`, and `mountRecipe(container, id, options?)` which resolves the id fail-closed (did-you-mean on a typo) and mounts it as its declared type.
|
|
86
|
-
- **One option convention for theming** -- `{ colors, theme: { light, mid, dark }, text, font }` honoured by all
|
|
86
|
+
- **One option convention for theming** -- `{ colors, theme: { light, mid, dark }, text, font }` honoured by all 65 recipes, resolved once in `init` so a themed mount stays zero-GC and a bare mount is byte-identical to pre-theming.
|
|
87
87
|
- **Host integration** -- ride a caller-supplied `lite-ticker` (`{ ticker }`), drive frames by hand (`{ driven: true }` + `instance.tick(dtMs)`), or take the shared ref-counted ticker by default. Plus `state.reducedMotion` (matchMedia-watched) and `state.budget` (0..1 frame budget).
|
|
88
88
|
- **Full TypeScript declarations** for both entry points, and a written recipe guide ([`UIFX-RECIPE-GUIDE.md`](UIFX-RECIPE-GUIDE.md)) shipped in the package.
|
|
89
89
|
|
|
@@ -213,7 +213,7 @@ The fourth mount adapter, on the `./headless` subpath: **skin a `@zakkster/lite-
|
|
|
213
213
|
|
|
214
214
|
`options`: `host` (the element the primitive paints on, **required**) plus `padding`, `seed`, `colors`, `theme`, `text`, `font`, `ticker`, `driven`. `setValue`/`setChecked` throw -- a skin reflects the primitive, it does not drive it.
|
|
215
215
|
|
|
216
|
-
A skin is an ordinary recipe plus a descriptor -- `recipe.headless = { attrs, read(host, handle, state) }`. `skinHeadless` observes `attrs` and calls `read()` at **event time** (never per frame) to parse the painted state into preallocated slots. A painted attribute is truthy when present with any value but `"false"` (so both a boolean `data-disabled` and a value `data-checked="true"` work). The
|
|
216
|
+
A skin is an ordinary recipe plus a descriptor -- `recipe.headless = { attrs, read(host, handle, state) }`. `skinHeadless` observes `attrs` and calls `read()` at **event time** (never per frame) to parse the painted state into preallocated slots. A painted attribute is truthy when present with any value but `"false"` (so both a boolean `data-disabled` and a value `data-checked="true"` work). The 11 skins live in a registry (`HEADLESS_SKINS` / `SKIN_META`) separate from the 65 recipes.
|
|
217
217
|
|
|
218
218
|
```js
|
|
219
219
|
import { skinHeadless, SwitchSkin } from '@zakkster/lite-ui-fx/headless';
|
|
@@ -231,7 +231,7 @@ const skin = skinHeadless(sw, SwitchSkin, {
|
|
|
231
231
|
skin.destroy(); // host + handle left untouched
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
E1 skins: `SwitchSkin` (`data-checked`), `SliderSkin` (`aria-valuenow`/`min`/`max`), `ProgressSkin` (`aria-valuenow`/`max` + `data-complete`/`data-loading`), `RatingSkin` (`aria-valuenow`/`max`). See [0008](decisions/0008-headless-skins.md).
|
|
234
|
+
E1 skins: `SwitchSkin` (`data-checked`), `SliderSkin` (`aria-valuenow`/`min`/`max`), `ProgressSkin` (`aria-valuenow`/`max` + `data-complete`/`data-loading`), `RatingSkin` (`aria-valuenow`/`max`). E1b skins: `CheckboxSkin` (tri-state `aria-checked` `true`|`false`|`mixed`), `CheckboxGroupSkin` (the same body over a checkbox-group master), `SelectSkin` (the select **trigger** -- `aria-expanded` + `handle.value()`; the portaled listbox open-state is a follow-on), `MeterSkin` (`aria-valuenow`/`min`/`max` + `data-zone`), `StepsSkin` (`data-step-count`/`data-current-index`), `AccordionSkin` (`aria-expanded`/`data-open`), `SkeletonSkin` (`data-loading`/`aria-busy`). See [0008](decisions/0008-headless-skins.md) + [0009](decisions/0009-headless-skins-select.md).
|
|
235
235
|
|
|
236
236
|
### The recipe registry
|
|
237
237
|
|
|
@@ -273,7 +273,7 @@ The `state` object passed to `tick(ctx, dt, now, state)` every frame:
|
|
|
273
273
|
| `index` / `count` / `hoverIndex` | `number` | group mode only: selection, item count, hovered item (-1 none) |
|
|
274
274
|
| `labels` / `itemX` / `itemY` / `itemW` / `itemH` | `string[]` / `Float32Array` | group mode only: item labels + per-item geometry lanes (read by index) |
|
|
275
275
|
|
|
276
|
-
`RECIPE_META` rows: `{ id, name, type, family, themeable, motionSafe }`. `themeable` is true for all
|
|
276
|
+
`RECIPE_META` rows: `{ id, name, type, family, themeable, motionSafe }`. `themeable` is true for all 65; `motionSafe` is true for exactly the recipes that ship a calm reduced-motion path (14 today: SwarmToggle plus the five U4b decorate recipes and the eight E2 decorations) and honestly false for the rest.
|
|
277
277
|
|
|
278
278
|
---
|
|
279
279
|
|
|
@@ -353,13 +353,13 @@ Everything a recipe needs is resolved in `init` (cold): the palette and any ramp
|
|
|
353
353
|
| Pointer move / drag | **0** | arithmetic only; the bounding rect is cached on pointer-enter, not read per move |
|
|
354
354
|
| `init` / theme resolve | once, cold | palette, ramps, gradients, pools -- then read-only in the loop |
|
|
355
355
|
|
|
356
|
-
The `t3-frame-alloc` torture tier asserts, per recipe, **zero distinct `fillStyle` string allocations per frame at steady state** and **zero gradient constructions after `init`** -- in both a default and a themed mount, across all
|
|
356
|
+
The `t3-frame-alloc` torture tier asserts, per recipe, **zero distinct `fillStyle` string allocations per frame at steady state** and **zero gradient constructions after `init`** -- in both a default and a themed mount, across all 65 recipes (grouped controls included, driven through their selection). Two positive controls -- one allocating a color string per frame, one per group item per frame -- must FAIL the gate, or it would be decorative. The full harness (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`) proves **0 retained bytes, 0 major GCs, and ~0.88 B/op** across the whole mount / interact / destroy loop under `--expose-gc`:
|
|
357
357
|
|
|
358
358
|
```
|
|
359
359
|
GATE leak=size 0/0 findings=0 warnings=0 | gc major=0 minor=0 maxMs=0.00 | alloc=0.8759765625 B/op
|
|
360
360
|
```
|
|
361
361
|
|
|
362
|
-
For size: the controller alone is **~7.1 KB min+gzip** (its three deps external); the full catalog of
|
|
362
|
+
For size: the controller alone is **~7.1 KB min+gzip** (its three deps external); the full catalog of 65 recipes is **~29 KB min+gzip**, and it tree-shakes -- import one recipe and the bundler drops the other 64.
|
|
363
363
|
|
|
364
364
|
</details>
|
|
365
365
|
|
|
@@ -369,20 +369,22 @@ For size: the controller alone is **~7.1 KB min+gzip** (its three deps external)
|
|
|
369
369
|
|
|
370
370
|
Each is an ADR under [`decisions/`](decisions/):
|
|
371
371
|
|
|
372
|
-
- **[0001](decisions/0001-recipes-position.md) -- Recipes ship inside the package.** No more copy-paste-from-a-ZIP:
|
|
373
|
-
- **[0002](decisions/0002-recipe-options.md) -- One recipe option convention.** `{ colors, theme, text, font }` across all
|
|
372
|
+
- **[0001](decisions/0001-recipes-position.md) -- Recipes ship inside the package.** No more copy-paste-from-a-ZIP: 65 recipes are versioned, typed, and tree-shakeable behind the `./recipes` subpath, exactly the shape the sibling fx packages use.
|
|
373
|
+
- **[0002](decisions/0002-recipe-options.md) -- One recipe option convention.** `{ colors, theme, text, font }` across all 65, resolved cold in `init`; defaults reproduce today's literals byte-for-byte; `text` closes the WCAG label-in-name gap.
|
|
374
374
|
- **[0003](decisions/0003-element-types.md) -- Real native element types.** CHECKBOX (tri-state), PROGRESS (`setValue`-driven, opt-in `aria-live`), KNOB (native arrows + pointer map) wrap the *correct* native element, not a faked toggle.
|
|
375
375
|
- **[0004](decisions/0004-decorate-mode.md) -- Decorate mode.** A second mount mode for a canvas around a live element -- the honest home for a decoration over a real input, host byte-identical.
|
|
376
376
|
- **[0005](decisions/0005-host-clock.md) -- Host clock, reduced motion, frame budget.** Three clock modes, `state.reducedMotion` as a flag the recipe reads, `state.budget` for graceful degradation -- all additive, default path byte-identical.
|
|
377
377
|
- **[0006](decisions/0006-docs-and-demo.md) -- Blueprint docs + a demo that consumes the package.** This README on the blueprint spine, and one demo generated from `RECIPE_META` that imports only public exports (no more inline reimplementation).
|
|
378
378
|
- **[0007](decisions/0007-group-contract.md) -- Grouped controls: one canvas, N native elements.** A third mount mode (`mountUIFXGroup`) for radio/tabs/stepper/rating; `onSelect` is a ninth, group-only hook and group state a superset of scalar state, so the single-element API is byte-identical (additive, 1.9.0).
|
|
379
|
-
- **[0008](decisions/0008-headless-skins.md) -- Headless skins: paint a lite-headless primitive.** `skinHeadless` couples through the painted-attribute contract (one `MutationObserver`, parsed at event time), never an import -- so lite-headless is a compose-target, never a dependency. The four skins live in a registry separate from the
|
|
379
|
+
- **[0008](decisions/0008-headless-skins.md) -- Headless skins: paint a lite-headless primitive.** `skinHeadless` couples through the painted-attribute contract (one `MutationObserver`, parsed at event time), never an import -- so lite-headless is a compose-target, never a dependency. The four skins live in a registry separate from the recipe registry (additive, 1.10.0).
|
|
380
|
+
- **[0009](decisions/0009-headless-skins-select.md) -- Headless skins II: the select + tri-state pack.** Adds seven bespoke skins (checkbox tri-state + checkbox-group master, select trigger, meter, steps, accordion, skeleton), all single-host and mapping onto the existing state slots (no state-shape change). `SelectSkin` resolves the `<select>`/dropdown decision toward a V4 skin of the trigger; the portaled listbox open-state is a recorded follow-on. No new dependency (additive, 1.11.0).
|
|
381
|
+
- **[0010](decisions/0010-text-pointer-card-decorations.md) -- Text-fx + pointer + card decorations.** Eight decorate-mode recipes (text shimmer/spotlight/underline, card border-beam/tilt-shine/spotlight, pointer magnetic-pull/ripple) grow the main recipe registry to 65. Each is zero per-frame alloc (const colours + `globalAlpha`, no gradient), `motionSafe` with a real calm path, and structurally reflow-safe -- a recipe receives `ctx` + `state` + `pointer`, never a DOM handle, so it cannot read layout. No new dependency: particle trail/glow and a host-owned scramble defer to later phases (additive, 1.12.0).
|
|
380
382
|
|
|
381
383
|
---
|
|
382
384
|
|
|
383
385
|
## Testing
|
|
384
386
|
|
|
385
|
-
**
|
|
387
|
+
**299 deterministic node:test cases across 38 suites, all pass**, plus a torture gate that proves 0 B/op steady state and leak-freedom.
|
|
386
388
|
|
|
387
389
|
```bash
|
|
388
390
|
npm test # node:test: contract, boundary, registry, theming, reduced-motion, docs
|
|
@@ -399,7 +401,7 @@ The suite covers the a11y state machine (one Space press = one `onToggle`, state
|
|
|
399
401
|
- **Not a component framework.** It paints controls; it does not do layout, routing, or state management. Bring your own.
|
|
400
402
|
- **Not a worker-mode renderer.** A 200x48 UI canvas does not amortise a worker hop; the shared main-thread ticker is the right tool. (`@zakkster/lite-ambient-fx` is the worker-mode fullscreen backdrop.)
|
|
401
403
|
- **Not a chart or data-viz library.** These are interactive *controls*, not plots. Charts are `@zakkster/lite-charts`.
|
|
402
|
-
- **Not an ARIA behaviour engine.** It renders. The one keyboard behaviour it writes is the tablist roving-tabindex for a `TABS` group (radio/rating/stepper selection is the browser's own); it does not own focus traps, dismiss stacks, or listbox/combobox/menu patterns. `@zakkster/lite-headless` owns behaviour, permanently -- and its
|
|
404
|
+
- **Not an ARIA behaviour engine.** It renders. The one keyboard behaviour it writes is the tablist roving-tabindex for a `TABS` group (radio/rating/stepper selection is the browser's own); it does not own focus traps, dismiss stacks, or listbox/combobox/menu patterns. `@zakkster/lite-headless` owns behaviour, permanently -- and its 62 primitives are a headless-skin target.
|
|
403
405
|
|
|
404
406
|
---
|
|
405
407
|
|
|
@@ -412,7 +414,7 @@ Part of the **@zakkster** zero-GC stack:
|
|
|
412
414
|
- [`lite-random`](https://www.npmjs.com/package/@zakkster/lite-random) -- seeded Mulberry32 RNG (deterministic particle recipes)
|
|
413
415
|
- [`lite-scratch-fx`](https://www.npmjs.com/package/@zakkster/lite-scratch-fx) -- canvas scratch-reveal recipes; shares the `{ light, mid, dark }` theme shape
|
|
414
416
|
- [`lite-ambient-fx`](https://www.npmjs.com/package/@zakkster/lite-ambient-fx) -- fullscreen ambient backdrops (the worker-mode sibling)
|
|
415
|
-
- [`lite-headless`](https://www.npmjs.com/package/@zakkster/lite-headless) --
|
|
417
|
+
- [`lite-headless`](https://www.npmjs.com/package/@zakkster/lite-headless) -- 62 ARIA-correct primitives; a headless-skin target (`skinHeadless`)
|
|
416
418
|
- **`lite-ui-fx`** -- this package
|
|
417
419
|
|
|
418
420
|
---
|
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.12.0';
|
|
26
26
|
|
|
27
27
|
// ---------------------------------------------------------
|
|
28
28
|
// SHARED TICKER (ref-counted, one RAF for all UI components)
|
package/UIFXHeadless.d.ts
CHANGED
package/UIFXHeadless.js
CHANGED
package/UIFXRecipes.d.ts
CHANGED
|
@@ -134,6 +134,15 @@ export declare function PullRefresh(options?: RecipeOptions): UIFXRecipe;
|
|
|
134
134
|
export declare function FocusHalo(options?: RecipeOptions): UIFXRecipe;
|
|
135
135
|
export declare function ErrorShake(options?: RecipeOptions): UIFXRecipe;
|
|
136
136
|
export declare function SuccessBloom(options?: RecipeOptions): UIFXRecipe;
|
|
137
|
+
// E2 decorations (text-fx + card + pointer; decisions/0010)
|
|
138
|
+
export declare function TextShimmer(options?: RecipeOptions): UIFXRecipe;
|
|
139
|
+
export declare function SpotlightText(options?: RecipeOptions): UIFXRecipe;
|
|
140
|
+
export declare function UnderlineDraw(options?: RecipeOptions): UIFXRecipe;
|
|
141
|
+
export declare function BorderBeam(options?: RecipeOptions): UIFXRecipe;
|
|
142
|
+
export declare function TiltShine(options?: RecipeOptions): UIFXRecipe;
|
|
143
|
+
export declare function CardSpotlight(options?: RecipeOptions): UIFXRecipe;
|
|
144
|
+
export declare function MagneticPull(options?: RecipeOptions & { maxPull?: number }): UIFXRecipe;
|
|
145
|
+
export declare function PointerRipple(options?: RecipeOptions): UIFXRecipe;
|
|
137
146
|
|
|
138
147
|
// ===========================================================
|
|
139
148
|
// BARREL OBJECTS (back-compat)
|
|
@@ -334,6 +343,14 @@ declare const UIFXAllRecipes: {
|
|
|
334
343
|
FocusHalo: typeof FocusHalo;
|
|
335
344
|
ErrorShake: typeof ErrorShake;
|
|
336
345
|
SuccessBloom: typeof SuccessBloom;
|
|
346
|
+
TextShimmer: typeof TextShimmer;
|
|
347
|
+
SpotlightText: typeof SpotlightText;
|
|
348
|
+
UnderlineDraw: typeof UnderlineDraw;
|
|
349
|
+
BorderBeam: typeof BorderBeam;
|
|
350
|
+
TiltShine: typeof TiltShine;
|
|
351
|
+
CardSpotlight: typeof CardSpotlight;
|
|
352
|
+
MagneticPull: typeof MagneticPull;
|
|
353
|
+
PointerRipple: typeof PointerRipple;
|
|
337
354
|
};
|
|
338
355
|
export default UIFXAllRecipes;
|
|
339
356
|
|
|
@@ -341,13 +358,20 @@ export default UIFXAllRecipes;
|
|
|
341
358
|
// HEADLESS SKINS (E1, decisions/0008)
|
|
342
359
|
// A sibling registry of RECIPES/RECIPE_META: skins are driven by skinHeadless
|
|
343
360
|
// (a handle + host), never by mountRecipe (a container), so they are kept
|
|
344
|
-
// separate and the
|
|
361
|
+
// separate and the recipe count is unchanged by skins.
|
|
345
362
|
// ===========================================================
|
|
346
363
|
|
|
347
364
|
export declare function SwitchSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
348
365
|
export declare function SliderSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
349
366
|
export declare function ProgressSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
350
367
|
export declare function RatingSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
368
|
+
export declare function CheckboxSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
369
|
+
export declare function CheckboxGroupSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
370
|
+
export declare function SelectSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
371
|
+
export declare function MeterSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
372
|
+
export declare function StepsSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
373
|
+
export declare function AccordionSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
374
|
+
export declare function SkeletonSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
351
375
|
|
|
352
376
|
/** A headless-skin meta row. `primitive` names the lite-headless primitive the
|
|
353
377
|
* skin is designed to paint. */
|
package/UIFXRecipes.js
CHANGED
|
@@ -2548,13 +2548,15 @@ export function DayNightToggle(o = {}) {
|
|
|
2548
2548
|
|
|
2549
2549
|
/** 13. Reaction Picker -- 5 emoji-style circles that inflate on hover region. */
|
|
2550
2550
|
export function ReactionPicker(o = {}) {
|
|
2551
|
-
let sizes=new Float32Array(5), selected=-1;
|
|
2551
|
+
let sizes=new Float32Array(5).fill(10), selected=-1; // seed at the rest radius so frame 0 draws full discs, not radius-0 nothing
|
|
2552
2552
|
const emojis=['\u{1F610}','\u{1F642}','\u{1F60A}','\u{1F604}','\u{1F929}'];
|
|
2553
2553
|
// `colors` is the 5-reaction palette; `theme` seeds it. Normalised to 5.
|
|
2554
2554
|
const base = o.colors ? o.colors
|
|
2555
2555
|
: (o.theme ? [o.theme.light,o.theme.mid,o.theme.dark,o.theme.light,o.theme.mid] : ['#9999b8','#fbbf24','#fb923c','#f472b6','#ff6b6b']);
|
|
2556
2556
|
const colors = base.length>=5 ? base : Array.from({length:5},(_,i)=>base[i%base.length]);
|
|
2557
2557
|
const colors30=colors.map((col)=>col+'30'); // active fill (was `${color}30`)
|
|
2558
|
+
const REST_FILL='rgba(255,255,255,.10)'; // at-rest disc: was 4% (near-invisible); 10% so the row always reads
|
|
2559
|
+
const RING='rgba(255,255,255,.22)'; // hairline ring, theme-invariant: keeps the control legible even if the emoji glyph never paints
|
|
2558
2560
|
const REACT_LABELS=['MEH','OK','NICE','GREAT','LOVE']; // was a per-frame array literal
|
|
2559
2561
|
const FONTS=[]; for(let i=0;i<=48;i++)FONTS[i]=i+'px sans-serif'; // emoji font by rounded size
|
|
2560
2562
|
return {
|
|
@@ -2562,21 +2564,25 @@ export function ReactionPicker(o = {}) {
|
|
|
2562
2564
|
const gap=st.w/5;
|
|
2563
2565
|
const hoverIdx=st.hover?clamp(Math.floor(ptr.x/gap),0,4):-1;
|
|
2564
2566
|
|
|
2567
|
+
c.lineWidth=1;c.strokeStyle=RING; // ring state is constant across the 5 discs -- set once per frame
|
|
2565
2568
|
for(let i=0;i<5;i++){
|
|
2566
2569
|
const active=i===hoverIdx;
|
|
2567
2570
|
sizes[i]=lerp(sizes[i],active?16:10,dt*12);
|
|
2568
2571
|
const cx=gap*i+gap/2,cy=st.h/2;
|
|
2569
2572
|
|
|
2570
|
-
//
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
+
// Disc + hairline ring. The ring is the robustness net: the disc
|
|
2574
|
+
// fill is faint and the emoji may not paint at all (a colour-emoji
|
|
2575
|
+
// font draws its own glyph; a mono/absent font leaves almost
|
|
2576
|
+
// nothing), but a stroked ring always renders, so the control never
|
|
2577
|
+
// reads blank -- on frame 0 or in an emoji-less environment.
|
|
2578
|
+
c.fillStyle=active?colors30[i]:REST_FILL;
|
|
2579
|
+
c.beginPath();c.arc(cx,cy-sizes[i]+10,sizes[i],0,PI2);c.fill();c.stroke();
|
|
2573
2580
|
|
|
2574
2581
|
// Emoji face -- font from a const-string LUT. Set an OPAQUE fill
|
|
2575
|
-
// first: the
|
|
2576
|
-
//
|
|
2577
|
-
//
|
|
2578
|
-
//
|
|
2579
|
-
// takes the full alpha. Precomputed array read -- zero alloc.
|
|
2582
|
+
// first: the disc + ring above are translucent, and a colour glyph
|
|
2583
|
+
// inherits that alpha, so without this the faces are dim. colors[i]
|
|
2584
|
+
// keeps a mono-emoji fallback tinted per reaction; a colour-emoji
|
|
2585
|
+
// font ignores the hue and only takes the full alpha. Array read -- zero alloc.
|
|
2580
2586
|
c.font=FONTS[Math.round(sizes[i]*1.2)]||FONTS[48];c.textAlign='center';c.textBaseline='middle';
|
|
2581
2587
|
c.fillStyle=colors[i];
|
|
2582
2588
|
c.fillText(emojis[i],cx,cy-sizes[i]+10);
|
|
@@ -3043,6 +3049,256 @@ export function SuccessBloom(o = {}) {
|
|
|
3043
3049
|
}
|
|
3044
3050
|
|
|
3045
3051
|
|
|
3052
|
+
// ===========================================================
|
|
3053
|
+
// E2 DECORATIONS -- text-fx, card, and pointer (decorate mode)
|
|
3054
|
+
// ===========================================================
|
|
3055
|
+
// Eight decorateUIFX recipes (RECIPE_META type 'decorate'): a canvas wrapped
|
|
3056
|
+
// AROUND a live element, never modifying it. A recipe receives only (ctx, dt,
|
|
3057
|
+
// now, state, pointer) -- never a DOM handle -- so it cannot read layout and is
|
|
3058
|
+
// structurally reflow-safe (the decorate adapter owns every DOM read). Each is
|
|
3059
|
+
// zero per-frame alloc (const colours + globalAlpha; NO gradient or string built
|
|
3060
|
+
// in tick) and ships a reduced-motion calm path (motionSafe). The text-fx paint
|
|
3061
|
+
// OVER the host's own live glyphs -- they never re-render the string. See
|
|
3062
|
+
// decisions/0010-text-pointer-card-decorations.md.
|
|
3063
|
+
|
|
3064
|
+
/** Text Shimmer (E2 DECORATE) -- a soft light band sweeps across the host's text
|
|
3065
|
+
* left->right on a loop, painting OVER the live glyphs (never rendering them).
|
|
3066
|
+
* Reduced motion (U5): a faint static sheen, no sweep. */
|
|
3067
|
+
export function TextShimmer(o = {}) {
|
|
3068
|
+
const P = resolveTheme(o, { accent: '#e2e8f0' });
|
|
3069
|
+
let phase = 0;
|
|
3070
|
+
return {
|
|
3071
|
+
tick(c, dt, now, st) {
|
|
3072
|
+
if (st.reducedMotion) {
|
|
3073
|
+
c.fillStyle = P.accent; c.globalAlpha = 0.05;
|
|
3074
|
+
c.fillRect(0, 0, st.w, st.h); c.globalAlpha = 1; return;
|
|
3075
|
+
}
|
|
3076
|
+
const BW = st.w * 0.26, slant = st.h * 0.4;
|
|
3077
|
+
phase = (phase + dt * 0.55) % 1;
|
|
3078
|
+
const cx = -BW + phase * (st.w + BW * 2);
|
|
3079
|
+
c.fillStyle = P.accent;
|
|
3080
|
+
for (let i = 0; i < 3; i++) {
|
|
3081
|
+
const w = BW * (1 - i * 0.3);
|
|
3082
|
+
c.globalAlpha = 0.09 - i * 0.025;
|
|
3083
|
+
c.beginPath();
|
|
3084
|
+
c.moveTo(cx - w + slant, 0); c.lineTo(cx + w + slant, 0);
|
|
3085
|
+
c.lineTo(cx + w - slant, st.h); c.lineTo(cx - w - slant, st.h);
|
|
3086
|
+
c.closePath(); c.fill();
|
|
3087
|
+
}
|
|
3088
|
+
c.globalAlpha = 1;
|
|
3089
|
+
},
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
/** Spotlight Text (E2 DECORATE) -- a soft highlight tracks the pointer's x across
|
|
3094
|
+
* the text line while hovering, a reading light over the live glyphs. Reduced
|
|
3095
|
+
* motion (U5): a centred static highlight on hover, no follow. */
|
|
3096
|
+
export function SpotlightText(o = {}) {
|
|
3097
|
+
const P = resolveTheme(o, { accent: '#fde68a' });
|
|
3098
|
+
let on = 0, sx = -1;
|
|
3099
|
+
return {
|
|
3100
|
+
tick(c, dt, now, st, ptr) {
|
|
3101
|
+
const target = st.hover ? 1 : 0;
|
|
3102
|
+
on = st.reducedMotion ? target : lerp(on, target, dt * 8);
|
|
3103
|
+
if (on < 0.01) return;
|
|
3104
|
+
const tx = st.reducedMotion ? st.w / 2 : clamp(ptr.x, 0, st.w);
|
|
3105
|
+
sx = (st.reducedMotion || sx < 0) ? tx : lerp(sx, tx, dt * 12);
|
|
3106
|
+
const rx = st.w * 0.22, ry = st.h * 0.55, cy = st.h / 2;
|
|
3107
|
+
c.fillStyle = P.accent;
|
|
3108
|
+
for (let i = 3; i >= 1; i--) {
|
|
3109
|
+
c.globalAlpha = on * 0.05 * i;
|
|
3110
|
+
c.beginPath(); c.ellipse(sx, cy, rx * (i / 3), ry * (i / 3), 0, 0, PI2); c.fill();
|
|
3111
|
+
}
|
|
3112
|
+
c.globalAlpha = 1;
|
|
3113
|
+
},
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
/** Underline Draw (E2 DECORATE) -- a gradient-free underline wipes in left->right
|
|
3118
|
+
* when the host gains focus and retracts on blur, with a soft leading dot.
|
|
3119
|
+
* Distinct from TypewriterField (which grows with typed length). Reduced motion
|
|
3120
|
+
* (U5): the underline snaps to full-width on focus, no wipe, no dot. */
|
|
3121
|
+
export function UnderlineDraw(o = {}) {
|
|
3122
|
+
const P = resolveTheme(o, { accent: '#38bdf8' });
|
|
3123
|
+
let draw = 0;
|
|
3124
|
+
return {
|
|
3125
|
+
tick(c, dt, now, st) {
|
|
3126
|
+
const target = st.focused ? 1 : 0;
|
|
3127
|
+
draw = st.reducedMotion ? target : lerp(draw, target, dt * 9);
|
|
3128
|
+
const y = st.h - 3, x0 = 2, full = st.w - 4;
|
|
3129
|
+
c.strokeStyle = P.accent; c.lineWidth = 2;
|
|
3130
|
+
c.globalAlpha = 0.15; c.beginPath(); c.moveTo(x0, y); c.lineTo(x0 + full, y); c.stroke();
|
|
3131
|
+
if (draw > 0.01) {
|
|
3132
|
+
const x1 = x0 + full * draw;
|
|
3133
|
+
c.globalAlpha = 1; c.beginPath(); c.moveTo(x0, y); c.lineTo(x1, y); c.stroke();
|
|
3134
|
+
if (!st.reducedMotion && draw < 0.995) {
|
|
3135
|
+
c.fillStyle = P.accent;
|
|
3136
|
+
c.beginPath(); c.arc(x1, y, 2.5, 0, PI2); c.fill();
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
c.globalAlpha = 1;
|
|
3140
|
+
},
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/** Border Beam (E2 DECORATE) -- a bright comet travels around the host's border
|
|
3145
|
+
* over a faint full outline (Magic UI style). Reduced motion (U5): the static
|
|
3146
|
+
* faint outline only, no travelling beam. */
|
|
3147
|
+
export function BorderBeam(o = {}) {
|
|
3148
|
+
const P = resolveTheme(o, { accent: '#a78bfa' });
|
|
3149
|
+
const R = 10;
|
|
3150
|
+
let t = 0;
|
|
3151
|
+
return {
|
|
3152
|
+
tick(c, dt, now, st) {
|
|
3153
|
+
c.strokeStyle = P.accent; c.globalAlpha = 0.14; c.lineWidth = 1.5;
|
|
3154
|
+
rr(c, 0, 0, st.w, st.h, R); c.stroke();
|
|
3155
|
+
c.globalAlpha = 1;
|
|
3156
|
+
if (st.reducedMotion) return;
|
|
3157
|
+
t = (t + dt * 0.4) % 1;
|
|
3158
|
+
const per = 2 * (st.w + st.h), head = t * per;
|
|
3159
|
+
c.fillStyle = P.accent;
|
|
3160
|
+
for (let i = 0; i < 6; i++) {
|
|
3161
|
+
let d = head - i * (per * 0.012); if (d < 0) d += per;
|
|
3162
|
+
let x, y;
|
|
3163
|
+
if (d < st.w) { x = d; y = 0; }
|
|
3164
|
+
else if (d < st.w + st.h) { x = st.w; y = d - st.w; }
|
|
3165
|
+
else if (d < 2 * st.w + st.h) { x = st.w - (d - st.w - st.h); y = st.h; }
|
|
3166
|
+
else { x = 0; y = st.h - (d - 2 * st.w - st.h); }
|
|
3167
|
+
c.globalAlpha = 0.6 * (1 - i / 6);
|
|
3168
|
+
c.beginPath(); c.arc(x, y, 2.2, 0, PI2); c.fill();
|
|
3169
|
+
}
|
|
3170
|
+
c.globalAlpha = 1;
|
|
3171
|
+
},
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
/** Tilt Shine (E2 DECORATE) -- a diagonal sheen sweeps across the card toward the
|
|
3176
|
+
* pointer while hovering, with the border catching a little light. Clipped to
|
|
3177
|
+
* the card. Reduced motion (U5): a static centred sheen on hover, no follow. */
|
|
3178
|
+
export function TiltShine(o = {}) {
|
|
3179
|
+
const P = resolveTheme(o, { accent: '#ffffff' });
|
|
3180
|
+
const R = 10;
|
|
3181
|
+
let on = 0, sx = 0.5;
|
|
3182
|
+
return {
|
|
3183
|
+
tick(c, dt, now, st, ptr) {
|
|
3184
|
+
const target = st.hover ? 1 : 0;
|
|
3185
|
+
on = st.reducedMotion ? target : lerp(on, target, dt * 8);
|
|
3186
|
+
if (on < 0.01) return;
|
|
3187
|
+
const nx = st.reducedMotion ? 0.5 : clamp(ptr.x / st.w, 0, 1);
|
|
3188
|
+
sx = st.reducedMotion ? nx : lerp(sx, nx, dt * 10);
|
|
3189
|
+
c.save();
|
|
3190
|
+
rr(c, 0, 0, st.w, st.h, R); c.clip();
|
|
3191
|
+
const bx = sx * st.w, bw = st.w * 0.5, slant = st.h * 0.6;
|
|
3192
|
+
c.fillStyle = P.accent;
|
|
3193
|
+
for (let i = 0; i < 3; i++) {
|
|
3194
|
+
const w = bw * (1 - i * 0.3);
|
|
3195
|
+
c.globalAlpha = on * (0.10 - i * 0.03);
|
|
3196
|
+
c.beginPath();
|
|
3197
|
+
c.moveTo(bx - w + slant, 0); c.lineTo(bx + w + slant, 0);
|
|
3198
|
+
c.lineTo(bx + w - slant, st.h); c.lineTo(bx - w - slant, st.h);
|
|
3199
|
+
c.closePath(); c.fill();
|
|
3200
|
+
}
|
|
3201
|
+
c.restore();
|
|
3202
|
+
c.globalAlpha = on * 0.4; c.strokeStyle = P.accent; c.lineWidth = 1.5;
|
|
3203
|
+
rr(c, 0, 0, st.w, st.h, R); c.stroke();
|
|
3204
|
+
c.globalAlpha = 1;
|
|
3205
|
+
},
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
/** Card Spotlight (E2 DECORATE) -- a soft radial glow follows the pointer over the
|
|
3210
|
+
* card surface with the border brightening (Aceternity style). Clipped to the
|
|
3211
|
+
* card. Reduced motion (U5): a static centred glow on hover, no follow. */
|
|
3212
|
+
export function CardSpotlight(o = {}) {
|
|
3213
|
+
const P = resolveTheme(o, { accent: '#38bdf8' });
|
|
3214
|
+
const R = 10;
|
|
3215
|
+
let on = 0, gx = 0, gy = 0, seeded = 0;
|
|
3216
|
+
return {
|
|
3217
|
+
tick(c, dt, now, st, ptr) {
|
|
3218
|
+
const target = st.hover ? 1 : 0;
|
|
3219
|
+
on = st.reducedMotion ? target : lerp(on, target, dt * 7);
|
|
3220
|
+
const tx = st.reducedMotion ? st.w / 2 : clamp(ptr.x, 0, st.w);
|
|
3221
|
+
const ty = st.reducedMotion ? st.h / 2 : clamp(ptr.y, 0, st.h);
|
|
3222
|
+
if (!seeded) { gx = tx; gy = ty; seeded = 1; }
|
|
3223
|
+
else { gx = st.reducedMotion ? tx : lerp(gx, tx, dt * 12); gy = st.reducedMotion ? ty : lerp(gy, ty, dt * 12); }
|
|
3224
|
+
if (on < 0.01) return;
|
|
3225
|
+
c.save();
|
|
3226
|
+
rr(c, 0, 0, st.w, st.h, R); c.clip();
|
|
3227
|
+
const rad = Math.min(st.w, st.h) * 0.9;
|
|
3228
|
+
c.fillStyle = P.accent;
|
|
3229
|
+
for (let i = 5; i >= 1; i--) {
|
|
3230
|
+
c.globalAlpha = on * 0.03 * (6 - i);
|
|
3231
|
+
c.beginPath(); c.arc(gx, gy, rad * (i / 5), 0, PI2); c.fill();
|
|
3232
|
+
}
|
|
3233
|
+
c.restore();
|
|
3234
|
+
c.globalAlpha = on * 0.35; c.strokeStyle = P.accent; c.lineWidth = 1.5;
|
|
3235
|
+
rr(c, 0, 0, st.w, st.h, R); c.stroke();
|
|
3236
|
+
c.globalAlpha = 1;
|
|
3237
|
+
},
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
/** Magnetic Pull (E2 DECORATE) -- an accent ring is drawn pulled toward the
|
|
3242
|
+
* pointer within a radius while hovering; the host itself never moves (a
|
|
3243
|
+
* decoration reflects, it does not drive). Reduced motion (U5): a centred static
|
|
3244
|
+
* ring on hover, no pull. */
|
|
3245
|
+
export function MagneticPull(o = {}) {
|
|
3246
|
+
const { maxPull = 14 } = o;
|
|
3247
|
+
const P = resolveTheme(o, { accent: '#f472b6' });
|
|
3248
|
+
const R = 10;
|
|
3249
|
+
let bx = 0, by = 0, on = 0;
|
|
3250
|
+
return {
|
|
3251
|
+
tick(c, dt, now, st, ptr) {
|
|
3252
|
+
const target = st.hover ? 1 : 0;
|
|
3253
|
+
on = st.reducedMotion ? target : lerp(on, target, dt * 8);
|
|
3254
|
+
const tx = (st.hover && !st.reducedMotion) ? clamp(ptr.x - st.w / 2, -maxPull, maxPull) : 0;
|
|
3255
|
+
const ty = (st.hover && !st.reducedMotion) ? clamp(ptr.y - st.h / 2, -maxPull, maxPull) : 0;
|
|
3256
|
+
bx = st.reducedMotion ? 0 : lerp(bx, tx, dt * 10);
|
|
3257
|
+
by = st.reducedMotion ? 0 : lerp(by, ty, dt * 10);
|
|
3258
|
+
if (on < 0.01) return;
|
|
3259
|
+
c.save(); c.translate(bx, by);
|
|
3260
|
+
c.strokeStyle = P.accent; c.lineWidth = 1.5; c.globalAlpha = on * 0.9;
|
|
3261
|
+
rr(c, 1, 1, st.w - 2, st.h - 2, R); c.stroke();
|
|
3262
|
+
c.globalAlpha = on * 0.18;
|
|
3263
|
+
rr(c, -3, -3, st.w + 6, st.h + 6, R + 3); c.stroke();
|
|
3264
|
+
c.restore();
|
|
3265
|
+
c.globalAlpha = 1;
|
|
3266
|
+
},
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
/** Pointer Ripple (E2 DECORATE) -- concentric rings expand from each pointer press
|
|
3271
|
+
* on the host, fading as they grow (Material style). Zero-alloc: a fixed
|
|
3272
|
+
* Float32Array ring pool preallocated in the factory. Reduced motion (U5): a
|
|
3273
|
+
* brief static ring at the press, no expansion. */
|
|
3274
|
+
export function PointerRipple(o = {}) {
|
|
3275
|
+
const P = resolveTheme(o, { accent: '#22d3ee' });
|
|
3276
|
+
const N = 6;
|
|
3277
|
+
const rx = new Float32Array(N), ry = new Float32Array(N), ra = new Float32Array(N);
|
|
3278
|
+
let head = 0;
|
|
3279
|
+
function spawn(x, y) {
|
|
3280
|
+
rx[head] = x; ry[head] = y; ra[head] = 1;
|
|
3281
|
+
head = (head + 1) % N;
|
|
3282
|
+
}
|
|
3283
|
+
return {
|
|
3284
|
+
onClick(x, y) { spawn(x, y); },
|
|
3285
|
+
tick(c, dt, now, st) {
|
|
3286
|
+
c.strokeStyle = P.accent; c.lineWidth = 2;
|
|
3287
|
+
const maxR = Math.max(st.w, st.h) * 0.8;
|
|
3288
|
+
for (let i = 0; i < N; i++) {
|
|
3289
|
+
if (ra[i] <= 0) continue;
|
|
3290
|
+
ra[i] -= dt * (st.reducedMotion ? 3 : 1.4);
|
|
3291
|
+
if (ra[i] <= 0) { ra[i] = 0; continue; }
|
|
3292
|
+
const grow = st.reducedMotion ? 0.2 : (1 - ra[i]);
|
|
3293
|
+
c.globalAlpha = ra[i] * 0.7;
|
|
3294
|
+
c.beginPath(); c.arc(rx[i], ry[i], 4 + grow * maxR, 0, PI2); c.stroke();
|
|
3295
|
+
}
|
|
3296
|
+
c.globalAlpha = 1;
|
|
3297
|
+
},
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
|
|
3046
3302
|
// U4a additions -- new native element types (CHECKBOX, PROGRESS). Kept out of the
|
|
3047
3303
|
// Vol.1-3 historical snapshots above so those stay accurate; all recipes remain
|
|
3048
3304
|
// reachable via RECIPES / RECIPE_META and their named exports regardless.
|
|
@@ -3199,6 +3455,14 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
3199
3455
|
focusHalo: FocusHalo,
|
|
3200
3456
|
errorShake: ErrorShake,
|
|
3201
3457
|
successBloom: SuccessBloom,
|
|
3458
|
+
textShimmer: TextShimmer,
|
|
3459
|
+
spotlightText: SpotlightText,
|
|
3460
|
+
underlineDraw: UnderlineDraw,
|
|
3461
|
+
borderBeam: BorderBeam,
|
|
3462
|
+
tiltShine: TiltShine,
|
|
3463
|
+
cardSpotlight: CardSpotlight,
|
|
3464
|
+
magneticPull: MagneticPull,
|
|
3465
|
+
pointerRipple: PointerRipple,
|
|
3202
3466
|
});
|
|
3203
3467
|
|
|
3204
3468
|
/**
|
|
@@ -3271,6 +3535,14 @@ export const RECIPE_META = [
|
|
|
3271
3535
|
{ id: 'focusHalo', name: 'Focus Halo', type: 'decorate', family: 'Form', themeable: true, motionSafe: true },
|
|
3272
3536
|
{ id: 'errorShake', name: 'Error Shake', type: 'decorate', family: 'Form', themeable: true, motionSafe: true },
|
|
3273
3537
|
{ id: 'successBloom', name: 'Success Bloom', type: 'decorate', family: 'Form', themeable: true, motionSafe: true },
|
|
3538
|
+
{ id: 'textShimmer', name: 'Text Shimmer', type: 'decorate', family: 'Text', themeable: true, motionSafe: true },
|
|
3539
|
+
{ id: 'spotlightText', name: 'Spotlight Text', type: 'decorate', family: 'Text', themeable: true, motionSafe: true },
|
|
3540
|
+
{ id: 'underlineDraw', name: 'Underline Draw', type: 'decorate', family: 'Text', themeable: true, motionSafe: true },
|
|
3541
|
+
{ id: 'borderBeam', name: 'Border Beam', type: 'decorate', family: 'Card', themeable: true, motionSafe: true },
|
|
3542
|
+
{ id: 'tiltShine', name: 'Tilt Shine', type: 'decorate', family: 'Card', themeable: true, motionSafe: true },
|
|
3543
|
+
{ id: 'cardSpotlight', name: 'Card Spotlight', type: 'decorate', family: 'Card', themeable: true, motionSafe: true },
|
|
3544
|
+
{ id: 'magneticPull', name: 'Magnetic Pull', type: 'decorate', family: 'Pointer', themeable: true, motionSafe: true },
|
|
3545
|
+
{ id: 'pointerRipple', name: 'Pointer Ripple', type: 'decorate', family: 'Pointer', themeable: true, motionSafe: true },
|
|
3274
3546
|
];
|
|
3275
3547
|
|
|
3276
3548
|
/** Names of every built-in recipe (the keys of RECIPES at load time). */
|
|
@@ -3615,6 +3887,251 @@ export function RatingSkin(o = {}) {
|
|
|
3615
3887
|
};
|
|
3616
3888
|
}
|
|
3617
3889
|
|
|
3890
|
+
/** Checkbox skin (E1b, decisions/0009) -- a box that draws a tick when checked
|
|
3891
|
+
* and a dash when indeterminate ("mixed"). Tri-state read from aria-checked (the
|
|
3892
|
+
* VALUE form true|false|mixed) or the data-* presence pair -- both map onto the
|
|
3893
|
+
* EXISTING toggled + indeterminate slots (no state-shape change). CheckboxGroupSkin
|
|
3894
|
+
* aliases this: a checkbox-group master paints the identical 3-state contract, so
|
|
3895
|
+
* "mixed" just reads as "some members". Zero per-frame alloc. */
|
|
3896
|
+
export function CheckboxSkin(o = {}) {
|
|
3897
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3898
|
+
let on = 0; // eased box fill (checked or mixed)
|
|
3899
|
+
let mk = 0; // eased mark draw
|
|
3900
|
+
return {
|
|
3901
|
+
headless: {
|
|
3902
|
+
attrs: ['aria-checked', 'data-checked', 'data-indeterminate', 'data-disabled', 'aria-disabled'],
|
|
3903
|
+
read(host, handle, st) {
|
|
3904
|
+
const ac = host.getAttribute('aria-checked'); // value form: true|false|mixed
|
|
3905
|
+
st.indeterminate = ac === 'mixed' || _skinBool(host, 'data-indeterminate');
|
|
3906
|
+
st.toggled = !st.indeterminate && (ac === 'true' || _skinBool(host, 'data-checked'));
|
|
3907
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
3908
|
+
},
|
|
3909
|
+
},
|
|
3910
|
+
tick(c, dt, now, st) {
|
|
3911
|
+
const s = st.w < st.h ? st.w : st.h;
|
|
3912
|
+
const bx = (st.w - s) / 2, by = (st.h - s) / 2, rr = s * 0.22;
|
|
3913
|
+
const filled = st.toggled || st.indeterminate;
|
|
3914
|
+
const k = dt * 14;
|
|
3915
|
+
on += ((filled ? 1 : 0) - on) * (k > 1 ? 1 : k);
|
|
3916
|
+
mk += ((filled ? 1 : 0) - mk) * (k > 1 ? 1 : k);
|
|
3917
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3918
|
+
c.globalAlpha = base;
|
|
3919
|
+
c.strokeStyle = P.dim; c.lineWidth = 2;
|
|
3920
|
+
roundRect(c, bx + 1, by + 1, s - 2, s - 2, rr); c.stroke();
|
|
3921
|
+
if (on > 0.01) {
|
|
3922
|
+
c.globalAlpha = base * on;
|
|
3923
|
+
c.fillStyle = P.accent; roundRect(c, bx + 1, by + 1, s - 2, s - 2, rr); c.fill();
|
|
3924
|
+
}
|
|
3925
|
+
if (mk > 0.01) {
|
|
3926
|
+
c.globalAlpha = base * mk;
|
|
3927
|
+
c.strokeStyle = P.dim2; c.lineWidth = s * 0.1; c.lineCap = 'round';
|
|
3928
|
+
c.beginPath();
|
|
3929
|
+
if (st.indeterminate) {
|
|
3930
|
+
c.moveTo(bx + s * 0.28, by + s * 0.5); c.lineTo(bx + s * 0.72, by + s * 0.5);
|
|
3931
|
+
} else {
|
|
3932
|
+
c.moveTo(bx + s * 0.26, by + s * 0.52);
|
|
3933
|
+
c.lineTo(bx + s * 0.44, by + s * 0.70);
|
|
3934
|
+
c.lineTo(bx + s * 0.76, by + s * 0.32);
|
|
3935
|
+
}
|
|
3936
|
+
c.stroke(); c.lineCap = 'butt';
|
|
3937
|
+
}
|
|
3938
|
+
c.globalAlpha = 1;
|
|
3939
|
+
if (st.focused) fr(c, st.w, st.h, rr);
|
|
3940
|
+
},
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
export const CheckboxGroupSkin = CheckboxSkin;
|
|
3944
|
+
|
|
3945
|
+
/** Select skin (E1b, decisions/0009) -- the closed/expanded TRIGGER of a
|
|
3946
|
+
* lite-headless select (the item-6 dropdown resolution). A rounded field with a
|
|
3947
|
+
* chevron that flips on aria-expanded and a dot when a value is selected (handle
|
|
3948
|
+
* value() fast path). The portaled listbox is NOT painted -- the single-host
|
|
3949
|
+
* adapter places one overlay over the trigger box; the open-state popup is the
|
|
3950
|
+
* recorded follow-on (decisions/0009). Zero per-frame alloc. */
|
|
3951
|
+
export function SelectSkin(o = {}) {
|
|
3952
|
+
const P = resolveTheme(o, { accent: '#a78bfa', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3953
|
+
let op = 0; // eased open
|
|
3954
|
+
return {
|
|
3955
|
+
headless: {
|
|
3956
|
+
attrs: ['aria-expanded', 'data-disabled', 'aria-disabled'],
|
|
3957
|
+
read(host, handle, st) {
|
|
3958
|
+
st.toggled = _ariaTrue(host, 'aria-expanded');
|
|
3959
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
3960
|
+
st.complete = !!(handle && typeof handle.value === 'function' && handle.value() != null);
|
|
3961
|
+
},
|
|
3962
|
+
},
|
|
3963
|
+
tick(c, dt, now, st) {
|
|
3964
|
+
const w = st.w, h = st.h, rr = h * 0.28;
|
|
3965
|
+
const k = dt * 12; op += ((st.toggled ? 1 : 0) - op) * (k > 1 ? 1 : k);
|
|
3966
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3967
|
+
c.globalAlpha = base;
|
|
3968
|
+
c.strokeStyle = st.toggled ? P.accent : P.dim; c.lineWidth = 2;
|
|
3969
|
+
roundRect(c, 1, 1, w - 2, h - 2, rr); c.stroke();
|
|
3970
|
+
if (op > 0.01) {
|
|
3971
|
+
c.globalAlpha = base * 0.12 * op;
|
|
3972
|
+
c.fillStyle = P.accent; roundRect(c, 1, 1, w - 2, h - 2, rr); c.fill();
|
|
3973
|
+
c.globalAlpha = base;
|
|
3974
|
+
}
|
|
3975
|
+
if (st.complete) {
|
|
3976
|
+
c.fillStyle = P.accent;
|
|
3977
|
+
c.beginPath(); c.arc(h * 0.5, h * 0.5, h * 0.14, 0, PI2); c.fill();
|
|
3978
|
+
}
|
|
3979
|
+
const cx = w - h * 0.5, cy = h * 0.5, s = h * 0.16, dir = 1 - 2 * op;
|
|
3980
|
+
c.strokeStyle = P.dim2; c.lineWidth = 2; c.lineCap = 'round';
|
|
3981
|
+
c.beginPath();
|
|
3982
|
+
c.moveTo(cx - s, cy - s * 0.5 * dir); c.lineTo(cx, cy + s * 0.5 * dir); c.lineTo(cx + s, cy - s * 0.5 * dir);
|
|
3983
|
+
c.stroke(); c.lineCap = 'butt';
|
|
3984
|
+
c.globalAlpha = 1;
|
|
3985
|
+
if (st.focused) fr(c, w, h, rr);
|
|
3986
|
+
},
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
/** Meter skin (E1b) -- a horizontal gauge from aria-valuenow/min/max, zone-tinted
|
|
3991
|
+
* from data-zone (optimum|sub-optimum|low). Zero per-frame alloc. */
|
|
3992
|
+
export function MeterSkin(o = {}) {
|
|
3993
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#fbbf24', dim: '#9999b8', dim2: '#f87171' });
|
|
3994
|
+
let dv = 0;
|
|
3995
|
+
return {
|
|
3996
|
+
headless: {
|
|
3997
|
+
attrs: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'data-zone', 'data-disabled'],
|
|
3998
|
+
read(host, handle, st) {
|
|
3999
|
+
const mn = _skinNum(host, 'aria-valuemin', 0);
|
|
4000
|
+
const mx = _skinNum(host, 'aria-valuemax', 100);
|
|
4001
|
+
const nw = _skinNum(host, 'aria-valuenow', mn);
|
|
4002
|
+
const span = mx - mn;
|
|
4003
|
+
let v = span > 0 ? (nw - mn) / span : 0;
|
|
4004
|
+
st.val = v < 0 ? 0 : (v > 1 ? 1 : v);
|
|
4005
|
+
const zone = host.getAttribute('data-zone');
|
|
4006
|
+
st.complete = zone === 'optimum';
|
|
4007
|
+
st.error = zone === 'low';
|
|
4008
|
+
st.disabled = _skinBool(host, 'data-disabled');
|
|
4009
|
+
},
|
|
4010
|
+
},
|
|
4011
|
+
tick(c, dt, now, st) {
|
|
4012
|
+
const w = st.w, cy = st.h / 2, bh = st.h < 12 ? st.h : 12;
|
|
4013
|
+
const k = dt * 8; dv += (st.val - dv) * (k > 1 ? 1 : k);
|
|
4014
|
+
const col = st.error ? P.dim2 : (st.complete ? P.accent : P.accent2);
|
|
4015
|
+
c.globalAlpha = st.disabled ? 0.2 : 0.25;
|
|
4016
|
+
c.fillStyle = P.dim; roundRect(c, 0, cy - bh / 2, w, bh, bh / 2); c.fill();
|
|
4017
|
+
c.globalAlpha = st.disabled ? 0.4 : 1;
|
|
4018
|
+
const fx = dv * w;
|
|
4019
|
+
c.fillStyle = col; roundRect(c, 0, cy - bh / 2, fx > 0 ? fx : 0, bh, bh / 2); c.fill();
|
|
4020
|
+
c.globalAlpha = 1;
|
|
4021
|
+
if (st.focused) fr(c, w, st.h, bh / 2);
|
|
4022
|
+
},
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
/** Steps skin (E1b) -- a rail of N nodes filled up to data-current-index (count
|
|
4027
|
+
* from data-step-count), the current node on the leading edge; data-complete
|
|
4028
|
+
* fills all. count/val slots reused (val carries the raw current index). Fixed
|
|
4029
|
+
* loop bounded to 12, zero per-frame alloc. */
|
|
4030
|
+
export function StepsSkin(o = {}) {
|
|
4031
|
+
const P = resolveTheme(o, { accent: '#a78bfa', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
4032
|
+
let dv = 0; // eased current index
|
|
4033
|
+
return {
|
|
4034
|
+
headless: {
|
|
4035
|
+
attrs: ['data-step-count', 'data-current-index', 'data-complete', 'data-disabled'],
|
|
4036
|
+
read(host, handle, st) {
|
|
4037
|
+
let n = _skinNum(host, 'data-step-count', 3) | 0;
|
|
4038
|
+
st.count = n < 2 ? 2 : (n > 12 ? 12 : n);
|
|
4039
|
+
st.val = _skinNum(host, 'data-current-index', 0); // raw current index (0-based)
|
|
4040
|
+
st.complete = _skinBool(host, 'data-complete');
|
|
4041
|
+
st.disabled = _skinBool(host, 'data-disabled');
|
|
4042
|
+
},
|
|
4043
|
+
},
|
|
4044
|
+
tick(c, dt, now, st) {
|
|
4045
|
+
const n = st.count > 1 ? st.count : 3;
|
|
4046
|
+
const cur = st.complete ? n - 1 : (st.val < 0 ? 0 : st.val);
|
|
4047
|
+
const k = dt * 10; dv += (cur - dv) * (k > 1 ? 1 : k);
|
|
4048
|
+
const cy = st.h / 2, gap = st.w / n, rad = (st.h < gap ? st.h : gap) * 0.22;
|
|
4049
|
+
const base = st.disabled ? 0.4 : 1;
|
|
4050
|
+
const x0 = gap * 0.5, xN = gap * (n - 0.5);
|
|
4051
|
+
c.globalAlpha = base * 0.25; c.strokeStyle = P.dim; c.lineWidth = 3;
|
|
4052
|
+
c.beginPath(); c.moveTo(x0, cy); c.lineTo(xN, cy); c.stroke();
|
|
4053
|
+
c.globalAlpha = base; c.strokeStyle = P.accent;
|
|
4054
|
+
const fx = x0 + gap * dv;
|
|
4055
|
+
c.beginPath(); c.moveTo(x0, cy); c.lineTo(fx > xN ? xN : (fx < x0 ? x0 : fx), cy); c.stroke();
|
|
4056
|
+
for (let i = 0; i < n; i++) {
|
|
4057
|
+
const cx = gap * (i + 0.5);
|
|
4058
|
+
c.globalAlpha = base; c.fillStyle = (i <= dv + 0.001) ? P.accent : P.dim;
|
|
4059
|
+
c.beginPath(); c.arc(cx, cy, rad, 0, PI2); c.fill();
|
|
4060
|
+
}
|
|
4061
|
+
c.globalAlpha = 1;
|
|
4062
|
+
if (st.focused) fr(c, st.w, st.h, rad);
|
|
4063
|
+
},
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
/** Accordion skin (E1b) -- a header chevron that flips on aria-expanded /
|
|
4068
|
+
* data-open plus an underline hint that grows with open. Single trigger host,
|
|
4069
|
+
* zero per-frame alloc. */
|
|
4070
|
+
export function AccordionSkin(o = {}) {
|
|
4071
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
4072
|
+
let op = 0;
|
|
4073
|
+
return {
|
|
4074
|
+
headless: {
|
|
4075
|
+
attrs: ['aria-expanded', 'data-open', 'data-disabled', 'aria-disabled'],
|
|
4076
|
+
read(host, handle, st) {
|
|
4077
|
+
st.toggled = _ariaTrue(host, 'aria-expanded') || _skinBool(host, 'data-open');
|
|
4078
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
4079
|
+
},
|
|
4080
|
+
},
|
|
4081
|
+
tick(c, dt, now, st) {
|
|
4082
|
+
const w = st.w, h = st.h;
|
|
4083
|
+
const k = dt * 12; op += ((st.toggled ? 1 : 0) - op) * (k > 1 ? 1 : k);
|
|
4084
|
+
const base = st.disabled ? 0.4 : 1;
|
|
4085
|
+
const cx = w - h * 0.5, cy = h * 0.5, s = h * 0.16, dir = 1 - 2 * op;
|
|
4086
|
+
c.globalAlpha = base;
|
|
4087
|
+
c.strokeStyle = st.toggled ? P.accent : P.dim2; c.lineWidth = 2; c.lineCap = 'round';
|
|
4088
|
+
c.beginPath();
|
|
4089
|
+
c.moveTo(cx - s, cy - s * 0.5 * dir); c.lineTo(cx, cy + s * 0.5 * dir); c.lineTo(cx + s, cy - s * 0.5 * dir);
|
|
4090
|
+
c.stroke(); c.lineCap = 'butt';
|
|
4091
|
+
c.globalAlpha = base * 0.5;
|
|
4092
|
+
c.strokeStyle = P.accent; c.lineWidth = 2;
|
|
4093
|
+
const uw = (w - h) * op;
|
|
4094
|
+
c.beginPath(); c.moveTo(2, h - 3); c.lineTo(2 + (uw > 0 ? uw : 0), h - 3); c.stroke();
|
|
4095
|
+
c.globalAlpha = 1;
|
|
4096
|
+
if (st.focused) fr(c, w, h, 4);
|
|
4097
|
+
},
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
/** Skeleton skin (E1b) -- a shimmer sweep over a rounded placeholder while
|
|
4102
|
+
* data-loading / aria-busy; fades out when ready. Zero per-frame alloc: a moving
|
|
4103
|
+
* highlight band via globalAlpha, never a per-frame gradient. */
|
|
4104
|
+
export function SkeletonSkin(o = {}) {
|
|
4105
|
+
const P = resolveTheme(o, { accent: '#e2e2f0', dim: '#3a3a4a' });
|
|
4106
|
+
let ph = 0; // shimmer phase 0..1
|
|
4107
|
+
let vis = 0; // eased placeholder visibility (1 loading, 0 ready)
|
|
4108
|
+
return {
|
|
4109
|
+
headless: {
|
|
4110
|
+
attrs: ['data-loading', 'aria-busy'],
|
|
4111
|
+
read(host, handle, st) {
|
|
4112
|
+
st.indeterminate = _skinBool(host, 'data-loading') || _ariaTrue(host, 'aria-busy');
|
|
4113
|
+
st.complete = !st.indeterminate;
|
|
4114
|
+
},
|
|
4115
|
+
},
|
|
4116
|
+
tick(c, dt, now, st) {
|
|
4117
|
+
const w = st.w, h = st.h, rr = (h < 10 ? h : 10) * 0.5;
|
|
4118
|
+
const loading = st.indeterminate;
|
|
4119
|
+
const k = dt * 6; vis += ((loading ? 1 : 0) - vis) * (k > 1 ? 1 : k);
|
|
4120
|
+
if (vis < 0.01) { c.globalAlpha = 1; return; }
|
|
4121
|
+
if (loading) { ph += dt * 0.8; if (ph > 1) ph -= 1; }
|
|
4122
|
+
c.globalAlpha = vis * 0.5;
|
|
4123
|
+
c.fillStyle = P.dim; roundRect(c, 0, 0, w, h, rr); c.fill();
|
|
4124
|
+
const bw = w * 0.28, bx = -bw + ph * (w + bw);
|
|
4125
|
+
const bx0 = bx < 0 ? 0 : bx, bx1 = bx + bw > w ? w : bx + bw;
|
|
4126
|
+
if (bx1 > bx0) {
|
|
4127
|
+
c.globalAlpha = vis * 0.18;
|
|
4128
|
+
c.fillStyle = P.accent; roundRect(c, bx0, 0, bx1 - bx0, h, rr); c.fill();
|
|
4129
|
+
}
|
|
4130
|
+
c.globalAlpha = 1;
|
|
4131
|
+
},
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
|
|
3618
4135
|
/**
|
|
3619
4136
|
* The headless-skin registry -- a SIBLING of RECIPES, not part of it (decisions/
|
|
3620
4137
|
* 0008 decision 4). id -> factory (null-prototype), the meta rows a picker/demo
|
|
@@ -3626,6 +4143,13 @@ export const HEADLESS_SKINS = Object.assign(Object.create(null), {
|
|
|
3626
4143
|
sliderSkin: SliderSkin,
|
|
3627
4144
|
progressSkin: ProgressSkin,
|
|
3628
4145
|
ratingSkin: RatingSkin,
|
|
4146
|
+
checkboxSkin: CheckboxSkin,
|
|
4147
|
+
checkboxGroupSkin: CheckboxGroupSkin,
|
|
4148
|
+
selectSkin: SelectSkin,
|
|
4149
|
+
meterSkin: MeterSkin,
|
|
4150
|
+
stepsSkin: StepsSkin,
|
|
4151
|
+
accordionSkin: AccordionSkin,
|
|
4152
|
+
skeletonSkin: SkeletonSkin,
|
|
3629
4153
|
});
|
|
3630
4154
|
|
|
3631
4155
|
export const SKIN_META = [
|
|
@@ -3633,6 +4157,13 @@ export const SKIN_META = [
|
|
|
3633
4157
|
{ id: 'sliderSkin', name: 'Slider Skin', primitive: 'slider', themeable: true, motionSafe: false },
|
|
3634
4158
|
{ id: 'progressSkin', name: 'Progress Skin', primitive: 'progress', themeable: true, motionSafe: false },
|
|
3635
4159
|
{ id: 'ratingSkin', name: 'Rating Skin', primitive: 'rating', themeable: true, motionSafe: false },
|
|
4160
|
+
{ id: 'checkboxSkin', name: 'Checkbox Skin', primitive: 'checkbox', themeable: true, motionSafe: false },
|
|
4161
|
+
{ id: 'checkboxGroupSkin', name: 'Checkbox Group Skin', primitive: 'checkbox-group', themeable: true, motionSafe: false },
|
|
4162
|
+
{ id: 'selectSkin', name: 'Select Skin', primitive: 'select', themeable: true, motionSafe: false },
|
|
4163
|
+
{ id: 'meterSkin', name: 'Meter Skin', primitive: 'meter', themeable: true, motionSafe: false },
|
|
4164
|
+
{ id: 'stepsSkin', name: 'Steps Skin', primitive: 'steps', themeable: true, motionSafe: false },
|
|
4165
|
+
{ id: 'accordionSkin', name: 'Accordion Skin', primitive: 'accordion', themeable: true, motionSafe: false },
|
|
4166
|
+
{ id: 'skeletonSkin', name: 'Skeleton Skin', primitive: 'skeleton', themeable: true, motionSafe: false },
|
|
3636
4167
|
];
|
|
3637
4168
|
|
|
3638
4169
|
export const SKIN_NAMES = Object.freeze(Object.keys(HEADLESS_SKINS));
|
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. 65 built-in recipes.
|
|
3
3
|
|
|
4
|
-
VERSION 1.
|
|
4
|
+
VERSION 1.12.0
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
npm i @zakkster/lite-ui-fx
|
|
@@ -14,11 +14,12 @@ Recipe = { tick(), init?(), onHover?(), onClick?(), onToggle?(), onDrag?(), dest
|
|
|
14
14
|
## Import -- Controller
|
|
15
15
|
import { mountUIFX, decorateUIFX, mountUIFXGroup, UIType, GroupType } from '@zakkster/lite-ui-fx';
|
|
16
16
|
|
|
17
|
-
## Import -- Recipes (one ./recipes subpath,
|
|
17
|
+
## Import -- Recipes (one ./recipes subpath, 65 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';
|
|
21
21
|
import { PillTabs, SegmentedSlide, RadioOrbit, BubbleRating } from '@zakkster/lite-ui-fx/recipes'; // U7 group recipes
|
|
22
|
+
import { TextShimmer, SpotlightText, BorderBeam, CardSpotlight, MagneticPull, PointerRipple } from '@zakkster/lite-ui-fx/recipes'; // E2 decorations
|
|
22
23
|
|
|
23
24
|
## Import -- Recipe registry (data-driven pickers)
|
|
24
25
|
import { RECIPES, RECIPE_META, RECIPE_NAMES, registerRecipe, mountRecipe } from '@zakkster/lite-ui-fx/recipes';
|
|
@@ -96,6 +97,7 @@ driven // U5 host clock: boolean; true = no ticker/RAF, the host calls instan
|
|
|
96
97
|
// identical, one overlay canvas + one MutationObserver removed on destroy; the
|
|
97
98
|
// lite-headless handle is never destroyed (the caller owns it).
|
|
98
99
|
import { skinHeadless, SwitchSkin, SliderSkin, ProgressSkin, RatingSkin } from '@zakkster/lite-ui-fx/headless';
|
|
100
|
+
import { CheckboxSkin, CheckboxGroupSkin, SelectSkin, MeterSkin, StepsSkin, AccordionSkin, SkeletonSkin } from '@zakkster/lite-ui-fx/headless'; // E1b
|
|
99
101
|
import { HEADLESS_SKINS, SKIN_META, SKIN_NAMES } from '@zakkster/lite-ui-fx/headless';
|
|
100
102
|
const skin = skinHeadless(handle, SwitchSkin, { host: switchThumbEl, theme });
|
|
101
103
|
skin.el // the skinned host (unchanged)
|
|
@@ -108,12 +110,19 @@ skin.destroy() // remove overlay + observer + listeners; host + handle untouch
|
|
|
108
110
|
// EVENT time (never per frame) to parse painted state into preallocated slots.
|
|
109
111
|
// options: { host(REQUIRED), padding, seed, colors, theme, text, font, ticker, driven };
|
|
110
112
|
// hijack-only keys throw. setValue/setChecked throw (a skin reflects the primitive).
|
|
111
|
-
//
|
|
112
|
-
// unchanged): SwitchSkin (data-checked/aria-checked), SliderSkin (aria-
|
|
113
|
-
// min/max + data-dragging), ProgressSkin (aria-valuenow/max + data-
|
|
114
|
-
// data-loading), RatingSkin (aria-valuenow/max over N
|
|
115
|
-
//
|
|
116
|
-
//
|
|
113
|
+
// Skins (SKIN_META, a registry SEPARATE from RECIPES -- the recipe count is
|
|
114
|
+
// unchanged). E1: SwitchSkin (data-checked/aria-checked), SliderSkin (aria-
|
|
115
|
+
// valuenow/min/max + data-dragging), ProgressSkin (aria-valuenow/max + data-
|
|
116
|
+
// complete/data-loading), RatingSkin (aria-valuenow/max over N). E1b (0009):
|
|
117
|
+
// CheckboxSkin (tri-state aria-checked true|false|mixed + data-checked/data-
|
|
118
|
+
// indeterminate), CheckboxGroupSkin (same body over a checkbox-group master),
|
|
119
|
+
// SelectSkin (the select TRIGGER: aria-expanded + handle.value(); the portaled
|
|
120
|
+
// listbox open-state is a follow-on), MeterSkin (aria-valuenow/min/max + data-zone),
|
|
121
|
+
// StepsSkin (data-step-count/data-current-index/data-complete), AccordionSkin
|
|
122
|
+
// (aria-expanded/data-open), SkeletonSkin (data-loading/aria-busy shimmer). 11
|
|
123
|
+
// total, each themeable, born zero-alloc + t6-gated. A painted attr is truthy when
|
|
124
|
+
// PRESENT with any value but "false" (handles boolean data-disabled and value
|
|
125
|
+
// data-checked="true").
|
|
117
126
|
|
|
118
127
|
## Host clock (U5) -- three mutually-exclusive modes, both mount modes
|
|
119
128
|
// default (neither option): the shared ref-counted ticker -- one RAF for all
|
|
@@ -157,7 +166,7 @@ GroupType.RATING -> radiogroup of N radios (rating semantics); native roving
|
|
|
157
166
|
// itemX/itemY/itemW/itemH (Float32Array per-item geometry lanes, read by index).
|
|
158
167
|
// The scalar value fields (val/toggled/indeterminate) are neutral for a group.
|
|
159
168
|
|
|
160
|
-
##
|
|
169
|
+
## 65 Built-in Recipes
|
|
161
170
|
|
|
162
171
|
### Vol. 1 -- 10 recipes
|
|
163
172
|
Toggles: SwarmToggle, LiquidToggle, NeonPulseToggle
|
|
@@ -200,6 +209,21 @@ Rating (GroupType.RATING): BubbleRating
|
|
|
200
209
|
// fakes to real group types, so their native arrow-key selection is now correct;
|
|
201
210
|
// SegmentedSlide is new. Selection is state.index; onSelect(index, state) fires once.
|
|
202
211
|
|
|
212
|
+
### E2 -- text-fx + pointer + card decorations (decorateUIFX: a canvas AROUND a live element)
|
|
213
|
+
Text (paint OVER live text): TextShimmer, SpotlightText, UnderlineDraw
|
|
214
|
+
Card: BorderBeam, TiltShine, CardSpotlight
|
|
215
|
+
Pointer: MagneticPull, PointerRipple
|
|
216
|
+
// All type 'decorate' (RECIPE_META), motionSafe (a reduced-motion calm path), zero
|
|
217
|
+
// per-frame alloc. They paint OVER a live element and never modify it; a recipe
|
|
218
|
+
// receives only (ctx, dt, now, state, pointer) -- never a DOM handle -- so it cannot
|
|
219
|
+
// read layout and is structurally reflow-safe (the decorate adapter owns every DOM
|
|
220
|
+
// read). No lite-particles/lite-noise: shimmer/glow/beam are const colours +
|
|
221
|
+
// globalAlpha, no gradient built in tick. Text-fx cover <input> AND <textarea>;
|
|
222
|
+
// card/pointer decorate any live element. DEFERRED (decisions/0010): pointer trail +
|
|
223
|
+
// proximity-glow (need lite-particles -> the 1.13.0 phase); ScrambleIn (needs
|
|
224
|
+
// host-owned text -> a future hijack-mode text tranche); Dock magnify (a multi-
|
|
225
|
+
// element row -> a future group/dock vehicle). See 0010.
|
|
226
|
+
|
|
203
227
|
## Writing Custom Recipes
|
|
204
228
|
See UIFX-RECIPE-GUIDE.md (included in package).
|
|
205
229
|
|
|
@@ -215,9 +239,9 @@ See UIFX-RECIPE-GUIDE.md (included in package).
|
|
|
215
239
|
- Zero-GC in all built-in recipes: const colors + globalAlpha, precomputed
|
|
216
240
|
color/label LUTs, fixed preallocated particle pools, gradients built in init.
|
|
217
241
|
Gated per recipe by the t3-frame-alloc torture tier (default AND themed mount).
|
|
218
|
-
- Themeable: all
|
|
242
|
+
- Themeable: all 65 recipes honour { colors, theme:{light,mid,dark}, text, font },
|
|
219
243
|
resolved once in init (zero per-frame alloc). RECIPE_META.themeable is true for
|
|
220
|
-
all
|
|
244
|
+
all 65. A bare mount is byte-identical to pre-theming. Shipped palettes + APCA
|
|
221
245
|
contrast are authored with @zakkster/lite-hueforge (a dev-only tool, never a
|
|
222
246
|
runtime dependency).
|
|
223
247
|
- U5 host clock: mount { ticker } to ride a caller-supplied lite-ticker (destroy
|
|
@@ -226,7 +250,8 @@ See UIFX-RECIPE-GUIDE.md (included in package).
|
|
|
226
250
|
- U5 reduced motion: state.reducedMotion (matchMedia, watched) makes calm-path
|
|
227
251
|
recipes render statically -- ErrorShake stops shaking, SwarmToggle/SuccessBloom/
|
|
228
252
|
FocusHalo drop their motion. RECIPE_META.motionSafe is true for EXACTLY the
|
|
229
|
-
recipes that ship a calm path (SwarmToggle + the 5 decorate recipes
|
|
253
|
+
recipes that ship a calm path (SwarmToggle + the 5 U4b decorate recipes + the 8 E2
|
|
254
|
+
decorations = 14 today; the
|
|
230
255
|
rest honestly false until each lands one). mountRecipe warns (not throws) mounting
|
|
231
256
|
a motionSafe:false recipe under active reduce. state.budget (0..1) lets budget-aware
|
|
232
257
|
recipes shed work before frames drop.
|
|
@@ -250,7 +275,11 @@ See UIFX-RECIPE-GUIDE.md (included in package).
|
|
|
250
275
|
paints a @zakkster/lite-headless primitive by OBSERVING its painted state
|
|
251
276
|
attributes (one MutationObserver; parsed at event time, never per frame) --
|
|
252
277
|
lite-ui-fx paints, lite-headless behaves. lite-headless is a compose-target, NEVER
|
|
253
|
-
a dependency (the package imports it nowhere).
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
278
|
+
a dependency (the package imports it nowhere). 11 skins live in a registry
|
|
279
|
+
(HEADLESS_SKINS/SKIN_META) SEPARATE from the 65 recipes: E1 switch/slider/progress/
|
|
280
|
+
rating; E1b (decisions/0009) checkbox (tri-state) + checkbox-group master, select
|
|
281
|
+
(the trigger -- resolving the dropdown/item-6 decision toward a V4 skin; the
|
|
282
|
+
portaled listbox open-state is a follow-on), meter, steps, accordion, skeleton --
|
|
283
|
+
still bespoke (no lite-particles). Each themeable + zero per-frame alloc (t6-gated).
|
|
284
|
+
setValue/setChecked throw (a skin reflects the primitive); destroy leaves the host
|
|
285
|
+
+ handle untouched. See 0008 + 0009.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-ui-fx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Canvas-hijacked UI components with a pluggable recipe system: 57 built-in recipes across toggles, buttons, sliders, knobs, loaders, checkboxes, counters and ratings, in three mount modes (hijack, decorate, grouped controls) plus a lite-headless skin adapter.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|