@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,36 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/fab.rs
3
+
4
+ export function defineCxFab(wasmFn, baseClass) {
5
+ class CxFab extends baseClass {
6
+ static observedAttributes = ['icon', 'label', 'aria-label', 'variant', 'intent', 'shape', 'size', 'disabled', 'has-popup', 'expanded', 'controls'];
7
+ static _booleanAttrs = new Set(['disabled', 'has-popup', 'expanded']);
8
+ static _hostDisplay = 'inline-flex';
9
+
10
+
11
+ connectedCallback() {
12
+ if (!this._isInitialized) {
13
+ this._markInitialized();
14
+ // Delegate click from inner element to Custom Event
15
+ this._shadow.addEventListener('click', (e) => {
16
+ if (!this.hasAttribute('disabled')) {
17
+ this._emit('cx-click', { originalEvent: e });
18
+ }
19
+ });
20
+ }
21
+ super.connectedCallback();
22
+ }
23
+
24
+ _doRender() {
25
+ try {
26
+ const result = wasmFn(this._props);
27
+ this._injectHtml(result);
28
+ } catch (e) {
29
+ console.error('[cx-fab]', e);
30
+ }
31
+ }
32
+ }
33
+
34
+ customElements.define('cx-fab', CxFab);
35
+ return CxFab;
36
+ }
@@ -0,0 +1,26 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/file_upload.rs
3
+
4
+ export interface CxFileUploadAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ mode?: 'inline' | 'overlay';
8
+ variant?: 'outline' | 'filled' | 'subtle';
9
+ shape?: 'sharp' | 'rounded' | 'pill';
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ disabled?: boolean;
12
+ multiple?: boolean;
13
+ accept?: string;
14
+ maxSize?: number;
15
+ heading?: string;
16
+ browseText?: string;
17
+ hint?: string;
18
+ capture?: string;
19
+ preview?: boolean;
20
+ }
21
+
22
+ declare global {
23
+ interface HTMLElementTagNameMap {
24
+ 'cx-file-upload': HTMLElement & CxFileUploadAttributes;
25
+ }
26
+ }
@@ -0,0 +1,59 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/file_upload.rs
3
+
4
+ export function defineCxFileUpload(wasmFn, baseClass) {
5
+ class CxFileUpload extends baseClass {
6
+ static observedAttributes = ['id', 'label', 'mode', 'variant', 'shape', 'size', 'disabled', 'multiple', 'accept', 'heading', 'browse-text', 'hint', 'capture', 'preview'];
7
+ static _booleanAttrs = new Set(['disabled', 'multiple', 'preview']);
8
+ static _hostDisplay = 'block';
9
+
10
+ set max_size(v) { this._setProp('max_size', v); }
11
+ get max_size() { return this._props.max_size; }
12
+
13
+ connectedCallback() {
14
+ if (!this._isInitialized) {
15
+ this._markInitialized();
16
+ const shadow = this._shadow;
17
+
18
+ // Delegate change events from the hidden file input → cx-change on the host
19
+ shadow.addEventListener('change', (e) => {
20
+ const input = e.target.closest('input[type="file"]');
21
+ if (input && input.files) {
22
+ const files = Array.from(input.files).map(f => ({
23
+ name: f.name,
24
+ size: f.size,
25
+ type: f.type,
26
+ }));
27
+ this._emit('cx-change', { files });
28
+ }
29
+ });
30
+
31
+ // Delegate click on zone or browse button → open file picker
32
+ shadow.addEventListener('click', (e) => {
33
+ if (this.hasAttribute('disabled')) return;
34
+ const input = shadow.querySelector('input[type="file"]');
35
+ if (!input) return;
36
+ // Don't re-trigger if clicking the input itself
37
+ if (e.target === input) return;
38
+ // Don't trigger if clicking file list dismiss buttons
39
+ if (e.target.closest('[data-file-remove]')) return;
40
+ if (e.target.closest('[data-file-upload-list]')) return;
41
+ input.click();
42
+ });
43
+ }
44
+ super.connectedCallback();
45
+ }
46
+
47
+ _doRender() {
48
+ try {
49
+ const result = wasmFn(this._props);
50
+ this._injectHtml(result);
51
+ } catch (e) {
52
+ console.error('[cx-file-upload]', e);
53
+ }
54
+ }
55
+ }
56
+
57
+ customElements.define('cx-file-upload', CxFileUpload);
58
+ return CxFileUpload;
59
+ }
@@ -0,0 +1,19 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/listbox.rs
3
+
4
+ export interface CxListboxAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ shape?: 'sharp' | 'rounded' | 'pill';
8
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
9
+ selectionMode?: string;
10
+ selected?: string;
11
+ items?: string;
12
+ groups?: string;
13
+ }
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'cx-listbox': HTMLElement & CxListboxAttributes;
18
+ }
19
+ }
@@ -0,0 +1,250 @@
1
+ // Custom behavior for <cx-listbox> — option selection + keyboard navigation.
2
+ //
3
+ // The Rust component renders:
4
+ // <div role="listbox"> — the container
5
+ // <div role="option" data-value="..." aria-selected="..."> — each option
6
+ // <span class="truncate">Label</span>
7
+ // <span data-check class="hidden|inline-flex">✓</span>
8
+ //
9
+ // This Custom Element wires up:
10
+ // - Click option to select/deselect
11
+ // - Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space)
12
+ // - Single and multiple selection modes
13
+ // - cx-change event emission with selected value(s)
14
+ //
15
+ // Source: crates/wasm-api/src/listbox.rs
16
+
17
+ let _sheet;
18
+ function getSheet() {
19
+ if (!_sheet) {
20
+ _sheet = new CSSStyleSheet();
21
+ _sheet.replaceSync([
22
+ '[role="option"][data-focused] {',
23
+ ' background-color: var(--color-secondary);',
24
+ '}',
25
+ '[role="option"]:not([aria-disabled="true"]):hover {',
26
+ ' background-color: var(--color-secondary);',
27
+ ' cursor: pointer;',
28
+ '}',
29
+ ].join('\n'));
30
+ }
31
+ return _sheet;
32
+ }
33
+
34
+ export function defineCxListbox(wasmFn, baseClass) {
35
+ class CxListbox extends baseClass {
36
+ static observedAttributes = ['id', 'label', 'shape', 'size', 'selection-mode', 'selected', 'items', 'groups'];
37
+ static _booleanAttrs = new Set([]);
38
+
39
+ connectedCallback() {
40
+ if (!this._isInitialized) {
41
+ this._markInitialized();
42
+ const shadow = this._shadow;
43
+
44
+ const sheet = getSheet();
45
+ if (!shadow.adoptedStyleSheets.includes(sheet)) {
46
+ shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, sheet];
47
+ }
48
+
49
+ // ── Click handler ──
50
+ shadow.addEventListener('click', (e) => {
51
+ const option = e.target.closest('[role="option"]');
52
+ if (option && !option.hasAttribute('aria-disabled')) {
53
+ this.#selectOption(option);
54
+ }
55
+ });
56
+
57
+ // ── Keyboard handler ──
58
+ shadow.addEventListener('keydown', (e) => this.#handleKey(e));
59
+
60
+ // Forward focus events from inner interactive elements
61
+ shadow.addEventListener('focusin', (e) => {
62
+ this._emit('cx-focus', { relatedTarget: e.relatedTarget });
63
+ });
64
+ shadow.addEventListener('focusout', (e) => {
65
+ this._emit('cx-blur', { relatedTarget: e.relatedTarget });
66
+ });
67
+
68
+ // Forward keyboard events from inner interactive elements
69
+ shadow.addEventListener('keydown', (e) => {
70
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
71
+ });
72
+ shadow.addEventListener('keyup', (e) => {
73
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
74
+ });
75
+ } // end _isInitialized guard
76
+ super.connectedCallback();
77
+ }
78
+
79
+ #getListbox() {
80
+ return this._shadow.querySelector('[role="listbox"]');
81
+ }
82
+
83
+ #getVisibleOptions() {
84
+ return Array.from(
85
+ this._shadow.querySelectorAll(
86
+ '[role="option"]:not([aria-disabled="true"]):not([hidden])'
87
+ )
88
+ );
89
+ }
90
+
91
+ #getMode() {
92
+ return this._props.selection_mode || 'single';
93
+ }
94
+
95
+ // ── Keyboard navigation ──
96
+
97
+ #handleKey(e) {
98
+ const listbox = this.#getListbox();
99
+ if (!listbox) return;
100
+
101
+ // Only handle if focus is inside the listbox
102
+ if (!listbox.contains(e.target) && e.target !== listbox) return;
103
+
104
+ switch (e.key) {
105
+ case 'ArrowDown':
106
+ e.preventDefault();
107
+ this.#focusNext();
108
+ break;
109
+ case 'ArrowUp':
110
+ e.preventDefault();
111
+ this.#focusPrev();
112
+ break;
113
+ case 'Home':
114
+ e.preventDefault();
115
+ this.#focusFirst();
116
+ break;
117
+ case 'End':
118
+ e.preventDefault();
119
+ this.#focusLast();
120
+ break;
121
+ case 'Enter':
122
+ case ' ':
123
+ e.preventDefault();
124
+ const active = this.#getActiveOption();
125
+ if (active) this.#selectOption(active);
126
+ break;
127
+ }
128
+ }
129
+
130
+ // ── Focus management (roving tabindex) ──
131
+
132
+ #clearFocused() {
133
+ this._shadow.querySelectorAll('[role="option"][data-focused]')
134
+ .forEach(o => {
135
+ o.removeAttribute('data-focused');
136
+ o.setAttribute('tabindex', '-1');
137
+ });
138
+ }
139
+
140
+ #setActive(option) {
141
+ this.#clearFocused();
142
+ if (option) {
143
+ option.setAttribute('data-focused', '');
144
+ option.setAttribute('tabindex', '0');
145
+ option.focus();
146
+ }
147
+ }
148
+
149
+ #getActiveOption() {
150
+ return this._shadow.querySelector('[role="option"][data-focused]');
151
+ }
152
+
153
+ #focusFirst() {
154
+ const opts = this.#getVisibleOptions();
155
+ if (opts.length) this.#setActive(opts[0]);
156
+ }
157
+
158
+ #focusLast() {
159
+ const opts = this.#getVisibleOptions();
160
+ if (opts.length) this.#setActive(opts[opts.length - 1]);
161
+ }
162
+
163
+ #focusNext() {
164
+ const opts = this.#getVisibleOptions();
165
+ if (!opts.length) return;
166
+ const active = this.#getActiveOption();
167
+ const idx = active ? opts.indexOf(active) : -1;
168
+ this.#setActive(opts[(idx + 1) % opts.length]);
169
+ }
170
+
171
+ #focusPrev() {
172
+ const opts = this.#getVisibleOptions();
173
+ if (!opts.length) return;
174
+ const active = this.#getActiveOption();
175
+ const idx = active ? opts.indexOf(active) : 0;
176
+ this.#setActive(opts[(idx - 1 + opts.length) % opts.length]);
177
+ }
178
+
179
+ // ── Option selection ──
180
+
181
+ #selectOption(option) {
182
+ const value = option.getAttribute('data-value') || '';
183
+ const mode = this.#getMode();
184
+
185
+ if (mode === 'single') {
186
+ // Single: replace selection
187
+ this._shadow.querySelectorAll('[role="option"]').forEach(o => {
188
+ const isThis = o === option;
189
+ o.setAttribute('aria-selected', String(isThis));
190
+ o.setAttribute('data-selected', String(isThis));
191
+ const check = o.querySelector('[data-check]');
192
+ if (check) {
193
+ if (isThis) check.classList.remove('hidden');
194
+ else check.classList.add('hidden');
195
+ }
196
+ });
197
+
198
+ // Sync host prop — single source of truth for WASM re-renders.
199
+ this._props.selected = value;
200
+
201
+ this._emit('cx-change', { value });
202
+ } else {
203
+ // Multiple: toggle
204
+ const wasSelected = option.getAttribute('aria-selected') === 'true';
205
+ const nowSelected = !wasSelected;
206
+
207
+ option.setAttribute('aria-selected', String(nowSelected));
208
+ option.setAttribute('data-selected', String(nowSelected));
209
+ const check = option.querySelector('[data-check]');
210
+ if (check) {
211
+ if (nowSelected) check.classList.remove('hidden');
212
+ else check.classList.add('hidden');
213
+ }
214
+
215
+ // Collect all selected
216
+ const selected = [];
217
+ this._shadow.querySelectorAll('[role="option"][aria-selected="true"]')
218
+ .forEach(o => selected.push(o.getAttribute('data-value') || ''));
219
+
220
+ // Sync host prop — single source of truth for WASM re-renders.
221
+ this._props.selected = selected.join(',');
222
+
223
+ this._emit('cx-change', { value: selected });
224
+ }
225
+ }
226
+
227
+ _doRender() {
228
+ try {
229
+ const result = wasmFn(this._props);
230
+ this._injectHtml(result);
231
+
232
+ const sheet = getSheet();
233
+ if (!this._shadow.adoptedStyleSheets.includes(sheet)) {
234
+ this._shadow.adoptedStyleSheets = [...this._shadow.adoptedStyleSheets, sheet];
235
+ }
236
+
237
+ // Make listbox focusable
238
+ const listbox = this.#getListbox();
239
+ if (listbox && !listbox.hasAttribute('tabindex')) {
240
+ listbox.setAttribute('tabindex', '0');
241
+ }
242
+ } catch (e) {
243
+ console.error('[cx-listbox]', e);
244
+ }
245
+ }
246
+ }
247
+
248
+ customElements.define('cx-listbox', CxListbox);
249
+ return CxListbox;
250
+ }
@@ -0,0 +1,20 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/menu.rs
3
+
4
+ export interface CxMenuAttributes {
5
+ id?: string;
6
+ triggerLabel: string;
7
+ entries?: string;
8
+ variant?: 'filled' | 'outline' | 'ghost';
9
+ shape?: 'sharp' | 'rounded' | 'pill';
10
+ width?: 'auto' | 'sm' | 'md' | 'lg';
11
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
12
+ icon?: string;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ 'cx-menu': HTMLElement & CxMenuAttributes;
19
+ }
20
+ }
@@ -0,0 +1,224 @@
1
+ // Custom behavior for <cx-menu> — dropdown open/close, keyboard nav, item click.
2
+ // Mirrors static/_behaviors/menu.js but operates inside shadow DOM.
3
+ // Source: crates/wasm-api/src/menu.rs
4
+
5
+ export function defineCxMenu(wasmFn, baseClass) {
6
+ class CxMenu extends baseClass {
7
+ static observedAttributes = ['id', 'trigger-label', 'entries', 'variant', 'shape', 'width', 'size', 'icon', 'disabled'];
8
+ static _booleanAttrs = new Set(['disabled']);
9
+ static _hostDisplay = 'inline-flex';
10
+
11
+ #outsideClick = null;
12
+
13
+ connectedCallback() {
14
+ if (!this._isInitialized) {
15
+ this._markInitialized();
16
+ const shadow = this._shadow;
17
+
18
+ // Click delegation
19
+ shadow.addEventListener('click', (e) => {
20
+ // Trigger button: toggle menu
21
+ const trigger = e.target.closest('[data-floating-trigger]');
22
+ if (trigger) {
23
+ if (this.#isOpen()) {
24
+ this.#closeMenu();
25
+ } else {
26
+ this.#openMenu();
27
+ }
28
+ return;
29
+ }
30
+
31
+ // Menu item click (regular menuitem): close and emit
32
+ const mi = e.target.closest('[role="menuitem"]');
33
+ if (mi && !mi.hasAttribute('aria-disabled')) {
34
+ const id = mi.getAttribute('data-item-id') || mi.textContent.trim();
35
+ this._emit('cx-action', { id });
36
+ this.#closeMenu();
37
+ return;
38
+ }
39
+
40
+ // Checkbox/radio items: toggle state but don't close
41
+ const mci = e.target.closest('[role="menuitemcheckbox"],[role="menuitemradio"]');
42
+ if (mci && !mci.hasAttribute('aria-disabled')) {
43
+ const checked = mci.getAttribute('aria-checked') === 'true';
44
+ const nowChecked = !checked;
45
+ mci.setAttribute('aria-checked', String(nowChecked));
46
+
47
+ // For radio items: deselect siblings in same group
48
+ if (mci.getAttribute('role') === 'menuitemradio' && nowChecked) {
49
+ const group = mci.closest('[role="group"]');
50
+ if (group) {
51
+ group.querySelectorAll('[role="menuitemradio"]').forEach(r => {
52
+ if (r !== mci) r.setAttribute('aria-checked', 'false');
53
+ });
54
+ }
55
+ }
56
+
57
+ // Sync checked item IDs to _props for re-render survival.
58
+ // Store as comma-separated list of checked data-item-ids.
59
+ const panel = shadow.querySelector('[data-menu]');
60
+ if (panel) {
61
+ const checkedIds = [];
62
+ panel.querySelectorAll('[role="menuitemcheckbox"][aria-checked="true"],[role="menuitemradio"][aria-checked="true"]')
63
+ .forEach(el => checkedIds.push(el.getAttribute('data-item-id') || ''));
64
+ this._props._checked_items = checkedIds.join(',');
65
+ }
66
+
67
+ this._emit('cx-change', {
68
+ id: mci.getAttribute('data-item-id') || mci.textContent.trim(),
69
+ checked: nowChecked,
70
+ });
71
+ }
72
+ });
73
+
74
+ // Keyboard navigation
75
+ shadow.addEventListener('keydown', (e) => {
76
+ const menuitem = e.target.closest('[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]');
77
+
78
+ // Escape: close menu
79
+ if (e.key === 'Escape' && this.#isOpen()) {
80
+ e.preventDefault();
81
+ this.#closeMenu();
82
+ return;
83
+ }
84
+
85
+ // Arrow keys on trigger
86
+ if (e.target.closest('[data-floating-trigger]') && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {
87
+ if (!this.#isOpen()) {
88
+ e.preventDefault();
89
+ this.#openMenu();
90
+ return;
91
+ }
92
+ }
93
+
94
+ if (!menuitem) return;
95
+
96
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {
97
+ e.preventDefault();
98
+ const panel = shadow.querySelector('[data-menu]');
99
+ if (!panel) return;
100
+ const all = Array.from(panel.querySelectorAll(
101
+ '[role="menuitem"]:not([aria-disabled]),[role="menuitemcheckbox"]:not([aria-disabled]),[role="menuitemradio"]:not([aria-disabled])'
102
+ ));
103
+ const ci = all.indexOf(menuitem);
104
+ if (ci === -1) return;
105
+ let ni = ci;
106
+ if (e.key === 'ArrowDown') ni = (ci + 1) % all.length;
107
+ else if (e.key === 'ArrowUp') ni = (ci - 1 + all.length) % all.length;
108
+ else if (e.key === 'Home') ni = 0;
109
+ else if (e.key === 'End') ni = all.length - 1;
110
+ all[ni].focus();
111
+ return;
112
+ }
113
+
114
+ if (e.key === 'Enter' || e.key === ' ') {
115
+ e.preventDefault();
116
+ menuitem.click();
117
+ }
118
+ });
119
+
120
+ // Close on outside click — use mousedown to fire before click
121
+ this.#outsideClick = (e) => {
122
+ if (this.#isOpen() && !this.contains(e.target) && !this._shadow.contains(e.target)) {
123
+ this.#closeMenu();
124
+ }
125
+ };
126
+ document.addEventListener('mousedown', this.#outsideClick);
127
+
128
+ // Forward keyboard events from inner interactive elements
129
+ shadow.addEventListener('keydown', (e) => {
130
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
131
+ });
132
+ shadow.addEventListener('keyup', (e) => {
133
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
134
+ });
135
+ } // end _isInitialized guard
136
+
137
+ super.connectedCallback();
138
+ }
139
+
140
+ disconnectedCallback() {
141
+ if (this.#outsideClick) {
142
+ document.removeEventListener('mousedown', this.#outsideClick);
143
+ }
144
+ super.disconnectedCallback();
145
+ }
146
+
147
+ // ── State via DOM (single source of truth) ──
148
+
149
+ #isOpen() {
150
+ const trigger = this._shadow.querySelector('[data-floating-trigger]');
151
+ return trigger ? trigger.getAttribute('aria-expanded') === 'true' : false;
152
+ }
153
+
154
+ #openMenu() {
155
+ const panel = this._shadow.querySelector('[data-menu]');
156
+ const trigger = this._shadow.querySelector('[data-floating-trigger]');
157
+ if (!panel || !trigger) return;
158
+
159
+ // Position with fixed coordinates — escapes overflow:auto/scroll clipping.
160
+ this._positionFloatingFixed(trigger, panel, { matchWidth: true });
161
+
162
+ panel.setAttribute('data-open', '');
163
+ panel.classList.remove('hidden');
164
+ panel.style.display = 'block';
165
+ panel.style.pointerEvents = 'auto';
166
+ panel.style.opacity = '1';
167
+ trigger.setAttribute('aria-expanded', 'true');
168
+
169
+ // Focus first non-disabled item
170
+ requestAnimationFrame(() => {
171
+ const first = panel.querySelector(
172
+ '[role="menuitem"]:not([aria-disabled]),[role="menuitemcheckbox"]:not([aria-disabled]),[role="menuitemradio"]:not([aria-disabled])'
173
+ );
174
+ if (first) first.focus();
175
+ });
176
+ }
177
+
178
+ #closeMenu() {
179
+ const panel = this._shadow.querySelector('[data-menu]');
180
+ const trigger = this._shadow.querySelector('[data-floating-trigger]');
181
+ if (panel) {
182
+ panel.removeAttribute('data-open');
183
+ panel.classList.add('hidden');
184
+ panel.style.display = '';
185
+ panel.style.pointerEvents = '';
186
+ panel.style.opacity = '';
187
+ this._resetFloatingFixed(panel);
188
+ }
189
+ if (trigger) {
190
+ trigger.setAttribute('aria-expanded', 'false');
191
+ trigger.focus();
192
+ }
193
+ }
194
+
195
+ // ── Public imperative API ──
196
+ open() { this.#openMenu(); }
197
+ close() { this.#closeMenu(); }
198
+
199
+ _doRender() {
200
+ try {
201
+ const wasOpen = this.#isOpen();
202
+ // Save checked item IDs before re-render
203
+ const checkedIds = this._props._checked_items || '';
204
+ const result = wasmFn(this._props);
205
+ this._injectHtml(result);
206
+ if (wasOpen) this.#openMenu();
207
+ // Restore checkbox/radio checked states after re-render
208
+ if (checkedIds) {
209
+ const ids = checkedIds.split(',').filter(Boolean);
210
+ const shadow = this._shadow;
211
+ shadow.querySelectorAll('[role="menuitemcheckbox"],[role="menuitemradio"]').forEach(el => {
212
+ const id = el.getAttribute('data-item-id') || '';
213
+ if (ids.includes(id)) el.setAttribute('aria-checked', 'true');
214
+ });
215
+ }
216
+ } catch (e) {
217
+ console.error('[cx-menu]', e);
218
+ }
219
+ }
220
+ }
221
+
222
+ customElements.define('cx-menu', CxMenu);
223
+ return CxMenu;
224
+ }
@@ -0,0 +1,23 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/message_bubble.rs
3
+
4
+ export interface CxMessageBubbleAttributes {
5
+ id?: string;
6
+ role?: 'user' | 'assistant';
7
+ variant?: 'filled' | 'ghost';
8
+ shape?: 'sharp' | 'rounded';
9
+ alignment?: 'auto' | 'start' | 'end';
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ content?: string;
12
+ htmlContent?: string;
13
+ senderName?: string;
14
+ timestamp?: string;
15
+ border?: string;
16
+ animated?: boolean;
17
+ }
18
+
19
+ declare global {
20
+ interface HTMLElementTagNameMap {
21
+ 'cx-message-bubble': HTMLElement & CxMessageBubbleAttributes;
22
+ }
23
+ }
@@ -0,0 +1,29 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/message_bubble.rs
3
+
4
+ export function defineCxMessageBubble(wasmFn, baseClass) {
5
+ class CxMessageBubble extends baseClass {
6
+ static observedAttributes = ['id', 'role', 'variant', 'shape', 'alignment', 'size', 'html-content', 'sender-name', 'timestamp', 'border', 'animated'];
7
+ static _booleanAttrs = new Set(['animated']);
8
+ static _focusable = false;
9
+ static _hostDisplay = 'block';
10
+
11
+
12
+ connectedCallback() {
13
+ super.connectedCallback();
14
+ }
15
+
16
+ _doRender() {
17
+ try {
18
+ this._props.slotted = true;
19
+ const result = wasmFn(this._props);
20
+ this._injectHtml(result);
21
+ } catch (e) {
22
+ console.error('[cx-message-bubble]', e);
23
+ }
24
+ }
25
+ }
26
+
27
+ customElements.define('cx-message-bubble', CxMessageBubble);
28
+ return CxMessageBubble;
29
+ }