@zeyos/zx 4.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 (49) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/DESIGN-SYSTEM.md +59 -3
  3. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  4. package/dist/types/components/filter/filter-model.d.ts +87 -0
  5. package/dist/types/components/filter/filter.d.ts +331 -0
  6. package/dist/types/components/select/select.d.ts +46 -4
  7. package/dist/types/components/table/table.d.ts +103 -0
  8. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  9. package/dist/types/core/dom.d.ts +1 -1
  10. package/dist/types/core/util.d.ts +9 -0
  11. package/dist/types/index.d.ts +4 -1
  12. package/dist/types/zeyos/icons.d.ts +19 -2
  13. package/dist/types/zeyos/index.d.ts +1 -1
  14. package/dist/types/zeyos/select.d.ts +2 -0
  15. package/dist/zx-zeyos.esm.js +702 -75
  16. package/dist/zx-zeyos.esm.min.js +1 -1
  17. package/dist/zx.css +415 -4
  18. package/dist/zx.esm.js +1568 -115
  19. package/dist/zx.esm.min.js +3 -3
  20. package/dist/zx.global.js +1568 -115
  21. package/dist/zx.global.min.js +3 -3
  22. package/dist/zx.min.css +1 -1
  23. package/docs/api.json +379 -0
  24. package/docs/llms.md +93 -16
  25. package/docs/llms.txt +11 -9
  26. package/package.json +1 -1
  27. package/src/components/app-icon/app-icon.css +105 -0
  28. package/src/components/app-icon/app-icon.js +134 -0
  29. package/src/components/filter/filter-model.js +226 -0
  30. package/src/components/filter/filter.css +147 -0
  31. package/src/components/filter/filter.js +554 -0
  32. package/src/components/grid/grid.css +1 -1
  33. package/src/components/grid/grid.js +4 -1
  34. package/src/components/message/message.css +36 -3
  35. package/src/components/select/select.css +17 -0
  36. package/src/components/select/select.js +105 -24
  37. package/src/components/table/table.css +85 -0
  38. package/src/components/table/table.js +418 -24
  39. package/src/components/tag-picker/tag-picker.css +19 -0
  40. package/src/components/tag-picker/tag-picker.js +42 -5
  41. package/src/core/dom.js +1 -1
  42. package/src/core/util.js +20 -0
  43. package/src/index.js +7 -1
  44. package/src/zeyos/icons.js +34 -21
  45. package/src/zeyos/index.js +1 -1
  46. package/src/zeyos/select.js +6 -5
  47. package/styles/tokens/semantic.css +9 -0
  48. package/styles/tokens/themes.css +2 -2
  49. package/styles/zx.css +2 -0
@@ -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,7 +92,7 @@ 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
98
  /**
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
@@ -101,6 +101,7 @@ export { Stepper } from './components/stepper/stepper.js';
101
101
  export { Breadcrumb } from './components/breadcrumb/breadcrumb.js';
102
102
  export { Pagination, paginationRange } from './components/pagination/pagination.js';
103
103
  export { Search } from './components/search/search.js';
104
+ export { AppIcon, appIcon } from './components/app-icon/app-icon.js';
104
105
  export { Launcher, rankLauncherItems } from './components/launcher/launcher.js';
105
106
  export { Avatar, avatarInitials } from './components/avatar/avatar.js';
106
107
  export { AccountMenu } from './components/account-menu/account-menu.js';
@@ -132,7 +133,7 @@ export {
132
133
  DateRangeBox, formatRangeText, parseRangeText
133
134
  } from './components/datebox/date-range-box.js';
134
135
  export { Timebox, splitTime, joinTime } from './components/timebox/timebox.js';
135
- export { Table } from './components/table/table.js';
136
+ export { Table, reorderTableRows } from './components/table/table.js';
136
137
  export { Grid, billingColumns, isBillingLine } from './components/grid/grid.js';
137
138
  export {
138
139
  Chart, ChartJsAdapter, chartSummaryTable, cloneChartValue, isEmptyChartData
@@ -140,6 +141,11 @@ export {
140
141
  export { TreeView } from './components/tree/tree.js';
141
142
  export { Finder } from './components/finder/finder.js';
142
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';
143
149
  export { Field } from './components/field/field.js';
144
150
  export { Fieldset } from './components/fieldset/fieldset.js';
145
151
  export { Form } from './components/form/form.js';
@@ -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,11 +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.
58
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.
59
63
  * @property {string|string[]} [class] Extra class names.
60
64
  */
