@zeyos/zx 3.0.0 → 4.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 +51 -0
- package/DESIGN-SYSTEM.md +28 -68
- package/README.md +16 -37
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +1 -0
- package/dist/types/components/launcher/launcher.d.ts +65 -6
- package/dist/types/components/select/select.d.ts +51 -0
- package/dist/types/components/table/table.d.ts +4 -3
- package/dist/types/core/dom.d.ts +8 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
- package/dist/types/zeyos/icons.d.ts +5 -0
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +39 -0
- package/dist/zx-zeyos.esm.js +1324 -46
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +400 -12
- package/dist/zx.esm.js +1059 -189
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +1059 -189
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +258 -145
- package/docs/llms.md +93 -112
- package/docs/llms.txt +16 -15
- package/package.json +8 -17
- package/src/components/app-sidebar/app-sidebar.css +12 -0
- package/src/components/app-sidebar/app-sidebar.js +310 -110
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.js +8 -3
- package/src/components/grid/grid.js +10 -3
- package/src/components/launcher/launcher.css +113 -3
- package/src/components/launcher/launcher.js +234 -26
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +144 -10
- package/src/components/select/select.js +259 -27
- package/src/components/table/table.js +8 -7
- package/src/core/dom.js +20 -0
- package/src/index.js +2 -1
- package/src/{components/app-rail → internal}/app-rail.js +80 -30
- package/src/zeyos/icons.js +8 -3
- package/src/zeyos/index.js +7 -1
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +115 -1
- package/styles/tokens/global.css +1 -1
- package/styles/tokens/modules.css +1 -0
- package/styles/zx.css +1 -0
- package/MIGRATION.md +0 -749
- package/dist/types/compat/base.d.ts +0 -156
- package/dist/types/compat/globals.d.ts +0 -20
- package/dist/types/compat/index.d.ts +0 -128
- package/dist/types/compat/map/basic.d.ts +0 -175
- package/dist/types/compat/map/data.d.ts +0 -103
- package/dist/types/compat/map/date-time.d.ts +0 -80
- package/dist/types/compat/map/factory.d.ts +0 -9
- package/dist/types/compat/map/form.d.ts +0 -98
- package/dist/types/compat/map/helpers.d.ts +0 -12
- package/dist/types/compat/map/http.d.ts +0 -36
- package/dist/types/compat/map/layout.d.ts +0 -106
- package/dist/types/compat/map/options.d.ts +0 -80
- package/dist/types/compat/map/select.d.ts +0 -95
- package/dist/types/compat/map/stubs.d.ts +0 -32
- package/dist/types/compat/map/utilities.d.ts +0 -37
- package/dist/types/compat-entry.d.ts +0 -5
- package/dist/zx-compat.esm.js +0 -17848
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -29008
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -167
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -254
- package/src/compat/map/date-time.js +0 -177
- package/src/compat/map/factory.js +0 -45
- package/src/compat/map/form.js +0 -330
- package/src/compat/map/helpers.js +0 -46
- package/src/compat/map/http.js +0 -117
- package/src/compat/map/layout.js +0 -256
- package/src/compat/map/options.js +0 -267
- package/src/compat/map/select.js +0 -198
- package/src/compat/map/stubs.js +0 -26
- package/src/compat/map/utilities.js +0 -113
- package/src/compat-entry.js +0 -11
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { Component } from '
|
|
2
|
-
import { h } from '
|
|
3
|
-
import { icon } from '
|
|
4
|
-
import { Dropdown } from '../dropdown/dropdown.js';
|
|
5
|
-
import { Tooltip } from '../tooltip/tooltip.js';
|
|
1
|
+
import { Component } from '../core/component.js';
|
|
2
|
+
import { h, safeHref } from '../core/dom.js';
|
|
3
|
+
import { icon } from '../core/icons.js';
|
|
4
|
+
import { Dropdown } from '../components/dropdown/dropdown.js';
|
|
5
|
+
import { Tooltip } from '../components/tooltip/tooltip.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {Object} AppNavItem
|
|
9
9
|
* @property {string|number} id Stable item identifier.
|
|
10
10
|
* @property {string} label Visible/accessibility label.
|
|
11
|
-
* @property {string} [icon]
|
|
11
|
+
* @property {string|Node|{toElement: () => Node|null}|((context: Record<string, any>, item: AppNavItem) => Node|null)} [icon] Icon name, node, component, or factory.
|
|
12
12
|
* @property {string|number} [badge] Count or status badge.
|
|
13
13
|
* @property {unknown} [value] Emitted value; defaults to `id`.
|
|
14
|
-
* @property {string} [href] Native navigation target.
|
|
14
|
+
* @property {string} [href] Native navigation target; executable/data schemes are disabled.
|
|
15
15
|
* @property {string} [target] Native link browsing context.
|
|
16
16
|
* @property {() => void} [invoke] Application-owned non-link action.
|
|
17
17
|
* @property {boolean} [disabled] Whether activation is unavailable.
|
|
18
18
|
* @property {AppNavItem[]} [children] Child destinations/actions.
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
|
-
* @typedef {Object}
|
|
21
|
+
* @typedef {Object} AppRailPresenterOptions
|
|
22
22
|
* @property {AppNavItem[]} [items=[]] Application navigation tree.
|
|
23
23
|
* @property {string|number|null} [active=null] Active item id.
|
|
24
24
|
* @property {'vertical'|'horizontal'} [orientation='vertical'] Rail flow.
|
|
@@ -28,6 +28,7 @@ import { Tooltip } from '../tooltip/tooltip.js';
|
|
|
28
28
|
* @property {Node|string|number|{toElement: () => Node|null}|null} [footer=null] Trailing rail content.
|
|
29
29
|
* @property {number} [openDelay=80] Hover-open delay in milliseconds.
|
|
30
30
|
* @property {number} [closeDelay=160] Pointer/focus crossing grace in milliseconds.
|
|
31
|
+
* @property {(item: AppNavItem, context: Record<string, any>) => Node|null} [renderIcon] Optional host icon renderer.
|
|
31
32
|
* @property {(event: CustomEvent<AppNavSelectDetail>) => void} [onselect] Preventable action listener.
|
|
32
33
|
* @property {(event: CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>) => void} [onflyoutchange] Flyout-state listener.
|
|
33
34
|
*/
|
|
@@ -36,14 +37,14 @@ import { Tooltip } from '../tooltip/tooltip.js';
|
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Minimized application navigation; descendants always open in hover/focus flyouts, never inline.
|
|
39
|
-
* @fires
|
|
40
|
-
* @fires
|
|
41
|
-
* @extends {Component<
|
|
40
|
+
* @fires AppRailPresenter#select
|
|
41
|
+
* @fires AppRailPresenter#flyoutchange
|
|
42
|
+
* @extends {Component<AppRailPresenterOptions>}
|
|
42
43
|
*/
|
|
43
|
-
export class
|
|
44
|
+
export class AppRailPresenter extends Component {
|
|
44
45
|
static cssName = 'app-rail';
|
|
45
46
|
|
|
46
|
-
/** @type {Readonly<
|
|
47
|
+
/** @type {Readonly<AppRailPresenterOptions>} */
|
|
47
48
|
static defaults = {
|
|
48
49
|
items: [],
|
|
49
50
|
active: null,
|
|
@@ -53,13 +54,14 @@ export class AppRail extends Component {
|
|
|
53
54
|
header: null,
|
|
54
55
|
footer: null,
|
|
55
56
|
openDelay: 80,
|
|
56
|
-
closeDelay: 160
|
|
57
|
+
closeDelay: 160,
|
|
58
|
+
renderIcon: null
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* Creates or enhances a rail host.
|
|
61
63
|
* @param {Element|string|null} target Rail target.
|
|
62
|
-
* @param {
|
|
64
|
+
* @param {AppRailPresenterOptions} [options={}] Rail options.
|
|
63
65
|
*/
|
|
64
66
|
constructor(target = null, options = {}) {
|
|
65
67
|
super(target, options);
|
|
@@ -213,7 +215,14 @@ export class AppRail extends Component {
|
|
|
213
215
|
index: String(item.id),
|
|
214
216
|
iconOnly: true,
|
|
215
217
|
disclosure: hasChildren,
|
|
216
|
-
disclosureIcon: railDisclosureIcon(this.el.dataset.side)
|
|
218
|
+
disclosureIcon: railDisclosureIcon(this.el.dataset.side),
|
|
219
|
+
visual: appNavIcon(item, this.options.renderIcon, {
|
|
220
|
+
orientation: this.el.dataset.orientation,
|
|
221
|
+
collapsed: true,
|
|
222
|
+
side: this.el.dataset.side,
|
|
223
|
+
level: 1,
|
|
224
|
+
location: 'root'
|
|
225
|
+
})
|
|
217
226
|
});
|
|
218
227
|
const row = h('li', { class: 'zx-app-rail__row' }, control);
|
|
219
228
|
this.refs.list.append(row);
|
|
@@ -255,7 +264,14 @@ export class AppRail extends Component {
|
|
|
255
264
|
index: childKey,
|
|
256
265
|
iconOnly: false,
|
|
257
266
|
disclosure: hasChildren,
|
|
258
|
-
disclosureIcon: nestedDisclosureIcon(this.el.dataset.side)
|
|
267
|
+
disclosureIcon: nestedDisclosureIcon(this.el.dataset.side),
|
|
268
|
+
visual: appNavIcon(child, this.options.renderIcon, {
|
|
269
|
+
orientation: this.el.dataset.orientation,
|
|
270
|
+
collapsed: true,
|
|
271
|
+
side: this.el.dataset.side,
|
|
272
|
+
level: childKey.split('/').length,
|
|
273
|
+
location: 'flyout'
|
|
274
|
+
})
|
|
259
275
|
});
|
|
260
276
|
list.append(h('li', {}, control));
|
|
261
277
|
if (hasChildren) this._buildFlyout(child, control, childKey, key);
|
|
@@ -305,7 +321,6 @@ export class AppRail extends Component {
|
|
|
305
321
|
this._interaction.listen(trigger, 'click', (event) => {
|
|
306
322
|
event.preventDefault();
|
|
307
323
|
if (event instanceof MouseEvent && event.detail === 0) this.openFlyout(key, { focus: true });
|
|
308
|
-
else if (dropdown.isOpen()) this.closeFlyout(key);
|
|
309
324
|
else this.openFlyout(key);
|
|
310
325
|
});
|
|
311
326
|
this._interaction.listen(actualPanel, 'pointerenter', () => this._cancelFlyoutTimers(flyout, true));
|
|
@@ -582,26 +597,61 @@ class RailListenerScope extends Component {
|
|
|
582
597
|
export function normalizeAppItems(items) {
|
|
583
598
|
if (!Array.isArray(items)) return [];
|
|
584
599
|
return items.filter((item) => item && typeof item === 'object' && item.id != null && item.label != null)
|
|
585
|
-
.map((item) =>
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
600
|
+
.map((item) => {
|
|
601
|
+
const normalized = {
|
|
602
|
+
...item,
|
|
603
|
+
label: String(item.label),
|
|
604
|
+
children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
|
|
605
|
+
};
|
|
606
|
+
if (item.href == null) return normalized;
|
|
607
|
+
const href = safeHref(item.href);
|
|
608
|
+
if (href !== null) normalized.href = href;
|
|
609
|
+
else {
|
|
610
|
+
delete normalized.href;
|
|
611
|
+
if (normalized.children.length === 0 && typeof normalized.invoke !== 'function') {
|
|
612
|
+
normalized.disabled = true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return normalized;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Resolves a host-rendered or item-owned navigation icon without moving a mounted source node.
|
|
621
|
+
* @param {AppNavItem} item Navigation item.
|
|
622
|
+
* @param {unknown} renderer Optional component-level renderer.
|
|
623
|
+
* @param {Record<string, any>} context Presentation context.
|
|
624
|
+
* @returns {Node|null}
|
|
625
|
+
*/
|
|
626
|
+
export function appNavIcon(item, renderer, context) {
|
|
627
|
+
let visual = typeof renderer === 'function' ? renderer(item, context) : null;
|
|
628
|
+
if (visual == null) visual = item.icon;
|
|
629
|
+
if (typeof visual === 'function') visual = visual(context, item);
|
|
630
|
+
if (typeof visual === 'string') return icon(visual);
|
|
631
|
+
if (visual instanceof Node) return visual.cloneNode(true);
|
|
632
|
+
if (visual && typeof visual === 'object'
|
|
633
|
+
&& typeof (/** @type {any} */ (visual)).toElement === 'function') {
|
|
634
|
+
const element = (/** @type {any} */ (visual)).toElement();
|
|
635
|
+
return element instanceof Node ? element.cloneNode(true) : null;
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
590
638
|
}
|
|
591
639
|
|
|
592
|
-
/** @param {AppNavItem} item @param {{className: string, index: string, iconOnly: boolean, disclosure: boolean, disclosureIcon: string}} options @returns {HTMLElement} */
|
|
593
|
-
function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon }) {
|
|
594
|
-
const
|
|
640
|
+
/** @param {AppNavItem} item @param {{className: string, index: string, iconOnly: boolean, disclosure: boolean, disclosureIcon: string, visual: Node|null}} options @returns {HTMLElement} */
|
|
641
|
+
function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon, visual }) {
|
|
642
|
+
const href = safeHref(item.href);
|
|
643
|
+
const tag = href && !disclosure ? 'a' : 'button';
|
|
595
644
|
return /** @type {HTMLElement} */ (h(tag, {
|
|
596
645
|
class: className,
|
|
597
646
|
type: tag === 'button' ? 'button' : null,
|
|
598
|
-
href: tag === 'a' ?
|
|
647
|
+
href: tag === 'a' ? href : null,
|
|
599
648
|
target: tag === 'a' ? item.target ?? null : null,
|
|
649
|
+
rel: tag === 'a' ? 'noopener' : null,
|
|
600
650
|
ariaLabel: iconOnly ? item.label : null,
|
|
601
651
|
ariaDisabled: item.disabled ? 'true' : null,
|
|
602
652
|
dataset: { appNavId: index, appNavValueId: String(item.id) }
|
|
603
653
|
},
|
|
604
|
-
|
|
654
|
+
visual ? h('span', { class: `${className}-icon`, ariaHidden: 'true' }, visual) : null,
|
|
605
655
|
h('span', { class: `${className}-label` }, item.label),
|
|
606
656
|
item.badge != null ? h('span', { class: `${className}-badge` }, String(item.badge)) : null,
|
|
607
657
|
disclosure ? h('span', { class: `${className}-disclosure`, ariaHidden: 'true' }, icon(disclosureIcon)) : null));
|
|
@@ -664,5 +714,5 @@ function appendSlot(target, content) {
|
|
|
664
714
|
} else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
|
|
665
715
|
}
|
|
666
716
|
|
|
667
|
-
/** Fired when a destination/action is activated. @event
|
|
668
|
-
/** Fired when a flyout opens or closes. @event
|
|
717
|
+
/** Fired when a destination/action is activated. @event AppRailPresenter#select @type {CustomEvent<AppNavSelectDetail>} */
|
|
718
|
+
/** Fired when a flyout opens or closes. @event AppRailPresenter#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>} */
|
package/src/zeyos/icons.js
CHANGED
|
@@ -55,6 +55,7 @@ export function moduleIcon(name, options = {}) {
|
|
|
55
55
|
* @property {string|null} [label=null] Accessible label; null marks the chip decorative.
|
|
56
56
|
* @property {boolean} [title=false] Adds the module's display name as a `title` tooltip.
|
|
57
57
|
* @property {boolean} [standard=false] Use the stock Font Awesome fallback glyph.
|
|
58
|
+
* @property {string} [color] Override the module colour for a server-defined fork or weblet.
|
|
58
59
|
* @property {string|string[]} [class] Extra class names.
|
|
59
60
|
*/
|
|
60
61
|
|
|
@@ -67,16 +68,20 @@ export function moduleIcon(name, options = {}) {
|
|
|
67
68
|
* @returns {HTMLElement} A `<span class="zx-module-icon">` wrapping the glyph.
|
|
68
69
|
*/
|
|
69
70
|
export function moduleChip(name, options = {}) {
|
|
70
|
-
const {
|
|
71
|
+
const {
|
|
72
|
+
size = 24, iconSize = '1em', label = null, title = false, standard = false,
|
|
73
|
+
color = null
|
|
74
|
+
} = options;
|
|
71
75
|
const key = normalizeModuleName(name);
|
|
72
76
|
const info = moduleInfo(name);
|
|
77
|
+
const background = color || info.color;
|
|
73
78
|
|
|
74
79
|
const chip = h('span', {
|
|
75
80
|
class: ['zx-module-icon', options.class].flat().filter(Boolean),
|
|
76
81
|
dataset: { module: key },
|
|
77
82
|
style: {
|
|
78
|
-
'--zx-module-color':
|
|
79
|
-
'--zx-module-glyph': moduleGlyphColor(
|
|
83
|
+
'--zx-module-color': background,
|
|
84
|
+
'--zx-module-glyph': moduleGlyphColor(background),
|
|
80
85
|
'--zx-module-icon-size': typeof size === 'number' ? `${size}px` : String(size)
|
|
81
86
|
},
|
|
82
87
|
title: title ? info.label : null
|
package/src/zeyos/index.js
CHANGED
|
@@ -16,6 +16,12 @@ export {
|
|
|
16
16
|
moduleGlyphColor, normalizeModuleName, registerModules
|
|
17
17
|
} from './modules.js';
|
|
18
18
|
export { ZEYOS_ICON_KIT, useZeyosIcons, moduleIcon, moduleChip } from './icons.js';
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
ZEYOS_LAUNCHER_APPLICATIONS, buildZeyosLauncherConfig, zeyosLauncher,
|
|
21
|
+
normalizeZeyosLauncherApplications, normalizeZeyosLauncherRecords, zeyosActiveIdentifier
|
|
22
|
+
} from './launcher.js';
|
|
23
|
+
export {
|
|
24
|
+
zeyosSelect, buildZeyosSelectConfig, zeyosEntitySelect, buildZeyosEntitySelectConfig
|
|
25
|
+
} from './select.js';
|
|
20
26
|
export { zeyosForm } from './form.js';
|
|
21
27
|
export { zeyosTable } from './table.js';
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Launcher } from '../components/launcher/launcher.js';
|
|
2
|
+
import { moduleChip } from './icons.js';
|
|
3
|
+
import { moduleInfo } from './modules.js';
|
|
4
|
+
|
|
5
|
+
/** Static application identifiers present in the current ZeyOS shell prototype. */
|
|
6
|
+
export const ZEYOS_LAUNCHER_APPLICATIONS = Object.freeze([
|
|
7
|
+
'accounts', 'actionsteps', 'admin', 'auth', 'billing', 'calendar', 'campaigns', 'collection',
|
|
8
|
+
'contacts', 'contracts', 'control', 'coupons', 'dev', 'documents', 'inventory', 'mailinglists',
|
|
9
|
+
'main', 'messages', 'links', 'notes', 'objects', 'opportunities', 'payments', 'pricelists',
|
|
10
|
+
'procurement', 'production', 'projects', 'tasks', 'tickets'
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ZeyosLauncherState
|
|
15
|
+
* @property {Record<string, string>|Array<string|Record<string, any>>} [modules={}] Permitted module catalogue.
|
|
16
|
+
* @property {Array<Array<any>|Record<string, any>>} [menuApps=[]] Forks/weblets and authoritative menu entries.
|
|
17
|
+
* @property {string[]} [pinned=[]] Application identifiers in pinned order.
|
|
18
|
+
* @property {Array<Array<any>|Record<string, any>>} [recent=[]] Recent records.
|
|
19
|
+
* @property {string|null} [activeIdentifier=null] Current module, `fork#…`, or `weblet#…` identifier.
|
|
20
|
+
* @property {(module: string) => boolean} [canUseModule] Permission predicate.
|
|
21
|
+
* @property {(query: string, context: {signal: AbortSignal}) => Promise<unknown>|unknown} [search] Record search loader.
|
|
22
|
+
* @property {(context: {signal: AbortSignal}) => Promise<unknown>|unknown} [loadRecent] Recent-record loader.
|
|
23
|
+
* @property {(app: unknown) => Record<string, any>|null} [resolveApplication] Maps an app to href/invoke/value metadata.
|
|
24
|
+
* @property {(record: unknown) => Record<string, any>|null} [resolveRecord] Maps a record to href/invoke/value metadata.
|
|
25
|
+
* @property {Record<string, any>} [labels] Localized applications/recent/results/entity labels and hints.
|
|
26
|
+
* @property {(app: unknown, normalized: Record<string, any>) => Node|null} [renderApplicationIcon] Host icon renderer for modules, forks, and weblets.
|
|
27
|
+
* @property {(record: unknown, normalized: Record<string, any>) => Node|null} [renderRecordIcon] Host record icon renderer.
|
|
28
|
+
* @property {(app: unknown, normalized: Record<string, any>) => Node|null} [renderWebletIcon] Legacy weblet-only icon renderer.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} ZeyosLauncherOptions
|
|
33
|
+
* @property {boolean} [standardIcons=false] Use stock icon fallbacks instead of the optional ZeyOS kit.
|
|
34
|
+
* @property {Array<Record<string, any>>} [items] Extra generic Launcher items.
|
|
35
|
+
* @property {Array<Record<string, any>>} [sources] Extra generic Launcher sources.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds a generic Launcher configuration from explicitly injected ZeyOS shell state. It never
|
|
40
|
+
* reads PG/C globals and never resolves a route itself.
|
|
41
|
+
* @param {ZeyosLauncherState} [state={}] Application-owned shell state.
|
|
42
|
+
* @param {ZeyosLauncherOptions & Record<string, any>} [options={}] Adapter and Launcher options.
|
|
43
|
+
* @returns {Record<string, any>} Plain Launcher options.
|
|
44
|
+
*/
|
|
45
|
+
export function buildZeyosLauncherConfig(state = {}, options = {}) {
|
|
46
|
+
const {
|
|
47
|
+
standardIcons = false,
|
|
48
|
+
items: extraItems = [],
|
|
49
|
+
sources: extraSources = [],
|
|
50
|
+
...launcherOptions
|
|
51
|
+
} = options;
|
|
52
|
+
const labels = launcherLabels(state.labels);
|
|
53
|
+
const iconOptions = {
|
|
54
|
+
standardIcons,
|
|
55
|
+
renderApplicationIcon: state.renderApplicationIcon,
|
|
56
|
+
renderWebletIcon: state.renderWebletIcon
|
|
57
|
+
};
|
|
58
|
+
const applications = normalizeZeyosLauncherApplications(state, iconOptions);
|
|
59
|
+
const recent = typeof state.loadRecent === 'function' ? []
|
|
60
|
+
: normalizeZeyosLauncherRecords(state.recent ?? [], {
|
|
61
|
+
query: '', labels, resolveRecord: state.resolveRecord,
|
|
62
|
+
renderRecordIcon: state.renderRecordIcon, standardIcons
|
|
63
|
+
});
|
|
64
|
+
const sources = [];
|
|
65
|
+
if (typeof state.loadRecent === 'function') {
|
|
66
|
+
sources.push({
|
|
67
|
+
id: 'zeyos-recent',
|
|
68
|
+
minQuery: 0,
|
|
69
|
+
when: 'empty',
|
|
70
|
+
order: 'source',
|
|
71
|
+
load: async (_query, context) => normalizeZeyosLauncherRecords(
|
|
72
|
+
await state.loadRecent(context),
|
|
73
|
+
{
|
|
74
|
+
query: '', labels, resolveRecord: state.resolveRecord,
|
|
75
|
+
renderRecordIcon: state.renderRecordIcon, standardIcons
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (typeof state.search === 'function') {
|
|
81
|
+
sources.push({
|
|
82
|
+
id: 'zeyos-records',
|
|
83
|
+
minQuery: 1,
|
|
84
|
+
when: 'query',
|
|
85
|
+
order: 'source',
|
|
86
|
+
load: async (query, context) => normalizeZeyosLauncherRecords(
|
|
87
|
+
await state.search(query, context),
|
|
88
|
+
{
|
|
89
|
+
query, labels, resolveRecord: state.resolveRecord,
|
|
90
|
+
renderRecordIcon: state.renderRecordIcon, standardIcons
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
sources.push(...extraSources);
|
|
96
|
+
return {
|
|
97
|
+
...launcherOptions,
|
|
98
|
+
items: [...applications, ...recent, ...extraItems],
|
|
99
|
+
sources,
|
|
100
|
+
hints: options.hints ?? {
|
|
101
|
+
move: labels.move,
|
|
102
|
+
open: labels.open,
|
|
103
|
+
close: labels.close
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates the ZeyOS-configured Launcher. State and route resolvers remain caller-owned inputs.
|
|
110
|
+
* @param {ZeyosLauncherState} [state={}] Injected shell state.
|
|
111
|
+
* @param {ZeyosLauncherOptions & Record<string, any>} [options={}] Adapter and Launcher options.
|
|
112
|
+
* @returns {Launcher}
|
|
113
|
+
*/
|
|
114
|
+
export function zeyosLauncher(state = {}, options = {}) {
|
|
115
|
+
return new Launcher(null, buildZeyosLauncherConfig(state, options));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalizes module, fork, and weblet applications while preserving menu authority and pin order.
|
|
120
|
+
* @param {ZeyosLauncherState} [state={}] Injected shell state.
|
|
121
|
+
* @param {{standardIcons?: boolean, renderApplicationIcon?: Function, renderWebletIcon?: Function}} [options={}] Icon options.
|
|
122
|
+
* @returns {Array<Record<string, any>>}
|
|
123
|
+
*/
|
|
124
|
+
export function normalizeZeyosLauncherApplications(state = {}, options = {}) {
|
|
125
|
+
const labels = launcherLabels(state.labels);
|
|
126
|
+
const pinned = Array.isArray(state.pinned) ? state.pinned.map(String) : [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const items = [];
|
|
129
|
+
const add = (raw, fallbackId = null, fallbackLabel = null) => {
|
|
130
|
+
const app = normalizeApplication(raw, fallbackId, fallbackLabel);
|
|
131
|
+
if (!app || seen.has(app.identifier)) return;
|
|
132
|
+
if (app.type === 'module' && typeof state.canUseModule === 'function'
|
|
133
|
+
&& !state.canUseModule(app.module)) return;
|
|
134
|
+
seen.add(app.identifier);
|
|
135
|
+
const resolved = mergeDestination(app.raw, state.resolveApplication);
|
|
136
|
+
const pin = pinned.indexOf(app.identifier);
|
|
137
|
+
const icon = () => {
|
|
138
|
+
const custom = typeof options.renderApplicationIcon === 'function'
|
|
139
|
+
? options.renderApplicationIcon(app.raw, app)
|
|
140
|
+
: app.type === 'weblet' && typeof options.renderWebletIcon === 'function'
|
|
141
|
+
? options.renderWebletIcon(app.raw, app) : null;
|
|
142
|
+
return custom ?? moduleChip(app.module, {
|
|
143
|
+
size: 36,
|
|
144
|
+
standard: Boolean(options.standardIcons),
|
|
145
|
+
color: app.color
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
items.push({
|
|
149
|
+
id: app.identifier,
|
|
150
|
+
label: app.label,
|
|
151
|
+
keywords: [app.identifier, app.module],
|
|
152
|
+
group: labels.applications,
|
|
153
|
+
groupOrder: 0,
|
|
154
|
+
kind: 'application',
|
|
155
|
+
icon,
|
|
156
|
+
pinned: pin >= 0 ? pin : undefined,
|
|
157
|
+
current: app.identifier === state.activeIdentifier,
|
|
158
|
+
disabled: !resolved.href && typeof resolved.invoke !== 'function',
|
|
159
|
+
...resolved,
|
|
160
|
+
_zeyos: app
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
for (const raw of Array.isArray(state.menuApps) ? state.menuApps : []) add(raw);
|
|
165
|
+
if (Array.isArray(state.modules)) {
|
|
166
|
+
for (const module of state.modules) {
|
|
167
|
+
if (typeof module === 'string') add([module, moduleInfo(module).label]);
|
|
168
|
+
else add(module, module?.id ?? module?.module, module?.label);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
for (const [module, label] of Object.entries(state.modules ?? {})) add([module, label]);
|
|
172
|
+
}
|
|
173
|
+
return items;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Normalizes recent records or grouped search payloads without losing server/group order.
|
|
178
|
+
* @param {unknown} payload Recent/search payload.
|
|
179
|
+
* @param {{query?: string, labels?: Record<string, any>, resolveRecord?: Function, renderRecordIcon?: Function, standardIcons?: boolean}} [options={}] Mapping options.
|
|
180
|
+
* @returns {Array<Record<string, any>>}
|
|
181
|
+
*/
|
|
182
|
+
export function normalizeZeyosLauncherRecords(payload, options = {}) {
|
|
183
|
+
const query = String(options.query ?? '');
|
|
184
|
+
const labels = launcherLabels(options.labels);
|
|
185
|
+
const records = flattenRecords(payload, query);
|
|
186
|
+
const groupOrders = new Map();
|
|
187
|
+
return records.flatMap(({ raw, group }, itemOrder) => {
|
|
188
|
+
const record = normalizeRecord(raw, group, query);
|
|
189
|
+
if (!record) return [];
|
|
190
|
+
const groupName = query === '' ? labels.recent
|
|
191
|
+
: record.group === '' ? labels.results
|
|
192
|
+
: labels.entities[record.group] ?? record.group;
|
|
193
|
+
if (!groupOrders.has(groupName)) groupOrders.set(groupName, groupOrders.size + 1);
|
|
194
|
+
const resolved = mergeDestination(record.raw, options.resolveRecord);
|
|
195
|
+
return [{
|
|
196
|
+
id: `record:${record.entity}:${record.fork ?? ''}:${record.ID}`,
|
|
197
|
+
label: record.name,
|
|
198
|
+
description: record.sec,
|
|
199
|
+
keywords: [record.entity, record.ID, record.fork ?? ''],
|
|
200
|
+
group: groupName,
|
|
201
|
+
groupOrder: groupOrders.get(groupName),
|
|
202
|
+
itemOrder,
|
|
203
|
+
kind: 'record',
|
|
204
|
+
when: query === '' ? 'empty' : 'query',
|
|
205
|
+
icon: () => {
|
|
206
|
+
const custom = typeof options.renderRecordIcon === 'function'
|
|
207
|
+
? options.renderRecordIcon(record.raw, record) : null;
|
|
208
|
+
return custom ?? moduleChip(record.entity, {
|
|
209
|
+
size: 28,
|
|
210
|
+
standard: Boolean(options.standardIcons)
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
disabled: !resolved.href && typeof resolved.invoke !== 'function',
|
|
214
|
+
...resolved,
|
|
215
|
+
_zeyos: record
|
|
216
|
+
}];
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @param {{module?: string, fork?: unknown, weblet?: unknown}} state @returns {string} */
|
|
221
|
+
export function zeyosActiveIdentifier(state = {}) {
|
|
222
|
+
if (state.weblet != null && state.weblet !== '') return `weblet#${state.weblet}`;
|
|
223
|
+
if (state.fork != null && state.fork !== '') return `fork#${state.fork}`;
|
|
224
|
+
return String(state.module ?? '');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @param {unknown} raw @param {string|null} fallbackId @param {unknown} fallbackLabel @returns {Record<string, any>|null} */
|
|
228
|
+
function normalizeApplication(raw, fallbackId, fallbackLabel) {
|
|
229
|
+
if (Array.isArray(raw)) {
|
|
230
|
+
if (raw.length < 2) return null;
|
|
231
|
+
if (raw.length === 2) {
|
|
232
|
+
const module = String(raw[0]);
|
|
233
|
+
return { raw, identifier: module, label: String(raw[1]), module, type: 'module', color: null };
|
|
234
|
+
}
|
|
235
|
+
if (raw.length === 4) {
|
|
236
|
+
const module = String(raw[2] || 'default');
|
|
237
|
+
return {
|
|
238
|
+
raw, identifier: `fork#${raw[0]}`, label: String(raw[1]), module, type: 'fork',
|
|
239
|
+
color: normalizeServerColor(raw[3])
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
raw, identifier: `weblet#${raw[0]}`, label: String(raw[1]), module: 'weblets',
|
|
244
|
+
type: 'weblet', color: normalizeServerColor(raw[8])
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
248
|
+
const identifier = String(raw.identifier ?? raw.id ?? fallbackId ?? '');
|
|
249
|
+
if (!identifier) return null;
|
|
250
|
+
const type = identifier.startsWith('fork#') ? 'fork'
|
|
251
|
+
: identifier.startsWith('weblet#') ? 'weblet' : 'module';
|
|
252
|
+
return {
|
|
253
|
+
raw,
|
|
254
|
+
identifier,
|
|
255
|
+
label: String(raw.label ?? fallbackLabel ?? moduleInfo(identifier).label),
|
|
256
|
+
module: String(raw.module ?? (type === 'module' ? identifier : type === 'fork' ? 'default' : 'weblets')),
|
|
257
|
+
type,
|
|
258
|
+
color: normalizeServerColor(raw.color)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @param {unknown} payload @param {string} query @returns {Array<{raw: unknown, group: string}>} */
|
|
263
|
+
function flattenRecords(payload, query) {
|
|
264
|
+
if (Array.isArray(payload)) return payload.map((raw) => ({ raw, group: raw?.group ?? '' }));
|
|
265
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
266
|
+
const result = [];
|
|
267
|
+
for (const [group, rows] of Object.entries(payload)) {
|
|
268
|
+
if (!Array.isArray(rows)) continue;
|
|
269
|
+
for (const row of rows) {
|
|
270
|
+
if (!Array.isArray(row)) {
|
|
271
|
+
result.push({ raw: row, group });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// Search payloads use [ID,fork,name,sec] under an entity key; top results carry entity last.
|
|
275
|
+
const raw = group === '' && row.length >= 5
|
|
276
|
+
? { ID: row[0], fork: row[1], name: row[2], sec: row[3], entity: row[4], group }
|
|
277
|
+
: { ID: row[0], fork: row[1], name: row[2], sec: row[3], entity: group, group };
|
|
278
|
+
result.push({ raw, group });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @param {unknown} raw @param {string} group @param {string} query @returns {Record<string, any>|null} */
|
|
285
|
+
function normalizeRecord(raw, group, query) {
|
|
286
|
+
if (Array.isArray(raw)) {
|
|
287
|
+
if (query === '' && raw.length >= 4) {
|
|
288
|
+
return {
|
|
289
|
+
raw, entity: String(raw[0]), ID: raw[1], fork: raw[2] ?? null,
|
|
290
|
+
name: String(raw[3]), sec: String(raw[4] ?? ''), group: ''
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (!raw || typeof raw !== 'object' || raw.ID == null || raw.name == null) return null;
|
|
296
|
+
return {
|
|
297
|
+
raw,
|
|
298
|
+
entity: String(raw.entity ?? group ?? 'records'),
|
|
299
|
+
ID: raw.ID,
|
|
300
|
+
fork: raw.fork ?? null,
|
|
301
|
+
name: String(raw.name),
|
|
302
|
+
sec: String(raw.sec ?? ''),
|
|
303
|
+
group: String(raw.group ?? group ?? '')
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** @param {unknown} raw @param {unknown} resolver @returns {Record<string, any>} */
|
|
308
|
+
function mergeDestination(raw, resolver) {
|
|
309
|
+
const direct = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
310
|
+
const resolved = typeof resolver === 'function' ? resolver(raw) : null;
|
|
311
|
+
const destination = resolved && typeof resolved === 'object' ? resolved : {};
|
|
312
|
+
return Object.fromEntries(['href', 'target', 'invoke', 'value'].flatMap((key) => {
|
|
313
|
+
const value = destination[key] ?? direct[key];
|
|
314
|
+
return value === undefined ? [] : [[key, value]];
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @param {unknown} value @returns {string|null} */
|
|
319
|
+
function normalizeServerColor(value) {
|
|
320
|
+
const source = String(value ?? '').trim();
|
|
321
|
+
if (!source) return null;
|
|
322
|
+
const color = source.startsWith('#') ? source : `#${source}`;
|
|
323
|
+
return /^#[\da-f]{3}(?:[\da-f]{3})?$/i.test(color) ? color : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @param {unknown} input @returns {Record<string, any>} */
|
|
327
|
+
function launcherLabels(input) {
|
|
328
|
+
const labels = input && typeof input === 'object' ? input : {};
|
|
329
|
+
return {
|
|
330
|
+
applications: String(labels.applications ?? 'Applications'),
|
|
331
|
+
recent: String(labels.recent ?? 'Recent'),
|
|
332
|
+
results: String(labels.results ?? 'Top results'),
|
|
333
|
+
entities: labels.entities && typeof labels.entities === 'object' ? labels.entities : {},
|
|
334
|
+
move: String(labels.move ?? 'Move'),
|
|
335
|
+
open: String(labels.open ?? 'Open'),
|
|
336
|
+
close: String(labels.close ?? 'Close')
|
|
337
|
+
};
|
|
338
|
+
}
|
package/src/zeyos/modules.js
CHANGED