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.
- package/README.md +47 -0
- package/dist/componentes/ui/accordion/index.d.ts +2 -0
- package/dist/componentes/ui/accordion/index.js +114 -0
- package/dist/componentes/ui/calendar/index.d.ts +2 -0
- package/dist/componentes/ui/calendar/index.js +132 -0
- package/dist/componentes/ui/combobox/index.d.ts +2 -0
- package/dist/componentes/ui/combobox/index.js +161 -0
- package/dist/componentes/ui/dialog/index.d.ts +10 -0
- package/dist/componentes/ui/dialog/index.js +54 -0
- package/dist/componentes/ui/drawer/index.d.ts +2 -0
- package/dist/componentes/ui/drawer/index.js +41 -0
- package/dist/componentes/ui/dropdown/index.d.ts +2 -0
- package/dist/componentes/ui/dropdown/index.js +48 -0
- package/dist/componentes/ui/dropzone/index.d.ts +2 -0
- package/dist/componentes/ui/dropzone/index.js +92 -0
- package/dist/componentes/ui/env.d.ts +1 -0
- package/dist/componentes/ui/env.js +2 -0
- package/dist/componentes/ui/index.d.ts +13 -0
- package/dist/componentes/ui/index.js +12 -0
- package/dist/componentes/ui/popover/index.d.ts +2 -0
- package/dist/componentes/ui/popover/index.js +156 -0
- package/dist/componentes/ui/runtime.d.ts +20 -0
- package/dist/componentes/ui/runtime.js +421 -0
- package/dist/componentes/ui/tabs/index.d.ts +3 -0
- package/dist/componentes/ui/tabs/index.js +101 -0
- package/dist/componentes/ui/toast/index.d.ts +3 -0
- package/dist/componentes/ui/toast/index.js +115 -0
- package/dist/componentes/ui/ui-types.d.ts +175 -0
- package/dist/componentes/ui/ui-types.js +1 -0
- package/dist/componentes/ui/validate.d.ts +7 -0
- package/dist/componentes/ui/validate.js +71 -0
- package/dist/components/ui/accordion/index.d.ts +2 -0
- package/dist/components/ui/accordion/index.js +114 -0
- package/dist/components/ui/calendar/index.d.ts +2 -0
- package/dist/components/ui/calendar/index.js +132 -0
- package/dist/components/ui/combobox/index.d.ts +2 -0
- package/dist/components/ui/combobox/index.js +161 -0
- package/dist/components/ui/dialog/index.d.ts +10 -0
- package/dist/components/ui/dialog/index.js +54 -0
- package/dist/components/ui/drawer/index.d.ts +2 -0
- package/dist/components/ui/drawer/index.js +41 -0
- package/dist/components/ui/dropdown/index.d.ts +2 -0
- package/dist/components/ui/dropdown/index.js +48 -0
- package/dist/components/ui/dropzone/index.d.ts +2 -0
- package/dist/components/ui/dropzone/index.js +92 -0
- package/dist/components/ui/env.d.ts +1 -0
- package/dist/components/ui/env.js +2 -0
- package/dist/components/ui/index.d.ts +13 -0
- package/dist/components/ui/index.js +12 -0
- package/dist/components/ui/popover/index.d.ts +2 -0
- package/dist/components/ui/popover/index.js +156 -0
- package/dist/components/ui/runtime.d.ts +20 -0
- package/dist/components/ui/runtime.js +421 -0
- package/dist/components/ui/tabs/index.d.ts +3 -0
- package/dist/components/ui/tabs/index.js +101 -0
- package/dist/components/ui/toast/index.d.ts +3 -0
- package/dist/components/ui/toast/index.js +115 -0
- package/dist/components/ui/ui-types.d.ts +175 -0
- package/dist/components/ui/ui-types.js +1 -0
- package/dist/components/ui/validate.d.ts +7 -0
- package/dist/components/ui/validate.js +71 -0
- package/dist/form/form-types.d.ts +181 -0
- package/dist/form/form-types.js +4 -0
- package/dist/form/form.d.ts +71 -0
- package/dist/form/form.js +1073 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/bind.js +567 -9
- package/dist/ui/accordion.d.ts +2 -0
- package/dist/ui/accordion.js +114 -0
- package/dist/ui/calendar.d.ts +2 -0
- package/dist/ui/calendar.js +132 -0
- package/dist/ui/combobox.d.ts +2 -0
- package/dist/ui/combobox.js +161 -0
- package/dist/ui/dialog.d.ts +10 -0
- package/dist/ui/dialog.js +54 -0
- package/dist/ui/drawer.d.ts +2 -0
- package/dist/ui/drawer.js +41 -0
- package/dist/ui/dropdown.d.ts +2 -0
- package/dist/ui/dropdown.js +48 -0
- package/dist/ui/dropzone.d.ts +2 -0
- package/dist/ui/dropzone.js +92 -0
- package/dist/ui/env.d.ts +1 -0
- package/dist/ui/env.js +2 -0
- package/dist/ui/index.d.ts +13 -0
- package/dist/ui/index.js +12 -0
- package/dist/ui/popover.d.ts +2 -0
- package/dist/ui/popover.js +156 -0
- package/dist/ui/runtime.d.ts +20 -0
- package/dist/ui/runtime.js +421 -0
- package/dist/ui/tabs.d.ts +3 -0
- package/dist/ui/tabs.js +101 -0
- package/dist/ui/toast.d.ts +3 -0
- package/dist/ui/toast.js +115 -0
- package/dist/ui/ui-types.d.ts +175 -0
- package/dist/ui/ui-types.js +1 -0
- package/dist/ui/validate.d.ts +7 -0
- package/dist/ui/validate.js +71 -0
- package/package.json +60 -2
- package/src/components/ui/accordion/accordion.css +90 -0
- package/src/components/ui/alert/alert.css +78 -0
- package/src/components/ui/avatar/avatar.css +45 -0
- package/src/components/ui/badge/badge.css +71 -0
- package/src/components/ui/breadcrumb/breadcrumb.css +41 -0
- package/src/components/ui/button/button.css +135 -0
- package/src/components/ui/calendar/calendar.css +96 -0
- package/src/components/ui/card/card.css +93 -0
- package/src/components/ui/checkbox/checkbox.css +57 -0
- package/src/components/ui/chip/chip.css +62 -0
- package/src/components/ui/collapsible/collapsible.css +61 -0
- package/src/components/ui/combobox/combobox.css +85 -0
- package/src/components/ui/dalila/dalila.css +42 -0
- package/src/components/ui/dalila-core/dalila-core.css +14 -0
- package/src/components/ui/dialog/dialog.css +125 -0
- package/src/components/ui/drawer/drawer.css +122 -0
- package/src/components/ui/dropdown/dropdown.css +87 -0
- package/src/components/ui/dropzone/dropzone.css +47 -0
- package/src/components/ui/empty-state/empty-state.css +33 -0
- package/src/components/ui/form/form.css +44 -0
- package/src/components/ui/input/input.css +106 -0
- package/src/components/ui/layout/layout.css +62 -0
- package/src/components/ui/pagination/pagination.css +55 -0
- package/src/components/ui/popover/popover.css +55 -0
- package/src/components/ui/radio/radio.css +56 -0
- package/src/components/ui/separator/separator.css +38 -0
- package/src/components/ui/skeleton/skeleton.css +57 -0
- package/src/components/ui/slider/slider.css +60 -0
- package/src/components/ui/spinner/spinner.css +38 -0
- package/src/components/ui/table/table.css +54 -0
- package/src/components/ui/tabs/tabs.css +74 -0
- package/src/components/ui/toast/toast.css +100 -0
- package/src/components/ui/toggle/toggle.css +90 -0
- package/src/components/ui/tokens/tokens.css +161 -0
- package/src/components/ui/tooltip/tooltip.css +53 -0
- 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,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,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,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,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,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
|
+
}
|