@zeyos/zx 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/DESIGN-SYSTEM.md +33 -3
  3. package/README.md +17 -5
  4. package/dist/types/components/app-icon/app-icon.d.ts +8 -1
  5. package/dist/types/components/aurora/aurora.d.ts +72 -0
  6. package/dist/types/components/calendar/calendar-model.d.ts +202 -0
  7. package/dist/types/components/calendar/calendar.d.ts +500 -0
  8. package/dist/types/components/card-view/card-view.d.ts +303 -0
  9. package/dist/types/components/card-view/record-card.d.ts +242 -0
  10. package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
  11. package/dist/types/components/table/table.d.ts +120 -31
  12. package/dist/types/components/table-view/table-view.d.ts +192 -0
  13. package/dist/types/components/view/record-view.d.ts +464 -0
  14. package/dist/types/index.d.ts +7 -0
  15. package/dist/types/zeyos/calendar.d.ts +91 -0
  16. package/dist/types/zeyos/icons.d.ts +19 -3
  17. package/dist/types/zeyos/index.d.ts +4 -0
  18. package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
  19. package/dist/types/zeyos/saved-views.d.ts +331 -0
  20. package/dist/types/zeyos/view.d.ts +305 -0
  21. package/dist/zx-zeyos.esm.js +4627 -1460
  22. package/dist/zx-zeyos.esm.min.js +1 -1
  23. package/dist/zx.css +2492 -410
  24. package/dist/zx.esm.js +4891 -115
  25. package/dist/zx.esm.min.js +3 -3
  26. package/dist/zx.global.js +4891 -115
  27. package/dist/zx.global.min.js +3 -3
  28. package/dist/zx.min.css +1 -1
  29. package/docs/api.json +1702 -191
  30. package/docs/llms.md +207 -20
  31. package/docs/llms.txt +16 -6
  32. package/package.json +1 -1
  33. package/src/components/app-icon/app-icon.css +140 -30
  34. package/src/components/app-icon/app-icon.js +32 -5
  35. package/src/components/app-rail/app-rail.css +11 -2
  36. package/src/components/app-sidebar/app-sidebar.css +11 -2
  37. package/src/components/aurora/aurora.css +143 -0
  38. package/src/components/aurora/aurora.js +146 -0
  39. package/src/components/avatar/avatar.css +1 -1
  40. package/src/components/button/button.css +34 -2
  41. package/src/components/calendar/calendar-model.js +402 -0
  42. package/src/components/calendar/calendar.css +567 -0
  43. package/src/components/calendar/calendar.js +1524 -0
  44. package/src/components/card/card.css +25 -3
  45. package/src/components/card-view/card-view.css +389 -0
  46. package/src/components/card-view/card-view.js +527 -0
  47. package/src/components/card-view/record-card.js +392 -0
  48. package/src/components/context-menu/context-menu.css +2 -2
  49. package/src/components/date-picker/date-picker.css +15 -3
  50. package/src/components/datebox/date-range-box.css +15 -2
  51. package/src/components/datebox/datebox.css +16 -3
  52. package/src/components/dropdown/dropdown.css +15 -3
  53. package/src/components/kanban-view/kanban-view.css +216 -0
  54. package/src/components/kanban-view/kanban-view.js +1228 -0
  55. package/src/components/launcher/launcher.css +18 -5
  56. package/src/components/master-panel/master-panel.css +14 -3
  57. package/src/components/menu-button/menu-button.css +2 -2
  58. package/src/components/message/message.css +15 -10
  59. package/src/components/modal/modal.css +15 -3
  60. package/src/components/navigation-bar/navigation-bar.css +17 -5
  61. package/src/components/panel/panel.css +10 -2
  62. package/src/components/select/select.css +20 -4
  63. package/src/components/sheet/sheet.css +4 -1
  64. package/src/components/table/table.css +80 -4
  65. package/src/components/table/table.js +322 -37
  66. package/src/components/table-view/table-view.css +18 -0
  67. package/src/components/table-view/table-view.js +347 -0
  68. package/src/components/tag-picker/tag-picker.css +15 -3
  69. package/src/components/tooltip/tooltip.css +15 -3
  70. package/src/components/view/record-view.css +114 -0
  71. package/src/components/view/record-view.js +770 -0
  72. package/src/index.js +15 -0
  73. package/src/zeyos/calendar.js +115 -0
  74. package/src/zeyos/icons.js +15 -8
  75. package/src/zeyos/index.js +13 -0
  76. package/src/zeyos/launcher.js +2 -2
  77. package/src/zeyos/legacy-saved-views.js +155 -0
  78. package/src/zeyos/saved-views.js +815 -0
  79. package/src/zeyos/table.js +3 -1
  80. package/src/zeyos/view.js +543 -0
  81. package/styles/icons.css +3 -10
  82. package/styles/tokens/semantic.css +74 -2
  83. package/styles/zx.css +6 -0
@@ -196,7 +196,9 @@ export function zeyosTable(client, resource, opts = {}) {
196
196
  };
197
197
 
