create-dalila 1.1.13 → 1.2.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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/template/src/components/ui/accordion/accordion.css +90 -0
  3. package/template/src/components/ui/accordion/index.ts +121 -0
  4. package/template/src/components/ui/alert/alert.css +78 -0
  5. package/template/src/components/ui/avatar/avatar.css +45 -0
  6. package/template/src/components/ui/badge/badge.css +71 -0
  7. package/template/src/components/ui/breadcrumb/breadcrumb.css +41 -0
  8. package/template/src/components/ui/button/button.css +135 -0
  9. package/template/src/components/ui/calendar/calendar.css +96 -0
  10. package/template/src/components/ui/calendar/index.ts +157 -0
  11. package/template/src/components/ui/card/card.css +93 -0
  12. package/template/src/components/ui/checkbox/checkbox.css +57 -0
  13. package/template/src/components/ui/chip/chip.css +62 -0
  14. package/template/src/components/ui/collapsible/collapsible.css +61 -0
  15. package/template/src/components/ui/combobox/combobox.css +85 -0
  16. package/template/src/components/ui/combobox/index.ts +181 -0
  17. package/template/src/components/ui/dalila/dalila.css +42 -0
  18. package/template/src/components/ui/dalila-core/dalila-core.css +14 -0
  19. package/template/src/components/ui/dialog/dialog.css +125 -0
  20. package/template/src/components/ui/dialog/index.ts +68 -0
  21. package/template/src/components/ui/drawer/drawer.css +122 -0
  22. package/template/src/components/ui/drawer/index.ts +53 -0
  23. package/template/src/components/ui/dropdown/dropdown.css +87 -0
  24. package/template/src/components/ui/dropdown/index.ts +57 -0
  25. package/template/src/components/ui/dropzone/dropzone.css +47 -0
  26. package/template/src/components/ui/dropzone/index.ts +114 -0
  27. package/template/src/components/ui/empty-state/empty-state.css +33 -0
  28. package/template/src/components/ui/env.ts +4 -0
  29. package/template/src/components/ui/form/form.css +44 -0
  30. package/template/src/components/ui/index.ts +13 -0
  31. package/template/src/components/ui/input/input.css +106 -0
  32. package/template/src/components/ui/layout/layout.css +62 -0
  33. package/template/src/components/ui/pagination/pagination.css +55 -0
  34. package/template/src/components/ui/popover/index.ts +185 -0
  35. package/template/src/components/ui/popover/popover.css +55 -0
  36. package/template/src/components/ui/radio/radio.css +56 -0
  37. package/template/src/components/ui/runtime.ts +514 -0
  38. package/template/src/components/ui/separator/separator.css +38 -0
  39. package/template/src/components/ui/skeleton/skeleton.css +57 -0
  40. package/template/src/components/ui/slider/slider.css +60 -0
  41. package/template/src/components/ui/spinner/spinner.css +38 -0
  42. package/template/src/components/ui/table/table.css +54 -0
  43. package/template/src/components/ui/tabs/index.ts +128 -0
  44. package/template/src/components/ui/tabs/tabs.css +74 -0
  45. package/template/src/components/ui/toast/index.ts +144 -0
  46. package/template/src/components/ui/toast/toast.css +100 -0
  47. package/template/src/components/ui/toggle/toggle.css +90 -0
  48. package/template/src/components/ui/tokens/tokens.css +161 -0
  49. package/template/src/components/ui/tooltip/tooltip.css +53 -0
  50. package/template/src/components/ui/typography/typography.css +81 -0
  51. package/template/src/components/ui/ui-types.ts +238 -0
  52. package/template/src/components/ui/validate.ts +83 -0
  53. package/template/src/style.css +13 -0
