@zeyos/zx 4.3.0 → 4.4.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 (136) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/README.md +7 -3
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +195 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
  6. package/dist/types/components/button/button.d.ts +8 -0
  7. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  8. package/dist/types/components/calendar/calendar.d.ts +19 -0
  9. package/dist/types/components/card/card.d.ts +7 -0
  10. package/dist/types/components/card-view/card-view.d.ts +12 -4
  11. package/dist/types/components/card-view/record-card.d.ts +53 -5
  12. package/dist/types/components/code-editor/code-editor.d.ts +160 -0
  13. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  14. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  15. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  16. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  17. package/dist/types/components/dialog/dialog.d.ts +29 -0
  18. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  19. package/dist/types/components/file-item/file-item.d.ts +234 -0
  20. package/dist/types/components/file-list/file-list.d.ts +171 -0
  21. package/dist/types/components/filter/filter.d.ts +91 -36
  22. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  23. package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
  24. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  25. package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
  26. package/dist/types/components/launcher/launcher.d.ts +19 -12
  27. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  28. package/dist/types/components/menu-button/menu-button.d.ts +4 -37
  29. package/dist/types/components/message/message.d.ts +7 -0
  30. package/dist/types/components/modal/modal.d.ts +48 -0
  31. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  32. package/dist/types/components/panel/panel.d.ts +7 -0
  33. package/dist/types/components/search/search.d.ts +7 -0
  34. package/dist/types/components/select/select.d.ts +4 -0
  35. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  36. package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
  37. package/dist/types/components/tabbox/tabbox.d.ts +7 -0
  38. package/dist/types/components/table/table.d.ts +144 -0
  39. package/dist/types/components/table-view/table-view.d.ts +11 -5
  40. package/dist/types/components/timebox/timebox.d.ts +7 -0
  41. package/dist/types/components/value-list/value-list.d.ts +8 -0
  42. package/dist/types/components/view/record-view.d.ts +96 -5
  43. package/dist/types/core/component.d.ts +6 -1
  44. package/dist/types/core/icons.d.ts +17 -0
  45. package/dist/types/index.d.ts +13 -1
  46. package/dist/types/internal/app-rail.d.ts +10 -0
  47. package/dist/types/internal/menu-items.d.ts +134 -0
  48. package/dist/zx-zeyos.esm.js +651 -88
  49. package/dist/zx-zeyos.esm.min.js +8 -1
  50. package/dist/zx.css +6266 -3900
  51. package/dist/zx.esm.js +8487 -1805
  52. package/dist/zx.esm.min.js +10 -3
  53. package/dist/zx.global.js +8487 -1805
  54. package/dist/zx.global.min.js +10 -3
  55. package/dist/zx.min.css +1 -1
  56. package/docs/api.json +1811 -247
  57. package/docs/llms.md +345 -40
  58. package/docs/llms.txt +11 -7
  59. package/package.json +1 -2
  60. package/src/components/activity-item/activity-item.css +137 -0
  61. package/src/components/activity-item/activity-item.js +599 -0
  62. package/src/components/activity-list/activity-list.css +64 -0
  63. package/src/components/activity-list/activity-list.js +375 -0
  64. package/src/components/app-rail/app-rail.css +227 -19
  65. package/src/components/app-sidebar/app-sidebar.css +414 -24
  66. package/src/components/app-sidebar/app-sidebar.js +65 -5
  67. package/src/components/button/button.css +8 -0
  68. package/src/components/button/button.js +6 -1
  69. package/src/components/calendar/calendar-model.js +45 -0
  70. package/src/components/calendar/calendar.css +30 -10
  71. package/src/components/calendar/calendar.js +41 -13
  72. package/src/components/card/card.js +12 -1
  73. package/src/components/card-view/card-view.css +212 -9
  74. package/src/components/card-view/card-view.js +32 -16
  75. package/src/components/card-view/record-card.js +98 -21
  76. package/src/components/code-editor/code-editor.css +67 -0
  77. package/src/components/code-editor/code-editor.js +288 -0
  78. package/src/components/code-editor/field-adapter.js +34 -0
  79. package/src/components/context-menu/context-menu.css +80 -3
  80. package/src/components/context-menu/context-menu.js +22 -30
  81. package/src/components/date-picker/date-picker.js +22 -8
  82. package/src/components/date-picker/month-picker.js +3 -3
  83. package/src/components/date-picker/time-picker.js +20 -8
  84. package/src/components/dialog/dialog.js +88 -66
  85. package/src/components/dropdown/dropdown.js +2 -2
  86. package/src/components/entity-ref/entity-ref.css +182 -0
  87. package/src/components/entity-ref/entity-ref.js +496 -0
  88. package/src/components/file-item/file-item.css +88 -0
  89. package/src/components/file-item/file-item.js +371 -0
  90. package/src/components/file-list/file-list.css +51 -0
  91. package/src/components/file-list/file-list.js +320 -0
  92. package/src/components/filter/filter.css +27 -1
  93. package/src/components/filter/filter.js +199 -45
  94. package/src/components/filter-panel/filter-panel.css +104 -0
  95. package/src/components/filter-panel/filter-panel.js +964 -0
  96. package/src/components/kanban-view/kanban-card.js +239 -0
  97. package/src/components/kanban-view/kanban-policy.js +412 -0
  98. package/src/components/kanban-view/kanban-view.css +674 -36
  99. package/src/components/kanban-view/kanban-view.js +1762 -240
  100. package/src/components/launcher/launcher.js +59 -27
  101. package/src/components/list-toolbar/list-toolbar.css +189 -0
  102. package/src/components/list-toolbar/list-toolbar.js +851 -0
  103. package/src/components/menu-button/menu-button.css +80 -3
  104. package/src/components/menu-button/menu-button.js +21 -37
  105. package/src/components/message/message.css +14 -12
  106. package/src/components/message/message.js +13 -2
  107. package/src/components/modal/modal.css +15 -0
  108. package/src/components/modal/modal.js +141 -0
  109. package/src/components/navigation-bar/navigation-bar.css +77 -2
  110. package/src/components/navigation-bar/navigation-bar.js +171 -4
  111. package/src/components/panel/panel.js +13 -2
  112. package/src/components/search/search.js +17 -5
  113. package/src/components/select/field-adapter.js +4 -1
  114. package/src/components/select/select.js +3 -0
  115. package/src/components/sort-control/sort-control.css +17 -0
  116. package/src/components/sort-control/sort-control.js +140 -0
  117. package/src/components/stat-tile/stat-tile.css +213 -0
  118. package/src/components/stat-tile/stat-tile.js +590 -0
  119. package/src/components/tabbox/tabbox.js +12 -1
  120. package/src/components/table/table.css +51 -2
  121. package/src/components/table/table.js +298 -19
  122. package/src/components/table-view/table-view.css +8 -2
  123. package/src/components/table-view/table-view.js +21 -6
  124. package/src/components/tag-picker/tag-picker.css +3 -1
  125. package/src/components/timebox/timebox.js +18 -5
  126. package/src/components/value-list/value-list.js +22 -6
  127. package/src/components/view/record-view.css +5 -0
  128. package/src/components/view/record-view.js +100 -7
  129. package/src/core/component.js +8 -3
  130. package/src/core/icons.js +74 -5
  131. package/src/index.js +33 -3
  132. package/src/internal/app-rail.js +19 -1
  133. package/src/internal/menu-items.js +142 -0
  134. package/styles/print.css +192 -0
  135. package/styles/zx.css +11 -0
  136. package/DESIGN-SYSTEM.md +0 -182