61
65
 
@@ -69,29 +73,38 @@ export function moduleIcon(name, options = {}) {
69
73
  */
70
74
  export function moduleChip(name, options = {}) {
71
75
  const {
72
- size = 24, iconSize = '1em', label = null, title = false, standard = false,
76
+ size = 24, iconSize = '52%', label = null, title = false, standard = false,
73
77
  color = null
74
78
  } = options;
75
79
  const key = normalizeModuleName(name);
76
80
  const info = moduleInfo(name);
77
- const background = color || info.color;
81
+ const background = isCssColor(color) ? color : info.color;
78
82
 
79
- const chip = h('span', {
80
- class: ['zx-module-icon', options.class].flat().filter(Boolean),
81
- dataset: { module: key },
82
- style: {
83
- '--zx-module-color': background,
84
- '--zx-module-glyph': moduleGlyphColor(background),
85
- '--zx-module-icon-size': typeof size === 'number' ? `${size}px` : String(size)
86
- },
87
- title: title ? info.label : null
88
- }, moduleIcon(name, { size: iconSize, standard }));
89
-
90
- if (label === null) {
91
- chip.setAttribute('aria-hidden', 'true');
92
- } else {
93
- chip.setAttribute('role', 'img');
94
- chip.setAttribute('aria-label', label);
95
- }
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;
96
99
  return chip;
97
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,7 @@ 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';
18
+ export { ZEYOS_ICON_KIT, useZeyosIcons, moduleIcon, moduleChip, zeyosAppIcon } from './icons.js';
19
19
  export {
20
20
  ZEYOS_LAUNCHER_APPLICATIONS, buildZeyosLauncherConfig, zeyosLauncher,
21
21
  normalizeZeyosLauncherApplications, normalizeZeyosLauncherRecords, zeyosActiveIdentifier
@@ -1,4 +1,4 @@
1
- import { Select, h, icon } from '../index.js';
1
+ import { Select, h } from '../index.js';
2
2
  import { connect } from './connect.js';
3
3
  import { moduleChip } from './icons.js';
4
4
  import { buildListQuery } from './query.js';
@@ -104,6 +104,7 @@ export function zeyosSelect(client, resource, opts = {}) {
104
104
  * subtitleKey?: string|((item: Record<string, unknown>) => unknown)|null,
105
105
  * moduleKey?: string|((item: Record<string, unknown>) => unknown),
106
106
  * none?: boolean|string,
107
+ * recent?: Record<string, unknown>[],
107
108
  * create?: boolean|{id?: string|number, label?: string, group?: string, icon?: string|Node, oninvoke?: Function},
108
109
  * renderIcon?: ((item: Record<string, unknown>, context: Record<string, unknown>) => Node|null)|null
109
110
  * }} [opts={}] Entity presentation, query, and Select options.
@@ -119,6 +120,7 @@ export function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
119
120
  moduleKey = () => resource,
120
121
  groupKey = null,
121
122
  none = true,
123
+ recent = [],
122
124
  create = false,
123
125
  renderIcon = defaultEntityIcon,
124
126
  renderItem,
@@ -138,6 +140,7 @@ export function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
138
140
  groupKey,
139
141
  clearable: none === false ? Boolean(rest.clearable) : true
140
142
  });
