@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.
- package/CHANGELOG.md +266 -1
- package/README.md +7 -3
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +195 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
- package/dist/types/components/button/button.d.ts +8 -0
- package/dist/types/components/calendar/calendar-model.d.ts +22 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +12 -4
- package/dist/types/components/card-view/record-card.d.ts +53 -5
- package/dist/types/components/code-editor/code-editor.d.ts +160 -0
- package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +18 -2
- package/dist/types/components/date-picker/date-picker.d.ts +8 -0
- package/dist/types/components/date-picker/time-picker.d.ts +7 -0
- package/dist/types/components/dialog/dialog.d.ts +29 -0
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/file-item/file-item.d.ts +234 -0
- package/dist/types/components/file-list/file-list.d.ts +171 -0
- package/dist/types/components/filter/filter.d.ts +91 -36
- package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
- package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
- package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
- package/dist/types/components/launcher/launcher.d.ts +19 -12
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +4 -37
- package/dist/types/components/message/message.d.ts +7 -0
- package/dist/types/components/modal/modal.d.ts +48 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
- package/dist/types/components/panel/panel.d.ts +7 -0
- package/dist/types/components/search/search.d.ts +7 -0
- package/dist/types/components/select/select.d.ts +4 -0
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
- package/dist/types/components/tabbox/tabbox.d.ts +7 -0
- package/dist/types/components/table/table.d.ts +144 -0
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/value-list/value-list.d.ts +8 -0
- package/dist/types/components/view/record-view.d.ts +96 -5
- package/dist/types/core/component.d.ts +6 -1
- package/dist/types/core/icons.d.ts +17 -0
- package/dist/types/index.d.ts +13 -1
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/zx-zeyos.esm.js +651 -88
- package/dist/zx-zeyos.esm.min.js +8 -1
- package/dist/zx.css +6266 -3900
- package/dist/zx.esm.js +8487 -1805
- package/dist/zx.esm.min.js +10 -3
- package/dist/zx.global.js +8487 -1805
- package/dist/zx.global.min.js +10 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1811 -247
- package/docs/llms.md +345 -40
- package/docs/llms.txt +11 -7
- package/package.json +1 -2
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +599 -0
- package/src/components/activity-list/activity-list.css +64 -0
- package/src/components/activity-list/activity-list.js +375 -0
- package/src/components/app-rail/app-rail.css +227 -19
- package/src/components/app-sidebar/app-sidebar.css +414 -24
- package/src/components/app-sidebar/app-sidebar.js +65 -5
- package/src/components/button/button.css +8 -0
- package/src/components/button/button.js +6 -1
- package/src/components/calendar/calendar-model.js +45 -0
- package/src/components/calendar/calendar.css +30 -10
- package/src/components/calendar/calendar.js +41 -13
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +212 -9
- package/src/components/card-view/card-view.js +32 -16
- package/src/components/card-view/record-card.js +98 -21
- package/src/components/code-editor/code-editor.css +67 -0
- package/src/components/code-editor/code-editor.js +288 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +80 -3
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/dialog/dialog.js +88 -66
- package/src/components/dropdown/dropdown.js +2 -2
- package/src/components/entity-ref/entity-ref.css +182 -0
- package/src/components/entity-ref/entity-ref.js +496 -0
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +371 -0
- package/src/components/file-list/file-list.css +51 -0
- package/src/components/file-list/file-list.js +320 -0
- package/src/components/filter/filter.css +27 -1
- package/src/components/filter/filter.js +199 -45
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/kanban-view/kanban-card.js +239 -0
- package/src/components/kanban-view/kanban-policy.js +412 -0
- package/src/components/kanban-view/kanban-view.css +674 -36
- package/src/components/kanban-view/kanban-view.js +1762 -240
- package/src/components/launcher/launcher.js +59 -27
- package/src/components/list-toolbar/list-toolbar.css +189 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/menu-button/menu-button.css +80 -3
- package/src/components/menu-button/menu-button.js +21 -37
- package/src/components/message/message.css +14 -12
- package/src/components/message/message.js +13 -2
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +141 -0
- package/src/components/navigation-bar/navigation-bar.css +77 -2
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/panel/panel.js +13 -2
- package/src/components/search/search.js +17 -5
- package/src/components/select/field-adapter.js +4 -1
- package/src/components/select/select.js +3 -0
- package/src/components/sort-control/sort-control.css +17 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +213 -0
- package/src/components/stat-tile/stat-tile.js +590 -0
- package/src/components/tabbox/tabbox.js +12 -1
- package/src/components/table/table.css +51 -2
- package/src/components/table/table.js +298 -19
- package/src/components/table-view/table-view.css +8 -2
- package/src/components/table-view/table-view.js +21 -6
- package/src/components/tag-picker/tag-picker.css +3 -1
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/value-list/value-list.js +22 -6
- package/src/components/view/record-view.css +5 -0
- package/src/components/view/record-view.js +100 -7
- package/src/core/component.js +8 -3
- package/src/core/icons.js +74 -5
- package/src/index.js +33 -3
- package/src/internal/app-rail.js +19 -1
- package/src/internal/menu-items.js +142 -0
- package/styles/print.css +192 -0
- package/styles/zx.css +11 -0
- 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:
|
|
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
|
|
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 {
|
|
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 {
|
|
63
|
-
|
|
64
|
-
|
|
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-
|
|
69
|
-
background: var(--zx-color-bg-
|
|
70
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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:
|
|
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-
|
|
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-
|
|
55
|
+
margin: 0 0 var(--zx-space-2);
|
|
52
56
|
color: var(--zx-color-text);
|
|
53
|
-
font-size: var(--zx-text-
|
|
54
|
-
|
|
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-
|
|
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-
|
|
62
|
-
gap: var(--zx-
|
|
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[
|
|
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]
|
|
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='
|
|
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: '
|
|
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)
|
|
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', {
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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-
|
|
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.
|
|
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` : '
|
|
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 '
|
|
475
|
-
if (/(?:url|image|image-set|cross-fade|element|expression)\s*\(/i.test(text)) return '
|
|
476
|
-
if (typeof globalThis.CSS?.supports === 'function' && !globalThis.CSS.supports('width', text)) return '
|
|
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
|
|