@@ -70,6 +70,51 @@ export function calendarRange(view, anchor, options = {}) {
70
70
  return { start, end };
71
71
  }
72
72
 
73
+ /**
74
+ * Returns the anchor date one visible period away from `anchor`.
75
+ *
76
+ * Month and year steps clamp the day of month into the target month instead of letting it
77
+ * overflow: `setMonth()` alone turns 31 January into 3 March, which makes February unreachable
78
+ * from a month-end anchor and leaves `prev()` on 31 March landing back inside March.
79
+ * @param {CalendarView} view Current view.
80
+ * @param {Date} anchor Current anchor date.
81
+ * @param {number} direction -1 for the previous period, 1 for the next.
82
+ * @param {{agendaDays?: number}} [options={}] Paging options.
83
+ * @returns {Date}
84
+ */
85
+ export function calendarPageDate(view, anchor, direction, options = {}) {
86
+ const date = validDate(anchor, 'Calendar date');
87
+ const step = integer(direction, 'direction');
88
+ if (view === 'day') date.setDate(date.getDate() + step);
89
+ else if (view === 'agenda') {
90
+ date.setDate(date.getDate() + step * positiveInteger(options.agendaDays ?? 14, 'agendaDays'));
91
+ } else if (view === 'week') date.setDate(date.getDate() + step * 7);
92
+ else if (view === 'month') return addCalendarMonths(date, step);
93
+ else if (view === 'year') return addCalendarMonths(date, step * 12);
94
+ else throw new RangeError(`Unknown calendar view: ${view}`);
95
+ return date;
96
+ }
97
+
98
+ /**
99
+ * Adds whole months, clamping the day of month to the last day the target month actually has.
100
+ * @param {Date} value Local date.
101
+ * @param {number} months Months to add.
102
+ * @returns {Date}
103
+ */
104
+ export function addCalendarMonths(value, months) {
105
+ const date = validDate(value, 'Calendar date');
106
+ const day = date.getDate();
107
+ date.setDate(1);
108
+ date.setMonth(date.getMonth() + integer(months, 'month amount'));
109
+ date.setDate(Math.min(day, daysInCalendarMonth(date.getFullYear(), date.getMonth())));
110
+ return date;
111
+ }
112
+
113
+ /** @param {number} year @param {number} month @returns {number} */
114
+ function daysInCalendarMonth(year, month) {
115
+ return new Date(year, month + 1, 0).getDate();
116
+ }
117
+
73
118
  /**
74
119
  * Normalizes caller records atomically into the Calendar event contract.
75
120
  * @param {Record<string, any>[]} records Source records.
@@ -9,15 +9,20 @@
9
9
  min-inline-size: 0;
10
10
  overflow: hidden;
11
11
  border: 1px solid var(--zx-color-border);
12
- border-radius: var(--zx-radius-lg);
12
+ border-radius: 0;
13
13
  background: var(--zx-color-bg-surface);
14
14
  color: var(--zx-color-text);
15
15
  font-size: var(--zx-text-sm);
16
16
  }
17
17
 
18
+ /*
19
+ * The heading is the only elastic track. With `1fr auto 1fr` the end-justified commands group
20
+ * kept its min-content width inside a narrower `1fr` track and bled back over the centered
21
+ * heading; giving navigation and commands their natural width lets the heading ellipsize instead.
22
+ */
18
23
  .zx-calendar__toolbar {
19
24
  display: grid;
20
- grid-template-columns: 1fr auto 1fr;
25
+ grid-template-columns: auto minmax(0, 1fr) auto;
21
26
  align-items: center;
22
27
  gap: var(--zx-space-3);
23
28
  min-inline-size: 0;
@@ -35,7 +40,14 @@
35
40
 
36
41
  .zx-calendar__navigation { justify-self: start; gap: var(--zx-space-1); }
37
42
  .zx-calendar__commands { justify-self: end; gap: var(--zx-space-3); }
38
- .zx-calendar__views { overflow-x: auto; }
43
+ .zx-calendar__views {
44
+ block-size: var(--zx-control-height);
45
+ gap: 2px;
46
+ overflow-x: auto;
47
+ border-radius: 0;
48
+ background: var(--zx-color-bg-muted);
49
+ padding: 3px;
50
+ }
39
51
 
40
52
  .zx-calendar__toolbar-button,
41
53
  .zx-calendar__view-button,
@@ -59,15 +71,21 @@
59
71
  }
