@zeyos/zx 4.2.0 → 4.3.4

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 (69) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/README.md +5 -3
  3. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  4. package/dist/types/components/calendar/calendar.d.ts +19 -2
  5. package/dist/types/components/card-view/card-view.d.ts +1 -1
  6. package/dist/types/components/card-view/record-card.d.ts +47 -5
  7. package/dist/types/components/dialog/dialog.d.ts +6 -1
  8. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  9. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  10. package/dist/types/components/kanban-view/kanban-view.d.ts +669 -24
  11. package/dist/types/components/launcher/launcher.d.ts +5 -0
  12. package/dist/types/components/message/message.d.ts +9 -1
  13. package/dist/types/components/modal/modal.d.ts +44 -7
  14. package/dist/types/components/sheet/sheet.d.ts +22 -12
  15. package/dist/types/core/component.d.ts +6 -1
  16. package/dist/types/core/overlay-host.d.ts +12 -0
  17. package/dist/types/index.d.ts +2 -1
  18. package/dist/zx-zeyos.esm.js +173 -43
  19. package/dist/zx-zeyos.esm.min.js +1 -1
  20. package/dist/zx.css +876 -230
  21. package/dist/zx.esm.js +2053 -338
  22. package/dist/zx.esm.min.js +3 -3
  23. package/dist/zx.global.js +2053 -338
  24. package/dist/zx.global.min.js +3 -3
  25. package/dist/zx.min.css +1 -1
  26. package/docs/api.json +250 -2
  27. package/docs/llms.md +67 -48
  28. package/docs/llms.txt +1 -3
  29. package/package.json +1 -2
  30. package/src/components/account-menu/account-menu.css +1 -1
  31. package/src/components/app-rail/app-rail.css +1 -1
  32. package/src/components/calendar/calendar-model.js +45 -0
  33. package/src/components/calendar/calendar.css +96 -34
  34. package/src/components/calendar/calendar.js +122 -29
  35. package/src/components/card-view/card-view.css +211 -9
  36. package/src/components/card-view/card-view.js +7 -7
  37. package/src/components/card-view/record-card.js +92 -19
  38. package/src/components/context-menu/context-menu.css +2 -2
  39. package/src/components/context-menu/context-menu.js +5 -1
  40. package/src/components/date-picker/date-picker.css +18 -11
  41. package/src/components/datebox/date-range-box.css +4 -2
  42. package/src/components/datebox/datebox.css +4 -2
  43. package/src/components/dialog/dialog.css +4 -3
  44. package/src/components/dialog/dialog.js +2 -1
  45. package/src/components/dropdown/dropdown.css +4 -2
  46. package/src/components/dropdown/dropdown.js +7 -2
  47. package/src/components/kanban-view/kanban-policy.js +412 -0
  48. package/src/components/kanban-view/kanban-view.css +480 -36
  49. package/src/components/kanban-view/kanban-view.js +1514 -235
  50. package/src/components/launcher/launcher.css +10 -8
  51. package/src/components/launcher/launcher.js +9 -2
  52. package/src/components/menu-button/menu-button.css +2 -2
  53. package/src/components/message/message.css +15 -25
  54. package/src/components/message/message.js +17 -11
  55. package/src/components/modal/modal.css +6 -4
  56. package/src/components/modal/modal.js +99 -25
  57. package/src/components/select/select.css +9 -7
  58. package/src/components/sheet/sheet.css +25 -7
  59. package/src/components/sheet/sheet.js +43 -23
  60. package/src/components/table/table.css +5 -3
  61. package/src/components/tag-picker/tag-picker.css +5 -3
  62. package/src/components/tooltip/tooltip.css +4 -2
  63. package/src/components/tooltip/tooltip.js +4 -1
  64. package/src/core/component.js +8 -3
  65. package/src/core/overlay-host.js +23 -0
  66. package/src/index.js +8 -3
  67. package/styles/tokens/dark.css +4 -0
  68. package/styles/tokens/semantic.css +83 -8
  69. package/DESIGN-SYSTEM.md +0 -182
