@vmz/ui 0.3.6 → 0.3.8

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 (68) hide show
  1. package/README.md +53 -51
  2. package/contracts/token-requirements.v0.json +58 -1
  3. package/package.json +5 -2
  4. package/presets/web-surface.v0.json +14 -12
  5. package/src/components/Accordion.vmz +127 -94
  6. package/src/components/Alert.vmz +125 -28
  7. package/src/components/AppShell.vmz +125 -57
  8. package/src/components/Autocomplete.vmz +261 -154
  9. package/src/components/Avatar.vmz +20 -12
  10. package/src/components/Badge.vmz +62 -70
  11. package/src/components/Breadcrumb.vmz +86 -72
  12. package/src/components/BulkActions.vmz +56 -51
  13. package/src/components/Button.vmz +237 -211
  14. package/src/components/Callout.vmz +178 -77
  15. package/src/components/Card.vmz +141 -120
  16. package/src/components/Checkbox.vmz +131 -55
  17. package/src/components/CodeBlock.vmz +77 -30
  18. package/src/components/Collapse.vmz +64 -11
  19. package/src/components/ConsoleShell.vmz +405 -251
  20. package/src/components/Content.vmz +15 -1
  21. package/src/components/DataTable.vmz +294 -194
  22. package/src/components/DatePicker.vmz +766 -674
  23. package/src/components/Descriptions.vmz +90 -0
  24. package/src/components/Dialog.vmz +137 -38
  25. package/src/components/Divider.vmz +8 -1
  26. package/src/components/Drawer.vmz +518 -427
  27. package/src/components/Dropdown.vmz +195 -51
  28. package/src/components/Empty.vmz +96 -54
  29. package/src/components/Field.vmz +170 -115
  30. package/src/components/FilterBar.vmz +34 -4
  31. package/src/components/Flex.vmz +3 -1
  32. package/src/components/Footer.vmz +6 -3
  33. package/src/components/Form.vmz +41 -0
  34. package/src/components/FormItem.vmz +78 -79
  35. package/src/components/Grid.vmz +6 -3
  36. package/src/components/Header.vmz +193 -6
  37. package/src/components/Layout.vmz +3 -1
  38. package/src/components/Link.vmz +25 -10
  39. package/src/components/List.vmz +124 -102
  40. package/src/components/Menu.vmz +416 -243
  41. package/src/components/Message.vmz +167 -0
  42. package/src/components/Notification.vmz +132 -37
  43. package/src/components/Pagination.vmz +237 -69
  44. package/src/components/Popconfirm.vmz +287 -0
  45. package/src/components/Popover.vmz +341 -223
  46. package/src/components/Progress.vmz +16 -5
  47. package/src/components/Prose.vmz +180 -81
  48. package/src/components/QueryForm.vmz +31 -9
  49. package/src/components/RadioGroup.vmz +204 -124
  50. package/src/components/Result.vmz +60 -27
  51. package/src/components/Segmented.vmz +86 -51
  52. package/src/components/Select.vmz +620 -533
  53. package/src/components/Sider.vmz +120 -7
  54. package/src/components/Skeleton.vmz +9 -7
  55. package/src/components/Space.vmz +9 -3
  56. package/src/components/Spinner.vmz +5 -4
  57. package/src/components/Steps.vmz +118 -103
  58. package/src/components/Switch.vmz +131 -98
  59. package/src/components/Table.vmz +270 -165
  60. package/src/components/Tabs.vmz +125 -110
  61. package/src/components/Tag.vmz +60 -43
  62. package/src/components/TextArea.vmz +125 -118
  63. package/src/components/Timeline.vmz +111 -76
  64. package/src/components/Toc.vmz +90 -74
  65. package/src/components/Tooltip.vmz +107 -58
  66. package/src/components/Tree.vmz +214 -191
  67. package/src/components/Typography.vmz +23 -8
  68. package/src/components/Upload.vmz +1394 -1289
