@zeyos/zx 3.0.0 → 4.1.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/DESIGN-SYSTEM.md +87 -71
  3. package/README.md +16 -37
  4. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  6. package/dist/types/components/card/card.d.ts +128 -0
  7. package/dist/types/components/chart/chart.d.ts +1 -0
  8. package/dist/types/components/filter/filter-model.d.ts +87 -0
  9. package/dist/types/components/filter/filter.d.ts +331 -0
  10. package/dist/types/components/launcher/launcher.d.ts +65 -6
  11. package/dist/types/components/select/select.d.ts +97 -4
  12. package/dist/types/components/table/table.d.ts +107 -3
  13. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  14. package/dist/types/core/dom.d.ts +9 -1
  15. package/dist/types/core/util.d.ts +9 -0
  16. package/dist/types/index.d.ts +6 -2
  17. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  18. package/dist/types/zeyos/icons.d.ts +24 -2
  19. package/dist/types/zeyos/index.d.ts +3 -2
  20. package/dist/types/zeyos/launcher.d.ts +147 -0
  21. package/dist/types/zeyos/select.d.ts +41 -0
  22. package/dist/zx-zeyos.esm.js +2018 -113
  23. package/dist/zx-zeyos.esm.min.js +1 -1
  24. package/dist/zx.css +815 -16
  25. package/dist/zx.esm.js +2661 -338
  26. package/dist/zx.esm.min.js +3 -3
  27. package/dist/zx.global.js +2661 -338
  28. package/dist/zx.global.min.js +3 -3
  29. package/dist/zx.min.css +1 -1
  30. package/docs/api.json +602 -110
  31. package/docs/llms.md +181 -123
  32. package/docs/llms.txt +24 -21
  33. package/package.json +8 -17
  34. package/src/components/app-icon/app-icon.css +105 -0
  35. package/src/components/app-icon/app-icon.js +134 -0
  36. package/src/components/app-sidebar/app-sidebar.css +12 -0
  37. package/src/components/app-sidebar/app-sidebar.js +310 -110
  38. package/src/components/card/card.css +149 -0
  39. package/src/components/card/card.js +285 -0
  40. package/src/components/chart/chart.js +8 -3
  41. package/src/components/filter/filter-model.js +226 -0
  42. package/src/components/filter/filter.css +147 -0
  43. package/src/components/filter/filter.js +554 -0
  44. package/src/components/grid/grid.css +1 -1
  45. package/src/components/grid/grid.js +14 -4
  46. package/src/components/launcher/launcher.css +113 -3
  47. package/src/components/launcher/launcher.js +234 -26
  48. package/src/components/message/message.css +36 -3
  49. package/src/components/select/field-adapter.js +9 -4
  50. package/src/components/select/select.css +161 -10
  51. package/src/components/select/select.js +361 -48
  52. package/src/components/table/table.css +85 -0
  53. package/src/components/table/table.js +426 -31
  54. package/src/components/tag-picker/tag-picker.css +19 -0
  55. package/src/components/tag-picker/tag-picker.js +42 -5
  56. package/src/core/dom.js +21 -1
  57. package/src/core/util.js +20 -0
  58. package/src/index.js +9 -2
  59. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  60. package/src/zeyos/icons.js +38 -20
  61. package/src/zeyos/index.js +8 -2
  62. package/src/zeyos/launcher.js +338 -0
  63. package/src/zeyos/modules.js +1 -0
  64. package/src/zeyos/select.js +116 -1
  65. package/styles/tokens/global.css +1 -1
  66. package/styles/tokens/modules.css +1 -0
  67. package/styles/tokens/semantic.css +9 -0
  68. package/styles/tokens/themes.css +2 -2
  69. package/styles/zx.css +3 -0
  70. package/MIGRATION.md +0 -749
  71. package/dist/types/compat/base.d.ts +0 -156
  72. package/dist/types/compat/globals.d.ts +0 -20
  73. package/dist/types/compat/index.d.ts +0 -128
  74. package/dist/types/compat/map/basic.d.ts +0 -175
  75. package/dist/types/compat/map/data.d.ts +0 -103
  76. package/dist/types/compat/map/date-time.d.ts +0 -80
  77. package/dist/types/compat/map/factory.d.ts +0 -9
  78. package/dist/types/compat/map/form.d.ts +0 -98
  79. package/dist/types/compat/map/helpers.d.ts +0 -12
  80. package/dist/types/compat/map/http.d.ts +0 -36
  81. package/dist/types/compat/map/layout.d.ts +0 -106
  82. package/dist/types/compat/map/options.d.ts +0 -80
  83. package/dist/types/compat/map/select.d.ts +0 -95
  84. package/dist/types/compat/map/stubs.d.ts +0 -32
  85. package/dist/types/compat/map/utilities.d.ts +0 -37
  86. package/dist/types/compat-entry.d.ts +0 -5
  87. package/dist/zx-compat.esm.js +0 -17848
  88. package/dist/zx-compat.esm.min.js +0 -1
  89. package/dist/zx-compat.global.js +0 -29008
  90. package/dist/zx-compat.global.min.js +0 -3
  91. package/src/compat/base.js +0 -291
  92. package/src/compat/globals.js +0 -167
  93. package/src/compat/index.js +0 -84
  94. package/src/compat/map/basic.js +0 -464
  95. package/src/compat/map/data.js +0 -254
  96. package/src/compat/map/date-time.js +0 -177
  97. package/src/compat/map/factory.js +0 -45
  98. package/src/compat/map/form.js +0 -330
  99. package/src/compat/map/helpers.js +0 -46
  100. package/src/compat/map/http.js +0 -117
  101. package/src/compat/map/layout.js +0 -256
  102. package/src/compat/map/options.js +0 -267
  103. package/src/compat/map/select.js +0 -198
  104. package/src/compat/map/stubs.js +0 -26
  105. package/src/compat/map/utilities.js +0 -113
  106. package/src/compat-entry.js +0 -11