@@ -1,6 +1,10 @@
1
1
  .zx-card-view {
2
- --zx-card-view-min-card-width: 18rem;
2
+ --zx-card-view-min-card-width: 15rem;
3
3
  --zx-card-view-column-basis: 0%;
4
+ --zx-card-view-card-padding: var(--zx-space-3);
5
+ --zx-card-view-card-gap: var(--zx-space-2);
6
+ --zx-card-view-grid-gap: var(--zx-space-3);
7
+ --zx-card-view-preview-size: 1.75rem;
4
8
  inline-size: 100%;
5
9
  min-inline-size: 0;
6
10
  container-type: inline-size;
@@ -44,22 +48,23 @@
44
48
  }
45
49
 
46
50
  .zx-card-view__group + .zx-card-view__group {
47
- margin-block-start: var(--zx-space-6);
51
+ margin-block-start: var(--zx-space-4);
48
52
  }
49
53
 
50
54
  .zx-card-view__group-heading {
51
- margin: 0 0 var(--zx-space-3);
55
+ margin: 0 0 var(--zx-space-2);
52
56
  color: var(--zx-color-text);
53
- font-size: var(--zx-text-lg);
54
- line-height: 1.35;
57
+ font-size: var(--zx-text-sm);
58
+ font-weight: 600;
59
+ line-height: 1.3;
55
60
  }
56
61
 
57
62
  .zx-card-view__list {
58
63
  display: grid;
59
- grid-template-columns: repeat(auto-fit,
64
+ grid-template-columns: repeat(auto-fill,
60
65
  minmax(min(100%, max(var(--zx-card-view-min-card-width),
61
- calc(var(--zx-card-view-column-basis) - var(--zx-space-4)))), 1fr));
62
- gap: var(--zx-space-4);
66
+ calc(var(--zx-card-view-column-basis) - var(--zx-card-view-grid-gap)))), 1fr));
67
+ gap: var(--zx-card-view-grid-gap);
63
68
  min-inline-size: 0;
64
69
  margin: 0;
65
70
  padding: 0;
@@ -102,7 +107,7 @@
102
107
  box-shadow: var(--zx-focus-ring);
103
108
  }
104
109
 
105
- .zx-record-card[aria-selected="true"] {
110
+ .zx-record-card[data-selected="true"] {
106
111
  border-color: var(--zx-color-accent);
107
112
  box-shadow: inset 0 0 0 1px var(--zx-color-accent);
108
113
  }
@@ -181,6 +186,40 @@
181
186
  min-inline-size: 0;
182
187
  }
183
188
 
189
+ .zx-record-card__eyebrow {
190
+ display: flex;
191
+ align-items: flex-start;
192
+ gap: var(--zx-space-2);
193
+ min-inline-size: 0;
194
+ }
195
+
196
+ .zx-record-card__eyebrow-group,
197
+ .zx-record-card__title-prefix {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ align-items: center;
201
+ gap: var(--zx-space-1);
202
+ min-inline-size: 0;
203
+ margin: 0;
204
+ }
205
+
206
+ .zx-record-card__eyebrow-group[data-card-slot="eyebrow-end"] {
207
+ justify-content: flex-end;
208
+ margin-inline-start: auto;
209
+ }
210
+
211
+ .zx-record-card__title-row {
212
+ display: flex;
213
+ align-items: flex-start;
214
+ gap: var(--zx-space-2);
215
+ min-inline-size: 0;
216
+ }
217
+
218
+ .zx-record-card__title-row > .zx-record-card__heading {
219
+ flex: 1 1 auto;
220
+ min-inline-size: 0;
221
+ }
222
+
184
223
  .zx-record-card__heading {
185
224
  margin: 0;
186
225
  overflow-wrap: anywhere;
@@ -387,3 +426,166 @@
387
426
  box-shadow var(--zx-dur-fast) var(--zx-ease);
388
427
  }
389
428
  }
