dalila 1.5.13 → 1.7.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 (137) hide show
  1. package/README.md +47 -0
  2. package/dist/componentes/ui/accordion/index.d.ts +2 -0
  3. package/dist/componentes/ui/accordion/index.js +114 -0
  4. package/dist/componentes/ui/calendar/index.d.ts +2 -0
  5. package/dist/componentes/ui/calendar/index.js +132 -0
  6. package/dist/componentes/ui/combobox/index.d.ts +2 -0
  7. package/dist/componentes/ui/combobox/index.js +161 -0
  8. package/dist/componentes/ui/dialog/index.d.ts +10 -0
  9. package/dist/componentes/ui/dialog/index.js +54 -0
  10. package/dist/componentes/ui/drawer/index.d.ts +2 -0
  11. package/dist/componentes/ui/drawer/index.js +41 -0
  12. package/dist/componentes/ui/dropdown/index.d.ts +2 -0
  13. package/dist/componentes/ui/dropdown/index.js +48 -0
  14. package/dist/componentes/ui/dropzone/index.d.ts +2 -0
  15. package/dist/componentes/ui/dropzone/index.js +92 -0
  16. package/dist/componentes/ui/env.d.ts +1 -0
  17. package/dist/componentes/ui/env.js +2 -0
  18. package/dist/componentes/ui/index.d.ts +13 -0
  19. package/dist/componentes/ui/index.js +12 -0
  20. package/dist/componentes/ui/popover/index.d.ts +2 -0
  21. package/dist/componentes/ui/popover/index.js +156 -0
  22. package/dist/componentes/ui/runtime.d.ts +20 -0
  23. package/dist/componentes/ui/runtime.js +421 -0
  24. package/dist/componentes/ui/tabs/index.d.ts +3 -0
  25. package/dist/componentes/ui/tabs/index.js +101 -0
  26. package/dist/componentes/ui/toast/index.d.ts +3 -0
  27. package/dist/componentes/ui/toast/index.js +115 -0
  28. package/dist/componentes/ui/ui-types.d.ts +175 -0
  29. package/dist/componentes/ui/ui-types.js +1 -0
  30. package/dist/componentes/ui/validate.d.ts +7 -0
  31. package/dist/componentes/ui/validate.js +71 -0
  32. package/dist/components/ui/accordion/index.d.ts +2 -0
  33. package/dist/components/ui/accordion/index.js +114 -0
  34. package/dist/components/ui/calendar/index.d.ts +2 -0
  35. package/dist/components/ui/calendar/index.js +132 -0
  36. package/dist/components/ui/combobox/index.d.ts +2 -0
  37. package/dist/components/ui/combobox/index.js +161 -0
  38. package/dist/components/ui/dialog/index.d.ts +10 -0
  39. package/dist/components/ui/dialog/index.js +54 -0
  40. package/dist/components/ui/drawer/index.d.ts +2 -0
  41. package/dist/components/ui/drawer/index.js +41 -0
  42. package/dist/components/ui/dropdown/index.d.ts +2 -0
  43. package/dist/components/ui/dropdown/index.js +48 -0
  44. package/dist/components/ui/dropzone/index.d.ts +2 -0
  45. package/dist/components/ui/dropzone/index.js +92 -0
  46. package/dist/components/ui/env.d.ts +1 -0
  47. package/dist/components/ui/env.js +2 -0
  48. package/dist/components/ui/index.d.ts +13 -0
  49. package/dist/components/ui/index.js +12 -0
  50. package/dist/components/ui/popover/index.d.ts +2 -0
  51. package/dist/components/ui/popover/index.js +156 -0
  52. package/dist/components/ui/runtime.d.ts +20 -0
  53. package/dist/components/ui/runtime.js +421 -0
  54. package/dist/components/ui/tabs/index.d.ts +3 -0
  55. package/dist/components/ui/tabs/index.js +101 -0
  56. package/dist/components/ui/toast/index.d.ts +3 -0
  57. package/dist/components/ui/toast/index.js +115 -0
  58. package/dist/components/ui/ui-types.d.ts +175 -0
  59. package/dist/components/ui/ui-types.js +1 -0
  60. package/dist/components/ui/validate.d.ts +7 -0
  61. package/dist/components/ui/validate.js +71 -0
  62. package/dist/form/form-types.d.ts +181 -0
  63. package/dist/form/form-types.js +4 -0
  64. package/dist/form/form.d.ts +71 -0
  65. package/dist/form/form.js +1073 -0
  66. package/dist/form/index.d.ts +2 -0
  67. package/dist/form/index.js +2 -0
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +1 -0
  70. package/dist/runtime/bind.js +567 -9
  71. package/dist/ui/accordion.d.ts +2 -0
  72. package/dist/ui/accordion.js +114 -0
  73. package/dist/ui/calendar.d.ts +2 -0
  74. package/dist/ui/calendar.js +132 -0
  75. package/dist/ui/combobox.d.ts +2 -0
  76. package/dist/ui/combobox.js +161 -0
  77. package/dist/ui/dialog.d.ts +10 -0
  78. package/dist/ui/dialog.js +54 -0
  79. package/dist/ui/drawer.d.ts +2 -0
  80. package/dist/ui/drawer.js +41 -0
  81. package/dist/ui/dropdown.d.ts +2 -0
  82. package/dist/ui/dropdown.js +48 -0
  83. package/dist/ui/dropzone.d.ts +2 -0
  84. package/dist/ui/dropzone.js +92 -0
  85. package/dist/ui/env.d.ts +1 -0
  86. package/dist/ui/env.js +2 -0
  87. package/dist/ui/index.d.ts +13 -0
  88. package/dist/ui/index.js +12 -0
  89. package/dist/ui/popover.d.ts +2 -0
  90. package/dist/ui/popover.js +156 -0
  91. package/dist/ui/runtime.d.ts +20 -0
  92. package/dist/ui/runtime.js +421 -0
  93. package/dist/ui/tabs.d.ts +3 -0
  94. package/dist/ui/tabs.js +101 -0
  95. package/dist/ui/toast.d.ts +3 -0
  96. package/dist/ui/toast.js +115 -0
  97. package/dist/ui/ui-types.d.ts +175 -0
  98. package/dist/ui/ui-types.js +1 -0
  99. package/dist/ui/validate.d.ts +7 -0
  100. package/dist/ui/validate.js +71 -0
  101. package/package.json +60 -2
  102. package/src/components/ui/accordion/accordion.css +90 -0
  103. package/src/components/ui/alert/alert.css +78 -0
  104. package/src/components/ui/avatar/avatar.css +45 -0
  105. package/src/components/ui/badge/badge.css +71 -0
  106. package/src/components/ui/breadcrumb/breadcrumb.css +41 -0
  107. package/src/components/ui/button/button.css +135 -0
  108. package/src/components/ui/calendar/calendar.css +96 -0
  109. package/src/components/ui/card/card.css +93 -0
  110. package/src/components/ui/checkbox/checkbox.css +57 -0
  111. package/src/components/ui/chip/chip.css +62 -0
  112. package/src/components/ui/collapsible/collapsible.css +61 -0
  113. package/src/components/ui/combobox/combobox.css +85 -0
  114. package/src/components/ui/dalila/dalila.css +42 -0
  115. package/src/components/ui/dalila-core/dalila-core.css +14 -0
  116. package/src/components/ui/dialog/dialog.css +125 -0
  117. package/src/components/ui/drawer/drawer.css +122 -0
  118. package/src/components/ui/dropdown/dropdown.css +87 -0
  119. package/src/components/ui/dropzone/dropzone.css +47 -0
  120. package/src/components/ui/empty-state/empty-state.css +33 -0
  121. package/src/components/ui/form/form.css +44 -0
  122. package/src/components/ui/input/input.css +106 -0
  123. package/src/components/ui/layout/layout.css +62 -0
  124. package/src/components/ui/pagination/pagination.css +55 -0
  125. package/src/components/ui/popover/popover.css +55 -0
  126. package/src/components/ui/radio/radio.css +56 -0
  127. package/src/components/ui/separator/separator.css +38 -0
  128. package/src/components/ui/skeleton/skeleton.css +57 -0
  129. package/src/components/ui/slider/slider.css +60 -0
  130. package/src/components/ui/spinner/spinner.css +38 -0
  131. package/src/components/ui/table/table.css +54 -0
  132. package/src/components/ui/tabs/tabs.css +74 -0
  133. package/src/components/ui/toast/toast.css +100 -0
  134. package/src/components/ui/toggle/toggle.css +90 -0
  135. package/src/components/ui/tokens/tokens.css +161 -0
  136. package/src/components/ui/tooltip/tooltip.css +53 -0
  137. package/src/components/ui/typography/typography.css +81 -0