60
72
 
61
73
  .zx-calendar__toolbar-button:has(.zx-icon) { padding-inline: var(--zx-space-2); }
62
- .zx-calendar__view-button { border-inline-end-width: 0; padding-inline: var(--zx-space-3); }
63
- .zx-calendar__view-button:first-child { border-start-start-radius: var(--zx-control-radius); border-end-start-radius: var(--zx-control-radius); }
64
- .zx-calendar__view-button:last-child { border-inline-end-width: 1px; border-start-end-radius: var(--zx-control-radius); border-end-end-radius: var(--zx-control-radius); }
74
+ .zx-calendar__view-button {
75
+ block-size: 100%;
76
+ min-block-size: 0;
77
+ border-color: transparent;
78
+ border-radius: 0;
79
+ background: transparent;
80
+ padding-inline: var(--zx-space-3);
81
+ }
65
82
  .zx-calendar__view-button[aria-pressed="true"] {
66
83
  position: relative;
67
84
  z-index: 1;
68
- border-color: var(--zx-color-accent);
69
- background: var(--zx-color-bg-selected);
70
- color: var(--zx-color-accent);
85
+ border-color: var(--zx-color-border-control);
86
+ background: var(--zx-color-bg-control);
87
+ box-shadow: var(--zx-shadow-2);
88
+ color: var(--zx-color-text);
71
89
  }
