@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.
- package/README.md +77 -0
- package/custom-elements.json +6037 -0
- package/generated/.gitattributes +2 -0
- package/generated/index.d.ts +120 -0
- package/generated/index.js +521 -0
- package/generated/styles.js +2845 -0
- package/package.json +56 -0
- package/src/elements/accordion.d.ts +20 -0
- package/src/elements/accordion.js +92 -0
- package/src/elements/activity_group.d.ts +19 -0
- package/src/elements/activity_group.js +27 -0
- package/src/elements/alert.d.ts +24 -0
- package/src/elements/alert.js +40 -0
- package/src/elements/autocomplete.d.ts +30 -0
- package/src/elements/autocomplete.js +671 -0
- package/src/elements/avatar.d.ts +18 -0
- package/src/elements/avatar.js +28 -0
- package/src/elements/backdrop.d.ts +14 -0
- package/src/elements/backdrop.js +28 -0
- package/src/elements/badge.d.ts +21 -0
- package/src/elements/badge.js +42 -0
- package/src/elements/breadcrumb.d.ts +17 -0
- package/src/elements/breadcrumb.js +41 -0
- package/src/elements/button.d.ts +24 -0
- package/src/elements/button.js +36 -0
- package/src/elements/card.d.ts +21 -0
- package/src/elements/card.js +67 -0
- package/src/elements/carousel.d.ts +23 -0
- package/src/elements/carousel.js +895 -0
- package/src/elements/chat_input.d.ts +22 -0
- package/src/elements/chat_input.js +78 -0
- package/src/elements/checkbox.d.ts +21 -0
- package/src/elements/checkbox.js +114 -0
- package/src/elements/code_block.d.ts +21 -0
- package/src/elements/code_block.js +27 -0
- package/src/elements/collapsible.d.ts +20 -0
- package/src/elements/collapsible.js +93 -0
- package/src/elements/date_picker.d.ts +30 -0
- package/src/elements/date_picker.js +528 -0
- package/src/elements/dialog.d.ts +20 -0
- package/src/elements/dialog.js +314 -0
- package/src/elements/drawer.d.ts +20 -0
- package/src/elements/drawer.js +318 -0
- package/src/elements/fab.d.ts +22 -0
- package/src/elements/fab.js +36 -0
- package/src/elements/file_upload.d.ts +26 -0
- package/src/elements/file_upload.js +59 -0
- package/src/elements/listbox.d.ts +19 -0
- package/src/elements/listbox.js +250 -0
- package/src/elements/menu.d.ts +20 -0
- package/src/elements/menu.js +224 -0
- package/src/elements/message_bubble.d.ts +23 -0
- package/src/elements/message_bubble.js +29 -0
- package/src/elements/message_group.d.ts +18 -0
- package/src/elements/message_group.js +28 -0
- package/src/elements/message_part.d.ts +35 -0
- package/src/elements/message_part.js +153 -0
- package/src/elements/pagination.d.ts +22 -0
- package/src/elements/pagination.js +36 -0
- package/src/elements/popover.d.ts +26 -0
- package/src/elements/popover.js +191 -0
- package/src/elements/profile_menu.d.ts +20 -0
- package/src/elements/profile_menu.js +213 -0
- package/src/elements/progress.d.ts +18 -0
- package/src/elements/progress.js +31 -0
- package/src/elements/radio_group.d.ts +22 -0
- package/src/elements/radio_group.js +70 -0
- package/src/elements/scrollbar.d.ts +19 -0
- package/src/elements/scrollbar.js +299 -0
- package/src/elements/search_bar.d.ts +27 -0
- package/src/elements/search_bar.js +98 -0
- package/src/elements/select.d.ts +26 -0
- package/src/elements/select.js +485 -0
- package/src/elements/sidebar.d.ts +21 -0
- package/src/elements/sidebar.js +322 -0
- package/src/elements/skeleton.d.ts +17 -0
- package/src/elements/skeleton.js +31 -0
- package/src/elements/slider.d.ts +28 -0
- package/src/elements/slider.js +93 -0
- package/src/elements/speed_dial.d.ts +23 -0
- package/src/elements/speed_dial.js +370 -0
- package/src/elements/spinner.d.ts +15 -0
- package/src/elements/spinner.js +28 -0
- package/src/elements/split_button.d.ts +23 -0
- package/src/elements/split_button.js +281 -0
- package/src/elements/stepper.d.ts +20 -0
- package/src/elements/stepper.js +31 -0
- package/src/elements/switch.d.ts +22 -0
- package/src/elements/switch.js +129 -0
- package/src/elements/table.d.ts +29 -0
- package/src/elements/table.js +371 -0
- package/src/elements/tabs.d.ts +19 -0
- package/src/elements/tabs.js +139 -0
- package/src/elements/text.d.ts +26 -0
- package/src/elements/text.js +32 -0
- package/src/elements/text_input.d.ts +36 -0
- package/src/elements/text_input.js +121 -0
- package/src/elements/thinking.d.ts +17 -0
- package/src/elements/thinking.js +28 -0
- package/src/elements/toast.d.ts +23 -0
- package/src/elements/toast.js +209 -0
- package/src/elements/toggle_group.d.ts +22 -0
- package/src/elements/toggle_group.js +176 -0
- package/src/elements/tooltip.d.ts +18 -0
- package/src/elements/tooltip.js +64 -0
- package/src/markdown.d.ts +24 -0
- package/src/markdown.js +66 -0
- package/src/runtime.d.ts +35 -0
- package/src/runtime.js +790 -0
- package/src/server.d.ts +69 -0
- package/src/server.js +176 -0
- package/src/streaming-markdown.js +43 -0
- package/src/vite-plugin.d.ts +46 -0
- package/src/vite-plugin.js +221 -0
- package/wasm/package.json +16 -0
- package/wasm/wasm_api.d.ts +72 -0
- package/wasm/wasm_api.js +593 -0
- package/wasm/wasm_api_bg.wasm +0 -0
- 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
|
+
}
|