@zakkster/lite-ui-fx 1.10.0 → 1.11.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 +49 -1
- package/README.md +5 -4
- package/UIFXController.js +1 -1
- package/UIFXHeadless.d.ts +7 -0
- package/UIFXHeadless.js +7 -0
- package/UIFXRecipes.d.ts +7 -0
- package/UIFXRecipes.js +274 -9
- package/llms.txt +23 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ 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.11.0] -- 2026-09-08
|
|
9
|
+
|
|
10
|
+
Enrichment session E1b (decisions/0009): seven bespoke headless skins continuing the
|
|
11
|
+
E1 adapter -- the select trigger (resolving the dropdown decision toward a V4 skin),
|
|
12
|
+
a tri-state checkbox + checkbox-group master, and meter/steps/accordion/skeleton.
|
|
13
|
+
Single-host, no new dependency; the 57-recipe registry is byte-identical.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Seven headless skins on the `./headless` subpath, registered in the sibling
|
|
18
|
+
`HEADLESS_SKINS` / `SKIN_META` / `SKIN_NAMES` (4 -> 11 skins; RECIPES / RECIPE_META
|
|
19
|
+
and the 57-recipe count unchanged). Each is single-host, themeable, and allocates
|
|
20
|
+
zero bytes per frame, gated by the t6 torture tier (now gating 11 skins):
|
|
21
|
+
- `CheckboxSkin` -- tri-state from `aria-checked` (`true`|`false`|`mixed`) or the
|
|
22
|
+
`data-checked` / `data-indeterminate` presence pair, onto the existing `toggled`
|
|
23
|
+
/ `indeterminate` state slots.
|
|
24
|
+
- `CheckboxGroupSkin` -- the same body over a checkbox-group master (identical
|
|
25
|
+
3-state contract; "mixed" reads as partial members).
|
|
26
|
+
- `SelectSkin` -- the select TRIGGER: `aria-expanded` drives an open/close chevron,
|
|
27
|
+
the handle `value()` fast path drives a selected-value dot. The portaled listbox
|
|
28
|
+
open-state is a recorded follow-on (decisions/0009).
|
|
29
|
+
- `MeterSkin` -- `aria-valuenow`/`min`/`max` fill, zone-tinted from `data-zone`.
|
|
30
|
+
- `StepsSkin` -- a node rail from `data-step-count` / `data-current-index` /
|
|
31
|
+
`data-complete`.
|
|
32
|
+
- `AccordionSkin` -- a header chevron + underline from `aria-expanded` / `data-open`.
|
|
33
|
+
- `SkeletonSkin` -- a zero-allocation `globalAlpha` shimmer from `data-loading` /
|
|
34
|
+
`aria-busy`.
|
|
35
|
+
`@zakkster/lite-headless` is imported nowhere and is not a dependency; no dependency
|
|
36
|
+
was added.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `llms.txt` and `README.md`: the `./headless` skin catalog lists all 11 skins, and
|
|
41
|
+
the sibling primitive count is corrected ("59" -> 62, the `@zakkster/lite-headless`
|
|
42
|
+
1.9.1 catalog). Documentation only; no code path changed.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
none
|
|
47
|
+
|
|
48
|
+
### Removed
|
|
49
|
+
|
|
50
|
+
none
|
|
51
|
+
|
|
8
52
|
## [1.10.0] -- 2026-09-08
|
|
9
53
|
|
|
10
54
|
Enrichment session E1: skinHeadless, a fourth mount adapter that paints a
|
|
@@ -41,7 +85,11 @@ byte-identical.
|
|
|
41
85
|
|
|
42
86
|
### Fixed
|
|
43
87
|
|
|
44
|
-
|
|
88
|
+
- ReactionPicker: paints a hairline ring around each disc and seeds the discs at
|
|
89
|
+
rest radius, so the row is visible on the first frame and where colour-emoji
|
|
90
|
+
glyphs do not render. Previously only the 4%-alpha discs and the emoji drew, so
|
|
91
|
+
an un-ticked or emoji-less environment showed an empty card. No added per-frame
|
|
92
|
+
allocation; the torture alloc baseline is unchanged.
|
|
45
93
|
|
|
46
94
|
### Removed
|
|
47
95
|
|
package/README.md
CHANGED
|
@@ -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 57 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
|
|
|
@@ -377,6 +377,7 @@ Each is an ADR under [`decisions/`](decisions/):
|
|
|
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
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 57 recipes (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).
|
|
380
381
|
|
|
381
382
|
---
|
|
382
383
|
|
|
@@ -399,7 +400,7 @@ The suite covers the a11y state machine (one Space press = one `onToggle`, state
|
|
|
399
400
|
- **Not a component framework.** It paints controls; it does not do layout, routing, or state management. Bring your own.
|
|
400
401
|
- **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
402
|
- **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
|
|
403
|
+
- **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
404
|
|
|
404
405
|
---
|
|
405
406
|
|
|
@@ -412,7 +413,7 @@ Part of the **@zakkster** zero-GC stack:
|
|
|
412
413
|
- [`lite-random`](https://www.npmjs.com/package/@zakkster/lite-random) -- seeded Mulberry32 RNG (deterministic particle recipes)
|
|
413
414
|
- [`lite-scratch-fx`](https://www.npmjs.com/package/@zakkster/lite-scratch-fx) -- canvas scratch-reveal recipes; shares the `{ light, mid, dark }` theme shape
|
|
414
415
|
- [`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) --
|
|
416
|
+
- [`lite-headless`](https://www.npmjs.com/package/@zakkster/lite-headless) -- 62 ARIA-correct primitives; a headless-skin target (`skinHeadless`)
|
|
416
417
|
- **`lite-ui-fx`** -- this package
|
|
417
418
|
|
|
418
419
|
---
|
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.11.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
|
@@ -348,6 +348,13 @@ export declare function SwitchSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
|
348
348
|
export declare function SliderSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
349
349
|
export declare function ProgressSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
350
350
|
export declare function RatingSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
351
|
+
export declare function CheckboxSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
352
|
+
export declare function CheckboxGroupSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
353
|
+
export declare function SelectSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
354
|
+
export declare function MeterSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
355
|
+
export declare function StepsSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
356
|
+
export declare function AccordionSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
357
|
+
export declare function SkeletonSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
351
358
|
|
|
352
359
|
/** A headless-skin meta row. `primitive` names the lite-headless primitive the
|
|
353
360
|
* 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);
|
|
@@ -3615,6 +3621,251 @@ export function RatingSkin(o = {}) {
|
|
|
3615
3621
|
};
|
|
3616
3622
|
}
|
|
3617
3623
|
|
|
3624
|
+
/** Checkbox skin (E1b, decisions/0009) -- a box that draws a tick when checked
|
|
3625
|
+
* and a dash when indeterminate ("mixed"). Tri-state read from aria-checked (the
|
|
3626
|
+
* VALUE form true|false|mixed) or the data-* presence pair -- both map onto the
|
|
3627
|
+
* EXISTING toggled + indeterminate slots (no state-shape change). CheckboxGroupSkin
|
|
3628
|
+
* aliases this: a checkbox-group master paints the identical 3-state contract, so
|
|
3629
|
+
* "mixed" just reads as "some members". Zero per-frame alloc. */
|
|
3630
|
+
export function CheckboxSkin(o = {}) {
|
|
3631
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3632
|
+
let on = 0; // eased box fill (checked or mixed)
|
|
3633
|
+
let mk = 0; // eased mark draw
|
|
3634
|
+
return {
|
|
3635
|
+
headless: {
|
|
3636
|
+
attrs: ['aria-checked', 'data-checked', 'data-indeterminate', 'data-disabled', 'aria-disabled'],
|
|
3637
|
+
read(host, handle, st) {
|
|
3638
|
+
const ac = host.getAttribute('aria-checked'); // value form: true|false|mixed
|
|
3639
|
+
st.indeterminate = ac === 'mixed' || _skinBool(host, 'data-indeterminate');
|
|
3640
|
+
st.toggled = !st.indeterminate && (ac === 'true' || _skinBool(host, 'data-checked'));
|
|
3641
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
3642
|
+
},
|
|
3643
|
+
},
|
|
3644
|
+
tick(c, dt, now, st) {
|
|
3645
|
+
const s = st.w < st.h ? st.w : st.h;
|
|
3646
|
+
const bx = (st.w - s) / 2, by = (st.h - s) / 2, rr = s * 0.22;
|
|
3647
|
+
const filled = st.toggled || st.indeterminate;
|
|
3648
|
+
const k = dt * 14;
|
|
3649
|
+
on += ((filled ? 1 : 0) - on) * (k > 1 ? 1 : k);
|
|
3650
|
+
mk += ((filled ? 1 : 0) - mk) * (k > 1 ? 1 : k);
|
|
3651
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3652
|
+
c.globalAlpha = base;
|
|
3653
|
+
c.strokeStyle = P.dim; c.lineWidth = 2;
|
|
3654
|
+
roundRect(c, bx + 1, by + 1, s - 2, s - 2, rr); c.stroke();
|
|
3655
|
+
if (on > 0.01) {
|
|
3656
|
+
c.globalAlpha = base * on;
|
|
3657
|
+
c.fillStyle = P.accent; roundRect(c, bx + 1, by + 1, s - 2, s - 2, rr); c.fill();
|
|
3658
|
+
}
|
|
3659
|
+
if (mk > 0.01) {
|
|
3660
|
+
c.globalAlpha = base * mk;
|
|
3661
|
+
c.strokeStyle = P.dim2; c.lineWidth = s * 0.1; c.lineCap = 'round';
|
|
3662
|
+
c.beginPath();
|
|
3663
|
+
if (st.indeterminate) {
|
|
3664
|
+
c.moveTo(bx + s * 0.28, by + s * 0.5); c.lineTo(bx + s * 0.72, by + s * 0.5);
|
|
3665
|
+
} else {
|
|
3666
|
+
c.moveTo(bx + s * 0.26, by + s * 0.52);
|
|
3667
|
+
c.lineTo(bx + s * 0.44, by + s * 0.70);
|
|
3668
|
+
c.lineTo(bx + s * 0.76, by + s * 0.32);
|
|
3669
|
+
}
|
|
3670
|
+
c.stroke(); c.lineCap = 'butt';
|
|
3671
|
+
}
|
|
3672
|
+
c.globalAlpha = 1;
|
|
3673
|
+
if (st.focused) fr(c, st.w, st.h, rr);
|
|
3674
|
+
},
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
export const CheckboxGroupSkin = CheckboxSkin;
|
|
3678
|
+
|
|
3679
|
+
/** Select skin (E1b, decisions/0009) -- the closed/expanded TRIGGER of a
|
|
3680
|
+
* lite-headless select (the item-6 dropdown resolution). A rounded field with a
|
|
3681
|
+
* chevron that flips on aria-expanded and a dot when a value is selected (handle
|
|
3682
|
+
* value() fast path). The portaled listbox is NOT painted -- the single-host
|
|
3683
|
+
* adapter places one overlay over the trigger box; the open-state popup is the
|
|
3684
|
+
* recorded follow-on (decisions/0009). Zero per-frame alloc. */
|
|
3685
|
+
export function SelectSkin(o = {}) {
|
|
3686
|
+
const P = resolveTheme(o, { accent: '#a78bfa', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3687
|
+
let op = 0; // eased open
|
|
3688
|
+
return {
|
|
3689
|
+
headless: {
|
|
3690
|
+
attrs: ['aria-expanded', 'data-disabled', 'aria-disabled'],
|
|
3691
|
+
read(host, handle, st) {
|
|
3692
|
+
st.toggled = _ariaTrue(host, 'aria-expanded');
|
|
3693
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
3694
|
+
st.complete = !!(handle && typeof handle.value === 'function' && handle.value() != null);
|
|
3695
|
+
},
|
|
3696
|
+
},
|
|
3697
|
+
tick(c, dt, now, st) {
|
|
3698
|
+
const w = st.w, h = st.h, rr = h * 0.28;
|
|
3699
|
+
const k = dt * 12; op += ((st.toggled ? 1 : 0) - op) * (k > 1 ? 1 : k);
|
|
3700
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3701
|
+
c.globalAlpha = base;
|
|
3702
|
+
c.strokeStyle = st.toggled ? P.accent : P.dim; c.lineWidth = 2;
|
|
3703
|
+
roundRect(c, 1, 1, w - 2, h - 2, rr); c.stroke();
|
|
3704
|
+
if (op > 0.01) {
|
|
3705
|
+
c.globalAlpha = base * 0.12 * op;
|
|
3706
|
+
c.fillStyle = P.accent; roundRect(c, 1, 1, w - 2, h - 2, rr); c.fill();
|
|
3707
|
+
c.globalAlpha = base;
|
|
3708
|
+
}
|
|
3709
|
+
if (st.complete) {
|
|
3710
|
+
c.fillStyle = P.accent;
|
|
3711
|
+
c.beginPath(); c.arc(h * 0.5, h * 0.5, h * 0.14, 0, PI2); c.fill();
|
|
3712
|
+
}
|
|
3713
|
+
const cx = w - h * 0.5, cy = h * 0.5, s = h * 0.16, dir = 1 - 2 * op;
|
|
3714
|
+
c.strokeStyle = P.dim2; c.lineWidth = 2; c.lineCap = 'round';
|
|
3715
|
+
c.beginPath();
|
|
3716
|
+
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);
|
|
3717
|
+
c.stroke(); c.lineCap = 'butt';
|
|
3718
|
+
c.globalAlpha = 1;
|
|
3719
|
+
if (st.focused) fr(c, w, h, rr);
|
|
3720
|
+
},
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/** Meter skin (E1b) -- a horizontal gauge from aria-valuenow/min/max, zone-tinted
|
|
3725
|
+
* from data-zone (optimum|sub-optimum|low). Zero per-frame alloc. */
|
|
3726
|
+
export function MeterSkin(o = {}) {
|
|
3727
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#fbbf24', dim: '#9999b8', dim2: '#f87171' });
|
|
3728
|
+
let dv = 0;
|
|
3729
|
+
return {
|
|
3730
|
+
headless: {
|
|
3731
|
+
attrs: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'data-zone', 'data-disabled'],
|
|
3732
|
+
read(host, handle, st) {
|
|
3733
|
+
const mn = _skinNum(host, 'aria-valuemin', 0);
|
|
3734
|
+
const mx = _skinNum(host, 'aria-valuemax', 100);
|
|
3735
|
+
const nw = _skinNum(host, 'aria-valuenow', mn);
|
|
3736
|
+
const span = mx - mn;
|
|
3737
|
+
let v = span > 0 ? (nw - mn) / span : 0;
|
|
3738
|
+
st.val = v < 0 ? 0 : (v > 1 ? 1 : v);
|
|
3739
|
+
const zone = host.getAttribute('data-zone');
|
|
3740
|
+
st.complete = zone === 'optimum';
|
|
3741
|
+
st.error = zone === 'low';
|
|
3742
|
+
st.disabled = _skinBool(host, 'data-disabled');
|
|
3743
|
+
},
|
|
3744
|
+
},
|
|
3745
|
+
tick(c, dt, now, st) {
|
|
3746
|
+
const w = st.w, cy = st.h / 2, bh = st.h < 12 ? st.h : 12;
|
|
3747
|
+
const k = dt * 8; dv += (st.val - dv) * (k > 1 ? 1 : k);
|
|
3748
|
+
const col = st.error ? P.dim2 : (st.complete ? P.accent : P.accent2);
|
|
3749
|
+
c.globalAlpha = st.disabled ? 0.2 : 0.25;
|
|
3750
|
+
c.fillStyle = P.dim; roundRect(c, 0, cy - bh / 2, w, bh, bh / 2); c.fill();
|
|
3751
|
+
c.globalAlpha = st.disabled ? 0.4 : 1;
|
|
3752
|
+
const fx = dv * w;
|
|
3753
|
+
c.fillStyle = col; roundRect(c, 0, cy - bh / 2, fx > 0 ? fx : 0, bh, bh / 2); c.fill();
|
|
3754
|
+
c.globalAlpha = 1;
|
|
3755
|
+
if (st.focused) fr(c, w, st.h, bh / 2);
|
|
3756
|
+
},
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
/** Steps skin (E1b) -- a rail of N nodes filled up to data-current-index (count
|
|
3761
|
+
* from data-step-count), the current node on the leading edge; data-complete
|
|
3762
|
+
* fills all. count/val slots reused (val carries the raw current index). Fixed
|
|
3763
|
+
* loop bounded to 12, zero per-frame alloc. */
|
|
3764
|
+
export function StepsSkin(o = {}) {
|
|
3765
|
+
const P = resolveTheme(o, { accent: '#a78bfa', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3766
|
+
let dv = 0; // eased current index
|
|
3767
|
+
return {
|
|
3768
|
+
headless: {
|
|
3769
|
+
attrs: ['data-step-count', 'data-current-index', 'data-complete', 'data-disabled'],
|
|
3770
|
+
read(host, handle, st) {
|
|
3771
|
+
let n = _skinNum(host, 'data-step-count', 3) | 0;
|
|
3772
|
+
st.count = n < 2 ? 2 : (n > 12 ? 12 : n);
|
|
3773
|
+
st.val = _skinNum(host, 'data-current-index', 0); // raw current index (0-based)
|
|
3774
|
+
st.complete = _skinBool(host, 'data-complete');
|
|
3775
|
+
st.disabled = _skinBool(host, 'data-disabled');
|
|
3776
|
+
},
|
|
3777
|
+
},
|
|
3778
|
+
tick(c, dt, now, st) {
|
|
3779
|
+
const n = st.count > 1 ? st.count : 3;
|
|
3780
|
+
const cur = st.complete ? n - 1 : (st.val < 0 ? 0 : st.val);
|
|
3781
|
+
const k = dt * 10; dv += (cur - dv) * (k > 1 ? 1 : k);
|
|
3782
|
+
const cy = st.h / 2, gap = st.w / n, rad = (st.h < gap ? st.h : gap) * 0.22;
|
|
3783
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3784
|
+
const x0 = gap * 0.5, xN = gap * (n - 0.5);
|
|
3785
|
+
c.globalAlpha = base * 0.25; c.strokeStyle = P.dim; c.lineWidth = 3;
|
|
3786
|
+
c.beginPath(); c.moveTo(x0, cy); c.lineTo(xN, cy); c.stroke();
|
|
3787
|
+
c.globalAlpha = base; c.strokeStyle = P.accent;
|
|
3788
|
+
const fx = x0 + gap * dv;
|
|
3789
|
+
c.beginPath(); c.moveTo(x0, cy); c.lineTo(fx > xN ? xN : (fx < x0 ? x0 : fx), cy); c.stroke();
|
|
3790
|
+
for (let i = 0; i < n; i++) {
|
|
3791
|
+
const cx = gap * (i + 0.5);
|
|
3792
|
+
c.globalAlpha = base; c.fillStyle = (i <= dv + 0.001) ? P.accent : P.dim;
|
|
3793
|
+
c.beginPath(); c.arc(cx, cy, rad, 0, PI2); c.fill();
|
|
3794
|
+
}
|
|
3795
|
+
c.globalAlpha = 1;
|
|
3796
|
+
if (st.focused) fr(c, st.w, st.h, rad);
|
|
3797
|
+
},
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
/** Accordion skin (E1b) -- a header chevron that flips on aria-expanded /
|
|
3802
|
+
* data-open plus an underline hint that grows with open. Single trigger host,
|
|
3803
|
+
* zero per-frame alloc. */
|
|
3804
|
+
export function AccordionSkin(o = {}) {
|
|
3805
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3806
|
+
let op = 0;
|
|
3807
|
+
return {
|
|
3808
|
+
headless: {
|
|
3809
|
+
attrs: ['aria-expanded', 'data-open', 'data-disabled', 'aria-disabled'],
|
|
3810
|
+
read(host, handle, st) {
|
|
3811
|
+
st.toggled = _ariaTrue(host, 'aria-expanded') || _skinBool(host, 'data-open');
|
|
3812
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
3813
|
+
},
|
|
3814
|
+
},
|
|
3815
|
+
tick(c, dt, now, st) {
|
|
3816
|
+
const w = st.w, h = st.h;
|
|
3817
|
+
const k = dt * 12; op += ((st.toggled ? 1 : 0) - op) * (k > 1 ? 1 : k);
|
|
3818
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3819
|
+
const cx = w - h * 0.5, cy = h * 0.5, s = h * 0.16, dir = 1 - 2 * op;
|
|
3820
|
+
c.globalAlpha = base;
|
|
3821
|
+
c.strokeStyle = st.toggled ? P.accent : P.dim2; c.lineWidth = 2; c.lineCap = 'round';
|
|
3822
|
+
c.beginPath();
|
|
3823
|
+
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);
|
|
3824
|
+
c.stroke(); c.lineCap = 'butt';
|
|
3825
|
+
c.globalAlpha = base * 0.5;
|
|
3826
|
+
c.strokeStyle = P.accent; c.lineWidth = 2;
|
|
3827
|
+
const uw = (w - h) * op;
|
|
3828
|
+
c.beginPath(); c.moveTo(2, h - 3); c.lineTo(2 + (uw > 0 ? uw : 0), h - 3); c.stroke();
|
|
3829
|
+
c.globalAlpha = 1;
|
|
3830
|
+
if (st.focused) fr(c, w, h, 4);
|
|
3831
|
+
},
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
/** Skeleton skin (E1b) -- a shimmer sweep over a rounded placeholder while
|
|
3836
|
+
* data-loading / aria-busy; fades out when ready. Zero per-frame alloc: a moving
|
|
3837
|
+
* highlight band via globalAlpha, never a per-frame gradient. */
|
|
3838
|
+
export function SkeletonSkin(o = {}) {
|
|
3839
|
+
const P = resolveTheme(o, { accent: '#e2e2f0', dim: '#3a3a4a' });
|
|
3840
|
+
let ph = 0; // shimmer phase 0..1
|
|
3841
|
+
let vis = 0; // eased placeholder visibility (1 loading, 0 ready)
|
|
3842
|
+
return {
|
|
3843
|
+
headless: {
|
|
3844
|
+
attrs: ['data-loading', 'aria-busy'],
|
|
3845
|
+
read(host, handle, st) {
|
|
3846
|
+
st.indeterminate = _skinBool(host, 'data-loading') || _ariaTrue(host, 'aria-busy');
|
|
3847
|
+
st.complete = !st.indeterminate;
|
|
3848
|
+
},
|
|
3849
|
+
},
|
|
3850
|
+
tick(c, dt, now, st) {
|
|
3851
|
+
const w = st.w, h = st.h, rr = (h < 10 ? h : 10) * 0.5;
|
|
3852
|
+
const loading = st.indeterminate;
|
|
3853
|
+
const k = dt * 6; vis += ((loading ? 1 : 0) - vis) * (k > 1 ? 1 : k);
|
|
3854
|
+
if (vis < 0.01) { c.globalAlpha = 1; return; }
|
|
3855
|
+
if (loading) { ph += dt * 0.8; if (ph > 1) ph -= 1; }
|
|
3856
|
+
c.globalAlpha = vis * 0.5;
|
|
3857
|
+
c.fillStyle = P.dim; roundRect(c, 0, 0, w, h, rr); c.fill();
|
|
3858
|
+
const bw = w * 0.28, bx = -bw + ph * (w + bw);
|
|
3859
|
+
const bx0 = bx < 0 ? 0 : bx, bx1 = bx + bw > w ? w : bx + bw;
|
|
3860
|
+
if (bx1 > bx0) {
|
|
3861
|
+
c.globalAlpha = vis * 0.18;
|
|
3862
|
+
c.fillStyle = P.accent; roundRect(c, bx0, 0, bx1 - bx0, h, rr); c.fill();
|
|
3863
|
+
}
|
|
3864
|
+
c.globalAlpha = 1;
|
|
3865
|
+
},
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3618
3869
|
/**
|
|
3619
3870
|
* The headless-skin registry -- a SIBLING of RECIPES, not part of it (decisions/
|
|
3620
3871
|
* 0008 decision 4). id -> factory (null-prototype), the meta rows a picker/demo
|
|
@@ -3626,6 +3877,13 @@ export const HEADLESS_SKINS = Object.assign(Object.create(null), {
|
|
|
3626
3877
|
sliderSkin: SliderSkin,
|
|
3627
3878
|
progressSkin: ProgressSkin,
|
|
3628
3879
|
ratingSkin: RatingSkin,
|
|
3880
|
+
checkboxSkin: CheckboxSkin,
|
|
3881
|
+
checkboxGroupSkin: CheckboxGroupSkin,
|
|
3882
|
+
selectSkin: SelectSkin,
|
|
3883
|
+
meterSkin: MeterSkin,
|
|
3884
|
+
stepsSkin: StepsSkin,
|
|
3885
|
+
accordionSkin: AccordionSkin,
|
|
3886
|
+
skeletonSkin: SkeletonSkin,
|
|
3629
3887
|
});
|
|
3630
3888
|
|
|
3631
3889
|
export const SKIN_META = [
|
|
@@ -3633,6 +3891,13 @@ export const SKIN_META = [
|
|
|
3633
3891
|
{ id: 'sliderSkin', name: 'Slider Skin', primitive: 'slider', themeable: true, motionSafe: false },
|
|
3634
3892
|
{ id: 'progressSkin', name: 'Progress Skin', primitive: 'progress', themeable: true, motionSafe: false },
|
|
3635
3893
|
{ id: 'ratingSkin', name: 'Rating Skin', primitive: 'rating', themeable: true, motionSafe: false },
|
|
3894
|
+
{ id: 'checkboxSkin', name: 'Checkbox Skin', primitive: 'checkbox', themeable: true, motionSafe: false },
|
|
3895
|
+
{ id: 'checkboxGroupSkin', name: 'Checkbox Group Skin', primitive: 'checkbox-group', themeable: true, motionSafe: false },
|
|
3896
|
+
{ id: 'selectSkin', name: 'Select Skin', primitive: 'select', themeable: true, motionSafe: false },
|
|
3897
|
+
{ id: 'meterSkin', name: 'Meter Skin', primitive: 'meter', themeable: true, motionSafe: false },
|
|
3898
|
+
{ id: 'stepsSkin', name: 'Steps Skin', primitive: 'steps', themeable: true, motionSafe: false },
|
|
3899
|
+
{ id: 'accordionSkin', name: 'Accordion Skin', primitive: 'accordion', themeable: true, motionSafe: false },
|
|
3900
|
+
{ id: 'skeletonSkin', name: 'Skeleton Skin', primitive: 'skeleton', themeable: true, motionSafe: false },
|
|
3636
3901
|
];
|
|
3637
3902
|
|
|
3638
3903
|
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
2
|
> Canvas-hijacked UI components with pluggable recipe system. 57 built-in recipes.
|
|
3
3
|
|
|
4
|
-
VERSION 1.
|
|
4
|
+
VERSION 1.11.0
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
npm i @zakkster/lite-ui-fx
|
|
@@ -96,6 +96,7 @@ driven // U5 host clock: boolean; true = no ticker/RAF, the host calls instan
|
|
|
96
96
|
// identical, one overlay canvas + one MutationObserver removed on destroy; the
|
|
97
97
|
// lite-headless handle is never destroyed (the caller owns it).
|
|
98
98
|
import { skinHeadless, SwitchSkin, SliderSkin, ProgressSkin, RatingSkin } from '@zakkster/lite-ui-fx/headless';
|
|
99
|
+
import { CheckboxSkin, CheckboxGroupSkin, SelectSkin, MeterSkin, StepsSkin, AccordionSkin, SkeletonSkin } from '@zakkster/lite-ui-fx/headless'; // E1b
|
|
99
100
|
import { HEADLESS_SKINS, SKIN_META, SKIN_NAMES } from '@zakkster/lite-ui-fx/headless';
|
|
100
101
|
const skin = skinHeadless(handle, SwitchSkin, { host: switchThumbEl, theme });
|
|
101
102
|
skin.el // the skinned host (unchanged)
|
|
@@ -108,12 +109,19 @@ skin.destroy() // remove overlay + observer + listeners; host + handle untouch
|
|
|
108
109
|
// EVENT time (never per frame) to parse painted state into preallocated slots.
|
|
109
110
|
// options: { host(REQUIRED), padding, seed, colors, theme, text, font, ticker, driven };
|
|
110
111
|
// 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
|
-
//
|
|
112
|
+
// Skins (SKIN_META, a registry SEPARATE from RECIPES -- the 57-recipe count is
|
|
113
|
+
// unchanged). E1: SwitchSkin (data-checked/aria-checked), SliderSkin (aria-
|
|
114
|
+
// valuenow/min/max + data-dragging), ProgressSkin (aria-valuenow/max + data-
|
|
115
|
+
// complete/data-loading), RatingSkin (aria-valuenow/max over N). E1b (0009):
|
|
116
|
+
// CheckboxSkin (tri-state aria-checked true|false|mixed + data-checked/data-
|
|
117
|
+
// indeterminate), CheckboxGroupSkin (same body over a checkbox-group master),
|
|
118
|
+
// SelectSkin (the select TRIGGER: aria-expanded + handle.value(); the portaled
|
|
119
|
+
// listbox open-state is a follow-on), MeterSkin (aria-valuenow/min/max + data-zone),
|
|
120
|
+
// StepsSkin (data-step-count/data-current-index/data-complete), AccordionSkin
|
|
121
|
+
// (aria-expanded/data-open), SkeletonSkin (data-loading/aria-busy shimmer). 11
|
|
122
|
+
// total, each themeable, born zero-alloc + t6-gated. A painted attr is truthy when
|
|
123
|
+
// PRESENT with any value but "false" (handles boolean data-disabled and value
|
|
124
|
+
// data-checked="true").
|
|
117
125
|
|
|
118
126
|
## Host clock (U5) -- three mutually-exclusive modes, both mount modes
|
|
119
127
|
// default (neither option): the shared ref-counted ticker -- one RAF for all
|
|
@@ -250,7 +258,11 @@ See UIFX-RECIPE-GUIDE.md (included in package).
|
|
|
250
258
|
paints a @zakkster/lite-headless primitive by OBSERVING its painted state
|
|
251
259
|
attributes (one MutationObserver; parsed at event time, never per frame) --
|
|
252
260
|
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
|
-
|
|
261
|
+
a dependency (the package imports it nowhere). 11 skins live in a registry
|
|
262
|
+
(HEADLESS_SKINS/SKIN_META) SEPARATE from the 57 recipes: E1 switch/slider/progress/
|
|
263
|
+
rating; E1b (decisions/0009) checkbox (tri-state) + checkbox-group master, select
|
|
264
|
+
(the trigger -- resolving the dropdown/item-6 decision toward a V4 skin; the
|
|
265
|
+
portaled listbox open-state is a follow-on), meter, steps, accordion, skeleton --
|
|
266
|
+
still bespoke (no lite-particles). Each themeable + zero per-frame alloc (t6-gated).
|
|
267
|
+
setValue/setChecked throw (a skin reflects the primitive); destroy leaves the host
|
|
268
|
+
+ 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.11.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",
|