198
198
  table.on('sort', (event) => {
199
- currentSort = { id: String(event.detail.id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' };
199
+ const id = event.detail?.id;
200
+ currentSort = id == null ? null
201
+ : { id: String(id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' };
200
202
  void load({ page: 0 }).catch(() => { /* The connection facade already reported the failure. */ });
201
203
  });
202
204
  return binding;
@@ -0,0 +1,543 @@
1
+ import { buildListQuery } from './query.js';
2
+ import { connect } from './connect.js';
3
+ import { normalizeZeyosListResult } from './select.js';
4
+ import { createSavedViewRegistry } from './saved-views.js';
5
+ import { buildZeyosTableConfig } from './table.js';
6
+
7
+ /** @typedef {import('../components/view/record-view.js').ViewField} ViewField */
8
+ /** @typedef {import('../components/view/record-view.js').ViewSort} ViewSort */
9
+ /** @typedef {import('./saved-views.js').SavedViewEntry} SavedViewEntry */
10
+ /** @typedef {import('./saved-views.js').SavedViewApplyResult} SavedViewApplyResult */
11
+
12
+ /**
13
+ * @typedef {Object} ZeyosSavedViewsOptions
14
+ * @property {Record<string,any>} [controller] Existing saved-view registry. It remains caller-owned.
15
+ * @property {Record<string,any>} [registry] Alias for `controller`.
16
+ * @property {Record<string,any>} [transport] Injected persistence transport used to construct a registry.
17
+ * @property {{userId:string|number,workspaceId:string|number|null,resource?:string}} [scope]
18
+ * Saved-view isolation scope; null workspace identifies ZeyOS's base workspace.
19
+ * resource defaults to the bound ZeyOS resource.
20
+ * @property {Record<string,unknown>} [options] Options passed to `createSavedViewRegistry`.
21
+ * @property {'table'|'card'|'kanban'} [type] Presentation identity; normally derived from the View.
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} ZeyosViewOptions
26
+ * @property {string[]} [fields] Curated ordered schema field allow-list.
27
+ * @property {string[]} [exclude] Schema fields to omit.
28
+ * @property {Record<string, string>} [labels] Per-field label overrides.
29
+ * @property {Record<string, Partial<ViewField>>} [fieldOverrides] View descriptor overrides.
30
+ * @property {string|((record:Record<string,unknown>)=>unknown)} [recordId='ID'] Record id accessor.
31
+ * @property {number} [pageSize=50] Server page size.
32
+ * @property {ViewSort|null} [sort=null] Initial server sort.
33
+ * @property {Record<string, unknown>} [filters] Initial ZeyOS predicates.
34
+ * @property {string} [search=''] Initial full-text query.
35
+ * @property {string[]} [searchFields] Server full-text fields where supported.
36
+ * @property {Record<string, unknown>} [viewOptions] Options passed to the concrete view.
37
+ * @property {Element|string|null} [target=null] Concrete view target.
38
+ * @property {string} [titleField] Card title field included in the projection.
39
+ * @property {string} [subtitleField] Card subtitle field included in the projection.
40
+ * @property {string|Function} [preview] Preview field or resolver.
41
+ * @property {string|Function} [groupBy] Card grouping field or resolver.
42
+ * @property {string|Function} [columnBy] Kanban column field or resolver.
43
+ * @property {string|Function} [swimlaneBy] Kanban swim-lane field or resolver.
44
+ * @property {false|Record<string,any>|ZeyosSavedViewsOptions|null} [savedViews] Existing registry or
45
+ * injected transport/scope configuration for named saved views.
46
+ * @property {(record:Record<string,unknown>,event:CustomEvent<Record<string,unknown>>)=>void} [onRecordClick]
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} ZeyosViewConfig
51
+ * @property {Record<string, any>[]} fieldMeta Ordered normalized schema metadata.
52
+ * @property {ViewField[]} fields Common view fields.
53
+ * @property {Record<string, string>} projection Complete server projection.
54
+ * @property {Record<string, any>} viewOptions Concrete view options.
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} ZeyosViewLoadOptions
59
+ * @property {string} [search] Replacement full-text query.
60
+ * @property {Record<string, unknown>} [filters] Replacement predicates.
61
+ * @property {number} [page=0] Zero-based page; pages above zero append.
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} ZeyosViewLoadResult
66
+ * @property {Record<string, unknown>[]} data Loaded records.
67
+ * @property {number|null} count Server count when supplied.
68
+ * @property {number} page Loaded page.
69
+ * @property {boolean} hasMore Whether another page is available.
70
+ * @property {Record<string, unknown>} query Request sent to the list operation.
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} ZeyosSavedViewCapture
75
+ * @property {string} [id] Existing saved-view id.
76
+ * @property {string} name Saved-view name.
77
+ * @property {'table'|'card'|'kanban'} type Concrete presentation.
78
+ * @property {Record<string,unknown>} state JSON-safe component view state.
79
+ * @property {Record<string,unknown>} filters Current server filters.
80
+ * @property {string} search Current full-text query.
81
+ */
82
+
83
+ /**
84
+ * @typedef {Object} ZeyosSaveViewOptions
85
+ * @property {string} [id] Existing saved-view id to replace.
86
+ * @property {string} name Saved-view name.
87
+ * @property {'reject'|'replace'} [onDuplicate] Duplicate-name behavior.
88
+ * @property {boolean} [setDefault=false] Make the saved entry the default.
89
+ */
90
+
91
+ /**
92
+ * @typedef {Object} ZeyosApplyViewOptions
93
+ * @property {boolean} [load=true] Reload the first server page after an applied view.
94
+ */
95
+
96
+ /**
97
+ * @typedef {Object} ZeyosViewBinding
98
+ * @property {Record<string, any>} view Generated RecordView-compatible instance.
99
+ * @property {(options?:ZeyosViewLoadOptions)=>Promise<ZeyosViewLoadResult>} load Loads a page.
100
+ * @property {(search:string)=>Promise<ZeyosViewLoadResult>} setSearch Replaces search and reloads.
101
+ * @property {(filters:Record<string,unknown>)=>Promise<ZeyosViewLoadResult>} setFilters Replaces filters and reloads.
102
+ * @property {()=>Promise<ZeyosViewLoadResult>} loadMore Loads the next page when available.
103
+ * @property {number|null} count Current server count.
104
+ * @property {number} page Current zero-based page.
105
+ * @property {boolean} hasMore Whether another page is available.
106
+ * @property {Record<string,any>|null} savedViews Saved-view registry when configured.
107
+ * @property {Promise<SavedViewApplyResult|null>} ready Default restoration completed before loads.
108
+ * @property {(options?:ZeyosApplyViewOptions)=>Promise<SavedViewApplyResult|null>} restoreDefault Restores the current default.
109
+ * @property {(details:{id?:string,name:string})=>ZeyosSavedViewCapture} captureView Captures current query/view state without persistence.
110
+ * @property {(details:ZeyosSaveViewOptions)=>Promise<SavedViewEntry>} saveView Captures and persists the current view.
111
+ * @property {(idOrName:string,options?:ZeyosApplyViewOptions)=>Promise<SavedViewApplyResult>} applyView Applies a compatible named view.
112
+ * @property {()=>void} destroy Destroys the generated view.
113
+ */
114
+
115
+ /** View-specific top-level conveniences copied into the concrete component options. */
116
+ const VIEW_KEYS = [
117
+ 'titleField', 'subtitleField', 'preview', 'previewAlt', 'link', 'actions', 'groupBy', 'groupOrder',
118
+ 'columnBy', 'columns', 'swimlaneBy', 'swimlanes', 'moveMode', 'columnOrder', 'swimlaneOrder',
119
+ 'collapsedColumns', 'collapsedSwimlanes', 'showCounts', 'showEmptyColumns', 'hiddenFields',
120
+ 'fieldOrder', 'fieldControls', 'selectable', 'selection', 'emptyText'
121
+ ];
122
+
123
+ /**
124
+ * Produces common view fields and a complete ZeyOS projection without touching the DOM. It reuses
125
+ * the established table schema conversion so entity labels, enums, amounts, currencies, and Unix
126
+ * dates render identically in TableView, CardView, and KanbanView.
127
+ * @param {Record<string, any>} client Injected ZeyOS client.
128
+ * @param {string} resource Schema resource.
129
+ * @param {ZeyosViewOptions} [opts={}] Schema and view options.
130
+ * @returns {ZeyosViewConfig} DOM-free configuration.
131
+ */
132
+ export function buildZeyosViewConfig(client, resource, opts = {}) {
133
+ const requested = requestedViewFields(client, resource, opts);
134
+ const table = buildZeyosTableConfig(client, resource, {
135
+ fields: requested,
136
+ labels: opts.labels,
137
+ rowId: opts.recordId ?? 'ID',
138
+ sort: opts.sort ?? null
139
+ });
140
+ const overrides = opts.fieldOverrides && typeof opts.fieldOverrides === 'object'
141
+ ? opts.fieldOverrides : {};
142
+ const explicitlyVisible = Array.isArray(opts.fields) ? new Set(opts.fields.map(String)) : null;
143
+ const fields = table.columns.map((column) => ({
144
+ ...column,
145
+ ...(explicitlyVisible && !explicitlyVisible.has(column.id) ? { visible: false } : {}),
146
+ ...(overrides[column.id] ?? {}),
147
+ id: column.id
148
+ }));
149
+ const projection = { ...table.projection };
150
+
151
+ // A view resolver may name a schema field outside the visible field allow-list. The server still
152
+ // has to return it for grouping/media. Direct projections are safe even for compatible client
153
+ // schemas whose field metadata omitted a virtual field.
154
+ for (const id of supplementalFieldIds(opts)) {
155
+ if (!Object.prototype.hasOwnProperty.call(projection, id)) projection[id] = id;
156
+ }
157
+ const viewOptions = { ...(opts.viewOptions ?? {}) };
158
+ for (const key of VIEW_KEYS) {
159
+ if (Object.prototype.hasOwnProperty.call(opts, key)) viewOptions[key] = opts[key];
160
+ }
161
+ Object.assign(viewOptions, {
162
+ fields,
163
+ data: [],
164
+ recordId: opts.recordId ?? 'ID',
165
+ sort: opts.sort ?? null,
166
+ sortMode: 'server'
167
+ });
168
+ if (typeof opts.onRecordClick === 'function') {
169
+ viewOptions.onrecordclick = (event) => opts.onRecordClick(event.detail.record, event);
170
+ }
171
+ return { fieldMeta: table.fieldMeta, fields, projection, viewOptions };
172
+ }
173
+
174
+ /**
175
+ * Creates and binds any RecordView-compatible constructor to server paging. The caller supplies the
176
+ * constructor, so this adapter has no dependency on one concrete presentation and remains simple
177
+ * to unit test or wrap from a framework.
178
+ * @param {Record<string, any>} client Injected ZeyOS client.
179
+ * @param {string} resource Schema resource.
180
+ * @param {new(target:Element|string|null,options:Record<string,unknown>)=>Record<string,any>} View Concrete view class.
181
+ * @param {ZeyosViewOptions} [opts={}] Binding options.
182
+ * @returns {ZeyosViewBinding} View and server-state methods.
183
+ */
184
+ export function zeyosView(client, resource, View, opts = {}) {
185
+ if (typeof View !== 'function') throw new TypeError('zeyosView() requires a view constructor');
186
+ const config = buildZeyosViewConfig(client, resource, opts);
187
+ const view = new View(opts.target ?? null, config.viewOptions);
188
+ for (const method of ['setData', 'addData', 'setLoading', 'on', 'destroy']) {
189
+ if (typeof view?.[method] !== 'function') {
190
+ try { view?.destroy?.(); } catch { /* Preserve the contract error. */ }
191
+ throw new TypeError(`zeyosView() constructor must provide ${method}()`);
192
+ }
193
+ }
194
+ const facade = connect(client);
195
+ const pageSize = normalizePageSize(opts.pageSize);
196
+ let saved;
197
+ try {
198
+ saved = resolveSavedViews(opts.savedViews, View, resource);
199
+ } catch (error) {
200
+ view.destroy();
201
+ throw error;
202
+ }
203
+ const savedViews = saved?.registry ?? null;
204
+ const presentation = saved?.type ?? presentationType(View);
205
+ if (savedViews) {
206
+ for (const method of ['getViewState', 'setViewState']) {
207
+ if (typeof view?.[method] !== 'function') {
208
+ if (saved?.owned) savedViews.destroy?.();
209
+ view.destroy();
210
+ throw new TypeError(`zeyosView() saved views require ${method}()`);
211
+ }
212
+ }
213
+ }
214
+ let currentSort = opts.sort ?? null;
215
+ let currentFilters = copyFilters(opts.filters ?? {});
216
+ let currentSearch = String(opts.search ?? '');
217
+ let currentPage = 0;
218
+ let totalCount = null;
219
+ let more = false;
220
+ let requestSequence = 0;
221
+ let destroyed = false;
222
+
223
+ const performLoad = async (options = {}) => {
224
+ if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
225
+ if (Object.prototype.hasOwnProperty.call(options, 'search')) currentSearch = String(options.search ?? '');
226
+ if (Object.prototype.hasOwnProperty.call(options, 'filters')) currentFilters = copyFilters(options.filters ?? {});
227
+ const page = normalizePage(options.page ?? 0);
228
+ const query = buildListQuery({
229
+ fields: config.projection,
230
+ sort: currentSort,
231
+ filters: currentFilters,
232
+ search: currentSearch,
233
+ searchFields: opts.searchFields,
234
+ limit: pageSize,
235
+ offset: page * pageSize,
236
+ count: true
237
+ });
238
+ const sequence = ++requestSequence;
239
+ view.setLoading(true);
240
+ try {
241
+ const normalized = normalizeZeyosListResult(client, await facade.list(resource, query));
242
+ const count = normalized.count ?? null;
243
+ const hasMore = count == null
244
+ ? normalized.data.length === pageSize
245
+ : page * pageSize + normalized.data.length < count;
246
+ if (!destroyed && sequence === requestSequence) {
247
+ currentPage = page;
248
+ totalCount = count;
249
+ more = hasMore;
250
+ if (page === 0) view.setData(normalized.data);
251
+ else view.addData(normalized.data);
252
+ }
253
+ return { data: normalized.data, count, page, hasMore, query };
254
+ } finally {
255
+ if (!destroyed && sequence === requestSequence) view.setLoading(false);
256
+ }
257
+ };
258
+
259
+ /** @param {string} idOrName @param {{load?:boolean}} [options={}] @returns {Promise<SavedViewApplyResult>} */
260
+ const applySaved = async (idOrName, options = {}) => {
261
+ if (!savedViews || !presentation) throw new Error('This ZeyOS view binding has no saved-view registry');
262
+ if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
263
+ const result = await savedViews.apply(idOrName, view, {
264
+ type: presentation,
265
+ onQuery: async ({ filters, search }) => {
266
+ if (destroyed) return;
267
+ currentFilters = copyFilters(filters ?? {});
268
+ currentSearch = String(search ?? '');
269
+ }
270
+ });
271
+ if (!result.applied || destroyed) return result;
272
+ currentSort = appliedViewSort(view, result.entry);
273
+ currentPage = 0;
274
+ totalCount = null;
275
+ more = false;
276
+ if (options.load !== false) await performLoad({ page: 0 });
277
+ return result;
278
+ };
279
+
280
+ /** @param {{load?:boolean}} [options={}] @returns {Promise<SavedViewApplyResult|null>} */
281
+ const restoreDefaultInternal = async (options = {}) => {
282
+ if (!savedViews) return null;
283
+ await readySavedViews(savedViews);
284
+ if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
285
+ const entry = await savedViews.getDefault();
286
+ if (!entry) return /** @type {SavedViewApplyResult} */ ({
287
+ status: 'missing', applied: false, entry: null
288
+ });
289
+ return applySaved(entry.id, options);
290
+ };
291
+
292
+ /** Default application starts immediately and every server load below awaits it. */
293
+ const ready = savedViews ? restoreDefaultInternal({ load: false }) : Promise.resolve(null);
294
+ let initialized = !savedViews;
295
+ let retryingInitialization = null;
296
+ // The promise remains rejectable to callers/load(), while this observer prevents an ignored
297
+ // optional saved-view configuration from becoming a process-level unhandled rejection.
298
+ void ready.then(() => { initialized = true; }, () => {});
299
+
300
+ /**
301
+ * Later operations retry a failed initial registry/default restoration instead of remaining
302
+ * poisoned by the intentionally observable first `ready` rejection.
303
+ * @param {{load?:boolean}} [options={load:false}] Retry apply behavior.
304
+ * @returns {Promise<SavedViewApplyResult|null>}
305
+ */
306
+ const ensureInitialized = async (options = { load: false }) => {
307
+ if (initialized) return null;
308
+ try {
309
+ const result = await ready;
310
+ initialized = true;
311
+ return result;
312
+ } catch {
313
+ // The first failure remains available through binding.ready; this operation gets a retry.
314
+ }
315
+ if (!retryingInitialization) {
316
+ const retry = restoreDefaultInternal(options).then((result) => {
317
+ initialized = true;
318
+ return result;
319
+ });
320
+ const pending = retry.finally(() => {
321
+ if (retryingInitialization === pending) retryingInitialization = null;
322
+ });
323
+ retryingInitialization = pending;
324
+ }
325
+ return retryingInitialization;
326
+ };
327
+
328
+ const load = async (options = {}) => {
329
+ await ensureInitialized({ load: false });
330
+ return performLoad(options);
331
+ };
332
+
333
+ const captureView = (details) => {
334
+ if (!presentation || typeof view.getViewState !== 'function') {
335
+ throw new Error('This view does not expose serializable saved-view state');
336
+ }
337
+ const source = details && typeof details === 'object' ? details : {};
338
+ return {
339
+ ...(source.id == null ? {} : { id: String(source.id) }),
340
+ name: String(source.name ?? ''),
341
+ type: presentation,
342
+ state: view.getViewState(),
343
+ filters: copyFilters(currentFilters),
344
+ search: currentSearch
345
+ };
346
+ };
347
+
348
+ const saveView = async (details) => {
349
+ if (!savedViews || !presentation) throw new Error('This ZeyOS view binding has no saved-view registry');
350
+ await ensureInitialized({ load: false });
351
+ if (destroyed) throw new Error('ZeyOS view binding has been destroyed');
352
+ const capture = captureView(details);
353
+ const entry = await savedViews.capture(view, {
354
+ ...(capture.id == null ? {} : { id: capture.id }),
355
+ name: capture.name,
356
+ type: presentation,
357
+ filters: capture.filters,
358
+ search: capture.search
359
+ }, { onDuplicate: details?.onDuplicate, setDefault: Boolean(details?.setDefault) });
360
+ return entry;
361
+ };
362
+
363
+ const applyView = async (idOrName, options = {}) => {
364
+ await ensureInitialized({ load: false });
365
+ return applySaved(idOrName, options);
366
+ };
367
+
368
+ const restoreDefault = async (options = {}) => {
369
+ if (!initialized) return ensureInitialized(options);
370
+ return restoreDefaultInternal(options);
371
+ };
372
+
373
+ const sortHandler = (event) => {
374
+ const id = event.detail?.id;
375
+ currentSort = id == null ? null : { id: String(id), dir: event.detail.dir === 'desc' ? 'desc' : 'asc' };
376
+ void load({ page: 0 }).catch(() => { /* connect() already reported the API failure. */ });
377
+ };
378
+
379
+ const binding = {
380
+ view,
381
+ load,
382
+ setSearch: (search) => load({ search, page: 0 }),
383
+ setFilters: (filters) => load({ filters, page: 0 }),
384
+ loadMore: async () => {
385
+ await ensureInitialized({ load: false });
386
+ return more ? performLoad({ page: currentPage + 1 }) : {
387
+ data: [],
388
+ count: totalCount,
389
+ page: currentPage,
390
+ hasMore: false,
391
+ query: buildListQuery({
392
+ fields: config.projection,
393
+ sort: currentSort,
394
+ filters: currentFilters,
395
+ search: currentSearch,
396
+ searchFields: opts.searchFields,
397
+ limit: pageSize,
398
+ offset: currentPage * pageSize,
399
+ count: true
400
+ })
401
+ };
402
+ },
403
+ get count() { return totalCount; },
404
+ get page() { return currentPage; },
405
+ get hasMore() { return more; },
406
+ savedViews,
407
+ ready,
408
+ restoreDefault,
409
+ captureView,
410
+ saveView,
411
+ applyView,
412
+ destroy: () => {
413
+ if (destroyed) return;
414
+ destroyed = true;
415
+ requestSequence += 1;
416
+ if (typeof view.off === 'function') view.off('sortchange', sortHandler);
417
+ else view.removeEventListener?.('sortchange', sortHandler);
418
+ if (saved?.owned) savedViews?.destroy?.();
419
+ view.destroy();
420
+ }
421
+ };
422
+ view.on('sortchange', sortHandler);
423
+ return binding;
424
+ }
425
+
426
+ /** @param {Record<string,any>} client @param {string} resource @param {ZeyosViewOptions} opts @returns {string[]|undefined} */
427
+ function requestedViewFields(client, resource, opts) {
428
+ if (!Array.isArray(opts.fields) && !Array.isArray(opts.exclude)) return undefined;
429
+ const excluded = new Set(Array.isArray(opts.exclude) ? opts.exclude.map(String) : []);
430
+ const listed = Array.isArray(opts.fields) ? opts.fields.map(String) : schemaFieldIds(client, resource);
431
+ return [...new Set([...listed, ...supplementalFieldIds(opts)])]
432
+ .filter((id) => !excluded.has(id));
433
+ }
434
+
435
+ /** @param {Record<string,any>} client @param {string} resource @returns {string[]} */
436
+ function schemaFieldIds(client, resource) {
437
+ const collection = client?.schema?.fields?.(resource);
438
+ if (Array.isArray(collection)) return collection.map((entry) => typeof entry === 'string'
439
+ ? entry : String(entry?.id ?? entry?.name ?? entry?.field ?? '')).filter(Boolean);
440
+ return collection && typeof collection === 'object' ? Object.keys(collection) : [];
441
+ }
442
+
443
+ /** @param {ZeyosViewOptions} opts @returns {string[]} */
444
+ function supplementalFieldIds(opts) {
445
+ const ids = [];
446
+ for (const key of ['titleField', 'subtitleField', 'preview', 'groupBy', 'columnBy', 'swimlaneBy']) {
447
+ const value = opts[key];
448
+ if (typeof value === 'string' && value.trim()) ids.push(value.trim());
449
+ }
450
+ return [...new Set(ids)];
451
+ }
452
+
453
+ /** @param {unknown} value @returns {number} */
454
+ function normalizePageSize(value) {
455
+ const number = Number(value ?? 50);
456
+ return Number.isFinite(number) ? Math.max(1, Math.floor(number)) : 50;
457
+ }
458
+
459
+ /** @param {unknown} value @returns {number} */
460
+ function normalizePage(value) {
461
+ const number = Number(value);
462
+ return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0;
463
+ }
464
+
465
+ /** @param {unknown} value @returns {Record<string, unknown>} */
466
+ function copyFilters(value) {
467
+ return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
468
+ }
469
+
470
+ /**
471
+ * Resolves a supplied registry or constructs one from an injected transport and scope.
472
+ * @param {ZeyosViewOptions['savedViews']} value Saved-view option.
473
+ * @param {Function & {cssName?:string}} View Concrete view constructor.
474
+ * @param {string} resource ZeyOS resource.
475
+ * @returns {{registry:Record<string,any>,type:'table'|'card'|'kanban',owned:boolean}|null}
476
+ */
477
+ function resolveSavedViews(value, View, resource) {
478
+ if (value == null || value === false) return null;
479
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
480
+ throw new TypeError('savedViews must be a registry or transport/scope configuration');
481
+ }
482
+ const source = /** @type {Record<string,any>} */ (value);
483
+ const configured = source.controller ?? source.registry;
484
+ const direct = isSavedViewRegistry(source) ? source : isSavedViewRegistry(configured) ? configured : null;
485
+ const type = normalizePresentationType(source.type ?? presentationType(View));
486
+ if (!type) throw new TypeError('savedViews requires a table, card, or kanban presentation type');
487
+ if (direct) return { registry: direct, type, owned: false };
488
+ if (!source.transport || !source.scope || typeof source.scope !== 'object') {
489
+ throw new TypeError('savedViews requires a controller or injected transport and scope');
490
+ }
491
+ const scope = { ...source.scope, resource: source.scope.resource ?? resource };
492
+ return {
493
+ registry: createSavedViewRegistry(source.transport, scope, source.options ?? {}),
494
+ type,
495
+ owned: true
496
+ };
497
+ }
498
+
499
+ /** @param {unknown} value @returns {boolean} */
500
+ function isSavedViewRegistry(value) {
501
+ return Boolean(value && typeof value === 'object'
502
+ && typeof /** @type {Record<string,any>} */ (value).apply === 'function'
503
+ && typeof /** @type {Record<string,any>} */ (value).capture === 'function'
504
+ && typeof /** @type {Record<string,any>} */ (value).getDefault === 'function');
505
+ }
506
+
507
+ /** @param {Function & {cssName?:string}} View @returns {'table'|'card'|'kanban'|null} */
508
+ function presentationType(View) {
509
+ const cssName = String(View.cssName ?? '').toLowerCase().replace(/-view$/, '');
510
+ if (['table', 'card', 'kanban'].includes(cssName)) {
511
+ return /** @type {'table'|'card'|'kanban'} */ (cssName);
512
+ }
513
+ const name = String(View.name ?? '').toLowerCase().replace(/view$/, '');
514
+ return ['table', 'card', 'kanban'].includes(name)
515
+ ? /** @type {'table'|'card'|'kanban'} */ (name) : null;
516
+ }
517
+
518
+ /** @param {unknown} value @returns {'table'|'card'|'kanban'|null} */
519
+ function normalizePresentationType(value) {
520
+ const type = String(value ?? '').toLowerCase();
521
+ return ['table', 'card', 'kanban'].includes(type)
522
+ ? /** @type {'table'|'card'|'kanban'} */ (type) : null;
523
+ }
524
+
525
+ /** @param {Record<string,any>} registry @returns {Promise<void>} */
526
+ async function readySavedViews(registry) {
527
+ if (typeof registry.ready === 'function') await registry.ready();
528
+ else if (typeof registry.load === 'function') await registry.load();
529
+ }
530
+
531
+ /**
532
+ * Reads the sort after `setViewState()` so unknown saved fields rejected by RecordView do not leak
533
+ * into the server query. The entry is only a fallback for compatible facade implementations.
534
+ * @param {Record<string,any>} view Applied view.
535
+ * @param {SavedViewEntry|null|undefined} entry Saved entry.
536
+ * @returns {ViewSort|null}
537
+ */
538
+ function appliedViewSort(view, entry) {
539
+ const sort = typeof view.getSort === 'function' ? view.getSort()
540
+ : typeof view.getViewState === 'function' ? view.getViewState()?.sort : entry?.state?.sort;
541
+ if (!sort || typeof sort !== 'object' || Array.isArray(sort) || sort.id == null) return null;
542
+ return { id: String(sort.id), dir: sort.dir === 'desc' ? 'desc' : 'asc' };
543
+ }
package/styles/icons.css CHANGED
@@ -19,16 +19,9 @@ svg.zx-icon {
19
19
  vertical-align: middle;
20
20
  }
21
21
 
22
- /* ZeyOS module icon chip: the module glyph on its identity colour. `moduleChip()` sets the three
23
- custom properties per module; the glyph is whichever ZeyOS foreground contrasts better. */
22
+ /* ZeyOS identity hook. `moduleChip()` now delegates vessel geometry and material entirely to
23
+ AppIcon; this class only preserves the module variables for host styling and older selectors. */
24
24
  .zx-module-icon {
25
- display: inline-grid;
26
- place-items: center;
27
25
  flex: none;
28
- inline-size: var(--zx-module-icon-size, 1.5rem);
29
- block-size: var(--zx-module-icon-size, 1.5rem);
30
- border-radius: var(--zx-radius-sm);
31
- background: var(--zx-module-color, var(--zx-module-default));
32
- color: var(--zx-module-glyph, var(--zx-module-fg-light));
33
- font-size: calc(var(--zx-module-icon-size, 1.5rem) * 0.6);
26
+ color: var(--zx-module-glyph, var(--zx-color-app-icon-glyph));
34
27
  }
@@ -27,6 +27,8 @@
27
27
  --zx-color-text-muted: var(--zx-gray-500);
28
28
  --zx-color-text-placeholder: var(--zx-gray-400);
29
29
  --zx-color-text-invert: var(--zx-gray-50);
30
+ --zx-color-app-icon-glyph: var(--zx-gray-0);
31
+ --zx-color-app-icon-shade: var(--zx-gray-950);
30
32
  --zx-color-accent: var(--zx-accent-600);
31
33
  --zx-color-accent-hover: var(--zx-accent-700);
32
34
  --zx-color-accent-active: var(--zx-accent-700);
@@ -53,12 +55,82 @@
53
55
  --zx-color-glass-border: color-mix(in srgb, var(--zx-gray-0) 24%, transparent);
54
56
  --zx-color-glass-highlight: color-mix(in srgb, var(--zx-gray-0) 24%, transparent);
55
57
  --zx-color-glass-highlight-strong: color-mix(in srgb, var(--zx-gray-0) 38%, transparent);
58
+ --zx-color-app-icon-depth: color-mix(in srgb, var(--zx-color-app-icon-shade) 26%, transparent);
59
+ /* AppIcon is a dark chromatic lens: colour lives in its rim and low bloom, not a glossy fill. */
60
+ /* Strengths remain inherited, while each AppIcon mixes them with its own module colour. */
61
+ --zx-app-icon-core-strength: 52%;
62
+ --zx-app-icon-rim-strength: 58%;
63
+ --zx-app-icon-bloom-strength: 64%;
64
+ --zx-app-icon-specular-strength: 72%;
65
+ --zx-app-icon-rim-base: var(--zx-color-glass-highlight);
66
+ --zx-app-icon-halo-size: 14px;
67
+ --zx-app-icon-halo-strength: 22%;
56
68
  --zx-color-glass-surface: color-mix(in srgb, var(--zx-color-bg-raised) 78%, transparent);
69
+ /* Persistent application chrome is quieter and more opaque than a raised feature card. */
70
+ --zx-color-glass-chrome: color-mix(in srgb, var(--zx-color-bg-surface) 90%, transparent);
71
+ --zx-color-glass-control: color-mix(in srgb, var(--zx-color-bg-control) 88%, transparent);
72
+ --zx-color-glass-control-hover: color-mix(in srgb, var(--zx-color-bg-hover) 76%, var(--zx-color-glass-surface));
73
+ /* Compact overlays can show more of the page; reading-sized panels stay quieter and more opaque. */
74
+ --zx-color-overlay-surface: color-mix(in srgb, var(--zx-color-bg-raised) 88%, transparent);
75
+ --zx-color-overlay-panel: color-mix(in srgb, var(--zx-color-bg-raised) 94%, transparent);
76
+ --zx-color-overlay-border: color-mix(in srgb, var(--zx-color-border-strong) 72%, var(--zx-color-glass-highlight));
57
77
  --zx-glass-blur: 8px;
58
78
  --zx-glass-saturation: 130%;
59
79
  --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);
80
+ /* Material filters are roles, not component recipes. Aurora remains a separate canvas layer. */
81
+ --zx-glass-filter-icon: blur(4px) saturate(118%);
82
+ --zx-glass-filter-control: blur(6px) saturate(116%);
83
+ --zx-glass-filter-raised: blur(8px) saturate(120%);
84
+ --zx-glass-filter-chrome: blur(10px) saturate(122%);
85
+ --zx-glass-filter-overlay: blur(12px) saturate(128%);
86
+ --zx-glass-filter-panel: blur(9px) saturate(120%);
87
+ --zx-app-icon-shadow: inset 0 1px 0 color-mix(in srgb, var(--zx-gray-0) 14%, transparent), inset 0 -1px 0 color-mix(in srgb, var(--zx-gray-950) 28%, transparent), 0 2px 6px color-mix(in srgb, var(--zx-color-app-icon-shade) 24%, transparent);
88
+ --zx-app-icon-shadow-hover: inset 0 1px 0 color-mix(in srgb, var(--zx-gray-0) 20%, transparent), inset 0 -1px 0 color-mix(in srgb, var(--zx-gray-950) 32%, transparent), 0 4px 10px color-mix(in srgb, var(--zx-color-app-icon-shade) 30%, transparent);
89
+ --zx-glass-control-shadow: inset 0 1px 0 color-mix(in srgb, var(--zx-gray-0) 18%, transparent), var(--zx-shadow-1);
90
+ --zx-glass-chrome-shadow: inset 0 1px 0 color-mix(in srgb, var(--zx-gray-0) 16%, transparent), var(--zx-shadow-1);
91
+ --zx-glass-raised-shadow: inset 0 1px 0 color-mix(in srgb, var(--zx-gray-0) 18%, transparent), var(--zx-overlay-shadow);
62
92
  --zx-header-bg: var(--zx-gray-900);
63
93
  --zx-header-text: var(--zx-gray-50);
64
94
  }
95
+
96
+ /* Preferences are resolved at the token seam so every material consumer, including application
97
+ chrome outside a component stylesheet, receives the same deterministic opaque fallback. */
98
+ @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
99
+ :root,
100
+ [data-zx-theme] {
101
+ --zx-color-glass-chrome: var(--zx-color-bg-surface) !important;
102
+ --zx-color-glass-surface: var(--zx-color-bg-raised) !important;
103
+ --zx-color-glass-control: var(--zx-color-bg-control) !important;
104
+ --zx-color-glass-control-hover: var(--zx-color-bg-hover) !important;
105
+ --zx-color-overlay-surface: var(--zx-color-bg-raised) !important;
106
+ --zx-color-overlay-panel: var(--zx-color-bg-raised) !important;
107
+ --zx-app-icon-core-strength: 68% !important;
108
+ --zx-app-icon-rim-strength: 62% !important;
109
+ --zx-app-icon-bloom-strength: 0% !important;
110
+ --zx-app-icon-specular-strength: 0% !important;
111
+ --zx-app-icon-rim-base: var(--zx-color-border) !important;
112
+ --zx-app-icon-halo-strength: 0% !important;
113
+ --zx-glass-filter-icon: none !important;
114
+ --zx-glass-filter-control: none !important;
115
+ --zx-glass-filter-raised: none !important;
116
+ --zx-glass-filter-chrome: none !important;
117
+ --zx-glass-filter-overlay: none !important;
118
+ --zx-glass-filter-panel: none !important;
119
+ }
120
+ }
121
+
122
+ @media (forced-colors: active) {
123
+ :root,
124
+ [data-zx-theme] {
125
+ --zx-color-glass-chrome: Canvas !important;
126
+ --zx-color-glass-surface: Canvas !important;
127
+ --zx-color-glass-control: ButtonFace !important;
128
+ --zx-color-glass-control-hover: Highlight !important;
129
+ --zx-color-overlay-surface: Canvas !important;
130
+ --zx-color-overlay-panel: Canvas !important;
131
+ --zx-color-overlay-border: CanvasText !important;
132
+ --zx-glass-control-shadow: none !important;
133
+ --zx-glass-chrome-shadow: none !important;
134
+ --zx-glass-raised-shadow: none !important;
135
+ }
136
+ }