@colletdev/core 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +77 -0
  2. package/custom-elements.json +6037 -0
  3. package/generated/.gitattributes +2 -0
  4. package/generated/index.d.ts +120 -0
  5. package/generated/index.js +521 -0
  6. package/generated/styles.js +2845 -0
  7. package/package.json +56 -0
  8. package/src/elements/accordion.d.ts +20 -0
  9. package/src/elements/accordion.js +92 -0
  10. package/src/elements/activity_group.d.ts +19 -0
  11. package/src/elements/activity_group.js +27 -0
  12. package/src/elements/alert.d.ts +24 -0
  13. package/src/elements/alert.js +40 -0
  14. package/src/elements/autocomplete.d.ts +30 -0
  15. package/src/elements/autocomplete.js +671 -0
  16. package/src/elements/avatar.d.ts +18 -0
  17. package/src/elements/avatar.js +28 -0
  18. package/src/elements/backdrop.d.ts +14 -0
  19. package/src/elements/backdrop.js +28 -0
  20. package/src/elements/badge.d.ts +21 -0
  21. package/src/elements/badge.js +42 -0
  22. package/src/elements/breadcrumb.d.ts +17 -0
  23. package/src/elements/breadcrumb.js +41 -0
  24. package/src/elements/button.d.ts +24 -0
  25. package/src/elements/button.js +36 -0
  26. package/src/elements/card.d.ts +21 -0
  27. package/src/elements/card.js +67 -0
  28. package/src/elements/carousel.d.ts +23 -0
  29. package/src/elements/carousel.js +895 -0
  30. package/src/elements/chat_input.d.ts +22 -0
  31. package/src/elements/chat_input.js +78 -0
  32. package/src/elements/checkbox.d.ts +21 -0
  33. package/src/elements/checkbox.js +114 -0
  34. package/src/elements/code_block.d.ts +21 -0
  35. package/src/elements/code_block.js +27 -0
  36. package/src/elements/collapsible.d.ts +20 -0
  37. package/src/elements/collapsible.js +93 -0
  38. package/src/elements/date_picker.d.ts +30 -0
  39. package/src/elements/date_picker.js +528 -0
  40. package/src/elements/dialog.d.ts +20 -0
  41. package/src/elements/dialog.js +314 -0
  42. package/src/elements/drawer.d.ts +20 -0
  43. package/src/elements/drawer.js +318 -0
  44. package/src/elements/fab.d.ts +22 -0
  45. package/src/elements/fab.js +36 -0
  46. package/src/elements/file_upload.d.ts +26 -0
  47. package/src/elements/file_upload.js +59 -0
  48. package/src/elements/listbox.d.ts +19 -0
  49. package/src/elements/listbox.js +250 -0
  50. package/src/elements/menu.d.ts +20 -0
  51. package/src/elements/menu.js +224 -0
  52. package/src/elements/message_bubble.d.ts +23 -0
  53. package/src/elements/message_bubble.js +29 -0
  54. package/src/elements/message_group.d.ts +18 -0
  55. package/src/elements/message_group.js +28 -0
  56. package/src/elements/message_part.d.ts +35 -0
  57. package/src/elements/message_part.js +153 -0
  58. package/src/elements/pagination.d.ts +22 -0
  59. package/src/elements/pagination.js +36 -0
  60. package/src/elements/popover.d.ts +26 -0
  61. package/src/elements/popover.js +191 -0
  62. package/src/elements/profile_menu.d.ts +20 -0
  63. package/src/elements/profile_menu.js +213 -0
  64. package/src/elements/progress.d.ts +18 -0
  65. package/src/elements/progress.js +31 -0
  66. package/src/elements/radio_group.d.ts +22 -0
  67. package/src/elements/radio_group.js +70 -0
  68. package/src/elements/scrollbar.d.ts +19 -0
  69. package/src/elements/scrollbar.js +299 -0
  70. package/src/elements/search_bar.d.ts +27 -0
  71. package/src/elements/search_bar.js +98 -0
  72. package/src/elements/select.d.ts +26 -0
  73. package/src/elements/select.js +485 -0
  74. package/src/elements/sidebar.d.ts +21 -0
  75. package/src/elements/sidebar.js +322 -0
  76. package/src/elements/skeleton.d.ts +17 -0
  77. package/src/elements/skeleton.js +31 -0
  78. package/src/elements/slider.d.ts +28 -0
  79. package/src/elements/slider.js +93 -0
  80. package/src/elements/speed_dial.d.ts +23 -0
  81. package/src/elements/speed_dial.js +370 -0
  82. package/src/elements/spinner.d.ts +15 -0
  83. package/src/elements/spinner.js +28 -0
  84. package/src/elements/split_button.d.ts +23 -0
  85. package/src/elements/split_button.js +281 -0
  86. package/src/elements/stepper.d.ts +20 -0
  87. package/src/elements/stepper.js +31 -0
  88. package/src/elements/switch.d.ts +22 -0
  89. package/src/elements/switch.js +129 -0
  90. package/src/elements/table.d.ts +29 -0
  91. package/src/elements/table.js +371 -0
  92. package/src/elements/tabs.d.ts +19 -0
  93. package/src/elements/tabs.js +139 -0
  94. package/src/elements/text.d.ts +26 -0
  95. package/src/elements/text.js +32 -0
  96. package/src/elements/text_input.d.ts +36 -0
  97. package/src/elements/text_input.js +121 -0
  98. package/src/elements/thinking.d.ts +17 -0
  99. package/src/elements/thinking.js +28 -0
  100. package/src/elements/toast.d.ts +23 -0
  101. package/src/elements/toast.js +209 -0
  102. package/src/elements/toggle_group.d.ts +22 -0
  103. package/src/elements/toggle_group.js +176 -0
  104. package/src/elements/tooltip.d.ts +18 -0
  105. package/src/elements/tooltip.js +64 -0
  106. package/src/markdown.d.ts +24 -0
  107. package/src/markdown.js +66 -0
  108. package/src/runtime.d.ts +35 -0
  109. package/src/runtime.js +790 -0
  110. package/src/server.d.ts +69 -0
  111. package/src/server.js +176 -0
  112. package/src/streaming-markdown.js +43 -0
  113. package/src/vite-plugin.d.ts +46 -0
  114. package/src/vite-plugin.js +221 -0
  115. package/wasm/package.json +16 -0
  116. package/wasm/wasm_api.d.ts +72 -0
  117. package/wasm/wasm_api.js +593 -0
  118. package/wasm/wasm_api_bg.wasm +0 -0
  119. package/wasm/wasm_api_bg.wasm.d.ts +10 -0
