create-dalila 1.2.0 → 1.2.1

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.
@@ -1,514 +0,0 @@
1
- import { computed, signal, type Signal } from "../../core/signal.js";
2
- import { createScope, withScope } from "../../core/scope.js";
3
- import { bind, type BindContext, type BindOptions } from "../../runtime/bind.js";
4
- import { tabBindings } from "./tabs/index.js";
5
- import { isBrowser } from "./env.js";
6
- import type {
7
- Calendar,
8
- Combobox,
9
- Dialog,
10
- Drawer,
11
- Dropdown,
12
- Dropzone,
13
- Popover,
14
- PopoverMount,
15
- Tabs,
16
- TabsMount,
17
- Toast,
18
- ToastVariant,
19
- } from "./ui-types.js";
20
-
21
- // ── MountUI Options ─────────────────────────────────────────────────
22
-
23
- export interface MountUIOptions {
24
- context?: BindContext;
25
- events?: string[];
26
- theme?: boolean;
27
- sliderValue?: Signal<string>;
28
-
29
- dialogs?: Record<string, Dialog>;
30
- drawers?: Record<string, Drawer>;
31
- dropdowns?: Record<string, Dropdown>;
32
- combos?: Record<string, Combobox>;
33
- tabs?: Record<string, TabsMount>;
34
- toasts?: Record<string, Toast>;
35
- popovers?: Record<string, PopoverMount>;
36
- dropzones?: Record<string, Dropzone>;
37
- calendars?: Record<string, Calendar>;
38
- accordions?: Record<string, import("./ui-types.js").Accordion>;
39
- }
40
-
41
- // ── Constants ───────────────────────────────────────────────────────
42
-
43
- const DEFAULT_EVENTS = [
44
- "click", "input", "change", "submit",
45
- "keydown", "keyup", "focus",
46
- "dragover", "dragleave", "drop",
47
- ];
48
-
49
- const DEFAULT_TOAST_VARIANTS: ToastVariant[] = ["success", "error", "warning", "info"];
50
-
51
- // ── Tag Aliases ─────────────────────────────────────────────────────
52
-
53
- const TAG_ALIASES: Record<string, string> = {
54
- "d-h1": "h1", "d-h2": "h2", "d-h3": "h3", "d-h4": "h4", "d-h5": "h5", "d-h6": "h6",
55
- "d-text": "p", "d-link": "a", "d-code-inline": "span", "d-kbd": "span",
56
- "d-field": "label", "d-field-label": "span", "d-field-hint": "span", "d-field-error": "span",
57
- "d-input": "input", "d-button-group": "div", "d-select": "select", "d-textarea": "textarea",
58
- "d-checkbox": "label", "d-radio-group": "div", "d-radio": "label",
59
- "d-slider": "input", "d-toggle": "label", "d-toggle-track": "span",
60
- "d-form": "form", "d-form-row": "div", "d-form-section-title": "h3", "d-form-actions": "div",
61
- "d-card": "div", "d-card-section": "div", "d-card-header": "div", "d-card-title": "h3",
62
- "d-card-description": "p", "d-card-body": "div", "d-card-footer": "div",
63
- "d-badge": "span", "d-chip": "span", "d-chip-remove": "button",
64
- "d-avatar": "div", "d-avatar-group": "div",
65
- "d-alert": "div", "d-alert-title": "strong", "d-alert-text": "p",
66
- "d-tooltip-trigger": "button",
67
- "d-accordion": "div", "d-accordion-item": "details", "d-accordion-body": "div",
68
- "d-collapsible": "details", "d-collapsible-body": "div",
69
- "d-table-wrapper": "div", "d-table": "table",
70
- "d-pagination": "nav", "d-page": "button", "d-page-ellipsis": "span",
71
- "d-breadcrumb": "ol", "d-breadcrumb-item": "li",
72
- "d-separator": "hr", "d-separator-label": "div",
73
- "d-skeleton": "div", "d-loading": "div", "d-spinner": "span",
74
- "d-empty": "div", "d-empty-icon": "div", "d-empty-title": "h3", "d-empty-text": "p",
75
- "d-stack": "div", "d-flex": "div", "d-grid": "div",
76
- "d-dialog": "dialog", "d-dialog-header": "div", "d-dialog-title": "h3",
77
- "d-dialog-body": "div", "d-dialog-footer": "div", "d-dialog-close": "button",
78
- "d-drawer": "dialog", "d-drawer-header": "div", "d-drawer-title": "h3",
79
- "d-drawer-body": "div", "d-drawer-footer": "div",
80
- "d-sheet": "dialog", "d-button": "button",
81
- "d-toast-container": "div", "d-toast": "div", "d-toast-icon": "div",
82
- "d-toast-body": "div", "d-toast-title": "div", "d-toast-text": "p", "d-toast-close": "button",
83
- "d-popover-trigger": "button",
84
- "d-dropdown": "div", "d-menu": "div", "d-menu-label": "div",
85
- "d-menu-item": "button", "d-menu-separator": "div",
86
- "d-combobox": "div", "d-combobox-input": "input", "d-combobox-trigger": "button",
87
- "d-combobox-list": "ul", "d-combobox-option": "li",
88
- "d-tabs": "div", "d-tab-list": "div", "d-tab": "button", "d-tab-panel": "div",
89
- "d-calendar": "div", "d-calendar-header": "div", "d-calendar-nav": "button",
90
- "d-calendar-title": "span", "d-calendar-grid": "div",
91
- "d-calendar-dow": "span", "d-calendar-day": "button",
92
- "d-dropzone": "div", "d-dropzone-icon": "div", "d-dropzone-text": "p", "d-dropzone-hint": "p",
93
- "d-popover": "div", "d-popover-title": "p", "d-popover-text": "p",
94
- };
95
-
96
- const TAG_DEFAULT_CLASS: Record<string, string> = {
97
- "d-h1": "d-h1", "d-h2": "d-h2", "d-h3": "d-h3", "d-h4": "d-h4", "d-h5": "d-h5", "d-h6": "d-h6",
98
- "d-text": "d-text", "d-link": "d-link", "d-code-inline": "d-code", "d-kbd": "d-kbd",
99
- "d-field": "d-field", "d-field-label": "d-field-label", "d-field-hint": "d-field-hint", "d-field-error": "d-field-error",
100
- "d-input": "d-input", "d-button-group": "d-btn-group", "d-select": "d-select", "d-textarea": "d-textarea",
101
- "d-checkbox": "d-checkbox", "d-radio-group": "d-radio-group", "d-radio": "d-radio",
102
- "d-slider": "d-slider", "d-toggle": "d-toggle", "d-toggle-track": "d-toggle-track",
103
- "d-form": "d-form", "d-form-row": "d-form-row", "d-form-section-title": "d-form-section-title", "d-form-actions": "d-form-actions",
104
- "d-card": "d-card", "d-card-section": "d-card-section", "d-card-header": "d-card-header", "d-card-title": "d-card-title",
105
- "d-card-description": "d-card-description", "d-card-body": "d-card-body", "d-card-footer": "d-card-footer",
106
- "d-badge": "d-badge", "d-chip": "d-chip", "d-chip-remove": "d-chip-remove",
107
- "d-avatar": "d-avatar", "d-avatar-group": "d-avatar-group",
108
- "d-alert": "d-alert", "d-alert-title": "d-alert-title", "d-alert-text": "d-alert-text",
109
- "d-tooltip-trigger": "d-btn d-tooltip",
110
- "d-accordion": "d-accordion", "d-accordion-item": "d-accordion-item", "d-accordion-body": "d-accordion-body",
111
- "d-collapsible": "d-collapsible", "d-collapsible-body": "d-collapsible-body",
112
- "d-table-wrapper": "d-table-wrapper", "d-table": "d-table",
113
- "d-pagination": "d-pagination", "d-page": "d-page", "d-page-ellipsis": "d-page-ellipsis",
114
- "d-breadcrumb": "d-breadcrumb", "d-breadcrumb-item": "d-breadcrumb-item",
115
- "d-separator": "d-separator", "d-separator-label": "d-separator-label",
116
- "d-skeleton": "d-skeleton", "d-loading": "d-loading", "d-spinner": "d-spinner",
117
- "d-empty": "d-empty", "d-empty-icon": "d-empty-icon", "d-empty-title": "d-empty-title", "d-empty-text": "d-empty-text",
118
- "d-stack": "d-stack", "d-flex": "d-flex", "d-grid": "d-grid",
119
- "d-button": "d-btn",
120
- "d-toast-container": "d-toast-container", "d-toast": "d-toast", "d-toast-icon": "d-toast-icon",
121
- "d-toast-body": "d-toast-body", "d-toast-title": "d-toast-title", "d-toast-text": "d-toast-text", "d-toast-close": "d-toast-close",
122
- "d-popover-trigger": "d-btn d-btn-primary",
123
- "d-dialog": "d-dialog", "d-dialog-header": "d-dialog-header", "d-dialog-title": "d-dialog-title",
124
- "d-dialog-body": "d-dialog-body", "d-dialog-footer": "d-dialog-footer", "d-dialog-close": "d-dialog-close",
125
- "d-drawer": "d-drawer", "d-sheet": "d-drawer d-sheet",
126
- "d-drawer-header": "d-drawer-header", "d-drawer-title": "d-drawer-title",
127
- "d-drawer-body": "d-drawer-body", "d-drawer-footer": "d-drawer-footer",
128
- "d-dropdown": "d-dropdown", "d-menu": "d-menu", "d-menu-label": "d-menu-label",
129
- "d-menu-item": "d-menu-item", "d-menu-separator": "d-menu-separator",
130
- "d-combobox": "d-combobox", "d-combobox-input": "d-input d-combobox-input",
131
- "d-combobox-trigger": "d-combobox-trigger", "d-combobox-list": "d-combobox-list", "d-combobox-option": "d-combobox-option",
132
- "d-tabs": "d-tabs", "d-tab-list": "d-tab-list", "d-tab": "d-tab", "d-tab-panel": "d-tab-panel",
133
- "d-calendar": "d-calendar", "d-calendar-header": "d-calendar-header", "d-calendar-nav": "d-calendar-nav",
134
- "d-calendar-title": "d-calendar-title", "d-calendar-grid": "d-calendar-grid", "d-calendar-dow": "d-calendar-dow", "d-calendar-day": "d-calendar-day",
135
- "d-dropzone": "d-dropzone", "d-dropzone-icon": "d-dropzone-icon", "d-dropzone-text": "d-dropzone-text", "d-dropzone-hint": "d-dropzone-hint",
136
- "d-popover": "d-popover", "d-popover-title": "d-popover-title", "d-popover-text": "d-popover-text",
137
- };
138
-
139
- // ── Data-driven tag upgrade rules ───────────────────────────────────
140
-
141
- interface TagUpgradeRule {
142
- /** attr → class template ("d-btn-{}" produces "d-btn-primary" for variant="primary") */
143
- attrs?: Record<string, string>;
144
- /** boolean attrs → class (presence adds the class) */
145
- flags?: Record<string, string>;
146
- /** default type attribute when missing ("button" or "text") */
147
- defaultType?: string;
148
- }
149
-
150
- const TAG_UPGRADE_RULES: Record<string, TagUpgradeRule> = {
151
- "d-button": { attrs: { variant: "d-btn-{}", size: "d-btn-{}" }, flags: { icon: "d-btn-icon" }, defaultType: "button" },
152
- "d-text": { attrs: { tone: "d-text-{}", size: "d-text-{}" } },
153
- "d-link": { attrs: { tone: "d-link-{}" } },
154
- "d-radio-group": { attrs: { variant: "d-radio-group-{}" } },
155
- "d-toggle": { attrs: { size: "d-toggle-{}" } },
156
- "d-form-actions": { attrs: { align: "d-form-actions-{}" } },
157
- "d-card": { attrs: { variant: "d-card-{}" } },
158
- "d-badge": { attrs: { variant: "d-badge-{}" }, flags: { dot: "d-badge-dot" } },
159
- "d-chip": { attrs: { variant: "d-chip-{}" } },
160
- "d-avatar": { attrs: { size: "d-avatar-{}", shape: "d-avatar-{}" } },
161
- "d-alert": { attrs: { variant: "d-alert-{}" } },
162
- "d-tooltip-trigger": { attrs: { place: "d-tooltip-{}" }, defaultType: "button" },
163
- "d-skeleton": { attrs: { kind: "d-skeleton-{}" } },
164
- "d-spinner": { attrs: { size: "d-spinner-{}" } },
165
- "d-stack": { attrs: { gap: "d-stack-{}" } },
166
- "d-flex": { flags: { wrap: "d-flex-wrap" } },
167
- "d-grid": { attrs: { cols: "d-grid-{}" } },
168
- "d-tab-list": { attrs: { variant: "d-tab-list-{}" } },
169
- "d-menu-item": { attrs: { variant: "d-menu-item-{}" }, defaultType: "button" },
170
- "d-chip-remove": { defaultType: "button" },
171
- "d-page": { defaultType: "button" },
172
- "d-toast-close": { defaultType: "button" },
173
- "d-popover-trigger": { defaultType: "button" },
174
- "d-dialog-close": { defaultType: "button" },
175
- "d-combobox-trigger":{ defaultType: "button" },
176
- "d-tab": { defaultType: "button" },
177
- "d-calendar-nav": { defaultType: "button" },
178
- "d-calendar-day": { defaultType: "button" },
179
- "d-combobox-input": { defaultType: "text" },
180
- };
181
-
182
- // ── Tag upgrade engine ──────────────────────────────────────────────
183
-
184
- function upgradeDalilaTags(root: Element): Element {
185
- if (!isBrowser) return root;
186
- let currentRoot = root;
187
-
188
- for (const [sourceTag, targetTag] of Object.entries(TAG_ALIASES)) {
189
- const nodes: HTMLElement[] = [];
190
- if (currentRoot.matches(sourceTag)) nodes.push(currentRoot as HTMLElement);
191
- nodes.push(...Array.from(currentRoot.querySelectorAll<HTMLElement>(sourceTag)));
192
-
193
- for (const node of nodes) {
194
- const replacement = currentRoot.ownerDocument.createElement(targetTag);
195
- for (const attr of Array.from(node.attributes)) {
196
- replacement.setAttribute(attr.name, attr.value);
197
- }
198
- replacement.setAttribute("data-d-tag", sourceTag);
199
-
200
- const defaultClass = TAG_DEFAULT_CLASS[sourceTag];
201
- if (defaultClass) {
202
- const current = replacement.getAttribute("class");
203
- replacement.setAttribute("class", current ? `${defaultClass} ${current}` : defaultClass);
204
- }
205
-
206
- const rule = TAG_UPGRADE_RULES[sourceTag];
207
- if (rule) {
208
- // Default type attribute
209
- if (rule.defaultType && !replacement.hasAttribute("type")) {
210
- replacement.setAttribute("type", rule.defaultType);
211
- }
212
-
213
- // Attribute → class mappings
214
- if (rule.attrs) {
215
- for (const [attr, template] of Object.entries(rule.attrs)) {
216
- const val = replacement.getAttribute(attr);
217
- if (val) {
218
- replacement.classList.add(template.replace("{}", val));
219
- replacement.removeAttribute(attr);
220
- }
221
- }
222
- }
223
-
224
- // Boolean flag → class mappings
225
- if (rule.flags) {
226
- for (const [attr, className] of Object.entries(rule.flags)) {
227
- if (replacement.hasAttribute(attr)) {
228
- replacement.classList.add(className);
229
- replacement.removeAttribute(attr);
230
- }
231
- }
232
- }
233
- }
234
-
235
- while (node.firstChild) replacement.appendChild(node.firstChild);
236
- node.replaceWith(replacement);
237
- if (node === currentRoot) {
238
- currentRoot = replacement;
239
- }
240
- }
241
- }
242
- return currentRoot;
243
- }
244
-
245
- // ── Element discovery ───────────────────────────────────────────────
246
-
247
- function findByUI(root: Element, name: string, fallbackTag?: string): HTMLElement | null {
248
- // 0. Root itself
249
- if (root instanceof HTMLElement) {
250
- if (root.getAttribute("d-ui") === name) return root;
251
- if (fallbackTag && root.getAttribute("data-d-tag") === fallbackTag) return root;
252
- }
253
-
254
- // 1. d-ui attribute
255
- let el = root.querySelector<HTMLElement>(`[d-ui="${name}"]`);
256
- if (el) return el;
257
-
258
- // 2. ID match
259
- el = root.ownerDocument.getElementById(name) as HTMLElement | null;
260
- if (el && root.contains(el)) return el;
261
-
262
- // 3. Fallback: first matching data-d-tag
263
- if (fallbackTag) {
264
- el = root.querySelector<HTMLElement>(`[data-d-tag="${fallbackTag}"]`);
265
- if (el) return el;
266
- }
267
-
268
- return null;
269
- }
270
-
271
- function findPopoverTrigger(root: Element, id?: string): HTMLElement | null {
272
- if (id) return root.ownerDocument.getElementById(id) as HTMLElement | null;
273
- return (
274
- root.querySelector<HTMLElement>(`[d-ui="popover-trigger"]`) ??
275
- root.querySelector<HTMLElement>(`[data-d-tag="d-popover-trigger"]`) ??
276
- null
277
- );
278
- }
279
-
280
- function findPopoverPanel(root: Element, id?: string): HTMLElement | null {
281
- if (id) return root.ownerDocument.getElementById(id) as HTMLElement | null;
282
- return (
283
- root.querySelector<HTMLElement>(`[d-ui="popover"]`) ??
284
- root.querySelector<HTMLElement>(`[data-d-tag="d-popover"]`) ??
285
- root.querySelector<HTMLElement>(`[popover]`) ??
286
- null
287
- );
288
- }
289
-
290
- // ── Context binding generators ──────────────────────────────────────
291
-
292
- function capitalize(s: string): string {
293
- return s.charAt(0).toUpperCase() + s.slice(1);
294
- }
295
-
296
- function addDialogBindings(ctx: BindContext, key: string, dialog: Dialog): void {
297
- ctx[`${key}Show`] = dialog.show;
298
- ctx[`${key}Close`] = dialog.close;
299
- ctx[`${key}Open`] = dialog.open;
300
- }
301
-
302
- function addDropdownBindings(ctx: BindContext, key: string, dd: Dropdown): void {
303
- ctx[`${key}Class`] = computed(() => dd.open() ? "d-dropdown open" : "d-dropdown");
304
- ctx[`${key}Toggle`] = dd.toggle;
305
- ctx[`${key}Select`] = dd.select;
306
- }
307
-
308
- function addComboBindings(ctx: BindContext, key: string, combo: Combobox): void {
309
- ctx[`${key}Class`] = computed(() => combo.open() ? "d-combobox open" : "d-combobox");
310
- ctx[`${key}Items`] = computed(() => {
311
- const items = combo.filtered();
312
- const hi = combo.highlightedIndex();
313
- const sel = combo.value();
314
- return items.map((opt, i) => ({
315
- value: opt.value,
316
- label: opt.label,
317
- optionClass:
318
- "d-combobox-option" + (opt.value === sel ? " selected" : "") + (i === hi ? " highlighted" : ""),
319
- }));
320
- });
321
- ctx[`${key}Label`] = combo.label;
322
- ctx[`${key}Input`] = combo.handleInput;
323
- ctx[`${key}Show`] = combo.show;
324
- ctx[`${key}Trigger`] = (ev: Event) => {
325
- ev.stopPropagation();
326
- combo.toggle();
327
- };
328
- ctx[`${key}Select`] = combo.handleSelect;
329
- ctx[`${key}Keydown`] = combo.handleKeydown;
330
- }
331
-
332
- function addTabsBindings(ctx: BindContext, key: string, mount: TabsMount): void {
333
- ctx[`${key}Click`] = mount.api.handleClick;
334
- for (const [bindKey, tabId] of mount.bindings) {
335
- const b = tabBindings(mount.api, tabId);
336
- ctx[`${bindKey}Class`] = b.tabClass;
337
- ctx[`${bindKey}Visible`] = b.visible;
338
- }
339
- }
340
-
341
- function addToastBindings(ctx: BindContext, key: string, toast: Toast): void {
342
- let cycle = 0;
343
- ctx[`${key}ContainerClass`] = toast.containerClass;
344
- ctx[`${key}Items`] = toast.items;
345
- ctx[`show${capitalize(key)}`] = () => {
346
- const variant = DEFAULT_TOAST_VARIANTS[cycle++ % DEFAULT_TOAST_VARIANTS.length];
347
- toast.show(variant, variant.charAt(0).toUpperCase() + variant.slice(1), `This is a ${variant} toast notification.`);
348
- };
349
- ctx[`dismiss${capitalize(key)}`] = (ev: Event) => {
350
- const target = ev.currentTarget as HTMLElement;
351
- const id = target.dataset.id;
352
- if (id) toast.dismiss(id);
353
- };
354
- }
355
-
356
- function addCalendarBindings(ctx: BindContext, key: string, cal: Calendar): void {
357
- ctx[`${key}Title`] = cal.title;
358
- ctx[`${key}Days`] = computed(() =>
359
- cal.days().map((day) => ({
360
- date: day.date,
361
- dayClass: ["d-calendar-day", day.month !== "current" ? "other-month" : "", day.isToday ? "today" : "", day.isSelected ? "selected" : ""]
362
- .filter(Boolean)
363
- .join(" "),
364
- dateStr: day.fullDate.toISOString(),
365
- isDisabled: day.disabled || day.month !== "current",
366
- }))
367
- );
368
- ctx[`${key}Prev`] = cal.prev;
369
- ctx[`${key}Next`] = cal.next;
370
- ctx[`${key}DayClick`] = cal.handleDayClick;
371
- }
372
-
373
- function addDropzoneBindings(ctx: BindContext, key: string, dz: Dropzone): void {
374
- ctx[`${key}Class`] = computed(() => dz.dragging() ? "d-dropzone dragover" : "d-dropzone");
375
- ctx[`${key}Click`] = dz.handleClick;
376
- ctx[`${key}Dragover`] = dz.handleDragover;
377
- ctx[`${key}Dragleave`] = dz.handleDragleave;
378
- ctx[`${key}Drop`] = dz.handleDrop;
379
- }
380
-
381
- // ── mountUI ─────────────────────────────────────────────────────────
382
-
383
- export function mountUI(root: Element, options: MountUIOptions): () => void {
384
- if (!isBrowser) return () => {};
385
- const mountedRoot = upgradeDalilaTags(root);
386
-
387
- const sliderValue = options.sliderValue ?? signal("50");
388
- const ctx: BindContext = { ...(options.context ?? {}) };
389
- const cleanups: Array<() => void> = [];
390
- const scope = createScope();
391
-
392
- // Theme toggle
393
- if (options.theme !== false) {
394
- ctx.onThemeToggle = (ev: Event) => {
395
- const target = ev.target as HTMLInputElement;
396
- mountedRoot.ownerDocument.documentElement.setAttribute("data-theme", target.checked ? "dark" : "");
397
- };
398
- }
399
-
400
- // Slider
401
- ctx.sliderValue = sliderValue;
402
- ctx.onSliderInput = (ev: Event) => {
403
- const target = ev.target as HTMLInputElement;
404
- sliderValue.set(target.value);
405
- };
406
-
407
- // ── Phase 1: Generate context bindings ──
408
-
409
- for (const [key, dialog] of Object.entries(options.dialogs ?? {})) {
410
- addDialogBindings(ctx, key, dialog);
411
- }
412
-
413
- for (const [key, drawer] of Object.entries(options.drawers ?? {})) {
414
- addDialogBindings(ctx, key, drawer);
415
- }
416
-
417
- for (const [key, dd] of Object.entries(options.dropdowns ?? {})) {
418
- addDropdownBindings(ctx, key, dd);
419
- }
420
-
421
- for (const [key, combo] of Object.entries(options.combos ?? {})) {
422
- addComboBindings(ctx, key, combo);
423
- }
424
-
425
- for (const [key, mount] of Object.entries(options.tabs ?? {})) {
426
- addTabsBindings(ctx, key, mount);
427
- }
428
-
429
- for (const [key, toast] of Object.entries(options.toasts ?? {})) {
430
- addToastBindings(ctx, key, toast);
431
- }
432
-
433
- for (const [key, cal] of Object.entries(options.calendars ?? {})) {
434
- addCalendarBindings(ctx, key, cal);
435
- }
436
-
437
- for (const [key, dz] of Object.entries(options.dropzones ?? {})) {
438
- addDropzoneBindings(ctx, key, dz);
439
- }
440
-
441
- // ── Phase 2: Bind + attach to DOM ──
442
-
443
- withScope(scope, () => {
444
- cleanups.push(
445
- bind(mountedRoot, ctx, {
446
- events: options.events ?? DEFAULT_EVENTS,
447
- })
448
- );
449
-
450
- // Attach dialogs
451
- for (const [key, dialog] of Object.entries(options.dialogs ?? {})) {
452
- const el = findByUI(mountedRoot, key, "d-dialog");
453
- if (el) dialog._attachTo(el as HTMLDialogElement);
454
- }
455
-
456
- // Attach drawers
457
- for (const [key, drawer] of Object.entries(options.drawers ?? {})) {
458
- const el = findByUI(mountedRoot, key, "d-drawer");
459
- if (el) drawer._attachTo(el as HTMLDialogElement);
460
- }
461
-
462
- // Attach dropdowns
463
- for (const [key, dd] of Object.entries(options.dropdowns ?? {})) {
464
- const el = findByUI(mountedRoot, key, "d-dropdown");
465
- if (el) dd._attachTo(el);
466
- }
467
-
468
- // Attach combos
469
- for (const [key, combo] of Object.entries(options.combos ?? {})) {
470
- const el = findByUI(mountedRoot, key, "d-combobox");
471
- if (el) combo._attachTo(el);
472
- }
473
-
474
- // Attach tabs
475
- for (const [key, mount] of Object.entries(options.tabs ?? {})) {
476
- const el = findByUI(mountedRoot, key, "d-tabs");
477
- if (el) mount.api._attachTo(el);
478
- }
479
-
480
- // Attach dropzones
481
- for (const [key, dz] of Object.entries(options.dropzones ?? {})) {
482
- const el = findByUI(mountedRoot, key, "d-dropzone");
483
- if (el) dz._attachTo(el);
484
- }
485
-
486
- // Attach accordions
487
- for (const [key, acc] of Object.entries(options.accordions ?? {})) {
488
- const el = findByUI(mountedRoot, key, "d-accordion");
489
- if (el) acc._attachTo(el);
490
- }
491
- });
492
-
493
- // Attach popovers (outside scope — manages its own listeners)
494
- for (const [key, mount] of Object.entries(options.popovers ?? {})) {
495
- const triggerEl = findPopoverTrigger(mountedRoot, mount.triggerId) ??
496
- findByUI(mountedRoot, `${key}-trigger`);
497
- const panelEl = findPopoverPanel(mountedRoot, mount.panelId) ??
498
- findByUI(mountedRoot, key, "d-popover");
499
-
500
- if (triggerEl && panelEl) {
501
- withScope(scope, () => {
502
- mount.api._attachTo(triggerEl, panelEl);
503
- });
504
- }
505
- }
506
-
507
- return () => {
508
- while (cleanups.length > 0) {
509
- const cleanup = cleanups.pop();
510
- if (cleanup) cleanup();
511
- }
512
- scope.dispose();
513
- };
514
- }
@@ -1,128 +0,0 @@
1
- import { signal, computed } from "../../../core/signal.js";
2
- import { getCurrentScope } from "../../../core/scope.js";
3
- import type { Tabs, TabBindings, TabsOptions } from "../ui-types.js";
4
-
5
- export function createTabs(options: TabsOptions = {}): Tabs {
6
- const { initial = "", orientation = "horizontal" } = options;
7
- const active = signal(initial);
8
-
9
- const select = (tabId: string) => active.set(tabId);
10
-
11
- const isActive = (tabId: string): boolean => active() === tabId;
12
-
13
- const handleClick = (ev: Event) => {
14
- const target = (ev.target as HTMLElement).closest<HTMLElement>("[data-tab]");
15
- if (target && target.dataset.tab) {
16
- select(target.dataset.tab);
17
- }
18
- };
19
-
20
- const _attachTo = (el: HTMLElement) => {
21
- const scope = getCurrentScope();
22
-
23
- // ARIA setup
24
- const tabList = el.querySelector<HTMLElement>(
25
- "[data-d-tag='d-tab-list'], .d-tab-list"
26
- );
27
- if (tabList) {
28
- tabList.setAttribute("role", "tablist");
29
- tabList.setAttribute("aria-orientation", orientation);
30
- }
31
-
32
- const tabButtons = el.querySelectorAll<HTMLElement>("[data-tab]");
33
- const panels = el.querySelectorAll<HTMLElement>(
34
- "[data-d-tag='d-tab-panel'], .d-tab-panel"
35
- );
36
-
37
- tabButtons.forEach((btn, i) => {
38
- btn.setAttribute("role", "tab");
39
- const tabId = btn.dataset.tab!;
40
- const btnId = btn.id || `d-tab-${tabId}`;
41
- if (!btn.id) btn.id = btnId;
42
-
43
- if (panels[i]) {
44
- const panelId = panels[i].id || `d-tabpanel-${tabId}`;
45
- if (!panels[i].id) panels[i].id = panelId;
46
- panels[i].setAttribute("role", "tabpanel");
47
- panels[i].setAttribute("aria-labelledby", btnId);
48
- btn.setAttribute("aria-controls", panelId);
49
- }
50
- });
51
-
52
- const syncAria = (activeId: string) => {
53
- tabButtons.forEach((btn, i) => {
54
- const isAct = btn.dataset.tab === activeId;
55
- btn.setAttribute("aria-selected", String(isAct));
56
- btn.setAttribute("tabindex", isAct ? "0" : "-1");
57
- if (panels[i]) panels[i].setAttribute("aria-hidden", String(!isAct));
58
- });
59
- };
60
-
61
- // Apply initial state
62
- syncAria(active());
63
-
64
- const unsub = active.on(syncAria);
65
-
66
- // Keyboard navigation
67
- const prevKey = orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
68
- const nextKey = orientation === "vertical" ? "ArrowDown" : "ArrowRight";
69
-
70
- const onKeydown = (ev: KeyboardEvent) => {
71
- const tabs = Array.from(tabButtons);
72
- const currentIndex = tabs.findIndex((btn) => btn.dataset.tab === active());
73
- let nextIndex = -1;
74
-
75
- switch (ev.key) {
76
- case nextKey:
77
- ev.preventDefault();
78
- nextIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
79
- break;
80
- case prevKey:
81
- ev.preventDefault();
82
- nextIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
83
- break;
84
- case "Home":
85
- ev.preventDefault();
86
- nextIndex = 0;
87
- break;
88
- case "End":
89
- ev.preventDefault();
90
- nextIndex = tabs.length - 1;
91
- break;
92
- default:
93
- return;
94
- }
95
-
96
- const nextTab = tabs[nextIndex];
97
- if (nextTab?.dataset.tab) {
98
- select(nextTab.dataset.tab);
99
- nextTab.focus();
100
- }
101
- };
102
-
103
- if (tabList) tabList.addEventListener("keydown", onKeydown);
104
-
105
- if (scope) {
106
- scope.onCleanup(() => {
107
- unsub();
108
- if (tabList) tabList.removeEventListener("keydown", onKeydown);
109
- });
110
- }
111
- };
112
-
113
- return { active, select, isActive, handleClick, _attachTo };
114
- }
115
-
116
- export function tabBindings(tabs: Tabs, tabId: string): TabBindings {
117
- const tabClass = computed(() =>
118
- tabs.active() === tabId ? "d-tab active" : "d-tab"
119
- );
120
-
121
- const selected = computed(() =>
122
- tabs.active() === tabId ? "true" : "false"
123
- );
124
-
125
- const visible = computed(() => tabs.active() === tabId);
126
-
127
- return { tabClass, selected, visible };
128
- }