@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
@@ -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
+ }
@@ -109,6 +109,7 @@ export const zeyosModules = Object.freeze({
109
109
  * @type {Readonly<Record<string, string>>}
110
110
  */
111
111
  export const moduleAliases = Object.freeze({
112
+ main: 'zeyos',
112
113
  addresses: 'contacts',
113
114
  clocking: 'actionsteps',
114
115
  comments: 'notes',
@@ -1,5 +1,6 @@
1
- import { Select } from '../index.js';
1
+ import { Select, h } from '../index.js';
2
2
  import { connect } from './connect.js';
3
+ import { moduleChip } from './icons.js';
3
4
  import { buildListQuery } from './query.js';
4
5
 
5
6
  /**
@@ -93,6 +94,108 @@ export function zeyosSelect(client, resource, opts = {}) {
93
94
  return select;
94
95
  }
95
96
 
97
+ /**
98
+ * Builds the rich, resource-specific Entity-box preset over the generic injected Select binding.
99
+ * It adds module-colored icons, optional subtitles/groups, a fixed clear row, and an optional
100
+ * application-owned create command without changing the selected ID contract.
101
+ * @param {Record<string, any>} client Injected ZeyOS client instance.
102
+ * @param {string} resource Resource such as `projects` or `accounts`.
103
+ * @param {ZeyosSelectOptions & {
104
+ * subtitleKey?: string|((item: Record<string, unknown>) => unknown)|null,
105
+ * moduleKey?: string|((item: Record<string, unknown>) => unknown),
106
+ * none?: boolean|string,
107
+ * recent?: Record<string, unknown>[],
108
+ * create?: boolean|{id?: string|number, label?: string, group?: string, icon?: string|Node, oninvoke?: Function},
109
+ * renderIcon?: ((item: Record<string, unknown>, context: Record<string, unknown>) => Node|null)|null
110
+ * }} [opts={}] Entity presentation, query, and Select options.
111
+ * @returns {Record<string, any>} Plain Select options.
112
+ */
113
+ export function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
114
+ const {
115
+ fields,
116
+ labelKey = inferLabelKey(client, resource),
117
+ valueKey = 'ID',
118
+ searchFields = inferSearchFields(client, resource),
119
+ subtitleKey = null,
120
+ moduleKey = () => resource,
121
+ groupKey = null,
122
+ none = true,
123
+ recent = [],
124
+ create = false,
125
+ renderIcon = defaultEntityIcon,
126
+ renderItem,
127
+ renderValueAdornment,
128
+ ...rest
129
+ } = opts;
130
+ const projection = fields ?? defaultProjection(valueKey, labelKey, [
131
+ ...searchFields,
132
+ ...[subtitleKey, moduleKey, groupKey].filter((reader) => typeof reader === 'string')
133
+ ]);
134
+ const base = buildZeyosSelectConfig(client, resource, {
135
+ ...rest,
136
+ fields: projection,
137
+ labelKey,
138
+ valueKey,
139
+ searchFields,
140
+ groupKey,
141
+ clearable: none === false ? Boolean(rest.clearable) : true
142
+ });
143
+ base.recent = Array.isArray(recent) ? recent : [];
144
+ const renderModuleIcon = (item, selected = false) => {
145
+ if (renderIcon === null) return null;
146
+ const module = readRecord(item, moduleKey) ?? resource;
147
+ return renderIcon(item, { resource, module: String(module), selected });
148
+ };
149
+ base.renderItem = renderItem ?? ((item) => {
150
+ const visual = renderModuleIcon(item);
151
+ return h('span', { class: 'zx-select__entity' },
152
+ visual ? h('span', {
153
+ class: 'zx-select__entity-icon',
154
+ ariaHidden: 'true'
155
+ }, visual) : null,
156
+ h('span', { class: 'zx-select__entity-copy' },
157
+ h('span', { class: 'zx-select__entity-label' }, String(readRecord(item, labelKey) ?? '')),
158
+ subtitleKey && readRecord(item, subtitleKey) != null
159
+ ? h('span', { class: 'zx-select__entity-subtitle' }, String(readRecord(item, subtitleKey)))
160
+ : null));
161
+ });
162
+ base.renderValueAdornment = renderValueAdornment ?? ((item) => renderModuleIcon(item, true));
163
+ if (none !== false) {
164
+ base.noneLabel = typeof none === 'string' ? none : rest.noneLabel ?? 'No selection';
165
+ base.renderNone = rest.renderNone ?? ((label) => h('span', { class: 'zx-select__entity-none' },
166
+ h('span', { class: 'zx-select__entity-none-icon', ariaHidden: 'true' }),
167
+ h('span', {}, label)));
168
+ }
169
+ if (create) {
170
+ const descriptor = create === true ? {} : create;
171
+ const invoke = typeof descriptor.oninvoke === 'function' ? descriptor.oninvoke : null;
172
+ base.actions = [...(Array.isArray(base.actions) ? base.actions : []), {
173
+ id: descriptor.id ?? 'create',
174
+ label: descriptor.label ?? 'Create new…',
175
+ group: descriptor.group ?? 'New',
176
+ icon: descriptor.icon ?? 'plus',
177
+ invoke: (detail) => invoke?.({ ...detail, resource })
178
+ }];
179
+ }
180
+ return base;
181
+ }
182
+
183
+ /**
184
+ * Creates a ZeyOS Entity-box preset. Await `select.ready` when a non-null initial ID must be
185
+ * hydrated before presentation.
186
+ * @param {Record<string, any>} client Injected ZeyOS client instance.
187
+ * @param {string} resource Resource name.
188
+ * @param {Record<string, any>} [opts={}] Entity Select options.
189
+ * @returns {Select & {ready: Promise<Select>}}
190
+ */
191
+ export function zeyosEntitySelect(client, resource, opts = {}) {
192
+ const config = buildZeyosEntitySelectConfig(client, resource, opts);
193
+ const requestedValue = opts.value ?? null;
194
+ const select = /** @type {Select & {ready: Promise<Select>}} */ (Select.entity(null, config));
195
+ select.ready = hydrateInitialValue(select, client, resource, requestedValue);
196
+ return select;
197
+ }
198
+
96
199
  /**
97
200
  * Normalizes list output without importing the client package. If the injected client exposes its
98
201
  * own `normalizeListResult`, that implementation wins; otherwise arrays and `.data` are handled
@@ -139,6 +242,18 @@ function normalizeRecord(result) {
139
242
  throw new TypeError('ZeyOS get operation did not return a record');
140
243
  }
141
244
 
245
+ /** @param {Record<string, unknown>} item @param {unknown} reader @returns {unknown} */
246
+ function readRecord(item, reader) {
247
+ if (typeof reader === 'function') return reader(item);
248
+ if (item && typeof item === 'object' && typeof reader === 'string') return item[reader];
249
+ return null;
250
+ }
251
+
252
+ /** Dependency-free module-color example; exact product glyphs remain host-injectable. @param {Record<string, unknown>} _item @param {{module?: unknown}} context @returns {Node} */
253
+ function defaultEntityIcon(_item, context) {
254
+ return moduleChip(String(context.module ?? 'default'), { size: 20 });
255
+ }
256
+
142
257
  /** @param {unknown} valueKey @param {unknown} labelKey @param {string[]} searchFields @returns {string[]|undefined} */
143
258
  function defaultProjection(valueKey, labelKey, searchFields) {
144
259
  const keys = [valueKey, labelKey, ...searchFields].filter((key) => typeof key === 'string' && key);
@@ -9,7 +9,7 @@
9
9
  * Component CSS must never use them directly — tests/lint-tokens.js enforces that.
10
10
  */
11
11
  :root {
12
- /* Neutrals — Tailwind Zinc */
12
+ /* Neutrals */
13
13
  --zx-gray-0: #ffffff;
14
14
  --zx-gray-50: #fafafa;
15
15
  --zx-gray-100: #f4f4f5;
@@ -89,6 +89,7 @@
89
89
  --zx-module-linecoupon: var(--zx-module-line);
90
90
  --zx-module-logins: var(--zx-module-auth);
91
91
  --zx-module-mailservers: var(--zx-module-channels);
92
+ --zx-module-main: var(--zx-module-zeyos);
92
93
  --zx-module-notifications: var(--zx-module-messages);
93
94
  --zx-module-participants-campaigns: var(--zx-module-participants);
94
95
  --zx-module-participants-mailinglists: var(--zx-module-participants);
@@ -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
@@ -21,11 +21,13 @@
21
21
  @import url("../src/components/toolbar/toolbar.css");
22
22
  @import url("../src/components/toggle/toggle.css");
23
23
  @import url("../src/components/groupbox/groupbox.css");
24
+ @import url("../src/components/card/card.css");
24
25
  @import url("../src/components/panel/panel.css");
25
26
  @import url("../src/components/master-panel/master-panel.css");
26
27
  @import url("../src/components/tabbox/tabbox.css");
27
28
  @import url("../src/components/navigation-bar/navigation-bar.css");
28
29
  @import url("../src/components/search/search.css");
30
+ @import url("../src/components/app-icon/app-icon.css");
29
31
  @import url("../src/components/launcher/launcher.css");
30
32
  @import url("../src/components/avatar/avatar.css");
31
33
  @import url("../src/components/account-menu/account-menu.css");
@@ -59,6 +61,7 @@
59
61
  @import url("../src/components/tree/tree.css");
60
62
  @import url("../src/components/finder/finder.css");
61
63
  @import url("../src/components/data-filter/data-filter.css");
64
+ @import url("../src/components/filter/filter.css");
62
65
  @import url("../src/components/pagination/pagination.css");
63
66
  @import url("../src/components/stepper/stepper.css");
64
67
  @import url("../src/components/breadcrumb/breadcrumb.css");