@zeyos/zx 4.1.1 → 4.3.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 (102) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/DESIGN-SYSTEM.md +33 -3
  3. package/README.md +22 -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 +517 -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/dialog/dialog.d.ts +6 -1
  11. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  12. package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
  13. package/dist/types/components/launcher/launcher.d.ts +5 -0
  14. package/dist/types/components/message/message.d.ts +9 -1
  15. package/dist/types/components/modal/modal.d.ts +44 -7
  16. package/dist/types/components/sheet/sheet.d.ts +22 -12
  17. package/dist/types/components/table/table.d.ts +120 -31
  18. package/dist/types/components/table-view/table-view.d.ts +192 -0
  19. package/dist/types/components/view/record-view.d.ts +464 -0
  20. package/dist/types/core/overlay-host.d.ts +12 -0
  21. package/dist/types/index.d.ts +7 -0
  22. package/dist/types/zeyos/calendar.d.ts +91 -0
  23. package/dist/types/zeyos/icons.d.ts +18 -2
  24. package/dist/types/zeyos/index.d.ts +4 -0
  25. package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
  26. package/dist/types/zeyos/saved-views.d.ts +331 -0
  27. package/dist/types/zeyos/view.d.ts +305 -0
  28. package/dist/zx-zeyos.esm.js +4742 -1470
  29. package/dist/zx-zeyos.esm.min.js +1 -1
  30. package/dist/zx.css +2536 -471
  31. package/dist/zx.esm.js +5193 -223
  32. package/dist/zx.esm.min.js +3 -3
  33. package/dist/zx.global.js +5193 -223
  34. package/dist/zx.global.min.js +3 -3
  35. package/dist/zx.min.css +1 -1
  36. package/docs/api.json +1729 -182
  37. package/docs/llms.md +236 -32
  38. package/docs/llms.txt +16 -6
  39. package/package.json +1 -1
  40. package/src/components/account-menu/account-menu.css +1 -1
  41. package/src/components/app-icon/app-icon.css +77 -51
  42. package/src/components/app-icon/app-icon.js +28 -4
  43. package/src/components/app-rail/app-rail.css +12 -3
  44. package/src/components/app-sidebar/app-sidebar.css +11 -2
  45. package/src/components/aurora/aurora.css +143 -0
  46. package/src/components/aurora/aurora.js +146 -0
  47. package/src/components/avatar/avatar.css +1 -1
  48. package/src/components/button/button.css +2 -2
  49. package/src/components/calendar/calendar-model.js +402 -0
  50. package/src/components/calendar/calendar.css +609 -0
  51. package/src/components/calendar/calendar.js +1619 -0
  52. package/src/components/card/card.css +2 -2
  53. package/src/components/card-view/card-view.css +389 -0
  54. package/src/components/card-view/card-view.js +527 -0
  55. package/src/components/card-view/record-card.js +392 -0
  56. package/src/components/context-menu/context-menu.css +4 -4
  57. package/src/components/context-menu/context-menu.js +5 -1
  58. package/src/components/date-picker/date-picker.css +31 -12
  59. package/src/components/datebox/date-range-box.css +17 -2
  60. package/src/components/datebox/datebox.css +18 -3
  61. package/src/components/dialog/dialog.css +4 -3
  62. package/src/components/dialog/dialog.js +2 -1
  63. package/src/components/dropdown/dropdown.css +17 -3
  64. package/src/components/dropdown/dropdown.js +7 -2
  65. package/src/components/kanban-view/kanban-view.css +216 -0
  66. package/src/components/kanban-view/kanban-view.js +1228 -0
  67. package/src/components/launcher/launcher.css +24 -9
  68. package/src/components/launcher/launcher.js +9 -2
  69. package/src/components/master-panel/master-panel.css +14 -3
  70. package/src/components/menu-button/menu-button.css +4 -4
  71. package/src/components/message/message.css +13 -21
  72. package/src/components/message/message.js +17 -11
  73. package/src/components/modal/modal.css +19 -5
  74. package/src/components/modal/modal.js +99 -25
  75. package/src/components/navigation-bar/navigation-bar.css +17 -5
  76. package/src/components/panel/panel.css +10 -2
  77. package/src/components/select/select.css +26 -8
  78. package/src/components/sheet/sheet.css +28 -7
  79. package/src/components/sheet/sheet.js +43 -23
  80. package/src/components/table/table.css +83 -5
  81. package/src/components/table/table.js +322 -37
  82. package/src/components/table-view/table-view.css +18 -0
  83. package/src/components/table-view/table-view.js +347 -0
  84. package/src/components/tag-picker/tag-picker.css +18 -4
  85. package/src/components/tooltip/tooltip.css +17 -3
  86. package/src/components/tooltip/tooltip.js +4 -1
  87. package/src/components/view/record-view.css +114 -0
  88. package/src/components/view/record-view.js +770 -0
  89. package/src/core/overlay-host.js +23 -0
  90. package/src/index.js +15 -0
  91. package/src/zeyos/calendar.js +115 -0
  92. package/src/zeyos/icons.js +11 -4
  93. package/src/zeyos/index.js +13 -0
  94. package/src/zeyos/launcher.js +2 -2
  95. package/src/zeyos/legacy-saved-views.js +155 -0
  96. package/src/zeyos/saved-views.js +815 -0
  97. package/src/zeyos/table.js +3 -1
  98. package/src/zeyos/view.js +543 -0
  99. package/styles/icons.css +3 -10
  100. package/styles/tokens/dark.css +4 -0
  101. package/styles/tokens/semantic.css +142 -3
  102. package/styles/zx.css +6 -0
