@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,392 @@
1
+ // @ts-check
2
+ import { h, safeHref } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { uid } from '../../core/util.js';
5
+ import { readViewField, renderViewField } from '../view/record-view.js';
6
+
7
+ /** @typedef {Record<string, any>} ViewRecord */
8
+ /** @typedef {import('../view/record-view.js').ViewField} ViewField */
9
+ /** @typedef {string|((record:ViewRecord,index:number)=>unknown)|null} RecordCardValueSource */
10
+
11
+ /**
12
+ * @typedef {Object} RecordCardPreviewDescriptor
13
+ * @property {string} src Image URL.
14
+ * @property {string} [alt] Image alternative text.
15
+ * @property {'cover'|'contain'|'fill'|'none'|'scale-down'} [fit='cover'] Image fitting behavior.
16
+ */
17
+
18
+ /** @typedef {string|Node|RecordCardPreviewDescriptor|null} RecordCardPreview */
19
+
20
+ /**
21
+ * @typedef {Object} RecordCardLinkDescriptor
22
+ * @property {string} href Native link destination.
23
+ * @property {string} [target] Native browsing context.
24
+ * @property {string} [rel] Native link relationship.
25
+ */
26
+
27
+ /** @typedef {string|RecordCardLinkDescriptor|null} RecordCardLink */
28
+
29
+ /**
30
+ * @typedef {Object} RecordCardAction
31
+ * @property {string} id Stable delegated-action identifier.
32
+ * @property {string} [label=''] Visible action label.
33
+ * @property {string|null} [icon=null] Optional Zx icon name.
34
+ * @property {string} [title] Accessible/native title, especially for an icon-only action.
35
+ * @property {string} [href] Optional safe native link destination; otherwise a button is rendered.
36
+ * @property {string} [target] Native link browsing context.
37
+ * @property {string} [rel] Native link relationship.
38
+ * @property {boolean} [disabled=false] Whether a button action is unavailable.
39
+ * @property {(record:ViewRecord,index:number,event:MouseEvent)=>void} [onclick]
40
+ * Callback invoked by an owning view's delegated listener; this helper never installs listeners.
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} RecordCardOptions
45
+ * @property {ViewField[]} [fields=[]] Complete fields, used to resolve title and subtitle sources.
46
+ * @property {ViewField[]} [visibleFields=fields] Ordered fields rendered as labelled metadata.
47
+ * @property {RecordCardValueSource} [titleField=null] Field id or explicit value resolver.
48
+ * @property {RecordCardValueSource} [subtitleField=null] Field id or explicit value resolver.
49
+ * @property {string|((record:ViewRecord,index:number)=>RecordCardPreview)} [preview]
50
+ * Preview field id or explicit resolver.
51
+ * @property {string|((record:ViewRecord,index:number)=>unknown)|null} [previewAlt=null]
52
+ * Alternative-text field, literal text, or resolver.
53
+ * @property {RecordCardLink|((record:ViewRecord,index:number)=>RecordCardLink)} [link]
54
+ * @property {RecordCardAction[]|((record:ViewRecord,index:number)=>RecordCardAction[])} [actions=[]]
55
+ * @property {false|'single'|'multi'} [selectable=false] Selection presentation.
56
+ * @property {boolean} [selected=false] Current selection state.
57
+ * @property {'outlined'|'raised'|'filled'} [variant='outlined'] Surface treatment.
58
+ * @property {1|2|3|4|5|6} [headingLevel=3] Card title heading level.
59
+ * @property {Node[]} [controls=[]] Listener-free, owner-created controls such as a move handle.
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} ResolvedRecordPreview
64
+ * @property {Node|null} node Explicit preview node.
65
+ * @property {string|null} src Safe image URL, or null when rejected.
66
+ * @property {string} alt Alternative text.
67
+ * @property {'cover'|'contain'|'fill'|'none'|'scale-down'} fit Image fitting behavior.
68
+ * @property {boolean} rejected Whether a supplied URL was unsafe or malformed.
69
+ */
70
+
71
+ /**
72
+ * Builds the listener-free card anatomy shared by CardView and KanbanView. All primitive content
73
+ * becomes text, URLs are normalized before reaching native attributes, and actions expose stable
74
+ * data attributes for a component-owned delegated listener.
75
+ *
76
+ * The returned `<li>` belongs in a semantic `<ul>`/`<ol>`. When selectable it carries
77
+ * `aria-selected`; multi-selection adds a native checkbox. It deliberately retains list-item
78
+ * semantics rather than becoming an ARIA option because cards may contain native links and
79
+ * buttons, which the listbox pattern does not permit inside an option. Preview images are paired
80
+ * with a stable fallback. The owning component listens for image `error` and marks the fallback.
81
+ *
82
+ * @param {ViewRecord} record Record to render.
83
+ * @param {number} index Current display index.
84
+ * @param {RecordCardOptions} [options={}] Rendering options.
85
+ * @returns {HTMLLIElement} Semantic record card list item.
86
+ */
87
+ export function createRecordCard(record, index, options = {}) {
88
+ const fields = Array.isArray(options.fields) ? options.fields : [];
89
+ const visibleFields = Array.isArray(options.visibleFields) ? options.visibleFields : fields;
90
+ const titleSource = options.titleField ?? fields[0]?.id ?? null;
91
+ const subtitleSource = options.subtitleField ?? null;
92
+ const title = resolveRecordCardContent(record, index, titleSource, fields);
93
+ const subtitle = resolveRecordCardContent(record, index, subtitleSource, fields);
94
+ const preview = resolveRecordPreview(record, index, options.preview, options.previewAlt, fields);
95
+ const link = resolveRecordLink(record, index, options.link);
96
+ const actions = resolveRecordActions(record, index, options.actions);
97
+ const controls = normalizeControls(options.controls);
98
+ const selectable = normalizeSelectable(options.selectable);
99
+ const selected = Boolean(options.selected);
100
+ const headingId = uid('zx-record-card-title');
101
+ const headingLevel = normalizeHeadingLevel(options.headingLevel);
102
+
103
+ const headingContent = link
104
+ ? h('a', {
105
+ class: 'zx-record-card__primary',
106
+ href: link.href,
107
+ target: link.target,
108
+ rel: link.rel
109
+ }, contentNode(title))
110
+ : h('span', { class: 'zx-record-card__title' }, contentNode(title));
111
+ const heading = h(`h${headingLevel}`, {
112
+ class: 'zx-record-card__heading',
113
+ id: headingId
114
+ }, headingContent);
115
+ const selection = selectable === 'multi' ? h('label', {
116
+ class: 'zx-record-card__selection'
117
+ }, h('input', {
118
+ type: 'checkbox',
119
+ checked: selected,
120
+ dataset: { recordSelection: '' },
121
+ ariaLabel: `Select ${textAlternative(title) || 'record'}`
122
+ })) : null;
123
+ const actionNodes = actions.map(createAction);
124
+ const actionArea = actionNodes.length || controls.length ? h('div', {
125
+ class: 'zx-record-card__actions',
126
+ role: 'group',
127
+ ariaLabel: `Actions for ${textAlternative(title) || 'record'}`
128
+ }, controls, actionNodes) : null;
129
+ const header = h('header', { class: 'zx-record-card__header' }, selection,
130
+ h('div', { class: 'zx-record-card__titles' }, heading,
131
+ isEmptyContent(subtitle) ? null : h('p', { class: 'zx-record-card__subtitle' }, contentNode(subtitle))),
132
+ actionArea);
133
+ const metadata = createMetadata(record, index, visibleFields, titleSource, subtitleSource);
134
+ const body = h('div', { class: 'zx-record-card__body' }, header, metadata);
135
+ const card = /** @type {HTMLLIElement} */ (h('li', {
136
+ class: 'zx-record-card',
137
+ tabindex: 0,
138
+ ariaSelected: selectable ? String(selected) : undefined,
139
+ ariaLabelledby: headingId,
140
+ dataset: {
141
+ recordIndex: String(index),
142
+ variant: normalizeVariant(options.variant),
143
+ selected: String(selected)
144
+ }
145
+ }, preview ? createPreview(preview) : null, body));
146
+ return card;
147
+ }
148
+
149
+ /**
150
+ * Resolves a preview without touching the DOM. Unsafe, malformed, and non-image URL schemes are
151
+ * rejected into the same descriptor shape so callers can still render the stable fallback.
152
+ * @param {ViewRecord} record Record.
153
+ * @param {number} index Display index.
154
+ * @param {RecordCardOptions['preview']} preview Preview field or resolver.
155
+ * @param {RecordCardOptions['previewAlt']} previewAlt Alt field, literal, or resolver.
156
+ * @param {ViewField[]} [fields=[]] Complete fields.
157
+ * @returns {ResolvedRecordPreview|null} Resolved preview.
158
+ */
159
+ export function resolveRecordPreview(record, index, preview, previewAlt, fields = []) {
160
+ if (preview == null) return null;
161
+ const candidate = typeof preview === 'function'
162
+ ? preview(record, index)
163
+ : readRecordSource(record, index, preview, fields, false);
164
+ if (candidate == null || candidate === '') return null;
165
+ const explicitAlt = resolvePreviewAlt(record, index, previewAlt, fields);
166
+ if (isNode(candidate)) {
167
+ return { node: candidate, src: null, alt: explicitAlt, fit: 'cover', rejected: false };
168
+ }
169
+ const descriptor = typeof candidate === 'object' && !Array.isArray(candidate)
170
+ ? /** @type {RecordCardPreviewDescriptor} */ (candidate)
171
+ : { src: String(candidate) };
172
+ const source = typeof descriptor.src === 'string' ? descriptor.src : '';
173
+ const src = safePreviewUrl(source);
174
+ return {
175
+ node: null,
176
+ src,
177
+ alt: descriptor.alt == null ? explicitAlt : String(descriptor.alt),
178
+ fit: normalizePreviewFit(descriptor.fit),
179
+ rejected: src === null
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Resolves and validates a native title link without touching the DOM.
185
+ * @param {ViewRecord} record Record.
186
+ * @param {number} index Display index.
187
+ * @param {RecordCardOptions['link']} link Link value or resolver.
188
+ * @returns {RecordCardLinkDescriptor|null} Safe normalized link.
189
+ */
190
+ export function resolveRecordLink(record, index, link) {
191
+ const candidate = typeof link === 'function' ? link(record, index) : link;
192
+ if (candidate == null) return null;
193
+ const descriptor = typeof candidate === 'string' ? { href: candidate } : candidate;
194
+ if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) return null;
195
+ const href = safeHref(descriptor.href);
196
+ if (href === null) return null;
197
+ const target = descriptor.target == null ? undefined : String(descriptor.target);
198
+ const rel = safeRel(descriptor.rel, target);
199
+ return { href, ...(target ? { target } : {}), ...(rel ? { rel } : {}) };
200
+ }
201
+
202
+ /**
203
+ * Resolves and clones delegated action descriptors. Invalid entries are omitted rather than
204
+ * creating unnamed or ambiguously interactive controls.
205
+ * @param {ViewRecord} record Record.
206
+ * @param {number} index Display index.
207
+ * @param {RecordCardOptions['actions']} actions Actions or resolver.
208
+ * @returns {RecordCardAction[]} Normalized action copies.
209
+ */
210
+ export function resolveRecordActions(record, index, actions) {
211
+ const candidate = typeof actions === 'function' ? actions(record, index) : actions;
212
+ if (!Array.isArray(candidate)) return [];
213
+ return candidate.flatMap((action) => {
214
+ if (!action || typeof action !== 'object' || Array.isArray(action)) return [];
215
+ const id = String(action.id ?? '').trim();
216
+ const label = String(action.label ?? '');
217
+ const title = action.title == null ? '' : String(action.title);
218
+ if (!id || (!label && !title)) return [];
219
+ if (action.href != null && safeHref(action.href) === null) return [];
220
+ return [{ ...action, id, label, ...(title ? { title } : {}) }];
221
+ });
222
+ }
223
+
224
+ /** @param {ResolvedRecordPreview} preview @returns {HTMLElement} */
225
+ function createPreview(preview) {
226
+ const fallback = h('span', {
227
+ class: 'zx-record-card__preview-fallback',
228
+ ariaHidden: 'true'
229
+ }, icon('file', { size: 24 }));
230
+ if (preview.node) {
231
+ return h('div', { class: 'zx-record-card__preview' }, preview.node);
232
+ }
233
+ const image = preview.src ? h('img', {
234
+ class: 'zx-record-card__preview-image',
235
+ src: preview.src,
236
+ alt: preview.alt,
237
+ loading: 'lazy',
238
+ decoding: 'async',
239
+ dataset: { recordPreview: '' },
240
+ style: { objectFit: preview.fit }
241
+ }) : null;
242
+ return h('div', {
243
+ class: 'zx-record-card__preview',
244
+ dataset: { failed: String(preview.rejected) }
245
+ }, fallback, image);
246
+ }
247
+
248
+ /**
249
+ * @param {ViewRecord} record
250
+ * @param {number} index
251
+ * @param {ViewField[]} fields
252
+ * @param {RecordCardValueSource} titleSource
253
+ * @param {RecordCardValueSource} subtitleSource
254
+ * @returns {HTMLElement|null}
255
+ */
256
+ function createMetadata(record, index, fields, titleSource, subtitleSource) {
257
+ const titleId = typeof titleSource === 'string' ? titleSource : null;
258
+ const subtitleId = typeof subtitleSource === 'string' ? subtitleSource : null;
259
+ const visible = fields.filter((field) => field.duplicate || field.id !== titleId && field.id !== subtitleId);
260
+ if (visible.length === 0) return null;
261
+ return h('dl', { class: 'zx-record-card__metadata' }, visible.map((field) => h('div', {
262
+ class: 'zx-record-card__field',
263
+ dataset: { fieldId: field.id }
264
+ }, h('dt', { class: 'zx-record-card__label' }, field.label),
265
+ h('dd', { class: 'zx-record-card__value' }, contentNode(renderViewField(field, record, index))))));
266
+ }
267
+
268
+ /** @param {RecordCardAction} action @returns {HTMLElement} */
269
+ function createAction(action) {
270
+ const label = String(action.label ?? '');
271
+ const title = String(action.title ?? (label || action.id));
272
+ const children = [action.icon ? icon(String(action.icon), { size: 14 }) : null,
273
+ label ? h('span', { class: 'zx-record-card__action-label' }, label) : null];
274
+ const href = action.href == null ? null : safeHref(action.href);
275
+ if (href && !action.disabled) {
276
+ const target = action.target == null ? undefined : String(action.target);
277
+ return h('a', {
278
+ class: 'zx-record-card__action',
279
+ href,
280
+ target,
281
+ rel: safeRel(action.rel, target),
282
+ title,
283
+ ariaLabel: label || title,
284
+ dataset: { recordAction: action.id }
285
+ }, children);
286
+ }
287
+ return h('button', {
288
+ class: 'zx-record-card__action',
289
+ type: 'button',
290
+ disabled: Boolean(action.disabled),
291
+ title,
292
+ ariaLabel: label || title,
293
+ dataset: { recordAction: action.id }
294
+ }, children);
295
+ }
296
+
297
+ /** @param {ViewRecord} record @param {number} index @param {RecordCardValueSource} source @param {ViewField[]} fields @returns {unknown} */
298
+ function resolveRecordCardContent(record, index, source, fields) {
299
+ if (source == null) return '';
300
+ if (typeof source === 'function') return source(record, index);
301
+ const field = fields.find((candidate) => candidate.id === source);
302
+ return field ? renderViewField(field, record, index) : record?.[source] ?? '';
303
+ }
304
+
305
+ /**
306
+ * @param {ViewRecord} record @param {number} index @param {string} source @param {ViewField[]} fields
307
+ * @param {boolean} literalFallback @returns {unknown}
308
+ */
309
+ function readRecordSource(record, index, source, fields, literalFallback) {
310
+ const field = fields.find((candidate) => candidate.id === source);
311
+ if (field) return readViewField(field, record, index);
312
+ if (Object.prototype.hasOwnProperty.call(record, source)) return record[source];
313
+ return literalFallback ? source : null;
314
+ }
315
+
316
+ /** @param {ViewRecord} record @param {number} index @param {RecordCardOptions['previewAlt']} source @param {ViewField[]} fields @returns {string} */
317
+ function resolvePreviewAlt(record, index, source, fields) {
318
+ if (source == null) return '';
319
+ const value = typeof source === 'function'
320
+ ? source(record, index)
321
+ : readRecordSource(record, index, source, fields, true);
322
+ return value == null ? '' : String(value);
323
+ }
324
+
325
+ /** @param {unknown} content @returns {Node|string} */
326
+ function contentNode(content) {
327
+ return isNode(content) ? content : content == null ? '' : String(content);
328
+ }
329
+
330
+ /** @param {unknown} content @returns {boolean} */
331
+ function isEmptyContent(content) {
332
+ return content == null || typeof content !== 'object' && String(content) === '';
333
+ }
334
+
335
+ /** @param {unknown} content @returns {string} */
336
+ function textAlternative(content) {
337
+ if (isNode(content)) return content.textContent?.trim() ?? '';
338
+ return content == null ? '' : String(content).trim();
339
+ }
340
+
341
+ /** @param {unknown} value @returns {value is Node} */
342
+ function isNode(value) {
343
+ return Boolean(value && typeof value === 'object'
344
+ && typeof /** @type {{nodeType?:unknown}} */ (value).nodeType === 'number');
345
+ }
346
+
347
+ /** @param {unknown} controls @returns {Node[]} */
348
+ function normalizeControls(controls) {
349
+ return Array.isArray(controls) ? controls.filter(isNode) : [];
350
+ }
351
+
352
+ /** @param {unknown} value @returns {string|null} */
353
+ function safePreviewUrl(value) {
354
+ const href = safeHref(value);
355
+ if (!href) return null;
356
+ try {
357
+ const base = globalThis.location?.href ?? 'https://zx.invalid/';
358
+ const protocol = new URL(href, base).protocol.toLowerCase();
359
+ return ['http:', 'https:', 'blob:', 'file:'].includes(protocol) ? href : null;
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+
365
+ /** @param {unknown} rel @param {string|undefined} target @returns {string|undefined} */
366
+ function safeRel(rel, target) {
367
+ const tokens = String(rel ?? '').split(/\s+/).filter((token) => /^[a-z-]+$/i.test(token));
368
+ if (target === '_blank' && !tokens.includes('noopener')) tokens.push('noopener');
369
+ return tokens.length ? [...new Set(tokens)].join(' ') : undefined;
370
+ }
371
+
372
+ /** @param {unknown} value @returns {false|'single'|'multi'} */
373
+ function normalizeSelectable(value) {
374
+ return value === 'single' || value === 'multi' ? value : false;
375
+ }
376
+
377
+ /** @param {unknown} value @returns {'outlined'|'raised'|'filled'} */
378
+ function normalizeVariant(value) {
379
+ return value === 'raised' || value === 'filled' ? value : 'outlined';
380
+ }
381
+
382
+ /** @param {unknown} value @returns {1|2|3|4|5|6} */
383
+ function normalizeHeadingLevel(value) {
384
+ const number = Number(value ?? 3);
385
+ return /** @type {1|2|3|4|5|6} */ (Number.isInteger(number) && number >= 1 && number <= 6 ? number : 3);
386
+ }
387
+
388
+ /** @param {unknown} value @returns {'cover'|'contain'|'fill'|'none'|'scale-down'} */
389
+ function normalizePreviewFit(value) {
390
+ return ['contain', 'fill', 'none', 'scale-down'].includes(String(value))
391
+ ? /** @type {'contain'|'fill'|'none'|'scale-down'} */ (value) : 'cover';
392
+ }
@@ -21,15 +21,15 @@
21
21
  gap: var(--zx-space-2);
22
22
  inline-size: 100%;
23
23
  min-block-size: var(--zx-control-height);
24
- border-color: var(--zx-color-bg-raised);
25
- background: var(--zx-color-bg-raised);
24
+ border-color: transparent;
25
+ background: transparent;
26
26
  padding-inline: var(--zx-control-pad-x);
27
27
  text-align: start;
28
28
  }
29
29
 
30
30
  .zx-context-menu__item:hover,
31
31
  .zx-context-menu__item:focus-visible {
32
- background: var(--zx-color-bg-hover);
32
+ background: var(--zx-color-overlay-hover);
33
33
  }
34
34
 
35
35
  .zx-context-menu__item[data-danger="true"] {
@@ -50,5 +50,5 @@
50
50
  .zx-context-menu__separator {
51
51
  block-size: 1px;
52
52
  margin: var(--zx-space-1) var(--zx-space-2);
53
- background: var(--zx-color-border);
53
+ background: var(--zx-color-overlay-divider);
54
54
  }
@@ -82,7 +82,11 @@ export class ContextMenu extends Component {
82
82
  this.#target = resolved;
83
83
  document.body.append(this.el);
84
84
 
85
- this.#dropdown = new Dropdown(this.el, { openOn: 'manual', placement: 'bottom-start' });
85
+ this.#dropdown = new Dropdown(this.el, {
86
+ openOn: 'manual',
87
+ placement: 'bottom-start',
88
+ scope: resolved
89
+ });
86
90
  this.#panel = this.#dropdown.getPanel();
87
91
  this.#panel.classList.add('zx-context-menu');
88
92
  this.#panel.setAttribute('role', 'menu');
@@ -31,14 +31,14 @@
31
31
  place-items: center;
32
32
  min-inline-size: var(--zx-control-height);
33
33
  padding: 0;
34
- border-color: var(--zx-color-bg-surface);
35
- background: inherit;
34
+ border-color: transparent;
35
+ background: transparent;
36
36
  }
37
37
 
38
38
  .zx-date-picker__heading {
39
39
  overflow: hidden;
40
- border-color: var(--zx-color-bg-surface);
41
- background: inherit;
40
+ border-color: transparent;
41
+ background: transparent;
42
42
  font-weight: 650;
43
43
  text-overflow: ellipsis;
44
44
  text-wrap: nowrap;
@@ -76,9 +76,9 @@
76
76
  min-inline-size: 0;
77
77
  min-block-size: var(--zx-control-height);
78
78
  padding: 0;
79
- border-color: var(--zx-color-bg-surface);
79
+ border-color: transparent;
80
80
  border-radius: var(--zx-radius-md);
81
- background: inherit;
81
+ background: transparent;
82
82
  font-variant-numeric: tabular-nums;
83
83
  }
84
84
 
@@ -123,13 +123,27 @@
123
123
  position: absolute;
124
124
  z-index: 2;
125
125
  inset: var(--zx-space-3);
126
- border: 1px solid var(--zx-color-border-strong);
126
+ border: 1px solid var(--zx-color-overlay-border);
127
127
  border-radius: var(--zx-radius-md);
128
- background: var(--zx-color-bg-raised);
129
- box-shadow: var(--zx-overlay-shadow);
128
+ background-color: var(--zx-color-overlay-surface);
129
+ background-image: var(--zx-overlay-surface-image);
130
+ background-clip: padding-box;
131
+ -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
132
+ backdrop-filter: var(--zx-glass-filter-overlay);
133
+ box-shadow: var(--zx-glass-overlay-shadow);
130
134
  padding: var(--zx-space-3);
131
135
  }
