@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,27 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/search_bar.rs
3
+
4
+ export interface CxSearchBarAttributes {
5
+ id?: string;
6
+ placeholder?: string;
7
+ label?: string;
8
+ variant?: 'outline' | 'filled' | 'ghost';
9
+ shape?: 'sharp' | 'rounded' | 'pill';
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ name?: string;
12
+ value?: string;
13
+ debounceMs?: number;
14
+ loading?: boolean;
15
+ shortcut?: string;
16
+ expandable?: boolean;
17
+ disabled?: boolean;
18
+ readonly?: boolean;
19
+ controls?: string;
20
+ shimmer?: boolean;
21
+ }
22
+
23
+ declare global {
24
+ interface HTMLElementTagNameMap {
25
+ 'cx-search-bar': HTMLElement & CxSearchBarAttributes;
26
+ }
27
+ }
@@ -0,0 +1,98 @@
1
+ // Custom behavior for <cx-search-bar> — shimmer, keyboard shortcuts, clear, debounce.
2
+ // Hand-authored to support focus shimmer animation with 10s cooldown.
3
+ // Source: crates/wasm-api/src/search_bar.rs
4
+
5
+ export function defineCxSearchBar(wasmFn, baseClass) {
6
+ class CxSearchBar extends baseClass {
7
+ static observedAttributes = ['id', 'placeholder', 'label', 'variant', 'shape', 'size', 'name', 'value', 'debounce-ms', 'loading', 'shortcut', 'expandable', 'disabled', 'readonly', 'controls', 'shimmer'];
8
+ static _booleanAttrs = new Set(['loading', 'expandable', 'disabled', 'readonly', 'shimmer']);
9
+ static _numericAttrs = new Set(['debounce-ms']);
10
+ static _hostDisplay = 'block';
11
+
12
+ #lastShimmer = 0;
13
+ #shimmerCooldown = 10_000;
14
+
15
+ set debounce_ms(v) { this._setProp('debounce_ms', v); }
16
+ get debounce_ms() { return this._props.debounce_ms; }
17
+
18
+ connectedCallback() {
19
+ if (!this._isInitialized) {
20
+ this._markInitialized();
21
+ const shadow = this._shadow;
22
+
23
+ // Forward focus events from inner interactive elements
24
+ shadow.addEventListener('focusin', (e) => {
25
+ this._emit('cx-focus', { relatedTarget: e.relatedTarget });
26
+
27
+ // ── Focus shimmer (one-shot, 10s cooldown, reduced-motion aware) ──
28
+ const shimmerEl = shadow.querySelector('[data-search-shimmer]');
29
+ if (!shimmerEl) return;
30
+ const shimmerText = shimmerEl.querySelector('span:last-child');
31
+ if (!shimmerText) return;
32
+
33
+ const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)');
34
+ if (prefersReduced.matches) return;
35
+
36
+ const now = Date.now();
37
+ if (now - this.#lastShimmer < this.#shimmerCooldown) return;
38
+ this.#lastShimmer = now;
39
+ shimmerText.classList.add('cx-search-shimmer-text');
40
+ });
41
+
42
+ shadow.addEventListener('focusout', (e) => {
43
+ this._emit('cx-blur', { relatedTarget: e.relatedTarget });
44
+ });
45
+
46
+ // Remove shimmer class after animation completes
47
+ shadow.addEventListener('animationend', (e) => {
48
+ if (e.animationName === 'cx-search-shimmer') {
49
+ e.target.classList.remove('cx-search-shimmer-text');
50
+ }
51
+ });
52
+
53
+ // Forward keyboard events from inner interactive elements
54
+ shadow.addEventListener('keydown', (e) => {
55
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
56
+ });
57
+ shadow.addEventListener('keyup', (e) => {
58
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
59
+ });
60
+
61
+ // Hide shimmer overlay when input has value
62
+ shadow.addEventListener('input', (e) => {
63
+ if (e.target.tagName === 'INPUT') {
64
+ const shimmerEl = shadow.querySelector('[data-search-shimmer]');
65
+ if (shimmerEl) {
66
+ shimmerEl.style.display = e.target.value.length > 0 ? 'none' : '';
67
+ }
68
+ this._emit('cx-input', { value: e.target.value });
69
+ }
70
+ });
71
+ shadow.addEventListener('change', (e) => {
72
+ if (e.target.tagName === 'INPUT') {
73
+ this._emit('cx-change', { value: e.target.value });
74
+ }
75
+ });
76
+ } // end _isInitialized guard
77
+ super.connectedCallback();
78
+ }
79
+
80
+ // ── Public imperative API ──
81
+ focus() {
82
+ const el = this._shadow.querySelector('input[type="search"]');
83
+ if (el) el.focus(); else super.focus();
84
+ }
85
+
86
+ _doRender() {
87
+ try {
88
+ const result = wasmFn(this._props);
89
+ this._injectHtml(result);
90
+ } catch (e) {
91
+ console.error('[cx-search-bar]', e);
92
+ }
93
+ }
94
+ }
95
+
96
+ customElements.define('cx-search-bar', CxSearchBar);
97
+ return CxSearchBar;
98
+ }
@@ -0,0 +1,26 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/select.rs
3
+
4
+ export interface CxSelectAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ variant?: 'outline' | 'filled' | 'ghost';
8
+ shape?: 'sharp' | 'rounded' | 'pill';
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
10
+ mode?: 'single' | 'multiple';
11
+ selected?: string;
12
+ placeholder?: string;
13
+ helperText?: string;
14
+ error?: string;
15
+ disabled?: boolean;
16
+ required?: boolean;
17
+ name?: string;
18
+ items?: string;
19
+ groups?: string;
20
+ }
21
+
22
+ declare global {
23
+ interface HTMLElementTagNameMap {
24
+ 'cx-select': HTMLElement & CxSelectAttributes;
25
+ }
26
+ }
@@ -0,0 +1,485 @@
1
+ // Custom behavior for <cx-select> — dropdown toggle, option selection, keyboard nav.
2
+ //
3
+ // The Rust component renders a WAI-ARIA APG Select-Only Combobox:
4
+ // <button role="combobox" data-floating-trigger> — the trigger
5
+ // <div data-floating class="hidden data-[open]:block"> — the dropdown
6
+ // <div role="listbox"> — the option list
7
+ // <div role="option" data-value="..."> — each option
8
+ //
9
+ // This Custom Element wires up all interactive behavior:
10
+ // - Click/keyboard on trigger to toggle dropdown
11
+ // - Option selection with display text + form value sync
12
+ // - Keyboard navigation per WAI-ARIA APG Select-Only Combobox
13
+ // - Click-outside + focus-exit to close
14
+ // - Chevron rotation animation via aria-expanded CSS
15
+ //
16
+ // Source: crates/wasm-api/src/select.rs
17
+
18
+ let _sheet;
19
+ function getSheet() {
20
+ if (!_sheet) {
21
+ _sheet = new CSSStyleSheet();
22
+ _sheet.replaceSync([
23
+ // Dropdown uses position:fixed (set by JS) to escape scroll container
24
+ // clipping. The sheet provides base z-index and scroll behavior only.
25
+ '[data-floating] {',
26
+ ' z-index: 50;',
27
+ ' max-height: 15rem;',
28
+ ' overflow-y: auto;',
29
+ '}',
30
+ // Chevron rotation follows aria-expanded (animated via transition-transform on span)
31
+ 'button[aria-expanded="true"] > span[aria-hidden="true"] {',
32
+ ' transform: rotate(180deg);',
33
+ '}',
34
+ // Visual focus indicator for keyboard-navigated options
35
+ '[role="option"][data-focused] {',
36
+ ' background-color: var(--color-secondary);',
37
+ '}',
38
+ // Hover effect for options (only non-disabled)
39
+ '[role="option"]:not([aria-disabled="true"]):hover {',
40
+ ' background-color: var(--color-secondary);',
41
+ ' cursor: pointer;',
42
+ '}',
43
+ ].join('\n'));
44
+ }
45
+ return _sheet;
46
+ }
47
+
48
+ export function defineCxSelect(wasmFn, baseClass) {
49
+ class CxSelect extends baseClass {
50
+ static observedAttributes = ['id', 'label', 'variant', 'shape', 'size', 'mode', 'selected', 'placeholder', 'helper-text', 'error', 'disabled', 'required', 'name', 'items', 'groups'];
51
+ static _booleanAttrs = new Set(['disabled', 'required']);
52
+
53
+ #outsideClick = null;
54
+ #focusOut = null;
55
+
56
+ connectedCallback() {
57
+ if (!this._isInitialized) {
58
+ this._markInitialized();
59
+ const shadow = this._shadow;
60
+
61
+ // Add positioning + visual feedback styles
62
+ const sheet = getSheet();
63
+ if (!shadow.adoptedStyleSheets.includes(sheet)) {
64
+ shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, sheet];
65
+ }
66
+
67
+ // ── Click handler (delegated on shadow root — survives re-renders) ──
68
+ shadow.addEventListener('click', (e) => {
69
+ // Option click → select it
70
+ const option = e.target.closest('[role="option"]');
71
+ if (option && !option.hasAttribute('aria-disabled')) {
72
+ this.#selectOption(option);
73
+ return;
74
+ }
75
+
76
+ // Trigger click → toggle dropdown
77
+ const trigger = this.#getTrigger();
78
+ if (trigger && trigger.contains(e.target) && !this._props.disabled) {
79
+ this.#toggle();
80
+ }
81
+ });
82
+
83
+ // ── Keyboard handler ──
84
+ shadow.addEventListener('keydown', (e) => this.#handleKey(e));
85
+
86
+ // ── Click outside → close ──
87
+ // mousedown fires before the click that opened, avoiding immediate close
88
+ this.#outsideClick = (e) => {
89
+ if (this.#isOpen() && !this.contains(e.target) && !this._shadow.contains(e.target)) {
90
+ this.#close();
91
+ }
92
+ };
93
+ document.addEventListener('mousedown', this.#outsideClick);
94
+
95
+ // ── Focus exit → close ──
96
+ // When focus leaves the component entirely, close the dropdown.
97
+ // setTimeout lets the new focus target settle before checking.
98
+ // Must check both light DOM and shadow DOM — Node.contains() doesn't cross shadow boundaries.
99
+ this.#focusOut = () => {
100
+ setTimeout(() => {
101
+ if (this.#isOpen()) {
102
+ const active = this._shadow.activeElement || document.activeElement;
103
+ if (!this.contains(active) && !this._shadow.contains(active) && active !== this) {
104
+ this.#close();
105
+ }
106
+ }
107
+ }, 0);
108
+ };
109
+ shadow.addEventListener('focusout', this.#focusOut);
110
+
111
+ // Forward focus events from inner interactive elements
112
+ shadow.addEventListener('focusin', (e) => {
113
+ this._emit('cx-focus', { relatedTarget: e.relatedTarget });
114
+ });
115
+ shadow.addEventListener('focusout', (e) => {
116
+ this._emit('cx-blur', { relatedTarget: e.relatedTarget });
117
+ });
118
+
119
+ // Forward keyboard events from inner interactive elements
120
+ shadow.addEventListener('keydown', (e) => {
121
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
122
+ });
123
+ shadow.addEventListener('keyup', (e) => {
124
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
125
+ });
126
+ } // end _isInitialized guard
127
+ super.connectedCallback();
128
+ }
129
+
130
+ disconnectedCallback() {
131
+ if (this.#outsideClick) {
132
+ document.removeEventListener('mousedown', this.#outsideClick);
133
+ this.#outsideClick = null;
134
+ }
135
+ super.disconnectedCallback();
136
+ }
137
+
138
+ // ── DOM accessors ──
139
+
140
+ #getTrigger() {
141
+ return this._shadow.querySelector('button[role="combobox"]');
142
+ }
143
+
144
+ #getDropdown() {
145
+ return this._shadow.querySelector('[data-floating]');
146
+ }
147
+
148
+ #getVisibleOptions() {
149
+ return Array.from(
150
+ this._shadow.querySelectorAll(
151
+ '[role="option"]:not([aria-disabled="true"]):not([hidden])'
152
+ )
153
+ );
154
+ }
155
+
156
+ // ── Dropdown state ──
157
+
158
+ #isOpen() {
159
+ const dd = this.#getDropdown();
160
+ return dd ? dd.hasAttribute('data-open') : false;
161
+ }
162
+
163
+ #open() {
164
+ const trigger = this.#getTrigger();
165
+ const dd = this.#getDropdown();
166
+ if (!trigger || !dd || this.#isOpen()) return;
167
+
168
+ // Position with fixed coordinates — escapes overflow:auto/scroll clipping.
169
+ this._positionFloatingFixed(trigger, dd, { matchWidth: true });
170
+
171
+ dd.setAttribute('data-open', '');
172
+ dd.classList.remove('hidden');
173
+ dd.style.display = 'block';
174
+ dd.style.pointerEvents = 'auto';
175
+ dd.style.opacity = '1';
176
+ trigger.setAttribute('aria-expanded', 'true');
177
+ }
178
+
179
+ #close() {
180
+ const trigger = this.#getTrigger();
181
+ const dd = this.#getDropdown();
182
+ if (!trigger || !dd) return;
183
+
184
+ dd.removeAttribute('data-open');
185
+ dd.classList.add('hidden');
186
+ dd.style.display = '';
187
+ dd.style.pointerEvents = '';
188
+ dd.style.opacity = '';
189
+ this._resetFloatingFixed(dd);
190
+ trigger.setAttribute('aria-expanded', 'false');
191
+ trigger.removeAttribute('aria-activedescendant');
192
+ this.#clearFocused();
193
+ }
194
+
195
+ #toggle() {
196
+ if (this.#isOpen()) {
197
+ this.#close();
198
+ } else {
199
+ this.#open();
200
+ // Highlight currently selected option (if any) on open
201
+ const selected = this._shadow.querySelector(
202
+ '[role="option"][aria-selected="true"]:not([aria-disabled="true"])'
203
+ );
204
+ if (selected) {
205
+ this.#setActive(selected);
206
+ }
207
+ }
208
+ }
209
+
210
+ // ── Keyboard navigation (WAI-ARIA APG Select-Only Combobox) ──
211
+
212
+ #handleKey(e) {
213
+ const trigger = this.#getTrigger();
214
+ if (!trigger || this._props.disabled) return;
215
+
216
+ // Only handle keys when trigger has focus
217
+ if (e.target !== trigger && !trigger.contains(e.target)) return;
218
+
219
+ const isOpen = this.#isOpen();
220
+
221
+ switch (e.key) {
222
+ case 'Enter':
223
+ case ' ':
224
+ e.preventDefault();
225
+ if (isOpen) {
226
+ const active = this.#getActiveOption();
227
+ if (active) {
228
+ this.#selectOption(active);
229
+ } else {
230
+ this.#close();
231
+ }
232
+ } else {
233
+ this.#open();
234
+ this.#focusSelectedOrFirst();
235
+ }
236
+ break;
237
+
238
+ case 'ArrowDown':
239
+ e.preventDefault();
240
+ if (!isOpen) {
241
+ this.#open();
242
+ this.#focusSelectedOrFirst();
243
+ } else {
244
+ this.#focusNext();
245
+ }
246
+ break;
247
+
248
+ case 'ArrowUp':
249
+ e.preventDefault();
250
+ if (!isOpen) {
251
+ this.#open();
252
+ this.#focusLast();
253
+ } else {
254
+ this.#focusPrev();
255
+ }
256
+ break;
257
+
258
+ case 'Home':
259
+ if (isOpen) {
260
+ e.preventDefault();
261
+ this.#focusFirst();
262
+ }
263
+ break;
264
+
265
+ case 'End':
266
+ if (isOpen) {
267
+ e.preventDefault();
268
+ this.#focusLast();
269
+ }
270
+ break;
271
+
272
+ case 'Escape':
273
+ if (isOpen) {
274
+ e.preventDefault();
275
+ this.#close();
276
+ trigger.focus();
277
+ }
278
+ break;
279
+
280
+ case 'Tab':
281
+ // Close on tab, but let default tab behavior proceed
282
+ if (isOpen) this.#close();
283
+ break;
284
+ }
285
+ }
286
+
287
+ // ── Virtual focus management (aria-activedescendant) ──
288
+ // DOM focus stays on the trigger button. Visual highlighting
289
+ // and screen reader focus use aria-activedescendant + data-focused.
290
+
291
+ #clearFocused() {
292
+ this._shadow.querySelectorAll('[role="option"][data-focused]')
293
+ .forEach(o => o.removeAttribute('data-focused'));
294
+ }
295
+
296
+ #setActive(option) {
297
+ const trigger = this.#getTrigger();
298
+ this.#clearFocused();
299
+ if (option && trigger) {
300
+ option.setAttribute('data-focused', '');
301
+ trigger.setAttribute('aria-activedescendant', option.id);
302
+ option.scrollIntoView({ block: 'nearest' });
303
+ }
304
+ }
305
+
306
+ #getActiveOption() {
307
+ const trigger = this.#getTrigger();
308
+ if (!trigger) return null;
309
+ const id = trigger.getAttribute('aria-activedescendant');
310
+ return id ? this._shadow.getElementById(id) : null;
311
+ }
312
+
313
+ #focusFirst() {
314
+ const opts = this.#getVisibleOptions();
315
+ if (opts.length) this.#setActive(opts[0]);
316
+ }
317
+
318
+ #focusLast() {
319
+ const opts = this.#getVisibleOptions();
320
+ if (opts.length) this.#setActive(opts[opts.length - 1]);
321
+ }
322
+
323
+ #focusSelectedOrFirst() {
324
+ const selected = this._shadow.querySelector(
325
+ '[role="option"][aria-selected="true"]:not([aria-disabled="true"])'
326
+ );
327
+ if (selected) {
328
+ this.#setActive(selected);
329
+ } else {
330
+ this.#focusFirst();
331
+ }
332
+ }
333
+
334
+ #focusNext() {
335
+ const opts = this.#getVisibleOptions();
336
+ if (!opts.length) return;
337
+ const active = this.#getActiveOption();
338
+ const idx = active ? opts.indexOf(active) : -1;
339
+ this.#setActive(opts[(idx + 1) % opts.length]);
340
+ }
341
+
342
+ #focusPrev() {
343
+ const opts = this.#getVisibleOptions();
344
+ if (!opts.length) return;
345
+ const active = this.#getActiveOption();
346
+ const idx = active ? opts.indexOf(active) : 0;
347
+ this.#setActive(opts[(idx - 1 + opts.length) % opts.length]);
348
+ }
349
+
350
+ // ── Option selection ──
351
+
352
+ #selectOption(option) {
353
+ const value = option.getAttribute('data-value') || '';
354
+ const mode = this._props.mode || 'single';
355
+
356
+ if (mode === 'single') {
357
+ // Single select: set this as the only selected option
358
+ this._props.selected = value;
359
+
360
+ // Update display text with the option's label
361
+ const labelSpan = option.querySelector('.truncate');
362
+ const displayText = labelSpan ? labelSpan.textContent : value;
363
+ this.#updateDisplayText(displayText, false);
364
+
365
+ // Update aria-selected + check indicators on all options
366
+ this._shadow.querySelectorAll('[role="option"]').forEach(o => {
367
+ const isThis = o === option;
368
+ o.setAttribute('aria-selected', String(isThis));
369
+ o.setAttribute('data-selected', String(isThis));
370
+ const check = o.querySelector('[data-check]');
371
+ if (check) {
372
+ if (isThis) check.classList.remove('hidden');
373
+ else check.classList.add('hidden');
374
+ }
375
+ });
376
+
377
+ // Close dropdown, return focus to trigger
378
+ this.#close();
379
+ this.#getTrigger()?.focus();
380
+
381
+ // Emit events + form value sync
382
+ this._setFormValue(value);
383
+ this._emit('cx-input', { value });
384
+ this._emit('cx-change', { value });
385
+ } else {
386
+ // Multiple: toggle this option's selection
387
+ const wasSelected = option.getAttribute('aria-selected') === 'true';
388
+ const nowSelected = !wasSelected;
389
+
390
+ option.setAttribute('aria-selected', String(nowSelected));
391
+ option.setAttribute('data-selected', String(nowSelected));
392
+ const check = option.querySelector('[data-check]');
393
+ if (check) {
394
+ if (nowSelected) check.classList.remove('hidden');
395
+ else check.classList.add('hidden');
396
+ }
397
+
398
+ // Collect all currently selected values
399
+ const selected = [];
400
+ const labels = [];
401
+ this._shadow.querySelectorAll('[role="option"][aria-selected="true"]')
402
+ .forEach(o => {
403
+ selected.push(o.getAttribute('data-value') || '');
404
+ const l = o.querySelector('.truncate');
405
+ labels.push(l ? l.textContent : (o.getAttribute('data-value') || ''));
406
+ });
407
+
408
+ // Update display text
409
+ if (labels.length === 0) {
410
+ this.#updateDisplayText(this.#getPlaceholder(), true);
411
+ } else if (labels.length <= 3) {
412
+ this.#updateDisplayText(labels.join(', '), false);
413
+ } else {
414
+ this.#updateDisplayText(`${labels.length} items selected`, false);
415
+ }
416
+
417
+ // Keep dropdown open for more selections
418
+ this._setFormValue(selected.join(','));
419
+ this._emit('cx-change', { value: selected });
420
+ }
421
+ }
422
+
423
+ #updateDisplayText(text, isPlaceholder) {
424
+ const trigger = this.#getTrigger();
425
+ if (!trigger) return;
426
+ // The display text is the first span child with truncate class
427
+ const display = trigger.querySelector('span.truncate');
428
+ if (display) {
429
+ display.textContent = text;
430
+ display.style.color = isPlaceholder ? 'var(--color-text-muted)' : '';
431
+ }
432
+ }
433
+
434
+ #getPlaceholder() {
435
+ const trigger = this.#getTrigger();
436
+ if (!trigger) return '';
437
+ const display = trigger.querySelector('span.truncate');
438
+ return display?.getAttribute('data-placeholder') || '';
439
+ }
440
+
441
+ // ── Public imperative API ──
442
+ open() { this.#open(); }
443
+ close() { this.#close(); }
444
+ focus() { const el = this._shadow.querySelector('button[role="combobox"]'); if (el) el.focus(); else super.focus(); }
445
+
446
+ // ── Render ──
447
+
448
+ _doRender() {
449
+ try {
450
+ // Preserve open state across re-renders (React prop changes
451
+ // trigger _doRender which replaces shadow DOM via innerHTML,
452
+ // losing the data-open attribute on the dropdown).
453
+ const wasOpen = this.#isOpen();
454
+ const activeId = this.#getTrigger()?.getAttribute('aria-activedescendant');
455
+
456
+ const result = wasmFn(this._props);
457
+ this._injectHtml(result);
458
+
459
+ // Ensure positioning styles survive re-render
460
+ const sheet = getSheet();
461
+ if (!this._shadow.adoptedStyleSheets.includes(sheet)) {
462
+ this._shadow.adoptedStyleSheets = [...this._shadow.adoptedStyleSheets, sheet];
463
+ }
464
+
465
+ // Restore open state if dropdown was open before re-render
466
+ if (wasOpen) {
467
+ this.#open();
468
+ if (activeId) {
469
+ const opt = this._shadow.getElementById(activeId);
470
+ if (opt) this.#setActive(opt);
471
+ }
472
+ }
473
+
474
+ // Sync form value
475
+ const selected = this._props.selected;
476
+ if (selected) this._setFormValue(String(selected));
477
+ } catch (e) {
478
+ console.error('[cx-select]', e);
479
+ }
480
+ }
481
+ }
482
+
483
+ customElements.define('cx-select', CxSelect);
484
+ return CxSelect;
485
+ }
@@ -0,0 +1,21 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/sidebar.rs
3
+
4
+ export interface CxSidebarAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ header?: string;
8
+ footer?: string;
9
+ groups?: string;
10
+ separatorsAfter?: string;
11
+ side?: 'left' | 'right';
12
+ state?: 'expanded' | 'narrow' | 'hidden';
13
+ shape?: 'sharp' | 'rounded' | 'pill';
14
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
15
+ }
16
+
17
+ declare global {
18
+ interface HTMLElementTagNameMap {
19
+ 'cx-sidebar': HTMLElement & CxSidebarAttributes;
20
+ }
21
+ }