@colletdev/core 0.1.3

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 (119) hide show
  1. package/README.md +77 -0
  2. package/custom-elements.json +6037 -0
  3. package/generated/.gitattributes +2 -0
  4. package/generated/index.d.ts +120 -0
  5. package/generated/index.js +521 -0
  6. package/generated/styles.js +2845 -0
  7. package/package.json +56 -0
  8. package/src/elements/accordion.d.ts +20 -0
  9. package/src/elements/accordion.js +92 -0
  10. package/src/elements/activity_group.d.ts +19 -0
  11. package/src/elements/activity_group.js +27 -0
  12. package/src/elements/alert.d.ts +24 -0
  13. package/src/elements/alert.js +40 -0
  14. package/src/elements/autocomplete.d.ts +30 -0
  15. package/src/elements/autocomplete.js +671 -0
  16. package/src/elements/avatar.d.ts +18 -0
  17. package/src/elements/avatar.js +28 -0
  18. package/src/elements/backdrop.d.ts +14 -0
  19. package/src/elements/backdrop.js +28 -0
  20. package/src/elements/badge.d.ts +21 -0
  21. package/src/elements/badge.js +42 -0
  22. package/src/elements/breadcrumb.d.ts +17 -0
  23. package/src/elements/breadcrumb.js +41 -0
  24. package/src/elements/button.d.ts +24 -0
  25. package/src/elements/button.js +36 -0
  26. package/src/elements/card.d.ts +21 -0
  27. package/src/elements/card.js +67 -0
  28. package/src/elements/carousel.d.ts +23 -0
  29. package/src/elements/carousel.js +895 -0
  30. package/src/elements/chat_input.d.ts +22 -0
  31. package/src/elements/chat_input.js +78 -0
  32. package/src/elements/checkbox.d.ts +21 -0
  33. package/src/elements/checkbox.js +114 -0
  34. package/src/elements/code_block.d.ts +21 -0
  35. package/src/elements/code_block.js +27 -0
  36. package/src/elements/collapsible.d.ts +20 -0
  37. package/src/elements/collapsible.js +93 -0
  38. package/src/elements/date_picker.d.ts +30 -0
  39. package/src/elements/date_picker.js +528 -0
  40. package/src/elements/dialog.d.ts +20 -0
  41. package/src/elements/dialog.js +314 -0
  42. package/src/elements/drawer.d.ts +20 -0
  43. package/src/elements/drawer.js +318 -0
  44. package/src/elements/fab.d.ts +22 -0
  45. package/src/elements/fab.js +36 -0
  46. package/src/elements/file_upload.d.ts +26 -0
  47. package/src/elements/file_upload.js +59 -0
  48. package/src/elements/listbox.d.ts +19 -0
  49. package/src/elements/listbox.js +250 -0
  50. package/src/elements/menu.d.ts +20 -0
  51. package/src/elements/menu.js +224 -0
  52. package/src/elements/message_bubble.d.ts +23 -0
  53. package/src/elements/message_bubble.js +29 -0
  54. package/src/elements/message_group.d.ts +18 -0
  55. package/src/elements/message_group.js +28 -0
  56. package/src/elements/message_part.d.ts +35 -0
  57. package/src/elements/message_part.js +153 -0
  58. package/src/elements/pagination.d.ts +22 -0
  59. package/src/elements/pagination.js +36 -0
  60. package/src/elements/popover.d.ts +26 -0
  61. package/src/elements/popover.js +191 -0
  62. package/src/elements/profile_menu.d.ts +20 -0
  63. package/src/elements/profile_menu.js +213 -0
  64. package/src/elements/progress.d.ts +18 -0
  65. package/src/elements/progress.js +31 -0
  66. package/src/elements/radio_group.d.ts +22 -0
  67. package/src/elements/radio_group.js +70 -0
  68. package/src/elements/scrollbar.d.ts +19 -0
  69. package/src/elements/scrollbar.js +299 -0
  70. package/src/elements/search_bar.d.ts +27 -0
  71. package/src/elements/search_bar.js +98 -0
  72. package/src/elements/select.d.ts +26 -0
  73. package/src/elements/select.js +485 -0
  74. package/src/elements/sidebar.d.ts +21 -0
  75. package/src/elements/sidebar.js +322 -0
  76. package/src/elements/skeleton.d.ts +17 -0
  77. package/src/elements/skeleton.js +31 -0
  78. package/src/elements/slider.d.ts +28 -0
  79. package/src/elements/slider.js +93 -0
  80. package/src/elements/speed_dial.d.ts +23 -0
  81. package/src/elements/speed_dial.js +370 -0
  82. package/src/elements/spinner.d.ts +15 -0
  83. package/src/elements/spinner.js +28 -0
  84. package/src/elements/split_button.d.ts +23 -0
  85. package/src/elements/split_button.js +281 -0
  86. package/src/elements/stepper.d.ts +20 -0
  87. package/src/elements/stepper.js +31 -0
  88. package/src/elements/switch.d.ts +22 -0
  89. package/src/elements/switch.js +129 -0
  90. package/src/elements/table.d.ts +29 -0
  91. package/src/elements/table.js +371 -0
  92. package/src/elements/tabs.d.ts +19 -0
  93. package/src/elements/tabs.js +139 -0
  94. package/src/elements/text.d.ts +26 -0
  95. package/src/elements/text.js +32 -0
  96. package/src/elements/text_input.d.ts +36 -0
  97. package/src/elements/text_input.js +121 -0
  98. package/src/elements/thinking.d.ts +17 -0
  99. package/src/elements/thinking.js +28 -0
  100. package/src/elements/toast.d.ts +23 -0
  101. package/src/elements/toast.js +209 -0
  102. package/src/elements/toggle_group.d.ts +22 -0
  103. package/src/elements/toggle_group.js +176 -0
  104. package/src/elements/tooltip.d.ts +18 -0
  105. package/src/elements/tooltip.js +64 -0
  106. package/src/markdown.d.ts +24 -0
  107. package/src/markdown.js +66 -0
  108. package/src/runtime.d.ts +35 -0
  109. package/src/runtime.js +790 -0
  110. package/src/server.d.ts +69 -0
  111. package/src/server.js +176 -0
  112. package/src/streaming-markdown.js +43 -0
  113. package/src/vite-plugin.d.ts +46 -0
  114. package/src/vite-plugin.js +221 -0
  115. package/wasm/package.json +16 -0
  116. package/wasm/wasm_api.d.ts +72 -0
  117. package/wasm/wasm_api.js +593 -0
  118. package/wasm/wasm_api_bg.wasm +0 -0
  119. package/wasm/wasm_api_bg.wasm.d.ts +10 -0
