@zeyos/zx 4.2.0 → 4.3.4

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 (69) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/README.md +5 -3
  3. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  4. package/dist/types/components/calendar/calendar.d.ts +19 -2
  5. package/dist/types/components/card-view/card-view.d.ts +1 -1
  6. package/dist/types/components/card-view/record-card.d.ts +47 -5
  7. package/dist/types/components/dialog/dialog.d.ts +6 -1
  8. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  9. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  10. package/dist/types/components/kanban-view/kanban-view.d.ts +669 -24
  11. package/dist/types/components/launcher/launcher.d.ts +5 -0
  12. package/dist/types/components/message/message.d.ts +9 -1
  13. package/dist/types/components/modal/modal.d.ts +44 -7
  14. package/dist/types/components/sheet/sheet.d.ts +22 -12
  15. package/dist/types/core/component.d.ts +6 -1
  16. package/dist/types/core/overlay-host.d.ts +12 -0
  17. package/dist/types/index.d.ts +2 -1
  18. package/dist/zx-zeyos.esm.js +173 -43
  19. package/dist/zx-zeyos.esm.min.js +1 -1
  20. package/dist/zx.css +876 -230
  21. package/dist/zx.esm.js +2053 -338
  22. package/dist/zx.esm.min.js +3 -3
  23. package/dist/zx.global.js +2053 -338
  24. package/dist/zx.global.min.js +3 -3
  25. package/dist/zx.min.css +1 -1
  26. package/docs/api.json +250 -2
  27. package/docs/llms.md +67 -48
  28. package/docs/llms.txt +1 -3
  29. package/package.json +1 -2
  30. package/src/components/account-menu/account-menu.css +1 -1
  31. package/src/components/app-rail/app-rail.css +1 -1
  32. package/src/components/calendar/calendar-model.js +45 -0
  33. package/src/components/calendar/calendar.css +96 -34
  34. package/src/components/calendar/calendar.js +122 -29
  35. package/src/components/card-view/card-view.css +211 -9
  36. package/src/components/card-view/card-view.js +7 -7
  37. package/src/components/card-view/record-card.js +92 -19
  38. package/src/components/context-menu/context-menu.css +2 -2
  39. package/src/components/context-menu/context-menu.js +5 -1
  40. package/src/components/date-picker/date-picker.css +18 -11
  41. package/src/components/datebox/date-range-box.css +4 -2
  42. package/src/components/datebox/datebox.css +4 -2
  43. package/src/components/dialog/dialog.css +4 -3
  44. package/src/components/dialog/dialog.js +2 -1
  45. package/src/components/dropdown/dropdown.css +4 -2
  46. package/src/components/dropdown/dropdown.js +7 -2
  47. package/src/components/kanban-view/kanban-policy.js +412 -0
  48. package/src/components/kanban-view/kanban-view.css +480 -36
  49. package/src/components/kanban-view/kanban-view.js +1514 -235
  50. package/src/components/launcher/launcher.css +10 -8
  51. package/src/components/launcher/launcher.js +9 -2
  52. package/src/components/menu-button/menu-button.css +2 -2
  53. package/src/components/message/message.css +15 -25
  54. package/src/components/message/message.js +17 -11
  55. package/src/components/modal/modal.css +6 -4
  56. package/src/components/modal/modal.js +99 -25
  57. package/src/components/select/select.css +9 -7
  58. package/src/components/sheet/sheet.css +25 -7
  59. package/src/components/sheet/sheet.js +43 -23
  60. package/src/components/table/table.css +5 -3
  61. package/src/components/tag-picker/tag-picker.css +5 -3
  62. package/src/components/tooltip/tooltip.css +4 -2
  63. package/src/components/tooltip/tooltip.js +4 -1
  64. package/src/core/component.js +8 -3
  65. package/src/core/overlay-host.js +23 -0
  66. package/src/index.js +8 -3
  67. package/styles/tokens/dark.css +4 -0
  68. package/styles/tokens/semantic.css +83 -8
  69. package/DESIGN-SYSTEM.md +0 -182
@@ -3,10 +3,12 @@
3
3
  max-block-size: min(720px, calc(100vh - (2 * var(--zx-space-6))));
4
4
  border: 1px solid var(--zx-color-overlay-border);
5
5
  border-radius: var(--zx-radius-xl);