@@ -1,427 +1,518 @@
1
- <template>
2
- <div data-vmz-ui="drawer" :data-vmz-overlay-open="open" class="vmz-ui-drawer-root">
3
- <div v-if="open" data-vmz-overlay="drawer" data-vmz-overlay-owner="drawer" :data-vmz-overlay-stack="stackLevel" class="vmz-ui-drawer">
4
- <button type="button" data-vmz-overlay-layer="backdrop" aria-label="Dismiss drawer" @click="dismiss" class="vmz-ui-drawer__backdrop" />
5
- <div role="dialog" aria-modal="true" :aria-labelledby="titleId" tabindex="-1" data-vmz-overlay-layer="panel" data-vmz-focus="enter" data-vmz-motion="overlay-enter" data-vmz-motion-token="motion.overlay" :onKeyDown="onPanelKeyDown" class="vmz-ui-drawer__panel">
6
- <h2 :id="titleId" class="vmz-ui-drawer__title">
7
- {{ title }}
8
- </h2>
9
- <div class="vmz-ui-drawer__body">
10
- <slot />
11
- </div>
12
- <div class="vmz-ui-drawer__actions">
13
- <button type="button" data-vmz-focus="close" @click="dismiss" class="vmz-ui-drawer__close">
14
- Close
15
- </button>
16
- </div>
17
- </div>
18
- </div>
19
- </div>
20
- </template>
21
- <style>
22
- .vmz-ui-drawer-root {
23
- display: block;
24
- }
25
-
26
- .vmz-ui-drawer {
27
- position: fixed;
28
- inset: 0;
29
- z-index: 40;
30
- }
31
-
32
- .vmz-ui-drawer[data-vmz-overlay-stack="1"] {
33
- z-index: 50;
34
- }
35
-
36
- .vmz-ui-drawer[data-vmz-overlay-stack="2"] {
37
- z-index: 60;
38
- }
39
-
40
- .vmz-ui-drawer[data-vmz-overlay-stack="3"] {
41
- z-index: 70;
42
- }
43
-
44
- .vmz-ui-drawer__backdrop {
45
- position: absolute;
46
- inset: 0;
47
- border: 0;
48
- padding: 0;
49
- margin: 0;
50
- cursor: pointer;
51
- background: rgba(0, 0, 0, 0.45);
52
- animation: vmz-ui-drawer-fade-in var(--vmz-motion-overlay-duration)
53
- var(--vmz-motion-overlay-easing);
54
- }
55
-
56
- .vmz-ui-drawer__panel {
57
- position: absolute;
58
- top: 0;
59
- right: 0;
60
- z-index: 1;
61
- box-sizing: border-box;
62
- width: min(23.75rem, 100vw);
63
- height: 100%;
64
- padding: 1.25rem 1.5rem;
65
- border-left: 1px solid var(--vmz-line-subtle, var(--vmz-line-default));
66
- background: var(--vmz-surface-paper, #fff);
67
- color: var(--vmz-text-ink);
68
- box-shadow: var(--vmz-shadow-popup);
69
- outline: none;
70
- display: flex;
71
- flex-direction: column;
72
- animation: vmz-ui-drawer-panel-in var(--vmz-motion-overlay-duration)
73
- var(--vmz-motion-overlay-easing);
74
- }
75
-
76
- .vmz-ui-drawer[data-vmz-motion-adopt="true"] .vmz-ui-drawer__backdrop,
77
- .vmz-ui-drawer[data-vmz-motion-adopt="true"] .vmz-ui-drawer__panel {
78
- animation: none;
79
- }
80
-
81
- .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__backdrop {
82
- animation: vmz-ui-drawer-fade-out var(--vmz-motion-overlay-duration)
83
- var(--vmz-motion-overlay-easing);
84
- }
85
-
86
- .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__panel {
87
- animation: vmz-ui-drawer-panel-out var(--vmz-motion-overlay-duration)
88
- var(--vmz-motion-overlay-easing);
89
- }
90
-
91
- .vmz-ui-drawer__panel:focus-visible {
92
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
93
- }
94
-
95
- .vmz-ui-drawer__title {
96
- margin: 0 0 0.75rem;
97
- font-size: 1rem;
98
- font-weight: 600;
99
- color: var(--vmz-text-ink);
100
- }
101
-
102
- .vmz-ui-drawer__body {
103
- flex: 1;
104
- margin-bottom: 1rem;
105
- overflow: auto;
106
- color: var(--vmz-text-ink);
107
- }
108
-
109
- .vmz-ui-drawer__close {
110
- font: inherit;
111
- font-size: 0.875rem;
112
- font-weight: 400;
113
- min-height: var(--vmz-control-height, 2rem);
114
- padding: 0.25rem 1rem;
115
- border: 1px solid var(--vmz-line-default);
116
- border-radius: var(--vmz-radius-md, 6px);
117
- cursor: pointer;
118
- background: var(--vmz-surface-paper, #fff);
119
- color: var(--vmz-text-ink);
120
- }
121
-
122
- .vmz-ui-drawer__close:hover {
123
- color: var(--vmz-action-primary-background);
124
- border-color: var(--vmz-action-primary-background);
125
- }
126
-
127
- .vmz-ui-drawer__close:focus-visible {
128
- box-shadow: 0 0 0 3px var(--vmz-focus-ring);
129
- }
130
-
131
- @keyframes vmz-ui-drawer-fade-in {
132
- from {
133
- opacity: 0;
134
- }
135
- to {
136
- opacity: 1;
137
- }
138
- }
139
-
140
- @keyframes vmz-ui-drawer-panel-in {
141
- from {
142
- opacity: 0;
143
- transform: translateX(0.75rem);
144
- }
145
- to {
146
- opacity: 1;
147
- transform: none;
148
- }
149
- }
150
-
151
- @keyframes vmz-ui-drawer-fade-out {
152
- from {
153
- opacity: 1;
154
- }
155
- to {
156
- opacity: 0;
157
- }
158
- }
159
-
160
- @keyframes vmz-ui-drawer-panel-out {
161
- from {
162
- opacity: 1;
163
- transform: none;
164
- }
165
- to {
166
- opacity: 0;
167
- transform: translateX(0.75rem);
168
- }
169
- }
170
-
171
- @media (prefers-reduced-motion: reduce) {
172
- .vmz-ui-drawer__backdrop,
173
- .vmz-ui-drawer__panel,
174
- .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__backdrop,
175
- .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__panel {
176
- animation: none;
177
- }
178
- }
179
- </style>
180
- <script client>
181
- /**
182
- * VMZ UI — Drawer (UI2).
183
- * Same overlay ownership / focus / Escape / stack as Dialog.
184
- * SSR/resume adopt skips enter replay; dismiss plays overlay-exit then calls onClose.
185
- */
186
- export default class Drawer {
187
- public open: boolean = false;
188
- public title: string = "Drawer";
189
- public titleId: string = "vmz-drawer-title";
190
- public stackLevel: string = "0";
191
- public onClose: (() => void) | null = null;
192
- _restoreFocus = null;
193
- _panel = null;
194
- _docKeyHandler = null;
195
- _observer = null;
196
- _adoptNextEnter = false;
197
- _exiting = false;
198
- _exitTimer = null;
199
- /** Motion generation — exit timer and cancel edges key off this owner counter. */
200
- _motionGen = 0;
201
- async onMount() {
202
- if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
203
- const root = this.__vmzDomRoot;
204
- if (!root || typeof root.querySelector !== "function") return;
205
- if (root.querySelector('[data-vmz-overlay="drawer"]')) this._adoptNextEnter = true;
206
- this._observer = new MutationObserver(() => this._syncFromDom());
207
- this._observer.observe(root, {
208
- childList: true,
209
- subtree: true,
210
- });
211
- this._syncFromDom();
212
- }
213
- onDestroy() {
214
- if (this._exitTimer) {
215
- clearTimeout(this._exitTimer);
216
- this._exitTimer = null;
217
- }
218
- this._exiting = false;
219
- if (this._observer) {
220
- this._observer.disconnect();
221
- this._observer = null;
222
- }
223
- this._teardownDocKey();
224
- this._panel = null;
225
- this._restoreFocus = null;
226
- }
227
- dismiss() {
228
- if (!this._isTopmostStack()) return;
229
- if (this._exiting) {
230
- this._cancelExit("reverse");
231
- return;
232
- }
233
- const panel = this._panel;
234
- const overlay =
235
- panel && typeof panel.closest === "function"
236
- ? panel.closest('[data-vmz-overlay="drawer"]')
237
- : null;
238
- const reduce =
239
- typeof window !== "undefined" &&
240
- typeof window.matchMedia === "function" &&
241
- window.matchMedia("(prefers-reduced-motion: reduce)").matches;
242
- if (panel && overlay && !reduce) {
243
- this._exiting = true;
244
- const exitGen = this._motionGen;
245
- overlay.setAttribute("data-vmz-motion-phase", "exit");
246
- overlay.removeAttribute("data-vmz-motion-cancelled");
247
- panel.setAttribute("data-vmz-motion", "overlay-exit");
248
- const ms = this._overlayDurationMs(panel);
249
- this._exitTimer = setTimeout(() => {
250
- this._exitTimer = null;
251
- if (!this._exiting || exitGen !== this._motionGen) return;
252
- this._exiting = false;
253
- this.emit("close");
254
- if (typeof this.onClose === "function") this.onClose();
255
- }, ms);
256
- return;
257
- }
258
- this.emit("close");
259
- if (typeof this.onClose === "function") this.onClose();
260
- }
261
- _cancelExit(reason) {
262
- if (this._exitTimer) {
263
- clearTimeout(this._exitTimer);
264
- this._exitTimer = null;
265
- }
266
- this._exiting = false;
267
- this._motionGen += 1;
268
- const panel = this._panel;
269
- const overlay =
270
- panel && typeof panel.closest === "function"
271
- ? panel.closest('[data-vmz-overlay="drawer"]')
272
- : null;
273
- if (overlay) {
274
- overlay.removeAttribute("data-vmz-motion-phase");
275
- overlay.setAttribute("data-vmz-motion-cancelled", reason || "true");
276
- overlay.setAttribute("data-vmz-motion-gen", String(this._motionGen));
277
- }
278
- if (panel) {
279
- panel.setAttribute("data-vmz-motion", "overlay-enter");
280
- }
281
- }
282
- _overlayDurationMs(panel) {
283
- try {
284
- const raw =
285
- (
286
- getComputedStyle(panel).getPropertyValue("--vmz-motion-overlay-duration") || ""
287
- ).trim() || "180ms";
288
- if (raw.endsWith("ms")) return Math.max(0, Number.parseFloat(raw) || 180);
289
- if (raw.endsWith("s")) return Math.max(0, (Number.parseFloat(raw) || 0.18) * 1e3);
290
- return 180;
291
- } catch {
292
- return 180;
293
- }
294
- }
295
- _isTopmostStack() {
296
- if (typeof document === "undefined") return true;
297
- const root = this.__vmzDomRoot;
298
- const mine =
299
- root && typeof root.querySelector === "function"
300
- ? root.querySelector("[data-vmz-overlay][data-vmz-overlay-stack]")
301
- : null;
302
- if (!mine) return true;
303
- const myStack = Number(
304
- mine.getAttribute("data-vmz-overlay-stack") || this.stackLevel || "0",
305
- );
306
- const open = [...document.querySelectorAll("[data-vmz-overlay][data-vmz-overlay-stack]")];
307
- let max = -Infinity;
308
- for (const el of open) {
309
- const n = Number(el.getAttribute("data-vmz-overlay-stack") || "0");
310
- if (Number.isFinite(n) && n > max) max = n;
311
- }
312
- if (!Number.isFinite(max)) return true;
313
- if ((Number.isFinite(myStack) ? myStack : 0) < max) return false;
314
- if ((Number.isFinite(myStack) ? myStack : 0) > max) return true;
315
- const same = open.filter(
316
- (el) => Number(el.getAttribute("data-vmz-overlay-stack") || "0") === max,
317
- );
318
- return same[same.length - 1] === mine;
319
- }
320
- onPanelKeyDown(ev) {
321
- if (!ev || ev.key !== "Tab") return;
322
- const panel = this._panel;
323
- if (!panel) return;
324
- const nodes = panel.querySelectorAll(
325
- 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])',
326
- );
327
- const list = [];
328
- for (const n of nodes) {
329
- if (n instanceof HTMLElement && n.offsetParent !== null) list.push(n);
330
- }
331
- if (panel.tabIndex >= 0) list.unshift(panel);
332
- if (!list.length) {
333
- ev.preventDefault();
334
- panel.focus();
335
- return;
336
- }
337
- const first = list[0];
338
- const last = list[list.length - 1];
339
- const active = document.activeElement;
340
- if (ev.shiftKey) {
341
- if (active === first || active === panel) {
342
- ev.preventDefault();
343
- last.focus();
344
- }
345
- } else if (active === last) {
346
- ev.preventDefault();
347
- first.focus();
348
- }
349
- }
350
- _syncFromDom() {
351
- const root = this.__vmzDomRoot;
352
- const panel =
353
- root && typeof root.querySelector === "function"
354
- ? root.querySelector('[data-vmz-overlay="drawer"] [data-vmz-focus="enter"]')
355
- : null;
356
- if (panel && !this._panel) this._enterFocus(panel);
357
- else if (!panel && this._panel) this._exitFocus();
358
- }
359
- _enterFocus(panel) {
360
- if (typeof document === "undefined") return;
361
- const active = document.activeElement;
362
- if (!this._restoreFocus) {
363
- const root = this.__vmzDomRoot;
364
- const outside =
365
- active instanceof HTMLElement &&
366
- active !== document.body &&
367
- !(root && typeof root.contains === "function" && root.contains(active));
368
- if (outside) this._restoreFocus = active;
369
- }
370
- this._panel = panel instanceof HTMLElement ? panel : null;
371
- const overlay =
372
- this._panel && typeof this._panel.closest === "function"
373
- ? this._panel.closest('[data-vmz-overlay="drawer"]')
374
- : null;
375
- if (this._exiting) {
376
- this._cancelExit("reenter");
377
- } else {
378
- this._motionGen += 1;
379
- }
380
- if (overlay) {
381
- overlay.setAttribute("data-vmz-motion-gen", String(this._motionGen));
382
- }
383
- if (this._adoptNextEnter && overlay) {
384
- overlay.setAttribute("data-vmz-motion-adopt", "true");
385
- this._panel.setAttribute("data-vmz-motion", "overlay-stable");
386
- this._adoptNextEnter = false;
387
- } else if (overlay) {
388
- overlay.removeAttribute("data-vmz-motion-adopt");
389
- overlay.removeAttribute("data-vmz-motion-phase");
390
- overlay.removeAttribute("data-vmz-motion-cancelled");
391
- this._panel.setAttribute("data-vmz-motion", "overlay-enter");
392
- }
393
- if (this._panel && typeof this._panel.focus === "function") this._panel.focus();
394
- if (!this._docKeyHandler) {
395
- this._docKeyHandler = (ev) => {
396
- if (ev.key !== "Escape") return;
397
- if (ev.defaultPrevented) return;
398
- if (!this._isTopmostStack()) return;
399
- ev.preventDefault();
400
- ev.stopPropagation();
401
- this.dismiss();
402
- };
403
- document.addEventListener("keydown", this._docKeyHandler);
404
- }
405
- }
406
- _exitFocus() {
407
- if (this._exitTimer) {
408
- clearTimeout(this._exitTimer);
409
- this._exitTimer = null;
410
- }
411
- this._exiting = false;
412
- this._teardownDocKey();
413
- this._panel = null;
414
- const restore = this._restoreFocus;
415
- this._restoreFocus = null;
416
- if (restore && typeof restore.focus === "function" && document.contains(restore)) {
417
- restore.focus();
418
- }
419
- }
420
- _teardownDocKey() {
421
- if (typeof document !== "undefined" && this._docKeyHandler) {
422
- document.removeEventListener("keydown", this._docKeyHandler);
423
- }
424
- this._docKeyHandler = null;
425
- }
426
- }
427
- </script>
1
+ <template>
2
+ <div data-vmz-ui="drawer" :data-vmz-overlay-open="open" class="vmz-ui-drawer-root">
3
+ <div v-if="open" data-vmz-overlay="drawer" data-vmz-overlay-owner="drawer" :data-vmz-overlay-stack="stackLevel" :data-size="size" class="vmz-ui-drawer">
4
+ <button type="button" data-vmz-overlay-layer="backdrop" :aria-label="dismissLabel || closeLabel || title" @click="dismiss" class="vmz-ui-drawer__backdrop" />
5
+ <div role="dialog" aria-modal="true" :aria-labelledby="title ? titleId : null" tabindex="-1" data-vmz-overlay-layer="panel" data-vmz-focus="enter" data-vmz-motion="overlay-enter" data-vmz-motion-token="motion.overlay" :onKeyDown="onPanelKeyDown" class="vmz-ui-drawer__panel">
6
+ <header class="vmz-ui-drawer__header">
7
+ <div class="vmz-ui-drawer__heading">
8
+ <h2 v-if="title" :id="titleId" class="vmz-ui-drawer__title">
9
+ {{ title }}
10
+ </h2>
11
+ <p v-if="description" class="vmz-ui-drawer__description">
12
+ {{ description }}
13
+ </p>
14
+ </div>
15
+ <button type="button" data-vmz-focus="close" :aria-label="closeLabel || title" @click="dismiss" class="vmz-ui-drawer__close">
16
+ <Icon name="action.close" size="md" decorative />
17
+ </button>
18
+ </header>
19
+ <div class="vmz-ui-drawer__body">
20
+ <slot />
21
+ </div>
22
+ <footer class="vmz-ui-drawer__footer">
23
+ <slot name="footer" />
24
+ </footer>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </template>
29
+ <style>
30
+ .vmz-ui-drawer-root {
31
+ display: block;
32
+ }
33
+
34
+ .vmz-ui-drawer {
35
+ position: fixed;
36
+ inset: 0;
37
+ z-index: 40;
38
+ }
39
+
40
+ .vmz-ui-drawer[data-vmz-overlay-stack="1"] {
41
+ z-index: 50;
42
+ }
43
+
44
+ .vmz-ui-drawer[data-vmz-overlay-stack="2"] {
45
+ z-index: 60;
46
+ }
47
+
48
+ .vmz-ui-drawer[data-vmz-overlay-stack="3"] {
49
+ z-index: 70;
50
+ }
51
+
52
+ .vmz-ui-drawer__backdrop {
53
+ position: absolute;
54
+ inset: 0;
55
+ border: 0;
56
+ padding: 0;
57
+ margin: 0;
58
+ cursor: pointer;
59
+ background: color-mix(in srgb, var(--vmz-surface-ink) 45%, transparent);
60
+ animation: vmz-ui-drawer-fade-in var(--vmz-motion-overlay-duration)
61
+ var(--vmz-motion-overlay-easing);
62
+ }
63
+
64
+ .vmz-ui-drawer__panel {
65
+ position: absolute;
66
+ top: 0;
67
+ right: 0;
68
+ z-index: 1;
69
+ box-sizing: border-box;
70
+ display: flex;
71
+ flex-direction: column;
72
+ width: min(420px, 100vw);
73
+ height: 100%;
74
+ border-left: 1px solid var(--vmz-line-subtle);
75
+ background: var(--vmz-surface-paper);
76
+ color: var(--vmz-text-ink);
77
+ box-shadow: var(--vmz-shadow-popup);
78
+ outline: none;
79
+ animation: vmz-ui-drawer-panel-in var(--vmz-motion-overlay-duration)
80
+ var(--vmz-motion-overlay-easing);
81
+ }
82
+
83
+ .vmz-ui-drawer[data-size="sm"] .vmz-ui-drawer__panel {
84
+ width: min(320px, 100vw);
85
+ }
86
+
87
+ .vmz-ui-drawer[data-size="lg"] .vmz-ui-drawer__panel {
88
+ width: min(560px, 100vw);
89
+ }
90
+
91
+ .vmz-ui-drawer[data-vmz-motion-adopt="true"] .vmz-ui-drawer__backdrop,
92
+ .vmz-ui-drawer[data-vmz-motion-adopt="true"] .vmz-ui-drawer__panel {
93
+ animation: none;
94
+ }
95
+
96
+ .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__backdrop {
97
+ animation: vmz-ui-drawer-fade-out var(--vmz-motion-overlay-duration)
98
+ var(--vmz-motion-overlay-easing);
99
+ }
100
+
101
+ .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__panel {
102
+ animation: vmz-ui-drawer-panel-out var(--vmz-motion-overlay-duration)
103
+ var(--vmz-motion-overlay-easing);
104
+ }
105
+
106
+ .vmz-ui-drawer__panel:focus-visible {
107
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring), var(--vmz-shadow-popup);
108
+ }
109
+
110
+ .vmz-ui-drawer__header {
111
+ display: flex;
112
+ align-items: flex-start;
113
+ gap: 12px;
114
+ padding: 16px 20px;
115
+ border-bottom: 1px solid var(--vmz-line-subtle);
116
+ }
117
+
118
+ .vmz-ui-drawer__heading {
119
+ flex: 1 1 auto;
120
+ min-width: 0;
121
+ }
122
+
123
+ .vmz-ui-drawer__title {
124
+ margin: 0;
125
+ font-size: 16px;
126
+ font-weight: 600;
127
+ line-height: 24px;
128
+ color: var(--vmz-text-ink);
129
+ }
130
+
131
+ .vmz-ui-drawer__description {
132
+ margin: 4px 0 0;
133
+ font-size: 14px;
134
+ line-height: 22px;
135
+ color: var(--vmz-text-tertiary);
136
+ }
137
+
138
+ .vmz-ui-drawer__close {
139
+ flex: 0 0 auto;
140
+ display: inline-flex;
141
+ align-items: center;
142
+ justify-content: center;
143
+ width: 32px;
144
+ height: 32px;
145
+ margin: -4px -8px -4px 0;
146
+ padding: 0;
147
+ border: 0;
148
+ border-radius: var(--vmz-radius-md);
149
+ background: transparent;
150
+ color: var(--vmz-text-steel);
151
+ cursor: pointer;
152
+ outline: none;
153
+ transition:
154
+ color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing),
155
+ background-color var(--vmz-motion-control-duration) var(--vmz-motion-control-easing);
156
+ }
157
+
158
+ .vmz-ui-drawer__close:hover {
159
+ background: var(--vmz-surface-mist);
160
+ color: var(--vmz-text-ink);
161
+ }
162
+
163
+ .vmz-ui-drawer__close:focus-visible {
164
+ box-shadow: 0 0 0 3px var(--vmz-focus-ring);
165
+ }
166
+
167
+ .vmz-ui-drawer__body {
168
+ flex: 1 1 auto;
169
+ min-height: 0;
170
+ padding: 16px 20px;
171
+ overflow: auto;
172
+ font-size: 14px;
173
+ line-height: 22px;
174
+ color: var(--vmz-text-ink);
175
+ }
176
+
177
+ .vmz-ui-drawer__body :where(a) {
178
+ color: var(--vmz-action-primary-background);
179
+ text-underline-offset: 2px;
180
+ }
181
+
182
+ .vmz-ui-drawer__body :where(a):hover {
183
+ color: var(--vmz-action-primary-hover);
184
+ }
185
+
186
+ .vmz-ui-drawer__footer {
187
+ display: flex;
188
+ flex-wrap: wrap;
189
+ align-items: center;
190
+ justify-content: flex-end;
191
+ gap: 8px;
192
+ padding: 12px 20px;
193
+ border-top: 1px solid var(--vmz-line-subtle);
194
+ }
195
+
196
+ .vmz-ui-drawer__footer:empty {
197
+ display: none;
198
+ }
199
+
200
+ .vmz-ui-drawer__footer :where(button, a) {
201
+ min-height: var(--vmz-control-height, 32px);
202
+ }
203
+
204
+ @keyframes vmz-ui-drawer-fade-in {
205
+ from {
206
+ opacity: 0;
207
+ }
208
+ to {
209
+ opacity: 1;
210
+ }
211
+ }
212
+
213
+ @keyframes vmz-ui-drawer-panel-in {
214
+ from {
215
+ opacity: 0;
216
+ transform: translateX(16px);
217
+ }
218
+ to {
219
+ opacity: 1;
220
+ transform: none;
221
+ }
222
+ }
223
+
224
+ @keyframes vmz-ui-drawer-fade-out {
225
+ from {
226
+ opacity: 1;
227
+ }
228
+ to {
229
+ opacity: 0;
230
+ }
231
+ }
232
+
233
+ @keyframes vmz-ui-drawer-panel-out {
234
+ from {
235
+ opacity: 1;
236
+ transform: none;
237
+ }
238
+ to {
239
+ opacity: 0;
240
+ transform: translateX(16px);
241
+ }
242
+ }
243
+
244
+ @media (max-width: 720px) {
245
+ .vmz-ui-drawer__panel {
246
+ width: 100vw;
247
+ border-left: 0;
248
+ }
249
+ }
250
+
251
+ @media (prefers-reduced-motion: reduce) {
252
+ .vmz-ui-drawer__backdrop,
253
+ .vmz-ui-drawer__panel,
254
+ .vmz-ui-drawer__close,
255
+ .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__backdrop,
256
+ .vmz-ui-drawer[data-vmz-motion-phase="exit"] .vmz-ui-drawer__panel {
257
+ animation: none;
258
+ transition: none;
259
+ }
260
+ }
261
+ </style>
262
+ <script client>
263
+ /**
264
+ * VMZ UI — Drawer (UI2).
265
+ * Same overlay ownership / focus / Escape / stack as Dialog.
266
+ * SSR/resume adopt skips enter replay; dismiss plays overlay-exit then calls onClose.
267
+ *
268
+ * Appearance props: `size` (sm 320 / md 420 / lg 560), `description`.
269
+ * Visible copy is author-supplied; `closeLabel` / `dismissLabel` only name the
270
+ * icon button and backdrop for assistive tech.
271
+ */
272
+ export default class Drawer {
273
+ public open: boolean = false;
274
+ public title: string = "";
275
+ public description: string = "";
276
+ /** sm | md | lg */
277
+ public size: string = "md";
278
+ public closeLabel: string = "";
279
+ public dismissLabel: string = "";
280
+ public titleId: string = "vmz-drawer-title";
281
+ public stackLevel: string = "0";
282
+ public onClose: (() => void) | null = null;
283
+ _restoreFocus = null;
284
+ _panel = null;
285
+ _docKeyHandler = null;
286
+ _observer = null;
287
+ _adoptNextEnter = false;
288
+ _exiting = false;
289
+ _exitTimer = null;
290
+ /** Motion generation — exit timer and cancel edges key off this owner counter. */
291
+ _motionGen = 0;
292
+ async onMount() {
293
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
294
+ const root = this.__vmzDomRoot;
295
+ if (!root || typeof root.querySelector !== "function") return;
296
+ if (root.querySelector('[data-vmz-overlay="drawer"]')) this._adoptNextEnter = true;
297
+ this._observer = new MutationObserver(() => this._syncFromDom());
298
+ this._observer.observe(root, {
299
+ childList: true,
300
+ subtree: true,
301
+ });
302
+ this._syncFromDom();
303
+ }
304
+ onDestroy() {
305
+ if (this._exitTimer) {
306
+ clearTimeout(this._exitTimer);
307
+ this._exitTimer = null;
308
+ }
309
+ this._exiting = false;
310
+ if (this._observer) {
311
+ this._observer.disconnect();
312
+ this._observer = null;
313
+ }
314
+ this._teardownDocKey();
315
+ this._panel = null;
316
+ this._restoreFocus = null;
317
+ }
318
+ dismiss() {
319
+ if (!this._isTopmostStack()) return;
320
+ if (this._exiting) {
321
+ this._cancelExit("reverse");
322
+ return;
323
+ }
324
+ const panel = this._panel;
325
+ const overlay =
326
+ panel && typeof panel.closest === "function"
327
+ ? panel.closest('[data-vmz-overlay="drawer"]')
328
+ : null;
329
+ const reduce =
330
+ typeof window !== "undefined" &&
331
+ typeof window.matchMedia === "function" &&
332
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
333
+ if (panel && overlay && !reduce) {
334
+ this._exiting = true;
335
+ const exitGen = this._motionGen;
336
+ overlay.setAttribute("data-vmz-motion-phase", "exit");
337
+ overlay.removeAttribute("data-vmz-motion-cancelled");
338
+ panel.setAttribute("data-vmz-motion", "overlay-exit");
339
+ const ms = this._overlayDurationMs(panel);
340
+ this._exitTimer = setTimeout(() => {
341
+ this._exitTimer = null;
342
+ if (!this._exiting || exitGen !== this._motionGen) return;
343
+ this._exiting = false;
344
+ this.emit("close");
345
+ if (typeof this.onClose === "function") this.onClose();
346
+ }, ms);
347
+ return;
348
+ }
349
+ this.emit("close");
350
+ if (typeof this.onClose === "function") this.onClose();
351
+ }
352
+ _cancelExit(reason) {
353
+ if (this._exitTimer) {
354
+ clearTimeout(this._exitTimer);
355
+ this._exitTimer = null;
356
+ }
357
+ this._exiting = false;
358
+ this._motionGen += 1;
359
+ const panel = this._panel;
360
+ const overlay =
361
+ panel && typeof panel.closest === "function"
362
+ ? panel.closest('[data-vmz-overlay="drawer"]')
363
+ : null;
364
+ if (overlay) {
365
+ overlay.removeAttribute("data-vmz-motion-phase");
366
+ overlay.setAttribute("data-vmz-motion-cancelled", reason || "true");
367
+ overlay.setAttribute("data-vmz-motion-gen", String(this._motionGen));
368
+ }
369
+ if (panel) {
370
+ panel.setAttribute("data-vmz-motion", "overlay-enter");
371
+ }
372
+ }
373
+ _overlayDurationMs(panel) {
374
+ try {
375
+ const raw =
376
+ (
377
+ getComputedStyle(panel).getPropertyValue("--vmz-motion-overlay-duration") || ""
378
+ ).trim() || "180ms";
379
+ if (raw.endsWith("ms")) return Math.max(0, Number.parseFloat(raw) || 180);
380
+ if (raw.endsWith("s")) return Math.max(0, (Number.parseFloat(raw) || 0.18) * 1e3);
381
+ return 180;
382
+ } catch {
383
+ return 180;
384
+ }
385
+ }
386
+ _isTopmostStack() {
387
+ if (typeof document === "undefined") return true;
388
+ const root = this.__vmzDomRoot;
389
+ const mine =
390
+ root && typeof root.querySelector === "function"
391
+ ? root.querySelector("[data-vmz-overlay][data-vmz-overlay-stack]")
392
+ : null;
393
+ if (!mine) return true;
394
+ const myStack = Number(
395
+ mine.getAttribute("data-vmz-overlay-stack") || this.stackLevel || "0",
396
+ );
397
+ const open = [...document.querySelectorAll("[data-vmz-overlay][data-vmz-overlay-stack]")];
398
+ let max = -Infinity;
399
+ for (const el of open) {
400
+ const n = Number(el.getAttribute("data-vmz-overlay-stack") || "0");
401
+ if (Number.isFinite(n) && n > max) max = n;
402
+ }
403
+ if (!Number.isFinite(max)) return true;
404
+ if ((Number.isFinite(myStack) ? myStack : 0) < max) return false;
405
+ if ((Number.isFinite(myStack) ? myStack : 0) > max) return true;
406
+ const same = open.filter(
407
+ (el) => Number(el.getAttribute("data-vmz-overlay-stack") || "0") === max,
408
+ );
409
+ return same[same.length - 1] === mine;
410
+ }
411
+ onPanelKeyDown(ev) {
412
+ if (!ev || ev.key !== "Tab") return;
413
+ const panel = this._panel;
414
+ if (!panel) return;
415
+ const nodes = panel.querySelectorAll(
416
+ 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])',
417
+ );
418
+ const list = [];
419
+ for (const n of nodes) {
420
+ if (n instanceof HTMLElement && n.offsetParent !== null) list.push(n);
421
+ }
422
+ if (panel.tabIndex >= 0) list.unshift(panel);
423
+ if (!list.length) {
424
+ ev.preventDefault();
425
+ panel.focus();
426
+ return;
427
+ }
428
+ const first = list[0];
429
+ const last = list[list.length - 1];
430
+ const active = document.activeElement;
431
+ if (ev.shiftKey) {
432
+ if (active === first || active === panel) {
433
+ ev.preventDefault();
434
+ last.focus();
435
+ }
436
+ } else if (active === last) {
437
+ ev.preventDefault();
438
+ first.focus();
439
+ }
440
+ }
441
+ _syncFromDom() {
442
+ const root = this.__vmzDomRoot;
443
+ const panel =
444
+ root && typeof root.querySelector === "function"
445
+ ? root.querySelector('[data-vmz-overlay="drawer"] [data-vmz-focus="enter"]')
446
+ : null;
447
+ if (panel && !this._panel) this._enterFocus(panel);
448
+ else if (!panel && this._panel) this._exitFocus();
449
+ }
450
+ _enterFocus(panel) {
451
+ if (typeof document === "undefined") return;
452
+ const active = document.activeElement;
453
+ if (!this._restoreFocus) {
454
+ const root = this.__vmzDomRoot;
455
+ const outside =
456
+ active instanceof HTMLElement &&
457
+ active !== document.body &&
458
+ !(root && typeof root.contains === "function" && root.contains(active));
459
+ if (outside) this._restoreFocus = active;
460
+ }
461
+ this._panel = panel instanceof HTMLElement ? panel : null;
462
+ const overlay =
463
+ this._panel && typeof this._panel.closest === "function"
464
+ ? this._panel.closest('[data-vmz-overlay="drawer"]')
465
+ : null;
466
+ if (this._exiting) {
467
+ this._cancelExit("reenter");
468
+ } else {
469
+ this._motionGen += 1;
470
+ }
471
+ if (overlay) {
472
+ overlay.setAttribute("data-vmz-motion-gen", String(this._motionGen));
473
+ }
474
+ if (this._adoptNextEnter && overlay) {
475
+ overlay.setAttribute("data-vmz-motion-adopt", "true");
476
+ this._panel.setAttribute("data-vmz-motion", "overlay-stable");
477
+ this._adoptNextEnter = false;
478
+ } else if (overlay) {
479
+ overlay.removeAttribute("data-vmz-motion-adopt");
480
+ overlay.removeAttribute("data-vmz-motion-phase");
481
+ overlay.removeAttribute("data-vmz-motion-cancelled");
482
+ this._panel.setAttribute("data-vmz-motion", "overlay-enter");
483
+ }
484
+ if (this._panel && typeof this._panel.focus === "function") this._panel.focus();
485
+ if (!this._docKeyHandler) {
486
+ this._docKeyHandler = (ev) => {
487
+ if (ev.key !== "Escape") return;
488
+ if (ev.defaultPrevented) return;
489
+ if (!this._isTopmostStack()) return;
490
+ ev.preventDefault();
491
+ ev.stopPropagation();
492
+ this.dismiss();
493
+ };
494
+ document.addEventListener("keydown", this._docKeyHandler);
495
+ }
496
+ }
497
+ _exitFocus() {
498
+ if (this._exitTimer) {
499
+ clearTimeout(this._exitTimer);
500
+ this._exitTimer = null;
501
+ }
502
+ this._exiting = false;
503
+ this._teardownDocKey();
504
+ this._panel = null;
505
+ const restore = this._restoreFocus;
506
+ this._restoreFocus = null;
507
+ if (restore && typeof restore.focus === "function" && document.contains(restore)) {
508
+ restore.focus();
509
+ }
510
+ }
511
+ _teardownDocKey() {
512
+ if (typeof document !== "undefined" && this._docKeyHandler) {
513
+ document.removeEventListener("keydown", this._docKeyHandler);
514
+ }
515
+ this._docKeyHandler = null;
516
+ }
517
+ }
518
+ </script>