@@ -47,6 +47,22 @@
47
47
  line-height: 1.5;
48
48
  }
49
49
 
50
+ .zx-tag-picker__tag[data-colored="true"] {
51
+ border-color: color-mix(in srgb, var(--zx-tag-color) 48%, var(--zx-color-border));
52
+ background: color-mix(in srgb, var(--zx-tag-color) 14%, var(--zx-color-bg-muted));
53
+ }
54
+
55
+ .zx-tag-picker__visual {
56
+ display: inline-grid;
57
+ place-items: center;
58
+ flex: none;
59
+ inline-size: 18px;
60
+ block-size: 18px;
61
+ border-radius: var(--zx-radius-sm);
62
+ background: color-mix(in srgb, var(--zx-tag-color, var(--zx-color-accent)) 16%, transparent);
63
+ color: var(--zx-tag-color, var(--zx-color-accent));
64
+ }
65
+
50
66
  .zx-tag-picker__tag:not(:has(.zx-tag-picker__remove)) {
51
67
  padding-inline-end: var(--zx-space-2);
52
68
  }
@@ -152,6 +168,9 @@
152
168
  }
153
169
 
154
170
  .zx-tag-picker__option-label {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: var(--zx-space-2);
155
174
  overflow: hidden;
156
175
  text-overflow: ellipsis;
157
176
  white-space: nowrap;
@@ -2,7 +2,7 @@ import { Component } from '../../core/component.js';
2
2
  import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
4
  import { position } from '../../core/position.js';
5
- import { uid } from '../../core/util.js';
5
+ import { isCssColor, uid } from '../../core/util.js';
6
6
  import { matchItems } from '../select/filter.js';
7
7
 
8
8
  /** @typedef {Record<string, any>|string|number} TagItem */
@@ -14,6 +14,8 @@ import { matchItems } from '../select/filter.js';
14
14
  * @property {unknown[]} [values=[]] Initially selected IDs.
15
15
  * @property {TagValueReader} [valueKey='ID'] Item ID property or reader.
16
16
  * @property {TagLabelReader} [labelKey='name'] Item label property or reader.
17
+ * @property {TagValueReader} [iconKey='icon'] Optional item icon property or reader.
18
+ * @property {TagValueReader} [colorKey='color'] Optional item colour property or reader.
17
19
  * @property {Array<TagValueReader>|null} [searchKeys=null] Fields searched by local filtering.
18
20
  * @property {'local'|((query: string) => Promise<TagItem[]>|TagItem[])} [filter='local'] Filtering mode.
19
21
  * @property {number} [minQuery=0] Minimum async query length.
@@ -65,6 +67,8 @@ export class TagPicker extends Component {
65
67
  values: [],
66
68
  valueKey: 'ID',
67
69
  labelKey: 'name',
70
+ iconKey: 'icon',
71
+ colorKey: 'color',
68
72
  searchKeys: null,
69
73
  filter: 'local',
70
74
  minQuery: 0,
@@ -411,9 +415,14 @@ export class TagPicker extends Component {
411
415
  _renderTags() {
412
416
  const tags = this._selected.map((item) => {
413
417
  const label = this._labelOf(item);
414
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
415
- const tag = h('span', { class: 'zx-tag-picker__tag' },
416
- h('span', { class: 'zx-tag-picker__tag-label' }, content));
418
+ const color = this._colorOf(item);
419
+ const content = this.options.renderTag ? this.options.renderTag(item)
420
+ : [this._iconOf(item), h('span', { class: 'zx-tag-picker__tag-label' }, label)];
421
+ const tag = h('span', {
422
+ class: 'zx-tag-picker__tag',
423
+ dataset: { colored: String(Boolean(color)) },
424
+ style: color ? { '--zx-tag-color': color } : null
425
+ }, content);
417
426
  if (!this._readonly && !this._disabled) {
418
427
  tag.append(h('button', {
419
428
  class: 'zx-icon-btn zx-tag-picker__remove',
@@ -446,7 +455,8 @@ export class TagPicker extends Component {
446
455
  h('span', { class: 'zx-tag-picker__check', ariaHidden: 'true' },
447
456
  selected ? icon('check', { size: 13 }) : null),
448
457
  h('span', { class: 'zx-tag-picker__option-label' },
449
- this.options.renderItem ? this.options.renderItem(item) : this._labelOf(item)));
458
+ this.options.renderItem ? this.options.renderItem(item)
459
+ : [this._iconOf(item), h('span', {}, this._labelOf(item))]));
450
460
  if (!selected && this.isFull()) option.setAttribute('aria-disabled', 'true');
451
461
  this._entries.push({ item, element: option, create: false });
452
462
  nodes.push(option);
@@ -773,6 +783,33 @@ export class TagPicker extends Component {
773
783
  return String(item ?? '');
774
784
  }
775
785
 
786
+ /** @param {unknown} item @returns {Node|null} */
787
+ _iconOf(item) {
788
+ const value = this._readItem(item, this.options.iconKey);
789
+ const visual = value && typeof value === 'object' && typeof value.nodeType === 'number'
790
+ ? /** @type {Node} */ (value).cloneNode(true)
791
+ : typeof value === 'string' && value.trim() ? icon(value, { size: 12 }) : null;
792
+ if (!visual) return null;
793
+ return h('span', {
794
+ class: 'zx-tag-picker__visual',
795
+ style: this._colorOf(item) ? { '--zx-tag-color': this._colorOf(item) } : null,
796
+ ariaHidden: 'true'
797
+ }, visual);
798
+ }
799
+
800
+ /** @param {unknown} item @returns {string|null} */
801
+ _colorOf(item) {
802
+ const value = this._readItem(item, this.options.colorKey);
803
+ return isCssColor(value) ? value : null;
804
+ }
805
+
806
+ /** @param {unknown} item @param {unknown} reader @returns {unknown} */
807
+ _readItem(item, reader) {
808
+ if (typeof reader === 'function') return reader(/** @type {TagItem} */ (item));
809
+ if (item !== null && typeof item === 'object' && typeof reader === 'string') return item[reader];
810
+ return null;
811
+ }
812
+
776
813
  /** @returns {void} */
777
814
  _emitChange() {
778
815
  this.emit('change', { values: this.getValues(), items: this.getItems() });
package/src/core/dom.js CHANGED
@@ -92,9 +92,29 @@ export function compactElement(compact, properties = null, content = null) {
92
92
  return element;
93
93
  }
94
94
 
95
- /** Exact migration-facing alias retained for existing ZeyOS builder calls. */
95
+ /** Public compact-builder alias for concise application DOM construction. */
96
96
  export const __ = compactElement;
97
97
 
98
+ /**
99
+ * Returns a trimmed native-link destination unless its parsed scheme can execute document code.
100
+ * Relative URLs, fragments, and ordinary application protocols remain valid; malformed and
101
+ * control-obfuscated script/data schemes are rejected.
102
+ * @param {unknown} value Candidate href.
103
+ * @returns {string|null}
104
+ */
105
+ export function safeHref(value) {
106
+ if (typeof value !== 'string') return null;
107
+ const href = value.trim();
108
+ if (!href) return null;
109
+ try {
110
+ const protocol = new URL(href, globalThis.location?.href ?? 'https://zx.invalid/').protocol;
111
+ if (['javascript:', 'data:', 'vbscript:'].includes(protocol.toLowerCase())) return null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ return href;
116
+ }
117
+
98
118
  /**
99
119
  * Applies the established ZeyOS property rules to an existing element:
100
120
  * `Dname` → `data-name`, `SfontSize` → `style.fontSize`, `on*` → a legacy-compatible listener,
package/src/core/util.js CHANGED
@@ -122,6 +122,26 @@ export function isElement(value) {
122
122
  && /** @type {{nodeType?: unknown}} */ (value).nodeType === 1);
123
123
  }
124
124
 
125
+ /**
126
+ * Accepts concrete CSS colours while rejecting image/function values that could make a colour
127
+ * descriptor trigger a network request when it is later consumed through a custom property.
128
+ * `CSS.supports()` provides the authoritative browser check; the conservative fallback covers
129
+ * ordinary named, hexadecimal, and functional colours in non-browser test environments.
130
+ * @param {unknown} value Candidate colour.
131
+ * @returns {value is string}
132
+ */
133
+ export function isCssColor(value) {
134
+ if (typeof value !== 'string') return false;
135
+ const text = value.trim();
136
+ if (!text || text.length > 256 || /[;{}\u0000-\u001f\u007f]/.test(text)
137
+ || /(?:url|image|image-set|cross-fade|element|var)\s*\(/i.test(text)) return false;
138
+ const supports = globalThis.CSS?.supports;
139
+ if (typeof supports === 'function') {
140
+ try { return supports.call(globalThis.CSS, 'color', text); } catch { return false; }
141
+ }
142
+ return /^(?:#[\da-f]{3,8}|[a-z]+|(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\([^;{}]+\))$/i.test(text);
143
+ }
144
+
125
145
  /**
126
146
  * Restricts a number to an inclusive range.
127
147
  * @param {number} n Value to restrict.
package/src/index.js CHANGED
@@ -88,6 +88,7 @@ export { CopyInput } from './components/copy/copy-input.js';
88
88
  export { Toolbar } from './components/toolbar/toolbar.js';
89
89
  export { Toggle } from './components/toggle/toggle.js';
90
90
  export { Groupbox } from './components/groupbox/groupbox.js';
91
+ export { Card } from './components/card/card.js';
91
92
  export { Panel } from './components/panel/panel.js';
92
93
  export { MasterPanel } from './components/master-panel/master-panel.js';
93
94
  export {
@@ -100,10 +101,11 @@ export { Stepper } from './components/stepper/stepper.js';
100
101
  export { Breadcrumb } from './components/breadcrumb/breadcrumb.js';
101
102
  export { Pagination, paginationRange } from './components/pagination/pagination.js';
102
103
  export { Search } from './components/search/search.js';
104
+ export { AppIcon, appIcon } from './components/app-icon/app-icon.js';
103
105
  export { Launcher, rankLauncherItems } from './components/launcher/launcher.js';
104
106
  export { Avatar, avatarInitials } from './components/avatar/avatar.js';
105
107
  export { AccountMenu } from './components/account-menu/account-menu.js';
106
- export { AppRail, normalizeAppItems } from './components/app-rail/app-rail.js';
108
+ export { normalizeAppItems } from './internal/app-rail.js';
107
109
  export { AppSidebar } from './components/app-sidebar/app-sidebar.js';
108
110
  export { Message } from './components/message/message.js';
109
111
  export { Modal } from './components/modal/modal.js';
@@ -131,7 +133,7 @@ export {
131
133
  DateRangeBox, formatRangeText, parseRangeText
132
134
  } from './components/datebox/date-range-box.js';
133
135
  export { Timebox, splitTime, joinTime } from './components/timebox/timebox.js';
134
- export { Table } from './components/table/table.js';
136
+ export { Table, reorderTableRows } from './components/table/table.js';
135
137
  export { Grid, billingColumns, isBillingLine } from './components/grid/grid.js';
136
138
  export {
137
139
  Chart, ChartJsAdapter, chartSummaryTable, cloneChartValue, isEmptyChartData
@@ -139,6 +141,11 @@ export {
139
141
  export { TreeView } from './components/tree/tree.js';
140
142
  export { Finder } from './components/finder/finder.js';
141
143
  export { DataFilter } from './components/data-filter/data-filter.js';
144
+ export { Filter } from './components/filter/filter.js';
145
+ export {
146
+ filterOperators, emptyFilterAst, filterCondition, filterGroup,
147
+ parseFilterAst, cloneFilterAst, stringifyFilterAst, validateFilterAst
148
+ } from './components/filter/filter-model.js';
142
149
  export { Field } from './components/field/field.js';
143
150
  export { Fieldset } from './components/fieldset/fieldset.js';
144
151
  export { Form } from './components/form/form.js';
@@ -1,24 +1,24 @@
1
- import { Component } from '../../core/component.js';
2
- import { h } from '../../core/dom.js';
3
- import { icon } from '../../core/icons.js';
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] Kernel icon name.
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} AppRailOptions
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 AppRail#select
40
- * @fires AppRail#flyoutchange
41
- * @extends {Component<AppRailOptions>}
40
+ * @fires AppRailPresenter#select
41
+ * @fires AppRailPresenter#flyoutchange
42
+ * @extends {Component<AppRailPresenterOptions>}
42
43
  */
43
- export class AppRail extends Component {
44
+ export class AppRailPresenter extends Component {
44
45
  static cssName = 'app-rail';
45
46
 
46
- /** @type {Readonly<AppRailOptions>} */
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 {AppRailOptions} [options={}] Rail options.
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
- ...item,
587
- label: String(item.label),
588
- children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
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 tag = item.href && !disclosure ? 'a' : 'button';
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' ? item.href : null,
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
- item.icon ? h('span', { class: `${className}-icon`, ariaHidden: 'true' }, icon(item.icon)) : null,
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 AppRail#select @type {CustomEvent<AppNavSelectDetail>} */
668
- /** Fired when a flyout opens or closes. @event AppRail#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>} */
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}>} */
@@ -4,7 +4,9 @@
4
4
  * @module zx/zeyos/icons
5
5
  */
6
6
 
7
- import { h, icon, loadFontAwesome } from '../index.js';
7
+ import { icon, loadFontAwesome } from '../index.js';
8
+ import { appIcon } from '../components/app-icon/app-icon.js';
9
+ import { isCssColor } from '../core/util.js';
8
10
  import { moduleGlyphColor, moduleInfo, normalizeModuleName } from './modules.js';
9
11
 
10
12
  /**
@@ -51,10 +53,13 @@ export function moduleIcon(name, options = {}) {
51
53
  /**
52
54
  * @typedef {Object} ModuleChipOptions
53
55
  * @property {number|string} [size=24] Chip edge length.
54
- * @property {number|string} [iconSize='1em'] Glyph size; `1em` is 60% of the chip edge.
56
+ * @property {number|string} [iconSize='52%'] Glyph size relative to the tile edge.
55
57
  * @property {string|null} [label=null] Accessible label; null marks the chip decorative.
56
58
  * @property {boolean} [title=false] Adds the module's display name as a `title` tooltip.
57
59
  * @property {boolean} [standard=false] Use the stock Font Awesome fallback glyph.
60
+ * @property {string|Node|(()=>string|Node)|null} [icon] Explicit glyph override, useful for an offline fallback.
61
+ * @property {string} [color] Override the module colour for a server-defined fork or weblet.
62
+ * @property {false|'subtle'|'strong'} [glass='subtle'] AppIcon material treatment.
58
63
  * @property {string|string[]} [class] Extra class names.
59
64
  */
60
65
 
@@ -67,26 +72,39 @@ export function moduleIcon(name, options = {}) {
67
72
  * @returns {HTMLElement} A `<span class="zx-module-icon">` wrapping the glyph.
68
73
  */
69
74
  export function moduleChip(name, options = {}) {
70
- const { size = 24, iconSize = '1em', label = null, title = false, standard = false } = options;
75
+ const {
76
+ size = 24, iconSize = '52%', label = null, title = false, standard = false,
77
+ color = null
78
+ } = options;
71
79
  const key = normalizeModuleName(name);
72
80
  const info = moduleInfo(name);
81
+ const background = isCssColor(color) ? color : info.color;
73
82
 
74
- const chip = h('span', {
75
- class: ['zx-module-icon', options.class].flat().filter(Boolean),
76
- dataset: { module: key },
77
- style: {
78
- '--zx-module-color': info.color,
79
- '--zx-module-glyph': moduleGlyphColor(info.color),
80
- '--zx-module-icon-size': typeof size === 'number' ? `${size}px` : String(size)
81
- },
82
- title: title ? info.label : null
83
- }, moduleIcon(name, { size: iconSize, standard }));
84
-
85
- if (label === null) {
86
- chip.setAttribute('aria-hidden', 'true');
87
- } else {
88
- chip.setAttribute('role', 'img');
89
- chip.setAttribute('aria-label', label);
90
- }
83
+ const chip = appIcon({
84
+ // AppIcon owns glyph sizing. Keep the provider glyph at 1em so the percentage is not applied
85
+ // once to the wrapper and a second time to a Font Awesome <i>.
86
+ icon: options.icon ?? moduleIcon(name, { size: '1em', standard }),
87
+ size,
88
+ iconSize,
89
+ color: background,
90
+ label,
91
+ glass: options.glass ?? 'subtle',
92
+ class: ['zx-module-icon', options.class].flat().filter(Boolean)
93
+ });
94
+ chip.dataset.module = key;
95
+ chip.style.setProperty('--zx-module-color', background);
96
+ chip.style.setProperty('--zx-module-glyph', moduleGlyphColor(background));
97
+ chip.style.setProperty('--zx-app-icon-glyph', moduleGlyphColor(background));
98
+ if (title) chip.title = info.label;
91
99
  return chip;
92
100
  }
101
+
102
+ /**
103
+ * Creates the public ZeyOS AppIcon preset for any module, entity, fork, or weblet identity.
104
+ * @param {string} name Module/entity identifier.
105
+ * @param {ModuleChipOptions} [options={}] AppIcon options.
106
+ * @returns {HTMLElement}
107
+ */
108
+ export function zeyosAppIcon(name, options = {}) {
109
+ return moduleChip(name, options);
110
+ }
@@ -15,7 +15,13 @@ export {
15
15
  zeyosModules, moduleAliases, moduleInfo, moduleColor, moduleIconName, moduleKeys,
16
16
  moduleGlyphColor, normalizeModuleName, registerModules
17
17
  } from './modules.js';
18
- export { ZEYOS_ICON_KIT, useZeyosIcons, moduleIcon, moduleChip } from './icons.js';
19
- export { zeyosSelect } from './select.js';
18
+ export { ZEYOS_ICON_KIT, useZeyosIcons, moduleIcon, moduleChip, zeyosAppIcon } from './icons.js';
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';