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