429
+
430
+ /*
431
+ * CardView intentionally presents the shared record-card anatomy more densely than Kanban. Keep
432
+ * these selectors scoped: Kanban owns its own board-card rhythm while both views retain the same
433
+ * safe, semantic markup and delegated interaction hooks.
434
+ */
435
+ .zx-card-view .zx-record-card {
436
+ border-radius: var(--zx-radius-md);
437
+ }
438
+
439
+ .zx-card-view .zx-record-card:has(> .zx-record-card__preview) {
440
+ grid-template-columns: auto minmax(0, 1fr);
441
+ align-items: start;
442
+ }
443
+
444
+ .zx-card-view .zx-record-card__preview {
445
+ inline-size: var(--zx-card-view-preview-size);
446
+ block-size: var(--zx-card-view-preview-size);
447
+ margin-block-start: var(--zx-card-view-card-padding);
448
+ margin-inline-start: var(--zx-card-view-card-padding);
449
+ border: 0;
450
+ border-radius: var(--zx-radius-sm);
451
+ aspect-ratio: 1;
452
+ }
453
+
454
+ .zx-card-view .zx-record-card__preview .zx-icon {
455
+ inline-size: .875rem;
456
+ block-size: .875rem;
457
+ }
458
+
459
+ .zx-card-view .zx-record-card__body {
460
+ gap: var(--zx-card-view-card-gap);
461
+ padding: var(--zx-card-view-card-padding);
462
+ }
463
+
464
+ .zx-card-view .zx-record-card:has(> .zx-record-card__preview) .zx-record-card__body {
465
+ padding-inline-start: var(--zx-card-view-card-gap);
466
+ }
467
+
468
+ .zx-card-view .zx-record-card__header {
469
+ gap: var(--zx-space-2);
470
+ }
471
+
472
+ .zx-card-view .zx-record-card__selection {
473
+ min-block-size: 1.25rem;
474
+ }
475
+
476
+ .zx-card-view .zx-record-card__heading {
477
+ font-size: var(--zx-text-md);
478
+ font-weight: 600;
479
+ line-height: 1.3;
480
+ }
481
+
482
+ .zx-card-view .zx-record-card__primary,
483
+ .zx-card-view .zx-record-card__title {
484
+ display: -webkit-box;
485
+ overflow: hidden;
486
+ -webkit-box-orient: vertical;
487
+ -webkit-line-clamp: 2;
488
+ }
489
+
490
+ .zx-card-view .zx-record-card__subtitle {
491
+ margin-block-start: calc(var(--zx-space-1) / 2);
492
+ overflow: hidden;
493
+ font-size: var(--zx-text-xs);
494
+ line-height: 1.3;
495
+ text-overflow: ellipsis;
496
+ white-space: nowrap;
497
+ }
498
+
499
+ .zx-card-view .zx-record-card__actions {
500
+ flex-wrap: nowrap;
501
+ gap: calc(var(--zx-space-1) / 2);
502
+ }
503
+
504
+ .zx-card-view .zx-record-card__action {
505
+ min-block-size: 1.75rem;
506
+ border-color: transparent;
507
+ background: transparent;
508
+ padding-inline: var(--zx-space-2);
509
+ font-size: var(--zx-text-xs);
510
+ }
511
+
512
+ .zx-card-view .zx-record-card__metadata {
513
+ display: flex;
514
+ flex-wrap: wrap;
515
+ gap: var(--zx-space-1);
516
+ }
517
+
518
+ .zx-card-view .zx-record-card__field {
519
+ display: inline-flex;
520
+ flex: 0 1 auto;
521
+ align-items: center;
522
+ gap: var(--zx-space-1);
523
+ max-inline-size: 100%;
524
+ border: 1px solid var(--zx-color-border);
525
+ border-radius: var(--zx-radius-full);
526
+ background: var(--zx-color-bg-muted);
527
+ padding: calc(var(--zx-space-1) / 2) var(--zx-space-2);
528
+ }
529
+
530
+ .zx-card-view .zx-record-card__label {
531
+ flex: none;
532
+ font-size: var(--zx-text-xs);
533
+ font-weight: 500;
534
+ line-height: 1.2;
535
+ }
536
+
537
+ .zx-card-view .zx-record-card__value {
538
+ overflow: hidden;
539
+ font-size: var(--zx-text-xs);
540
+ line-height: 1.2;
541
+ text-overflow: ellipsis;
542
+ white-space: nowrap;
543
+ }
544
+
545
+ .zx-card-view .zx-card-view__group-empty {
546
+ padding: var(--zx-space-3);
547
+ font-size: var(--zx-text-sm);
548
+ }
549
+
550
+ .zx-card-view .zx-card-view__empty {
551
+ min-block-size: 6rem;
552
+ border-radius: var(--zx-radius-md);
553
+ padding: var(--zx-space-4);
554
+ font-size: var(--zx-text-sm);
555
+ }
556
+
557
+ .zx-card-view .zx-card-view__skeleton {
558
+ grid-template-columns: var(--zx-card-view-preview-size) minmax(0, 1fr);
559
+ gap: var(--zx-card-view-card-gap);
560
+ padding: var(--zx-card-view-card-padding);
561
+ }
562
+
563
+ .zx-card-view .zx-card-view__skeleton-preview {
564
+ grid-row: 1 / span 2;
565
+ inline-size: var(--zx-card-view-preview-size);
566
+ margin: 0;
567
+ aspect-ratio: 1;
568
+ border-radius: var(--zx-radius-sm);
569
+ }
570
+
571
+ .zx-card-view .zx-card-view__skeleton-line {
572
+ min-inline-size: 0;
573
+ block-size: .625rem;
574
+ }
575
+
576
+ .zx-card-view .zx-card-view__skeleton-line:nth-child(3) {
577
+ inline-size: 62%;
578
+ }
579
+
580
+ .zx-card-view .zx-card-view__skeleton-line:nth-child(4) {
581
+ grid-column: 1 / -1;
582
+ inline-size: 42%;
583
+ }
584
+
585
+ @media (pointer: coarse) {
586
+ .zx-card-view .zx-record-card__selection,
587
+ .zx-card-view .zx-record-card__action {
588
+ min-inline-size: 44px;
589
+ min-block-size: 44px;
590
+ }
591
+ }
@@ -39,7 +39,7 @@ import {
39
39
  * @property {string|((record:ViewRecord,index:number)=>unknown)|null} [groupBy=null]
40
40
  * Field id or local group resolver.
41
41
  * @property {unknown[]} [groupOrder=[]] Preferred group ids; missing ids render as empty groups.
42
- * @property {number|string} [minCardWidth='18rem'] Responsive minimum card width.
42
+ * @property {number|string} [minCardWidth='15rem'] Responsive minimum card width.
43
43
  * @property {number|null} [maxColumns=null] Maximum cards per row; null is unconstrained.
44
44
  * @property {'outlined'|'raised'|'filled'} [variant='outlined'] Card surface treatment.
45
45
  * @property {number} [loadingCount=6] Skeleton cards shown while loading.
@@ -87,7 +87,7 @@ export class CardView extends RecordView {
87
87
  actions: [],
88
88
  groupBy: null,
89
89
  groupOrder: [],
90
- minCardWidth: '18rem',
90
+ minCardWidth: '15rem',
91
91
  maxColumns: null,
92
92
  variant: 'outlined',
93
93
  loadingCount: 6,
@@ -329,7 +329,7 @@ export class CardView extends RecordView {
329
329
  for (const [id, card] of this._cardNodesById) {
330
330
  const selected = this._isViewSelected(id);
331
331
  card.dataset.selected = String(selected);
332
- if (this.options.selectable) card.setAttribute('aria-selected', String(selected));
332
+ if (this.options.selectable) card.setAttribute('aria-description', selected ? 'Selected' : 'Not selected');
333
333
  const checkbox = /** @type {HTMLInputElement|null} */ (card.querySelector('[data-record-selection]'));
334
334
  if (checkbox) checkbox.checked = selected;
335
335
  }
@@ -469,11 +469,11 @@ function normalizeGroupId(value) {
469
469
 
470
470
  /** @param {unknown} value @returns {string} */
471
471
  export function normalizeCardWidth(value) {
472
- if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? `${value}px` : '18rem';
472
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? `${value}px` : '15rem';
473
473
  const text = String(value ?? '').trim();
474
- if (!text || text.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text)) return '18rem';
475
- if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text)) return '18rem';
476
- if (typeof globalThis.CSS?.supports === 'function' && !globalThis.CSS.supports('width', text)) return '18rem';
474
+ if (!text || text.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text)) return '15rem';
475
+ if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text)) return '15rem';
476
+ if (typeof globalThis.CSS?.supports === 'function' && !globalThis.CSS.supports('width', text)) return '15rem';
477
477
  return text;
