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