@zakkster/lite-ui-fx 1.9.1 → 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 +90 -0
- package/README.md +32 -2
- package/UIFXController.d.ts +81 -0
- package/UIFXController.js +381 -1
- package/UIFXHeadless.d.ts +30 -0
- package/UIFXHeadless.js +30 -0
- package/UIFXRecipes.d.ts +37 -2
- package/UIFXRecipes.js +477 -9
- package/llms.txt +51 -3
- package/package.json +8 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// UIFXHeadless.d.ts -- types for the ./headless subpath (E1, decisions/0008).
|
|
2
|
+
// Curates the headless-skin surface: skinHeadless (the mount adapter, in the
|
|
3
|
+
// controller) + the skin family and its sibling registry (in the recipes module).
|
|
4
|
+
|
|
5
|
+
export { skinHeadless } from './UIFXController';
|
|
6
|
+
export type {
|
|
7
|
+
SkinOptions,
|
|
8
|
+
SkinInstance,
|
|
9
|
+
HeadlessSkinRecipe,
|
|
10
|
+
HeadlessSkinRecipeFactory,
|
|
11
|
+
HeadlessSkinDescriptor,
|
|
12
|
+
} from './UIFXController';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
SwitchSkin,
|
|
16
|
+
SliderSkin,
|
|
17
|
+
ProgressSkin,
|
|
18
|
+
RatingSkin,
|
|
19
|
+
CheckboxSkin,
|
|
20
|
+
CheckboxGroupSkin,
|
|
21
|
+
SelectSkin,
|
|
22
|
+
MeterSkin,
|
|
23
|
+
StepsSkin,
|
|
24
|
+
AccordionSkin,
|
|
25
|
+
SkeletonSkin,
|
|
26
|
+
HEADLESS_SKINS,
|
|
27
|
+
SKIN_META,
|
|
28
|
+
SKIN_NAMES,
|
|
29
|
+
} from './UIFXRecipes';
|
|
30
|
+
export type { SkinMeta } from './UIFXRecipes';
|
package/UIFXHeadless.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// UIFXHeadless.js -- the ./headless subpath (E1, decisions/0008).
|
|
2
|
+
//
|
|
3
|
+
// Curates the headless-skin surface: skinHeadless (the mount adapter, which lives
|
|
4
|
+
// with the other mounts in UIFXController.js) + the skin family and its sibling
|
|
5
|
+
// registry (in UIFXRecipes.js). Importing one skin tree-shakes the rest
|
|
6
|
+
// (sideEffects:false).
|
|
7
|
+
//
|
|
8
|
+
// @zakkster/lite-headless is a COMPOSE-TARGET, never a dependency: this module (and
|
|
9
|
+
// the whole package) imports it nowhere. skinHeadless couples to a primitive ONLY
|
|
10
|
+
// through the painted-attribute contract (its docs/CSS_CONTRACT.md) that the skin's
|
|
11
|
+
// `headless.read` parses -- so ./headless works against any lite-headless version
|
|
12
|
+
// honouring that contract.
|
|
13
|
+
|
|
14
|
+
export { skinHeadless } from './UIFXController.js';
|
|
15
|
+
export {
|
|
16
|
+
SwitchSkin,
|
|
17
|
+
SliderSkin,
|
|
18
|
+
ProgressSkin,
|
|
19
|
+
RatingSkin,
|
|
20
|
+
CheckboxSkin,
|
|
21
|
+
CheckboxGroupSkin,
|
|
22
|
+
SelectSkin,
|
|
23
|
+
MeterSkin,
|
|
24
|
+
StepsSkin,
|
|
25
|
+
AccordionSkin,
|
|
26
|
+
SkeletonSkin,
|
|
27
|
+
HEADLESS_SKINS,
|
|
28
|
+
SKIN_META,
|
|
29
|
+
SKIN_NAMES,
|
|
30
|
+
} from './UIFXRecipes.js';
|
package/UIFXRecipes.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
UIFXRecipe, UIFXInstance, MountOptions,
|
|
3
3
|
UIFXGroupRecipe, GroupOptions, UIFXGroupInstance, DecorateInstance,
|
|
4
|
+
HeadlessSkinRecipe,
|
|
4
5
|
} from './UIFXController';
|
|
5
6
|
|
|
6
7
|
// ===========================================================
|
|
7
|
-
// RECIPE OPTIONS + FACTORIES (all
|
|
8
|
+
// RECIPE OPTIONS + FACTORIES (all 57)
|
|
8
9
|
// ===========================================================
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -272,7 +273,7 @@ export declare function mountRecipe(
|
|
|
272
273
|
): UIFXInstance | UIFXGroupInstance | DecorateInstance;
|
|
273
274
|
|
|
274
275
|
// ===========================================================
|
|
275
|
-
// DEFAULT EXPORT -- combined all-
|
|
276
|
+
// DEFAULT EXPORT -- combined all-57 namespace
|
|
276
277
|
// ===========================================================
|
|
277
278
|
|
|
278
279
|
declare const UIFXAllRecipes: {
|
|
@@ -335,3 +336,37 @@ declare const UIFXAllRecipes: {
|
|
|
335
336
|
SuccessBloom: typeof SuccessBloom;
|
|
336
337
|
};
|
|
337
338
|
export default UIFXAllRecipes;
|
|
339
|
+
|
|
340
|
+
// ===========================================================
|
|
341
|
+
// HEADLESS SKINS (E1, decisions/0008)
|
|
342
|
+
// A sibling registry of RECIPES/RECIPE_META: skins are driven by skinHeadless
|
|
343
|
+
// (a handle + host), never by mountRecipe (a container), so they are kept
|
|
344
|
+
// separate and the 57-recipe count is unchanged.
|
|
345
|
+
// ===========================================================
|
|
346
|
+
|
|
347
|
+
export declare function SwitchSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
348
|
+
export declare function SliderSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
349
|
+
export declare function ProgressSkin(options?: RecipeOptions): HeadlessSkinRecipe;
|
|
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;
|
|
358
|
+
|
|
359
|
+
/** A headless-skin meta row. `primitive` names the lite-headless primitive the
|
|
360
|
+
* skin is designed to paint. */
|
|
361
|
+
export interface SkinMeta {
|
|
362
|
+
id: string;
|
|
363
|
+
name: string;
|
|
364
|
+
primitive: string;
|
|
365
|
+
themeable: boolean;
|
|
366
|
+
motionSafe: boolean;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** id -> skin factory (null-prototype). Driven by skinHeadless, never mountRecipe. */
|
|
370
|
+
export declare const HEADLESS_SKINS: Record<string, (options?: RecipeOptions) => HeadlessSkinRecipe>;
|
|
371
|
+
export declare const SKIN_META: SkinMeta[];
|
|
372
|
+
export declare const SKIN_NAMES: readonly string[];
|
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);
|
|
@@ -3434,3 +3440,465 @@ export function mountRecipe(container, id, options) {
|
|
|
3434
3440
|
return mountUIFX(container, type, factory, mountOptions);
|
|
3435
3441
|
}
|
|
3436
3442
|
|
|
3443
|
+
// ===========================================================
|
|
3444
|
+
// HEADLESS SKINS (E1, decisions/0008)
|
|
3445
|
+
// A skin is an ordinary recipe PLUS a `headless` descriptor:
|
|
3446
|
+
// { attrs: string[], read(host, handle, state) }
|
|
3447
|
+
// skinHeadless (controller) observes `attrs` on the lite-headless primitive's
|
|
3448
|
+
// painted element and calls read() at EVENT time to parse the painted state into
|
|
3449
|
+
// preallocated state slots. Skins live in their OWN registry (HEADLESS_SKINS /
|
|
3450
|
+
// SKIN_META), NOT RECIPES/RECIPE_META: a skin needs a handle + host, not a
|
|
3451
|
+
// container, so it does not fit mountRecipe. The 57-recipe count is unchanged.
|
|
3452
|
+
// ===========================================================
|
|
3453
|
+
|
|
3454
|
+
// Painted-attribute readers (EVENT time only -- never a per-frame call, so
|
|
3455
|
+
// getAttribute/parseFloat here are off the hot path). A boolean is PRESENT with any
|
|
3456
|
+
// value except the string "false" (handles presence-booleans like data-disabled and
|
|
3457
|
+
// value-booleans like the switch's data-checked="true"; decisions/0008 decision 3).
|
|
3458
|
+
function _skinBool(el, name) {
|
|
3459
|
+
if (!el.hasAttribute(name)) return false;
|
|
3460
|
+
return el.getAttribute(name) !== 'false';
|
|
3461
|
+
}
|
|
3462
|
+
function _ariaTrue(el, name) { return el.getAttribute(name) === 'true'; }
|
|
3463
|
+
function _skinNum(el, name, def) {
|
|
3464
|
+
const v = el.getAttribute(name);
|
|
3465
|
+
if (v == null) return def;
|
|
3466
|
+
const n = parseFloat(v);
|
|
3467
|
+
return n === n ? n : def; // n===n rejects NaN without an isNaN call
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
/** Switch skin -- a sliding track+knob driven by the primitive's data-checked /
|
|
3471
|
+
* aria-checked. Zero per-frame alloc: const colors, globalAlpha, one eased scalar. */
|
|
3472
|
+
export function SwitchSkin(o = {}) {
|
|
3473
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#3a3a4a', dim2: '#e2e2f0' });
|
|
3474
|
+
let t = 0; // animated 0..1 toward the checked state
|
|
3475
|
+
return {
|
|
3476
|
+
headless: {
|
|
3477
|
+
attrs: ['data-checked', 'aria-checked', 'data-disabled', 'aria-disabled'],
|
|
3478
|
+
read(host, handle, st) {
|
|
3479
|
+
st.toggled = _skinBool(host, 'data-checked') || _ariaTrue(host, 'aria-checked')
|
|
3480
|
+
|| (handle && typeof handle.isChecked === 'function' ? !!handle.isChecked() : false);
|
|
3481
|
+
st.disabled = _skinBool(host, 'data-disabled') || _ariaTrue(host, 'aria-disabled');
|
|
3482
|
+
},
|
|
3483
|
+
},
|
|
3484
|
+
tick(c, dt, now, st) {
|
|
3485
|
+
const w = st.w, h = st.h, r = h / 2;
|
|
3486
|
+
const k = dt * 12; t += ((st.toggled ? 1 : 0) - t) * (k > 1 ? 1 : k);
|
|
3487
|
+
c.globalAlpha = st.disabled ? 0.4 : 1;
|
|
3488
|
+
c.fillStyle = st.toggled ? P.accent : P.dim;
|
|
3489
|
+
roundRect(c, 0, 0, w, h, r); c.fill();
|
|
3490
|
+
const kx = r + t * (w - h);
|
|
3491
|
+
c.fillStyle = P.dim2;
|
|
3492
|
+
c.beginPath(); c.arc(kx, r, r - 3, 0, PI2); c.fill();
|
|
3493
|
+
c.globalAlpha = 1;
|
|
3494
|
+
if (st.focused) fr(c, w, h, r);
|
|
3495
|
+
},
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
/** Slider skin -- rail + fill + thumb, driven by aria-valuenow/min/max; the thumb
|
|
3500
|
+
* pulses while data-dragging is painted. */
|
|
3501
|
+
export function SliderSkin(o = {}) {
|
|
3502
|
+
const P = resolveTheme(o, { accent: '#fbbf24', dim: '#9999b8', dim2: '#e2e2f0' });
|
|
3503
|
+
let dv = 0; // displayed value, eased toward st.val
|
|
3504
|
+
return {
|
|
3505
|
+
headless: {
|
|
3506
|
+
attrs: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'data-disabled', 'data-dragging'],
|
|
3507
|
+
read(host, handle, st) {
|
|
3508
|
+
const mn = _skinNum(host, 'aria-valuemin', 0);
|
|
3509
|
+
const mx = _skinNum(host, 'aria-valuemax', 100);
|
|
3510
|
+
const nw = _skinNum(host, 'aria-valuenow', mn);
|
|
3511
|
+
const span = mx - mn;
|
|
3512
|
+
let v = span > 0 ? (nw - mn) / span : 0;
|
|
3513
|
+
st.val = v < 0 ? 0 : (v > 1 ? 1 : v);
|
|
3514
|
+
st.active = _skinBool(host, 'data-dragging');
|
|
3515
|
+
st.disabled = _skinBool(host, 'data-disabled');
|
|
3516
|
+
},
|
|
3517
|
+
},
|
|
3518
|
+
tick(c, dt, now, st) {
|
|
3519
|
+
const w = st.w, cy = st.h / 2;
|
|
3520
|
+
const k = dt * 10; dv += (st.val - dv) * (k > 1 ? 1 : k);
|
|
3521
|
+
c.globalAlpha = st.disabled ? 0.2 : 0.4;
|
|
3522
|
+
c.fillStyle = P.dim; roundRect(c, 0, cy - 2, w, 4, 2); c.fill();
|
|
3523
|
+
const fx = dv * w;
|
|
3524
|
+
c.globalAlpha = st.disabled ? 0.4 : 1;
|
|
3525
|
+
c.fillStyle = P.accent; roundRect(c, 0, cy - 2, fx > 0 ? fx : 0, 4, 2); c.fill();
|
|
3526
|
+
const tr = st.active ? 8 : 6;
|
|
3527
|
+
c.fillStyle = P.dim2;
|
|
3528
|
+
c.beginPath(); c.arc(fx, cy, tr, 0, PI2); c.fill();
|
|
3529
|
+
if (st.active) {
|
|
3530
|
+
c.globalAlpha = 0.3; c.fillStyle = P.accent;
|
|
3531
|
+
c.beginPath(); c.arc(fx, cy, tr + 5, 0, PI2); c.fill();
|
|
3532
|
+
}
|
|
3533
|
+
c.globalAlpha = 1;
|
|
3534
|
+
if (st.focused) fr(c, w, st.h, cy);
|
|
3535
|
+
},
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
/** Progress skin -- a value ring (aria-valuenow/max) with a percent label; an
|
|
3540
|
+
* indeterminate sweep when data-loading, a full accent ring when data-complete. */
|
|
3541
|
+
export function ProgressSkin(o = {}) {
|
|
3542
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#a78bfa', dim: '#e2e2f0' });
|
|
3543
|
+
const FONT = pickFont(o, "700 14px 'JetBrains Mono',monospace");
|
|
3544
|
+
let dv = 0; // displayed value
|
|
3545
|
+
let spin = 0; // indeterminate sweep phase
|
|
3546
|
+
return {
|
|
3547
|
+
headless: {
|
|
3548
|
+
attrs: ['aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'data-complete', 'data-loading'],
|
|
3549
|
+
read(host, handle, st) {
|
|
3550
|
+
const mn = _skinNum(host, 'aria-valuemin', 0);
|
|
3551
|
+
const mx = _skinNum(host, 'aria-valuemax', 100);
|
|
3552
|
+
const nw = _skinNum(host, 'aria-valuenow', mn);
|
|
3553
|
+
const span = mx - mn;
|
|
3554
|
+
let v = span > 0 ? (nw - mn) / span : 0;
|
|
3555
|
+
st.val = v < 0 ? 0 : (v > 1 ? 1 : v);
|
|
3556
|
+
st.complete = _skinBool(host, 'data-complete');
|
|
3557
|
+
st.indeterminate = _skinBool(host, 'data-loading');
|
|
3558
|
+
},
|
|
3559
|
+
},
|
|
3560
|
+
tick(c, dt, now, st) {
|
|
3561
|
+
const cx = st.w / 2, cy = st.h / 2, R = Math.min(cx, cy) - 6, lw = 6;
|
|
3562
|
+
c.strokeStyle = P.dim; c.globalAlpha = 0.15; c.lineWidth = lw;
|
|
3563
|
+
c.beginPath(); c.arc(cx, cy, R, 0, PI2); c.stroke();
|
|
3564
|
+
c.globalAlpha = 1;
|
|
3565
|
+
if (st.indeterminate && !st.complete) {
|
|
3566
|
+
spin += dt * 3;
|
|
3567
|
+
c.strokeStyle = P.accent; c.lineWidth = lw;
|
|
3568
|
+
c.beginPath(); c.arc(cx, cy, R, spin, spin + 1.6); c.stroke();
|
|
3569
|
+
} else {
|
|
3570
|
+
const target = st.complete ? 1 : st.val;
|
|
3571
|
+
const k = dt * 6; dv += (target - dv) * (k > 1 ? 1 : k);
|
|
3572
|
+
const a = -Math.PI / 2, ea = a + dv * PI2;
|
|
3573
|
+
c.strokeStyle = st.complete ? P.accent : P.accent2; c.lineWidth = lw;
|
|
3574
|
+
c.beginPath(); c.arc(cx, cy, R, a, ea); c.stroke();
|
|
3575
|
+
c.fillStyle = P.dim; c.font = FONT; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
3576
|
+
c.fillText(PCT[Math.round(dv * 100)], cx, cy);
|
|
3577
|
+
}
|
|
3578
|
+
if (st.focused) fr(c, st.w, st.h, st.h / 2);
|
|
3579
|
+
},
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
/** Rating skin -- N bubbles (count from aria-valuemax, bounded) filled to
|
|
3584
|
+
* aria-valuenow/max. Fixed-count loop, no per-frame allocation. */
|
|
3585
|
+
export function RatingSkin(o = {}) {
|
|
3586
|
+
const P = resolveTheme(o, { accent: '#f472b6', dim: '#3a3a4a' });
|
|
3587
|
+
let dv = 0; // animated filled count
|
|
3588
|
+
return {
|
|
3589
|
+
headless: {
|
|
3590
|
+
attrs: ['aria-valuenow', 'aria-valuemax', 'data-disabled'],
|
|
3591
|
+
read(host, handle, st) {
|
|
3592
|
+
const mx = _skinNum(host, 'aria-valuemax', 5);
|
|
3593
|
+
const nw = _skinNum(host, 'aria-valuenow', 0);
|
|
3594
|
+
let n = mx > 0 ? (mx | 0) : 5;
|
|
3595
|
+
st.count = n > 10 ? 10 : n;
|
|
3596
|
+
let v = mx > 0 ? nw / mx : 0;
|
|
3597
|
+
st.val = v < 0 ? 0 : (v > 1 ? 1 : v);
|
|
3598
|
+
st.disabled = _skinBool(host, 'data-disabled');
|
|
3599
|
+
},
|
|
3600
|
+
},
|
|
3601
|
+
tick(c, dt, now, st) {
|
|
3602
|
+
const n = st.count > 0 ? st.count : 5;
|
|
3603
|
+
const k = dt * 12; dv += (st.val * n - dv) * (k > 1 ? 1 : k);
|
|
3604
|
+
const cy = st.h / 2;
|
|
3605
|
+
const gap = st.w / n;
|
|
3606
|
+
const rad = (gap < st.h ? gap : st.h) * 0.32;
|
|
3607
|
+
const base = st.disabled ? 0.4 : 1;
|
|
3608
|
+
for (let i = 0; i < n; i++) {
|
|
3609
|
+
const cx = gap * (i + 0.5);
|
|
3610
|
+
const fillAmt = dv - i;
|
|
3611
|
+
c.globalAlpha = base * 0.35; c.fillStyle = P.dim;
|
|
3612
|
+
c.beginPath(); c.arc(cx, cy, rad, 0, PI2); c.fill();
|
|
3613
|
+
if (fillAmt > 0) {
|
|
3614
|
+
c.globalAlpha = base * (fillAmt > 1 ? 1 : fillAmt); c.fillStyle = P.accent;
|
|
3615
|
+
c.beginPath(); c.arc(cx, cy, rad, 0, PI2); c.fill();
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
c.globalAlpha = 1;
|
|
3619
|
+
if (st.focused) fr(c, st.w, st.h, st.h / 2);
|
|
3620
|
+
},
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
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
|
+
|
|
3869
|
+
/**
|
|
3870
|
+
* The headless-skin registry -- a SIBLING of RECIPES, not part of it (decisions/
|
|
3871
|
+
* 0008 decision 4). id -> factory (null-prototype), the meta rows a picker/demo
|
|
3872
|
+
* iterates, and the frozen id list. Each skin is driven by skinHeadless with a
|
|
3873
|
+
* lite-headless handle + host, never by mountRecipe.
|
|
3874
|
+
*/
|
|
3875
|
+
export const HEADLESS_SKINS = Object.assign(Object.create(null), {
|
|
3876
|
+
switchSkin: SwitchSkin,
|
|
3877
|
+
sliderSkin: SliderSkin,
|
|
3878
|
+
progressSkin: ProgressSkin,
|
|
3879
|
+
ratingSkin: RatingSkin,
|
|
3880
|
+
checkboxSkin: CheckboxSkin,
|
|
3881
|
+
checkboxGroupSkin: CheckboxGroupSkin,
|
|
3882
|
+
selectSkin: SelectSkin,
|
|
3883
|
+
meterSkin: MeterSkin,
|
|
3884
|
+
stepsSkin: StepsSkin,
|
|
3885
|
+
accordionSkin: AccordionSkin,
|
|
3886
|
+
skeletonSkin: SkeletonSkin,
|
|
3887
|
+
});
|
|
3888
|
+
|
|
3889
|
+
export const SKIN_META = [
|
|
3890
|
+
{ id: 'switchSkin', name: 'Switch Skin', primitive: 'switch', themeable: true, motionSafe: false },
|
|
3891
|
+
{ id: 'sliderSkin', name: 'Slider Skin', primitive: 'slider', themeable: true, motionSafe: false },
|
|
3892
|
+
{ id: 'progressSkin', name: 'Progress Skin', primitive: 'progress', themeable: true, motionSafe: false },
|
|
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 },
|
|
3901
|
+
];
|
|
3902
|
+
|
|
3903
|
+
export const SKIN_NAMES = Object.freeze(Object.keys(HEADLESS_SKINS));
|
|
3904
|
+
|