@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,314 @@
1
+ // Custom behavior for <cx-dialog> — open/close modal, backdrop, scroll lock.
2
+ //
3
+ // All animations use Web Animations API. CSS @keyframes in adopted stylesheets
4
+ // have browser inconsistencies with showModal() top-layer promotion.
5
+ // Same approach as drawer.js — see that file for detailed rationale.
6
+ //
7
+ // Source: crates/wasm-api/src/dialog.rs
8
+
9
+ // ─── Static Styles (no @keyframes) ───
10
+
11
+ let _sheet;
12
+ function getSheet() {
13
+ if (!_sheet) {
14
+ _sheet = new CSSStyleSheet();
15
+ _sheet.replaceSync([
16
+ 'dialog[data-dialog]::backdrop {',
17
+ ' background: transparent !important;',
18
+ ' backdrop-filter: none !important;',
19
+ ' -webkit-backdrop-filter: none !important;',
20
+ '}',
21
+ '[data-dialog-panel] { position: relative; z-index: 1; }',
22
+ ].join('\n'));
23
+ }
24
+ return _sheet;
25
+ }
26
+
27
+ // ─── Animation Constants (from design system tokens.rs) ───
28
+
29
+ // Entrance: 400ms spring (--duration-smooth, --ease-spring)
30
+ const ENTER_DURATION = 400;
31
+ const ENTER_EASING = 'cubic-bezier(0.175, 0.885, 0.32, 1.275)';
32
+
33
+ // Exit: 250ms ease-in (accelerating away — decisive, not lingering)
34
+ const EXIT_DURATION = 250;
35
+ const EXIT_EASING = 'cubic-bezier(0.4, 0, 1, 1)';
36
+
37
+ // Backdrop timing
38
+ const BACKDROP_ENTER_DURATION = 300;
39
+ const BACKDROP_EASING = 'ease-out';
40
+
41
+ export function defineCxDialog(wasmFn, baseClass) {
42
+ class CxDialog extends baseClass {
43
+ static observedAttributes = ['id', 'title', 'variant', 'description', 'size', 'close-button', 'drawer', 'open'];
44
+ static _booleanAttrs = new Set(['close-button', 'open']);
45
+
46
+ // Guard: prevents attributeChangedCallback re-entry when syncing
47
+ // the open attribute from imperative open()/close() calls.
48
+ #syncing = false;
49
+
50
+ connectedCallback() {
51
+ if (!this._isInitialized) {
52
+ this._markInitialized();
53
+ const shadow = this._shadow;
54
+
55
+ const sheet = getSheet();
56
+ if (!shadow.adoptedStyleSheets.includes(sheet)) {
57
+ shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, sheet];
58
+ }
59
+
60
+ // Only auto-open if the element has an explicit 'open' attribute.
61
+ // React mounts components into the tree and expects to control
62
+ // visibility via ref.current.open() — auto-opening on mount
63
+ // causes unwanted flashes.
64
+ this._autoOpen = this.hasAttribute('open');
65
+
66
+ shadow.addEventListener('click', (e) => {
67
+ if (e.target.closest('[data-dialog-close]')) {
68
+ this.#closeDialog();
69
+ return;
70
+ }
71
+ // Backdrop click (standard dialogs only, not alert)
72
+ const dialog = shadow.querySelector('dialog[data-dialog]');
73
+ if (dialog && e.target === dialog && !dialog.hasAttribute('data-dialog-alert')) {
74
+ this.#closeDialog();
75
+ }
76
+ });
77
+
78
+ shadow.addEventListener('keydown', (e) => {
79
+ if (e.key === 'Escape') {
80
+ const dialog = shadow.querySelector('dialog[data-dialog]');
81
+ if (dialog && dialog.hasAttribute('data-dialog-alert')) {
82
+ e.preventDefault();
83
+ return;
84
+ }
85
+ this.#closeDialog();
86
+ }
87
+ });
88
+
89
+ shadow.addEventListener('cancel', (e) => {
90
+ const dialog = shadow.querySelector('dialog[data-dialog]');
91
+ if (dialog && dialog.hasAttribute('data-dialog-alert')) {
92
+ e.preventDefault();
93
+ return;
94
+ }
95
+ this.#closeDialog();
96
+ });
97
+ } // end _isInitialized guard
98
+ super.connectedCallback();
99
+ }
100
+
101
+ // Intercept 'open' attribute — toggle dialog without WASM re-render.
102
+ // All other attributes delegate to base class for props → WASM pipeline.
103
+ attributeChangedCallback(name, oldVal, newVal) {
104
+ if (name === 'open') {
105
+ if (this.#syncing) return;
106
+ if (newVal !== null) {
107
+ // open attribute added → open the dialog
108
+ this._autoOpen = true;
109
+ if (this._isInitialized) this.#openDialog();
110
+ } else {
111
+ // open attribute removed → close the dialog
112
+ this._autoOpen = false;
113
+ const d = this._shadow?.querySelector('dialog[data-dialog]');
114
+ if (d?.open && !d.hasAttribute('data-closing')) {
115
+ this.#closeDialog();
116
+ }
117
+ }
118
+ return;
119
+ }
120
+ super.attributeChangedCallback(name, oldVal, newVal);
121
+ }
122
+
123
+ // Property accessor for dialog open state (read-only).
124
+ // Named `isOpen` to avoid collision with the imperative `open()` method.
125
+ get isOpen() {
126
+ const dialog = this._shadow?.querySelector('dialog[data-dialog]');
127
+ return dialog ? dialog.open : this.hasAttribute('open');
128
+ }
129
+
130
+ disconnectedCallback() {
131
+ this._unlockScroll();
132
+ super.disconnectedCallback();
133
+ }
134
+
135
+ // ─── Overlay Management ───
136
+
137
+ #ensureOverlay(dialog) {
138
+ if (dialog.querySelector('[data-cx-overlay]')) return;
139
+ const el = document.createElement('div');
140
+ el.setAttribute('data-cx-overlay', '');
141
+ Object.assign(el.style, {
142
+ position: 'fixed',
143
+ inset: '0',
144
+ backgroundColor: 'oklch(0 0 0 / 0.3)',
145
+ backdropFilter: 'blur(4px)',
146
+ WebkitBackdropFilter: 'blur(4px)',
147
+ pointerEvents: 'none',
148
+ zIndex: '0',
149
+ opacity: '0',
150
+ });
151
+ dialog.prepend(el);
152
+ }
153
+
154
+ // ─── Open ───
155
+
156
+ #openDialog() {
157
+ const dialog = this._shadow.querySelector('dialog[data-dialog]');
158
+ if (!dialog || dialog.open) return;
159
+
160
+ // Clear _autoOpen — the open is now being fulfilled. Without this,
161
+ // _autoOpen stays true from attributeChangedCallback and the close
162
+ // cleanup misinterprets it as a rapid-toggle re-open request.
163
+ this._autoOpen = false;
164
+
165
+ // Sync attribute (guard prevents re-entry via attributeChangedCallback)
166
+ if (!this.hasAttribute('open')) {
167
+ this.#syncing = true;
168
+ this.setAttribute('open', '');
169
+ this.#syncing = false;
170
+ }
171
+
172
+ dialog.showModal();
173
+ this._lockScroll();
174
+
175
+ requestAnimationFrame(() => {
176
+ const panel = dialog.querySelector('[data-dialog-panel]');
177
+ const overlay = dialog.querySelector('[data-cx-overlay]');
178
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
179
+
180
+ // Panel: scale up + slide down with spring easing
181
+ if (panel) {
182
+ panel.animate(
183
+ [
184
+ { opacity: 0, transform: 'scale(0.95) translateY(-8px)' },
185
+ { opacity: 1, transform: 'scale(1) translateY(0)' },
186
+ ],
187
+ { duration: reducedMotion ? 0 : ENTER_DURATION, easing: ENTER_EASING, fill: 'forwards' }
188
+ );
189
+ }
190
+
191
+ // Backdrop overlay fade-in
192
+ if (overlay) {
193
+ overlay.animate(
194
+ [{ opacity: 0 }, { opacity: 1 }],
195
+ { duration: reducedMotion ? 0 : BACKDROP_ENTER_DURATION, easing: BACKDROP_EASING, fill: 'forwards' }
196
+ );
197
+ }
198
+
199
+ dialog.setAttribute('data-open', '');
200
+ });
201
+ }
202
+
203
+ // ─── Close ───
204
+
205
+ #closeDialog() {
206
+ const dialog = this._shadow.querySelector('dialog[data-dialog]');
207
+ if (!dialog || !dialog.open) return;
208
+ if (dialog.hasAttribute('data-closing')) return;
209
+
210
+ // Clear _autoOpen so cleanup doesn't misinterpret a stale true
211
+ // as a rapid-toggle request. Only a NEW setAttribute('open', '')
212
+ // during the close animation should set _autoOpen = true.
213
+ this._autoOpen = false;
214
+
215
+ dialog.setAttribute('data-closing', '');
216
+ dialog.removeAttribute('data-open');
217
+
218
+ // Sync attribute (guard prevents re-entry via attributeChangedCallback)
219
+ if (this.hasAttribute('open')) {
220
+ this.#syncing = true;
221
+ this.removeAttribute('open');
222
+ this.#syncing = false;
223
+ }
224
+
225
+ const panel = dialog.querySelector('[data-dialog-panel]');
226
+ const overlay = dialog.querySelector('[data-cx-overlay]');
227
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
228
+ const dur = reducedMotion ? 0 : EXIT_DURATION;
229
+
230
+ const cleanup = () => {
231
+ dialog.removeAttribute('data-closing');
232
+ // Pin final visual state as inline styles BEFORE dialog.close().
233
+ // dialog.close() removes the element from the top layer, which
234
+ // cancels all Web Animations (including fill: 'forwards').
235
+ // Without pinning, the panel snaps to its original position for
236
+ // one paint frame before the dialog fully hides.
237
+ if (panel) {
238
+ panel.style.opacity = '0';
239
+ panel.style.transform = 'scale(0.97) translateY(-8px)';
240
+ }
241
+ if (overlay) overlay.style.opacity = '0';
242
+ dialog.close();
243
+ // Clear pinned styles (dialog is now hidden, no visual impact)
244
+ if (panel) {
245
+ panel.style.opacity = '';
246
+ panel.style.transform = '';
247
+ }
248
+ if (overlay) overlay.style.opacity = '';
249
+ this._unlockScroll();
250
+ this._emit('cx-close', {});
251
+ // If open was re-requested during close animation (rapid toggle),
252
+ // re-open now that the dialog has fully closed.
253
+ if (this._autoOpen) {
254
+ this._autoOpen = false;
255
+ requestAnimationFrame(() => this.#openDialog());
256
+ }
257
+ };
258
+
259
+ // Backdrop fade-out (parallel)
260
+ if (overlay) {
261
+ overlay.animate(
262
+ [{ opacity: 1 }, { opacity: 0 }],
263
+ { duration: dur, easing: EXIT_EASING, fill: 'forwards' }
264
+ );
265
+ }
266
+
267
+ // Panel: subtle scale + return upward (reverses entrance direction)
268
+ if (panel) {
269
+ const anim = panel.animate(
270
+ [
271
+ { opacity: 1, transform: 'scale(1) translateY(0)' },
272
+ { opacity: 0, transform: 'scale(0.97) translateY(-8px)' },
273
+ ],
274
+ { duration: dur, easing: EXIT_EASING, fill: 'forwards' }
275
+ );
276
+ anim.finished.then(cleanup).catch(cleanup);
277
+ } else {
278
+ cleanup();
279
+ }
280
+ }
281
+
282
+ // ── Public imperative API ──
283
+ open() { this.#openDialog(); }
284
+ close() { this.#closeDialog(); }
285
+
286
+ // ─── Render ───
287
+
288
+ _doRender() {
289
+ try {
290
+ this._props.slotted = true;
291
+ const result = wasmFn(this._props);
292
+ this._injectHtml(result);
293
+
294
+ const sheet = getSheet();
295
+ if (!this._shadow.adoptedStyleSheets.includes(sheet)) {
296
+ this._shadow.adoptedStyleSheets = [...this._shadow.adoptedStyleSheets, sheet];
297
+ }
298
+
299
+ const dialog = this._shadow.querySelector('dialog[data-dialog]');
300
+ if (dialog) this.#ensureOverlay(dialog);
301
+
302
+ if (this._autoOpen) {
303
+ this._autoOpen = false;
304
+ requestAnimationFrame(() => this.#openDialog());
305
+ }
306
+ } catch (e) {
307
+ console.error('[cx-dialog]', e);
308
+ }
309
+ }
310
+ }
311
+
312
+ customElements.define('cx-dialog', CxDialog);
313
+ return CxDialog;
314
+ }
@@ -0,0 +1,20 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/drawer.rs
3
+
4
+ export interface CxDrawerAttributes {
5
+ id?: string;
6
+ title?: string;
7
+ description?: string;
8
+ body?: string;
9
+ footer?: string;
10
+ side?: 'left' | 'right' | 'top' | 'bottom';
11
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
12
+ shape?: 'sharp' | 'rounded' | 'pill';
13
+ closeButton?: string;
14
+ }
15
+
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ 'cx-drawer': HTMLElement & CxDrawerAttributes;
19
+ }
20
+ }
@@ -0,0 +1,318 @@
1
+ // Custom behavior for <cx-drawer> — open/close drawer panel, backdrop, scroll lock.
2
+ //
3
+ // All animations use Web Animations API. CSS @keyframes in adopted stylesheets
4
+ // have browser inconsistencies with showModal() top-layer promotion — the
5
+ // animationend event fires but no visual rendering occurs. Direct API calls
6
+ // bypass this entirely and give us reliable, programmatic animation control.
7
+ //
8
+ // The native ::backdrop pseudo-element can't be animated via JS (no DOM handle).
9
+ // We hide it and inject a real overlay <div> inside the dialog instead,
10
+ // animated alongside the panel for coordinated entrance/exit transitions.
11
+ //
12
+ // Source: crates/wasm-api/src/drawer.rs
13
+
14
+ // ─── Static Styles (no @keyframes) ───
15
+
16
+ let _sheet;
17
+ function getSheet() {
18
+ if (!_sheet) {
19
+ _sheet = new CSSStyleSheet();
20
+ _sheet.replaceSync([
21
+ // Hide native ::backdrop — our overlay div handles the visual.
22
+ // ::backdrop still blocks pointer events behind the dialog (showModal).
23
+ 'dialog[data-dialog]::backdrop {',
24
+ ' background: transparent !important;',
25
+ ' backdrop-filter: none !important;',
26
+ ' -webkit-backdrop-filter: none !important;',
27
+ '}',
28
+ // Panel above overlay in stacking order
29
+ '[data-dialog-panel] { position: relative; z-index: 1; }',
30
+ ].join('\n'));
31
+ }
32
+ return _sheet;
33
+ }
34
+
35
+ // ─── Animation Constants (from design system tokens.rs) ───
36
+
37
+ // Off-screen transform per drawer direction
38
+ const OFF_SCREEN = {
39
+ right: 'translateX(100%)',
40
+ left: 'translateX(-100%)',
41
+ top: 'translateY(-100%)',
42
+ bottom: 'translateY(100%)',
43
+ };
44
+
45
+ // Entrance: 400ms snappy (--duration-smooth, --ease-snappy)
46
+ const ENTER_DURATION = 400;
47
+ const ENTER_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
48
+
49
+ // Exit: 250ms ease-in (accelerating away — decisive, not lingering)
50
+ const EXIT_DURATION = 250;
51
+ const EXIT_EASING = 'cubic-bezier(0.4, 0, 1, 1)';
52
+
53
+ // Backdrop fade matches exit timing
54
+ const BACKDROP_ENTER_DURATION = 300;
55
+ const BACKDROP_EASING = 'ease-out';
56
+
57
+ export function defineCxDrawer(wasmFn, baseClass) {
58
+ class CxDrawer extends baseClass {
59
+ static observedAttributes = ['id', 'title', 'description', 'side', 'size', 'shape', 'close-button', 'open'];
60
+ static _booleanAttrs = new Set(['close-button', 'open']);
61
+
62
+ // Guard: prevents attributeChangedCallback re-entry when syncing
63
+ // the open attribute from imperative open()/close() calls.
64
+ #syncing = false;
65
+
66
+ connectedCallback() {
67
+ if (!this._isInitialized) {
68
+ this._markInitialized();
69
+ const shadow = this._shadow;
70
+
71
+ // Adopt static stylesheet (no keyframes — just ::backdrop override + z-index)
72
+ const sheet = getSheet();
73
+ if (!shadow.adoptedStyleSheets.includes(sheet)) {
74
+ shadow.adoptedStyleSheets = [...shadow.adoptedStyleSheets, sheet];
75
+ }
76
+
77
+ // Queue open for after first WASM render if the element has an
78
+ // explicit 'open' attribute (HTML) or React set the property.
79
+ this._autoOpen = this.hasAttribute('open');
80
+
81
+ // Close button + backdrop click
82
+ shadow.addEventListener('click', (e) => {
83
+ if (e.target.closest('[data-dialog-close]')) {
84
+ this.#closeDrawer();
85
+ return;
86
+ }
87
+ const dialog = shadow.querySelector('dialog[data-dialog]');
88
+ if (dialog && e.target === dialog) {
89
+ this.#closeDrawer();
90
+ }
91
+ });
92
+
93
+ shadow.addEventListener('keydown', (e) => {
94
+ if (e.key === 'Escape') this.#closeDrawer();
95
+ });
96
+
97
+ shadow.addEventListener('cancel', (e) => {
98
+ e.preventDefault();
99
+ this.#closeDrawer();
100
+ });
101
+ } // end _isInitialized guard
102
+ super.connectedCallback();
103
+ }
104
+
105
+ // Intercept 'open' attribute — toggle drawer without WASM re-render.
106
+ // All other attributes delegate to base class for props → WASM pipeline.
107
+ attributeChangedCallback(name, oldVal, newVal) {
108
+ if (name === 'open') {
109
+ if (this.#syncing) return;
110
+ if (newVal !== null) {
111
+ // open attribute added → open the drawer
112
+ this._autoOpen = true;
113
+ if (this._isInitialized) this.#openDrawer();
114
+ } else {
115
+ // open attribute removed → close the drawer
116
+ this._autoOpen = false;
117
+ // Guard: skip if already closed or mid-close — prevents double
118
+ // animation when internal close (X/backdrop/Escape) removes the
119
+ // attribute, then React's onClose removes it again.
120
+ const d = this._shadow?.querySelector('dialog[data-dialog]');
121
+ if (d?.open && !d.hasAttribute('data-closing')) {
122
+ this.#closeDrawer();
123
+ }
124
+ }
125
+ return;
126
+ }
127
+ super.attributeChangedCallback(name, oldVal, newVal);
128
+ }
129
+
130
+ // Property accessor for drawer open state (read-only).
131
+ // Named `isOpen` to avoid collision with the imperative `open()` method.
132
+ // Write path: use setAttribute('open', '') / removeAttribute('open'),
133
+ // or the imperative open()/close() methods.
134
+ get isOpen() {
135
+ const dialog = this._shadow?.querySelector('dialog[data-dialog]');
136
+ return dialog ? dialog.open : this.hasAttribute('open');
137
+ }
138
+
139
+ disconnectedCallback() {
140
+ this._unlockScroll();
141
+ super.disconnectedCallback();
142
+ }
143
+
144
+ // ─── Overlay Management ───
145
+
146
+ #ensureOverlay(dialog) {
147
+ if (dialog.querySelector('[data-cx-overlay]')) return;
148
+ const el = document.createElement('div');
149
+ el.setAttribute('data-cx-overlay', '');
150
+ Object.assign(el.style, {
151
+ position: 'fixed',
152
+ inset: '0',
153
+ backgroundColor: 'oklch(0 0 0 / 0.3)',
154
+ backdropFilter: 'blur(4px)',
155
+ WebkitBackdropFilter: 'blur(4px)',
156
+ pointerEvents: 'none',
157
+ zIndex: '0',
158
+ opacity: '0',
159
+ });
160
+ dialog.prepend(el);
161
+ }
162
+
163
+ // ─── Open ───
164
+
165
+ #openDrawer() {
166
+ const dialog = this._shadow.querySelector('dialog[data-dialog]');
167
+ if (!dialog || dialog.open) return;
168
+
169
+ // Clear _autoOpen — the open is now being fulfilled. Without this,
170
+ // _autoOpen stays true from attributeChangedCallback and the close
171
+ // cleanup misinterprets it as a rapid-toggle re-open request.
172
+ this._autoOpen = false;
173
+
174
+ // Sync attribute (guard prevents re-entry via attributeChangedCallback)
175
+ if (!this.hasAttribute('open')) {
176
+ this.#syncing = true;
177
+ this.setAttribute('open', '');
178
+ this.#syncing = false;
179
+ }
180
+
181
+ dialog.showModal();
182
+ this._lockScroll();
183
+
184
+ // rAF: dialog must be composited in the top layer before we animate.
185
+ // Without this, display:none → visible transition skips animations.
186
+ requestAnimationFrame(() => {
187
+ const panel = dialog.querySelector('[data-dialog-panel]');
188
+ const overlay = dialog.querySelector('[data-cx-overlay]');
189
+ const side = dialog.getAttribute('data-drawer') || 'right';
190
+ const offscreen = OFF_SCREEN[side] || OFF_SCREEN.right;
191
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
192
+
193
+ if (panel) {
194
+ panel.animate(
195
+ [{ transform: offscreen }, { transform: 'none' }],
196
+ { duration: reducedMotion ? 0 : ENTER_DURATION, easing: ENTER_EASING, fill: 'forwards' }
197
+ );
198
+ }
199
+
200
+ if (overlay) {
201
+ overlay.animate(
202
+ [{ opacity: 0 }, { opacity: 1 }],
203
+ { duration: reducedMotion ? 0 : BACKDROP_ENTER_DURATION, easing: BACKDROP_EASING, fill: 'forwards' }
204
+ );
205
+ }
206
+
207
+ dialog.setAttribute('data-open', '');
208
+ });
209
+ }
210
+
211
+ // ─── Close ───
212
+
213
+ #closeDrawer() {
214
+ const dialog = this._shadow.querySelector('dialog[data-dialog]');
215
+ if (!dialog || !dialog.open) return;
216
+ if (dialog.hasAttribute('data-closing')) return;
217
+
218
+ // Clear _autoOpen so cleanup doesn't misinterpret a stale true
219
+ // as a rapid-toggle request. Only a NEW setAttribute('open', '')
220
+ // during the close animation should set _autoOpen = true.
221
+ this._autoOpen = false;
222
+
223
+ dialog.setAttribute('data-closing', '');
224
+ dialog.removeAttribute('data-open');
225
+
226
+ // Sync attribute (guard prevents re-entry via attributeChangedCallback)
227
+ if (this.hasAttribute('open')) {
228
+ this.#syncing = true;
229
+ this.removeAttribute('open');
230
+ this.#syncing = false;
231
+ }
232
+
233
+ const panel = dialog.querySelector('[data-dialog-panel]');
234
+ const overlay = dialog.querySelector('[data-cx-overlay]');
235
+ const side = dialog.getAttribute('data-drawer') || 'right';
236
+ const offscreen = OFF_SCREEN[side] || OFF_SCREEN.right;
237
+ const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
238
+ const dur = reducedMotion ? 0 : EXIT_DURATION;
239
+
240
+ const cleanup = () => {
241
+ dialog.removeAttribute('data-closing');
242
+ // Pin final visual state as inline styles BEFORE dialog.close().
243
+ // dialog.close() removes the element from the top layer, which
244
+ // cancels all Web Animations (including fill: 'forwards').
245
+ // Without pinning, the panel snaps to its original position for
246
+ // one paint frame before the dialog fully hides.
247
+ if (panel) panel.style.transform = offscreen;
248
+ if (overlay) overlay.style.opacity = '0';
249
+ dialog.close();
250
+ // Clear pinned styles (dialog is now hidden, no visual impact)
251
+ if (panel) panel.style.transform = '';
252
+ if (overlay) overlay.style.opacity = '';
253
+ this._unlockScroll();
254
+ this._emit('cx-close', {});
255
+ // If open was re-requested during close animation (rapid toggle),
256
+ // re-open now that the dialog has fully closed.
257
+ if (this._autoOpen) {
258
+ this._autoOpen = false;
259
+ requestAnimationFrame(() => this.#openDrawer());
260
+ }
261
+ };
262
+
263
+ // Animate backdrop overlay fade-out (runs in parallel)
264
+ if (overlay) {
265
+ overlay.animate(
266
+ [{ opacity: 1 }, { opacity: 0 }],
267
+ { duration: dur, easing: EXIT_EASING, fill: 'forwards' }
268
+ );
269
+ }
270
+
271
+ // Animate panel slide-out — cleanup on finish
272
+ if (panel) {
273
+ const anim = panel.animate(
274
+ [{ transform: 'none' }, { transform: offscreen }],
275
+ { duration: dur, easing: EXIT_EASING, fill: 'forwards' }
276
+ );
277
+ anim.finished.then(cleanup).catch(cleanup);
278
+ } else {
279
+ cleanup();
280
+ }
281
+ }
282
+
283
+ // ── Public imperative API ──
284
+ open() { this.#openDrawer(); }
285
+ close() { this.#closeDrawer(); }
286
+
287
+ // ─── Render ───
288
+
289
+ _doRender() {
290
+ try {
291
+ this._props.slotted = true;
292
+ const result = wasmFn(this._props);
293
+ this._injectHtml(result);
294
+
295
+ // Re-adopt static sheet (may be stripped by base _scheduleRender)
296
+ const sheet = getSheet();
297
+ if (!this._shadow.adoptedStyleSheets.includes(sheet)) {
298
+ this._shadow.adoptedStyleSheets = [...this._shadow.adoptedStyleSheets, sheet];
299
+ }
300
+
301
+ // Inject backdrop overlay into dialog (destroyed by innerHTML, re-created here)
302
+ const dialog = this._shadow.querySelector('dialog[data-dialog]');
303
+ if (dialog) this.#ensureOverlay(dialog);
304
+
305
+ // Auto-open after first render
306
+ if (this._autoOpen) {
307
+ this._autoOpen = false;
308
+ requestAnimationFrame(() => this.#openDrawer());
309
+ }
310
+ } catch (e) {
311
+ console.error('[cx-drawer]', e);
312
+ }
313
+ }
314
+ }
315
+
316
+ customElements.define('cx-drawer', CxDrawer);
317
+ return CxDrawer;
318
+ }
@@ -0,0 +1,22 @@
1
+ // Auto-generated by scripts/generate-elements.mjs — DO NOT EDIT
2
+ // Source: crates/wasm-api/src/fab.rs
3
+
4
+ export interface CxFabAttributes {
5
+ icon?: string;
6
+ label?: string;
7
+ ariaLabel?: string;
8
+ variant?: 'filled' | 'outline' | 'ghost';
9
+ intent?: 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
10
+ shape?: 'rounded' | 'pill';
11
+ size?: 'sm' | 'md' | 'lg';
12
+ disabled?: boolean;
13
+ hasPopup?: boolean;
14
+ expanded?: boolean;
15
+ controls?: string;
16
+ }
17
+
18
+ declare global {
19
+ interface HTMLElementTagNameMap {
20
+ 'cx-fab': HTMLElement & CxFabAttributes;
21
+ }
22
+ }