create-dalila 1.2.0 → 1.2.2
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/package.json +1 -1
- package/template/src/style.css +8 -6
- package/template/src/components/ui/accordion/index.ts +0 -121
- package/template/src/components/ui/calendar/index.ts +0 -157
- package/template/src/components/ui/combobox/index.ts +0 -181
- package/template/src/components/ui/dialog/index.ts +0 -68
- package/template/src/components/ui/drawer/index.ts +0 -53
- package/template/src/components/ui/dropdown/index.ts +0 -57
- package/template/src/components/ui/dropzone/index.ts +0 -114
- package/template/src/components/ui/env.ts +0 -4
- package/template/src/components/ui/index.ts +0 -13
- package/template/src/components/ui/popover/index.ts +0 -185
- package/template/src/components/ui/runtime.ts +0 -514
- package/template/src/components/ui/tabs/index.ts +0 -128
- package/template/src/components/ui/toast/index.ts +0 -144
- package/template/src/components/ui/ui-types.ts +0 -238
- package/template/src/components/ui/validate.ts +0 -83
package/package.json
CHANGED
package/template/src/style.css
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dalila UI Components
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 1. Import styles locally:
|
|
5
|
+
* @import './components/ui/button/button.css';
|
|
6
|
+
* @import './components/ui/card/card.css';
|
|
7
|
+
* @import './components/ui/dialog/dialog.css';
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @import './components/ui/dialog/dialog.css';
|
|
9
|
+
* Or import all at once:
|
|
10
|
+
* @import './components/ui/dalila/dalila.css';
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* 2. Import JavaScript functionality from the npm package:
|
|
13
|
+
* import { createDialog, createToast } from 'dalila/components/ui';
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
* {
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { signal, computed, type Signal } from "../../../core/signal.js";
|
|
2
|
-
import { getCurrentScope } from "../../../core/scope.js";
|
|
3
|
-
import type { Accordion, AccordionOptions } from "../ui-types.js";
|
|
4
|
-
|
|
5
|
-
export function createAccordion(options: AccordionOptions = {}): Accordion {
|
|
6
|
-
const { single = false, initial = [] } = options;
|
|
7
|
-
const hasInitial = Object.prototype.hasOwnProperty.call(options, "initial");
|
|
8
|
-
const seededInitial = single ? initial.slice(0, 1) : initial;
|
|
9
|
-
const openItems = signal(new Set<string>(seededInitial));
|
|
10
|
-
|
|
11
|
-
const toggle = (itemId: string) => {
|
|
12
|
-
openItems.update((current) => {
|
|
13
|
-
const next = new Set(current);
|
|
14
|
-
if (next.has(itemId)) {
|
|
15
|
-
next.delete(itemId);
|
|
16
|
-
} else {
|
|
17
|
-
if (single) next.clear();
|
|
18
|
-
next.add(itemId);
|
|
19
|
-
}
|
|
20
|
-
return next;
|
|
21
|
-
});
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const open = (itemId: string) => {
|
|
25
|
-
openItems.update((current) => {
|
|
26
|
-
const next = new Set(current);
|
|
27
|
-
if (single) next.clear();
|
|
28
|
-
next.add(itemId);
|
|
29
|
-
return next;
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const close = (itemId: string) => {
|
|
34
|
-
openItems.update((current) => {
|
|
35
|
-
const next = new Set(current);
|
|
36
|
-
next.delete(itemId);
|
|
37
|
-
return next;
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const _isOpenCache = new Map<string, Signal<boolean>>();
|
|
42
|
-
|
|
43
|
-
const isOpen = (itemId: string): Signal<boolean> => {
|
|
44
|
-
let sig = _isOpenCache.get(itemId);
|
|
45
|
-
if (!sig) {
|
|
46
|
-
sig = computed(() => openItems().has(itemId));
|
|
47
|
-
_isOpenCache.set(itemId, sig);
|
|
48
|
-
}
|
|
49
|
-
return sig;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const _attachTo = (el: HTMLElement) => {
|
|
53
|
-
const scope = getCurrentScope();
|
|
54
|
-
let syncing = false;
|
|
55
|
-
|
|
56
|
-
const allDetails = () =>
|
|
57
|
-
Array.from(el.querySelectorAll<HTMLDetailsElement>("details[data-accordion]"));
|
|
58
|
-
|
|
59
|
-
const syncDOMFromSignal = (set: Set<string>) => {
|
|
60
|
-
syncing = true;
|
|
61
|
-
for (const details of allDetails()) {
|
|
62
|
-
const itemId = details.dataset.accordion;
|
|
63
|
-
if (!itemId) continue;
|
|
64
|
-
details.open = set.has(itemId);
|
|
65
|
-
}
|
|
66
|
-
syncing = false;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const syncSignalFromDOM = () => {
|
|
70
|
-
const next = new Set<string>();
|
|
71
|
-
for (const details of allDetails()) {
|
|
72
|
-
if (!details.open) continue;
|
|
73
|
-
const itemId = details.dataset.accordion;
|
|
74
|
-
if (!itemId) continue;
|
|
75
|
-
if (single) {
|
|
76
|
-
next.clear();
|
|
77
|
-
}
|
|
78
|
-
next.add(itemId);
|
|
79
|
-
}
|
|
80
|
-
openItems.set(next);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const onToggle = (ev: Event) => {
|
|
84
|
-
const details = ev.target as HTMLDetailsElement;
|
|
85
|
-
if (syncing) return;
|
|
86
|
-
|
|
87
|
-
const itemId = details.dataset.accordion;
|
|
88
|
-
if (!itemId) return;
|
|
89
|
-
|
|
90
|
-
openItems.update((current) => {
|
|
91
|
-
const next = new Set(current);
|
|
92
|
-
if (details.open) {
|
|
93
|
-
if (single) next.clear();
|
|
94
|
-
next.add(itemId);
|
|
95
|
-
} else {
|
|
96
|
-
next.delete(itemId);
|
|
97
|
-
}
|
|
98
|
-
return next;
|
|
99
|
-
});
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const unsub = openItems.on((set) => {
|
|
103
|
-
if (!syncing) syncDOMFromSignal(set);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// If initial was not provided, respect current DOM open state.
|
|
107
|
-
if (!hasInitial) syncSignalFromDOM();
|
|
108
|
-
else syncDOMFromSignal(openItems());
|
|
109
|
-
|
|
110
|
-
el.addEventListener("toggle", onToggle, true);
|
|
111
|
-
|
|
112
|
-
if (scope) {
|
|
113
|
-
scope.onCleanup(() => {
|
|
114
|
-
unsub();
|
|
115
|
-
el.removeEventListener("toggle", onToggle, true);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
return { openItems, toggle, open, close, isOpen, _attachTo };
|
|
121
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { signal, type Signal } from "../../../core/signal.js";
|
|
2
|
-
import { getCurrentScope } from "../../../core/scope.js";
|
|
3
|
-
import type { Dialog, DialogOptions } from "../ui-types.js";
|
|
4
|
-
import { validateDialogOptions } from "../validate.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Shared dialog behavior — used by both createDialog and createDrawer.
|
|
8
|
-
*/
|
|
9
|
-
export function _attachDialogBehavior(
|
|
10
|
-
el: HTMLDialogElement,
|
|
11
|
-
open: Signal<boolean>,
|
|
12
|
-
closeFn: () => void,
|
|
13
|
-
opts: { closeOnBackdrop: boolean; closeOnEscape: boolean }
|
|
14
|
-
): void {
|
|
15
|
-
const scope = getCurrentScope();
|
|
16
|
-
|
|
17
|
-
// Sync signal → native dialog
|
|
18
|
-
const unsub = open.on((isOpen) => {
|
|
19
|
-
if (isOpen && !el.open) el.showModal();
|
|
20
|
-
else if (!isOpen && el.open) el.close();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Native close event → sync signal
|
|
24
|
-
const onClose = () => open.set(false);
|
|
25
|
-
el.addEventListener("close", onClose);
|
|
26
|
-
|
|
27
|
-
// Backdrop click
|
|
28
|
-
const onBackdropClick = (e: MouseEvent) => {
|
|
29
|
-
if (opts.closeOnBackdrop && e.target === el) closeFn();
|
|
30
|
-
};
|
|
31
|
-
el.addEventListener("click", onBackdropClick);
|
|
32
|
-
|
|
33
|
-
// Escape key
|
|
34
|
-
if (!opts.closeOnEscape) {
|
|
35
|
-
const onCancel = (e: Event) => e.preventDefault();
|
|
36
|
-
el.addEventListener("cancel", onCancel);
|
|
37
|
-
if (scope) {
|
|
38
|
-
scope.onCleanup(() => el.removeEventListener("cancel", onCancel));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ARIA
|
|
43
|
-
el.setAttribute("aria-modal", "true");
|
|
44
|
-
|
|
45
|
-
if (scope) {
|
|
46
|
-
scope.onCleanup(() => {
|
|
47
|
-
unsub();
|
|
48
|
-
el.removeEventListener("close", onClose);
|
|
49
|
-
el.removeEventListener("click", onBackdropClick);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createDialog(options: DialogOptions = {}): Dialog {
|
|
55
|
-
validateDialogOptions(options as Record<string, unknown>);
|
|
56
|
-
const { closeOnBackdrop = true, closeOnEscape = true } = options;
|
|
57
|
-
const open = signal(false);
|
|
58
|
-
|
|
59
|
-
const show = () => open.set(true);
|
|
60
|
-
const close = () => open.set(false);
|
|
61
|
-
const toggle = () => open.update((v) => !v);
|
|
62
|
-
|
|
63
|
-
const _attachTo = (el: HTMLDialogElement) => {
|
|
64
|
-
_attachDialogBehavior(el, open, close, { closeOnBackdrop, closeOnEscape });
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return { open, show, close, toggle, _attachTo };
|
|
68
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { signal } from "../../../core/signal.js";
|
|
2
|
-
import { getCurrentScope } from "../../../core/scope.js";
|
|
3
|
-
import { _attachDialogBehavior } from "../dialog/index.js";
|
|
4
|
-
import type { Drawer, DrawerOptions, DrawerSide } from "../ui-types.js";
|
|
5
|
-
import { validateDrawerOptions } from "../validate.js";
|
|
6
|
-
|
|
7
|
-
const SIDE_CLASSES: Record<DrawerSide, string> = {
|
|
8
|
-
right: "",
|
|
9
|
-
left: "d-drawer-left",
|
|
10
|
-
bottom: "d-sheet",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function createDrawer(options: DrawerOptions = {}): Drawer {
|
|
14
|
-
validateDrawerOptions(options as Record<string, unknown>);
|
|
15
|
-
const {
|
|
16
|
-
closeOnBackdrop = true,
|
|
17
|
-
closeOnEscape = true,
|
|
18
|
-
side: initialSide = "right",
|
|
19
|
-
} = options;
|
|
20
|
-
|
|
21
|
-
const open = signal(false);
|
|
22
|
-
const side = signal<DrawerSide>(initialSide);
|
|
23
|
-
|
|
24
|
-
const show = () => open.set(true);
|
|
25
|
-
const close = () => open.set(false);
|
|
26
|
-
const toggle = () => open.update((v) => !v);
|
|
27
|
-
|
|
28
|
-
const _attachTo = (el: HTMLDialogElement) => {
|
|
29
|
-
const scope = getCurrentScope();
|
|
30
|
-
|
|
31
|
-
// Shared dialog behavior (open sync, backdrop, escape, ARIA)
|
|
32
|
-
_attachDialogBehavior(el, open, close, { closeOnBackdrop, closeOnEscape });
|
|
33
|
-
|
|
34
|
-
// Apply initial side class
|
|
35
|
-
const initial = SIDE_CLASSES[side()];
|
|
36
|
-
if (initial) el.classList.add(initial);
|
|
37
|
-
|
|
38
|
-
// React to side changes
|
|
39
|
-
const unsub = side.on((s) => {
|
|
40
|
-
for (const cls of Object.values(SIDE_CLASSES)) {
|
|
41
|
-
if (cls) el.classList.remove(cls);
|
|
42
|
-
}
|
|
43
|
-
const cls = SIDE_CLASSES[s];
|
|
44
|
-
if (cls) el.classList.add(cls);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (scope) {
|
|
48
|
-
scope.onCleanup(() => unsub());
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return { open, side, show, close, toggle, _attachTo };
|
|
53
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { signal } from "../../../core/signal.js";
|
|
2
|
-
import { getCurrentScope } from "../../../core/scope.js";
|
|
3
|
-
import type { Dropdown, DropdownOptions } from "../ui-types.js";
|
|
4
|
-
import { isBrowser } from "../env.js";
|
|
5
|
-
|
|
6
|
-
export function createDropdown(options: DropdownOptions = {}): Dropdown {
|
|
7
|
-
const { closeOnSelect = true } = options;
|
|
8
|
-
const open = signal(false);
|
|
9
|
-
|
|
10
|
-
const toggle = (ev?: Event) => {
|
|
11
|
-
if (ev) ev.stopPropagation();
|
|
12
|
-
open.update((v) => !v);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const close = () => open.set(false);
|
|
16
|
-
|
|
17
|
-
const select = (ev?: Event) => {
|
|
18
|
-
if (ev) ev.stopPropagation();
|
|
19
|
-
if (closeOnSelect) close();
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const _attachTo = (el: HTMLElement) => {
|
|
23
|
-
const scope = getCurrentScope();
|
|
24
|
-
|
|
25
|
-
// ARIA
|
|
26
|
-
const trigger = el.querySelector<HTMLElement>(
|
|
27
|
-
"button, [role='button'], [data-d-tag='d-button']"
|
|
28
|
-
);
|
|
29
|
-
const menu = el.querySelector<HTMLElement>(
|
|
30
|
-
"[data-d-tag='d-menu'], .d-menu"
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
if (menu) menu.setAttribute("role", "menu");
|
|
34
|
-
if (trigger) {
|
|
35
|
-
trigger.setAttribute("aria-haspopup", "true");
|
|
36
|
-
trigger.setAttribute("aria-expanded", "false");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const unsubAria = open.on((isOpen) => {
|
|
40
|
-
trigger?.setAttribute("aria-expanded", String(isOpen));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const onDocClick = (e: MouseEvent) => {
|
|
44
|
-
if (!el.contains(e.target as Node)) close();
|
|
45
|
-
};
|
|
46
|
-
if (isBrowser) document.addEventListener("click", onDocClick);
|
|
47
|
-
|
|
48
|
-
if (scope) {
|
|
49
|
-
scope.onCleanup(() => {
|
|
50
|
-
unsubAria();
|
|
51
|
-
if (isBrowser) document.removeEventListener("click", onDocClick);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
return { open, toggle, close, select, _attachTo };
|
|
57
|
-
}
|