@zeyos/zx 4.4.0 → 5.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.
- package/CHANGELOG.md +283 -0
- package/README.md +42 -10
- package/dist/types/components/activity-list/activity-list.d.ts +22 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +5 -3
- 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/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/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 +2 -2
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +24 -2
- package/dist/types/components/fieldset/fieldset.d.ts +19 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- 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 +260 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +1 -1
- package/dist/types/components/menu-button/menu-button.d.ts +33 -5
- package/dist/types/components/message/message.d.ts +119 -17
- package/dist/types/components/modal/modal.d.ts +13 -2
- 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 +11 -3
- 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 +46 -1
- 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/core/breakpoint.d.ts +1 -1
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +15 -1
- package/dist/types/index.d.ts +5 -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 +1 -1
- package/dist/types/zeyos/query.d.ts +79 -0
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +100 -14
- package/dist/types/zeyos/view.d.ts +10 -0
- package/dist/zx-zeyos.esm.js +1621 -443
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +1411 -660
- package/dist/zx.esm.js +1976 -703
- package/dist/zx.esm.min.js +56 -8
- package/dist/zx.global.js +1978 -705
- package/dist/zx.global.min.js +56 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +219 -34
- package/docs/llms.md +472 -110
- package/docs/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- 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 +34 -0
- package/src/components/app-icon/app-icon.css +1 -1
- package/src/components/app-rail/app-rail.css +40 -79
- package/src/components/app-sidebar/app-sidebar.css +109 -174
- package/src/components/app-sidebar/app-sidebar.js +21 -6
- 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/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +46 -46
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +9 -7
- 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 +6 -5
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +8 -3
- package/src/components/data-filter/data-filter.css +5 -4
- 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 +1 -1
- 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/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +5 -3
- 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 +68 -26
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +28 -5
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/filter/filter.js +7 -3
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/finder/finder.css +15 -13
- 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 +39 -15
- package/src/components/kanban-view/kanban-view.js +4 -4
- package/src/components/launcher/launcher.css +107 -25
- package/src/components/launcher/launcher.js +503 -41
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +1 -1
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/menu-button/menu-button.css +9 -4
- package/src/components/menu-button/menu-button.js +65 -9
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +217 -15
- package/src/components/modal/modal.js +18 -3
- package/src/components/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +1 -0
- 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 +8 -6
- package/src/components/panel/panel.css +1 -1
- package/src/components/rating/rating.css +7 -0
- 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 +83 -47
- package/src/components/sheet/sheet.css +11 -8
- package/src/components/slider/slider.css +16 -2
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/stat-tile/stat-tile.css +41 -46
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/tabbox/tabbox.css +32 -27
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +104 -29
- package/src/components/table/table.js +90 -16
- 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 +10 -3
- package/src/components/tree/tree.css +25 -16
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/view/record-view.js +1 -1
- package/src/core/breakpoint.js +1 -1
- package/src/core/icons.js +193 -60
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +21 -2
- package/src/index.js +5 -3
- package/src/internal/app-rail.js +1 -2
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +67 -27
- package/src/zeyos/index.js +1 -1
- package/src/zeyos/query.js +153 -0
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +68 -5
- package/src/zeyos/view.js +6 -0
- package/styles/base.css +56 -0
- package/styles/icons.css +11 -1
- package/styles/tokens/dark.css +113 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +100 -31
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +0 -1
- package/styles/tokens/density.css +0 -9
|
@@ -1,15 +1,32 @@
|
|
|
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
4
|
import { overlayHost } from '../../core/overlay-host.js';
|
|
5
|
+
import { button } from '../button/button.js';
|
|
5
6
|
|
|
6
7
|
/** @typedef {'info'|'success'|'warning'|'error'} MessageKind */
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The one command a message may carry, drawn as a small button beside its text.
|
|
11
|
+
* @typedef {Object} MessageAction
|
|
12
|
+
* @property {string} label Button text: a verb naming what happens ("Undo", "Approve", "Retry").
|
|
13
|
+
* @property {(event: MouseEvent) => void} [onclick] Runs when the button is activated, by pointer
|
|
14
|
+
* or keyboard; the message closes right after it.
|
|
15
|
+
* @property {'default'|'primary'|'danger'|'ghost'} [kind='default'] Button kind; the default is the
|
|
16
|
+
* secondary button.
|
|
17
|
+
*/
|
|
18
|
+
|
|
8
19
|
/**
|
|
9
20
|
* @typedef {Object} MessageShowOptions
|
|
10
21
|
* @property {MessageKind|'danger'} [kind='info'] Message intent (`danger` aliases `error`).
|
|
11
|
-
* @property {number} [timeout=4000] Auto-dismiss delay in milliseconds; zero persists.
|
|
22
|
+
* @property {number} [timeout=4000] Auto-dismiss delay in milliseconds; zero persists. A message
|
|
23
|
+
* with an `action` stays at least 8000ms.
|
|
12
24
|
* @property {boolean} [closable=true] Whether to render a close button.
|
|
25
|
+
* @property {MessageAction|null} [action=null] A command button inside the message. The message
|
|
26
|
+
* never times out while the pointer is over it or focus is inside it.
|
|
27
|
+
* @property {Node|{toElement: () => Node|null}|null} [leading=null] Visual shown in place of the
|
|
28
|
+
* kind icon, such as an Avatar's element. It is decorative — the message text must say what it
|
|
29
|
+
* shows.
|
|
13
30
|
* @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns a floating toast.
|
|
14
31
|
*/
|
|
15
32
|
|
|
@@ -21,7 +38,15 @@ import { overlayHost } from '../../core/overlay-host.js';
|
|
|
21
38
|
* @property {number} [maxVisible=5] Maximum number of visible messages.
|
|
22
39
|
*/
|
|
23
40
|
|
|
24
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Returned by every `show()`: closes that message early. Closing one that already closed, or one
|
|
43
|
+
* still waiting in the queue, is safe.
|
|
44
|
+
* @typedef {Object} MessageHandle
|
|
45
|
+
* @property {() => void} close Closes the message.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The shortest time a message that carries an action stays, in milliseconds. */
|
|
49
|
+
export const MESSAGE_ACTION_MIN_TIMEOUT = 8000;
|
|
25
50
|
|
|
26
51
|
/**
|
|
27
52
|
* @typedef {Object} ProgressHandle
|
|
@@ -135,7 +160,7 @@ export class Message extends Component {
|
|
|
135
160
|
maxVisible: 5
|
|
136
161
|
};
|
|
137
162
|
|
|
138
|
-
/** @type {WeakMap<Element, Message>} */
|
|
163
|
+
/** @type {WeakMap<Element, Message>} Floating region per overlay host. */
|
|
139
164
|
static #floating = new WeakMap();
|
|
140
165
|
|
|
141
166
|
/** @returns {HTMLElement} */
|
|
@@ -145,6 +170,8 @@ export class Message extends Component {
|
|
|
145
170
|
this._createdRoot = created;
|
|
146
171
|
this._original = created ? null : snapshot(root);
|
|
147
172
|
this._entries = new Set();
|
|
173
|
+
/** @type {Element|null} The overlay host this instance is the floating region of; null when inline. */
|
|
174
|
+
this._floatingHost = null;
|
|
148
175
|
this._queue = new MessageQueue(normalizeLimit(this.options.maxVisible), (entry) => {
|
|
149
176
|
this._activate(entry);
|
|
150
177
|
});
|
|
@@ -161,10 +188,13 @@ export class Message extends Component {
|
|
|
161
188
|
* @returns {MessageHandle}
|
|
162
189
|
*/
|
|
163
190
|
show(msg, options = {}) {
|
|
191
|
+
const action = normalizeMessageAction(options.action);
|
|
164
192
|
const settings = {
|
|
165
193
|
kind: normalizeKind(options.kind ?? this.options.kind),
|
|
166
|
-
timeout:
|
|
167
|
-
closable: options.closable ?? Boolean(this.options.closable)
|
|
194
|
+
timeout: messageTimeout(options.timeout ?? this.options.timeout, action !== null),
|
|
195
|
+
closable: options.closable ?? Boolean(this.options.closable),
|
|
196
|
+
action,
|
|
197
|
+
leading: resolveLeading(options.leading)
|
|
168
198
|
};
|
|
169
199
|
const entry = this._createEntry(msg, settings);
|
|
170
200
|
this._entries.add(entry);
|
|
@@ -232,10 +262,33 @@ export class Message extends Component {
|
|
|
232
262
|
return this.#floatingRegion(options.scope)._progress(text);
|
|
233
263
|
}
|
|
234
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Removes a floating region at once, toasts and all, without waiting for them to time out.
|
|
267
|
+
*
|
|
268
|
+
* A region already removes itself when its last toast closes, and one whose host has left the
|
|
269
|
+
* document is replaced on the next toast, so this is only for a host being torn down while
|
|
270
|
+
* toasts are still showing — an application view unmounting its themed shell, say.
|
|
271
|
+
* @param {Element|string} scope Element or selector whose nearest Zx theme scope owns the region,
|
|
272
|
+
* as passed to `show()`.
|
|
273
|
+
* @returns {boolean} Whether there was a region to remove.
|
|
274
|
+
*/
|
|
275
|
+
static disposeRegion(scope) {
|
|
276
|
+
if (scope == null) return false;
|
|
277
|
+
const context = resolveElement(scope);
|
|
278
|
+
if (!context) return false;
|
|
279
|
+
const host = overlayHost(context);
|
|
280
|
+
const region = Message.#floating.get(host);
|
|
281
|
+
if (!region) return false;
|
|
282
|
+
Message.#floating.delete(host);
|
|
283
|
+
region._disposeFloating();
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
235
287
|
/** @returns {void} */
|
|
236
288
|
destroy() {
|
|
237
289
|
if (this._cleaned) return;
|
|
238
290
|
this._cleaned = true;
|
|
291
|
+
this._floatingHost = null;
|
|
239
292
|
for (const entry of this._entries) {
|
|
240
293
|
clearEntryTimers(entry);
|
|
241
294
|
entry.node.remove();
|
|
@@ -288,13 +341,13 @@ export class Message extends Component {
|
|
|
288
341
|
|
|
289
342
|
/**
|
|
290
343
|
* @param {string|Node} msg
|
|
291
|
-
* @param {{kind: MessageKind, timeout: number, closable: boolean}} settings
|
|
344
|
+
* @param {{kind: MessageKind, timeout: number, closable: boolean, action?: MessageAction|null, leading?: Node|null}} settings
|
|
292
345
|
* @param {{pct: number, text: string}|null} [progress=null]
|
|
293
346
|
* @returns {any}
|
|
294
347
|
*/
|
|
295
348
|
_createEntry(msg, settings, progress = null) {
|
|
296
349
|
const iconHost = h('span', { class: 'zx-message__icon', ariaHidden: 'true' },
|
|
297
|
-
icon(settings.kind, { size: 18 })
|
|
350
|
+
settings.leading ?? icon(settings.kind, { size: 18 })
|
|
298
351
|
);
|
|
299
352
|
const content = h('div', { class: 'zx-message__content' });
|
|
300
353
|
if (isNode(msg)) content.append(msg);
|
|
@@ -315,9 +368,13 @@ export class Message extends Component {
|
|
|
315
368
|
timer: null,
|
|
316
369
|
exitTimer: null,
|
|
317
370
|
closed: false,
|
|
371
|
+
hovered: false,
|
|
372
|
+
focused: false,
|
|
373
|
+
returnFocus: null,
|
|
318
374
|
progressBar: null,
|
|
319
375
|
progressFill: null
|
|
320
376
|
};
|
|
377
|
+
if (settings.leading) node.dataset.leading = '';
|
|
321
378
|
|
|
322
379
|
if (progress) {
|
|
323
380
|
node.dataset.progress = '';
|
|
@@ -336,6 +393,21 @@ export class Message extends Component {
|
|
|
336
393
|
syncProgress(entry, progress);
|
|
337
394
|
}
|
|
338
395
|
|
|
396
|
+
if (settings.action) {
|
|
397
|
+
const { label, kind, onclick } = settings.action;
|
|
398
|
+
const control = button({ label, kind, size: 'sm' });
|
|
399
|
+
control.classList.add('zx-message__action');
|
|
400
|
+
node.append(control);
|
|
401
|
+
this.listen(control, 'click', (event) => {
|
|
402
|
+
if (entry.closed) return;
|
|
403
|
+
try {
|
|
404
|
+
onclick?.(/** @type {MouseEvent} */ (event));
|
|
405
|
+
} finally {
|
|
406
|
+
this._close(entry);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
339
411
|
if (settings.closable) {
|
|
340
412
|
const close = h('button', {
|
|
341
413
|
class: 'zx-icon-btn zx-message__close',
|
|
@@ -346,8 +418,31 @@ export class Message extends Component {
|
|
|
346
418
|
node.append(close);
|
|
347
419
|
this.listen(close, 'click', () => this._close(entry));
|
|
348
420
|
}
|
|
349
|
-
|
|
350
|
-
|
|
421
|
+
// Hover and focus each hold the timer (WCAG 2.2.1): a reader moving to the action or the close
|
|
422
|
+
// button must not have the message vanish under them, and it counts down again only once
|
|
423
|
+
// neither holds it.
|
|
424
|
+
this.listen(node, 'mouseenter', () => {
|
|
425
|
+
entry.hovered = true;
|
|
426
|
+
this._pause(entry);
|
|
427
|
+
});
|
|
428
|
+
this.listen(node, 'mouseleave', () => {
|
|
429
|
+
entry.hovered = false;
|
|
430
|
+
this._resume(entry);
|
|
431
|
+
});
|
|
432
|
+
this.listen(node, 'focusin', (event) => {
|
|
433
|
+
if (!entry.focused) {
|
|
434
|
+
const from = /** @type {FocusEvent} */ (event).relatedTarget;
|
|
435
|
+
entry.returnFocus = isNode(from) && !node.contains(/** @type {Node} */ (from)) ? from : null;
|
|
436
|
+
}
|
|
437
|
+
entry.focused = true;
|
|
438
|
+
this._pause(entry);
|
|
439
|
+
});
|
|
440
|
+
this.listen(node, 'focusout', (event) => {
|
|
441
|
+
const to = /** @type {FocusEvent} */ (event).relatedTarget;
|
|
442
|
+
if (isNode(to) && node.contains(/** @type {Node} */ (to))) return;
|
|
443
|
+
entry.focused = false;
|
|
444
|
+
this._resume(entry);
|
|
445
|
+
});
|
|
351
446
|
return entry;
|
|
352
447
|
}
|
|
353
448
|
|
|
@@ -361,7 +456,7 @@ export class Message extends Component {
|
|
|
361
456
|
|
|
362
457
|
/** @param {any} entry @returns {void} */
|
|
363
458
|
_schedule(entry) {
|
|
364
|
-
if (entry.closed || entry.remaining <= 0) return;
|
|
459
|
+
if (entry.closed || entry.remaining <= 0 || entry.hovered || entry.focused) return;
|
|
365
460
|
entry.startedAt = Date.now();
|
|
366
461
|
entry.timer = setTimeout(() => this._close(entry), entry.remaining);
|
|
367
462
|
}
|
|
@@ -376,7 +471,7 @@ export class Message extends Component {
|
|
|
376
471
|
|
|
377
472
|
/** @param {any} entry @returns {void} */
|
|
378
473
|
_resume(entry) {
|
|
379
|
-
if (entry.closed || entry.timer !== null) return;
|
|
474
|
+
if (entry.closed || entry.timer !== null || entry.hovered || entry.focused) return;
|
|
380
475
|
if (entry.remaining <= 0) {
|
|
381
476
|
this._close(entry);
|
|
382
477
|
return;
|
|
@@ -389,10 +484,12 @@ export class Message extends Component {
|
|
|
389
484
|
if (entry.closed) return;
|
|
390
485
|
entry.closed = true;
|
|
391
486
|
clearEntryTimers(entry);
|
|
487
|
+
returnFocus(entry);
|
|
392
488
|
if (!this._queue.isActive(entry)) {
|
|
393
489
|
this._queue.remove(entry);
|
|
394
490
|
entry.node.remove();
|
|
395
491
|
this._entries.delete(entry);
|
|
492
|
+
this._releaseIfEmpty();
|
|
396
493
|
return;
|
|
397
494
|
}
|
|
398
495
|
|
|
@@ -415,6 +512,27 @@ export class Message extends Component {
|
|
|
415
512
|
entry.node.remove();
|
|
416
513
|
this._entries.delete(entry);
|
|
417
514
|
this._queue.remove(entry);
|
|
515
|
+
this._releaseIfEmpty();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Takes a floating region out of the document once its last toast has gone, so an emptied host
|
|
520
|
+
* is not left holding a region nobody uses. Inline areas stay: their element is the caller's.
|
|
521
|
+
* @returns {void}
|
|
522
|
+
*/
|
|
523
|
+
_releaseIfEmpty() {
|
|
524
|
+
if (!this._floatingHost || this._entries.size > 0) return;
|
|
525
|
+
Message.#release(this);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Destroys a floating region and removes its element.
|
|
530
|
+
* @returns {void}
|
|
531
|
+
*/
|
|
532
|
+
_disposeFloating() {
|
|
533
|
+
const region = this.el;
|
|
534
|
+
this.destroy();
|
|
535
|
+
region.remove();
|
|
418
536
|
}
|
|
419
537
|
|
|
420
538
|
/**
|
|
@@ -428,11 +546,23 @@ export class Message extends Component {
|
|
|
428
546
|
return message === key ? fallback : message;
|
|
429
547
|
}
|
|
430
548
|
|
|
431
|
-
/**
|
|
549
|
+
/**
|
|
550
|
+
* Finds or creates the floating region for a scope. A cached region is reused only while it is
|
|
551
|
+
* still where it was put: a host emptied by `replaceChildren()` would otherwise keep receiving
|
|
552
|
+
* toasts in a node nobody can see.
|
|
553
|
+
* @param {Element|string|null|undefined} scope
|
|
554
|
+
* @returns {Message}
|
|
555
|
+
*/
|
|
432
556
|
static #floatingRegion(scope) {
|
|
433
|
-
|
|
557
|
+
let host = overlayHost(scope ?? null);
|
|
558
|
+
// A scope that has itself left the document cannot show anything; the toast still must be seen.
|
|
559
|
+
if (!host.isConnected) host = overlayHost(null);
|
|
434
560
|
const existing = this.#floating.get(host);
|
|
435
|
-
if (existing) return existing;
|
|
561
|
+
if (existing && existing.el.isConnected && existing.el.parentElement === host) return existing;
|
|
562
|
+
if (existing) {
|
|
563
|
+
this.#floating.delete(host);
|
|
564
|
+
existing._disposeFloating();
|
|
565
|
+
}
|
|
436
566
|
const region = h('div', {
|
|
437
567
|
class: 'zx-message-region',
|
|
438
568
|
popover: 'manual',
|
|
@@ -441,10 +571,18 @@ export class Message extends Component {
|
|
|
441
571
|
});
|
|
442
572
|
host.append(region);
|
|
443
573
|
const floating = new Message(region);
|
|
574
|
+
floating._floatingHost = host;
|
|
444
575
|
this.#floating.set(host, floating);
|
|
445
576
|
if (typeof region.showPopover === 'function') region.showPopover();
|
|
446
577
|
return floating;
|
|
447
578
|
}
|
|
579
|
+
|
|
580
|
+
/** @param {Message} region Floating region whose last toast closed. @returns {void} */
|
|
581
|
+
static #release(region) {
|
|
582
|
+
const host = region._floatingHost;
|
|
583
|
+
if (host && Message.#floating.get(host) === region) Message.#floating.delete(host);
|
|
584
|
+
region._disposeFloating();
|
|
585
|
+
}
|
|
448
586
|
}
|
|
449
587
|
|
|
450
588
|
/** @param {unknown} value @returns {MessageKind} */
|
|
@@ -460,6 +598,70 @@ function normalizeTimeout(value) {
|
|
|
460
598
|
return Number.isFinite(timeout) ? Math.max(0, timeout) : 4000;
|
|
461
599
|
}
|
|
462
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Settles how long a message stays. A message that carries an action stays at least
|
|
603
|
+
* `MESSAGE_ACTION_MIN_TIMEOUT`, so there is time to reach it; zero still means it stays until
|
|
604
|
+
* closed, and anything that is not a finite number falls back to the 4000ms default.
|
|
605
|
+
* @param {unknown} timeout Requested auto-dismiss delay in milliseconds.
|
|
606
|
+
* @param {boolean} [hasAction=false] Whether the message carries an action.
|
|
607
|
+
* @returns {number}
|
|
608
|
+
*/
|
|
609
|
+
export function messageTimeout(timeout, hasAction = false) {
|
|
610
|
+
const settled = normalizeTimeout(timeout);
|
|
611
|
+
return hasAction && settled > 0 ? Math.max(settled, MESSAGE_ACTION_MIN_TIMEOUT) : settled;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Copies a message action into the shape the message renders, or returns null when there is
|
|
616
|
+
* nothing to render: no object, or no label. An unknown `kind` reads as the default, secondary
|
|
617
|
+
* button, and a missing `onclick` leaves a button that only closes the message.
|
|
618
|
+
* @param {unknown} value Action descriptor.
|
|
619
|
+
* @returns {{label: string, kind: 'default'|'primary'|'danger'|'ghost', onclick: ((event: MouseEvent) => void)|null}|null}
|
|
620
|
+
*/
|
|
621
|
+
export function normalizeMessageAction(value) {
|
|
622
|
+
if (!value || typeof value !== 'object') return null;
|
|
623
|
+
const action = /** @type {Record<string, unknown>} */ (value);
|
|
624
|
+
const label = action.label == null ? '' : String(action.label).trim();
|
|
625
|
+
if (!label) return null;
|
|
626
|
+
const kind = ['primary', 'danger', 'ghost'].includes(String(action.kind))
|
|
627
|
+
? /** @type {'primary'|'danger'|'ghost'} */ (action.kind) : 'default';
|
|
628
|
+
return {
|
|
629
|
+
label,
|
|
630
|
+
kind,
|
|
631
|
+
onclick: typeof action.onclick === 'function'
|
|
632
|
+
? /** @type {(event: MouseEvent) => void} */ (action.onclick) : null
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Accepts a node, or a component such as an Avatar, for the leading slot.
|
|
638
|
+
* @param {any} value Leading content.
|
|
639
|
+
* @returns {Node|null}
|
|
640
|
+
*/
|
|
641
|
+
function resolveLeading(value) {
|
|
642
|
+
if (isNode(value)) return /** @type {Node} */ (value);
|
|
643
|
+
if (value && typeof value === 'object' && typeof value.toElement === 'function') {
|
|
644
|
+
const element = value.toElement();
|
|
645
|
+
return isNode(element) ? element : null;
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Hands focus back to where it came from when a message closes with focus inside it, so a keyboard
|
|
652
|
+
* reader is not dropped on the page body with the element they were on gone.
|
|
653
|
+
* @param {any} entry Message entry.
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
function returnFocus(entry) {
|
|
657
|
+
const root = entry.node.getRootNode?.();
|
|
658
|
+
const active = root?.activeElement ?? globalThis.document?.activeElement;
|
|
659
|
+
if (!active || !entry.node.contains(active)) return;
|
|
660
|
+
const target = entry.returnFocus;
|
|
661
|
+
entry.returnFocus = null;
|
|
662
|
+
if (target?.isConnected && typeof target.focus === 'function') target.focus({ preventScroll: true });
|
|
663
|
+
}
|
|
664
|
+
|
|
463
665
|
/** @param {unknown} value @returns {number} */
|
|
464
666
|
function normalizeLimit(value) {
|
|
465
667
|
const limit = Number(value);
|
|
@@ -1,6 +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
|
+
import { connectedOverlayHost, overlayHost } from '../../core/overlay-host.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Controls initial focus may be placed on: what a reader reaches with Tab, rather than everything
|
|
@@ -18,7 +18,7 @@ const FOCUSABLE_SELECTOR = [
|
|
|
18
18
|
* @property {boolean} [closable=true] Whether Escape may close the modal.
|
|
19
19
|
* @property {boolean} [lightDismiss=false] Whether a backdrop click closes the modal.
|
|
20
20
|
* @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.
|
|
21
|
+
* @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
22
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
|
|
23
23
|
* @property {(event: CustomEvent<{result: unknown}>) => void} [onclose] Close event listener.
|
|
24
24
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [oncancel] Cancel event listener.
|
|
@@ -266,10 +266,25 @@ export class Modal extends Component {
|
|
|
266
266
|
class: 'zx-modal'
|
|
267
267
|
}, h('div', { class: 'zx-modal__content', ref: 'content' })));
|
|
268
268
|
dialog.dataset.state = 'closed';
|
|
269
|
-
this.
|
|
269
|
+
this._mountInitial(dialog);
|
|
270
270
|
return dialog;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Gives a freshly rendered dialog its first home.
|
|
275
|
+
*
|
|
276
|
+
* A `scope` that is not in the document yet cannot say which theme it belongs to, and the
|
|
277
|
+
* `<body>` that `mountTarget()` would answer is outside it. The dialog then stays detached and
|
|
278
|
+
* `open()` places it, by which time the scope is in place. Called from `render()`, so it may
|
|
279
|
+
* read `this.options` but no instance state.
|
|
280
|
+
* @param {HTMLDialogElement} dialog The rendered dialog.
|
|
281
|
+
* @returns {void}
|
|
282
|
+
*/
|
|
283
|
+
_mountInitial(dialog) {
|
|
284
|
+
if (this.options.scope != null && !connectedOverlayHost(this.options.scope)) return;
|
|
285
|
+
this.mountTarget().append(dialog);
|
|
286
|
+
}
|
|
287
|
+
|
|
273
288
|
/**
|
|
274
289
|
* Opens the modal in the browser top layer.
|
|
275
290
|
* @returns {this}
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
padding-inline: var(--zx-space-2);
|
|
30
30
|
color: var(--zx-color-accent);
|
|
31
31
|
font-size: var(--zx-text-xl);
|
|
32
|
-
font-weight:
|
|
32
|
+
font-weight: 650;
|
|
33
|
+
letter-spacing: var(--zx-tracking-title);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
.zx-navigation-bar__navigation {
|
|
@@ -51,10 +52,11 @@
|
|
|
51
52
|
gap: var(--zx-space-2);
|
|
52
53
|
border: 0;
|
|
53
54
|
border-radius: var(--zx-control-radius);
|
|
54
|
-
background:
|
|
55
|
+
background: transparent;
|
|
55
56
|
padding-inline: var(--zx-control-pad-x);
|
|
56
57
|
color: var(--zx-color-text-muted);
|
|
57
58
|
font: inherit;
|
|
59
|
+
font-weight: var(--zx-weight-medium);
|
|
58
60
|
text-decoration: none;
|
|
59
61
|
cursor: pointer;
|
|
60
62
|
}
|
|
@@ -64,11 +66,12 @@
|
|
|
64
66
|
color: var(--zx-color-text);
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/* Where you are, not a pick: the neutral current fill, the full text colour, a heavier label. */
|
|
67
70
|
.zx-navigation-bar__item[aria-current="page"],
|
|
68
71
|
.zx-navigation-bar__item[aria-selected="true"] {
|
|
69
|
-
background: var(--zx-color-bg-
|
|
70
|
-
color: var(--zx-color-
|
|
71
|
-
font-weight:
|
|
72
|
+
background: var(--zx-color-bg-current);
|
|
73
|
+
color: var(--zx-color-text);
|
|
74
|
+
font-weight: var(--zx-weight-semibold);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
.zx-navigation-bar__item[aria-disabled="true"] {
|
|
@@ -81,18 +84,24 @@
|
|
|
81
84
|
box-shadow: var(--zx-focus-ring);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/* A count beside a label, not a call to action: neutral, like the Tabbox count. */
|
|
84
88
|
.zx-navigation-bar__badge {
|
|
85
89
|
min-inline-size: var(--zx-space-4);
|
|
86
90
|
border-radius: var(--zx-radius-full);
|
|
87
|
-
background: var(--zx-color-
|
|
91
|
+
background: var(--zx-color-bg-current);
|
|
88
92
|
padding-inline: var(--zx-space-1);
|
|
89
|
-
color: var(--zx-color-
|
|
90
|
-
font-size: var(--zx-text-
|
|
91
|
-
font-
|
|
93
|
+
color: var(--zx-color-text-muted);
|
|
94
|
+
font-size: var(--zx-text-xs);
|
|
95
|
+
font-variant-numeric: tabular-nums;
|
|
96
|
+
font-weight: var(--zx-weight-medium);
|
|
92
97
|
line-height: var(--zx-space-4);
|
|
93
98
|
text-align: center;
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
.zx-navigation-bar__item:is([aria-current="page"], [aria-selected="true"]) .zx-navigation-bar__badge {
|
|
102
|
+
color: var(--zx-color-text);
|
|
103
|
+
}
|
|
104
|
+
|
|
96
105
|
.zx-navigation-bar__more {
|
|
97
106
|
display: none;
|
|
98
107
|
}
|
|
@@ -11,6 +11,7 @@ export function registerNumberFieldAdapter() {
|
|
|
11
11
|
label: options.label ? String(options.label) : undefined,
|
|
12
12
|
placeholder: options.placeholder,
|
|
13
13
|
required: options.required,
|
|
14
|
+
name: field.name || null,
|
|
14
15
|
...(options.props ?? {})
|
|
15
16
|
}));
|
|
16
17
|
let setting = false;
|
|
@@ -75,3 +75,73 @@
|
|
|
75
75
|
border-end-end-radius: var(--zx-control-radius);
|
|
76
76
|
border-inline-start: 1px solid var(--zx-color-border-control);
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/* In a Field the control fills its column, as a text input does, and gives way in a narrow one. */
|
|
80
|
+
.zx-field__control > .zx-number-field {
|
|
81
|
+
display: flex;
|
|
82
|
+
inline-size: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.zx-field__control > .zx-number-field .zx-number-field__input {
|
|
86
|
+
min-inline-size: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* Quiet fields. Inside `[data-zx-fields="quiet"]` the number reads as text: no border, no fill,
|
|
91
|
+
* and the value at the start, where the text of the quiet fields around it starts. The steppers
|
|
92
|
+
* move after it, both at the trailing end, and stay hidden until hover or focus — kept in the
|
|
93
|
+
* layout, so nothing moves when they return. Focus brings back the border, the control fill and
|
|
94
|
+
* the focus ring. Disabled and read-only fields never light up.
|
|
95
|
+
*/
|
|
96
|
+
[data-zx-fields="quiet"] .zx-number-field {
|
|
97
|
+
border-color: transparent;
|
|
98
|
+
background: transparent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
[data-zx-fields="quiet"] .zx-number-field__input {
|
|
102
|
+
padding-inline-start: var(--zx-control-pad-x);
|
|
103
|
+
text-align: start;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[data-zx-fields="quiet"] .zx-number-field__step {
|
|
107
|
+
border-color: transparent;
|
|
108
|
+
opacity: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[data-zx-fields="quiet"] .zx-number-field__step[data-step="down"] {
|
|
112
|
+
order: 1;
|
|
113
|
+
border-inline-end: 0;
|
|
114
|
+
border-inline-start: 1px solid transparent;
|
|
115
|
+
border-radius: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[data-zx-fields="quiet"] .zx-number-field__step[data-step="up"] {
|
|
119
|
+
order: 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[data-zx-fields="quiet"] .zx-number-field:not([data-disabled], [data-readonly]):hover {
|
|
123
|
+
background: var(--zx-color-bg-hover);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[data-zx-fields="quiet"] .zx-number-field:not([data-disabled], [data-readonly]):is(:hover, :focus-within) .zx-number-field__step {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[data-zx-fields="quiet"] .zx-number-field:not([data-disabled], [data-readonly]):focus-within {
|
|
131
|
+
border-color: var(--zx-color-border-control);
|
|
132
|
+
background: var(--zx-color-bg-control);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[data-zx-fields="quiet"] .zx-number-field:not([data-disabled], [data-readonly]):focus-within .zx-number-field__step {
|
|
136
|
+
border-color: var(--zx-color-border-control);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
140
|
+
[data-zx-fields="quiet"] .zx-number-field {
|
|
141
|
+
transition: background-color var(--zx-dur-fast) var(--zx-ease), border-color var(--zx-dur-fast) var(--zx-ease);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-zx-fields="quiet"] .zx-number-field__step {
|
|
145
|
+
transition: opacity var(--zx-dur-fast) var(--zx-ease), border-color var(--zx-dur-fast) var(--zx-ease);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -336,6 +336,8 @@ export class NumberField extends Component {
|
|
|
336
336
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
337
337
|
_onKeydown(event) {
|
|
338
338
|
if (this._disabled || this._readonly) return;
|
|
339
|
+
// Chords belong to the page — Mod+Enter submits the form or dialog the field is in.
|
|
340
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
339
341
|
const large = Math.max(1, Math.trunc(Number(this.options.largeStep) || 1));
|
|
340
342
|
const actions = {
|
|
341
343
|
ArrowUp: () => this._stepBy(1),
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One row that wraps: the summary is pushed to the start, the page-size select and the pager sit
|
|
5
5
|
* at the end. Page buttons are ordinary `.zx-btn` controls so they inherit the library's control
|
|
6
|
-
* height, radius, and focus ring;
|
|
6
|
+
* height, radius, and focus ring; the current page takes the neutral current fill — it is where
|
|
7
|
+
* you are, not a call to action.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
.zx-pagination {
|
|
@@ -70,15 +71,16 @@
|
|
|
70
71
|
font-variant-numeric: tabular-nums;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/* The page you are on: the neutral current fill and a heavier figure, not a solid accent. */
|
|
73
75
|
.zx-pagination__page[aria-current="page"] {
|
|
74
|
-
border-color:
|
|
75
|
-
background: var(--zx-color-
|
|
76
|
-
color: var(--zx-color-
|
|
76
|
+
border-color: transparent;
|
|
77
|
+
background: var(--zx-color-bg-current);
|
|
78
|
+
color: var(--zx-color-text);
|
|
79
|
+
font-weight: var(--zx-weight-semibold);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
.zx-pagination__page[aria-current="page"]:hover:not(:disabled) {
|
|
80
|
-
|
|
81
|
-
background: var(--zx-color-accent-hover);
|
|
83
|
+
background: var(--zx-color-bg-current);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/* Decorative gap marker: never focusable, never announced. */
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
clip-path: inset(0 calc(100% - var(--zx-rating-fill)) 0 0);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* The icons are outlines; a chosen star is a solid one, so the two states differ in shape as well
|
|
34
|
+
as in colour. */
|
|
35
|
+
.zx-rating__glyph[data-part="on"],
|
|
36
|
+
.zx-rating__glyph[data-part="on"] .zx-icon {
|
|
37
|
+
fill: currentColor;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
.zx-rating__symbol[data-preview] .zx-rating__glyph[data-part="on"] {
|
|
34
41
|
color: var(--zx-color-warning);
|
|
35
42
|
opacity: 0.75;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isCssColor } from '../../core/util.js';
|
|
2
|
+
|
|
3
|
+
/** A reference to one custom property, such as `var(--zx-color-tone-blue-fg)`. */
|
|
4
|
+
const TOKEN_REFERENCE = /^var\(\s*--[\w-]+\s*\)$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Accepts an item colour for an inline custom property: a CSS colour literal (`#0b7285`, `rgb(…)`,
|
|
8
|
+
* `oklch(…)`, a named colour) or a reference to a design token (`var(--zx-color-tone-blue-fg)`),
|
|
9
|
+
* so an item can follow the theme rather than fix one colour for light and dark alike. Anything
|
|
10
|
+
* else — other functions, declarations, a `var()` with a fallback — is refused.
|
|
11
|
+
*
|
|
12
|
+
* Shared by `Select.entity()` and `TagPicker`, the two components that take a colour per item.
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} value Candidate colour.
|
|
15
|
+
* @returns {string|null} The trimmed colour, or null when the value is not one.
|
|
16
|
+
*/
|
|
17
|
+
export function itemColor(value) {
|
|
18
|
+
if (typeof value !== 'string') return null;
|
|
19
|
+
const text = value.trim();
|
|
20
|
+
if (TOKEN_REFERENCE.test(text)) return text;
|
|
21
|
+
return isCssColor(text) ? text : null;
|
|
22
|
+
}
|
|
@@ -14,6 +14,8 @@ export function registerSelectFieldAdapter() {
|
|
|
14
14
|
if (event.target.closest?.('.zx-field__label')) field.adapter.focus();
|
|
15
15
|
});
|
|
16
16
|
delete selectOptions.preset;
|
|
17
|
+
// The priority preset offers "No priority" (null) by default; a required field cannot hold it.
|
|
18
|
+
if (preset === 'priority' && props.clearable === undefined) selectOptions.clearable = !options.required;
|
|
17
19
|
const select = field.own(preset === 'priority'
|
|
18
20
|
? Select.priority(null, selectOptions)
|
|
19
21
|
: preset === 'status'
|