@zeyos/zx 2.3.0 → 3.0.0

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 (105) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/DESIGN-SYSTEM.md +136 -0
  3. package/MIGRATION.md +155 -4
  4. package/README.md +24 -13
  5. package/dist/types/compat/map/data.d.ts +3 -2
  6. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  7. package/dist/types/components/app-rail/app-rail.d.ts +250 -0
  8. package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
  9. package/dist/types/components/avatar/avatar.d.ts +100 -0
  10. package/dist/types/components/chart/chart.d.ts +321 -0
  11. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  12. package/dist/types/components/dialog/dialog.d.ts +9 -0
  13. package/dist/types/components/dock/dock.d.ts +495 -0
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/field/field.d.ts +14 -0
  16. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  17. package/dist/types/components/form/form.d.ts +25 -3
  18. package/dist/types/components/grid/grid.d.ts +172 -0
  19. package/dist/types/components/launcher/launcher.d.ts +300 -0
  20. package/dist/types/components/layout/layout.d.ts +4 -4
  21. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  22. package/dist/types/components/modal/modal.d.ts +25 -0
  23. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
  24. package/dist/types/components/questionnaire/items.d.ts +327 -0
  25. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  26. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  27. package/dist/types/components/sheet/sheet.d.ts +294 -0
  28. package/dist/types/components/split-view/split-view.d.ts +2 -21
  29. package/dist/types/components/table/table.d.ts +219 -12
  30. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  31. package/dist/types/components/value-list/value-list.d.ts +24 -3
  32. package/dist/types/core/dom.d.ts +114 -0
  33. package/dist/types/core/drag-axis.d.ts +111 -0
  34. package/dist/types/core/position.d.ts +4 -2
  35. package/dist/types/index.d.ts +12 -2
  36. package/dist/zx-compat.esm.js +641 -277
  37. package/dist/zx-compat.esm.min.js +1 -1
  38. package/dist/zx-compat.global.js +7609 -2236
  39. package/dist/zx-compat.global.min.js +3 -3
  40. package/dist/zx-zeyos.esm.js +546 -87
  41. package/dist/zx-zeyos.esm.min.js +1 -1
  42. package/dist/zx.css +1452 -101
  43. package/dist/zx.esm.js +7952 -2592
  44. package/dist/zx.esm.min.js +3 -3
  45. package/dist/zx.global.js +7952 -2592
  46. package/dist/zx.global.min.js +3 -3
  47. package/dist/zx.min.css +1 -1
  48. package/docs/api.json +2276 -337
  49. package/docs/llms.md +426 -30
  50. package/docs/llms.txt +21 -9
  51. package/package.json +4 -2
  52. package/src/compat/globals.js +3 -1
  53. package/src/compat/map/data.js +31 -9
  54. package/src/components/account-menu/account-menu.css +70 -0
  55. package/src/components/account-menu/account-menu.js +208 -0
  56. package/src/components/app-rail/app-rail.css +107 -0
  57. package/src/components/app-rail/app-rail.js +668 -0
  58. package/src/components/app-sidebar/app-sidebar.css +68 -0
  59. package/src/components/app-sidebar/app-sidebar.js +388 -0
  60. package/src/components/avatar/avatar.css +61 -0
  61. package/src/components/avatar/avatar.js +222 -0
  62. package/src/components/button/button.css +0 -1
  63. package/src/components/chart/chart.css +61 -0
  64. package/src/components/chart/chart.js +518 -0
  65. package/src/components/date-picker/month-picker.js +9 -0
  66. package/src/components/dialog/dialog.js +11 -4
  67. package/src/components/dock/dock.css +228 -0
  68. package/src/components/dock/dock.js +977 -0
  69. package/src/components/dropdown/dropdown.js +1 -1
  70. package/src/components/field/field.js +2 -0
  71. package/src/components/field-upload/field-upload.js +9 -4
  72. package/src/components/form/form.js +6 -3
  73. package/src/components/grid/grid.css +6 -0
  74. package/src/components/grid/grid.js +213 -0
  75. package/src/components/launcher/launcher.css +100 -0
  76. package/src/components/launcher/launcher.js +557 -0
  77. package/src/components/layout/layout.js +2 -2
  78. package/src/components/menu-button/menu-button.js +27 -2
  79. package/src/components/modal/modal.css +5 -1
  80. package/src/components/modal/modal.js +37 -2
  81. package/src/components/multi-value-editor/multi-value-editor.js +2 -1
  82. package/src/components/questionnaire/items.js +370 -0
  83. package/src/components/questionnaire/questionnaire.css +387 -0
  84. package/src/components/questionnaire/questionnaire.js +1033 -0
  85. package/src/components/select/select.css +0 -1
  86. package/src/components/sheet/sheet-stack.js +304 -0
  87. package/src/components/sheet/sheet.css +351 -0
  88. package/src/components/sheet/sheet.js +551 -0
  89. package/src/components/split-view/split-view.js +76 -161
  90. package/src/components/table/table.css +43 -0
  91. package/src/components/table/table.js +523 -45
  92. package/src/components/tooltip/tooltip.js +5 -2
  93. package/src/components/value-list/value-list.js +6 -3
  94. package/src/core/dom.js +302 -0
  95. package/src/core/drag-axis.js +203 -0
  96. package/src/core/position.js +68 -22
  97. package/src/index.js +19 -2
  98. package/styles/tokens/dark.css +2 -3
  99. package/styles/tokens/global.css +5 -5
  100. package/styles/tokens/semantic.css +5 -3
  101. package/styles/tokens/themes.css +1 -1
  102. package/styles/zx.css +10 -1
  103. package/dist/types/components/permission/permission.d.ts +0 -96
  104. package/src/components/permission/permission.css +0 -47
  105. package/src/components/permission/permission.js +0 -201
