@nuykon/basecoat-react 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,832 @@
1
+ // src/chart.ts
2
+ function attachCanvasChart(canvas, config) {
3
+ const doc = canvas.ownerDocument, win = doc.defaultView, ctx = canvas.getContext("2d");
4
+ if (!ctx) return () => {
5
+ };
6
+ const data = config.data ?? [], series = Object.entries(config.series ?? {}), labelKey = config.labelKey ?? "label";
7
+ const color = (value) => value.startsWith("var(") ? win.getComputedStyle(canvas).getPropertyValue(value.slice(4, -1)).trim() || "#888" : value;
8
+ const legend = doc.createElement("ul");
9
+ legend.className = "chart-legend";
10
+ if (config.legend) {
11
+ for (const [index, [key, options]] of series.entries()) {
12
+ const item = doc.createElement("li"), content = doc.createElement("span"), indicator = doc.createElement("span");
13
+ content.className = "chart-legend-item";
14
+ indicator.className = "chart-legend-indicator";
15
+ indicator.style.setProperty(
16
+ "--chart-indicator-color",
17
+ options.color ?? `var(--chart-${index + 1})`
18
+ );
19
+ content.append(indicator, doc.createTextNode(options.label ?? key));
20
+ item.append(content);
21
+ legend.append(item);
22
+ }
23
+ canvas.after(legend);
24
+ }
25
+ canvas.style.width = "100%";
26
+ canvas.style.aspectRatio = "16 / 9";
27
+ canvas.style.display = "block";
28
+ const draw = () => {
29
+ const width = Math.max(200, canvas.parentElement?.clientWidth ?? 400), height = width * 9 / 16, ratio = win.devicePixelRatio || 1;
30
+ canvas.width = Math.round(width * ratio);
31
+ canvas.height = Math.round(height * ratio);
32
+ canvas.style.height = `${height}px`;
33
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
34
+ ctx.clearRect(0, 0, width, height);
35
+ const padding = { left: 30, top: 10, right: 10, bottom: 30 }, w = width - padding.left - padding.right, h = height - padding.top - padding.bottom;
36
+ const max = Math.max(
37
+ 1,
38
+ ...data.map(
39
+ (row) => config.options?.scales?.y?.stacked ? series.reduce((sum, [key]) => sum + Number(row[key] ?? 0), 0) : Math.max(...series.map(([key]) => Number(row[key] ?? 0)))
40
+ )
41
+ );
42
+ const x = (index) => padding.left + index / Math.max(1, data.length - 1) * w, y = (value) => padding.top + h - value / max * h;
43
+ ctx.font = '12px "Geist Sans",system-ui';
44
+ ctx.fillStyle = color("var(--muted-foreground)");
45
+ ctx.strokeStyle = color("var(--border)");
46
+ ctx.lineWidth = 0.5;
47
+ if (config.type !== "doughnut" && config.type !== "radar") {
48
+ for (let index = 0; index <= 4; index++) {
49
+ const top = padding.top + index * h / 4;
50
+ ctx.beginPath();
51
+ ctx.moveTo(padding.left, top);
52
+ ctx.lineTo(width - padding.right, top);
53
+ ctx.stroke();
54
+ ctx.fillText(String(Math.round(max * (4 - index) / 4)), 0, top + 4);
55
+ }
56
+ for (let index = 0; index < data.length; index++)
57
+ if (index % Math.max(1, Math.ceil(data.length / 6)) === 0)
58
+ ctx.fillText(String(data[index][labelKey] ?? ""), x(index) - 10, height - 8);
59
+ }
60
+ if (config.type === "doughnut") {
61
+ const key = series[0]?.[0] ?? "value", values = data.map((row) => Number(row[key] ?? 0)), total = values.reduce((a, b) => a + b, 0) || 1, radius = Math.min(w, h) * 0.45, cx2 = width / 2, cy = height / 2;
62
+ let start = -Math.PI / 2;
63
+ values.forEach((value, index) => {
64
+ const end = start + value / total * Math.PI * 2;
65
+ ctx.beginPath();
66
+ ctx.arc(cx2, cy, radius, start, end);
67
+ ctx.arc(cx2, cy, radius * 0.62, end, start, true);
68
+ ctx.closePath();
69
+ ctx.fillStyle = color(`var(--chart-${index % 5 + 1})`);
70
+ ctx.fill();
71
+ start = end;
72
+ });
73
+ } else if (config.type === "radar") {
74
+ const radius = Math.min(w, h) * 0.4, cx2 = width / 2, cy = height / 2;
75
+ const point = (index, scale) => {
76
+ const angle = index / Math.max(1, data.length) * Math.PI * 2 - Math.PI / 2;
77
+ return [
78
+ cx2 + Math.cos(angle) * radius * scale,
79
+ cy + Math.sin(angle) * radius * scale
80
+ ];
81
+ };
82
+ for (let ring = 1; ring <= 4; ring++) {
83
+ ctx.beginPath();
84
+ data.forEach((_, i) => {
85
+ const [x2, y2] = point(i, ring / 4);
86
+ if (i === 0) ctx.moveTo(x2, y2);
87
+ else ctx.lineTo(x2, y2);
88
+ });
89
+ ctx.closePath();
90
+ ctx.stroke();
91
+ }
92
+ series.forEach(([key, options], index) => {
93
+ ctx.beginPath();
94
+ data.forEach((row, i) => {
95
+ const [x2, y2] = point(i, Number(row[key] ?? 0) / max);
96
+ if (i === 0) ctx.moveTo(x2, y2);
97
+ else ctx.lineTo(x2, y2);
98
+ });
99
+ ctx.closePath();
100
+ ctx.strokeStyle = color(options.color ?? `var(--chart-${index + 1})`);
101
+ ctx.stroke();
102
+ ctx.globalAlpha = 0.2;
103
+ ctx.fillStyle = ctx.strokeStyle;
104
+ ctx.fill();
105
+ ctx.globalAlpha = 1;
106
+ });
107
+ ctx.fillStyle = color("var(--muted-foreground)");
108
+ data.forEach((row, i) => {
109
+ const [x2, y2] = point(i, 1.15);
110
+ ctx.fillText(String(row[labelKey] ?? ""), x2 - 15, y2);
111
+ });
112
+ } else {
113
+ const stacks2 = data.map(() => 0);
114
+ series.forEach(([key, options], seriesIndex) => {
115
+ const fill = color(options.color ?? `var(--chart-${seriesIndex + 1})`);
116
+ ctx.strokeStyle = fill;
117
+ ctx.fillStyle = fill;
118
+ ctx.lineWidth = 1.5;
119
+ if (config.type === "bar")
120
+ data.forEach((row, index) => {
121
+ const value = Number(row[key] ?? 0), stacked = config.options?.scales?.y?.stacked, group = w / Math.max(1, data.length), barWidth = group * 0.65 / (stacked ? 1 : series.length), left = padding.left + index * group + group * 0.15 + (stacked ? 0 : seriesIndex * barWidth), base = stacks2[index];
122
+ ctx.beginPath();
123
+ ctx.roundRect(
124
+ left,
125
+ y(value + base),
126
+ Math.max(1, barWidth - 2),
127
+ Math.max(0, y(base) - y(value + base)),
128
+ 3
129
+ );
130
+ ctx.fill();
131
+ if (stacked) stacks2[index] += value;
132
+ });
133
+ else {
134
+ ctx.beginPath();
135
+ data.forEach((row, index) => {
136
+ const top = y(Number(row[key] ?? 0));
137
+ if (index === 0) ctx.moveTo(x(index), top);
138
+ else if (options.dataset?.stepped) {
139
+ ctx.lineTo(x(index), y(Number(data[index - 1][key] ?? 0)));
140
+ ctx.lineTo(x(index), top);
141
+ } else ctx.lineTo(x(index), top);
142
+ });
143
+ ctx.stroke();
144
+ if (options.dataset?.fill || options.surface === "gradient") {
145
+ ctx.lineTo(x(data.length - 1), y(0));
146
+ ctx.lineTo(x(0), y(0));
147
+ ctx.closePath();
148
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
149
+ gradient.addColorStop(0, fill);
150
+ gradient.addColorStop(1, "transparent");
151
+ ctx.globalAlpha = 0.3;
152
+ ctx.fillStyle = gradient;
153
+ ctx.fill();
154
+ ctx.globalAlpha = 1;
155
+ }
156
+ }
157
+ });
158
+ }
159
+ };
160
+ const resize = new win.ResizeObserver(draw), themes = new win.MutationObserver(draw);
161
+ resize.observe(canvas.parentElement ?? canvas);
162
+ themes.observe(doc.documentElement, {
163
+ attributes: true,
164
+ attributeFilter: ["class", "data-style-variant"]
165
+ });
166
+ doc.addEventListener("basecoat:stylechange", draw);
167
+ draw();
168
+ return () => {
169
+ resize.disconnect();
170
+ themes.disconnect();
171
+ legend.remove();
172
+ doc.removeEventListener("basecoat:stylechange", draw);
173
+ };
174
+ }
175
+
176
+ // src/widgets.ts
177
+ function attachWidget(root) {
178
+ const abort = new AbortController(), signal = abort.signal, cleanups = [];
179
+ const on = (element, name, fn) => element?.addEventListener(name, fn, { signal });
180
+ const emit = (detail) => root.dispatchEvent(new CustomEvent("basecoat:change", { bubbles: true, detail }));
181
+ if (root instanceof HTMLCanvasElement && root.dataset.chartConfig)
182
+ cleanups.push(attachCanvasChart(root, JSON.parse(root.dataset.chartConfig)));
183
+ if (root.classList.contains("sidebar")) {
184
+ const media = root.ownerDocument.defaultView.matchMedia("(min-width: 768px)");
185
+ root.dataset.sidebarInitialized = "true";
186
+ const set = (open) => root.setAttribute("aria-hidden", String(!open));
187
+ const toggle = () => set(root.getAttribute("aria-hidden") === "true");
188
+ const sidebar = root;
189
+ sidebar.toggle = toggle;
190
+ set(media.matches);
191
+ on(media, "change", () => set(media.matches));
192
+ on(root, "click", (event) => {
193
+ if (event.target === root && !media.matches) set(false);
194
+ });
195
+ on(root.ownerDocument, "keydown", (event) => {
196
+ if (event.key === "Escape" && !media.matches) set(false);
197
+ });
198
+ cleanups.push(() => {
199
+ delete sidebar.toggle;
200
+ delete root.dataset.sidebarInitialized;
201
+ });
202
+ }
203
+ if (root.classList.contains("toaster")) {
204
+ const timers = /* @__PURE__ */ new Set();
205
+ const toast = (message) => {
206
+ const item = root.ownerDocument.createElement("div");
207
+ item.className = "toast";
208
+ item.setAttribute("role", "status");
209
+ const content = root.ownerDocument.createElement("div");
210
+ content.className = "toast-content";
211
+ const section = root.ownerDocument.createElement("section"), heading = root.ownerDocument.createElement("h2");
212
+ heading.textContent = message.title ?? "";
213
+ section.append(heading);
214
+ if (message.description) {
215
+ const description = root.ownerDocument.createElement("p");
216
+ description.textContent = message.description;
217
+ section.append(description);
218
+ }
219
+ const footer = root.ownerDocument.createElement("footer"), close = root.ownerDocument.createElement("button");
220
+ close.type = "button";
221
+ close.className = "btn";
222
+ close.dataset.variant = "outline";
223
+ close.dataset.toastCancel = "";
224
+ close.textContent = message.cancel?.label ?? "Dismiss";
225
+ close.addEventListener("click", () => item.remove(), { signal });
226
+ footer.append(close);
227
+ if (message.action) {
228
+ const action = root.ownerDocument.createElement("button");
229
+ action.type = "button";
230
+ action.className = "btn";
231
+ action.textContent = message.action.label ?? "Action";
232
+ action.addEventListener(
233
+ "click",
234
+ () => {
235
+ message.action?.onclick?.();
236
+ item.remove();
237
+ },
238
+ { signal }
239
+ );
240
+ footer.append(action);
241
+ }
242
+ content.append(section, footer);
243
+ item.append(content);
244
+ root.append(item);
245
+ if ((message.duration ?? 5e3) > 0) {
246
+ const timer = setTimeout(() => {
247
+ item.remove();
248
+ timers.delete(timer);
249
+ }, message.duration ?? 5e3);
250
+ timers.add(timer);
251
+ }
252
+ return item;
253
+ };
254
+ root.toast = toast;
255
+ on(root, "click", (event) => {
256
+ const button = event.target.closest("[data-toast-cancel]");
257
+ button?.closest(".toast")?.remove();
258
+ });
259
+ cleanups.push(() => {
260
+ timers.forEach(clearTimeout);
261
+ delete root.toast;
262
+ });
263
+ }
264
+ if (root.classList.contains("tabs")) {
265
+ const tabs = [...root.querySelectorAll("[role=tab]")].filter(
266
+ (tab) => tab.closest(".tabs") === root
267
+ );
268
+ const set = (target) => {
269
+ if (target.disabled || target.getAttribute("aria-disabled") === "true") return;
270
+ for (const tab of tabs) {
271
+ const active = tab === target;
272
+ tab.setAttribute("aria-selected", String(active));
273
+ tab.tabIndex = active ? 0 : -1;
274
+ const panel = root.ownerDocument.getElementById(tab.getAttribute("aria-controls") ?? "");
275
+ if (panel) {
276
+ panel.hidden = !active;
277
+ panel.setAttribute("aria-selected", String(active));
278
+ }
279
+ }
280
+ emit(target.id);
281
+ };
282
+ for (const [index, tab] of tabs.entries()) {
283
+ on(tab, "click", () => set(tab));
284
+ on(tab, "keydown", (event) => {
285
+ const key = event.key, vertical = tab.parentElement?.getAttribute("aria-orientation") === "vertical";
286
+ if (!(vertical ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"]).includes(key) && !["Home", "End"].includes(key))
287
+ return;
288
+ event.preventDefault();
289
+ const target = tabs[nextIndex(
290
+ tabs.map((item) => ({
291
+ disabled: item.disabled || item.getAttribute("aria-disabled") === "true"
292
+ })),
293
+ index,
294
+ key
295
+ )];
296
+ target?.focus();
297
+ if (target) set(target);
298
+ });
299
+ }
300
+ }
301
+ if (root.classList.contains("accordion")) {
302
+ const summaries = [...root.querySelectorAll("summary")];
303
+ on(root, "keydown", (event) => {
304
+ const index = summaries.indexOf(event.target), key = event.key;
305
+ if (index >= 0 && ["ArrowUp", "ArrowDown", "Home", "End"].includes(key)) {
306
+ event.preventDefault();
307
+ summaries[nextIndex(
308
+ summaries.map((el) => ({ disabled: el.getAttribute("aria-disabled") === "true" })),
309
+ index,
310
+ key
311
+ )]?.focus();
312
+ }
313
+ });
314
+ on(root, "click", (event) => {
315
+ const summary = event.target.closest("summary");
316
+ if (summary?.getAttribute("aria-disabled") === "true" || summary?.parentElement?.getAttribute("aria-disabled") === "true")
317
+ event.preventDefault();
318
+ });
319
+ }
320
+ const kind = ["select", "combobox", "dropdown-menu", "popover"].find(
321
+ (kind2) => root.classList.contains(kind2)
322
+ );
323
+ if (kind) {
324
+ const trigger = root.querySelector(
325
+ ":scope > button, :scope > input[role=combobox], :scope > .input-group input[role=combobox]"
326
+ ), popup = root.querySelector(":scope > [data-popover]");
327
+ const search = root.querySelector(
328
+ "[data-popover] input, input[role=combobox]"
329
+ );
330
+ if (trigger && popup) {
331
+ let open = false, active = -1, disposePosition, disposeLayer;
332
+ const selector = kind === "dropdown-menu" ? "[role=menuitem], [role=menuitemcheckbox], [role=menuitemradio]" : "[role=option]";
333
+ const items = () => [...popup.querySelectorAll(selector)].filter(
334
+ (item) => !item.hidden && item.getAttribute("aria-hidden") !== "true"
335
+ );
336
+ const disabled = (item) => item.matches(":disabled") || item.getAttribute("aria-disabled") === "true";
337
+ const cursor = () => items().map((item) => ({
338
+ value: item.dataset.value ?? item.id,
339
+ label: item.textContent?.trim() ?? "",
340
+ disabled: disabled(item)
341
+ }));
342
+ const typeahead = createTypeahead();
343
+ const highlight = (index) => {
344
+ active = index;
345
+ const all = items();
346
+ for (const [i, item2] of all.entries()) {
347
+ item2.classList.toggle("active", i === index);
348
+ if (kind === "dropdown-menu") item2.tabIndex = i === index ? 0 : -1;
349
+ }
350
+ const item = all[index];
351
+ if (item) {
352
+ if (kind === "dropdown-menu") item.focus();
353
+ else {
354
+ const list = popup.querySelector("[role=listbox]");
355
+ if (list) {
356
+ list.setAttribute("aria-activedescendant", item.id);
357
+ scrollWithin(list, item);
358
+ }
359
+ search?.setAttribute("aria-activedescendant", item.id);
360
+ }
361
+ }
362
+ };
363
+ const close = (restore = false) => {
364
+ if (!open) return;
365
+ open = false;
366
+ disposeLayer?.();
367
+ disposePosition?.();
368
+ trigger.setAttribute("aria-expanded", "false");
369
+ popup.setAttribute("aria-hidden", "true");
370
+ popup.hidden = true;
371
+ if (restore) trigger.focus();
372
+ };
373
+ const show = (last = false) => {
374
+ if (open || disabled(trigger)) return;
375
+ open = true;
376
+ popup.hidden = false;
377
+ popup.setAttribute("aria-hidden", "false");
378
+ popup.setAttribute("popover", "manual");
379
+ popup.classList.add("bc-floating");
380
+ trigger.setAttribute("aria-expanded", "true");
381
+ disposePosition = attachFloating(trigger, popup, {
382
+ side: popup.dataset.side,
383
+ align: popup.dataset.align,
384
+ matchWidth: kind === "select" || kind === "combobox"
385
+ });
386
+ disposeLayer = dismissableLayer(root.ownerDocument, {
387
+ elements: () => [trigger, popup],
388
+ dismiss: (reason) => close(reason === "escape")
389
+ });
390
+ const list = popup.querySelector("[role=listbox],[role=menu]");
391
+ if (list) {
392
+ list.tabIndex = -1;
393
+ (search ?? list).focus();
394
+ }
395
+ if (kind === "popover") focusFirst(popup);
396
+ else highlight(nextIndex(cursor(), -1, last ? "End" : "Home"));
397
+ };
398
+ const choose = (item) => {
399
+ if (disabled(item)) return;
400
+ if (kind === "select" || kind === "combobox") {
401
+ const multiple = popup.querySelector("[aria-multiselectable=true]") !== null;
402
+ if (!multiple)
403
+ items().forEach(
404
+ (option) => option.setAttribute("aria-selected", String(option === item))
405
+ );
406
+ else
407
+ item.setAttribute(
408
+ "aria-selected",
409
+ String(item.getAttribute("aria-selected") !== "true")
410
+ );
411
+ const selected = items().filter(
412
+ (option) => option.getAttribute("aria-selected") === "true"
413
+ ), values = selected.map(
414
+ (option) => option.dataset.value ?? option.textContent?.trim() ?? ""
415
+ );
416
+ if (trigger instanceof HTMLInputElement) trigger.value = item.textContent?.trim() ?? "";
417
+ else {
418
+ const label = trigger.querySelector("span");
419
+ if (label)
420
+ label.textContent = selected.map((option) => option.textContent?.trim()).join(", ");
421
+ }
422
+ const hidden = root.querySelector("input[type=hidden]");
423
+ if (hidden) {
424
+ hidden.value = multiple ? JSON.stringify(values) : values[0] ?? "";
425
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
426
+ }
427
+ emit(multiple ? values : values[0]);
428
+ if (!multiple) close(true);
429
+ } else if (kind === "dropdown-menu") {
430
+ if (item.getAttribute("role") === "menuitemcheckbox") {
431
+ item.setAttribute("aria-checked", String(item.getAttribute("aria-checked") !== "true"));
432
+ emit(item.id);
433
+ } else close(true);
434
+ }
435
+ };
436
+ popup.hidden = popup.getAttribute("aria-hidden") !== "false";
437
+ on(trigger, "click", () => open ? close() : show());
438
+ on(trigger, "keydown", (event) => {
439
+ const key = event.key;
440
+ if (!open && ["ArrowDown", "ArrowUp", "Enter", " "].includes(key)) {
441
+ event.preventDefault();
442
+ show(key === "ArrowUp");
443
+ }
444
+ });
445
+ on(popup, "keydown", (event) => {
446
+ const keyboard = event;
447
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(keyboard.key)) {
448
+ event.preventDefault();
449
+ highlight(nextIndex(cursor(), active, keyboard.key));
450
+ } else if (keyboard.key === "Enter" || !search && keyboard.key === " ") {
451
+ event.preventDefault();
452
+ const item = items()[active];
453
+ if (item) {
454
+ choose(item);
455
+ if (item instanceof HTMLAnchorElement) item.click();
456
+ }
457
+ } else if (keyboard.key === "Tab") close();
458
+ else if (!search && keyboard.key.length === 1 && !keyboard.ctrlKey && !keyboard.metaKey) {
459
+ event.preventDefault();
460
+ highlight(typeahead(keyboard.key, cursor(), active));
461
+ }
462
+ });
463
+ on(popup, "click", (event) => {
464
+ const item = event.target.closest(selector);
465
+ if (item) {
466
+ if (disabled(item)) event.preventDefault();
467
+ else choose(item);
468
+ }
469
+ });
470
+ on(popup, "pointermove", (event) => {
471
+ const pointer = event, item = event.target.closest(selector);
472
+ if (item && !disabled(item) && (pointer.movementX || pointer.movementY))
473
+ highlight(items().indexOf(item));
474
+ });
475
+ on(search, "input", () => {
476
+ for (const item of popup.querySelectorAll(selector)) {
477
+ const hidden = !item.textContent?.toLocaleLowerCase().includes(search.value.toLocaleLowerCase());
478
+ item.hidden = hidden;
479
+ item.setAttribute("aria-hidden", String(hidden));
480
+ }
481
+ if (!open) show();
482
+ highlight(nextIndex(cursor(), -1, "Home"));
483
+ });
484
+ cleanups.push(() => close());
485
+ }
486
+ }
487
+ if (root.classList.contains("command")) {
488
+ const input = root.querySelector("input"), list = root.querySelector("[role=menu],[role=listbox]");
489
+ let active = 0;
490
+ const items = () => [...list?.querySelectorAll("[role=menuitem],[role=option]") ?? []].filter(
491
+ (item) => !item.hidden
492
+ );
493
+ const refresh = () => {
494
+ const all = items();
495
+ all.forEach((item, index) => item.classList.toggle("active", index === active));
496
+ const current = all[active];
497
+ if (current) input?.setAttribute("aria-activedescendant", current.id);
498
+ };
499
+ on(input, "input", () => {
500
+ list?.querySelectorAll("[role=menuitem],[role=option]").forEach((item) => {
501
+ item.hidden = !item.textContent?.toLocaleLowerCase().includes(input.value.toLocaleLowerCase());
502
+ item.setAttribute("aria-hidden", String(item.hidden));
503
+ });
504
+ active = 0;
505
+ refresh();
506
+ });
507
+ on(input, "keydown", (event) => {
508
+ const key = event.key;
509
+ if (["ArrowUp", "ArrowDown", "Home", "End"].includes(key)) {
510
+ event.preventDefault();
511
+ active = nextIndex(
512
+ items().map((item) => ({ disabled: item.getAttribute("aria-disabled") === "true" })),
513
+ active,
514
+ key
515
+ );
516
+ refresh();
517
+ } else if (key === "Enter") {
518
+ event.preventDefault();
519
+ items()[active]?.click();
520
+ }
521
+ });
522
+ root.refresh = refresh;
523
+ }
524
+ if (root.matches("input[type=range]")) {
525
+ const range = root, update = () => range.style.setProperty(
526
+ "--slider-value",
527
+ `${percentage(range.valueAsNumber, Number(range.min || 0), Number(range.max || 100))}%`
528
+ );
529
+ on(range, "input", update);
530
+ update();
531
+ }
532
+ return () => {
533
+ abort.abort();
534
+ cleanups.forEach((cleanup) => cleanup());
535
+ };
536
+ }
537
+
538
+ // src/index.ts
539
+ var stylePacks = [
540
+ "vega",
541
+ "nova",
542
+ "maia",
543
+ "lyra",
544
+ "mira",
545
+ "luma",
546
+ "sera",
547
+ "rhea"
548
+ ];
549
+ function cx(...values) {
550
+ return values.filter(Boolean).join(" ");
551
+ }
552
+ function percentage(value, min = 0, max = 100) {
553
+ return Number.isFinite(value) && max > min ? Math.min(100, Math.max(0, (value - min) / (max - min) * 100)) : 0;
554
+ }
555
+ function nextIndex(items, current, key, loop = true) {
556
+ const enabled = items.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
557
+ if (!enabled.length) return -1;
558
+ if (key === "Home") return enabled[0];
559
+ if (key === "End") return enabled[enabled.length - 1];
560
+ const direction = key === "ArrowUp" || key === "ArrowLeft" ? -1 : 1;
561
+ const position = enabled.indexOf(current);
562
+ if (position < 0) return direction < 0 ? enabled[enabled.length - 1] : enabled[0];
563
+ const next = position + direction;
564
+ return enabled[loop ? (next + enabled.length) % enabled.length : Math.min(enabled.length - 1, Math.max(0, next))];
565
+ }
566
+ function createTypeahead() {
567
+ let buffer = "", last = 0;
568
+ return (key, items, current) => {
569
+ const now = Date.now();
570
+ buffer = now - last > 700 ? key : buffer + key;
571
+ last = now;
572
+ const query = [...buffer].every((char) => char === buffer[0]) ? buffer[0] : buffer;
573
+ for (let offset = 1; offset <= items.length; offset++) {
574
+ const index = (current + offset + items.length) % items.length;
575
+ if (!items[index].disabled && items[index].label.toLocaleLowerCase().startsWith(query.toLocaleLowerCase()))
576
+ return index;
577
+ }
578
+ return current;
579
+ };
580
+ }
581
+ var stacks = /* @__PURE__ */ new WeakMap();
582
+ function dismissableLayer(doc, layer) {
583
+ let stack = stacks.get(doc);
584
+ if (!stack) {
585
+ const layers = [];
586
+ const pointer = (event) => {
587
+ const top = layers.at(-1);
588
+ if (top && !top.elements().some((el) => el && event.composedPath().includes(el)))
589
+ top.dismiss("outside");
590
+ };
591
+ const keyboard = (event) => {
592
+ if (event.key === "Escape" && !event.defaultPrevented && layers.length) {
593
+ event.preventDefault();
594
+ event.stopPropagation();
595
+ layers.at(-1)?.dismiss("escape");
596
+ }
597
+ };
598
+ doc.addEventListener("pointerdown", pointer, true);
599
+ doc.addEventListener("keydown", keyboard);
600
+ stack = {
601
+ layers,
602
+ dispose: () => {
603
+ doc.removeEventListener("pointerdown", pointer, true);
604
+ doc.removeEventListener("keydown", keyboard);
605
+ }
606
+ };
607
+ stacks.set(doc, stack);
608
+ }
609
+ stack.layers.push(layer);
610
+ return () => {
611
+ const index = stack.layers.indexOf(layer);
612
+ if (index >= 0) stack.layers.splice(index, 1);
613
+ if (!stack.layers.length) {
614
+ stack.dispose();
615
+ stacks.delete(doc);
616
+ }
617
+ };
618
+ }
619
+ function floatingPosition(anchor, popup, width, height, options = {}) {
620
+ let side = options.side ?? "bottom";
621
+ const align = options.align ?? "start", offset = options.offset ?? 6, padding = 8;
622
+ const available = {
623
+ bottom: height - anchor.bottom,
624
+ top: anchor.top,
625
+ right: width - anchor.right,
626
+ left: anchor.left
627
+ };
628
+ const opposite = {
629
+ top: "bottom",
630
+ bottom: "top",
631
+ left: "right",
632
+ right: "left"
633
+ };
634
+ const required = (side === "top" || side === "bottom" ? popup.height : popup.width) + offset + padding;
635
+ if (available[side] < required && available[opposite[side]] > available[side])
636
+ side = opposite[side];
637
+ const vertical = side === "top" || side === "bottom";
638
+ let x = vertical ? anchor.left : side === "left" ? anchor.left - popup.width - offset : anchor.right + offset;
639
+ let y = vertical ? side === "top" ? anchor.top - popup.height - offset : anchor.bottom + offset : anchor.top;
640
+ if (vertical)
641
+ x += align === "end" ? anchor.width - popup.width : align === "center" ? (anchor.width - popup.width) / 2 : 0;
642
+ else
643
+ y += align === "end" ? anchor.height - popup.height : align === "center" ? (anchor.height - popup.height) / 2 : 0;
644
+ return {
645
+ x: Math.max(padding, Math.min(x, width - popup.width - padding)),
646
+ y: Math.max(padding, Math.min(y, height - popup.height - padding)),
647
+ side,
648
+ maxHeight: Math.max(40, available[side] - offset - padding)
649
+ };
650
+ }
651
+ function attachFloating(anchor, popup, options = {}) {
652
+ const win = anchor.ownerDocument.defaultView;
653
+ if (typeof popup.showPopover === "function") popup.showPopover();
654
+ const update = () => {
655
+ const rect = anchor.getBoundingClientRect();
656
+ if (options.matchWidth) popup.style.minWidth = `${rect.width}px`;
657
+ const viewport = win.visualViewport;
658
+ const result = floatingPosition(
659
+ rect,
660
+ popup.getBoundingClientRect(),
661
+ viewport?.width ?? win.innerWidth,
662
+ viewport?.height ?? win.innerHeight,
663
+ options
664
+ );
665
+ Object.assign(popup.style, {
666
+ left: `${result.x + (viewport?.offsetLeft ?? 0)}px`,
667
+ top: `${result.y + (viewport?.offsetTop ?? 0)}px`,
668
+ maxHeight: `${result.maxHeight}px`
669
+ });
670
+ popup.dataset.side = result.side;
671
+ };
672
+ let frame = 0;
673
+ const schedule = () => {
674
+ win.cancelAnimationFrame(frame);
675
+ frame = win.requestAnimationFrame(update);
676
+ };
677
+ const observer = typeof win.ResizeObserver === "function" ? new win.ResizeObserver(schedule) : null;
678
+ observer?.observe(anchor);
679
+ observer?.observe(popup);
680
+ win.addEventListener("resize", schedule);
681
+ win.addEventListener("scroll", schedule, true);
682
+ win.visualViewport?.addEventListener("resize", schedule);
683
+ win.visualViewport?.addEventListener("scroll", schedule);
684
+ update();
685
+ return () => {
686
+ win.cancelAnimationFrame(frame);
687
+ observer?.disconnect();
688
+ win.removeEventListener("resize", schedule);
689
+ win.removeEventListener("scroll", schedule, true);
690
+ win.visualViewport?.removeEventListener("resize", schedule);
691
+ win.visualViewport?.removeEventListener("scroll", schedule);
692
+ if (typeof popup.hidePopover === "function" && popup.matches(":popover-open"))
693
+ popup.hidePopover();
694
+ };
695
+ }
696
+ var scrollLocks = /* @__PURE__ */ new WeakMap();
697
+ function lockScroll(doc) {
698
+ const record = scrollLocks.get(doc) ?? { count: 0, overflow: doc.documentElement.style.overflow };
699
+ record.count++;
700
+ scrollLocks.set(doc, record);
701
+ doc.documentElement.style.overflow = "hidden";
702
+ let disposed = false;
703
+ return () => {
704
+ if (disposed) return;
705
+ disposed = true;
706
+ if (--record.count === 0) {
707
+ doc.documentElement.style.overflow = record.overflow;
708
+ scrollLocks.delete(doc);
709
+ }
710
+ };
711
+ }
712
+ function focusFirst(root) {
713
+ const target = root.querySelector(
714
+ '[autofocus],button:not(:disabled),[href],input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex="0"]'
715
+ );
716
+ (target ?? root).focus({ preventScroll: true });
717
+ }
718
+ function scrollWithin(container, item) {
719
+ const parent = container.scrollHeight > container.clientHeight ? container : container.parentElement ?? container;
720
+ const outer = parent.getBoundingClientRect(), inner = item.getBoundingClientRect();
721
+ if (inner.top < outer.top) parent.scrollTop -= outer.top - inner.top;
722
+ else if (inner.bottom > outer.bottom) parent.scrollTop += inner.bottom - outer.bottom;
723
+ }
724
+ function formReset(element, reset) {
725
+ const form = element.closest("form");
726
+ let timer;
727
+ const listener = (event) => {
728
+ clearTimeout(timer);
729
+ timer = setTimeout(() => {
730
+ if (!event.defaultPrevented) reset();
731
+ }, 0);
732
+ };
733
+ form?.addEventListener("reset", listener);
734
+ return () => {
735
+ clearTimeout(timer);
736
+ form?.removeEventListener("reset", listener);
737
+ };
738
+ }
739
+ var primitiveDefinitions = {
740
+ ChartRoot: ["canvas", "chart-canvas"],
741
+ SelectRoot: ["div", "select"],
742
+ ComboboxRoot: ["div", "combobox"],
743
+ DropdownMenuRoot: ["div", "dropdown-menu"],
744
+ PopoverRoot: ["div", "popover"],
745
+ TabsRoot: ["div", "tabs"],
746
+ AccordionRoot: ["div", "accordion"],
747
+ CommandRoot: ["div", "command"],
748
+ SidebarRoot: ["div", "sidebar"],
749
+ DialogRoot: ["dialog", "dialog"],
750
+ DrawerRoot: ["dialog", "drawer"],
751
+ AlertDialogRoot: ["dialog", "alert-dialog"],
752
+ Button: ["button", "btn"],
753
+ Badge: ["span", "badge"],
754
+ Card: ["article", "card"],
755
+ CardHeader: ["header", ""],
756
+ CardTitle: ["h3", "card-title"],
757
+ CardDescription: ["p", ""],
758
+ CardContent: ["section", ""],
759
+ CardFooter: ["footer", ""],
760
+ CardAction: ["div", "card-action"],
761
+ Alert: ["div", "alert"],
762
+ AlertTitle: ["h2", ""],
763
+ AlertDescription: ["section", ""],
764
+ Avatar: ["span", "avatar"],
765
+ AvatarImage: ["img", ""],
766
+ AvatarFallback: ["span", ""],
767
+ Breadcrumb: ["nav", "breadcrumb"],
768
+ BreadcrumbList: ["ol", ""],
769
+ BreadcrumbItem: ["li", ""],
770
+ BreadcrumbLink: ["a", ""],
771
+ BreadcrumbSeparator: ["li", ""],
772
+ ButtonGroup: ["div", "button-group"],
773
+ Field: ["div", "field"],
774
+ FieldLabel: ["label", "label"],
775
+ FieldDescription: ["p", ""],
776
+ FieldError: ["p", ""],
777
+ Label: ["label", "label"],
778
+ Input: ["input", "input"],
779
+ Textarea: ["textarea", "textarea"],
780
+ NativeSelect: ["select", "select"],
781
+ InputGroup: ["div", "input-group"],
782
+ InputGroupAddon: ["div", ""],
783
+ Empty: ["div", "empty"],
784
+ EmptyHeader: ["header", ""],
785
+ EmptyTitle: ["h3", ""],
786
+ EmptyDescription: ["p", ""],
787
+ EmptyContent: ["section", ""],
788
+ Item: ["div", "item"],
789
+ ItemContent: ["section", ""],
790
+ ItemTitle: ["h3", ""],
791
+ ItemDescription: ["p", ""],
792
+ ItemActions: ["div", ""],
793
+ Kbd: ["kbd", "kbd"],
794
+ ScrollArea: ["div", "scrollbar"],
795
+ Skeleton: ["div", "skeleton"],
796
+ Spinner: ["span", "bc-spinner"],
797
+ Separator: ["hr", ""],
798
+ Table: ["table", "table"],
799
+ TableHeader: ["thead", ""],
800
+ TableBody: ["tbody", ""],
801
+ TableFooter: ["tfoot", ""],
802
+ TableRow: ["tr", ""],
803
+ TableHead: ["th", ""],
804
+ TableCell: ["td", ""],
805
+ TableCaption: ["caption", ""],
806
+ DialogHeader: ["header", ""],
807
+ DialogTitle: ["h2", ""],
808
+ DialogDescription: ["p", ""],
809
+ DialogContent: ["section", ""],
810
+ DialogFooter: ["footer", ""],
811
+ SidebarHeader: ["header", ""],
812
+ SidebarContent: ["section", ""],
813
+ SidebarFooter: ["footer", ""],
814
+ SidebarGroup: ["section", ""],
815
+ SidebarItem: ["a", ""]
816
+ };
817
+ export {
818
+ attachFloating,
819
+ attachWidget,
820
+ createTypeahead,
821
+ cx,
822
+ dismissableLayer,
823
+ floatingPosition,
824
+ focusFirst,
825
+ formReset,
826
+ lockScroll,
827
+ nextIndex,
828
+ percentage,
829
+ primitiveDefinitions,
830
+ scrollWithin,
831
+ stylePacks
832
+ };