@joewinke/jatui 0.41.1 → 0.43.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/CLAUDE.md +32 -6
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/actions/dismissOutside.d.ts +52 -0
- package/dist/actions/dismissOutside.js +80 -0
- package/dist/components/BookingScheduler.svelte +412 -0
- package/dist/components/BookingScheduler.svelte.d.ts +66 -0
- package/dist/components/CalendarPicker.svelte +190 -205
- package/dist/components/CalendarPicker.svelte.d.ts +19 -1
- package/dist/components/DatePicker.svelte +419 -0
- package/dist/components/DatePicker.svelte.d.ts +44 -0
- package/dist/components/DateRangePicker.svelte +471 -5
- package/dist/components/DateRangePicker.svelte.d.ts +32 -3
- package/dist/components/EventCalendar.svelte +1324 -0
- package/dist/components/EventCalendar.svelte.d.ts +101 -0
- package/dist/components/FileDropzone.svelte +165 -3
- package/dist/components/FileDropzone.svelte.d.ts +9 -0
- package/dist/components/TextInput.svelte +71 -20
- package/dist/components/TextInput.svelte.d.ts +7 -0
- package/dist/components/ThumbnailGrid.svelte +106 -1
- package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
- package/dist/components/TimeSlotPicker.svelte +174 -108
- package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
- package/dist/components/basic/Alert.svelte +113 -16
- package/dist/components/basic/Alert.svelte.d.ts +17 -1
- package/dist/components/basic/Checkbox.svelte +53 -1
- package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
- package/dist/components/basic/RadioGroup.svelte +59 -3
- package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
- package/dist/components/basic/Slider.svelte +257 -9
- package/dist/components/basic/Slider.svelte.d.ts +14 -2
- package/dist/components/basic/sliderMath.d.ts +29 -0
- package/dist/components/basic/sliderMath.js +64 -0
- package/dist/components/calendar/CalendarHeader.svelte +98 -0
- package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
- package/dist/components/calendar/MonthGrid.svelte +361 -0
- package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
- package/dist/components/calendar/calendarGrid.d.ts +219 -0
- package/dist/components/calendar/calendarGrid.js +494 -0
- package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
- package/dist/components/calendar/createCalendarGrid.js +57 -0
- package/dist/components/calendar/dayOverlap.d.ts +92 -0
- package/dist/components/calendar/dayOverlap.js +147 -0
- package/dist/components/calendar/eventCalendar.d.ts +75 -0
- package/dist/components/calendar/eventCalendar.js +187 -0
- package/dist/components/calendar/index.d.ts +32 -0
- package/dist/components/calendar/index.js +25 -0
- package/dist/components/calendar/monthPacking.d.ts +97 -0
- package/dist/components/calendar/monthPacking.js +254 -0
- package/dist/components/calendar/timeSlots.d.ts +57 -0
- package/dist/components/calendar/timeSlots.js +163 -0
- package/dist/components/field/FieldHint.svelte +29 -0
- package/dist/components/field/FieldHint.svelte.d.ts +17 -0
- package/dist/components/field/FieldLabel.svelte +100 -0
- package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
- package/dist/components/form-cards/CheckGlyph.svelte +138 -0
- package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
- package/dist/components/form-cards/CheckboxCard.svelte +120 -0
- package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
- package/dist/components/form-cards/ChoiceCard.svelte +156 -0
- package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
- package/dist/components/form-cards/SwitchCard.svelte +114 -0
- package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
- package/dist/components/toast/ToastHost.svelte +56 -3
- package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
- package/dist/components/toast/ToastItem.svelte +75 -23
- package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
- package/dist/components/uploadProgress.d.ts +30 -0
- package/dist/components/uploadProgress.js +47 -0
- package/dist/index.d.ts +29 -2
- package/dist/index.js +22 -1
- package/dist/stores/toast.d.ts +35 -0
- package/dist/stores/toast.js +15 -1
- package/dist/styles/motion.css +72 -0
- package/dist/styles/rail.css +51 -0
- package/package.json +6 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Week/day-view overlap packing.
|
|
3
|
+
*
|
|
4
|
+
* Upstreamed from MOS `lib/components/calendar/shared/dayOverlap.ts`
|
|
5
|
+
* (jatui-lfxyy) with its tests; generic over any `PackableEvent`.
|
|
6
|
+
*
|
|
7
|
+
* Month view stacks intervals across seven discrete day columns into vertical
|
|
8
|
+
* lanes (`monthPacking.ts`). Week and day view have the same interval-graph
|
|
9
|
+
* problem on the other axis: events run over continuous time inside ONE day
|
|
10
|
+
* column, so concurrent events have to be sliced into side-by-side
|
|
11
|
+
* sub-columns. Same shape, different axis — which is why this is a sibling
|
|
12
|
+
* module rather than a mode flag on `packWeek`.
|
|
13
|
+
*
|
|
14
|
+
* The allocator hands out a horizontal slot for every timed event and the grid
|
|
15
|
+
* renders it as a percentage `left`/`width`, so two meetings at 2pm sit beside
|
|
16
|
+
* each other instead of one hiding the other.
|
|
17
|
+
*
|
|
18
|
+
* The intervals compared here are RENDERED extents, not literal ones. The grid
|
|
19
|
+
* gives an event with no end a one-hour block and never draws anything shorter
|
|
20
|
+
* than half an hour, so two five-minute events ten minutes apart really do
|
|
21
|
+
* collide on screen even though their times do not. Packing the literal times
|
|
22
|
+
* would call that pair disjoint and stack them back on top of each other.
|
|
23
|
+
*/
|
|
24
|
+
import { parseLocalDate } from "./monthPacking";
|
|
25
|
+
const MS_PER_MIN = 60_000;
|
|
26
|
+
/** Grid block for an event with no end — matches `eventHeightPx`'s 48px. */
|
|
27
|
+
export const DEFAULT_DURATION_MIN = 60;
|
|
28
|
+
/** Shortest block the grid will draw — matches `eventHeightPx`'s 24px floor. */
|
|
29
|
+
export const MIN_DURATION_MIN = 30;
|
|
30
|
+
/**
|
|
31
|
+
* The vertical extent the grid will actually draw for `ev`, in minutes.
|
|
32
|
+
*
|
|
33
|
+
* `start` is read in local time and measured from its own midnight, so an
|
|
34
|
+
* event running past midnight simply reports an `endMin` above 1440 rather
|
|
35
|
+
* than wrapping — the comparisons below only ever need it to be monotonic.
|
|
36
|
+
*/
|
|
37
|
+
export function eventInterval(ev, opts = {}) {
|
|
38
|
+
const defaultDuration = opts.defaultDurationMin ?? DEFAULT_DURATION_MIN;
|
|
39
|
+
const minDuration = opts.minDurationMin ?? MIN_DURATION_MIN;
|
|
40
|
+
const start = parseLocalDate(ev.start);
|
|
41
|
+
const startMin = start.getHours() * 60 + start.getMinutes();
|
|
42
|
+
let duration = defaultDuration;
|
|
43
|
+
if (ev.end) {
|
|
44
|
+
const end = parseLocalDate(ev.end);
|
|
45
|
+
const ms = end.getTime() - start.getTime();
|
|
46
|
+
// A missing, unparseable or backwards end is no information at all, so
|
|
47
|
+
// fall back to the same default block a null end gets.
|
|
48
|
+
if (Number.isFinite(ms) && ms > 0)
|
|
49
|
+
duration = ms / MS_PER_MIN;
|
|
50
|
+
}
|
|
51
|
+
return { startMin, endMin: startMin + Math.max(duration, minDuration) };
|
|
52
|
+
}
|
|
53
|
+
/** Half-open overlap: two blocks that merely touch do NOT overlap. */
|
|
54
|
+
export function intervalsOverlap(a, b) {
|
|
55
|
+
return a.startMin < b.endMin && b.startMin < a.endMin;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Split one day's timed events into side-by-side sub-columns.
|
|
59
|
+
*
|
|
60
|
+
* Three passes over the events sorted by start time:
|
|
61
|
+
*
|
|
62
|
+
* 1. **Cluster.** Walk in start order carrying the furthest end seen; an event
|
|
63
|
+
* starting at or after that end can share no space with anything before it,
|
|
64
|
+
* so it opens a new cluster. This is exactly the transitive closure of
|
|
65
|
+
* "overlaps" — A and C never touching still share a cluster if B bridges
|
|
66
|
+
* them, and they must, or the two halves would be scaled differently and
|
|
67
|
+
* B would collide with one of them.
|
|
68
|
+
* 2. **Assign columns.** Greedily take the first sub-column whose last event
|
|
69
|
+
* has already ended. On an interval graph this uses exactly as many columns
|
|
70
|
+
* as the busiest instant needs — no cluster is ever sliced thinner than the
|
|
71
|
+
* real peak concurrency.
|
|
72
|
+
* 3. **Widen.** Let each event grow rightward while no event in the next
|
|
73
|
+
* sub-column overlaps it, so a lone 4pm event beside a busy 2pm stack still
|
|
74
|
+
* reads full width. This cannot reintroduce a collision: for A to widen
|
|
75
|
+
* past column c it must be disjoint from everything in column c, and any B
|
|
76
|
+
* widening out of column c is in column c.
|
|
77
|
+
*/
|
|
78
|
+
export function layoutDay(events, opts = {}) {
|
|
79
|
+
const expand = opts.expand ?? true;
|
|
80
|
+
const items = events.map((ev) => ({
|
|
81
|
+
ev,
|
|
82
|
+
...eventInterval(ev, opts),
|
|
83
|
+
cluster: 0,
|
|
84
|
+
column: 0,
|
|
85
|
+
span: 1,
|
|
86
|
+
}));
|
|
87
|
+
// Earliest first, then longest, then title/id — so the same day always packs
|
|
88
|
+
// the same way regardless of the order the events arrived in.
|
|
89
|
+
items.sort((a, b) => a.startMin - b.startMin ||
|
|
90
|
+
b.endMin - a.endMin ||
|
|
91
|
+
a.ev.title.localeCompare(b.ev.title) ||
|
|
92
|
+
a.ev.id.localeCompare(b.ev.id));
|
|
93
|
+
const clusters = [];
|
|
94
|
+
let current = [];
|
|
95
|
+
let clusterEnd = -Infinity;
|
|
96
|
+
for (const it of items) {
|
|
97
|
+
if (current.length > 0 && it.startMin >= clusterEnd) {
|
|
98
|
+
clusters.push(current);
|
|
99
|
+
current = [];
|
|
100
|
+
clusterEnd = -Infinity;
|
|
101
|
+
}
|
|
102
|
+
it.cluster = clusters.length;
|
|
103
|
+
current.push(it);
|
|
104
|
+
clusterEnd = Math.max(clusterEnd, it.endMin);
|
|
105
|
+
}
|
|
106
|
+
if (current.length > 0)
|
|
107
|
+
clusters.push(current);
|
|
108
|
+
const placements = [];
|
|
109
|
+
for (const cluster of clusters) {
|
|
110
|
+
// Last end time per sub-column. Within a column every event starts at or
|
|
111
|
+
// after the previous one ended, so this is that column's max end.
|
|
112
|
+
const columnEnds = [];
|
|
113
|
+
for (const it of cluster) {
|
|
114
|
+
let c = 0;
|
|
115
|
+
while (c < columnEnds.length && columnEnds[c] > it.startMin)
|
|
116
|
+
c++;
|
|
117
|
+
it.column = c;
|
|
118
|
+
columnEnds[c] = it.endMin;
|
|
119
|
+
}
|
|
120
|
+
const columns = columnEnds.length;
|
|
121
|
+
const unit = 100 / columns;
|
|
122
|
+
for (const it of cluster) {
|
|
123
|
+
let span = 1;
|
|
124
|
+
if (expand) {
|
|
125
|
+
for (let c = it.column + 1; c < columns; c++) {
|
|
126
|
+
const blocked = cluster.some((other) => other.column === c && intervalsOverlap(other, it));
|
|
127
|
+
if (blocked)
|
|
128
|
+
break;
|
|
129
|
+
span++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
it.span = span;
|
|
133
|
+
placements.push({
|
|
134
|
+
ev: it.ev,
|
|
135
|
+
startMin: it.startMin,
|
|
136
|
+
endMin: it.endMin,
|
|
137
|
+
cluster: it.cluster,
|
|
138
|
+
column: it.column,
|
|
139
|
+
columns,
|
|
140
|
+
span,
|
|
141
|
+
leftPct: it.column * unit,
|
|
142
|
+
widthPct: span * unit,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return placements;
|
|
147
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventCalendar — the pure layer.
|
|
3
|
+
*
|
|
4
|
+
* Everything the month / week / day / agenda views compute that is not
|
|
5
|
+
* markup: event colour tokens, view titles and stepping, the visible range a
|
|
6
|
+
* host should load, the month and week matrices with events placed on them
|
|
7
|
+
* (lane packing is `monthPacking.ts`, timed overlap is `dayOverlap.ts`, both
|
|
8
|
+
* upstreamed from MOS), and the agenda grouping.
|
|
9
|
+
*/
|
|
10
|
+
import { type Weekday } from './calendarGrid';
|
|
11
|
+
import { type MonthDay, type PackableEvent } from './monthPacking';
|
|
12
|
+
import type { HourCycle } from './timeSlots';
|
|
13
|
+
/** A jatui event hue (1–6 → `--jatui-event-N`) or a DaisyUI semantic name. */
|
|
14
|
+
export type EventColor = 1 | 2 | 3 | 4 | 5 | 6 | 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'neutral';
|
|
15
|
+
export interface CalendarEventItem extends PackableEvent {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
/** ISO datetime, `YYYY-MM-DD` for an all-day day, or a Date. */
|
|
19
|
+
start: string | Date;
|
|
20
|
+
/** Inclusive last day for all-day events; the end instant for timed ones. */
|
|
21
|
+
end?: string | Date | null;
|
|
22
|
+
allDay?: boolean;
|
|
23
|
+
color?: EventColor;
|
|
24
|
+
description?: string;
|
|
25
|
+
location?: string;
|
|
26
|
+
/** Set false to pin this event when drag-to-move is on. */
|
|
27
|
+
draggable?: boolean;
|
|
28
|
+
/** Anything the host wants back in callbacks. */
|
|
29
|
+
meta?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export type EventCalendarView = 'month' | 'week' | 'day' | 'agenda';
|
|
32
|
+
/**
|
|
33
|
+
* Inline custom properties for one event: `--ev` (the hue: dots, stripes),
|
|
34
|
+
* `--ev-bg` (chip fill) and `--ev-fg` (time / secondary text). All from the
|
|
35
|
+
* `--jatui-event-*` triplets in tokens.css, so they flip with the theme.
|
|
36
|
+
* Titles stay `base-content` on the tinted fill, which keeps contrast in
|
|
37
|
+
* every theme (a saturated fill with `-content` text does not).
|
|
38
|
+
*/
|
|
39
|
+
export declare function eventColorStyle(color: EventColor | undefined): string;
|
|
40
|
+
/** Move the focus date one view-step (month, week, day; agenda pages by month). */
|
|
41
|
+
export declare function stepDate(view: EventCalendarView, date: Date, dir: -1 | 1): Date;
|
|
42
|
+
/** The first and last day a view shows — what a host should load events for. */
|
|
43
|
+
export declare function visibleRange(view: EventCalendarView, date: Date, weekStart?: Weekday): {
|
|
44
|
+
start: Date;
|
|
45
|
+
end: Date;
|
|
46
|
+
};
|
|
47
|
+
/** "March 2026", "Mar 1 – 7, 2026", "Wednesday, March 4, 2026". */
|
|
48
|
+
export declare function viewTitle(view: EventCalendarView, date: Date, weekStart?: Weekday, locale?: string): string;
|
|
49
|
+
/** True when an event belongs on the all-day row / a filled bar (all-day or crossing midnight). */
|
|
50
|
+
export declare function isAllDayLike(ev: PackableEvent): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Month weeks with every event recorded on each day it touches (the week
|
|
53
|
+
* packer collapses the repeats into one spanning segment).
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildMonthWeeks<E extends PackableEvent>(date: Date, weekStart: Weekday, events: E[]): MonthDay<E>[][];
|
|
56
|
+
/** Seven days from the week start, each with its all-day-like and timed events. */
|
|
57
|
+
export interface TimeGridDay<E extends PackableEvent> extends MonthDay<E> {
|
|
58
|
+
timed: E[];
|
|
59
|
+
}
|
|
60
|
+
export declare function buildTimeGridDays<E extends PackableEvent>(days: Date[], events: E[]): TimeGridDay<E>[];
|
|
61
|
+
export declare function weekDates(date: Date, weekStart: Weekday): Date[];
|
|
62
|
+
/** Agenda: the days in [start, end] that have events, each sorted all-day first then by time. */
|
|
63
|
+
export declare function agendaDays<E extends PackableEvent>(events: E[], start: Date, end: Date): {
|
|
64
|
+
date: Date;
|
|
65
|
+
events: E[];
|
|
66
|
+
}[];
|
|
67
|
+
/** Compact chip time: "9a" / "9:30a" (h12) or "09:30" (h23). Empty for all-day. */
|
|
68
|
+
export declare function chipTime(ev: PackableEvent, hourCycle?: HourCycle): string;
|
|
69
|
+
/** "Mon, Aug 10 · 9:00 AM – 10:30 AM", "Aug 10 – Aug 16", "Mon, Aug 10 · All day". */
|
|
70
|
+
export declare function eventWhen(ev: PackableEvent, opts?: {
|
|
71
|
+
hourCycle?: HourCycle;
|
|
72
|
+
locale?: string;
|
|
73
|
+
}): string;
|
|
74
|
+
/** Hour-row label: "6 AM" (h12) / "06:00" (h23). */
|
|
75
|
+
export declare function hourLabel(h: number, hourCycle?: HourCycle): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventCalendar — the pure layer.
|
|
3
|
+
*
|
|
4
|
+
* Everything the month / week / day / agenda views compute that is not
|
|
5
|
+
* markup: event colour tokens, view titles and stepping, the visible range a
|
|
6
|
+
* host should load, the month and week matrices with events placed on them
|
|
7
|
+
* (lane packing is `monthPacking.ts`, timed overlap is `dayOverlap.ts`, both
|
|
8
|
+
* upstreamed from MOS), and the agenda grouping.
|
|
9
|
+
*/
|
|
10
|
+
import { addDays, addMonths, buildMonthMatrix, endOfMonth, formatMonthYear, isSameDay, startOfDay, startOfMonth, startOfWeek } from './calendarGrid';
|
|
11
|
+
import { eventDayRange, parseLocalDate } from './monthPacking';
|
|
12
|
+
// ── Colour ─────────────────────────────────────────────────────────────────
|
|
13
|
+
const SEMANTIC_HUE = { primary: 1, secondary: 2, accent: 3, info: 4, success: 5, warning: 6 };
|
|
14
|
+
/**
|
|
15
|
+
* Inline custom properties for one event: `--ev` (the hue: dots, stripes),
|
|
16
|
+
* `--ev-bg` (chip fill) and `--ev-fg` (time / secondary text). All from the
|
|
17
|
+
* `--jatui-event-*` triplets in tokens.css, so they flip with the theme.
|
|
18
|
+
* Titles stay `base-content` on the tinted fill, which keeps contrast in
|
|
19
|
+
* every theme (a saturated fill with `-content` text does not).
|
|
20
|
+
*/
|
|
21
|
+
export function eventColorStyle(color) {
|
|
22
|
+
if (color === 'error') {
|
|
23
|
+
return [
|
|
24
|
+
'--ev: var(--jatui-status-negative, var(--color-error))',
|
|
25
|
+
'--ev-bg: color-mix(in oklab, var(--color-error) 16%, var(--color-base-100))',
|
|
26
|
+
'--ev-fg: color-mix(in oklab, var(--color-error) 55%, var(--color-base-content))'
|
|
27
|
+
].join('; ');
|
|
28
|
+
}
|
|
29
|
+
if (color === 'neutral') {
|
|
30
|
+
return [
|
|
31
|
+
'--ev: color-mix(in oklab, var(--color-base-content) 45%, transparent)',
|
|
32
|
+
'--ev-bg: color-mix(in oklab, var(--color-base-content) 9%, var(--color-base-100))',
|
|
33
|
+
'--ev-fg: color-mix(in oklab, var(--color-base-content) 65%, transparent)'
|
|
34
|
+
].join('; ');
|
|
35
|
+
}
|
|
36
|
+
const n = typeof color === 'number' ? Math.min(6, Math.max(1, Math.round(color))) : (SEMANTIC_HUE[color ?? 'primary'] ?? 1);
|
|
37
|
+
return `--ev: var(--jatui-event-${n}); --ev-bg: var(--jatui-event-${n}-bg); --ev-fg: var(--jatui-event-${n}-fg)`;
|
|
38
|
+
}
|
|
39
|
+
// ── Views ──────────────────────────────────────────────────────────────────
|
|
40
|
+
/** Move the focus date one view-step (month, week, day; agenda pages by month). */
|
|
41
|
+
export function stepDate(view, date, dir) {
|
|
42
|
+
switch (view) {
|
|
43
|
+
case 'week':
|
|
44
|
+
return addDays(date, 7 * dir);
|
|
45
|
+
case 'day':
|
|
46
|
+
return addDays(date, dir);
|
|
47
|
+
default:
|
|
48
|
+
return addMonths(date, dir);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The first and last day a view shows — what a host should load events for. */
|
|
52
|
+
export function visibleRange(view, date, weekStart = 0) {
|
|
53
|
+
switch (view) {
|
|
54
|
+
case 'week': {
|
|
55
|
+
const start = startOfWeek(date, weekStart);
|
|
56
|
+
return { start, end: addDays(start, 6) };
|
|
57
|
+
}
|
|
58
|
+
case 'day':
|
|
59
|
+
return { start: startOfDay(date), end: startOfDay(date) };
|
|
60
|
+
case 'agenda':
|
|
61
|
+
return { start: startOfMonth(date), end: endOfMonth(date) };
|
|
62
|
+
default: {
|
|
63
|
+
const weeks = buildMonthMatrix(date, weekStart);
|
|
64
|
+
return { start: weeks[0][0].date, end: weeks[weeks.length - 1][6].date };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** "March 2026", "Mar 1 – 7, 2026", "Wednesday, March 4, 2026". */
|
|
69
|
+
export function viewTitle(view, date, weekStart = 0, locale) {
|
|
70
|
+
if (view === 'day') {
|
|
71
|
+
return new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }).format(date);
|
|
72
|
+
}
|
|
73
|
+
if (view === 'week') {
|
|
74
|
+
const { start, end } = visibleRange('week', date, weekStart);
|
|
75
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
76
|
+
return typeof fmt.formatRange === 'function' ? fmt.formatRange(start, end) : `${fmt.format(start)} – ${fmt.format(end)}`;
|
|
77
|
+
}
|
|
78
|
+
return formatMonthYear(date, locale);
|
|
79
|
+
}
|
|
80
|
+
// ── Placement ──────────────────────────────────────────────────────────────
|
|
81
|
+
/** True when an event belongs on the all-day row / a filled bar (all-day or crossing midnight). */
|
|
82
|
+
export function isAllDayLike(ev) {
|
|
83
|
+
if (ev.allDay)
|
|
84
|
+
return true;
|
|
85
|
+
const [s, e] = eventDayRange(ev);
|
|
86
|
+
return !isSameDay(s, e);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Month weeks with every event recorded on each day it touches (the week
|
|
90
|
+
* packer collapses the repeats into one spanning segment).
|
|
91
|
+
*/
|
|
92
|
+
export function buildMonthWeeks(date, weekStart, events) {
|
|
93
|
+
const matrix = buildMonthMatrix(date, weekStart);
|
|
94
|
+
const weeks = matrix.map((w) => w.map((c) => ({ date: c.date, inMonth: c.inMonth, events: [] })));
|
|
95
|
+
const first = weeks[0][0].date;
|
|
96
|
+
const last = weeks[weeks.length - 1][6].date;
|
|
97
|
+
for (const ev of events) {
|
|
98
|
+
const [s, e] = eventDayRange(ev);
|
|
99
|
+
if (e < first || s > last)
|
|
100
|
+
continue;
|
|
101
|
+
for (const week of weeks) {
|
|
102
|
+
for (const day of week) {
|
|
103
|
+
if (day.date >= s && day.date <= e)
|
|
104
|
+
day.events.push(ev);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return weeks;
|
|
109
|
+
}
|
|
110
|
+
export function buildTimeGridDays(days, events) {
|
|
111
|
+
const out = days.map((date) => ({ date, inMonth: true, events: [], timed: [] }));
|
|
112
|
+
for (const ev of events) {
|
|
113
|
+
if (isAllDayLike(ev)) {
|
|
114
|
+
const [s, e] = eventDayRange(ev);
|
|
115
|
+
for (const d of out)
|
|
116
|
+
if (d.date >= s && d.date <= e)
|
|
117
|
+
d.events.push(ev);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const start = parseLocalDate(ev.start);
|
|
121
|
+
const d = out.find((x) => isSameDay(x.date, start));
|
|
122
|
+
d?.timed.push(ev);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
export function weekDates(date, weekStart) {
|
|
128
|
+
const start = startOfWeek(date, weekStart);
|
|
129
|
+
return Array.from({ length: 7 }, (_, i) => addDays(start, i));
|
|
130
|
+
}
|
|
131
|
+
/** Agenda: the days in [start, end] that have events, each sorted all-day first then by time. */
|
|
132
|
+
export function agendaDays(events, start, end) {
|
|
133
|
+
const out = [];
|
|
134
|
+
for (let d = startOfDay(start); d <= end; d = addDays(d, 1)) {
|
|
135
|
+
const onDay = events.filter((ev) => {
|
|
136
|
+
const [s, e] = eventDayRange(ev);
|
|
137
|
+
return d >= s && d <= e;
|
|
138
|
+
});
|
|
139
|
+
if (onDay.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
onDay.sort((a, b) => {
|
|
142
|
+
if (!!a.allDay !== !!b.allDay)
|
|
143
|
+
return a.allDay ? -1 : 1;
|
|
144
|
+
return parseLocalDate(a.start).getTime() - parseLocalDate(b.start).getTime() || a.title.localeCompare(b.title);
|
|
145
|
+
});
|
|
146
|
+
out.push({ date: d, events: onDay });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
// ── Labels ─────────────────────────────────────────────────────────────────
|
|
151
|
+
/** Compact chip time: "9a" / "9:30a" (h12) or "09:30" (h23). Empty for all-day. */
|
|
152
|
+
export function chipTime(ev, hourCycle = 'h12') {
|
|
153
|
+
if (ev.allDay)
|
|
154
|
+
return '';
|
|
155
|
+
const d = parseLocalDate(ev.start);
|
|
156
|
+
const h = d.getHours();
|
|
157
|
+
const m = d.getMinutes();
|
|
158
|
+
if (hourCycle === 'h23')
|
|
159
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
|
160
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
161
|
+
const suffix = h < 12 ? 'a' : 'p';
|
|
162
|
+
return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
|
|
163
|
+
}
|
|
164
|
+
/** "Mon, Aug 10 · 9:00 AM – 10:30 AM", "Aug 10 – Aug 16", "Mon, Aug 10 · All day". */
|
|
165
|
+
export function eventWhen(ev, opts = {}) {
|
|
166
|
+
const { hourCycle = 'h12', locale } = opts;
|
|
167
|
+
const [s, e] = eventDayRange(ev);
|
|
168
|
+
const day = (d) => new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(d);
|
|
169
|
+
if (!isSameDay(s, e))
|
|
170
|
+
return `${day(s)} – ${day(e)}`;
|
|
171
|
+
if (ev.allDay)
|
|
172
|
+
return `${day(s)} · All day`;
|
|
173
|
+
const t = (v) => new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit', hourCycle }).format(parseLocalDate(v));
|
|
174
|
+
return ev.end ? `${day(s)} · ${t(ev.start)} – ${t(ev.end)}` : `${day(s)} · ${t(ev.start)}`;
|
|
175
|
+
}
|
|
176
|
+
/** Hour-row label: "6 AM" (h12) / "06:00" (h23). */
|
|
177
|
+
export function hourLabel(h, hourCycle = 'h12') {
|
|
178
|
+
if (hourCycle === 'h23')
|
|
179
|
+
return `${String(h).padStart(2, '0')}:00`;
|
|
180
|
+
if (h === 0)
|
|
181
|
+
return '12 AM';
|
|
182
|
+
if (h < 12)
|
|
183
|
+
return `${h} AM`;
|
|
184
|
+
if (h === 12)
|
|
185
|
+
return '12 PM';
|
|
186
|
+
return `${h - 12} PM`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jatui/calendar — the dates + calendar surface (BoardUI wave 4, jatui-lfxyy).
|
|
3
|
+
*
|
|
4
|
+
* Components (also exported from the `jatui` root): DatePicker, the
|
|
5
|
+
* calendar-mode DateRangePicker, CalendarPicker, TimeSlotPicker,
|
|
6
|
+
* BookingScheduler, EventCalendar and the shared CalendarMonthGrid panel.
|
|
7
|
+
* This subpath adds every pure helper behind them: date math, the APG grid
|
|
8
|
+
* keyboard model, range/preset logic, typed-entry parsing, time-slot zone
|
|
9
|
+
* handling, and the event engine upstreamed from MOS (month lane packing,
|
|
10
|
+
* day overlap layout).
|
|
11
|
+
*/
|
|
12
|
+
export { default as CalendarMonthGrid } from './MonthGrid.svelte';
|
|
13
|
+
export type { MonthGridDay, MonthGridProps } from './MonthGrid.svelte';
|
|
14
|
+
export { default as CalendarHeader } from './CalendarHeader.svelte';
|
|
15
|
+
export { default as CalendarPicker } from '../CalendarPicker.svelte';
|
|
16
|
+
export { default as DatePicker } from '../DatePicker.svelte';
|
|
17
|
+
export { default as DateRangePicker } from '../DateRangePicker.svelte';
|
|
18
|
+
export { default as TimeSlotPicker } from '../TimeSlotPicker.svelte';
|
|
19
|
+
export { default as BookingScheduler } from '../BookingScheduler.svelte';
|
|
20
|
+
export { default as EventCalendar } from '../EventCalendar.svelte';
|
|
21
|
+
export { createCalendarGrid } from './createCalendarGrid';
|
|
22
|
+
export type { CalendarGrid, CalendarGridOptions } from './createCalendarGrid';
|
|
23
|
+
export { startOfDay, isSameDay, isSameMonth, addDays, addMonths, startOfMonth, endOfMonth, daysInMonth, dayDiff, startOfWeek, endOfWeek, compareDays, clampDay, toIsoDate, parseIsoDate, toDay, weekStartForLocale, resolveWeekStart, weekdayLabels, formatMonthYear, formatLongDate, formatMediumDate, buildMonthMatrix, nextFocusDate, makeIsDisabled, normalizeRange, isInRange, rangeDayCount, rangeCellState, pickRangeDate, toIsoRange, fromIsoRange, resolvePreset, defaultRangePresets, defaultRangePresetsWithAll, matchPreset, formatRangeLabel, dateFieldOrder, dateSeparator, datePlaceholder, formatTypedDate, parseTypedDate, calendarAccentStyle, followFocus, canStepMonth } from './calendarGrid';
|
|
24
|
+
export type { Weekday, DateRange, IsoDateRange, MonthCell, GridKeyOptions, DateRules, RangeCellState, RangePreset, CalendarAccent } from './calendarGrid';
|
|
25
|
+
export { hourInZone, dayPart, groupSlotsByDayPart, formatSlotTime, formatSlotLabel, localeHourCycle, localTimeZone, timeZoneOffsetLabel, timeZoneCity, commonTimeZones, nextSlotIndex } from './timeSlots';
|
|
26
|
+
export type { HourCycle, DayPart, SlotGroup } from './timeSlots';
|
|
27
|
+
export { parseLocalDate, eventDayRange, packWeek, computeVisibility } from './monthPacking';
|
|
28
|
+
export type { PackableEvent, MonthDay, WeekSegment, PackedWeek, WeekVisibility } from './monthPacking';
|
|
29
|
+
export { eventInterval, intervalsOverlap, layoutDay, DEFAULT_DURATION_MIN, MIN_DURATION_MIN } from './dayOverlap';
|
|
30
|
+
export type { DayOverlapOptions, DayInterval, DayPlacement } from './dayOverlap';
|
|
31
|
+
export { eventColorStyle, stepDate, visibleRange, viewTitle, isAllDayLike, buildMonthWeeks, buildTimeGridDays, weekDates, agendaDays, chipTime, eventWhen, hourLabel } from './eventCalendar';
|
|
32
|
+
export type { CalendarEventItem, EventColor, EventCalendarView, TimeGridDay } from './eventCalendar';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jatui/calendar — the dates + calendar surface (BoardUI wave 4, jatui-lfxyy).
|
|
3
|
+
*
|
|
4
|
+
* Components (also exported from the `jatui` root): DatePicker, the
|
|
5
|
+
* calendar-mode DateRangePicker, CalendarPicker, TimeSlotPicker,
|
|
6
|
+
* BookingScheduler, EventCalendar and the shared CalendarMonthGrid panel.
|
|
7
|
+
* This subpath adds every pure helper behind them: date math, the APG grid
|
|
8
|
+
* keyboard model, range/preset logic, typed-entry parsing, time-slot zone
|
|
9
|
+
* handling, and the event engine upstreamed from MOS (month lane packing,
|
|
10
|
+
* day overlap layout).
|
|
11
|
+
*/
|
|
12
|
+
export { default as CalendarMonthGrid } from './MonthGrid.svelte';
|
|
13
|
+
export { default as CalendarHeader } from './CalendarHeader.svelte';
|
|
14
|
+
export { default as CalendarPicker } from '../CalendarPicker.svelte';
|
|
15
|
+
export { default as DatePicker } from '../DatePicker.svelte';
|
|
16
|
+
export { default as DateRangePicker } from '../DateRangePicker.svelte';
|
|
17
|
+
export { default as TimeSlotPicker } from '../TimeSlotPicker.svelte';
|
|
18
|
+
export { default as BookingScheduler } from '../BookingScheduler.svelte';
|
|
19
|
+
export { default as EventCalendar } from '../EventCalendar.svelte';
|
|
20
|
+
export { createCalendarGrid } from './createCalendarGrid';
|
|
21
|
+
export { startOfDay, isSameDay, isSameMonth, addDays, addMonths, startOfMonth, endOfMonth, daysInMonth, dayDiff, startOfWeek, endOfWeek, compareDays, clampDay, toIsoDate, parseIsoDate, toDay, weekStartForLocale, resolveWeekStart, weekdayLabels, formatMonthYear, formatLongDate, formatMediumDate, buildMonthMatrix, nextFocusDate, makeIsDisabled, normalizeRange, isInRange, rangeDayCount, rangeCellState, pickRangeDate, toIsoRange, fromIsoRange, resolvePreset, defaultRangePresets, defaultRangePresetsWithAll, matchPreset, formatRangeLabel, dateFieldOrder, dateSeparator, datePlaceholder, formatTypedDate, parseTypedDate, calendarAccentStyle, followFocus, canStepMonth } from './calendarGrid';
|
|
22
|
+
export { hourInZone, dayPart, groupSlotsByDayPart, formatSlotTime, formatSlotLabel, localeHourCycle, localTimeZone, timeZoneOffsetLabel, timeZoneCity, commonTimeZones, nextSlotIndex } from './timeSlots';
|
|
23
|
+
export { parseLocalDate, eventDayRange, packWeek, computeVisibility } from './monthPacking';
|
|
24
|
+
export { eventInterval, intervalsOverlap, layoutDay, DEFAULT_DURATION_MIN, MIN_DURATION_MIN } from './dayOverlap';
|
|
25
|
+
export { eventColorStyle, stepDate, visibleRange, viewTitle, isAllDayLike, buildMonthWeeks, buildTimeGridDays, weekDates, agendaDays, chipTime, eventWhen, hourLabel } from './eventCalendar';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Month-view lane packing.
|
|
3
|
+
*
|
|
4
|
+
* Upstreamed from MOS `lib/components/calendar/shared/monthPacking.ts`
|
|
5
|
+
* (jatui-lfxyy) with its tests. The only change is the event type: any
|
|
6
|
+
* `PackableEvent` (id, title, start, end?, allDay?) works, and start/end may
|
|
7
|
+
* be ISO strings or `Date`s. The MOS event type satisfies it as-is.
|
|
8
|
+
*
|
|
9
|
+
* A month row has to place two kinds of thing — bars that span several days and
|
|
10
|
+
* chips that sit on one — and the only way to guarantee they never collide is to
|
|
11
|
+
* let a single allocator hand out every slot. This module is that allocator. The
|
|
12
|
+
* grid then renders each segment as a CSS grid item at (column span, lane), so
|
|
13
|
+
* overlap becomes structurally impossible instead of a matter of keeping two
|
|
14
|
+
* pixel offsets in agreement.
|
|
15
|
+
*/
|
|
16
|
+
/** The fields packing reads. Extra fields ride along untouched. */
|
|
17
|
+
export interface PackableEvent {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
/** ISO datetime, `YYYY-MM-DD` (local day) or a Date. */
|
|
21
|
+
start: string | Date;
|
|
22
|
+
end?: string | Date | null;
|
|
23
|
+
allDay?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Parse "YYYY-MM-DD" as local midnight, not the UTC midnight `new Date` gives. */
|
|
26
|
+
export declare function parseLocalDate(str: string | Date): Date;
|
|
27
|
+
export declare function isSameDay(d1: Date, d2: Date): boolean;
|
|
28
|
+
/** Whole days between two dates, ignoring time of day. */
|
|
29
|
+
export declare function dayDiff(from: Date, to: Date): number;
|
|
30
|
+
export declare function startOfDay(d: Date): Date;
|
|
31
|
+
/**
|
|
32
|
+
* Inclusive first and last day an event occupies, both at local midnight.
|
|
33
|
+
*
|
|
34
|
+
* Flooring is load-bearing: month cells are midnight dates, so a 9am event
|
|
35
|
+
* whose range kept its time would fail `cell >= start` and disappear from the
|
|
36
|
+
* grid rather than render an hour late.
|
|
37
|
+
*/
|
|
38
|
+
export declare function eventDayRange(ev: PackableEvent): [Date, Date];
|
|
39
|
+
export interface MonthDay<E extends PackableEvent = PackableEvent> {
|
|
40
|
+
date: Date;
|
|
41
|
+
inMonth: boolean;
|
|
42
|
+
events: E[];
|
|
43
|
+
}
|
|
44
|
+
export interface WeekSegment<E extends PackableEvent = PackableEvent> {
|
|
45
|
+
ev: E;
|
|
46
|
+
/** 0-6, clamped into this week */
|
|
47
|
+
startCol: number;
|
|
48
|
+
/** 0-6, clamped into this week */
|
|
49
|
+
endCol: number;
|
|
50
|
+
/** 0-based vertical slot */
|
|
51
|
+
lane: number;
|
|
52
|
+
/** Began before this week's first day */
|
|
53
|
+
continuesLeft: boolean;
|
|
54
|
+
/** Ends after this week's last day */
|
|
55
|
+
continuesRight: boolean;
|
|
56
|
+
/** Solid bar (all-day or spanning) rather than a dot-and-label chip */
|
|
57
|
+
filled: boolean;
|
|
58
|
+
/** Finished before today */
|
|
59
|
+
past: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface PackedWeek<E extends PackableEvent = PackableEvent> {
|
|
62
|
+
segments: WeekSegment<E>[];
|
|
63
|
+
laneCount: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Assign every event touching `week` a lane, greedily, widest span first.
|
|
67
|
+
*
|
|
68
|
+
* `week` must be exactly seven consecutive days, each carrying the events that
|
|
69
|
+
* touch it (an event covering three days appears on all three). Duplicates are
|
|
70
|
+
* collapsed into one segment spanning the columns it covers.
|
|
71
|
+
*/
|
|
72
|
+
export declare function packWeek<E extends PackableEvent>(week: MonthDay<E>[], now?: Date): PackedWeek<E>;
|
|
73
|
+
export interface WeekVisibility {
|
|
74
|
+
/** Lanes actually rendered. */
|
|
75
|
+
visibleLanes: number;
|
|
76
|
+
/** Event ids folded into a "+N more" marker. */
|
|
77
|
+
hiddenIds: Set<string>;
|
|
78
|
+
/** Per-column hidden count; 0 means no marker for that day. */
|
|
79
|
+
moreCounts: number[];
|
|
80
|
+
/** Lane to render a segment in, when recovery moved it off its packed lane. */
|
|
81
|
+
laneOverrides: Map<string, number>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decide what a row shows given how many lanes fit.
|
|
85
|
+
*
|
|
86
|
+
* The "+N more" marker lives in the LAST VISIBLE lane rather than in a lane of
|
|
87
|
+
* its own. A dedicated marker row costs every column a slot because one busy
|
|
88
|
+
* day overflowed, which on a short window collapsed whole rows to a single
|
|
89
|
+
* event. Here only the columns that actually overflow give up their last lane,
|
|
90
|
+
* so a quiet day beside a busy one keeps showing its events.
|
|
91
|
+
*
|
|
92
|
+
* A segment spanning several columns is one grid item, so it cannot be shown
|
|
93
|
+
* for some of its days and hidden for others: if any column it covers needs the
|
|
94
|
+
* marker, the whole segment folds in. That can push a further column over, so
|
|
95
|
+
* this iterates to a fixed point.
|
|
96
|
+
*/
|
|
97
|
+
export declare function computeVisibility<E extends PackableEvent>(segments: WeekSegment<E>[], capacity: number): WeekVisibility;
|