@zeyos/zx 4.4.0 → 5.2.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.
- package/CHANGELOG.md +447 -0
- package/README.md +44 -11
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +31 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +15 -0
- package/dist/types/components/calendar/calendar.d.ts +67 -5
- package/dist/types/components/card-view/card-view.d.ts +7 -4
- package/dist/types/components/card-view/record-card.d.ts +11 -0
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +6 -17
- package/dist/types/components/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +46 -6
- package/dist/types/components/dropdown/dropdown.d.ts +24 -9
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +54 -2
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +26 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- package/dist/types/components/file-list/file-list.d.ts +35 -3
- package/dist/types/components/filter/filter.d.ts +127 -3
- package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
- package/dist/types/components/launcher/launcher.d.ts +268 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
- package/dist/types/components/menu-button/menu-button.d.ts +39 -5
- package/dist/types/components/message/message.d.ts +132 -17
- package/dist/types/components/modal/modal.d.ts +43 -2
- package/dist/types/components/module-bar/module-bar.d.ts +395 -0
- package/dist/types/components/pagination/pagination.d.ts +17 -3
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +17 -3
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
- package/dist/types/components/tabbox/tabbox.d.ts +9 -0
- package/dist/types/components/table/table.d.ts +311 -18
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/components/view/record-view.d.ts +59 -2
- package/dist/types/components/view-settings/view-settings.d.ts +879 -0
- package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +42 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +10 -5
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +9 -0
- package/dist/types/zeyos/module-bar.d.ts +176 -0
- package/dist/types/zeyos/query.d.ts +110 -5
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +345 -22
- package/dist/types/zeyos/view.d.ts +26 -2
- package/dist/zx-zeyos.esm.js +6178 -1008
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +2861 -827
- package/dist/zx.esm.js +8909 -1480
- package/dist/zx.esm.min.js +55 -7
- package/dist/zx.global.js +8911 -1482
- package/dist/zx.global.min.js +55 -7
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1079 -117
- package/docs/llms.md +956 -154
- package/docs/llms.txt +9 -5
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-item/activity-item.css +1 -1
- package/src/components/activity-item/activity-item.js +3 -0
- package/src/components/activity-list/activity-list.css +38 -3
- package/src/components/activity-list/activity-list.js +61 -1
- package/src/components/app-icon/app-icon.css +14 -3
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +59 -79
- package/src/components/app-sidebar/app-sidebar.css +304 -168
- package/src/components/app-sidebar/app-sidebar.js +667 -46
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +47 -47
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +4 -4
- package/src/components/card-view/card-view.css +14 -12
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +7 -6
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +9 -4
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/time-picker.css +3 -3
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +99 -17
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +159 -72
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +1 -1
- package/src/components/entity-ref/entity-ref.js +1 -1
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +101 -29
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +38 -64
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +307 -11
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +16 -14
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +11 -3
- package/src/components/kanban-view/kanban-view.css +42 -18
- package/src/components/kanban-view/kanban-view.js +9 -6
- package/src/components/launcher/launcher.css +108 -26
- package/src/components/launcher/launcher.js +530 -42
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +40 -14
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +72 -4
- package/src/components/menu-button/menu-button.js +160 -18
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +264 -22
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +83 -15
- package/src/components/module-bar/module-bar.css +486 -0
- package/src/components/module-bar/module-bar.js +1074 -0
- package/src/components/multi-value-editor/multi-value-editor.css +1 -1
- package/src/components/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +3 -1
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +13 -11
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +4 -3
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +8 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +2 -0
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +92 -47
- package/src/components/sheet/sheet.css +12 -9
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +18 -4
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +42 -47
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +41 -32
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +187 -37
- package/src/components/table/table.js +718 -73
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tag-picker/tag-picker.css +83 -5
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +44 -19
- package/src/components/tree/tree.css +26 -17
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/view/record-view.css +1 -1
- package/src/components/view/record-view.js +178 -18
- package/src/components/view-settings/view-settings.css +285 -0
- package/src/components/view-settings/view-settings.js +1830 -0
- package/src/components/workspace-switcher/workspace-switcher.css +188 -0
- package/src/components/workspace-switcher/workspace-switcher.js +463 -0
- package/src/core/breakpoint.js +1 -1
- package/src/core/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +204 -61
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +77 -3
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +10 -5
- package/src/internal/app-rail.js +135 -55
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +69 -27
- package/src/zeyos/index.js +4 -1
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +197 -13
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +287 -84
- package/src/zeyos/view.js +49 -93
- package/styles/base.css +72 -1
- package/styles/icons.css +11 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +117 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +140 -43
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +3 -1
- package/styles/tokens/density.css +0 -9
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
|
-
import { h } from '../../core/dom.js';
|
|
2
|
+
import { h, resolveElement } from '../../core/dom.js';
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
|
-
import {
|
|
4
|
+
import { overlayScope } from '../../core/overlay-host.js';
|
|
5
|
+
import { overlayStack } from '../../core/overlay-stack.js';
|
|
6
|
+
import { button } from '../button/button.js';
|
|
5
7
|
|
|
6
8
|
/** @typedef {'info'|'success'|'warning'|'error'} MessageKind */
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The one command a message may carry, drawn as a small button beside its text.
|
|
12
|
+
* @typedef {Object} MessageAction
|
|
13
|
+
* @property {string} label Button text: a verb naming what happens ("Undo", "Approve", "Retry").
|
|
14
|
+
* @property {(event: MouseEvent) => void} [onclick] Runs when the button is activated, by pointer
|
|
15
|
+
* or keyboard; the message closes right after it.
|
|
16
|
+
* @property {'default'|'primary'|'danger'|'ghost'} [kind='default'] Button kind; the default is the
|
|
17
|
+
* secondary button.
|
|
18
|
+
*/
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* @typedef {Object} MessageShowOptions
|
|
10
22
|
* @property {MessageKind|'danger'} [kind='info'] Message intent (`danger` aliases `error`).
|
|
11
|
-
* @property {number} [timeout=4000] Auto-dismiss delay in milliseconds; zero persists.
|
|
23
|
+
* @property {number} [timeout=4000] Auto-dismiss delay in milliseconds; zero persists. A message
|
|
24
|
+
* with an `action` stays at least 8000ms.
|
|
12
25
|
* @property {boolean} [closable=true] Whether to render a close button.
|
|
26
|
+
* @property {MessageAction|null} [action=null] A command button inside the message. The message
|
|
27
|
+
* never times out while the pointer is over it or focus is inside it.
|
|
28
|
+
* @property {Node|{toElement: () => Node|null}|null} [leading=null] Visual shown in place of the
|
|
29
|
+
* kind icon, such as an Avatar's element. It is decorative — the message text must say what it
|
|
30
|
+
* shows.
|
|
13
31
|
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns a floating toast.
|
|
14
32
|
*/
|
|
15
33
|
|
|
@@ -21,7 +39,15 @@ import { overlayHost } from '../../core/overlay-host.js';
|
|
|
21
39
|
* @property {number} [maxVisible=5] Maximum number of visible messages.
|
|
22
40
|
*/
|
|
23
41
|
|
|
24
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Returned by every `show()`: closes that message early. Closing one that already closed, or one
|
|
44
|
+
* still waiting in the queue, is safe.
|
|
45
|
+
* @typedef {Object} MessageHandle
|
|
46
|
+
* @property {() => void} close Closes the message.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** The shortest time a message that carries an action stays, in milliseconds. */
|
|
50
|
+
export const MESSAGE_ACTION_MIN_TIMEOUT = 8000;
|
|
25
51
|
|
|
26
52
|
/**
|
|
27
53
|
* @typedef {Object} ProgressHandle
|
|
@@ -135,9 +161,6 @@ export class Message extends Component {
|
|
|
135
161
|
maxVisible: 5
|
|
136
162
|
};
|
|
137
163
|
|
|
138
|
-
/** @type {WeakMap<Element, Message>} */
|
|
139
|
-
static #floating = new WeakMap();
|
|
140
|
-
|
|
141
164
|
/** @returns {HTMLElement} */
|
|
142
165
|
render() {
|
|
143
166
|
const created = !this.el;
|
|
@@ -145,6 +168,14 @@ export class Message extends Component {
|
|
|
145
168
|
this._createdRoot = created;
|
|
146
169
|
this._original = created ? null : snapshot(root);
|
|
147
170
|
this._entries = new Set();
|
|
171
|
+
/** @type {Element|null} The theme scope this instance is the floating region of; null when inline. */
|
|
172
|
+
this._floatingHost = null;
|
|
173
|
+
/** @type {Element|null} Where the floating region was last put: its scope, or the topmost modal. */
|
|
174
|
+
this._placedIn = null;
|
|
175
|
+
/** @type {import('../../core/overlay-stack.js').OverlayHandle|null} The region's toast entry. */
|
|
176
|
+
this._overlay = null;
|
|
177
|
+
/** @type {(() => void)|null} Stops following the overlay stack. */
|
|
178
|
+
this._unfollow = null;
|
|
148
179
|
this._queue = new MessageQueue(normalizeLimit(this.options.maxVisible), (entry) => {
|
|
149
180
|
this._activate(entry);
|
|
150
181
|
});
|
|
@@ -161,10 +192,13 @@ export class Message extends Component {
|
|
|
161
192
|
* @returns {MessageHandle}
|
|
162
193
|
*/
|
|
163
194
|
show(msg, options = {}) {
|
|
195
|
+
const action = normalizeMessageAction(options.action);
|
|
164
196
|
const settings = {
|
|
165
197
|
kind: normalizeKind(options.kind ?? this.options.kind),
|
|
166
|
-
timeout:
|
|
167
|
-
closable: options.closable ?? Boolean(this.options.closable)
|
|
198
|
+
timeout: messageTimeout(options.timeout ?? this.options.timeout, action !== null),
|
|
199
|
+
closable: options.closable ?? Boolean(this.options.closable),
|
|
200
|
+
action,
|
|
201
|
+
leading: resolveLeading(options.leading)
|
|
168
202
|
};
|
|
169
203
|
const entry = this._createEntry(msg, settings);
|
|
170
204
|
this._entries.add(entry);
|
|
@@ -232,10 +266,37 @@ export class Message extends Component {
|
|
|
232
266
|
return this.#floatingRegion(options.scope)._progress(text);
|
|
233
267
|
}
|
|
234
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Removes a floating region at once, toasts and all, without waiting for them to time out.
|
|
271
|
+
*
|
|
272
|
+
* A region already removes itself when its last toast closes, and one whose host has left the
|
|
273
|
+
* document is replaced on the next toast, so this is only for a host being torn down while
|
|
274
|
+
* toasts are still showing — an application view unmounting its themed shell, say.
|
|
275
|
+
* @param {Element|string} scope Element or selector whose nearest Zx theme scope owns the region,
|
|
276
|
+
* as passed to `show()`.
|
|
277
|
+
* @returns {boolean} Whether there was a region to remove.
|
|
278
|
+
*/
|
|
279
|
+
static disposeRegion(scope) {
|
|
280
|
+
if (scope == null) return false;
|
|
281
|
+
const context = resolveElement(scope);
|
|
282
|
+
if (!context) return false;
|
|
283
|
+
const region = Message.#regionFor(overlayScope(context));
|
|
284
|
+
if (!region) return false;
|
|
285
|
+
region._disposeFloating();
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
235
289
|
/** @returns {void} */
|
|
236
290
|
destroy() {
|
|
237
291
|
if (this._cleaned) return;
|
|
238
292
|
this._cleaned = true;
|
|
293
|
+
this._unfollow?.();
|
|
294
|
+
this._unfollow = null;
|
|
295
|
+
const overlay = this._overlay;
|
|
296
|
+
this._overlay = null;
|
|
297
|
+
overlay?.release();
|
|
298
|
+
this._floatingHost = null;
|
|
299
|
+
this._placedIn = null;
|
|
239
300
|
for (const entry of this._entries) {
|
|
240
301
|
clearEntryTimers(entry);
|
|
241
302
|
entry.node.remove();
|
|
@@ -288,13 +349,13 @@ export class Message extends Component {
|
|
|
288
349
|
|
|
289
350
|
/**
|
|
290
351
|
* @param {string|Node} msg
|
|
291
|
-
* @param {{kind: MessageKind, timeout: number, closable: boolean}} settings
|
|
352
|
+
* @param {{kind: MessageKind, timeout: number, closable: boolean, action?: MessageAction|null, leading?: Node|null}} settings
|
|
292
353
|
* @param {{pct: number, text: string}|null} [progress=null]
|
|
293
354
|
* @returns {any}
|
|
294
355
|
*/
|
|
295
356
|
_createEntry(msg, settings, progress = null) {
|
|
296
357
|
const iconHost = h('span', { class: 'zx-message__icon', ariaHidden: 'true' },
|
|
297
|
-
icon(settings.kind, { size: 18 })
|
|
358
|
+
settings.leading ?? icon(settings.kind, { size: 18 })
|
|
298
359
|
);
|
|
299
360
|
const content = h('div', { class: 'zx-message__content' });
|
|
300
361
|
if (isNode(msg)) content.append(msg);
|
|
@@ -315,9 +376,13 @@ export class Message extends Component {
|
|
|
315
376
|
timer: null,
|
|
316
377
|
exitTimer: null,
|
|
317
378
|
closed: false,
|
|
379
|
+
hovered: false,
|
|
380
|
+
focused: false,
|
|
381
|
+
returnFocus: null,
|
|
318
382
|
progressBar: null,
|
|
319
383
|
progressFill: null
|
|
320
384
|
};
|
|
385
|
+
if (settings.leading) node.dataset.leading = '';
|
|
321
386
|
|
|
322
387
|
if (progress) {
|
|
323
388
|
node.dataset.progress = '';
|
|
@@ -336,6 +401,21 @@ export class Message extends Component {
|
|
|
336
401
|
syncProgress(entry, progress);
|
|
337
402
|
}
|
|
338
403
|
|
|
404
|
+
if (settings.action) {
|
|
405
|
+
const { label, kind, onclick } = settings.action;
|
|
406
|
+
const control = button({ label, kind, size: 'sm' });
|
|
407
|
+
control.classList.add('zx-message__action');
|
|
408
|
+
node.append(control);
|
|
409
|
+
this.listen(control, 'click', (event) => {
|
|
410
|
+
if (entry.closed) return;
|
|
411
|
+
try {
|
|
412
|
+
onclick?.(/** @type {MouseEvent} */ (event));
|
|
413
|
+
} finally {
|
|
414
|
+
this._close(entry);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
339
419
|
if (settings.closable) {
|
|
340
420
|
const close = h('button', {
|
|
341
421
|
class: 'zx-icon-btn zx-message__close',
|
|
@@ -346,8 +426,31 @@ export class Message extends Component {
|
|
|
346
426
|
node.append(close);
|
|
347
427
|
this.listen(close, 'click', () => this._close(entry));
|
|
348
428
|
}
|
|
349
|
-
|
|
350
|
-
|
|
429
|
+
// Hover and focus each hold the timer (WCAG 2.2.1): a reader moving to the action or the close
|
|
430
|
+
// button must not have the message vanish under them, and it counts down again only once
|
|
431
|
+
// neither holds it.
|
|
432
|
+
this.listen(node, 'mouseenter', () => {
|
|
433
|
+
entry.hovered = true;
|
|
434
|
+
this._pause(entry);
|
|
435
|
+
});
|
|
436
|
+
this.listen(node, 'mouseleave', () => {
|
|
437
|
+
entry.hovered = false;
|
|
438
|
+
this._resume(entry);
|
|
439
|
+
});
|
|
440
|
+
this.listen(node, 'focusin', (event) => {
|
|
441
|
+
if (!entry.focused) {
|
|
442
|
+
const from = /** @type {FocusEvent} */ (event).relatedTarget;
|
|
443
|
+
entry.returnFocus = isNode(from) && !node.contains(/** @type {Node} */ (from)) ? from : null;
|
|
444
|
+
}
|
|
445
|
+
entry.focused = true;
|
|
446
|
+
this._pause(entry);
|
|
447
|
+
});
|
|
448
|
+
this.listen(node, 'focusout', (event) => {
|
|
449
|
+
const to = /** @type {FocusEvent} */ (event).relatedTarget;
|
|
450
|
+
if (isNode(to) && node.contains(/** @type {Node} */ (to))) return;
|
|
451
|
+
entry.focused = false;
|
|
452
|
+
this._resume(entry);
|
|
453
|
+
});
|
|
351
454
|
return entry;
|
|
352
455
|
}
|
|
353
456
|
|
|
@@ -361,7 +464,7 @@ export class Message extends Component {
|
|
|
361
464
|
|
|
362
465
|
/** @param {any} entry @returns {void} */
|
|
363
466
|
_schedule(entry) {
|
|
364
|
-
if (entry.closed || entry.remaining <= 0) return;
|
|
467
|
+
if (entry.closed || entry.remaining <= 0 || entry.hovered || entry.focused) return;
|
|
365
468
|
entry.startedAt = Date.now();
|
|
366
469
|
entry.timer = setTimeout(() => this._close(entry), entry.remaining);
|
|
367
470
|
}
|
|
@@ -376,7 +479,7 @@ export class Message extends Component {
|
|
|
376
479
|
|
|
377
480
|
/** @param {any} entry @returns {void} */
|
|
378
481
|
_resume(entry) {
|
|
379
|
-
if (entry.closed || entry.timer !== null) return;
|
|
482
|
+
if (entry.closed || entry.timer !== null || entry.hovered || entry.focused) return;
|
|
380
483
|
if (entry.remaining <= 0) {
|
|
381
484
|
this._close(entry);
|
|
382
485
|
return;
|
|
@@ -389,10 +492,12 @@ export class Message extends Component {
|
|
|
389
492
|
if (entry.closed) return;
|
|
390
493
|
entry.closed = true;
|
|
391
494
|
clearEntryTimers(entry);
|
|
495
|
+
returnFocus(entry);
|
|
392
496
|
if (!this._queue.isActive(entry)) {
|
|
393
497
|
this._queue.remove(entry);
|
|
394
498
|
entry.node.remove();
|
|
395
499
|
this._entries.delete(entry);
|
|
500
|
+
this._releaseIfEmpty();
|
|
396
501
|
return;
|
|
397
502
|
}
|
|
398
503
|
|
|
@@ -415,6 +520,44 @@ export class Message extends Component {
|
|
|
415
520
|
entry.node.remove();
|
|
416
521
|
this._entries.delete(entry);
|
|
417
522
|
this._queue.remove(entry);
|
|
523
|
+
this._releaseIfEmpty();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Takes a floating region out of the document once its last toast has gone, so an emptied host
|
|
528
|
+
* is not left holding a region nobody uses. Inline areas stay: their element is the caller's.
|
|
529
|
+
* @returns {void}
|
|
530
|
+
*/
|
|
531
|
+
_releaseIfEmpty() {
|
|
532
|
+
if (!this._floatingHost || this._entries.size > 0) return;
|
|
533
|
+
Message.#release(this);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Puts a floating region where it can be seen and used: inside the topmost open modal dialog
|
|
538
|
+
* while one is open — outside it, `showModal()` would leave every toast unclickable — and in its
|
|
539
|
+
* own theme scope otherwise. Re-showing it after a move puts it back on top of the top layer.
|
|
540
|
+
* @returns {void}
|
|
541
|
+
*/
|
|
542
|
+
_rehome() {
|
|
543
|
+
const host = this._floatingHost;
|
|
544
|
+
if (!host) return;
|
|
545
|
+
const target = overlayStack.topmostModal() ?? host;
|
|
546
|
+
if (this.el.parentElement === target && this._placedIn === target) return;
|
|
547
|
+
target.append(this.el);
|
|
548
|
+
this._placedIn = target;
|
|
549
|
+
const region = /** @type {HTMLElement} */ (this.el);
|
|
550
|
+
if (typeof region.showPopover === 'function' && !region.matches(':popover-open')) region.showPopover();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Destroys a floating region and removes its element.
|
|
555
|
+
* @returns {void}
|
|
556
|
+
*/
|
|
557
|
+
_disposeFloating() {
|
|
558
|
+
const region = this.el;
|
|
559
|
+
this.destroy();
|
|
560
|
+
region.remove();
|
|
418
561
|
}
|
|
419
562
|
|
|
420
563
|
/**
|
|
@@ -428,23 +571,58 @@ export class Message extends Component {
|
|
|
428
571
|
return message === key ? fallback : message;
|
|
429
572
|
}
|
|
430
573
|
|
|
431
|
-
/**
|
|
574
|
+
/**
|
|
575
|
+
* Finds or creates the floating region for a scope. A cached region is reused only while it is
|
|
576
|
+
* still where it was put: a host emptied by `replaceChildren()` would otherwise keep receiving
|
|
577
|
+
* toasts in a node nobody can see.
|
|
578
|
+
* @param {Element|string|null|undefined} scope
|
|
579
|
+
* @returns {Message}
|
|
580
|
+
*/
|
|
432
581
|
static #floatingRegion(scope) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (
|
|
582
|
+
let host = overlayScope(scope ?? null);
|
|
583
|
+
// A scope that has itself left the document cannot show anything; the toast still must be seen.
|
|
584
|
+
if (!host.isConnected) host = overlayScope(null);
|
|
585
|
+
const existing = this.#regionFor(host);
|
|
586
|
+
if (existing && existing.el.isConnected && existing.el.parentElement === existing._placedIn) {
|
|
587
|
+
existing._rehome();
|
|
588
|
+
return existing;
|
|
589
|
+
}
|
|
590
|
+
existing?._disposeFloating();
|
|
436
591
|
const region = h('div', {
|
|
437
592
|
class: 'zx-message-region',
|
|
438
593
|
popover: 'manual',
|
|
439
594
|
role: 'status',
|
|
440
595
|
ariaLive: 'polite'
|
|
441
596
|
});
|
|
442
|
-
host.append(region);
|
|
443
597
|
const floating = new Message(region);
|
|
444
|
-
|
|
445
|
-
|
|
598
|
+
floating._floatingHost = host;
|
|
599
|
+
// A toast entry on the overlay stack: the registry of regions, and the reason a press on a
|
|
600
|
+
// toast dismisses no drawer or menu. Following the stack moves the region into a modal dialog
|
|
601
|
+
// when one opens and back out when it closes.
|
|
602
|
+
floating._overlay = overlayStack.push({ kind: 'toast', root: region, owner: floating });
|
|
603
|
+
floating._unfollow = overlayStack.subscribe(() => floating._rehome());
|
|
604
|
+
floating._rehome();
|
|
446
605
|
return floating;
|
|
447
606
|
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* The live floating region of a theme scope, from the overlay stack's toast entries.
|
|
610
|
+
* @param {Element} host Theme scope.
|
|
611
|
+
* @returns {Message|null}
|
|
612
|
+
*/
|
|
613
|
+
static #regionFor(host) {
|
|
614
|
+
for (const entry of overlayStack.entries()) {
|
|
615
|
+
if (entry.kind === 'toast' && entry.owner instanceof Message && entry.owner._floatingHost === host) {
|
|
616
|
+
return entry.owner;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** @param {Message} region Floating region whose last toast closed. @returns {void} */
|
|
623
|
+
static #release(region) {
|
|
624
|
+
region._disposeFloating();
|
|
625
|
+
}
|
|
448
626
|
}
|
|
449
627
|
|
|
450
628
|
/** @param {unknown} value @returns {MessageKind} */
|
|
@@ -460,6 +638,70 @@ function normalizeTimeout(value) {
|
|
|
460
638
|
return Number.isFinite(timeout) ? Math.max(0, timeout) : 4000;
|
|
461
639
|
}
|
|
462
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Settles how long a message stays. A message that carries an action stays at least
|
|
643
|
+
* `MESSAGE_ACTION_MIN_TIMEOUT`, so there is time to reach it; zero still means it stays until
|
|
644
|
+
* closed, and anything that is not a finite number falls back to the 4000ms default.
|
|
645
|
+
* @param {unknown} timeout Requested auto-dismiss delay in milliseconds.
|
|
646
|
+
* @param {boolean} [hasAction=false] Whether the message carries an action.
|
|
647
|
+
* @returns {number}
|
|
648
|
+
*/
|
|
649
|
+
export function messageTimeout(timeout, hasAction = false) {
|
|
650
|
+
const settled = normalizeTimeout(timeout);
|
|
651
|
+
return hasAction && settled > 0 ? Math.max(settled, MESSAGE_ACTION_MIN_TIMEOUT) : settled;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Copies a message action into the shape the message renders, or returns null when there is
|
|
656
|
+
* nothing to render: no object, or no label. An unknown `kind` reads as the default, secondary
|
|
657
|
+
* button, and a missing `onclick` leaves a button that only closes the message.
|
|
658
|
+
* @param {unknown} value Action descriptor.
|
|
659
|
+
* @returns {{label: string, kind: 'default'|'primary'|'danger'|'ghost', onclick: ((event: MouseEvent) => void)|null}|null}
|
|
660
|
+
*/
|
|
661
|
+
export function normalizeMessageAction(value) {
|
|
662
|
+
if (!value || typeof value !== 'object') return null;
|
|
663
|
+
const action = /** @type {Record<string, unknown>} */ (value);
|
|
664
|
+
const label = action.label == null ? '' : String(action.label).trim();
|
|
665
|
+
if (!label) return null;
|
|
666
|
+
const kind = ['primary', 'danger', 'ghost'].includes(String(action.kind))
|
|
667
|
+
? /** @type {'primary'|'danger'|'ghost'} */ (action.kind) : 'default';
|
|
668
|
+
return {
|
|
669
|
+
label,
|
|
670
|
+
kind,
|
|
671
|
+
onclick: typeof action.onclick === 'function'
|
|
672
|
+
? /** @type {(event: MouseEvent) => void} */ (action.onclick) : null
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Accepts a node, or a component such as an Avatar, for the leading slot.
|
|
678
|
+
* @param {any} value Leading content.
|
|
679
|
+
* @returns {Node|null}
|
|
680
|
+
*/
|
|
681
|
+
function resolveLeading(value) {
|
|
682
|
+
if (isNode(value)) return /** @type {Node} */ (value);
|
|
683
|
+
if (value && typeof value === 'object' && typeof value.toElement === 'function') {
|
|
684
|
+
const element = value.toElement();
|
|
685
|
+
return isNode(element) ? element : null;
|
|
686
|
+
}
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Hands focus back to where it came from when a message closes with focus inside it, so a keyboard
|
|
692
|
+
* reader is not dropped on the page body with the element they were on gone.
|
|
693
|
+
* @param {any} entry Message entry.
|
|
694
|
+
* @returns {void}
|
|
695
|
+
*/
|
|
696
|
+
function returnFocus(entry) {
|
|
697
|
+
const root = entry.node.getRootNode?.();
|
|
698
|
+
const active = root?.activeElement ?? globalThis.document?.activeElement;
|
|
699
|
+
if (!active || !entry.node.contains(active)) return;
|
|
700
|
+
const target = entry.returnFocus;
|
|
701
|
+
entry.returnFocus = null;
|
|
702
|
+
if (target?.isConnected && typeof target.focus === 'function') target.focus({ preventScroll: true });
|
|
703
|
+
}
|
|
704
|
+
|
|
463
705
|
/** @param {unknown} value @returns {number} */
|
|
464
706
|
function normalizeLimit(value) {
|
|
465
707
|
const limit = Number(value);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
|
-
import {
|
|
3
|
+
import { connectedOverlayHost, overlayScope } from '../../core/overlay-host.js';
|
|
4
|
+
import { pushOverlay } from '../../core/overlay-stack.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Controls initial focus may be placed on: what a reader reaches with Tab, rather than everything
|
|
@@ -18,7 +19,7 @@ const FOCUSABLE_SELECTOR = [
|
|
|
18
19
|
* @property {boolean} [closable=true] Whether Escape may close the modal.
|
|
19
20
|
* @property {boolean} [lightDismiss=false] Whether a backdrop click closes the modal.
|
|
20
21
|
* @property {boolean} [destroyOnClose=false] Whether to destroy the modal after it closes.
|
|
21
|
-
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener.
|
|
22
|
+
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener. Resolved on every open, so a scope connected after construction still owns it.
|
|
22
23
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
|
|
23
24
|
* @property {(event: CustomEvent<{result: unknown}>) => void} [onclose] Close event listener.
|
|
24
25
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [oncancel] Cancel event listener.
|
|
@@ -62,6 +63,8 @@ export class Modal extends Component {
|
|
|
62
63
|
#nativeClosingPresentations = [];
|
|
63
64
|
/** @type {Element|null} Element that had focus when the overlay was opened. */
|
|
64
65
|
#opener = null;
|
|
66
|
+
/** @type {import('../../core/overlay-stack.js').OverlayHandle|null} Entry on the overlay stack while shown. */
|
|
67
|
+
#overlay = null;
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* Creates a modal in the configured theme scope, or at document level when none is present.
|
|
@@ -103,7 +106,9 @@ export class Modal extends Component {
|
|
|
103
106
|
* @returns {Element}
|
|
104
107
|
*/
|
|
105
108
|
mountTarget() {
|
|
106
|
-
|
|
109
|
+
// The theme scope, never an open dialog: a dialog opened from inside another stacks above it in
|
|
110
|
+
// the top layer wherever it lives, and nesting it would tie its rendering to the outer one.
|
|
111
|
+
return overlayScope(this.options.scope);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
/**
|
|
@@ -159,6 +164,61 @@ export class Modal extends Component {
|
|
|
159
164
|
else if (this.el.getAttribute('aria-modal') === 'true') this.el.removeAttribute('aria-modal');
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Describes the current presentation for the overlay stack, or returns null to stay off it.
|
|
169
|
+
*
|
|
170
|
+
* The kind is read from the platform like `aria-modal` is: a `showModal()` dialog is `modal`,
|
|
171
|
+
* and its Escape and backdrop stay the browser's; anything else is `non-modal`, and a subclass
|
|
172
|
+
* presenting that way adds the Escape and outside-press handling it wants.
|
|
173
|
+
* @returns {import('../../core/overlay-stack.js').OverlayEntryOptions|null}
|
|
174
|
+
* @protected
|
|
175
|
+
*/
|
|
176
|
+
_overlayEntry() {
|
|
177
|
+
return {
|
|
178
|
+
kind: this.el.matches(':modal') ? 'modal' : 'non-modal',
|
|
179
|
+
root: this.el,
|
|
180
|
+
owner: this,
|
|
181
|
+
anchor: this.#opener
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Re-registers the current presentation on the overlay stack: called after every presentation,
|
|
187
|
+
* including a subclass's change of address that bypasses `open()`.
|
|
188
|
+
* @returns {void}
|
|
189
|
+
* @protected
|
|
190
|
+
*/
|
|
191
|
+
_syncOverlay() {
|
|
192
|
+
this._releaseOverlay();
|
|
193
|
+
if (!this._isShown()) return;
|
|
194
|
+
const entry = this._overlayEntry();
|
|
195
|
+
if (!entry) return;
|
|
196
|
+
this.#overlay = pushOverlay(entry);
|
|
197
|
+
this.el.dataset.zxOverlayOrder = String(this.#overlay.order);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Takes the overlay off the stack. Safe to call repeatedly.
|
|
202
|
+
* @returns {void}
|
|
203
|
+
* @protected
|
|
204
|
+
*/
|
|
205
|
+
_releaseOverlay() {
|
|
206
|
+
const overlay = this.#overlay;
|
|
207
|
+
this.#overlay = null;
|
|
208
|
+
delete (/** @type {HTMLElement} */ (this.el)).dataset.zxOverlayOrder;
|
|
209
|
+
overlay?.release();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Whether a node is inside this overlay or anything opened from it, as the overlay stack sees it.
|
|
214
|
+
* @param {Node|null|undefined} node Candidate.
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
* @protected
|
|
217
|
+
*/
|
|
218
|
+
_overlayContains(node) {
|
|
219
|
+
return this.#overlay ? this.#overlay.contains(node) : this.el.contains(node ?? null);
|
|
220
|
+
}
|
|
221
|
+
|
|
162
222
|
/**
|
|
163
223
|
* Hides the current presentation and records which generation its queued close event belongs to.
|
|
164
224
|
* @param {string} [result=''] Native string return value.
|
|
@@ -202,7 +262,7 @@ export class Modal extends Component {
|
|
|
202
262
|
return false;
|
|
203
263
|
}
|
|
204
264
|
this.el.dataset.state = 'closed';
|
|
205
|
-
|
|
265
|
+
this._releaseOverlay();
|
|
206
266
|
/*
|
|
207
267
|
* Only the flag this class raised. A subclass presenting non-modally writes
|
|
208
268
|
* `aria-modal="false"` and owns that value across its own presentations.
|
|
@@ -266,10 +326,25 @@ export class Modal extends Component {
|
|
|
266
326
|
class: 'zx-modal'
|
|
267
327
|
}, h('div', { class: 'zx-modal__content', ref: 'content' })));
|
|
268
328
|
dialog.dataset.state = 'closed';
|
|
269
|
-
this.
|
|
329
|
+
this._mountInitial(dialog);
|
|
270
330
|
return dialog;
|
|
271
331
|
}
|
|
272
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Gives a freshly rendered dialog its first home.
|
|
335
|
+
*
|
|
336
|
+
* A `scope` that is not in the document yet cannot say which theme it belongs to, and the
|
|
337
|
+
* `<body>` that `mountTarget()` would answer is outside it. The dialog then stays detached and
|
|
338
|
+
* `open()` places it, by which time the scope is in place. Called from `render()`, so it may
|
|
339
|
+
* read `this.options` but no instance state.
|
|
340
|
+
* @param {HTMLDialogElement} dialog The rendered dialog.
|
|
341
|
+
* @returns {void}
|
|
342
|
+
*/
|
|
343
|
+
_mountInitial(dialog) {
|
|
344
|
+
if (this.options.scope != null && !connectedOverlayHost(this.options.scope)) return;
|
|
345
|
+
this.mountTarget().append(dialog);
|
|
346
|
+
}
|
|
347
|
+
|
|
273
348
|
/**
|
|
274
349
|
* Opens the modal in the browser top layer.
|
|
275
350
|
* @returns {this}
|
|
@@ -285,16 +360,15 @@ export class Modal extends Component {
|
|
|
285
360
|
this.#opener = document.activeElement instanceof Element ? document.activeElement : null;
|
|
286
361
|
const host = this.mountTarget();
|
|
287
362
|
if (this.el.parentElement !== host) host.append(this.el);
|
|
288
|
-
this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
|
|
289
363
|
this.el.returnValue = '';
|
|
290
364
|
try {
|
|
291
365
|
this._present();
|
|
292
366
|
} catch (error) {
|
|
293
|
-
delete this.el.dataset.zxOverlayOrder;
|
|
294
367
|
// Nothing was presented, so nothing will close; a capture left behind would outlive its open.
|
|
295
368
|
this.#opener = null;
|
|
296
369
|
throw error;
|
|
297
370
|
}
|
|
371
|
+
this._syncOverlay();
|
|
298
372
|
this.el.dataset.state = 'open';
|
|
299
373
|
this.emit('open');
|
|
300
374
|
// Deferred by one microtask: the platform's own focusing step runs as part of presenting, and
|
|
@@ -347,6 +421,8 @@ export class Modal extends Component {
|
|
|
347
421
|
if (this.#destroying) return;
|
|
348
422
|
this.#destroying = true;
|
|
349
423
|
if (this.isOpen()) this._dismiss();
|
|
424
|
+
// The queued close that would settle it never arrives once listeners are aborted.
|
|
425
|
+
this._releaseOverlay();
|
|
350
426
|
super.destroy();
|
|
351
427
|
}
|
|
352
428
|
|
|
@@ -443,11 +519,3 @@ function isBackdropClick(dialog, event) {
|
|
|
443
519
|
return event.clientX < rect.left || event.clientX > rect.right ||
|
|
444
520
|
event.clientY < rect.top || event.clientY > rect.bottom;
|
|
445
521
|
}
|
|
446
|
-
|
|
447
|
-
/** @returns {number} */
|
|
448
|
-
function nextOverlayOrder() {
|
|
449
|
-
const orders = Array.from(document.querySelectorAll('[data-zx-overlay-order]'), (element) =>
|
|
450
|
-
Number(element.getAttribute('data-zx-overlay-order')) || 0
|
|
451
|
-
);
|
|
452
|
-
return Math.max(0, ...orders) + 1;
|
|
453
|
-
}
|