@@ -0,0 +1,157 @@
1
+ import { signal, computed } from "../../../core/signal.js";
2
+ import { batch } from "../../../core/scheduler.js";
3
+ import type { Calendar, CalendarDay, CalendarOptions } from "../ui-types.js";
4
+ import { validateCalendarOptions } from "../validate.js";
5
+
6
+ const DEFAULT_DAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
7
+ const DEFAULT_MONTH_LABELS = [
8
+ "January", "February", "March", "April", "May", "June",
9
+ "July", "August", "September", "October", "November", "December",
10
+ ];
11
+
12
+ function isSameDay(a: Date, b: Date): boolean {
13
+ return (
14
+ a.getFullYear() === b.getFullYear() &&
15
+ a.getMonth() === b.getMonth() &&
16
+ a.getDate() === b.getDate()
17
+ );
18
+ }
19
+
20
+ export function createCalendar(options: CalendarOptions = {}): Calendar {
21
+ validateCalendarOptions(options as Record<string, unknown>);
22
+ const {
23
+ initial,
24
+ min,
25
+ max,
26
+ dayLabels: customDayLabels,
27
+ monthLabels: customMonthLabels,
28
+ } = options;
29
+
30
+ const now = initial ?? new Date();
31
+ const monthLabels = customMonthLabels ?? DEFAULT_MONTH_LABELS;
32
+ const calDayLabels = customDayLabels ?? DEFAULT_DAY_LABELS;
33
+
34
+ const year = signal(now.getFullYear());
35
+ const month = signal(now.getMonth());
36
+ const selected = signal<Date | null>(initial ?? null);
37
+
38
+ const title = computed(() => `${monthLabels[month()]} ${year()}`);
39
+
40
+ const days = computed<CalendarDay[]>(() => {
41
+ const y = year();
42
+ const m = month();
43
+ const sel = selected();
44
+ const today = new Date();
45
+
46
+ const firstDow = new Date(y, m, 1).getDay();
47
+ const daysInMonth = new Date(y, m + 1, 0).getDate();
48
+ const prevMonthDays = new Date(y, m, 0).getDate();
49
+
50
+ const grid: CalendarDay[] = [];
51
+ const GRID_SIZE = 42;
52
+
53
+ // Previous month days
54
+ for (let i = firstDow - 1; i >= 0; i--) {
55
+ const date = prevMonthDays - i;
56
+ const fullDate = new Date(y, m - 1, date);
57
+ grid.push({
58
+ date,
59
+ month: "prev",
60
+ fullDate,
61
+ isToday: isSameDay(fullDate, today),
62
+ isSelected: sel !== null && isSameDay(fullDate, sel),
63
+ disabled: isOutOfBounds(fullDate),
64
+ });
65
+ }
66
+
67
+ // Current month days
68
+ for (let d = 1; d <= daysInMonth; d++) {
69
+ const fullDate = new Date(y, m, d);
70
+ grid.push({
71
+ date: d,
72
+ month: "current",
73
+ fullDate,
74
+ isToday: isSameDay(fullDate, today),
75
+ isSelected: sel !== null && isSameDay(fullDate, sel),
76
+ disabled: isOutOfBounds(fullDate),
77
+ });
78
+ }
79
+
80
+ // Next month days
81
+ const remaining = GRID_SIZE - grid.length;
82
+ for (let d = 1; d <= remaining; d++) {
83
+ const fullDate = new Date(y, m + 1, d);
84
+ grid.push({
85
+ date: d,
86
+ month: "next",
87
+ fullDate,
88
+ isToday: isSameDay(fullDate, today),
89
+ isSelected: sel !== null && isSameDay(fullDate, sel),
90
+ disabled: isOutOfBounds(fullDate),
91
+ });
92
+ }
93
+
94
+ return grid;
95
+ });
96
+
97
+ function isOutOfBounds(date: Date): boolean {
98
+ if (min && date < min) return true;
99
+ if (max && date > max) return true;
100
+ return false;
101
+ }
102
+
103
+ const prev = () => {
104
+ batch(() => {
105
+ if (month() === 0) {
106
+ month.set(11);
107
+ year.update((y) => y - 1);
108
+ } else {
109
+ month.update((m) => m - 1);
110
+ }
111
+ });
112
+ };
113
+
114
+ const next = () => {
115
+ batch(() => {
116
+ if (month() === 11) {
117
+ month.set(0);
118
+ year.update((y) => y + 1);
119
+ } else {
120
+ month.update((m) => m + 1);
121
+ }
122
+ });
123
+ };
124
+
125
+ const select = (date: Date) => {
126
+ if (isOutOfBounds(date)) return;
127
+ selected.set(date);
128
+ };
129
+
130
+ const handleDayClick = (ev: Event) => {
131
+ const target = (ev.target as HTMLElement).closest<HTMLElement>(
132
+ "[data-date]"
133
+ );
134
+ if (!target) return;
135
+
136
+ const dateStr = target.dataset.date;
137
+ if (!dateStr) return;
138
+
139
+ const date = new Date(dateStr);
140
+ if (isNaN(date.getTime())) return;
141
+
142
+ select(date);
143
+ };
144
+
145
+ return {
146
+ year,
147
+ month,
148
+ selected,
149
+ title,
150
+ days,
151
+ dayLabels: calDayLabels,
152
+ prev,
153
+ next,
154
+ select,
155
+ handleDayClick,
156
+ };
157
+ }
@@ -0,0 +1,93 @@
1
+ /* Dalila UI — Card */
2
+
3
+ .d-card {
4
+ position: relative;
5
+ background: var(--d-surface-card);
6
+ border: 1px solid var(--d-border-color);
7
+ border-radius: var(--d-radius-lg);
8
+ overflow: hidden;
9
+ }
10
+
11
+ .d-card-header {
12
+ padding: var(--d-space-6) var(--d-space-6) var(--d-space-3);
13
+ }
14
+
15
+ .d-card-body {
16
+ padding: var(--d-space-3) var(--d-space-6);
17
+ }
18
+
19
+ .d-card-footer {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: var(--d-space-3);
23
+ padding: var(--d-space-3) var(--d-space-6) var(--d-space-6);
24
+ }
25
+
26
+ .d-card-body:only-child {
27
+ padding: var(--d-space-6);
28
+ }
29
+
30
+ .d-card-title {
31
+ margin: 0;
32
+ font-family: var(--d-font-sans);
33
+ font-size: var(--d-text-lg);
34
+ font-weight: var(--d-font-semibold);
35
+ color: var(--d-text-primary);
36
+ line-height: 1.3;
37
+ }
38
+
39
+ .d-card-description {
40
+ margin: var(--d-space-1) 0 0;
41
+ font-size: var(--d-text-sm);
42
+ color: var(--d-text-secondary);
43
+ line-height: var(--d-leading);
44
+ }
45
+
46
+ .d-card-accent {
47
+ border: none;
48
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.06);
49
+ }
50
+
51
+ [data-theme="dark"] .d-card-accent {
52
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
53
+ }
54
+
55
+ .d-card-section {
56
+ overflow: hidden;
57
+ }
58
+
59
+ .d-card-section img {
60
+ display: block;
61
+ width: 100%;
62
+ }
63
+
64
+ .d-card-hover {
65
+ transition: border-color var(--d-duration-fast) ease;
66
+ cursor: pointer;
67
+ }
68
+
69
+ .d-card-hover:hover {
70
+ border-color: #404040;
71
+ }
72
+
73
+ [data-theme="light"] .d-card-hover:hover {
74
+ border-color: #c9c9c9;
75
+ }
76
+
77
+ .d-card-flat {
78
+ background: var(--d-surface-raised);
79
+ }
80
+
81
+ .d-card-header + .d-card-body {
82
+ border-top: 1px solid var(--d-border-color);
83
+ padding-top: var(--d-space-4);
84
+ }
85
+
86
+ .d-card-body + .d-card-footer {
87
+ border-top: 1px solid var(--d-border-color);
88
+ padding-top: var(--d-space-4);
89
+ }
90
+
91
+ .d-card-compact .d-card-header { padding: var(--d-space-4) var(--d-space-4) var(--d-space-2); }
92
+ .d-card-compact .d-card-body { padding: var(--d-space-2) var(--d-space-4); }
93
+ .d-card-compact .d-card-footer { padding: var(--d-space-2) var(--d-space-4) var(--d-space-4); }
@@ -0,0 +1,57 @@
1
+ /* Dalila UI — Checkbox */
2
+
3
+ .d-checkbox {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: var(--d-space-2);
7
+ font-family: var(--d-font-sans);
8
+ font-size: var(--d-text-base);
9
+ color: var(--d-text-primary);
10
+ cursor: pointer;
11
+ user-select: none;
12
+ }
13
+
14
+ .d-checkbox input {
15
+ position: relative;
16
+ appearance: none;
17
+ width: 1.125rem;
18
+ height: 1.125rem;
19
+ flex-shrink: 0;
20
+ background: var(--d-surface-page);
21
+ border: 1px solid var(--d-border-color);
22
+ border-radius: var(--d-radius-sm);
23
+ cursor: pointer;
24
+ transition: all var(--d-duration-fast) ease;
25
+ }
26
+
27
+ .d-checkbox input:checked {
28
+ background: var(--d-accent);
29
+ border-color: var(--d-accent);
30
+ }
31
+
32
+ .d-checkbox input:checked::after {
33
+ content: "";
34
+ position: absolute;
35
+ top: 2px;
36
+ left: 5px;
37
+ width: 5px;
38
+ height: 9px;
39
+ border: solid #fff;
40
+ border-width: 0 2px 2px 0;
41
+ transform: rotate(45deg);
42
+ }
43
+
44
+ .d-checkbox input:focus-visible {
45
+ outline: none;
46
+ box-shadow: var(--d-shadow-glow);
47
+ }
48
+
49
+ .d-checkbox input:disabled {
50
+ opacity: 0.5;
51
+ cursor: not-allowed;
52
+ }
53
+
54
+ .d-checkbox:has(input:disabled) {
55
+ opacity: 0.6;
56
+ cursor: not-allowed;
57
+ }
@@ -0,0 +1,62 @@
1
+ /* Dalila UI — Chip / Tag */
2
+
3
+ .d-chip {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: var(--d-space-1);
7
+ padding: 0.25rem 0.625rem;
8
+ font-family: var(--d-font-sans);
9
+ font-size: var(--d-text-xs);
10
+ font-weight: var(--d-font-medium);
11
+ color: var(--d-text-primary);
12
+ background: var(--d-surface-raised);
13
+ border: 1px solid var(--d-border-color);
14
+ border-radius: var(--d-radius-full);
15
+ white-space: nowrap;
16
+ }
17
+
18
+ .d-chip-primary {
19
+ color: var(--d-accent);
20
+ background: rgba(37, 99, 235, 0.1);
21
+ border-color: transparent;
22
+ }
23
+
24
+ .d-chip-success {
25
+ color: var(--d-success);
26
+ background: var(--d-success-light);
27
+ border-color: transparent;
28
+ }
29
+
30
+ .d-chip-error {
31
+ color: var(--d-error);
32
+ background: var(--d-error-light);
33
+ border-color: transparent;
34
+ }
35
+
36
+ .d-chip-warning {
37
+ color: var(--d-warning);
38
+ background: var(--d-warning-light);
39
+ border-color: transparent;
40
+ }
41
+
42
+ .d-chip-remove {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ width: 1rem;
47
+ height: 1rem;
48
+ padding: 0;
49
+ font-size: 0.75rem;
50
+ line-height: 1;
51
+ color: inherit;
52
+ opacity: 0.6;
53
+ background: none;
54
+ border: none;
55
+ border-radius: 50%;
56
+ cursor: pointer;
57
+ transition: opacity var(--d-duration-fast) ease;
58
+ }
59
+
60
+ .d-chip-remove:hover {
61
+ opacity: 1;
62
+ }
@@ -0,0 +1,61 @@
1
+ /* Dalila UI — Collapsible */
2
+
3
+ .d-collapsible {
4
+ border: 1px solid var(--d-border-color);
5
+ border-radius: var(--d-radius-md);
6
+ overflow: hidden;
7
+ }
8
+
9
+ .d-collapsible > summary,
10
+ .d-collapsible-trigger {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ padding: var(--d-space-3) var(--d-space-4);
15
+ font-family: var(--d-font-sans);
16
+ font-size: var(--d-text-sm);
17
+ font-weight: var(--d-font-medium);
18
+ color: var(--d-text-primary);
19
+ cursor: pointer;
20
+ list-style: none;
21
+ user-select: none;
22
+ transition: background var(--d-duration-fast) ease;
23
+ }
24
+
25
+ .d-collapsible > summary::-webkit-details-marker {
26
+ display: none;
27
+ }
28
+
29
+ .d-collapsible > summary:hover,
30
+ .d-collapsible-trigger:hover {
31
+ background: var(--d-surface-raised);
32
+ }
33
+
34
+ .d-collapsible > summary::after,
35
+ .d-collapsible-trigger::after {
36
+ content: "";
37
+ width: 0.4rem;
38
+ height: 0.4rem;
39
+ border-right: 2px solid var(--d-text-muted);
40
+ border-bottom: 2px solid var(--d-text-muted);
41
+ transform: rotate(45deg);
42
+ transition: transform var(--d-duration) var(--d-ease);
43
+ flex-shrink: 0;
44
+ }
45
+
46
+ .d-collapsible[open] > summary::after {
47
+ transform: rotate(-135deg);
48
+ }
49
+
50
+ .d-collapsible-body {
51
+ padding: 0 var(--d-space-4) var(--d-space-4);
52
+ font-family: var(--d-font-sans);
53
+ font-size: var(--d-text-sm);
54
+ color: var(--d-text-secondary);
55
+ line-height: var(--d-leading);
56
+ }
57
+
58
+ .d-collapsible[open] > .d-collapsible-body,
59
+ .d-collapsible.open .d-collapsible-body {
60
+ padding-top: var(--d-space-2);
61
+ }
@@ -0,0 +1,85 @@
1
+ /* Dalila UI — Combobox */
2
+
3
+ .d-combobox {
4
+ position: relative;
5
+ }
6
+
7
+ .d-combobox-input {
8
+ width: 100%;
9
+ padding-right: var(--d-space-8);
10
+ }
11
+
12
+ .d-combobox-trigger {
13
+ position: absolute;
14
+ right: var(--d-space-2);
15
+ top: 50%;
16
+ transform: translateY(-50%);
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ width: 1.5rem;
21
+ height: 1.5rem;
22
+ background: none;
23
+ border: none;
24
+ color: var(--d-text-muted);
25
+ cursor: pointer;
26
+ border-radius: var(--d-radius-sm);
27
+ transition: color var(--d-duration-fast) ease;
28
+ }
29
+
30
+ .d-combobox-trigger:hover {
31
+ color: var(--d-text-primary);
32
+ }
33
+
34
+ .d-combobox-list {
35
+ position: absolute;
36
+ top: calc(100% + 4px);
37
+ left: 0;
38
+ right: 0;
39
+ z-index: var(--d-z-dropdown);
40
+ max-height: 12rem;
41
+ overflow-y: auto;
42
+ padding: var(--d-space-1) 0;
43
+ background: var(--d-surface-card);
44
+ border: 1px solid var(--d-border-color);
45
+ border-radius: var(--d-radius-md);
46
+ box-shadow: var(--d-shadow-md);
47
+ list-style: none;
48
+ margin: 0;
49
+
50
+ opacity: 0;
51
+ visibility: hidden;
52
+ transition: all var(--d-duration-fast) ease;
53
+ }
54
+
55
+ .d-combobox.open .d-combobox-list {
56
+ opacity: 1;
57
+ visibility: visible;
58
+ }
59
+
60
+ .d-combobox-option {
61
+ padding: var(--d-space-2) var(--d-space-3);
62
+ font-family: var(--d-font-sans);
63
+ font-size: var(--d-text-sm);
64
+ color: var(--d-text-primary);
65
+ cursor: pointer;
66
+ transition: background var(--d-duration-fast) ease;
67
+ }
68
+
69
+ .d-combobox-option:hover,
70
+ .d-combobox-option.highlighted {
71
+ background: var(--d-surface-raised);
72
+ }
73
+
74
+ .d-combobox-option.selected {
75
+ color: var(--d-accent);
76
+ font-weight: var(--d-font-medium);
77
+ }
78
+
79
+ .d-combobox-empty {
80
+ padding: var(--d-space-3);
81
+ font-family: var(--d-font-sans);
82
+ font-size: var(--d-text-sm);
83
+ color: var(--d-text-muted);
84
+ text-align: center;
85
+ }
@@ -0,0 +1,181 @@
1
+ import { signal, computed } from "../../../core/signal.js";
2
+ import { getCurrentScope } from "../../../core/scope.js";
3
+ import type { Combobox, ComboboxOptions, ComboboxOption } from "../ui-types.js";
4
+ import { validateComboboxOptions } from "../validate.js";
5
+ import { isBrowser } from "../env.js";
6
+
7
+ let comboboxUid = 0;
8
+
9
+ export function createCombobox(options: ComboboxOptions): Combobox {
10
+ validateComboboxOptions(options as unknown as Record<string, unknown>);
11
+ const { options: items, placeholder = "", name: fieldName } = options;
12
+
13
+ const open = signal(false);
14
+ const query = signal("");
15
+ const value = signal("");
16
+ const label = signal("");
17
+ const highlightedIndex = signal(-1);
18
+
19
+ const filtered = computed<ComboboxOption[]>(() => {
20
+ const q = query().toLowerCase();
21
+ if (!q) return items;
22
+ return items.filter((opt) => opt.label.toLowerCase().includes(q));
23
+ });
24
+
25
+ const show = () => open.set(true);
26
+ const close = () => {
27
+ open.set(false);
28
+ highlightedIndex.set(-1);
29
+ };
30
+ const toggle = () => open.update((v) => !v);
31
+
32
+ const handleInput = (ev: Event) => {
33
+ const input = ev.target as HTMLInputElement;
34
+ query.set(input.value);
35
+ open.set(true);
36
+ highlightedIndex.set(-1);
37
+ };
38
+
39
+ const handleSelect = (ev: Event) => {
40
+ const target = (ev.target as HTMLElement).closest<HTMLElement>(
41
+ "[data-value]"
42
+ );
43
+ if (!target) return;
44
+ const val = target.dataset.value ?? "";
45
+ const lbl = target.textContent?.trim() ?? val;
46
+ value.set(val);
47
+ label.set(lbl);
48
+ query.set(lbl);
49
+ close();
50
+ };
51
+
52
+ const handleKeydown = (ev: KeyboardEvent) => {
53
+ const list = filtered();
54
+
55
+ switch (ev.key) {
56
+ case "ArrowDown":
57
+ ev.preventDefault();
58
+ if (!open()) {
59
+ show();
60
+ } else {
61
+ highlightedIndex.update((i) =>
62
+ i < list.length - 1 ? i + 1 : 0
63
+ );
64
+ }
65
+ break;
66
+
67
+ case "ArrowUp":
68
+ ev.preventDefault();
69
+ if (open()) {
70
+ highlightedIndex.update((i) =>
71
+ i > 0 ? i - 1 : list.length - 1
72
+ );
73
+ }
74
+ break;
75
+
76
+ case "Enter":
77
+ ev.preventDefault();
78
+ if (open() && highlightedIndex() >= 0) {
79
+ const opt = list[highlightedIndex()];
80
+ if (opt) {
81
+ value.set(opt.value);
82
+ label.set(opt.label);
83
+ query.set(opt.label);
84
+ close();
85
+ }
86
+ }
87
+ break;
88
+
89
+ case "Escape":
90
+ close();
91
+ break;
92
+ }
93
+ };
94
+
95
+ const _attachTo = (el: HTMLElement) => {
96
+ const scope = getCurrentScope();
97
+ const uid = ++comboboxUid;
98
+
99
+ // ARIA setup
100
+ const input = el.querySelector<HTMLElement>("input");
101
+ const list = el.querySelector<HTMLElement>(
102
+ "ul, [data-d-tag='d-combobox-list'], .d-combobox-list"
103
+ );
104
+
105
+ if (input) {
106
+ if (placeholder) input.setAttribute("placeholder", placeholder);
107
+ input.setAttribute("role", "combobox");
108
+ input.setAttribute("aria-autocomplete", "list");
109
+ input.setAttribute("aria-expanded", "false");
110
+ }
111
+
112
+ if (list) {
113
+ const listId = list.id || `d-combobox-list-${uid}`;
114
+ if (!list.id) list.id = listId;
115
+ list.setAttribute("role", "listbox");
116
+ if (input) input.setAttribute("aria-controls", listId);
117
+ }
118
+
119
+ const unsubExpanded = open.on((isOpen) => {
120
+ input?.setAttribute("aria-expanded", String(isOpen));
121
+ });
122
+
123
+ const unsubHighlight = highlightedIndex.on((idx) => {
124
+ if (idx >= 0 && list) {
125
+ const opts = list.querySelectorAll("[data-value]");
126
+ const active = opts[idx] as HTMLElement | undefined;
127
+ if (active) {
128
+ const activeId = active.id || `d-combobox-opt-${uid}-${idx}`;
129
+ if (!active.id) active.id = activeId;
130
+ active.setAttribute("role", "option");
131
+ input?.setAttribute("aria-activedescendant", activeId);
132
+ }
133
+ } else {
134
+ input?.removeAttribute("aria-activedescendant");
135
+ }
136
+ });
137
+
138
+ // Hidden input for form submission
139
+ let hidden = el.querySelector<HTMLInputElement>('input[type="hidden"]');
140
+ if (!hidden) {
141
+ hidden = el.ownerDocument.createElement("input");
142
+ hidden.type = "hidden";
143
+ hidden.name = fieldName || (input as HTMLInputElement | null)?.getAttribute("name") || "";
144
+ el.appendChild(hidden);
145
+ }
146
+
147
+ const unsubValue = value.on((v) => {
148
+ if (hidden) hidden.value = v;
149
+ });
150
+
151
+ const onDocClick = (e: MouseEvent) => {
152
+ if (!el.contains(e.target as Node)) close();
153
+ };
154
+ if (isBrowser) document.addEventListener("click", onDocClick);
155
+
156
+ if (scope) {
157
+ scope.onCleanup(() => {
158
+ unsubExpanded();
159
+ unsubHighlight();
160
+ unsubValue();
161
+ if (isBrowser) document.removeEventListener("click", onDocClick);
162
+ });
163
+ }
164
+ };
165
+
166
+ return {
167
+ open,
168
+ query,
169
+ value,
170
+ label,
171
+ filtered,
172
+ highlightedIndex,
173
+ show,
174
+ close,
175
+ toggle,
176
+ handleInput,
177
+ handleSelect,
178
+ handleKeydown,
179
+ _attachTo,
180
+ };
181
+ }