@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
|
@@ -3,8 +3,12 @@ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
|
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
4
|
import { typeahead } from '../../core/keyboard.js';
|
|
5
5
|
import { position } from '../../core/position.js';
|
|
6
|
-
import {
|
|
6
|
+
import { uid } from '../../core/util.js';
|
|
7
|
+
import { itemColor } from './color.js';
|
|
7
8
|
import { matchItems } from './filter.js';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_STATUSES, STATUS_INDICATORS, STATUS_TONES, normalizePriorityLevel, priorityIcon, statusIcon
|
|
11
|
+
} from './glyphs.js';
|
|
8
12
|
|
|
9
13
|
/** @typedef {string|number|boolean|symbol|bigint|null|undefined} Primitive */
|
|
10
14
|
/** @typedef {Record<string, any>|Primitive} SelectItem */
|
|
@@ -49,7 +53,9 @@ import { matchItems } from './filter.js';
|
|
|
49
53
|
* @property {string} [recentLabel='Recent'] Heading above recent choices.
|
|
50
54
|
* @property {boolean|SelectAction} [create=false] Optional create command at the bottom.
|
|
51
55
|
* @property {string|((item: SelectItem)=>unknown)} [iconKey='icon'] Item icon reader.
|
|
52
|
-
* @property {string|((item: SelectItem)=>unknown)} [colorKey='color'] Optional icon colour reader
|
|
56
|
+
* @property {string|((item: SelectItem)=>unknown)} [colorKey='color'] Optional icon colour reader: a CSS
|
|
57
|
+
* colour or a token reference such as `var(--zx-color-tone-blue-fg)`. Without one the glyph is a
|
|
58
|
+
* neutral tile in muted ink.
|
|
53
59
|
* @property {string|((item: SelectItem)=>unknown)} [subtitleKey='description'] Secondary-text reader.
|
|
54
60
|
* @property {((item: SelectItem, context:{selected:boolean})=>Node|string|null)|null} [renderIcon=null] Entity icon renderer.
|
|
55
61
|
*/
|
|
@@ -69,18 +75,6 @@ const PRIORITY_KEYS = [
|
|
|
69
75
|
];
|
|
70
76
|
const PRIORITY_FALLBACKS = ['Lowest', 'Low', 'Medium', 'High', 'Highest'];
|
|
71
77
|
|
|
72
|
-
const DEFAULT_STATUSES = Object.freeze([
|
|
73
|
-
['backlog', 'status.backlog', 'Backlog', 'dotted', 'muted'],
|
|
74
|
-
['todo', 'status.todo', 'Todo', 'ring', 'neutral'],
|
|
75
|
-
['in-progress', 'status.inProgress', 'In Progress', 'progress', 'warning'],
|
|
76
|
-
['in-review', 'status.inReview', 'In Review', 'review', 'info'],
|
|
77
|
-
['ready', 'status.ready', 'Ready to Merge', 'ready', 'accent'],
|
|
78
|
-
['waiting', 'status.waiting', 'Waiting for feedback', 'waiting', 'warning'],
|
|
79
|
-
['done', 'status.done', 'Done', 'done', 'info'],
|
|
80
|
-
['canceled', 'status.canceled', 'Canceled', 'canceled', 'muted'],
|
|
81
|
-
['duplicate', 'status.duplicate', 'Duplicate', 'duplicate', 'muted']
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
78
|
/**
|
|
85
79
|
* APG editable combobox with optional local or asynchronous filtering.
|
|
86
80
|
*
|
|
@@ -125,7 +119,7 @@ export class Select extends Component {
|
|
|
125
119
|
render() {
|
|
126
120
|
// Instance state initialized here because render() runs inside the base
|
|
127
121
|
// constructor, before class-field initializers would run (and clobber it).
|
|
128
|
-
this._items = []; this._visibleItems = []; this._fixedItems = []; this._visibleFixed = []; this._actions = []; this._selected = null; this._optionEntries = []; this._activeIndex = -1; this._open = false; this._disabled = false; this._loading = false; this._requestSequence = 0; this._queryTimer = null; this._position = null; this._createdRoot = false; this._root = null; this._snapshot = null;
|
|
122
|
+
this._items = []; this._visibleItems = []; this._fixedItems = []; this._visibleFixed = []; this._actions = []; this._selected = null; this._optionEntries = []; this._activeIndex = -1; this._open = false; this._disabled = false; this._loading = false; this._requestSequence = 0; this._queryTimer = null; this._position = null; this._createdRoot = false; this._root = null; this._snapshot = null; this._pointerModality = false;
|
|
129
123
|
this._createdRoot = this.el === null;
|
|
130
124
|
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
131
125
|
this.el = root;
|
|
@@ -209,7 +203,22 @@ export class Select extends Component {
|
|
|
209
203
|
|
|
210
204
|
this.listen(input, 'click', () => this.open());
|
|
211
205
|
this.listen(input, 'input', () => this._onInput());
|
|
212
|
-
this.listen(input, 'keydown', (event) =>
|
|
206
|
+
this.listen(input, 'keydown', (event) => {
|
|
207
|
+
if (!isModifierKey(/** @type {KeyboardEvent} */ (event))) root.removeAttribute('data-focus-origin');
|
|
208
|
+
this._onKeydown(/** @type {KeyboardEvent} */ (event));
|
|
209
|
+
});
|
|
210
|
+
// A text input matches :focus-visible however it was focused, read-only or not, so a click on
|
|
211
|
+
// a read-only Select would draw the keyboard ring. Track how the user last interacted and mark
|
|
212
|
+
// a pointer focus; the ring then follows keyboard focus only, and returns on the first key.
|
|
213
|
+
this.listen(input, 'focus', () => {
|
|
214
|
+
if (this._pointerModality && /** @type {HTMLInputElement} */ (input).readOnly) {
|
|
215
|
+
root.setAttribute('data-focus-origin', 'pointer');
|
|
216
|
+
} else root.removeAttribute('data-focus-origin');
|
|
217
|
+
});
|
|
218
|
+
this.listen(input, 'blur', () => root.removeAttribute('data-focus-origin'));
|
|
219
|
+
this.listen(document, 'keydown', (event) => {
|
|
220
|
+
if (!isModifierKey(/** @type {KeyboardEvent} */ (event))) this._pointerModality = false;
|
|
221
|
+
}, { capture: true });
|
|
213
222
|
this.listen(toggle, 'click', () => {
|
|
214
223
|
this.focus();
|
|
215
224
|
if (this._open) this.close();
|
|
@@ -222,6 +231,7 @@ export class Select extends Component {
|
|
|
222
231
|
this.listen(panel, 'pointerdown', (event) => event.preventDefault());
|
|
223
232
|
this.listen(panel, 'click', (event) => this._onOptionClick(event));
|
|
224
233
|
this.listen(document, 'pointerdown', (event) => {
|
|
234
|
+
this._pointerModality = true;
|
|
225
235
|
if (this._open && !root.contains(/** @type {Node} */ (event.target))) this.close();
|
|
226
236
|
}, { capture: true });
|
|
227
237
|
|
|
@@ -381,26 +391,44 @@ export class Select extends Component {
|
|
|
381
391
|
}
|
|
382
392
|
|
|
383
393
|
/**
|
|
384
|
-
* Creates the five-level priority preset
|
|
394
|
+
* Creates the five-level priority preset: values 0 (lowest) to 4 (highest), each drawn as grey
|
|
395
|
+
* signal bars filled to its level, except the highest, which is the one coloured level — a
|
|
396
|
+
* danger square with an exclamation mark. A "No priority" row (value null, a row of dashes)
|
|
397
|
+
* heads the list and labels the empty control; `clearable: false` removes it.
|
|
385
398
|
* @param {Element|string|null} target Component target.
|
|
386
|
-
* @param {SelectOptions} [options={}] Select overrides other than
|
|
399
|
+
* @param {SelectOptions} [options={}] Select overrides other than items and renderers.
|
|
400
|
+
* `clearable` defaults to true here; `noneLabel` and `placeholder` default to the
|
|
401
|
+
* `priority.none` message ("No priority").
|
|
387
402
|
* @returns {Select}
|
|
388
403
|
*/
|
|
389
404
|
static priority(target, options = {}) {
|
|
390
405
|
const requestedValue = options.value ?? null;
|
|
391
|
-
const
|
|
406
|
+
const clearable = options.clearable ?? true;
|
|
407
|
+
/** @type {Select|null} */
|
|
408
|
+
let select = null;
|
|
409
|
+
// The constructor renders the list once before `select` is assigned; the label is settled by
|
|
410
|
+
// the setItems() below, which renders it again.
|
|
411
|
+
const noneLabel = () => options.noneLabel
|
|
412
|
+
?? (select ? select._message('priority.none', 'No priority') : 'No priority');
|
|
413
|
+
select = new Select(target, {
|
|
392
414
|
...options,
|
|
415
|
+
clearable,
|
|
393
416
|
value: null,
|
|
394
417
|
items: [],
|
|
395
418
|
renderItem(item) {
|
|
396
419
|
return priorityContent(item, true);
|
|
397
420
|
},
|
|
421
|
+
renderNone: options.renderNone ?? (() => priorityContent({ level: null, name: noneLabel() }, true)),
|
|
398
422
|
renderValueAdornment(item) { return priorityIndicator(item.level); }
|
|
399
423
|
});
|
|
424
|
+
if (clearable && !options.placeholder) {
|
|
425
|
+
/** @type {HTMLInputElement} */ (select.refs.input).placeholder = noneLabel();
|
|
426
|
+
}
|
|
427
|
+
const preset = select;
|
|
400
428
|
select.setItems(PRIORITY_KEYS.map((key, level) => {
|
|
401
|
-
let name = level === 2 ?
|
|
402
|
-
if (name === 'priority.medium') name =
|
|
403
|
-
else if (level !== 2) name =
|
|
429
|
+
let name = level === 2 ? preset.msg('priority.medium') : key;
|
|
430
|
+
if (name === 'priority.medium') name = preset._message(key, PRIORITY_FALLBACKS[level]);
|
|
431
|
+
else if (level !== 2) name = preset._message(key, PRIORITY_FALLBACKS[level]);
|
|
404
432
|
return { ID: level, name, level };
|
|
405
433
|
}));
|
|
406
434
|
select.set(requestedValue, { silent: true });
|
|
@@ -453,10 +481,10 @@ export class Select extends Component {
|
|
|
453
481
|
const value = readSelectItem(item, iconKey);
|
|
454
482
|
if (value && typeof value === 'object' && typeof value.nodeType === 'number') return value.cloneNode(true);
|
|
455
483
|
if (typeof value !== 'string' || !value) return null;
|
|
456
|
-
const color = readSelectItem(item, colorKey);
|
|
484
|
+
const color = itemColor(readSelectItem(item, colorKey));
|
|
457
485
|
return h('span', {
|
|
458
486
|
class: 'zx-select__entity-glyph',
|
|
459
|
-
style:
|
|
487
|
+
style: color ? { '--zx-select-entity-color': color } : null,
|
|
460
488
|
ariaHidden: 'true'
|
|
461
489
|
}, icon(value, { size: 14 }));
|
|
462
490
|
};
|
|
@@ -992,28 +1020,26 @@ function priorityContent(item, label) {
|
|
|
992
1020
|
label ? h('span', {}, String(item.name)) : null);
|
|
993
1021
|
}
|
|
994
1022
|
|
|
995
|
-
/**
|
|
1023
|
+
/**
|
|
1024
|
+
* The priority glyph `priorityIcon()` draws, carrying the Select's own class as well so styles
|
|
1025
|
+
* written against it keep applying.
|
|
1026
|
+
* @param {unknown} level Priority level 0–4, or null for none.
|
|
1027
|
+
* @returns {Node}
|
|
1028
|
+
*/
|
|
996
1029
|
function priorityIndicator(level) {
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
dataset: { level: normalized },
|
|
1001
|
-
ariaHidden: 'true'
|
|
1002
|
-
}, Array.from({ length: 5 }, (_, index) => h('span', {
|
|
1003
|
-
dataset: { filled: String(index <= normalized) }
|
|
1004
|
-
})));
|
|
1030
|
+
const glyph = priorityIcon(/** @type {number|null} */ (normalizePriorityLevel(level)));
|
|
1031
|
+
glyph.classList.add('zx-select__priority-icon');
|
|
1032
|
+
return glyph;
|
|
1005
1033
|
}
|
|
1006
1034
|
|
|
1007
1035
|
/** @param {unknown} input @returns {Record<string, any>|null} */
|
|
1008
1036
|
function normalizeStatusItem(input) {
|
|
1009
1037
|
if (!input || typeof input !== 'object' || input.ID == null || input.name == null) return null;
|
|
1010
|
-
const indicators = ['dotted', 'ring', 'progress', 'review', 'ready', 'waiting', 'done', 'canceled', 'duplicate'];
|
|
1011
|
-
const tones = ['muted', 'neutral', 'warning', 'info', 'accent', 'success', 'danger'];
|
|
1012
1038
|
return {
|
|
1013
1039
|
...input,
|
|
1014
1040
|
name: String(input.name),
|
|
1015
|
-
indicator:
|
|
1016
|
-
tone:
|
|
1041
|
+
indicator: STATUS_INDICATORS.includes(String(input.indicator)) ? String(input.indicator) : 'ring',
|
|
1042
|
+
tone: STATUS_TONES.includes(String(input.tone)) ? String(input.tone) : 'muted',
|
|
1017
1043
|
description: input.description == null ? '' : String(input.description),
|
|
1018
1044
|
shortcut: input.shortcut == null ? '' : String(input.shortcut)
|
|
1019
1045
|
};
|
|
@@ -1029,17 +1055,16 @@ function statusContent(item) {
|
|
|
1029
1055
|
item.shortcut ? h('kbd', { class: 'zx-select__status-shortcut' }, item.shortcut) : null);
|
|
1030
1056
|
}
|
|
1031
1057
|
|
|
1032
|
-
/**
|
|
1058
|
+
/**
|
|
1059
|
+
* The status glyph `statusIcon()` draws for a settled status item, carrying the Select's own class
|
|
1060
|
+
* as well so styles written against it keep applying.
|
|
1061
|
+
* @param {Record<string, any>} item Normalized status item.
|
|
1062
|
+
* @returns {Node}
|
|
1063
|
+
*/
|
|
1033
1064
|
function statusIndicator(item) {
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
dataset: { indicator, tone: String(item.tone ?? 'muted') },
|
|
1038
|
-
ariaHidden: 'true'
|
|
1039
|
-
});
|
|
1040
|
-
if (indicator === 'done') marker.append(icon('check', { size: 8 }));
|
|
1041
|
-
if (indicator === 'canceled') marker.append(icon('x', { size: 7 }));
|
|
1042
|
-
return marker;
|
|
1065
|
+
const glyph = statusIcon({ indicator: item.indicator ?? 'ring', tone: item.tone ?? 'muted' });
|
|
1066
|
+
glyph.classList.add('zx-select__status-indicator');
|
|
1067
|
+
return glyph;
|
|
1043
1068
|
}
|
|
1044
1069
|
|
|
1045
1070
|
/** Change emitted when the selected value changes. @event Select#change @type {CustomEvent<SelectChangeDetail>} */
|
|
@@ -1061,6 +1086,17 @@ function readItem(item, reader) {
|
|
|
1061
1086
|
return item[reader];
|
|
1062
1087
|
}
|
|
1063
1088
|
|
|
1089
|
+
/**
|
|
1090
|
+
* Reports a key press that says nothing about how the user is moving focus: a lone modifier, or a
|
|
1091
|
+
* shortcut chord the page or the browser handles.
|
|
1092
|
+
* @param {KeyboardEvent} event Key event.
|
|
1093
|
+
* @returns {boolean}
|
|
1094
|
+
*/
|
|
1095
|
+
function isModifierKey(event) {
|
|
1096
|
+
return ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)
|
|
1097
|
+
|| event.ctrlKey || event.metaKey;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1064
1100
|
/** @param {KeyboardEvent} event @returns {boolean} */
|
|
1065
1101
|
function isPrintable(event) {
|
|
1066
1102
|
return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
inset-block-start: 0;
|
|
58
58
|
inset-block-end: auto;
|
|
59
59
|
border-block-end: 1px solid var(--zx-color-overlay-border);
|
|
60
|
-
border-end-start-radius: var(--zx-sheet-radius, var(--zx-radius-
|
|
61
|
-
border-end-end-radius: var(--zx-sheet-radius, var(--zx-radius-
|
|
60
|
+
border-end-start-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
|
|
61
|
+
border-end-end-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
|
|
62
62
|
--zx-sheet-enter: 0 -100%;
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
inset-block-start: auto;
|
|
67
67
|
inset-block-end: 0;
|
|
68
68
|
border-block-start: 1px solid var(--zx-color-overlay-border);
|
|
69
|
-
border-start-start-radius: var(--zx-sheet-radius, var(--zx-radius-
|
|
70
|
-
border-start-end-radius: var(--zx-sheet-radius, var(--zx-radius-
|
|
69
|
+
border-start-start-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
|
|
70
|
+
border-start-end-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
|
|
71
71
|
--zx-sheet-enter: 0 100%;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -105,11 +105,14 @@
|
|
|
105
105
|
* `display: none` immediately — so `display` and `overlay` transition with `allow-discrete`, which
|
|
106
106
|
* defers the discrete jump until the rest of the transition has finished. `@starting-style` gives
|
|
107
107
|
* the entry its from-state. Where either is unsupported the sheet simply appears and disappears.
|
|
108
|
+
*
|
|
109
|
+
* The slide runs on the drawer curve — most of the distance at once, then a settle — while the
|
|
110
|
+
* fade keeps the ordinary ease, so the panel arrives decisively without flashing in.
|
|
108
111
|
*/
|
|
109
112
|
@media (prefers-reduced-motion: no-preference) {
|
|
110
113
|
.zx-sheet {
|
|
111
114
|
transition:
|
|
112
|
-
translate var(--zx-dur-base) var(--zx-ease),
|
|
115
|
+
translate var(--zx-dur-base) var(--zx-ease-drawer),
|
|
113
116
|
opacity var(--zx-dur-base) var(--zx-ease),
|
|
114
117
|
display var(--zx-dur-base) allow-discrete,
|
|
115
118
|
overlay var(--zx-dur-base) allow-discrete;
|
|
@@ -193,7 +196,7 @@
|
|
|
193
196
|
@media (prefers-reduced-motion: no-preference) {
|
|
194
197
|
.zx-sheet[data-docked] {
|
|
195
198
|
transition:
|
|
196
|
-
flex-basis var(--zx-dur-base) var(--zx-ease),
|
|
199
|
+
flex-basis var(--zx-dur-base) var(--zx-ease-drawer),
|
|
197
200
|
opacity var(--zx-dur-base) var(--zx-ease),
|
|
198
201
|
display var(--zx-dur-base) allow-discrete,
|
|
199
202
|
overlay var(--zx-dur-base) allow-discrete;
|
|
@@ -363,8 +366,8 @@
|
|
|
363
366
|
@media (prefers-reduced-motion: no-preference) {
|
|
364
367
|
.zx-sheet[data-stack] {
|
|
365
368
|
transition:
|
|
366
|
-
transform var(--zx-dur-base) var(--zx-ease),
|
|
367
|
-
translate var(--zx-dur-base) var(--zx-ease),
|
|
369
|
+
transform var(--zx-dur-base) var(--zx-ease-drawer),
|
|
370
|
+
translate var(--zx-dur-base) var(--zx-ease-drawer),
|
|
368
371
|
opacity var(--zx-dur-base) var(--zx-ease),
|
|
369
372
|
display var(--zx-dur-base) allow-discrete,
|
|
370
373
|
overlay var(--zx-dur-base) allow-discrete;
|
|
@@ -147,17 +147,31 @@
|
|
|
147
147
|
font-variant-numeric: tabular-nums;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/*
|
|
151
|
+
* A mark sits under the thumb's centre at its value — half a thumb in from either end of the rail
|
|
152
|
+
* — and centres its label there, except that a label is never pushed past either end of the
|
|
153
|
+
* track: the outermost ones end flush with it instead of spilling out of the component.
|
|
154
|
+
* `--zx-slider-mark` is the value's position on the scale, from 0 to 1.
|
|
155
|
+
*/
|
|
150
156
|
.zx-slider__marks {
|
|
151
157
|
position: relative;
|
|
158
|
+
inline-size: 100%;
|
|
152
159
|
block-size: var(--zx-space-5);
|
|
153
160
|
margin-block-start: var(--zx-space-1);
|
|
161
|
+
container-type: inline-size;
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
.zx-slider__mark {
|
|
165
|
+
--zx-slider-mark-at: calc(var(--zx-slider-thumb) / 2
|
|
166
|
+
+ (100cqi - var(--zx-slider-thumb)) * var(--zx-slider-mark, 0));
|
|
157
167
|
position: absolute;
|
|
158
168
|
inset-block-start: 0;
|
|
159
|
-
inset-inline-start: var(--zx-slider-mark
|
|
160
|
-
transform: translateX(
|
|
169
|
+
inset-inline-start: var(--zx-slider-mark-at);
|
|
170
|
+
transform: translateX(clamp(
|
|
171
|
+
-1 * var(--zx-slider-mark-at),
|
|
172
|
+
-50%,
|
|
173
|
+
100cqi - var(--zx-slider-mark-at) - 100%
|
|
174
|
+
));
|
|
161
175
|
color: var(--zx-color-text-muted);
|
|
162
176
|
font-size: var(--zx-text-xs);
|
|
163
177
|
white-space: nowrap;
|
|
@@ -279,7 +279,7 @@ export class Slider extends Component {
|
|
|
279
279
|
const label = typeof mark === 'number' ? this._format(value) : String(mark.label ?? this._format(value));
|
|
280
280
|
element.append(h('span', {
|
|
281
281
|
class: 'zx-slider__mark',
|
|
282
|
-
style: { '--zx-slider-mark':
|
|
282
|
+
style: { '--zx-slider-mark': String(this._ratio(value)) }
|
|
283
283
|
}, label));
|
|
284
284
|
}
|
|
285
285
|
return element;
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
.zx-sort-control__direction {
|
|
11
11
|
inline-size: var(--zx-control-height);
|
|
12
12
|
padding: 0;
|
|
13
|
-
border-radius: var(--zx-radius
|
|
13
|
+
border-radius: var(--zx-control-radius);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.zx-sort-control__direction:focus-visible { box-shadow: var(--zx-focus-ring); }
|
|
17
|
+
/* The arrow is the current direction, not decoration, so it keeps the text colour a ghost glyph
|
|
18
|
+
would otherwise give up. */
|
|
19
|
+
.zx-sort-control .zx-sort-control__direction > .zx-icon { color: var(--zx-color-text); }
|
|
17
20
|
.zx-sort-control .zx-select__control { background: var(--zx-color-bg-control); }
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The tile is a size ladder, not a colour: the value is the loudest thing on it, the label is
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* a server assigns the metric.
|
|
2
|
+
* The tile is a size ladder, not a colour: the value is the loudest thing on it, the label is a
|
|
3
|
+
* quiet sentence-case line above it, and the caption, delta and sparkline are quieter than both.
|
|
4
|
+
* The icon is a muted glyph, not a coloured chip. `kind` tints the sparkline only — never the
|
|
5
|
+
* value, which has to stay at full text contrast whatever intent a server assigns the metric.
|
|
6
|
+
*
|
|
7
|
+
* A column, with the sparkline pushed to the foot: tiles in one row are as tall as the tallest,
|
|
8
|
+
* so their labels, values and sparklines line up whether or not each tile has every part.
|
|
6
9
|
*/
|
|
7
10
|
.zx-stat-tile {
|
|
8
11
|
--zx-stat-tile-value-size: var(--zx-text-2xl);
|
|
9
|
-
display:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: var(--zx-space-1);
|
|
12
15
|
min-inline-size: 0;
|
|
13
16
|
padding: var(--zx-space-4);
|
|
14
17
|
border: 1px solid var(--zx-color-border);
|
|
@@ -60,25 +63,19 @@ button.zx-stat-tile:focus-visible {
|
|
|
60
63
|
.zx-stat-tile__label {
|
|
61
64
|
min-inline-size: 0;
|
|
62
65
|
overflow: hidden;
|
|
63
|
-
font-size: var(--zx-text-
|
|
64
|
-
font-weight:
|
|
65
|
-
letter-spacing: 0.04em;
|
|
66
|
+
font-size: var(--zx-text-sm);
|
|
67
|
+
font-weight: var(--zx-weight-medium);
|
|
66
68
|
line-height: 1.4;
|
|
67
|
-
text-transform: uppercase;
|
|
68
69
|
text-overflow: ellipsis;
|
|
69
70
|
white-space: nowrap;
|
|
70
71
|
color: var(--zx-color-text-muted);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
.zx-stat-tile__icon {
|
|
74
|
-
display: grid;
|
|
75
|
+
display: inline-grid;
|
|
75
76
|
flex: none;
|
|
76
77
|
place-items: center;
|
|
77
|
-
|
|
78
|
-
block-size: 1.75rem;
|
|
79
|
-
border-radius: var(--zx-radius-full);
|
|
80
|
-
background: var(--zx-color-bg-muted);
|
|
81
|
-
color: var(--zx-color-text-muted);
|
|
78
|
+
color: var(--zx-color-text-subtle);
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
/*
|
|
@@ -87,21 +84,35 @@ button.zx-stat-tile:focus-visible {
|
|
|
87
84
|
*/
|
|
88
85
|
.zx-stat-tile__value {
|
|
89
86
|
min-inline-size: 0;
|
|
87
|
+
margin-block-start: var(--zx-space-1);
|
|
90
88
|
font-size: var(--zx-stat-tile-value-size);
|
|
91
|
-
font-weight:
|
|
89
|
+
font-weight: 650;
|
|
92
90
|
font-variant-numeric: tabular-nums;
|
|
91
|
+
letter-spacing: var(--zx-tracking-title);
|
|
93
92
|
line-height: 1.15;
|
|
94
93
|
overflow-wrap: break-word;
|
|
95
94
|
color: var(--zx-color-text);
|
|
96
95
|
}
|
|
97
96
|
|
|
97
|
+
/* What the value is out of, or measured in: one muted line, read with the value. */
|
|
98
|
+
.zx-stat-tile__caption {
|
|
99
|
+
min-inline-size: 0;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
color: var(--zx-color-text-muted);
|
|
102
|
+
font-size: var(--zx-text-sm);
|
|
103
|
+
font-variant-numeric: tabular-nums;
|
|
104
|
+
line-height: 1.4;
|
|
105
|
+
text-overflow: ellipsis;
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
.zx-stat-tile__delta {
|
|
99
110
|
display: flex;
|
|
100
111
|
gap: var(--zx-space-1);
|
|
101
112
|
align-items: baseline;
|
|
102
113
|
min-inline-size: 0;
|
|
103
114
|
font-size: var(--zx-text-xs);
|
|
104
|
-
font-weight:
|
|
115
|
+
font-weight: var(--zx-weight-medium);
|
|
105
116
|
font-variant-numeric: tabular-nums;
|
|
106
117
|
line-height: 1.4;
|
|
107
118
|
color: var(--zx-color-text-muted);
|
|
@@ -124,6 +135,12 @@ button.zx-stat-tile:focus-visible {
|
|
|
124
135
|
align-self: center;
|
|
125
136
|
}
|
|
126
137
|
|
|
138
|
+
.zx-stat-tile__delta-value {
|
|
139
|
+
/* A figure never breaks ("+23" over "pcs"); the comparison beside it gives way first. */
|
|
140
|
+
flex: none;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
.zx-stat-tile__delta-label {
|
|
128
145
|
min-inline-size: 0;
|
|
129
146
|
overflow: hidden;
|
|
@@ -136,9 +153,12 @@ button.zx-stat-tile:focus-visible {
|
|
|
136
153
|
|
|
137
154
|
.zx-stat-tile__trend {
|
|
138
155
|
display: block;
|
|
156
|
+
flex: none;
|
|
157
|
+
box-sizing: content-box;
|
|
139
158
|
inline-size: 100%;
|
|
140
159
|
block-size: 2rem;
|
|
141
|
-
margin-block-start:
|
|
160
|
+
margin-block-start: auto;
|
|
161
|
+
padding-block-start: var(--zx-space-2);
|
|
142
162
|
color: var(--zx-color-text-placeholder);
|
|
143
163
|
}
|
|
144
164
|
|
|
@@ -147,32 +167,7 @@ button.zx-stat-tile:focus-visible {
|
|
|
147
167
|
gap: var(--zx-space-2);
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
/* Intent. The
|
|
151
|
-
.zx-stat-tile[data-kind="accent"] .zx-stat-tile__icon {
|
|
152
|
-
background: var(--zx-color-accent-subtle);
|
|
153
|
-
color: var(--zx-color-accent);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.zx-stat-tile[data-kind="success"] .zx-stat-tile__icon {
|
|
157
|
-
background: var(--zx-color-success-bg);
|
|
158
|
-
color: var(--zx-color-success);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.zx-stat-tile[data-kind="warning"] .zx-stat-tile__icon {
|
|
162
|
-
background: var(--zx-color-warning-bg);
|
|
163
|
-
color: var(--zx-color-warning);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.zx-stat-tile[data-kind="danger"] .zx-stat-tile__icon {
|
|
167
|
-
background: var(--zx-color-danger-bg);
|
|
168
|
-
color: var(--zx-color-danger);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.zx-stat-tile[data-kind="info"] .zx-stat-tile__icon {
|
|
172
|
-
background: var(--zx-color-info-bg);
|
|
173
|
-
color: var(--zx-color-info);
|
|
174
|
-
}
|
|
175
|
-
|
|
170
|
+
/* Intent. The sparkline carries it as its stroke; the icon stays a muted glyph. */
|
|
176
171
|
.zx-stat-tile[data-kind="accent"] .zx-stat-tile__trend {
|
|
177
172
|
color: var(--zx-color-accent);
|
|
178
173
|
}
|
|
@@ -25,6 +25,7 @@ import { skeleton } from '../skeleton/skeleton.js';
|
|
|
25
25
|
* @typedef {Object} StatTileNameParts
|
|
26
26
|
* @property {string} [label] The metric's name.
|
|
27
27
|
* @property {string} [value] The formatted value.
|
|
28
|
+
* @property {string} [caption] The context line under the value, e.g. `'of 13,184'`.
|
|
28
29
|
* @property {string} [delta] The change, already worded for speech (`'up 2'`, not `'+2'`).
|
|
29
30
|
* @property {string} [deltaLabel] What the change is measured against.
|
|
30
31
|
*/
|
|
@@ -36,6 +37,8 @@ import { skeleton } from '../skeleton/skeleton.js';
|
|
|
36
37
|
* @property {StatTileFormat} [format='number'] How a numeric value is rendered: a named mode, a
|
|
37
38
|
* callback receiving the raw value, or null to print the value unformatted.
|
|
38
39
|
* @property {string|null} [currency=null] ISO 4217 code used by `format: 'currency'`.
|
|
40
|
+
* @property {string} [caption=''] A muted context line under the value — what the number is out
|
|
41
|
+
* of or measured in, e.g. `'of 13,184'` or `'open tickets'`. Part of the accessible name.
|
|
39
42
|
* @property {string} [locale] BCP 47 tag for every formatter; defaults to the active language.
|
|
40
43
|
* @property {number|string|null} [delta=null] Change against the previous period. Rendered with
|
|
41
44
|
* its own sign and an arrow, never with colour alone.
|
|
@@ -46,9 +49,10 @@ import { skeleton } from '../skeleton/skeleton.js';
|
|
|
46
49
|
* @property {string} [deltaLabel=''] What the change is measured against, e.g. `'vs. last week'`.
|
|
47
50
|
* @property {number[]} [trend=[]] Values for a decorative sparkline. Fewer than two usable numbers
|
|
48
51
|
* render nothing.
|
|
49
|
-
* @property {string|null} [icon=null] Icon name from `icons.js`, shown beside the label
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
+
* @property {string|null} [icon=null] Icon name from `icons.js`, shown muted beside the label —
|
|
53
|
+
* a glyph, not a coloured chip.
|
|
54
|
+
* @property {StatTileKind} [kind='neutral'] Semantic intent. It tints the sparkline and never the
|
|
55
|
+
* value, which stays at full text contrast whatever intent a record carries.
|
|
52
56
|
* @property {string|null} [href=null] Destination. A tile with an href is an `<a>`.
|
|
53
57
|
* @property {boolean} [clickable=false] Renders a `<button>` without an href. Passing `onclick`
|
|
54
58
|
* sets this implicitly; set it directly to subscribe with `on('click', …)` instead.
|
|
@@ -93,6 +97,7 @@ const defaults = Object.freeze({
|
|
|
93
97
|
value: null,
|
|
94
98
|
format: 'number',
|
|
95
99
|
currency: null,
|
|
100
|
+
caption: '',
|
|
96
101
|
delta: null,
|
|
97
102
|
deltaKind: 'neutral',
|
|
98
103
|
deltaLabel: '',
|
|
@@ -306,17 +311,19 @@ export function statTileTag(options = {}) {
|
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
/**
|
|
309
|
-
* Composes the tile's accessible name: label, then value, then how it changed.
|
|
314
|
+
* Composes the tile's accessible name: label, then value with its caption, then how it changed.
|
|
310
315
|
*
|
|
311
316
|
* The sparkline is decoration and contributes nothing, and the delta arrives already worded —
|
|
312
|
-
* `'up 2'` rather than `'+2'` — because a screen reader should not have to interpret a glyph.
|
|
317
|
+
* `'up 2'` rather than `'+2'` — because a screen reader should not have to interpret a glyph. The
|
|
318
|
+
* caption is read with the value it qualifies (`'2,184 of 13,184'`), not as a separate clause.
|
|
313
319
|
*
|
|
314
320
|
* @param {StatTileNameParts} [parts={}] The pieces, already formatted and translated.
|
|
315
321
|
* @returns {string} The name, with empty pieces dropped rather than left as stray punctuation.
|
|
316
322
|
*/
|
|
317
323
|
export function statTileName(parts = {}) {
|
|
324
|
+
const value = [parts.value, parts.caption].map(trimmed).filter(Boolean).join(' ');
|
|
318
325
|
const change = [parts.delta, parts.deltaLabel].map(trimmed).filter(Boolean).join(' ');
|
|
319
|
-
return [trimmed(parts.label),
|
|
326
|
+
return [trimmed(parts.label), value, change].filter(Boolean).join(', ');
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
/**
|
|
@@ -391,10 +398,12 @@ function fillStatTile(root, options, message) {
|
|
|
391
398
|
currency: options.currency
|
|
392
399
|
});
|
|
393
400
|
const deltaLabel = asText(options.deltaLabel);
|
|
401
|
+
const caption = asText(options.caption).trim();
|
|
394
402
|
|
|
395
403
|
applyName(root, statTileName({
|
|
396
404
|
label,
|
|
397
405
|
value,
|
|
406
|
+
caption,
|
|
398
407
|
delta: delta === null ? '' : spokenDelta(delta, message),
|
|
399
408
|
deltaLabel
|
|
400
409
|
}));
|
|
@@ -407,6 +416,7 @@ function fillStatTile(root, options, message) {
|
|
|
407
416
|
// `Element`, not `HTMLElement`: the sparkline is an SVG element and belongs in the same list.
|
|
408
417
|
/** @type {Element[]} */
|
|
409
418
|
const children = [header, h('div', { class: 'zx-stat-tile__value' }, value)];
|
|
419
|
+
if (caption) children.push(h('div', { class: 'zx-stat-tile__caption' }, caption));
|
|
410
420
|
if (delta !== null) children.push(deltaBody(delta, deltaLabel, options.deltaKind));
|
|
411
421
|
const trend = trendBody(options.trend);
|
|
412
422
|
if (trend) children.push(trend);
|
|
@@ -481,6 +491,7 @@ function loadingBody(options) {
|
|
|
481
491
|
h('div', { class: 'zx-stat-tile__header' }, skeleton({ width: '55%', height: '0.75rem' })),
|
|
482
492
|
skeleton({ width: '70%', height: '1.75rem', radius: 'md' })
|
|
483
493
|
];
|
|
494
|
+
if (asText(options.caption).trim()) blocks.push(skeleton({ width: '35%', height: '0.75rem' }));
|
|
484
495
|
if (options.delta !== null && options.delta !== undefined) {
|
|
485
496
|
blocks.push(skeleton({ width: '40%', height: '0.75rem' }));
|
|
486
497
|
}
|