@@ -0,0 +1,347 @@
1
+ // @ts-check
2
+ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
+ import { formatTableCell, Table } from '../table/table.js';
4
+ import { RecordView, readViewField } from '../view/record-view.js';
5
+
6
+ /** @typedef {Record<string, any>} TableViewRecord */
7
+ /** @typedef {import('../view/record-view.js').ViewField} ViewField */
8
+ /** @typedef {import('../view/record-view.js').ViewSort} ViewSort */
9
+ /** @typedef {import('../table/table.js').TableColumn} TableColumn */
10
+ /** @typedef {import('../table/table.js').TableOptions} TableOptions */
11
+
12
+ /**
13
+ * @typedef {Object} TableViewOptions
14
+ * @property {ViewField[]} [fields=[]] Shared ordered field descriptors. These win over `table.columns`.
15
+ * @property {TableViewRecord[]} [data=[]] Initial records. These win over `table.data`.
16
+ * @property {string|((record:TableViewRecord)=>unknown)} [recordId='ID'] Stable record id accessor.
17
+ * @property {ViewSort|null} [sort=null] Initial shared sort.
18
+ * @property {'local'|'server'} [sortMode='local'] Local or server-controlled sorting.
19
+ * @property {false|'single'|'multi'} [selectable=false] Shared selection behavior.
20
+ * @property {unknown[]} [selection=[]] Initially selected record ids.
21
+ * @property {string[]} [fieldOrder=[]] Preferred stable field order.
22
+ * @property {string[]} [hiddenFields=[]] Initially hidden field ids.
23
+ * @property {boolean} [fieldControls=true] Show the accessible visibility and reorder disclosure.
24
+ * @property {string|Node|(()=>string|Node)|null} [emptyText=null] Empty-result content.
25
+ * @property {TableOptions} [table={}] Advanced low-level Table options. Shared options win where
26
+ * their contracts overlap; all other options and callbacks are forwarded unchanged.
27
+ * @property {(event:CustomEvent<Record<string,unknown>>)=>void} [onrecordclick]
28
+ * @property {(event:CustomEvent<Record<string,unknown>>)=>void} [onrecorddblclick]
29
+ * @property {(event:CustomEvent<{records:TableViewRecord[]}> )=>void} [ondatachange]
30
+ * @property {(event:CustomEvent<{records:TableViewRecord[],ids:unknown[]}> )=>void} [onselectionchange]
31
+ * @property {(event:CustomEvent<{id:string|null,dir:'asc'|'desc'|null}>)=>void} [onsortchange]
32
+ * @property {(event:CustomEvent<{visible:string[],hidden:string[]}> )=>void} [onfieldvisibilitychange]
33
+ * @property {(event:CustomEvent<{order:string[]}> )=>void} [onfieldorderchange]
34
+ * @property {(event:CustomEvent<Record<string,unknown>>)=>void} [onstatechange]
35
+ */
36
+
37
+ /**
38
+ * Record-oriented table view. Shared record state stays in RecordView while the composed `table`
39
+ * instance retains Table's editing, hierarchy, growth, movement, responsive, and loading behavior.
40
+ * @fires TableView#recordclick
41
+ * @fires TableView#recorddblclick
42
+ * @fires TableView#datachange
43
+ * @fires TableView#selectionchange
44
+ * @fires TableView#sortchange
45
+ * @fires TableView#fieldvisibilitychange
46
+ * @fires TableView#fieldorderchange
47
+ * @fires TableView#statechange
48
+ * @extends {RecordView}
49
+ */
50
+ export class TableView extends RecordView {
51
+ static cssName = 'table-view';
52
+
53
+ /** @type {Readonly<TableViewOptions>} */
54
+ static defaults = { table: {} };
55
+
56
+ /**
57
+ * Creates or enhances a record table view.
58
+ * @param {Element|string|null} [target=null] Existing root, selector, or null.
59
+ * @param {TableViewOptions} [options={}] Shared and table-specific options.
60
+ */
61
+ constructor(target = null, options = {}) {
62
+ super(target, options);
63
+ }
64
+
65
+ /** @returns {HTMLElement} Table view root. */
66
+ render() {
67
+ const created = this.el === null;
68
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
69
+ this.el = root;
70
+ this._createdTableViewRoot = created;
71
+ this._tableViewOriginal = created ? null : snapshotTarget(root);
72
+ this._tableViewRestored = false;
73
+ this._tableSyncDepth = 0;
74
+ this._tableViewDestroyed = false;
75
+ this._tableHost = h('div', { class: 'zx-table-view__table' });
76
+ this._initRecordView(root);
77
+
78
+ try {
79
+ root.replaceChildren(this._tableHost);
80
+ const viewOptions = /** @type {Readonly<TableViewOptions>} */ (this.options);
81
+ const options = tableOptionsForView(viewOptions.table, {
82
+ fields: this.getFields(),
83
+ data: this.getData(),
84
+ recordId: this.options.recordId,
85
+ sort: this.getSort(),
86
+ sortMode: this.options.sortMode,
87
+ selectable: this.options.selectable,
88
+ hiddenFields: this.getHiddenFields(),
89
+ fieldControls: this.options.fieldControls,
90
+ emptyText: this.options.emptyText
91
+ });
92
+ this.table = new Table(this._tableHost, options);
93
+ // Seed selection before bridging events so construction stays silent like RecordView itself.
94
+ this.table.setSelection(this.getSelectionIds());
95
+ this._bridgeTableEvents();
96
+ return root;
97
+ } catch (error) {
98
+ this.table?.destroy();
99
+ if (!created) restoreTarget(root, this._tableViewOriginal);
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ /** Returns the composed Table for advanced methods and Table-specific events. @returns {Table} */
105
+ getTable() {
106
+ return this.table;
107
+ }
108
+
109
+ /** @protected @param {string} reason Shared refresh reason. @returns {void} */
110
+ _refreshView(reason) {
111
+ if (!this.table || this._tableSyncDepth > 0) return;
112
+ this._withTableSync(() => {
113
+ if (reason === 'fields' || reason === 'state') {
114
+ this.table.setColumns(fieldsToTableColumns(this.getFields()), { silent: true });
115
+ this._syncTableFieldState();
116
+ }
117
+ if (reason === 'data' || reason === 'fields' || reason === 'sort' || reason === 'state') {
118
+ this.table.setData(this.getData());
119
+ }
120
+ if (reason === 'sort' || reason === 'fields' || reason === 'state') {
121
+ const sort = this.getSort();
122
+ if (sort) this.table.setSort(sort.id, sort.dir, { silent: true });
123
+ else this.table.clearSort({ silent: true });
124
+ }
125
+ if (reason === 'selection' || reason === 'data' || reason === 'state') {
126
+ this.table.setSelection(this.getSelectionIds());
127
+ }
128
+ if (reason === 'loading') {
129
+ this.table.setLoading(/** @type {HTMLElement} */ (this.el).dataset.loading === 'true');
130
+ }
131
+ });
132
+ }
133
+
134
+ /** Destroys the inner Table and restores an enhanced target exactly once. @returns {void} */
135
+ destroy() {
136
+ if (this._tableViewDestroyed) return;
137
+ this._tableViewDestroyed = true;
138
+ this.table?.destroy();
139
+ if (!this._tableViewRestored && !this._createdTableViewRoot) {
140
+ restoreTarget(this.el, this._tableViewOriginal);
141
+ this._tableViewRestored = true;
142
+ }
143
+ super.destroy();
144
+ }
145
+
146
+ /** @private @param {()=>void} callback Guarded synchronization. @returns {void} */
147
+ _withTableSync(callback) {
148
+ this._tableSyncDepth += 1;
149
+ try {
150
+ callback();
151
+ } finally {
152
+ this._tableSyncDepth -= 1;
153
+ }
154
+ }
155
+
156
+ /** @private @returns {void} */
157
+ _syncTableFieldState() {
158
+ const hidden = new Set(this.getHiddenFields());
159
+ // Show before hiding so Table's one-visible-column invariant never blocks a valid transition.
160
+ for (const id of this.getFieldOrder()) {
161
+ if (!hidden.has(id)) this.table.setColumnVisible(id, true, { silent: true });
162
+ }
163
+ for (const id of this.getFieldOrder()) {
164
+ if (hidden.has(id)) this.table.setColumnVisible(id, false, { silent: true });
165
+ }
166
+ }
167
+
168
+ /** @private @returns {void} */
169
+ _bridgeTableEvents() {
170
+ // Table and RecordView intentionally share these event names but not their detail keys. Keep
171
+ // the inner DOM events available on the Table root without leaking a second, row-shaped event
172
+ // through the TableView root; the bridged record-shaped event is emitted there once below.
173
+ this.listen(this._tableHost, 'zx-datachange', (event) => event.stopPropagation());
174
+ this.listen(this._tableHost, 'zx-selectionchange', (event) => event.stopPropagation());
175
+ this.listen(this.table, 'rowclick', (event) => {
176
+ const detail = /** @type {CustomEvent<Record<string,unknown>>} */ (event).detail;
177
+ this.emit('recordclick', { ...detail, record: detail.row });
178
+ });
179
+ this.listen(this.table, 'rowdblclick', (event) => {
180
+ const detail = /** @type {CustomEvent<Record<string,unknown>>} */ (event).detail;
181
+ this.emit('recorddblclick', { ...detail, record: detail.row });
182
+ });
183
+ this.listen(this.table, 'sort', (event) => this._tableSortChanged(
184
+ /** @type {CustomEvent<{id:string|null,dir:'asc'|'desc'|null}>} */ (event).detail));
185
+ this.listen(this.table, 'selectionchange', (event) => this._tableSelectionChanged(
186
+ /** @type {CustomEvent<{rows:TableViewRecord[],ids:unknown[]}>} */ (event).detail));
187
+ this.listen(this.table, 'datachange', () => this._tableDataChanged());
188
+ this.listen(this.table, 'columnvisibilitychange', (event) => this._tableVisibilityChanged(
189
+ /** @type {CustomEvent<{visible:string[],hidden:string[]}>} */ (event).detail));
190
+ this.listen(this.table, 'columnorderchange', (event) => this._tableOrderChanged(
191
+ /** @type {CustomEvent<{order:string[]}>} */ (event).detail));
192
+ }
193
+
194
+ /** @private @param {{id:string|null,dir:'asc'|'desc'|null}} detail Sort detail. @returns {void} */
195
+ _tableSortChanged(detail) {
196
+ if (this._tableSyncDepth > 0) return;
197
+ const next = /** @type {ViewSort|null} */ (detail.id == null ? null
198
+ : { id: String(detail.id), dir: detail.dir === 'desc' ? 'desc' : 'asc' });
199
+ if (sameSort(next, this._viewSort)) return;
200
+ this._viewSort = next;
201
+ if (this.options.sortMode === 'local') this._viewData = this.table.getData();
202
+ this.emit('sortchange', next ? { ...next } : { id: null, dir: null });
203
+ this._emitTableViewState('sort');
204
+ }
205
+
206
+ /** @private @param {{rows:TableViewRecord[],ids:unknown[]}} detail Selection detail. @returns {void} */
207
+ _tableSelectionChanged(detail) {
208
+ if (this._tableSyncDepth > 0) return;
209
+ const next = new Set(detail.ids);
210
+ if (sameSet(next, this._viewSelected)) return;
211
+ this._viewSelected = next;
212
+ this._viewSelectionAnchor = detail.ids.at(-1) ?? null;
213
+ this._emitTableViewSelection();
214
+ }
215
+
216
+ /** @private @returns {void} */
217
+ _tableDataChanged() {
218
+ if (this._tableSyncDepth > 0) return;
219
+ this._viewData = this.table.getData();
220
+ const valid = new Set(this._viewData.map((record) => this._viewRecordId(record)));
221
+ const nextSelection = new Set([...this._viewSelected].filter((id) => valid.has(id)));
222
+ const selectionChanged = !sameSet(nextSelection, this._viewSelected);
223
+ this._viewSelected = nextSelection;
224
+ if (this._viewSelectionAnchor != null && !valid.has(this._viewSelectionAnchor)) {
225
+ this._viewSelectionAnchor = null;
226
+ }
227
+ if (selectionChanged) this._emitTableViewSelection();
228
+ this.emit('datachange', { records: this.getData() });
229
+ }
230
+
231
+ /** @private @param {{visible:string[],hidden:string[]}} detail Visibility detail. @returns {void} */
232
+ _tableVisibilityChanged(detail) {
233
+ if (this._tableSyncDepth > 0) return;
234
+ const allowed = new Set(this.getFieldOrder());
235
+ const next = new Set(detail.hidden.filter((id) => allowed.has(id)));
236
+ if (sameSet(next, this._viewHidden)) return;
237
+ this._viewHidden = next;
238
+ this.emit('fieldvisibilitychange', {
239
+ visible: this.getVisibleFields().map((field) => field.id), hidden: this.getHiddenFields()
240
+ });
241
+ this._emitTableViewState('fieldvisibility');
242
+ }
243
+
244
+ /** @private @param {{order:string[]}} detail Order detail. @returns {void} */
245
+ _tableOrderChanged(detail) {
246
+ if (this._tableSyncDepth > 0) return;
247
+ const allowed = new Set(this.getFieldOrder());
248
+ const next = [...new Set(detail.order.filter((id) => allowed.has(id))),
249
+ ...this.getFieldOrder().filter((id) => !detail.order.includes(id))];
250
+ if (sameArray(next, this._viewFieldOrder)) return;
251
+ this._viewFieldOrder = next;
252
+ this.emit('fieldorderchange', { order: this.getFieldOrder() });
253
+ this._emitTableViewState('fieldorder');
254
+ }
255
+
256
+ /** @private @returns {void} */
257
+ _emitTableViewSelection() {
258
+ this.emit('selectionchange', { records: this.getSelection(), ids: this.getSelectionIds() });
259
+ }
260
+
261
+ /** @private @param {string} reason State-change reason. @returns {void} */
262
+ _emitTableViewState(reason) {
263
+ this.emit('statechange', { state: this.getViewState(), reason });
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Maps common fields to Table columns without mutating descriptors. Shared get/render/sortValue
269
+ * semantics win; `field.view.table` may provide the remaining advanced TableColumn properties.
270
+ * @param {ViewField[]} fields Shared fields in display order.
271
+ * @returns {TableColumn[]} Table columns.
272
+ */
273
+ export function fieldsToTableColumns(fields) {
274
+ return fields.map((field) => {
275
+ const table = /** @type {Record<string,any>} */ (field.view && typeof field.view === 'object'
276
+ && field.view.table && typeof field.view.table === 'object'
277
+ ? field.view.table : {});
278
+ const column = /** @type {TableColumn & Record<string,unknown>} */ ({ ...field, ...table,
279
+ id: field.id, label: field.label, sortable: Boolean(field.sortable) });
280
+ delete column.get;
281
+ delete column.visible;
282
+ delete column.duplicate;
283
+ delete column.view;
284
+ if (typeof field.render === 'function') {
285
+ column.render = (record, index) => field.render(record, index, readViewField(field, record, index));
286
+ } else if (typeof table.render !== 'function' && typeof field.get === 'function') {
287
+ const formatter = { ...column };
288
+ delete formatter.render;
289
+ column.render = (record, index) => formatTableCell(formatter,
290
+ { ...record, [field.id]: readViewField(field, record, index) }, index);
291
+ }
292
+ if (typeof field.sortValue === 'function') {
293
+ column.sortValue = (record) => field.sortValue(record, 0);
294
+ } else if (typeof table.sortValue !== 'function' && typeof field.get === 'function') {
295
+ column.sortValue = (record) => readViewField(field, record, 0);
296
+ }
297
+ return column;
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Builds the composed Table configuration. Shared options deliberately overwrite overlapping
303
+ * low-level options; every advanced option remains untouched.
304
+ * @param {TableOptions|undefined} table Advanced Table options.
305
+ * @param {{fields:ViewField[],data:TableViewRecord[],recordId:string|((record:TableViewRecord)=>unknown),sort:ViewSort|null,sortMode:'local'|'server',selectable:false|'single'|'multi',hiddenFields:string[],fieldControls:boolean,emptyText:string|Node|(()=>string|Node)|null}} shared Shared state.
306
+ * @returns {TableOptions} Table configuration.
307
+ */
308
+ export function tableOptionsForView(table, shared) {
309
+ return {
310
+ ...(table && typeof table === 'object' ? table : {}),
311
+ columns: fieldsToTableColumns(shared.fields),
312
+ data: [...shared.data],
313
+ rowId: shared.recordId,
314
+ sort: shared.sort ? { ...shared.sort } : null,
315
+ sortMode: shared.sortMode,
316
+ selectable: shared.selectable,
317
+ hiddenColumns: [...shared.hiddenFields],
318
+ columnVisibility: Boolean(shared.fieldControls),
319
+ columnReorder: Boolean(shared.fieldControls),
320
+ emptyText: shared.emptyText
321
+ };
322
+ }
323
+
324
+ /** @event TableView#recordclick @type {CustomEvent<Record<string,unknown>>} */
325
+ /** @event TableView#recorddblclick @type {CustomEvent<Record<string,unknown>>} */
326
+ /** @event TableView#datachange @type {CustomEvent<{records:TableViewRecord[]}>} */
327
+ /** @event TableView#selectionchange @type {CustomEvent<{records:TableViewRecord[],ids:unknown[]}>} */
328
+ /** @event TableView#sortchange @type {CustomEvent<{id:string|null,dir:'asc'|'desc'|null}>} */
329
+ /** @event TableView#fieldvisibilitychange @type {CustomEvent<{visible:string[],hidden:string[]}>} */
330
+ /** @event TableView#fieldorderchange @type {CustomEvent<{order:string[]}>} */
331
+ /** @event TableView#statechange @type {CustomEvent<Record<string,unknown>>} */
332
+
333
+ /** @param {ViewSort|null} left @param {ViewSort|null} right @returns {boolean} */
334
+ function sameSort(left, right) {
335
+ return left === null && right === null
336
+ || Boolean(left && right && left.id === right.id && left.dir === right.dir);
337
+ }
338
+
339
+ /** @param {Set<unknown>} left @param {Set<unknown>} right @returns {boolean} */
340
+ function sameSet(left, right) {
341
+ return left.size === right.size && [...left].every((value) => right.has(value));
342
+ }
343
+
344
+ /** @param {unknown[]} left @param {unknown[]} right @returns {boolean} */
345
+ function sameArray(left, right) {
346
+ return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
347
+ }
@@ -122,14 +122,28 @@
122
122
  inline-size: max-content;
123
123
  max-inline-size: calc(100vw - var(--zx-space-4));
124
124
  overflow: hidden;
125
- border: 1px solid var(--zx-color-border-strong);
125
+ border: 1px solid var(--zx-color-overlay-border);
126
126
  border-radius: var(--zx-radius-md);
127
- background: var(--zx-color-bg-raised);
128
- box-shadow: var(--zx-overlay-shadow);
127
+ background-color: var(--zx-color-overlay-surface);
128
+ background-image: var(--zx-overlay-surface-image);
129
+ background-clip: padding-box;
130
+ -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
131
+ backdrop-filter: var(--zx-glass-filter-overlay);
132
+ box-shadow: var(--zx-glass-overlay-shadow);
129
133
  color: var(--zx-color-text);
130
134
  padding: var(--zx-space-1);
131
135
  }
132
136
 
137
+ @media (prefers-reduced-transparency: reduce) {
138
+ .zx-tag-picker__panel {
139
+ border-color: var(--zx-color-border-strong);
140
+ background: var(--zx-color-bg-raised);
141
+ -webkit-backdrop-filter: none;
142
+ backdrop-filter: none;
143
+ box-shadow: var(--zx-overlay-shadow);
144
+ }
145
+ }
146
+
133
147
  .zx-tag-picker__list {
134
148
  min-inline-size: 0;
135
149
  overflow-y: auto;
@@ -146,7 +160,7 @@
146
160
  }
147
161
 
148
162
  .zx-tag-picker__option[data-active] {
149
- background: var(--zx-color-bg-hover);
163
+ background: var(--zx-color-overlay-hover);
150
164
  }
151
165
 
152
166
  .zx-tag-picker__option[aria-selected="true"] {
@@ -6,10 +6,14 @@
6
6
 
7
7
  .zx-tooltip {
8
8
  max-inline-size: var(--zx-tooltip-max-inline-size, 260px);
9
- border: 1px solid var(--zx-color-border);
9
+ border: 1px solid var(--zx-color-overlay-border);
10
10
  border-radius: var(--zx-radius-md);
11
- background: var(--zx-color-bg-raised);
12
- box-shadow: var(--zx-overlay-shadow);
11
+ background-color: var(--zx-color-overlay-surface);
12
+ background-image: var(--zx-overlay-surface-image);
13
+ background-clip: padding-box;
14
+ -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
15
+ backdrop-filter: var(--zx-glass-filter-overlay);
16
+ box-shadow: var(--zx-glass-overlay-shadow);
13
17
  padding: var(--zx-space-1) var(--zx-space-2);
14
18
  color: var(--zx-color-text);
15
19
  font-size: var(--zx-text-sm);
@@ -22,6 +26,16 @@
22
26
  user-select: none;
23
27
  }
24
28
 
29
+ @media (prefers-reduced-transparency: reduce) {
30
+ .zx-tooltip {
31
+ border-color: var(--zx-color-border);
32
+ background: var(--zx-color-bg-raised);
33
+ -webkit-backdrop-filter: none;
34
+ backdrop-filter: none;
35
+ box-shadow: var(--zx-overlay-shadow);
36
+ }
37
+ }
38
+
25
39
  @media (prefers-reduced-motion: no-preference) {
26
40
  .zx-tooltip:popover-open {
27
41
  animation: zx-tooltip-enter var(--zx-dur-fast) var(--zx-ease);
@@ -1,5 +1,6 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h, resolveElement } from '../../core/dom.js';
3
+ import { overlayHost } from '../../core/overlay-host.js';
3
4
  import { position } from '../../core/position.js';
4
5
  import { uid } from '../../core/util.js';
5
6
 
@@ -95,6 +96,7 @@ export class Tooltip extends Component {
95
96
  super(null, options);
96
97
 
97
98
  this.#anchor = resolved;
99
+ overlayHost(resolved).append(this.el);
98
100
  this.#content = this.options.content;
99
101
  this.#disabled = Boolean(this.options.disabled);
100
102
  this.#describedBy = resolved.getAttribute('aria-describedby');
@@ -154,7 +156,6 @@ export class Tooltip extends Component {
154
156
  popover: 'manual'
155
157
  });
156
158
  panel.dataset.state = 'closed';
157
- document.body.append(panel);
158
159
  return panel;
159
160
  }
160
161
 
@@ -167,6 +168,8 @@ export class Tooltip extends Component {
167
168
  show() {
168
169
  if (this.#destroyed || this.#disabled || this.isOpen()) return this;
169
170
  this.#cancelTimers();
171
+ const host = overlayHost(this.#anchor);
172
+ if (this.el.parentElement !== host) host.append(this.el);
170
173
  const node = resolveContent(this.#content);
171
174
  if (node === null) return this;
172
175
 
@@ -0,0 +1,114 @@
1
+ .zx-record-view {
2
+ min-inline-size: 0;
3
+ color: var(--zx-color-text);
4
+ }
5
+
6
+ .zx-record-view__toolbar {
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: flex-end;
10
+ gap: var(--zx-space-2);
11
+ margin-block-end: var(--zx-space-2);
12
+ }
13
+
14
+ .zx-record-view__field-controls {
15
+ position: relative;
16
+ inline-size: max-content;
17
+ margin-inline-start: auto;
18
+ }
19
+
20
+ .zx-record-view__field-controls > summary {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--zx-space-1);
24
+ cursor: pointer;
25
+ list-style: none;
26
+ }
27
+
28
+ .zx-record-view__field-controls > summary::-webkit-details-marker {
29
+ display: none;
30
+ }
31
+
32
+ .zx-record-view__field-list {
33
+ position: absolute;
34
+ z-index: 4;
35
+ inset-block-start: calc(100% + var(--zx-space-1));
36
+ inset-inline-end: 0;
37
+ display: grid;
38
+ min-inline-size: 230px;
39
+ max-block-size: min(360px, 70vh);
40
+ overflow: auto;
41
+ border: 1px solid var(--zx-color-border-strong);
42
+ border-radius: var(--zx-radius-md);
43
+ background: var(--zx-color-bg-raised);
44
+ box-shadow: var(--zx-overlay-shadow);
45
+ padding: var(--zx-space-2);
46
+ }
47
+
48
+ .zx-record-view__field-option {
49
+ display: grid;
50
+ grid-template-columns: minmax(0, 1fr) auto;
51
+ align-items: center;
52
+ gap: var(--zx-space-2);
53
+ min-block-size: var(--zx-control-height);
54
+ }
55
+
56
+ .zx-record-view__field-option > label {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: var(--zx-space-2);
60
+ min-inline-size: 0;
61
+ }
62
+
63
+ .zx-record-view__field-option > label > span {
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .zx-record-view__field-order {
70
+ display: inline-flex;
71
+ }
72
+
73
+ .zx-record-view__field-move {
74
+ display: inline-grid;
75
+ place-items: center;
76
+ inline-size: 26px;
77
+ block-size: 26px;
78
+ border: 0;
79
+ border-radius: var(--zx-radius-sm);
80
+ background: transparent;
81
+ color: var(--zx-color-text-muted);
82
+ cursor: pointer;
83
+ }
84
+
85
+ .zx-record-view__field-move:hover:not(:disabled) {
86
+ background: var(--zx-color-bg-hover);
87
+ color: var(--zx-color-text);
88
+ }
89
+
90
+ .zx-record-view__field-move:focus-visible {
91
+ outline: none;
92
+ box-shadow: var(--zx-focus-ring);
93
+ }
94
+
95
+ .zx-record-view__field-move:disabled {
96
+ opacity: .35;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ .zx-record-view[data-loading="true"] {
101
+ cursor: progress;
102
+ }
103
+
104
+ .zx-record-view[data-loading="true"] > :not(.zx-record-view__toolbar) {
105
+ opacity: .62;
106
+ }
107
+
108
+ @media (prefers-reduced-motion: no-preference) {
109
+ .zx-record-view__field-move,
110
+ .zx-record-view[data-loading="true"] > :not(.zx-record-view__toolbar) {
111
+ transition: color var(--zx-dur-fast) var(--zx-ease),
112
+ background var(--zx-dur-fast) var(--zx-ease), opacity var(--zx-dur-fast) var(--zx-ease);
113
+ }
114
+ }