6
- background: var(--zx-color-overlay-panel);
6
+ background-color: var(--zx-color-overlay-panel);
7
+ background-image: var(--zx-overlay-panel-image);
8
+ background-clip: padding-box;
7
9
  -webkit-backdrop-filter: var(--zx-glass-filter-panel);
8
10
  backdrop-filter: var(--zx-glass-filter-panel);
9
- box-shadow: var(--zx-glass-raised-shadow);
11
+ box-shadow: var(--zx-glass-panel-shadow);
10
12
  color: var(--zx-color-text);
11
13
  padding: 0;
12
14
  }
@@ -22,7 +24,7 @@
22
24
  }
23
25
 
24
26
  .zx-launcher::backdrop {
25
- background: var(--zx-color-bg-backdrop);
27
+ background: var(--zx-color-overlay-backdrop);
26
28
  }
27
29
 
28
30
  .zx-launcher__surface {
@@ -36,7 +38,7 @@
36
38
  grid-template-columns: auto minmax(0, 1fr) auto auto;
37
39
  align-items: center;
38
40
  gap: var(--zx-space-3);
39
- border-block-end: 1px solid var(--zx-color-border);
41
+ border-block-end: 1px solid var(--zx-color-overlay-divider);
40
42
  padding: var(--zx-space-3) var(--zx-space-4);
41
43
  }
42
44
 
@@ -129,11 +131,11 @@
129
131
 
130
132
  .zx-launcher__option[aria-selected="true"],
131
133
  .zx-launcher__option:hover {
132
- background: var(--zx-color-bg-hover);
134
+ background: var(--zx-color-overlay-hover);
133
135
  }
134
136
 
135
137
  .zx-launcher__option[aria-current="page"] {
136
- background: var(--zx-color-bg-selected);
138
+ background: var(--zx-color-overlay-selected);
137
139
  color: var(--zx-color-accent);
138
140
  }
139
141
 
@@ -148,7 +150,7 @@
148
150
  }
149
151
 
150
152
  .zx-launcher__option[data-kind="application"][aria-current="page"] {
151
- background: var(--zx-color-bg-selected);
153
+ background: var(--zx-color-overlay-selected);
152
154
  }
153
155
 
154
156
  .zx-launcher__option[data-kind="application"] .zx-launcher__icon {
@@ -187,7 +189,7 @@
187
189
  justify-content: end;
188
190
  gap: var(--zx-space-4);
189
191
  padding: var(--zx-space-2) var(--zx-space-4);
190
- border-block-start: 1px solid var(--zx-color-border);
192
+ border-block-start: 1px solid var(--zx-color-overlay-divider);
191
193
  color: var(--zx-color-text-muted);
192
194
  font-size: var(--zx-text-xs);
193
195
  }
@@ -1,6 +1,7 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h, safeHref } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
+ import { overlayHost } from '../../core/overlay-host.js';
4
5
  import { uid } from '../../core/util.js';
5
6
 
6
7
  /**
@@ -49,6 +50,7 @@ import { uid } from '../../core/util.js';
49
50
  * @property {string} [loadingText='Searching…'] Loading-state text.
50
51
  * @property {'mod+k'|false} [shortcut='mod+k'] Optional global keyboard shortcut.
51
52
  * @property {false|{move?: string, open?: string, close?: string}} [hints] Visible keyboard hints, or false.
53
+ * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns an internally created launcher; defaults to the opener.
52
54
  * @property {(event: CustomEvent<LauncherSelectDetail>) => void} [onselect] Preventable selection listener.
53
55
  * @property {(event: CustomEvent<{query: string}>) => void} [onquery] Query-change listener.
54
56
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open listener.
@@ -85,7 +87,8 @@ export class Launcher extends Component {
85
87
  emptyText: 'No results',
86
88
  loadingText: 'Searching…',
87
89
  shortcut: 'mod+k',
88
- hints: { move: 'Move', open: 'Open', close: 'Close' }
90
+ hints: { move: 'Move', open: 'Open', close: 'Close' },
91
+ scope: null
89
92
  };
90
93
 
91
94
  /**
@@ -172,7 +175,7 @@ export class Launcher extends Component {
172
175
  results,
173
176
  footer));
174
177
  dialog.dataset.state = 'closed';
175
- if (this._createdRoot) document.body.append(dialog);
178
+ if (this._createdRoot) overlayHost(this.options.scope).append(dialog);
176
179
 
177
180
  this.listen(input, 'input', () => {
178
181
  this._query = /** @type {HTMLInputElement} */ (input).value;
