@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,646 @@
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 trigger = root.querySelector(':scope > button');
184
+ const selectedLabel = trigger?.querySelector(':scope > span') || null;
185
+ const popover = root.querySelector(':scope > [data-popover]');
186
+ const listbox = popover ? popover.querySelector('[role="listbox"]') : null;
187
+ const input = root.querySelector(':scope > input[type="hidden"]');
188
+ return { trigger, selectedLabel, popover, listbox, input };
189
+ };
190
+
191
+ const getValue = (option) => option.dataset.value ?? option.textContent.trim();
192
+ const getLabel = (option) => option.dataset.label || option.textContent.trim();
193
+ const getFormat = (root) => root.dataset.format === 'object' ? 'object' : 'value';
194
+ const isDisabled = (option) => option.getAttribute('aria-disabled') === 'true';
195
+ const toSelected = (option) => ({ value: getValue(option), label: getLabel(option) });
196
+
197
+ const getOptions = (listbox) => {
198
+ const allOptions = Array.from(listbox.querySelectorAll('[role="option"]'));
199
+ return {
200
+ allOptions,
201
+ options: allOptions.filter(option => !isDisabled(option)),
202
+ };
203
+ };
204
+
205
+ const parseStoredValues = (storedValue, { isMultiple, format }) => {
206
+ if (isMultiple) {
207
+ try {
208
+ const parsed = JSON.parse(storedValue || '[]');
209
+ if (!Array.isArray(parsed)) return [];
210
+ return parsed
211
+ .map(item => format === 'object' && item && typeof item === 'object' ? item.value : item)
212
+ .filter(value => value != null)
213
+ .map(String);
214
+ } catch (_) {
215
+ return [];
216
+ }
217
+ }
218
+
219
+ if (format === 'object') {
220
+ try {
221
+ const parsed = JSON.parse(storedValue || 'null');
222
+ return parsed && typeof parsed === 'object' && parsed.value != null ? String(parsed.value) : '';
223
+ } catch (_) {
224
+ return '';
225
+ }
226
+ }
227
+
228
+ return storedValue || '';
229
+ };
230
+
231
+ const serializeSelection = (state, selected) => {
232
+ if (state.format === 'object') {
233
+ return JSON.stringify(state.isMultiple ? selected : (selected[0] || null));
234
+ }
235
+
236
+ const value = selected.map(item => item.value);
237
+ return state.isMultiple ? JSON.stringify(value) : (value[0] || '');
238
+ };
239
+
240
+ const showPlaceholder = (state) => {
241
+ state.selectedLabel.textContent = state.placeholder || '';
242
+ state.selectedLabel.classList.toggle('text-muted-foreground', Boolean(state.placeholder));
243
+ state.input.value = state.isMultiple ? serializeSelection(state, []) : '';
244
+ };
245
+
246
+ const scrollOptionIntoListbox = (state, option) => {
247
+ const optionRect = option.getBoundingClientRect();
248
+ const listboxRect = state.listbox.getBoundingClientRect();
249
+
250
+ if (optionRect.top < listboxRect.top) {
251
+ state.listbox.scrollTop -= listboxRect.top - optionRect.top;
252
+ } else if (optionRect.bottom > listboxRect.bottom) {
253
+ state.listbox.scrollTop += optionRect.bottom - listboxRect.bottom;
254
+ }
255
+ };
256
+
257
+ const setActiveOption = (state, index) => {
258
+ if (state.activeIndex > -1 && state.options[state.activeIndex]) {
259
+ state.options[state.activeIndex].classList.remove('active');
260
+ }
261
+
262
+ state.activeIndex = index;
263
+
264
+ if (state.activeIndex > -1) {
265
+ const activeOption = state.options[state.activeIndex];
266
+ activeOption.classList.add('active');
267
+ if (activeOption.id) {
268
+ state.trigger.setAttribute('aria-activedescendant', activeOption.id);
269
+ } else {
270
+ state.trigger.removeAttribute('aria-activedescendant');
271
+ }
272
+ } else {
273
+ state.trigger.removeAttribute('aria-activedescendant');
274
+ }
275
+ };
276
+
277
+ const updateValue = (root, optionOrOptions, triggerEvent = true) => {
278
+ const state = states.get(root);
279
+ let value;
280
+ let selectedDetail;
281
+
282
+ if (state.isMultiple) {
283
+ const selected = Array.isArray(optionOrOptions) ? optionOrOptions : [];
284
+ state.selectedOptions.clear();
285
+ selected.forEach(option => state.selectedOptions.add(option));
286
+
287
+ const selectedInOrder = state.options.filter(option => state.selectedOptions.has(option));
288
+ selectedDetail = selectedInOrder.map(toSelected);
289
+ if (selectedInOrder.length === 0) {
290
+ state.selectedLabel.textContent = state.placeholder;
291
+ state.selectedLabel.classList.add('text-muted-foreground');
292
+ } else {
293
+ state.selectedLabel.textContent = selectedDetail.map(item => item.label).join(', ');
294
+ state.selectedLabel.classList.remove('text-muted-foreground');
295
+ }
296
+
297
+ value = selectedDetail.map(item => item.value);
298
+ state.input.value = serializeSelection(state, selectedDetail);
299
+ } else {
300
+ const option = optionOrOptions;
301
+ if (!option) {
302
+ state.options.forEach(option => option.removeAttribute('aria-selected'));
303
+ showPlaceholder(state);
304
+ selectedDetail = null;
305
+ value = '';
306
+ } else {
307
+ if (option.dataset.label) {
308
+ state.selectedLabel.textContent = option.dataset.label;
309
+ } else {
310
+ state.selectedLabel.innerHTML = option.innerHTML;
311
+ }
312
+ state.selectedLabel.classList.remove('text-muted-foreground');
313
+ selectedDetail = toSelected(option);
314
+ value = selectedDetail.value;
315
+ state.input.value = serializeSelection(state, [selectedDetail]);
316
+ }
317
+ }
318
+
319
+ state.options.forEach(option => {
320
+ const isSelected = state.isMultiple ? state.selectedOptions.has(option) : optionOrOptions && option === optionOrOptions;
321
+ if (isSelected) {
322
+ option.setAttribute('aria-selected', 'true');
323
+ } else {
324
+ option.removeAttribute('aria-selected');
325
+ }
326
+ });
327
+
328
+ if (triggerEvent) {
329
+ root.dispatchEvent(new CustomEvent('change', {
330
+ detail: { value, selected: selectedDetail },
331
+ bubbles: true,
332
+ }));
333
+ }
334
+ };
335
+
336
+ const closePopover = (state, focusOnTrigger = true) => {
337
+ if (state.popover.getAttribute('aria-hidden') === 'true') return;
338
+ if (focusOnTrigger) state.trigger.focus();
339
+ state.popover.setAttribute('aria-hidden', 'true');
340
+ state.trigger.setAttribute('aria-expanded', 'false');
341
+ setActiveOption(state, -1);
342
+ };
343
+
344
+ const refreshSelect = (root) => {
345
+ const state = states.get(root);
346
+ if (!state) return;
347
+
348
+ const elements = getElements(root);
349
+ if (!elements.trigger || !elements.selectedLabel || !elements.popover || !elements.listbox || !elements.input) {
350
+ const missing = [];
351
+ if (!elements.trigger) missing.push('trigger');
352
+ if (!elements.selectedLabel) missing.push('selected label');
353
+ if (!elements.popover) missing.push('popover');
354
+ if (!elements.listbox) missing.push('listbox');
355
+ if (!elements.input) missing.push('input');
356
+ console.error(`Select component refresh failed. Missing element(s): ${missing.join(', ')}`, root);
357
+ return;
358
+ }
359
+
360
+ const previousValue = elements.input.value;
361
+ Object.assign(state, elements, getOptions(elements.listbox));
362
+ state.visibleOptions = [...state.options];
363
+ state.isMultiple = state.listbox.getAttribute('aria-multiselectable') === 'true';
364
+ state.format = getFormat(root);
365
+ state.placeholder = root.dataset.placeholder || '';
366
+ state.closeOnSelect = root.dataset.closeOnSelect === 'true';
367
+
368
+ if (state.isMultiple) {
369
+ if (!state.selectedOptions) state.selectedOptions = new Set();
370
+ const values = parseStoredValues(previousValue, state);
371
+ const selected = values.length
372
+ ? values.map(value => state.options.find(option => getValue(option) === value)).filter(Boolean)
373
+ : state.options.filter(option => option.getAttribute('aria-selected') === 'true');
374
+ updateValue(root, selected, false);
375
+ } else {
376
+ const value = parseStoredValues(previousValue, state);
377
+ const selected = value === '' && state.placeholder
378
+ ? null
379
+ : state.options.find(option => getValue(option) === value)
380
+ || state.options.find(option => option.getAttribute('aria-selected') === 'true');
381
+ state.options.forEach(option => option.removeAttribute('aria-selected'));
382
+ updateValue(root, selected || null, false);
383
+ }
384
+
385
+ const selectedOption = state.listbox.querySelector('[role="option"][aria-selected="true"]');
386
+ setActiveOption(state, selectedOption ? state.options.indexOf(selectedOption) : -1);
387
+ };
388
+
389
+ const toggleMultipleValue = (root, option) => {
390
+ const state = states.get(root);
391
+ if (state.selectedOptions.has(option)) {
392
+ state.selectedOptions.delete(option);
393
+ } else {
394
+ state.selectedOptions.add(option);
395
+ }
396
+ updateValue(root, state.options.filter(opt => state.selectedOptions.has(opt)));
397
+ };
398
+
399
+ const selectValue = (root, value) => {
400
+ const state = states.get(root);
401
+ if (state.isMultiple) {
402
+ const option = state.options.find(opt => getValue(opt) === value && !state.selectedOptions.has(opt));
403
+ if (!option) return;
404
+ state.selectedOptions.add(option);
405
+ updateValue(root, state.options.filter(opt => state.selectedOptions.has(opt)));
406
+ } else {
407
+ const option = state.options.find(opt => getValue(opt) === value);
408
+ if (!option) return;
409
+ if (state.placeholder && getValue(option) === '') {
410
+ updateValue(root, null);
411
+ closePopover(state);
412
+ return;
413
+ }
414
+ if (root.value !== value) updateValue(root, option);
415
+ closePopover(state);
416
+ }
417
+ };
418
+
419
+ const deselectValue = (root, value) => {
420
+ const state = states.get(root);
421
+ if (!state.isMultiple) return;
422
+ const option = state.options.find(opt => getValue(opt) === value && state.selectedOptions.has(opt));
423
+ if (!option) return;
424
+ state.selectedOptions.delete(option);
425
+ updateValue(root, state.options.filter(opt => state.selectedOptions.has(opt)));
426
+ };
427
+
428
+ const handleKeyNavigation = (event, root) => {
429
+ const state = states.get(root);
430
+ const isPopoverOpen = state.popover.getAttribute('aria-hidden') === 'false';
431
+
432
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(event.key)) return;
433
+
434
+ if (!isPopoverOpen) {
435
+ if (event.key !== 'Enter' && event.key !== 'Escape') {
436
+ event.preventDefault();
437
+ root.open();
438
+ }
439
+ return;
440
+ }
441
+
442
+ event.preventDefault();
443
+
444
+ if (event.key === 'Escape') {
445
+ root.close();
446
+ return;
447
+ }
448
+
449
+ if (event.key === 'Enter') {
450
+ if (state.activeIndex > -1) {
451
+ const option = state.options[state.activeIndex];
452
+ if (state.isMultiple) {
453
+ toggleMultipleValue(root, option);
454
+ if (state.closeOnSelect) root.close();
455
+ } else {
456
+ if (state.placeholder && getValue(option) === '') {
457
+ updateValue(root, null);
458
+ } else if (root.value !== getValue(option)) {
459
+ updateValue(root, option);
460
+ }
461
+ root.close();
462
+ }
463
+ }
464
+ return;
465
+ }
466
+
467
+ if (state.visibleOptions.length === 0) return;
468
+
469
+ const currentVisibleIndex = state.activeIndex > -1 ? state.visibleOptions.indexOf(state.options[state.activeIndex]) : -1;
470
+ let nextVisibleIndex = currentVisibleIndex;
471
+
472
+ if (event.key === 'ArrowDown' && currentVisibleIndex < state.visibleOptions.length - 1) nextVisibleIndex = currentVisibleIndex + 1;
473
+ if (event.key === 'ArrowUp') nextVisibleIndex = currentVisibleIndex > 0 ? currentVisibleIndex - 1 : 0;
474
+ if (event.key === 'Home') nextVisibleIndex = 0;
475
+ if (event.key === 'End') nextVisibleIndex = state.visibleOptions.length - 1;
476
+
477
+ if (nextVisibleIndex !== currentVisibleIndex) {
478
+ const newActiveOption = state.visibleOptions[nextVisibleIndex];
479
+ setActiveOption(state, state.options.indexOf(newActiveOption));
480
+ scrollOptionIntoListbox(state, newActiveOption);
481
+ }
482
+ };
483
+
484
+ const initSelect = (root) => {
485
+ if (root.dataset.selectInitialized) return;
486
+
487
+ const state = { activeIndex: -1, selectedOptions: null, options: [], allOptions: [], visibleOptions: [], format: 'value' };
488
+ states.set(root, state);
489
+ root.refresh = () => refreshSelect(root);
490
+
491
+ refreshSelect(root);
492
+ if (!state.trigger || !state.selectedLabel || !state.popover || !state.listbox || !state.input) {
493
+ states.delete(root);
494
+ delete root.refresh;
495
+ return;
496
+ }
497
+
498
+ root.open = () => {
499
+ document.dispatchEvent(new CustomEvent('basecoat:popover', { detail: { source: root } }));
500
+ root.refresh();
501
+ state.popover.setAttribute('aria-hidden', 'false');
502
+ state.trigger.setAttribute('aria-expanded', 'true');
503
+
504
+ const selectedOption = state.listbox.querySelector('[role="option"][aria-selected="true"]');
505
+ if (selectedOption) {
506
+ setActiveOption(state, state.options.indexOf(selectedOption));
507
+ scrollOptionIntoListbox(state, selectedOption);
508
+ }
509
+ };
510
+ root.close = (focusOnTrigger = true) => closePopover(state, focusOnTrigger);
511
+ root.togglePopover = () => state.trigger.getAttribute('aria-expanded') === 'true' ? root.close() : root.open();
512
+
513
+ const handleTriggerKeydown = (event) => handleKeyNavigation(event, root);
514
+ const handleTriggerClick = root.togglePopover;
515
+ const handleListboxMousemove = (event) => {
516
+ const option = event.target.closest('[role="option"]');
517
+ if (option && state.visibleOptions.includes(option)) {
518
+ const index = state.options.indexOf(option);
519
+ if (index !== state.activeIndex) setActiveOption(state, index);
520
+ }
521
+ };
522
+ const handleListboxMouseleave = () => {
523
+ const selectedOption = state.listbox.querySelector('[role="option"][aria-selected="true"]');
524
+ setActiveOption(state, selectedOption ? state.options.indexOf(selectedOption) : -1);
525
+ };
526
+ const handleListboxClick = (event) => {
527
+ const clickedOption = event.target.closest('[role="option"]');
528
+ if (!clickedOption) return;
529
+
530
+ const option = state.options.find(opt => opt === clickedOption);
531
+ if (!option) return;
532
+
533
+ if (state.isMultiple) {
534
+ toggleMultipleValue(root, option);
535
+ if (state.closeOnSelect) {
536
+ root.close();
537
+ } else {
538
+ setActiveOption(state, state.options.indexOf(option));
539
+ state.trigger.focus();
540
+ }
541
+ } else {
542
+ if (state.placeholder && getValue(option) === '') {
543
+ updateValue(root, null);
544
+ } else if (root.value !== getValue(option)) {
545
+ updateValue(root, option);
546
+ }
547
+ root.close();
548
+ }
549
+ };
550
+ const handleDocumentClick = (event) => {
551
+ if (!root.contains(event.target)) root.close(false);
552
+ };
553
+ const handleDocumentPopover = (event) => {
554
+ if (!root.contains(event.detail.source)) root.close(false);
555
+ };
556
+
557
+ state.trigger.addEventListener('keydown', handleTriggerKeydown);
558
+ state.trigger.addEventListener('click', handleTriggerClick);
559
+ state.listbox.addEventListener('mousemove', handleListboxMousemove);
560
+ state.listbox.addEventListener('mouseleave', handleListboxMouseleave);
561
+ state.listbox.addEventListener('click', handleListboxClick);
562
+ document.addEventListener('click', handleDocumentClick);
563
+ document.addEventListener('basecoat:popover', handleDocumentPopover);
564
+
565
+ root._destroy = () => {
566
+ state.trigger.removeEventListener('keydown', handleTriggerKeydown);
567
+ state.trigger.removeEventListener('click', handleTriggerClick);
568
+ state.listbox.removeEventListener('mousemove', handleListboxMousemove);
569
+ state.listbox.removeEventListener('mouseleave', handleListboxMouseleave);
570
+ state.listbox.removeEventListener('click', handleListboxClick);
571
+ document.removeEventListener('click', handleDocumentClick);
572
+ document.removeEventListener('basecoat:popover', handleDocumentPopover);
573
+ states.delete(root);
574
+ delete root.refresh;
575
+ delete root.open;
576
+ delete root.close;
577
+ delete root.togglePopover;
578
+ delete root.select;
579
+ delete root.selectByValue;
580
+ delete root.deselect;
581
+ delete root.toggle;
582
+ delete root.selectAll;
583
+ delete root.selectNone;
584
+ };
585
+
586
+ Object.defineProperty(root, 'value', {
587
+ configurable: true,
588
+ get: () => state.isMultiple ? state.options.filter(option => state.selectedOptions.has(option)).map(getValue) : parseStoredValues(state.input.value, state),
589
+ set: (value) => {
590
+ if (state.isMultiple) {
591
+ const values = Array.isArray(value) ? value : (value != null ? [value] : []);
592
+ updateValue(root, values.map(v => state.options.find(option => getValue(option) === v)).filter(Boolean));
593
+ } else {
594
+ if (value == null || value === '') {
595
+ updateValue(root, null);
596
+ root.close();
597
+ return;
598
+ }
599
+ const option = state.options.find(opt => getValue(opt) === value);
600
+ if (option) {
601
+ updateValue(root, option);
602
+ root.close();
603
+ }
604
+ }
605
+ },
606
+ });
607
+
608
+ Object.defineProperty(root, 'selected', {
609
+ configurable: true,
610
+ get: () => {
611
+ if (state.isMultiple) return state.options.filter(option => state.selectedOptions.has(option)).map(toSelected);
612
+ const value = root.value;
613
+ const option = state.options.find(opt => getValue(opt) === value);
614
+ return option ? toSelected(option) : null;
615
+ },
616
+ });
617
+
618
+ root.select = (value) => selectValue(root, value);
619
+ root.selectByValue = root.select;
620
+ if (state.isMultiple) {
621
+ root.deselect = (value) => deselectValue(root, value);
622
+ root.toggle = (value) => {
623
+ const option = state.options.find(opt => getValue(opt) === value);
624
+ if (option) toggleMultipleValue(root, option);
625
+ };
626
+ root.selectAll = () => updateValue(root, state.options);
627
+ root.selectNone = () => updateValue(root, []);
628
+ }
629
+
630
+ state.popover.setAttribute('aria-hidden', 'true');
631
+ state.trigger.setAttribute('aria-expanded', 'false');
632
+ root.dataset.selectInitialized = 'true';
633
+ root.dispatchEvent(new CustomEvent('basecoat:initialized'));
634
+ };
635
+
636
+ if (window.basecoat) {
637
+ window.basecoat.register('select', {
638
+ selector: 'div.select:not([data-select-initialized])',
639
+ init: initSelect,
640
+ refresh: refreshSelect,
641
+ });
642
+ }
643
+ })();
644
+
645
+ // Loaded after the DOM is ready: initialize now.
646
+ if (document.readyState !== 'loading') { window.basecoat.initAll(); window.basecoat.start(); }