@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,213 @@
|
|
|
1
|
+
// Custom behavior for <cx-profile-menu> — avatar trigger, dropdown menu, keyboard nav.
|
|
2
|
+
//
|
|
3
|
+
// The Rust component renders:
|
|
4
|
+
// <div data-profile-menu> — container
|
|
5
|
+
// <button data-floating-trigger aria-haspopup="menu" aria-expanded="false"> — avatar trigger
|
|
6
|
+
// <div role="menu" data-menu data-floating> — dropdown panel
|
|
7
|
+
// <button role="menuitem" data-item-id="{id}"> — menu items
|
|
8
|
+
// <button role="menuitemcheckbox" data-item-id="{id}" aria-checked> — checkbox items
|
|
9
|
+
// <button role="menuitemradio" data-item-id="{id}" aria-checked> — radio items
|
|
10
|
+
// <div role="separator"> — dividers
|
|
11
|
+
//
|
|
12
|
+
// This Custom Element wires up:
|
|
13
|
+
// - Click avatar to toggle menu open/close
|
|
14
|
+
// - Click menu item → cx-action event + close
|
|
15
|
+
// - Click checkbox/radio → toggle aria-checked + cx-change event (no close)
|
|
16
|
+
// - Click outside / focus exit → close
|
|
17
|
+
// - Keyboard: ArrowDown/Up nav, Home/End, Enter/Space activate, Escape closes
|
|
18
|
+
//
|
|
19
|
+
// Source: crates/wasm-api/src/profile_menu.rs
|
|
20
|
+
|
|
21
|
+
export function defineCxProfileMenu(wasmFn, baseClass) {
|
|
22
|
+
class CxProfileMenu extends baseClass {
|
|
23
|
+
static observedAttributes = ['id', 'label', 'image', 'initials', 'shape', 'size', 'width', 'entries', 'disabled'];
|
|
24
|
+
static _booleanAttrs = new Set(['disabled']);
|
|
25
|
+
static _hostDisplay = 'inline-flex';
|
|
26
|
+
|
|
27
|
+
#outsideClick = null;
|
|
28
|
+
|
|
29
|
+
connectedCallback() {
|
|
30
|
+
if (!this._isInitialized) {
|
|
31
|
+
this._markInitialized();
|
|
32
|
+
const shadow = this._shadow;
|
|
33
|
+
|
|
34
|
+
// ── Click delegation ──
|
|
35
|
+
shadow.addEventListener('click', (e) => {
|
|
36
|
+
// Trigger button: toggle menu
|
|
37
|
+
const trigger = e.target.closest('[data-floating-trigger]');
|
|
38
|
+
if (trigger && !this._props.disabled) {
|
|
39
|
+
if (this.#isOpen()) {
|
|
40
|
+
this.#closeMenu();
|
|
41
|
+
} else {
|
|
42
|
+
this.#openMenu();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Menu item click (regular menuitem): close and emit
|
|
48
|
+
const mi = e.target.closest('[role="menuitem"]');
|
|
49
|
+
if (mi && !mi.hasAttribute('aria-disabled')) {
|
|
50
|
+
const id = mi.getAttribute('data-item-id') || mi.textContent.trim();
|
|
51
|
+
this._emit('cx-action', { id });
|
|
52
|
+
this.#closeMenu();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Checkbox/radio items: toggle state but don't close
|
|
57
|
+
const mci = e.target.closest('[role="menuitemcheckbox"],[role="menuitemradio"]');
|
|
58
|
+
if (mci && !mci.hasAttribute('aria-disabled')) {
|
|
59
|
+
const checked = mci.getAttribute('aria-checked') === 'true';
|
|
60
|
+
mci.setAttribute('aria-checked', String(!checked));
|
|
61
|
+
this._emit('cx-change', {
|
|
62
|
+
id: mci.getAttribute('data-item-id') || mci.textContent.trim(),
|
|
63
|
+
checked: !checked,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Keyboard navigation ──
|
|
69
|
+
shadow.addEventListener('keydown', (e) => {
|
|
70
|
+
const menuitem = e.target.closest('[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]');
|
|
71
|
+
|
|
72
|
+
// Escape: close menu
|
|
73
|
+
if (e.key === 'Escape' && this.#isOpen()) {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
this.#closeMenu();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Arrow keys on trigger
|
|
80
|
+
if (e.target.closest('[data-floating-trigger]') && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {
|
|
81
|
+
if (!this.#isOpen()) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
this.#openMenu();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!menuitem) return;
|
|
89
|
+
|
|
90
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
const panel = shadow.querySelector('[data-menu]');
|
|
93
|
+
if (!panel) return;
|
|
94
|
+
const all = Array.from(panel.querySelectorAll(
|
|
95
|
+
'[role="menuitem"]:not([aria-disabled]),[role="menuitemcheckbox"]:not([aria-disabled]),[role="menuitemradio"]:not([aria-disabled])'
|
|
96
|
+
));
|
|
97
|
+
const ci = all.indexOf(menuitem);
|
|
98
|
+
if (ci === -1) return;
|
|
99
|
+
let ni = ci;
|
|
100
|
+
if (e.key === 'ArrowDown') ni = (ci + 1) % all.length;
|
|
101
|
+
else if (e.key === 'ArrowUp') ni = (ci - 1 + all.length) % all.length;
|
|
102
|
+
else if (e.key === 'Home') ni = 0;
|
|
103
|
+
else if (e.key === 'End') ni = all.length - 1;
|
|
104
|
+
all[ni].focus();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
menuitem.click();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── Click outside → close (mousedown fires before click) ──
|
|
115
|
+
this.#outsideClick = (e) => {
|
|
116
|
+
if (this.#isOpen() && !this.contains(e.target) && !this._shadow.contains(e.target)) {
|
|
117
|
+
this.#closeMenu();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
document.addEventListener('mousedown', this.#outsideClick);
|
|
121
|
+
|
|
122
|
+
// ── Focus exit → close ──
|
|
123
|
+
// Must check both light DOM and shadow DOM — Node.contains() doesn't cross shadow boundaries.
|
|
124
|
+
shadow.addEventListener('focusout', () => {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (this.#isOpen()) {
|
|
127
|
+
const active = this._shadow.activeElement || document.activeElement;
|
|
128
|
+
if (!this.contains(active) && !this._shadow.contains(active) && active !== this) {
|
|
129
|
+
this.#closeMenu();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, 0);
|
|
133
|
+
});
|
|
134
|
+
} // end _isInitialized guard
|
|
135
|
+
|
|
136
|
+
super.connectedCallback();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
disconnectedCallback() {
|
|
140
|
+
if (this.#outsideClick) {
|
|
141
|
+
document.removeEventListener('mousedown', this.#outsideClick);
|
|
142
|
+
this.#outsideClick = null;
|
|
143
|
+
}
|
|
144
|
+
super.disconnectedCallback();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── State via DOM (single source of truth) ──
|
|
148
|
+
|
|
149
|
+
#isOpen() {
|
|
150
|
+
const trigger = this._shadow.querySelector('[data-floating-trigger]');
|
|
151
|
+
return trigger ? trigger.getAttribute('aria-expanded') === 'true' : false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#openMenu() {
|
|
155
|
+
const panel = this._shadow.querySelector('[data-menu]');
|
|
156
|
+
const trigger = this._shadow.querySelector('[data-floating-trigger]');
|
|
157
|
+
if (!panel || !trigger) return;
|
|
158
|
+
|
|
159
|
+
// Position with fixed coordinates — escapes overflow:auto/scroll clipping.
|
|
160
|
+
this._positionFloatingFixed(trigger, panel, { matchWidth: true });
|
|
161
|
+
|
|
162
|
+
panel.setAttribute('data-open', '');
|
|
163
|
+
panel.classList.remove('hidden');
|
|
164
|
+
panel.style.display = 'block';
|
|
165
|
+
panel.style.pointerEvents = 'auto';
|
|
166
|
+
panel.style.opacity = '1';
|
|
167
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
168
|
+
|
|
169
|
+
// Focus first non-disabled item
|
|
170
|
+
requestAnimationFrame(() => {
|
|
171
|
+
const first = panel.querySelector(
|
|
172
|
+
'[role="menuitem"]:not([aria-disabled]),[role="menuitemcheckbox"]:not([aria-disabled]),[role="menuitemradio"]:not([aria-disabled])'
|
|
173
|
+
);
|
|
174
|
+
if (first) first.focus();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#closeMenu() {
|
|
179
|
+
const panel = this._shadow.querySelector('[data-menu]');
|
|
180
|
+
const trigger = this._shadow.querySelector('[data-floating-trigger]');
|
|
181
|
+
if (panel) {
|
|
182
|
+
panel.removeAttribute('data-open');
|
|
183
|
+
panel.classList.add('hidden');
|
|
184
|
+
panel.style.display = '';
|
|
185
|
+
panel.style.pointerEvents = '';
|
|
186
|
+
panel.style.opacity = '';
|
|
187
|
+
this._resetFloatingFixed(panel);
|
|
188
|
+
}
|
|
189
|
+
if (trigger) {
|
|
190
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
191
|
+
trigger.focus();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Public imperative API ──
|
|
196
|
+
open() { this.#openMenu(); }
|
|
197
|
+
close() { this.#closeMenu(); }
|
|
198
|
+
|
|
199
|
+
_doRender() {
|
|
200
|
+
try {
|
|
201
|
+
const wasOpen = this.#isOpen();
|
|
202
|
+
const result = wasmFn(this._props);
|
|
203
|
+
this._injectHtml(result);
|
|
204
|
+
if (wasOpen) this.#openMenu();
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('[cx-profile-menu]', e);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
customElements.define('cx-profile-menu', CxProfileMenu);
|
|
212
|
+
return CxProfileMenu;
|
|
213
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
|
|
2
|
+
// Source: crates/wasm-api/src/progress.rs
|
|
3
|
+
|
|
4
|
+
export interface CxProgressAttributes {
|
|
5
|
+
id?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
value?: number;
|
|
8
|
+
valueText?: string;
|
|
9
|
+
intent?: 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
|
|
10
|
+
shape?: 'rounded' | 'sharp' | 'pill';
|
|
11
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface HTMLElementTagNameMap {
|
|
16
|
+
'cx-progress': HTMLElement & CxProgressAttributes;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
|
|
2
|
+
// Source: crates/wasm-api/src/progress.rs
|
|
3
|
+
|
|
4
|
+
export function defineCxProgress(wasmFn, baseClass) {
|
|
5
|
+
class CxProgress extends baseClass {
|
|
6
|
+
static observedAttributes = ['id', 'label', 'value', 'value-text', 'intent', 'shape', 'size'];
|
|
7
|
+
static _booleanAttrs = new Set([]);
|
|
8
|
+
static _numericAttrs = new Set(['value']);
|
|
9
|
+
static _focusable = false;
|
|
10
|
+
static _hostDisplay = 'block';
|
|
11
|
+
|
|
12
|
+
set value(v) { this._setProp('value', v); }
|
|
13
|
+
get value() { return this._props.value; }
|
|
14
|
+
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
super.connectedCallback();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_doRender() {
|
|
20
|
+
try {
|
|
21
|
+
const result = wasmFn(this._props);
|
|
22
|
+
this._injectHtml(result);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error('[cx-progress]', e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
customElements.define('cx-progress', CxProgress);
|
|
30
|
+
return CxProgress;
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
|
|
2
|
+
// Source: crates/wasm-api/src/radio_group.rs
|
|
3
|
+
|
|
4
|
+
export interface CxRadioGroupAttributes {
|
|
5
|
+
name?: string;
|
|
6
|
+
legend: string;
|
|
7
|
+
options?: string;
|
|
8
|
+
shape?: 'round' | 'rounded';
|
|
9
|
+
orientation?: 'vertical' | 'horizontal';
|
|
10
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
selected?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
helperText?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
'cx-radio-group': HTMLElement & CxRadioGroupAttributes;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
|
|
2
|
+
// Source: crates/wasm-api/src/radio_group.rs
|
|
3
|
+
|
|
4
|
+
export function defineCxRadioGroup(wasmFn, baseClass) {
|
|
5
|
+
class CxRadioGroup extends baseClass {
|
|
6
|
+
static observedAttributes = ['name', 'legend', 'options', 'shape', 'orientation', 'size', 'selected', 'disabled', 'required', 'helper-text', 'error'];
|
|
7
|
+
static _booleanAttrs = new Set(['disabled', 'required']);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
connectedCallback() {
|
|
11
|
+
if (!this._isInitialized) {
|
|
12
|
+
this._markInitialized();
|
|
13
|
+
// Delegated event listeners — attach once on shadow root
|
|
14
|
+
this._shadow.addEventListener('input', (e) => {
|
|
15
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
16
|
+
this._setFormValue(e.target.value);
|
|
17
|
+
this._emit('cx-input', { value: e.target.value });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
this._shadow.addEventListener('change', (e) => {
|
|
21
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
|
|
22
|
+
this._emit('cx-change', { value: e.target.value });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Forward focus events from inner interactive elements
|
|
27
|
+
this._shadow.addEventListener('focusin', (e) => {
|
|
28
|
+
this._emit('cx-focus', { relatedTarget: e.relatedTarget });
|
|
29
|
+
});
|
|
30
|
+
this._shadow.addEventListener('focusout', (e) => {
|
|
31
|
+
this._emit('cx-blur', { relatedTarget: e.relatedTarget });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Forward keyboard events from inner interactive elements
|
|
35
|
+
this._shadow.addEventListener('keydown', (e) => {
|
|
36
|
+
this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
|
|
37
|
+
});
|
|
38
|
+
this._shadow.addEventListener('keyup', (e) => {
|
|
39
|
+
this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
|
|
40
|
+
});
|
|
41
|
+
} // end _isInitialized guard
|
|
42
|
+
super.connectedCallback();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_doRender() {
|
|
46
|
+
try {
|
|
47
|
+
const result = wasmFn(this._props);
|
|
48
|
+
this._injectHtml(result);
|
|
49
|
+
// Sync form value after render. Event delegation on shadow root
|
|
50
|
+
// avoids duplicate listeners (innerHTML replaces old DOM nodes,
|
|
51
|
+
// but shadow root persists — delegation handles new inputs).
|
|
52
|
+
const input = this._shadow.querySelector('input, textarea, select');
|
|
53
|
+
if (input) {
|
|
54
|
+
// Sync controlled value prop → internal input property.
|
|
55
|
+
// _injectHtml's focus-preservation restores the OLD typed value
|
|
56
|
+
// to maintain cursor position during re-renders. But when the
|
|
57
|
+
// value prop explicitly changes (e.g., clearing after submit),
|
|
58
|
+
// the controlled value must win.
|
|
59
|
+
if ('value' in this._props) input.value = this._props.value;
|
|
60
|
+
this._setFormValue(input.value || '');
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('[cx-radio-group]', e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
customElements.define('cx-radio-group', CxRadioGroup);
|
|
69
|
+
return CxRadioGroup;
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
|
|
2
|
+
// Source: crates/wasm-api/src/scrollbar.rs
|
|
3
|
+
|
|
4
|
+
export interface CxScrollbarAttributes {
|
|
5
|
+
id?: string;
|
|
6
|
+
shape?: 'sharp' | 'rounded' | 'pill';
|
|
7
|
+
track?: 'with-track' | 'floating';
|
|
8
|
+
axis?: 'vertical' | 'horizontal' | 'both';
|
|
9
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
10
|
+
height?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare global {
|
|
16
|
+
interface HTMLElementTagNameMap {
|
|
17
|
+
'cx-scrollbar': HTMLElement & CxScrollbarAttributes;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Custom behavior for <cx-scrollbar> — scroll tracking, thumb positioning, drag.
|
|
2
|
+
//
|
|
3
|
+
// Progressive enhancement:
|
|
4
|
+
// CSS-only: thumb visible at idle opacity (20%) with 30% height fallback
|
|
5
|
+
// JS-enhanced: precise thumb sizing, hide/show on scroll, drag support
|
|
6
|
+
//
|
|
7
|
+
// Sets data-scrollbar-ready on the container when behavior initializes.
|
|
8
|
+
// CSS uses this to switch from visible-fallback to hide-until-scroll mode.
|
|
9
|
+
//
|
|
10
|
+
// Source: crates/wasm-api/src/scrollbar.rs
|
|
11
|
+
|
|
12
|
+
export function defineCxScrollbar(wasmFn, baseClass) {
|
|
13
|
+
class CxScrollbar extends baseClass {
|
|
14
|
+
static observedAttributes = ['id', 'shape', 'track', 'axis', 'size', 'height', 'label'];
|
|
15
|
+
static _booleanAttrs = new Set([]);
|
|
16
|
+
static _focusable = false;
|
|
17
|
+
// Flex column + min-height:0 lets cx-scrollbar fill grid/flex cells.
|
|
18
|
+
// Without min-height:0, grid items default to min-height:auto and
|
|
19
|
+
// height:100% children inside Shadow DOM can't resolve the cell height.
|
|
20
|
+
static _hostDisplay = 'flex';
|
|
21
|
+
static _hostSheet = (() => {
|
|
22
|
+
if (typeof CSSStyleSheet === 'undefined') return null;
|
|
23
|
+
const s = new CSSStyleSheet();
|
|
24
|
+
s.replaceSync(':host { display: flex; flex-direction: column; min-height: 0; }');
|
|
25
|
+
return s;
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
#sb = null;
|
|
29
|
+
|
|
30
|
+
connectedCallback() {
|
|
31
|
+
if (!this._isInitialized) {
|
|
32
|
+
this._markInitialized();
|
|
33
|
+
}
|
|
34
|
+
super.connectedCallback();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
disconnectedCallback() {
|
|
38
|
+
this.#cleanup();
|
|
39
|
+
super.disconnectedCallback();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_doRender() {
|
|
43
|
+
try {
|
|
44
|
+
this._props.slotted = true;
|
|
45
|
+
const result = wasmFn(this._props);
|
|
46
|
+
this._injectHtml(result);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('[cx-scrollbar] render:', e);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Init scrollbar behavior separately — errors here should not
|
|
52
|
+
// prevent the component from rendering its content.
|
|
53
|
+
try {
|
|
54
|
+
this.#initScrollbar();
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error('[cx-scrollbar] init:', e);
|
|
57
|
+
// Attempt to set data-scrollbar-ready even on failure so CSS
|
|
58
|
+
// doesn't show a stale fallback thumb with no JS positioning.
|
|
59
|
+
const root = this._shadow.querySelector('[data-scrollbar]');
|
|
60
|
+
if (root) root.setAttribute('data-scrollbar-ready', '');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#initScrollbar() {
|
|
65
|
+
this.#cleanup();
|
|
66
|
+
const shadow = this._shadow;
|
|
67
|
+
const root = shadow.querySelector('[data-scrollbar]');
|
|
68
|
+
if (!root) return;
|
|
69
|
+
|
|
70
|
+
const vp = root.querySelector('[data-scrollbar-viewport]');
|
|
71
|
+
if (!vp) return;
|
|
72
|
+
|
|
73
|
+
const axis = root.getAttribute('data-scrollbar-axis') || 'y';
|
|
74
|
+
const isBoth = axis === 'both';
|
|
75
|
+
|
|
76
|
+
const sb = {
|
|
77
|
+
vp,
|
|
78
|
+
axis,
|
|
79
|
+
dragging: false,
|
|
80
|
+
hideTimer: 0,
|
|
81
|
+
tracks: [],
|
|
82
|
+
cleanups: [],
|
|
83
|
+
rafId: 0,
|
|
84
|
+
};
|
|
85
|
+
this.#sb = sb;
|
|
86
|
+
|
|
87
|
+
// Mark as JS-enhanced — CSS hides thumb when idle
|
|
88
|
+
root.setAttribute('data-scrollbar-ready', '');
|
|
89
|
+
|
|
90
|
+
// Build track entries
|
|
91
|
+
if (isBoth) {
|
|
92
|
+
this.#addTrack(root, sb, 'y',
|
|
93
|
+
root.querySelector('[data-scrollbar-track="y"]'),
|
|
94
|
+
root.querySelector('[data-scrollbar-thumb="y"]'));
|
|
95
|
+
this.#addTrack(root, sb, 'x',
|
|
96
|
+
root.querySelector('[data-scrollbar-track="x"]'),
|
|
97
|
+
root.querySelector('[data-scrollbar-thumb="x"]'));
|
|
98
|
+
} else {
|
|
99
|
+
this.#addTrack(root, sb, axis,
|
|
100
|
+
root.querySelector('[data-scrollbar-track]'),
|
|
101
|
+
root.querySelector('[data-scrollbar-thumb]'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resize observer for content changes
|
|
105
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
106
|
+
const ro = new ResizeObserver(() => this.#updateAllThumbs(root, sb));
|
|
107
|
+
ro.observe(vp);
|
|
108
|
+
// For slotted content, observe assigned elements
|
|
109
|
+
const slot = vp.querySelector('slot');
|
|
110
|
+
if (slot) {
|
|
111
|
+
const assigned = slot.assignedElements();
|
|
112
|
+
if (assigned.length) ro.observe(assigned[0]);
|
|
113
|
+
slot.addEventListener('slotchange', () => {
|
|
114
|
+
const els = slot.assignedElements();
|
|
115
|
+
if (els.length) ro.observe(els[0]);
|
|
116
|
+
this.#updateAllThumbs(root, sb);
|
|
117
|
+
});
|
|
118
|
+
} else if (vp.firstElementChild) {
|
|
119
|
+
ro.observe(vp.firstElementChild);
|
|
120
|
+
}
|
|
121
|
+
sb.ro = ro;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Scroll listener on viewport
|
|
125
|
+
const onScroll = () => this.#onScroll(root, sb);
|
|
126
|
+
vp.addEventListener('scroll', onScroll, { passive: true });
|
|
127
|
+
sb.cleanups.push(() => vp.removeEventListener('scroll', onScroll));
|
|
128
|
+
|
|
129
|
+
// Initial thumb calculation
|
|
130
|
+
this.#updateAllThumbs(root, sb);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#addTrack(root, sb, axisKey, track, thumb) {
|
|
134
|
+
if (!track || !thumb) return;
|
|
135
|
+
const isY = axisKey === 'y';
|
|
136
|
+
const entry = { track, thumb, isY };
|
|
137
|
+
sb.tracks.push(entry);
|
|
138
|
+
|
|
139
|
+
// Track hover
|
|
140
|
+
const enter = () => root.setAttribute('data-scroll-hover', '');
|
|
141
|
+
const leave = () => {
|
|
142
|
+
if (!sb.dragging) root.removeAttribute('data-scroll-hover');
|
|
143
|
+
};
|
|
144
|
+
track.addEventListener('mouseenter', enter);
|
|
145
|
+
track.addEventListener('mouseleave', leave);
|
|
146
|
+
|
|
147
|
+
// Thumb drag — mouse
|
|
148
|
+
const mouseDown = (e) => {
|
|
149
|
+
this.#startDrag(root, sb, entry, e.clientX, e.clientY, false);
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
};
|
|
152
|
+
thumb.addEventListener('mousedown', mouseDown);
|
|
153
|
+
|
|
154
|
+
// Thumb drag — touch
|
|
155
|
+
const touchStart = (e) => {
|
|
156
|
+
if (e.touches.length === 1) {
|
|
157
|
+
this.#startDrag(root, sb, entry, e.touches[0].clientX, e.touches[0].clientY, true);
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
thumb.addEventListener('touchstart', touchStart, { passive: false });
|
|
162
|
+
|
|
163
|
+
sb.cleanups.push(() => {
|
|
164
|
+
track.removeEventListener('mouseenter', enter);
|
|
165
|
+
track.removeEventListener('mouseleave', leave);
|
|
166
|
+
thumb.removeEventListener('mousedown', mouseDown);
|
|
167
|
+
thumb.removeEventListener('touchstart', touchStart);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#updateAllThumbs(root, sb) {
|
|
172
|
+
if (!sb) return;
|
|
173
|
+
for (const entry of sb.tracks) {
|
|
174
|
+
this.#updateThumb(sb, entry);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#updateThumb(sb, entry) {
|
|
179
|
+
if (!sb) return;
|
|
180
|
+
const vp = sb.vp;
|
|
181
|
+
const { thumb, isY } = entry;
|
|
182
|
+
|
|
183
|
+
const scrollSize = isY ? vp.scrollHeight : vp.scrollWidth;
|
|
184
|
+
const clientSize = isY ? vp.clientHeight : vp.clientWidth;
|
|
185
|
+
|
|
186
|
+
// No overflow — hide thumb entirely
|
|
187
|
+
if (scrollSize <= clientSize) {
|
|
188
|
+
thumb.style.display = 'none';
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
thumb.style.display = '';
|
|
192
|
+
|
|
193
|
+
// Thumb size as percentage of track (minimum 10% for grabbability)
|
|
194
|
+
const ratio = clientSize / scrollSize;
|
|
195
|
+
const thumbPct = Math.max(ratio * 100, 10);
|
|
196
|
+
|
|
197
|
+
// Thumb position
|
|
198
|
+
const scrollPos = isY ? vp.scrollTop : vp.scrollLeft;
|
|
199
|
+
const maxScroll = scrollSize - clientSize;
|
|
200
|
+
const posPct = maxScroll > 0 ? (scrollPos / maxScroll) * (100 - thumbPct) : 0;
|
|
201
|
+
|
|
202
|
+
if (isY) {
|
|
203
|
+
thumb.style.top = posPct + '%';
|
|
204
|
+
thumb.style.height = thumbPct + '%';
|
|
205
|
+
thumb.style.left = '';
|
|
206
|
+
thumb.style.width = '';
|
|
207
|
+
} else {
|
|
208
|
+
thumb.style.left = posPct + '%';
|
|
209
|
+
thumb.style.width = thumbPct + '%';
|
|
210
|
+
thumb.style.top = '';
|
|
211
|
+
thumb.style.height = '';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#onScroll(root, sb) {
|
|
216
|
+
if (!sb) return;
|
|
217
|
+
|
|
218
|
+
root.setAttribute('data-scrolling', '');
|
|
219
|
+
clearTimeout(sb.hideTimer);
|
|
220
|
+
sb.hideTimer = setTimeout(() => {
|
|
221
|
+
if (!sb.dragging && !root.hasAttribute('data-scroll-hover')) {
|
|
222
|
+
root.removeAttribute('data-scrolling');
|
|
223
|
+
}
|
|
224
|
+
}, 1200);
|
|
225
|
+
|
|
226
|
+
if (sb.rafId) return;
|
|
227
|
+
sb.rafId = requestAnimationFrame(() => {
|
|
228
|
+
sb.rafId = 0;
|
|
229
|
+
this.#updateAllThumbs(root, sb);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#startDrag(root, sb, entry, startX, startY, isTouch) {
|
|
234
|
+
if (!sb) return;
|
|
235
|
+
sb.dragging = true;
|
|
236
|
+
root.setAttribute('data-scroll-active', '');
|
|
237
|
+
|
|
238
|
+
const { isY } = entry;
|
|
239
|
+
const startMouse = isY ? startY : startX;
|
|
240
|
+
const startScroll = isY ? sb.vp.scrollTop : sb.vp.scrollLeft;
|
|
241
|
+
const trackSize = isY ? entry.track.clientHeight : entry.track.clientWidth;
|
|
242
|
+
const scrollSize = isY ? sb.vp.scrollHeight : sb.vp.scrollWidth;
|
|
243
|
+
const clientSize = isY ? sb.vp.clientHeight : sb.vp.clientWidth;
|
|
244
|
+
const maxScroll = scrollSize - clientSize;
|
|
245
|
+
|
|
246
|
+
const onMove = (ev) => {
|
|
247
|
+
let pos;
|
|
248
|
+
if (isTouch) {
|
|
249
|
+
if (ev.touches.length === 0) return;
|
|
250
|
+
pos = isY ? ev.touches[0].clientY : ev.touches[0].clientX;
|
|
251
|
+
} else {
|
|
252
|
+
pos = isY ? ev.clientY : ev.clientX;
|
|
253
|
+
}
|
|
254
|
+
const delta = pos - startMouse;
|
|
255
|
+
const scrollDelta = (delta / trackSize) * scrollSize;
|
|
256
|
+
const newScroll = Math.max(0, Math.min(maxScroll, startScroll + scrollDelta));
|
|
257
|
+
if (isY) sb.vp.scrollTop = newScroll;
|
|
258
|
+
else sb.vp.scrollLeft = newScroll;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const onUp = () => {
|
|
262
|
+
sb.dragging = false;
|
|
263
|
+
root.removeAttribute('data-scroll-active');
|
|
264
|
+
if (!root.querySelector('[data-scrollbar-track]:hover')) {
|
|
265
|
+
root.removeAttribute('data-scroll-hover');
|
|
266
|
+
}
|
|
267
|
+
if (isTouch) {
|
|
268
|
+
document.removeEventListener('touchmove', onMove);
|
|
269
|
+
document.removeEventListener('touchend', onUp);
|
|
270
|
+
document.removeEventListener('touchcancel', onUp);
|
|
271
|
+
} else {
|
|
272
|
+
document.removeEventListener('mousemove', onMove);
|
|
273
|
+
document.removeEventListener('mouseup', onUp);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (isTouch) {
|
|
278
|
+
document.addEventListener('touchmove', onMove, { passive: false });
|
|
279
|
+
document.addEventListener('touchend', onUp);
|
|
280
|
+
document.addEventListener('touchcancel', onUp);
|
|
281
|
+
} else {
|
|
282
|
+
document.addEventListener('mousemove', onMove);
|
|
283
|
+
document.addEventListener('mouseup', onUp);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#cleanup() {
|
|
288
|
+
const sb = this.#sb;
|
|
289
|
+
if (!sb) return;
|
|
290
|
+
if (sb.ro) sb.ro.disconnect();
|
|
291
|
+
clearTimeout(sb.hideTimer);
|
|
292
|
+
for (const fn of sb.cleanups) fn();
|
|
293
|
+
this.#sb = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
customElements.define('cx-scrollbar', CxScrollbar);
|
|
298
|
+
return CxScrollbar;
|
|
299
|
+
}
|