@vertz/ui-primitives 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1215 @@
1
+ // src/accordion/accordion.ts
2
+ import { signal } from "@vertz/ui";
3
+
4
+ // src/utils/aria.ts
5
+ function setExpanded(el, expanded) {
6
+ el.setAttribute("aria-expanded", String(expanded));
7
+ }
8
+ function setSelected(el, selected) {
9
+ el.setAttribute("aria-selected", String(selected));
10
+ }
11
+ function setHidden(el, hidden) {
12
+ el.setAttribute("aria-hidden", String(hidden));
13
+ el.style.display = hidden ? "none" : "";
14
+ }
15
+ function setChecked(el, checked) {
16
+ el.setAttribute("aria-checked", String(checked));
17
+ }
18
+ function setDisabled(el, disabled) {
19
+ el.setAttribute("aria-disabled", String(disabled));
20
+ }
21
+ function setDataState(el, state) {
22
+ el.setAttribute("data-state", state);
23
+ }
24
+ function setLabelledBy(el, labelId) {
25
+ el.setAttribute("aria-labelledby", labelId);
26
+ }
27
+ function setDescribedBy(el, descriptionId) {
28
+ el.setAttribute("aria-describedby", descriptionId);
29
+ }
30
+ function setValueRange(el, now, min, max) {
31
+ el.setAttribute("aria-valuenow", String(now));
32
+ el.setAttribute("aria-valuemin", String(min));
33
+ el.setAttribute("aria-valuemax", String(max));
34
+ }
35
+
36
+ // src/utils/id.ts
37
+ var counter = 0;
38
+ function uniqueId(prefix = "vz") {
39
+ return `${prefix}-${++counter}`;
40
+ }
41
+ function linkedIds(prefix = "vz") {
42
+ const base = uniqueId(prefix);
43
+ return {
44
+ triggerId: `${base}-trigger`,
45
+ contentId: `${base}-content`
46
+ };
47
+ }
48
+
49
+ // src/utils/keyboard.ts
50
+ var Keys = {
51
+ Enter: "Enter",
52
+ Space: " ",
53
+ Escape: "Escape",
54
+ ArrowUp: "ArrowUp",
55
+ ArrowDown: "ArrowDown",
56
+ ArrowLeft: "ArrowLeft",
57
+ ArrowRight: "ArrowRight",
58
+ Home: "Home",
59
+ End: "End",
60
+ Tab: "Tab"
61
+ };
62
+ function isKey(event, ...keys) {
63
+ return keys.includes(event.key);
64
+ }
65
+ function handleListNavigation(event, items, options = {}) {
66
+ const { orientation = "vertical", loop = true } = options;
67
+ if (items.length === 0)
68
+ return null;
69
+ const prevKey = orientation === "vertical" ? Keys.ArrowUp : Keys.ArrowLeft;
70
+ const nextKey = orientation === "vertical" ? Keys.ArrowDown : Keys.ArrowRight;
71
+ const currentIndex = items.indexOf(document.activeElement);
72
+ let nextIndex = -1;
73
+ if (isKey(event, prevKey)) {
74
+ event.preventDefault();
75
+ if (currentIndex <= 0) {
76
+ nextIndex = loop ? items.length - 1 : 0;
77
+ } else {
78
+ nextIndex = currentIndex - 1;
79
+ }
80
+ } else if (isKey(event, nextKey)) {
81
+ event.preventDefault();
82
+ if (currentIndex >= items.length - 1) {
83
+ nextIndex = loop ? 0 : items.length - 1;
84
+ } else {
85
+ nextIndex = currentIndex + 1;
86
+ }
87
+ } else if (isKey(event, Keys.Home)) {
88
+ event.preventDefault();
89
+ nextIndex = 0;
90
+ } else if (isKey(event, Keys.End)) {
91
+ event.preventDefault();
92
+ nextIndex = items.length - 1;
93
+ }
94
+ const target = items[nextIndex];
95
+ if (target) {
96
+ target.focus();
97
+ return target;
98
+ }
99
+ return null;
100
+ }
101
+ function handleActivation(event, handler) {
102
+ if (isKey(event, Keys.Enter, Keys.Space)) {
103
+ event.preventDefault();
104
+ handler();
105
+ }
106
+ }
107
+
108
+ // src/accordion/accordion.ts
109
+ var Accordion = {
110
+ Root(options = {}) {
111
+ const { multiple = false, defaultValue = [], onValueChange } = options;
112
+ const state = { value: signal([...defaultValue]) };
113
+ const triggers = [];
114
+ const root = document.createElement("div");
115
+ root.setAttribute("data-orientation", "vertical");
116
+ function toggleItem(value) {
117
+ const current = [...state.value.peek()];
118
+ const idx = current.indexOf(value);
119
+ if (idx >= 0) {
120
+ current.splice(idx, 1);
121
+ } else {
122
+ if (multiple) {
123
+ current.push(value);
124
+ } else {
125
+ current.length = 0;
126
+ current.push(value);
127
+ }
128
+ }
129
+ state.value.value = current;
130
+ onValueChange?.(current);
131
+ }
132
+ root.addEventListener("keydown", (event) => {
133
+ if (isKey(event, Keys.ArrowUp, Keys.ArrowDown, Keys.Home, Keys.End)) {
134
+ handleListNavigation(event, triggers, { orientation: "vertical" });
135
+ }
136
+ });
137
+ function Item(value) {
138
+ const baseId = uniqueId("accordion");
139
+ const triggerId = `${baseId}-trigger`;
140
+ const contentId = `${baseId}-content`;
141
+ const isOpen = state.value.peek().includes(value);
142
+ const item = document.createElement("div");
143
+ item.setAttribute("data-value", value);
144
+ const trigger = document.createElement("button");
145
+ trigger.setAttribute("type", "button");
146
+ trigger.id = triggerId;
147
+ trigger.setAttribute("aria-controls", contentId);
148
+ trigger.setAttribute("data-value", value);
149
+ setExpanded(trigger, isOpen);
150
+ setDataState(trigger, isOpen ? "open" : "closed");
151
+ const content = document.createElement("div");
152
+ content.setAttribute("role", "region");
153
+ content.id = contentId;
154
+ content.setAttribute("aria-labelledby", triggerId);
155
+ setHidden(content, !isOpen);
156
+ setDataState(content, isOpen ? "open" : "closed");
157
+ trigger.addEventListener("click", () => {
158
+ toggleItem(value);
159
+ const nowOpen = state.value.peek().includes(value);
160
+ setExpanded(trigger, nowOpen);
161
+ setHidden(content, !nowOpen);
162
+ setDataState(trigger, nowOpen ? "open" : "closed");
163
+ setDataState(content, nowOpen ? "open" : "closed");
164
+ });
165
+ triggers.push(trigger);
166
+ item.appendChild(trigger);
167
+ item.appendChild(content);
168
+ root.appendChild(item);
169
+ return { item, trigger, content };
170
+ }
171
+ return { root, state, Item };
172
+ }
173
+ };
174
+ // src/button/button.ts
175
+ import { signal as signal2 } from "@vertz/ui";
176
+ function createButtonRoot(state, options) {
177
+ const el = document.createElement("button");
178
+ el.setAttribute("type", "button");
179
+ el.setAttribute("role", "button");
180
+ setDataState(el, "idle");
181
+ if (options.disabled) {
182
+ el.disabled = true;
183
+ setDisabled(el, true);
184
+ }
185
+ el.addEventListener("click", () => {
186
+ if (state.disabled.peek())
187
+ return;
188
+ state.pressed.value = true;
189
+ setDataState(el, "pressed");
190
+ options.onPress?.();
191
+ queueMicrotask(() => {
192
+ state.pressed.value = false;
193
+ setDataState(el, "idle");
194
+ });
195
+ });
196
+ el.addEventListener("keydown", (event) => {
197
+ if (state.disabled.peek())
198
+ return;
199
+ handleActivation(event, () => {
200
+ el.click();
201
+ });
202
+ });
203
+ return el;
204
+ }
205
+ var Button = {
206
+ Root(options = {}) {
207
+ const state = {
208
+ disabled: signal2(options.disabled ?? false),
209
+ pressed: signal2(false)
210
+ };
211
+ const root = createButtonRoot(state, options);
212
+ return { root, state };
213
+ }
214
+ };
215
+ // src/checkbox/checkbox.ts
216
+ import { signal as signal3 } from "@vertz/ui";
217
+ function dataStateFor(checked) {
218
+ if (checked === "mixed")
219
+ return "indeterminate";
220
+ return checked ? "checked" : "unchecked";
221
+ }
222
+ var Checkbox = {
223
+ Root(options = {}) {
224
+ const { defaultChecked = false, disabled = false, onCheckedChange } = options;
225
+ const state = {
226
+ checked: signal3(defaultChecked),
227
+ disabled: signal3(disabled)
228
+ };
229
+ const root = document.createElement("button");
230
+ root.setAttribute("type", "button");
231
+ root.setAttribute("role", "checkbox");
232
+ root.id = uniqueId("checkbox");
233
+ setChecked(root, defaultChecked);
234
+ setDataState(root, dataStateFor(defaultChecked));
235
+ if (disabled) {
236
+ root.disabled = true;
237
+ root.setAttribute("aria-disabled", "true");
238
+ }
239
+ function toggle() {
240
+ if (state.disabled.peek())
241
+ return;
242
+ const current = state.checked.peek();
243
+ const next = current === "mixed" ? true : !current;
244
+ state.checked.value = next;
245
+ setChecked(root, next);
246
+ setDataState(root, dataStateFor(next));
247
+ onCheckedChange?.(next);
248
+ }
249
+ root.addEventListener("click", toggle);
250
+ root.addEventListener("keydown", (event) => {
251
+ if (isKey(event, Keys.Space)) {
252
+ event.preventDefault();
253
+ toggle();
254
+ }
255
+ });
256
+ return { root, state };
257
+ }
258
+ };
259
+ // src/combobox/combobox.ts
260
+ import { signal as signal4 } from "@vertz/ui";
261
+ var Combobox = {
262
+ Root(options = {}) {
263
+ const { defaultValue = "", onValueChange, onInputChange } = options;
264
+ const ids = linkedIds("combobox");
265
+ const state = {
266
+ open: signal4(false),
267
+ value: signal4(defaultValue),
268
+ inputValue: signal4(defaultValue),
269
+ activeIndex: signal4(-1)
270
+ };
271
+ const optionElements = [];
272
+ const input = document.createElement("input");
273
+ input.setAttribute("type", "text");
274
+ input.setAttribute("role", "combobox");
275
+ input.setAttribute("aria-autocomplete", "list");
276
+ input.setAttribute("aria-controls", ids.contentId);
277
+ input.setAttribute("aria-haspopup", "listbox");
278
+ input.id = ids.triggerId;
279
+ input.value = defaultValue;
280
+ setExpanded(input, false);
281
+ const listbox = document.createElement("div");
282
+ listbox.setAttribute("role", "listbox");
283
+ listbox.id = ids.contentId;
284
+ setHidden(listbox, true);
285
+ setDataState(listbox, "closed");
286
+ function open() {
287
+ state.open.value = true;
288
+ setExpanded(input, true);
289
+ setHidden(listbox, false);
290
+ setDataState(listbox, "open");
291
+ }
292
+ function close() {
293
+ state.open.value = false;
294
+ state.activeIndex.value = -1;
295
+ setExpanded(input, false);
296
+ setHidden(listbox, true);
297
+ setDataState(listbox, "closed");
298
+ updateActiveDescendant(-1);
299
+ }
300
+ function selectOption(value) {
301
+ state.value.value = value;
302
+ state.inputValue.value = value;
303
+ input.value = value;
304
+ for (const opt of optionElements) {
305
+ const isActive = opt.getAttribute("data-value") === value;
306
+ setSelected(opt, isActive);
307
+ setDataState(opt, isActive ? "active" : "inactive");
308
+ }
309
+ onValueChange?.(value);
310
+ close();
311
+ input.focus();
312
+ }
313
+ function updateActiveDescendant(index) {
314
+ const opt = optionElements[index];
315
+ if (index >= 0 && opt) {
316
+ input.setAttribute("aria-activedescendant", opt.id);
317
+ for (let i = 0;i < optionElements.length; i++) {
318
+ const el = optionElements[i];
319
+ if (el)
320
+ setDataState(el, i === index ? "active" : "inactive");
321
+ }
322
+ } else {
323
+ input.removeAttribute("aria-activedescendant");
324
+ }
325
+ }
326
+ input.addEventListener("input", () => {
327
+ state.inputValue.value = input.value;
328
+ onInputChange?.(input.value);
329
+ if (!state.open.peek())
330
+ open();
331
+ });
332
+ input.addEventListener("focus", () => {
333
+ if (!state.open.peek() && input.value.length > 0)
334
+ open();
335
+ });
336
+ input.addEventListener("keydown", (event) => {
337
+ if (isKey(event, Keys.Escape)) {
338
+ event.preventDefault();
339
+ close();
340
+ return;
341
+ }
342
+ if (isKey(event, Keys.ArrowDown)) {
343
+ event.preventDefault();
344
+ if (!state.open.peek()) {
345
+ open();
346
+ }
347
+ const next = Math.min(state.activeIndex.peek() + 1, optionElements.length - 1);
348
+ state.activeIndex.value = next;
349
+ updateActiveDescendant(next);
350
+ return;
351
+ }
352
+ if (isKey(event, Keys.ArrowUp)) {
353
+ event.preventDefault();
354
+ const prev = Math.max(state.activeIndex.peek() - 1, 0);
355
+ state.activeIndex.value = prev;
356
+ updateActiveDescendant(prev);
357
+ return;
358
+ }
359
+ if (isKey(event, Keys.Enter)) {
360
+ event.preventDefault();
361
+ const idx = state.activeIndex.peek();
362
+ if (idx >= 0 && idx < optionElements.length) {
363
+ const val = optionElements[idx]?.getAttribute("data-value");
364
+ if (val != null)
365
+ selectOption(val);
366
+ }
367
+ return;
368
+ }
369
+ });
370
+ function Option(value, label) {
371
+ const opt = document.createElement("div");
372
+ const optId = `${ids.contentId}-opt-${optionElements.length}`;
373
+ opt.setAttribute("role", "option");
374
+ opt.id = optId;
375
+ opt.setAttribute("data-value", value);
376
+ opt.textContent = label ?? value;
377
+ const isSelected = value === defaultValue;
378
+ setSelected(opt, isSelected);
379
+ setDataState(opt, isSelected ? "active" : "inactive");
380
+ opt.addEventListener("click", () => {
381
+ selectOption(value);
382
+ });
383
+ optionElements.push(opt);
384
+ listbox.appendChild(opt);
385
+ return opt;
386
+ }
387
+ return { input, listbox, state, Option };
388
+ }
389
+ };
390
+ // src/dialog/dialog.ts
391
+ import { signal as signal5 } from "@vertz/ui";
392
+
393
+ // src/utils/focus.ts
394
+ var FOCUSABLE_SELECTOR = [
395
+ "a[href]",
396
+ "button:not([disabled])",
397
+ "input:not([disabled])",
398
+ "select:not([disabled])",
399
+ "textarea:not([disabled])",
400
+ '[tabindex]:not([tabindex="-1"])',
401
+ "[contenteditable]"
402
+ ].join(", ");
403
+ function getFocusableElements(container) {
404
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
405
+ }
406
+ function trapFocus(container) {
407
+ function handleKeyDown(event) {
408
+ if (event.key !== "Tab")
409
+ return;
410
+ const focusable = getFocusableElements(container);
411
+ if (focusable.length === 0)
412
+ return;
413
+ const first = focusable[0];
414
+ const last = focusable[focusable.length - 1];
415
+ if (!first || !last)
416
+ return;
417
+ if (event.shiftKey) {
418
+ if (document.activeElement === first) {
419
+ event.preventDefault();
420
+ last.focus();
421
+ }
422
+ } else {
423
+ if (document.activeElement === last) {
424
+ event.preventDefault();
425
+ first.focus();
426
+ }
427
+ }
428
+ }
429
+ container.addEventListener("keydown", handleKeyDown);
430
+ return () => {
431
+ container.removeEventListener("keydown", handleKeyDown);
432
+ };
433
+ }
434
+ function focusFirst(container) {
435
+ const focusable = getFocusableElements(container);
436
+ if (focusable.length > 0) {
437
+ focusable[0]?.focus();
438
+ }
439
+ }
440
+ function saveFocus() {
441
+ const previously = document.activeElement;
442
+ return () => {
443
+ if (previously && typeof previously.focus === "function") {
444
+ previously.focus();
445
+ }
446
+ };
447
+ }
448
+ function setRovingTabindex(items, activeIndex) {
449
+ for (let i = 0;i < items.length; i++) {
450
+ items[i]?.setAttribute("tabindex", i === activeIndex ? "0" : "-1");
451
+ }
452
+ }
453
+
454
+ // src/dialog/dialog.ts
455
+ var Dialog = {
456
+ Root(options = {}) {
457
+ const { modal = true, defaultOpen = false, onOpenChange } = options;
458
+ const ids = linkedIds("dialog");
459
+ const titleId = `${ids.contentId}-title`;
460
+ const state = { open: signal5(defaultOpen) };
461
+ let restoreFocus = null;
462
+ let removeTrap = null;
463
+ const trigger = document.createElement("button");
464
+ trigger.setAttribute("type", "button");
465
+ trigger.id = ids.triggerId;
466
+ trigger.setAttribute("aria-controls", ids.contentId);
467
+ setExpanded(trigger, defaultOpen);
468
+ setDataState(trigger, defaultOpen ? "open" : "closed");
469
+ const overlay = document.createElement("div");
470
+ overlay.setAttribute("data-dialog-overlay", "");
471
+ if (modal) {
472
+ overlay.style.position = "fixed";
473
+ overlay.style.inset = "0";
474
+ overlay.style.zIndex = "49";
475
+ }
476
+ setHidden(overlay, !defaultOpen);
477
+ setDataState(overlay, defaultOpen ? "open" : "closed");
478
+ const content = document.createElement("div");
479
+ content.setAttribute("role", "dialog");
480
+ content.id = ids.contentId;
481
+ if (modal) {
482
+ content.setAttribute("aria-modal", "true");
483
+ content.style.position = "fixed";
484
+ content.style.top = "50%";
485
+ content.style.left = "50%";
486
+ content.style.transform = "translate(-50%, -50%)";
487
+ content.style.zIndex = "50";
488
+ }
489
+ setLabelledBy(content, titleId);
490
+ setHidden(content, !defaultOpen);
491
+ setDataState(content, defaultOpen ? "open" : "closed");
492
+ const title = document.createElement("h2");
493
+ title.id = titleId;
494
+ const close = document.createElement("button");
495
+ close.setAttribute("type", "button");
496
+ close.setAttribute("aria-label", "Close");
497
+ function openDialog() {
498
+ state.open.value = true;
499
+ setExpanded(trigger, true);
500
+ setHidden(overlay, false);
501
+ setHidden(content, false);
502
+ setDataState(trigger, "open");
503
+ setDataState(overlay, "open");
504
+ setDataState(content, "open");
505
+ restoreFocus = saveFocus();
506
+ if (modal) {
507
+ removeTrap = trapFocus(content);
508
+ }
509
+ queueMicrotask(() => focusFirst(content));
510
+ onOpenChange?.(true);
511
+ }
512
+ function closeDialog() {
513
+ state.open.value = false;
514
+ setExpanded(trigger, false);
515
+ setHidden(overlay, true);
516
+ setHidden(content, true);
517
+ setDataState(trigger, "closed");
518
+ setDataState(overlay, "closed");
519
+ setDataState(content, "closed");
520
+ removeTrap?.();
521
+ removeTrap = null;
522
+ restoreFocus?.();
523
+ restoreFocus = null;
524
+ onOpenChange?.(false);
525
+ }
526
+ trigger.addEventListener("click", () => {
527
+ if (state.open.peek()) {
528
+ closeDialog();
529
+ } else {
530
+ openDialog();
531
+ }
532
+ });
533
+ close.addEventListener("click", () => {
534
+ closeDialog();
535
+ });
536
+ overlay.addEventListener("click", () => {
537
+ closeDialog();
538
+ });
539
+ content.addEventListener("keydown", (event) => {
540
+ if (isKey(event, Keys.Escape)) {
541
+ event.preventDefault();
542
+ event.stopPropagation();
543
+ closeDialog();
544
+ }
545
+ });
546
+ return { trigger, overlay, content, title, close, state };
547
+ }
548
+ };
549
+ // src/menu/menu.ts
550
+ import { signal as signal6 } from "@vertz/ui";
551
+ var Menu = {
552
+ Root(options = {}) {
553
+ const { onSelect } = options;
554
+ const ids = linkedIds("menu");
555
+ const state = {
556
+ open: signal6(false),
557
+ activeIndex: signal6(-1)
558
+ };
559
+ const items = [];
560
+ const trigger = document.createElement("button");
561
+ trigger.setAttribute("type", "button");
562
+ trigger.id = ids.triggerId;
563
+ trigger.setAttribute("aria-controls", ids.contentId);
564
+ trigger.setAttribute("aria-haspopup", "menu");
565
+ setExpanded(trigger, false);
566
+ setDataState(trigger, "closed");
567
+ const content = document.createElement("div");
568
+ content.setAttribute("role", "menu");
569
+ content.id = ids.contentId;
570
+ setHidden(content, true);
571
+ setDataState(content, "closed");
572
+ function open() {
573
+ state.open.value = true;
574
+ setExpanded(trigger, true);
575
+ setHidden(content, false);
576
+ setDataState(trigger, "open");
577
+ setDataState(content, "open");
578
+ state.activeIndex.value = 0;
579
+ updateActiveItem(0);
580
+ items[0]?.focus();
581
+ }
582
+ function close() {
583
+ state.open.value = false;
584
+ setExpanded(trigger, false);
585
+ setHidden(content, true);
586
+ setDataState(trigger, "closed");
587
+ setDataState(content, "closed");
588
+ trigger.focus();
589
+ }
590
+ function updateActiveItem(index) {
591
+ for (let i = 0;i < items.length; i++) {
592
+ items[i]?.setAttribute("tabindex", i === index ? "0" : "-1");
593
+ }
594
+ }
595
+ trigger.addEventListener("click", () => {
596
+ if (state.open.peek()) {
597
+ close();
598
+ } else {
599
+ open();
600
+ }
601
+ });
602
+ trigger.addEventListener("keydown", (event) => {
603
+ if (isKey(event, Keys.ArrowDown, Keys.Enter, Keys.Space)) {
604
+ event.preventDefault();
605
+ if (!state.open.peek())
606
+ open();
607
+ }
608
+ });
609
+ content.addEventListener("keydown", (event) => {
610
+ if (isKey(event, Keys.Escape)) {
611
+ event.preventDefault();
612
+ close();
613
+ return;
614
+ }
615
+ if (isKey(event, Keys.Enter, Keys.Space)) {
616
+ event.preventDefault();
617
+ const active = items[state.activeIndex.peek()];
618
+ if (active) {
619
+ const val = active.getAttribute("data-value");
620
+ if (val !== null) {
621
+ onSelect?.(val);
622
+ close();
623
+ }
624
+ }
625
+ return;
626
+ }
627
+ const result = handleListNavigation(event, items, { orientation: "vertical" });
628
+ if (result) {
629
+ const idx = items.indexOf(result);
630
+ if (idx >= 0) {
631
+ state.activeIndex.value = idx;
632
+ updateActiveItem(idx);
633
+ }
634
+ }
635
+ });
636
+ function Item(value, label) {
637
+ const item = document.createElement("div");
638
+ item.setAttribute("role", "menuitem");
639
+ item.setAttribute("data-value", value);
640
+ item.setAttribute("tabindex", "-1");
641
+ item.textContent = label ?? value;
642
+ item.addEventListener("click", () => {
643
+ onSelect?.(value);
644
+ close();
645
+ });
646
+ items.push(item);
647
+ content.appendChild(item);
648
+ return item;
649
+ }
650
+ return { trigger, content, state, Item };
651
+ }
652
+ };
653
+ // src/popover/popover.ts
654
+ import { signal as signal7 } from "@vertz/ui";
655
+ var Popover = {
656
+ Root(options = {}) {
657
+ const { defaultOpen = false, onOpenChange } = options;
658
+ const ids = linkedIds("popover");
659
+ const state = { open: signal7(defaultOpen) };
660
+ let restoreFocus = null;
661
+ const trigger = document.createElement("button");
662
+ trigger.setAttribute("type", "button");
663
+ trigger.id = ids.triggerId;
664
+ trigger.setAttribute("aria-controls", ids.contentId);
665
+ trigger.setAttribute("aria-haspopup", "dialog");
666
+ setExpanded(trigger, defaultOpen);
667
+ setDataState(trigger, defaultOpen ? "open" : "closed");
668
+ const content = document.createElement("div");
669
+ content.setAttribute("role", "dialog");
670
+ content.id = ids.contentId;
671
+ setHidden(content, !defaultOpen);
672
+ setDataState(content, defaultOpen ? "open" : "closed");
673
+ function open() {
674
+ state.open.value = true;
675
+ setExpanded(trigger, true);
676
+ setHidden(content, false);
677
+ setDataState(trigger, "open");
678
+ setDataState(content, "open");
679
+ restoreFocus = saveFocus();
680
+ queueMicrotask(() => focusFirst(content));
681
+ onOpenChange?.(true);
682
+ }
683
+ function close() {
684
+ state.open.value = false;
685
+ setExpanded(trigger, false);
686
+ setHidden(content, true);
687
+ setDataState(trigger, "closed");
688
+ setDataState(content, "closed");
689
+ restoreFocus?.();
690
+ restoreFocus = null;
691
+ onOpenChange?.(false);
692
+ }
693
+ trigger.addEventListener("click", () => {
694
+ if (state.open.peek()) {
695
+ close();
696
+ } else {
697
+ open();
698
+ }
699
+ });
700
+ content.addEventListener("keydown", (event) => {
701
+ if (isKey(event, Keys.Escape)) {
702
+ event.preventDefault();
703
+ close();
704
+ }
705
+ });
706
+ return { trigger, content, state };
707
+ }
708
+ };
709
+ // src/progress/progress.ts
710
+ import { signal as signal8 } from "@vertz/ui";
711
+ var Progress = {
712
+ Root(options = {}) {
713
+ const { defaultValue = 0, min = 0, max = 100 } = options;
714
+ const state = { value: signal8(defaultValue) };
715
+ const root = document.createElement("div");
716
+ root.setAttribute("role", "progressbar");
717
+ root.id = uniqueId("progress");
718
+ setValueRange(root, defaultValue, min, max);
719
+ const pct = (defaultValue - min) / (max - min) * 100;
720
+ if (pct >= 100) {
721
+ setDataState(root, "complete");
722
+ } else if (pct > 0) {
723
+ setDataState(root, "loading");
724
+ } else {
725
+ setDataState(root, "idle");
726
+ }
727
+ const indicator = document.createElement("div");
728
+ indicator.setAttribute("data-part", "indicator");
729
+ indicator.style.width = `${pct}%`;
730
+ root.appendChild(indicator);
731
+ function setValue(val) {
732
+ const clamped = Math.min(max, Math.max(min, val));
733
+ state.value.value = clamped;
734
+ setValueRange(root, clamped, min, max);
735
+ const p = (clamped - min) / (max - min) * 100;
736
+ indicator.style.width = `${p}%`;
737
+ if (p >= 100) {
738
+ setDataState(root, "complete");
739
+ } else if (p > 0) {
740
+ setDataState(root, "loading");
741
+ } else {
742
+ setDataState(root, "idle");
743
+ }
744
+ }
745
+ return { root, indicator, state, setValue };
746
+ }
747
+ };
748
+ // src/radio/radio.ts
749
+ import { signal as signal9 } from "@vertz/ui";
750
+ var Radio = {
751
+ Root(options = {}) {
752
+ const { defaultValue = "", onValueChange } = options;
753
+ const state = { value: signal9(defaultValue) };
754
+ const items = [];
755
+ const itemValues = [];
756
+ const root = document.createElement("div");
757
+ root.setAttribute("role", "radiogroup");
758
+ root.id = uniqueId("radiogroup");
759
+ function selectItem(value) {
760
+ state.value.value = value;
761
+ for (let i = 0;i < items.length; i++) {
762
+ const item = items[i];
763
+ if (!item)
764
+ continue;
765
+ const isActive = itemValues[i] === value;
766
+ setChecked(item, isActive);
767
+ setDataState(item, isActive ? "checked" : "unchecked");
768
+ }
769
+ setRovingTabindex(items, itemValues.indexOf(value));
770
+ onValueChange?.(value);
771
+ }
772
+ root.addEventListener("keydown", (event) => {
773
+ const result = handleListNavigation(event, items, { orientation: "vertical" });
774
+ if (result) {
775
+ const idx = items.indexOf(result);
776
+ if (idx >= 0) {
777
+ const val = itemValues[idx];
778
+ if (val !== undefined)
779
+ selectItem(val);
780
+ }
781
+ }
782
+ });
783
+ function Item(value, label) {
784
+ const item = document.createElement("div");
785
+ item.setAttribute("role", "radio");
786
+ item.id = uniqueId("radio");
787
+ item.setAttribute("data-value", value);
788
+ item.textContent = label ?? value;
789
+ const isActive = value === state.value.peek();
790
+ setChecked(item, isActive);
791
+ setDataState(item, isActive ? "checked" : "unchecked");
792
+ item.addEventListener("click", () => {
793
+ selectItem(value);
794
+ item.focus();
795
+ });
796
+ items.push(item);
797
+ itemValues.push(value);
798
+ root.appendChild(item);
799
+ setRovingTabindex(items, itemValues.indexOf(state.value.peek()));
800
+ return item;
801
+ }
802
+ return { root, state, Item };
803
+ }
804
+ };
805
+ // src/select/select.ts
806
+ import { signal as signal10 } from "@vertz/ui";
807
+ var Select = {
808
+ Root(options = {}) {
809
+ const { defaultValue = "", onValueChange } = options;
810
+ const ids = linkedIds("select");
811
+ const state = {
812
+ open: signal10(false),
813
+ value: signal10(defaultValue),
814
+ activeIndex: signal10(-1)
815
+ };
816
+ const items = [];
817
+ const trigger = document.createElement("button");
818
+ trigger.setAttribute("type", "button");
819
+ trigger.setAttribute("role", "combobox");
820
+ trigger.id = ids.triggerId;
821
+ trigger.setAttribute("aria-controls", ids.contentId);
822
+ trigger.setAttribute("aria-haspopup", "listbox");
823
+ setExpanded(trigger, false);
824
+ setDataState(trigger, "closed");
825
+ const content = document.createElement("div");
826
+ content.setAttribute("role", "listbox");
827
+ content.id = ids.contentId;
828
+ setHidden(content, true);
829
+ setDataState(content, "closed");
830
+ function open() {
831
+ state.open.value = true;
832
+ setExpanded(trigger, true);
833
+ setHidden(content, false);
834
+ setDataState(trigger, "open");
835
+ setDataState(content, "open");
836
+ const selectedIdx = items.findIndex((item) => item.getAttribute("data-value") === state.value.peek());
837
+ const focusIdx = selectedIdx >= 0 ? selectedIdx : 0;
838
+ state.activeIndex.value = focusIdx;
839
+ updateActiveItem(focusIdx);
840
+ items[focusIdx]?.focus();
841
+ }
842
+ function close() {
843
+ state.open.value = false;
844
+ setExpanded(trigger, false);
845
+ setHidden(content, true);
846
+ setDataState(trigger, "closed");
847
+ setDataState(content, "closed");
848
+ trigger.focus();
849
+ }
850
+ function selectItem(value) {
851
+ state.value.value = value;
852
+ for (const item of items) {
853
+ const isActive = item.getAttribute("data-value") === value;
854
+ setSelected(item, isActive);
855
+ setDataState(item, isActive ? "active" : "inactive");
856
+ }
857
+ onValueChange?.(value);
858
+ close();
859
+ }
860
+ function updateActiveItem(index) {
861
+ for (let i = 0;i < items.length; i++) {
862
+ items[i]?.setAttribute("tabindex", i === index ? "0" : "-1");
863
+ }
864
+ }
865
+ trigger.addEventListener("click", () => {
866
+ if (state.open.peek()) {
867
+ close();
868
+ } else {
869
+ open();
870
+ }
871
+ });
872
+ trigger.addEventListener("keydown", (event) => {
873
+ if (isKey(event, Keys.ArrowDown, Keys.ArrowUp, Keys.Enter, Keys.Space)) {
874
+ event.preventDefault();
875
+ if (!state.open.peek()) {
876
+ open();
877
+ }
878
+ }
879
+ });
880
+ content.addEventListener("keydown", (event) => {
881
+ if (isKey(event, Keys.Escape)) {
882
+ event.preventDefault();
883
+ close();
884
+ return;
885
+ }
886
+ if (isKey(event, Keys.Enter, Keys.Space)) {
887
+ event.preventDefault();
888
+ const active = items[state.activeIndex.peek()];
889
+ if (active) {
890
+ const val = active.getAttribute("data-value");
891
+ if (val !== null)
892
+ selectItem(val);
893
+ }
894
+ return;
895
+ }
896
+ const result = handleListNavigation(event, items, { orientation: "vertical" });
897
+ if (result) {
898
+ const idx = items.indexOf(result);
899
+ if (idx >= 0) {
900
+ state.activeIndex.value = idx;
901
+ updateActiveItem(idx);
902
+ }
903
+ }
904
+ });
905
+ function Item(value, label) {
906
+ const item = document.createElement("div");
907
+ item.setAttribute("role", "option");
908
+ item.setAttribute("data-value", value);
909
+ item.setAttribute("tabindex", "-1");
910
+ item.textContent = label ?? value;
911
+ const isSelected = value === defaultValue;
912
+ setSelected(item, isSelected);
913
+ setDataState(item, isSelected ? "active" : "inactive");
914
+ item.addEventListener("click", () => {
915
+ selectItem(value);
916
+ });
917
+ items.push(item);
918
+ content.appendChild(item);
919
+ return item;
920
+ }
921
+ return { trigger, content, state, Item };
922
+ }
923
+ };
924
+ // src/slider/slider.ts
925
+ import { signal as signal11 } from "@vertz/ui";
926
+ var Slider = {
927
+ Root(options = {}) {
928
+ const {
929
+ defaultValue = 0,
930
+ min = 0,
931
+ max = 100,
932
+ step = 1,
933
+ disabled = false,
934
+ onValueChange
935
+ } = options;
936
+ const state = {
937
+ value: signal11(defaultValue),
938
+ disabled: signal11(disabled)
939
+ };
940
+ const root = document.createElement("div");
941
+ root.id = uniqueId("slider");
942
+ setDataState(root, disabled ? "disabled" : "active");
943
+ const track = document.createElement("div");
944
+ track.setAttribute("data-part", "track");
945
+ const thumb = document.createElement("div");
946
+ thumb.setAttribute("role", "slider");
947
+ thumb.setAttribute("tabindex", disabled ? "-1" : "0");
948
+ thumb.setAttribute("data-part", "thumb");
949
+ setValueRange(thumb, defaultValue, min, max);
950
+ if (disabled) {
951
+ thumb.setAttribute("aria-disabled", "true");
952
+ }
953
+ function clamp(val) {
954
+ return Math.min(max, Math.max(min, val));
955
+ }
956
+ function setValue(val) {
957
+ if (state.disabled.peek())
958
+ return;
959
+ const clamped = clamp(val);
960
+ state.value.value = clamped;
961
+ setValueRange(thumb, clamped, min, max);
962
+ const pct2 = (clamped - min) / (max - min) * 100;
963
+ thumb.style.left = `${pct2}%`;
964
+ setDataState(root, "active");
965
+ onValueChange?.(clamped);
966
+ }
967
+ thumb.addEventListener("keydown", (event) => {
968
+ if (state.disabled.peek())
969
+ return;
970
+ const current = state.value.peek();
971
+ if (isKey(event, Keys.ArrowRight, Keys.ArrowUp)) {
972
+ event.preventDefault();
973
+ setValue(current + step);
974
+ } else if (isKey(event, Keys.ArrowLeft, Keys.ArrowDown)) {
975
+ event.preventDefault();
976
+ setValue(current - step);
977
+ } else if (isKey(event, Keys.Home)) {
978
+ event.preventDefault();
979
+ setValue(min);
980
+ } else if (isKey(event, Keys.End)) {
981
+ event.preventDefault();
982
+ setValue(max);
983
+ }
984
+ });
985
+ track.appendChild(thumb);
986
+ root.appendChild(track);
987
+ const pct = (defaultValue - min) / (max - min) * 100;
988
+ thumb.style.left = `${pct}%`;
989
+ return { root, thumb, track, state };
990
+ }
991
+ };
992
+ // src/switch/switch.ts
993
+ import { signal as signal12 } from "@vertz/ui";
994
+ var Switch = {
995
+ Root(options = {}) {
996
+ const { defaultChecked = false, disabled = false, onCheckedChange } = options;
997
+ const state = {
998
+ checked: signal12(defaultChecked),
999
+ disabled: signal12(disabled)
1000
+ };
1001
+ const root = document.createElement("button");
1002
+ root.setAttribute("type", "button");
1003
+ root.setAttribute("role", "switch");
1004
+ root.id = uniqueId("switch");
1005
+ setChecked(root, defaultChecked);
1006
+ setDataState(root, defaultChecked ? "checked" : "unchecked");
1007
+ if (disabled) {
1008
+ root.disabled = true;
1009
+ root.setAttribute("aria-disabled", "true");
1010
+ }
1011
+ function toggle() {
1012
+ if (state.disabled.peek())
1013
+ return;
1014
+ const next = !state.checked.peek();
1015
+ state.checked.value = next;
1016
+ setChecked(root, next);
1017
+ setDataState(root, next ? "checked" : "unchecked");
1018
+ onCheckedChange?.(next);
1019
+ }
1020
+ root.addEventListener("click", toggle);
1021
+ root.addEventListener("keydown", (event) => {
1022
+ if (isKey(event, Keys.Space)) {
1023
+ event.preventDefault();
1024
+ toggle();
1025
+ }
1026
+ });
1027
+ return { root, state };
1028
+ }
1029
+ };
1030
+ // src/tabs/tabs.ts
1031
+ import { signal as signal13 } from "@vertz/ui";
1032
+ var Tabs = {
1033
+ Root(options = {}) {
1034
+ const { defaultValue = "", onValueChange } = options;
1035
+ const state = { value: signal13(defaultValue) };
1036
+ const triggers = [];
1037
+ const panels = [];
1038
+ const tabValues = [];
1039
+ const root = document.createElement("div");
1040
+ const list = document.createElement("div");
1041
+ list.setAttribute("role", "tablist");
1042
+ root.appendChild(list);
1043
+ function selectTab(value) {
1044
+ state.value.value = value;
1045
+ for (let i = 0;i < tabValues.length; i++) {
1046
+ const isActive = tabValues[i] === value;
1047
+ const trig = triggers[i];
1048
+ const panel = panels[i];
1049
+ if (!trig || !panel)
1050
+ continue;
1051
+ setSelected(trig, isActive);
1052
+ setDataState(trig, isActive ? "active" : "inactive");
1053
+ trig.setAttribute("tabindex", isActive ? "0" : "-1");
1054
+ setHidden(panel, !isActive);
1055
+ setDataState(panel, isActive ? "active" : "inactive");
1056
+ }
1057
+ onValueChange?.(value);
1058
+ }
1059
+ list.addEventListener("keydown", (event) => {
1060
+ const result = handleListNavigation(event, triggers, {
1061
+ orientation: "horizontal"
1062
+ });
1063
+ if (result) {
1064
+ const idx = triggers.indexOf(result);
1065
+ if (idx >= 0) {
1066
+ const val = tabValues[idx];
1067
+ if (val !== undefined)
1068
+ selectTab(val);
1069
+ }
1070
+ }
1071
+ });
1072
+ function Tab(value, label) {
1073
+ const baseId = uniqueId("tab");
1074
+ const triggerId = `${baseId}-trigger`;
1075
+ const panelId = `${baseId}-panel`;
1076
+ const isActive = value === state.value.peek();
1077
+ const trig = document.createElement("button");
1078
+ trig.setAttribute("type", "button");
1079
+ trig.setAttribute("role", "tab");
1080
+ trig.id = triggerId;
1081
+ trig.setAttribute("aria-controls", panelId);
1082
+ trig.setAttribute("data-value", value);
1083
+ trig.textContent = label ?? value;
1084
+ setSelected(trig, isActive);
1085
+ setDataState(trig, isActive ? "active" : "inactive");
1086
+ const panel = document.createElement("div");
1087
+ panel.setAttribute("role", "tabpanel");
1088
+ panel.id = panelId;
1089
+ panel.setAttribute("aria-labelledby", triggerId);
1090
+ panel.setAttribute("tabindex", "0");
1091
+ setHidden(panel, !isActive);
1092
+ setDataState(panel, isActive ? "active" : "inactive");
1093
+ trig.addEventListener("click", () => {
1094
+ selectTab(value);
1095
+ trig.focus();
1096
+ });
1097
+ triggers.push(trig);
1098
+ panels.push(panel);
1099
+ tabValues.push(value);
1100
+ list.appendChild(trig);
1101
+ root.appendChild(panel);
1102
+ setRovingTabindex(triggers, triggers.findIndex((t) => tabValues[triggers.indexOf(t)] === state.value.peek()));
1103
+ return { trigger: trig, panel };
1104
+ }
1105
+ return { root, list, state, Tab };
1106
+ }
1107
+ };
1108
+ // src/toast/toast.ts
1109
+ import { signal as signal14 } from "@vertz/ui";
1110
+ var Toast = {
1111
+ Root(options = {}) {
1112
+ const { duration = 5000, politeness = "polite" } = options;
1113
+ const state = { messages: signal14([]) };
1114
+ const region = document.createElement("div");
1115
+ region.setAttribute("role", "status");
1116
+ region.setAttribute("aria-live", politeness);
1117
+ region.setAttribute("aria-atomic", "false");
1118
+ setDataState(region, "empty");
1119
+ function announce(content) {
1120
+ const id = uniqueId("toast");
1121
+ const el = document.createElement("div");
1122
+ el.setAttribute("role", "status");
1123
+ el.setAttribute("data-toast-id", id);
1124
+ el.textContent = content;
1125
+ setDataState(el, "open");
1126
+ const msg = { id, content, el };
1127
+ const messages = [...state.messages.peek(), msg];
1128
+ state.messages.value = messages;
1129
+ region.appendChild(el);
1130
+ setDataState(region, "active");
1131
+ if (duration > 0) {
1132
+ setTimeout(() => dismiss(id), duration);
1133
+ }
1134
+ return msg;
1135
+ }
1136
+ function dismiss(id) {
1137
+ const messages = state.messages.peek().filter((m) => m.id !== id);
1138
+ state.messages.value = messages;
1139
+ const el = region.querySelector(`[data-toast-id="${id}"]`);
1140
+ if (el) {
1141
+ setDataState(el, "closed");
1142
+ el.remove();
1143
+ }
1144
+ if (messages.length === 0) {
1145
+ setDataState(region, "empty");
1146
+ }
1147
+ }
1148
+ return { region, state, announce, dismiss };
1149
+ }
1150
+ };
1151
+ // src/tooltip/tooltip.ts
1152
+ import { signal as signal15 } from "@vertz/ui";
1153
+ var Tooltip = {
1154
+ Root(options = {}) {
1155
+ const { delay = 300, onOpenChange } = options;
1156
+ const contentId = uniqueId("tooltip");
1157
+ const state = { open: signal15(false) };
1158
+ let showTimeout = null;
1159
+ const trigger = document.createElement("span");
1160
+ setDescribedBy(trigger, contentId);
1161
+ const content = document.createElement("div");
1162
+ content.setAttribute("role", "tooltip");
1163
+ content.id = contentId;
1164
+ setHidden(content, true);
1165
+ setDataState(content, "closed");
1166
+ function show() {
1167
+ if (showTimeout !== null)
1168
+ return;
1169
+ showTimeout = setTimeout(() => {
1170
+ state.open.value = true;
1171
+ setHidden(content, false);
1172
+ setDataState(content, "open");
1173
+ onOpenChange?.(true);
1174
+ showTimeout = null;
1175
+ }, delay);
1176
+ }
1177
+ function hide() {
1178
+ if (showTimeout !== null) {
1179
+ clearTimeout(showTimeout);
1180
+ showTimeout = null;
1181
+ }
1182
+ state.open.value = false;
1183
+ setHidden(content, true);
1184
+ setDataState(content, "closed");
1185
+ onOpenChange?.(false);
1186
+ }
1187
+ trigger.addEventListener("mouseenter", show);
1188
+ trigger.addEventListener("mouseleave", hide);
1189
+ trigger.addEventListener("focus", show);
1190
+ trigger.addEventListener("blur", hide);
1191
+ trigger.addEventListener("keydown", (event) => {
1192
+ if (isKey(event, Keys.Escape)) {
1193
+ hide();
1194
+ }
1195
+ });
1196
+ return { trigger, content, state };
1197
+ }
1198
+ };
1199
+ export {
1200
+ Tooltip,
1201
+ Toast,
1202
+ Tabs,
1203
+ Switch,
1204
+ Slider,
1205
+ Select,
1206
+ Radio,
1207
+ Progress,
1208
+ Popover,
1209
+ Menu,
1210
+ Dialog,
1211
+ Combobox,
1212
+ Checkbox,
1213
+ Button,
1214
+ Accordion
1215
+ };