72
90
 
73
91
  .zx-calendar__new {
@@ -81,7 +99,7 @@
81
99
  }
82
100
 
83
101
  .zx-calendar__toolbar-button:hover:not(:disabled),
84
- .zx-calendar__view-button:hover:not(:disabled) { background: var(--zx-color-bg-hover); }
102
+ .zx-calendar__view-button:hover:not(:disabled):not([aria-pressed="true"]) { background: var(--zx-color-bg-hover); }
85
103
  .zx-calendar__new:hover:not(:disabled) { background: var(--zx-color-accent-hover); }
86
104
  .zx-calendar button:disabled { cursor: default; opacity: .55; }
87
105
 
@@ -148,6 +166,7 @@
148
166
  min-block-size: calc(var(--zx-control-height) + var(--zx-space-2));
149
167
  place-content: center;
150
168
  border: 0;
169
+ border-radius: 0;
151
170
  border-inline-end: 1px solid var(--zx-color-border);
152
171
  background: var(--zx-color-bg-surface);
153
172
  color: var(--zx-color-text);
@@ -227,6 +246,7 @@
227
246
  .zx-calendar__slot {
228
247
  min-inline-size: 0;
229
248
  border: 0;
249
+ border-radius: 0;
230
250
  border-block-start: 1px solid var(--zx-color-border);
231
251
  border-inline-end: 1px solid var(--zx-color-border);
232
252
  background: var(--zx-color-bg-surface);
@@ -1,10 +1,11 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
+ import { printf } from '../../core/i18n.js';
4
5
  import {
5
6
  CALENDAR_VIEWS, addCalendarDays, calendarDayDifference, calendarDayKey,
6
- calendarEventIntersects, calendarEventSpansDays, calendarRange, cloneCalendarEvent,
7
- layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
7
+ calendarEventIntersects, calendarEventSpansDays, calendarPageDate, calendarRange,
8
+ cloneCalendarEvent, layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
8
9
  shiftCalendarEvent, startOfCalendarDay
9
10
  } from './calendar-model.js';
10
11
 
@@ -189,7 +190,7 @@ export class Calendar extends Component {
189
190
  ref: 'views',
190
191
  class: 'zx-calendar__views',
191
192
  role: 'group',
192
- ariaLabel: 'Calendar view'
193
+ ariaLabel: this._message('calendar.viewSwitch', 'Calendar view')
193
194
  }, this._views.map((view) => h('button', {
194
195
  type: 'button',
195
196
  class: 'zx-calendar__view-button',
@@ -289,7 +290,9 @@ export class Calendar extends Component {
289
290
  ? changes.durationEditable : current.durationEditable,
290
291
  data: changes.data ?? current.data
291
292
  };
292
- const [next] = normalizeCalendarEvents([source]);
293
+ // `source` is assembled from canonical keys, so the configured readers must not be reapplied
294
+ // here — but `dateUnit` must, or a numeric change is read in the wrong unit.
295
+ const [next] = normalizeCalendarEvents([source], { dateUnit: this.options.dateUnit });
293
296
  next.data = source.data;
294
297
  this._events = this._events.map((event, eventIndex) => eventIndex === index ? next : event);
295
298
  this._mutationVersions.delete(String(id));
@@ -641,7 +644,7 @@ export class Calendar extends Component {
641
644
  eventCount: String(count)
642
645
  },
643
646
  ariaCurrent: sameCalendarDay(day, this._now()) ? 'date' : null,
644
- ariaLabel: `${this._format(day, { dateStyle: 'full' })}, ${count} ${count === 1 ? 'event' : 'events'}`,
647
+ ariaLabel: this._dayCountLabel(day, count),
645
648
  disabled: this.options.disabled
646
649
  }, this._format(day, { day: 'numeric' })));
647
650
  }
