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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dalila",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Create Dalila apps with one command",
5
5
  "bin": {
6
6
  "create-dalila": "index.js"
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Dalila UI Components
3
3
  *
4
- * To use Dalila UI components, import the necessary styles:
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
- * @import './components/ui/button/button.css';
7
- * @import './components/ui/card/card.css';
8
- * @import './components/ui/dialog/dialog.css';
9
+ * Or import all at once:
10
+ * @import './components/ui/dalila/dalila.css';
9
11
  *
10
- * Or import all styles at once:
11
- * @import './components/ui/dalila/dalila.css';
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
- }