@auvyx/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +43 -2
  3. package/dist/auvyx.bundle.css +5941 -0
  4. package/dist/auvyx.chart.min.js +2 -0
  5. package/dist/auvyx.corners.min.js +2 -0
  6. package/dist/auvyx.css +10880 -0
  7. package/dist/auvyx.js +3222 -0
  8. package/dist/auvyx.min.css +2 -0
  9. package/dist/auvyx.min.js +19 -0
  10. package/dist/custom-elements.json +948 -0
  11. package/dist/elements/calendar.min.js +2 -0
  12. package/dist/elements/context-menu.min.js +2 -0
  13. package/dist/elements/data-table.min.js +2 -0
  14. package/dist/elements/date-picker.min.js +2 -0
  15. package/dist/elements/index.min.js +2 -0
  16. package/dist/elements/menu.min.js +2 -0
  17. package/dist/elements/resizable.min.js +2 -0
  18. package/dist/js/accordion.min.js +2 -0
  19. package/dist/js/calendar.min.js +2 -0
  20. package/dist/js/combobox.min.js +2 -0
  21. package/dist/js/command.min.js +2 -0
  22. package/dist/js/context-menu.min.js +2 -0
  23. package/dist/js/copy.min.js +2 -0
  24. package/dist/js/data-table.min.js +2 -0
  25. package/dist/js/date-picker.min.js +2 -0
  26. package/dist/js/drawer.min.js +2 -0
  27. package/dist/js/dropdown-menu.min.js +2 -0
  28. package/dist/js/manifest.json +114 -0
  29. package/dist/js/menu.min.js +2 -0
  30. package/dist/js/popover.min.js +2 -0
  31. package/dist/js/resizable.min.js +2 -0
  32. package/dist/js/select.min.js +2 -0
  33. package/dist/js/sidebar.min.js +2 -0
  34. package/dist/js/slider.min.js +2 -0
  35. package/dist/js/tabs.min.js +2 -0
  36. package/dist/js/toast.min.js +19 -0
  37. package/dist/styles/auvyx-luma.min.css +3 -0
  38. package/dist/styles/auvyx-lyra.min.css +3 -0
  39. package/dist/styles/auvyx-maia.min.css +3 -0
  40. package/dist/styles/auvyx-mira.min.css +3 -0
  41. package/dist/styles/auvyx-nova.min.css +3 -0
  42. package/dist/styles/auvyx-rhea.min.css +3 -0
  43. package/dist/styles/auvyx-sera.min.css +3 -0
  44. package/dist/styles/auvyx-vega.min.css +3 -0
  45. package/dist/types/elements.d.ts +262 -0
  46. package/dist/types/react.d.ts +181 -0
  47. package/dist/types/vue.d.ts +155 -0
  48. package/dist/ui/accordion.css +91 -0
  49. package/dist/ui/accordion.js +249 -0
  50. package/dist/ui/accordion.native.css +24 -0
  51. package/dist/ui/alert.css +119 -0
  52. package/dist/ui/aspect-ratio.css +38 -0
  53. package/dist/ui/attachment.css +211 -0
  54. package/dist/ui/avatar.css +204 -0
  55. package/dist/ui/badge.css +183 -0
  56. package/dist/ui/base.css +1200 -0
  57. package/dist/ui/breadcrumb.css +90 -0
  58. package/dist/ui/bubble.css +171 -0
  59. package/dist/ui/button-group.css +110 -0
  60. package/dist/ui/button.css +328 -0
  61. package/dist/ui/calendar.css +260 -0
  62. package/dist/ui/calendar.js +317 -0
  63. package/dist/ui/card.css +126 -0
  64. package/dist/ui/carousel.css +172 -0
  65. package/dist/ui/chart.css +130 -0
  66. package/dist/ui/checkbox.css +119 -0
  67. package/dist/ui/code-block.css +235 -0
  68. package/dist/ui/combobox.css +500 -0
  69. package/dist/ui/combobox.js +757 -0
  70. package/dist/ui/command.css +366 -0
  71. package/dist/ui/command.js +382 -0
  72. package/dist/ui/context-menu.css +17 -0
  73. package/dist/ui/context-menu.js +52 -0
  74. package/dist/ui/copy.js +56 -0
  75. package/dist/ui/data-table.css +114 -0
  76. package/dist/ui/data-table.js +140 -0
  77. package/dist/ui/date-picker.css +14 -0
  78. package/dist/ui/date-picker.js +101 -0
  79. package/dist/ui/dialog.css +363 -0
  80. package/dist/ui/dialog.native.css +6 -0
  81. package/dist/ui/drawer.css +238 -0
  82. package/dist/ui/drawer.js +265 -0
  83. package/dist/ui/dropdown-menu.css +193 -0
  84. package/dist/ui/dropdown-menu.js +388 -0
  85. package/dist/ui/empty.css +111 -0
  86. package/dist/ui/field.css +293 -0
  87. package/dist/ui/hover-card.css +85 -0
  88. package/dist/ui/input-group.css +269 -0
  89. package/dist/ui/input-otp.css +101 -0
  90. package/dist/ui/input.css +117 -0
  91. package/dist/ui/item.css +223 -0
  92. package/dist/ui/kbd.css +50 -0
  93. package/dist/ui/label.css +48 -0
  94. package/dist/ui/manifest.json +667 -0
  95. package/dist/ui/marker.css +66 -0
  96. package/dist/ui/menu.js +161 -0
  97. package/dist/ui/menubar.css +55 -0
  98. package/dist/ui/message-scroller.css +83 -0
  99. package/dist/ui/message.css +69 -0
  100. package/dist/ui/native-select.css +130 -0
  101. package/dist/ui/navigation-menu.css +166 -0
  102. package/dist/ui/popover.css +154 -0
  103. package/dist/ui/popover.js +268 -0
  104. package/dist/ui/popover.native.css +84 -0
  105. package/dist/ui/progress.css +29 -0
  106. package/dist/ui/questionnaire.css +188 -0
  107. package/dist/ui/radio-group.css +115 -0
  108. package/dist/ui/resizable.css +94 -0
  109. package/dist/ui/resizable.js +95 -0
  110. package/dist/ui/scroll-area.css +43 -0
  111. package/dist/ui/select.css +289 -0
  112. package/dist/ui/select.js +646 -0
  113. package/dist/ui/sheet.css +219 -0
  114. package/dist/ui/sidebar.css +716 -0
  115. package/dist/ui/sidebar.js +287 -0
  116. package/dist/ui/skeleton.css +13 -0
  117. package/dist/ui/slider.css +184 -0
  118. package/dist/ui/slider.js +206 -0
  119. package/dist/ui/switch.css +156 -0
  120. package/dist/ui/table.css +157 -0
  121. package/dist/ui/tabs.css +240 -0
  122. package/dist/ui/tabs.js +288 -0
  123. package/dist/ui/textarea.css +91 -0
  124. package/dist/ui/toast.css +110 -0
  125. package/dist/ui/toast.js +378 -0
  126. package/dist/ui/toggle-group.css +42 -0
  127. package/dist/ui/toggle.css +150 -0
  128. package/dist/ui/tooltip.css +319 -0
  129. package/dist/ui/trace.css +142 -0
  130. package/dist/ui/typography.css +184 -0
  131. package/dist/wc/accordion.min.js +2 -0
  132. package/dist/wc/combobox.min.js +2 -0
  133. package/dist/wc/command.min.js +2 -0
  134. package/dist/wc/drawer.min.js +2 -0
  135. package/dist/wc/dropdown-menu.min.js +2 -0
  136. package/dist/wc/manifest.json +154 -0
  137. package/dist/wc/popover.min.js +2 -0
  138. package/dist/wc/select.min.js +2 -0
  139. package/dist/wc/sidebar.min.js +2 -0
  140. package/dist/wc/slider.min.js +2 -0
  141. package/dist/wc/tabs.min.js +2 -0
  142. package/dist/wc/toast.min.js +19 -0
  143. package/package.json +62 -4