@@ -706,7 +709,7 @@ export class Calendar extends Component {
706
709
  activity: String(Math.min(3, count))
707
710
  },
708
711
  ariaCurrent: sameCalendarDay(day, this._now()) ? 'date' : null,
709
- ariaLabel: `${this._format(day, { dateStyle: 'full' })}, ${count} ${count === 1 ? 'event' : 'events'}`,
712
+ ariaLabel: this._dayCountLabel(day, count),
710
713
  disabled: this.options.disabled
711
714
  }, String(day.getDate())));
712
715
  }
@@ -1335,13 +1338,9 @@ export class Calendar extends Component {
1335
1338
 
1336
1339
  /** @param {number} direction @returns {this} */
1337
1340
  _page(direction) {
1338
- const date = new Date(this._date.getTime());
1339
- if (this._view === 'day') date.setDate(date.getDate() + direction);
1340
- else if (this._view === 'agenda') date.setDate(date.getDate() + direction * this.options.agendaDays);
1341
- else if (this._view === 'week') date.setDate(date.getDate() + direction * 7);
1342
- else if (this._view === 'month') date.setMonth(date.getMonth() + direction);
1343
- else date.setFullYear(date.getFullYear() + direction);
1344
- return this.setDate(date);
1341
+ return this.setDate(calendarPageDate(this._view, this._date, direction, {
1342
+ agendaDays: this.options.agendaDays
1343
+ }));
1345
1344
  }
1346
1345
 
1347
1346
  /** @param {{start: Date, end: Date}} range @returns {string} */
@@ -1475,6 +1474,35 @@ export class Calendar extends Component {
1475
1474
  if (!this._destroyed) this.refs.status.textContent = message;
1476
1475
  });
1477
1476
  }
