@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,371 @@
1
+ // Custom behavior for <cx-table> — uncontrolled by default, controlled when opted in.
2
+ //
3
+ // Uncontrolled: sort, select, expand, and paginate work without wiring up
4
+ // state. The table manages its own sort order, selection, and page internally.
5
+ // Controlled: pass sorts/selected props to take over. Same pattern as native
6
+ // <input> — unmanaged until you pass value + onChange.
7
+ //
8
+ // Source: crates/wasm-api/src/table.rs
9
+
10
+ // ─── Sort Direction Icons ───
11
+
12
+ const SORT_ASC = '<svg class="size-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v10M4 7l4-4 4 4"/></svg>';
13
+ const SORT_DESC = '<svg class="size-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13V3M4 9l4 4 4-4"/></svg>';
14
+ const SORT_NEUTRAL = '<svg class="size-3.5 opacity-40" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4-3 4 3M4 10l4 3 4-3"/></svg>';
15
+
16
+ export function defineCxTable(wasmFn, baseClass) {
17
+ class CxTable extends baseClass {
18
+ static observedAttributes = ['id', 'caption', 'columns', 'rows', 'variant', 'size', 'selectable', 'selected', 'sorts', 'pagination', 'hoverable', 'sticky-header', 'footer', 'empty-state', 'column-order', 'disabled', 'loading', 'error'];
19
+ static _booleanAttrs = new Set(['hoverable', 'sticky-header', 'disabled']);
20
+ static _numericAttrs = new Set(['loading']);
21
+
22
+ // ── Internal state (uncontrolled mode) ──
23
+ #sorts = []; // [{column, direction}] — direction is 'ascending'|'descending'
24
+ #selected = new Set(); // row IDs
25
+
26
+ // ── Controlled detection ──
27
+ // Once a consumer sets sorts/selected externally (via attribute), the
28
+ // table stays controlled for that feature. Unset → uncontrolled.
29
+ #controlledSorts = false;
30
+ #controlledSelected = false;
31
+
32
+ attributeChangedCallback(name, oldVal, newVal) {
33
+ if (name === 'sorts' && newVal !== null) this.#controlledSorts = true;
34
+ if (name === 'selected' && newVal !== null) this.#controlledSelected = true;
35
+ super.attributeChangedCallback(name, oldVal, newVal);
36
+ }
37
+
38
+ connectedCallback() {
39
+ if (!this._isInitialized) {
40
+ this._markInitialized();
41
+ const shadow = this._shadow;
42
+
43
+ // ── Checkbox change events (selection) ──
44
+ shadow.addEventListener('change', (e) => {
45
+ if (e.target.type !== 'checkbox') return;
46
+
47
+ // Select-all checkbox (in thead)
48
+ if (e.target.closest('thead')) {
49
+ this.#handleSelectAll(e.target.checked);
50
+ return;
51
+ }
52
+
53
+ // Individual row checkbox
54
+ const row = e.target.closest('tr[data-row-id]');
55
+ if (row) {
56
+ const rowId = row.getAttribute('data-row-id');
57
+ this.#handleRowSelect(rowId, e.target.checked);
58
+ }
59
+ });
60
+
61
+ // ── Click events (sort, expand, page, row select) ──
62
+ shadow.addEventListener('click', (e) => {
63
+ // Sort header button
64
+ const sortBtn = e.target.closest('[data-sort-column]');
65
+ if (sortBtn) {
66
+ this.#handleSort(sortBtn);
67
+ return;
68
+ }
69
+
70
+ // Expand/collapse button
71
+ const expandBtn = e.target.closest('button[aria-expanded][data-row-id]');
72
+ if (expandBtn) {
73
+ this.#handleExpand(expandBtn);
74
+ return;
75
+ }
76
+
77
+ // Page number button
78
+ const pageBtn = e.target.closest('[data-page]');
79
+ if (pageBtn) {
80
+ const page = parseInt(pageBtn.getAttribute('data-page'), 10);
81
+ if (!isNaN(page)) this.#handlePage(page);
82
+ return;
83
+ }
84
+
85
+ // Prev/Next pagination button (no data-page, inside <nav>)
86
+ const navBtn = e.target.closest('nav button');
87
+ if (navBtn && !navBtn.disabled && !navBtn.hasAttribute('data-page')) {
88
+ this.#handlePrevNext(navBtn);
89
+ return;
90
+ }
91
+
92
+ // Row click (non-button, non-checkbox area) → toggle selection
93
+ if (this._props.selectable && this._props.selectable !== 'none') {
94
+ const row = e.target.closest('tr[data-row-id]');
95
+ if (row && !row.hasAttribute('data-disabled') && !e.target.closest('button') && !e.target.closest('input')) {
96
+ const rowId = row.getAttribute('data-row-id');
97
+ const isSelected = this.#selected.has(rowId);
98
+ this.#handleRowSelect(rowId, !isSelected);
99
+ }
100
+ }
101
+ });
102
+
103
+ // Keyboard forwarding
104
+ shadow.addEventListener('keydown', (e) => {
105
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
106
+ });
107
+ shadow.addEventListener('keyup', (e) => {
108
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
109
+ });
110
+ } // end _isInitialized guard
111
+ super.connectedCallback();
112
+ }
113
+
114
+ // ─── Sort ───
115
+
116
+ #handleSort(btn) {
117
+ const columnId = btn.getAttribute('data-sort-column');
118
+ const th = btn.closest('th[data-column-id]');
119
+ const currentSort = th?.getAttribute('aria-sort') || 'none';
120
+ // Cycle: none → ascending → descending → none
121
+ const next = currentSort === 'ascending' ? 'descending'
122
+ : currentSort === 'descending' ? 'none' : 'ascending';
123
+
124
+ if (!this.#controlledSorts) {
125
+ this.#applySortDOM(columnId, next);
126
+ }
127
+
128
+ this._emit('cx-sort', {
129
+ column_id: columnId,
130
+ direction: next === 'ascending' ? 'asc' : next === 'descending' ? 'desc' : 'none'
131
+ });
132
+ }
133
+
134
+ #applySortDOM(columnId, direction) {
135
+ const table = this._shadow.querySelector('table');
136
+ if (!table) return;
137
+
138
+ // Update internal state
139
+ this.#sorts = direction === 'none' ? [] : [{ column: columnId, direction }];
140
+
141
+ // Sync to _props so WASM re-renders preserve sort indicators.
142
+ // _props.sorts is consumed by the WASM adapter for aria-sort + icon rendering.
143
+ this._props.sorts = this.#sorts.map(s => ({
144
+ column_id: s.column,
145
+ direction: s.direction === 'ascending' ? 'asc' : 'desc'
146
+ }));
147
+
148
+ // Update aria-sort + icons on all sortable headers
149
+ table.querySelectorAll('th[data-column-id]').forEach(h => {
150
+ const hId = h.getAttribute('data-column-id');
151
+ const isTarget = hId === columnId;
152
+ const sortDir = isTarget ? direction : 'none';
153
+ if (h.hasAttribute('aria-sort')) h.setAttribute('aria-sort', sortDir);
154
+ const btn = h.querySelector('button[data-sort-column]');
155
+ if (btn) {
156
+ const icon = btn.querySelector('span[aria-hidden]');
157
+ if (icon) {
158
+ icon.innerHTML = sortDir === 'ascending' ? SORT_ASC
159
+ : sortDir === 'descending' ? SORT_DESC : SORT_NEUTRAL;
160
+ }
161
+ }
162
+ });
163
+
164
+ // Remove multi-sort priority badges (uncontrolled = single-column sort)
165
+ table.querySelectorAll('[aria-label^="Sort priority"]').forEach(b => b.remove());
166
+
167
+ // Reorder tbody rows
168
+ this.#sortTbodyRows(table, columnId, direction);
169
+ }
170
+
171
+ #sortTbodyRows(table, columnId, direction) {
172
+ const tbody = table.querySelector('tbody');
173
+ if (!tbody) return;
174
+
175
+ const headers = Array.from(table.querySelectorAll('thead th'));
176
+ const th = table.querySelector(`th[data-column-id="${CSS.escape(columnId)}"]`);
177
+ const colIdx = headers.indexOf(th);
178
+ if (colIdx === -1) return;
179
+
180
+ // Collect non-detail rows
181
+ const rows = Array.from(tbody.querySelectorAll(':scope > tr:not([id$="-detail"])'));
182
+
183
+ if (direction === 'none') {
184
+ // Restore original order
185
+ rows.sort((a, b) => {
186
+ return (parseInt(a.getAttribute('data-original-index') || '0', 10))
187
+ - (parseInt(b.getAttribute('data-original-index') || '0', 10));
188
+ });
189
+ } else {
190
+ // Tag original order on first sort
191
+ rows.forEach((r, i) => {
192
+ if (!r.hasAttribute('data-original-index')) r.setAttribute('data-original-index', String(i));
193
+ });
194
+
195
+ // Detect numeric column (all non-empty cells parse as numbers)
196
+ const isNumeric = rows.every(r => {
197
+ const cells = r.querySelectorAll(':scope > td');
198
+ const v = (cells[colIdx]?.textContent || '').trim();
199
+ return v === '' || !isNaN(parseFloat(v));
200
+ });
201
+
202
+ rows.sort((a, b) => {
203
+ const va = (a.querySelectorAll(':scope > td')[colIdx]?.textContent || '').trim();
204
+ const vb = (b.querySelectorAll(':scope > td')[colIdx]?.textContent || '').trim();
205
+ const cmp = isNumeric
206
+ ? (parseFloat(va || '0') - parseFloat(vb || '0'))
207
+ : va.localeCompare(vb);
208
+ return direction === 'descending' ? -cmp : cmp;
209
+ });
210
+ }
211
+
212
+ // Re-append in sorted order (detail rows follow their parent)
213
+ rows.forEach(r => {
214
+ tbody.appendChild(r);
215
+ const ctrl = r.querySelector('button[aria-controls]');
216
+ if (ctrl) {
217
+ const detailId = ctrl.getAttribute('aria-controls');
218
+ if (detailId) {
219
+ const detail = tbody.querySelector(`[id="${CSS.escape(detailId)}"]`);
220
+ if (detail) tbody.appendChild(detail);
221
+ }
222
+ }
223
+ });
224
+ }
225
+
226
+ // ─── Selection ───
227
+
228
+ #handleRowSelect(rowId, selected) {
229
+ if (!this.#controlledSelected) {
230
+ const mode = this._props.selectable;
231
+ if (mode === 'single') {
232
+ this.#selected.clear();
233
+ if (selected) this.#selected.add(rowId);
234
+ } else {
235
+ if (selected) this.#selected.add(rowId);
236
+ else this.#selected.delete(rowId);
237
+ }
238
+ this._props.selected = [...this.#selected];
239
+ this.#syncSelectionDOM();
240
+ }
241
+
242
+ this._emit('cx-select', { selected: [...this.#selected], row_id: rowId });
243
+ }
244
+
245
+ #handleSelectAll(checked) {
246
+ if (!this.#controlledSelected) {
247
+ if (checked) {
248
+ // Select all non-disabled rows
249
+ this._shadow.querySelectorAll('tr[data-row-id]:not([data-disabled])').forEach(tr => {
250
+ this.#selected.add(tr.getAttribute('data-row-id'));
251
+ });
252
+ } else {
253
+ this.#selected.clear();
254
+ }
255
+ this._props.selected = [...this.#selected];
256
+ this.#syncSelectionDOM();
257
+ }
258
+
259
+ this._emit('cx-select', { selected: [...this.#selected] });
260
+ }
261
+
262
+ #syncSelectionDOM() {
263
+ const shadow = this._shadow;
264
+
265
+ // Update row checkboxes and aria-selected
266
+ shadow.querySelectorAll('tr[data-row-id]').forEach(tr => {
267
+ const rid = tr.getAttribute('data-row-id');
268
+ const isSel = this.#selected.has(rid);
269
+ if (tr.hasAttribute('aria-selected')) {
270
+ tr.setAttribute('aria-selected', String(isSel));
271
+ }
272
+ const cb = tr.querySelector('input[type="checkbox"]');
273
+ if (cb && !cb.disabled) cb.checked = isSel;
274
+ });
275
+
276
+ // Update select-all checkbox (multiple mode)
277
+ const selectAllCb = shadow.querySelector('thead input[type="checkbox"]');
278
+ if (selectAllCb) {
279
+ const enabledRows = shadow.querySelectorAll('tr[data-row-id]:not([data-disabled])');
280
+ const allSelected = enabledRows.length > 0
281
+ && [...enabledRows].every(r => this.#selected.has(r.getAttribute('data-row-id')));
282
+ selectAllCb.checked = allSelected;
283
+ selectAllCb.indeterminate = !allSelected && this.#selected.size > 0;
284
+ }
285
+ }
286
+
287
+ // ─── Expand ───
288
+
289
+ #handleExpand(btn) {
290
+ const rowId = btn.getAttribute('data-row-id');
291
+ const wasExpanded = btn.getAttribute('aria-expanded') === 'true';
292
+ const newExpanded = !wasExpanded;
293
+
294
+ btn.setAttribute('aria-expanded', String(newExpanded));
295
+
296
+ const detailId = btn.getAttribute('aria-controls');
297
+ const detail = detailId ? this._shadow.querySelector(`[id="${CSS.escape(detailId)}"]`) : null;
298
+ if (detail) {
299
+ if (newExpanded) detail.removeAttribute('hidden');
300
+ else detail.setAttribute('hidden', '');
301
+ }
302
+
303
+ // Swap chevron icon (SVG sprite <use> href)
304
+ const use = btn.querySelector('use');
305
+ if (use) {
306
+ const href = use.getAttribute('href') || '';
307
+ if (href === '#icon-chevron-right') use.setAttribute('href', '#icon-chevron-down');
308
+ else if (href === '#icon-chevron-down') use.setAttribute('href', '#icon-chevron-right');
309
+ }
310
+
311
+ this._emit('cx-expand', { row_id: rowId, expanded: newExpanded });
312
+ }
313
+
314
+ // ─── Pagination ───
315
+
316
+ #handlePage(page) {
317
+ const p = this._props.pagination;
318
+ if (!p || typeof p !== 'object') return;
319
+
320
+ p.current_page = page;
321
+ this._emit('cx-page', { page, page_size: p.page_size });
322
+ this._scheduleRender();
323
+ }
324
+
325
+ #handlePrevNext(btn) {
326
+ const p = this._props.pagination;
327
+ if (!p || typeof p !== 'object') return;
328
+
329
+ const current = p.current_page || 1;
330
+ const totalPages = Math.ceil((p.total_items || 0) / (p.page_size || 10));
331
+
332
+ // First non-page button in nav = prev, last = next
333
+ const nav = btn.closest('nav');
334
+ if (!nav) return;
335
+ const nonPageBtns = nav.querySelectorAll('button:not([data-page])');
336
+ const isPrev = nonPageBtns[0] === btn;
337
+ const target = isPrev ? current - 1 : current + 1;
338
+
339
+ if (target >= 1 && target <= totalPages) {
340
+ this.#handlePage(target);
341
+ }
342
+ }
343
+
344
+ // ─── Render ───
345
+
346
+ _doRender() {
347
+ try {
348
+ const result = wasmFn(this._props);
349
+ this._injectHtml(result);
350
+
351
+ // Re-apply internal sort after DOM replacement (WASM renders rows
352
+ // in source order — DOM-level sort must be re-applied).
353
+ if (!this.#controlledSorts && this.#sorts.length > 0) {
354
+ const { column, direction } = this.#sorts[0];
355
+ const table = this._shadow.querySelector('table');
356
+ if (table) this.#sortTbodyRows(table, column, direction);
357
+ }
358
+
359
+ // Re-apply internal selection after DOM replacement
360
+ if (!this.#controlledSelected && this.#selected.size > 0) {
361
+ this.#syncSelectionDOM();
362
+ }
363
+ } catch (e) {
364
+ console.error('[cx-table]', e);
365
+ }
366
+ }
367
+ }
368
+
369
+ customElements.define('cx-table', CxTable);
370
+ return CxTable;
371
+ }
@@ -0,0 +1,19 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/tabs.rs
3
+
4
+ export interface CxTabsAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ items?: string;
8
+ defaultTab?: string;
9
+ variant?: 'underline' | 'enclosed' | 'pill';
10
+ orientation?: 'horizontal' | 'vertical';
11
+ width?: 'auto' | 'full';
12
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
13
+ }
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'cx-tabs': HTMLElement & CxTabsAttributes;
18
+ }
19
+ }
@@ -0,0 +1,139 @@
1
+ // Custom behavior for <cx-tabs> — tab switching, indicator positioning, keyboard nav.
2
+ // Mirrors static/_behaviors/tabs.js but operates inside shadow DOM.
3
+ // Source: crates/wasm-api/src/tabs.rs
4
+
5
+ export function defineCxTabs(wasmFn, baseClass) {
6
+ class CxTabs extends baseClass {
7
+ static observedAttributes = ['id', 'label', 'items', 'default-tab', 'variant', 'orientation', 'width', 'size'];
8
+ static _booleanAttrs = new Set([]);
9
+
10
+ #resizeObserver = null;
11
+
12
+ connectedCallback() {
13
+ if (!this._isInitialized) {
14
+ this._markInitialized();
15
+ const shadow = this._shadow;
16
+
17
+ // Tab click
18
+ shadow.addEventListener('click', (e) => {
19
+ const tab = e.target.closest('[role="tab"]');
20
+ if (tab && !tab.hasAttribute('aria-disabled')) {
21
+ const idx = parseInt(tab.getAttribute('data-tab-index'), 10);
22
+ if (!isNaN(idx)) {
23
+ this.#activateTab(idx);
24
+ tab.focus();
25
+ }
26
+ }
27
+ });
28
+
29
+ // Keyboard navigation (Arrow keys, Home/End)
30
+ shadow.addEventListener('keydown', (e) => {
31
+ const tab = e.target.closest('[role="tab"]');
32
+ if (!tab) return;
33
+
34
+ const tabs = shadow.querySelector('[data-tabs]');
35
+ const ori = tabs?.getAttribute('data-orientation') || 'horizontal';
36
+ const prev = ori === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
37
+ const next = ori === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
38
+
39
+ if (e.key === prev || e.key === next || e.key === 'Home' || e.key === 'End') {
40
+ e.preventDefault();
41
+ const all = Array.from(shadow.querySelectorAll('[role="tab"]:not([aria-disabled="true"])'));
42
+ const cur = all.indexOf(tab);
43
+ if (cur === -1) return;
44
+ let ni = cur;
45
+ if (e.key === next) ni = (cur + 1) % all.length;
46
+ else if (e.key === prev) ni = (cur - 1 + all.length) % all.length;
47
+ else if (e.key === 'Home') ni = 0;
48
+ else if (e.key === 'End') ni = all.length - 1;
49
+ const ti = parseInt(all[ni].getAttribute('data-tab-index'), 10);
50
+ if (!isNaN(ti)) {
51
+ this.#activateTab(ti);
52
+ all[ni].focus();
53
+ }
54
+ }
55
+ });
56
+ } // end _isInitialized guard
57
+ super.connectedCallback();
58
+ }
59
+
60
+ disconnectedCallback() {
61
+ if (this.#resizeObserver) {
62
+ this.#resizeObserver.disconnect();
63
+ this.#resizeObserver = null;
64
+ }
65
+ super.disconnectedCallback();
66
+ }
67
+
68
+ #activateTab(index) {
69
+ const shadow = this._shadow;
70
+ const tabs = shadow.querySelector('[data-tabs]');
71
+ if (!tabs) return;
72
+
73
+ // Toggle radio inputs
74
+ const radios = tabs.querySelectorAll(':scope > input[type="radio"]');
75
+ if (radios[index]) radios[index].checked = true;
76
+
77
+ // Update ARIA on all tab buttons
78
+ const allTabs = tabs.querySelectorAll('[role="tab"]');
79
+ allTabs.forEach((t, i) => {
80
+ t.setAttribute('aria-selected', i === index ? 'true' : 'false');
81
+ t.setAttribute('tabindex', i === index ? '0' : '-1');
82
+ });
83
+
84
+ // Sync host prop — single source of truth for WASM re-renders.
85
+ // Direct _props mutation avoids triggering a re-render that would
86
+ // destroy DOM state and kill the indicator animation.
87
+ if (radios[index]) {
88
+ this._props.default_tab = radios[index].value || String(index);
89
+ }
90
+
91
+ this.#positionIndicator();
92
+ this._emit('cx-change', { index, value: radios[index]?.value });
93
+ }
94
+
95
+ #positionIndicator() {
96
+ const shadow = this._shadow;
97
+ const tablist = shadow.querySelector('[role="tablist"]');
98
+ const indicator = shadow.querySelector('[data-tab-indicator]');
99
+ const active = shadow.querySelector('[role="tab"][aria-selected="true"]');
100
+ if (!tablist || !indicator || !active) return;
101
+
102
+ tablist.style.setProperty('--ind-x', active.offsetLeft + 'px');
103
+ tablist.style.setProperty('--ind-y', active.offsetTop + 'px');
104
+ tablist.style.setProperty('--ind-w', active.offsetWidth + 'px');
105
+ tablist.style.setProperty('--ind-h', active.offsetHeight + 'px');
106
+ }
107
+
108
+ // ── Public imperative API ──
109
+ focus() { const el = this._shadow.querySelector('[role="tab"][aria-selected="true"]'); if (el) el.focus(); else super.focus(); }
110
+
111
+ _doRender() {
112
+ try {
113
+ const result = wasmFn(this._props);
114
+ this._injectHtml(result);
115
+
116
+ // Position indicator after render (no transition on first paint)
117
+ requestAnimationFrame(() => {
118
+ this.#positionIndicator();
119
+ const ind = this._shadow.querySelector('[data-tab-indicator]');
120
+ if (ind) {
121
+ requestAnimationFrame(() => ind.setAttribute('data-ready', ''));
122
+ }
123
+
124
+ // Observe tablist for resizes
125
+ if (!this.#resizeObserver && window.ResizeObserver) {
126
+ this.#resizeObserver = new ResizeObserver(() => this.#positionIndicator());
127
+ const tl = this._shadow.querySelector('[role="tablist"]');
128
+ if (tl) this.#resizeObserver.observe(tl);
129
+ }
130
+ });
131
+ } catch (e) {
132
+ console.error('[cx-tabs]', e);
133
+ }
134
+ }
135
+ }
136
+
137
+ customElements.define('cx-tabs', CxTabs);
138
+ return CxTabs;
139
+ }
@@ -0,0 +1,26 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/text.rs
3
+
4
+ export interface CxTextAttributes {
5
+ id?: string;
6
+ role?: 'display' | 'h1' | 'h2' | 'h3' | 'label-lg' | 'label-md' | 'label-sm' | 'body-lg' | 'body-md' | 'body-sm' | 'overline' | 'caption' | 'code';
7
+ content?: string;
8
+ align?: 'start' | 'center' | 'end';
9
+ color?: 'primary' | 'inverse' | 'muted';
10
+ muted?: boolean;
11
+ strong?: boolean;
12
+ italic?: boolean;
13
+ underline?: boolean;
14
+ underlineStrong?: boolean;
15
+ truncate?: boolean;
16
+ lineClamp?: number;
17
+ balance?: boolean;
18
+ prose?: boolean;
19
+ elementAs?: string;
20
+ }
21
+
22
+ declare global {
23
+ interface HTMLElementTagNameMap {
24
+ 'cx-text': HTMLElement & CxTextAttributes;
25
+ }
26
+ }
@@ -0,0 +1,32 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/text.rs
3
+
4
+ export function defineCxText(wasmFn, baseClass) {
5
+ class CxText extends baseClass {
6
+ static observedAttributes = ['id', 'role', 'align', 'color', 'muted', 'strong', 'italic', 'underline', 'underline-strong', 'truncate', 'line-clamp', 'balance', 'prose', 'element-as'];
7
+ static _booleanAttrs = new Set(['muted', 'strong', 'italic', 'underline', 'underline-strong', 'truncate', 'balance', 'prose']);
8
+ static _numericAttrs = new Set(['line-clamp']);
9
+ static _focusable = false;
10
+ static _hostDisplay = 'contents';
11
+
12
+ set line_clamp(v) { this._setProp('line_clamp', v); }
13
+ get line_clamp() { return this._props.line_clamp; }
14
+
15
+ connectedCallback() {
16
+ super.connectedCallback();
17
+ }
18
+
19
+ _doRender() {
20
+ try {
21
+ this._props.slotted = true;
22
+ const result = wasmFn(this._props);
23
+ this._injectHtml(result);
24
+ } catch (e) {
25
+ console.error('[cx-text]', e);
26
+ }
27
+ }
28
+ }
29
+
30
+ customElements.define('cx-text', CxText);
31
+ return CxText;
32
+ }
@@ -0,0 +1,36 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/text_input.rs
3
+
4
+ export interface CxTextInputAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ variant?: 'outline' | 'filled' | 'ghost';
8
+ shape?: 'sharp' | 'rounded' | 'pill';
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
10
+ kind?: 'text' | 'email' | 'password' | 'search' | 'multiline';
11
+ placeholder?: string;
12
+ value?: string;
13
+ helperText?: string;
14
+ error?: string;
15
+ disabled?: boolean;
16
+ readonly?: boolean;
17
+ required?: boolean;
18
+ clearable?: boolean;
19
+ prefixIcon?: string;
20
+ suffixIcon?: string;
21
+ passwordToggle?: boolean;
22
+ passwordVisible?: boolean;
23
+ name?: string;
24
+ minLength?: number;
25
+ maxLength?: number;
26
+ pattern?: string;
27
+ autocomplete?: string;
28
+ rows?: number;
29
+ autoGrow?: boolean;
30
+ }
31
+
32
+ declare global {
33
+ interface HTMLElementTagNameMap {
34
+ 'cx-text-input': HTMLElement & CxTextInputAttributes;
35
+ }
36
+ }