@@ -0,0 +1,121 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/text_input.rs
3
+
4
+ export function defineCxTextInput(wasmFn, baseClass) {
5
+ class CxTextInput extends baseClass {
6
+ static observedAttributes = ['id', 'label', 'variant', 'shape', 'size', 'kind', 'placeholder', 'value', 'helper-text', 'error', 'disabled', 'readonly', 'required', 'clearable', 'prefix-icon', 'suffix-icon', 'password-toggle', 'password-visible', 'name', 'pattern', 'autocomplete', 'auto-grow'];
7
+ static _booleanAttrs = new Set(['disabled', 'readonly', 'required', 'clearable', 'password-toggle', 'password-visible', 'auto-grow']);
8
+
9
+ set min_length(v) { this._setProp('min_length', v); }
10
+ get min_length() { return this._props.min_length; }
11
+
12
+ set max_length(v) { this._setProp('max_length', v); }
13
+ get max_length() { return this._props.max_length; }
14
+
15
+ set rows(v) { this._setProp('rows', v); }
16
+ get rows() { return this._props.rows; }
17
+
18
+ connectedCallback() {
19
+ if (!this._isInitialized) {
20
+ this._markInitialized();
21
+ const shadow = this._shadow;
22
+
23
+ // --- Click delegation: password toggle + clear button ---
24
+ shadow.addEventListener('click', (e) => {
25
+ // Password toggle button
26
+ const toggleBtn = e.target.closest('button[aria-label="Show password"], button[aria-label="Hide password"]');
27
+ if (toggleBtn) {
28
+ const input = shadow.querySelector('input');
29
+ if (!input) return;
30
+ const isPassword = input.type === 'password';
31
+ input.type = isPassword ? 'text' : 'password';
32
+ toggleBtn.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password');
33
+ const use = toggleBtn.querySelector('use');
34
+ if (use) {
35
+ use.setAttribute('href', isPassword ? '#icon-eye-off' : '#icon-eye');
36
+ }
37
+ this._props.password_visible = isPassword;
38
+ return;
39
+ }
40
+
41
+ // Clear button
42
+ const clearBtn = e.target.closest('button[aria-label="Clear"]');
43
+ if (clearBtn) {
44
+ const input = shadow.querySelector('input, textarea');
45
+ if (!input) return;
46
+ input.value = '';
47
+ input.focus();
48
+ this._setFormValue('');
49
+ this._props.value = '';
50
+ this._emit('cx-input', { value: '' });
51
+ this._emit('cx-change', { value: '' });
52
+ return;
53
+ }
54
+ });
55
+
56
+ // Delegated event listeners — attach once on shadow root
57
+ shadow.addEventListener('input', (e) => {
58
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
59
+ // Sync props immediately so any re-render before React echoes
60
+ // the value back doesn't reset the input to a stale value.
61
+ this._props.value = e.target.value;
62
+ this._setFormValue(e.target.value);
63
+ this._emit('cx-input', { value: e.target.value });
64
+ }
65
+ });
66
+ shadow.addEventListener('change', (e) => {
67
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
68
+ this._emit('cx-change', { value: e.target.value });
69
+ }
70
+ });
71
+
72
+ // Forward focus events from inner interactive elements
73
+ shadow.addEventListener('focusin', (e) => {
74
+ this._emit('cx-focus', { relatedTarget: e.relatedTarget });
75
+ });
76
+ shadow.addEventListener('focusout', (e) => {
77
+ this._emit('cx-blur', { relatedTarget: e.relatedTarget });
78
+ });
79
+
80
+ // Forward keyboard events from inner interactive elements
81
+ shadow.addEventListener('keydown', (e) => {
82
+ this._emit('cx-keydown', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
83
+ });
84
+ shadow.addEventListener('keyup', (e) => {
85
+ this._emit('cx-keyup', { key: e.key, code: e.code, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
86
+ });
87
+ } // end _isInitialized guard
88
+ super.connectedCallback();
89
+ }
90
+
91
+ // ── Public imperative API ──
92
+ focus() { const el = this._shadow.querySelector('input, textarea'); if (el) el.focus(); else super.focus(); }
93
+ blur() { const el = this._shadow.querySelector('input, textarea'); if (el) el.blur(); }
94
+ select() { const el = this._shadow.querySelector('input, textarea'); if (el) el.select(); }
95
+
96
+ _doRender() {
97
+ try {
98
+ const result = wasmFn(this._props);
99
+ this._injectHtml(result);
100
+ // Sync form value after render. Event delegation on shadow root
101
+ // avoids duplicate listeners (innerHTML replaces old DOM nodes,
102
+ // but shadow root persists — delegation handles new inputs).
103
+ const input = this._shadow.querySelector('input, textarea, select');
104
+ if (input) {
105
+ // Sync controlled value prop → internal input property.
106
+ // _injectHtml's focus-preservation restores the OLD typed value
107
+ // to maintain cursor position during re-renders. But when the
108
+ // value prop explicitly changes (e.g., clearing after submit),
109
+ // the controlled value must win.
110
+ if ('value' in this._props) input.value = this._props.value;
111
+ this._setFormValue(input.value || '');
112
+ }
113
+ } catch (e) {
114
+ console.error('[cx-text-input]', e);
115
+ }
116
+ }
117
+ }
118
+
119
+ customElements.define('cx-text-input', CxTextInput);
120
+ return CxTextInput;
121
+ }
@@ -0,0 +1,17 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/thinking.rs
3
+
4
+ export interface CxThinkingAttributes {
5
+ id?: string;
6
+ variant?: 'ghost' | 'filled';
7
+ shape?: 'sharp' | 'rounded' | 'pill';
8
+ label?: string;
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
10
+ speed?: string;
11
+ }
12
+
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ 'cx-thinking': HTMLElement & CxThinkingAttributes;
16
+ }
17
+ }
@@ -0,0 +1,28 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/thinking.rs
3
+
4
+ export function defineCxThinking(wasmFn, baseClass) {
5
+ class CxThinking extends baseClass {
6
+ static observedAttributes = ['id', 'variant', 'shape', 'label', 'size', 'speed'];
7
+ static _booleanAttrs = new Set([]);
8
+ static _focusable = false;
9
+ static _hostDisplay = 'inline-flex';
10
+
11
+
12
+ connectedCallback() {
13
+ super.connectedCallback();
14
+ }
15
+
16
+ _doRender() {
17
+ try {
18
+ const result = wasmFn(this._props);
19
+ this._injectHtml(result);
20
+ } catch (e) {
21
+ console.error('[cx-thinking]', e);
22
+ }
23
+ }
24
+ }
25
+
26
+ customElements.define('cx-thinking', CxThinking);
27
+ return CxThinking;
28
+ }
@@ -0,0 +1,23 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/toast.rs
3
+
4
+ export interface CxToastAttributes {
5
+ id?: string;
6
+ title?: string;
7
+ description?: string;
8
+ variant?: 'subtle' | 'filled' | 'outline';
9
+ intent?: 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ showIcon?: boolean;
12
+ icon?: string;
13
+ dismissible?: boolean;
14
+ dismissLabel?: string;
15
+ action?: string;
16
+ duration?: number;
17
+ }
18
+
19
+ declare global {
20
+ interface HTMLElementTagNameMap {
21
+ 'cx-toast': HTMLElement & CxToastAttributes;
22
+ }
23
+ }
@@ -0,0 +1,209 @@
1
+ // Custom behavior for <cx-toast> — entrance/exit animation, auto-dismiss timer,
2
+ // hover-pause, dismiss button handling, and progress indicator.
3
+ //
4
+ // Animations use CSS @keyframes driven by data attributes:
5
+ // [data-open] → entrance (350ms snappy slide + scale + fade)
6
+ // [data-closing] → exit (250ms ease-out slide + scale + fade)
7
+ //
8
+ // Auto-dismiss: configurable via `duration` property (default: 4250ms).
9
+ // Timer pauses on hover and resumes on mouse leave. A progress bar
10
+ // (`data-toast-progress`) shows remaining time visually.
11
+ //
12
+ // Source: crates/wasm-api/src/toast.rs
13
+
14
+ // ─── Animation Timing ───
15
+
16
+ const ENTER_DURATION = 350;
17
+ const EXIT_DURATION = 250;
18
+
19
+ export function defineCxToast(wasmFn, baseClass) {
20
+ class CxToast extends baseClass {
21
+ static observedAttributes = ['id', 'title', 'description', 'variant', 'intent', 'size', 'show-icon', 'icon', 'dismissible', 'dismiss-label'];
22
+ static _booleanAttrs = new Set(['show-icon', 'dismissible']);
23
+
24
+ // Duration property (ms). 0 = persistent (no auto-dismiss).
25
+ #duration = 4250;
26
+ #timer = null;
27
+ #remaining = 0;
28
+ #startedAt = 0;
29
+ #paused = false;
30
+ #dismissed = false;
31
+
32
+ set duration(v) {
33
+ const ms = typeof v === 'number' ? v : parseInt(v, 10);
34
+ if (!Number.isNaN(ms) && ms >= 0) {
35
+ this.#duration = ms;
36
+ this._setProp('duration', ms);
37
+ }
38
+ }
39
+ get duration() { return this.#duration; }
40
+
41
+ connectedCallback() {
42
+ if (!this._isInitialized) {
43
+ this._markInitialized();
44
+ const shadow = this._shadow;
45
+
46
+ // Dismiss button click
47
+ shadow.addEventListener('click', (e) => {
48
+ const btn = e.target.closest('[data-handler*="dismiss"], [data-toast-dismiss]');
49
+ if (btn) this.#dismiss();
50
+ });
51
+
52
+ // Hover: pause auto-dismiss timer
53
+ this.addEventListener('mouseenter', () => this.#pauseTimer());
54
+ this.addEventListener('mouseleave', () => this.#resumeTimer());
55
+
56
+ // Focus within: also pause (keyboard users hovering dismiss button)
57
+ this.addEventListener('focusin', () => this.#pauseTimer());
58
+ this.addEventListener('focusout', (e) => {
59
+ if (!this.contains(e.relatedTarget) && !this._shadow.contains(e.relatedTarget)) this.#resumeTimer();
60
+ });
61
+ } // end _isInitialized guard
62
+
63
+ super.connectedCallback();
64
+ }
65
+
66
+ disconnectedCallback() {
67
+ this.#clearTimer();
68
+ super.disconnectedCallback();
69
+ }
70
+
71
+ // ── Public imperative API ──
72
+ dismiss() { this.#dismiss(); }
73
+
74
+ // ─── Render ───
75
+
76
+ _doRender() {
77
+ try {
78
+ this._props.slotted = true;
79
+ const result = wasmFn(this._props);
80
+ this._injectHtml(result);
81
+
82
+ // Inject progress bar for timed toasts
83
+ if (this.#duration > 0) {
84
+ const toast = this._shadow.querySelector('[data-toast]');
85
+ if (toast && !toast.querySelector('[data-toast-progress]')) {
86
+ // Ensure relative positioning for the absolute progress bar
87
+ toast.style.position = 'relative';
88
+ toast.style.overflow = 'hidden';
89
+ const bar = document.createElement('div');
90
+ bar.setAttribute('data-toast-progress', '');
91
+ bar.style.animation = `toast-progress ${this.#duration}ms linear forwards`;
92
+ toast.appendChild(bar);
93
+ }
94
+ }
95
+
96
+ // Trigger entrance animation after first render
97
+ requestAnimationFrame(() => this.#enter());
98
+ } catch (e) {
99
+ console.error('[cx-toast]', e);
100
+ }
101
+ }
102
+
103
+ // ─── Entrance ───
104
+
105
+ #enter() {
106
+ if (this.#dismissed) return;
107
+ const toast = this._shadow.querySelector('[data-toast]');
108
+ if (!toast) return;
109
+
110
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
111
+
112
+ if (reducedMotion) {
113
+ toast.setAttribute('data-open', '');
114
+ } else {
115
+ toast.setAttribute('data-open', '');
116
+ }
117
+
118
+ // Start auto-dismiss timer after entrance animation completes
119
+ if (this.#duration > 0) {
120
+ this.#remaining = this.#duration;
121
+ this.#startedAt = Date.now();
122
+ this.#timer = setTimeout(() => this.#dismiss(), this.#duration);
123
+ }
124
+ }
125
+
126
+ // ─── Exit + Dismiss ───
127
+
128
+ #dismiss() {
129
+ if (this.#dismissed) return;
130
+ this.#dismissed = true;
131
+ this.#clearTimer();
132
+
133
+ const toast = this._shadow.querySelector('[data-toast]');
134
+ if (!toast) {
135
+ this.#cleanup();
136
+ return;
137
+ }
138
+
139
+ toast.removeAttribute('data-open');
140
+ toast.setAttribute('data-closing', '');
141
+
142
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
143
+ const dur = reducedMotion ? 0 : EXIT_DURATION;
144
+
145
+ // Wait for exit animation to finish, then emit event and remove
146
+ if (dur > 0) {
147
+ toast.addEventListener('animationend', () => this.#cleanup(), { once: true });
148
+ // Safety fallback in case animationend doesn't fire
149
+ setTimeout(() => this.#cleanup(), dur + 50);
150
+ } else {
151
+ this.#cleanup();
152
+ }
153
+ }
154
+
155
+ #cleanedUp = false;
156
+ #cleanup() {
157
+ // Guard: animationend + setTimeout safety fallback can both call this.
158
+ // Only emit cx-dismiss once — double events cause React state errors.
159
+ if (this.#cleanedUp) return;
160
+ this.#cleanedUp = true;
161
+ this._emit('cx-dismiss', {});
162
+ // Don't call this.remove() — let the consumer (React, vanilla JS)
163
+ // handle DOM removal via the cx-dismiss event. Self-removal breaks
164
+ // React's virtual DOM reconciliation.
165
+ }
166
+
167
+ // ─── Timer Management ───
168
+
169
+ #pauseTimer() {
170
+ if (this.#paused || this.#dismissed || this.#duration === 0) return;
171
+ this.#paused = true;
172
+
173
+ if (this.#timer) {
174
+ clearTimeout(this.#timer);
175
+ this.#timer = null;
176
+ this.#remaining -= (Date.now() - this.#startedAt);
177
+ if (this.#remaining < 0) this.#remaining = 0;
178
+ }
179
+
180
+ // Pause progress bar animation
181
+ const bar = this._shadow.querySelector('[data-toast-progress]');
182
+ if (bar) bar.style.animationPlayState = 'paused';
183
+ }
184
+
185
+ #resumeTimer() {
186
+ if (!this.#paused || this.#dismissed || this.#duration === 0) return;
187
+ this.#paused = false;
188
+
189
+ if (this.#remaining > 0) {
190
+ this.#startedAt = Date.now();
191
+ this.#timer = setTimeout(() => this.#dismiss(), this.#remaining);
192
+ }
193
+
194
+ // Resume progress bar animation
195
+ const bar = this._shadow.querySelector('[data-toast-progress]');
196
+ if (bar) bar.style.animationPlayState = 'running';
197
+ }
198
+
199
+ #clearTimer() {
200
+ if (this.#timer) {
201
+ clearTimeout(this.#timer);
202
+ this.#timer = null;
203
+ }
204
+ }
205
+ }
206
+
207
+ customElements.define('cx-toast', CxToast);
208
+ return CxToast;
209
+ }
@@ -0,0 +1,22 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/toggle_group.rs
3
+
4
+ export interface CxToggleGroupAttributes {
5
+ id?: string;
6
+ label?: string;
7
+ items?: string;
8
+ variant?: 'outline' | 'ghost' | 'filled';
9
+ shape?: 'sharp' | 'rounded' | 'pill';
10
+ mode?: 'single' | 'multiple';
11
+ orientation?: 'horizontal' | 'vertical';
12
+ intent?: 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
13
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
14
+ disabled?: boolean;
15
+ value?: string;
16
+ }
17
+
18
+ declare global {
19
+ interface HTMLElementTagNameMap {
20
+ 'cx-toggle-group': HTMLElement & CxToggleGroupAttributes;
21
+ }
22
+ }
@@ -0,0 +1,176 @@
1
+ // Custom behavior for <cx-toggle-group> — button toggle state management.
2
+ //
3
+ // The Rust component renders:
4
+ // <div role="group" data-toggle-group data-toggle-mode="single|multiple">
5
+ // <span data-toggle-indicator> (single mode only — sliding highlight)
6
+ // <button data-toggle-item data-toggle-value="{id}" aria-pressed="true|false">
7
+ //
8
+ // This Custom Element wires up:
9
+ // - Click button to toggle pressed state
10
+ // - Single mode: only one can be pressed at a time
11
+ // - Multiple mode: each toggles independently
12
+ // - Sliding indicator positioning (single mode)
13
+ // - Keyboard navigation (ArrowLeft/Right, Home/End)
14
+ // - cx-change event with { pressed: string[] }
15
+ //
16
+ // Source: crates/wasm-api/src/toggle_group.rs
17
+
18
+ export function defineCxToggleGroup(wasmFn, baseClass) {
19
+ class CxToggleGroup extends baseClass {
20
+ static observedAttributes = ['id', 'label', 'items', 'value', 'variant', 'shape', 'mode', 'orientation', 'intent', 'size', 'disabled'];
21
+ static _booleanAttrs = new Set(['disabled']);
22
+ static _hostDisplay = 'inline-flex';
23
+
24
+ connectedCallback() {
25
+ if (!this._isInitialized) {
26
+ this._markInitialized();
27
+ const shadow = this._shadow;
28
+
29
+ // ── Click handler ──
30
+ shadow.addEventListener('click', (e) => {
31
+ const btn = e.target.closest('[data-toggle-item]');
32
+ if (!btn || btn.disabled || btn.hasAttribute('aria-disabled')) return;
33
+
34
+ this.#toggleItem(btn);
35
+ });
36
+
37
+ // ── Keyboard handler (arrow navigation) ──
38
+ shadow.addEventListener('keydown', (e) => {
39
+ const btn = e.target.closest('[data-toggle-item]');
40
+ if (!btn) return;
41
+
42
+ const isHorizontal = this.#getOrientation() !== 'vertical';
43
+ const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';
44
+ const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';
45
+
46
+ if (e.key === prevKey || e.key === nextKey) {
47
+ e.preventDefault();
48
+ const items = this.#getItems();
49
+ const idx = items.indexOf(btn);
50
+ if (idx === -1) return;
51
+
52
+ let next;
53
+ if (e.key === nextKey) {
54
+ next = items[(idx + 1) % items.length];
55
+ } else {
56
+ next = items[(idx - 1 + items.length) % items.length];
57
+ }
58
+ next.focus();
59
+ } else if (e.key === 'Home') {
60
+ e.preventDefault();
61
+ const items = this.#getItems();
62
+ if (items.length) items[0].focus();
63
+ } else if (e.key === 'End') {
64
+ e.preventDefault();
65
+ const items = this.#getItems();
66
+ if (items.length) items[items.length - 1].focus();
67
+ }
68
+ });
69
+ } // end _isInitialized guard
70
+ super.connectedCallback();
71
+ }
72
+
73
+ #getGroup() {
74
+ return this._shadow.querySelector('[data-toggle-group]');
75
+ }
76
+
77
+ #getItems() {
78
+ return Array.from(
79
+ this._shadow.querySelectorAll('[data-toggle-item]:not([disabled]):not([aria-disabled="true"])')
80
+ );
81
+ }
82
+
83
+ #getMode() {
84
+ const group = this.#getGroup();
85
+ return group?.getAttribute('data-toggle-mode') || 'single';
86
+ }
87
+
88
+ #getOrientation() {
89
+ const group = this.#getGroup();
90
+ return group?.getAttribute('aria-orientation') || 'horizontal';
91
+ }
92
+
93
+ #toggleItem(btn) {
94
+ const mode = this.#getMode();
95
+ const value = btn.getAttribute('data-toggle-value') || '';
96
+ const wasPressed = btn.getAttribute('aria-pressed') === 'true';
97
+
98
+ let pressed;
99
+ if (mode === 'single') {
100
+ // Single: press this one, depress all others
101
+ // (no deselection — matches radio button behavior)
102
+ pressed = [value];
103
+ } else {
104
+ // Multiple: toggle independently, collect all pressed values
105
+ pressed = this.#getPressedValues();
106
+ if (wasPressed) {
107
+ pressed = pressed.filter(v => v !== value);
108
+ } else {
109
+ pressed.push(value);
110
+ }
111
+ }
112
+
113
+ // Mutate shadow DOM directly for instant visual feedback + indicator animation
114
+ this._shadow.querySelectorAll('[data-toggle-item]').forEach(b => {
115
+ const isPressed = pressed.includes(b.getAttribute('data-toggle-value') || '');
116
+ b.setAttribute('aria-pressed', String(isPressed));
117
+ b.setAttribute('data-pressed', String(isPressed));
118
+ });
119
+
120
+ if (mode === 'single') {
121
+ this.#positionIndicator(btn);
122
+ }
123
+
124
+ // Sync host prop — single source of truth for WASM re-renders.
125
+ // Uses direct _props mutation (not _setProp) to avoid triggering a
126
+ // re-render that would destroy DOM state and kill indicator animation.
127
+ // External attribute changes still trigger WASM re-render via
128
+ // attributeChangedCallback, keeping the two in sync.
129
+ this._props.value = pressed.join(',');
130
+
131
+ this._emit('cx-change', { pressed });
132
+ }
133
+
134
+ #getPressedValues() {
135
+ return Array.from(
136
+ this._shadow.querySelectorAll('[data-toggle-item][aria-pressed="true"]')
137
+ ).map(b => b.getAttribute('data-toggle-value') || '');
138
+ }
139
+
140
+ #positionIndicator(activeBtn) {
141
+ const indicator = this._shadow.querySelector('[data-toggle-indicator]');
142
+ const group = this.#getGroup();
143
+ if (!indicator || !group || !activeBtn) return;
144
+
145
+ // Position indicator to match the active button's dimensions
146
+ const groupRect = group.getBoundingClientRect();
147
+ const btnRect = activeBtn.getBoundingClientRect();
148
+
149
+ indicator.style.setProperty('--ind-x', `${btnRect.left - groupRect.left}px`);
150
+ indicator.style.setProperty('--ind-y', `${btnRect.top - groupRect.top}px`);
151
+ indicator.style.setProperty('--ind-w', `${btnRect.width}px`);
152
+ indicator.style.setProperty('--ind-h', `${btnRect.height}px`);
153
+ indicator.style.opacity = '1';
154
+ }
155
+
156
+ _doRender() {
157
+ try {
158
+ const result = wasmFn(this._props);
159
+ this._injectHtml(result);
160
+
161
+ // Position indicator on initial pressed item
162
+ requestAnimationFrame(() => {
163
+ const pressed = this._shadow.querySelector(
164
+ '[data-toggle-item][aria-pressed="true"]'
165
+ );
166
+ if (pressed) this.#positionIndicator(pressed);
167
+ });
168
+ } catch (e) {
169
+ console.error('[cx-toggle-group]', e);
170
+ }
171
+ }
172
+ }
173
+
174
+ customElements.define('cx-toggle-group', CxToggleGroup);
175
+ return CxToggleGroup;
176
+ }
@@ -0,0 +1,18 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/tooltip.rs
3
+
4
+ export interface CxTooltipAttributes {
5
+ id?: string;
6
+ content?: string;
7
+ triggerHtml?: string;
8
+ position?: 'top' | 'bottom' | 'left' | 'right';
9
+ arrow?: boolean;
10
+ openDelay?: number;
11
+ closeDelay?: number;
12
+ }
13
+
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ 'cx-tooltip': HTMLElement & CxTooltipAttributes;
17
+ }
18
+ }
@@ -0,0 +1,64 @@
1
+ // Custom behavior for <cx-tooltip> — connects shadow DOM tooltip wrapper
2
+ // to the shared floating tooltip on document.body.
3
+ //
4
+ // The tooltip behavior module (static/_behaviors/tooltip.js) creates a single
5
+ // shared tooltip element on document.body. This Custom Element wires up the
6
+ // shadow DOM trigger to that system, ensuring the tooltip escapes scroll
7
+ // container clipping (position: fixed on body, not absolute in shadow DOM).
8
+ //
9
+ // The Rust component renders:
10
+ // <div data-tooltip-wrapper> — positioning anchor
11
+ // <slot name="trigger"> — the trigger element
12
+ // <div role="tooltip"> — CSS-only fallback content (hidden when JS loads)
13
+ //
14
+ // Source: crates/wasm-api/src/tooltip.rs
15
+
16
+ export function defineCxTooltip(wasmFn, baseClass) {
17
+ class CxTooltip extends baseClass {
18
+ static observedAttributes = ['id', 'content', 'trigger-html', 'position', 'arrow', 'open-delay', 'close-delay'];
19
+ static _booleanAttrs = new Set(['arrow']);
20
+ static _numericAttrs = new Set(['open-delay', 'close-delay']);
21
+ static _hostDisplay = 'inline-flex';
22
+
23
+ set open_delay(v) { this._setProp('open_delay', v); }
24
+ get open_delay() { return this._props.open_delay; }
25
+
26
+ set close_delay(v) { this._setProp('close_delay', v); }
27
+ get close_delay() { return this._props.close_delay; }
28
+
29
+ connectedCallback() {
30
+ super.connectedCallback();
31
+ }
32
+
33
+ _doRender() {
34
+ try {
35
+ this._props.slotted = true;
36
+ const result = wasmFn(this._props);
37
+ this._injectHtml(result);
38
+
39
+ // Connect shadow DOM tooltip wrapper to the shared floating tooltip
40
+ // on document.body. The behavior module's init() attaches pointerenter/
41
+ // pointerleave listeners that show a single shared tooltip element,
42
+ // positioned via getBoundingClientRect (viewport coordinates, escapes
43
+ // all scroll containers).
44
+ //
45
+ // innerHTML replacement destroys the old wrapper — each render needs
46
+ // fresh listener attachment (old listeners are GC'd with old DOM).
47
+ const wrapper = this._shadow.querySelector('[data-tooltip-wrapper]');
48
+ if (wrapper && typeof __cx !== 'undefined' && __cx._behaviors?.tooltip?.init) {
49
+ __cx._behaviors.tooltip.init(wrapper);
50
+ // Hide the CSS-only inline tooltip — the floating one on body takes over.
51
+ // This prevents the clipped inline tooltip from appearing alongside the
52
+ // correctly-positioned floating one.
53
+ const inlineTip = wrapper.querySelector('[role="tooltip"]');
54
+ if (inlineTip) inlineTip.style.display = 'none';
55
+ }
56
+ } catch (e) {
57
+ console.error('[cx-tooltip]', e);
58
+ }
59
+ }
60
+ }
61
+
62
+ customElements.define('cx-tooltip', CxTooltip);
63
+ return CxTooltip;
64
+ }