@zeyos/zx 4.1.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/DESIGN-SYSTEM.md +33 -3
  3. package/README.md +22 -5
  4. package/dist/types/components/app-icon/app-icon.d.ts +8 -1
  5. package/dist/types/components/aurora/aurora.d.ts +72 -0
  6. package/dist/types/components/calendar/calendar-model.d.ts +202 -0
  7. package/dist/types/components/calendar/calendar.d.ts +517 -0
  8. package/dist/types/components/card-view/card-view.d.ts +303 -0
  9. package/dist/types/components/card-view/record-card.d.ts +242 -0
  10. package/dist/types/components/dialog/dialog.d.ts +6 -1
  11. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  12. package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
  13. package/dist/types/components/launcher/launcher.d.ts +5 -0
  14. package/dist/types/components/message/message.d.ts +9 -1
  15. package/dist/types/components/modal/modal.d.ts +44 -7
  16. package/dist/types/components/sheet/sheet.d.ts +22 -12
  17. package/dist/types/components/table/table.d.ts +120 -31
  18. package/dist/types/components/table-view/table-view.d.ts +192 -0
  19. package/dist/types/components/view/record-view.d.ts +464 -0
  20. package/dist/types/core/overlay-host.d.ts +12 -0
  21. package/dist/types/index.d.ts +7 -0
  22. package/dist/types/zeyos/calendar.d.ts +91 -0
  23. package/dist/types/zeyos/icons.d.ts +18 -2
  24. package/dist/types/zeyos/index.d.ts +4 -0
  25. package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
  26. package/dist/types/zeyos/saved-views.d.ts +331 -0
  27. package/dist/types/zeyos/view.d.ts +305 -0
  28. package/dist/zx-zeyos.esm.js +4742 -1470
  29. package/dist/zx-zeyos.esm.min.js +1 -1
  30. package/dist/zx.css +2536 -471
  31. package/dist/zx.esm.js +5193 -223
  32. package/dist/zx.esm.min.js +3 -3
  33. package/dist/zx.global.js +5193 -223
  34. package/dist/zx.global.min.js +3 -3
  35. package/dist/zx.min.css +1 -1
  36. package/docs/api.json +1729 -182
  37. package/docs/llms.md +236 -32
  38. package/docs/llms.txt +16 -6
  39. package/package.json +1 -1
  40. package/src/components/account-menu/account-menu.css +1 -1
  41. package/src/components/app-icon/app-icon.css +77 -51
  42. package/src/components/app-icon/app-icon.js +28 -4
  43. package/src/components/app-rail/app-rail.css +12 -3
  44. package/src/components/app-sidebar/app-sidebar.css +11 -2
  45. package/src/components/aurora/aurora.css +143 -0
  46. package/src/components/aurora/aurora.js +146 -0
  47. package/src/components/avatar/avatar.css +1 -1
  48. package/src/components/button/button.css +2 -2
  49. package/src/components/calendar/calendar-model.js +402 -0
  50. package/src/components/calendar/calendar.css +609 -0
  51. package/src/components/calendar/calendar.js +1619 -0
  52. package/src/components/card/card.css +2 -2
  53. package/src/components/card-view/card-view.css +389 -0
  54. package/src/components/card-view/card-view.js +527 -0
  55. package/src/components/card-view/record-card.js +392 -0
  56. package/src/components/context-menu/context-menu.css +4 -4
  57. package/src/components/context-menu/context-menu.js +5 -1
  58. package/src/components/date-picker/date-picker.css +31 -12
  59. package/src/components/datebox/date-range-box.css +17 -2
  60. package/src/components/datebox/datebox.css +18 -3
  61. package/src/components/dialog/dialog.css +4 -3
  62. package/src/components/dialog/dialog.js +2 -1
  63. package/src/components/dropdown/dropdown.css +17 -3
  64. package/src/components/dropdown/dropdown.js +7 -2
  65. package/src/components/kanban-view/kanban-view.css +216 -0
  66. package/src/components/kanban-view/kanban-view.js +1228 -0
  67. package/src/components/launcher/launcher.css +24 -9
  68. package/src/components/launcher/launcher.js +9 -2
  69. package/src/components/master-panel/master-panel.css +14 -3
  70. package/src/components/menu-button/menu-button.css +4 -4
  71. package/src/components/message/message.css +13 -21
  72. package/src/components/message/message.js +17 -11
  73. package/src/components/modal/modal.css +19 -5
  74. package/src/components/modal/modal.js +99 -25
  75. package/src/components/navigation-bar/navigation-bar.css +17 -5
  76. package/src/components/panel/panel.css +10 -2
  77. package/src/components/select/select.css +26 -8
  78. package/src/components/sheet/sheet.css +28 -7
  79. package/src/components/sheet/sheet.js +43 -23
  80. package/src/components/table/table.css +83 -5
  81. package/src/components/table/table.js +322 -37
  82. package/src/components/table-view/table-view.css +18 -0
  83. package/src/components/table-view/table-view.js +347 -0
  84. package/src/components/tag-picker/tag-picker.css +18 -4
  85. package/src/components/tooltip/tooltip.css +17 -3
  86. package/src/components/tooltip/tooltip.js +4 -1
  87. package/src/components/view/record-view.css +114 -0
  88. package/src/components/view/record-view.js +770 -0
  89. package/src/core/overlay-host.js +23 -0
  90. package/src/index.js +15 -0
  91. package/src/zeyos/calendar.js +115 -0
  92. package/src/zeyos/icons.js +11 -4
  93. package/src/zeyos/index.js +13 -0
  94. package/src/zeyos/launcher.js +2 -2
  95. package/src/zeyos/legacy-saved-views.js +155 -0
  96. package/src/zeyos/saved-views.js +815 -0
  97. package/src/zeyos/table.js +3 -1
  98. package/src/zeyos/view.js +543 -0
  99. package/styles/icons.css +3 -10
  100. package/styles/tokens/dark.css +4 -0
  101. package/styles/tokens/semantic.css +142 -3
  102. package/styles/zx.css +6 -0