132
136
 
137
+ @media (prefers-reduced-transparency: reduce) {
138
+ .zx-date-picker__quick {
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-date-picker__quick[hidden] {
134
148
  display: none;
135
149
  }
@@ -145,11 +159,16 @@
145
159
  .zx-month-picker__month {
146
160
  min-inline-size: 0;
147
161
  padding-inline: var(--zx-space-1);
148
- border-color: var(--zx-color-bg-surface);
149
- background: inherit;
162
+ border-color: transparent;
163
+ background: transparent;
164
+ }
165
+
166
+ .zx-date-picker__quick-month[aria-selected="true"] {
167
+ background: var(--zx-color-overlay-selected);
168
+ color: var(--zx-color-accent);
169
+ font-weight: 700;
150
170
  }
151
171
 
152
- .zx-date-picker__quick-month[aria-selected="true"],
153
172
  .zx-month-picker__month[aria-selected="true"] {
154
173
  background: var(--zx-color-bg-selected);
155
174
  color: var(--zx-color-accent);
@@ -123,6 +123,21 @@
123
123
 
124
124
  .zx-date-range-box__popover .zx-date-range-picker__surface {
125
125
  max-inline-size: none;
126
- background: var(--zx-color-bg-raised);
127
- box-shadow: var(--zx-overlay-shadow);
126
+ border-color: var(--zx-color-overlay-border);
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);
133
+ }
134
+
135
+ @media (prefers-reduced-transparency: reduce) {
136
+ .zx-date-range-box__popover .zx-date-range-picker__surface {
137
+ border-color: var(--zx-color-border);
138
+ background: var(--zx-color-bg-raised);
139
+ -webkit-backdrop-filter: none;
140
+ backdrop-filter: none;
141
+ box-shadow: var(--zx-overlay-shadow);
142
+ }
128
143
  }
@@ -90,7 +90,7 @@
90
90
  overflow: visible;
91
91
  max-inline-size: calc(100vw - var(--zx-space-4));
92
92
  border: 0;
93
- background: var(--zx-color-bg-raised);
93
+ background: none;
94
94
  padding: 0;
95
95
  }
96
96
 
@@ -103,6 +103,21 @@
103
103
  }
