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/LICENSE +15 -0
- package/README.md +149 -0
- package/dist/index.js +1317 -0
- package/package.json +52 -0
- package/src/announce.ts +61 -0
- package/src/controlled.ts +44 -0
- package/src/core.ts +254 -0
- package/src/dismiss.ts +53 -0
- package/src/grid.ts +52 -0
- package/src/hotkey.ts +56 -0
- package/src/hover.ts +58 -0
- package/src/index.ts +36 -0
- package/src/indicator.ts +122 -0
- package/src/label.ts +171 -0
- package/src/media.ts +95 -0
- package/src/move.ts +185 -0
- package/src/overflow.ts +87 -0
- package/src/resize.ts +93 -0
- package/src/restore.ts +29 -0
- package/src/roving.ts +77 -0
- package/src/scheme.ts +25 -0
- package/src/scrolling.ts +40 -0
- package/src/selection.ts +55 -0
- package/src/spin.ts +48 -0
- package/src/storage.ts +146 -0
- package/src/timer.ts +70 -0
- package/src/typeahead.ts +50 -0
- package/src/visibility.ts +45 -0
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 };
|