@@ -0,0 +1,402 @@
1
+ /** @typedef {'agenda'|'day'|'week'|'month'|'year'} CalendarView */
2
+ /** @typedef {string|((record: Record<string, any>) => unknown)} CalendarReader */
3
+
4
+ /**
5
+ * @typedef {Object} CalendarEvent
6
+ * @property {string|number} id Stable event identifier.
7
+ * @property {string} title Visible event title.
8
+ * @property {Date} start Local start date and time.
9
+ * @property {Date} end Local end date and time; equal to start for an instant event.
10
+ * @property {boolean} allDay Whether the event belongs in an all-day lane.
11
+ * @property {string|null} color Optional application-supplied CSS colour.
12
+ * @property {string} location Optional location.
13
+ * @property {boolean|null} editable Per-event move policy, or null to inherit.
14
+ * @property {boolean|null} durationEditable Per-event resize policy, or null to inherit.
15
+ * @property {Record<string, any>} data Original application record.
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} CalendarNormalizeOptions
20
+ * @property {CalendarReader} [eventId='id'] ID reader.
21
+ * @property {CalendarReader} [eventTitle='title'] Title reader.
22
+ * @property {CalendarReader} [eventStart='start'] Start reader.
23
+ * @property {CalendarReader} [eventEnd='end'] End reader.
24
+ * @property {CalendarReader} [eventAllDay='allDay'] All-day reader.
25
+ * @property {CalendarReader} [eventColor='color'] Colour reader.
26
+ * @property {CalendarReader} [eventLocation='location'] Location reader.
27
+ * @property {CalendarReader} [eventEditable='editable'] Move-policy reader.
28
+ * @property {CalendarReader} [eventDurationEditable='durationEditable'] Resize-policy reader.
29
+ * @property {'milliseconds'|'seconds'} [dateUnit='milliseconds'] Unit used for numeric dates.
30
+ */
31
+
32
+ /** @type {readonly CalendarView[]} */
33
+ export const CALENDAR_VIEWS = Object.freeze(['agenda', 'day', 'week', 'month', 'year']);
34
+
35
+ const DAY_MS = 86_400_000;
36
+
37
+ /**
38
+ * Returns the complete local-date window rendered by a view. The end is exclusive.
39
+ * @param {CalendarView} view View name.
40
+ * @param {Date} anchor Date inside the requested period.
41
+ * @param {{weekStart?: number, agendaDays?: number}} [options={}] Range options.
42
+ * @returns {{start: Date, end: Date}}
43
+ */
44
+ export function calendarRange(view, anchor, options = {}) {
45
+ const date = validDate(anchor, 'Calendar date');
46
+ const weekStart = normalizeWeekStart(options.weekStart ?? 1);
47
+ let start;
48
+ let end;
49
+
50
+ if (view === 'agenda') {
51
+ start = startOfCalendarDay(date);
52
+ end = addCalendarDays(start, positiveInteger(options.agendaDays ?? 14, 'agendaDays'));
53
+ } else if (view === 'day') {
54
+ start = startOfCalendarDay(date);
55
+ end = addCalendarDays(start, 1);
56
+ } else if (view === 'week') {
57
+ start = startOfCalendarWeek(date, weekStart);
58
+ end = addCalendarDays(start, 7);
59
+ } else if (view === 'month') {
60
+ const first = new Date(date.getFullYear(), date.getMonth(), 1);
61
+ const last = new Date(date.getFullYear(), date.getMonth() + 1, 0);
62
+ start = startOfCalendarWeek(first, weekStart);
63
+ end = addCalendarDays(startOfCalendarWeek(last, weekStart), 7);
64
+ } else if (view === 'year') {
65
+ start = new Date(date.getFullYear(), 0, 1);
66
+ end = new Date(date.getFullYear() + 1, 0, 1);
67
+ } else {
68
+ throw new RangeError(`Unknown calendar view: ${view}`);
69
+ }
70
+ return { start, end };
71
+ }
72
+
73
+ /**
74
+ * Normalizes caller records atomically into the Calendar event contract.
75
+ * @param {Record<string, any>[]} records Source records.
76
+ * @param {CalendarNormalizeOptions} [options={}] Reader and date options.
77
+ * @returns {CalendarEvent[]}
78
+ */
79
+ export function normalizeCalendarEvents(records, options = {}) {
80
+ if (!Array.isArray(records)) throw new TypeError('Calendar events must be an array');
81
+ const readers = {
82
+ id: options.eventId ?? 'id',
83
+ title: options.eventTitle ?? 'title',
84
+ start: options.eventStart ?? 'start',
85
+ end: options.eventEnd ?? 'end',
86
+ allDay: options.eventAllDay ?? 'allDay',
87
+ color: options.eventColor ?? 'color',
88
+ location: options.eventLocation ?? 'location',
89
+ editable: options.eventEditable ?? 'editable',
90
+ durationEditable: options.eventDurationEditable ?? 'durationEditable'
91
+ };
92
+ const unit = options.dateUnit === 'seconds' ? 'seconds' : 'milliseconds';
93
+ const ids = new Set();
94
+ return records.map((record, index) => {
95
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
96
+ throw new TypeError(`Calendar event at index ${index} must be an object`);
97
+ }
98
+ const id = readCalendarValue(record, readers.id);
99
+ if (id == null || id === '') throw new TypeError(`Calendar event at index ${index} requires an id`);
100
+ const key = String(id);
101
+ if (ids.has(key)) throw new RangeError(`Duplicate Calendar event id: ${key}`);
102
+ ids.add(key);
103
+
104
+ const start = calendarDate(readCalendarValue(record, readers.start), unit, `event ${key} start`);
105
+ const rawEnd = readCalendarValue(record, readers.end);
106
+ const end = rawEnd == null || rawEnd === '' ? new Date(start.getTime())
107
+ : calendarDate(rawEnd, unit, `event ${key} end`);
108
+ if (end < start) throw new RangeError(`Calendar event ${key} ends before it starts`);
109
+
110
+ const title = String(readCalendarValue(record, readers.title) ?? '').trim() || 'Untitled event';
111
+ const location = String(readCalendarValue(record, readers.location) ?? '').trim();
112
+ const editable = optionalBoolean(readCalendarValue(record, readers.editable));
113
+ const durationEditable = optionalBoolean(readCalendarValue(record, readers.durationEditable));
114
+ return {
115
+ id,
116
+ title,
117
+ start,
118
+ end,
119
+ allDay: Boolean(readCalendarValue(record, readers.allDay)),
120
+ color: normalizeCalendarColor(readCalendarValue(record, readers.color)),
121
+ location,
122
+ editable,
123
+ durationEditable,
124
+ data: record
125
+ };
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Returns defensive Date copies while retaining the caller's opaque source record.
131
+ * @param {CalendarEvent} event Event to clone.
132
+ * @returns {CalendarEvent}
133
+ */
134
+ export function cloneCalendarEvent(event) {
135
+ return { ...event, start: new Date(event.start.getTime()), end: new Date(event.end.getTime()) };
136
+ }
137
+
138
+ /**
139
+ * Lays out timed events for one day into collision columns.
140
+ * @param {CalendarEvent[]} events Normalized events.
141
+ * @param {Date} day Local day.
142
+ * @param {{minMinutes?: number, maxMinutes?: number, minimumMinutes?: number}} [options={}] Grid bounds.
143
+ * @returns {Array<{event: CalendarEvent, start: Date, end: Date, startMinutes: number, endMinutes: number, column: number, columns: number}>}
144
+ */
145
+ export function layoutTimedCalendarEvents(events, day, options = {}) {
146
+ const minMinutes = calendarMinutes(options.minMinutes ?? 0, 'minMinutes');
147
+ const maxMinutes = calendarMinutes(options.maxMinutes ?? 1440, 'maxMinutes', true);
148
+ const minimumMinutes = positiveInteger(options.minimumMinutes ?? 30, 'minimumMinutes');
149
+ if (maxMinutes <= minMinutes) throw new RangeError('Calendar maxMinutes must exceed minMinutes');
150
+ const dayStart = startOfCalendarDay(day);
151
+ const gridStart = dateAtMinutes(dayStart, minMinutes);
152
+ const gridEnd = dateAtMinutes(dayStart, maxMinutes);
153
+ const candidates = events.filter((event) => !calendarEventSpansDays(event)
154
+ && eventIntersects(event, gridStart, gridEnd)).map((event) => {
155
+ const start = new Date(Math.max(event.start.getTime(), gridStart.getTime()));
156
+ let end = new Date(Math.min(event.end.getTime(), gridEnd.getTime()));
157
+ if (end <= start) end = new Date(Math.min(gridEnd.getTime(), start.getTime() + minimumMinutes * 60_000));
158
+ return {
159
+ event,
160
+ start,
161
+ end,
162
+ startMinutes: minutesSinceDay(dayStart, start),
163
+ endMinutes: minutesSinceDay(dayStart, end),
164
+ column: 0,
165
+ columns: 1
166
+ };
167
+ }).sort((left, right) => left.start - right.start || right.end - left.end
168
+ || String(left.event.id).localeCompare(String(right.event.id)));
169
+
170
+ /** @type {typeof candidates} */
171
+ let cluster = [];
172
+ let clusterEnd = -Infinity;
173
+ const finishCluster = () => {
174
+ if (!cluster.length) return;
175
+ /** @type {number[]} */
176
+ const activeEnds = [];
177
+ let columnCount = 1;
178
+ for (const item of cluster) {
179
+ let column = activeEnds.findIndex((end) => end <= item.start.getTime());
180
+ if (column === -1) column = activeEnds.length;
181
+ activeEnds[column] = item.end.getTime();
182
+ item.column = column;
183
+ columnCount = Math.max(columnCount, column + 1);
184
+ }
185
+ for (const item of cluster) item.columns = columnCount;
186
+ cluster = [];
187
+ clusterEnd = -Infinity;
188
+ };
189
+
190
+ for (const item of candidates) {
191
+ if (cluster.length && item.start.getTime() >= clusterEnd) finishCluster();
192
+ cluster.push(item);
193
+ clusterEnd = Math.max(clusterEnd, item.end.getTime());
194
+ }
195
+ finishCluster();
196
+ return candidates;
197
+ }
198
+
199
+ /**
200
+ * Assigns clipped all-day/multi-day spans to non-overlapping lanes in a period.
201
+ * @param {CalendarEvent[]} events Normalized events.
202
+ * @param {Date} rangeStart First visible local day.
203
+ * @param {number} days Number of visible days.
204
+ * @returns {Array<{event: CalendarEvent, start: number, end: number, lane: number}>}
205
+ */
206
+ export function layoutCalendarSpans(events, rangeStart, days) {
207
+ const start = startOfCalendarDay(rangeStart);
208
+ const end = addCalendarDays(start, positiveInteger(days, 'days'));
209
+ const spans = events.filter((event) => calendarEventSpansDays(event)
210
+ && eventIntersects(event, start, end)).map((event) => {
211
+ const eventStart = startOfCalendarDay(event.start);
212
+ const eventEnd = calendarEventEndDay(event);
213
+ return {
214
+ event,
215
+ start: Math.max(0, calendarDayDifference(start, eventStart)),
216
+ end: Math.min(days, calendarDayDifference(start, eventEnd)),
217
+ lane: 0
218
+ };
219
+ }).filter((span) => span.end > span.start)
220
+ .sort((left, right) => left.start - right.start || right.end - left.end
221
+ || String(left.event.id).localeCompare(String(right.event.id)));
222
+
223
+ /** @type {number[]} */
224
+ const lanes = [];
225
+ for (const span of spans) {
226
+ let lane = lanes.findIndex((laneEnd) => laneEnd <= span.start);
227
+ if (lane === -1) lane = lanes.length;
228
+ span.lane = lane;
229
+ lanes[lane] = span.end;
230
+ }
231
+ return spans;
232
+ }
233
+
234
+ /**
235
+ * Creates an immutable local-time move/resize proposal.
236
+ * @param {CalendarEvent} event Source event.
237
+ * @param {{days?: number, minutes?: number, endDays?: number, endMinutes?: number}} delta Local delta.
238
+ * @returns {CalendarEvent}
239
+ */
240
+ export function shiftCalendarEvent(event, delta = {}) {
241
+ const days = integer(delta.days ?? 0, 'days');
242
+ const minutes = integer(delta.minutes ?? 0, 'minutes');
243
+ const endDays = integer(delta.endDays ?? days, 'endDays');
244
+ const endMinutes = integer(delta.endMinutes ?? minutes, 'endMinutes');
245
+ const next = cloneCalendarEvent(event);
246
+ next.start = shiftLocalDate(next.start, days, minutes);
247
+ next.end = shiftLocalDate(next.end, endDays, endMinutes);
248
+ if (next.end < next.start) throw new RangeError('Calendar event proposal ends before it starts');
249
+ return next;
250
+ }
251
+
252
+ /** @param {Date} value @returns {Date} */
253
+ export function startOfCalendarDay(value) {
254
+ const date = validDate(value, 'Calendar date');
255
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
256
+ }
257
+
258
+ /** @param {Date} value @param {number} amount @returns {Date} */
259
+ export function addCalendarDays(value, amount) {
260
+ const date = validDate(value, 'Calendar date');
261
+ const next = new Date(date.getTime());
262
+ next.setDate(next.getDate() + integer(amount, 'day amount'));
263
+ return next;
264
+ }
265
+
266
+ /** @param {Date} value @param {number} weekStart @returns {Date} */
267
+ export function startOfCalendarWeek(value, weekStart = 1) {
268
+ const date = startOfCalendarDay(value);
269
+ const normalized = normalizeWeekStart(weekStart);
270
+ return addCalendarDays(date, -((date.getDay() - normalized + 7) % 7));
271
+ }
272
+
273
+ /** @param {Date} start @param {Date} end @returns {number} */
274
+ export function calendarDayDifference(start, end) {
275
+ const left = startOfCalendarDay(start);
276
+ const right = startOfCalendarDay(end);
277
+ const leftUtc = Date.UTC(left.getFullYear(), left.getMonth(), left.getDate());
278
+ const rightUtc = Date.UTC(right.getFullYear(), right.getMonth(), right.getDate());
279
+ return Math.round((rightUtc - leftUtc) / DAY_MS);
280
+ }
281
+
282
+ /** @param {Date} value @returns {string} */
283
+ export function calendarDayKey(value) {
284
+ const date = validDate(value, 'Calendar date');
285
+ const pad = (number) => String(number).padStart(2, '0');
286
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
287
+ }
288
+
289
+ /** @param {CalendarEvent} event @returns {boolean} */
290
+ export function calendarEventSpansDays(event) {
291
+ if (event.allDay) return true;
292
+ if (event.end <= event.start) return false;
293
+ const lastInstant = new Date(event.end.getTime() - 1);
294
+ return calendarDayKey(event.start) !== calendarDayKey(lastInstant);
295
+ }
296
+
297
+ /** @param {CalendarEvent} event @param {Date} start @param {Date} end @returns {boolean} */
298
+ export function calendarEventIntersects(event, start, end) {
299
+ return eventIntersects(event, start, end);
300
+ }
301
+
302
+ /** @param {Date} value @param {number} days @param {number} minutes @returns {Date} */
303
+ function shiftLocalDate(value, days, minutes) {
304
+ const result = new Date(value.getTime());
305
+ result.setDate(result.getDate() + days);
306
+ result.setMinutes(result.getMinutes() + minutes);
307
+ return result;
308
+ }
309
+
310
+ /** @param {CalendarEvent} event @returns {Date} */
311
+ function calendarEventEndDay(event) {
312
+ if (event.end <= event.start) return addCalendarDays(startOfCalendarDay(event.start), 1);
313
+ const endDay = startOfCalendarDay(event.end);
314
+ const exactlyMidnight = event.end.getTime() === endDay.getTime();
315
+ return exactlyMidnight ? endDay : addCalendarDays(endDay, 1);
316
+ }
317
+
318
+ /** @param {CalendarEvent} event @param {Date} start @param {Date} end @returns {boolean} */
319
+ function eventIntersects(event, start, end) {
320
+ const eventEnd = event.end > event.start ? event.end : new Date(event.start.getTime() + 1);
321
+ return event.start < end && eventEnd > start;
322
+ }
323
+
324
+ /** @param {Record<string, any>} record @param {CalendarReader} reader @returns {unknown} */
325
+ function readCalendarValue(record, reader) {
326
+ return typeof reader === 'function' ? reader(record) : record[reader];
327
+ }
328
+
329
+ /** @param {unknown} value @param {'milliseconds'|'seconds'} unit @param {string} label @returns {Date} */
330
+ function calendarDate(value, unit, label) {
331
+ let date;
332
+ if (value instanceof Date) date = new Date(value.getTime());
333
+ else if (typeof value === 'number' || (typeof value === 'string' && /^-?\d+(?:\.\d+)?$/.test(value.trim()))) {
334
+ const numeric = Number(value);
335
+ date = new Date(numeric * (unit === 'seconds' ? 1000 : 1));
336
+ } else date = new Date(/** @type {string} */ (value));
337
+ return validDate(date, label);
338
+ }
339
+
340
+ /** @param {unknown} value @returns {boolean|null} */
341
+ function optionalBoolean(value) {
342
+ return value == null ? null : Boolean(value);
343
+ }
344
+
345
+ /** @param {unknown} value @returns {string|null} */
346
+ function normalizeCalendarColor(value) {
347
+ const color = String(value ?? '').trim();
348
+ if (!color) return null;
349
+ if (color.length > 100 || /[;{}]|url\s*\(|image\s*\(/i.test(color)) return null;
350
+ if (/^#[\da-f]{3,8}$/i.test(color)
351
+ || /^(?:rgb|hsl|hwb|lab|lch|oklab|oklch|color)\([^{};]+\)$/i.test(color)
352
+ || /^var\(--[\w-]+\)$/i.test(color)
353
+ || /^[a-z]+$/i.test(color)) return color;
354
+ return null;
355
+ }
356
+
357
+ /** @param {Date} day @param {number} minutes @returns {Date} */
358
+ function dateAtMinutes(day, minutes) {
359
+ const date = startOfCalendarDay(day);
360
+ date.setMinutes(minutes);
361
+ return date;
362
+ }
363
+
364
+ /** @param {Date} day @param {Date} value @returns {number} */
365
+ function minutesSinceDay(day, value) {
366
+ return (value.getTime() - day.getTime()) / 60_000;
367
+ }
368
+
369
+ /** @param {unknown} value @param {string} label @returns {Date} */
370
+ function validDate(value, label) {
371
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) throw new TypeError(`${label} must be a valid Date`);
372
+ return new Date(value.getTime());
373
+ }
374
+
375
+ /** @param {unknown} value @returns {number} */
376
+ function normalizeWeekStart(value) {
377
+ const number = Number(value);
378
+ if (!Number.isInteger(number) || number < 0 || number > 6) throw new RangeError('weekStart must be from 0 through 6');
379
+ return number;
380
+ }
381
+
382
+ /** @param {unknown} value @param {string} label @returns {number} */
383
+ function integer(value, label) {
384
+ const number = Number(value);
385
+ if (!Number.isInteger(number)) throw new TypeError(`${label} must be an integer`);
386
+ return number;
387
+ }
388
+
389
+ /** @param {unknown} value @param {string} label @returns {number} */
390
+ function positiveInteger(value, label) {
391
+ const number = integer(value, label);
392
+ if (number <= 0) throw new RangeError(`${label} must be greater than zero`);
393
+ return number;
394
+ }
395
+
396
+ /** @param {unknown} value @param {string} label @param {boolean} [allowEnd=false] @returns {number} */
397
+ function calendarMinutes(value, label, allowEnd = false) {
398
+ const number = integer(value, label);
399
+ const max = allowEnd ? 1440 : 1439;
400
+ if (number < 0 || number > max) throw new RangeError(`${label} must be from 0 through ${max}`);
401
+ return number;
402
+ }