@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
@@ -0,0 +1,757 @@
1
+ /* @auvyx/ui v0.1.0 | MIT | based on Basecoat (MIT) */
2
+ // Basecoat core, once per page.
3
+ if (!window.basecoat) (() => {
4
+ const componentRegistry = {};
5
+ let observer = null;
6
+
7
+ const registerComponent = (name, selectorOrOptions, initFunction) => {
8
+ const options = typeof selectorOrOptions === 'object'
9
+ ? selectorOrOptions
10
+ : { selector: selectorOrOptions, init: initFunction };
11
+
12
+ componentRegistry[name] = {
13
+ selector: options.selector,
14
+ init: options.init,
15
+ refresh: options.refresh,
16
+ };
17
+ };
18
+
19
+ const initComponent = (element, componentName) => {
20
+ const component = componentRegistry[componentName];
21
+ if (!component) return;
22
+
23
+ try {
24
+ component.init(element);
25
+ if (element.hasAttribute(`data-${componentName}-initialized`)) {
26
+ element.dataset.basecoatComponent = componentName;
27
+ }
28
+ } catch (error) {
29
+ console.error(`Failed to initialize ${componentName}:`, error);
30
+ if (typeof element._destroy === 'function') {
31
+ try {
32
+ element._destroy();
33
+ } catch (destroyError) {
34
+ console.error(`Failed to clean up ${componentName} after initialization error:`, destroyError);
35
+ }
36
+ }
37
+ delete element._destroy;
38
+ element.removeAttribute(`data-${componentName}-initialized`);
39
+ delete element.dataset.basecoatComponent;
40
+ }
41
+ };
42
+
43
+ const destroyComponent = (element) => {
44
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
45
+ const componentName = element.dataset?.basecoatComponent;
46
+
47
+ if (typeof element._destroy === 'function') {
48
+ try {
49
+ element._destroy();
50
+ } catch (error) {
51
+ console.error('Failed to destroy Basecoat component:', error);
52
+ }
53
+ }
54
+
55
+ delete element._destroy;
56
+ if (componentName) element.removeAttribute(`data-${componentName}-initialized`);
57
+ delete element.dataset.basecoatComponent;
58
+ };
59
+
60
+ const destroyRemovedComponents = (node) => {
61
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
62
+ if (node.isConnected) return;
63
+
64
+ if (node.dataset?.basecoatComponent) destroyComponent(node);
65
+ node.querySelectorAll('[data-basecoat-component]').forEach(destroyComponent);
66
+ };
67
+
68
+ const uniqueElements = (elements) => Array.from(new Set(elements));
69
+
70
+ const getComponentElements = (componentName, selector, force = false) => {
71
+ const elements = Array.from(document.querySelectorAll(selector));
72
+ if (force) {
73
+ elements.push(...document.querySelectorAll(`[data-basecoat-component="${componentName}"]`));
74
+ }
75
+ return uniqueElements(elements);
76
+ };
77
+
78
+ const initAllComponents = (options = {}) => {
79
+ const force = options.force === true;
80
+ Object.entries(componentRegistry).forEach(([name, { selector }]) => {
81
+ getComponentElements(name, selector, force).forEach((element) => {
82
+ const wasComponent = element.dataset?.basecoatComponent === name;
83
+ if (force) destroyComponent(element);
84
+ if (wasComponent || element.matches(selector)) initComponent(element, name);
85
+ });
86
+ });
87
+ };
88
+
89
+ const initNewComponents = (node) => {
90
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
91
+
92
+ Object.entries(componentRegistry).forEach(([name, { selector }]) => {
93
+ if (node.matches(selector)) initComponent(node, name);
94
+ node.querySelectorAll(selector).forEach(element => initComponent(element, name));
95
+ });
96
+ };
97
+
98
+ const refreshComponent = (element) => {
99
+ if (!element) return;
100
+ if (typeof element.refresh === 'function') {
101
+ element.refresh();
102
+ return;
103
+ }
104
+
105
+ const componentName = element.dataset?.basecoatComponent;
106
+ const component = componentName ? componentRegistry[componentName] : null;
107
+ if (component?.refresh) {
108
+ component.refresh(element);
109
+ }
110
+ };
111
+
112
+ const startObserver = () => {
113
+ if (observer) return;
114
+
115
+ observer = new MutationObserver((mutations) => {
116
+ mutations.forEach((mutation) => {
117
+ mutation.addedNodes.forEach(initNewComponents);
118
+ mutation.removedNodes.forEach(destroyRemovedComponents);
119
+ });
120
+ });
121
+
122
+ observer.observe(document.body, { childList: true, subtree: true });
123
+ };
124
+
125
+ const stopObserver = () => {
126
+ if (!observer) return;
127
+ observer.disconnect();
128
+ observer = null;
129
+ };
130
+
131
+ const initRegisteredComponent = (componentName, options = {}) => {
132
+ const component = componentRegistry[componentName];
133
+ if (!component) {
134
+ console.warn(`Component '${componentName}' not found in registry`);
135
+ return;
136
+ }
137
+
138
+ const force = options.force === true;
139
+ getComponentElements(componentName, component.selector, force).forEach((element) => {
140
+ const wasComponent = element.dataset?.basecoatComponent === componentName;
141
+ if (force) destroyComponent(element);
142
+ if (wasComponent || element.matches(component.selector)) initComponent(element, componentName);
143
+ });
144
+ };
145
+
146
+ const initAllRegisteredComponents = (options = {}) => {
147
+ initAllComponents(options);
148
+ };
149
+
150
+ const setTheme = (mode) => {
151
+ const dark = mode === 'dark';
152
+ document.documentElement.classList.toggle('dark', dark);
153
+ try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
154
+ document.dispatchEvent(new CustomEvent('basecoat:themechange', { detail: { mode: dark ? 'dark' : 'light' } }));
155
+ };
156
+
157
+ const getTheme = () => document.documentElement.classList.contains('dark') ? 'dark' : 'light';
158
+
159
+ window.basecoat = {
160
+ register: registerComponent,
161
+ init: initRegisteredComponent,
162
+ initAll: initAllRegisteredComponents,
163
+ refresh: refreshComponent,
164
+ start: startObserver,
165
+ stop: stopObserver,
166
+ theme: {
167
+ get: getTheme,
168
+ set: setTheme,
169
+ toggle: () => setTheme(getTheme() === 'dark' ? 'light' : 'dark'),
170
+ },
171
+ };
172
+
173
+ document.addEventListener('DOMContentLoaded', () => {
174
+ initAllComponents();
175
+ startObserver();
176
+ });
177
+ })();
178
+
179
+ (() => {
180
+ const states = new WeakMap();
181
+
182
+ const getElements = (root) => {
183
+ const popover = root.querySelector(':scope > [data-popover]');
184
+ const input = root.querySelector(':scope > input[role="combobox"], :scope > .input-group input[role="combobox"], :scope > .combobox-chips input[role="combobox"]')
185
+ || popover?.querySelector('input[role="combobox"]');
186
+ const chips = root.querySelector(':scope > .combobox-chips');
187
+ const popupTrigger = root.querySelector(':scope > button[aria-haspopup="listbox"]');
188
+ const trigger = popupTrigger || root.querySelector(':scope > .input-group button[aria-haspopup="listbox"]');
189
+ const clearButton = root.querySelector('[data-clear]');
190
+ const valueTarget = popupTrigger?.querySelector('[data-value]') || (popupTrigger?.matches('[data-value]') ? popupTrigger : null);
191
+ const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
192
+ const hiddenInput = root.querySelector(':scope > input[type="hidden"]');
193
+ return { input, chips, trigger, clearButton, valueTarget, popover, listbox, hiddenInput };
194
+ };
195
+
196
+ const ensureMultipleInputSurface = (root, elements) => {
197
+ if (elements.listbox?.getAttribute('aria-multiselectable') !== 'true' || elements.chips || !elements.input) return elements;
198
+ if (elements.input.parentElement !== root) return elements;
199
+
200
+ const chips = document.createElement('div');
201
+ chips.className = 'combobox-chips';
202
+ root.insertBefore(chips, elements.input);
203
+ chips.appendChild(elements.input);
204
+
205
+ return getElements(root);
206
+ };
207
+
208
+ const getValue = option => option.dataset.value ?? option.textContent.trim();
209
+ const getLabel = option => option.dataset.label || option.textContent.trim();
210
+ const getFormat = root => root.dataset.format === 'object' ? 'object' : 'value';
211
+ const isDisabled = option => option.getAttribute('aria-disabled') === 'true';
212
+ const toSelected = option => ({ value: getValue(option), label: getLabel(option) });
213
+ const normalizeEntry = entry => {
214
+ if (entry && typeof entry === 'object') {
215
+ const value = entry.value == null ? '' : String(entry.value);
216
+ return value ? { value, label: String(entry.label ?? entry.value) } : null;
217
+ }
218
+ const value = entry == null ? '' : String(entry);
219
+ return value ? { value, label: value } : null;
220
+ };
221
+
222
+ const getOptions = (listbox) => {
223
+ const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
224
+ return {
225
+ allOptions,
226
+ options: allOptions.filter(option => !isDisabled(option)),
227
+ };
228
+ };
229
+
230
+ const getSelection = (state) => Array.from(state.selected.values());
231
+ const getCanonicalValue = (state) => state.isMultiple ? getSelection(state).map(item => item.value) : (getSelection(state)[0]?.value || '');
232
+ const getSelectedDetail = (state) => state.isMultiple ? getSelection(state) : (getSelection(state)[0] || null);
233
+
234
+ const serializeSelection = (state) => {
235
+ const selected = getSelection(state);
236
+ if (state.format === 'object') {
237
+ return JSON.stringify(state.isMultiple ? selected : (selected[0] || null));
238
+ }
239
+ const value = selected.map(item => item.value);
240
+ return state.isMultiple ? JSON.stringify(value) : (value[0] || '');
241
+ };
242
+
243
+ const parseStoredSelection = (storedValue, inputValue, state) => {
244
+ if (state.isMultiple) {
245
+ let parsed = [];
246
+ try {
247
+ parsed = JSON.parse(storedValue || '[]');
248
+ } catch (_) {
249
+ parsed = [];
250
+ }
251
+ if (!Array.isArray(parsed)) return [];
252
+ return parsed.map(item => {
253
+ const entry = normalizeEntry(state.format === 'object' ? item : { value: item, label: state.selected.get(String(item))?.label ?? item });
254
+ if (!entry) return null;
255
+ const option = state.options.find(opt => getValue(opt) === entry.value);
256
+ return option ? toSelected(option) : entry;
257
+ }).filter(Boolean);
258
+ }
259
+
260
+ if (state.format === 'object') {
261
+ try {
262
+ const entry = normalizeEntry(JSON.parse(storedValue || 'null'));
263
+ if (!entry) return [];
264
+ const option = state.options.find(opt => getValue(opt) === entry.value);
265
+ return [option ? toSelected(option) : entry];
266
+ } catch (_) {
267
+ return [];
268
+ }
269
+ }
270
+
271
+ const value = storedValue || '';
272
+ if (!value) return [];
273
+ const option = state.options.find(opt => getValue(opt) === value);
274
+ return [option ? toSelected(option) : { value, label: state.selected.get(value)?.label || inputValue || value }];
275
+ };
276
+
277
+ const scrollOptionIntoListbox = (state, option) => {
278
+ const optionRect = option.getBoundingClientRect();
279
+ const listboxRect = state.listbox.getBoundingClientRect();
280
+
281
+ if (optionRect.top < listboxRect.top) {
282
+ state.listbox.scrollTop -= listboxRect.top - optionRect.top;
283
+ } else if (optionRect.bottom > listboxRect.bottom) {
284
+ state.listbox.scrollTop += optionRect.bottom - listboxRect.bottom;
285
+ }
286
+ };
287
+
288
+ const setActiveOption = (state, index) => {
289
+ if (state.activeIndex > -1 && state.options[state.activeIndex]) {
290
+ state.options[state.activeIndex].classList.remove('active');
291
+ }
292
+
293
+ state.activeIndex = index;
294
+
295
+ if (state.activeIndex > -1) {
296
+ const activeOption = state.options[state.activeIndex];
297
+ activeOption.classList.add('active');
298
+ if (!activeOption.id) activeOption.id = `${state.listbox.id || state.root.id || 'combobox'}-option-${state.activeIndex}`;
299
+ state.input.setAttribute('aria-activedescendant', activeOption.id);
300
+ } else {
301
+ state.input.removeAttribute('aria-activedescendant');
302
+ }
303
+ };
304
+
305
+ const syncEmptyState = (state) => {
306
+ state.popover.dataset.empty = String(state.visibleOptions.length === 0);
307
+ };
308
+
309
+ const filterOptions = (state, { preserveActive = false, search: forcedSearch } = {}) => {
310
+ const previousActive = state.activeIndex > -1 ? state.options[state.activeIndex] : null;
311
+ state.visibleOptions = [];
312
+
313
+ if (state.manualFilter) {
314
+ state.visibleOptions = state.options.filter(option => option.getAttribute('aria-hidden') !== 'true');
315
+
316
+ if (preserveActive && previousActive && state.visibleOptions.includes(previousActive)) {
317
+ setActiveOption(state, state.options.indexOf(previousActive));
318
+ } else {
319
+ setActiveOption(state, state.autoHighlight && state.visibleOptions.length > 0 ? state.options.indexOf(state.visibleOptions[0]) : -1);
320
+ }
321
+ syncEmptyState(state);
322
+ return;
323
+ }
324
+
325
+ const search = (forcedSearch ?? state.input.value).trim().toLowerCase();
326
+
327
+ state.allOptions.forEach(option => {
328
+ if (option.hasAttribute('data-force')) {
329
+ option.setAttribute('aria-hidden', 'false');
330
+ if (state.options.includes(option)) state.visibleOptions.push(option);
331
+ return;
332
+ }
333
+
334
+ const optionText = (option.dataset.filter || option.dataset.label || option.textContent).trim().toLowerCase();
335
+ const keywords = (option.dataset.keywords || '').toLowerCase().split(/[\s,]+/).filter(Boolean);
336
+ const matches = !search || optionText.includes(search) || keywords.some(keyword => keyword.includes(search));
337
+ option.setAttribute('aria-hidden', String(!matches));
338
+ if (matches && state.options.includes(option)) state.visibleOptions.push(option);
339
+ });
340
+
341
+ if (preserveActive && previousActive && state.visibleOptions.includes(previousActive)) {
342
+ setActiveOption(state, state.options.indexOf(previousActive));
343
+ } else {
344
+ setActiveOption(state, state.autoHighlight && state.visibleOptions.length > 0 ? state.options.indexOf(state.visibleOptions[0]) : -1);
345
+ }
346
+ syncEmptyState(state);
347
+ };
348
+
349
+ const renderChips = (root) => {
350
+ const state = states.get(root);
351
+ if (!state.chips) return;
352
+
353
+ state.chips.querySelectorAll('.combobox-chip').forEach(chip => chip.remove());
354
+
355
+ getSelection(state).forEach(entry => {
356
+ const chip = document.createElement('span');
357
+ chip.className = 'combobox-chip';
358
+ chip.dataset.value = entry.value;
359
+
360
+ const label = document.createElement('span');
361
+ label.textContent = entry.label;
362
+
363
+ const remove = document.createElement('button');
364
+ remove.type = 'button';
365
+ remove.className = 'combobox-chip-remove btn';
366
+ remove.dataset.variant = 'ghost';
367
+ remove.dataset.size = 'icon-xs';
368
+ remove.setAttribute('aria-label', `Remove ${entry.label}`);
369
+ 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>';
370
+ remove.addEventListener('click', (event) => {
371
+ event.stopPropagation();
372
+ root.deselect(entry.value);
373
+ state.input.focus();
374
+ });
375
+
376
+ chip.appendChild(label);
377
+ chip.appendChild(remove);
378
+ state.chips.insertBefore(chip, state.input);
379
+ });
380
+ };
381
+
382
+ const syncTriggerValue = (state) => {
383
+ if (!state.valueTarget) return;
384
+ const selected = getSelection(state);
385
+ state.valueTarget.textContent = state.isMultiple
386
+ ? selected.map(entry => entry.label).join(', ')
387
+ : (selected[0]?.label || state.valueTarget.dataset.placeholder || '');
388
+ };
389
+
390
+ const syncClearButton = (state) => {
391
+ if (!state.clearButton) return;
392
+ state.clearButton.hidden = getSelection(state).length === 0 && state.input.value === '';
393
+ };
394
+
395
+ const syncSelectedOptions = (state) => {
396
+ state.options.forEach(option => {
397
+ if (state.selected.has(getValue(option))) {
398
+ option.setAttribute('aria-selected', 'true');
399
+ } else {
400
+ option.removeAttribute('aria-selected');
401
+ }
402
+ });
403
+ };
404
+
405
+ const setSelected = (root, entries, triggerEvent = true) => {
406
+ const state = states.get(root);
407
+ const normalized = (Array.isArray(entries) ? entries : [entries]).map(normalizeEntry).filter(Boolean);
408
+
409
+ state.selected.clear();
410
+ if (state.isMultiple) {
411
+ normalized.forEach(entry => state.selected.set(entry.value, entry));
412
+ state.input.value = '';
413
+ } else if (normalized[0]) {
414
+ state.selected.set(normalized[0].value, normalized[0]);
415
+ state.input.value = state.valueTarget ? '' : normalized[0].label;
416
+ } else {
417
+ state.input.value = '';
418
+ }
419
+
420
+ state.hiddenInput.value = serializeSelection(state);
421
+ syncSelectedOptions(state);
422
+ if (state.isMultiple) {
423
+ renderChips(root);
424
+ filterOptions(state, { preserveActive: true });
425
+ }
426
+ syncTriggerValue(state);
427
+ syncClearButton(state);
428
+
429
+ if (triggerEvent) {
430
+ root.dispatchEvent(new CustomEvent('change', {
431
+ detail: { value: getCanonicalValue(state), selected: getSelectedDetail(state) },
432
+ bubbles: true,
433
+ }));
434
+ }
435
+ };
436
+
437
+ const closePopover = (state, focusInput = false) => {
438
+ if (state.popover.getAttribute('aria-hidden') === 'true') return;
439
+ state.popover.setAttribute('aria-hidden', 'true');
440
+ state.input.setAttribute('aria-expanded', 'false');
441
+ state.trigger?.setAttribute('aria-expanded', 'false');
442
+ setActiveOption(state, -1);
443
+ if (focusInput) {
444
+ state.skipOpenOnFocus = true;
445
+ state.input.focus();
446
+ requestAnimationFrame(() => { state.skipOpenOnFocus = false; });
447
+ }
448
+ };
449
+
450
+ const openPopover = (state) => {
451
+ const { root } = state;
452
+ if (state.popover.getAttribute('aria-hidden') === 'false') return;
453
+ document.dispatchEvent(new CustomEvent('basecoat:popover', { detail: { source: root } }));
454
+ root.refresh();
455
+ const selected = getSelection(state)[0];
456
+ if (state.valueTarget) state.input.value = '';
457
+ const search = !state.isMultiple && selected && state.input.value === selected.label ? '' : undefined;
458
+ filterOptions(state, { search });
459
+ state.popover.setAttribute('aria-hidden', 'false');
460
+ state.input.setAttribute('aria-expanded', 'true');
461
+ state.trigger?.setAttribute('aria-expanded', 'true');
462
+ if (state.trigger) state.input.focus();
463
+ };
464
+
465
+ const refreshCombobox = (root) => {
466
+ const state = states.get(root);
467
+ if (!state) return;
468
+
469
+ let elements = getElements(root);
470
+ if (!elements.input || !elements.popover || !elements.listbox || !elements.hiddenInput) {
471
+ const missing = [];
472
+ if (!elements.input) missing.push('input');
473
+ if (!elements.popover) missing.push('popover');
474
+ if (!elements.listbox) missing.push('listbox');
475
+ if (!elements.hiddenInput) missing.push('hidden input');
476
+ console.error(`Combobox refresh failed. Missing element(s): ${missing.join(', ')}`, root);
477
+ return;
478
+ }
479
+
480
+ elements = ensureMultipleInputSurface(root, elements);
481
+ const previousValue = elements.hiddenInput.value;
482
+ const previousInputValue = elements.input.value;
483
+ Object.assign(state, elements, getOptions(elements.listbox));
484
+ state.isMultiple = state.listbox.getAttribute('aria-multiselectable') === 'true';
485
+ state.closeOnSelect = root.dataset.closeOnSelect === 'true';
486
+ state.autoHighlight = root.dataset.autoHighlight === 'true';
487
+ state.manualFilter = root.dataset.filter === 'manual';
488
+ state.format = getFormat(root);
489
+
490
+ const stored = parseStoredSelection(previousValue, previousInputValue, state);
491
+ if (stored.length > 0) {
492
+ setSelected(root, stored, false);
493
+ } else {
494
+ const selectedOptions = state.options.filter(option => option.getAttribute('aria-selected') === 'true').map(toSelected);
495
+ if (selectedOptions.length > 0) {
496
+ setSelected(root, state.isMultiple ? selectedOptions : selectedOptions[0], false);
497
+ } else if (state.isMultiple) {
498
+ setSelected(root, [], false);
499
+ } else {
500
+ state.input.value = previousInputValue;
501
+ state.selected.clear();
502
+ state.hiddenInput.value = '';
503
+ syncSelectedOptions(state);
504
+ filterOptions(state, { preserveActive: true });
505
+ }
506
+ }
507
+ syncTriggerValue(state);
508
+ syncClearButton(state);
509
+ };
510
+
511
+ const resolveEntry = (state, value) => {
512
+ if (value && typeof value === 'object') {
513
+ const entry = normalizeEntry(value);
514
+ if (!entry) return null;
515
+ const option = state.options.find(opt => getValue(opt) === entry.value);
516
+ return option ? toSelected(option) : entry;
517
+ }
518
+
519
+ const option = state.options.find(opt => getValue(opt) === value);
520
+ if (option) return toSelected(option);
521
+ const existing = state.selected.get(String(value));
522
+ return existing || null;
523
+ };
524
+
525
+ const selectValue = (root, value) => {
526
+ const state = states.get(root);
527
+ const entry = resolveEntry(state, value);
528
+ if (!entry) return;
529
+
530
+ if (state.isMultiple) {
531
+ setSelected(root, [...getSelection(state).filter(item => item.value !== entry.value), entry]);
532
+ if (state.closeOnSelect) root.close(true);
533
+ } else {
534
+ setSelected(root, entry);
535
+ root.close(state.trigger ? false : true);
536
+ state.trigger?.focus();
537
+ }
538
+ };
539
+
540
+ const deselectValue = (root, value) => {
541
+ const state = states.get(root);
542
+ if (!state.isMultiple) return;
543
+ const normalized = String(value);
544
+ if (!state.selected.has(normalized)) return;
545
+ setSelected(root, getSelection(state).filter(item => item.value !== normalized));
546
+ };
547
+
548
+ const clearValue = (root) => {
549
+ const state = states.get(root);
550
+ setSelected(root, [], true);
551
+ state.input.value = '';
552
+ filterOptions(state);
553
+ syncClearButton(state);
554
+ if (state.popover.getAttribute('aria-hidden') === 'false') state.input.focus();
555
+ };
556
+
557
+ const handleKeydown = (event, root) => {
558
+ const state = states.get(root);
559
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape', 'Backspace'].includes(event.key)) return;
560
+
561
+ const isOpen = state.popover.getAttribute('aria-hidden') === 'false';
562
+
563
+ if (event.key === 'Backspace' && state.isMultiple && state.input.value === '') {
564
+ const selected = getSelection(state);
565
+ const last = selected[selected.length - 1];
566
+ if (last) root.deselect(last.value);
567
+ return;
568
+ }
569
+
570
+ if (event.key === 'Escape') {
571
+ if (isOpen) {
572
+ event.preventDefault();
573
+ event.stopPropagation();
574
+ root.close(true);
575
+ }
576
+ return;
577
+ }
578
+
579
+ if (!isOpen && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
580
+ event.preventDefault();
581
+ openPopover(state);
582
+ }
583
+
584
+ if (state.popover.getAttribute('aria-hidden') === 'true') return;
585
+
586
+ if (event.key === 'Enter') {
587
+ if (state.activeIndex > -1) {
588
+ event.preventDefault();
589
+ const option = state.options[state.activeIndex];
590
+ state.isMultiple ? root.toggle(getValue(option)) : root.select(getValue(option));
591
+ }
592
+ return;
593
+ }
594
+
595
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || state.visibleOptions.length === 0) return;
596
+ event.preventDefault();
597
+
598
+ const currentVisibleIndex = state.activeIndex > -1 ? state.visibleOptions.indexOf(state.options[state.activeIndex]) : -1;
599
+ let nextVisibleIndex = currentVisibleIndex;
600
+
601
+ if (event.key === 'ArrowDown') nextVisibleIndex = Math.min(currentVisibleIndex + 1, state.visibleOptions.length - 1);
602
+ if (event.key === 'ArrowUp') nextVisibleIndex = currentVisibleIndex <= 0 ? 0 : currentVisibleIndex - 1;
603
+ if (event.key === 'Home') nextVisibleIndex = 0;
604
+ if (event.key === 'End') nextVisibleIndex = state.visibleOptions.length - 1;
605
+
606
+ const nextOption = state.visibleOptions[nextVisibleIndex];
607
+ setActiveOption(state, state.options.indexOf(nextOption));
608
+ scrollOptionIntoListbox(state, nextOption);
609
+ };
610
+
611
+ const initCombobox = (root) => {
612
+ if (root.dataset.comboboxInitialized) return;
613
+
614
+ const state = { root, activeIndex: -1, allOptions: [], options: [], visibleOptions: [], selected: new Map(), format: 'value', manualFilter: false, skipOpenOnFocus: false };
615
+ states.set(root, state);
616
+ root.refresh = () => refreshCombobox(root);
617
+
618
+ refreshCombobox(root);
619
+ if (!state.input || !state.popover || !state.listbox || !state.hiddenInput) {
620
+ states.delete(root);
621
+ delete root.refresh;
622
+ return;
623
+ }
624
+
625
+ root.close = (focusInput = false) => closePopover(state, focusInput);
626
+
627
+ root.select = (value) => selectValue(root, value);
628
+ root.selectByValue = root.select;
629
+ root.setValue = (value) => {
630
+ const entries = state.isMultiple ? (Array.isArray(value) ? value : (value == null ? [] : [value])) : [value];
631
+ const resolved = entries.map(entry => resolveEntry(state, entry) || normalizeEntry(entry)).filter(Boolean);
632
+ setSelected(root, state.isMultiple ? resolved : resolved[0]);
633
+ };
634
+ if (state.isMultiple) {
635
+ root.deselect = (value) => deselectValue(root, value);
636
+ root.toggle = (value) => {
637
+ const entry = resolveEntry(state, value);
638
+ if (!entry) return;
639
+ state.selected.has(entry.value) ? root.deselect(entry.value) : root.select(entry);
640
+ };
641
+ root.selectAll = () => setSelected(root, state.options.map(toSelected));
642
+ root.selectNone = () => setSelected(root, []);
643
+ }
644
+
645
+ const handleInputFocus = () => {
646
+ if (state.skipOpenOnFocus) return;
647
+ openPopover(state);
648
+ };
649
+ const handleInputClick = () => openPopover(state);
650
+ const handleInput = () => {
651
+ openPopover(state);
652
+ filterOptions(state);
653
+ if (!state.isMultiple) {
654
+ state.hiddenInput.value = '';
655
+ state.selected.clear();
656
+ syncSelectedOptions(state);
657
+ syncTriggerValue(state);
658
+ syncClearButton(state);
659
+ }
660
+ };
661
+ const handleTriggerClick = () => {
662
+ if (state.popover.getAttribute('aria-hidden') === 'false') {
663
+ root.close(false);
664
+ } else {
665
+ openPopover(state);
666
+ }
667
+ };
668
+ const handleClearClick = (event) => {
669
+ event.preventDefault();
670
+ event.stopPropagation();
671
+ clearValue(root);
672
+ };
673
+ const handleInputKeydown = (event) => handleKeydown(event, root);
674
+ const handleListboxMousemove = (event) => {
675
+ const option = event.target.closest('[role="option"]');
676
+ if (option && state.visibleOptions.includes(option)) setActiveOption(state, state.options.indexOf(option));
677
+ };
678
+ const handleListboxClick = (event) => {
679
+ const option = event.target.closest('[role="option"]');
680
+ if (!option || !state.options.includes(option)) return;
681
+ state.isMultiple ? root.toggle(getValue(option)) : root.select(getValue(option));
682
+ if (state.isMultiple && !state.closeOnSelect) state.input.focus();
683
+ };
684
+ const handleDocumentClick = (event) => {
685
+ if (!root.contains(event.target)) root.close(false);
686
+ };
687
+ const handleDocumentPopover = (event) => {
688
+ if (!root.contains(event.detail.source)) root.close(false);
689
+ };
690
+
691
+ state.input.addEventListener('focus', handleInputFocus);
692
+ state.input.addEventListener('click', handleInputClick);
693
+ state.input.addEventListener('input', handleInput);
694
+ state.input.addEventListener('keydown', handleInputKeydown);
695
+ state.trigger?.addEventListener('click', handleTriggerClick);
696
+ state.clearButton?.addEventListener('click', handleClearClick);
697
+ state.listbox.addEventListener('mousemove', handleListboxMousemove);
698
+ state.listbox.addEventListener('click', handleListboxClick);
699
+ document.addEventListener('click', handleDocumentClick);
700
+ document.addEventListener('basecoat:popover', handleDocumentPopover);
701
+
702
+ root._destroy = () => {
703
+ state.input.removeEventListener('focus', handleInputFocus);
704
+ state.input.removeEventListener('click', handleInputClick);
705
+ state.input.removeEventListener('input', handleInput);
706
+ state.input.removeEventListener('keydown', handleInputKeydown);
707
+ state.trigger?.removeEventListener('click', handleTriggerClick);
708
+ state.clearButton?.removeEventListener('click', handleClearClick);
709
+ state.listbox.removeEventListener('mousemove', handleListboxMousemove);
710
+ state.listbox.removeEventListener('click', handleListboxClick);
711
+ document.removeEventListener('click', handleDocumentClick);
712
+ document.removeEventListener('basecoat:popover', handleDocumentPopover);
713
+ state.chips?.querySelectorAll('.combobox-chip').forEach(chip => chip.remove());
714
+ states.delete(root);
715
+ delete root.refresh;
716
+ delete root.close;
717
+ delete root.select;
718
+ delete root.selectByValue;
719
+ delete root.setValue;
720
+ delete root.clear;
721
+ delete root.deselect;
722
+ delete root.toggle;
723
+ delete root.selectAll;
724
+ delete root.selectNone;
725
+ };
726
+
727
+ state.popover.setAttribute('aria-hidden', 'true');
728
+ state.input.setAttribute('aria-expanded', 'false');
729
+ state.trigger?.setAttribute('aria-expanded', 'false');
730
+ root.clear = () => clearValue(root);
731
+
732
+ Object.defineProperty(root, 'value', {
733
+ configurable: true,
734
+ get: () => getCanonicalValue(state),
735
+ set: (value) => root.setValue(value),
736
+ });
737
+
738
+ Object.defineProperty(root, 'selected', {
739
+ configurable: true,
740
+ get: () => getSelectedDetail(state),
741
+ });
742
+
743
+ root.dataset.comboboxInitialized = 'true';
744
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
745
+ };
746
+
747
+ if (window.basecoat) {
748
+ window.basecoat.register('combobox', {
749
+ selector: '.combobox:not([data-combobox-initialized])',
750
+ init: initCombobox,
751
+ refresh: refreshCombobox,
752
+ });
753
+ }
754
+ })();
755
+
756
+ // Loaded after the DOM is ready: initialize now.
757
+ if (document.readyState !== 'loading') { window.basecoat.initAll(); window.basecoat.start(); }