@@ -212,6 +215,10 @@ export class Launcher extends Component {
212
215
  /** Opens the launcher and focuses/selects its query. @returns {this} @fires Launcher#open */
213
216
  open() {
214
217
  if (this.isOpen() || this._destroyed) return this;
218
+ if (this._createdRoot) {
219
+ const host = overlayHost(this.options.scope);
220
+ if (this.el.parentElement !== host) host.append(this.el);
221
+ }
215
222
  this.el.showModal();
216
223
  this.el.dataset.state = 'open';
217
224
  this.refs.input.setAttribute('aria-expanded', 'true');
@@ -24,7 +24,7 @@
24
24
 
25
25
  .zx-menu-button__item:hover,
26
26
  .zx-menu-button__item:focus-visible {
27
- background: var(--zx-color-bg-hover);
27
+ background: var(--zx-color-overlay-hover);
28
28
  }
29
29
 
30
30
  .zx-menu-button__item[data-danger="true"] {
@@ -45,5 +45,5 @@
45
45
  .zx-menu-button__separator {
46
46
  block-size: 1px;
47
47
  margin: var(--zx-space-1) var(--zx-space-2);
48
- background: var(--zx-color-border);
48
+ background: var(--zx-color-overlay-divider);
49
49
  }
@@ -27,14 +27,15 @@
27
27
 
28
28
  .zx-message__toast {
29
29
  --zx-message-kind: var(--zx-color-info);
30
+ --zx-message-fallback-bg: var(--zx-color-info-bg);
30
31
  position: relative;
31
32
  display: grid;
32
33
  grid-template-columns: auto minmax(0, 1fr) auto;
33
34
  align-items: start;
34
35
  gap: var(--zx-space-3);
35
36
  border: 1px solid var(--zx-color-overlay-border);
36
- border-radius: var(--zx-radius-md);
37
- background: var(--zx-color-info-bg);
37
+ border-radius: var(--zx-radius-lg);
38
+ background: var(--zx-message-fallback-bg);
38
39
  box-shadow: inset 0 0 0 1px var(--zx-message-kind), var(--zx-glass-raised-shadow);
39
40
  padding: var(--zx-space-3);
40
41
  color: var(--zx-color-text);
@@ -42,51 +43,40 @@
42
43
 
43
44
  .zx-message__toast[data-kind="success"] {
44
45
  --zx-message-kind: var(--zx-color-success);
45
- background: var(--zx-color-success-bg);
46
+ --zx-message-fallback-bg: var(--zx-color-success-bg);
46
47
  }
47
48
 
48
49
  .zx-message__toast[data-kind="warning"] {
49
50
  --zx-message-kind: var(--zx-color-warning);
50
- background: var(--zx-color-warning-bg);
51
+ --zx-message-fallback-bg: var(--zx-color-warning-bg);
51
52
  }
52
53
 
53
54
  .zx-message__toast[data-kind="error"] {
54
55
  --zx-message-kind: var(--zx-color-danger);
55
- background: var(--zx-color-danger-bg);
56
+ --zx-message-fallback-bg: var(--zx-color-danger-bg);
56
57
  }
57
58
 
58
- @supports (backdrop-filter: blur(1px)) {
59
+ @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
59
60
  .zx-message__toast {
60
- background: color-mix(in srgb, var(--zx-color-info-bg) 34%, var(--zx-color-overlay-surface));
61
+ border-color: color-mix(in srgb, var(--zx-message-kind) 28%, var(--zx-color-overlay-border));
62
+ background-color: var(--zx-color-overlay-surface);
63
+ background-image: var(--zx-overlay-toast-image);
64
+ background-clip: padding-box;
61
65
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
62
66
  backdrop-filter: var(--zx-glass-filter-overlay);
63
- box-shadow: inset 0 0 0 1px var(--zx-message-kind), var(--zx-glass-raised-shadow);
64
- }
65
-
66
- .zx-message__toast[data-kind="success"] {
67
- background: color-mix(in srgb, var(--zx-color-success-bg) 34%, var(--zx-color-overlay-surface));
68
- }
69
-
70
- .zx-message__toast[data-kind="warning"] {
71
- background: color-mix(in srgb, var(--zx-color-warning-bg) 34%, var(--zx-color-overlay-surface));
72
- }
73
-
74
- .zx-message__toast[data-kind="error"] {
75
- background: color-mix(in srgb, var(--zx-color-danger-bg) 34%, var(--zx-color-overlay-surface));
67
+ box-shadow: inset 3px 0 0 color-mix(in srgb, var(--zx-message-kind) 72%,
68
+ var(--zx-color-glass-highlight-strong)), var(--zx-glass-overlay-shadow);
76
69
  }
77
70
  }
78
71
 
79
72
  @media (prefers-reduced-transparency: reduce) {
80
73
  .zx-message__toast {
81
74
  border-color: var(--zx-color-border);
75
+ background: var(--zx-message-fallback-bg);
82
76
  -webkit-backdrop-filter: none;
83
77
  backdrop-filter: none;
84
78
  box-shadow: var(--zx-overlay-shadow);
85
79
  }
86
- .zx-message__toast[data-kind="info"] { background: var(--zx-color-info-bg); }
87
- .zx-message__toast[data-kind="success"] { background: var(--zx-color-success-bg); }
88
- .zx-message__toast[data-kind="warning"] { background: var(--zx-color-warning-bg); }
89
- .zx-message__toast[data-kind="error"] { background: var(--zx-color-danger-bg); }
90
80
  }
91
81
 
92
82
  .zx-message__icon {
@@ -111,7 +101,7 @@
111
101
  block-size: 4px;
112
102
  margin-block-start: var(--zx-space-2);
113
103
  border-radius: var(--zx-radius-full);
114
- background: var(--zx-color-border);
104
+ background: var(--zx-color-overlay-divider);
115
105
  }
116
106
 
117
107
  .zx-message__progress-fill {
@@ -1,6 +1,7 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
+ import { overlayHost } from '../../core/overlay-host.js';
4
5
 
5
6
  /** @typedef {'info'|'success'|'warning'|'error'} MessageKind */
6
7
 
@@ -9,6 +10,7 @@ import { icon } from '../../core/icons.js';
9
10
  * @property {MessageKind|'danger'} [kind='info'] Message intent (`danger` aliases `error`).
10
11
  * @property {number} [timeout=4000] Auto-dismiss delay in milliseconds; zero persists.
11
12
  * @property {boolean} [closable=true] Whether to render a close button.
13
+ * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns a floating toast.
12
14
  */
13
15
 
14
16
  /**
@@ -133,8 +135,8 @@ export class Message extends Component {
133
135
  maxVisible: 5
134
136
  };
135
137
 
136
- /** @type {Message|null} */
137
- static #floating = null;
138
+ /** @type {WeakMap<Element, Message>} */
139
+ static #floating = new WeakMap();
138
140
 
139
141
  /** @returns {HTMLElement} */
140
142
  render() {
@@ -217,16 +219,17 @@ export class Message extends Component {
217
219
  * @returns {MessageHandle}
218
220
  */
219
221
  static show(msg, options = {}) {
220
- return this.#floatingRegion().show(msg, options);
222
+ return this.#floatingRegion(options.scope).show(msg, options);
221
223
  }
222
224
 
223
225
  /**
224
226
  * Shows a floating progress status.
225
227
  * @param {string} text Initial status text.
228
+ * @param {{scope?: Element|string|null}} [options={}] Floating-region scope.
226
229
  * @returns {ProgressHandle}
227
230
  */
228
- static progress(text) {
229
- return this.#floatingRegion()._progress(text);
231
+ static progress(text, options = {}) {
232
+ return this.#floatingRegion(options.scope)._progress(text);
230
233
  }
231
234
 
232
235
  /** @returns {void} */
@@ -414,19 +417,22 @@ export class Message extends Component {
414
417
  this._queue.remove(entry);
415
418
  }
416
419
 
417
- /** @returns {Message} */
418
- static #floatingRegion() {
419
- if (this.#floating) return this.#floating;
420
+ /** @param {Element|string|null|undefined} scope @returns {Message} */
421
+ static #floatingRegion(scope) {
422
+ const host = overlayHost(scope ?? null);
423
+ const existing = this.#floating.get(host);
424
+ if (existing) return existing;
420
425
  const region = h('div', {
421
426
  class: 'zx-message-region',
422
427
  popover: 'manual',
423
428
  role: 'status',
424
429
  ariaLive: 'polite'
425
430
  });
426
- document.body.append(region);
427
- this.#floating = new Message(region);
431
+ host.append(region);
432
+ const floating = new Message(region);
433
+ this.#floating.set(host, floating);
428
434
  if (typeof region.showPopover === 'function') region.showPopover();
429
- return this.#floating;
435
+ return floating;
430
436
  }
431
437
  }
432
438
 
@@ -4,10 +4,12 @@
4
4
  overflow: auto;
5
5
  border: 1px solid var(--zx-color-overlay-border);
6
6
  border-radius: var(--zx-radius-lg);
7
- background: var(--zx-color-overlay-panel);
7
+ background-color: var(--zx-color-overlay-panel);
8
+ background-image: var(--zx-overlay-panel-image);
9
+ background-clip: padding-box;
8
10
  -webkit-backdrop-filter: var(--zx-glass-filter-panel);
9
11
  backdrop-filter: var(--zx-glass-filter-panel);
10
- box-shadow: var(--zx-glass-raised-shadow);
12
+ box-shadow: var(--zx-glass-panel-shadow);
11
13
  color: var(--zx-color-text);
12
14
  padding: 0;
13
15
  }
@@ -23,7 +25,7 @@
23
25
  }
24
26
 
25
27
  .zx-modal::backdrop {
26
- background: var(--zx-color-bg-backdrop);
28
+ background: var(--zx-color-overlay-backdrop);
27
29
  }
28
30
 
29
31
  .zx-modal__content {
@@ -34,7 +36,7 @@
34
36
  * `:modal` rather than `[open]`: only a dialog opened with `showModal()` blocks the page, and a
35
37
  * non-modal one (Sheet's `modal: false`) must leave the page scrollable behind it.
36
38
  */
37
- body:has(> .zx-modal:modal) {
39
+ body:has(.zx-modal:modal) {
38
40
  overflow: hidden;
39
41
  overscroll-behavior: none;
40
42
  }
@@ -1,5 +1,6 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
+ import { overlayHost } from '../../core/overlay-host.js';
3
4
 
4
5
  /**
5
6
  * @typedef {Object} ModalOptions
@@ -8,6 +9,7 @@ import { h } from '../../core/dom.js';
8
9
  * @property {boolean} [closable=true] Whether Escape may close the modal.
9
10
  * @property {boolean} [lightDismiss=false] Whether a backdrop click closes the modal.
10
11
  * @property {boolean} [destroyOnClose=false] Whether to destroy the modal after it closes.
12
+ * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener.
11
13
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
12
14
  * @property {(event: CustomEvent<{result: unknown}>) => void} [onclose] Close event listener.
13
15
  * @property {(event: CustomEvent<Record<string, never>>) => void} [oncancel] Cancel event listener.
@@ -30,15 +32,19 @@ export class Modal extends Component {
30
32
  width: 'auto',
31
33
  closable: true,
32
34
  lightDismiss: false,
33
- destroyOnClose: false
35
+ destroyOnClose: false,
36
+ scope: null
34
37
  };
35
38
 
36
- #pendingResult;
37
- #hasPendingResult = false;
39
+ /** @type {Map<number, {result: unknown}>} */
40
+ #pendingResults = new Map();
38
41
  #destroying = false;
42
+ #presentation = 0;
43
+ /** @type {number[]} */
44
+ #nativeClosingPresentations = [];
39
45
 
40
46
  /**
41
- * Creates a modal appended to `document.body`.
47
+ * Creates a modal in the configured theme scope, or at document level when none is present.
42
48
  * @param {Element|string|null} [_target=null] Ignored; Modal always owns its root dialog.
43
49
  * @param {ModalOptions} [options={}] Modal options.
44
50
  */
@@ -52,14 +58,7 @@ export class Modal extends Component {
52
58
  if (!this.options.closable || cancelEvent.defaultPrevented) event.preventDefault();
53
59
  });
54
60
  this.listen(this.el, 'close', () => {
55
- if (!this._isRealClose()) return;
56
- this.el.dataset.state = 'closed';
57
- delete this.el.dataset.zxOverlayOrder;
58
- const result = this.#hasPendingResult ? this.#pendingResult : (this.el.returnValue || undefined);
59
- this.#pendingResult = undefined;
60
- this.#hasPendingResult = false;
61
- this.emit('close', { result });
62
- if (this.options.destroyOnClose && !this.#destroying) this.destroy();
61
+ this._settleClose(this.#nativeClosingPresentations.shift());
63
62
  });
64
63
  this.listen(this.el, 'click', (event) => {
65
64
  if (!this.options.lightDismiss || event.target !== this.el || !isBackdropClick(this.el, event)) return;
@@ -76,7 +75,7 @@ export class Modal extends Component {
76
75
  * @returns {Element}
77
76
  */
78
77
  mountTarget() {
79
- return document.body;
78
+ return overlayHost(this.options.scope);
80
79
  }
81
80
 
82
81
  /**
@@ -87,17 +86,85 @@ export class Modal extends Component {
87
86
  this.el.showModal();
88
87
  }
89
88
 
89
+ /**
90
+ * Removes the dialog from its current presentation layer.
91
+ * Subclasses may replace native dialog presentation while preserving Modal lifecycle events.
92
+ * @param {string} [result=''] Native string return value.
93
+ * @param {number} [presentation=this.#presentation] Presentation generation being hidden.
94
+ * @returns {void}
95
+ */
96
+ _hide(result = '', presentation = this.#presentation) {
97
+ this.#nativeClosingPresentations.push(presentation);
98
+ try {
99
+ this.el.close(result);
100
+ } catch (error) {
101
+ this.#nativeClosingPresentations.pop();
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Shows a new presentation and gives it a generation of its own. Close events are queued by the
108
+ * platform, so the generation lets an event from an earlier presentation expire harmlessly if
109
+ * the same overlay has already reopened.
110
+ * @returns {void}
111
+ */
112
+ _present() {
113
+ this._show();
114
+ this.#presentation += 1;
115
+ }
116
+
117
+ /**
118
+ * Hides the current presentation and records which generation its queued close event belongs to.
119
+ * @param {string} [result=''] Native string return value.
120
+ * @returns {void}
121
+ */
122
+ _dismiss(result = '') {
123
+ this._hide(result, this.#presentation);
124
+ }
125
+
126
+ /**
127
+ * Reports whether the current presentation layer is visible.
128
+ * @returns {boolean}
129
+ */
130
+ _isShown() {
131
+ return this.el.open;
132
+ }
133
+
90
134
  /**
91
135
  * Whether an incoming `close` event is a real dismissal.
92
136
  *
93
- * A dialog cannot move between the top layer and the page flow while it is open, so a subclass
94
- * that re-hosts itself has to close and immediately reopen. `close` is dispatched in a queued
95
- * task, not synchronously, so that stale event arrives once the dialog is already open again —
96
- * late enough to clobber `data-state`, fire a `close` nobody asked for, and trip
97
- * `destroyOnClose`. Returning false from an override drops it at the source instead.
137
+ * `close` is dispatched in a queued task, not synchronously. By the time it arrives the overlay
138
+ * may have reopened or crossed presentation layers, so only the event for the current hidden
139
+ * generation may settle state, emit lifecycle events, or trigger `destroyOnClose`.
140
+ * @param {number|undefined} closingPresentation Presentation that produced the event, if known.
98
141
  * @returns {boolean}
99
142
  */
100
- _isRealClose() {
143
+ _isRealClose(closingPresentation) {
144
+ if (closingPresentation !== undefined && closingPresentation !== this.#presentation) return false;
145
+ return !this._isShown();
146
+ }
147
+
148
+ /**
149
+ * Settles one presentation's close without allowing a queued event to invalidate a newer one.
150
+ * Popover-based subclasses call this directly; native dialog events arrive through the listener.
151
+ * @param {number|undefined} closingPresentation Presentation that was hidden, if known.
152
+ * @returns {boolean} Whether this event closed the current presentation.
153
+ */
154
+ _settleClose(closingPresentation) {
155
+ if (!this._isRealClose(closingPresentation)) {
156
+ if (closingPresentation !== undefined) this.#pendingResults.delete(closingPresentation);
157
+ return false;
158
+ }
159
+ this.el.dataset.state = 'closed';
160
+ delete this.el.dataset.zxOverlayOrder;
161
+ const pending = closingPresentation === undefined
162
+ ? undefined
163
+ : this.#pendingResults.get(closingPresentation);
164
+ if (closingPresentation !== undefined) this.#pendingResults.delete(closingPresentation);
165
+ const result = pending ? pending.result : (this.el.returnValue || undefined);
166
+ this.emit('close', { result });
167
+ if (this.options.destroyOnClose && !this.#destroying) this.destroy();
101
168
  return true;
102
169
  }
103
170
 
@@ -121,10 +188,12 @@ export class Modal extends Component {
121
188
  */
122
189
  open() {
123
190
  if (this.isOpen()) return this;
191
+ const host = this.mountTarget();
192
+ if (this.el.parentElement !== host) host.append(this.el);
124
193
  this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
125
194
  this.el.returnValue = '';
126
195
  try {
127
- this._show();
196
+ this._present();
128
197
  } catch (error) {
129
198
  delete this.el.dataset.zxOverlayOrder;
130
199
  throw error;
@@ -141,9 +210,14 @@ export class Modal extends Component {
141
210
  */
142
211
  close(result) {
143
212
  if (!this.isOpen()) return this;
144
- this.#pendingResult = result;
145
- this.#hasPendingResult = true;
146
- this.el.close(typeof result === 'string' ? result : '');
213
+ const presentation = this.#presentation;
214
+ this.#pendingResults.set(presentation, { result });
215
+ try {
216
+ this._dismiss(typeof result === 'string' ? result : '');
217
+ } catch (error) {
218
+ this.#pendingResults.delete(presentation);
219
+ throw error;
220
+ }
147
221
  return this;
148
222
  }
149
223
 
@@ -162,7 +236,7 @@ export class Modal extends Component {
162
236
  * @returns {boolean}
163
237
  */
164
238
  isOpen() {
165
- return this.el.open;
239
+ return this._isShown();
166
240
  }
167
241
 
168
242
  /**
@@ -172,7 +246,7 @@ export class Modal extends Component {
172
246
  destroy() {
173
247
  if (this.#destroying) return;
174
248
  this.#destroying = true;
175
- if (this.isOpen()) this.el.close();
249
+ if (this.isOpen()) this._dismiss();
176
250
  super.destroy();
177
251
  }
178
252
 
@@ -98,10 +98,12 @@
98
98
  overflow: hidden;
99
99
  border: 1px solid var(--zx-color-overlay-border);
100
100
  border-radius: var(--zx-radius-md);
101
- background: var(--zx-color-overlay-surface);
101
+ background-color: var(--zx-color-overlay-surface);
102
+ background-image: var(--zx-overlay-surface-image);
103
+ background-clip: padding-box;
102
104
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
103
105
  backdrop-filter: var(--zx-glass-filter-overlay);
104
- box-shadow: var(--zx-glass-raised-shadow);
106
+ box-shadow: var(--zx-glass-overlay-shadow);
105
107
  color: var(--zx-color-text);
106
108
  padding: var(--zx-space-1);
107
109
  }
@@ -128,15 +130,15 @@
128
130
  }
129
131
 
130
132
  .zx-select__option[data-active="true"] {
131
- background: var(--zx-color-bg-hover);
133
+ background: var(--zx-color-overlay-hover);
132
134
  }
133
135
 
134
136
  .zx-select__option:hover {
135
- background: var(--zx-color-bg-hover);
137
+ background: var(--zx-color-overlay-hover);
136
138
  }
137
139
 
138
140
  .zx-select__option[aria-selected="true"] {
139
- background: var(--zx-color-bg-selected);
141
+ background: var(--zx-color-overlay-selected);
140
142
  }
141
143
 
142
144
  .zx-select__option > .zx-icon {
@@ -148,7 +150,7 @@
148
150
  position: sticky;
149
151
  z-index: 1;
150
152
  inset-block-start: 0;
151
- background: var(--zx-color-overlay-surface);
153
+ background: var(--zx-color-overlay-scrim);
152
154
  color: var(--zx-color-text-muted);
153
155
  font-size: var(--zx-text-xs);
154
156
  font-weight: 700;
@@ -174,7 +176,7 @@
174
176
  .zx-select__separator {
175
177
  block-size: 1px;
176
178
  margin: var(--zx-space-1) var(--zx-control-pad-x);
177
- background: var(--zx-color-border);
179
+ background: var(--zx-color-overlay-divider);
178
180
  }
179
181
 
180
182
  .zx-select__empty {
@@ -12,7 +12,7 @@
12
12
  border-radius: 0;
13
13
  /* The header and footer stay put; `.zx-dialog__body` is what scrolls. */
14
14
  overflow: hidden;
15
- box-shadow: var(--zx-glass-raised-shadow);
15
+ box-shadow: var(--zx-glass-panel-shadow);
16
16
  }
17
17
 
18
18
  /* Horizontal sheets: full height, sized across the inline axis. */
@@ -26,14 +26,14 @@
26
26
  .zx-sheet[data-side="start"] {
27
27
  inset-inline-start: 0;
28
28
  inset-inline-end: auto;
29
- border-inline-end: 1px solid var(--zx-color-border);
29
+ border-inline-end: 1px solid var(--zx-color-overlay-border);
30
30
  --zx-sheet-enter: -100% 0;
31
31
  }
32
32
 
33
33
  .zx-sheet[data-side="end"] {
34
34
  inset-inline-start: auto;
35
35
  inset-inline-end: 0;
36
- border-inline-start: 1px solid var(--zx-color-border);
36
+ border-inline-start: 1px solid var(--zx-color-overlay-border);
37
37
  --zx-sheet-enter: 100% 0;
38
38
  }
39
39
 
@@ -56,7 +56,7 @@
56
56
  .zx-sheet[data-side="top"] {
57
57
  inset-block-start: 0;
58
58
  inset-block-end: auto;
59
- border-block-end: 1px solid var(--zx-color-border);
59
+ border-block-end: 1px solid var(--zx-color-overlay-border);
60
60
  border-end-start-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
61
61
  border-end-end-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
62
62
  --zx-sheet-enter: 0 -100%;
@@ -65,7 +65,7 @@
65
65
  .zx-sheet[data-side="bottom"] {
66
66
  inset-block-start: auto;
67
67
  inset-block-end: 0;
68
- border-block-start: 1px solid var(--zx-color-border);
68
+ border-block-start: 1px solid var(--zx-color-overlay-border);
69
69
  border-start-start-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
70
70
  border-start-end-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
71
71
  --zx-sheet-enter: 0 100%;
@@ -80,9 +80,26 @@
80
80
  }
81
81
 
82
82
  .zx-sheet[data-backdrop="blur"]::backdrop {
83
+ -webkit-backdrop-filter: blur(var(--zx-overlay-blur));
83
84
  backdrop-filter: blur(var(--zx-overlay-blur));
84
85
  }
85
86
 
87
+ /* A non-modal Sheet uses the popover top layer to escape transformed/clipped application shells,
88
+ but it must not gain a visual or interactive modal backdrop in the process. */
89
+ .zx-sheet[data-modality="none"]::backdrop,
90
+ .zx-sheet[data-modality="trap-focus"]::backdrop {
91
+ background: transparent;
92
+ -webkit-backdrop-filter: none;
93
+ backdrop-filter: none;
94
+ }
95
+
96
+ @media (prefers-reduced-transparency: reduce), (prefers-contrast: more), (forced-colors: active) {
97
+ .zx-sheet[data-backdrop="blur"]::backdrop {
98
+ -webkit-backdrop-filter: none;
99
+ backdrop-filter: none;
100
+ }
101
+ }
102
+
86
103
  /*
87
104
  * Entry and exit both animate. A `<dialog>` normally cannot animate out — `close()` applies
88
105
  * `display: none` immediately — so `display` and `overlay` transition with `allow-discrete`, which
@@ -103,13 +120,14 @@
103
120
  animation: none;
104
121
  }
105
122
 
106
- .zx-sheet:not([open]) {
123
+ .zx-sheet:not([open]):not(:popover-open) {
107
124
  translate: var(--zx-sheet-enter, 0 0);
108
125
  opacity: 0;
109
126
  }
110
127
 
111
128
  @starting-style {
112
- .zx-sheet[open] {
129
+ .zx-sheet[open],
130
+ .zx-sheet:popover-open {
113
131
  translate: var(--zx-sheet-enter, 0 0);
114
132
  opacity: 0;
115
133
  }