@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,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
|
+
}
|