package/dist/auvyx.js ADDED
@@ -0,0 +1,3222 @@
1
+ /*! @auvyx/ui v0.1.0 | MIT | based on Basecoat (MIT) */
2
+ (() => {
3
+ const componentRegistry = {};
4
+ let observer = null;
5
+
6
+ const registerComponent = (name, selectorOrOptions, initFunction) => {
7
+ const options = typeof selectorOrOptions === 'object'
8
+ ? selectorOrOptions
9
+ : { selector: selectorOrOptions, init: initFunction };
10
+
11
+ componentRegistry[name] = {
12
+ selector: options.selector,
13
+ init: options.init,
14
+ refresh: options.refresh,
15
+ };
16
+ };
17
+
18
+ const initComponent = (element, componentName) => {
19
+ const component = componentRegistry[componentName];
20
+ if (!component) return;
21
+
22
+ try {
23
+ component.init(element);
24
+ if (element.hasAttribute(`data-${componentName}-initialized`)) {
25
+ element.dataset.basecoatComponent = componentName;
26
+ }
27
+ } catch (error) {
28
+ console.error(`Failed to initialize ${componentName}:`, error);
29
+ if (typeof element._destroy === 'function') {
30
+ try {
31
+ element._destroy();
32
+ } catch (destroyError) {
33
+ console.error(`Failed to clean up ${componentName} after initialization error:`, destroyError);
34
+ }
35
+ }
36
+ delete element._destroy;
37
+ element.removeAttribute(`data-${componentName}-initialized`);
38
+ delete element.dataset.basecoatComponent;
39
+ }
40
+ };
41
+
42
+ const destroyComponent = (element) => {
43
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
44
+ const componentName = element.dataset?.basecoatComponent;
45
+
46
+ if (typeof element._destroy === 'function') {
47
+ try {
48
+ element._destroy();
49
+ } catch (error) {
50
+ console.error('Failed to destroy Basecoat component:', error);
51
+ }
52
+ }
53
+
54
+ delete element._destroy;
55
+ if (componentName) element.removeAttribute(`data-${componentName}-initialized`);
56
+ delete element.dataset.basecoatComponent;
57
+ };
58
+
59
+ const destroyRemovedComponents = (node) => {
60
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
61
+ if (node.isConnected) return;
62
+
63
+ if (node.dataset?.basecoatComponent) destroyComponent(node);
64
+ node.querySelectorAll('[data-basecoat-component]').forEach(destroyComponent);
65
+ };
66
+
67
+ const uniqueElements = (elements) => Array.from(new Set(elements));
68
+
69
+ const getComponentElements = (componentName, selector, force = false) => {
70
+ const elements = Array.from(document.querySelectorAll(selector));
71
+ if (force) {
72
+ elements.push(...document.querySelectorAll(`[data-basecoat-component="${componentName}"]`));
73
+ }
74
+ return uniqueElements(elements);
75
+ };
76
+
77
+ const initAllComponents = (options = {}) => {
78
+ const force = options.force === true;
79
+ Object.entries(componentRegistry).forEach(([name, { selector }]) => {
80
+ getComponentElements(name, selector, force).forEach((element) => {
81
+ const wasComponent = element.dataset?.basecoatComponent === name;
82
+ if (force) destroyComponent(element);
83
+ if (wasComponent || element.matches(selector)) initComponent(element, name);
84
+ });
85
+ });
86
+ };
87
+
88
+ const initNewComponents = (node) => {
89
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
90
+
91
+ Object.entries(componentRegistry).forEach(([name, { selector }]) => {
92
+ if (node.matches(selector)) initComponent(node, name);
93
+ node.querySelectorAll(selector).forEach(element => initComponent(element, name));
94
+ });
95
+ };
96
+
97
+ const refreshComponent = (element) => {
98
+ if (!element) return;
99
+ if (typeof element.refresh === 'function') {
100
+ element.refresh();
101
+ return;
102
+ }
103
+
104
+ const componentName = element.dataset?.basecoatComponent;
105
+ const component = componentName ? componentRegistry[componentName] : null;
106
+ if (component?.refresh) {
107
+ component.refresh(element);
108
+ }
109
+ };
110
+
111
+ const startObserver = () => {
112
+ if (observer) return;
113
+
114
+ observer = new MutationObserver((mutations) => {
115
+ mutations.forEach((mutation) => {
116
+ mutation.addedNodes.forEach(initNewComponents);
117
+ mutation.removedNodes.forEach(destroyRemovedComponents);
118
+ });
119
+ });
120
+
121
+ observer.observe(document.body, { childList: true, subtree: true });
122
+ };
123
+
124
+ const stopObserver = () => {
125
+ if (!observer) return;
126
+ observer.disconnect();
127
+ observer = null;
128
+ };
129
+
130
+ const initRegisteredComponent = (componentName, options = {}) => {
131
+ const component = componentRegistry[componentName];
132
+ if (!component) {
133
+ console.warn(`Component '${componentName}' not found in registry`);
134
+ return;
135
+ }
136
+
137
+ const force = options.force === true;
138
+ getComponentElements(componentName, component.selector, force).forEach((element) => {
139
+ const wasComponent = element.dataset?.basecoatComponent === componentName;
140
+ if (force) destroyComponent(element);
141
+ if (wasComponent || element.matches(component.selector)) initComponent(element, componentName);
142
+ });
143
+ };
144
+
145
+ const initAllRegisteredComponents = (options = {}) => {
146
+ initAllComponents(options);
147
+ };
148
+
149
+ const setTheme = (mode) => {
150
+ const dark = mode === 'dark';
151
+ document.documentElement.classList.toggle('dark', dark);
152
+ try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
153
+ document.dispatchEvent(new CustomEvent('basecoat:themechange', { detail: { mode: dark ? 'dark' : 'light' } }));
154
+ };
155
+
156
+ const getTheme = () => document.documentElement.classList.contains('dark') ? 'dark' : 'light';
157
+
158
+ window.basecoat = {
159
+ register: registerComponent,
160
+ init: initRegisteredComponent,
161
+ initAll: initAllRegisteredComponents,
162
+ refresh: refreshComponent,
163
+ start: startObserver,
164
+ stop: stopObserver,
165
+ theme: {
166
+ get: getTheme,
167
+ set: setTheme,
168
+ toggle: () => setTheme(getTheme() === 'dark' ? 'light' : 'dark'),
169
+ },
170
+ };
171
+
172
+ document.addEventListener('DOMContentLoaded', () => {
173
+ initAllComponents();
174
+ startObserver();
175
+ });
176
+ })();
177
+
178
+ (() => {
179
+ const states = new WeakMap();
180
+
181
+ const isDisabled = (details) => {
182
+ const summary = details.querySelector(':scope > summary');
183
+ return details.getAttribute('aria-disabled') === 'true'
184
+ || details.dataset.disabled === 'true'
185
+ || summary?.getAttribute('aria-disabled') === 'true';
186
+ };
187
+
188
+ const isMultiple = (root) => root.hasAttribute('data-multiple');
189
+
190
+ const closeSiblings = (root, activeDetails) => {
191
+ if (isMultiple(root) || !activeDetails.open) return;
192
+ root.querySelectorAll(':scope > details[open]').forEach((details) => {
193
+ if (details !== activeDetails) details.open = false;
194
+ });
195
+ };
196
+
197
+ const initAccordion = (root) => {
198
+ if (root.dataset.accordionInitialized) return;
199
+
200
+ const handleClick = (event) => {
201
+ const summary = event.target.closest('summary');
202
+ const details = summary?.closest('details');
203
+ if (!details || details.parentElement !== root || !isDisabled(details)) return;
204
+ event.preventDefault();
205
+ };
206
+
207
+ const handleKeydown = (event) => {
208
+ if (event.key !== 'Enter' && event.key !== ' ') return;
209
+ const summary = event.target.closest('summary');
210
+ const details = summary?.closest('details');
211
+ if (!details || details.parentElement !== root || !isDisabled(details)) return;
212
+ event.preventDefault();
213
+ };
214
+
215
+ const handleToggle = (event) => {
216
+ const details = event.target;
217
+ if (details.parentElement !== root) return;
218
+ if (isDisabled(details)) {
219
+ details.open = false;
220
+ return;
221
+ }
222
+ closeSiblings(root, details);
223
+ };
224
+
225
+ root.addEventListener('click', handleClick);
226
+ root.addEventListener('keydown', handleKeydown);
227
+ root.addEventListener('toggle', handleToggle, true);
228
+ root.querySelectorAll(':scope > details[open]').forEach((details) => closeSiblings(root, details));
229
+
230
+ states.set(root, { handleClick, handleToggle });
231
+ root._destroy = () => {
232
+ root.removeEventListener('click', handleClick);
233
+ root.removeEventListener('keydown', handleKeydown);
234
+ root.removeEventListener('toggle', handleToggle, true);
235
+ states.delete(root);
236
+ };
237
+
238
+ root.dataset.accordionInitialized = 'true';
239
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
240
+ };
241
+
242
+ if (window.basecoat) {
243
+ window.basecoat.register('accordion', '.accordion:not([data-accordion-initialized])', initAccordion);
244
+ }
245
+ })();
246
+
247
+ (() => {
248
+ const states = new WeakMap();
249
+
250
+ const getElements = (root) => {
251
+ const popover = root.querySelector(':scope > [data-popover]');
252
+ const input = root.querySelector(':scope > input[role="combobox"], :scope > .input-group input[role="combobox"], :scope > .combobox-chips input[role="combobox"]')
253
+ || popover?.querySelector('input[role="combobox"]');
254
+ const chips = root.querySelector(':scope > .combobox-chips');
255
+ const popupTrigger = root.querySelector(':scope > button[aria-haspopup="listbox"]');
256
+ const trigger = popupTrigger || root.querySelector(':scope > .input-group button[aria-haspopup="listbox"]');
257
+ const clearButton = root.querySelector('[data-clear]');
258
+ const valueTarget = popupTrigger?.querySelector('[data-value]') || (popupTrigger?.matches('[data-value]') ? popupTrigger : null);
259
+ const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
260
+ const hiddenInput = root.querySelector(':scope > input[type="hidden"]');
261
+ return { input, chips, trigger, clearButton, valueTarget, popover, listbox, hiddenInput };
262
+ };
263
+
264
+ const ensureMultipleInputSurface = (root, elements) => {
265
+ if (elements.listbox?.getAttribute('aria-multiselectable') !== 'true' || elements.chips || !elements.input) return elements;
266
+ if (elements.input.parentElement !== root) return elements;
267
+
268
+ const chips = document.createElement('div');
269
+ chips.className = 'combobox-chips';
270
+ root.insertBefore(chips, elements.input);
271
+ chips.appendChild(elements.input);
272
+
273
+ return getElements(root);
274
+ };
275
+
276
+ const getValue = option => option.dataset.value ?? option.textContent.trim();
277
+ const getLabel = option => option.dataset.label || option.textContent.trim();
278
+ const getFormat = root => root.dataset.format === 'object' ? 'object' : 'value';
279
+ const isDisabled = option => option.getAttribute('aria-disabled') === 'true';
280
+ const toSelected = option => ({ value: getValue(option), label: getLabel(option) });
281
+ const normalizeEntry = entry => {
282
+ if (entry && typeof entry === 'object') {
283
+ const value = entry.value == null ? '' : String(entry.value);
284
+ return value ? { value, label: String(entry.label ?? entry.value) } : null;
285
+ }
286
+ const value = entry == null ? '' : String(entry);
287
+ return value ? { value, label: value } : null;
288
+ };
289
+
290
+ const getOptions = (listbox) => {
291
+ const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
292
+ return {
293
+ allOptions,
294
+ options: allOptions.filter(option => !isDisabled(option)),
295
+ };
296
+ };
297
+
298
+ const getSelection = (state) => Array.from(state.selected.values());
299
+ const getCanonicalValue = (state) => state.isMultiple ? getSelection(state).map(item => item.value) : (getSelection(state)[0]?.value || '');
300
+ const getSelectedDetail = (state) => state.isMultiple ? getSelection(state) : (getSelection(state)[0] || null);
301
+
302
+ const serializeSelection = (state) => {
303
+ const selected = getSelection(state);
304
+ if (state.format === 'object') {
305
+ return JSON.stringify(state.isMultiple ? selected : (selected[0] || null));
306
+ }
307
+ const value = selected.map(item => item.value);
308
+ return state.isMultiple ? JSON.stringify(value) : (value[0] || '');
309
+ };
310
+
311
+ const parseStoredSelection = (storedValue, inputValue, state) => {
312
+ if (state.isMultiple) {
313
+ let parsed = [];
314
+ try {
315
+ parsed = JSON.parse(storedValue || '[]');
316
+ } catch (_) {
317
+ parsed = [];
318
+ }
319
+ if (!Array.isArray(parsed)) return [];
320
+ return parsed.map(item => {
321
+ const entry = normalizeEntry(state.format === 'object' ? item : { value: item, label: state.selected.get(String(item))?.label ?? item });
322
+ if (!entry) return null;
323
+ const option = state.options.find(opt => getValue(opt) === entry.value);
324
+ return option ? toSelected(option) : entry;
325
+ }).filter(Boolean);
326
+ }
327
+
328
+ if (state.format === 'object') {
329
+ try {
330
+ const entry = normalizeEntry(JSON.parse(storedValue || 'null'));
331
+ if (!entry) return [];
332
+ const option = state.options.find(opt => getValue(opt) === entry.value);
333
+ return [option ? toSelected(option) : entry];
334
+ } catch (_) {
335
+ return [];
336
+ }
337
+ }
338
+
339
+ const value = storedValue || '';
340
+ if (!value) return [];
341
+ const option = state.options.find(opt => getValue(opt) === value);
342
+ return [option ? toSelected(option) : { value, label: state.selected.get(value)?.label || inputValue || value }];
343
+ };
344
+
345
+ const scrollOptionIntoListbox = (state, option) => {
346
+ const optionRect = option.getBoundingClientRect();
347
+ const listboxRect = state.listbox.getBoundingClientRect();
348
+
349
+ if (optionRect.top < listboxRect.top) {
350
+ state.listbox.scrollTop -= listboxRect.top - optionRect.top;
351
+ } else if (optionRect.bottom > listboxRect.bottom) {
352
+ state.listbox.scrollTop += optionRect.bottom - listboxRect.bottom;
353
+ }
354
+ };
355
+
356
+ const setActiveOption = (state, index) => {
357
+ if (state.activeIndex > -1 && state.options[state.activeIndex]) {
358
+ state.options[state.activeIndex].classList.remove('active');
359
+ }
360
+
361
+ state.activeIndex = index;
362
+
363
+ if (state.activeIndex > -1) {
364
+ const activeOption = state.options[state.activeIndex];
365
+ activeOption.classList.add('active');
366
+ if (!activeOption.id) activeOption.id = `${state.listbox.id || state.root.id || 'combobox'}-option-${state.activeIndex}`;
367
+ state.input.setAttribute('aria-activedescendant', activeOption.id);
368
+ } else {
369
+ state.input.removeAttribute('aria-activedescendant');
370
+ }
371
+ };
372
+
373
+ const syncEmptyState = (state) => {
374
+ state.popover.dataset.empty = String(state.visibleOptions.length === 0);
375
+ };
376
+
377
+ const filterOptions = (state, { preserveActive = false, search: forcedSearch } = {}) => {
378
+ const previousActive = state.activeIndex > -1 ? state.options[state.activeIndex] : null;
379
+ state.visibleOptions = [];
380
+
381
+ if (state.manualFilter) {
382
+ state.visibleOptions = state.options.filter(option => option.getAttribute('aria-hidden') !== 'true');
383
+
384
+ if (preserveActive && previousActive && state.visibleOptions.includes(previousActive)) {
385
+ setActiveOption(state, state.options.indexOf(previousActive));
386
+ } else {
387
+ setActiveOption(state, state.autoHighlight && state.visibleOptions.length > 0 ? state.options.indexOf(state.visibleOptions[0]) : -1);
388
+ }
389
+ syncEmptyState(state);
390
+ return;
391
+ }
392
+
393
+ const search = (forcedSearch ?? state.input.value).trim().toLowerCase();
394
+
395
+ state.allOptions.forEach(option => {
396
+ if (option.hasAttribute('data-force')) {
397
+ option.setAttribute('aria-hidden', 'false');
398
+ if (state.options.includes(option)) state.visibleOptions.push(option);
399
+ return;
400
+ }
401
+
402
+ const optionText = (option.dataset.filter || option.dataset.label || option.textContent).trim().toLowerCase();
403
+ const keywords = (option.dataset.keywords || '').toLowerCase().split(/[\s,]+/).filter(Boolean);
404
+ const matches = !search || optionText.includes(search) || keywords.some(keyword => keyword.includes(search));
405
+ option.setAttribute('aria-hidden', String(!matches));
406
+ if (matches && state.options.includes(option)) state.visibleOptions.push(option);
407
+ });
408
+
409
+ if (preserveActive && previousActive && state.visibleOptions.includes(previousActive)) {
410
+ setActiveOption(state, state.options.indexOf(previousActive));
411
+ } else {
412
+ setActiveOption(state, state.autoHighlight && state.visibleOptions.length > 0 ? state.options.indexOf(state.visibleOptions[0]) : -1);
413
+ }
414
+ syncEmptyState(state);
415
+ };
416
+
417
+ const renderChips = (root) => {
418
+ const state = states.get(root);
419
+ if (!state.chips) return;
420
+
421
+ state.chips.querySelectorAll('.combobox-chip').forEach(chip => chip.remove());
422
+
423
+ getSelection(state).forEach(entry => {
424
+ const chip = document.createElement('span');
425
+ chip.className = 'combobox-chip';
426
+ chip.dataset.value = entry.value;
427
+
428
+ const label = document.createElement('span');
429
+ label.textContent = entry.label;
430
+
431
+ const remove = document.createElement('button');
432
+ remove.type = 'button';
433
+ remove.className = 'combobox-chip-remove btn';
434
+ remove.dataset.variant = 'ghost';
435
+ remove.dataset.size = 'icon-xs';
436
+ remove.setAttribute('aria-label', `Remove ${entry.label}`);
437
+ remove.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>';
438
+ remove.addEventListener('click', (event) => {
439
+ event.stopPropagation();
440
+ root.deselect(entry.value);
441
+ state.input.focus();
442
+ });
443
+
444
+ chip.appendChild(label);
445
+ chip.appendChild(remove);
446
+ state.chips.insertBefore(chip, state.input);
447
+ });
448
+ };
449
+
450
+ const syncTriggerValue = (state) => {
451
+ if (!state.valueTarget) return;
452
+ const selected = getSelection(state);
453
+ state.valueTarget.textContent = state.isMultiple
454
+ ? selected.map(entry => entry.label).join(', ')
455
+ : (selected[0]?.label || state.valueTarget.dataset.placeholder || '');
456
+ };
457
+
458
+ const syncClearButton = (state) => {
459
+ if (!state.clearButton) return;
460
+ state.clearButton.hidden = getSelection(state).length === 0 && state.input.value === '';
461
+ };
462
+
463
+ const syncSelectedOptions = (state) => {
464
+ state.options.forEach(option => {
465
+ if (state.selected.has(getValue(option))) {
466
+ option.setAttribute('aria-selected', 'true');
467
+ } else {
468
+ option.removeAttribute('aria-selected');
469
+ }
470
+ });
471
+ };
472
+
473
+ const setSelected = (root, entries, triggerEvent = true) => {
474
+ const state = states.get(root);
475
+ const normalized = (Array.isArray(entries) ? entries : [entries]).map(normalizeEntry).filter(Boolean);
476
+
477
+ state.selected.clear();
478
+ if (state.isMultiple) {
479
+ normalized.forEach(entry => state.selected.set(entry.value, entry));
480
+ state.input.value = '';
481
+ } else if (normalized[0]) {
482
+ state.selected.set(normalized[0].value, normalized[0]);
483
+ state.input.value = state.valueTarget ? '' : normalized[0].label;
484
+ } else {
485
+ state.input.value = '';
486
+ }
487
+
488
+ state.hiddenInput.value = serializeSelection(state);
489
+ syncSelectedOptions(state);
490
+ if (state.isMultiple) {
491
+ renderChips(root);
492
+ filterOptions(state, { preserveActive: true });
493
+ }
494
+ syncTriggerValue(state);
495
+ syncClearButton(state);
496
+
497
+ if (triggerEvent) {
498
+ root.dispatchEvent(new CustomEvent('change', {
499
+ detail: { value: getCanonicalValue(state), selected: getSelectedDetail(state) },
500
+ bubbles: true,
501
+ }));
502
+ }
503
+ };
504
+
505
+ const closePopover = (state, focusInput = false) => {
506
+ if (state.popover.getAttribute('aria-hidden') === 'true') return;
507
+ state.popover.setAttribute('aria-hidden', 'true');
508
+ state.input.setAttribute('aria-expanded', 'false');
509
+ state.trigger?.setAttribute('aria-expanded', 'false');
510
+ setActiveOption(state, -1);
511
+ if (focusInput) {
512
+ state.skipOpenOnFocus = true;
513
+ state.input.focus();
514
+ requestAnimationFrame(() => { state.skipOpenOnFocus = false; });
515
+ }
516
+ };
517
+
518
+ const openPopover = (state) => {
519
+ const { root } = state;
520
+ if (state.popover.getAttribute('aria-hidden') === 'false') return;
521
+ document.dispatchEvent(new CustomEvent('basecoat:popover', { detail: { source: root } }));
522
+ root.refresh();
523
+ const selected = getSelection(state)[0];
524
+ if (state.valueTarget) state.input.value = '';
525
+ const search = !state.isMultiple && selected && state.input.value === selected.label ? '' : undefined;
526
+ filterOptions(state, { search });
527
+ state.popover.setAttribute('aria-hidden', 'false');
528
+ state.input.setAttribute('aria-expanded', 'true');
529
+ state.trigger?.setAttribute('aria-expanded', 'true');
530
+ if (state.trigger) state.input.focus();
531
+ };
532
+
533
+ const refreshCombobox = (root) => {
534
+ const state = states.get(root);
535
+ if (!state) return;
536
+
537
+ let elements = getElements(root);
538
+ if (!elements.input || !elements.popover || !elements.listbox || !elements.hiddenInput) {
539
+ const missing = [];
540
+ if (!elements.input) missing.push('input');
541
+ if (!elements.popover) missing.push('popover');
542
+ if (!elements.listbox) missing.push('listbox');
543
+ if (!elements.hiddenInput) missing.push('hidden input');
544
+ console.error(`Combobox refresh failed. Missing element(s): ${missing.join(', ')}`, root);
545
+ return;
546
+ }
547
+
548
+ elements = ensureMultipleInputSurface(root, elements);
549
+ const previousValue = elements.hiddenInput.value;
550
+ const previousInputValue = elements.input.value;
551
+ Object.assign(state, elements, getOptions(elements.listbox));
552
+ state.isMultiple = state.listbox.getAttribute('aria-multiselectable') === 'true';
553
+ state.closeOnSelect = root.dataset.closeOnSelect === 'true';
554
+ state.autoHighlight = root.dataset.autoHighlight === 'true';
555
+ state.manualFilter = root.dataset.filter === 'manual';
556
+ state.format = getFormat(root);
557
+
558
+ const stored = parseStoredSelection(previousValue, previousInputValue, state);
559
+ if (stored.length > 0) {
560
+ setSelected(root, stored, false);
561
+ } else {
562
+ const selectedOptions = state.options.filter(option => option.getAttribute('aria-selected') === 'true').map(toSelected);
563
+ if (selectedOptions.length > 0) {
564
+ setSelected(root, state.isMultiple ? selectedOptions : selectedOptions[0], false);
565
+ } else if (state.isMultiple) {
566
+ setSelected(root, [], false);
567
+ } else {
568
+ state.input.value = previousInputValue;
569
+ state.selected.clear();
570
+ state.hiddenInput.value = '';
571
+ syncSelectedOptions(state);
572
+ filterOptions(state, { preserveActive: true });
573
+ }
574
+ }
575
+ syncTriggerValue(state);
576
+ syncClearButton(state);
577
+ };
578
+
579
+ const resolveEntry = (state, value) => {
580
+ if (value && typeof value === 'object') {
581
+ const entry = normalizeEntry(value);
582
+ if (!entry) return null;
583
+ const option = state.options.find(opt => getValue(opt) === entry.value);
584
+ return option ? toSelected(option) : entry;
585
+ }
586
+
587
+ const option = state.options.find(opt => getValue(opt) === value);
588
+ if (option) return toSelected(option);
589
+ const existing = state.selected.get(String(value));
590
+ return existing || null;
591
+ };
592
+
593
+ const selectValue = (root, value) => {
594
+ const state = states.get(root);
595
+ const entry = resolveEntry(state, value);
596
+ if (!entry) return;
597
+
598
+ if (state.isMultiple) {
599
+ setSelected(root, [...getSelection(state).filter(item => item.value !== entry.value), entry]);
600
+ if (state.closeOnSelect) root.close(true);
601
+ } else {
602
+ setSelected(root, entry);
603
+ root.close(state.trigger ? false : true);
604
+ state.trigger?.focus();
605
+ }
606
+ };
607
+
608
+ const deselectValue = (root, value) => {
609
+ const state = states.get(root);
610
+ if (!state.isMultiple) return;
611
+ const normalized = String(value);
612
+ if (!state.selected.has(normalized)) return;
613
+ setSelected(root, getSelection(state).filter(item => item.value !== normalized));
614
+ };
615
+
616
+ const clearValue = (root) => {
617
+ const state = states.get(root);
618
+ setSelected(root, [], true);
619
+ state.input.value = '';
620
+ filterOptions(state);
621
+ syncClearButton(state);
622
+ if (state.popover.getAttribute('aria-hidden') === 'false') state.input.focus();
623
+ };
624
+
625
+ const handleKeydown = (event, root) => {
626
+ const state = states.get(root);
627
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape', 'Backspace'].includes(event.key)) return;
628
+
629
+ const isOpen = state.popover.getAttribute('aria-hidden') === 'false';
630
+
631
+ if (event.key === 'Backspace' && state.isMultiple && state.input.value === '') {
632
+ const selected = getSelection(state);
633
+ const last = selected[selected.length - 1];
634
+ if (last) root.deselect(last.value);
635
+ return;
636
+ }
637
+
638
+ if (event.key === 'Escape') {
639
+ if (isOpen) {
640
+ event.preventDefault();
641
+ event.stopPropagation();
642
+ root.close(true);
643
+ }
644
+ return;
645
+ }
646
+
647
+ if (!isOpen && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
648
+ event.preventDefault();
649
+ openPopover(state);
650
+ }
651
+
652
+ if (state.popover.getAttribute('aria-hidden') === 'true') return;
653
+
654
+ if (event.key === 'Enter') {
655
+ if (state.activeIndex > -1) {
656
+ event.preventDefault();
657
+ const option = state.options[state.activeIndex];
658
+ state.isMultiple ? root.toggle(getValue(option)) : root.select(getValue(option));
659
+ }
660
+ return;
661
+ }
662
+
663
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || state.visibleOptions.length === 0) return;
664
+ event.preventDefault();
665
+
666
+ const currentVisibleIndex = state.activeIndex > -1 ? state.visibleOptions.indexOf(state.options[state.activeIndex]) : -1;
667
+ let nextVisibleIndex = currentVisibleIndex;
668
+
669
+ if (event.key === 'ArrowDown') nextVisibleIndex = Math.min(currentVisibleIndex + 1, state.visibleOptions.length - 1);
670
+ if (event.key === 'ArrowUp') nextVisibleIndex = currentVisibleIndex <= 0 ? 0 : currentVisibleIndex - 1;
671
+ if (event.key === 'Home') nextVisibleIndex = 0;
672
+ if (event.key === 'End') nextVisibleIndex = state.visibleOptions.length - 1;
673
+
674
+ const nextOption = state.visibleOptions[nextVisibleIndex];
675
+ setActiveOption(state, state.options.indexOf(nextOption));
676
+ scrollOptionIntoListbox(state, nextOption);
677
+ };
678
+
679
+ const initCombobox = (root) => {
680
+ if (root.dataset.comboboxInitialized) return;
681
+
682
+ const state = { root, activeIndex: -1, allOptions: [], options: [], visibleOptions: [], selected: new Map(), format: 'value', manualFilter: false, skipOpenOnFocus: false };
683
+ states.set(root, state);
684
+ root.refresh = () => refreshCombobox(root);
685
+
686
+ refreshCombobox(root);
687
+ if (!state.input || !state.popover || !state.listbox || !state.hiddenInput) {
688
+ states.delete(root);
689
+ delete root.refresh;
690
+ return;
691
+ }
692
+
693
+ root.close = (focusInput = false) => closePopover(state, focusInput);
694
+
695
+ root.select = (value) => selectValue(root, value);
696
+ root.selectByValue = root.select;
697
+ root.setValue = (value) => {
698
+ const entries = state.isMultiple ? (Array.isArray(value) ? value : (value == null ? [] : [value])) : [value];
699
+ const resolved = entries.map(entry => resolveEntry(state, entry) || normalizeEntry(entry)).filter(Boolean);
700
+ setSelected(root, state.isMultiple ? resolved : resolved[0]);
701
+ };
702
+ if (state.isMultiple) {
703
+ root.deselect = (value) => deselectValue(root, value);
704
+ root.toggle = (value) => {
705
+ const entry = resolveEntry(state, value);
706
+ if (!entry) return;
707
+ state.selected.has(entry.value) ? root.deselect(entry.value) : root.select(entry);
708
+ };
709
+ root.selectAll = () => setSelected(root, state.options.map(toSelected));
710
+ root.selectNone = () => setSelected(root, []);
711
+ }
712
+
713
+ const handleInputFocus = () => {
714
+ if (state.skipOpenOnFocus) return;
715
+ openPopover(state);
716
+ };
717
+ const handleInputClick = () => openPopover(state);
718
+ const handleInput = () => {
719
+ openPopover(state);
720
+ filterOptions(state);
721
+ if (!state.isMultiple) {
722
+ state.hiddenInput.value = '';
723
+ state.selected.clear();
724
+ syncSelectedOptions(state);
725
+ syncTriggerValue(state);
726
+ syncClearButton(state);
727
+ }
728
+ };
729
+ const handleTriggerClick = () => {
730
+ if (state.popover.getAttribute('aria-hidden') === 'false') {
731
+ root.close(false);
732
+ } else {
733
+ openPopover(state);
734
+ }
735
+ };
736
+ const handleClearClick = (event) => {
737
+ event.preventDefault();
738
+ event.stopPropagation();
739
+ clearValue(root);
740
+ };
741
+ const handleInputKeydown = (event) => handleKeydown(event, root);
742
+ const handleListboxMousemove = (event) => {
743
+ const option = event.target.closest('[role="option"]');
744
+ if (option && state.visibleOptions.includes(option)) setActiveOption(state, state.options.indexOf(option));
745
+ };
746
+ const handleListboxClick = (event) => {
747
+ const option = event.target.closest('[role="option"]');
748
+ if (!option || !state.options.includes(option)) return;
749
+ state.isMultiple ? root.toggle(getValue(option)) : root.select(getValue(option));
750
+ if (state.isMultiple && !state.closeOnSelect) state.input.focus();
751
+ };
752
+ const handleDocumentClick = (event) => {
753
+ if (!root.contains(event.target)) root.close(false);
754
+ };
755
+ const handleDocumentPopover = (event) => {
756
+ if (!root.contains(event.detail.source)) root.close(false);
757
+ };
758
+
759
+ state.input.addEventListener('focus', handleInputFocus);
760
+ state.input.addEventListener('click', handleInputClick);
761
+ state.input.addEventListener('input', handleInput);
762
+ state.input.addEventListener('keydown', handleInputKeydown);
763
+ state.trigger?.addEventListener('click', handleTriggerClick);
764
+ state.clearButton?.addEventListener('click', handleClearClick);
765
+ state.listbox.addEventListener('mousemove', handleListboxMousemove);
766
+ state.listbox.addEventListener('click', handleListboxClick);
767
+ document.addEventListener('click', handleDocumentClick);
768
+ document.addEventListener('basecoat:popover', handleDocumentPopover);
769
+
770
+ root._destroy = () => {
771
+ state.input.removeEventListener('focus', handleInputFocus);
772
+ state.input.removeEventListener('click', handleInputClick);
773
+ state.input.removeEventListener('input', handleInput);
774
+ state.input.removeEventListener('keydown', handleInputKeydown);
775
+ state.trigger?.removeEventListener('click', handleTriggerClick);
776
+ state.clearButton?.removeEventListener('click', handleClearClick);
777
+ state.listbox.removeEventListener('mousemove', handleListboxMousemove);
778
+ state.listbox.removeEventListener('click', handleListboxClick);
779
+ document.removeEventListener('click', handleDocumentClick);
780
+ document.removeEventListener('basecoat:popover', handleDocumentPopover);
781
+ state.chips?.querySelectorAll('.combobox-chip').forEach(chip => chip.remove());
782
+ states.delete(root);
783
+ delete root.refresh;
784
+ delete root.close;
785
+ delete root.select;
786
+ delete root.selectByValue;
787
+ delete root.setValue;
788
+ delete root.clear;
789
+ delete root.deselect;
790
+ delete root.toggle;
791
+ delete root.selectAll;
792
+ delete root.selectNone;
793
+ };
794
+
795
+ state.popover.setAttribute('aria-hidden', 'true');
796
+ state.input.setAttribute('aria-expanded', 'false');
797
+ state.trigger?.setAttribute('aria-expanded', 'false');
798
+ root.clear = () => clearValue(root);
799
+
800
+ Object.defineProperty(root, 'value', {
801
+ configurable: true,
802
+ get: () => getCanonicalValue(state),
803
+ set: (value) => root.setValue(value),
804
+ });
805
+
806
+ Object.defineProperty(root, 'selected', {
807
+ configurable: true,
808
+ get: () => getSelectedDetail(state),
809
+ });
810
+
811
+ root.dataset.comboboxInitialized = 'true';
812
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
813
+ };
814
+
815
+ if (window.basecoat) {
816
+ window.basecoat.register('combobox', {
817
+ selector: '.combobox:not([data-combobox-initialized])',
818
+ init: initCombobox,
819
+ refresh: refreshCombobox,
820
+ });
821
+ }
822
+ })();
823
+
824
+ (() => {
825
+ const states = new WeakMap();
826
+
827
+ const isDisabled = (item) =>
828
+ item.hasAttribute('disabled') ||
829
+ item.getAttribute('aria-disabled') === 'true' ||
830
+ item.getAttribute('data-disabled') === 'true';
831
+
832
+ const getElements = (container) => ({
833
+ input: container.querySelector('header input'),
834
+ menu: container.querySelector('[role="menu"]'),
835
+ });
836
+
837
+ const getItems = (menu) => {
838
+ const allItems = Array.from(menu.querySelectorAll('[role="menuitem"]'));
839
+ return {
840
+ allItems,
841
+ items: allItems.filter(item => !isDisabled(item)),
842
+ };
843
+ };
844
+
845
+ const scrollItemIntoMenu = (state, item) => {
846
+ const itemRect = item.getBoundingClientRect();
847
+ const menuRect = state.menu.getBoundingClientRect();
848
+
849
+ if (itemRect.top < menuRect.top) {
850
+ state.menu.scrollTop -= menuRect.top - itemRect.top;
851
+ } else if (itemRect.bottom > menuRect.bottom) {
852
+ state.menu.scrollTop += itemRect.bottom - menuRect.bottom;
853
+ }
854
+ };
855
+
856
+ const setActiveItem = (state, index) => {
857
+ if (state.activeIndex > -1 && state.items[state.activeIndex]) {
858
+ state.items[state.activeIndex].classList.remove('active');
859
+ }
860
+
861
+ state.activeIndex = index;
862
+
863
+ if (state.activeIndex > -1) {
864
+ const activeItem = state.items[state.activeIndex];
865
+ activeItem.classList.add('active');
866
+ if (activeItem.id) {
867
+ state.input.setAttribute('aria-activedescendant', activeItem.id);
868
+ } else {
869
+ state.input.removeAttribute('aria-activedescendant');
870
+ }
871
+ } else {
872
+ state.input.removeAttribute('aria-activedescendant');
873
+ }
874
+ };
875
+
876
+ const filterItems = (state) => {
877
+ if (state.manualFilter) {
878
+ setActiveItem(state, -1);
879
+ state.visibleItems = state.items.filter(item => item.getAttribute('aria-hidden') !== 'true');
880
+ if (state.visibleItems.length > 0) {
881
+ setActiveItem(state, state.items.indexOf(state.visibleItems[0]));
882
+ }
883
+ return;
884
+ }
885
+
886
+ const searchTerm = state.input.value.trim().toLowerCase();
887
+
888
+ setActiveItem(state, -1);
889
+ state.visibleItems = [];
890
+
891
+ state.allItems.forEach(item => {
892
+ if (item.hasAttribute('data-force')) {
893
+ item.setAttribute('aria-hidden', 'false');
894
+ if (state.items.includes(item)) state.visibleItems.push(item);
895
+ return;
896
+ }
897
+
898
+ const itemText = (item.dataset.filter || item.textContent).trim().toLowerCase();
899
+ const keywordList = (item.dataset.keywords || '')
900
+ .toLowerCase()
901
+ .split(/[\s,]+/)
902
+ .filter(Boolean);
903
+ const matchesKeyword = keywordList.some(keyword => keyword.includes(searchTerm));
904
+ const matches = itemText.includes(searchTerm) || matchesKeyword;
905
+ item.setAttribute('aria-hidden', String(!matches));
906
+ if (matches && state.items.includes(item)) state.visibleItems.push(item);
907
+ });
908
+
909
+ if (state.visibleItems.length > 0) {
910
+ setActiveItem(state, state.items.indexOf(state.visibleItems[0]));
911
+ scrollItemIntoMenu(state, state.visibleItems[0]);
912
+ }
913
+ };
914
+
915
+ const refreshCommand = (container) => {
916
+ const state = states.get(container);
917
+ if (!state) return;
918
+
919
+ const elements = getElements(container);
920
+ if (!elements.input || !elements.menu) {
921
+ const missing = [];
922
+ if (!elements.input) missing.push('input');
923
+ if (!elements.menu) missing.push('menu');
924
+ console.error(`Command component refresh failed. Missing element(s): ${missing.join(', ')}`, container);
925
+ return;
926
+ }
927
+
928
+ Object.assign(state, elements, getItems(elements.menu));
929
+ state.manualFilter = container.dataset.filter === 'manual';
930
+ filterItems(state);
931
+ };
932
+
933
+ const handleKeyNavigation = (event, state) => {
934
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(event.key)) return;
935
+
936
+ if (event.key === 'Enter') {
937
+ event.preventDefault();
938
+ if (state.activeIndex > -1) state.items[state.activeIndex]?.click();
939
+ return;
940
+ }
941
+
942
+ if (state.visibleItems.length === 0) return;
943
+
944
+ event.preventDefault();
945
+
946
+ const currentVisibleIndex = state.activeIndex > -1 ? state.visibleItems.indexOf(state.items[state.activeIndex]) : -1;
947
+ let nextVisibleIndex = currentVisibleIndex;
948
+
949
+ if (event.key === 'ArrowDown' && currentVisibleIndex < state.visibleItems.length - 1) nextVisibleIndex = currentVisibleIndex + 1;
950
+ if (event.key === 'ArrowUp') nextVisibleIndex = currentVisibleIndex > 0 ? currentVisibleIndex - 1 : 0;
951
+ if (event.key === 'Home') nextVisibleIndex = 0;
952
+ if (event.key === 'End') nextVisibleIndex = state.visibleItems.length - 1;
953
+
954
+ if (nextVisibleIndex !== currentVisibleIndex) {
955
+ const newActiveItem = state.visibleItems[nextVisibleIndex];
956
+ setActiveItem(state, state.items.indexOf(newActiveItem));
957
+ scrollItemIntoMenu(state, newActiveItem);
958
+ }
959
+ };
960
+
961
+ const initCommand = (container) => {
962
+ if (container.dataset.commandInitialized) return;
963
+
964
+ const state = { activeIndex: -1, allItems: [], items: [], visibleItems: [], manualFilter: false };
965
+ states.set(container, state);
966
+
967
+ container.refresh = () => refreshCommand(container);
968
+
969
+ const elements = getElements(container);
970
+ if (!elements.input || !elements.menu) {
971
+ const missing = [];
972
+ if (!elements.input) missing.push('input');
973
+ if (!elements.menu) missing.push('menu');
974
+ console.error(`Command component initialization failed. Missing element(s): ${missing.join(', ')}`, container);
975
+ states.delete(container);
976
+ delete container.refresh;
977
+ return;
978
+ }
979
+ Object.assign(state, elements);
980
+
981
+ const handleInput = () => filterItems(state);
982
+ const handleInputKeydown = (event) => handleKeyNavigation(event, state);
983
+ const handleMenuMousemove = (event) => {
984
+ const menuItem = event.target.closest('[role="menuitem"]');
985
+ if (menuItem && state.visibleItems.includes(menuItem)) {
986
+ const index = state.items.indexOf(menuItem);
987
+ if (index !== state.activeIndex) setActiveItem(state, index);
988
+ }
989
+ };
990
+ const handleMenuClick = (event) => {
991
+ const clickedItem = event.target.closest('[role="menuitem"]');
992
+ if (clickedItem && state.visibleItems.includes(clickedItem)) {
993
+ const dialog = container.closest('dialog.command-dialog');
994
+ if (dialog && !clickedItem.hasAttribute('data-keep-command-open')) dialog.close();
995
+ }
996
+ };
997
+
998
+ state.input.addEventListener('input', handleInput);
999
+ state.input.addEventListener('keydown', handleInputKeydown);
1000
+ state.menu.addEventListener('mousemove', handleMenuMousemove);
1001
+ state.menu.addEventListener('click', handleMenuClick);
1002
+
1003
+ container._destroy = () => {
1004
+ state.input.removeEventListener('input', handleInput);
1005
+ state.input.removeEventListener('keydown', handleInputKeydown);
1006
+ state.menu.removeEventListener('mousemove', handleMenuMousemove);
1007
+ state.menu.removeEventListener('click', handleMenuClick);
1008
+ states.delete(container);
1009
+ delete container.refresh;
1010
+ };
1011
+
1012
+ refreshCommand(container);
1013
+ container.dataset.commandInitialized = 'true';
1014
+ container.dispatchEvent(new CustomEvent('basecoat:initialized'));
1015
+ };
1016
+
1017
+ if (window.basecoat) {
1018
+ window.basecoat.register('command', {
1019
+ selector: '.command:not([data-command-initialized])',
1020
+ init: initCommand,
1021
+ refresh: refreshCommand,
1022
+ });
1023
+ }
1024
+ })();
1025
+
1026
+ // Copy to clipboard (fork addition).
1027
+ //
1028
+ // Any element with [data-copy] copies text on click:
1029
+ // data-copy="" → the <pre> of the closest .code-block
1030
+ // data-copy="#selector" → the textContent of that element
1031
+ // data-copy-text="…" → this literal text
1032
+ // While copied, the button gets [data-copied] for 2s and its aria-label
1033
+ // becomes the value of data-copied-label (default "Copied").
1034
+ // One delegated listener, so it also works for content added later.
1035
+ (() => {
1036
+ if (window.__basecoatCopy) return;
1037
+ window.__basecoatCopy = true;
1038
+
1039
+ const sourceText = (button) => {
1040
+ if (button.hasAttribute('data-copy-text')) return button.getAttribute('data-copy-text');
1041
+ const target = button.getAttribute('data-copy');
1042
+ const el = target ? document.querySelector(target) : button.closest('.code-block')?.querySelector(':scope > pre');
1043
+ return el ? el.textContent : null;
1044
+ };
1045
+
1046
+ const selectFallback = (button) => {
1047
+ const target = button.getAttribute('data-copy');
1048
+ const el = target ? document.querySelector(target) : button.closest('.code-block')?.querySelector(':scope > pre');
1049
+ if (!el) return;
1050
+ const range = document.createRange();
1051
+ range.selectNodeContents(el);
1052
+ const selection = window.getSelection();
1053
+ selection.removeAllRanges();
1054
+ selection.addRange(range);
1055
+ };
1056
+
1057
+ document.addEventListener('click', async (event) => {
1058
+ const button = event.target.closest('[data-copy], [data-copy-text]');
1059
+ if (!button || button.disabled) return;
1060
+ const text = sourceText(button);
1061
+ if (text == null) return;
1062
+
1063
+ try {
1064
+ await navigator.clipboard.writeText(text.replace(/\n$/, ''));
1065
+ } catch {
1066
+ selectFallback(button);
1067
+ return;
1068
+ }
1069
+
1070
+ const label = button.getAttribute('aria-label');
1071
+ if (!button.hasAttribute('data-copied')) button.dataset.copyLabel = label ?? '';
1072
+ button.setAttribute('data-copied', '');
1073
+ button.setAttribute('aria-label', button.getAttribute('data-copied-label') || 'Copied');
1074
+ clearTimeout(button._copyTimer);
1075
+ button._copyTimer = setTimeout(() => {
1076
+ button.removeAttribute('data-copied');
1077
+ if (button.dataset.copyLabel) button.setAttribute('aria-label', button.dataset.copyLabel);
1078
+ }, 2000);
1079
+ });
1080
+ })();
1081
+
1082
+ (() => {
1083
+ const toMs = (value) => {
1084
+ if (!value) return 0;
1085
+ const trimmed = value.trim();
1086
+ if (trimmed.endsWith('ms')) return parseFloat(trimmed) || 0;
1087
+ if (trimmed.endsWith('s')) return (parseFloat(trimmed) || 0) * 1000;
1088
+ return parseFloat(trimmed) || 0;
1089
+ };
1090
+
1091
+ const maxTransitionMs = (element) => {
1092
+ if (!element) return 0;
1093
+ const styles = getComputedStyle(element);
1094
+ const durations = styles.transitionDuration.split(',').map(toMs);
1095
+ const delays = styles.transitionDelay.split(',').map(toMs);
1096
+ return Math.max(0, ...durations.map((duration, index) => duration + (delays[index] || delays[0] || 0)));
1097
+ };
1098
+
1099
+ const initDrawer = (drawer) => {
1100
+ if (drawer.dataset.drawerInitialized) return;
1101
+
1102
+ const nativeClose = drawer.close.bind(drawer);
1103
+ let pointerStartedOnBackdrop = false;
1104
+ let closeTimer = null;
1105
+
1106
+ const finishClose = (returnValue) => {
1107
+ window.clearTimeout(closeTimer);
1108
+ closeTimer = null;
1109
+ drawer.removeAttribute('data-closing');
1110
+ nativeClose(returnValue);
1111
+ };
1112
+
1113
+ drawer.close = (returnValue = '') => {
1114
+ if (!drawer.open) return;
1115
+ if (drawer.dataset.closing) return;
1116
+
1117
+ const content = drawer.firstElementChild;
1118
+ const duration = maxTransitionMs(content);
1119
+
1120
+ if (duration === 0 || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
1121
+ finishClose(returnValue);
1122
+ return;
1123
+ }
1124
+
1125
+ drawer.dataset.closing = 'true';
1126
+ closeTimer = window.setTimeout(() => finishClose(returnValue), duration + 50);
1127
+ };
1128
+
1129
+ const handleCancel = (event) => {
1130
+ event.preventDefault();
1131
+ drawer.close();
1132
+ };
1133
+
1134
+ const handlePointerDown = (event) => {
1135
+ pointerStartedOnBackdrop = event.target === drawer;
1136
+ };
1137
+
1138
+ const handlePointerUp = (event) => {
1139
+ if (pointerStartedOnBackdrop && event.target === drawer) {
1140
+ drawer.close();
1141
+ }
1142
+ pointerStartedOnBackdrop = false;
1143
+ };
1144
+
1145
+ drawer.addEventListener('cancel', handleCancel);
1146
+ drawer.addEventListener('pointerdown', handlePointerDown);
1147
+ drawer.addEventListener('pointerup', handlePointerUp);
1148
+
1149
+ drawer._destroy = () => {
1150
+ window.clearTimeout(closeTimer);
1151
+ drawer.removeEventListener('cancel', handleCancel);
1152
+ drawer.removeEventListener('pointerdown', handlePointerDown);
1153
+ drawer.removeEventListener('pointerup', handlePointerUp);
1154
+ drawer.close = nativeClose;
1155
+ delete drawer._destroy;
1156
+ };
1157
+
1158
+ drawer.dataset.drawerInitialized = 'true';
1159
+ drawer.dispatchEvent(new CustomEvent('basecoat:initialized'));
1160
+ };
1161
+
1162
+ if (window.basecoat) {
1163
+ window.basecoat.register('drawer', '.drawer:not([data-drawer-initialized])', initDrawer);
1164
+ }
1165
+ })();
1166
+
1167
+ (() => {
1168
+ const states = new WeakMap();
1169
+
1170
+ const isDisabled = (item) =>
1171
+ item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true';
1172
+
1173
+ const getElements = (root) => {
1174
+ const trigger = root.querySelector(':scope > button');
1175
+ const popover = root.querySelector(':scope > [data-popover]');
1176
+ const menu = popover ? popover.querySelector('[role="menu"]') : null;
1177
+ return { trigger, popover, menu };
1178
+ };
1179
+
1180
+ const getItems = (menu) => Array.from(menu.querySelectorAll('[role^="menuitem"]')).filter(item => !isDisabled(item));
1181
+
1182
+ const setActiveItem = (state, index) => {
1183
+ if (state.activeIndex > -1 && state.items[state.activeIndex]) {
1184
+ state.items[state.activeIndex].classList.remove('active');
1185
+ }
1186
+ state.activeIndex = index;
1187
+ if (state.activeIndex > -1 && state.items[state.activeIndex]) {
1188
+ const activeItem = state.items[state.activeIndex];
1189
+ activeItem.classList.add('active');
1190
+ if (activeItem.id) state.trigger.setAttribute('aria-activedescendant', activeItem.id);
1191
+ } else {
1192
+ state.trigger.removeAttribute('aria-activedescendant');
1193
+ }
1194
+ };
1195
+
1196
+ const refreshDropdownMenu = (root) => {
1197
+ const state = states.get(root);
1198
+ if (!state) return;
1199
+
1200
+ const elements = getElements(root);
1201
+ if (!elements.trigger || !elements.popover || !elements.menu) {
1202
+ const missing = [];
1203
+ if (!elements.trigger) missing.push('trigger');
1204
+ if (!elements.popover) missing.push('popover');
1205
+ if (!elements.menu) missing.push('menu');
1206
+ console.error(`Dropdown menu refresh failed. Missing element(s): ${missing.join(', ')}`, root);
1207
+ return;
1208
+ }
1209
+
1210
+ Object.assign(state, elements);
1211
+ state.items = getItems(state.menu);
1212
+ if (state.activeIndex > -1 && !state.items[state.activeIndex]) setActiveItem(state, -1);
1213
+ };
1214
+
1215
+ const closePopover = (state, focusOnTrigger = true) => {
1216
+ if (state.trigger.getAttribute('aria-expanded') === 'false') return;
1217
+ state.trigger.setAttribute('aria-expanded', 'false');
1218
+ state.trigger.removeAttribute('aria-activedescendant');
1219
+ state.popover.setAttribute('aria-hidden', 'true');
1220
+ if (focusOnTrigger) state.trigger.focus();
1221
+ setActiveItem(state, -1);
1222
+ };
1223
+
1224
+ const openPopover = (root, state, initialSelection = false) => {
1225
+ document.dispatchEvent(new CustomEvent('basecoat:popover', { detail: { source: root } }));
1226
+ root.refresh();
1227
+ state.trigger.setAttribute('aria-expanded', 'true');
1228
+ state.popover.setAttribute('aria-hidden', 'false');
1229
+
1230
+ if (state.items.length > 0 && initialSelection) {
1231
+ setActiveItem(state, initialSelection === 'last' ? state.items.length - 1 : 0);
1232
+ }
1233
+ };
1234
+
1235
+ const initDropdownMenu = (root) => {
1236
+ if (root.dataset.dropdownMenuInitialized) return;
1237
+
1238
+ const state = { activeIndex: -1, items: [] };
1239
+ states.set(root, state);
1240
+ root.refresh = () => refreshDropdownMenu(root);
1241
+
1242
+ refreshDropdownMenu(root);
1243
+ if (!state.trigger || !state.popover || !state.menu) {
1244
+ states.delete(root);
1245
+ delete root.refresh;
1246
+ return;
1247
+ }
1248
+
1249
+ root.open = (initialSelection = false) => openPopover(root, state, initialSelection);
1250
+ root.close = (focusOnTrigger = true) => closePopover(state, focusOnTrigger);
1251
+ root.toggle = () => state.trigger.getAttribute('aria-expanded') === 'true' ? root.close() : root.open(false);
1252
+
1253
+ const handleTriggerClick = root.toggle;
1254
+
1255
+ const handleKeydown = (event) => {
1256
+ const isExpanded = state.trigger.getAttribute('aria-expanded') === 'true';
1257
+
1258
+ if (event.key === 'Escape') {
1259
+ if (isExpanded) root.close();
1260
+ return;
1261
+ }
1262
+
1263
+ if (!isExpanded) {
1264
+ if (['Enter', ' '].includes(event.key)) {
1265
+ event.preventDefault();
1266
+ root.open(false);
1267
+ } else if (event.key === 'ArrowDown') {
1268
+ event.preventDefault();
1269
+ root.open('first');
1270
+ } else if (event.key === 'ArrowUp') {
1271
+ event.preventDefault();
1272
+ root.open('last');
1273
+ }
1274
+ return;
1275
+ }
1276
+
1277
+ if (state.items.length === 0) return;
1278
+
1279
+ let nextIndex = state.activeIndex;
1280
+ if (event.key === 'ArrowDown') {
1281
+ event.preventDefault();
1282
+ nextIndex = state.activeIndex === -1 ? 0 : Math.min(state.activeIndex + 1, state.items.length - 1);
1283
+ } else if (event.key === 'ArrowUp') {
1284
+ event.preventDefault();
1285
+ nextIndex = state.activeIndex === -1 ? state.items.length - 1 : Math.max(state.activeIndex - 1, 0);
1286
+ } else if (event.key === 'Home') {
1287
+ event.preventDefault();
1288
+ nextIndex = 0;
1289
+ } else if (event.key === 'End') {
1290
+ event.preventDefault();
1291
+ nextIndex = state.items.length - 1;
1292
+ } else if (['Enter', ' '].includes(event.key)) {
1293
+ event.preventDefault();
1294
+ state.items[state.activeIndex]?.click();
1295
+ root.close();
1296
+ return;
1297
+ } else {
1298
+ return;
1299
+ }
1300
+
1301
+ if (nextIndex !== state.activeIndex) setActiveItem(state, nextIndex);
1302
+ };
1303
+
1304
+ const handleMenuMousemove = (event) => {
1305
+ const menuItem = event.target.closest('[role^="menuitem"]');
1306
+ if (menuItem && !isDisabled(menuItem) && state.items.includes(menuItem)) {
1307
+ const index = state.items.indexOf(menuItem);
1308
+ if (index !== state.activeIndex) setActiveItem(state, index);
1309
+ }
1310
+ };
1311
+
1312
+ const handleMenuMouseleave = () => setActiveItem(state, -1);
1313
+ const handleMenuClick = (event) => {
1314
+ const menuItem = event.target.closest('[role^="menuitem"]');
1315
+ if (!menuItem || isDisabled(menuItem)) return;
1316
+
1317
+ if (menuItem.getAttribute('role') === 'menuitemcheckbox') {
1318
+ menuItem.setAttribute('aria-checked', menuItem.getAttribute('aria-checked') !== 'true');
1319
+ } else if (menuItem.getAttribute('role') === 'menuitemradio') {
1320
+ const group = menuItem.closest('[role="group"], [role="menu"]');
1321
+ group?.querySelectorAll('[role="menuitemradio"]').forEach((item) => {
1322
+ if (!isDisabled(item)) item.setAttribute('aria-checked', item === menuItem ? 'true' : 'false');
1323
+ });
1324
+ }
1325
+
1326
+ root.close();
1327
+ };
1328
+
1329
+ const handleDocumentClick = (event) => {
1330
+ if (!root.contains(event.target)) root.close(false);
1331
+ };
1332
+
1333
+ const handleDocumentPopover = (event) => {
1334
+ if (!root.contains(event.detail.source)) root.close(false);
1335
+ };
1336
+
1337
+ state.trigger.addEventListener('click', handleTriggerClick);
1338
+ root.addEventListener('keydown', handleKeydown);
1339
+ state.menu.addEventListener('mousemove', handleMenuMousemove);
1340
+ state.menu.addEventListener('mouseleave', handleMenuMouseleave);
1341
+ state.menu.addEventListener('click', handleMenuClick);
1342
+ document.addEventListener('click', handleDocumentClick);
1343
+ document.addEventListener('basecoat:popover', handleDocumentPopover);
1344
+
1345
+ root._destroy = () => {
1346
+ state.trigger.removeEventListener('click', handleTriggerClick);
1347
+ root.removeEventListener('keydown', handleKeydown);
1348
+ state.menu.removeEventListener('mousemove', handleMenuMousemove);
1349
+ state.menu.removeEventListener('mouseleave', handleMenuMouseleave);
1350
+ state.menu.removeEventListener('click', handleMenuClick);
1351
+ document.removeEventListener('click', handleDocumentClick);
1352
+ document.removeEventListener('basecoat:popover', handleDocumentPopover);
1353
+ states.delete(root);
1354
+ delete root.refresh;
1355
+ delete root.open;
1356
+ delete root.close;
1357
+ delete root.toggle;
1358
+ };
1359
+
1360
+ state.trigger.setAttribute('aria-expanded', 'false');
1361
+ state.popover.setAttribute('aria-hidden', 'true');
1362
+ root.dataset.dropdownMenuInitialized = 'true';
1363
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
1364
+ };
1365
+
1366
+ if (window.basecoat) {
1367
+ window.basecoat.register('dropdown-menu', {
1368
+ selector: '.dropdown-menu:not([data-dropdown-menu-initialized]):not(:has(> [data-popover][popover]))',
1369
+ init: initDropdownMenu,
1370
+ refresh: refreshDropdownMenu,
1371
+ });
1372
+ }
1373
+ })();
1374
+
1375
+ (() => {
1376
+ const initPopover = (popoverComponent) => {
1377
+ if (popoverComponent.dataset.popoverInitialized) return;
1378
+
1379
+ const trigger = popoverComponent.querySelector(':scope > button');
1380
+ const content = popoverComponent.querySelector(':scope > [data-popover]');
1381
+
1382
+ if (!trigger || !content) {
1383
+ const missing = [];
1384
+ if (!trigger) missing.push('trigger');
1385
+ if (!content) missing.push('content');
1386
+ console.error(`Popover initialisation failed. Missing element(s): ${missing.join(', ')}`, popoverComponent);
1387
+ return;
1388
+ }
1389
+
1390
+ const closePopover = (focusOnTrigger = true) => {
1391
+ if (trigger.getAttribute('aria-expanded') === 'false') return;
1392
+ trigger.setAttribute('aria-expanded', 'false');
1393
+ content.setAttribute('aria-hidden', 'true');
1394
+ if (focusOnTrigger) {
1395
+ trigger.focus();
1396
+ }
1397
+ };
1398
+
1399
+ const openPopover = () => {
1400
+ document.dispatchEvent(new CustomEvent('basecoat:popover', {
1401
+ detail: { source: popoverComponent }
1402
+ }));
1403
+
1404
+ const elementToFocus = content.querySelector('[autofocus]');
1405
+ if (elementToFocus) {
1406
+ content.addEventListener('transitionend', () => {
1407
+ elementToFocus.focus();
1408
+ }, { once: true });
1409
+ }
1410
+
1411
+ trigger.setAttribute('aria-expanded', 'true');
1412
+ content.setAttribute('aria-hidden', 'false');
1413
+ };
1414
+
1415
+ const handleTriggerClick = () => {
1416
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
1417
+ if (isExpanded) {
1418
+ closePopover();
1419
+ } else {
1420
+ openPopover();
1421
+ }
1422
+ };
1423
+
1424
+ const handleKeydown = (event) => {
1425
+ if (event.key === 'Escape') {
1426
+ closePopover();
1427
+ }
1428
+ };
1429
+
1430
+ const handleDocumentClick = (event) => {
1431
+ if (!popoverComponent.contains(event.target)) {
1432
+ closePopover();
1433
+ }
1434
+ };
1435
+
1436
+ const handleDocumentPopover = (event) => {
1437
+ if (!popoverComponent.contains(event.detail.source)) {
1438
+ closePopover(false);
1439
+ }
1440
+ };
1441
+
1442
+ trigger.addEventListener('click', handleTriggerClick);
1443
+ popoverComponent.addEventListener('keydown', handleKeydown);
1444
+ document.addEventListener('click', handleDocumentClick);
1445
+ document.addEventListener('basecoat:popover', handleDocumentPopover);
1446
+
1447
+ popoverComponent._destroy = () => {
1448
+ trigger.removeEventListener('click', handleTriggerClick);
1449
+ popoverComponent.removeEventListener('keydown', handleKeydown);
1450
+ document.removeEventListener('click', handleDocumentClick);
1451
+ document.removeEventListener('basecoat:popover', handleDocumentPopover);
1452
+ };
1453
+
1454
+ popoverComponent.dataset.popoverInitialized = true;
1455
+ popoverComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
1456
+ };
1457
+
1458
+ if (window.basecoat) {
1459
+ window.basecoat.register('popover', '.popover:not([data-popover-initialized]):not(:has(> [data-popover][popover]))', initPopover);
1460
+ }
1461
+ })();
1462
+
1463
+ (() => {
1464
+ function updateRange(element) {
1465
+ const min = parseFloat(element.min || '0');
1466
+ const max = parseFloat(element.max || '100');
1467
+ const value = parseFloat(element.value || '0');
1468
+ const percent = max === min ? 0 : ((value - min) / (max - min)) * 100;
1469
+ element.style.setProperty('--slider-value', `${percent}%`);
1470
+ }
1471
+
1472
+ function initRange(element) {
1473
+ if (element.dataset.rangeInitialized) return;
1474
+
1475
+ updateRange(element);
1476
+ const handleInput = () => updateRange(element);
1477
+ element.addEventListener('input', handleInput);
1478
+ element._destroy = () => {
1479
+ element.removeEventListener('input', handleInput);
1480
+ };
1481
+ element.dataset.rangeInitialized = 'true';
1482
+ }
1483
+
1484
+ if (window.basecoat) {
1485
+ window.basecoat.register('range', 'input[type="range"]:not([data-range-initialized])', initRange);
1486
+ }
1487
+ })();
1488
+
1489
+ (() => {
1490
+ const states = new WeakMap();
1491
+
1492
+ const getElements = (root) => {
1493
+ const trigger = root.querySelector(':scope > button');
1494
+ const selectedLabel = trigger?.querySelector(':scope > span') || null;
1495
+ const popover = root.querySelector(':scope > [data-popover]');
1496
+ const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
1497
+ const input = root.querySelector(':scope > input[type="hidden"]');
1498
+ return { trigger, selectedLabel, popover, listbox, input };
1499
+ };
1500
+
1501
+ const getValue = (option) => option.dataset.value ?? option.textContent.trim();
1502
+ const getLabel = (option) => option.dataset.label || option.textContent.trim();
1503
+ const getFormat = (root) => root.dataset.format === 'object' ? 'object' : 'value';
1504
+ const isDisabled = (option) => option.getAttribute('aria-disabled') === 'true';
1505
+ const toSelected = (option) => ({ value: getValue(option), label: getLabel(option) });
1506
+
1507
+ const getOptions = (listbox) => {
1508
+ const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
1509
+ return {
1510
+ allOptions,
1511
+ options: allOptions.filter(option => !isDisabled(option)),
1512
+ };
1513
+ };
1514
+
1515
+ const parseStoredValues = (storedValue, { isMultiple, format }) => {
1516
+ if (isMultiple) {
1517
+ try {
1518
+ const parsed = JSON.parse(storedValue || '[]');
1519
+ if (!Array.isArray(parsed)) return [];
1520
+ return parsed
1521
+ .map(item => format === 'object' && item && typeof item === 'object' ? item.value : item)
1522
+ .filter(value => value != null)
1523
+ .map(String);
1524
+ } catch (_) {
1525
+ return [];
1526
+ }
1527
+ }
1528
+
1529
+ if (format === 'object') {
1530
+ try {
1531
+ const parsed = JSON.parse(storedValue || 'null');
1532
+ return parsed && typeof parsed === 'object' && parsed.value != null ? String(parsed.value) : '';
1533
+ } catch (_) {
1534
+ return '';
1535
+ }
1536
+ }
1537
+
1538
+ return storedValue || '';
1539
+ };
1540
+
1541
+ const serializeSelection = (state, selected) => {
1542
+ if (state.format === 'object') {
1543
+ return JSON.stringify(state.isMultiple ? selected : (selected[0] || null));
1544
+ }
1545
+
1546
+ const value = selected.map(item => item.value);
1547
+ return state.isMultiple ? JSON.stringify(value) : (value[0] || '');
1548
+ };
1549
+
1550
+ const showPlaceholder = (state) => {
1551
+ state.selectedLabel.textContent = state.placeholder || '';
1552
+ state.selectedLabel.classList.toggle('text-muted-foreground', Boolean(state.placeholder));
1553
+ state.input.value = state.isMultiple ? serializeSelection(state, []) : '';
1554
+ };
1555
+
1556
+ const scrollOptionIntoListbox = (state, option) => {
1557
+ const optionRect = option.getBoundingClientRect();
1558
+ const listboxRect = state.listbox.getBoundingClientRect();
1559
+
1560
+ if (optionRect.top < listboxRect.top) {
1561
+ state.listbox.scrollTop -= listboxRect.top - optionRect.top;
1562
+ } else if (optionRect.bottom > listboxRect.bottom) {
1563
+ state.listbox.scrollTop += optionRect.bottom - listboxRect.bottom;
1564
+ }
1565
+ };
1566
+
1567
+ const setActiveOption = (state, index) => {
1568
+ if (state.activeIndex > -1 && state.options[state.activeIndex]) {
1569
+ state.options[state.activeIndex].classList.remove('active');
1570
+ }
1571
+
1572
+ state.activeIndex = index;
1573
+
1574
+ if (state.activeIndex > -1) {
1575
+ const activeOption = state.options[state.activeIndex];
1576
+ activeOption.classList.add('active');
1577
+ if (activeOption.id) {
1578
+ state.trigger.setAttribute('aria-activedescendant', activeOption.id);
1579
+ } else {
1580
+ state.trigger.removeAttribute('aria-activedescendant');
1581
+ }
1582
+ } else {
1583
+ state.trigger.removeAttribute('aria-activedescendant');
1584
+ }
1585
+ };
1586
+
1587
+ const updateValue = (root, optionOrOptions, triggerEvent = true) => {
1588
+ const state = states.get(root);
1589
+ let value;
1590
+ let selectedDetail;
1591
+
1592
+ if (state.isMultiple) {
1593
+ const selected = Array.isArray(optionOrOptions) ? optionOrOptions : [];
1594
+ state.selectedOptions.clear();
1595
+ selected.forEach(option => state.selectedOptions.add(option));
1596
+
1597
+ const selectedInOrder = state.options.filter(option => state.selectedOptions.has(option));
1598
+ selectedDetail = selectedInOrder.map(toSelected);
1599
+ if (selectedInOrder.length === 0) {
1600
+ state.selectedLabel.textContent = state.placeholder;
1601
+ state.selectedLabel.classList.add('text-muted-foreground');
1602
+ } else {
1603
+ state.selectedLabel.textContent = selectedDetail.map(item => item.label).join(', ');
1604
+ state.selectedLabel.classList.remove('text-muted-foreground');
1605
+ }
1606
+
1607
+ value = selectedDetail.map(item => item.value);
1608
+ state.input.value = serializeSelection(state, selectedDetail);
1609
+ } else {
1610
+ const option = optionOrOptions;
1611
+ if (!option) {
1612
+ state.options.forEach(option => option.removeAttribute('aria-selected'));
1613
+ showPlaceholder(state);
1614
+ selectedDetail = null;
1615
+ value = '';
1616
+ } else {
1617
+ if (option.dataset.label) {
1618
+ state.selectedLabel.textContent = option.dataset.label;
1619
+ } else {
1620
+ state.selectedLabel.innerHTML = option.innerHTML;
1621
+ }
1622
+ state.selectedLabel.classList.remove('text-muted-foreground');
1623
+ selectedDetail = toSelected(option);
1624
+ value = selectedDetail.value;
1625
+ state.input.value = serializeSelection(state, [selectedDetail]);
1626
+ }
1627
+ }
1628
+
1629
+ state.options.forEach(option => {
1630
+ const isSelected = state.isMultiple ? state.selectedOptions.has(option) : optionOrOptions && option === optionOrOptions;
1631
+ if (isSelected) {
1632
+ option.setAttribute('aria-selected', 'true');
1633
+ } else {
1634
+ option.removeAttribute('aria-selected');
1635
+ }
1636
+ });
1637
+
1638
+ if (triggerEvent) {
1639
+ root.dispatchEvent(new CustomEvent('change', {
1640
+ detail: { value, selected: selectedDetail },
1641
+ bubbles: true,
1642
+ }));
1643
+ }
1644
+ };
1645
+
1646
+ const closePopover = (state, focusOnTrigger = true) => {
1647
+ if (state.popover.getAttribute('aria-hidden') === 'true') return;
1648
+ if (focusOnTrigger) state.trigger.focus();
1649
+ state.popover.setAttribute('aria-hidden', 'true');
1650
+ state.trigger.setAttribute('aria-expanded', 'false');
1651
+ setActiveOption(state, -1);
1652
+ };
1653
+
1654
+ const refreshSelect = (root) => {
1655
+ const state = states.get(root);
1656
+ if (!state) return;
1657
+
1658
+ const elements = getElements(root);
1659
+ if (!elements.trigger || !elements.selectedLabel || !elements.popover || !elements.listbox || !elements.input) {
1660
+ const missing = [];
1661
+ if (!elements.trigger) missing.push('trigger');
1662
+ if (!elements.selectedLabel) missing.push('selected label');
1663
+ if (!elements.popover) missing.push('popover');
1664
+ if (!elements.listbox) missing.push('listbox');
1665
+ if (!elements.input) missing.push('input');
1666
+ console.error(`Select component refresh failed. Missing element(s): ${missing.join(', ')}`, root);
1667
+ return;
1668
+ }
1669
+
1670
+ const previousValue = elements.input.value;
1671
+ Object.assign(state, elements, getOptions(elements.listbox));
1672
+ state.visibleOptions = [...state.options];
1673
+ state.isMultiple = state.listbox.getAttribute('aria-multiselectable') === 'true';
1674
+ state.format = getFormat(root);
1675
+ state.placeholder = root.dataset.placeholder || '';
1676
+ state.closeOnSelect = root.dataset.closeOnSelect === 'true';
1677
+
1678
+ if (state.isMultiple) {
1679
+ if (!state.selectedOptions) state.selectedOptions = new Set();
1680
+ const values = parseStoredValues(previousValue, state);
1681
+ const selected = values.length
1682
+ ? values.map(value => state.options.find(option => getValue(option) === value)).filter(Boolean)
1683
+ : state.options.filter(option => option.getAttribute('aria-selected') === 'true');
1684
+ updateValue(root, selected, false);
1685
+ } else {
1686
+ const value = parseStoredValues(previousValue, state);
1687
+ const selected = value === '' && state.placeholder
1688
+ ? null
1689
+ : state.options.find(option => getValue(option) === value)
1690
+ || state.options.find(option => option.getAttribute('aria-selected') === 'true');
1691
+ state.options.forEach(option => option.removeAttribute('aria-selected'));
1692
+ updateValue(root, selected || null, false);
1693
+ }
1694
+
1695
+ const selectedOption = state.listbox.querySelector('[role="option"][aria-selected="true"]');
1696
+ setActiveOption(state, selectedOption ? state.options.indexOf(selectedOption) : -1);
1697
+ };
1698
+
1699
+ const toggleMultipleValue = (root, option) => {
1700
+ const state = states.get(root);
1701
+ if (state.selectedOptions.has(option)) {
1702
+ state.selectedOptions.delete(option);
1703
+ } else {
1704
+ state.selectedOptions.add(option);
1705
+ }
1706
+ updateValue(root, state.options.filter(opt => state.selectedOptions.has(opt)));
1707
+ };
1708
+
1709
+ const selectValue = (root, value) => {
1710
+ const state = states.get(root);
1711
+ if (state.isMultiple) {
1712
+ const option = state.options.find(opt => getValue(opt) === value && !state.selectedOptions.has(opt));
1713
+ if (!option) return;
1714
+ state.selectedOptions.add(option);
1715
+ updateValue(root, state.options.filter(opt => state.selectedOptions.has(opt)));
1716
+ } else {
1717
+ const option = state.options.find(opt => getValue(opt) === value);
1718
+ if (!option) return;
1719
+ if (state.placeholder && getValue(option) === '') {
1720
+ updateValue(root, null);
1721
+ closePopover(state);
1722
+ return;
1723
+ }
1724
+ if (root.value !== value) updateValue(root, option);
1725
+ closePopover(state);
1726
+ }
1727
+ };
1728
+
1729
+ const deselectValue = (root, value) => {
1730
+ const state = states.get(root);
1731
+ if (!state.isMultiple) return;
1732
+ const option = state.options.find(opt => getValue(opt) === value && state.selectedOptions.has(opt));
1733
+ if (!option) return;
1734
+ state.selectedOptions.delete(option);
1735
+ updateValue(root, state.options.filter(opt => state.selectedOptions.has(opt)));
1736
+ };
1737
+
1738
+ const handleKeyNavigation = (event, root) => {
1739
+ const state = states.get(root);
1740
+ const isPopoverOpen = state.popover.getAttribute('aria-hidden') === 'false';
1741
+
1742
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(event.key)) return;
1743
+
1744
+ if (!isPopoverOpen) {
1745
+ if (event.key !== 'Enter' && event.key !== 'Escape') {
1746
+ event.preventDefault();
1747
+ root.open();
1748
+ }
1749
+ return;
1750
+ }
1751
+
1752
+ event.preventDefault();
1753
+
1754
+ if (event.key === 'Escape') {
1755
+ root.close();
1756
+ return;
1757
+ }
1758
+
1759
+ if (event.key === 'Enter') {
1760
+ if (state.activeIndex > -1) {
1761
+ const option = state.options[state.activeIndex];
1762
+ if (state.isMultiple) {
1763
+ toggleMultipleValue(root, option);
1764
+ if (state.closeOnSelect) root.close();
1765
+ } else {
1766
+ if (state.placeholder && getValue(option) === '') {
1767
+ updateValue(root, null);
1768
+ } else if (root.value !== getValue(option)) {
1769
+ updateValue(root, option);
1770
+ }
1771
+ root.close();
1772
+ }
1773
+ }
1774
+ return;
1775
+ }
1776
+
1777
+ if (state.visibleOptions.length === 0) return;
1778
+
1779
+ const currentVisibleIndex = state.activeIndex > -1 ? state.visibleOptions.indexOf(state.options[state.activeIndex]) : -1;
1780
+ let nextVisibleIndex = currentVisibleIndex;
1781
+
1782
+ if (event.key === 'ArrowDown' && currentVisibleIndex < state.visibleOptions.length - 1) nextVisibleIndex = currentVisibleIndex + 1;
1783
+ if (event.key === 'ArrowUp') nextVisibleIndex = currentVisibleIndex > 0 ? currentVisibleIndex - 1 : 0;
1784
+ if (event.key === 'Home') nextVisibleIndex = 0;
1785
+ if (event.key === 'End') nextVisibleIndex = state.visibleOptions.length - 1;
1786
+
1787
+ if (nextVisibleIndex !== currentVisibleIndex) {
1788
+ const newActiveOption = state.visibleOptions[nextVisibleIndex];
1789
+ setActiveOption(state, state.options.indexOf(newActiveOption));
1790
+ scrollOptionIntoListbox(state, newActiveOption);
1791
+ }
1792
+ };
1793
+
1794
+ const initSelect = (root) => {
1795
+ if (root.dataset.selectInitialized) return;
1796
+
1797
+ const state = { activeIndex: -1, selectedOptions: null, options: [], allOptions: [], visibleOptions: [], format: 'value' };
1798
+ states.set(root, state);
1799
+ root.refresh = () => refreshSelect(root);
1800
+
1801
+ refreshSelect(root);
1802
+ if (!state.trigger || !state.selectedLabel || !state.popover || !state.listbox || !state.input) {
1803
+ states.delete(root);
1804
+ delete root.refresh;
1805
+ return;
1806
+ }
1807
+
1808
+ root.open = () => {
1809
+ document.dispatchEvent(new CustomEvent('basecoat:popover', { detail: { source: root } }));
1810
+ root.refresh();
1811
+ state.popover.setAttribute('aria-hidden', 'false');
1812
+ state.trigger.setAttribute('aria-expanded', 'true');
1813
+
1814
+ const selectedOption = state.listbox.querySelector('[role="option"][aria-selected="true"]');
1815
+ if (selectedOption) {
1816
+ setActiveOption(state, state.options.indexOf(selectedOption));
1817
+ scrollOptionIntoListbox(state, selectedOption);
1818
+ }
1819
+ };
1820
+ root.close = (focusOnTrigger = true) => closePopover(state, focusOnTrigger);
1821
+ root.togglePopover = () => state.trigger.getAttribute('aria-expanded') === 'true' ? root.close() : root.open();
1822
+
1823
+ const handleTriggerKeydown = (event) => handleKeyNavigation(event, root);
1824
+ const handleTriggerClick = root.togglePopover;
1825
+ const handleListboxMousemove = (event) => {
1826
+ const option = event.target.closest('[role="option"]');
1827
+ if (option && state.visibleOptions.includes(option)) {
1828
+ const index = state.options.indexOf(option);
1829
+ if (index !== state.activeIndex) setActiveOption(state, index);
1830
+ }
1831
+ };
1832
+ const handleListboxMouseleave = () => {
1833
+ const selectedOption = state.listbox.querySelector('[role="option"][aria-selected="true"]');
1834
+ setActiveOption(state, selectedOption ? state.options.indexOf(selectedOption) : -1);
1835
+ };
1836
+ const handleListboxClick = (event) => {
1837
+ const clickedOption = event.target.closest('[role="option"]');
1838
+ if (!clickedOption) return;
1839
+
1840
+ const option = state.options.find(opt => opt === clickedOption);
1841
+ if (!option) return;
1842
+
1843
+ if (state.isMultiple) {
1844
+ toggleMultipleValue(root, option);
1845
+ if (state.closeOnSelect) {
1846
+ root.close();
1847
+ } else {
1848
+ setActiveOption(state, state.options.indexOf(option));
1849
+ state.trigger.focus();
1850
+ }
1851
+ } else {
1852
+ if (state.placeholder && getValue(option) === '') {
1853
+ updateValue(root, null);
1854
+ } else if (root.value !== getValue(option)) {
1855
+ updateValue(root, option);
1856
+ }
1857
+ root.close();
1858
+ }
1859
+ };
1860
+ const handleDocumentClick = (event) => {
1861
+ if (!root.contains(event.target)) root.close(false);
1862
+ };
1863
+ const handleDocumentPopover = (event) => {
1864
+ if (!root.contains(event.detail.source)) root.close(false);
1865
+ };
1866
+
1867
+ state.trigger.addEventListener('keydown', handleTriggerKeydown);
1868
+ state.trigger.addEventListener('click', handleTriggerClick);
1869
+ state.listbox.addEventListener('mousemove', handleListboxMousemove);
1870
+ state.listbox.addEventListener('mouseleave', handleListboxMouseleave);
1871
+ state.listbox.addEventListener('click', handleListboxClick);
1872
+ document.addEventListener('click', handleDocumentClick);
1873
+ document.addEventListener('basecoat:popover', handleDocumentPopover);
1874
+
1875
+ root._destroy = () => {
1876
+ state.trigger.removeEventListener('keydown', handleTriggerKeydown);
1877
+ state.trigger.removeEventListener('click', handleTriggerClick);
1878
+ state.listbox.removeEventListener('mousemove', handleListboxMousemove);
1879
+ state.listbox.removeEventListener('mouseleave', handleListboxMouseleave);
1880
+ state.listbox.removeEventListener('click', handleListboxClick);
1881
+ document.removeEventListener('click', handleDocumentClick);
1882
+ document.removeEventListener('basecoat:popover', handleDocumentPopover);
1883
+ states.delete(root);
1884
+ delete root.refresh;
1885
+ delete root.open;
1886
+ delete root.close;
1887
+ delete root.togglePopover;
1888
+ delete root.select;
1889
+ delete root.selectByValue;
1890
+ delete root.deselect;
1891
+ delete root.toggle;
1892
+ delete root.selectAll;
1893
+ delete root.selectNone;
1894
+ };
1895
+
1896
+ Object.defineProperty(root, 'value', {
1897
+ configurable: true,
1898
+ get: () => state.isMultiple ? state.options.filter(option => state.selectedOptions.has(option)).map(getValue) : parseStoredValues(state.input.value, state),
1899
+ set: (value) => {
1900
+ if (state.isMultiple) {
1901
+ const values = Array.isArray(value) ? value : (value != null ? [value] : []);
1902
+ updateValue(root, values.map(v => state.options.find(option => getValue(option) === v)).filter(Boolean));
1903
+ } else {
1904
+ if (value == null || value === '') {
1905
+ updateValue(root, null);
1906
+ root.close();
1907
+ return;
1908
+ }
1909
+ const option = state.options.find(opt => getValue(opt) === value);
1910
+ if (option) {
1911
+ updateValue(root, option);
1912
+ root.close();
1913
+ }
1914
+ }
1915
+ },
1916
+ });
1917
+
1918
+ Object.defineProperty(root, 'selected', {
1919
+ configurable: true,
1920
+ get: () => {
1921
+ if (state.isMultiple) return state.options.filter(option => state.selectedOptions.has(option)).map(toSelected);
1922
+ const value = root.value;
1923
+ const option = state.options.find(opt => getValue(opt) === value);
1924
+ return option ? toSelected(option) : null;
1925
+ },
1926
+ });
1927
+
1928
+ root.select = (value) => selectValue(root, value);
1929
+ root.selectByValue = root.select;
1930
+ if (state.isMultiple) {
1931
+ root.deselect = (value) => deselectValue(root, value);
1932
+ root.toggle = (value) => {
1933
+ const option = state.options.find(opt => getValue(opt) === value);
1934
+ if (option) toggleMultipleValue(root, option);
1935
+ };
1936
+ root.selectAll = () => updateValue(root, state.options);
1937
+ root.selectNone = () => updateValue(root, []);
1938
+ }
1939
+
1940
+ state.popover.setAttribute('aria-hidden', 'true');
1941
+ state.trigger.setAttribute('aria-expanded', 'false');
1942
+ root.dataset.selectInitialized = 'true';
1943
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
1944
+ };
1945
+
1946
+ if (window.basecoat) {
1947
+ window.basecoat.register('select', {
1948
+ selector: 'div.select:not([data-select-initialized])',
1949
+ init: initSelect,
1950
+ refresh: refreshSelect,
1951
+ });
1952
+ }
1953
+ })();
1954
+
1955
+ (() => {
1956
+ const initSidebar = (sidebarComponent) => {
1957
+ if (sidebarComponent.dataset.sidebarInitialized && typeof sidebarComponent.toggle === 'function') return;
1958
+
1959
+ // In-flow sidebars (inside a dialog, popover or layout cell) have no open state.
1960
+ if (sidebarComponent.dataset.position === 'static') {
1961
+ sidebarComponent.dataset.sidebarInitialized = 'true';
1962
+ return;
1963
+ }
1964
+
1965
+ const initialOpen = sidebarComponent.dataset.initialOpen !== 'false';
1966
+ const initialMobileOpen = sidebarComponent.dataset.initialMobileOpen === 'true';
1967
+ const breakpoint = parseInt(sidebarComponent.dataset.breakpoint) || 768;
1968
+
1969
+ let open = breakpoint > 0
1970
+ ? (window.innerWidth >= breakpoint ? initialOpen : initialMobileOpen)
1971
+ : initialOpen;
1972
+
1973
+ // Collapsing to an icon rail keeps the sidebar usable on desktop.
1974
+ const isIconRail = () => sidebarComponent.dataset.collapsible === 'icon' && (breakpoint <= 0 || window.innerWidth >= breakpoint);
1975
+
1976
+ const updateState = () => {
1977
+ const rail = isIconRail();
1978
+ // An icon rail stays visible to assistive tech; only a fully hidden sidebar is aria-hidden.
1979
+ sidebarComponent.setAttribute('aria-hidden', String(!open && !rail));
1980
+ sidebarComponent.dataset.state = open ? 'open' : 'collapsed';
1981
+ if (open || rail) {
1982
+ sidebarComponent.removeAttribute('inert');
1983
+ } else {
1984
+ sidebarComponent.setAttribute('inert', '');
1985
+ }
1986
+ if (sidebarComponent.id) {
1987
+ document.querySelectorAll(`[data-sidebar-toggle="${CSS.escape(sidebarComponent.id)}"]`).forEach((btn) => btn.setAttribute('aria-expanded', String(open)));
1988
+ }
1989
+ sidebarComponent.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { open } }));
1990
+ };
1991
+
1992
+ const setState = (state) => {
1993
+ open = Boolean(state);
1994
+ updateState();
1995
+ };
1996
+
1997
+ sidebarComponent.open = () => setState(true);
1998
+ sidebarComponent.close = () => setState(false);
1999
+ sidebarComponent.toggle = () => setState(!open);
2000
+
2001
+ const handleClick = (event) => {
2002
+ const target = event.target;
2003
+ const nav = sidebarComponent.querySelector(':scope > nav');
2004
+ const isMobile = window.innerWidth < breakpoint;
2005
+
2006
+ if (isMobile && target.closest('a, button') && !target.closest('[data-keep-mobile-sidebar-open]')) {
2007
+ if (document.activeElement) document.activeElement.blur();
2008
+ sidebarComponent.close();
2009
+ return;
2010
+ }
2011
+
2012
+ // Collapsed icon rail: opening a submenu expands the sidebar first.
2013
+ if (!open && isIconRail() && target.closest('summary')) {
2014
+ event.preventDefault();
2015
+ sidebarComponent.open();
2016
+ target.closest('details').open = true;
2017
+ return;
2018
+ }
2019
+
2020
+ if (target === sidebarComponent || (nav && !nav.contains(target))) {
2021
+ if (document.activeElement) document.activeElement.blur();
2022
+ sidebarComponent.close();
2023
+ }
2024
+ };
2025
+
2026
+ sidebarComponent.addEventListener('click', handleClick);
2027
+
2028
+ sidebarComponent._destroy = () => {
2029
+ sidebarComponent.removeEventListener('click', handleClick);
2030
+ delete sidebarComponent.open;
2031
+ delete sidebarComponent.close;
2032
+ delete sidebarComponent.toggle;
2033
+ };
2034
+
2035
+ updateState();
2036
+ sidebarComponent.dataset.sidebarInitialized = 'true';
2037
+ sidebarComponent.dispatchEvent(new CustomEvent('basecoat:initialized'));
2038
+ };
2039
+
2040
+ // Declarative toggles: <button data-sidebar-toggle="sidebar-id"> (empty value = first sidebar).
2041
+ const sidebarFor = (id) => (id ? document.getElementById(id) : document.querySelector('.sidebar:not([data-position="static"])'));
2042
+ document.addEventListener('click', (event) => {
2043
+ const button = event.target.closest('[data-sidebar-toggle]');
2044
+ if (!button) return;
2045
+ sidebarFor(button.getAttribute('data-sidebar-toggle'))?.toggle?.();
2046
+ });
2047
+ // Ctrl/Cmd+B toggles the first sidebar (opt out with data-shortcut="none").
2048
+ document.addEventListener('keydown', (event) => {
2049
+ if (event.key.toLowerCase() !== 'b' || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
2050
+ if (event.target.closest('input, textarea, select, [contenteditable]')) return;
2051
+ const sidebar = sidebarFor('');
2052
+ if (!sidebar || sidebar.dataset.shortcut === 'none' || typeof sidebar.toggle !== 'function') return;
2053
+ event.preventDefault();
2054
+ sidebar.toggle();
2055
+ });
2056
+
2057
+ if (window.basecoat) {
2058
+ window.basecoat.register('sidebar', '.sidebar', initSidebar);
2059
+ }
2060
+ })();
2061
+
2062
+ (() => {
2063
+ const states = new WeakMap();
2064
+
2065
+ const isDisabled = (tab) => tab.disabled || tab.getAttribute('aria-disabled') === 'true';
2066
+
2067
+ const getElements = (root) => {
2068
+ const tablist = root.querySelector('[role="tablist"]');
2069
+ const tabs = tablist ? Array.from(tablist.querySelectorAll('[role="tab"]')) : [];
2070
+ const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls'))).filter(Boolean);
2071
+ return { tablist, tabs, panels };
2072
+ };
2073
+
2074
+ const refreshTabs = (root) => {
2075
+ const state = states.get(root);
2076
+ if (!state) return;
2077
+
2078
+ Object.assign(state, getElements(root));
2079
+ if (!state.tablist) return;
2080
+
2081
+ const selected = state.tabs.find(tab => tab.getAttribute('aria-selected') === 'true' && !isDisabled(tab))
2082
+ || state.tabs.find(tab => !isDisabled(tab));
2083
+ if (selected) root.select(selected, false);
2084
+ };
2085
+
2086
+ const initTabs = (root) => {
2087
+ if (root.dataset.tabsInitialized) return;
2088
+
2089
+ const state = {};
2090
+ states.set(root, state);
2091
+ root.refresh = () => refreshTabs(root);
2092
+
2093
+ const selectTab = (tabToSelect, focus = false) => {
2094
+ if (!tabToSelect || isDisabled(tabToSelect)) return;
2095
+
2096
+ state.tabs.forEach((tab) => {
2097
+ tab.setAttribute('aria-selected', 'false');
2098
+ tab.setAttribute('tabindex', '-1');
2099
+ const panel = document.getElementById(tab.getAttribute('aria-controls'));
2100
+ if (panel) panel.hidden = true;
2101
+ });
2102
+
2103
+ tabToSelect.setAttribute('aria-selected', 'true');
2104
+ tabToSelect.setAttribute('tabindex', '0');
2105
+ const activePanel = document.getElementById(tabToSelect.getAttribute('aria-controls'));
2106
+ if (activePanel) activePanel.hidden = false;
2107
+ if (focus) tabToSelect.focus();
2108
+ };
2109
+
2110
+ root.select = selectTab;
2111
+ refreshTabs(root);
2112
+ if (!state.tablist) {
2113
+ states.delete(root);
2114
+ delete root.refresh;
2115
+ delete root.select;
2116
+ return;
2117
+ }
2118
+
2119
+ const handleClick = (event) => {
2120
+ const clickedTab = event.target.closest('[role="tab"]');
2121
+ if (clickedTab) root.select(clickedTab);
2122
+ };
2123
+
2124
+ const handleKeydown = (event) => {
2125
+ const currentTab = event.target;
2126
+ if (!state.tabs.includes(currentTab)) return;
2127
+
2128
+ const enabledTabs = state.tabs.filter(tab => !isDisabled(tab));
2129
+ const currentIndex = enabledTabs.indexOf(currentTab);
2130
+ const orientation = state.tablist.getAttribute('aria-orientation') || 'horizontal';
2131
+ if (currentIndex === -1) return;
2132
+
2133
+ let nextTab;
2134
+ if (event.key === 'ArrowRight' && orientation === 'horizontal') nextTab = enabledTabs[(currentIndex + 1) % enabledTabs.length];
2135
+ if (event.key === 'ArrowLeft' && orientation === 'horizontal') nextTab = enabledTabs[(currentIndex - 1 + enabledTabs.length) % enabledTabs.length];
2136
+ if (event.key === 'ArrowDown' && orientation === 'vertical') nextTab = enabledTabs[(currentIndex + 1) % enabledTabs.length];
2137
+ if (event.key === 'ArrowUp' && orientation === 'vertical') nextTab = enabledTabs[(currentIndex - 1 + enabledTabs.length) % enabledTabs.length];
2138
+ if (event.key === 'Home') nextTab = enabledTabs[0];
2139
+ if (event.key === 'End') nextTab = enabledTabs[enabledTabs.length - 1];
2140
+ if (!nextTab) return;
2141
+
2142
+ event.preventDefault();
2143
+ root.select(nextTab, true);
2144
+ };
2145
+
2146
+ state.tablist.addEventListener('click', handleClick);
2147
+ state.tablist.addEventListener('keydown', handleKeydown);
2148
+
2149
+ root._destroy = () => {
2150
+ state.tablist.removeEventListener('click', handleClick);
2151
+ state.tablist.removeEventListener('keydown', handleKeydown);
2152
+ states.delete(root);
2153
+ delete root.refresh;
2154
+ delete root.select;
2155
+ };
2156
+
2157
+ root.dataset.tabsInitialized = 'true';
2158
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
2159
+ };
2160
+
2161
+ if (window.basecoat) {
2162
+ window.basecoat.register('tabs', {
2163
+ selector: '.tabs:not([data-tabs-initialized])',
2164
+ init: initTabs,
2165
+ refresh: refreshTabs,
2166
+ });
2167
+ }
2168
+ })();
2169
+
2170
+ (() => {
2171
+ let toaster;
2172
+ const toasts = new WeakMap();
2173
+ let isPaused = false;
2174
+ const ICONS = {
2175
+ success: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
2176
+ error: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
2177
+ info: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
2178
+ warning: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
2179
+ };
2180
+
2181
+ function initToaster(toasterElement) {
2182
+ if (toasterElement.dataset.toasterInitialized) return;
2183
+ toaster = toasterElement;
2184
+
2185
+ const handleClick = (event) => {
2186
+ const actionLink = event.target.closest('.toast footer a');
2187
+ const actionButton = event.target.closest('.toast footer button');
2188
+ if (actionLink || actionButton) {
2189
+ closeToast(event.target.closest('.toast'));
2190
+ }
2191
+ };
2192
+
2193
+ toaster.addEventListener('mouseenter', pauseAllTimeouts);
2194
+ toaster.addEventListener('mouseleave', resumeAllTimeouts);
2195
+ toaster.addEventListener('click', handleClick);
2196
+
2197
+ toasterElement.toast = (config = {}) => {
2198
+ const toastElement = createToast(config);
2199
+ toasterElement.appendChild(toastElement);
2200
+ initToast(toastElement);
2201
+ return toastElement;
2202
+ };
2203
+ toasterElement.closeAll = () => {
2204
+ toasterElement.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(closeToast);
2205
+ };
2206
+
2207
+ toaster.querySelectorAll('.toast:not([data-toast-initialized])').forEach(initToast);
2208
+ toaster._destroy = () => {
2209
+ toaster.removeEventListener('mouseenter', pauseAllTimeouts);
2210
+ toaster.removeEventListener('mouseleave', resumeAllTimeouts);
2211
+ toaster.removeEventListener('click', handleClick);
2212
+ toaster.querySelectorAll('.toast[data-toast-initialized]').forEach(toast => toast._destroy?.());
2213
+ delete toaster.toast;
2214
+ delete toaster.closeAll;
2215
+ if (toaster === toasterElement) toaster = null;
2216
+ };
2217
+ toaster.dataset.toasterInitialized = 'true';
2218
+ toaster.dispatchEvent(new CustomEvent('basecoat:initialized'));
2219
+ }
2220
+
2221
+ function initToast(element) {
2222
+ if (element.dataset.toastInitialized) return;
2223
+
2224
+ const duration = parseInt(element.dataset.duration);
2225
+ const timeoutDuration = duration !== -1
2226
+ ? duration || (element.dataset.category === 'error' ? 5000 : 3000)
2227
+ : -1;
2228
+
2229
+ const state = {
2230
+ remainingTime: timeoutDuration,
2231
+ timeoutId: null,
2232
+ startTime: null,
2233
+ };
2234
+
2235
+ if (timeoutDuration !== -1) {
2236
+ if (isPaused) {
2237
+ state.timeoutId = null;
2238
+ } else {
2239
+ state.startTime = Date.now();
2240
+ state.timeoutId = setTimeout(() => closeToast(element), timeoutDuration);
2241
+ }
2242
+ }
2243
+ toasts.set(element, state);
2244
+
2245
+ element.close = () => closeToast(element);
2246
+ element._destroy = () => {
2247
+ clearTimeout(state.timeoutId);
2248
+ toasts.delete(element);
2249
+ delete element.close;
2250
+ };
2251
+ element.dataset.toastInitialized = 'true';
2252
+ }
2253
+
2254
+ function pauseAllTimeouts() {
2255
+ if (isPaused || !toaster) return;
2256
+
2257
+ isPaused = true;
2258
+
2259
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
2260
+ if (!toasts.has(element)) return;
2261
+
2262
+ const state = toasts.get(element);
2263
+ if (state.timeoutId) {
2264
+ clearTimeout(state.timeoutId);
2265
+ state.timeoutId = null;
2266
+ state.remainingTime -= Date.now() - state.startTime;
2267
+ }
2268
+ });
2269
+ }
2270
+
2271
+ function resumeAllTimeouts() {
2272
+ if (!isPaused || !toaster) return;
2273
+
2274
+ isPaused = false;
2275
+
2276
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
2277
+ if (!toasts.has(element)) return;
2278
+
2279
+ const state = toasts.get(element);
2280
+ if (state.remainingTime !== -1 && !state.timeoutId) {
2281
+ if (state.remainingTime > 0) {
2282
+ state.startTime = Date.now();
2283
+ state.timeoutId = setTimeout(() => closeToast(element), state.remainingTime);
2284
+ } else {
2285
+ closeToast(element);
2286
+ }
2287
+ }
2288
+ });
2289
+ }
2290
+
2291
+ function closeToast(element) {
2292
+ if (!element || !toasts.has(element)) return;
2293
+
2294
+ const state = toasts.get(element);
2295
+ clearTimeout(state.timeoutId);
2296
+ toasts.delete(element);
2297
+
2298
+ if (element.contains(document.activeElement)) document.activeElement.blur();
2299
+ element.setAttribute('aria-hidden', 'true');
2300
+ element.addEventListener('transitionend', () => element.remove(), { once: true });
2301
+ }
2302
+
2303
+ const escapeHtml = (value) => String(value)
2304
+ .replace(/&/g, '&amp;')
2305
+ .replace(/</g, '&lt;')
2306
+ .replace(/>/g, '&gt;')
2307
+ .replace(/"/g, '&quot;')
2308
+ .replace(/'/g, '&#39;');
2309
+
2310
+ function createToast(config) {
2311
+ const {
2312
+ category = 'info',
2313
+ title,
2314
+ description,
2315
+ action,
2316
+ cancel,
2317
+ duration,
2318
+ icon,
2319
+ html: allowHtml = false,
2320
+ } = config;
2321
+
2322
+ // Text is escaped unless `html: true`. `icon` is always raw markup (SVG).
2323
+ const text = (value) => (allowHtml ? String(value) : escapeHtml(value));
2324
+
2325
+ const iconHtml = icon || (category && ICONS[category]) || '';
2326
+ const titleHtml = title ? `<h2>${text(title)}</h2>` : '';
2327
+ const descriptionHtml = description ? `<p>${text(description)}</p>` : '';
2328
+ const actionHtml = action?.href
2329
+ ? `<a href="${escapeHtml(action.href)}" class="btn" data-toast-action>${text(action.label)}</a>`
2330
+ : action?.onclick
2331
+ ? `<button type="button" class="btn" data-toast-action onclick="${escapeHtml(action.onclick)}">${text(action.label)}</button>`
2332
+ : '';
2333
+ const cancelHtml = cancel
2334
+ ? `<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" data-variant="outline" data-toast-cancel onclick="${escapeHtml(cancel?.onclick || '')}">${text(cancel.label)}</button>`
2335
+ : '';
2336
+
2337
+ const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
2338
+
2339
+ const html = `
2340
+ <div
2341
+ class="toast"
2342
+ role="${category === 'error' ? 'alert' : 'status'}"
2343
+ aria-atomic="true"
2344
+ ${category ? `data-category="${escapeHtml(category)}"` : ''}
2345
+ ${duration !== undefined ? `data-duration="${escapeHtml(duration)}"` : ''}
2346
+ >
2347
+ <div class="toast-content">
2348
+ ${iconHtml}
2349
+ <section>
2350
+ ${titleHtml}
2351
+ ${descriptionHtml}
2352
+ </section>
2353
+ ${footerHtml}
2354
+ </div>
2355
+ </div>
2356
+ `;
2357
+ const template = document.createElement('template');
2358
+ template.innerHTML = html.trim();
2359
+ return template.content.firstChild;
2360
+ }
2361
+
2362
+ if (window.basecoat) {
2363
+ window.basecoat.register('toaster', '#toaster:not([data-toaster-initialized])', initToaster);
2364
+ window.basecoat.register('toast', '.toast:not([data-toast-initialized])', initToast);
2365
+ }
2366
+ })();
2367
+
2368
+ (() => {
2369
+ // ../elements/src/menu.js
2370
+ (() => {
2371
+ if (window.__auvyxMenu) return;
2372
+ window.__auvyxMenu = true;
2373
+ const ITEM = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
2374
+ const MENU_POPOVER = "[data-popover][popover]";
2375
+ const isUsable = (el) => !el.disabled && el.getAttribute("aria-disabled") !== "true" && !el.closest("[hidden]");
2376
+ const itemsOf = (popover) => [...popover.querySelectorAll(ITEM)].filter(isUsable);
2377
+ const triggerOf = (popover) => popover.id ? document.querySelector(`[popovertarget="${CSS.escape(popover.id)}"]`) : null;
2378
+ const popoverOf = (trigger) => {
2379
+ const id = trigger?.getAttribute("popovertarget");
2380
+ return id ? document.getElementById(id) : null;
2381
+ };
2382
+ const isMenuPopover = (el) => el instanceof HTMLElement && el.matches(MENU_POPOVER) && el.querySelector('[role="menu"]');
2383
+ const menubarOf = (el) => el?.closest(".menubar");
2384
+ const menubarTriggers = (bar) => [...bar.querySelectorAll(":scope > .dropdown-menu > [popovertarget]")];
2385
+ const focusItem = (item) => item?.focus({ preventScroll: true });
2386
+ const open = (trigger, focus = "first") => {
2387
+ const popover = popoverOf(trigger);
2388
+ if (!popover) return;
2389
+ popover.dataset.focusOnOpen = focus;
2390
+ if (!popover.matches(":popover-open")) popover.showPopover({ source: trigger });
2391
+ else focusItem(focus === "last" ? itemsOf(popover).at(-1) : itemsOf(popover)[0]);
2392
+ };
2393
+ document.addEventListener("toggle", (event) => {
2394
+ const popover = event.target;
2395
+ if (!(popover instanceof HTMLElement) || !popover.matches(MENU_POPOVER)) return;
2396
+ const trigger = triggerOf(popover);
2397
+ if (trigger && trigger.hasAttribute("aria-haspopup")) trigger.setAttribute("aria-expanded", String(event.newState === "open"));
2398
+ if (event.newState !== "open" || !isMenuPopover(popover)) return;
2399
+ const where = popover.dataset.focusOnOpen || "first";
2400
+ delete popover.dataset.focusOnOpen;
2401
+ if (where === "none") return;
2402
+ requestAnimationFrame(() => {
2403
+ const items = itemsOf(popover);
2404
+ focusItem(where === "last" ? items.at(-1) : items[0]);
2405
+ });
2406
+ }, true);
2407
+ document.addEventListener("keydown", (event) => {
2408
+ const target = event.target;
2409
+ if (!(target instanceof Element)) return;
2410
+ const popover = target.closest(MENU_POPOVER);
2411
+ if (popover && isMenuPopover(popover) && target.matches(ITEM)) {
2412
+ const items = itemsOf(popover);
2413
+ const index = items.indexOf(target);
2414
+ const move = { ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: items.length - 1 }[event.key];
2415
+ if (move !== void 0) {
2416
+ event.preventDefault();
2417
+ focusItem(items[(move + items.length) % items.length]);
2418
+ return;
2419
+ }
2420
+ if (event.key === "Tab") {
2421
+ popover.hidePopover();
2422
+ return;
2423
+ }
2424
+ if (event.key === "Escape") {
2425
+ event.preventDefault();
2426
+ popover.hidePopover();
2427
+ triggerOf(popover)?.focus({ preventScroll: true });
2428
+ return;
2429
+ }
2430
+ const bar = menubarOf(triggerOf(popover));
2431
+ if (bar && (event.key === "ArrowRight" || event.key === "ArrowLeft")) {
2432
+ event.preventDefault();
2433
+ const triggers = menubarTriggers(bar);
2434
+ const at = triggers.indexOf(triggerOf(popover));
2435
+ const rtl = getComputedStyle(bar).direction === "rtl";
2436
+ const step = event.key === "ArrowRight" !== rtl ? 1 : -1;
2437
+ const next = triggers[(at + step + triggers.length) % triggers.length];
2438
+ popover.hidePopover();
2439
+ open(next);
2440
+ return;
2441
+ }
2442
+ return;
2443
+ }
2444
+ if (target.matches("[popovertarget]")) {
2445
+ const menu = popoverOf(target);
2446
+ const bar = menubarOf(target);
2447
+ if (menu && isMenuPopover(menu) && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
2448
+ event.preventDefault();
2449
+ open(target, event.key === "ArrowUp" ? "last" : "first");
2450
+ return;
2451
+ }
2452
+ if (bar && (event.key === "ArrowRight" || event.key === "ArrowLeft" || event.key === "Home" || event.key === "End")) {
2453
+ event.preventDefault();
2454
+ const triggers = menubarTriggers(bar);
2455
+ const at = triggers.indexOf(target);
2456
+ const rtl = getComputedStyle(bar).direction === "rtl";
2457
+ const step = event.key === "ArrowRight" !== rtl ? 1 : -1;
2458
+ const next = event.key === "Home" ? triggers[0] : event.key === "End" ? triggers.at(-1) : triggers[(at + step + triggers.length) % triggers.length];
2459
+ next.focus();
2460
+ }
2461
+ }
2462
+ });
2463
+ document.addEventListener("click", (event) => {
2464
+ const item = event.target instanceof Element && event.target.closest(ITEM);
2465
+ if (!item || !isUsable(item)) return;
2466
+ const popover = item.closest(MENU_POPOVER);
2467
+ if (!popover) return;
2468
+ const role = item.getAttribute("role");
2469
+ if (role === "menuitemcheckbox") {
2470
+ item.setAttribute("aria-checked", String(item.getAttribute("aria-checked") !== "true"));
2471
+ } else if (role === "menuitemradio") {
2472
+ const group = item.closest('[role="group"]') || popover;
2473
+ group.querySelectorAll('[role="menuitemradio"]').forEach((radio) => radio.setAttribute("aria-checked", String(radio === item)));
2474
+ }
2475
+ if (item.hasAttribute("popovertarget") || item.hasAttribute("data-keep-open")) return;
2476
+ if (role !== "menuitem" && !item.hasAttribute("data-close")) return;
2477
+ popover.hidePopover();
2478
+ triggerOf(popover)?.focus({ preventScroll: true });
2479
+ });
2480
+ document.addEventListener("pointerover", (event) => {
2481
+ const trigger = event.target instanceof Element && event.target.closest(".menubar > .dropdown-menu > [popovertarget]");
2482
+ if (!trigger) return;
2483
+ const bar = menubarOf(trigger);
2484
+ const openMenu = menubarTriggers(bar).map(popoverOf).find((p) => p?.matches(":popover-open"));
2485
+ if (!openMenu || openMenu === popoverOf(trigger)) return;
2486
+ openMenu.hidePopover();
2487
+ open(trigger, "none");
2488
+ });
2489
+ const navTimers = /* @__PURE__ */ new WeakMap();
2490
+ const clearNav = (li) => clearTimeout(navTimers.get(li));
2491
+ document.addEventListener("pointerover", (event) => {
2492
+ if (event.pointerType !== "mouse") return;
2493
+ const li = event.target instanceof Element && event.target.closest(".navigation-menu > ul > li");
2494
+ if (!li) return;
2495
+ clearNav(li);
2496
+ const trigger = li.querySelector(":scope > [popovertarget]");
2497
+ const panel = popoverOf(trigger);
2498
+ if (!panel || panel.matches(":popover-open")) return;
2499
+ navTimers.set(li, setTimeout(() => {
2500
+ panel.dataset.openedByHover = String(Date.now());
2501
+ panel.showPopover({ source: trigger });
2502
+ }, 120));
2503
+ });
2504
+ document.addEventListener("pointerout", (event) => {
2505
+ if (event.pointerType !== "mouse") return;
2506
+ const li = event.target instanceof Element && event.target.closest(".navigation-menu > ul > li");
2507
+ if (!li || li.contains(event.relatedTarget)) return;
2508
+ clearNav(li);
2509
+ const panel = popoverOf(li.querySelector(":scope > [popovertarget]"));
2510
+ if (!panel || !panel.dataset.openedByHover) return;
2511
+ navTimers.set(li, setTimeout(() => {
2512
+ if (panel.matches(":popover-open") && !li.matches(":hover")) panel.hidePopover();
2513
+ delete panel.dataset.openedByHover;
2514
+ }, 200));
2515
+ });
2516
+ document.addEventListener("click", (event) => {
2517
+ const trigger = event.target instanceof Element && event.target.closest(".navigation-menu > ul > li > [popovertarget]");
2518
+ const panel = popoverOf(trigger);
2519
+ if (!panel?.dataset.openedByHover) return;
2520
+ if (Date.now() - Number(panel.dataset.openedByHover) < 600 && panel.matches(":popover-open")) {
2521
+ event.preventDefault();
2522
+ return;
2523
+ }
2524
+ delete panel.dataset.openedByHover;
2525
+ }, true);
2526
+ })();
2527
+
2528
+ // ../elements/src/context-menu.js
2529
+ var AuvyxContextMenu = class extends HTMLElement {
2530
+ connectedCallback() {
2531
+ if (this._init) return;
2532
+ this._init = true;
2533
+ this.menu = this.querySelector("[data-popover][popover]");
2534
+ if (!this.menu) return;
2535
+ let pressed = false;
2536
+ this.addEventListener("pointerdown", (e) => {
2537
+ if (e.button === 2) pressed = true;
2538
+ });
2539
+ this.addEventListener("contextmenu", (event) => {
2540
+ if (this.menu.contains(event.target)) return;
2541
+ event.preventDefault();
2542
+ const { clientX: x, clientY: y } = event;
2543
+ if (!pressed) return this.openAt(x, y);
2544
+ window.addEventListener("pointerup", () => {
2545
+ pressed = false;
2546
+ setTimeout(() => this.openAt(x, y));
2547
+ }, { once: true, capture: true });
2548
+ });
2549
+ this.addEventListener("keydown", (event) => {
2550
+ if (this.menu.contains(event.target)) return;
2551
+ if (event.key === "ContextMenu" || event.shiftKey && event.key === "F10") {
2552
+ event.preventDefault();
2553
+ const r = event.target.getBoundingClientRect();
2554
+ this.openAt(r.left, r.bottom);
2555
+ }
2556
+ });
2557
+ }
2558
+ openAt(x, y) {
2559
+ const menu = this.menu;
2560
+ if (menu.matches(":popover-open")) menu.hidePopover();
2561
+ Object.assign(menu.style, {
2562
+ position: "fixed",
2563
+ inset: "auto",
2564
+ margin: "0",
2565
+ positionArea: "none",
2566
+ positionTryFallbacks: "none",
2567
+ left: `${x}px`,
2568
+ top: `${y}px`
2569
+ });
2570
+ menu.showPopover();
2571
+ const r = menu.getBoundingClientRect();
2572
+ if (r.right > innerWidth - 4) menu.style.left = `${Math.max(4, x - r.width)}px`;
2573
+ if (r.bottom > innerHeight - 4) menu.style.top = `${Math.max(4, y - r.height)}px`;
2574
+ }
2575
+ };
2576
+ if (!customElements.get("auvyx-context-menu")) customElements.define("auvyx-context-menu", AuvyxContextMenu);
2577
+
2578
+ // ../elements/src/resizable.js
2579
+ var AuvyxResizable = class extends HTMLElement {
2580
+ connectedCallback() {
2581
+ if (this._init) return;
2582
+ this._init = true;
2583
+ this.vertical = this.dataset.orientation === "vertical";
2584
+ this.panels = [...this.children].filter((el) => el.getAttribute("role") !== "separator");
2585
+ this.handles = [...this.children].filter((el) => el.getAttribute("role") === "separator");
2586
+ if (this.panels.length < 2) return;
2587
+ const given = this.panels.map((p) => parseFloat(getComputedStyle(p).getPropertyValue("--size")) || 0);
2588
+ const missing = given.filter((v) => !v).length;
2589
+ const rest = Math.max(0, 100 - given.reduce((a, b) => a + b, 0));
2590
+ this.sizes = given.map((v) => v || (missing ? rest / missing : 0));
2591
+ const sum = this.sizes.reduce((a, b) => a + b, 0) || 1;
2592
+ this.sizes = this.sizes.map((v) => v / sum * 100);
2593
+ this.apply();
2594
+ this.handles.forEach((handle, i) => {
2595
+ handle.setAttribute("aria-orientation", this.vertical ? "horizontal" : "vertical");
2596
+ handle.setAttribute("aria-valuemin", "0");
2597
+ handle.setAttribute("aria-valuemax", "100");
2598
+ if (this.panels[i].id) handle.setAttribute("aria-controls", this.panels[i].id);
2599
+ if (!handle.hasAttribute("tabindex")) handle.tabIndex = 0;
2600
+ handle.addEventListener("pointerdown", (e) => this.startDrag(e, i));
2601
+ handle.addEventListener("keydown", (e) => this.onKey(e, i));
2602
+ handle.addEventListener("dblclick", () => this.resetPair(i));
2603
+ });
2604
+ }
2605
+ minOf(i) {
2606
+ const v = parseFloat(this.panels[i].dataset.min);
2607
+ return Number.isFinite(v) ? v : 10;
2608
+ }
2609
+ maxOf(i) {
2610
+ const v = parseFloat(this.panels[i].dataset.max);
2611
+ return Number.isFinite(v) ? v : 100;
2612
+ }
2613
+ // Move the boundary after panel i so panel i gets `target` percent.
2614
+ setPair(i, target) {
2615
+ const pair = this.sizes[i] + this.sizes[i + 1];
2616
+ const lo = Math.max(this.minOf(i), pair - this.maxOf(i + 1));
2617
+ const hi = Math.min(this.maxOf(i), pair - this.minOf(i + 1));
2618
+ const next = Math.min(hi, Math.max(lo, target));
2619
+ this.sizes[i] = next;
2620
+ this.sizes[i + 1] = pair - next;
2621
+ this.apply();
2622
+ }
2623
+ resetPair(i) {
2624
+ const pair = this.sizes[i] + this.sizes[i + 1];
2625
+ this.setPair(i, pair / 2);
2626
+ }
2627
+ apply() {
2628
+ this.panels.forEach((p, i) => p.style.setProperty("--size", this.sizes[i].toFixed(3)));
2629
+ this.handles?.forEach((h, i) => h.setAttribute("aria-valuenow", String(Math.round(this.sizes[i]))));
2630
+ this.dispatchEvent(new CustomEvent("resize", { detail: { sizes: [...this.sizes] } }));
2631
+ }
2632
+ startDrag(event, i) {
2633
+ if (event.button !== 0) return;
2634
+ event.preventDefault();
2635
+ const handle = event.currentTarget;
2636
+ handle.setPointerCapture(event.pointerId);
2637
+ handle.setAttribute("data-active", "");
2638
+ this.setAttribute("data-dragging", "");
2639
+ const rect = this.getBoundingClientRect();
2640
+ const total = this.vertical ? rect.height : rect.width;
2641
+ const rtl = !this.vertical && getComputedStyle(this).direction === "rtl";
2642
+ const start = this.vertical ? event.clientY : event.clientX;
2643
+ const initial = this.sizes[i];
2644
+ const move = (e) => {
2645
+ const delta = ((this.vertical ? e.clientY : e.clientX) - start) / total * 100;
2646
+ this.setPair(i, initial + (rtl ? -delta : delta));
2647
+ };
2648
+ const end = () => {
2649
+ handle.removeEventListener("pointermove", move);
2650
+ handle.removeAttribute("data-active");
2651
+ this.removeAttribute("data-dragging");
2652
+ };
2653
+ handle.addEventListener("pointermove", move);
2654
+ handle.addEventListener("pointerup", end, { once: true });
2655
+ handle.addEventListener("pointercancel", end, { once: true });
2656
+ }
2657
+ onKey(event, i) {
2658
+ const step = event.shiftKey ? 10 : 5;
2659
+ const rtl = !this.vertical && getComputedStyle(this).direction === "rtl";
2660
+ const grow = this.vertical ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
2661
+ const shrink = this.vertical ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
2662
+ const map = { [grow]: this.sizes[i] + step, [shrink]: this.sizes[i] - step, Home: 0, End: 100, Enter: null };
2663
+ if (!(event.key in map)) return;
2664
+ event.preventDefault();
2665
+ if (event.key === "Enter") this.resetPair(i);
2666
+ else this.setPair(i, map[event.key]);
2667
+ }
2668
+ };
2669
+ if (!customElements.get("auvyx-resizable")) customElements.define("auvyx-resizable", AuvyxResizable);
2670
+
2671
+ // ../elements/src/data-table.js
2672
+ var AuvyxDataTable = class extends HTMLElement {
2673
+ connectedCallback() {
2674
+ if (this._init) return;
2675
+ this._init = true;
2676
+ this.table = this.querySelector("table");
2677
+ this.tbody = this.table?.tBodies[0];
2678
+ if (!this.tbody) return;
2679
+ this.rows = [...this.tbody.rows].filter((r) => !r.hasAttribute("data-empty"));
2680
+ this.empty = this.tbody.querySelector("tr[data-empty]");
2681
+ this.pageSize = parseInt(this.dataset.pageSize, 10) || 0;
2682
+ this.page = 0;
2683
+ this.query = "";
2684
+ this.sort = { col: -1, dir: "ascending" };
2685
+ this.collator = new Intl.Collator(this.closest("[lang]")?.lang || void 0, { numeric: true, sensitivity: "base" });
2686
+ this.headers = [...this.table.tHead.rows[0].cells];
2687
+ this.headers.forEach((th, col) => {
2688
+ const button = th.hasAttribute("aria-sort") ? th.querySelector(":scope > button") : null;
2689
+ button?.addEventListener("click", () => this.toggleSort(col));
2690
+ });
2691
+ this.filterInput = this.querySelector("input[data-filter]");
2692
+ this.filterInput?.addEventListener("input", () => {
2693
+ this.query = this.filterInput.value.trim().toLocaleLowerCase();
2694
+ this.page = 0;
2695
+ this.render();
2696
+ });
2697
+ this.selectAll = this.querySelector("input[data-select-all]");
2698
+ this.selectAll?.addEventListener("change", () => {
2699
+ this.visible().forEach((row) => this.setSelected(row, this.selectAll.checked));
2700
+ this.updateSelection();
2701
+ });
2702
+ this.tbody.addEventListener("change", (e) => {
2703
+ if (!e.target.matches("input[data-select]")) return;
2704
+ this.setSelected(e.target.closest("tr"), e.target.checked);
2705
+ this.updateSelection();
2706
+ });
2707
+ this.querySelectorAll("input[data-column]").forEach((toggle) => {
2708
+ toggle.addEventListener("change", () => this.setColumn(parseInt(toggle.dataset.column, 10), toggle.checked));
2709
+ if (!toggle.checked) this.setColumn(parseInt(toggle.dataset.column, 10), false);
2710
+ });
2711
+ this.querySelector('[data-page="prev"]')?.addEventListener("click", () => {
2712
+ this.page--;
2713
+ this.render();
2714
+ });
2715
+ this.querySelector('[data-page="next"]')?.addEventListener("click", () => {
2716
+ this.page++;
2717
+ this.render();
2718
+ });
2719
+ this.rows.forEach((row) => {
2720
+ const box = row.querySelector("input[data-select]");
2721
+ if (box?.checked) row.setAttribute("aria-selected", "true");
2722
+ });
2723
+ this.render();
2724
+ }
2725
+ cellValue(row, col) {
2726
+ const cell = row.cells[col];
2727
+ return cell ? cell.dataset.value ?? cell.textContent.trim() : "";
2728
+ }
2729
+ matches(row) {
2730
+ if (!this.query) return true;
2731
+ const only = this.filterInput?.dataset.filter;
2732
+ const cols = only ? only.split(",").map(Number) : null;
2733
+ return [...row.cells].some((cell, i) => (!cols || cols.includes(i)) && cell.textContent.toLocaleLowerCase().includes(this.query));
2734
+ }
2735
+ toggleSort(col) {
2736
+ this.sort = this.sort.col === col ? { col, dir: this.sort.dir === "ascending" ? "descending" : "ascending" } : { col, dir: "ascending" };
2737
+ this.headers.forEach((th, i) => th.hasAttribute("aria-sort") && th.setAttribute("aria-sort", i === col ? this.sort.dir : "none"));
2738
+ this.render();
2739
+ }
2740
+ ordered() {
2741
+ const list = this.rows.filter((r) => this.matches(r));
2742
+ const { col, dir } = this.sort;
2743
+ if (col < 0) return list;
2744
+ const numeric = this.headers[col]?.dataset.type === "number";
2745
+ const sign = dir === "descending" ? -1 : 1;
2746
+ return list.sort((a, b) => {
2747
+ const x = this.cellValue(a, col);
2748
+ const y = this.cellValue(b, col);
2749
+ return sign * (numeric ? (parseFloat(x) || 0) - (parseFloat(y) || 0) : this.collator.compare(x, y));
2750
+ });
2751
+ }
2752
+ visible() {
2753
+ return this.rows.filter((r) => !r.hidden);
2754
+ }
2755
+ setSelected(row, on) {
2756
+ const box = row.querySelector("input[data-select]");
2757
+ if (box) box.checked = on;
2758
+ if (on) row.setAttribute("aria-selected", "true");
2759
+ else row.removeAttribute("aria-selected");
2760
+ }
2761
+ setColumn(col, show) {
2762
+ [this.table.tHead, ...this.table.tBodies, this.table.tFoot].filter(Boolean).forEach((section) => {
2763
+ [...section.rows].forEach((row) => {
2764
+ if (row.cells[col] && !row.hasAttribute("data-empty")) row.cells[col].hidden = !show;
2765
+ });
2766
+ });
2767
+ }
2768
+ updateSelection() {
2769
+ const shown = this.visible();
2770
+ const selected = this.rows.filter((r) => r.getAttribute("aria-selected") === "true").length;
2771
+ const shownSelected = shown.filter((r) => r.getAttribute("aria-selected") === "true").length;
2772
+ if (this.selectAll) {
2773
+ this.selectAll.checked = shown.length > 0 && shownSelected === shown.length;
2774
+ this.selectAll.indeterminate = shownSelected > 0 && shownSelected < shown.length;
2775
+ }
2776
+ const count = this.querySelector("[data-selected-count]");
2777
+ if (count) {
2778
+ const tpl = count.dataset.template || "{selected} of {total} row(s) selected.";
2779
+ count.textContent = tpl.replace("{selected}", selected).replace("{total}", this.rows.length);
2780
+ }
2781
+ this.dispatchEvent(new CustomEvent("selectionchange", { detail: { rows: this.rows.filter((r) => r.getAttribute("aria-selected") === "true") } }));
2782
+ }
2783
+ render() {
2784
+ const list = this.ordered();
2785
+ const pages = this.pageSize ? Math.max(1, Math.ceil(list.length / this.pageSize)) : 1;
2786
+ this.page = Math.min(Math.max(0, this.page), pages - 1);
2787
+ const from = this.pageSize ? this.page * this.pageSize : 0;
2788
+ const to = this.pageSize ? from + this.pageSize : list.length;
2789
+ const shown = new Set(list.slice(from, to));
2790
+ list.forEach((row) => this.tbody.insertBefore(row, this.empty));
2791
+ this.rows.forEach((row) => {
2792
+ row.hidden = !shown.has(row);
2793
+ });
2794
+ if (this.empty) this.empty.hidden = list.length > 0;
2795
+ const status = this.querySelector("[data-page-status]");
2796
+ if (status) {
2797
+ const tpl = status.dataset.template || "Page {page} of {pages}";
2798
+ status.textContent = tpl.replace("{page}", this.page + 1).replace("{pages}", pages);
2799
+ }
2800
+ const prev = this.querySelector('[data-page="prev"]');
2801
+ const next = this.querySelector('[data-page="next"]');
2802
+ if (prev) prev.disabled = this.page <= 0;
2803
+ if (next) next.disabled = this.page >= pages - 1;
2804
+ this.updateSelection();
2805
+ }
2806
+ };
2807
+ if (!customElements.get("auvyx-data-table")) customElements.define("auvyx-data-table", AuvyxDataTable);
2808
+
2809
+ // ../elements/src/calendar.js
2810
+ var pad = (n) => String(n).padStart(2, "0");
2811
+ var toISO = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
2812
+ var fromISO = (s) => {
2813
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec((s || "").trim());
2814
+ return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
2815
+ };
2816
+ var dayKey = (d) => d.getFullYear() * 1e4 + d.getMonth() * 100 + d.getDate();
2817
+ var sameDay = (a, b) => !!a && !!b && dayKey(a) === dayKey(b);
2818
+ var addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
2819
+ var addMonths = (d, n) => {
2820
+ const last = new Date(d.getFullYear(), d.getMonth() + n + 1, 0).getDate();
2821
+ return new Date(d.getFullYear(), d.getMonth() + n, Math.min(d.getDate(), last));
2822
+ };
2823
+ var monthStart = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
2824
+ var daysBetween = (a, b) => Math.round((new Date(b.getFullYear(), b.getMonth(), b.getDate()) - new Date(a.getFullYear(), a.getMonth(), a.getDate())) / 864e5);
2825
+ var isoWeek = (d) => {
2826
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
2827
+ t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));
2828
+ return Math.ceil(((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 864e5 + 1) / 7);
2829
+ };
2830
+ var parseList = (s) => (s || "").split(",").map((x) => x.trim()).filter(Boolean).map((x) => {
2831
+ const [a, b] = x.split("/");
2832
+ const from = fromISO(a);
2833
+ return from ? [from, fromISO(b) || from] : null;
2834
+ }).filter(Boolean);
2835
+ var inList = (list, d) => list.some(([a, b]) => d >= a && d <= b);
2836
+ var chevron = (path) => `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${path}"/></svg>`;
2837
+ var esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2838
+ var AuvyxCalendar = class extends HTMLElement {
2839
+ static observedAttributes = ["value", "min", "max", "mode", "months", "month", "disabled-dates", "disabled-days", "booked", "min-days", "max-days", "caption", "week-numbers", "lang"];
2840
+ connectedCallback() {
2841
+ this.readAll();
2842
+ const first = this.selected[0] || this.start || /* @__PURE__ */ new Date();
2843
+ this.focused ||= this.clamp(first);
2844
+ const m = /^(\d{4})-(\d{2})$/.exec(this.getAttribute("month") || "");
2845
+ this.view ||= m ? new Date(+m[1], +m[2] - 1, 1) : monthStart(this.focused);
2846
+ if (!this._init) {
2847
+ this._init = true;
2848
+ this.addEventListener("click", (e) => this.onClick(e));
2849
+ this.addEventListener("keydown", (e) => this.onKey(e));
2850
+ this.addEventListener("change", (e) => this.onSelectChange(e));
2851
+ }
2852
+ this.render();
2853
+ }
2854
+ attributeChangedCallback(name, oldValue, value) {
2855
+ if (!this._init || oldValue === value || this._writing) return;
2856
+ this.readAll();
2857
+ if (name === "value") {
2858
+ const first = this.selected[0] || this.start;
2859
+ if (first) {
2860
+ this.focused = first;
2861
+ this.view = monthStart(first);
2862
+ }
2863
+ }
2864
+ if (name === "month") {
2865
+ const m = /^(\d{4})-(\d{2})$/.exec(value || "");
2866
+ if (m) this.view = new Date(+m[1], +m[2] - 1, 1);
2867
+ }
2868
+ this.render();
2869
+ }
2870
+ // ---- state
2871
+ readAll() {
2872
+ const mode = this.getAttribute("mode");
2873
+ this.mode = mode === "range" || mode === "multiple" ? mode : "single";
2874
+ this.monthsShown = Math.max(1, Math.min(12, parseInt(this.getAttribute("months"), 10) || 1));
2875
+ this.min = fromISO(this.getAttribute("min"));
2876
+ this.max = fromISO(this.getAttribute("max"));
2877
+ this.disabledDates = parseList(this.getAttribute("disabled-dates"));
2878
+ this.bookedDates = parseList(this.getAttribute("booked"));
2879
+ this.disabledDays = (this.getAttribute("disabled-days") || "").split(",").map((x) => parseInt(x, 10)).filter((x) => x >= 0 && x <= 6);
2880
+ this.minDays = parseInt(this.getAttribute("min-days"), 10) || 0;
2881
+ this.maxDays = parseInt(this.getAttribute("max-days"), 10) || 0;
2882
+ const raw = this.getAttribute("value") || "";
2883
+ this.start = this.end = null;
2884
+ this.selected = [];
2885
+ if (this.mode === "range") {
2886
+ const [a, b] = raw.split("/");
2887
+ this.start = fromISO(a);
2888
+ this.end = fromISO(b);
2889
+ } else if (this.mode === "multiple") {
2890
+ this.selected = raw.split(",").map(fromISO).filter(Boolean);
2891
+ } else {
2892
+ this.start = fromISO(raw);
2893
+ if (this.start) this.selected = [this.start];
2894
+ }
2895
+ }
2896
+ get value() {
2897
+ if (this.mode === "range") return this.start ? `${toISO(this.start)}/${this.end ? toISO(this.end) : ""}` : "";
2898
+ if (this.mode === "multiple") return this.selected.map(toISO).join(",");
2899
+ return this.start ? toISO(this.start) : "";
2900
+ }
2901
+ set value(v) {
2902
+ this.setAttribute("value", v ?? "");
2903
+ }
2904
+ get dates() {
2905
+ if (this.mode === "range") return [this.start, this.end].filter(Boolean);
2906
+ return [...this.selected];
2907
+ }
2908
+ set renderDay(fn) {
2909
+ this._renderDay = fn;
2910
+ if (this._init) this.render();
2911
+ }
2912
+ get renderDay() {
2913
+ return this._renderDay;
2914
+ }
2915
+ locale() {
2916
+ return this.getAttribute("lang") || this.closest("[lang]")?.getAttribute("lang") || navigator.language;
2917
+ }
2918
+ weekStart() {
2919
+ const n = parseInt(this.dataset.weekStart, 10);
2920
+ return Number.isFinite(n) ? (n % 7 + 7) % 7 : 1;
2921
+ }
2922
+ clamp(d) {
2923
+ if (this.min && d < this.min) return this.min;
2924
+ if (this.max && d > this.max) return this.max;
2925
+ return d;
2926
+ }
2927
+ isDisabled(d) {
2928
+ if (this.min && d < this.min || this.max && d > this.max) return true;
2929
+ if (this.disabledDays.includes(d.getDay())) return true;
2930
+ if (inList(this.disabledDates, d) || inList(this.bookedDates, d)) return true;
2931
+ if (this.mode === "range" && this.start && !this.end && d > this.start) {
2932
+ const len = daysBetween(this.start, d) + 1;
2933
+ if (this.minDays && len < this.minDays) return true;
2934
+ if (this.maxDays && len > this.maxDays) return true;
2935
+ for (let x = addDays(this.start, 1); x < d; x = addDays(x, 1)) {
2936
+ if (this.disabledDays.includes(x.getDay()) || inList(this.disabledDates, x) || inList(this.bookedDates, x)) return true;
2937
+ }
2938
+ }
2939
+ return false;
2940
+ }
2941
+ // ---- rendering
2942
+ render() {
2943
+ const locale = this.locale();
2944
+ const fmtMonth = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" });
2945
+ const fmtMonthOnly = new Intl.DateTimeFormat(locale, { month: "short" });
2946
+ const fmtWeekday = new Intl.DateTimeFormat(locale, { weekday: "short" });
2947
+ const fmtWeekdayLong = new Intl.DateTimeFormat(locale, { weekday: "long" });
2948
+ const fmtDay = new Intl.DateTimeFormat(locale, { dateStyle: "full" });
2949
+ const today = /* @__PURE__ */ new Date();
2950
+ const ws = this.weekStart();
2951
+ const id = this.id || (this._uid ||= `cal-${Math.random().toString(36).slice(2, 8)}`);
2952
+ const weekNums = this.hasAttribute("week-numbers");
2953
+ const dropdown = this.getAttribute("caption") === "dropdown";
2954
+ const prevDisabled = this.min && addMonths(this.view, -1) < monthStart(this.min);
2955
+ const nextDisabled = this.max && addMonths(this.view, this.monthsShown) > this.max;
2956
+ const hadFocus = this.contains(document.activeElement) && document.activeElement.matches("td > button");
2957
+ let html = "";
2958
+ for (let m = 0; m < this.monthsShown; m++) {
2959
+ const first = addMonths(this.view, m);
2960
+ const lead = (first.getDay() - ws + 7) % 7;
2961
+ const gridStart = addDays(first, -lead);
2962
+ const dim = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
2963
+ const weeks = Math.ceil((lead + dim) / 7);
2964
+ const titleId = `${id}-title-${m}`;
2965
+ let caption = `<h2 id="${titleId}" aria-live="polite">${esc(fmtMonth.format(first))}</h2>`;
2966
+ if (dropdown && m === 0) {
2967
+ const y0 = (this.min || addMonths(today, -12 * 10)).getFullYear();
2968
+ const y1 = (this.max || addMonths(today, 12 * 10)).getFullYear();
2969
+ let months = "";
2970
+ for (let i = 0; i < 12; i++) {
2971
+ const d = new Date(first.getFullYear(), i, 1);
2972
+ const off = this.min && new Date(first.getFullYear(), i + 1, 0) < this.min || this.max && d > this.max;
2973
+ months += `<option value="${i}"${i === first.getMonth() ? " selected" : ""}${off ? " disabled" : ""}>${esc(fmtMonthOnly.format(d))}</option>`;
2974
+ }
2975
+ let years = "";
2976
+ for (let y = y0; y <= y1; y++) years += `<option value="${y}"${y === first.getFullYear() ? " selected" : ""}>${y}</option>`;
2977
+ caption = `<div class="calendar-caption" id="${titleId}" aria-label="${esc(fmtMonth.format(first))}" role="group"><select class="select" data-calendar-month aria-label="${esc(this.dataset.labelMonth || "Month")}">${months}</select><select class="select" data-calendar-year aria-label="${esc(this.dataset.labelYear || "Year")}">${years}</select></div>`;
2978
+ }
2979
+ const prev = m === 0 ? `<button type="button" data-nav="-1" aria-label="${esc(this.dataset.labelPrev || "Previous month")}"${prevDisabled ? " disabled" : ""}>${chevron("m15 18-6-6 6-6")}</button>` : "<span></span>";
2980
+ const next = m === this.monthsShown - 1 ? `<button type="button" data-nav="1" aria-label="${esc(this.dataset.labelNext || "Next month")}"${nextDisabled ? " disabled" : ""}>${chevron("m9 18 6-6-6-6")}</button>` : "<span></span>";
2981
+ let head = weekNums ? `<th scope="col" data-week-number><span class="sr-only">${esc(this.dataset.labelWeek || "Week")}</span></th>` : "";
2982
+ for (let i = 0; i < 7; i++) {
2983
+ const d = addDays(gridStart, i);
2984
+ head += `<th scope="col" abbr="${esc(fmtWeekdayLong.format(d))}">${esc(fmtWeekday.format(d))}</th>`;
2985
+ }
2986
+ let body = "";
2987
+ for (let w = 0; w < weeks; w++) {
2988
+ body += "<tr>";
2989
+ if (weekNums) body += `<th scope="row" data-week-number>${isoWeek(addDays(gridStart, w * 7 + (8 - ws) % 7))}</th>`;
2990
+ for (let i = 0; i < 7; i++) {
2991
+ const d = addDays(gridStart, w * 7 + i);
2992
+ const outside = d.getMonth() !== first.getMonth();
2993
+ const hideOutside = outside && this.monthsShown > 1;
2994
+ const attrs = [];
2995
+ if (outside) attrs.push("data-outside");
2996
+ if (inList(this.bookedDates, d)) attrs.push("data-booked");
2997
+ if (!hideOutside) {
2998
+ if (this.mode !== "range" && this.selected.some((s) => sameDay(s, d))) attrs.push('aria-selected="true"');
2999
+ if (this.mode === "range" && this.start) {
3000
+ const end = this.end || this.start;
3001
+ if (sameDay(d, this.start)) attrs.push("data-range-start");
3002
+ if (sameDay(d, end)) attrs.push("data-range-end");
3003
+ if (d > this.start && d < end) attrs.push("data-range-middle");
3004
+ if (d >= this.start && d <= end) attrs.push('aria-selected="true"');
3005
+ }
3006
+ }
3007
+ if (hideOutside) {
3008
+ body += `<td ${attrs.join(" ")} aria-hidden="true"></td>`;
3009
+ continue;
3010
+ }
3011
+ const tab = sameDay(d, this.focused) && !outside ? "0" : "-1";
3012
+ const extra = this._renderDay ? this._renderDay(d) : null;
3013
+ body += `<td ${attrs.join(" ")}><button type="button" data-date="${toISO(d)}" tabindex="${tab}" aria-label="${esc(fmtDay.format(d))}"${sameDay(d, today) ? ' aria-current="date"' : ""}${this.isDisabled(d) ? " disabled" : ""}><span>${d.getDate()}</span>${extra ? `<small>${extra}</small>` : ""}</button></td>`;
3014
+ }
3015
+ body += "</tr>";
3016
+ }
3017
+ html += `<section class="calendar-month"><header>${prev}${caption}${next}</header><table role="grid" aria-labelledby="${titleId}"${weekNums ? " data-week-numbers" : ""}><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></section>`;
3018
+ }
3019
+ const hidden = this.getAttribute("name") ? `<input type="hidden" name="${esc(this.getAttribute("name"))}" value="${esc(this.value)}">` : "";
3020
+ this.innerHTML = `<div class="calendar-months">${html}</div>${hidden}`;
3021
+ if (!this.querySelector('td > button[tabindex="0"]')) {
3022
+ const b = this.querySelector("td:not([data-outside]) > button:not(:disabled)") || this.querySelector("td > button");
3023
+ if (b) b.tabIndex = 0;
3024
+ }
3025
+ if (hadFocus) this.focusDate();
3026
+ }
3027
+ focusDate() {
3028
+ const b = this.querySelector(`td:not([data-outside]) > button[data-date="${toISO(this.focused)}"]`) || this.querySelector('td > button[tabindex="0"]');
3029
+ b?.focus({ preventScroll: true });
3030
+ }
3031
+ goTo(date) {
3032
+ const d = typeof date === "string" ? fromISO(date) : date;
3033
+ if (!d) return;
3034
+ this.view = monthStart(d);
3035
+ this.focused = d;
3036
+ this.render();
3037
+ }
3038
+ visible(d) {
3039
+ const last = addMonths(this.view, this.monthsShown - 1);
3040
+ return d >= this.view && d < addMonths(last, 1);
3041
+ }
3042
+ moveFocus(d) {
3043
+ if (this.min && d < this.min || this.max && d > this.max) return;
3044
+ this.focused = d;
3045
+ if (!this.visible(d)) {
3046
+ this.view = d < this.view ? monthStart(d) : monthStart(addMonths(d, -(this.monthsShown - 1)));
3047
+ this.render();
3048
+ } else {
3049
+ this.querySelectorAll("td > button").forEach((b) => {
3050
+ b.tabIndex = b.dataset.date === toISO(d) && !b.closest("[data-outside]") ? 0 : -1;
3051
+ });
3052
+ }
3053
+ this.focusDate();
3054
+ }
3055
+ select(d) {
3056
+ if (this.mode === "range") {
3057
+ if (!this.start || this.end || d < this.start) {
3058
+ this.start = d;
3059
+ this.end = null;
3060
+ } else this.end = d;
3061
+ } else if (this.mode === "multiple") {
3062
+ const i = this.selected.findIndex((s) => sameDay(s, d));
3063
+ if (i >= 0) this.selected.splice(i, 1);
3064
+ else this.selected.push(d);
3065
+ this.selected.sort((a, b) => a - b);
3066
+ } else {
3067
+ this.start = d;
3068
+ this.selected = [d];
3069
+ }
3070
+ this.focused = d;
3071
+ this._writing = true;
3072
+ this.setAttribute("value", this.value);
3073
+ this._writing = false;
3074
+ this.render();
3075
+ this.focusDate();
3076
+ this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { value: this.value, dates: this.dates, complete: this.mode !== "range" || !!this.end } }));
3077
+ }
3078
+ // ---- events
3079
+ onClick(event) {
3080
+ const nav = event.target.closest("button[data-nav]");
3081
+ if (nav && !nav.disabled) {
3082
+ this.view = addMonths(this.view, +nav.dataset.nav);
3083
+ const dim = new Date(this.view.getFullYear(), this.view.getMonth() + 1, 0).getDate();
3084
+ this.focused = this.clamp(new Date(this.view.getFullYear(), this.view.getMonth(), Math.min(this.focused.getDate(), dim)));
3085
+ this.render();
3086
+ return;
3087
+ }
3088
+ const day = event.target.closest("button[data-date]");
3089
+ if (day && !day.disabled) this.select(fromISO(day.dataset.date));
3090
+ }
3091
+ onSelectChange(event) {
3092
+ const t = event.target;
3093
+ if (!(t instanceof HTMLSelectElement) || !t.closest(".calendar-caption")) return;
3094
+ event.stopPropagation();
3095
+ const month = +this.querySelector("[data-calendar-month]").value;
3096
+ const year = +this.querySelector("[data-calendar-year]").value;
3097
+ this.view = new Date(year, month, 1);
3098
+ this.focused = this.clamp(new Date(year, month, Math.min(this.focused.getDate(), new Date(year, month + 1, 0).getDate())));
3099
+ this.render();
3100
+ this.querySelector(t.hasAttribute("data-calendar-month") ? "[data-calendar-month]" : "[data-calendar-year]")?.focus();
3101
+ }
3102
+ onKey(event) {
3103
+ if (!event.target.matches("td > button")) return;
3104
+ const rtl = getComputedStyle(this).direction === "rtl";
3105
+ const d = this.focused;
3106
+ const offset = (d.getDay() - this.weekStart() + 7) % 7;
3107
+ const moves = {
3108
+ ArrowLeft: addDays(d, rtl ? 1 : -1),
3109
+ ArrowRight: addDays(d, rtl ? -1 : 1),
3110
+ ArrowUp: addDays(d, -7),
3111
+ ArrowDown: addDays(d, 7),
3112
+ Home: addDays(d, -offset),
3113
+ End: addDays(d, 6 - offset),
3114
+ PageUp: addMonths(d, event.shiftKey ? -12 : -1),
3115
+ PageDown: addMonths(d, event.shiftKey ? 12 : 1)
3116
+ };
3117
+ if (!(event.key in moves)) return;
3118
+ event.preventDefault();
3119
+ this.moveFocus(moves[event.key]);
3120
+ }
3121
+ };
3122
+ if (!customElements.get("auvyx-calendar")) customElements.define("auvyx-calendar", AuvyxCalendar);
3123
+
3124
+ // ../elements/src/date-picker.js
3125
+ var calendarIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>';
3126
+ var PASS = ["months", "caption", "week-numbers", "disabled-days", "disabled-dates", "booked", "min-days", "max-days"];
3127
+ var asDate = (v) => v ? /* @__PURE__ */ new Date(`${v}T00:00:00`) : null;
3128
+ var AuvyxDatePicker = class extends HTMLElement {
3129
+ connectedCallback() {
3130
+ if (this._init) return;
3131
+ this.inputs = [...this.querySelectorAll(':scope > input[type="date"]')];
3132
+ if (!this.inputs.length) return;
3133
+ this._init = true;
3134
+ this.range = this.getAttribute("mode") === "range" && this.inputs.length >= 2;
3135
+ const first = this.inputs[0];
3136
+ const uid = first.id || `dp-${Math.random().toString(36).slice(2, 8)}`;
3137
+ const locale = this.closest("[lang]")?.getAttribute("lang") || navigator.language;
3138
+ this.format = new Intl.DateTimeFormat(locale, { dateStyle: "medium" });
3139
+ this.button = document.createElement("button");
3140
+ this.button.type = "button";
3141
+ this.button.className = "btn";
3142
+ this.button.dataset.variant = this.dataset.variant || "outline";
3143
+ this.button.setAttribute("popovertarget", `${uid}-popover`);
3144
+ this.button.setAttribute("aria-haspopup", "dialog");
3145
+ const label = first.labels?.[0];
3146
+ if (label) {
3147
+ label.id ||= `${uid}-label`;
3148
+ this.button.setAttribute("aria-labelledby", `${label.id} ${uid}-value`);
3149
+ label.htmlFor = "";
3150
+ label.addEventListener("click", () => this.button.focus());
3151
+ }
3152
+ this.button.innerHTML = `${calendarIcon}<span id="${uid}-value"></span>`;
3153
+ this.panel = document.createElement("div");
3154
+ this.panel.id = `${uid}-popover`;
3155
+ this.panel.setAttribute("data-popover", "");
3156
+ this.panel.setAttribute("popover", "");
3157
+ this.panel.setAttribute("role", "dialog");
3158
+ this.panel.setAttribute("aria-label", this.dataset.dialogLabel || (this.range ? "Choose dates" : "Choose date"));
3159
+ this.panel.dataset.side = this.dataset.side || "bottom";
3160
+ this.panel.dataset.align = this.dataset.align || "start";
3161
+ this.calendar = document.createElement("auvyx-calendar");
3162
+ this.calendar.className = "calendar";
3163
+ if (this.range) this.calendar.setAttribute("mode", "range");
3164
+ for (const attr of ["min", "max"]) if (first[attr]) this.calendar.setAttribute(attr, first[attr]);
3165
+ for (const attr of PASS) if (this.hasAttribute(attr)) this.calendar.setAttribute(attr, this.getAttribute(attr));
3166
+ if (this.dataset.weekStart) this.calendar.dataset.weekStart = this.dataset.weekStart;
3167
+ const v = this.range ? this.inputs[0].value ? `${this.inputs[0].value}/${this.inputs[1].value}` : "" : first.value;
3168
+ if (v) this.calendar.setAttribute("value", v);
3169
+ this.panel.append(this.calendar);
3170
+ for (const input of this.inputs) {
3171
+ input.hidden = true;
3172
+ input.tabIndex = -1;
3173
+ input.addEventListener("invalid", (e) => {
3174
+ e.preventDefault();
3175
+ this.button.setAttribute("aria-invalid", "true");
3176
+ this.button.focus();
3177
+ });
3178
+ }
3179
+ this.prepend(this.button);
3180
+ this.append(this.panel);
3181
+ this.update();
3182
+ this.calendar.addEventListener("change", (e) => {
3183
+ e.stopPropagation();
3184
+ const [a, b] = e.detail.value.split("/");
3185
+ this.setInput(this.inputs[0], a || "");
3186
+ if (this.range) this.setInput(this.inputs[1], b || "");
3187
+ this.button.removeAttribute("aria-invalid");
3188
+ this.update();
3189
+ if (e.detail.complete) {
3190
+ this.panel.hidePopover();
3191
+ this.button.focus();
3192
+ }
3193
+ this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { value: e.detail.value, complete: e.detail.complete } }));
3194
+ });
3195
+ this.panel.addEventListener("toggle", (e) => {
3196
+ if (e.newState === "open") requestAnimationFrame(() => this.calendar.focusDate?.());
3197
+ });
3198
+ }
3199
+ setInput(input, value) {
3200
+ if (input.value === value) return;
3201
+ input.value = value;
3202
+ input.dispatchEvent(new Event("input", { bubbles: true }));
3203
+ input.dispatchEvent(new Event("change", { bubbles: true }));
3204
+ }
3205
+ get value() {
3206
+ return this.range ? `${this.inputs[0].value}/${this.inputs[1].value}` : this.inputs[0].value;
3207
+ }
3208
+ update() {
3209
+ const text = this.button.querySelector("span");
3210
+ const a = asDate(this.inputs[0].value);
3211
+ const b = this.range ? asDate(this.inputs[1].value) : null;
3212
+ let label;
3213
+ if (this.range && a && b) label = this.format.formatRange(a, b);
3214
+ else if (a) label = this.format.format(a) + (this.range ? " \u2013" : "");
3215
+ else label = this.dataset.placeholder || this.inputs[0].placeholder || (this.range ? "Pick a date range" : "Pick a date");
3216
+ text.textContent = label;
3217
+ if (a) this.button.dataset.value = this.value;
3218
+ else delete this.button.dataset.value;
3219
+ }
3220
+ };
3221
+ if (!customElements.get("auvyx-date-picker")) customElements.define("auvyx-date-picker", AuvyxDatePicker);
3222
+ })();