@wovenui/components 0.0.0-stage → 1.0.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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -2
  3. package/dist/components.json +4204 -0
  4. package/dist/llms-full.txt +2839 -0
  5. package/dist/wovenui-kit.css +5002 -0
  6. package/dist/wovenui-kit.css.map +1 -0
  7. package/dist/wovenui-kit.iife.js +532 -0
  8. package/dist/wovenui-kit.js +529 -0
  9. package/dist/wovenui-kit.min.css +2 -0
  10. package/dist/wovenui-kit.min.css.map +1 -0
  11. package/package.json +268 -4
  12. package/src/_close.scss +45 -0
  13. package/src/_helpers.scss +11 -0
  14. package/src/_kit.scss +124 -0
  15. package/src/_shared.scss +10 -0
  16. package/src/components/_accordion.scss +132 -0
  17. package/src/components/_alert.scss +161 -0
  18. package/src/components/_autocomplete.scss +129 -0
  19. package/src/components/_avatar-group.scss +80 -0
  20. package/src/components/_avatar.scss +134 -0
  21. package/src/components/_badge.scss +91 -0
  22. package/src/components/_breadcrumb.scss +115 -0
  23. package/src/components/_btn-group.scss +113 -0
  24. package/src/components/_button.scss +301 -0
  25. package/src/components/_card.scss +120 -0
  26. package/src/components/_checkbox.scss +168 -0
  27. package/src/components/_code-block.scss +78 -0
  28. package/src/components/_confirm-modal.scss +75 -0
  29. package/src/components/_data-table.scss +79 -0
  30. package/src/components/_desc-list.scss +180 -0
  31. package/src/components/_divider.scss +66 -0
  32. package/src/components/_dot.scss +115 -0
  33. package/src/components/_drawer.scss +200 -0
  34. package/src/components/_dropdown.scss +182 -0
  35. package/src/components/_empty-state.scss +146 -0
  36. package/src/components/_field-group.scss +68 -0
  37. package/src/components/_field-message.scss +75 -0
  38. package/src/components/_field.scss +127 -0
  39. package/src/components/_form-actions.scss +58 -0
  40. package/src/components/_info-row.scss +69 -0
  41. package/src/components/_input-group.scss +87 -0
  42. package/src/components/_input.scss +278 -0
  43. package/src/components/_list.scss +76 -0
  44. package/src/components/_modal.scss +168 -0
  45. package/src/components/_page-header.scss +85 -0
  46. package/src/components/_pagination.scss +138 -0
  47. package/src/components/_panel.scss +69 -0
  48. package/src/components/_popover.scss +154 -0
  49. package/src/components/_progress-circle.scss +123 -0
  50. package/src/components/_progress.scss +122 -0
  51. package/src/components/_radio.scss +162 -0
  52. package/src/components/_segmented.scss +183 -0
  53. package/src/components/_select.scss +154 -0
  54. package/src/components/_sidebar.scss +300 -0
  55. package/src/components/_skeleton.scss +90 -0
  56. package/src/components/_spinner.scss +84 -0
  57. package/src/components/_stat.scss +92 -0
  58. package/src/components/_table.scss +460 -0
  59. package/src/components/_tabs.scss +327 -0
  60. package/src/components/_tag.scss +143 -0
  61. package/src/components/_text.scss +42 -0
  62. package/src/components/_textarea.scss +57 -0
  63. package/src/components/_timeline.scss +196 -0
  64. package/src/components/_title.scss +41 -0
  65. package/src/components/_toast.scss +221 -0
  66. package/src/components/_toggle.scss +171 -0
  67. package/src/components/_toolbar.scss +74 -0
  68. package/src/components/_topbar.scss +204 -0
  69. package/src/entries/accordion.scss +4 -0
  70. package/src/entries/alert.scss +4 -0
  71. package/src/entries/autocomplete.scss +4 -0
  72. package/src/entries/avatar-group.scss +4 -0
  73. package/src/entries/avatar.scss +4 -0
  74. package/src/entries/badge.scss +4 -0
  75. package/src/entries/breadcrumb.scss +4 -0
  76. package/src/entries/btn-group.scss +4 -0
  77. package/src/entries/button.scss +4 -0
  78. package/src/entries/card.scss +4 -0
  79. package/src/entries/checkbox.scss +4 -0
  80. package/src/entries/code-block.scss +4 -0
  81. package/src/entries/confirm-modal.scss +4 -0
  82. package/src/entries/data-table.scss +4 -0
  83. package/src/entries/desc-list.scss +4 -0
  84. package/src/entries/divider.scss +4 -0
  85. package/src/entries/dot.scss +4 -0
  86. package/src/entries/drawer.scss +4 -0
  87. package/src/entries/dropdown.scss +4 -0
  88. package/src/entries/empty-state.scss +4 -0
  89. package/src/entries/field-group.scss +4 -0
  90. package/src/entries/field-message.scss +4 -0
  91. package/src/entries/field.scss +4 -0
  92. package/src/entries/form-actions.scss +4 -0
  93. package/src/entries/info-row.scss +4 -0
  94. package/src/entries/input-group.scss +4 -0
  95. package/src/entries/input.scss +4 -0
  96. package/src/entries/list.scss +4 -0
  97. package/src/entries/modal.scss +4 -0
  98. package/src/entries/page-header.scss +4 -0
  99. package/src/entries/pagination.scss +4 -0
  100. package/src/entries/panel.scss +4 -0
  101. package/src/entries/popover.scss +4 -0
  102. package/src/entries/progress-circle.scss +4 -0
  103. package/src/entries/progress.scss +4 -0
  104. package/src/entries/radio.scss +4 -0
  105. package/src/entries/segmented.scss +4 -0
  106. package/src/entries/select.scss +4 -0
  107. package/src/entries/sidebar.scss +4 -0
  108. package/src/entries/skeleton.scss +4 -0
  109. package/src/entries/spinner.scss +4 -0
  110. package/src/entries/stat.scss +4 -0
  111. package/src/entries/table.scss +4 -0
  112. package/src/entries/tabs.scss +4 -0
  113. package/src/entries/tag.scss +4 -0
  114. package/src/entries/text.scss +4 -0
  115. package/src/entries/textarea.scss +4 -0
  116. package/src/entries/timeline.scss +4 -0
  117. package/src/entries/title.scss +4 -0
  118. package/src/entries/toast.scss +4 -0
  119. package/src/entries/toggle.scss +4 -0
  120. package/src/entries/toolbar.scss +4 -0
  121. package/src/entries/topbar.scss +4 -0
  122. package/src/index.scss +7 -0
  123. package/src/js/autocomplete.js +86 -0
  124. package/src/js/data-table.js +141 -0
  125. package/src/js/index.js +34 -0
  126. package/src/js/input-clear.js +25 -0
  127. package/src/js/package.json +3 -0
  128. package/src/js/tabs.js +73 -0
  129. package/src/js/toast.js +161 -0
  130. package/src/js/util.js +21 -0