143
+ base.recent = Array.isArray(recent) ? recent : [];
141
144
  const renderModuleIcon = (item, selected = false) => {
142
145
  if (renderIcon === null) return null;
143
146
  const module = readRecord(item, moduleKey) ?? resource;
@@ -188,7 +191,7 @@ export function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
188
191
  export function zeyosEntitySelect(client, resource, opts = {}) {
189
192
  const config = buildZeyosEntitySelectConfig(client, resource, opts);
190
193
  const requestedValue = opts.value ?? null;
191
- const select = /** @type {Select & {ready: Promise<Select>}} */ (new Select(null, config));
194
+ const select = /** @type {Select & {ready: Promise<Select>}} */ (Select.entity(null, config));
192
195
  select.ready = hydrateInitialValue(select, client, resource, requestedValue);
193
196
  return select;
194
197
  }
@@ -248,9 +251,7 @@ function readRecord(item, reader) {
248
251
 
249
252
  /** Dependency-free module-color example; exact product glyphs remain host-injectable. @param {Record<string, unknown>} _item @param {{module?: unknown}} context @returns {Node} */
250
253
  function defaultEntityIcon(_item, context) {
251
- const chip = moduleChip(String(context.module ?? 'default'), { size: 20 });
252
- chip.replaceChildren(icon('square', { size: 10 }));
253
- return chip;
254
+ return moduleChip(String(context.module ?? 'default'), { size: 20 });
254
255
  }
255
256
 
256
257
  /** @param {unknown} valueKey @param {unknown} labelKey @param {string[]} searchFields @returns {string[]|undefined} */
@@ -50,6 +50,15 @@
50
50
  --zx-overlay-shadow: var(--zx-shadow-3);
51
51
  /* Blur applied behind a backdrop that asked for it. Theme-independent, so light defines it once. */
52
52
  --zx-overlay-blur: 8px;
53
+ --zx-color-glass-border: color-mix(in srgb, var(--zx-gray-0) 24%, transparent);
54
+ --zx-color-glass-highlight: color-mix(in srgb, var(--zx-gray-0) 24%, transparent);
55
+ --zx-color-glass-highlight-strong: color-mix(in srgb, var(--zx-gray-0) 38%, transparent);
56
+ --zx-color-glass-surface: color-mix(in srgb, var(--zx-color-bg-raised) 78%, transparent);
57
+ --zx-glass-blur: 8px;
58
+ --zx-glass-saturation: 130%;
59
+ --zx-glass-color-strength: 82%;
60
+ --zx-app-icon-shadow: inset 0 1px 0 color-mix(in srgb, var(--zx-gray-0) 28%, transparent), inset 0 -1px 0 color-mix(in srgb, var(--zx-gray-950) 16%, transparent), 0 5px 14px color-mix(in srgb, var(--zx-app-icon-color) 28%, transparent);
61
+ --zx-glass-glyph-shadow: 0 1px 1px color-mix(in srgb, var(--zx-gray-950) 22%, transparent);
53
62
  --zx-header-bg: var(--zx-gray-900);
54
63
  --zx-header-text: var(--zx-gray-50);
55
64
  }
@@ -17,7 +17,7 @@
17
17
  * against the dark page, which is what keeps `--zx-color-on-accent` correct in both polarities.
18
18
  */
19
19
 
20
- /* Zx — the ZeyOS brand green on Xenon's emerald ramp. The default. */
20
+ /* ZeyOS — the ZeyOS brand green on Xenon's emerald ramp. The default. */
21
21
  [data-zx-preset="zx"] {
22
22
  --zx-accent-300: #5ee9b5;
23
23
  --zx-accent-400: #21cc75;
@@ -26,7 +26,7 @@
26
26
  --zx-accent-700: #006045;
27
27
  }
28
28
 
29
- /* ZeyOS — the gold of the ZeyOS product marque, anchored at 400. */
29
+ /* Marque Gold — the gold of the ZeyOS product marque, anchored at 400. */
30
30
  [data-zx-preset="zeyos"] {
31
31
  --zx-accent-300: #ffd9a8;
32
32
  --zx-accent-400: #f7bc60;
package/styles/zx.css CHANGED
@@ -27,6 +27,7 @@
27
27
  @import url("../src/components/tabbox/tabbox.css");
28
28
  @import url("../src/components/navigation-bar/navigation-bar.css");
29
29
  @import url("../src/components/search/search.css");
30
+ @import url("../src/components/app-icon/app-icon.css");
30
31
  @import url("../src/components/launcher/launcher.css");
31
32
  @import url("../src/components/avatar/avatar.css");
32
33
  @import url("../src/components/account-menu/account-menu.css");
@@ -60,6 +61,7 @@
60
61
  @import url("../src/components/tree/tree.css");
61
62
  @import url("../src/components/finder/finder.css");
62
63
  @import url("../src/components/data-filter/data-filter.css");
64
+ @import url("../src/components/filter/filter.css");
63
65
  @import url("../src/components/pagination/pagination.css");
64
66
  @import url("../src/components/stepper/stepper.css");
65
67
  @import url("../src/components/breadcrumb/breadcrumb.css");