ajo-cloves 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1317 @@
1
+ //#region packages/ajo-cloves/src/core.ts
2
+ /** True when a value is structurally a DOM element in a document-bearing runtime. */
3
+ var dom = (value) => typeof document != "undefined" && value?.nodeType === 1;
4
+ /** True when both Window and Document globals are available. */
5
+ var browser = () => typeof window != "undefined" && typeof document != "undefined";
6
+ var statefulArg = (key) => key === "key" || key === "memo" || key === "ref" || key === "skip" || key.startsWith("set:");
7
+ /** Maps rest attrs onto an Ajo stateful host, prefixing DOM attributes with `attr:`. */
8
+ var statefulRootAttrs = (attrs) => {
9
+ const result = {};
10
+ for (const [key, value] of Object.entries(attrs)) result[statefulArg(key) ? key : `attr:${key}`] = value;
11
+ return result;
12
+ };
13
+ /** Calls an externally supplied DOM event handler when it is a function. */
14
+ var callHandler = (handler, event) => {
15
+ if (typeof handler === "function") handler(event);
16
+ };
17
+ /** Calls an externally supplied Ajo ref callback when it is a function. */
18
+ var callRef = (ref, element) => {
19
+ if (typeof ref === "function") ref(element);
20
+ };
21
+ var hostSignal = (host, signal) => signal && signal !== host.signal ? AbortSignal.any([signal, host.signal]) : host.signal;
22
+ /** Adds a listener to a DOM host for at most the caller and host lifetimes. */
23
+ var listen = (host, type, handler, opts) => {
24
+ if (!dom(host)) return;
25
+ host.addEventListener(type, handler, {
26
+ ...opts,
27
+ signal: hostSignal(host, opts?.signal)
28
+ });
29
+ };
30
+ function on(target, type, fn, host, opts) {
31
+ target.addEventListener(type, fn, {
32
+ ...opts,
33
+ signal: hostSignal(host, opts?.signal)
34
+ });
35
+ }
36
+ var sources = /* @__PURE__ */ new Map();
37
+ /** Subscribes to a lazily-started shared source; the real source stops with the last unsubscribe. */
38
+ var shared = (key, start, fn, signal) => {
39
+ if (signal.aborted) return;
40
+ let source = sources.get(key);
41
+ const created = !source;
42
+ if (!source) {
43
+ source = {
44
+ stop: () => {},
45
+ subscribers: /* @__PURE__ */ new Set()
46
+ };
47
+ sources.set(key, source);
48
+ }
49
+ source.subscribers.add(fn);
50
+ const unsubscribe = () => {
51
+ const current = sources.get(key);
52
+ if (!current) return;
53
+ current.subscribers.delete(fn);
54
+ if (current.subscribers.size) return;
55
+ current.stop();
56
+ sources.delete(key);
57
+ };
58
+ signal.addEventListener("abort", unsubscribe, { once: true });
59
+ if (!created) return;
60
+ try {
61
+ source.stop = start(() => {
62
+ const current = sources.get(key);
63
+ if (!current) return;
64
+ for (const subscriber of [...current.subscribers]) subscriber();
65
+ });
66
+ } catch (error) {
67
+ unsubscribe();
68
+ throw error;
69
+ }
70
+ };
71
+ /** Wraps fn so multiple calls within one frame collapse into one run on the next frame. */
72
+ var frame = (fn) => {
73
+ let handle;
74
+ const run = () => {
75
+ handle = void 0;
76
+ fn();
77
+ };
78
+ const schedule = (() => {
79
+ if (typeof requestAnimationFrame == "undefined") {
80
+ fn();
81
+ return;
82
+ }
83
+ if (handle != null) return;
84
+ handle = requestAnimationFrame(run);
85
+ });
86
+ schedule.cancel = () => {
87
+ if (handle == null) return;
88
+ if (typeof cancelAnimationFrame != "undefined") cancelAnimationFrame(handle);
89
+ handle = void 0;
90
+ };
91
+ return schedule;
92
+ };
93
+ /** Owns frame-coalesced binding to one live element target at a time. */
94
+ var live = (host, opts) => {
95
+ const signal = host.signal;
96
+ let target;
97
+ let scope;
98
+ const schedule = frame(() => {
99
+ const current = target;
100
+ if (current) opts.onChange(current);
101
+ });
102
+ const stop = () => {
103
+ scope?.abort();
104
+ scope = void 0;
105
+ target = void 0;
106
+ schedule.cancel();
107
+ };
108
+ signal.addEventListener("abort", stop, { once: true });
109
+ return { sync() {
110
+ if (signal.aborted) return;
111
+ const next = opts.target() ?? void 0;
112
+ if (next === target) return;
113
+ scope?.abort();
114
+ scope = void 0;
115
+ schedule.cancel();
116
+ target = next;
117
+ if (!next) return;
118
+ const controller = new AbortController();
119
+ const notify = () => {
120
+ if (!controller.signal.aborted && target === next) schedule();
121
+ };
122
+ scope = controller;
123
+ try {
124
+ opts.bind(next, notify, controller.signal);
125
+ } catch (error) {
126
+ if (scope === controller) {
127
+ controller.abort();
128
+ scope = void 0;
129
+ target = void 0;
130
+ schedule.cancel();
131
+ }
132
+ throw error;
133
+ }
134
+ notify();
135
+ } };
136
+ };
137
+ var counters = Object.create(null);
138
+ /** Monotonic per-prefix unique id. */
139
+ var id = (prefix) => `${prefix}-${counters[prefix] = (counters[prefix] ?? 0) + 1}`;
140
+ /** Stores a value while keeping at most `limit` insertion-ordered cache keys. */
141
+ var remember = (cache, key, value, limit = 32) => {
142
+ if (!Number.isInteger(limit) || limit < 1) throw new RangeError("remember limit must be a positive integer");
143
+ cache.set(key, value);
144
+ while (cache.size > limit) {
145
+ const oldest = cache.keys().next();
146
+ if (oldest.done) break;
147
+ cache.delete(oldest.value);
148
+ }
149
+ return value;
150
+ };
151
+ /** Clamps a number into the inclusive [min, max] range. */
152
+ var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
153
+ //#endregion
154
+ //#region packages/ajo-cloves/src/announce.ts
155
+ var hidden = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0";
156
+ var polite;
157
+ var assertive;
158
+ var create = (channel) => {
159
+ const region = document.createElement("div");
160
+ region.setAttribute("role", channel === "polite" ? "status" : "alert");
161
+ region.setAttribute("aria-live", channel);
162
+ region.setAttribute("aria-atomic", "true");
163
+ region.setAttribute("style", hidden);
164
+ document.body.append(region);
165
+ return region;
166
+ };
167
+ var region = (channel) => {
168
+ const next = (channel === "polite" ? polite : assertive) ?? create(channel);
169
+ if (channel === "polite") polite = next;
170
+ else assertive = next;
171
+ if (!next.isConnected) document.body.append(next);
172
+ return next;
173
+ };
174
+ var send = (channel, message) => {
175
+ const target = region(channel);
176
+ target.textContent = "";
177
+ queueMicrotask(() => target.textContent = message);
178
+ };
179
+ /** Screen-reader announcements through lazily created document-lifetime aria-live regions. */
180
+ var announce = (host) => {
181
+ if (!dom(host)) return {
182
+ polite(_message) {},
183
+ assertive(_message) {}
184
+ };
185
+ return {
186
+ polite(message) {
187
+ send("polite", message);
188
+ },
189
+ assertive(message) {
190
+ send("assertive", message);
191
+ }
192
+ };
193
+ };
194
+ //#endregion
195
+ //#region packages/ajo-cloves/src/controlled.ts
196
+ /** Controlled/uncontrolled value unification for open/value-style component args. */
197
+ var controlled = (host, opts) => {
198
+ let local = opts.fallback;
199
+ let value = local;
200
+ let bound = false;
201
+ return {
202
+ get value() {
203
+ return value;
204
+ },
205
+ get controlled() {
206
+ return bound;
207
+ },
208
+ /** undefined = uncontrolled; any other value (null included) binds. */
209
+ sync(arg) {
210
+ bound = arg !== void 0;
211
+ value = bound ? arg : local;
212
+ return value;
213
+ },
214
+ set(next, event) {
215
+ opts.onChange?.(next, event);
216
+ if (!bound) local = next;
217
+ host.next(() => value = next);
218
+ },
219
+ accept(next, event) {
220
+ if (!bound) local = next;
221
+ host.next(() => value = next);
222
+ opts.onChange?.(next, event);
223
+ },
224
+ /** Seeds the uncontrolled value without notifying onChange (lazy defaults). */
225
+ init(next) {
226
+ if (bound) return;
227
+ local = next;
228
+ host.next(() => value = next);
229
+ }
230
+ };
231
+ };
232
+ //#endregion
233
+ //#region packages/ajo-cloves/src/dismiss.ts
234
+ /** Closes a surface on Escape within the given elements and/or pointerdown outside them. */
235
+ var dismiss = (host, opts) => {
236
+ if (!dom(host)) return;
237
+ const document = host.ownerDocument;
238
+ const view = document.defaultView;
239
+ const node = (value) => Boolean(view && value instanceof view.Node);
240
+ const escape = opts.escape ?? "host";
241
+ if (escape) on(escape === "document" ? document : host, "keydown", (event) => {
242
+ if (event.key !== "Escape" || !opts.active()) return;
243
+ if (escape === "host" && opts.inside) {
244
+ const target = event.target;
245
+ const elements = opts.inside().filter((element) => !!element);
246
+ if (elements.length && !(node(target) && elements.some((element) => element.contains(target)))) return;
247
+ }
248
+ if (opts.prevent) event.preventDefault();
249
+ opts.onDismiss(event);
250
+ }, host);
251
+ if (opts.outside) on(document, "pointerdown", (event) => {
252
+ if (!opts.active()) return;
253
+ const target = event.target;
254
+ if (!node(target)) return;
255
+ if (host.contains(target)) return;
256
+ if (opts.inside?.().some((element) => element?.contains(target))) return;
257
+ opts.onDismiss(event);
258
+ }, host, { capture: true });
259
+ };
260
+ //#endregion
261
+ //#region packages/ajo-cloves/src/grid.ts
262
+ var move$2 = (event, rtl) => {
263
+ if (event.key === "ArrowLeft") return { cols: rtl ? 1 : -1 };
264
+ if (event.key === "ArrowRight") return { cols: rtl ? -1 : 1 };
265
+ if (event.key === "ArrowUp") return { rows: -1 };
266
+ if (event.key === "ArrowDown") return { rows: 1 };
267
+ if (event.key === "Home") return {
268
+ edge: "start",
269
+ extent: event.ctrlKey ? "all" : "row"
270
+ };
271
+ if (event.key === "End") return {
272
+ edge: "end",
273
+ extent: event.ctrlKey ? "all" : "row"
274
+ };
275
+ if (event.key === "PageUp") return {
276
+ page: -1,
277
+ large: event.shiftKey
278
+ };
279
+ if (event.key === "PageDown") return {
280
+ page: 1,
281
+ large: event.shiftKey
282
+ };
283
+ };
284
+ /**
285
+ * 2D grid keyboard navigation: resolves arrows/Home/End/PageUp/PageDown to semantic movement.
286
+ *
287
+ * The host parameter keeps the clove signature uniform; grid holds no
288
+ * per-host state and needs no cleanup.
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const nav = grid(this, { onMove: move => focusCell(move) })
293
+ * ```
294
+ */
295
+ var grid = (host, opts) => {
296
+ return {
297
+ /** Routes a keydown. Returns true and prevents default for grid navigation keys. */
298
+ handle(event) {
299
+ const next = move$2(event, opts.rtl?.() ?? false);
300
+ if (!next) return false;
301
+ event.preventDefault();
302
+ opts.onMove(next, event);
303
+ return true;
304
+ } };
305
+ };
306
+ //#endregion
307
+ //#region packages/ajo-cloves/src/hotkey.ts
308
+ var modifier = (token) => token === "mod" || token === "ctrl" || token === "meta" || token === "alt" || token === "shift";
309
+ var matches = (chord, event) => {
310
+ const tokens = chord.split("+").map((token) => token.trim().toLowerCase()).filter(Boolean);
311
+ const key = tokens.find((token) => !modifier(token));
312
+ if (!key) return false;
313
+ const wantsMod = tokens.includes("mod");
314
+ const wantsCtrl = tokens.includes("ctrl");
315
+ const wantsMeta = tokens.includes("meta");
316
+ const wantsAlt = tokens.includes("alt");
317
+ const wantsShift = tokens.includes("shift");
318
+ const ctrl = event.ctrlKey;
319
+ const meta = event.metaKey;
320
+ if (wantsMod) {
321
+ if (ctrl === meta) return false;
322
+ } else if (ctrl !== wantsCtrl || meta !== wantsMeta) return false;
323
+ if (event.altKey !== wantsAlt) return false;
324
+ if (event.shiftKey !== wantsShift) return false;
325
+ return event.key.toLowerCase() === key;
326
+ };
327
+ /** Global keyboard shortcut for a single chord like 'mod+b' or 'F8'. */
328
+ var hotkey = (host, opts) => {
329
+ if (!dom(host)) return;
330
+ on(window, "keydown", (event) => {
331
+ if (!matches(opts.keys(), event)) return;
332
+ if (!(opts.active?.() ?? true)) return;
333
+ if (opts.prevent ?? true) event.preventDefault();
334
+ opts.onPress(event);
335
+ }, host);
336
+ };
337
+ //#endregion
338
+ //#region packages/ajo-cloves/src/timer.ts
339
+ /** Managed one-shot timer with pause/resume, auto-cleared with the host lifecycle. */
340
+ var timer = (host) => {
341
+ const signal = host.signal;
342
+ let handle;
343
+ let task;
344
+ let due = 0;
345
+ let left = 0;
346
+ let paused = false;
347
+ const stop = () => {
348
+ if (handle != null) clearTimeout(handle);
349
+ handle = void 0;
350
+ task = void 0;
351
+ due = 0;
352
+ left = 0;
353
+ paused = false;
354
+ };
355
+ const done = () => {
356
+ const fn = task;
357
+ handle = void 0;
358
+ task = void 0;
359
+ due = 0;
360
+ left = 0;
361
+ paused = false;
362
+ fn?.();
363
+ };
364
+ const schedule = (ms) => {
365
+ left = Math.max(0, ms);
366
+ due = Date.now() + left;
367
+ handle = setTimeout(done, left);
368
+ };
369
+ const view = {
370
+ start(ms, fn) {
371
+ stop();
372
+ if (signal.aborted) return;
373
+ task = fn;
374
+ paused = false;
375
+ schedule(ms);
376
+ },
377
+ stop,
378
+ pause() {
379
+ if (handle == null) return;
380
+ left = Math.max(0, due - Date.now());
381
+ clearTimeout(handle);
382
+ handle = void 0;
383
+ paused = true;
384
+ },
385
+ resume() {
386
+ if (!paused || !task) return;
387
+ paused = false;
388
+ schedule(left);
389
+ },
390
+ get running() {
391
+ return handle != null;
392
+ },
393
+ get remaining() {
394
+ if (handle) return Math.max(0, due - Date.now());
395
+ return paused ? left : 0;
396
+ }
397
+ };
398
+ signal.addEventListener("abort", stop, { once: true });
399
+ return view;
400
+ };
401
+ //#endregion
402
+ //#region packages/ajo-cloves/src/hover.ts
403
+ /** Hover intent: zones hold the surface open; delays gate open/close transitions. */
404
+ var hover = (host, opts) => {
405
+ const zones = /* @__PURE__ */ new Set();
406
+ const opening = timer(host);
407
+ const closing = timer(host);
408
+ let opened = false;
409
+ const flip = (next, event) => {
410
+ if (opened === next) return;
411
+ opened = next;
412
+ opts.onChange(next, event);
413
+ };
414
+ return {
415
+ get open() {
416
+ return opened;
417
+ },
418
+ hold(zone, event) {
419
+ zones.add(zone);
420
+ closing.stop();
421
+ if (opened || opening.running) return;
422
+ const delay = opts.openDelay?.() ?? 0;
423
+ if (delay <= 0) flip(true, event);
424
+ else opening.start(delay, () => flip(true, event));
425
+ },
426
+ release(zone, event) {
427
+ zones.delete(zone);
428
+ if (zones.size) return;
429
+ opening.stop();
430
+ if (!opened) return;
431
+ const delay = opts.closeDelay?.() ?? 0;
432
+ if (delay <= 0) flip(false, event);
433
+ else closing.start(delay, () => flip(false, event));
434
+ },
435
+ sync(open) {
436
+ opened = open;
437
+ },
438
+ /** Stops pending transitions and forgets every held zone without notifying. */
439
+ cancel() {
440
+ opening.stop();
441
+ closing.stop();
442
+ zones.clear();
443
+ }
444
+ };
445
+ };
446
+ //#endregion
447
+ //#region packages/ajo-cloves/src/resize.ts
448
+ var registry = /* @__PURE__ */ new WeakMap();
449
+ var observer;
450
+ var subscriptions = 0;
451
+ var notify = (entries, source) => {
452
+ if (source !== observer) return;
453
+ for (const entry of entries) {
454
+ const callbacks = registry.get(entry.target);
455
+ if (!callbacks) continue;
456
+ for (const callback of [...callbacks]) callback(entry.target);
457
+ }
458
+ };
459
+ var subscribe = (el, fn, signal) => {
460
+ if (signal.aborted) return;
461
+ observer ??= new ResizeObserver(notify);
462
+ const currentObserver = observer;
463
+ let callbacks = registry.get(el);
464
+ if (!callbacks) {
465
+ callbacks = /* @__PURE__ */ new Set();
466
+ try {
467
+ currentObserver.observe(el);
468
+ } catch (error) {
469
+ try {
470
+ currentObserver.unobserve(el);
471
+ } catch {}
472
+ if (!subscriptions) {
473
+ try {
474
+ currentObserver.disconnect();
475
+ } catch {}
476
+ if (observer === currentObserver) observer = void 0;
477
+ }
478
+ throw error;
479
+ }
480
+ registry.set(el, callbacks);
481
+ }
482
+ callbacks.add(fn);
483
+ subscriptions++;
484
+ const unsubscribe = () => {
485
+ const current = registry.get(el);
486
+ if (!current?.delete(fn)) return;
487
+ subscriptions--;
488
+ if (!current.size) {
489
+ registry.delete(el);
490
+ observer?.unobserve(el);
491
+ }
492
+ if (subscriptions) return;
493
+ observer?.disconnect();
494
+ observer = void 0;
495
+ };
496
+ signal.addEventListener("abort", unsubscribe, { once: true });
497
+ };
498
+ /**
499
+ * Shared ResizeObserver notification for a live target element.
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * const size = resize(this, { target: () => panel, onResize: el => measure(el) })
504
+ * while (true) {
505
+ * size.sync()
506
+ * yield <section ref={el => panel = el} />
507
+ * }
508
+ * ```
509
+ */
510
+ var resize = (host, opts) => {
511
+ if (!dom(host) || typeof ResizeObserver == "undefined") return { sync() {} };
512
+ return live(host, {
513
+ target: opts.target,
514
+ onChange: opts.onResize,
515
+ bind: (element, notify, signal) => subscribe(element, notify, signal)
516
+ });
517
+ };
518
+ //#endregion
519
+ //#region packages/ajo-cloves/src/indicator.ts
520
+ /**
521
+ * Tracks a marked child inside a live container and stamps its box on the
522
+ * container as CSS variables (`--indicator-x/y/w/h`, px in the container's
523
+ * content coordinates) plus a `data-indicator="true"` marker while a mark
524
+ * exists. Themes draw a pseudo-element positioned by the variables and
525
+ * transition it, so the active marker glides between children instead of
526
+ * jumping.
527
+ *
528
+ * On first placement the variables land one frame before the marker
529
+ * attribute, so a theme pseudo-element fades in at its resting position
530
+ * instead of sliding in from the container origin.
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * const mark = indicator(this, {
535
+ * target: () => list,
536
+ * of: container => container.querySelector('[data-state="active"]'),
537
+ * })
538
+ * while (true) {
539
+ * mark.sync()
540
+ * yield ...
541
+ * }
542
+ * ```
543
+ */
544
+ var indicator = (host, opts) => {
545
+ if (!dom(host)) return { sync() {} };
546
+ let container;
547
+ let control;
548
+ let placed = false;
549
+ const clear = (el) => {
550
+ el.removeAttribute("data-indicator");
551
+ for (const name of [
552
+ "x",
553
+ "y",
554
+ "w",
555
+ "h"
556
+ ]) el.style.removeProperty(`--indicator-${name}`);
557
+ placed = false;
558
+ };
559
+ const reveal = frame(() => {
560
+ if (container && placed) container.setAttribute("data-indicator", "true");
561
+ });
562
+ const measure = () => {
563
+ if (!container) return;
564
+ const mark = opts.of(container);
565
+ if (!mark || !container.contains(mark)) {
566
+ container.removeAttribute("data-indicator");
567
+ placed = false;
568
+ return;
569
+ }
570
+ const box = container.getBoundingClientRect();
571
+ const rect = mark.getBoundingClientRect();
572
+ const style = container.style;
573
+ style.setProperty("--indicator-x", `${rect.left - box.left + container.scrollLeft}px`);
574
+ style.setProperty("--indicator-y", `${rect.top - box.top + container.scrollTop}px`);
575
+ style.setProperty("--indicator-w", `${rect.width}px`);
576
+ style.setProperty("--indicator-h", `${rect.height}px`);
577
+ if (placed) return;
578
+ placed = true;
579
+ reveal();
580
+ };
581
+ const schedule = frame(measure);
582
+ const size = resize(host, {
583
+ target: () => container,
584
+ onResize: () => schedule()
585
+ });
586
+ const retarget = (next) => {
587
+ if (next === container) return;
588
+ control?.abort();
589
+ control = void 0;
590
+ if (container) clear(container);
591
+ container = next;
592
+ if (!container || !opts.on?.length) return;
593
+ control = new AbortController();
594
+ host.signal.addEventListener("abort", () => control?.abort(), { signal: control.signal });
595
+ for (const type of opts.on) container.addEventListener(type, () => schedule(), {
596
+ passive: true,
597
+ signal: control.signal
598
+ });
599
+ };
600
+ host.signal.addEventListener("abort", () => {
601
+ schedule.cancel();
602
+ reveal.cancel();
603
+ control?.abort();
604
+ if (container) clear(container);
605
+ container = void 0;
606
+ }, { once: true });
607
+ return { sync() {
608
+ if (host.signal.aborted) return;
609
+ retarget(opts.target() ?? void 0);
610
+ size.sync();
611
+ schedule();
612
+ } };
613
+ };
614
+ //#endregion
615
+ //#region packages/ajo-cloves/src/label.ts
616
+ /** Field labelling: stable ids and label/control/description/error wiring for one form field. */
617
+ var label = (host, options = {}) => {
618
+ const base = id(options.prefix?.() ?? "field");
619
+ const ids = {
620
+ control: base,
621
+ description: `${base}-description`,
622
+ error: `${base}-error`,
623
+ label: `${base}-label`
624
+ };
625
+ let described = false;
626
+ let invalid = false;
627
+ let pending = false;
628
+ let present = false;
629
+ let pass = 0;
630
+ const schedule = () => {
631
+ if (!dom(host) || pending) return;
632
+ pending = true;
633
+ queueMicrotask(() => {
634
+ pending = false;
635
+ host.next();
636
+ });
637
+ };
638
+ const describedby = () => {
639
+ const values = [];
640
+ if (described) values.push(ids.description);
641
+ if (invalid) values.push(ids.error);
642
+ return values.join(" ") || void 0;
643
+ };
644
+ const controlAttrs = () => ({
645
+ id: ids.control,
646
+ "aria-describedby": describedby(),
647
+ "aria-invalid": invalid ? "true" : void 0,
648
+ "aria-errormessage": invalid ? ids.error : void 0
649
+ });
650
+ return {
651
+ ids,
652
+ sync(next) {
653
+ invalid = next;
654
+ },
655
+ describe(next) {
656
+ if (next) {
657
+ present = true;
658
+ if (described) return;
659
+ described = true;
660
+ schedule();
661
+ return;
662
+ }
663
+ present = false;
664
+ if (!described) return;
665
+ described = false;
666
+ schedule();
667
+ },
668
+ get labelAttrs() {
669
+ return {
670
+ id: ids.label,
671
+ for: ids.control
672
+ };
673
+ },
674
+ get controlAttrs() {
675
+ return controlAttrs();
676
+ },
677
+ get buttonAttrs() {
678
+ return {
679
+ ...controlAttrs(),
680
+ "aria-labelledby": ids.label
681
+ };
682
+ },
683
+ get groupAttrs() {
684
+ return {
685
+ "aria-labelledby": ids.label,
686
+ "aria-describedby": describedby()
687
+ };
688
+ },
689
+ get descriptionAttrs() {
690
+ return { id: ids.description };
691
+ },
692
+ get errorAttrs() {
693
+ return { id: ids.error };
694
+ },
695
+ reset() {
696
+ present = false;
697
+ if (!described || !dom(host)) return;
698
+ const current = ++pass;
699
+ queueMicrotask(() => {
700
+ if (current !== pass || present || !described) return;
701
+ host.next(() => {
702
+ described = false;
703
+ });
704
+ });
705
+ }
706
+ };
707
+ };
708
+ //#endregion
709
+ //#region packages/ajo-cloves/src/media.ts
710
+ var lists = /* @__PURE__ */ new Map();
711
+ var fallback = (opts) => opts.fallback?.() ?? false;
712
+ var start$1 = (query) => (notify) => {
713
+ const list = window.matchMedia(query);
714
+ lists.set(query, list);
715
+ list.addEventListener("change", notify);
716
+ return () => {
717
+ list.removeEventListener("change", notify);
718
+ if (lists.get(query) === list) lists.delete(query);
719
+ };
720
+ };
721
+ var read$1 = (query, opts) => query ? lists.get(query)?.matches ?? fallback(opts) : fallback(opts);
722
+ /**
723
+ * Reactive media-query match, shared per query string.
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * const narrow = media(this, { query: () => '(max-width: 768px)' })
728
+ * for (const args of this) {
729
+ * narrow.sync()
730
+ * yield <span>{narrow.matches ? 'narrow' : 'wide'}</span>
731
+ * }
732
+ * ```
733
+ */
734
+ var media = (host, options) => {
735
+ const { query } = options;
736
+ let active;
737
+ let current = fallback(options);
738
+ let scope;
739
+ const stop = () => {
740
+ scope?.abort();
741
+ scope = void 0;
742
+ active = void 0;
743
+ };
744
+ const update = () => {
745
+ const next = read$1(active, options);
746
+ if (next === current) return;
747
+ host.next(() => {
748
+ current = next;
749
+ });
750
+ };
751
+ if (!dom(host) || typeof window.matchMedia != "function") return {
752
+ get matches() {
753
+ return fallback(options);
754
+ },
755
+ sync() {}
756
+ };
757
+ host.signal.addEventListener("abort", stop, { once: true });
758
+ return {
759
+ get matches() {
760
+ return read$1(active, options);
761
+ },
762
+ sync() {
763
+ if (host.signal.aborted) return;
764
+ const next = query();
765
+ if (next === active) {
766
+ current = read$1(active, options);
767
+ return;
768
+ }
769
+ scope?.abort();
770
+ scope = new AbortController();
771
+ active = next;
772
+ shared(`media:${next}`, start$1(next), update, scope.signal);
773
+ current = read$1(active, options);
774
+ }
775
+ };
776
+ };
777
+ //#endregion
778
+ //#region packages/ajo-cloves/src/move.ts
779
+ var isCaptureElement = (value) => value instanceof HTMLElement || value instanceof SVGElement;
780
+ var capture = (event) => isCaptureElement(event.currentTarget) ? event.currentTarget : isCaptureElement(event.target) ? event.target : void 0;
781
+ var update = (session, event, canceled) => {
782
+ const { data } = session;
783
+ data.x = event.clientX;
784
+ data.y = event.clientY;
785
+ data.dx = event.clientX - session.startX;
786
+ data.dy = event.clientY - session.startY;
787
+ data.canceled = canceled;
788
+ return data;
789
+ };
790
+ var release = (session) => {
791
+ try {
792
+ session.element.releasePointerCapture(session.pointer);
793
+ } catch {}
794
+ };
795
+ /**
796
+ * Pointer-drag session lifecycle: capture, move deltas, and guaranteed cleanup.
797
+ *
798
+ * `data` is one object mutated across callbacks within a session; read it
799
+ * during the callback and do not retain it across events. Consumers own
800
+ * gesture CSS such as `touch-action`, and Escape is reported without
801
+ * preventDefault so consumers can decide that policy.
802
+ *
803
+ * @example
804
+ * ```ts
805
+ * function* Handle(this: Host) {
806
+ * const drag = move(this, { onMove(data) { resize(data.dx, data.dy) } })
807
+ * yield <div set:onpointerdown={event => drag.start(event)} />
808
+ * }
809
+ * ```
810
+ */
811
+ var move = (host, opts) => {
812
+ if (!dom(host)) return {
813
+ start(_event) {
814
+ return false;
815
+ },
816
+ get active() {
817
+ return false;
818
+ }
819
+ };
820
+ const lifetime = host.signal;
821
+ let session;
822
+ const finish = (current, event, canceled, releaseCapture = true) => {
823
+ if (session !== current || current.controller.signal.aborted) return;
824
+ const data = event instanceof PointerEvent ? update(current, event, canceled) : current.data;
825
+ if (!(event instanceof PointerEvent)) data.canceled = canceled;
826
+ session = void 0;
827
+ current.controller.abort();
828
+ if (releaseCapture) release(current);
829
+ opts.onEnd?.(data, event);
830
+ };
831
+ return {
832
+ start(event) {
833
+ if (event.button !== 0 || !event.isPrimary || session || lifetime.aborted) return false;
834
+ const element = capture(event);
835
+ if (!element) return false;
836
+ try {
837
+ element.setPointerCapture(event.pointerId);
838
+ } catch {}
839
+ const current = {
840
+ controller: new AbortController(),
841
+ data: {
842
+ x: event.clientX,
843
+ y: event.clientY,
844
+ dx: 0,
845
+ dy: 0,
846
+ canceled: false
847
+ },
848
+ element,
849
+ pointer: event.pointerId,
850
+ startX: event.clientX,
851
+ startY: event.clientY
852
+ };
853
+ const { signal } = current.controller;
854
+ session = current;
855
+ lifetime.addEventListener("abort", () => {
856
+ if (session !== current || signal.aborted) return;
857
+ session = void 0;
858
+ current.controller.abort();
859
+ release(current);
860
+ }, {
861
+ once: true,
862
+ signal
863
+ });
864
+ on(element, "pointermove", (next) => {
865
+ if (next.pointerId !== current.pointer) return;
866
+ opts.onMove(update(current, next, false), next);
867
+ }, host, { signal });
868
+ on(element, "pointerup", (next) => {
869
+ if (next.pointerId !== current.pointer) return;
870
+ finish(current, next, false);
871
+ }, host, { signal });
872
+ on(element, "pointercancel", (next) => {
873
+ if (next.pointerId !== current.pointer) return;
874
+ finish(current, next, true);
875
+ }, host, { signal });
876
+ on(element, "lostpointercapture", (next) => {
877
+ if (next.pointerId !== current.pointer) return;
878
+ finish(current, next, true, false);
879
+ }, host, { signal });
880
+ on(element.ownerDocument, "keydown", (next) => {
881
+ if (next.key === "Escape") finish(current, next, true);
882
+ }, host, { signal });
883
+ opts.onStart?.(current.data, event);
884
+ return true;
885
+ },
886
+ get active() {
887
+ return !!session;
888
+ }
889
+ };
890
+ };
891
+ //#endregion
892
+ //#region packages/ajo-cloves/src/scrolling.ts
893
+ /**
894
+ * Scroll tracking for a live target element with leak-proof retargeting.
895
+ *
896
+ * @example
897
+ * ```ts
898
+ * const track = scrolling(this, { target: () => list, onScroll: el => read(el.scrollTop) })
899
+ * while (true) {
900
+ * track.sync()
901
+ * yield <div ref={el => list = el} />
902
+ * }
903
+ * ```
904
+ */
905
+ var scrolling = (host, opts) => {
906
+ if (!dom(host)) return { sync() {} };
907
+ return live(host, {
908
+ target: opts.target,
909
+ onChange: opts.onScroll,
910
+ bind: (element, notify, signal) => {
911
+ on(element, "scroll", notify, host, {
912
+ passive: true,
913
+ signal
914
+ });
915
+ if (opts.onEnd) on(element, "scrollend", () => opts.onEnd?.(element), host, {
916
+ passive: true,
917
+ signal
918
+ });
919
+ }
920
+ });
921
+ };
922
+ //#endregion
923
+ //#region packages/ajo-cloves/src/overflow.ts
924
+ var edge = (position, size, span) => {
925
+ if (span - size <= 1) return null;
926
+ const offset = Math.abs(position);
927
+ const start = offset > 1;
928
+ const end = offset < span - size - 1;
929
+ return start && end ? "both" : start ? "start" : "end";
930
+ };
931
+ var stamp = (el, name, value) => {
932
+ if (!value) el.removeAttribute(name);
933
+ else if (el.getAttribute(name) !== value) el.setAttribute(name, value);
934
+ };
935
+ /**
936
+ * Stamps `data-overflow-x` / `data-overflow-y` ("start" | "end" | "both") on
937
+ * a live scrollable element while content overflows it, tracking scroll,
938
+ * element resize, and re-renders. Themes pair the attrs with edge fades (the
939
+ * `[data-overflow-*]` mask preflight); the attrs are absent while everything
940
+ * fits.
941
+ *
942
+ * @example
943
+ * ```ts
944
+ * const edges = overflow(this, { target: () => list })
945
+ * while (true) {
946
+ * edges.sync()
947
+ * yield <div ref={el => list = el} />
948
+ * }
949
+ * ```
950
+ */
951
+ var overflow = (host, opts) => {
952
+ if (!dom(host)) return { sync() {} };
953
+ const measure = (el) => {
954
+ stamp(el, "data-overflow-x", edge(el.scrollLeft, el.clientWidth, el.scrollWidth));
955
+ stamp(el, "data-overflow-y", edge(el.scrollTop, el.clientHeight, el.scrollHeight));
956
+ };
957
+ const clear = (el) => {
958
+ stamp(el, "data-overflow-x", null);
959
+ stamp(el, "data-overflow-y", null);
960
+ };
961
+ let target;
962
+ const scroll = scrolling(host, {
963
+ target: () => target,
964
+ onScroll: measure
965
+ });
966
+ const size = resize(host, {
967
+ target: () => target,
968
+ onResize: (el) => measure(el)
969
+ });
970
+ const schedule = frame(() => {
971
+ if (target) measure(target);
972
+ });
973
+ host.signal.addEventListener("abort", () => {
974
+ schedule.cancel();
975
+ if (target) clear(target);
976
+ target = void 0;
977
+ }, { once: true });
978
+ return { sync() {
979
+ if (host.signal.aborted) return;
980
+ const next = opts.target() ?? void 0;
981
+ if (next !== target) {
982
+ if (target) clear(target);
983
+ target = next;
984
+ }
985
+ scroll.sync();
986
+ size.sync();
987
+ schedule();
988
+ } };
989
+ };
990
+ //#endregion
991
+ //#region packages/ajo-cloves/src/restore.ts
992
+ /** Captures the focused element and returns focus to it later. */
993
+ var restore = (host) => {
994
+ const inert = {
995
+ capture(_element) {},
996
+ restore() {}
997
+ };
998
+ if (!dom(host)) return inert;
999
+ let captured = null;
1000
+ return {
1001
+ capture(element) {
1002
+ captured = arguments.length ? element ?? null : document.activeElement;
1003
+ },
1004
+ restore() {
1005
+ const element = captured;
1006
+ captured = null;
1007
+ if (!element) return;
1008
+ queueMicrotask(() => {
1009
+ if (element.isConnected) element.focus();
1010
+ });
1011
+ }
1012
+ };
1013
+ };
1014
+ //#endregion
1015
+ //#region packages/ajo-cloves/src/roving.ts
1016
+ var keyStep = (event, orientation, dir) => {
1017
+ if (event.key === "Home") return "first";
1018
+ if (event.key === "End") return "last";
1019
+ if (orientation === "vertical" || orientation === "both") {
1020
+ if (event.key === "ArrowUp") return -1;
1021
+ if (event.key === "ArrowDown") return 1;
1022
+ }
1023
+ if (orientation === "horizontal" || orientation === "both") {
1024
+ if (event.key === "ArrowLeft") return dir === "rtl" ? 1 : -1;
1025
+ if (event.key === "ArrowRight") return dir === "rtl" ? -1 : 1;
1026
+ }
1027
+ };
1028
+ /** List keyboard navigation: resolves arrow/Home/End movement over items; consumers apply focus or virtual effects. */
1029
+ var roving = (host, opts) => {
1030
+ const inert = {
1031
+ handle: (_event) => false,
1032
+ move: (_step, _event) => false
1033
+ };
1034
+ if (!dom(host)) return inert;
1035
+ const move = (step, event) => {
1036
+ const list = opts.items();
1037
+ if (!list.length) return false;
1038
+ const active = opts.current?.() ?? document.activeElement;
1039
+ const index = Math.max(0, list.indexOf(active));
1040
+ const target = step === "first" ? list[0] : step === "last" ? list[list.length - 1] : list[index + step] ?? (opts.loop?.() ?? true ? list[step > 0 ? 0 : list.length - 1] : void 0);
1041
+ if (!target) return false;
1042
+ opts.onMove(target, event);
1043
+ return true;
1044
+ };
1045
+ return {
1046
+ handle(event) {
1047
+ const step = keyStep(event, opts.orientation?.() ?? "vertical", opts.dir?.() ?? "ltr");
1048
+ if (step == null || !opts.items().length) return false;
1049
+ event.preventDefault();
1050
+ move(step, event);
1051
+ return true;
1052
+ },
1053
+ move
1054
+ };
1055
+ };
1056
+ //#endregion
1057
+ //#region packages/ajo-cloves/src/scheme.ts
1058
+ var dark = "(prefers-color-scheme: dark)";
1059
+ /**
1060
+ * Reactive OS color-scheme preference.
1061
+ *
1062
+ * @example
1063
+ * ```ts
1064
+ * const color = scheme(this)
1065
+ * yield <span>{color.dark ? 'dark' : 'light'}</span>
1066
+ * ```
1067
+ */
1068
+ var scheme = (host) => {
1069
+ const query = media(host, { query: () => dark });
1070
+ query.sync();
1071
+ return { get dark() {
1072
+ return query.matches;
1073
+ } };
1074
+ };
1075
+ //#endregion
1076
+ //#region packages/ajo-cloves/src/selection.ts
1077
+ /** Single/multi selection semantics over a controlled list of values. */
1078
+ var selection = (host, opts) => {
1079
+ const state = controlled(host, {
1080
+ fallback: [...opts.fallback ?? []],
1081
+ onChange: opts.onChange
1082
+ });
1083
+ return {
1084
+ get values() {
1085
+ return state.value;
1086
+ },
1087
+ has(value) {
1088
+ return state.value.includes(value);
1089
+ },
1090
+ toggle(value, event) {
1091
+ const base = state.value;
1092
+ const exists = base.includes(value);
1093
+ let next;
1094
+ if (opts.multiple?.() ?? false) if (exists) {
1095
+ if ((opts.required?.() ?? false) && base.length <= 1) return;
1096
+ next = base.filter((item) => item !== value);
1097
+ } else next = Array.from(new Set([...base, value]));
1098
+ else if (exists) {
1099
+ if (opts.required?.() ?? false) return;
1100
+ next = [];
1101
+ } else next = [value];
1102
+ state.set(next, event);
1103
+ },
1104
+ set(values, event) {
1105
+ state.set([...values], event);
1106
+ },
1107
+ sync(values) {
1108
+ return state.sync(values == null ? void 0 : [...values]);
1109
+ }
1110
+ };
1111
+ };
1112
+ //#endregion
1113
+ //#region packages/ajo-cloves/src/spin.ts
1114
+ var move$1 = (event) => {
1115
+ if (event.key === "ArrowUp") return { step: 1 };
1116
+ if (event.key === "ArrowDown") return { step: -1 };
1117
+ if (event.key === "PageUp") return { page: 1 };
1118
+ if (event.key === "PageDown") return { page: -1 };
1119
+ if (event.key === "Home") return { edge: "min" };
1120
+ if (event.key === "End") return { edge: "max" };
1121
+ };
1122
+ /**
1123
+ * Spinbutton keyboard stepping: resolves ArrowUp/ArrowDown/PageUp/PageDown/Home/End to semantic movement.
1124
+ *
1125
+ * The host parameter keeps the clove signature uniform; spin holds no
1126
+ * per-host state and needs no cleanup. Aria and value state stay in the
1127
+ * consumer; composes with roving when the consumer dispatches spin first.
1128
+ *
1129
+ * @example
1130
+ * ```ts
1131
+ * const stepper = spin(this, { onMove: move => stepValue(move) })
1132
+ * ```
1133
+ */
1134
+ var spin = (host, opts) => {
1135
+ return {
1136
+ /** Routes a keydown. Returns true and prevents default for spin keys. */
1137
+ handle(event) {
1138
+ const next = move$1(event);
1139
+ if (!next) return false;
1140
+ event.preventDefault();
1141
+ opts.onMove(next, event);
1142
+ return true;
1143
+ } };
1144
+ };
1145
+ //#endregion
1146
+ //#region packages/ajo-cloves/src/storage.ts
1147
+ var last;
1148
+ var source = (notify) => {
1149
+ const handler = (event) => {
1150
+ last = event;
1151
+ notify();
1152
+ };
1153
+ window.addEventListener("storage", handler);
1154
+ return () => {
1155
+ window.removeEventListener("storage", handler);
1156
+ last = void 0;
1157
+ };
1158
+ };
1159
+ var area = (name) => {
1160
+ try {
1161
+ return name === "local" ? window.localStorage : window.sessionStorage;
1162
+ } catch {
1163
+ return;
1164
+ }
1165
+ };
1166
+ var get = (name, key, fallback) => {
1167
+ const store = area(name);
1168
+ if (!store) return fallback;
1169
+ try {
1170
+ return store.getItem(key) ?? fallback;
1171
+ } catch {
1172
+ return fallback;
1173
+ }
1174
+ };
1175
+ var put = (name, key, value) => {
1176
+ const store = area(name);
1177
+ if (!store) return;
1178
+ try {
1179
+ store.setItem(key, value);
1180
+ } catch {}
1181
+ };
1182
+ var drop = (name, key) => {
1183
+ const store = area(name);
1184
+ if (!store) return;
1185
+ try {
1186
+ store.removeItem(key);
1187
+ } catch {}
1188
+ };
1189
+ /**
1190
+ * Reactive localStorage or sessionStorage string value with cross-tab sync.
1191
+ *
1192
+ * @example
1193
+ * ```ts
1194
+ * const theme = storage(this, { key: () => 'theme', fallback: 'light' })
1195
+ * yield <button set:onclick={() => theme.set('dark')}>{theme.value}</button>
1196
+ * ```
1197
+ */
1198
+ var storage = (host, opts) => {
1199
+ const name = opts.area ?? "local";
1200
+ if (!dom(host)) return {
1201
+ get value() {
1202
+ return opts.fallback;
1203
+ },
1204
+ set(_next) {},
1205
+ remove() {}
1206
+ };
1207
+ let key = opts.key();
1208
+ let current = get(name, key, opts.fallback);
1209
+ const sync = () => {
1210
+ if (host.signal.aborted) return;
1211
+ const next = opts.key();
1212
+ if (next === key) return;
1213
+ host.next(() => {
1214
+ key = next;
1215
+ current = get(name, key, opts.fallback);
1216
+ });
1217
+ };
1218
+ const set = (next) => {
1219
+ host.next(() => {
1220
+ current = next;
1221
+ });
1222
+ };
1223
+ shared("storage", source, () => {
1224
+ sync();
1225
+ const event = last;
1226
+ const store = area(name);
1227
+ if (event?.storageArea && store && event.storageArea !== store) return;
1228
+ if (event?.key != null && event.key !== key) return;
1229
+ const next = event?.key == null ? opts.fallback : event.key === key ? event.newValue ?? opts.fallback : get(name, key, opts.fallback);
1230
+ if (next === current) return;
1231
+ host.next(() => {
1232
+ current = next;
1233
+ });
1234
+ }, host.signal);
1235
+ return {
1236
+ get value() {
1237
+ sync();
1238
+ return current;
1239
+ },
1240
+ set(next) {
1241
+ if (host.signal.aborted) return;
1242
+ sync();
1243
+ put(name, key, next);
1244
+ set(next);
1245
+ },
1246
+ remove() {
1247
+ if (host.signal.aborted) return;
1248
+ sync();
1249
+ drop(name, key);
1250
+ set(opts.fallback);
1251
+ }
1252
+ };
1253
+ };
1254
+ //#endregion
1255
+ //#region packages/ajo-cloves/src/typeahead.ts
1256
+ var label$1 = (item) => item.dataset.label ?? item.textContent?.trim() ?? "";
1257
+ var printable = (event) => event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey;
1258
+ /** Typeahead: buffers printable keys and matches by label prefix without preventing default. */
1259
+ var typeahead = (host, opts) => {
1260
+ const inert = {
1261
+ handle: (_event) => false,
1262
+ reset() {}
1263
+ };
1264
+ if (!dom(host)) return inert;
1265
+ const resetter = timer(host);
1266
+ let query = "";
1267
+ const reset = () => {
1268
+ query = "";
1269
+ resetter.stop();
1270
+ };
1271
+ return {
1272
+ handle(event) {
1273
+ if (!printable(event)) return false;
1274
+ query = `${query}${event.key}`.toLowerCase();
1275
+ resetter.start(opts.delay?.() ?? 600, () => query = "");
1276
+ const text = opts.text ?? label$1;
1277
+ const match = opts.items().find((item) => text(item).toLowerCase().startsWith(query));
1278
+ if (match) opts.onMatch(match, event);
1279
+ return true;
1280
+ },
1281
+ reset
1282
+ };
1283
+ };
1284
+ //#endregion
1285
+ //#region packages/ajo-cloves/src/visibility.ts
1286
+ var start = (notify) => {
1287
+ document.addEventListener("visibilitychange", notify);
1288
+ return () => document.removeEventListener("visibilitychange", notify);
1289
+ };
1290
+ var read = () => document.visibilityState !== "hidden";
1291
+ /**
1292
+ * Reactive document visibility.
1293
+ *
1294
+ * @example
1295
+ * ```ts
1296
+ * const page = visibility(this)
1297
+ * yield <span>{page.visible ? 'visible' : 'hidden'}</span>
1298
+ * ```
1299
+ */
1300
+ var visibility = (host) => {
1301
+ if (!dom(host)) return { get visible() {
1302
+ return true;
1303
+ } };
1304
+ let current = read();
1305
+ shared("visibility", start, () => {
1306
+ const next = read();
1307
+ if (next === current) return;
1308
+ host.next(() => {
1309
+ current = next;
1310
+ });
1311
+ }, host.signal);
1312
+ return { get visible() {
1313
+ return current;
1314
+ } };
1315
+ };
1316
+ //#endregion
1317
+ export { announce, browser, callHandler, callRef, clamp, controlled, dismiss, dom, frame, grid, hotkey, hover, id, indicator, label, listen, media, move, overflow, remember, resize, restore, roving, scheme, scrolling, selection, shared, spin, statefulRootAttrs, storage, timer, typeahead, visibility };