1477
+
1478
+ /**
1479
+ * Names a month or year cell by its date and how many events it holds. Singular and plural are
1480
+ * separate keys because a language that inflects the noun cannot reach it from one template.
1481
+ * @param {Date} day Day the cell stands for.
1482
+ * @param {number} count Events on that day.
1483
+ * @returns {string}
1484
+ */
1485
+ _dayCountLabel(day, count) {
1486
+ const date = this._format(day, { dateStyle: 'full' });
1487
+ return count === 1
1488
+ ? this._message('calendar.dayEvent', '%1, %2 event', date, count)
1489
+ : this._message('calendar.dayEvents', '%1, %2 events', date, count);
1490
+ }
1491
+
1492
+ /**
1493
+ * Resolves a message through the host translator, falling back to the built-in English text.
1494
+ *
1495
+ * Distinct from `msg()`: the built-in `msg` option answers the calendar's own vocabulary, so a
1496
+ * dotted key falls past it to the host translator, which is what these strings never had.
1497
+ * @param {string} key Message key.
1498
+ * @param {string} fallback Built-in text, with `%1`-style placeholders.
1499
+ * @param {...unknown} args Interpolation values.
1500
+ * @returns {string}
1501
+ */
1502
+ _message(key, fallback, ...args) {
1503
+ const message = this.msg(key, ...args);
1504
+ return message === key ? printf(fallback, args) : message;
1505
+ }
1478
1506
  }
1479
1507
 
1480
1508
  /** @param {unknown} value @returns {CalendarView[]} */
@@ -86,7 +86,7 @@ export class Card extends Component {
86
86
  ref: 'actions',
87
87
  class: 'zx-card__actions',
88
88
  role: 'group',
89
- ariaLabel: 'Card actions',
89
+ ariaLabel: this._message('card.actions', 'Card actions'),
90
90
  hidden: true
91
91
  });
92
92
  const content = h('div', { ref: 'content', class: 'zx-card__content', hidden: true });
@@ -225,6 +225,17 @@ export class Card extends Component {
225
225
  if (!this.refs.header) return;
226
226
  this.refs.header.hidden = this._title === '' && this.refs.actions.childElementCount === 0;
227
227
  }
228
+
229
+ /**
230
+ * Resolves a message through the host translator, falling back to the built-in English text.
231
+ * @param {string} key Message key.
232
+ * @param {string} fallback English default.
233
+ * @returns {string}
234
+ */
235
+ _message(key, fallback) {
236
+ const message = this.msg(key);
237
+ return message === key ? fallback : message;
238
+ }
228
239
  }
229
240
 
230
241
  /** @param {Element} host @param {CardContent} content @param {string} label */
@@ -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,167 @@
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
+ }
592
+ .zx-card-view .zx-record-card[data-selection-trigger="card"] { cursor: pointer; }
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
  import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
- import { RecordView, readViewField } from '../view/record-view.js';
3
+ import { normalizeViewFieldControls, RecordView, readViewField } from '../view/record-view.js';
4
4
  import {
5
5
  createRecordCard, resolveRecordActions
6
6
  } from './record-card.js';