478
478
  }
479
479
 
@@ -7,6 +7,15 @@ import { readViewField, renderViewField } from '../view/record-view.js';
7
7
  /** @typedef {Record<string, any>} ViewRecord */
8
8
  /** @typedef {import('../view/record-view.js').ViewField} ViewField */
9
9
  /** @typedef {string|((record:ViewRecord,index:number)=>unknown)|null} RecordCardValueSource */
10
+ /** @typedef {'eyebrow-start'|'eyebrow-end'|'title-prefix'} RecordCardFieldSlot */
11
+
12
+ /**
13
+ * @typedef {Object} RecordCardFieldGroups
14
+ * @property {ViewField[]} eyebrowStart Fields shown at the logical start of the eyebrow row.
15
+ * @property {ViewField[]} eyebrowEnd Fields shown at the logical end of the eyebrow row.
16
+ * @property {ViewField[]} titlePrefix Fields shown immediately before, but outside, the heading.
17
+ * @property {ViewField[]} metadata Remaining labelled metadata fields.
18
+ */
10
19
 
11
20
  /**
12
21
  * @typedef {Object} RecordCardPreviewDescriptor
@@ -73,11 +82,12 @@ import { readViewField, renderViewField } from '../view/record-view.js';
73
82
  * becomes text, URLs are normalized before reaching native attributes, and actions expose stable
74
83
  * data attributes for a component-owned delegated listener.
75
84
  *
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.
85
+ * The returned `<li>` belongs in a semantic `<ul>`/`<ol>`. When selectable its global
86
+ * `aria-description` announces selection without applying the unsupported `aria-selected` state
87
+ * to a listitem; multi-selection also adds a native checkbox. It deliberately remains a listitem
88
+ * rather than becoming an ARIA option because cards may contain native links and buttons, which
89
+ * the listbox pattern does not permit inside an option. Preview images are paired with a stable
90
+ * fallback. The owning component listens for image `error` and marks the fallback.
81
91
  *
82
92
  * @param {ViewRecord} record Record to render.
83
93
  * @param {number} index Current display index.
@@ -112,6 +122,13 @@ export function createRecordCard(record, index, options = {}) {
112
122
  class: 'zx-record-card__heading',
113
123
  id: headingId
114
124
  }, headingContent);
125
+ const fieldGroups = partitionRecordCardFields(visibleFields, titleSource, subtitleSource);
126
+ const eyebrow = createEyebrow(record, index, fieldGroups.eyebrowStart, fieldGroups.eyebrowEnd);
127
+ const titlePrefix = createFieldList(record, index, fieldGroups.titlePrefix,
128
+ 'zx-record-card__title-prefix', 'title-prefix');
129
+ const titleRow = titlePrefix
130
+ ? h('div', { class: 'zx-record-card__title-row' }, titlePrefix, heading)
131
+ : heading;
115
132
  const selection = selectable === 'multi' ? h('label', {
116
133
  class: 'zx-record-card__selection'
117
134
  }, h('input', {
@@ -127,15 +144,15 @@ export function createRecordCard(record, index, options = {}) {
127
144
  ariaLabel: `Actions for ${textAlternative(title) || 'record'}`
128
145
  }, controls, actionNodes) : null;
129
146
  const header = h('header', { class: 'zx-record-card__header' }, selection,
130
- h('div', { class: 'zx-record-card__titles' }, heading,
147
+ h('div', { class: 'zx-record-card__titles' }, titleRow,
131
148
  isEmptyContent(subtitle) ? null : h('p', { class: 'zx-record-card__subtitle' }, contentNode(subtitle))),
132
149
  actionArea);
133
- const metadata = createMetadata(record, index, visibleFields, titleSource, subtitleSource);
134
- const body = h('div', { class: 'zx-record-card__body' }, header, metadata);
150
+ const metadata = createFieldList(record, index, fieldGroups.metadata, 'zx-record-card__metadata');
151
+ const body = h('div', { class: 'zx-record-card__body' }, eyebrow, header, metadata);
135
152
  const card = /** @type {HTMLLIElement} */ (h('li', {
136
153
  class: 'zx-record-card',
137
154
  tabindex: 0,
138
- ariaSelected: selectable ? String(selected) : undefined,
155
+ ariaDescription: selectable ? selected ? 'Selected' : 'Not selected' : undefined,
139
156
  ariaLabelledby: headingId,
140
157
  dataset: {
141
158
  recordIndex: String(index),
@@ -146,6 +163,36 @@ export function createRecordCard(record, index, options = {}) {
146
163
  return card;
147
164
  }
148
165
 
166
+ /**
167
+ * Partitions ordered visible fields into the shared record-card slots. A recognized
168
+ * `field.view.card.slot` moves the field to that semantic region; absent or unknown values remain
169
+ * ordinary metadata. Title and subtitle fields retain the existing no-duplication rule.
170
+ *
171
+ * The input array and descriptors are never mutated. Each field appears in at most one result
172
+ * array, and the order within every result follows the supplied visible-field order.
173
+ *
174
+ * @param {ViewField[]} fields Ordered visible fields.
175
+ * @param {RecordCardValueSource} [titleSource=null] Active title field or resolver.
176
+ * @param {RecordCardValueSource} [subtitleSource=null] Active subtitle field or resolver.
177
+ * @returns {RecordCardFieldGroups} Partitioned fields.
178
+ */
179
+ export function partitionRecordCardFields(fields, titleSource = null, subtitleSource = null) {
180
+ /** @type {RecordCardFieldGroups} */
181
+ const groups = { eyebrowStart: [], eyebrowEnd: [], titlePrefix: [], metadata: [] };
182
+ const titleId = typeof titleSource === 'string' ? titleSource : null;
183
+ const subtitleId = typeof subtitleSource === 'string' ? subtitleSource : null;
184
+
185
+ for (const field of Array.isArray(fields) ? fields : []) {
186
+ if (!field.duplicate && (field.id === titleId || field.id === subtitleId)) continue;
187
+ const slot = resolveRecordCardFieldSlot(field);
188
+ if (slot === 'eyebrow-start') groups.eyebrowStart.push(field);
189
+ else if (slot === 'eyebrow-end') groups.eyebrowEnd.push(field);
190
+ else if (slot === 'title-prefix') groups.titlePrefix.push(field);
191
+ else groups.metadata.push(field);
192
+ }
193
+ return groups;
194
+ }
195
+
149
196
  /**
150
197
  * Resolves a preview without touching the DOM. Unsafe, malformed, and non-image URL schemes are
151
198
  * rejected into the same descriptor shape so callers can still render the stable fallback.
@@ -245,26 +292,52 @@ function createPreview(preview) {
245
292
  }, fallback, image);
246
293
  }
247
294
 
295
+ /**
296
+ * @param {ViewRecord} record
297
+ * @param {number} index
298
+ * @param {ViewField[]} eyebrowStart
299
+ * @param {ViewField[]} eyebrowEnd
300
+ * @returns {HTMLElement|null}
301
+ */
302
+ function createEyebrow(record, index, eyebrowStart, eyebrowEnd) {
303
+ const start = createFieldList(record, index, eyebrowStart,
304
+ 'zx-record-card__eyebrow-group', 'eyebrow-start');
305
+ const end = createFieldList(record, index, eyebrowEnd,
306
+ 'zx-record-card__eyebrow-group', 'eyebrow-end');
307
+ return start || end ? h('div', { class: 'zx-record-card__eyebrow' }, start, end) : null;
308
+ }
309
+
248
310
  /**
249
311
  * @param {ViewRecord} record
250
312
  * @param {number} index
251
313
  * @param {ViewField[]} fields
252
- * @param {RecordCardValueSource} titleSource
253
- * @param {RecordCardValueSource} subtitleSource
314
+ * @param {string} className
315
+ * @param {RecordCardFieldSlot|null} [slot=null]
254
316
  * @returns {HTMLElement|null}
255
317
  */
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', {
318
+ function createFieldList(record, index, fields, className, slot = null) {
319
+ if (fields.length === 0) return null;
320
+ return h('dl', {
321
+ class: className,
322
+ dataset: slot ? { cardSlot: slot } : undefined
323
+ }, fields.map((field) => h('div', {
262
324
  class: 'zx-record-card__field',
263
- dataset: { fieldId: field.id }
325
+ dataset: { fieldId: field.id, ...(slot ? { cardSlot: slot } : {}) }
264
326
  }, h('dt', { class: 'zx-record-card__label' }, field.label),
265
327
  h('dd', { class: 'zx-record-card__value' }, contentNode(renderViewField(field, record, index))))));
266
328
  }
267
329
 
330
+ /** @param {ViewField} field @returns {RecordCardFieldSlot|null} */
331
+ function resolveRecordCardFieldSlot(field) {
332
+ const view = field?.view;
333
+ if (!view || typeof view !== 'object' || Array.isArray(view)) return null;
334
+ const card = view.card;
335
+ if (!card || typeof card !== 'object' || Array.isArray(card)) return null;
336
+ const slot = /** @type {{slot?:unknown}} */ (card).slot;
337
+ return slot === 'eyebrow-start' || slot === 'eyebrow-end' || slot === 'title-prefix'
338
+ ? slot : null;
339
+ }
340
+
268
341
  /** @param {RecordCardAction} action @returns {HTMLElement} */
269
342
  function createAction(action) {
270
343
  const label = String(action.label ?? '');
@@ -356,7 +429,7 @@ function safePreviewUrl(value) {
356
429
  try {
357
430
  const base = globalThis.location?.href ?? 'https://zx.invalid/';
358
431
  const protocol = new URL(href, base).protocol.toLowerCase();
359
- return ['http:', 'https:', 'blob:', 'file:'].includes(protocol) ? href : null;
432
+ return ['http:', 'https:', 'blob:'].includes(protocol) ? href : null;
360
433
  } catch {
361
434
  return null;
362
435
  }
@@ -29,7 +29,7 @@
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
 
@@ -125,10 +125,12 @@
125
125
  inset: var(--zx-space-3);
126
126
  border: 1px solid var(--zx-color-overlay-border);
127
127
  border-radius: var(--zx-radius-md);
128
- background: var(--zx-color-overlay-surface);
128
+ background-color: var(--zx-color-overlay-surface);
129
+ background-image: var(--zx-overlay-surface-image);
130
+ background-clip: padding-box;
129
131
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
130
132
  backdrop-filter: var(--zx-glass-filter-overlay);
131
- box-shadow: var(--zx-glass-raised-shadow);
133
+ box-shadow: var(--zx-glass-overlay-shadow);
132
134
  padding: var(--zx-space-3);
133
135
  }
134
136
 
@@ -157,11 +159,16 @@
157
159
  .zx-month-picker__month {
158
160
  min-inline-size: 0;
159
161
  padding-inline: var(--zx-space-1);
160
- border-color: var(--zx-color-bg-surface);
161
- 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;
162
170
  }
163
171
 
164
- .zx-date-picker__quick-month[aria-selected="true"],
165
172
  .zx-month-picker__month[aria-selected="true"] {
166
173
  background: var(--zx-color-bg-selected);
167
174
  color: var(--zx-color-accent);
@@ -124,10 +124,12 @@
124
124
  .zx-date-range-box__popover .zx-date-range-picker__surface {
125
125
  max-inline-size: none;
126
126
  border-color: var(--zx-color-overlay-border);
127
- background: var(--zx-color-overlay-surface);
127
+ background-color: var(--zx-color-overlay-surface);
128
+ background-image: var(--zx-overlay-surface-image);
129
+ background-clip: padding-box;
128
130
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
129
131
  backdrop-filter: var(--zx-glass-filter-overlay);
130
- box-shadow: var(--zx-glass-raised-shadow);
132
+ box-shadow: var(--zx-glass-overlay-shadow);
131
133
  }
132
134
 
133
135
  @media (prefers-reduced-transparency: reduce) {
@@ -104,10 +104,12 @@
104
104
 
105
105
  .zx-datebox__popover .zx-date-picker__surface {
106
106
  border-color: var(--zx-color-overlay-border);
107
- background: var(--zx-color-overlay-surface);
107
+ background-color: var(--zx-color-overlay-surface);
108
+ background-image: var(--zx-overlay-surface-image);
109
+ background-clip: padding-box;
108
110
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
109
111
  backdrop-filter: var(--zx-glass-filter-overlay);
110
- box-shadow: var(--zx-glass-raised-shadow);
112
+ box-shadow: var(--zx-glass-overlay-shadow);
111
113
  }
112
114
 
113
115
  @media (prefers-reduced-transparency: reduce) {
@@ -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
  */
@@ -4,10 +4,12 @@
4
4
  overflow: auto;
5
5
  border: 1px solid var(--zx-color-overlay-border);
6
6
  border-radius: var(--zx-radius-md);
7
- background: var(--zx-color-overlay-surface);
7
+ background-color: var(--zx-color-overlay-surface);
8
+ background-image: var(--zx-overlay-surface-image);
9
+ background-clip: padding-box;
8
10
  -webkit-backdrop-filter: var(--zx-glass-filter-overlay);
9
11
  backdrop-filter: var(--zx-glass-filter-overlay);
10
- box-shadow: var(--zx-glass-raised-shadow);
12
+ box-shadow: var(--zx-glass-overlay-shadow);
11
13
  color: var(--zx-color-text);
12
14
  padding: var(--zx-space-3);
13
15
  }