@joewinke/jatui 0.41.1 → 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.
Files changed (34) hide show
  1. package/CLAUDE.md +9 -3
  2. package/dist/components/BookingScheduler.svelte +412 -0
  3. package/dist/components/BookingScheduler.svelte.d.ts +66 -0
  4. package/dist/components/CalendarPicker.svelte +190 -205
  5. package/dist/components/CalendarPicker.svelte.d.ts +19 -1
  6. package/dist/components/DatePicker.svelte +419 -0
  7. package/dist/components/DatePicker.svelte.d.ts +44 -0
  8. package/dist/components/DateRangePicker.svelte +471 -5
  9. package/dist/components/DateRangePicker.svelte.d.ts +32 -3
  10. package/dist/components/EventCalendar.svelte +1324 -0
  11. package/dist/components/EventCalendar.svelte.d.ts +101 -0
  12. package/dist/components/TimeSlotPicker.svelte +174 -108
  13. package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
  14. package/dist/components/calendar/CalendarHeader.svelte +98 -0
  15. package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
  16. package/dist/components/calendar/MonthGrid.svelte +361 -0
  17. package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
  18. package/dist/components/calendar/calendarGrid.d.ts +219 -0
  19. package/dist/components/calendar/calendarGrid.js +494 -0
  20. package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
  21. package/dist/components/calendar/createCalendarGrid.js +57 -0
  22. package/dist/components/calendar/dayOverlap.d.ts +92 -0
  23. package/dist/components/calendar/dayOverlap.js +147 -0
  24. package/dist/components/calendar/eventCalendar.d.ts +75 -0
  25. package/dist/components/calendar/eventCalendar.js +187 -0
  26. package/dist/components/calendar/index.d.ts +32 -0
  27. package/dist/components/calendar/index.js +25 -0
  28. package/dist/components/calendar/monthPacking.d.ts +97 -0
  29. package/dist/components/calendar/monthPacking.js +254 -0
  30. package/dist/components/calendar/timeSlots.d.ts +57 -0
  31. package/dist/components/calendar/timeSlots.js +163 -0
  32. package/dist/index.d.ts +17 -0
  33. package/dist/index.js +11 -0
  34. 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.41.1",
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",