@@ -21,10 +21,13 @@ import {
21
21
  * @property {import('../view/record-view.js').ViewSort|null} [sort=null] Initial shared sort.
22
22
  * @property {'local'|'server'} [sortMode='local'] Local sorting or event-only server sorting.
23
23
  * @property {false|'single'|'multi'} [selectable=false] Record selection behavior.
24
+ * @property {'checkbox'|'card'} [selectionTrigger='checkbox'] Select through a checkbox or the non-interactive card surface.
24
25
  * @property {unknown[]} [selection=[]] Initially selected record ids.
25
26
  * @property {string[]} [fieldOrder=[]] Preferred shared field order.
26
27
  * @property {string[]} [hiddenFields=[]] Initially hidden metadata fields.
27
- * @property {boolean} [fieldControls=true] Whether to expose shared field controls.
28
+ * @property {boolean|import('../view/record-view.js').ViewFieldControlsConfig} [fieldControls=true]
29
+ * Whether to expose shared field controls. `true` renders the chooser in the card toolbar and
30
+ * `false` omits it; an object customises its `label` and mounts it into `target`.
28
31
  * @property {string|Node|(()=>string|Node)|null} [emptyText=null] Empty-result content.
29
32
  * @property {RecordCardValueSource} [titleField=null] Title field id or explicit resolver.
30
33
  * @property {RecordCardValueSource} [subtitleField=null] Subtitle field id or explicit resolver.
@@ -39,7 +42,7 @@ import {
39
42
  * @property {string|((record:ViewRecord,index:number)=>unknown)|null} [groupBy=null]
40
43
  * Field id or local group resolver.
41
44
  * @property {unknown[]} [groupOrder=[]] Preferred group ids; missing ids render as empty groups.
42
- * @property {number|string} [minCardWidth='18rem'] Responsive minimum card width.
45
+ * @property {number|string} [minCardWidth='15rem'] Responsive minimum card width.
43
46
  * @property {number|null} [maxColumns=null] Maximum cards per row; null is unconstrained.
44
47
  * @property {'outlined'|'raised'|'filled'} [variant='outlined'] Card surface treatment.
45
48
  * @property {number} [loadingCount=6] Skeleton cards shown while loading.
@@ -79,6 +82,7 @@ export class CardView extends RecordView {
79
82
  /** @type {Readonly<CardViewOptions & RecordViewOptions>} */
80
83
  static defaults = {
81
84
  ...RecordView.defaults,
85
+ selectionTrigger: 'checkbox',
82
86
  titleField: null,
83
87
  subtitleField: null,
84
88
  preview: undefined,
@@ -87,7 +91,7 @@ export class CardView extends RecordView {
87
91
  actions: [],
88
92
  groupBy: null,
89
93
  groupOrder: [],
90
- minCardWidth: '18rem',
94
+ minCardWidth: '15rem',
91
95
  maxColumns: null,
92
96
  variant: 'outlined',
93
97
  loadingCount: 6,
@@ -124,10 +128,12 @@ export class CardView extends RecordView {
124
128
  this._initRecordView(root);
125
129
  root.setAttribute('aria-label', String(options.label || 'Card view'));
126
130
  const sort = h('label', { ref: 'sortWrap', class: 'zx-card-view__sort' },
127
- h('span', { class: 'zx-card-view__sort-label' }, 'Sort'),
128
- h('select', { ref: 'sort', ariaLabel: 'Sort cards' }));
131
+ h('span', { class: 'zx-card-view__sort-label' }, this._message('cardView.sort', 'Sort')),
132
+ h('select', { ref: 'sort', ariaLabel: this._message('cardView.sortCards', 'Sort cards') }));
129
133
  const toolbar = h('div', { class: 'zx-record-view__toolbar zx-card-view__toolbar' }, sort);
130
- if (this.options.fieldControls) toolbar.append(this._createViewFieldControls('Fields'));
134
+ if (normalizeViewFieldControls(this.options.fieldControls).enabled) {
135
+ this._mountViewFieldControls(toolbar);
136
+ }
131
137
  const content = h('div', { ref: 'content', class: 'zx-card-view__content' });
132
138
  root.replaceChildren(toolbar, content);
133
139
  this.listen(this.refs.sort, 'change', () => this._changeCardSort());
@@ -140,6 +146,7 @@ export class CardView extends RecordView {
140
146
  this._renderCards();
141
147
  return root;
142
148
  } catch (error) {
149
+ this._removeViewFieldControls();
143
150
  if (!created) restoreTarget(root, this._cardOriginal);
144
151
  throw error;
145
152
  }
@@ -212,16 +219,18 @@ export class CardView extends RecordView {
212
219
  if (!this.refs?.sort || !this.refs?.sortWrap) return;
213
220
  const select = /** @type {HTMLSelectElement} */ (this.refs.sort);
214
221
  const sort = this.getSort();
215
- const options = [h('option', { value: '', selected: sort === null }, 'Unsorted')];
222
+ const options = [h('option', {
223
+ value: '', selected: sort === null
224
+ }, this._message('cardView.unsorted', 'Unsorted'))];
216
225
  for (const field of this.getFields().filter((candidate) => candidate.sortable)) {
217
226
  options.push(h('option', {
218
227
  selected: sort?.id === field.id && sort.dir === 'asc',
219
228
  dataset: { sortId: field.id, sortDir: 'asc' }
220
- }, `${field.label} (ascending)`));
229
+ }, this._message('cardView.sortAscending', '%1 (ascending)', field.label)));
221
230
  options.push(h('option', {
222
231
  selected: sort?.id === field.id && sort.dir === 'desc',
223
232
  dataset: { sortId: field.id, sortDir: 'desc' }
224
- }, `${field.label} (descending)`));
233
+ }, this._message('cardView.sortDescending', '%1 (descending)', field.label)));
225
234
  }
