@joewinke/jatui 0.41.0 → 0.42.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 +9 -3
- package/dist/components/AnimatedNumber.svelte +22 -4
- 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/TimeSlotPicker.svelte +174 -108
- package/dist/components/TimeSlotPicker.svelte.d.ts +27 -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/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/showcase/Globe.svelte +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
const DAY_MS = 86400000;
|
|
17
|
+
/** Parse "YYYY-MM-DD" as local midnight, not the UTC midnight `new Date` gives. */
|
|
18
|
+
export function parseLocalDate(str) {
|
|
19
|
+
if (str instanceof Date)
|
|
20
|
+
return new Date(str.getTime());
|
|
21
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) {
|
|
22
|
+
const [y, m, d] = str.split("-").map(Number);
|
|
23
|
+
return new Date(y, m - 1, d);
|
|
24
|
+
}
|
|
25
|
+
return new Date(str);
|
|
26
|
+
}
|
|
27
|
+
export function isSameDay(d1, d2) {
|
|
28
|
+
return (d1.getFullYear() === d2.getFullYear() &&
|
|
29
|
+
d1.getMonth() === d2.getMonth() &&
|
|
30
|
+
d1.getDate() === d2.getDate());
|
|
31
|
+
}
|
|
32
|
+
/** Whole days between two dates, ignoring time of day. */
|
|
33
|
+
export function dayDiff(from, to) {
|
|
34
|
+
const a = new Date(from.getFullYear(), from.getMonth(), from.getDate());
|
|
35
|
+
const b = new Date(to.getFullYear(), to.getMonth(), to.getDate());
|
|
36
|
+
return Math.round((b.getTime() - a.getTime()) / DAY_MS);
|
|
37
|
+
}
|
|
38
|
+
export function startOfDay(d) {
|
|
39
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Inclusive first and last day an event occupies, both at local midnight.
|
|
43
|
+
*
|
|
44
|
+
* Flooring is load-bearing: month cells are midnight dates, so a 9am event
|
|
45
|
+
* whose range kept its time would fail `cell >= start` and disappear from the
|
|
46
|
+
* grid rather than render an hour late.
|
|
47
|
+
*/
|
|
48
|
+
export function eventDayRange(ev) {
|
|
49
|
+
const start = startOfDay(parseLocalDate(ev.start));
|
|
50
|
+
if (!ev.end)
|
|
51
|
+
return [start, start];
|
|
52
|
+
const end = startOfDay(parseLocalDate(ev.end));
|
|
53
|
+
return [start, end < start ? start : end];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Assign every event touching `week` a lane, greedily, widest span first.
|
|
57
|
+
*
|
|
58
|
+
* `week` must be exactly seven consecutive days, each carrying the events that
|
|
59
|
+
* touch it (an event covering three days appears on all three). Duplicates are
|
|
60
|
+
* collapsed into one segment spanning the columns it covers.
|
|
61
|
+
*/
|
|
62
|
+
export function packWeek(week, now = new Date()) {
|
|
63
|
+
const rowStart = week[0].date;
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const items = [];
|
|
66
|
+
for (const day of week) {
|
|
67
|
+
for (const ev of day.events) {
|
|
68
|
+
if (seen.has(ev.id))
|
|
69
|
+
continue;
|
|
70
|
+
seen.add(ev.id);
|
|
71
|
+
const [s, e] = eventDayRange(ev);
|
|
72
|
+
const rawStart = dayDiff(rowStart, s);
|
|
73
|
+
const rawEnd = dayDiff(rowStart, e);
|
|
74
|
+
items.push({
|
|
75
|
+
ev,
|
|
76
|
+
rawStart,
|
|
77
|
+
rawEnd,
|
|
78
|
+
startCol: Math.max(0, rawStart),
|
|
79
|
+
endCol: Math.min(6, rawEnd),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Widest spans claim the top lanes, then all-day before timed, then
|
|
84
|
+
// chronological. Reading down a column then matches reading across a row.
|
|
85
|
+
items.sort((a, b) => {
|
|
86
|
+
const spanA = a.endCol - a.startCol;
|
|
87
|
+
const spanB = b.endCol - b.startCol;
|
|
88
|
+
if (spanA !== spanB)
|
|
89
|
+
return spanB - spanA;
|
|
90
|
+
if (!!a.ev.allDay !== !!b.ev.allDay)
|
|
91
|
+
return a.ev.allDay ? -1 : 1;
|
|
92
|
+
const t = parseLocalDate(a.ev.start).getTime() - parseLocalDate(b.ev.start).getTime();
|
|
93
|
+
if (t !== 0)
|
|
94
|
+
return t;
|
|
95
|
+
return a.ev.title.localeCompare(b.ev.title);
|
|
96
|
+
});
|
|
97
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
98
|
+
const occupied = [];
|
|
99
|
+
const segments = [];
|
|
100
|
+
for (const it of items) {
|
|
101
|
+
let lane = 0;
|
|
102
|
+
for (;;) {
|
|
103
|
+
if (!occupied[lane])
|
|
104
|
+
occupied[lane] = new Array(7).fill(false);
|
|
105
|
+
let free = true;
|
|
106
|
+
for (let c = it.startCol; c <= it.endCol; c++) {
|
|
107
|
+
if (occupied[lane][c]) {
|
|
108
|
+
free = false;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (free)
|
|
113
|
+
break;
|
|
114
|
+
lane++;
|
|
115
|
+
}
|
|
116
|
+
for (let c = it.startCol; c <= it.endCol; c++)
|
|
117
|
+
occupied[lane][c] = true;
|
|
118
|
+
const [, endDate] = eventDayRange(it.ev);
|
|
119
|
+
segments.push({
|
|
120
|
+
ev: it.ev,
|
|
121
|
+
startCol: it.startCol,
|
|
122
|
+
endCol: it.endCol,
|
|
123
|
+
lane,
|
|
124
|
+
continuesLeft: it.rawStart < 0,
|
|
125
|
+
continuesRight: it.rawEnd > 6,
|
|
126
|
+
filled: !!it.ev.allDay || it.endCol > it.startCol || it.rawStart < 0,
|
|
127
|
+
past: endDate < today,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { segments, laneCount: occupied.length };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Decide what a row shows given how many lanes fit.
|
|
134
|
+
*
|
|
135
|
+
* The "+N more" marker lives in the LAST VISIBLE lane rather than in a lane of
|
|
136
|
+
* its own. A dedicated marker row costs every column a slot because one busy
|
|
137
|
+
* day overflowed, which on a short window collapsed whole rows to a single
|
|
138
|
+
* event. Here only the columns that actually overflow give up their last lane,
|
|
139
|
+
* so a quiet day beside a busy one keeps showing its events.
|
|
140
|
+
*
|
|
141
|
+
* A segment spanning several columns is one grid item, so it cannot be shown
|
|
142
|
+
* for some of its days and hidden for others: if any column it covers needs the
|
|
143
|
+
* marker, the whole segment folds in. That can push a further column over, so
|
|
144
|
+
* this iterates to a fixed point.
|
|
145
|
+
*/
|
|
146
|
+
export function computeVisibility(segments, capacity) {
|
|
147
|
+
const visibleLanes = Math.max(1, capacity);
|
|
148
|
+
const hiddenIds = new Set();
|
|
149
|
+
for (const seg of segments) {
|
|
150
|
+
if (seg.lane >= visibleLanes)
|
|
151
|
+
hiddenIds.add(seg.ev.id);
|
|
152
|
+
}
|
|
153
|
+
const markedColumns = () => {
|
|
154
|
+
const cols = new Array(7).fill(false);
|
|
155
|
+
for (const seg of segments) {
|
|
156
|
+
if (!hiddenIds.has(seg.ev.id))
|
|
157
|
+
continue;
|
|
158
|
+
for (let c = seg.startCol; c <= seg.endCol; c++)
|
|
159
|
+
cols[c] = true;
|
|
160
|
+
}
|
|
161
|
+
return cols;
|
|
162
|
+
};
|
|
163
|
+
let cols = markedColumns();
|
|
164
|
+
for (let guard = 0; guard < segments.length + 1; guard++) {
|
|
165
|
+
let changed = false;
|
|
166
|
+
for (const seg of segments) {
|
|
167
|
+
if (hiddenIds.has(seg.ev.id))
|
|
168
|
+
continue;
|
|
169
|
+
if (seg.lane !== visibleLanes - 1)
|
|
170
|
+
continue;
|
|
171
|
+
for (let c = seg.startCol; c <= seg.endCol; c++) {
|
|
172
|
+
if (cols[c]) {
|
|
173
|
+
hiddenIds.add(seg.ev.id);
|
|
174
|
+
changed = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!changed)
|
|
180
|
+
break;
|
|
181
|
+
cols = markedColumns();
|
|
182
|
+
}
|
|
183
|
+
// Recovery. A marker is only worth its slot when it stands for more than the
|
|
184
|
+
// event it replaced, so pull hidden segments back into any free lane —
|
|
185
|
+
// including the marker's own lane once nothing is left to mark. This is what
|
|
186
|
+
// stops a day from ever rendering "+1 more" beside empty space.
|
|
187
|
+
const laneOverrides = new Map();
|
|
188
|
+
const laneOf = (seg) => laneOverrides.get(seg.ev.id) ?? seg.lane;
|
|
189
|
+
const occupied = Array.from({ length: visibleLanes }, () => new Array(7).fill(false));
|
|
190
|
+
for (const seg of segments) {
|
|
191
|
+
if (hiddenIds.has(seg.ev.id))
|
|
192
|
+
continue;
|
|
193
|
+
const lane = laneOf(seg);
|
|
194
|
+
if (lane >= visibleLanes)
|
|
195
|
+
continue;
|
|
196
|
+
for (let c = seg.startCol; c <= seg.endCol; c++)
|
|
197
|
+
occupied[lane][c] = true;
|
|
198
|
+
}
|
|
199
|
+
for (let guard = 0; guard < segments.length + 1; guard++) {
|
|
200
|
+
const candidates = segments
|
|
201
|
+
.filter((s) => hiddenIds.has(s.ev.id))
|
|
202
|
+
.sort((a, b) => a.lane - b.lane);
|
|
203
|
+
let placed = false;
|
|
204
|
+
for (const seg of candidates) {
|
|
205
|
+
// Columns that still need a marker if this segment comes back.
|
|
206
|
+
const stillHidden = new Array(7).fill(0);
|
|
207
|
+
for (const other of segments) {
|
|
208
|
+
if (other.ev.id === seg.ev.id)
|
|
209
|
+
continue;
|
|
210
|
+
if (!hiddenIds.has(other.ev.id))
|
|
211
|
+
continue;
|
|
212
|
+
for (let c = other.startCol; c <= other.endCol; c++)
|
|
213
|
+
stillHidden[c]++;
|
|
214
|
+
}
|
|
215
|
+
let target = -1;
|
|
216
|
+
for (let lane = 0; lane < visibleLanes; lane++) {
|
|
217
|
+
let ok = true;
|
|
218
|
+
for (let c = seg.startCol; c <= seg.endCol; c++) {
|
|
219
|
+
if (occupied[lane][c]) {
|
|
220
|
+
ok = false;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
// The last lane has to stay free wherever a marker is still needed.
|
|
224
|
+
if (lane === visibleLanes - 1 && stillHidden[c] > 0) {
|
|
225
|
+
ok = false;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (ok) {
|
|
230
|
+
target = lane;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (target >= 0) {
|
|
235
|
+
hiddenIds.delete(seg.ev.id);
|
|
236
|
+
laneOverrides.set(seg.ev.id, target);
|
|
237
|
+
for (let c = seg.startCol; c <= seg.endCol; c++)
|
|
238
|
+
occupied[target][c] = true;
|
|
239
|
+
placed = true;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!placed)
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
const moreCounts = new Array(7).fill(0);
|
|
247
|
+
for (const seg of segments) {
|
|
248
|
+
if (!hiddenIds.has(seg.ev.id))
|
|
249
|
+
continue;
|
|
250
|
+
for (let c = seg.startCol; c <= seg.endCol; c++)
|
|
251
|
+
moreCounts[c]++;
|
|
252
|
+
}
|
|
253
|
+
return { visibleLanes, hiddenIds, moreCounts, laneOverrides };
|
|
254
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-slot helpers behind TimeSlotPicker and BookingScheduler.
|
|
3
|
+
*
|
|
4
|
+
* Slots are instants (`Date`). What a person sees — the clock time, which part
|
|
5
|
+
* of the day a slot falls in — depends on the time zone it is shown in, so
|
|
6
|
+
* every helper here takes an optional IANA `timeZone` and reads the wall clock
|
|
7
|
+
* through `Intl` rather than `Date#getHours` (which is always the runtime's
|
|
8
|
+
* zone).
|
|
9
|
+
*/
|
|
10
|
+
export type HourCycle = 'h12' | 'h23';
|
|
11
|
+
export type DayPart = 'morning' | 'afternoon' | 'evening';
|
|
12
|
+
/** Wall-clock hour (0–23) of `date` in `timeZone` (runtime zone when omitted). */
|
|
13
|
+
export declare function hourInZone(date: Date, timeZone?: string): number;
|
|
14
|
+
/** Morning < 12:00 ≤ afternoon < 17:00 ≤ evening, on the zone's wall clock. */
|
|
15
|
+
export declare function dayPart(date: Date, timeZone?: string): DayPart;
|
|
16
|
+
export interface SlotGroup<T> {
|
|
17
|
+
part: DayPart;
|
|
18
|
+
slots: T[];
|
|
19
|
+
}
|
|
20
|
+
/** Slots grouped by part of day, in order, empty groups dropped. */
|
|
21
|
+
export declare function groupSlotsByDayPart<T extends {
|
|
22
|
+
start: Date;
|
|
23
|
+
}>(slots: T[], timeZone?: string): SlotGroup<T>[];
|
|
24
|
+
/** "9:30 AM" (h12) or "09:30" (h23) on the zone's wall clock. */
|
|
25
|
+
export declare function formatSlotTime(date: Date, opts?: {
|
|
26
|
+
hourCycle?: HourCycle;
|
|
27
|
+
timeZone?: string;
|
|
28
|
+
locale?: string;
|
|
29
|
+
}): string;
|
|
30
|
+
/** "Wednesday, March 4 at 9:30 AM" — a slot's accessible name. */
|
|
31
|
+
export declare function formatSlotLabel(date: Date, opts?: {
|
|
32
|
+
hourCycle?: HourCycle;
|
|
33
|
+
timeZone?: string;
|
|
34
|
+
locale?: string;
|
|
35
|
+
}): string;
|
|
36
|
+
/** The hour cycle the locale prefers (en-US h12, en-GB/de h23). */
|
|
37
|
+
export declare function localeHourCycle(locale?: string): HourCycle;
|
|
38
|
+
/** The runtime's IANA zone ("America/Chicago"). */
|
|
39
|
+
export declare function localTimeZone(): string;
|
|
40
|
+
/** "GMT-5" style offset label for `timeZone` at `at`. */
|
|
41
|
+
export declare function timeZoneOffsetLabel(timeZone: string, at?: Date, locale?: string): string;
|
|
42
|
+
/** "America/New_York" → "New York". */
|
|
43
|
+
export declare function timeZoneCity(timeZone: string): string;
|
|
44
|
+
/** A short list for a time-zone picker: the viewer's own zone first, then common ones, de-duplicated. */
|
|
45
|
+
export declare function commonTimeZones(extra?: string[]): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Roving-focus step through an ordered slot list: arrows ±1 (Left/Right
|
|
48
|
+
* mirrored in RTL), Up/Down ±`columns` (one visual row), Home/End to the
|
|
49
|
+
* ends. Unavailable slots are skipped. Returns the new index, or null when
|
|
50
|
+
* the key is not a navigation key.
|
|
51
|
+
*/
|
|
52
|
+
export declare function nextSlotIndex(slots: {
|
|
53
|
+
available: boolean;
|
|
54
|
+
}[], current: number, key: string, opts?: {
|
|
55
|
+
columns?: number;
|
|
56
|
+
rtl?: boolean;
|
|
57
|
+
}): number | null;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-slot helpers behind TimeSlotPicker and BookingScheduler.
|
|
3
|
+
*
|
|
4
|
+
* Slots are instants (`Date`). What a person sees — the clock time, which part
|
|
5
|
+
* of the day a slot falls in — depends on the time zone it is shown in, so
|
|
6
|
+
* every helper here takes an optional IANA `timeZone` and reads the wall clock
|
|
7
|
+
* through `Intl` rather than `Date#getHours` (which is always the runtime's
|
|
8
|
+
* zone).
|
|
9
|
+
*/
|
|
10
|
+
/** Wall-clock hour (0–23) of `date` in `timeZone` (runtime zone when omitted). */
|
|
11
|
+
export function hourInZone(date, timeZone) {
|
|
12
|
+
if (!timeZone)
|
|
13
|
+
return date.getHours();
|
|
14
|
+
const parts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', hourCycle: 'h23', timeZone }).formatToParts(date);
|
|
15
|
+
const h = Number(parts.find((p) => p.type === 'hour')?.value ?? date.getHours());
|
|
16
|
+
return h === 24 ? 0 : h;
|
|
17
|
+
}
|
|
18
|
+
/** Morning < 12:00 ≤ afternoon < 17:00 ≤ evening, on the zone's wall clock. */
|
|
19
|
+
export function dayPart(date, timeZone) {
|
|
20
|
+
const h = hourInZone(date, timeZone);
|
|
21
|
+
if (h < 12)
|
|
22
|
+
return 'morning';
|
|
23
|
+
if (h < 17)
|
|
24
|
+
return 'afternoon';
|
|
25
|
+
return 'evening';
|
|
26
|
+
}
|
|
27
|
+
/** Slots grouped by part of day, in order, empty groups dropped. */
|
|
28
|
+
export function groupSlotsByDayPart(slots, timeZone) {
|
|
29
|
+
const groups = { morning: [], afternoon: [], evening: [] };
|
|
30
|
+
for (const s of slots)
|
|
31
|
+
groups[dayPart(s.start, timeZone)].push(s);
|
|
32
|
+
return ['morning', 'afternoon', 'evening']
|
|
33
|
+
.map((part) => ({ part, slots: groups[part] }))
|
|
34
|
+
.filter((g) => g.slots.length > 0);
|
|
35
|
+
}
|
|
36
|
+
/** "9:30 AM" (h12) or "09:30" (h23) on the zone's wall clock. */
|
|
37
|
+
export function formatSlotTime(date, opts = {}) {
|
|
38
|
+
const { hourCycle = 'h12', timeZone, locale = 'en-US' } = opts;
|
|
39
|
+
return new Intl.DateTimeFormat(locale, {
|
|
40
|
+
hour: hourCycle === 'h12' ? 'numeric' : '2-digit',
|
|
41
|
+
minute: '2-digit',
|
|
42
|
+
hourCycle,
|
|
43
|
+
timeZone
|
|
44
|
+
}).format(date);
|
|
45
|
+
}
|
|
46
|
+
/** "Wednesday, March 4 at 9:30 AM" — a slot's accessible name. */
|
|
47
|
+
export function formatSlotLabel(date, opts = {}) {
|
|
48
|
+
const { timeZone, locale = 'en-US' } = opts;
|
|
49
|
+
const day = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', timeZone }).format(date);
|
|
50
|
+
return `${day}, ${formatSlotTime(date, opts)}`;
|
|
51
|
+
}
|
|
52
|
+
/** The hour cycle the locale prefers (en-US h12, en-GB/de h23). */
|
|
53
|
+
export function localeHourCycle(locale) {
|
|
54
|
+
try {
|
|
55
|
+
const hc = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hourCycle;
|
|
56
|
+
return hc === 'h11' || hc === 'h12' ? 'h12' : 'h23';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return 'h12';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The runtime's IANA zone ("America/Chicago"). */
|
|
63
|
+
export function localTimeZone() {
|
|
64
|
+
try {
|
|
65
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return 'UTC';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** "GMT-5" style offset label for `timeZone` at `at`. */
|
|
72
|
+
export function timeZoneOffsetLabel(timeZone, at = new Date(), locale = 'en-US') {
|
|
73
|
+
try {
|
|
74
|
+
const parts = new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: 'shortOffset' }).formatToParts(at);
|
|
75
|
+
return parts.find((p) => p.type === 'timeZoneName')?.value ?? '';
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** "America/New_York" → "New York". */
|
|
82
|
+
export function timeZoneCity(timeZone) {
|
|
83
|
+
const tail = timeZone.split('/').pop() ?? timeZone;
|
|
84
|
+
return tail.replace(/_/g, ' ');
|
|
85
|
+
}
|
|
86
|
+
/** A short list for a time-zone picker: the viewer's own zone first, then common ones, de-duplicated. */
|
|
87
|
+
export function commonTimeZones(extra = []) {
|
|
88
|
+
const base = [
|
|
89
|
+
localTimeZone(),
|
|
90
|
+
...extra,
|
|
91
|
+
'America/Los_Angeles',
|
|
92
|
+
'America/Denver',
|
|
93
|
+
'America/Chicago',
|
|
94
|
+
'America/New_York',
|
|
95
|
+
'America/Sao_Paulo',
|
|
96
|
+
'Europe/London',
|
|
97
|
+
'Europe/Berlin',
|
|
98
|
+
'Africa/Johannesburg',
|
|
99
|
+
'Asia/Dubai',
|
|
100
|
+
'Asia/Kolkata',
|
|
101
|
+
'Asia/Singapore',
|
|
102
|
+
'Asia/Tokyo',
|
|
103
|
+
'Australia/Sydney',
|
|
104
|
+
'Pacific/Auckland',
|
|
105
|
+
'UTC'
|
|
106
|
+
];
|
|
107
|
+
return [...new Set(base)];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Roving-focus step through an ordered slot list: arrows ±1 (Left/Right
|
|
111
|
+
* mirrored in RTL), Up/Down ±`columns` (one visual row), Home/End to the
|
|
112
|
+
* ends. Unavailable slots are skipped. Returns the new index, or null when
|
|
113
|
+
* the key is not a navigation key.
|
|
114
|
+
*/
|
|
115
|
+
export function nextSlotIndex(slots, current, key, opts = {}) {
|
|
116
|
+
const { columns = 1, rtl = false } = opts;
|
|
117
|
+
const n = slots.length;
|
|
118
|
+
if (n === 0)
|
|
119
|
+
return null;
|
|
120
|
+
const ok = (i) => i >= 0 && i < n && slots[i].available;
|
|
121
|
+
const firstOk = () => {
|
|
122
|
+
for (let i = 0; i < n; i++)
|
|
123
|
+
if (ok(i))
|
|
124
|
+
return i;
|
|
125
|
+
return current;
|
|
126
|
+
};
|
|
127
|
+
const lastOk = () => {
|
|
128
|
+
for (let i = n - 1; i >= 0; i--)
|
|
129
|
+
if (ok(i))
|
|
130
|
+
return i;
|
|
131
|
+
return current;
|
|
132
|
+
};
|
|
133
|
+
let step;
|
|
134
|
+
switch (key) {
|
|
135
|
+
case 'ArrowRight':
|
|
136
|
+
step = rtl ? -1 : 1;
|
|
137
|
+
break;
|
|
138
|
+
case 'ArrowLeft':
|
|
139
|
+
step = rtl ? 1 : -1;
|
|
140
|
+
break;
|
|
141
|
+
case 'ArrowDown':
|
|
142
|
+
step = Math.max(1, columns);
|
|
143
|
+
break;
|
|
144
|
+
case 'ArrowUp':
|
|
145
|
+
step = -Math.max(1, columns);
|
|
146
|
+
break;
|
|
147
|
+
case 'Home':
|
|
148
|
+
return firstOk();
|
|
149
|
+
case 'End':
|
|
150
|
+
return lastOk();
|
|
151
|
+
default:
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
// Land `step` away, then walk on one slot at a time past unavailable ones.
|
|
155
|
+
// A move off either end stays put (no wrap), like a grid edge.
|
|
156
|
+
let i = current + step;
|
|
157
|
+
while (i >= 0 && i < n) {
|
|
158
|
+
if (ok(i))
|
|
159
|
+
return i;
|
|
160
|
+
i += step > 0 ? 1 : -1;
|
|
161
|
+
}
|
|
162
|
+
return current;
|
|
163
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -138,6 +138,23 @@ export { default as TimeSlotPicker } from './components/TimeSlotPicker.svelte';
|
|
|
138
138
|
export { default as CalendarPicker } from './components/CalendarPicker.svelte';
|
|
139
139
|
export { default as AvailabilityModal } from './components/AvailabilityModal.svelte';
|
|
140
140
|
export { default as BookingForm } from './components/BookingForm.svelte';
|
|
141
|
+
export { default as DatePicker } from './components/DatePicker.svelte';
|
|
142
|
+
export { default as BookingScheduler } from './components/BookingScheduler.svelte';
|
|
143
|
+
export { default as EventCalendar } from './components/EventCalendar.svelte';
|
|
144
|
+
export { default as CalendarMonthGrid } from './components/calendar/MonthGrid.svelte';
|
|
145
|
+
export type { MonthGridDay, MonthGridProps } from './components/calendar/MonthGrid.svelte';
|
|
146
|
+
export { createCalendarGrid } from './components/calendar/createCalendarGrid';
|
|
147
|
+
export type { CalendarGrid, CalendarGridOptions } from './components/calendar/createCalendarGrid';
|
|
148
|
+
export { buildMonthMatrix, nextFocusDate, weekStartForLocale, resolveWeekStart, weekdayLabels, toIsoDate, parseIsoDate, makeIsDisabled, defaultRangePresets, defaultRangePresetsWithAll, resolvePreset, matchPreset, rangeDayCount, formatRangeLabel, datePlaceholder, formatTypedDate, parseTypedDate } from './components/calendar/calendarGrid';
|
|
149
|
+
export type { Weekday, DateRange, IsoDateRange, RangePreset, MonthCell } from './components/calendar/calendarGrid';
|
|
150
|
+
export { formatSlotTime, groupSlotsByDayPart, nextSlotIndex, localeHourCycle, localTimeZone, commonTimeZones, timeZoneOffsetLabel } from './components/calendar/timeSlots';
|
|
151
|
+
export type { HourCycle, DayPart, SlotGroup } from './components/calendar/timeSlots';
|
|
152
|
+
export { packWeek, computeVisibility, eventDayRange } from './components/calendar/monthPacking';
|
|
153
|
+
export type { PackableEvent, MonthDay, WeekSegment, PackedWeek, WeekVisibility } from './components/calendar/monthPacking';
|
|
154
|
+
export { layoutDay, eventInterval } from './components/calendar/dayOverlap';
|
|
155
|
+
export type { DayPlacement, DayOverlapOptions } from './components/calendar/dayOverlap';
|
|
156
|
+
export { eventColorStyle, visibleRange, viewTitle, stepDate, agendaDays } from './components/calendar/eventCalendar';
|
|
157
|
+
export type { CalendarEventItem, EventColor, EventCalendarView } from './components/calendar/eventCalendar';
|
|
141
158
|
export type { ShortenedLink } from './components/LinkShortener.svelte';
|
|
142
159
|
export type { SearchDropdownOption, SearchDropdownGroup } from './components/SearchDropdown.svelte';
|
|
143
160
|
export type { ChipSuggestion, SuggestionGroup, ChipInfo } from './components/ChipInput.svelte';
|
package/dist/index.js
CHANGED
|
@@ -176,6 +176,17 @@ export { default as TimeSlotPicker } from './components/TimeSlotPicker.svelte';
|
|
|
176
176
|
export { default as CalendarPicker } from './components/CalendarPicker.svelte';
|
|
177
177
|
export { default as AvailabilityModal } from './components/AvailabilityModal.svelte';
|
|
178
178
|
export { default as BookingForm } from './components/BookingForm.svelte';
|
|
179
|
+
// Dates + calendar (BoardUI wave 4, jatui-lfxyy). Every helper is also on 'jatui/calendar'.
|
|
180
|
+
export { default as DatePicker } from './components/DatePicker.svelte';
|
|
181
|
+
export { default as BookingScheduler } from './components/BookingScheduler.svelte';
|
|
182
|
+
export { default as EventCalendar } from './components/EventCalendar.svelte';
|
|
183
|
+
export { default as CalendarMonthGrid } from './components/calendar/MonthGrid.svelte';
|
|
184
|
+
export { createCalendarGrid } from './components/calendar/createCalendarGrid';
|
|
185
|
+
export { buildMonthMatrix, nextFocusDate, weekStartForLocale, resolveWeekStart, weekdayLabels, toIsoDate, parseIsoDate, makeIsDisabled, defaultRangePresets, defaultRangePresetsWithAll, resolvePreset, matchPreset, rangeDayCount, formatRangeLabel, datePlaceholder, formatTypedDate, parseTypedDate } from './components/calendar/calendarGrid';
|
|
186
|
+
export { formatSlotTime, groupSlotsByDayPart, nextSlotIndex, localeHourCycle, localTimeZone, commonTimeZones, timeZoneOffsetLabel } from './components/calendar/timeSlots';
|
|
187
|
+
export { packWeek, computeVisibility, eventDayRange } from './components/calendar/monthPacking';
|
|
188
|
+
export { layoutDay, eventInterval } from './components/calendar/dayOverlap';
|
|
189
|
+
export { eventColorStyle, visibleRange, viewTitle, stepDate, agendaDays } from './components/calendar/eventCalendar';
|
|
179
190
|
export { DAYS_OF_WEEK, DAYS_SHORT, LOCATION_LABELS, LOCATION_ICONS } from './types/booking';
|
|
180
191
|
// Utilities — date formatting
|
|
181
192
|
export { normalizeTimestamp, parseTimestamp, formatRelativeTime, formatFullDate, formatShortDate, formatLastActivity, formatDate, getTimeSinceMs, getTimeSinceMinutes, isWithinMinutes, formatDateTime, taskAge, pacingColor, formatPacingDuration, } from './utils/dateFormatters';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joewinke/jatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Svelte 5 component library for JAT projects",
|
|
6
6
|
"type": "module",
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
"svelte": "./dist/components/review/index.js",
|
|
79
79
|
"default": "./dist/components/review/index.js"
|
|
80
80
|
},
|
|
81
|
+
"./calendar": {
|
|
82
|
+
"types": "./dist/components/calendar/index.d.ts",
|
|
83
|
+
"svelte": "./dist/components/calendar/index.js",
|
|
84
|
+
"default": "./dist/components/calendar/index.js"
|
|
85
|
+
},
|
|
81
86
|
"./artifact-blocks": {
|
|
82
87
|
"types": "./dist/artifact-blocks/index.d.ts",
|
|
83
88
|
"svelte": "./dist/artifact-blocks/index.js",
|