@zakkster/lite-ui-fx 1.7.0 → 1.9.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 +99 -0
- package/README.md +323 -212
- package/UIFXController.d.ts +127 -0
- package/UIFXController.js +525 -1
- package/UIFXRecipes.d.ts +33 -13
- package/UIFXRecipes.js +149 -98
- package/llms.txt +61 -5
- package/package.json +1 -1
package/UIFXRecipes.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
UIFXRecipe, UIFXInstance, MountOptions,
|
|
3
|
+
UIFXGroupRecipe, GroupOptions, UIFXGroupInstance, DecorateInstance,
|
|
4
|
+
} from './UIFXController';
|
|
2
5
|
|
|
3
6
|
// ===========================================================
|
|
4
7
|
// RECIPE OPTIONS + FACTORIES (all 56)
|
|
@@ -81,7 +84,8 @@ export declare function FlameCounter(options?: RecipeOptions): UIFXRecipe;
|
|
|
81
84
|
export declare function GlitchCounter(options?: RecipeOptions): UIFXRecipe;
|
|
82
85
|
|
|
83
86
|
// -- Vol.2: Rating --
|
|
84
|
-
|
|
87
|
+
/** U7 GROUP (rating): mount via mountUIFXGroup(GroupType.RATING, ...). */
|
|
88
|
+
export declare function BubbleRating(options?: RecipeOptions): UIFXGroupRecipe;
|
|
85
89
|
|
|
86
90
|
// -- Vol.3: Knobs --
|
|
87
91
|
export declare function VolumeKnob(options?: RecipeOptions): UIFXRecipe;
|
|
@@ -95,9 +99,13 @@ export declare function SignalMeter(options?: RecipeOptions): UIFXRecipe;
|
|
|
95
99
|
export declare function LiquidFill(options?: RecipeOptions): UIFXRecipe;
|
|
96
100
|
|
|
97
101
|
// -- Vol.3: Controls --
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
// -- U7 GROUP recipes (mounted via mountUIFXGroup): N native elements + one
|
|
103
|
+
// canvas. PillTabs/SegmentedSlide -> GroupType.TABS, Stepper -> STEPPER,
|
|
104
|
+
// RadioOrbit -> RADIO. See decisions/0007. --
|
|
105
|
+
export declare function PillTabs(options?: RecipeOptions): UIFXGroupRecipe;
|
|
106
|
+
export declare function SegmentedSlide(options?: RecipeOptions): UIFXGroupRecipe;
|
|
107
|
+
export declare function Stepper(options?: RecipeOptions): UIFXGroupRecipe;
|
|
108
|
+
export declare function RadioOrbit(options?: RecipeOptions): UIFXGroupRecipe;
|
|
101
109
|
|
|
102
110
|
// -- Vol.3: Indicators --
|
|
103
111
|
export declare function PasswordStrength(options?: RecipeOptions): UIFXRecipe;
|
|
@@ -203,14 +211,24 @@ export declare const UIFXRecipes5: {
|
|
|
203
211
|
SuccessBloom: typeof SuccessBloom;
|
|
204
212
|
};
|
|
205
213
|
|
|
214
|
+
/** U7 additions -- grouped controls (mounted via mountUIFXGroup). PillTabs/Stepper/
|
|
215
|
+
* RadioOrbit/BubbleRating re-home from vol.3 single-element fakes; SegmentedSlide
|
|
216
|
+
* is new. See decisions/0007. */
|
|
217
|
+
export declare const UIFXRecipes6: {
|
|
218
|
+
SegmentedSlide: typeof SegmentedSlide;
|
|
219
|
+
};
|
|
220
|
+
|
|
206
221
|
// ===========================================================
|
|
207
222
|
// RECIPE REGISTRY
|
|
208
223
|
// ===========================================================
|
|
209
224
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
export type RecipeType =
|
|
225
|
+
// Beyond the UITypes there are two non-UIType routing tags: 'decorate' (U4b,
|
|
226
|
+
// mounted AROUND a live element via decorateUIFX -- 0004) and the four GroupTypes
|
|
227
|
+
// (U7, mounted as N native elements + one canvas via mountUIFXGroup -- 0007).
|
|
228
|
+
export type RecipeType =
|
|
229
|
+
| 'toggle' | 'button' | 'slider' | 'checkbox' | 'progress' | 'knob'
|
|
230
|
+
| 'decorate'
|
|
231
|
+
| 'radio' | 'tabs' | 'stepper' | 'rating';
|
|
214
232
|
|
|
215
233
|
export type RecipeFactory = (options?: Record<string, unknown>) => UIFXRecipe;
|
|
216
234
|
|
|
@@ -243,14 +261,15 @@ export declare function registerRecipe(
|
|
|
243
261
|
* Resolve a recipe id to its factory + declared type and mount it. A hijack
|
|
244
262
|
* recipe mounts via mountUIFX (the native element is created inside `container`);
|
|
245
263
|
* a recipe whose meta.type is 'decorate' mounts via decorateUIFX, treating
|
|
246
|
-
* `container` as the LIVE element to decorate
|
|
247
|
-
*
|
|
264
|
+
* `container` as the LIVE element to decorate; a GROUP type (radio/tabs/stepper/
|
|
265
|
+
* rating) mounts via mountUIFXGroup with `container` as the parent and `items` in
|
|
266
|
+
* options. Fail closed: unknown id or a conflicting options.type throws.
|
|
248
267
|
*/
|
|
249
268
|
export declare function mountRecipe(
|
|
250
269
|
container: HTMLElement,
|
|
251
270
|
id: string,
|
|
252
|
-
options?: MountOptions & { type?: RecipeType },
|
|
253
|
-
): UIFXInstance;
|
|
271
|
+
options?: (MountOptions | GroupOptions) & { type?: RecipeType },
|
|
272
|
+
): UIFXInstance | UIFXGroupInstance | DecorateInstance;
|
|
254
273
|
|
|
255
274
|
// ===========================================================
|
|
256
275
|
// DEFAULT EXPORT -- combined all-56 namespace
|
|
@@ -296,6 +315,7 @@ declare const UIFXAllRecipes: {
|
|
|
296
315
|
SignalMeter: typeof SignalMeter;
|
|
297
316
|
LiquidFill: typeof LiquidFill;
|
|
298
317
|
PillTabs: typeof PillTabs;
|
|
318
|
+
SegmentedSlide: typeof SegmentedSlide;
|
|
299
319
|
Stepper: typeof Stepper;
|
|
300
320
|
RadioOrbit: typeof RadioOrbit;
|
|
301
321
|
PasswordStrength: typeof PasswordStrength;
|
package/UIFXRecipes.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import { lerp, clamp, easeOut, easeIn, easeInOut } from '@zakkster/lite-lerp';
|
|
37
37
|
import { Random } from '@zakkster/lite-random';
|
|
38
|
-
import { mountUIFX, decorateUIFX, UIType } from './UIFXController.js';
|
|
38
|
+
import { mountUIFX, decorateUIFX, mountUIFXGroup, UIType, GroupType } from './UIFXController.js';
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
// ---------------------------------------------------------
|
|
@@ -1800,51 +1800,57 @@ export function GlitchCounter(o = {}) {
|
|
|
1800
1800
|
|
|
1801
1801
|
/** 20. Bubble Rating -- 5 bubbles inflate based on slider position. Click pops them. */
|
|
1802
1802
|
export function BubbleRating(o = {}) {
|
|
1803
|
+
// U7 GROUP (rating): N native radios (a rating is "pick 1 of N stars"); native
|
|
1804
|
+
// arrow keys move the selection (st.index), 0007. Bubbles fill to st.index+1;
|
|
1805
|
+
// onSelect pops the newly-chosen bubble. Fixed pool + preallocated sizes lane.
|
|
1803
1806
|
const { seed = 42 } = o;
|
|
1804
1807
|
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#9999b8' });
|
|
1805
1808
|
const rng = new Random(seed);
|
|
1806
|
-
const R5 = ['0 / 5', '1 / 5', '2 / 5', '3 / 5', '4 / 5', '5 / 5']; // const labels
|
|
1807
1809
|
const POPS = 32;
|
|
1808
1810
|
const pop = [];
|
|
1809
1811
|
for (let i = 0; i < POPS; i++) pop[i] = { x: 0, y: 0, vx: 0, vy: 0, life: 0 };
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1812
|
+
// per-bubble size lane, sized from st.count on first tick (the mount owns count,
|
|
1813
|
+
// 0007) -- a one-time grow absorbed by warm-up, then zero-alloc forever.
|
|
1814
|
+
let sizes = new Float32Array((o.items && o.items.length) || 5);
|
|
1815
|
+
function spawnPops(cx, cy) {
|
|
1813
1816
|
for (let j = 0; j < 5; j++) {
|
|
1814
1817
|
for (let i = 0; i < POPS; i++) {
|
|
1815
1818
|
const p = pop[i];
|
|
1816
1819
|
if (p.life <= 0) {
|
|
1817
|
-
const a = rng.range(0,
|
|
1818
|
-
p.x = cx; p.y =
|
|
1820
|
+
const a = rng.range(0, PI2), v = rng.range(20, 50);
|
|
1821
|
+
p.x = cx; p.y = cy; p.vx = Math.cos(a) * v; p.vy = Math.sin(a) * v; p.life = 1;
|
|
1819
1822
|
break;
|
|
1820
1823
|
}
|
|
1821
1824
|
}
|
|
1822
1825
|
}
|
|
1823
1826
|
}
|
|
1824
1827
|
return {
|
|
1828
|
+
onSelect(i, st) {
|
|
1829
|
+
// Pop the newly selected bubble at its real centre (geometry lane).
|
|
1830
|
+
spawnPops(st.itemX[i] + st.itemW[i] / 2, st.h / 2);
|
|
1831
|
+
},
|
|
1825
1832
|
tick(ctx, dt, now, st) {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const gap = st.w / 5;
|
|
1833
|
+
const filled = st.index + 1; // index 0 -> 1 filled bubble
|
|
1834
|
+
if (sizes.length < st.count) sizes = new Float32Array(st.count); // one-time grow (cold)
|
|
1829
1835
|
|
|
1830
|
-
for (let i = 0; i <
|
|
1831
|
-
const active = i <
|
|
1836
|
+
for (let i = 0; i < st.count; i++) {
|
|
1837
|
+
const active = i < filled;
|
|
1832
1838
|
const targetSz = active ? 12 : 6;
|
|
1833
1839
|
sizes[i] = lerp(sizes[i] || 6, targetSz, dt * 8);
|
|
1834
|
-
const cx =
|
|
1840
|
+
const cx = st.itemX[i] + st.itemW[i] / 2, cy = st.h / 2;
|
|
1835
1841
|
|
|
1836
1842
|
// Bubble -- const color, active alpha via globalAlpha
|
|
1837
1843
|
if (active) { ctx.fillStyle = P.accent; ctx.globalAlpha = 0.3 + sizes[i] / 20; }
|
|
1838
1844
|
else { ctx.fillStyle = 'rgba(255,255,255,.05)'; ctx.globalAlpha = 1; }
|
|
1839
|
-
ctx.beginPath(); ctx.arc(cx, cy, sizes[i], 0,
|
|
1845
|
+
ctx.beginPath(); ctx.arc(cx, cy, sizes[i], 0, PI2); ctx.fill();
|
|
1840
1846
|
ctx.globalAlpha = 1;
|
|
1841
1847
|
ctx.strokeStyle = active ? P.accent : 'rgba(255,255,255,.08)';
|
|
1842
|
-
ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(cx, cy, sizes[i], 0,
|
|
1848
|
+
ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(cx, cy, sizes[i], 0, PI2); ctx.stroke();
|
|
1843
1849
|
|
|
1844
1850
|
// Highlight
|
|
1845
1851
|
if (active) {
|
|
1846
1852
|
ctx.fillStyle = 'rgba(255,255,255,.15)';
|
|
1847
|
-
ctx.beginPath(); ctx.arc(cx - 3, cy - 3, 3, 0,
|
|
1853
|
+
ctx.beginPath(); ctx.arc(cx - 3, cy - 3, 3, 0, PI2); ctx.fill();
|
|
1848
1854
|
}
|
|
1849
1855
|
}
|
|
1850
1856
|
|
|
@@ -1856,20 +1862,13 @@ export function BubbleRating(o = {}) {
|
|
|
1856
1862
|
p.x += p.vx * dt; p.y += p.vy * dt; p.life -= 3 * dt;
|
|
1857
1863
|
if (p.life <= 0) continue;
|
|
1858
1864
|
ctx.globalAlpha = p.life;
|
|
1859
|
-
ctx.beginPath(); ctx.arc(p.x, p.y, 1.5, 0,
|
|
1865
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, 1.5, 0, PI2); ctx.fill();
|
|
1860
1866
|
}
|
|
1861
1867
|
ctx.globalAlpha = 1;
|
|
1862
1868
|
|
|
1863
|
-
label(ctx,
|
|
1869
|
+
label(ctx, st.labels[st.index], st.w / 2, st.h + 12, filled >= st.count ? P.accent : P.dim);
|
|
1864
1870
|
if (st.focused) focusRing(ctx, st.w, st.h, 4);
|
|
1865
1871
|
},
|
|
1866
|
-
onDrag(val) {
|
|
1867
|
-
const rating = Math.round(val * 5);
|
|
1868
|
-
// Pop the newly activated bubble (position derived from st.w via lastW)
|
|
1869
|
-
const gap = lastW / 5;
|
|
1870
|
-
const cx = gap * (rating - 1) + gap / 2;
|
|
1871
|
-
spawnPops(cx);
|
|
1872
|
-
},
|
|
1873
1872
|
};
|
|
1874
1873
|
}
|
|
1875
1874
|
|
|
@@ -2180,122 +2179,154 @@ export function LiquidFill(o = {}) {
|
|
|
2180
2179
|
|
|
2181
2180
|
/** 6. Pill Tabs -- 3 segmented tabs with sliding indicator. */
|
|
2182
2181
|
export function PillTabs(o = {}) {
|
|
2182
|
+
// U7 GROUP (tabs): N native role=tab buttons, one canvas. The recipe reads the
|
|
2183
|
+
// live selection (st.index) and the per-item geometry lanes (st.itemX/itemW)
|
|
2184
|
+
// -- selection + arrow-key roving are owned by the native tablist (0007), not
|
|
2185
|
+
// faked here. The indicator springs toward the selected tab's real box.
|
|
2183
2186
|
const P = resolveTheme(o, { accent: '#c4b5fd', accent2: '#a78bfa', dim: '#9999b8' });
|
|
2184
2187
|
const themed = !!(o.theme || o.colors);
|
|
2185
2188
|
const indFill = themed ? rgbaOf(P.accent2, .12) : 'rgba(167,139,250,.12)';
|
|
2186
2189
|
const indStroke = themed ? rgbaOf(P.accent2, .25) : 'rgba(167,139,250,.25)';
|
|
2187
2190
|
const FONT = pickFont(o, "600 11px 'Space Grotesk',sans-serif");
|
|
2188
|
-
let
|
|
2189
|
-
const labels=['Alpha','Beta','Gamma'];
|
|
2191
|
+
let indX = -1, indW = 0; // indX < 0 snaps to the selected tab on frame 1
|
|
2190
2192
|
return {
|
|
2191
|
-
onClick(x,y,st) {
|
|
2192
|
-
const idx=Math.floor(x/(st.w/3));
|
|
2193
|
-
selected=clamp(idx,0,2);
|
|
2194
|
-
},
|
|
2195
2193
|
tick(c,dt,now,st) {
|
|
2196
|
-
const
|
|
2197
|
-
const tx=
|
|
2198
|
-
|
|
2199
|
-
|
|
2194
|
+
const sel = st.index;
|
|
2195
|
+
const tx = st.itemX[sel], tw = st.itemW[sel];
|
|
2196
|
+
if (indX < 0) { indX = tx; indW = tw; }
|
|
2197
|
+
else { indX = lerp(indX, tx, dt * 12); indW = lerp(indW, tw, dt * 12); }
|
|
2200
2198
|
|
|
2201
2199
|
// Background
|
|
2202
2200
|
c.fillStyle='rgba(255,255,255,.03)';rr(c,0,0,st.w,st.h,st.h/2);c.fill();
|
|
2203
2201
|
|
|
2204
|
-
// Indicator
|
|
2202
|
+
// Indicator (springs to the real selected-tab box)
|
|
2205
2203
|
c.fillStyle=indFill;c.strokeStyle=indStroke;c.lineWidth=1;
|
|
2206
|
-
rr(c,
|
|
2204
|
+
rr(c,indX+2,2,indW-4,st.h-4,st.h/2-2);c.fill();c.stroke();
|
|
2207
2205
|
|
|
2208
|
-
// Labels
|
|
2206
|
+
// Labels (from the native tabs' own text, via st.labels)
|
|
2209
2207
|
c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
2210
|
-
for(let i=0;i<
|
|
2211
|
-
c.fillStyle=i===
|
|
2212
|
-
c.fillText(labels[i],
|
|
2208
|
+
for(let i=0;i<st.count;i++){
|
|
2209
|
+
c.fillStyle=i===sel?P.accent:(i===st.hoverIndex?'#c4c4dc':P.dim);
|
|
2210
|
+
c.fillText(st.labels[i],st.itemX[i]+st.itemW[i]/2,st.h/2);
|
|
2213
2211
|
}
|
|
2214
2212
|
if(st.focused)fr(c,st.w,st.h,st.h/2);
|
|
2215
2213
|
},
|
|
2216
2214
|
};
|
|
2217
2215
|
}
|
|
2218
2216
|
|
|
2219
|
-
/**
|
|
2220
|
-
|
|
2221
|
-
|
|
2217
|
+
/** 6b. Segmented Slide (U7 GROUP tabs) -- a solid pill slides under the selected
|
|
2218
|
+
* segment. New in U7; reads st.index + the geometry lanes, zero-alloc tick. */
|
|
2219
|
+
export function SegmentedSlide(o = {}) {
|
|
2220
|
+
const P = resolveTheme(o, { accent: '#38bdf8', accent2: '#0ea5e9', dim: '#9999b8' });
|
|
2222
2221
|
const themed = !!(o.theme || o.colors);
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
let count=0,displayCount=0,flashDir=0,flashTimer=0;
|
|
2222
|
+
const pill = themed ? rgbaOf(P.accent, .92) : 'rgba(56,189,248,.92)';
|
|
2223
|
+
const FONT = pickFont(o, "600 11px 'Space Grotesk',sans-serif");
|
|
2224
|
+
let sx = -1, sw = 0;
|
|
2227
2225
|
return {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2226
|
+
tick(c,dt,now,st) {
|
|
2227
|
+
const sel = st.index;
|
|
2228
|
+
const tx = st.itemX[sel], tw = st.itemW[sel];
|
|
2229
|
+
if (sx < 0) { sx = tx; sw = tw; }
|
|
2230
|
+
else { sx = lerp(sx, tx, dt * 14); sw = lerp(sw, tw, dt * 14); }
|
|
2231
|
+
|
|
2232
|
+
c.fillStyle='rgba(255,255,255,.05)';rr(c,0,0,st.w,st.h,st.h/2);c.fill();
|
|
2233
|
+
c.fillStyle=pill;rr(c,sx+3,3,sw-6,st.h-6,(st.h-6)/2);c.fill();
|
|
2234
|
+
|
|
2235
|
+
c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
2236
|
+
for(let i=0;i<st.count;i++){
|
|
2237
|
+
c.fillStyle=i===sel?'#0b0b12':(i===st.hoverIndex?'#e2e2f0':P.dim);
|
|
2238
|
+
c.fillText(st.labels[i],st.itemX[i]+st.itemW[i]/2,st.h/2);
|
|
2239
|
+
}
|
|
2240
|
+
if(st.focused)fr(c,st.w,st.h,st.h/2);
|
|
2232
2241
|
},
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/** 7. Stepper (U7 GROUP: stepper) -- one native <input type=number> spinbutton;
|
|
2246
|
+
* ArrowUp/Down + typing drive st.index natively (0007). The recipe draws the
|
|
2247
|
+
* current step label + a pip row (one per step, filled to the selection) and
|
|
2248
|
+
* flashes on change via onSelect. Zero-alloc tick. */
|
|
2249
|
+
export function Stepper(o = {}) {
|
|
2250
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#ff6b6b', dim: '#e2e2f0' });
|
|
2251
|
+
const FONT = pickFont(o, "700 20px 'JetBrains Mono',monospace");
|
|
2252
|
+
let flash = 0; // brief highlight after a step change (onSelect)
|
|
2253
|
+
return {
|
|
2254
|
+
onSelect() { flash = 0.35; },
|
|
2233
2255
|
tick(c,dt,now,st) {
|
|
2234
|
-
|
|
2235
|
-
flashTimer=Math.max(0,flashTimer-dt);
|
|
2256
|
+
flash = flash > 0 ? flash - dt : 0;
|
|
2236
2257
|
|
|
2237
2258
|
// Background
|
|
2238
2259
|
c.fillStyle='rgba(255,255,255,.04)';rr(c,0,0,st.w,st.h,10);c.fill();
|
|
2239
2260
|
|
|
2240
|
-
//
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
c.
|
|
2245
|
-
c.
|
|
2261
|
+
// Up/Down chevron affordances (native spinbutton owns the behaviour)
|
|
2262
|
+
c.strokeStyle=P.accent;c.lineWidth=2;c.globalAlpha=0.7;
|
|
2263
|
+
const rx=st.w-14;
|
|
2264
|
+
c.beginPath();c.moveTo(rx-4,12);c.lineTo(rx,8);c.lineTo(rx+4,12);c.stroke();
|
|
2265
|
+
c.strokeStyle=P.accent2;
|
|
2266
|
+
c.beginPath();c.moveTo(rx-4,st.h-12);c.lineTo(rx,st.h-8);c.lineTo(rx+4,st.h-12);c.stroke();
|
|
2267
|
+
c.globalAlpha=1;
|
|
2246
2268
|
|
|
2247
|
-
//
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2269
|
+
// Step pips (one per step, filled up to the current index)
|
|
2270
|
+
for(let i=0;i<st.count;i++){
|
|
2271
|
+
const cx=st.itemX[i]+st.itemW[i]/2;
|
|
2272
|
+
c.globalAlpha=i===st.index?1:0.3;
|
|
2273
|
+
c.fillStyle=i<=st.index?P.accent:'rgba(255,255,255,.25)';
|
|
2274
|
+
c.beginPath();c.arc(cx,st.h-7,2.5,0,PI2);c.fill();
|
|
2275
|
+
}
|
|
2276
|
+
c.globalAlpha=1;
|
|
2251
2277
|
|
|
2252
|
-
//
|
|
2253
|
-
c.fillStyle=P.dim;c.font=
|
|
2254
|
-
c.
|
|
2278
|
+
// Current step label, brightening on change
|
|
2279
|
+
c.fillStyle=flash>0?P.accent:P.dim;c.font=FONT;
|
|
2280
|
+
c.textAlign='center';c.textBaseline='middle';
|
|
2281
|
+
c.fillText(st.labels[st.index],st.w/2,st.h/2-3);
|
|
2255
2282
|
|
|
2256
2283
|
if(st.focused)fr(c,st.w,st.h,10);
|
|
2257
2284
|
},
|
|
2258
2285
|
};
|
|
2259
2286
|
}
|
|
2260
2287
|
|
|
2261
|
-
/** 8. Radio Orbit --
|
|
2288
|
+
/** 8. Radio Orbit (U7 GROUP: radio) -- N options in a circle over N native
|
|
2289
|
+
* radios; native arrow keys move the selection (st.index), 0007. The recipe
|
|
2290
|
+
* reads st.count/st.index/st.labels; per-node glow is a preallocated lane. */
|
|
2262
2291
|
export function RadioOrbit(o = {}) {
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
//
|
|
2292
|
+
// per-node glow lane, sized from st.count on first tick (the mount owns count,
|
|
2293
|
+
// 0007) -- a one-time grow absorbed by warm-up, then zero-alloc forever.
|
|
2294
|
+
let glow = new Float32Array((o.items && o.items.length) || 4);
|
|
2295
|
+
// palette: `colors` is it; `theme` seeds it; else the default. Indexed base[i%bn]
|
|
2296
|
+
// so a node colour stays valid for any node count / override (no fixed array).
|
|
2267
2297
|
const base = o.colors ? o.colors
|
|
2268
2298
|
: (o.theme ? [o.theme.light, o.theme.mid, o.theme.dark, o.theme.light] : ['#ff6b6b','#fbbf24','#6ee7b6','#38bdf8']);
|
|
2269
|
-
const
|
|
2270
|
-
const OPTS=['OPTION A','OPTION B','OPTION C','OPTION D']; // const (was 'OPTION '+name concat)
|
|
2299
|
+
const bn = base.length;
|
|
2271
2300
|
return {
|
|
2272
2301
|
tick(c,dt,now,st) {
|
|
2273
|
-
const sel=
|
|
2302
|
+
const sel=st.index, n=st.count;
|
|
2303
|
+
if (glow.length < n) glow = new Float32Array(n); // one-time grow (cold)
|
|
2274
2304
|
const cx=st.w/2,cy=st.h/2,R=Math.min(cx,cy)-12;
|
|
2275
2305
|
|
|
2276
2306
|
// Center
|
|
2277
2307
|
c.fillStyle='rgba(255,255,255,.03)';c.beginPath();c.arc(cx,cy,8,0,PI2);c.fill();
|
|
2278
2308
|
|
|
2279
|
-
for(let i=0;i<
|
|
2280
|
-
const a=-Math.PI/2+i*
|
|
2309
|
+
for(let i=0;i<n;i++){
|
|
2310
|
+
const a=-Math.PI/2+i*(PI2/n);
|
|
2281
2311
|
const ox=cx+Math.cos(a)*R, oy=cy+Math.sin(a)*R;
|
|
2282
2312
|
const active=i===sel;
|
|
2283
|
-
|
|
2313
|
+
const col=base[i%bn];
|
|
2314
|
+
glow[i]=lerp(glow[i],active?1:(i===st.hoverIndex?0.4:0),dt*10);
|
|
2284
2315
|
|
|
2285
2316
|
// Orbit line -- const white, alpha via globalAlpha
|
|
2286
|
-
c.strokeStyle='#ffffff';c.globalAlpha=.03+
|
|
2317
|
+
c.strokeStyle='#ffffff';c.globalAlpha=.03+glow[i]*.05;c.lineWidth=1;
|
|
2287
2318
|
c.beginPath();c.moveTo(cx,cy);c.lineTo(ox,oy);c.stroke();c.globalAlpha=1;
|
|
2288
2319
|
|
|
2289
2320
|
// Node
|
|
2290
|
-
const sz=6+
|
|
2291
|
-
c.fillStyle=active?
|
|
2321
|
+
const sz=6+glow[i]*4;
|
|
2322
|
+
c.fillStyle=active?col:'rgba(255,255,255,.06)';
|
|
2292
2323
|
c.beginPath();c.arc(ox,oy,sz,0,PI2);c.fill();
|
|
2293
|
-
if(active){c.strokeStyle=
|
|
2324
|
+
if(active){c.strokeStyle=col;c.lineWidth=1;c.beginPath();c.arc(ox,oy,sz+3,0,PI2);c.stroke()}
|
|
2294
2325
|
|
|
2295
2326
|
c.fillStyle=active?'#fff':'#9999b8';c.font="600 9px 'JetBrains Mono',monospace";c.textAlign='center';c.textBaseline='middle';
|
|
2296
|
-
c.fillText(
|
|
2327
|
+
c.fillText(st.labels[i],ox,oy);
|
|
2297
2328
|
}
|
|
2298
|
-
lbl(c,
|
|
2329
|
+
lbl(c,st.labels[sel],cx,st.h+10,base[sel%bn]);
|
|
2299
2330
|
if(st.focused)fr(c,st.w,st.h,4);
|
|
2300
2331
|
},
|
|
2301
2332
|
};
|
|
@@ -3021,6 +3052,14 @@ export const UIFXRecipes5 = {
|
|
|
3021
3052
|
FocusHalo, ErrorShake, SuccessBloom,
|
|
3022
3053
|
};
|
|
3023
3054
|
|
|
3055
|
+
// U7 additions -- grouped controls (N native elements + one canvas, mounted via
|
|
3056
|
+
// mountUIFXGroup). PillTabs/Stepper/RadioOrbit/BubbleRating re-homed from their
|
|
3057
|
+
// vol.3 single-element fakes to real group types (see RECIPE_META); SegmentedSlide
|
|
3058
|
+
// is new. Reachable via RECIPES / RECIPE_META and their named exports regardless.
|
|
3059
|
+
export const UIFXRecipes6 = {
|
|
3060
|
+
SegmentedSlide,
|
|
3061
|
+
};
|
|
3062
|
+
|
|
3024
3063
|
|
|
3025
3064
|
// ===========================================================
|
|
3026
3065
|
// DEFAULT EXPORT -- combined all-53 namespace
|
|
@@ -3066,6 +3105,7 @@ export default {
|
|
|
3066
3105
|
SignalMeter,
|
|
3067
3106
|
LiquidFill,
|
|
3068
3107
|
PillTabs,
|
|
3108
|
+
SegmentedSlide,
|
|
3069
3109
|
Stepper,
|
|
3070
3110
|
RadioOrbit,
|
|
3071
3111
|
PasswordStrength,
|
|
@@ -3135,6 +3175,7 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
3135
3175
|
signalMeter: SignalMeter,
|
|
3136
3176
|
liquidFill: LiquidFill,
|
|
3137
3177
|
pillTabs: PillTabs,
|
|
3178
|
+
segmentedSlide: SegmentedSlide,
|
|
3138
3179
|
stepper: Stepper,
|
|
3139
3180
|
radioOrbit: RadioOrbit,
|
|
3140
3181
|
passwordStrength: PasswordStrength,
|
|
@@ -3198,16 +3239,17 @@ export const RECIPE_META = [
|
|
|
3198
3239
|
{ id: 'indeterminateScan', name: 'Indeterminate Scan', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3199
3240
|
{ id: 'flameCounter', name: 'Flame Counter', type: 'slider', family: 'Counters', themeable: true, motionSafe: false },
|
|
3200
3241
|
{ id: 'glitchCounter', name: 'Glitch Counter', type: 'slider', family: 'Counters', themeable: true, motionSafe: false },
|
|
3201
|
-
{ id: 'bubbleRating', name: 'Bubble Rating', type: '
|
|
3242
|
+
{ id: 'bubbleRating', name: 'Bubble Rating', type: 'rating', family: 'Rating', themeable: true, motionSafe: false },
|
|
3202
3243
|
{ id: 'volumeKnob', name: 'Volume Knob', type: 'knob', family: 'Knobs', themeable: true, motionSafe: false },
|
|
3203
3244
|
{ id: 'compassKnob', name: 'Compass Knob', type: 'knob', family: 'Knobs', themeable: true, motionSafe: false },
|
|
3204
3245
|
{ id: 'ringProgress', name: 'Ring Progress', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3205
3246
|
{ id: 'batteryGauge', name: 'Battery Gauge', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3206
3247
|
{ id: 'signalMeter', name: 'Signal Meter', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3207
3248
|
{ id: 'liquidFill', name: 'Liquid Fill', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3208
|
-
{ id: 'pillTabs', name: 'Pill Tabs', type: '
|
|
3209
|
-
{ id: '
|
|
3210
|
-
{ id: '
|
|
3249
|
+
{ id: 'pillTabs', name: 'Pill Tabs', type: 'tabs', family: 'Controls', themeable: true, motionSafe: false },
|
|
3250
|
+
{ id: 'segmentedSlide', name: 'Segmented Slide', type: 'tabs', family: 'Controls', themeable: true, motionSafe: false },
|
|
3251
|
+
{ id: 'stepper', name: 'Stepper', type: 'stepper', family: 'Controls', themeable: true, motionSafe: false },
|
|
3252
|
+
{ id: 'radioOrbit', name: 'Radio Orbit', type: 'radio', family: 'Controls', themeable: true, motionSafe: false },
|
|
3211
3253
|
{ id: 'passwordStrength', name: 'Password Strength', type: 'decorate', family: 'Indicators', themeable: true, motionSafe: true },
|
|
3212
3254
|
{ id: 'waterLevel', name: 'Water Level', type: 'slider', family: 'Indicators', themeable: true, motionSafe: false },
|
|
3213
3255
|
{ id: 'heatMap', name: 'Heat Map', type: 'slider', family: 'Indicators', themeable: true, motionSafe: false },
|
|
@@ -3228,14 +3270,15 @@ export const RECIPE_META = [
|
|
|
3228
3270
|
/** Names of every built-in recipe (the keys of RECIPES at load time). */
|
|
3229
3271
|
export const RECIPE_NAMES = Object.freeze(Object.keys(RECIPES));
|
|
3230
3272
|
|
|
3231
|
-
// The valid recipe/mount types, taken from the controller's UIType so
|
|
3232
|
-
// registry's fail-closed check and the controller's mount
|
|
3233
|
-
// of truth (they cannot drift as U4
|
|
3234
|
-
//
|
|
3235
|
-
//
|
|
3236
|
-
//
|
|
3237
|
-
// mountRecipe routes
|
|
3238
|
-
const
|
|
3273
|
+
// The valid recipe/mount types, taken from the controller's UIType + GroupType so
|
|
3274
|
+
// the registry's fail-closed check and the controller's mount guards are one
|
|
3275
|
+
// source of truth (they cannot drift as U4/U7 add types). Built once at load
|
|
3276
|
+
// (cold). Beyond the UITypes there are two non-UIType routing tags: 'decorate'
|
|
3277
|
+
// (U4b, mounted AROUND a live element by decorateUIFX) and the four GroupTypes
|
|
3278
|
+
// (U7: radio/tabs/stepper/rating, mounted as N native elements + one canvas by
|
|
3279
|
+
// mountUIFXGroup). mountRecipe routes each family to the right mount (see below).
|
|
3280
|
+
const _GROUP_TYPES = new Set(Object.values(GroupType));
|
|
3281
|
+
const VALID_META_TYPES = new Set([...Object.values(UIType), 'decorate', ..._GROUP_TYPES]);
|
|
3239
3282
|
|
|
3240
3283
|
/**
|
|
3241
3284
|
* Register a custom recipe, or override a built-in. Instantly usable via
|
|
@@ -3265,7 +3308,7 @@ export function registerRecipe(id, factory, meta) {
|
|
|
3265
3308
|
// valid set is UIType (VALID_META_TYPES), so registry + controller never
|
|
3266
3309
|
// disagree about what a type is.
|
|
3267
3310
|
if (!VALID_META_TYPES.has(type)) {
|
|
3268
|
-
throw new TypeError('registerRecipe: type must be
|
|
3311
|
+
throw new TypeError('registerRecipe: type must be a UIType ("button", "toggle", "slider", "checkbox", "progress", "knob"), "decorate", or a GroupType ("radio", "tabs", "stepper", "rating")');
|
|
3269
3312
|
}
|
|
3270
3313
|
|
|
3271
3314
|
RECIPES[id] = factory;
|
|
@@ -3367,10 +3410,18 @@ export function mountRecipe(container, id, options) {
|
|
|
3367
3410
|
mountOptions = {};
|
|
3368
3411
|
for (const k in options) if (k !== 'type') mountOptions[k] = options[k];
|
|
3369
3412
|
}
|
|
3413
|
+
// A grouped control (U7) is N native elements + one canvas, so it routes to
|
|
3414
|
+
// mountUIFXGroup with `container` as the parent and the group type selecting
|
|
3415
|
+
// the native structure (radios / tabs / spinbutton). It needs `items` in
|
|
3416
|
+
// options (fail closed inside mountUIFXGroup). mountUIFX/decorateUIFX reject a
|
|
3417
|
+
// group type via their own guards, so the three mount paths cannot cross.
|
|
3418
|
+
if (_GROUP_TYPES.has(type)) {
|
|
3419
|
+
return mountUIFXGroup(container, type, factory, mountOptions);
|
|
3420
|
+
}
|
|
3370
3421
|
// A decoration is mounted AROUND a live element (no native element created),
|
|
3371
3422
|
// so it routes to decorateUIFX with `container` as the host element. Every
|
|
3372
3423
|
// other type is a hijack mount. mountUIFX keeps rejecting 'decorate' via its
|
|
3373
|
-
// own _KNOWN_TYPES guard, so the
|
|
3424
|
+
// own _KNOWN_TYPES guard, so the paths cannot cross.
|
|
3374
3425
|
if (type === 'decorate') {
|
|
3375
3426
|
return decorateUIFX(container, factory, mountOptions);
|
|
3376
3427
|
}
|