226
235
  select.replaceChildren(...options);
227
236
  /** @type {HTMLElement} */ (this.refs.sortWrap).hidden = options.length === 1;
@@ -286,6 +295,7 @@ export class CardView extends RecordView {
286
295
  link: options.link,
287
296
  actions: options.actions,
288
297
  selectable: this.options.selectable,
298
+ selectionTrigger: options.selectionTrigger,
289
299
  selected: this._isViewSelected(id),
290
300
  variant: this._cardVariant,
291
301
  headingLevel: this._cardHeadingLevel
@@ -329,7 +339,7 @@ export class CardView extends RecordView {
329
339
  for (const [id, card] of this._cardNodesById) {
330
340
  const selected = this._isViewSelected(id);
331
341
  card.dataset.selected = String(selected);
332
- if (this.options.selectable) card.setAttribute('aria-selected', String(selected));
342
+ if (this.options.selectable) card.setAttribute('aria-description', selected ? 'Selected' : 'Not selected');
333
343
  const checkbox = /** @type {HTMLInputElement|null} */ (card.querySelector('[data-record-selection]'));
334
344
  if (checkbox) checkbox.checked = selected;
335
345
  }
@@ -355,6 +365,11 @@ export class CardView extends RecordView {
355
365
  return;
356
366
  }
357
367
  if (isInteractiveWithin(target, card) || hasCardTextSelection(card)) return;
368
+ if (this.options.selectable && this._cardOptions().selectionTrigger === 'card') {
369
+ this.toggleSelection(id, { range: event.shiftKey });
370
+ card.focus({ preventScroll: true });
371
+ return;
372
+ }
358
373
  this._activateCard(record, id, index, event);
359
374
  if (this.options.selectable === 'single') this.toggleSelection(id, { selected: true });
360
375
  }
@@ -381,7 +396,8 @@ export class CardView extends RecordView {
381
396
  const id = this._viewRecordId(record);
382
397
  if (event.key === 'Enter') {
383
398
  event.preventDefault();
384
- this._activateCard(record, id, index, event);
399
+ if (this.options.selectable && this._cardOptions().selectionTrigger === 'card') this.toggleSelection(id);
400
+ else this._activateCard(record, id, index, event);
385
401
  } else if (event.key === ' ' && this.options.selectable) {
386
402
  event.preventDefault();
387
403
  this.toggleSelection(id);
@@ -469,11 +485,11 @@ function normalizeGroupId(value) {
469
485
 
470
486
  /** @param {unknown} value @returns {string} */
471
487
  export function normalizeCardWidth(value) {
472
- if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? `${value}px` : '18rem';
488
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? `${value}px` : '15rem';
473
489
  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';
490
+ if (!text || text.length > 128 || /[;{}\u0000-\u001f\u007f]/.test(text)) return '15rem';
491
+ if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text)) return '15rem';
492
+ if (typeof globalThis.CSS?.supports === 'function' && !globalThis.CSS.supports('width', text)) return '15rem';
477
493
  return text;
478
494
  }
479
495