@@ -0,0 +1,529 @@
1
+ // @wovenui/components wovenui-kit.js, MIT. Built from src/js.
2
+ // Shared helpers for the kit scripts
3
+ const started = new WeakSet();
4
+ let count = 0;
5
+
6
+ // Elements matching the selector in root (root included), each returned only once
7
+ function fresh(root, selector) {
8
+ const found = [...root.querySelectorAll(selector)];
9
+ if (root.matches?.(selector)) found.unshift(root);
10
+ return found.filter((el) => !started.has(el) && started.add(el));
11
+ }
12
+
13
+ // The element's id, assigned when missing
14
+ function idOf(el, prefix) {
15
+ if (!el.id) el.id = `${prefix}-${++count}`;
16
+ return el.id;
17
+ }
18
+
19
+ // True when the component opted into motion and the user allows it
20
+ const animated = (el, name) =>
21
+ el.matches(`.${name}.animated, .wu-animated .${name}`) &&
22
+ matchMedia('(prefers-reduced-motion: no-preference)').matches;
23
+
24
+ // Tabs: WAI-ARIA tabs pattern, roving tabindex, automatic activation
25
+
26
+ const usable = (tab) =>
27
+ !tab.disabled && tab.getAttribute('aria-disabled') !== 'true';
28
+
29
+ function initTabs(root = document) {
30
+ for (const tabs of fresh(root, '.wu-tabs')) {
31
+ const list = tabs.querySelector(':scope > .wu-tabs-list');
32
+ if (!list) continue;
33
+ const all = [...list.querySelectorAll(':scope > .wu-tabs-tab')];
34
+ const panels = [...tabs.querySelectorAll(':scope > .wu-tabs-panel')];
35
+ const panelOf = (tab) =>
36
+ document.getElementById(tab.getAttribute('aria-controls'));
37
+
38
+ list.setAttribute('role', 'tablist');
39
+ if (tabs.classList.contains('vertical'))
40
+ list.setAttribute('aria-orientation', 'vertical');
41
+ all.forEach((tab, i) => {
42
+ const panel = panelOf(tab) || panels[i];
43
+ tab.setAttribute('role', 'tab');
44
+ if (!panel) return;
45
+ tab.setAttribute('aria-controls', idOf(panel, 'wu-panel'));
46
+ panel.setAttribute('role', 'tabpanel');
47
+ panel.setAttribute('aria-labelledby', idOf(tab, 'wu-tab'));
48
+ if (!panel.hasAttribute('tabindex')) panel.tabIndex = 0;
49
+ });
50
+
51
+ const select = (tab, focus) => {
52
+ for (const t of all) {
53
+ const on = t === tab;
54
+ t.setAttribute('aria-selected', on);
55
+ t.tabIndex = on ? 0 : -1;
56
+ const panel = panelOf(t);
57
+ if (panel) panel.hidden = !on;
58
+ }
59
+ if (focus) tab.focus();
60
+ };
61
+ const first =
62
+ all.find((t) => t.getAttribute('aria-selected') === 'true') ||
63
+ all.find(usable);
64
+ if (first) select(first);
65
+
66
+ list.addEventListener('click', (e) => {
67
+ const tab = e.target.closest('.wu-tabs-tab');
68
+ if (all.includes(tab) && usable(tab)) select(tab);
69
+ });
70
+
71
+ list.addEventListener('keydown', (e) => {
72
+ const items = all.filter(usable);
73
+ const i = items.indexOf(e.target);
74
+ if (i < 0) return;
75
+ const vertical =
76
+ list.getAttribute('aria-orientation') === 'vertical';
77
+ const back = getComputedStyle(list).direction === 'rtl' ? 1 : -1;
78
+ const prev = items.at((i - 1) % items.length);
79
+ const after = items.at((i + 1) % items.length);
80
+ const next = {
81
+ ...(vertical
82
+ ? { ArrowUp: prev, ArrowDown: after }
83
+ : {
84
+ ArrowLeft: items.at((i + back) % items.length),
85
+ ArrowRight: items.at((i - back) % items.length),
86
+ }),
87
+ Home: items[0],
88
+ End: items.at(-1),
89
+ }[e.key];
90
+ if (!next) return;
91
+ e.preventDefault();
92
+ select(next, true);
93
+ });
94
+ }
95
+ }
96
+
97
+ // Toasts: a polite live region, auto-dismiss paused on hover and focus, never takes focus
98
+
99
+ const MAX = 3;
100
+ const timers = new WeakMap();
101
+ const keys = new WeakMap();
102
+ const open = new Map();
103
+ const before = new WeakMap();
104
+ const pending = new WeakSet();
105
+ const toastsOf = (region) =>
106
+ region.querySelectorAll(':scope > .wu-toast:not([hidden])');
107
+ const paused = (region) => region.matches(':hover, :focus-within');
108
+
109
+ function start(el) {
110
+ const t = timers.get(el);
111
+ if (!t || t.id) return;
112
+ t.start = Date.now();
113
+ t.id = setTimeout(() => dismiss(el), t.left);
114
+ }
115
+
116
+ function stop(el) {
117
+ const t = timers.get(el);
118
+ if (!t?.id) return;
119
+ clearTimeout(t.id);
120
+ t.id = 0;
121
+ t.left -= Date.now() - t.start;
122
+ }
123
+
124
+ // Focus target when a focused toast closes: the next toast's close button, else where focus was before
125
+ function focusAfter(el) {
126
+ const region = el.parentElement;
127
+ const rest = region ? [...toastsOf(region)].filter((t) => t !== el) : [];
128
+ const next =
129
+ rest.find(
130
+ (t) =>
131
+ el.compareDocumentPosition(t) & Node.DOCUMENT_POSITION_FOLLOWING
132
+ ) ?? rest.at(-1);
133
+ const close = next?.querySelector('.wu-toast-close');
134
+ if (close) return close;
135
+ const back = region && before.get(region);
136
+ return back?.isConnected ? back : null;
137
+ }
138
+
139
+ // Hides the toast, then removes it once its exit transition ends
140
+ function dismiss(el, moveFocus = true) {
141
+ if (el.hidden) return;
142
+ if (moveFocus && el.contains(document.activeElement))
143
+ focusAfter(el)?.focus();
144
+ stop(el);
145
+ timers.delete(el);
146
+ if (open.get(keys.get(el)) === el) open.delete(keys.get(el));
147
+ el.hidden = true;
148
+ const done = () => el.remove();
149
+ Promise.all(el.getAnimations().map((a) => a.finished)).then(done, done);
150
+ }
151
+
152
+ function arm(el, region, ms) {
153
+ if (ms > 0) timers.set(el, { left: ms, ms, id: 0 });
154
+ if (!paused(region)) start(el);
155
+ }
156
+
157
+ // Closes the oldest toasts that time out, until at most MAX are open; never the new one or a focused one
158
+ function cap(region, added) {
159
+ const shown = [...toastsOf(region)];
160
+ let extra = shown.length - MAX;
161
+ for (const el of shown) {
162
+ if (extra <= 0) break;
163
+ if (
164
+ el === added ||
165
+ !timers.has(el) ||
166
+ el.contains(document.activeElement)
167
+ )
168
+ continue;
169
+ dismiss(el, false);
170
+ extra--;
171
+ }
172
+ }
173
+
174
+ function wire(root) {
175
+ for (const region of fresh(root, '.wu-toast-region')) {
176
+ const resume = () => toastsOf(region).forEach(start);
177
+ // detail 0: activated from the keyboard, so focus moves on; a pointer click lets it go
178
+ region.addEventListener('click', (e) => {
179
+ const close = e.target.closest('.wu-toast-close');
180
+ if (close) dismiss(close.closest('.wu-toast'), e.detail === 0);
181
+ });
182
+ region.addEventListener('mouseenter', () =>
183
+ toastsOf(region).forEach(stop)
184
+ );
185
+ region.addEventListener('focusin', (e) => {
186
+ if (e.relatedTarget && !region.contains(e.relatedTarget))
187
+ before.set(region, e.relatedTarget);
188
+ toastsOf(region).forEach(stop);
189
+ });
190
+ region.addEventListener('mouseleave', () => {
191
+ if (!region.matches(':focus-within')) resume();
192
+ });
193
+ region.addEventListener('focusout', (e) => {
194
+ if (!region.contains(e.relatedTarget) && !region.matches(':hover'))
195
+ resume();
196
+ });
197
+ for (const el of toastsOf(region))
198
+ arm(el, region, +(el.dataset.duration ?? 5000));
199
+ }
200
+ }
201
+
202
+ const initToast = (root = document) => wire(root);
203
+
204
+ // Shows a toast; returns its element. duration 0 keeps it until closed.
205
+ // A message and variant already open restart that toast's timer instead of adding another.
206
+ function toast(message, options = {}) {
207
+ const { title, variant, duration = 5000, closeLabel = 'Close' } = options;
208
+ const key = `${variant ?? ''}\n${message}`;
209
+ const same = open.get(key);
210
+ if (same && !same.hidden && (same.isConnected || pending.has(same))) {
211
+ const t = timers.get(same);
212
+ if (t) {
213
+ stop(same);
214
+ t.left = t.ms;
215
+ if (!paused(same.parentElement ?? same)) start(same);
216
+ }
217
+ return same;
218
+ }
219
+
220
+ let region = document.querySelector('.wu-toast-region');
221
+ if (!region) {
222
+ region = document.createElement('section');
223
+ region.className = 'wu-toast-region';
224
+ region.setAttribute('aria-live', 'polite');
225
+ region.setAttribute('aria-label', 'Notifications');
226
+ document.body.append(region);
227
+ }
228
+ wire(region);
229
+
230
+ const el = document.createElement('div');
231
+ el.className = variant ? `wu-toast ${variant}` : 'wu-toast';
232
+ const part = (tag, className, text) => {
233
+ const node = el.appendChild(document.createElement(tag));
234
+ if (className) node.className = className;
235
+ if (text) node.textContent = text;
236
+ return node;
237
+ };
238
+ if (title) part('p', 'wu-toast-title', title);
239
+ part('p', '', message);
240
+ const close = part('button', 'wu-toast-close');
241
+ close.type = 'button';
242
+ close.setAttribute('aria-label', closeLabel);
243
+ keys.set(el, key);
244
+ open.set(key, el);
245
+ pending.add(el);
246
+
247
+ // Next task, so a region added just now is known to screen readers first
248
+ setTimeout(() => {
249
+ pending.delete(el);
250
+ if (open.get(key) !== el) return;
251
+ region.append(el);
252
+ arm(el, region, duration);
253
+ cap(region, el);
254
+ }, 50);
255
+ return el;
256
+ }
257
+
258
+ // Autocomplete: WAI-ARIA combobox with a listbox, filters the options in the markup
259
+
260
+ function initAutocomplete(root = document) {
261
+ for (const box of fresh(root, '.wu-autocomplete')) {
262
+ const input = box.querySelector('input');
263
+ const list = box.querySelector('.wu-autocomplete-list');
264
+ if (!input || !list) continue;
265
+ const empty = box.querySelector('.wu-autocomplete-empty');
266
+ const options = [...list.querySelectorAll('.wu-autocomplete-option')];
267
+ let active = null;
268
+
269
+ input.setAttribute('role', 'combobox');
270
+ input.setAttribute('aria-autocomplete', 'list');
271
+ input.setAttribute('aria-controls', idOf(list, 'wu-listbox'));
272
+ input.autocomplete = 'off';
273
+ list.setAttribute('role', 'listbox');
274
+ for (const o of options) {
275
+ o.setAttribute('role', 'option');
276
+ idOf(o, 'wu-option');
277
+ }
278
+
279
+ const shown = () =>
280
+ options.filter(
281
+ (o) => !o.hidden && o.getAttribute('aria-disabled') !== 'true'
282
+ );
283
+ const setActive = (o) => {
284
+ active?.removeAttribute('aria-selected');
285
+ active = o;
286
+ if (o) {
287
+ o.setAttribute('aria-selected', 'true');
288
+ input.setAttribute('aria-activedescendant', o.id);
289
+ o.scrollIntoView({ block: 'nearest' });
290
+ } else input.removeAttribute('aria-activedescendant');
291
+ };
292
+ const open = (on) => {
293
+ const count = shown().length;
294
+ list.hidden = !on || !count;
295
+ if (empty) empty.hidden = !on || count > 0;
296
+ input.setAttribute('aria-expanded', !list.hidden);
297
+ setActive(null);
298
+ };
299
+ const filter = () => {
300
+ const q = input.value.trim().toLowerCase();
301
+ for (const o of options)
302
+ o.hidden = !o.textContent.toLowerCase().includes(q);
303
+ open(true);
304
+ };
305
+ const choose = (o) => {
306
+ input.value = o.textContent.trim();
307
+ open(false);
308
+ input.dispatchEvent(new Event('change', { bubbles: true }));
309
+ };
310
+ const move = (step) => {
311
+ if (list.hidden) filter();
312
+ const items = shown();
313
+ if (!items.length) return;
314
+ const i = items.indexOf(active);
315
+ const n = items.length;
316
+ setActive(
317
+ items[i < 0 ? (step > 0 ? 0 : n - 1) : (i + step + n) % n]
318
+ );
319
+ };
320
+
321
+ open(false);
322
+ input.addEventListener('input', filter);
323
+ input.addEventListener('blur', () => open(false));
324
+ input.addEventListener('keydown', (e) => {
325
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
326
+ move(e.key === 'ArrowDown' ? 1 : -1);
327
+ } else if (e.key === 'Enter' && active) {
328
+ choose(active);
329
+ } else if (e.key === 'Escape') {
330
+ if (list.hidden && (!empty || empty.hidden)) input.value = '';
331
+ open(false);
332
+ } else return;
333
+ e.preventDefault();
334
+ });
335
+ // Keep focus in the input while picking with the pointer
336
+ list.addEventListener('mousedown', (e) => e.preventDefault());
337
+ list.addEventListener('click', (e) => {
338
+ const o = e.target.closest('.wu-autocomplete-option');
339
+ if (o && o.getAttribute('aria-disabled') !== 'true') choose(o);
340
+ });
341
+ }
342
+ }
343
+
344
+ // Tables: data table sorting (sets aria-sort and announces the order), selection column and expandable rows
345
+
346
+ const statuses = new WeakMap();
347
+ // Polite live region after the table, added once so the first announcement is heard
348
+ const statusOf = (table) => {
349
+ let status = statuses.get(table);
350
+ if (!status?.isConnected) {
351
+ status = document.createElement('div');
352
+ status.className = 'wu-data-table-status';
353
+ status.setAttribute('role', 'status');
354
+ table.after(status);
355
+ statuses.set(table, status);
356
+ }
357
+ return status;
358
+ };
359
+
360
+ const collator = new Intl.Collator(undefined, {
361
+ numeric: true,
362
+ sensitivity: 'base',
363
+ });
364
+ const valueOf = (row, i) => {
365
+ const cell = row.cells[i];
366
+ return cell ? (cell.dataset.sortValue ?? cell.textContent.trim()) : '';
367
+ };
368
+ const numberOf = (v) => {
369
+ const n = v.replace(/[\s,]/g, '');
370
+ return n && isFinite(n) ? +n : NaN;
371
+ };
372
+
373
+ function sortTable(table, th) {
374
+ const body = table.tBodies[0];
375
+ if (!body) return;
376
+ const i = th.cellIndex;
377
+ const dir = th.getAttribute('aria-sort') === 'ascending' ? -1 : 1;
378
+ for (const h of th.parentElement.cells)
379
+ if (h !== th) h.removeAttribute('aria-sort');
380
+ const order = dir > 0 ? 'ascending' : 'descending';
381
+ th.setAttribute('aria-sort', order);
382
+ statusOf(table).textContent =
383
+ `Sorted by ${th.textContent.trim()}, ${order}`;
384
+
385
+ // A detail row moves with the row before it
386
+ const rows = [...body.rows]
387
+ .filter((r) => !r.classList.contains('wu-table-detail'))
388
+ .sort((a, b) => {
389
+ const x = valueOf(a, i);
390
+ const y = valueOf(b, i);
391
+ const d = numberOf(x) - numberOf(y);
392
+ return dir * (isNaN(d) ? collator.compare(x, y) : d);
393
+ })
394
+ .flatMap((r) => {
395
+ const next = r.nextElementSibling;
396
+ return next?.classList.contains('wu-table-detail')
397
+ ? [r, next]
398
+ : [r];
399
+ });
400
+ const update = () => body.append(...rows);
401
+
402
+ if (!document.startViewTransition || !animated(table, 'wu-data-table'))
403
+ return update();
404
+ table.setAttribute('aria-busy', 'true');
405
+ document
406
+ .startViewTransition(update)
407
+ .finished.finally(() => table.removeAttribute('aria-busy'));
408
+ }
409
+
410
+ function initDataTable(root = document) {
411
+ for (const table of fresh(root, '.wu-data-table')) {
412
+ statusOf(table);
413
+ table.addEventListener('click', (e) => {
414
+ const th = e.target.closest('.wu-table-sort')?.closest('th');
415
+ if (th && th.closest('table') === table && th.closest('thead'))
416
+ sortTable(table, th);
417
+ });
418
+ }
419
+ }
420
+
421
+ // Selection column: row checkboxes in .wu-table-select cells, select-all in the header
422
+ const tables = new WeakSet();
423
+ const selectBox = '.wu-table-select input[type="checkbox"]';
424
+ const allBoxOf = (table) => table.querySelector(`:scope > thead ${selectBox}`);
425
+ const rowBoxesOf = (table) =>
426
+ [...table.tBodies]
427
+ .flatMap((body) => [...body.rows])
428
+ .map((row) => row.querySelector(`:scope > ${selectBox}`))
429
+ .filter(Boolean);
430
+
431
+ // Select-all is checked, mixed or clear; rows of a role="grid" table also get aria-selected
432
+ function syncSelection(table) {
433
+ const boxes = rowBoxesOf(table);
434
+ const on = boxes.filter((b) => b.checked).length;
435
+ const all = allBoxOf(table);
436
+ if (all) {
437
+ all.checked = on > 0 && on === boxes.length;
438
+ all.indeterminate = on > 0 && on < boxes.length;
439
+ }
440
+ if (/^(tree)?grid$/.test(table.getAttribute('role') ?? ''))
441
+ for (const b of boxes)
442
+ b.closest('tr').setAttribute('aria-selected', b.checked);
443
+ }
444
+
445
+ // Expandable row: the button's aria-controls row, else the next row, when it is a .wu-table-detail
446
+ function setExpanded(button, open) {
447
+ const id = button.getAttribute('aria-controls');
448
+ const row = id
449
+ ? document.getElementById(id)
450
+ : button.closest('tr')?.nextElementSibling;
451
+ if (!row?.classList.contains('wu-table-detail')) return;
452
+ button.setAttribute('aria-controls', idOf(row, 'wu-detail'));
453
+ button.setAttribute('aria-expanded', open);
454
+ row.hidden = !open;
455
+ }
456
+
457
+ function initTable(root = document) {
458
+ const found = [...root.querySelectorAll('.wu-table')];
459
+ if (root.matches?.('.wu-table')) found.unshift(root);
460
+ for (const table of found) {
461
+ if (tables.has(table)) continue;
462
+ tables.add(table);
463
+ const own = (el) => el?.closest('table') === table;
464
+ syncSelection(table);
465
+ for (const b of table.querySelectorAll('.wu-table-expand'))
466
+ if (own(b))
467
+ setExpanded(b, b.getAttribute('aria-expanded') === 'true');
468
+
469
+ table.addEventListener('change', (e) => {
470
+ const box = e.target.closest?.(selectBox);
471
+ if (!own(box)) return;
472
+ if (box === allBoxOf(table))
473
+ for (const b of rowBoxesOf(table))
474
+ if (!b.disabled) b.checked = box.checked;
475
+ syncSelection(table);
476
+ });
477
+ table.addEventListener('click', (e) => {
478
+ const b = e.target.closest?.('.wu-table-expand');
479
+ if (own(b))
480
+ setExpanded(b, b.getAttribute('aria-expanded') !== 'true');
481
+ });
482
+ }
483
+ }
484
+
485
+ // Input clear button: .wu-input-clear inside .wu-input-affix shows while the field has text, clears it and refocuses
486
+
487
+ function initInputClear(root = document) {
488
+ for (const button of fresh(root, '.wu-input-clear')) {
489
+ const input = button
490
+ .closest('.wu-input-affix')
491
+ ?.querySelector('input, textarea');
492
+ if (!input) continue;
493
+
494
+ const sync = () => {
495
+ button.hidden = !input.value || input.disabled || input.readOnly;
496
+ };
497
+ button.addEventListener('click', () => {
498
+ input.value = '';
499
+ input.dispatchEvent(new Event('input', { bubbles: true }));
500
+ input.dispatchEvent(new Event('change', { bubbles: true }));
501
+ sync();
502
+ input.focus();
503
+ });
504
+ input.addEventListener('input', sync);
505
+ input.form?.addEventListener('reset', () => setTimeout(sync));
506
+ sync();
507
+ }
508
+ }
509
+
510
+ // @wovenui/components scripts: finds kit components by class and starts them.
511
+ // init(root) starts components added later; each element starts only once.
512
+
513
+ function init(root = document) {
514
+ initTabs(root);
515
+ initToast(root);
516
+ initAutocomplete(root);
517
+ initTable(root);
518
+ initDataTable(root);
519
+ initInputClear(root);
520
+ }
521
+
522
+
523
+ if (typeof document !== 'undefined') {
524
+ if (document.readyState === 'loading')
525
+ document.addEventListener('DOMContentLoaded', () => init());
526
+ else init();
527
+ }
528
+
529
+ export { init, toast, dismiss, sortTable, initTabs, initToast, initAutocomplete, initDataTable, initInputClear, initTable };