package/README.md CHANGED
@@ -77,6 +77,10 @@ bind(document.getElementById('app')!, ctx);
77
77
  - [Query](./docs/core/query.md) — Cached queries
78
78
  - [Mutations](./docs/core/mutation.md) — Write operations
79
79
 
80
+ ### Forms
81
+
82
+ - [Forms](./docs/forms.md) — DOM-first form management with validation, field arrays, and accessibility
83
+
80
84
  ### Utilities
81
85
 
82
86
  - [Scheduler](./docs/core/scheduler.md) — Batching and coordination
@@ -189,6 +193,49 @@ const router = createRouter({
189
193
  router.start();
190
194
  ```
191
195
 
196
+ ### Forms
197
+
198
+ ```ts
199
+ import { createForm } from 'dalila';
200
+
201
+ const userForm = createForm({
202
+ defaultValues: { name: '', email: '' },
203
+ validate: (data) => {
204
+ const errors: Record<string, string> = {};
205
+ if (!data.name) errors.name = 'Name is required';
206
+ if (!data.email?.includes('@')) errors.email = 'Invalid email';
207
+ return errors;
208
+ }
209
+ });
210
+
211
+ async function handleSubmit(data, { signal }) {
212
+ await fetch('/api/users', {
213
+ method: 'POST',
214
+ body: JSON.stringify(data),
215
+ signal
216
+ });
217
+ }
218
+ ```
219
+
220
+ ```html
221
+ <form d-form="userForm" d-on-submit="handleSubmit">
222
+ <label>
223
+ Name
224
+ <input d-field="name" />
225
+ </label>
226
+ <span d-error="name"></span>
227
+
228
+ <label>
229
+ Email
230
+ <input d-field="email" type="email" />
231
+ </label>
232
+ <span d-error="email"></span>
233
+
234
+ <button type="submit">Save</button>
235
+ <span d-form-error="userForm"></span>
236
+ </form>
237
+ ```
238
+
192
239
  ## Development
193
240
 
194
241
  ```bash
@@ -0,0 +1,2 @@
1
+ import type { Accordion, AccordionOptions } from "../ui-types.js";
2
+ export declare function createAccordion(options?: AccordionOptions): Accordion;
@@ -0,0 +1,114 @@
1
+ import { signal, computed } from "../../../core/signal.js";
2
+ import { getCurrentScope } from "../../../core/scope.js";
3
+ export function createAccordion(options = {}) {
4
+ const { single = false, initial = [] } = options;
5
+ const hasInitial = Object.prototype.hasOwnProperty.call(options, "initial");
6
+ const seededInitial = single ? initial.slice(0, 1) : initial;
7
+ const openItems = signal(new Set(seededInitial));
8
+ const toggle = (itemId) => {
9
+ openItems.update((current) => {
10
+ const next = new Set(current);
11
+ if (next.has(itemId)) {
12
+ next.delete(itemId);
13
+ }
14
+ else {
15
+ if (single)
16
+ next.clear();
17
+ next.add(itemId);
18
+ }
19
+ return next;
20
+ });
21
+ };
22
+ const open = (itemId) => {
23
+ openItems.update((current) => {
24
+ const next = new Set(current);
25
+ if (single)
26
+ next.clear();
27
+ next.add(itemId);
28
+ return next;
29
+ });
30
+ };
31
+ const close = (itemId) => {
32
+ openItems.update((current) => {
33
+ const next = new Set(current);
34
+ next.delete(itemId);
35
+ return next;
36
+ });
37
+ };
38
+ const _isOpenCache = new Map();
39
+ const isOpen = (itemId) => {
40
+ let sig = _isOpenCache.get(itemId);
41
+ if (!sig) {
42
+ sig = computed(() => openItems().has(itemId));
43
+ _isOpenCache.set(itemId, sig);
44
+ }
45
+ return sig;
46
+ };
47
+ const _attachTo = (el) => {
48
+ const scope = getCurrentScope();
49
+ let syncing = false;
50
+ const allDetails = () => Array.from(el.querySelectorAll("details[data-accordion]"));
51
+ const syncDOMFromSignal = (set) => {
52
+ syncing = true;
53
+ for (const details of allDetails()) {
54
+ const itemId = details.dataset.accordion;
55
+ if (!itemId)
56
+ continue;
57
+ details.open = set.has(itemId);
58
+ }
59
+ syncing = false;
60
+ };
61
+ const syncSignalFromDOM = () => {
62
+ const next = new Set();
63
+ for (const details of allDetails()) {
64
+ if (!details.open)
65
+ continue;
66
+ const itemId = details.dataset.accordion;
67
+ if (!itemId)
68
+ continue;
69
+ if (single) {
70
+ next.clear();
71
+ }
72
+ next.add(itemId);
73
+ }
74
+ openItems.set(next);
75
+ };
76
+ const onToggle = (ev) => {
77
+ const details = ev.target;
78
+ if (syncing)
79
+ return;
80
+ const itemId = details.dataset.accordion;
81
+ if (!itemId)
82
+ return;
83
+ openItems.update((current) => {
84
+ const next = new Set(current);
85
+ if (details.open) {
86
+ if (single)
87
+ next.clear();
88
+ next.add(itemId);
89
+ }
90
+ else {
91
+ next.delete(itemId);
92
+ }
93
+ return next;
94
+ });
95
+ };
96
+ const unsub = openItems.on((set) => {
97
+ if (!syncing)
98
+ syncDOMFromSignal(set);
99
+ });
100
+ // If initial was not provided, respect current DOM open state.
101
+ if (!hasInitial)
102
+ syncSignalFromDOM();
103
+ else
104
+ syncDOMFromSignal(openItems());
105
+ el.addEventListener("toggle", onToggle, true);
106
+ if (scope) {
107
+ scope.onCleanup(() => {
108
+ unsub();
109
+ el.removeEventListener("toggle", onToggle, true);
110
+ });
111
+ }
112
+ };
113
+ return { openItems, toggle, open, close, isOpen, _attachTo };
114
+ }
@@ -0,0 +1,2 @@
1
+ import type { Calendar, CalendarOptions } from "../ui-types.js";
2
+ export declare function createCalendar(options?: CalendarOptions): Calendar;
@@ -0,0 +1,132 @@
1
+ import { signal, computed } from "../../../core/signal.js";
2
+ import { batch } from "../../../core/scheduler.js";
3
+ import { validateCalendarOptions } from "../validate.js";
4
+ const DEFAULT_DAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
5
+ const DEFAULT_MONTH_LABELS = [
6
+ "January", "February", "March", "April", "May", "June",
7
+ "July", "August", "September", "October", "November", "December",
8
+ ];
9
+ function isSameDay(a, b) {
10
+ return (a.getFullYear() === b.getFullYear() &&
11
+ a.getMonth() === b.getMonth() &&
12
+ a.getDate() === b.getDate());
13
+ }
14
+ export function createCalendar(options = {}) {
15
+ validateCalendarOptions(options);
16
+ const { initial, min, max, dayLabels: customDayLabels, monthLabels: customMonthLabels, } = options;
17
+ const now = initial ?? new Date();
18
+ const monthLabels = customMonthLabels ?? DEFAULT_MONTH_LABELS;
19
+ const calDayLabels = customDayLabels ?? DEFAULT_DAY_LABELS;
20
+ const year = signal(now.getFullYear());
21
+ const month = signal(now.getMonth());
22
+ const selected = signal(initial ?? null);
23
+ const title = computed(() => `${monthLabels[month()]} ${year()}`);
24
+ const days = computed(() => {
25
+ const y = year();
26
+ const m = month();
27
+ const sel = selected();
28
+ const today = new Date();
29
+ const firstDow = new Date(y, m, 1).getDay();
30
+ const daysInMonth = new Date(y, m + 1, 0).getDate();
31
+ const prevMonthDays = new Date(y, m, 0).getDate();
32
+ const grid = [];
33
+ const GRID_SIZE = 42;
34
+ // Previous month days
35
+ for (let i = firstDow - 1; i >= 0; i--) {
36
+ const date = prevMonthDays - i;
37
+ const fullDate = new Date(y, m - 1, date);
38
+ grid.push({
39
+ date,
40
+ month: "prev",
41
+ fullDate,
42
+ isToday: isSameDay(fullDate, today),
43
+ isSelected: sel !== null && isSameDay(fullDate, sel),
44
+ disabled: isOutOfBounds(fullDate),
45
+ });
46
+ }
47
+ // Current month days
48
+ for (let d = 1; d <= daysInMonth; d++) {
49
+ const fullDate = new Date(y, m, d);
50
+ grid.push({
51
+ date: d,
52
+ month: "current",
53
+ fullDate,
54
+ isToday: isSameDay(fullDate, today),
55
+ isSelected: sel !== null && isSameDay(fullDate, sel),
56
+ disabled: isOutOfBounds(fullDate),
57
+ });
58
+ }
59
+ // Next month days
60
+ const remaining = GRID_SIZE - grid.length;
61
+ for (let d = 1; d <= remaining; d++) {
62
+ const fullDate = new Date(y, m + 1, d);
63
+ grid.push({
64
+ date: d,
65
+ month: "next",
66
+ fullDate,
67
+ isToday: isSameDay(fullDate, today),
68
+ isSelected: sel !== null && isSameDay(fullDate, sel),
69
+ disabled: isOutOfBounds(fullDate),
70
+ });
71
+ }
72
+ return grid;
73
+ });
74
+ function isOutOfBounds(date) {
75
+ if (min && date < min)
76
+ return true;
77
+ if (max && date > max)
78
+ return true;
79
+ return false;
80
+ }
81
+ const prev = () => {
82
+ batch(() => {
83
+ if (month() === 0) {
84
+ month.set(11);
85
+ year.update((y) => y - 1);
86
+ }
87
+ else {
88
+ month.update((m) => m - 1);
89
+ }
90
+ });
91
+ };
92
+ const next = () => {
93
+ batch(() => {
94
+ if (month() === 11) {
95
+ month.set(0);
96
+ year.update((y) => y + 1);
97
+ }
98
+ else {
99
+ month.update((m) => m + 1);
100
+ }
101
+ });
102
+ };
103
+ const select = (date) => {
104
+ if (isOutOfBounds(date))
105
+ return;
106
+ selected.set(date);
107
+ };
108
+ const handleDayClick = (ev) => {
109
+ const target = ev.target.closest("[data-date]");
110
+ if (!target)
111
+ return;
112
+ const dateStr = target.dataset.date;
113
+ if (!dateStr)
114
+ return;
115
+ const date = new Date(dateStr);
116
+ if (isNaN(date.getTime()))
117
+ return;
118
+ select(date);
119
+ };
120
+ return {
121
+ year,
122
+ month,
123
+ selected,
124
+ title,
125
+ days,
126
+ dayLabels: calDayLabels,
127
+ prev,
128
+ next,
129
+ select,
130
+ handleDayClick,
131
+ };
132
+ }
@@ -0,0 +1,2 @@
1
+ import type { Combobox, ComboboxOptions } from "../ui-types.js";
2
+ export declare function createCombobox(options: ComboboxOptions): Combobox;
@@ -0,0 +1,161 @@
1
+ import { signal, computed } from "../../../core/signal.js";
2
+ import { getCurrentScope } from "../../../core/scope.js";
3
+ import { validateComboboxOptions } from "../validate.js";
4
+ import { isBrowser } from "../env.js";
5
+ let comboboxUid = 0;
6
+ export function createCombobox(options) {
7
+ validateComboboxOptions(options);
8
+ const { options: items, placeholder = "", name: fieldName } = options;
9
+ const open = signal(false);
10
+ const query = signal("");
11
+ const value = signal("");
12
+ const label = signal("");
13
+ const highlightedIndex = signal(-1);
14
+ const filtered = computed(() => {
15
+ const q = query().toLowerCase();
16
+ if (!q)
17
+ return items;
18
+ return items.filter((opt) => opt.label.toLowerCase().includes(q));
19
+ });
20
+ const show = () => open.set(true);
21
+ const close = () => {
22
+ open.set(false);
23
+ highlightedIndex.set(-1);
24
+ };
25
+ const toggle = () => open.update((v) => !v);
26
+ const handleInput = (ev) => {
27
+ const input = ev.target;
28
+ query.set(input.value);
29
+ open.set(true);
30
+ highlightedIndex.set(-1);
31
+ };
32
+ const handleSelect = (ev) => {
33
+ const target = ev.target.closest("[data-value]");
34
+ if (!target)
35
+ return;
36
+ const val = target.dataset.value ?? "";
37
+ const lbl = target.textContent?.trim() ?? val;
38
+ value.set(val);
39
+ label.set(lbl);
40
+ query.set(lbl);
41
+ close();
42
+ };
43
+ const handleKeydown = (ev) => {
44
+ const list = filtered();
45
+ switch (ev.key) {
46
+ case "ArrowDown":
47
+ ev.preventDefault();
48
+ if (!open()) {
49
+ show();
50
+ }
51
+ else {
52
+ highlightedIndex.update((i) => i < list.length - 1 ? i + 1 : 0);
53
+ }
54
+ break;
55
+ case "ArrowUp":
56
+ ev.preventDefault();
57
+ if (open()) {
58
+ highlightedIndex.update((i) => i > 0 ? i - 1 : list.length - 1);
59
+ }
60
+ break;
61
+ case "Enter":
62
+ ev.preventDefault();
63
+ if (open() && highlightedIndex() >= 0) {
64
+ const opt = list[highlightedIndex()];
65
+ if (opt) {
66
+ value.set(opt.value);
67
+ label.set(opt.label);
68
+ query.set(opt.label);
69
+ close();
70
+ }
71
+ }
72
+ break;
73
+ case "Escape":
74
+ close();
75
+ break;
76
+ }
77
+ };
78
+ const _attachTo = (el) => {
79
+ const scope = getCurrentScope();
80
+ const uid = ++comboboxUid;
81
+ // ARIA setup
82
+ const input = el.querySelector("input");
83
+ const list = el.querySelector("ul, [data-d-tag='d-combobox-list'], .d-combobox-list");
84
+ if (input) {
85
+ if (placeholder)
86
+ input.setAttribute("placeholder", placeholder);
87
+ input.setAttribute("role", "combobox");
88
+ input.setAttribute("aria-autocomplete", "list");
89
+ input.setAttribute("aria-expanded", "false");
90
+ }
91
+ if (list) {
92
+ const listId = list.id || `d-combobox-list-${uid}`;
93
+ if (!list.id)
94
+ list.id = listId;
95
+ list.setAttribute("role", "listbox");
96
+ if (input)
97
+ input.setAttribute("aria-controls", listId);
98
+ }
99
+ const unsubExpanded = open.on((isOpen) => {
100
+ input?.setAttribute("aria-expanded", String(isOpen));
101
+ });
102
+ const unsubHighlight = highlightedIndex.on((idx) => {
103
+ if (idx >= 0 && list) {
104
+ const opts = list.querySelectorAll("[data-value]");
105
+ const active = opts[idx];
106
+ if (active) {
107
+ const activeId = active.id || `d-combobox-opt-${uid}-${idx}`;
108
+ if (!active.id)
109
+ active.id = activeId;
110
+ active.setAttribute("role", "option");
111
+ input?.setAttribute("aria-activedescendant", activeId);
112
+ }
113
+ }
114
+ else {
115
+ input?.removeAttribute("aria-activedescendant");
116
+ }
117
+ });
118
+ // Hidden input for form submission
119
+ let hidden = el.querySelector('input[type="hidden"]');
120
+ if (!hidden) {
121
+ hidden = el.ownerDocument.createElement("input");
122
+ hidden.type = "hidden";
123
+ hidden.name = fieldName || input?.getAttribute("name") || "";
124
+ el.appendChild(hidden);
125
+ }
126
+ const unsubValue = value.on((v) => {
127
+ if (hidden)
128
+ hidden.value = v;
129
+ });
130
+ const onDocClick = (e) => {
131
+ if (!el.contains(e.target))
132
+ close();
133
+ };
134
+ if (isBrowser)
135
+ document.addEventListener("click", onDocClick);
136
+ if (scope) {
137
+ scope.onCleanup(() => {
138
+ unsubExpanded();
139
+ unsubHighlight();
140
+ unsubValue();
141
+ if (isBrowser)
142
+ document.removeEventListener("click", onDocClick);
143
+ });
144
+ }
145
+ };
146
+ return {
147
+ open,
148
+ query,
149
+ value,
150
+ label,
151
+ filtered,
152
+ highlightedIndex,
153
+ show,
154
+ close,
155
+ toggle,
156
+ handleInput,
157
+ handleSelect,
158
+ handleKeydown,
159
+ _attachTo,
160
+ };
161
+ }
@@ -0,0 +1,10 @@
1
+ import { type Signal } from "../../../core/signal.js";
2
+ import type { Dialog, DialogOptions } from "../ui-types.js";
3
+ /**
4
+ * Shared dialog behavior — used by both createDialog and createDrawer.
5
+ */
6
+ export declare function _attachDialogBehavior(el: HTMLDialogElement, open: Signal<boolean>, closeFn: () => void, opts: {
7
+ closeOnBackdrop: boolean;
8
+ closeOnEscape: boolean;
9
+ }): void;
10
+ export declare function createDialog(options?: DialogOptions): Dialog;
@@ -0,0 +1,54 @@
1
+ import { signal } from "../../../core/signal.js";
2
+ import { getCurrentScope } from "../../../core/scope.js";
3
+ import { validateDialogOptions } from "../validate.js";
4
+ /**
5
+ * Shared dialog behavior — used by both createDialog and createDrawer.
6
+ */
7
+ export function _attachDialogBehavior(el, open, closeFn, opts) {
8
+ const scope = getCurrentScope();
9
+ // Sync signal → native dialog
10
+ const unsub = open.on((isOpen) => {
11
+ if (isOpen && !el.open)
12
+ el.showModal();
13
+ else if (!isOpen && el.open)
14
+ el.close();
15
+ });
16
+ // Native close event → sync signal
17
+ const onClose = () => open.set(false);
18
+ el.addEventListener("close", onClose);
19
+ // Backdrop click
20
+ const onBackdropClick = (e) => {
21
+ if (opts.closeOnBackdrop && e.target === el)
22
+ closeFn();
23
+ };
24
+ el.addEventListener("click", onBackdropClick);
25
+ // Escape key
26
+ if (!opts.closeOnEscape) {
27
+ const onCancel = (e) => e.preventDefault();
28
+ el.addEventListener("cancel", onCancel);
29
+ if (scope) {
30
+ scope.onCleanup(() => el.removeEventListener("cancel", onCancel));
31
+ }
32
+ }
33
+ // ARIA
34
+ el.setAttribute("aria-modal", "true");
35
+ if (scope) {
36
+ scope.onCleanup(() => {
37
+ unsub();
38
+ el.removeEventListener("close", onClose);
39
+ el.removeEventListener("click", onBackdropClick);
40
+ });
41
+ }
42
+ }
43
+ export function createDialog(options = {}) {
44
+ validateDialogOptions(options);
45
+ const { closeOnBackdrop = true, closeOnEscape = true } = options;
46
+ const open = signal(false);
47
+ const show = () => open.set(true);
48
+ const close = () => open.set(false);
49
+ const toggle = () => open.update((v) => !v);
50
+ const _attachTo = (el) => {
51
+ _attachDialogBehavior(el, open, close, { closeOnBackdrop, closeOnEscape });
52
+ };
53
+ return { open, show, close, toggle, _attachTo };
54
+ }
@@ -0,0 +1,2 @@
1
+ import type { Drawer, DrawerOptions } from "../ui-types.js";
2
+ export declare function createDrawer(options?: DrawerOptions): Drawer;
@@ -0,0 +1,41 @@
1
+ import { signal } from "../../../core/signal.js";
2
+ import { getCurrentScope } from "../../../core/scope.js";
3
+ import { _attachDialogBehavior } from "../dialog/index.js";
4
+ import { validateDrawerOptions } from "../validate.js";
5
+ const SIDE_CLASSES = {
6
+ right: "",
7
+ left: "d-drawer-left",
8
+ bottom: "d-sheet",
9
+ };
10
+ export function createDrawer(options = {}) {
11
+ validateDrawerOptions(options);
12
+ const { closeOnBackdrop = true, closeOnEscape = true, side: initialSide = "right", } = options;
13
+ const open = signal(false);
14
+ const side = signal(initialSide);
15
+ const show = () => open.set(true);
16
+ const close = () => open.set(false);
17
+ const toggle = () => open.update((v) => !v);
18
+ const _attachTo = (el) => {
19
+ const scope = getCurrentScope();
20
+ // Shared dialog behavior (open sync, backdrop, escape, ARIA)
21
+ _attachDialogBehavior(el, open, close, { closeOnBackdrop, closeOnEscape });
22
+ // Apply initial side class
23
+ const initial = SIDE_CLASSES[side()];
24
+ if (initial)
25
+ el.classList.add(initial);
26
+ // React to side changes
27
+ const unsub = side.on((s) => {
28
+ for (const cls of Object.values(SIDE_CLASSES)) {
29
+ if (cls)
30
+ el.classList.remove(cls);
31
+ }
32
+ const cls = SIDE_CLASSES[s];
33
+ if (cls)
34
+ el.classList.add(cls);
35
+ });
36
+ if (scope) {
37
+ scope.onCleanup(() => unsub());
38
+ }
39
+ };
40
+ return { open, side, show, close, toggle, _attachTo };
41
+ }
@@ -0,0 +1,2 @@
1
+ import type { Dropdown, DropdownOptions } from "../ui-types.js";
2
+ export declare function createDropdown(options?: DropdownOptions): Dropdown;
@@ -0,0 +1,48 @@
1
+ import { signal } from "../../../core/signal.js";
2
+ import { getCurrentScope } from "../../../core/scope.js";
3
+ import { isBrowser } from "../env.js";
4
+ export function createDropdown(options = {}) {
5
+ const { closeOnSelect = true } = options;
6
+ const open = signal(false);
7
+ const toggle = (ev) => {
8
+ if (ev)
9
+ ev.stopPropagation();
10
+ open.update((v) => !v);
11
+ };
12
+ const close = () => open.set(false);
13
+ const select = (ev) => {
14
+ if (ev)
15
+ ev.stopPropagation();
16
+ if (closeOnSelect)
17
+ close();
18
+ };
19
+ const _attachTo = (el) => {
20
+ const scope = getCurrentScope();
21
+ // ARIA
22
+ const trigger = el.querySelector("button, [role='button'], [data-d-tag='d-button']");
23
+ const menu = el.querySelector("[data-d-tag='d-menu'], .d-menu");
24
+ if (menu)
25
+ menu.setAttribute("role", "menu");
26
+ if (trigger) {
27
+ trigger.setAttribute("aria-haspopup", "true");
28
+ trigger.setAttribute("aria-expanded", "false");
29
+ }
30
+ const unsubAria = open.on((isOpen) => {
31
+ trigger?.setAttribute("aria-expanded", String(isOpen));
32
+ });
33
+ const onDocClick = (e) => {
34
+ if (!el.contains(e.target))
35
+ close();
36
+ };
37
+ if (isBrowser)
38
+ document.addEventListener("click", onDocClick);
39
+ if (scope) {
40
+ scope.onCleanup(() => {
41
+ unsubAria();
42
+ if (isBrowser)
43
+ document.removeEventListener("click", onDocClick);
44
+ });
45
+ }
46
+ };
47
+ return { open, toggle, close, select, _attachTo };
48
+ }
@@ -0,0 +1,2 @@
1
+ import type { Dropzone, DropzoneOptions } from "../ui-types.js";
2
+ export declare function createDropzone(options?: DropzoneOptions): Dropzone;