@zeyos/zx 5.0.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 +164 -0
- package/README.md +2 -1
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +9 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +288 -14
- 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/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/dropdown/dropdown.d.ts +22 -7
- package/dist/types/components/field/field.d.ts +30 -0
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +7 -0
- 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/launcher/launcher.d.ts +8 -0
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
- package/dist/types/components/menu-button/menu-button.d.ts +6 -0
- package/dist/types/components/message/message.d.ts +14 -1
- package/dist/types/components/modal/modal.d.ts +30 -0
- 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/select.d.ts +6 -0
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/table/table.d.ts +265 -17
- 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/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/overlay-host.d.ts +28 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/index.d.ts +1 -0
- 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 +31 -5
- package/dist/types/zeyos/table.d.ts +245 -8
- package/dist/types/zeyos/view.d.ts +16 -2
- package/dist/zx-zeyos.esm.js +4422 -430
- package/dist/zx-zeyos.esm.min.js +5 -5
- package/dist/zx.css +1417 -134
- package/dist/zx.esm.js +6986 -830
- package/dist/zx.esm.min.js +8 -8
- package/dist/zx.global.js +6986 -830
- package/dist/zx.global.min.js +8 -8
- package/dist/zx.min.css +1 -1
- package/docs/api.json +794 -17
- package/docs/llms.md +503 -63
- package/docs/llms.txt +6 -2
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-list/activity-list.js +27 -1
- package/src/components/app-icon/app-icon.css +13 -2
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +19 -0
- package/src/components/app-sidebar/app-sidebar.css +204 -3
- package/src/components/app-sidebar/app-sidebar.js +646 -40
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/calendar/calendar.css +1 -1
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +2 -2
- package/src/components/card-view/card-view.css +5 -5
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/code-editor/code-editor.css +1 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-input.css +1 -1
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/time-picker.css +2 -2
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.js +157 -72
- package/src/components/field/field.js +33 -3
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.js +10 -59
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +300 -8
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +1 -1
- package/src/components/kanban-view/kanban-view.css +3 -3
- package/src/components/kanban-view/kanban-view.js +5 -2
- package/src/components/launcher/launcher.css +1 -1
- package/src/components/launcher/launcher.js +29 -3
- package/src/components/list-toolbar/list-toolbar.js +39 -13
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +63 -0
- package/src/components/menu-button/menu-button.js +97 -11
- package/src/components/message/message.js +60 -20
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +66 -13
- 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/number-field/field-adapter.js +2 -1
- package/src/components/pagination/pagination.css +5 -5
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +3 -2
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +1 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/select.js +9 -0
- package/src/components/sheet/sheet.css +1 -1
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +2 -2
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +1 -1
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +9 -5
- package/src/components/table/table.css +86 -11
- package/src/components/table/table.js +628 -57
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tooltip/tooltip.js +35 -17
- package/src/components/tree/tree.css +1 -1
- 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/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +12 -2
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/overlay-host.js +64 -9
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +5 -2
- package/src/internal/app-rail.js +134 -53
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/form.js +5 -3
- package/src/zeyos/index.js +3 -0
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +44 -13
- package/src/zeyos/table.js +220 -80
- package/src/zeyos/view.js +43 -93
- package/styles/base.css +16 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +4 -0
- package/styles/tokens/semantic.css +56 -28
- package/styles/zx.css +3 -0
package/src/internal/app-rail.js
CHANGED
|
@@ -17,6 +17,9 @@ import { Tooltip } from '../components/tooltip/tooltip.js';
|
|
|
17
17
|
* @property {() => void} [invoke] Application-owned non-link action.
|
|
18
18
|
* @property {boolean} [disabled] Whether activation is unavailable.
|
|
19
19
|
* @property {AppNavItem[]} [children] Child destinations/actions.
|
|
20
|
+
* @property {boolean} [section] Makes the item a section: a quiet heading over its `children`, which
|
|
21
|
+
* render at the same level. A section is never focused, selected, or collapsed, and needs no `id`.
|
|
22
|
+
* @property {Node|{toElement: () => Node|null}} [actions] A section's trailing controls, such as an add button.
|
|
20
23
|
*/
|
|
21
24
|
/**
|
|
22
25
|
* @typedef {Object} AppRailPresenterOptions
|
|
@@ -45,6 +48,14 @@ import { Tooltip } from '../components/tooltip/tooltip.js';
|
|
|
45
48
|
export class AppRailPresenter extends Component {
|
|
46
49
|
static cssName = 'app-rail';
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* BEM block of the rail's own parts (header, list, footer, row, item). A component built on the
|
|
53
|
+
* presenter names its parts after itself; flyout panels keep the `zx-app-rail__` vocabulary,
|
|
54
|
+
* because every rail portals the same panel.
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
static partBlock = 'zx-app-rail';
|
|
58
|
+
|
|
48
59
|
/** @type {Readonly<AppRailPresenterOptions>} */
|
|
49
60
|
static defaults = {
|
|
50
61
|
items: [],
|
|
@@ -93,13 +104,14 @@ export class AppRailPresenter extends Component {
|
|
|
93
104
|
root.dataset.orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
94
105
|
const sides = root.dataset.orientation === 'horizontal' ? ['top', 'bottom'] : ['left', 'right'];
|
|
95
106
|
root.dataset.side = sides.includes(this.options.side) ? this.options.side : sides[0];
|
|
96
|
-
const
|
|
97
|
-
const
|
|
107
|
+
const block = this._partBlock();
|
|
108
|
+
const header = h('div', { class: `${block}__header` });
|
|
109
|
+
const footer = h('div', { class: `${block}__footer` });
|
|
98
110
|
appendSlot(header, this.options.header);
|
|
99
111
|
appendSlot(footer, this.options.footer);
|
|
100
112
|
header.hidden = !header.hasChildNodes();
|
|
101
113
|
footer.hidden = !footer.hasChildNodes();
|
|
102
|
-
root.replaceChildren(header, h('ul', { ref: 'list', class:
|
|
114
|
+
root.replaceChildren(header, h('ul', { ref: 'list', class: `${block}__list` }), footer);
|
|
103
115
|
this.listen(root, 'keydown', (event) => this._onRailKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
104
116
|
return root;
|
|
105
117
|
}
|
|
@@ -209,31 +221,28 @@ export class AppRailPresenter extends Component {
|
|
|
209
221
|
this._clearArtifacts();
|
|
210
222
|
this.refs.list.replaceChildren();
|
|
211
223
|
this._interaction = new RailListenerScope(/** @type {HTMLElement} */ (this.el));
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
level: 1,
|
|
225
|
-
location: 'root'
|
|
226
|
-
})
|
|
227
|
-
});
|
|
228
|
-
const row = h('li', { class: 'zx-app-rail__row' }, control);
|
|
224
|
+
let started = false;
|
|
225
|
+
for (const item of railEntries(this._items)) {
|
|
226
|
+
// A section has no rail control: the rail draws a rule between the groups instead.
|
|
227
|
+
if (item === SECTION_BREAK) {
|
|
228
|
+
if (started) this.refs.list.append(h('li', { class: `${this._partBlock()}__separator`, ariaHidden: 'true' }));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
started = true;
|
|
232
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0
|
|
233
|
+
&& this._flyoutsFor(item);
|
|
234
|
+
const control = this._renderControl(item, hasChildren);
|
|
235
|
+
const row = h('li', { class: `${this._partBlock()}__row` }, control);
|
|
229
236
|
this.refs.list.append(row);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
+
if (this._tooltipFor(item)) {
|
|
238
|
+
const tooltip = new Tooltip(control, {
|
|
239
|
+
content: item.label,
|
|
240
|
+
placement: tooltipPlacement(this.el.dataset.side),
|
|
241
|
+
delay: 450
|
|
242
|
+
});
|
|
243
|
+
this._tooltips.push(tooltip);
|
|
244
|
+
this._tooltipById.set(String(item.id), tooltip);
|
|
245
|
+
}
|
|
237
246
|
if (hasChildren) this._buildFlyout(item, control, String(item.id), null);
|
|
238
247
|
else this._interaction.listen(control, 'click', (event) => {
|
|
239
248
|
const selected = this._select(item, event);
|
|
@@ -243,21 +252,66 @@ export class AppRailPresenter extends Component {
|
|
|
243
252
|
}
|
|
244
253
|
}
|
|
245
254
|
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (open.some((flyout) => path.includes(flyout.panel) || path.includes(flyout.trigger))) return;
|
|
252
|
-
for (const flyout of open) if (flyout.parentKey === null) this.closeFlyout(flyout.key);
|
|
253
|
-
});
|
|
255
|
+
}
|
|
256
|
+
// Outside presses need no listener here: each open flyout is a Dropdown on the overlay stack,
|
|
257
|
+
// which closes it on a press outside it and every flyout opened from it, and a nested flyout,
|
|
258
|
+
// anchored inside its parent's panel, counts as part of that parent.
|
|
259
|
+
this._afterRebuild();
|
|
254
260
|
this._syncActive();
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
/** @returns {string} The BEM block of the rail's own parts. @protected */
|
|
264
|
+
_partBlock() {
|
|
265
|
+
return /** @type {typeof AppRailPresenter} */ (this.constructor).partBlock;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Builds one top-level rail control. The control must carry `data-app-nav-id` and
|
|
270
|
+
* `data-app-nav-value-id`, which keyboard handling, flyouts, and the active state read.
|
|
271
|
+
* @param {AppNavItem} item Top-level item.
|
|
272
|
+
* @param {boolean} hasFlyout Whether the control opens a flyout of the item's children.
|
|
273
|
+
* @returns {HTMLElement}
|
|
274
|
+
* @protected
|
|
275
|
+
*/
|
|
276
|
+
_renderControl(item, hasFlyout) {
|
|
277
|
+
return navControl(item, {
|
|
278
|
+
className: 'zx-app-rail__item',
|
|
279
|
+
index: String(item.id),
|
|
280
|
+
iconOnly: true,
|
|
281
|
+
disclosure: hasFlyout,
|
|
282
|
+
disclosureIcon: railDisclosureIcon(this.el.dataset.side),
|
|
283
|
+
visual: appNavIcon(item, this.options.renderIcon, {
|
|
284
|
+
orientation: this.el.dataset.orientation,
|
|
285
|
+
collapsed: true,
|
|
286
|
+
side: this.el.dataset.side,
|
|
287
|
+
level: 1,
|
|
288
|
+
location: 'root'
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Whether an item with children presents them in a flyout. @param {AppNavItem} item @returns {boolean} @protected */
|
|
294
|
+
_flyoutsFor(item) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Whether a top-level control gets a hover/focus tooltip naming it. @param {AppNavItem} item @returns {boolean} @protected */
|
|
299
|
+
_tooltipFor(item) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Runs after every rebuild, before the active state is applied. @returns {void} @protected */
|
|
304
|
+
_afterRebuild() {}
|
|
305
|
+
|
|
257
306
|
/** @param {AppNavItem} item @param {HTMLElement} trigger @param {string} key @param {string|null} parentKey @returns {void} */
|
|
258
307
|
_buildFlyout(item, trigger, key, parentKey) {
|
|
259
308
|
const list = h('ul', { class: 'zx-app-rail__flyout-list' });
|
|
260
|
-
for (const child of item.children ?? []) {
|
|
309
|
+
for (const child of railEntries(item.children ?? [], true)) {
|
|
310
|
+
if (child.section) {
|
|
311
|
+
// A caption, not a destination: keyboard movement skips it with every other non-action.
|
|
312
|
+
list.append(h('li', { class: 'zx-app-rail__flyout-section' }, child.label));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
261
315
|
const childKey = `${key}/${child.id}`;
|
|
262
316
|
const hasChildren = Boolean(child.children?.length);
|
|
263
317
|
const control = navControl(child, {
|
|
@@ -292,6 +346,17 @@ export class AppRailPresenter extends Component {
|
|
|
292
346
|
offset: 6
|
|
293
347
|
});
|
|
294
348
|
const actualPanel = dropdown.getPanel();
|
|
349
|
+
// Escape arrives through the overlay stack, which offers it to the topmost flyout only. The
|
|
350
|
+
// rail returns focus itself, so the trigger's focus does not reopen what Escape just closed,
|
|
351
|
+
// and a flyout is reached with the arrow keys rather than Tab: every tile is its own Tab stop.
|
|
352
|
+
dropdown._configure({
|
|
353
|
+
tabReach: false,
|
|
354
|
+
onEscape: () => {
|
|
355
|
+
const hadFocus = actualPanel.contains(document.activeElement);
|
|
356
|
+
this.closeFlyout(key);
|
|
357
|
+
if (hadFocus) this._restoreFocus(trigger);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
295
360
|
actualPanel.classList.add('zx-app-rail__popover');
|
|
296
361
|
// The panel is portaled out of the rail, so its stylesheet cannot ask which edge the rail
|
|
297
362
|
// occupies. Presentation only: it tells the panel's styles which side it opened from.
|
|
@@ -300,10 +365,15 @@ export class AppRailPresenter extends Component {
|
|
|
300
365
|
actualPanel.setAttribute('aria-label', this._message('appRail.subNavigation', '%1 sub-navigation', item.label));
|
|
301
366
|
const flyout = { key, parentKey, item, trigger, panel: actualPanel, dropdown };
|
|
302
367
|
this._flyouts.set(key, flyout);
|
|
368
|
+
// While a tile's own flyout is open its tooltip says nothing the flyout's title does not, and
|
|
369
|
+
// would take the first Escape meant for the flyout.
|
|
370
|
+
const tooltip = parentKey === null ? this._tooltipById.get(String(item.id)) : null;
|
|
303
371
|
dropdown.on('open', () => {
|
|
372
|
+
tooltip?.disable();
|
|
304
373
|
if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: true });
|
|
305
374
|
});
|
|
306
375
|
dropdown.on('close', () => {
|
|
376
|
+
tooltip?.enable();
|
|
307
377
|
if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: false });
|
|
308
378
|
});
|
|
309
379
|
|
|
@@ -393,20 +463,6 @@ export class AppRailPresenter extends Component {
|
|
|
393
463
|
const controls = actions(flyout.panel);
|
|
394
464
|
const current = /** @type {Element|null} */ (event.target)?.closest?.('a, button');
|
|
395
465
|
const index = controls.indexOf(current);
|
|
396
|
-
if (event.key === 'Escape') {
|
|
397
|
-
event.preventDefault();
|
|
398
|
-
event.stopPropagation();
|
|
399
|
-
const nested = [...this._flyouts.values()].find((candidate) =>
|
|
400
|
-
candidate.trigger === current && candidate.dropdown.isOpen());
|
|
401
|
-
if (nested) {
|
|
402
|
-
this.closeFlyout(nested.key);
|
|
403
|
-
this._restoreFocus(nested.trigger);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
this.closeFlyout(flyout.key);
|
|
407
|
-
this._restoreFocus(flyout.trigger);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
466
|
const nested = [...this._flyouts.values()].find((candidate) => candidate.trigger === current);
|
|
411
467
|
const into = this.el.dataset.side === 'right' ? 'ArrowLeft' : 'ArrowRight';
|
|
412
468
|
const out = into === 'ArrowRight' ? 'ArrowLeft' : 'ArrowRight';
|
|
@@ -416,7 +472,8 @@ export class AppRailPresenter extends Component {
|
|
|
416
472
|
this.openFlyout(nested.key, { focus: true });
|
|
417
473
|
return;
|
|
418
474
|
}
|
|
419
|
-
|
|
475
|
+
// Out of a nested flyout, or out of a top-level one beside a vertical rail, back to its trigger.
|
|
476
|
+
if ((flyout.parentKey !== null || this.el.dataset.orientation === 'vertical') && event.key === out) {
|
|
420
477
|
event.preventDefault();
|
|
421
478
|
event.stopPropagation();
|
|
422
479
|
this.closeFlyout(flyout.key);
|
|
@@ -606,6 +663,22 @@ class RailListenerScope extends Component {
|
|
|
606
663
|
}
|
|
607
664
|
}
|
|
608
665
|
|
|
666
|
+
/** Marks the boundary of a section in a flattened rail. */
|
|
667
|
+
const SECTION_BREAK = Object.freeze({});
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Flattens sections for a rail: a section's children take its place. With `labels`, each section
|
|
671
|
+
* leaves its own item ahead of its children, for a flyout to caption; otherwise a section break.
|
|
672
|
+
* @param {AppNavItem[]} items Normalized items.
|
|
673
|
+
* @param {boolean} [labels=false] Keep section items as captions.
|
|
674
|
+
* @returns {AppNavItem[]}
|
|
675
|
+
*/
|
|
676
|
+
function railEntries(items, labels = false) {
|
|
677
|
+
return items.flatMap((item) => (item.section
|
|
678
|
+
? [labels ? item : /** @type {AppNavItem} */ (SECTION_BREAK), ...railEntries(item.children ?? [], labels)]
|
|
679
|
+
: [item]));
|
|
680
|
+
}
|
|
681
|
+
|
|
609
682
|
/**
|
|
610
683
|
* Normalizes an application navigation tree without mutating caller data.
|
|
611
684
|
* @param {unknown} items Candidate tree.
|
|
@@ -613,13 +686,21 @@ class RailListenerScope extends Component {
|
|
|
613
686
|
*/
|
|
614
687
|
export function normalizeAppItems(items) {
|
|
615
688
|
if (!Array.isArray(items)) return [];
|
|
616
|
-
return items.filter((item) => item && typeof item === 'object' && item.
|
|
617
|
-
|
|
689
|
+
return items.filter((item) => item && typeof item === 'object' && item.label != null
|
|
690
|
+
&& (item.id != null || item.section === true))
|
|
691
|
+
.map((item, index) => {
|
|
618
692
|
const normalized = {
|
|
619
693
|
...item,
|
|
694
|
+
// A section is addressed by nothing, so one written without an id gets a stable one.
|
|
695
|
+
id: item.id ?? `section:${index}:${item.label}`,
|
|
620
696
|
label: String(item.label),
|
|
621
697
|
children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
|
|
622
698
|
};
|
|
699
|
+
if (item.section === true) {
|
|
700
|
+
normalized.section = true;
|
|
701
|
+
delete normalized.href;
|
|
702
|
+
return normalized;
|
|
703
|
+
}
|
|
623
704
|
if (item.href == null) return normalized;
|
|
624
705
|
const href = safeHref(item.href);
|
|
625
706
|
if (href !== null) normalized.href = href;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h, safeHref } from '../core/dom.js';
|
|
2
2
|
import { icon } from '../core/icons.js';
|
|
3
|
+
import { uid } from '../core/util.js';
|
|
3
4
|
|
|
4
5
|
/** @typedef {'menuitem'|'menuitemcheckbox'|'menuitemradio'} MenuItemRole */
|
|
5
6
|
/**
|
|
@@ -31,7 +32,27 @@ import { icon } from '../core/icons.js';
|
|
|
31
32
|
* @property {string} label Visible heading.
|
|
32
33
|
*/
|
|
33
34
|
/** @typedef {{type: 'separator'}} MenuSeparator */
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* One option of a menu choice.
|
|
37
|
+
* @typedef {Object} MenuChoiceOption
|
|
38
|
+
* @property {unknown} value Value the choice takes.
|
|
39
|
+
* @property {string} label Accessible name and tooltip; the option shows its glyph or icon.
|
|
40
|
+
* @property {string} [icon] Kernel icon name.
|
|
41
|
+
* @property {Node|(() => Node)} [glyph] A pictogram drawn instead of an icon.
|
|
42
|
+
* @property {boolean} [disabled=false] Whether the option is unavailable.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* A setting chosen in place: a labelled row of two to a few options, one checked — a theme, a
|
|
46
|
+
* layout. Choosing an option keeps the menu open, so its effect shows.
|
|
47
|
+
* @typedef {Object} MenuChoice
|
|
48
|
+
* @property {'choice'} type Entry kind.
|
|
49
|
+
* @property {string} label Visible row label.
|
|
50
|
+
* @property {unknown} [value] Value of the checked option.
|
|
51
|
+
* @property {MenuChoiceOption[]} options The options, in order.
|
|
52
|
+
* @property {(value: unknown, entry: MenuChoice, controller: MenuController) => void} [onselect]
|
|
53
|
+
* Called with the chosen option's value.
|
|
54
|
+
*/
|
|
55
|
+
/** @typedef {MenuItem|MenuHeading|MenuSeparator|MenuChoice|'-'} MenuEntry */
|
|
35
56
|
|
|
36
57
|
export const MENU_ITEM_SELECTOR = [
|
|
37
58
|
'[role="menuitem"]',
|
|
@@ -46,7 +67,7 @@ export const MENU_ITEM_SELECTOR = [
|
|
|
46
67
|
*/
|
|
47
68
|
export function isMenuItem(entry) {
|
|
48
69
|
return Boolean(entry && entry !== '-' && typeof entry === 'object'
|
|
49
|
-
&& !['heading', 'separator'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
|
|
70
|
+
&& !['heading', 'separator', 'choice'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
/**
|
|
@@ -64,9 +85,11 @@ export function menuItemRole(item) {
|
|
|
64
85
|
* Builds one menu's entries with shared structure and link safety.
|
|
65
86
|
* @param {MenuEntry[]} entries Entry definitions.
|
|
66
87
|
* @param {string} block CSS block name, without the `zx-` prefix.
|
|
88
|
+
* @param {{choices?: boolean}} [options={}] Whether the menu handles `choice` entries; a menu that
|
|
89
|
+
* does not leaves them out rather than drawing a row that does nothing.
|
|
67
90
|
* @returns {HTMLElement[]}
|
|
68
91
|
*/
|
|
69
|
-
export function renderMenuEntries(entries, block) {
|
|
92
|
+
export function renderMenuEntries(entries, block, { choices = false } = {}) {
|
|
70
93
|
const nodes = [];
|
|
71
94
|
entries.forEach((entry, index) => {
|
|
72
95
|
if (entry === '-' || (entry && typeof entry === 'object' && entry.type === 'separator')) {
|
|
@@ -80,6 +103,10 @@ export function renderMenuEntries(entries, block) {
|
|
|
80
103
|
}, String(entry.label ?? '')));
|
|
81
104
|
return;
|
|
82
105
|
}
|
|
106
|
+
if (entry && typeof entry === 'object' && entry.type === 'choice') {
|
|
107
|
+
if (choices) nodes.push(renderChoice(/** @type {MenuChoice} */ (entry), index, block));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
83
110
|
if (!isMenuItem(entry)) return;
|
|
84
111
|
|
|
85
112
|
const role = menuItemRole(entry);
|
|
@@ -140,3 +167,34 @@ export function renderMenuEntries(entries, block) {
|
|
|
140
167
|
});
|
|
141
168
|
return nodes;
|
|
142
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A choice row: its label, then a segmented group of `menuitemradio` options. The group is named
|
|
173
|
+
* by the label, so a screen reader hears "Theme, Dark, radio, checked, 2 of 3".
|
|
174
|
+
* @param {MenuChoice} entry Choice definition.
|
|
175
|
+
* @param {number} index Entry index.
|
|
176
|
+
* @param {string} block CSS block name, without the `zx-` prefix.
|
|
177
|
+
* @returns {HTMLElement}
|
|
178
|
+
*/
|
|
179
|
+
function renderChoice(entry, index, block) {
|
|
180
|
+
const labelId = uid(`zx-${block}-choice`);
|
|
181
|
+
const options = Array.isArray(entry.options) ? entry.options : [];
|
|
182
|
+
return h('div', { class: `zx-${block}__choice`, role: 'group', ariaLabelledby: labelId },
|
|
183
|
+
h('span', { class: `zx-${block}__choice-label`, id: labelId }, String(entry.label ?? '')),
|
|
184
|
+
h('span', { class: `zx-${block}__choice-options` }, options.map((option, position) => {
|
|
185
|
+
const glyph = typeof option.glyph === 'function' ? option.glyph() : option.glyph;
|
|
186
|
+
return h('button', {
|
|
187
|
+
class: `zx-${block}__choice-option`,
|
|
188
|
+
type: 'button',
|
|
189
|
+
role: 'menuitemradio',
|
|
190
|
+
tabindex: '-1',
|
|
191
|
+
title: String(option.label ?? ''),
|
|
192
|
+
ariaLabel: String(option.label ?? ''),
|
|
193
|
+
ariaChecked: String(Object.is(option.value, entry.value)),
|
|
194
|
+
ariaDisabled: option.disabled ? 'true' : null,
|
|
195
|
+
'data-menu-item': String(index),
|
|
196
|
+
'data-menu-option': String(position)
|
|
197
|
+
}, glyph && typeof glyph === 'object' && 'nodeType' in glyph ? glyph
|
|
198
|
+
: option.icon ? icon(option.icon) : String(option.label ?? ''));
|
|
199
|
+
})));
|
|
200
|
+
}
|
package/src/zeyos/form.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Form, Message, Select } from '../index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { overlayScope } from '../core/overlay-host.js';
|
|
3
3
|
import { connect, errorMessage } from './connect.js';
|
|
4
4
|
import {
|
|
5
5
|
calendarDayToUnixSeconds, dateToUnixSeconds, unixSecondsToCalendarDay, unixSecondsToDate
|
|
@@ -163,8 +163,10 @@ export function zeyosForm(client, resource, opts = {}) {
|
|
|
163
163
|
if (destroyed) return Promise.reject(new Error('Cannot save a destroyed ZeyOS form'));
|
|
164
164
|
if (savePending) return savePending;
|
|
165
165
|
if (!form.submit()) return Promise.resolve(null);
|
|
166
|
-
// Resolved now, while the form is in place
|
|
167
|
-
|
|
166
|
+
// Resolved now, while the form is in place, and to the theme scope rather than the open
|
|
167
|
+
// dialog the overlay host would be: `onSaved` may close and destroy that dialog, and a toast
|
|
168
|
+
// parked inside it would fall back to an unstyled <body>.
|
|
169
|
+
const host = notify ? overlayScope(toastScope()) : null;
|
|
168
170
|
|
|
169
171
|
savePending = (async () => {
|
|
170
172
|
let payload;
|
package/src/zeyos/index.js
CHANGED
|
@@ -20,6 +20,9 @@ export {
|
|
|
20
20
|
ZEYOS_LAUNCHER_APPLICATIONS, buildZeyosLauncherConfig, zeyosLauncher,
|
|
21
21
|
normalizeZeyosLauncherApplications, normalizeZeyosLauncherRecords, zeyosActiveIdentifier
|
|
22
22
|
} from './launcher.js';
|
|
23
|
+
export {
|
|
24
|
+
zeyosModuleItems, buildZeyosModuleBarConfig, buildZeyosAppSidebarConfig, zeyosModuleBar
|
|
25
|
+
} from './module-bar.js';
|
|
23
26
|
export {
|
|
24
27
|
zeyosSelect, buildZeyosSelectConfig, zeyosEntitySelect, buildZeyosEntitySelectConfig
|
|
25
28
|
} from './select.js';
|
package/src/zeyos/launcher.js
CHANGED
|
@@ -224,8 +224,13 @@ export function zeyosActiveIdentifier(state = {}) {
|
|
|
224
224
|
return String(state.module ?? '');
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
/**
|
|
228
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Normalizes one application of the ZeyOS shell catalogue: a `[module, label]` pair, a
|
|
229
|
+
* `[ID, name, module, color]` fork, a weblet row, or an object. Shared with the module bar.
|
|
230
|
+
* @param {unknown} raw @param {string|null} fallbackId @param {unknown} fallbackLabel
|
|
231
|
+
* @returns {Record<string, any>|null}
|
|
232
|
+
*/
|
|
233
|
+
export function normalizeApplication(raw, fallbackId, fallbackLabel) {
|
|
229
234
|
if (Array.isArray(raw)) {
|
|
230
235
|
if (raw.length < 2) return null;
|
|
231
236
|
if (raw.length === 2) {
|
|
@@ -305,7 +310,7 @@ function normalizeRecord(raw, group, query) {
|
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
/** @param {unknown} raw @param {unknown} resolver @returns {Record<string, any>} */
|
|
308
|
-
function mergeDestination(raw, resolver) {
|
|
313
|
+
export function mergeDestination(raw, resolver) {
|
|
309
314
|
const direct = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
310
315
|
const resolved = typeof resolver === 'function' ? resolver(raw) : null;
|
|
311
316
|
const destination = resolved && typeof resolved === 'object' ? resolved : {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ModuleBar } from '../components/module-bar/module-bar.js';
|
|
2
|
+
import { normalizeAppItems } from '../internal/app-rail.js';
|
|
3
|
+
import { moduleIcon } from './icons.js';
|
|
4
|
+
import { mergeDestination, normalizeApplication } from './launcher.js';
|
|
5
|
+
import { moduleInfo } from './modules.js';
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../components/module-bar/module-bar.js').ModuleItem} ModuleItem */
|
|
8
|
+
/** @typedef {import('../components/module-bar/module-bar.js').ModuleBarOptions} ModuleBarOptions */
|
|
9
|
+
/** @typedef {import('../components/app-sidebar/app-sidebar.js').AppSidebarOptions} AppSidebarOptions */
|
|
10
|
+
/**
|
|
11
|
+
* One entry of a module's menu, as the ZeyOS shell describes it: an object, or an
|
|
12
|
+
* `[id, label, href]` array.
|
|
13
|
+
* @typedef {Object} ZeyosMenuEntry
|
|
14
|
+
* @property {string|number} [id] Entry identifier; a section needs none.
|
|
15
|
+
* @property {string} label Entry label.
|
|
16
|
+
* @property {string} [href] Destination.
|
|
17
|
+
* @property {string} [target] Link browsing context.
|
|
18
|
+
* @property {() => void} [invoke] Application-owned action.
|
|
19
|
+
* @property {string|number} [badge] Count.
|
|
20
|
+
* @property {string|Node} [icon] Icon name or node.
|
|
21
|
+
* @property {boolean} [section] A heading over `children` rather than a destination.
|
|
22
|
+
* @property {Array<ZeyosMenuEntry|Array<any>>} [children] Nested entries.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The ZeyOS shell state a module bar is built from. The application fields are the launcher's
|
|
26
|
+
* (`buildZeyosLauncherConfig()`), so one state object can feed both.
|
|
27
|
+
* @typedef {Object} ZeyosModuleBarState
|
|
28
|
+
* @property {Record<string, string>|Array<string|Record<string, any>>} [modules={}] Permitted module catalogue.
|
|
29
|
+
* @property {Array<Array<any>|Record<string, any>>} [menuApps=[]] Forks, weblets, and authoritative menu entries.
|
|
30
|
+
* @property {string[]} [pinned] Application identifiers on the bar, in order; the rest are under "All modules".
|
|
31
|
+
* @property {string|null} [activeIdentifier=null] Current module, `fork#…`, or `weblet#…` identifier.
|
|
32
|
+
* @property {string|number|null} [active=null] Current menu entry, when it is more precise than the module.
|
|
33
|
+
* @property {(module: string) => boolean} [canUseModule] Permission predicate.
|
|
34
|
+
* @property {(app: unknown) => Record<string, any>|null} [resolveApplication] Maps an application to href/invoke/value metadata.
|
|
35
|
+
* @property {Record<string, Array<ZeyosMenuEntry|Array<any>>>} [menus={}] Each application's menu, keyed by identifier.
|
|
36
|
+
* @property {Record<string, string|number>} [badges={}] Badges keyed by application identifier or menu entry id.
|
|
37
|
+
* @property {Record<string, string>} [groups={}] "All modules" group per application identifier.
|
|
38
|
+
* @property {{forks?: string, weblets?: string}} [labels] Group labels for forks and weblets.
|
|
39
|
+
* @property {(app: unknown, normalized: Record<string, any>) => Node|null} [renderApplicationIcon] Host icon renderer.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} ZeyosModuleBarOptions
|
|
43
|
+
* @property {boolean} [standardIcons=false] Use stock Font Awesome glyphs instead of the ZeyOS kit.
|
|
44
|
+
* @property {'solid'|'tint'|'plain'} [appearance='solid'] Tile treatment; ZeyOS draws its modules solid.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Maps ZeyOS shell state to module bar items: every permitted module, fork, and weblet with its
|
|
49
|
+
* kit icon, its identity colour (the payload's, else the registry's), its menu, and its badge.
|
|
50
|
+
* A module with a menu is a panel module; one without is a destination.
|
|
51
|
+
* @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
|
|
52
|
+
* @param {ZeyosModuleBarOptions} [options={}] Mapping options.
|
|
53
|
+
* @returns {ModuleItem[]}
|
|
54
|
+
*/
|
|
55
|
+
export function zeyosModuleItems(state = {}, options = {}) {
|
|
56
|
+
const pinned = Array.isArray(state.pinned) ? state.pinned.map(String) : null;
|
|
57
|
+
const menus = state.menus && typeof state.menus === 'object' ? state.menus : {};
|
|
58
|
+
const badges = state.badges && typeof state.badges === 'object' ? state.badges : {};
|
|
59
|
+
const groups = state.groups && typeof state.groups === 'object' ? state.groups : {};
|
|
60
|
+
const labels = { forks: 'Forks', weblets: 'Weblets', ...(state.labels ?? {}) };
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
/** @type {Record<string, any>[]} */
|
|
63
|
+
const items = [];
|
|
64
|
+
const add = (raw, fallbackId = null, fallbackLabel = null) => {
|
|
65
|
+
const app = normalizeApplication(raw, fallbackId, fallbackLabel);
|
|
66
|
+
if (!app || seen.has(app.identifier)) return;
|
|
67
|
+
if (app.type === 'module' && typeof state.canUseModule === 'function' && !state.canUseModule(app.module)) return;
|
|
68
|
+
seen.add(app.identifier);
|
|
69
|
+
const children = menuEntries(menus[app.identifier], badges);
|
|
70
|
+
const destination = children.length ? {} : mergeDestination(app.raw, state.resolveApplication);
|
|
71
|
+
const pin = pinned ? pinned.indexOf(app.identifier) : -1;
|
|
72
|
+
const group = groups[app.identifier]
|
|
73
|
+
?? (app.type === 'fork' ? labels.forks : app.type === 'weblet' ? labels.weblets : undefined);
|
|
74
|
+
items.push({
|
|
75
|
+
id: app.identifier,
|
|
76
|
+
label: app.label,
|
|
77
|
+
// A factory, so a host renderer or the kit glyph is built per tile rather than shared.
|
|
78
|
+
icon: () => (typeof state.renderApplicationIcon === 'function' ? state.renderApplicationIcon(app.raw, app) : null)
|
|
79
|
+
?? moduleIcon(app.module, { size: '1em', standard: Boolean(options.standardIcons) }),
|
|
80
|
+
color: app.color ?? moduleInfo(app.module).color,
|
|
81
|
+
module: app.module,
|
|
82
|
+
...(group ? { group } : {}),
|
|
83
|
+
...(badges[app.identifier] != null ? { badge: badges[app.identifier] } : {}),
|
|
84
|
+
...(pinned ? { pinned: pin >= 0 ? pin : false } : {}),
|
|
85
|
+
...(children.length ? { children } : destination)
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
for (const raw of Array.isArray(state.menuApps) ? state.menuApps : []) add(raw);
|
|
89
|
+
if (Array.isArray(state.modules)) {
|
|
90
|
+
for (const module of state.modules) {
|
|
91
|
+
if (typeof module === 'string') add([module, moduleInfo(module).label]);
|
|
92
|
+
else add(module, module?.id ?? module?.module, module?.label);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
for (const [module, label] of Object.entries(state.modules ?? {})) add([module, label]);
|
|
96
|
+
}
|
|
97
|
+
return normalizeAppItems(items);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds plain ModuleBar options from ZeyOS shell state.
|
|
102
|
+
* @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
|
|
103
|
+
* @param {ZeyosModuleBarOptions & ModuleBarOptions} [options={}] Mapping and ModuleBar options.
|
|
104
|
+
* @returns {ModuleBarOptions}
|
|
105
|
+
*/
|
|
106
|
+
export function buildZeyosModuleBarConfig(state = {}, options = {}) {
|
|
107
|
+
const { standardIcons = false, appearance = 'solid', ...barOptions } = options;
|
|
108
|
+
return {
|
|
109
|
+
...barOptions,
|
|
110
|
+
items: zeyosModuleItems(state, { standardIcons }),
|
|
111
|
+
active: state.active ?? state.activeIdentifier ?? null,
|
|
112
|
+
appearance
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds AppSidebar options for the modules layout from ZeyOS shell state: the module strip beside
|
|
118
|
+
* a panel with the current module's menu.
|
|
119
|
+
* @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
|
|
120
|
+
* @param {ZeyosModuleBarOptions & AppSidebarOptions} [options={}] Mapping and AppSidebar options.
|
|
121
|
+
* @returns {AppSidebarOptions}
|
|
122
|
+
*/
|
|
123
|
+
export function buildZeyosAppSidebarConfig(state = {}, options = {}) {
|
|
124
|
+
const { standardIcons = false, appearance = 'solid', moduleBar = {}, ...sidebarOptions } = options;
|
|
125
|
+
return {
|
|
126
|
+
label: 'ZeyOS',
|
|
127
|
+
...sidebarOptions,
|
|
128
|
+
layout: 'modules',
|
|
129
|
+
items: zeyosModuleItems(state, { standardIcons }),
|
|
130
|
+
active: state.active ?? state.activeIdentifier ?? null,
|
|
131
|
+
moduleBar: { appearance, ...moduleBar }
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a ModuleBar from ZeyOS shell state.
|
|
137
|
+
* @param {Element|string|null} target Bar target, or null to create one.
|
|
138
|
+
* @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
|
|
139
|
+
* @param {ZeyosModuleBarOptions & ModuleBarOptions} [options={}] Mapping and ModuleBar options.
|
|
140
|
+
* @returns {ModuleBar}
|
|
141
|
+
*/
|
|
142
|
+
export function zeyosModuleBar(target, state = {}, options = {}) {
|
|
143
|
+
return new ModuleBar(target, buildZeyosModuleBarConfig(state, options));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Normalizes a ZeyOS menu: objects and `[id, label, href]` arrays, sections, nested entries, and
|
|
148
|
+
* badges looked up by entry id. Entries without a label, and leaves without an id, are dropped.
|
|
149
|
+
* @param {unknown} entries Menu entries.
|
|
150
|
+
* @param {Record<string, string|number>} badges Badges by id.
|
|
151
|
+
* @returns {Record<string, any>[]}
|
|
152
|
+
*/
|
|
153
|
+
function menuEntries(entries, badges) {
|
|
154
|
+
if (!Array.isArray(entries)) return [];
|
|
155
|
+
return entries.flatMap((entry) => {
|
|
156
|
+
if (Array.isArray(entry)) {
|
|
157
|
+
const [id, label, href] = entry;
|
|
158
|
+
if (id == null || label == null) return [];
|
|
159
|
+
return [{
|
|
160
|
+
id, label: String(label),
|
|
161
|
+
...(href != null ? { href: String(href) } : {}),
|
|
162
|
+
...(badges[String(id)] != null ? { badge: badges[String(id)] } : {})
|
|
163
|
+
}];
|
|
164
|
+
}
|
|
165
|
+
if (!entry || typeof entry !== 'object' || entry.label == null) return [];
|
|
166
|
+
const children = menuEntries(entry.children, badges);
|
|
167
|
+
if (entry.section === true) {
|
|
168
|
+
return children.length ? [{ ...(entry.id != null ? { id: entry.id } : {}), section: true, label: String(entry.label), children }] : [];
|
|
169
|
+
}
|
|
170
|
+
if (entry.id == null) return [];
|
|
171
|
+
const picked = Object.fromEntries(['id', 'href', 'target', 'invoke', 'icon', 'value', 'disabled']
|
|
172
|
+
.filter((key) => entry[key] !== undefined).map((key) => [key, entry[key]]));
|
|
173
|
+
const badge = badges[String(entry.id)] ?? entry.badge;
|
|
174
|
+
return [{
|
|
175
|
+
...picked,
|
|
176
|
+
label: String(entry.label),
|
|
177
|
+
...(badge != null ? { badge } : {}),
|
|
178
|
+
...(children.length ? { children } : {})
|
|
179
|
+
}];
|
|
180
|
+
});
|
|
181
|
+
}
|