104
104
 
105
105
  .zx-datebox__popover .zx-date-picker__surface {
106
- background: var(--zx-color-bg-raised);
107
- box-shadow: var(--zx-overlay-shadow);
106
+ border-color: var(--zx-color-overlay-border);
107
+ background-color: var(--zx-color-overlay-surface);
108
+ background-image: var(--zx-overlay-surface-image);
109
+ background-clip: padding-box;
110
+ -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
111
+ backdrop-filter: var(--zx-glass-filter-overlay);
112
+ box-shadow: var(--zx-glass-overlay-shadow);
113
+ }
114
+
115
+ @media (prefers-reduced-transparency: reduce) {
116
+ .zx-datebox__popover .zx-date-picker__surface {
117
+ border-color: var(--zx-color-border);
118
+ background: var(--zx-color-bg-raised);
119
+ -webkit-backdrop-filter: none;
120
+ backdrop-filter: none;
121
+ box-shadow: var(--zx-overlay-shadow);
122
+ }
108
123
  }
@@ -3,7 +3,8 @@
3
3
  padding: 0;
4
4
  }
5
5
 
6
- .zx-dialog[open] {
6
+ .zx-dialog[open],
7
+ .zx-dialog:popover-open {
7
8
  display: grid;
8
9
  }
9
10
 
@@ -17,7 +18,7 @@
17
18
 
18
19
  .zx-dialog__header {
19
20
  justify-content: space-between;
20
- border-block-end: 1px solid var(--zx-color-border);
21
+ border-block-end: 1px solid var(--zx-color-overlay-divider);
21
22
  }
22
23
 
23
24
  .zx-dialog__title {
@@ -40,7 +41,7 @@
40
41
 
41
42
  .zx-dialog__footer {
42
43
  justify-content: flex-end;
43
- border-block-start: 1px solid var(--zx-color-border);
44
+ border-block-start: 1px solid var(--zx-color-overlay-divider);
44
45
  }
45
46
 
46
47
  .zx-dialog__footer[hidden] {
@@ -22,6 +22,7 @@ const FOCUSABLE_SELECTOR = [
22
22
  * @property {DialogButton[]} [buttons=[]] Footer buttons.
23
23
  * @property {boolean} [closable=true] Whether Escape and the header button may close the dialog.
24
24
  * @property {Node|string|number|{toElement: () => Node|null}|null} [content=null] Body content.
25
+ * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the overlay; defaults to the opener.
25
26
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
26
27
  * @property {(event: CustomEvent<{result: unknown}>) => void} [onclose] Close event listener.
27
28
  * @property {(event: CustomEvent<Record<string, never>>) => void} [oncancel] Cancel event listener.
@@ -89,7 +90,7 @@ export class Dialog extends Modal {
89
90
  #opener = null;
90
91
 
91
92
  /**
92
- * Creates a structured dialog appended to `document.body`.
93
+ * Creates a structured dialog in the configured theme scope, or at document level when unscoped.
93
94
  * @param {Element|string|null} [_target=null] Ignored; Dialog always owns its root.
94
95
  * @param {DialogOptions} [options={}] Dialog options.
95
96
  */
@@ -2,14 +2,28 @@
2
2
  max-inline-size: calc(100vw - (2 * var(--zx-space-3)));
3
3
  max-block-size: calc(100vh - (2 * var(--zx-space-3)));
4
4
  overflow: auto;
5
- border: 1px solid var(--zx-color-border);
5
+ border: 1px solid var(--zx-color-overlay-border);
6
6
  border-radius: var(--zx-radius-md);
7
- background: var(--zx-color-bg-raised);
8
- box-shadow: var(--zx-overlay-shadow);
7
+ background-color: var(--zx-color-overlay-surface);
8
+ background-image: var(--zx-overlay-surface-image);
9
+ background-clip: padding-box;
10
+ -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
11
+ backdrop-filter: var(--zx-glass-filter-overlay);
12
+ box-shadow: var(--zx-glass-overlay-shadow);
9
13
  color: var(--zx-color-text);
10
14
  padding: var(--zx-space-3);
11
15
  }
12
16
 
17
+ @media (prefers-reduced-transparency: reduce) {
18
+ .zx-dropdown {
19
+ border-color: var(--zx-color-border);
20
+ background: var(--zx-color-bg-raised);
21
+ -webkit-backdrop-filter: none;
22
+ backdrop-filter: none;
23
+ box-shadow: var(--zx-overlay-shadow);
24
+ }
25
+ }
26
+
13
27
  @media (prefers-reduced-motion: no-preference) {
14
28
  .zx-dropdown:popover-open {
15
29
  animation: zx-dropdown-enter var(--zx-dur-fast) var(--zx-ease);