@@ -118,7 +118,6 @@
118
118
  background: var(--zx-color-bg-selected);
119
119
  }
120
120
 
121
- .zx-select__option > svg,
122
121
  .zx-select__option > .zx-icon {
123
122
  flex: none;
124
123
  color: var(--zx-color-accent);
@@ -0,0 +1,304 @@
1
+ /** @typedef {import('./sheet.js').Sheet} Sheet */
2
+
3
+ /**
4
+ * @typedef {Object} SheetStackOptions
5
+ * @property {'stack'|'cascade'} [layout='stack'] How depth is expressed. `stack` slides each
6
+ * covered sheet back toward its edge and scales it down, leaving only the top one usable —
7
+ * the drill-down feel. `cascade` shifts each covered sheet clear of the ones in front so they
8
+ * sit side by side and all stay usable, which is what an ERP screen usually wants: the parent
9
+ * record stays readable while a line item is edited.
10
+ * @property {number} [offset=24] Pixels a `stack` layout moves per depth step.
11
+ * @property {number} [scale=0.04] How much a `stack` layout shrinks per depth step, as a fraction.
12
+ * @property {number} [max=3] Depth at which a sheet stops being drawn. Deeper sheets stay open and
13
+ * in the stack; they are simply no longer visible behind the ones in front.
14
+ * @property {(event: CustomEvent<{sheet: Sheet, depth: number}>) => void} [onpush] Push listener.
15
+ * @property {(event: CustomEvent<{sheet: Sheet}>) => void} [onpop] Pop listener.
16
+ */
17
+
18
+ /**
19
+ * An ordered group of Sheets that read as one drill-down.
20
+ *
21
+ * Deliberately tiny, because the browser already does the hard part: nested dialogs stack in the
22
+ * top layer by open order, so there is no z-index bookkeeping here, and Escape closes only the
23
+ * topmost, so unwinding one level at a time is native behaviour rather than something to
24
+ * implement. What is left is bookkeeping — which sheet is at which depth — written onto the
25
+ * elements as `data-depth` and `--zx-sheet-depth` for CSS to interpret.
26
+ *
27
+ * Owns no element of its own, so it is a controller rather than a `Component`: there is nothing
28
+ * to render, only sheets to arrange.
29
+ *
30
+ * @fires SheetStack#push
31
+ * @fires SheetStack#pop
32
+ */
33
+ export class SheetStack extends EventTarget {
34
+ /** @type {Readonly<SheetStackOptions>} */
35
+ static defaults = Object.freeze({
36
+ layout: 'stack',
37
+ offset: 24,
38
+ scale: 0.04,
39
+ max: 3
40
+ });
41
+
42
+ /** @type {Sheet[]} Bottom first; the last entry is the top of the stack. */
43
+ #sheets = [];
44
+ /** @type {WeakMap<Sheet, () => void>} */
45
+ #closers = new WeakMap();
46
+ #destroyed = false;
47
+
48
+ /**
49
+ * Creates a sheet stack.
50
+ * @param {SheetStackOptions} [options={}] Stack options.
51
+ */
52
+ constructor(options = {}) {
53
+ super();
54
+ const merged = { ...SheetStack.defaults, ...(options && typeof options === 'object' ? options : {}) };
55
+ for (const [key, value] of Object.entries(merged)) {
56
+ if (!/^on[a-z]/.test(key) || typeof value !== 'function') continue;
57
+ this.on(key.slice(2), value);
58
+ delete merged[key];
59
+ }
60
+ this.options = Object.freeze(merged);
61
+ }
62
+
63
+ /**
64
+ * Adds a sheet to the top of the stack, opening it if it is closed. A sheet already in the stack
65
+ * is raised rather than added twice.
66
+ * @param {Sheet} sheet Sheet to push.
67
+ * @returns {this}
68
+ * @fires SheetStack#push
69
+ */
70
+ push(sheet) {
71
+ if (!sheet || typeof sheet.open !== 'function') throw new TypeError('SheetStack.push expects a Sheet');
72
+ const existing = this.#sheets.indexOf(sheet);
73
+ if (existing !== -1) this.#sheets.splice(existing, 1);
74
+ this.#sheets.push(sheet);
75
+
76
+ if (!this.#closers.has(sheet)) {
77
+ // A sheet dismissed by Escape, its close button, or its own code leaves the stack too.
78
+ const onClose = () => this.#drop(sheet);
79
+ this.#closers.set(sheet, onClose);
80
+ sheet.on('close', onClose);
81
+ }
82
+ if (!sheet.isOpen()) sheet.open();
83
+ this.#sync();
84
+ this.#emit('push', { sheet, depth: 0 });
85
+ return this;
86
+ }
87
+
88
+ /**
89
+ * Closes the top sheet and removes it from the stack.
90
+ *
91
+ * The removal is synchronous while the close is not — a dialog dispatches `close` in a queued
92
+ * task — so the stack updates itself here rather than waiting for the event. The close listener
93
+ * is left to handle the other direction: a sheet dismissed by Escape or its own button.
94
+ * @returns {Sheet|null} The sheet that was closed, or null on an empty stack.
95
+ * @fires SheetStack#pop
96
+ */
97
+ pop() {
98
+ const sheet = this.top();
99
+ if (!sheet) return null;
100
+ this.#remove(sheet);
101
+ sheet.close();
102
+ return sheet;
103
+ }
104
+
105
+ /**
106
+ * Closes everything above a sheet, leaving it on top.
107
+ * @param {Sheet} sheet Sheet to return to.
108
+ * @returns {this}
109
+ */
110
+ popTo(sheet) {
111
+ const index = this.#sheets.indexOf(sheet);
112
+ if (index === -1) return this;
113
+ while (this.#sheets.length > index + 1) this.pop();
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * Closes every sheet, from the top down.
119
+ * @returns {this}
120
+ */
121
+ clear() {
122
+ while (this.#sheets.length > 0) this.pop();
123
+ return this;
124
+ }
125
+
126
+ /**
127
+ * The sheet currently on top.
128
+ * @returns {Sheet|null}
129
+ */
130
+ top() {
131
+ return this.#sheets[this.#sheets.length - 1] ?? null;
132
+ }
133
+
134
+ /**
135
+ * How many sheets the stack holds.
136
+ * @returns {number}
137
+ */
138
+ size() {
139
+ return this.#sheets.length;
140
+ }
141
+
142
+ /**
143
+ * The sheets, bottom first.
144
+ * @returns {Sheet[]}
145
+ */
146
+ sheets() {
147
+ return [...this.#sheets];
148
+ }
149
+
150
+ /**
151
+ * Whether a sheet is in this stack.
152
+ * @param {Sheet} sheet Sheet to look for.
153
+ * @returns {boolean}
154
+ */
155
+ has(sheet) {
156
+ return this.#sheets.includes(sheet);
157
+ }
158
+
159
+ /**
160
+ * Subscribes to a stack event.
161
+ * @param {string} type Event name.
162
+ * @param {(event: any) => void} fn Listener.
163
+ * @returns {this}
164
+ */
165
+ on(type, fn) {
166
+ this.addEventListener(type, fn);
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Removes a stack event listener.
172
+ * @param {string} type Event name.
173
+ * @param {(event: any) => void} fn Listener.
174
+ * @returns {this}
175
+ */
176
+ off(type, fn) {
177
+ this.removeEventListener(type, fn);
178
+ return this;
179
+ }
180
+
181
+ /**
182
+ * Subscribes to one occurrence of a stack event.
183
+ * @param {string} type Event name.
184
+ * @param {(event: any) => void} fn Listener.
185
+ * @returns {this}
186
+ */
187
+ once(type, fn) {
188
+ this.addEventListener(type, fn, { once: true });
189
+ return this;
190
+ }
191
+
192
+ /**
193
+ * Empties the stack without closing anything. The sheets are not the stack's to destroy, so
194
+ * they are simply released with their depth styling removed.
195
+ * @returns {void}
196
+ */
197
+ destroy() {
198
+ if (this.#destroyed) return;
199
+ this.#destroyed = true;
200
+ for (const sheet of this.#sheets) {
201
+ const onClose = this.#closers.get(sheet);
202
+ if (onClose) sheet.off('close', onClose);
203
+ clearDepth(sheet);
204
+ }
205
+ this.#sheets = [];
206
+ }
207
+
208
+ /**
209
+ * Handles a sheet that closed on its own. A sheet popped through `pop()` has already been
210
+ * removed by the time its close event lands, so this is a no-op for that path.
211
+ * @param {Sheet} sheet Sheet that closed.
212
+ * @returns {void}
213
+ */
214
+ #drop(sheet) {
215
+ if (this.#sheets.includes(sheet)) this.#remove(sheet);
216
+ }
217
+
218
+ /**
219
+ * Takes a sheet out of the stack and re-numbers what is left.
220
+ * @param {Sheet} sheet Sheet to remove.
221
+ * @returns {void}
222
+ * @fires SheetStack#pop
223
+ */
224
+ #remove(sheet) {
225
+ const index = this.#sheets.indexOf(sheet);
226
+ if (index === -1) return;
227
+ this.#sheets.splice(index, 1);
228
+ const onClose = this.#closers.get(sheet);
229
+ if (onClose) sheet.off('close', onClose);
230
+ this.#closers.delete(sheet);
231
+ clearDepth(sheet);
232
+ this.#sync();
233
+ this.#emit('pop', { sheet });
234
+ }
235
+
236
+ /**
237
+ * Writes each sheet's depth, and for a cascade the distance it must clear.
238
+ * @returns {void}
239
+ */
240
+ #sync() {
241
+ const { layout, offset, scale, max } = this.options;
242
+ const cascade = layout === 'cascade';
243
+ /** Cumulative extent of the sheets in front of the one being positioned. */
244
+ let ahead = 0;
245
+
246
+ // Top first, so `ahead` accumulates from the front of the stack backwards.
247
+ for (let depth = 0; depth < this.#sheets.length; depth += 1) {
248
+ const sheet = this.#sheets[this.#sheets.length - 1 - depth];
249
+ const element = /** @type {HTMLElement} */ (sheet.el);
250
+ element.dataset.stack = cascade ? 'cascade' : 'stack';
251
+ element.dataset.depth = String(depth);
252
+ element.style.setProperty('--zx-sheet-depth', String(depth));
253
+ element.style.setProperty('--zx-sheet-stack-offset', `${Number(offset) || 0}px`);
254
+ element.style.setProperty('--zx-sheet-stack-scale', String(Number(scale) || 0));
255
+ element.toggleAttribute('data-stack-hidden', depth >= Math.max(1, Number(max) || 1));
256
+ /*
257
+ * A covered sheet in a drill-down is not just visually behind — it must leave the tab order
258
+ * too. The browser does that for stacked *modal* dialogs on its own, but a non-modal stack
259
+ * gets nothing, so the attribute is set either way.
260
+ */
261
+ element.inert = !cascade && depth > 0;
262
+ if (cascade) {
263
+ element.style.setProperty('--zx-sheet-depth-offset', `${ahead}px`);
264
+ ahead += sheet.getSize();
265
+ } else {
266
+ element.style.removeProperty('--zx-sheet-depth-offset');
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * @param {string} type
273
+ * @param {Record<string, unknown>} detail
274
+ * @returns {void}
275
+ */
276
+ #emit(type, detail) {
277
+ this.dispatchEvent(new CustomEvent(type, { detail }));
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Fired after a sheet is pushed onto the stack.
283
+ * @event SheetStack#push
284
+ * @type {CustomEvent<{sheet: Sheet, depth: number}>}
285
+ */
286
+
287
+ /**
288
+ * Fired after a sheet leaves the stack, however it was dismissed.
289
+ * @event SheetStack#pop
290
+ * @type {CustomEvent<{sheet: Sheet}>}
291
+ */
292
+
293
+ /** @param {Sheet} sheet @returns {void} */
294
+ function clearDepth(sheet) {
295
+ const element = /** @type {HTMLElement} */ (sheet.el);
296
+ if (!element) return;
297
+ delete element.dataset.stack;
298
+ delete element.dataset.depth;
299
+ element.removeAttribute('data-stack-hidden');
300
+ element.inert = false;
301
+ for (const property of [
302
+ '--zx-sheet-depth', '--zx-sheet-stack-offset', '--zx-sheet-stack-scale', '--zx-sheet-depth-offset'
303
+ ]) element.style.removeProperty(property);
304
+ }
@@ -0,0 +1,351 @@
1
+ /*
2
+ * Sheet builds on `.zx-modal` and `.zx-dialog`, so this file must be imported after both: the
3
+ * overrides below share their specificity and win on source order alone.
4
+ */
5
+ .zx-sheet {
6
+ position: fixed;
7
+ margin: 0;
8
+ padding: 0;
9
+ max-inline-size: 100vw;
10
+ max-block-size: 100dvh;
11
+ border: 0;
12
+ border-radius: 0;
13
+ /* The header and footer stay put; `.zx-dialog__body` is what scrolls. */
14
+ overflow: hidden;
15
+ box-shadow: var(--zx-overlay-shadow);
16
+ }
17
+
18
+ /* Horizontal sheets: full height, sized across the inline axis. */
19
+ .zx-sheet[data-side="start"],
20
+ .zx-sheet[data-side="end"] {
21
+ inset-block: 0;
22
+ block-size: 100dvh;
23
+ inline-size: var(--zx-sheet-size, min(24rem, 100vw));
24
+ }
25
+
26
+ .zx-sheet[data-side="start"] {
27
+ inset-inline-start: 0;
28
+ inset-inline-end: auto;
29
+ border-inline-end: 1px solid var(--zx-color-border);
30
+ --zx-sheet-enter: -100% 0;
31
+ }
32
+
33
+ .zx-sheet[data-side="end"] {
34
+ inset-inline-start: auto;
35
+ inset-inline-end: 0;
36
+ border-inline-start: 1px solid var(--zx-color-border);
37
+ --zx-sheet-enter: 100% 0;
38
+ }
39
+
40
+ /*
41
+ * `translate` is physical, so the logical sides need the one direction-aware rule in the library.
42
+ * Attribute rather than `:dir()` because the latter only reached Chrome in December 2023, at the
43
+ * very edge of the supported baseline, and this is the sole place it would buy anything.
44
+ */
45
+ [dir="rtl"] .zx-sheet[data-side="start"] { --zx-sheet-enter: 100% 0; }
46
+ [dir="rtl"] .zx-sheet[data-side="end"] { --zx-sheet-enter: -100% 0; }
47
+
48
+ /* Vertical sheets: full width, sized down the block axis, inner corners rounded like a drawer. */
49
+ .zx-sheet[data-side="top"],
50
+ .zx-sheet[data-side="bottom"] {
51
+ inset-inline: 0;
52
+ inline-size: 100vw;
53
+ block-size: var(--zx-sheet-size, min(60dvh, 100dvh));
54
+ }
55
+
56
+ .zx-sheet[data-side="top"] {
57
+ inset-block-start: 0;
58
+ inset-block-end: auto;
59
+ border-block-end: 1px solid var(--zx-color-border);
60
+ border-end-start-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
61
+ border-end-end-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
62
+ --zx-sheet-enter: 0 -100%;
63
+ }
64
+
65
+ .zx-sheet[data-side="bottom"] {
66
+ inset-block-start: auto;
67
+ inset-block-end: 0;
68
+ border-block-start: 1px solid var(--zx-color-border);
69
+ border-start-start-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
70
+ border-start-end-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
71
+ --zx-sheet-enter: 0 100%;
72
+ }
73
+
74
+ /*
75
+ * Backdrop treatments. Only a modal sheet has a `::backdrop` at all, which is why `backdrop` is
76
+ * documented as meaningful only alongside `modal: true`.
77
+ */
78
+ .zx-sheet[data-backdrop="none"]::backdrop {
79
+ background: transparent;
80
+ }
81
+
82
+ .zx-sheet[data-backdrop="blur"]::backdrop {
83
+ backdrop-filter: blur(var(--zx-overlay-blur));
84
+ }
85
+
86
+ /*
87
+ * Entry and exit both animate. A `<dialog>` normally cannot animate out — `close()` applies
88
+ * `display: none` immediately — so `display` and `overlay` transition with `allow-discrete`, which
89
+ * defers the discrete jump until the rest of the transition has finished. `@starting-style` gives
90
+ * the entry its from-state. Where either is unsupported the sheet simply appears and disappears.
91
+ */
92
+ @media (prefers-reduced-motion: no-preference) {
93
+ .zx-sheet {
94
+ transition:
95
+ translate var(--zx-dur-base) var(--zx-ease),
96
+ opacity var(--zx-dur-base) var(--zx-ease),
97
+ display var(--zx-dur-base) allow-discrete,
98
+ overlay var(--zx-dur-base) allow-discrete;
99
+ }
100
+
101
+ /* Modal's entry keyframes animate the centre-screen case and would fight the slide. */
102
+ .zx-sheet[data-state="open"] {
103
+ animation: none;
104
+ }
105
+
106
+ .zx-sheet:not([open]) {
107
+ translate: var(--zx-sheet-enter, 0 0);
108
+ opacity: 0;
109
+ }
110
+
111
+ @starting-style {
112
+ .zx-sheet[open] {
113
+ translate: var(--zx-sheet-enter, 0 0);
114
+ opacity: 0;
115
+ }
116
+ }
117
+
118
+ .zx-sheet::backdrop {
119
+ transition:
120
+ opacity var(--zx-dur-base) var(--zx-ease),
121
+ display var(--zx-dur-base) allow-discrete,
122
+ overlay var(--zx-dur-base) allow-discrete;
123
+ }
124
+
125
+ .zx-sheet[data-state="open"]::backdrop {
126
+ animation: none;
127
+ }
128
+
129
+ .zx-sheet:not([open])::backdrop {
130
+ opacity: 0;
131
+ }
132
+
133
+ @starting-style {
134
+ .zx-sheet[open]::backdrop {
135
+ opacity: 0;
136
+ }
137
+ }
138
+ }
139
+
140
+ /*
141
+ * A docked sheet is a track in its dock's flow, not an overlay: it drops out of fixed positioning
142
+ * and takes its size from the dock's own custom property, the same one the dock's panes use. It is
143
+ * always non-modal while docked, so there is no backdrop to consider.
144
+ *
145
+ * Closing animates the track to zero rather than sliding the sheet away — in the flow there is
146
+ * nowhere to slide to, and collapsing the space is what the surrounding layout should see.
147
+ */
148
+ .zx-sheet[data-docked] {
149
+ position: relative;
150
+ inset: auto;
151
+ flex: 0 0 var(--zx-pane-size, var(--zx-sheet-size, min(24rem, 100%)));
152
+ inline-size: auto;
153
+ block-size: auto;
154
+ max-inline-size: none;
155
+ max-block-size: none;
156
+ min-inline-size: 0;
157
+ min-block-size: 0;
158
+ border-radius: 0;
159
+ box-shadow: none;
160
+ }
161
+
162
+ .zx-dock[data-orientation="vertical"] > .zx-sheet[data-docked] {
163
+ border-inline: 0;
164
+ border-block: 1px solid var(--zx-color-border);
165
+ }
166
+
167
+ .zx-dock[data-orientation="horizontal"] > .zx-sheet[data-docked] {
168
+ border-block: 0;
169
+ border-inline: 1px solid var(--zx-color-border);
170
+ }
171
+
172
+ @media (prefers-reduced-motion: no-preference) {
173
+ .zx-sheet[data-docked] {
174
+ transition:
175
+ flex-basis var(--zx-dur-base) var(--zx-ease),
176
+ opacity var(--zx-dur-base) var(--zx-ease),
177
+ display var(--zx-dur-base) allow-discrete,
178
+ overlay var(--zx-dur-base) allow-discrete;
179
+ /* The slide belongs to the overlay life; in the flow the track itself is what moves. */
180
+ translate: none;
181
+ }
182
+
183
+ .zx-sheet[data-docked]:not([open]) {
184
+ flex-basis: 0;
185
+ translate: none;
186
+ opacity: 0;
187
+ }
188
+
189
+ @starting-style {
190
+ .zx-sheet[data-docked][open] {
191
+ flex-basis: 0;
192
+ translate: none;
193
+ opacity: 0;
194
+ }
195
+ }
196
+ }
197
+
198
+ /*
199
+ * The grab handle sits just inside the sheet's inner edge — the one facing the page. It is a
200
+ * `separator` with live values, so it is draggable, focusable, and steppable with the arrow keys.
201
+ * The hit area is deliberately wider than the pill it draws.
202
+ */
203
+ .zx-sheet__handle {
204
+ position: absolute;
205
+ z-index: 1;
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ touch-action: none;
210
+ user-select: none;
211
+ }
212
+
213
+ .zx-sheet__handle::after {
214
+ content: "";
215
+ border-radius: var(--zx-radius-full);
216
+ background: var(--zx-color-border-strong);
217
+ opacity: 0;
218
+ transition: opacity var(--zx-dur-fast) var(--zx-ease);
219
+ }
220
+
221
+ .zx-sheet__handle:hover::after,
222
+ .zx-sheet__handle:focus-visible::after,
223
+ .zx-sheet[data-dragging] .zx-sheet__handle::after {
224
+ opacity: 1;
225
+ }
226
+
227
+ .zx-sheet__handle:focus-visible {
228
+ outline: none;
229
+ box-shadow: var(--zx-focus-ring);
230
+ }
231
+
232
+ /* Inner edge for each side, and the pill drawn across it. */
233
+ .zx-sheet[data-side="start"] > .zx-sheet__handle,
234
+ .zx-sheet[data-side="end"] > .zx-sheet__handle {
235
+ inset-block: 0;
236
+ inline-size: var(--zx-space-3);
237
+ cursor: col-resize;
238
+ }
239
+
240
+ .zx-sheet[data-side="start"] > .zx-sheet__handle { inset-inline-end: 0; }
241
+ .zx-sheet[data-side="end"] > .zx-sheet__handle { inset-inline-start: 0; }
242
+
243
+ .zx-sheet[data-side="start"] > .zx-sheet__handle::after,
244
+ .zx-sheet[data-side="end"] > .zx-sheet__handle::after {
245
+ inline-size: 3px;
246
+ block-size: var(--zx-space-8);
247
+ }
248
+
249
+ .zx-sheet[data-side="top"] > .zx-sheet__handle,
250
+ .zx-sheet[data-side="bottom"] > .zx-sheet__handle {
251
+ inset-inline: 0;
252
+ block-size: var(--zx-space-4);
253
+ cursor: row-resize;
254
+ }
255
+
256
+ .zx-sheet[data-side="top"] > .zx-sheet__handle { inset-block-end: 0; }
257
+ .zx-sheet[data-side="bottom"] > .zx-sheet__handle { inset-block-start: 0; }
258
+
259
+ .zx-sheet[data-side="top"] > .zx-sheet__handle::after,
260
+ .zx-sheet[data-side="bottom"] > .zx-sheet__handle::after {
261
+ inline-size: var(--zx-space-8);
262
+ block-size: 4px;
263
+ }
264
+
265
+ /* A drawer's handle is a standing invitation, so it does not wait to be hovered. */
266
+ .zx-sheet[data-side="top"] > .zx-sheet__handle::after,
267
+ .zx-sheet[data-side="bottom"] > .zx-sheet__handle::after {
268
+ opacity: 1;
269
+ }
270
+
271
+ /* A docked sheet is resized by the dock's divider, so its own handle stands down. */
272
+ .zx-sheet[data-docked] > .zx-sheet__handle {
273
+ display: none;
274
+ }
275
+
276
+ /* Nothing may animate while the pointer is driving the size, or the sheet lags behind it. */
277
+ .zx-sheet[data-dragging] {
278
+ transition: none;
279
+ }
280
+
281
+ /*
282
+ * Stacking. Depth is written by `SheetStack` as `--zx-sheet-depth`; the direction it moves in is
283
+ * a per-side unit vector pointing at the sheet's own edge, so one rule covers all four sides.
284
+ *
285
+ * `transform` rather than `translate`, because the enter and exit animation already owns
286
+ * `translate` — the two are independent properties and compose, so a sheet can slide in and sit at
287
+ * a depth at the same time.
288
+ */
289
+ .zx-sheet[data-side="start"] { --zx-sheet-dx: -1; --zx-sheet-dy: 0; }
290
+ .zx-sheet[data-side="end"] { --zx-sheet-dx: 1; --zx-sheet-dy: 0; }
291
+ .zx-sheet[data-side="top"] { --zx-sheet-dx: 0; --zx-sheet-dy: -1; }
292
+ .zx-sheet[data-side="bottom"] { --zx-sheet-dx: 0; --zx-sheet-dy: 1; }
293
+
294
+ /* Scaling a covered sheet shrinks it toward its own edge, so it never unglues from it. */
295
+ .zx-sheet[data-side="start"] { transform-origin: left center; }
296
+ .zx-sheet[data-side="end"] { transform-origin: right center; }
297
+ .zx-sheet[data-side="top"] { transform-origin: center top; }
298
+ .zx-sheet[data-side="bottom"] { transform-origin: center bottom; }
299
+
300
+ .zx-sheet[data-stack="stack"] {
301
+ transform:
302
+ translate(
303
+ calc(var(--zx-sheet-dx, 0) * var(--zx-sheet-depth, 0) * var(--zx-sheet-stack-offset, 24px)),
304
+ calc(var(--zx-sheet-dy, 0) * var(--zx-sheet-depth, 0) * var(--zx-sheet-stack-offset, 24px))
305
+ )
306
+ scale(calc(1 - var(--zx-sheet-depth, 0) * var(--zx-sheet-stack-scale, 0.04)));
307
+ }
308
+
309
+ /* A cascade moves each covered sheet clear of everything in front of it, so all stay readable. */
310
+ .zx-sheet[data-stack="cascade"] {
311
+ transform: translate(
312
+ calc(var(--zx-sheet-dx, 0) * var(--zx-sheet-depth-offset, 0px) * -1),
313
+ calc(var(--zx-sheet-dy, 0) * var(--zx-sheet-depth-offset, 0px) * -1)
314
+ );
315
+ }
316
+
317
+ /* Past `max` a sheet stays open and in the stack, but stops being drawn. */
318
+ .zx-sheet[data-stack-hidden] {
319
+ opacity: 0;
320
+ pointer-events: none;
321
+ }
322
+
323
+ /*
324
+ * Only the topmost backdrop shades the page — one per depth would compound into black.
325
+ */
326
+ .zx-sheet[data-depth]:not([data-depth="0"])::backdrop {
327
+ background: transparent;
328
+ backdrop-filter: none;
329
+ }
330
+
331
+ /*
332
+ * A cascade shades nothing at all. Its whole point is that the covered sheets stay readable and
333
+ * usable beside the top one, and a backdrop is painted directly beneath its own dialog in the top
334
+ * layer — so the topmost sheet's backdrop would dim exactly the sheets the layout exists to keep
335
+ * available. Pair a cascade with `modal: false` when the page behind should stay live too.
336
+ */
337
+ .zx-sheet[data-stack="cascade"]::backdrop {
338
+ background: transparent;
339
+ backdrop-filter: none;
340
+ }
341
+
342
+ @media (prefers-reduced-motion: no-preference) {
343
+ .zx-sheet[data-stack] {
344
+ transition:
345
+ transform var(--zx-dur-base) var(--zx-ease),
346
+ translate var(--zx-dur-base) var(--zx-ease),
347
+ opacity var(--zx-dur-base) var(--zx-ease),
348
+ display var(--zx-dur-base) allow-discrete,
349
+ overlay var(--zx-dur-base) allow-discrete;
350
+ }
351
+ }