@@ -0,0 +1,671 @@
1
+ // Custom behavior for <cx-autocomplete> — fuzzy filter, selection, chips, keyboard nav.
2
+ //
3
+ // The Rust component renders:
4
+ // <div data-autocomplete data-floating-trigger> — input wrapper
5
+ // <input role="combobox"> — the text input
6
+ // <span data-chip="value"> — selected chips (multi-mode)
7
+ // <button data-autocomplete-clear> — clear button
8
+ // <div data-floating class="hidden data-[open]:block"> — dropdown
9
+ // <div role="listbox"> with <div role="option" data-value="..."> children
10
+ // <div data-no-results class="hidden"> — no results message
11
+ //
12
+ // This Custom Element wires up:
13
+ // - Input typing triggers fuzzy filter + dropdown open
14
+ // - Click/keyboard option selection
15
+ // - Single mode: show label in input, close dropdown
16
+ // - Multiple mode: show chips, keep dropdown open
17
+ // - Chip dismiss, clear button
18
+ // - Keyboard navigation (WAI-ARIA APG Editable Combobox)
19
+ //
20
+ // Source: crates/wasm-api/src/autocomplete.rs
21
+
22
+ let _sheet;
23
+ function getSheet() {
24
+ if (!_sheet) {
25
+ _sheet = new CSSStyleSheet();
26
+ _sheet.replaceSync([
27
+ // Dropdown uses position:fixed (set by JS) to escape scroll container clipping.
28
+ '[data-floating] {',
29
+ ' z-index: 50;',
30
+ ' max-height: 15rem;',
31
+ ' overflow-y: auto;',
32
+ '}',
33
+ 'button[aria-expanded="true"] > span[aria-hidden="true"],',
34
+ '[data-autocomplete][aria-expanded="true"] > span[aria-hidden="true"] {',
35
+ ' transform: rotate(180deg);',
36
+ '}',
37
+ '[role="option"][data-focused] {',
38
+ ' background-color: var(--color-secondary);',
39
+ '}',
40
+ '[role="option"]:not([aria-disabled="true"]):hover {',
41
+ ' background-color: var(--color-secondary);',
42
+ ' cursor: pointer;',
43
+ '}',
44
+ ].join('\n'));
45
+ }
46
+ return _sheet;
47
+ }
48
+
49
+ // ── Fuzzy matching ──
50
+
51
+ function fuzzyMatch(text, query) {
52
+ let ti = 0;
53
+ for (let qi = 0; qi < query.length; qi++) {
54
+ let found = false;
55
+ while (ti < text.length) {
56
+ if (text[ti] === query[qi]) { ti++; found = true; break; }
57
+ ti++;
58
+ }
59
+ if (!found) return false;
60
+ }
61
+ return true;
62
+ }
63
+
64
+ function highlightMatch(original, query) {
65
+ if (!query) return null;
66
+ const lc = original.toLowerCase();
67
+ let qi = 0;
68
+ const parts = [];
69
+ for (let i = 0; i < original.length; i++) {
70
+ if (qi < query.length && lc[i] === query[qi]) {
71
+ parts.push(`<mark class="bg-transparent font-semibold text-[var(--color-text)]">${original[i]}</mark>`);
72
+ qi++;
73
+ } else {
74
+ parts.push(original[i]);
75
+ }
76
+ }
77
+ return qi < query.length ? null : parts.join('');
78
+ }
79
+
80
+ export function defineCxAutocomplete(wasmFn, baseClass) {
81
+ class CxAutocomplete extends baseClass {
82
+ static observedAttributes = ['id', 'label', 'variant', 'shape', 'size', 'mode', 'selected', 'query', 'placeholder', 'helper-text', 'error', 'disabled', 'required', 'readonly', 'name', 'allow-custom', 'clearable', 'items', 'groups'];
83
+ static _booleanAttrs = new Set(['disabled', 'required', 'readonly', 'allow-custom', 'clearable']);
84
+
85
+ #outsideClick = null;
86
+ #skipOpen = false;
87
+ #debounceTimer = null;
88
+
89
+ connectedCallback() {
90
+ if (!this._isInitialized) {
91
+ this._markInitialized();
92
+ const shadow = this._shadow;
93
+
94
+ const sheet = getSheet();
95
+ if (!shadow.adoptedStyleSheets.includes(sheet)) {
96
+ shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, sheet];
97
+ }
98
+
99
+ // ── Click handler ──
100
+ shadow.addEventListener('click', (e) => {
101
+ // Chip dismiss
102
+ const chipBtn = e.target.closest('[data-chip-remove]');
103
+ if (chipBtn) {
104
+ this.#removeChip(chipBtn.getAttribute('data-chip-remove'));
105
+ return;
106
+ }
107
+
108
+ // Clear button
109
+ const clearBtn = e.target.closest('[data-autocomplete-clear]');
110
+ if (clearBtn) {
111
+ this.#clearAll();
112
+ return;
113
+ }
114
+
115
+ // Option click (in dropdown)
116
+ const option = e.target.closest('[role="option"]');
117
+ if (option && !option.hasAttribute('aria-disabled')) {
118
+ this.#selectOption(option);
119
+ return;
120
+ }
121
+
122
+ // Chevron click while open → close
123
+ const wrapper = this.#getWrapper();
124
+ if (wrapper && this.#isOpen()) {
125
+ if (e.target.closest('[aria-hidden="true"]')) {
126
+ this.#close();
127
+ return;
128
+ }
129
+ }
130
+ });
131
+
132
+ // ── Input handler (typing → filter) ──
133
+ shadow.addEventListener('input', (e) => {
134
+ const input = e.target;
135
+ if (!input.matches || !input.matches('input[role="combobox"]')) return;
136
+
137
+ if (!this.#isOpen()) this.#open();
138
+ this._setFormValue(input.value);
139
+ this._emit('cx-input', { value: input.value });
140
+
141
+ // Debounce filter
142
+ if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
143
+ this.#debounceTimer = setTimeout(() => this.#filter(input), 100);
144
+ });
145
+
146
+ // ── Focus → open ──
147
+ shadow.addEventListener('focusin', (e) => {
148
+ const input = e.target;
149
+ if (!input.matches || !input.matches('input[role="combobox"]')) return;
150
+ if (!this.#isOpen()) {
151
+ if (this.#skipOpen) { this.#skipOpen = false; }
152
+ else { this.#open(); }
153
+ }
154
+ });
155
+
156
+ // ── Keyboard handler ──
157
+ shadow.addEventListener('keydown', (e) => this.#handleKey(e));
158
+
159
+ // ── Click outside → close ──
160
+ this.#outsideClick = (e) => {
161
+ if (this.#isOpen() && !this.contains(e.target) && !this._shadow.contains(e.target)) {
162
+ this.#close();
163
+ }
164
+ };
165
+ document.addEventListener('mousedown', this.#outsideClick);
166
+
167
+ // ── Focus exit → close ──
168
+ // Must check both light DOM and shadow DOM — Node.contains() doesn't cross shadow boundaries.
169
+ shadow.addEventListener('focusout', () => {
170
+ setTimeout(() => {
171
+ if (this.#isOpen()) {
172
+ const active = shadow.activeElement || document.activeElement;
173
+ if (!this.contains(active) && !this._shadow.contains(active) && active !== this) {
174
+ this.#close();
175
+ }
176
+ }
177
+ }, 0);
178
+ });
179
+
180
+ // Forward focus events from inner interactive elements
181
+ shadow.addEventListener('focusin', (e) => {
182
+ this._emit('cx-focus', { relatedTarget: e.relatedTarget });
183
+ });
184
+ shadow.addEventListener('focusout', (e) => {
185
+ this._emit('cx-blur', { relatedTarget: e.relatedTarget });
186
+ });
187
+
188
+ // Forward keyboard events from inner interactive elements
189
+ shadow.addEventListener('keydown', (e) => {
190
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
191
+ });
192
+ shadow.addEventListener('keyup', (e) => {
193
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
194
+ });
195
+ } // end _isInitialized guard
196
+ super.connectedCallback();
197
+ }
198
+
199
+ disconnectedCallback() {
200
+ if (this.#outsideClick) {
201
+ document.removeEventListener('mousedown', this.#outsideClick);
202
+ this.#outsideClick = null;
203
+ }
204
+ if (this.#debounceTimer) {
205
+ clearTimeout(this.#debounceTimer);
206
+ }
207
+ super.disconnectedCallback();
208
+ }
209
+
210
+ // ── DOM accessors ──
211
+
212
+ #getWrapper() {
213
+ return this._shadow.querySelector('[data-autocomplete]');
214
+ }
215
+
216
+ #getInput() {
217
+ return this._shadow.querySelector('input[role="combobox"]');
218
+ }
219
+
220
+ #getDropdown() {
221
+ return this._shadow.querySelector('[data-floating]');
222
+ }
223
+
224
+ #getVisibleOptions() {
225
+ return Array.from(
226
+ this._shadow.querySelectorAll(
227
+ '[role="option"]:not([aria-disabled="true"]):not([hidden])'
228
+ )
229
+ );
230
+ }
231
+
232
+ #getMode() {
233
+ return this._props.mode || 'single';
234
+ }
235
+
236
+ // ── Dropdown state ──
237
+
238
+ #isOpen() {
239
+ const dd = this.#getDropdown();
240
+ return dd ? dd.hasAttribute('data-open') : false;
241
+ }
242
+
243
+ #open() {
244
+ const wrapper = this.#getWrapper();
245
+ const dd = this.#getDropdown();
246
+ if (!wrapper || !dd || this.#isOpen()) return;
247
+
248
+ // Position with fixed coordinates — escapes overflow:auto/scroll clipping.
249
+ this._positionFloatingFixed(wrapper, dd, { matchWidth: true });
250
+
251
+ dd.setAttribute('data-open', '');
252
+ dd.classList.remove('hidden');
253
+ dd.style.display = 'block';
254
+ dd.style.pointerEvents = 'auto';
255
+ dd.style.opacity = '1';
256
+ wrapper.setAttribute('aria-expanded', 'true');
257
+
258
+ const input = this.#getInput();
259
+ if (input) input.setAttribute('aria-expanded', 'true');
260
+ }
261
+
262
+ #close() {
263
+ const wrapper = this.#getWrapper();
264
+ const dd = this.#getDropdown();
265
+ if (!wrapper || !dd) return;
266
+
267
+ dd.removeAttribute('data-open');
268
+ dd.classList.add('hidden');
269
+ dd.style.display = '';
270
+ dd.style.pointerEvents = '';
271
+ dd.style.opacity = '';
272
+ this._resetFloatingFixed(dd);
273
+ wrapper.setAttribute('aria-expanded', 'false');
274
+
275
+ const input = this.#getInput();
276
+ if (input) {
277
+ input.setAttribute('aria-expanded', 'false');
278
+ input.removeAttribute('aria-activedescendant');
279
+ }
280
+ this.#clearFocused();
281
+ }
282
+
283
+ // ── Fuzzy filter ──
284
+
285
+ #filter(input) {
286
+ const dd = this.#getDropdown();
287
+ if (!dd) return;
288
+
289
+ const query = (input.value || '').toLowerCase().trim();
290
+ const options = dd.querySelectorAll('[role="option"]');
291
+ let visible = 0;
292
+
293
+ options.forEach(o => {
294
+ const labelSpan = o.querySelector('.truncate');
295
+ const origText = labelSpan
296
+ ? (labelSpan.getAttribute('data-text') || labelSpan.textContent)
297
+ : o.textContent;
298
+ const text = origText.toLowerCase();
299
+
300
+ if (!query || fuzzyMatch(text, query)) {
301
+ o.removeAttribute('hidden');
302
+ o.style.display = '';
303
+ visible++;
304
+
305
+ // Highlight matching characters
306
+ if (labelSpan) {
307
+ if (query) {
308
+ if (!labelSpan.hasAttribute('data-text')) {
309
+ labelSpan.setAttribute('data-text', origText);
310
+ }
311
+ const hl = highlightMatch(origText, query);
312
+ if (hl) labelSpan.innerHTML = hl;
313
+ else labelSpan.textContent = origText;
314
+ } else if (labelSpan.hasAttribute('data-text')) {
315
+ labelSpan.textContent = labelSpan.getAttribute('data-text');
316
+ }
317
+ }
318
+ } else {
319
+ o.setAttribute('hidden', '');
320
+ o.style.display = 'none';
321
+ if (labelSpan && labelSpan.hasAttribute('data-text')) {
322
+ labelSpan.textContent = labelSpan.getAttribute('data-text');
323
+ }
324
+ }
325
+ });
326
+
327
+ // Hide empty group headers
328
+ dd.querySelectorAll('[role="group"]').forEach(g => {
329
+ const groupOpts = g.querySelectorAll('[role="option"]');
330
+ let anyVisible = false;
331
+ groupOpts.forEach(o => { if (!o.hasAttribute('hidden')) anyVisible = true; });
332
+ const header = g.querySelector('[role="presentation"]');
333
+ if (header) {
334
+ if (anyVisible) { header.removeAttribute('hidden'); header.style.display = ''; }
335
+ else { header.setAttribute('hidden', ''); header.style.display = 'none'; }
336
+ }
337
+ if (!anyVisible) { g.setAttribute('hidden', ''); g.style.display = 'none'; }
338
+ else { g.removeAttribute('hidden'); g.style.display = ''; }
339
+ });
340
+
341
+ // No-results message
342
+ const noRes = dd.querySelector('[data-no-results]');
343
+ if (noRes) {
344
+ if (visible === 0 && query) noRes.classList.remove('hidden');
345
+ else noRes.classList.add('hidden');
346
+ }
347
+ }
348
+
349
+ // ── Keyboard navigation ──
350
+
351
+ #handleKey(e) {
352
+ const input = this.#getInput();
353
+ if (!input || e.target !== input) return;
354
+
355
+ const isOpen = this.#isOpen();
356
+
357
+ switch (e.key) {
358
+ case 'Escape':
359
+ if (isOpen) {
360
+ e.preventDefault();
361
+ this.#close();
362
+ }
363
+ break;
364
+
365
+ case 'ArrowDown':
366
+ case 'ArrowUp':
367
+ e.preventDefault();
368
+ if (!isOpen) {
369
+ this.#open();
370
+ this.#filter(input);
371
+ }
372
+ if (e.key === 'ArrowDown') this.#focusNext();
373
+ else this.#focusPrev();
374
+ break;
375
+
376
+ case 'Home':
377
+ if (isOpen) { e.preventDefault(); this.#focusFirst(); }
378
+ break;
379
+
380
+ case 'End':
381
+ if (isOpen) { e.preventDefault(); this.#focusLast(); }
382
+ break;
383
+
384
+ case 'Enter': {
385
+ const activeId = input.getAttribute('aria-activedescendant');
386
+ const activeOpt = activeId ? this._shadow.getElementById(activeId) : null;
387
+ if (activeOpt && !activeOpt.hasAttribute('aria-disabled')) {
388
+ e.preventDefault();
389
+ this.#selectOption(activeOpt);
390
+ }
391
+ break;
392
+ }
393
+
394
+ case 'Tab':
395
+ if (isOpen) this.#close();
396
+ break;
397
+ }
398
+ }
399
+
400
+ // ── Virtual focus ──
401
+
402
+ #clearFocused() {
403
+ this._shadow.querySelectorAll('[role="option"][data-focused]')
404
+ .forEach(o => o.removeAttribute('data-focused'));
405
+ }
406
+
407
+ #setActive(option) {
408
+ const input = this.#getInput();
409
+ this.#clearFocused();
410
+ if (option && input) {
411
+ option.setAttribute('data-focused', '');
412
+ input.setAttribute('aria-activedescendant', option.id);
413
+ option.scrollIntoView({ block: 'nearest' });
414
+ }
415
+ }
416
+
417
+ #focusFirst() {
418
+ const opts = this.#getVisibleOptions();
419
+ if (opts.length) this.#setActive(opts[0]);
420
+ }
421
+
422
+ #focusLast() {
423
+ const opts = this.#getVisibleOptions();
424
+ if (opts.length) this.#setActive(opts[opts.length - 1]);
425
+ }
426
+
427
+ #focusNext() {
428
+ const opts = this.#getVisibleOptions();
429
+ if (!opts.length) return;
430
+ const input = this.#getInput();
431
+ const curId = input?.getAttribute('aria-activedescendant');
432
+ const curOpt = curId ? this._shadow.getElementById(curId) : null;
433
+ const idx = curOpt ? opts.indexOf(curOpt) : -1;
434
+ this.#setActive(opts[(idx + 1) % opts.length]);
435
+ }
436
+
437
+ #focusPrev() {
438
+ const opts = this.#getVisibleOptions();
439
+ if (!opts.length) return;
440
+ const input = this.#getInput();
441
+ const curId = input?.getAttribute('aria-activedescendant');
442
+ const curOpt = curId ? this._shadow.getElementById(curId) : null;
443
+ const idx = curOpt ? opts.indexOf(curOpt) : 0;
444
+ this.#setActive(opts[(idx - 1 + opts.length) % opts.length]);
445
+ }
446
+
447
+ // ── Option selection ──
448
+
449
+ #getOptionLabel(opt) {
450
+ const ls = opt.querySelector('.truncate');
451
+ if (ls) return ls.getAttribute('data-text') || ls.textContent;
452
+ return opt.textContent.trim();
453
+ }
454
+
455
+ #selectOption(option) {
456
+ const value = option.getAttribute('data-value') || '';
457
+ const mode = this.#getMode();
458
+ const input = this.#getInput();
459
+
460
+ if (mode === 'single') {
461
+ // Single: show label in input, close dropdown
462
+ if (input) input.value = this.#getOptionLabel(option);
463
+
464
+ // Update aria-selected
465
+ this._shadow.querySelectorAll('[role="option"]').forEach(o => {
466
+ const isThis = o === option;
467
+ o.setAttribute('aria-selected', String(isThis));
468
+ o.setAttribute('data-selected', String(isThis));
469
+ const check = o.querySelector('[data-check]');
470
+ if (check) {
471
+ if (isThis) check.classList.remove('hidden');
472
+ else check.classList.add('hidden');
473
+ }
474
+ });
475
+
476
+ // Sync host prop — single source of truth for WASM re-renders.
477
+ this._props.selected = value;
478
+
479
+ this.#skipOpen = true;
480
+ this.#close();
481
+ if (input) {
482
+ this._setFormValue(value);
483
+ input.focus();
484
+ }
485
+
486
+ this._emit('cx-change', { value });
487
+ } else {
488
+ // Multiple: toggle selection, keep open
489
+ const wasSelected = option.getAttribute('aria-selected') === 'true';
490
+ const nowSelected = !wasSelected;
491
+
492
+ option.setAttribute('aria-selected', String(nowSelected));
493
+ option.setAttribute('data-selected', String(nowSelected));
494
+ const check = option.querySelector('[data-check]');
495
+ if (check) {
496
+ if (nowSelected) check.classList.remove('hidden');
497
+ else check.classList.add('hidden');
498
+ }
499
+
500
+ // Clear input after selection in multi-mode
501
+ if (input) input.value = '';
502
+
503
+ // Rebuild chips
504
+ this.#rebuildChips();
505
+
506
+ // Collect all selected
507
+ const selected = [];
508
+ this._shadow.querySelectorAll('[role="option"][aria-selected="true"]')
509
+ .forEach(o => selected.push(o.getAttribute('data-value') || ''));
510
+
511
+ // Sync host prop — single source of truth for WASM re-renders.
512
+ this._props.selected = selected.join(',');
513
+
514
+ this._setFormValue(selected.join(','));
515
+ this._emit('cx-change', { value: selected });
516
+
517
+ // Re-filter with cleared input
518
+ if (input) this.#filter(input);
519
+ this.#clearFocused();
520
+ if (input) input.focus();
521
+ }
522
+ }
523
+
524
+ // ── Chip management (multi-mode) ──
525
+
526
+ #rebuildChips() {
527
+ const wrapper = this.#getWrapper();
528
+ if (!wrapper) return;
529
+
530
+ const chipContainer = wrapper.querySelector('.flex.flex-wrap');
531
+ if (!chipContainer) return;
532
+
533
+ const template = chipContainer.querySelector('[data-chip-template]');
534
+ if (!template) return;
535
+
536
+ const input = this.#getInput();
537
+
538
+ // Remove existing chips
539
+ chipContainer.querySelectorAll('[data-chip]:not([data-chip-template])').forEach(c => c.remove());
540
+
541
+ // Create chips for each selected option
542
+ this._shadow.querySelectorAll('[role="option"][aria-selected="true"]').forEach(o => {
543
+ const val = o.getAttribute('data-value') || '';
544
+ const label = this.#getOptionLabel(o);
545
+
546
+ const chip = template.cloneNode(true);
547
+ chip.removeAttribute('data-chip-template');
548
+ chip.style.display = '';
549
+ chip.setAttribute('data-chip', val);
550
+
551
+ const ls = chip.querySelector('.truncate');
552
+ if (ls) ls.textContent = label;
553
+
554
+ const btn = chip.querySelector('button');
555
+ if (btn) {
556
+ btn.setAttribute('data-chip-remove', val);
557
+ btn.setAttribute('aria-label', `Remove ${label}`);
558
+ }
559
+
560
+ if (input) chipContainer.insertBefore(chip, input);
561
+ else chipContainer.appendChild(chip);
562
+ });
563
+ }
564
+
565
+ #removeChip(value) {
566
+ if (!value) return;
567
+
568
+ // Deselect the option
569
+ this._shadow.querySelectorAll('[role="option"]').forEach(o => {
570
+ if (o.getAttribute('data-value') === value) {
571
+ o.setAttribute('aria-selected', 'false');
572
+ o.setAttribute('data-selected', 'false');
573
+ const check = o.querySelector('[data-check]');
574
+ if (check) check.classList.add('hidden');
575
+ }
576
+ });
577
+
578
+ this.#rebuildChips();
579
+
580
+ const selected = [];
581
+ this._shadow.querySelectorAll('[role="option"][aria-selected="true"]')
582
+ .forEach(o => selected.push(o.getAttribute('data-value') || ''));
583
+
584
+ // Sync host prop — single source of truth for WASM re-renders.
585
+ this._props.selected = selected.join(',');
586
+
587
+ this._setFormValue(selected.join(','));
588
+ this._emit('cx-change', { value: selected });
589
+
590
+ const input = this.#getInput();
591
+ if (input) input.focus();
592
+ }
593
+
594
+ #clearAll() {
595
+ const input = this.#getInput();
596
+ if (input) {
597
+ input.value = '';
598
+ input.focus();
599
+ }
600
+
601
+ // Deselect all options
602
+ this._shadow.querySelectorAll('[role="option"]').forEach(o => {
603
+ o.setAttribute('aria-selected', 'false');
604
+ o.setAttribute('data-selected', 'false');
605
+ const check = o.querySelector('[data-check]');
606
+ if (check) check.classList.add('hidden');
607
+ });
608
+
609
+ if (this.#getMode() === 'multiple') {
610
+ this.#rebuildChips();
611
+ }
612
+
613
+ // Sync host prop — single source of truth for WASM re-renders.
614
+ this._props.selected = '';
615
+
616
+ this._setFormValue('');
617
+ this._emit('cx-input', { value: '' });
618
+ this._emit('cx-change', { value: this.#getMode() === 'multiple' ? [] : '' });
619
+
620
+ if (input) this.#filter(input);
621
+ }
622
+
623
+ // ── Public imperative API ──
624
+ open() { this.#open(); }
625
+ close() { this.#close(); }
626
+ clear() { this.#clearAll(); }
627
+ focus() { const el = this._shadow.querySelector('input[role="combobox"]'); if (el) el.focus(); else super.focus(); }
628
+
629
+ // ── Render ──
630
+
631
+ _doRender() {
632
+ try {
633
+ const wasOpen = this.#isOpen();
634
+ const activeId = this.#getInput()?.getAttribute('aria-activedescendant');
635
+
636
+ const result = wasmFn(this._props);
637
+ this._injectHtml(result);
638
+
639
+ const sheet = getSheet();
640
+ if (!this._shadow.adoptedStyleSheets.includes(sheet)) {
641
+ this._shadow.adoptedStyleSheets = [...this._shadow.adoptedStyleSheets, sheet];
642
+ }
643
+
644
+ // Sync controlled value
645
+ const input = this._shadow.querySelector('input, textarea');
646
+ if (input) {
647
+ if ('query' in this._props && this._props.query !== undefined) {
648
+ input.value = this._props.query;
649
+ } else if ('value' in this._props) {
650
+ input.value = this._props.value;
651
+ }
652
+ this._setFormValue(input.value || '');
653
+ }
654
+
655
+ // Restore open state
656
+ if (wasOpen) {
657
+ this.#open();
658
+ if (activeId) {
659
+ const opt = this._shadow.getElementById(activeId);
660
+ if (opt) this.#setActive(opt);
661
+ }
662
+ }
663
+ } catch (e) {
664
+ console.error('[cx-autocomplete]', e);
665
+ }
666
+ }
667
+ }
668
+
669
+ customElements.define('cx-autocomplete', CxAutocomplete);
670
+ return CxAutocomplete;
671
+ }
@@ -0,0 +1,18 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/avatar.rs
3
+
4
+ export interface CxAvatarAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ shape?: 'circle' | 'rounded';
8
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
9
+ image?: string;
10
+ initials?: string;
11
+ clickable?: boolean;
12
+ }
13
+
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ 'cx-avatar': HTMLElement & CxAvatarAttributes;
17
+ }
18
+ }