@super-calendar/core 2.0.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/dist/index.js ADDED
@@ -0,0 +1,794 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let date_fns = require("date-fns");
3
+ let react = require("react");
4
+ //#region src/tokens.ts
5
+ const lightColors = {
6
+ gridLine: "#E2E4E9",
7
+ weekendBackground: "#F6F7F9",
8
+ outsideHoursBackground: "#F1F2F4",
9
+ todayBackground: "#1F6FEB",
10
+ todayText: "#FFFFFF",
11
+ selectedBackground: "#1F6FEB",
12
+ selectedText: "#FFFFFF",
13
+ rangeBackground: "#DCE7FF",
14
+ hoverBackground: "#E6ECF5",
15
+ nowIndicator: "#E5484D",
16
+ text: "#1A1B1E",
17
+ textMuted: "#6B7280",
18
+ textDisabled: "#B5B9C0",
19
+ eventBackground: "#DCE7FF",
20
+ eventText: "#1A1B1E"
21
+ };
22
+ const darkColors = {
23
+ gridLine: "#2A2E37",
24
+ weekendBackground: "#15171C",
25
+ outsideHoursBackground: "#101216",
26
+ todayBackground: "#1F6FEB",
27
+ todayText: "#FFFFFF",
28
+ selectedBackground: "#1F6FEB",
29
+ selectedText: "#FFFFFF",
30
+ rangeBackground: "#243B53",
31
+ hoverBackground: "#2E3138",
32
+ nowIndicator: "#E5484D",
33
+ text: "#ECEDEE",
34
+ textMuted: "#9BA1A6",
35
+ textDisabled: "#4B4F58",
36
+ eventBackground: "#243B53",
37
+ eventText: "#EAF2FF"
38
+ };
39
+ //#endregion
40
+ //#region src/presentation.ts
41
+ /** The band shape for a day, given the fill-cell option. */
42
+ function rangeBandKind(day, fillCell) {
43
+ if (!day.isInRange || day.isRangeStart && day.isRangeEnd) return "none";
44
+ if (fillCell) return "fill";
45
+ if (day.isRangeStart) return "pill-start";
46
+ if (day.isRangeEnd) return "pill-end";
47
+ return "pill-mid";
48
+ }
49
+ /** Whether a band shape rounds its leading / trailing edge (pill ends). */
50
+ function bandRounding(kind) {
51
+ return {
52
+ start: kind === "pill-start",
53
+ end: kind === "pill-end"
54
+ };
55
+ }
56
+ /**
57
+ * The filled-badge kind for a day. `isSelected` is true for both range endpoints
58
+ * and discrete selected days; today always wins when it coincides.
59
+ */
60
+ function dayBadgeKind(day, isToday) {
61
+ if (isToday) return "today";
62
+ if (day.isSelected) return "selected";
63
+ return "none";
64
+ }
65
+ //#endregion
66
+ //#region src/utils/dates.ts
67
+ /** The seven dates of the week containing `date`, starting on `weekStartsOn`. */
68
+ const getWeekDays = (date, weekStartsOn) => {
69
+ const start = (0, date_fns.startOfWeek)(date, { weekStartsOn });
70
+ return Array.from({ length: 7 }, (_, index) => (0, date_fns.addDays)(start, index));
71
+ };
72
+ /** How many day columns a time-grid mode shows. `custom` uses `numberOfDays`. */
73
+ const viewDayCount = (mode, numberOfDays = 1) => {
74
+ switch (mode) {
75
+ case "week": return 7;
76
+ case "3days": return 3;
77
+ case "custom": return Math.max(1, Math.floor(numberOfDays));
78
+ default: return 1;
79
+ }
80
+ };
81
+ /**
82
+ * Days in the inclusive span from `weekStartsOn` to `weekEndsOn` (1–7),
83
+ * wrapping when the end precedes the start (e.g. Sat→Wed). Mirrors
84
+ * react-native-big-calendar's `weekDaysCount`.
85
+ */
86
+ const weekDaysCount = (weekStartsOn, weekEndsOn) => {
87
+ if (weekEndsOn < weekStartsOn) {
88
+ let count = 1;
89
+ let i = weekStartsOn;
90
+ while (i !== weekEndsOn && count <= 7) {
91
+ i = (i + 1) % 7;
92
+ count++;
93
+ }
94
+ return count;
95
+ }
96
+ if (weekEndsOn > weekStartsOn) return weekEndsOn - weekStartsOn + 1;
97
+ return 1;
98
+ };
99
+ /**
100
+ * The day columns to render for a time-grid page. `week` spans the calendar week
101
+ * (honouring `weekStartsOn`). `custom` with a `weekEndsOn` spans the partial week
102
+ * from `weekStartsOn` to `weekEndsOn` (anchored to `date`'s week, paging by week);
103
+ * otherwise every mode shows `viewDayCount` consecutive days starting at `date`.
104
+ */
105
+ const getViewDays = (mode, date, weekStartsOn, numberOfDays = 1, isRTL = false, weekEndsOn) => {
106
+ let days;
107
+ if (mode === "week") days = getWeekDays(date, weekStartsOn);
108
+ else if (mode === "custom" && weekEndsOn != null) {
109
+ const subject = (0, date_fns.startOfDay)(date);
110
+ const offset = weekStartsOn - subject.getDay();
111
+ days = Array.from({ length: weekDaysCount(weekStartsOn, weekEndsOn) }, (_, index) => (0, date_fns.addDays)(subject, index + offset));
112
+ } else days = Array.from({ length: viewDayCount(mode, numberOfDays) }, (_, index) => (0, date_fns.addDays)((0, date_fns.startOfDay)(date), index));
113
+ return isRTL ? days.reverse() : days;
114
+ };
115
+ /**
116
+ * The calendar weeks covering `month`, padded to whole weeks starting on
117
+ * `weekStartsOn`. `showSixWeeks` always returns six rows (42 days) for a
118
+ * fixed-height grid; `isRTL` reverses each week's day order.
119
+ */
120
+ const buildMonthWeeks = (month, weekStartsOn, { showSixWeeks = false, isRTL = false } = {}) => {
121
+ const start = (0, date_fns.startOfWeek)((0, date_fns.startOfMonth)(month), { weekStartsOn });
122
+ const days = (0, date_fns.eachDayOfInterval)({
123
+ start,
124
+ end: showSixWeeks ? (0, date_fns.addDays)(start, 41) : (0, date_fns.endOfWeek)((0, date_fns.endOfMonth)(month), { weekStartsOn })
125
+ });
126
+ const weeks = [];
127
+ for (let index = 0; index < days.length; index += 7) {
128
+ const week = days.slice(index, index + 7);
129
+ weeks.push(isRTL ? week.reverse() : week);
130
+ }
131
+ return weeks;
132
+ };
133
+ const isWeekend = (date) => {
134
+ const day = date.getDay();
135
+ return day === 0 || day === 6;
136
+ };
137
+ const getIsToday = (date) => (0, date_fns.isToday)(date);
138
+ const isSameCalendarDay = (a, b) => (0, date_fns.isSameDay)(a, b);
139
+ /** Minutes elapsed since midnight (0–1439). */
140
+ const minutesIntoDay = (date) => (0, date_fns.getHours)(date) * 60 + (0, date_fns.getMinutes)(date);
141
+ //#endregion
142
+ //#region src/utils/dateRange.ts
143
+ /** Whether `date` passes the min/max/disabled constraints (compared by calendar day). */
144
+ function isDateSelectable(date, constraints = {}) {
145
+ const day = (0, date_fns.startOfDay)(date);
146
+ if (constraints.minDate && (0, date_fns.isBefore)(day, (0, date_fns.startOfDay)(constraints.minDate))) return false;
147
+ if (constraints.maxDate && (0, date_fns.isAfter)(day, (0, date_fns.startOfDay)(constraints.maxDate))) return false;
148
+ if (constraints.isDateDisabled?.(day)) return false;
149
+ return true;
150
+ }
151
+ /**
152
+ * The range after pressing `pressed`, mirroring the familiar date-picker model:
153
+ * - no range yet, or a complete range exists → start fresh (`{ start: pressed, end: null }`),
154
+ * so a third press resets the selection.
155
+ * - an open range (a start but no end) → close it, auto-swapping when the press
156
+ * precedes the start so `start <= end` always holds.
157
+ *
158
+ * Returns `current` unchanged when `pressed` isn't selectable.
159
+ */
160
+ function nextDateRange(current, pressed, constraints = {}) {
161
+ if (!isDateSelectable(pressed, constraints)) return current;
162
+ const day = (0, date_fns.startOfDay)(pressed);
163
+ if (!current || current.end) return {
164
+ start: day,
165
+ end: null
166
+ };
167
+ if ((0, date_fns.isBefore)(day, current.start)) return {
168
+ start: day,
169
+ end: current.start
170
+ };
171
+ return {
172
+ start: current.start,
173
+ end: day
174
+ };
175
+ }
176
+ /** True when `date` is one of the range's two endpoints. */
177
+ function isRangeEndpoint(date, range) {
178
+ if (!range) return false;
179
+ if (isSameCalendarDay(date, range.start)) return true;
180
+ return range.end ? isSameCalendarDay(date, range.end) : false;
181
+ }
182
+ /** True when `date` falls within a complete range (endpoints included). */
183
+ function isWithinDateRange(date, range) {
184
+ if (!range || !range.end) return false;
185
+ const day = (0, date_fns.startOfDay)(date).getTime();
186
+ const a = (0, date_fns.startOfDay)(range.start).getTime();
187
+ const b = (0, date_fns.startOfDay)(range.end).getTime();
188
+ return day >= Math.min(a, b) && day <= Math.max(a, b);
189
+ }
190
+ /**
191
+ * The canonical per-day selection state, shared by `MonthView` (rendering) and
192
+ * `buildMonthGrid` (the headless grid) so the built-in views and a custom
193
+ * calendar can never disagree on what a day's state is.
194
+ */
195
+ function daySelectionState(date, selection, constraints = {}) {
196
+ const isDisabled = !isDateSelectable(date, constraints);
197
+ const range = selection.selectedRange ?? null;
198
+ return {
199
+ isDisabled,
200
+ isSelected: !isDisabled && ((selection.selectedDates?.some((selected) => isSameCalendarDay(selected, date)) ?? false) || isRangeEndpoint(date, range)),
201
+ isInRange: !isDisabled && isWithinDateRange(date, range),
202
+ isRangeStart: !isDisabled && range != null && isSameCalendarDay(date, range.start),
203
+ isRangeEnd: !isDisabled && range?.end != null && isSameCalendarDay(date, range.end)
204
+ };
205
+ }
206
+ const CalendarSelectionContext = (0, react.createContext)({});
207
+ /**
208
+ * Provides the active selection to the month grid. Day cells read it via
209
+ * {@link useCalendarSelection} so they repaint on selection changes even when
210
+ * the virtualized list has cached (and so won't re-render) their page.
211
+ */
212
+ const CalendarSelectionProvider = CalendarSelectionContext.Provider;
213
+ const useCalendarSelection = () => (0, react.useContext)(CalendarSelectionContext);
214
+ /**
215
+ * Controlled-ish range selection state for the month view. Returns the current
216
+ * `range` plus an `onPressDate` handler to wire to `Calendar`'s `onPressDay`, a
217
+ * `reset`, and the raw `setRange` for full control.
218
+ *
219
+ * ```tsx
220
+ * const { range, onPressDate } = useDateRange({ minDate: new Date() });
221
+ * <Calendar mode="month" selectedRange={range ?? undefined} onPressDay={onPressDate} … />
222
+ * ```
223
+ */
224
+ function useDateRange(options = {}) {
225
+ const { initialRange = null, minDate, maxDate, isDateDisabled } = options;
226
+ const [range, setRange] = (0, react.useState)(initialRange);
227
+ const constraints = (0, react.useMemo)(() => ({
228
+ minDate,
229
+ maxDate,
230
+ isDateDisabled
231
+ }), [
232
+ minDate,
233
+ maxDate,
234
+ isDateDisabled
235
+ ]);
236
+ return {
237
+ range,
238
+ onPressDate: (0, react.useCallback)((date) => setRange((previous) => nextDateRange(previous, date, constraints)), [constraints]),
239
+ selectRange: (0, react.useCallback)((a, b) => {
240
+ if (!isDateSelectable(a, constraints) || !isDateSelectable(b, constraints)) return;
241
+ const [start, end] = a.getTime() <= b.getTime() ? [a, b] : [b, a];
242
+ setRange({
243
+ start: (0, date_fns.startOfDay)(start),
244
+ end: (0, date_fns.startOfDay)(end)
245
+ });
246
+ }, [constraints]),
247
+ reset: (0, react.useCallback)(() => setRange(null), []),
248
+ setRange
249
+ };
250
+ }
251
+ //#endregion
252
+ //#region src/utils/drag.ts
253
+ /**
254
+ * Minutes to shift an event, snapping a vertical pixel drag to the nearest
255
+ * `stepMinutes`. Runs on the UI thread inside the drag gesture. Returns 0 for a
256
+ * degenerate grid (non-positive height/step).
257
+ */
258
+ function snapDeltaMinutes(translationPx, cellHeightPx, stepMinutes) {
259
+ "worklet";
260
+ if (cellHeightPx <= 0 || stepMinutes <= 0) return 0;
261
+ const rawMinutes = translationPx / cellHeightPx * 60;
262
+ return Math.round(rawMinutes / stepMinutes) * stepMinutes;
263
+ }
264
+ /** A copy of `date` shifted by `minutes` (may be negative). */
265
+ function shiftMinutes(date, minutes) {
266
+ const next = new Date(date);
267
+ next.setMinutes(next.getMinutes() + minutes);
268
+ return next;
269
+ }
270
+ /**
271
+ * Resolve a committed drag into the event's new bounds: `start` shifts by
272
+ * `deltaStartMinutes`, `end` by `deltaEndMinutes` (a move passes the same delta
273
+ * to both; a resize passes 0 for the start). Returns `null` when the change
274
+ * would collapse the event below one `snapMinutes` step, so the gesture leaves
275
+ * the event untouched rather than committing a degenerate duration. Pure, so the
276
+ * commit path is unit-testable without a running gesture.
277
+ */
278
+ function resolveDraggedBounds(start, end, deltaStartMinutes, deltaEndMinutes, snapMinutes) {
279
+ const nextStart = shiftMinutes(start, deltaStartMinutes);
280
+ const nextEnd = shiftMinutes(end, deltaEndMinutes);
281
+ if (nextEnd.getTime() - nextStart.getTime() < snapMinutes * 6e4) return null;
282
+ return {
283
+ start: nextStart,
284
+ end: nextEnd
285
+ };
286
+ }
287
+ /**
288
+ * The start/end of a new event swept out on `day` by dragging from `startPx` to
289
+ * `endPx` (vertical pixels from the grid's top, i.e. the `minHour` line). Both
290
+ * ends snap to `snapMinutes`; the range is ordered (drag up or down) and widened
291
+ * to at least one step so a stationary press still yields a usable event.
292
+ * Returns `null` for a degenerate grid (non-positive height/step). Pure, so the
293
+ * commit path is unit-testable without a running gesture.
294
+ */
295
+ function cellRangeFromDrag(day, startPx, endPx, cellHeightPx, minHour, snapMinutes) {
296
+ if (cellHeightPx <= 0 || snapMinutes <= 0) return null;
297
+ const snapAt = (px) => {
298
+ const rawMinutes = (minHour + px / cellHeightPx) * 60;
299
+ return Math.round(rawMinutes / snapMinutes) * snapMinutes;
300
+ };
301
+ const MAX = 1440;
302
+ let lower = Math.max(0, Math.min(MAX - snapMinutes, snapAt(startPx)));
303
+ let upper = Math.max(0, Math.min(MAX, snapAt(endPx)));
304
+ if (upper < lower) [lower, upper] = [upper, lower];
305
+ if (upper - lower < snapMinutes) upper = Math.min(MAX, lower + snapMinutes);
306
+ const start = new Date(day);
307
+ start.setHours(0, 0, 0, 0);
308
+ start.setMinutes(lower);
309
+ const end = new Date(day);
310
+ end.setHours(0, 0, 0, 0);
311
+ end.setMinutes(upper);
312
+ return {
313
+ start,
314
+ end
315
+ };
316
+ }
317
+ //#endregion
318
+ //#region src/utils/eventDisplay.ts
319
+ /**
320
+ * Minimum event-box height (px) before the built-in renderer shows the time line
321
+ * on a narrow multi-column timed grid. Tied to the default theme's font sizes.
322
+ */
323
+ const MIN_BOX_HEIGHT_FOR_TIME = 56;
324
+ /** Hard-clip an overflowing title by default; opt into a trailing ellipsis. */
325
+ function titleEllipsizeMode(ellipsizeTitle) {
326
+ return ellipsizeTitle ? "tail" : "clip";
327
+ }
328
+ /**
329
+ * Screen-reader label for an event: its title followed by "all day" or its time
330
+ * range (which the grid otherwise only conveys visually). Empty title is dropped.
331
+ */
332
+ function eventAccessibilityLabel(args) {
333
+ const timeFormat = args.ampm ? "h:mm a" : "HH:mm";
334
+ const time = args.isAllDay ? args.allDayLabel ?? "all day" : `${(0, date_fns.format)(args.start, timeFormat)} to ${(0, date_fns.format)(args.end, timeFormat)}`;
335
+ return [args.title, time].filter(Boolean).join(", ");
336
+ }
337
+ /**
338
+ * Month cells and the all-day lane show a single clipped line; timed-grid titles
339
+ * (`undefined`) wrap to fill the box.
340
+ */
341
+ function titleNumberOfLines(mode, isAllDay) {
342
+ return mode === "month" || isAllDay ? 1 : void 0;
343
+ }
344
+ /**
345
+ * The secondary line under the title in the built-in renderer, or `null` when
346
+ * none should show. Timed events get their `start - end` range. An all-day event
347
+ * gets the literal "All day" in the schedule (which has no all-day lane to
348
+ * signal it positionally) and nothing on the day/week grid (the lane already
349
+ * does). Month cells and `showTime={false}` always return `null`.
350
+ */
351
+ function eventTimeLabel(args) {
352
+ if (!args.showTime || args.mode === "month") return null;
353
+ if (args.isAllDay) return args.mode === "schedule" ? args.allDayLabel ?? "All day" : null;
354
+ const timeFormat = args.ampm ? "h:mm a" : "HH:mm";
355
+ return `${(0, date_fns.format)(args.start, timeFormat)} - ${(0, date_fns.format)(args.end, timeFormat)}`;
356
+ }
357
+ /**
358
+ * The default hour-axis label shared by both renderers' time grids, so the gutter
359
+ * reads the same on each: 24-hour "HH:00" (e.g. "08:00"), or a compact 12-hour
360
+ * "h AM/PM" (e.g. "8 AM") when `ampm` is set. Exported so a custom hour renderer
361
+ * can reuse the same formatting.
362
+ */
363
+ function formatHour(hour, opts) {
364
+ if (opts?.ampm) {
365
+ const period = hour < 12 ? "AM" : "PM";
366
+ return `${hour % 12 === 0 ? 12 : hour % 12} ${period}`;
367
+ }
368
+ return `${String(hour).padStart(2, "0")}:00`;
369
+ }
370
+ /**
371
+ * Whether the time line fits in the box. The wide `day` column and contexts with
372
+ * no live box height (e.g. schedule, where `boxHeightPx` is undefined) always
373
+ * show it; narrow multi-column modes only once the box is at least
374
+ * {@link MIN_BOX_HEIGHT_FOR_TIME} tall. Runs on the UI thread inside the event
375
+ * renderer's animated style.
376
+ */
377
+ function isTimeVisibleAtHeight(boxHeightPx, mode) {
378
+ "worklet";
379
+ if (boxHeightPx == null || mode === "day") return true;
380
+ return boxHeightPx >= 56;
381
+ }
382
+ /**
383
+ * Lay out the built-in timed-grid event chip for a box of `boxHeightPx`: how
384
+ * many whole title lines fit, and whether the time line still has room below
385
+ * them. The title is primary, so the title fills the box in whole lines (never a
386
+ * half-cropped line) and the time only shows once a full line is left over. Pass
387
+ * `titleLineHeightPx`/`timeLineHeightPx` matching the rendered line heights so
388
+ * the clamp lands on a line boundary.
389
+ *
390
+ * Worklet-safe, so the native renderer can drive the title's max-height on the UI
391
+ * thread as the grid zooms; the dom renderer calls it with its static box height.
392
+ * A `boxHeightPx` of `undefined` (the schedule has no live box height) returns
393
+ * `titleMaxLines: 0` (no clamp) with the unconditional time visibility.
394
+ */
395
+ function eventChipLayout(args) {
396
+ "worklet";
397
+ const wantTime = args.hasTime && isTimeVisibleAtHeight(args.boxHeightPx, args.mode);
398
+ if (args.boxHeightPx == null || args.titleLineHeightPx <= 0) return {
399
+ titleMaxLines: 0,
400
+ showTime: wantTime
401
+ };
402
+ const inner = args.boxHeightPx - args.paddingYPx * 2;
403
+ const titleMaxLines = Math.max(1, Math.floor((inner - (wantTime ? args.timeLineHeightPx : 0)) / args.titleLineHeightPx));
404
+ return {
405
+ titleMaxLines,
406
+ showTime: wantTime && inner - titleMaxLines * args.titleLineHeightPx >= args.timeLineHeightPx
407
+ };
408
+ }
409
+ /**
410
+ * Derive how many event chips fit in a month cell from the measured space.
411
+ * `chipRowHeightPx` is one chip plus its gap; `moreRowHeightPx` is the overflow
412
+ * label plus its gap. Both counts are clamped to >= 0.
413
+ */
414
+ function monthEventCapacity(availableHeightPx, chipRowHeightPx, moreRowHeightPx) {
415
+ if (chipRowHeightPx <= 0) return {
416
+ full: 0,
417
+ withMore: 0
418
+ };
419
+ return {
420
+ full: Math.max(0, Math.floor(availableHeightPx / chipRowHeightPx)),
421
+ withMore: Math.max(0, Math.floor((availableHeightPx - moreRowHeightPx) / chipRowHeightPx))
422
+ };
423
+ }
424
+ /**
425
+ * Chips to show for a day: all of them when they fit, otherwise `withMore` (at
426
+ * least one) so the rest collapse into a "+N more" label.
427
+ */
428
+ function monthVisibleCount(total, capacity) {
429
+ if (total <= capacity.full) return total;
430
+ return Math.max(1, capacity.withMore);
431
+ }
432
+ //#endregion
433
+ //#region src/utils/layout.ts
434
+ const MINUTES_PER_HOUR = 60;
435
+ const MIN_DURATION_HOURS = .25;
436
+ /**
437
+ * Lay out a single day's events: events that overlap in time are split into
438
+ * side-by-side columns. Multi-day events are clipped to the portion that falls
439
+ * on `day` (e.g. a 23:00→01:00 event renders 23:00–24:00 on the start day and
440
+ * 00:00–01:00 on the next). Pure — safe to call per render, never per frame.
441
+ */
442
+ function layoutDayEvents(events, day) {
443
+ const dayStart = (0, date_fns.startOfDay)(day);
444
+ const nextDayStart = (0, date_fns.addDays)(dayStart, 1);
445
+ const segments = events.filter((event) => !isAllDayEvent(event)).filter((event) => event.start < nextDayStart && event.end > dayStart).map((event) => {
446
+ const segStart = (0, date_fns.max)([event.start, dayStart]);
447
+ const segEnd = (0, date_fns.min)([event.end, nextDayStart]);
448
+ return {
449
+ event,
450
+ start: (0, date_fns.differenceInMinutes)(segStart, dayStart) / MINUTES_PER_HOUR,
451
+ end: (0, date_fns.differenceInMinutes)(segEnd, dayStart) / MINUTES_PER_HOUR,
452
+ continuesBefore: event.start < dayStart,
453
+ continuesAfter: event.end > nextDayStart
454
+ };
455
+ }).sort((a, b) => a.start - b.start);
456
+ const positioned = [];
457
+ let cluster = [];
458
+ let clusterEnd = Number.NEGATIVE_INFINITY;
459
+ const flushCluster = () => {
460
+ const columnEnds = [];
461
+ const columnOf = /* @__PURE__ */ new Map();
462
+ for (const seg of cluster) {
463
+ let column = columnEnds.findIndex((end) => end <= seg.start);
464
+ if (column === -1) {
465
+ column = columnEnds.length;
466
+ columnEnds.push(seg.end);
467
+ } else columnEnds[column] = seg.end;
468
+ columnOf.set(seg, column);
469
+ }
470
+ for (const seg of cluster) positioned.push({
471
+ event: seg.event,
472
+ startHours: seg.start,
473
+ durationHours: Math.max(seg.end - seg.start, MIN_DURATION_HOURS),
474
+ column: columnOf.get(seg) ?? 0,
475
+ columns: columnEnds.length,
476
+ continuesBefore: seg.continuesBefore,
477
+ continuesAfter: seg.continuesAfter
478
+ });
479
+ cluster = [];
480
+ };
481
+ for (const seg of segments) {
482
+ if (cluster.length > 0 && seg.start >= clusterEnd) flushCluster();
483
+ cluster.push(seg);
484
+ clusterEnd = Math.max(clusterEnd, seg.end);
485
+ }
486
+ if (cluster.length > 0) flushCluster();
487
+ return positioned;
488
+ }
489
+ const atMidnight = (date) => date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0;
490
+ /**
491
+ * Whether an event belongs in the all-day lane. An explicit `allDay` flag wins;
492
+ * otherwise it's inferred when the event spans whole days (both `start` and
493
+ * `end` land on midnight, e.g. an iCal-style all-day event). Pure.
494
+ */
495
+ function isAllDayEvent(event) {
496
+ if (typeof event.allDay === "boolean") return event.allDay;
497
+ return event.end > event.start && atMidnight(event.start) && atMidnight(event.end);
498
+ }
499
+ /**
500
+ * The `startOfDay` ISO keys of every calendar day an event touches (inclusive).
501
+ * An event ending exactly at midnight does not count the following day. Used to
502
+ * index events by day for the month grid. Pure.
503
+ */
504
+ function eventDayKeys(event) {
505
+ const first = (0, date_fns.startOfDay)(event.start);
506
+ const last = (0, date_fns.startOfDay)(event.end > event.start ? /* @__PURE__ */ new Date(event.end.getTime() - 1) : event.start);
507
+ const keys = [];
508
+ for (let cursor = first; cursor <= last; cursor = (0, date_fns.addDays)(cursor, 1)) keys.push(cursor.toISOString());
509
+ return keys;
510
+ }
511
+ /**
512
+ * Index events by the `startOfDay` ISO key of every day they touch (via
513
+ * {@link eventDayKeys}), so a month grid can look up a day's events with
514
+ * `startOfDay(date).toISOString()`. Built once and shared across month cells.
515
+ */
516
+ function groupEventsByDay(events) {
517
+ const map = /* @__PURE__ */ new Map();
518
+ for (const event of events) for (const key of eventDayKeys(event)) {
519
+ const list = map.get(key);
520
+ if (list) list.push(event);
521
+ else map.set(key, [event]);
522
+ }
523
+ return map;
524
+ }
525
+ /**
526
+ * Order a day's events for the month and list views: all-day events come first
527
+ * (they head the day regardless of their start time), then timed events by start.
528
+ * Shared by both renderers so the order is identical. Use as an `Array.sort`
529
+ * comparator.
530
+ */
531
+ function compareDayEvents(a, b) {
532
+ const aAllDay = isAllDayEvent(a);
533
+ if (aAllDay !== isAllDayEvent(b)) return aAllDay ? -1 : 1;
534
+ return a.start.getTime() - b.start.getTime();
535
+ }
536
+ /**
537
+ * The closed hour-spans of a day to shade on the time grid, given a
538
+ * `businessHours` callback and the visible `[minHour, maxHour]` window: the spans
539
+ * before open and after close (clamped to the window), the whole window when the
540
+ * day is closed (`null`) or the open hours are inverted/empty, or none when the
541
+ * callback returns `undefined`. Shared by both renderers so shading stays
542
+ * identical. Co-located with `groupEventsByDay`; both feed the grid layout.
543
+ */
544
+ function closedHourBands(day, businessHours, minHour = 0, maxHour = 24) {
545
+ const open = businessHours?.(day);
546
+ if (open === void 0) return [];
547
+ if (open === null) return [{
548
+ start: minHour,
549
+ end: maxHour
550
+ }];
551
+ const start = Math.max(minHour, Math.min(maxHour, open.start));
552
+ const end = Math.max(minHour, Math.min(maxHour, open.end));
553
+ if (start >= end) return [{
554
+ start: minHour,
555
+ end: maxHour
556
+ }];
557
+ const bands = [];
558
+ if (start > minHour) bands.push({
559
+ start: minHour,
560
+ end: start
561
+ });
562
+ if (end < maxHour) bands.push({
563
+ start: end,
564
+ end: maxHour
565
+ });
566
+ return bands;
567
+ }
568
+ //#endregion
569
+ //#region src/utils/monthGrid.ts
570
+ /**
571
+ * Pure month-grid builder: the weeks and weekday headers for `month`, each day
572
+ * annotated with selection/disabled/today state. Use this when you need the
573
+ * data outside React; inside a component prefer {@link useMonthGrid}.
574
+ */
575
+ function buildMonthGrid(month, options = {}) {
576
+ const { weekStartsOn = 0, showSixWeeks = false, isRTL = false, selectedDates, selectedRange, minDate, maxDate, isDateDisabled, locale } = options;
577
+ const rows = buildMonthWeeks(month, weekStartsOn, {
578
+ showSixWeeks,
579
+ isRTL
580
+ });
581
+ return {
582
+ weeks: rows.map((days) => ({
583
+ id: days[0].toISOString(),
584
+ days: days.map((date) => ({
585
+ date,
586
+ id: (0, date_fns.format)(date, "yyyy-MM-dd"),
587
+ label: (0, date_fns.format)(date, "d"),
588
+ isCurrentMonth: (0, date_fns.isSameMonth)(date, month),
589
+ isToday: getIsToday(date),
590
+ isWeekend: isWeekend(date),
591
+ ...daySelectionState(date, {
592
+ selectedDates,
593
+ selectedRange
594
+ }, {
595
+ minDate,
596
+ maxDate,
597
+ isDateDisabled
598
+ })
599
+ }))
600
+ })),
601
+ weekdays: rows[0].map((date) => ({
602
+ date,
603
+ label: (0, date_fns.format)(date, "EEE", { locale })
604
+ }))
605
+ };
606
+ }
607
+ /**
608
+ * Headless month-grid hook. Returns the weeks and weekday headers for `month`,
609
+ * each day annotated with selection/disabled/today state, so you can render a
610
+ * fully custom calendar without reimplementing the date maths.
611
+ *
612
+ * ```tsx
613
+ * const { weeks, weekdays } = useMonthGrid(month, { selectedRange: range });
614
+ * // map weekdays -> header cells, weeks -> rows, days -> your own <DayCell />
615
+ * ```
616
+ */
617
+ function useMonthGrid(month, options = {}) {
618
+ const { weekStartsOn, showSixWeeks, isRTL, selectedDates, selectedRange, minDate, maxDate, isDateDisabled, locale } = options;
619
+ return (0, react.useMemo)(() => buildMonthGrid(month, options), [
620
+ month,
621
+ weekStartsOn,
622
+ showSixWeeks,
623
+ isRTL,
624
+ selectedDates,
625
+ selectedRange,
626
+ minDate,
627
+ maxDate,
628
+ isDateDisabled,
629
+ locale
630
+ ]);
631
+ }
632
+ //#endregion
633
+ //#region src/utils/recurrence.ts
634
+ const STEP = {
635
+ daily: date_fns.addDays,
636
+ weekly: date_fns.addWeeks,
637
+ monthly: date_fns.addMonths,
638
+ yearly: date_fns.addYears
639
+ };
640
+ const MAX_OCCURRENCES = 5e3;
641
+ function withTimeOf(date, source) {
642
+ const next = new Date(date);
643
+ next.setHours(source.getHours(), source.getMinutes(), source.getSeconds(), source.getMilliseconds());
644
+ return next;
645
+ }
646
+ function* occurrenceStarts(start, rule, rangeEnd) {
647
+ const interval = Math.max(1, Math.trunc(rule.interval ?? 1));
648
+ let produced = 0;
649
+ const within = (date) => date.getTime() <= rangeEnd.getTime() && (rule.until == null || date.getTime() <= rule.until.getTime()) && (rule.count == null || produced < rule.count) && produced < MAX_OCCURRENCES;
650
+ if (rule.freq === "weekly" && rule.weekdays?.length) {
651
+ const weekdays = [...new Set(rule.weekdays)].sort((a, b) => a - b);
652
+ let weekStart = (0, date_fns.startOfWeek)(start, { weekStartsOn: 0 });
653
+ while (true) {
654
+ let advanced = false;
655
+ for (const weekday of weekdays) {
656
+ const date = withTimeOf((0, date_fns.addDays)(weekStart, weekday), start);
657
+ if (date.getTime() < start.getTime()) continue;
658
+ if (!within(date)) return;
659
+ produced += 1;
660
+ advanced = true;
661
+ yield date;
662
+ }
663
+ const nextWeek = (0, date_fns.addWeeks)(weekStart, interval);
664
+ if (!advanced && nextWeek.getTime() > rangeEnd.getTime()) return;
665
+ weekStart = nextWeek;
666
+ }
667
+ }
668
+ let cursor = start;
669
+ while (within(cursor)) {
670
+ produced += 1;
671
+ yield cursor;
672
+ cursor = STEP[rule.freq](cursor, interval);
673
+ }
674
+ }
675
+ function instanceAt(event, start, durationMs) {
676
+ const instance = {
677
+ ...event,
678
+ start,
679
+ end: new Date(start.getTime() + durationMs)
680
+ };
681
+ delete instance.recurrence;
682
+ return instance;
683
+ }
684
+ /**
685
+ * Materialise recurring events into concrete occurrences overlapping
686
+ * `[rangeStart, rangeEnd]`. Non-recurring events pass through untouched, so the
687
+ * result is ready to hand to `<Calendar events={...} />`. Each occurrence keeps
688
+ * the original event's duration and fields (minus `recurrence`).
689
+ */
690
+ function expandRecurringEvents(events, rangeStart, rangeEnd) {
691
+ const out = [];
692
+ for (const event of events) {
693
+ if (!event.recurrence) {
694
+ out.push(event);
695
+ continue;
696
+ }
697
+ const durationMs = event.end.getTime() - event.start.getTime();
698
+ for (const start of occurrenceStarts(event.start, event.recurrence, rangeEnd)) {
699
+ if (start.getTime() + durationMs < rangeStart.getTime()) continue;
700
+ out.push(instanceAt(event, start, durationMs));
701
+ }
702
+ }
703
+ return out;
704
+ }
705
+ //#endregion
706
+ //#region src/utils/timezone.ts
707
+ /**
708
+ * Reinterpret an instant as its wall-clock time in `timeZone`, returned as a
709
+ * device-local `Date` whose fields (hours, minutes, …) read back as that zone's
710
+ * clock. The calendar lays events out from `getHours()`/`getMinutes()`, so
711
+ * passing zoned dates makes it render in `timeZone` regardless of the device.
712
+ *
713
+ * DST-correct via `Intl` (available on modern React Native Hermes/JSC and the
714
+ * web). The result is for display/layout only; it no longer points at the
715
+ * original UTC instant, so don't round-trip it back to a real time.
716
+ */
717
+ function toZonedTime(date, timeZone) {
718
+ const parts = new Intl.DateTimeFormat("en-US", {
719
+ timeZone,
720
+ hourCycle: "h23",
721
+ year: "numeric",
722
+ month: "2-digit",
723
+ day: "2-digit",
724
+ hour: "2-digit",
725
+ minute: "2-digit",
726
+ second: "2-digit"
727
+ }).formatToParts(date);
728
+ const field = (type) => {
729
+ const part = parts.find((p) => p.type === type);
730
+ return part ? Number(part.value) : 0;
731
+ };
732
+ const hour = field("hour") % 24;
733
+ return new Date(field("year"), field("month") - 1, field("day"), hour, field("minute"), field("second"), date.getMilliseconds());
734
+ }
735
+ /**
736
+ * Map every event's `start`/`end` through {@link toZonedTime} so the calendar
737
+ * displays them in `timeZone`. Other fields are preserved. Memoize the result
738
+ * (e.g. with `useMemo`) since it allocates new dates.
739
+ */
740
+ function eventsInTimeZone(events, timeZone) {
741
+ return events.map((event) => ({
742
+ ...event,
743
+ start: toZonedTime(event.start, timeZone),
744
+ end: toZonedTime(event.end, timeZone)
745
+ }));
746
+ }
747
+ //#endregion
748
+ exports.CalendarSelectionProvider = CalendarSelectionProvider;
749
+ exports.MIN_BOX_HEIGHT_FOR_TIME = MIN_BOX_HEIGHT_FOR_TIME;
750
+ exports.bandRounding = bandRounding;
751
+ exports.buildMonthGrid = buildMonthGrid;
752
+ exports.buildMonthWeeks = buildMonthWeeks;
753
+ exports.cellRangeFromDrag = cellRangeFromDrag;
754
+ exports.closedHourBands = closedHourBands;
755
+ exports.compareDayEvents = compareDayEvents;
756
+ exports.darkColors = darkColors;
757
+ exports.dayBadgeKind = dayBadgeKind;
758
+ exports.daySelectionState = daySelectionState;
759
+ exports.eventAccessibilityLabel = eventAccessibilityLabel;
760
+ exports.eventChipLayout = eventChipLayout;
761
+ exports.eventDayKeys = eventDayKeys;
762
+ exports.eventTimeLabel = eventTimeLabel;
763
+ exports.eventsInTimeZone = eventsInTimeZone;
764
+ exports.expandRecurringEvents = expandRecurringEvents;
765
+ exports.formatHour = formatHour;
766
+ exports.getIsToday = getIsToday;
767
+ exports.getViewDays = getViewDays;
768
+ exports.getWeekDays = getWeekDays;
769
+ exports.groupEventsByDay = groupEventsByDay;
770
+ exports.isAllDayEvent = isAllDayEvent;
771
+ exports.isDateSelectable = isDateSelectable;
772
+ exports.isRangeEndpoint = isRangeEndpoint;
773
+ exports.isSameCalendarDay = isSameCalendarDay;
774
+ exports.isTimeVisibleAtHeight = isTimeVisibleAtHeight;
775
+ exports.isWeekend = isWeekend;
776
+ exports.isWithinDateRange = isWithinDateRange;
777
+ exports.layoutDayEvents = layoutDayEvents;
778
+ exports.lightColors = lightColors;
779
+ exports.minutesIntoDay = minutesIntoDay;
780
+ exports.monthEventCapacity = monthEventCapacity;
781
+ exports.monthVisibleCount = monthVisibleCount;
782
+ exports.nextDateRange = nextDateRange;
783
+ exports.rangeBandKind = rangeBandKind;
784
+ exports.resolveDraggedBounds = resolveDraggedBounds;
785
+ exports.shiftMinutes = shiftMinutes;
786
+ exports.snapDeltaMinutes = snapDeltaMinutes;
787
+ exports.titleEllipsizeMode = titleEllipsizeMode;
788
+ exports.titleNumberOfLines = titleNumberOfLines;
789
+ exports.toZonedTime = toZonedTime;
790
+ exports.useCalendarSelection = useCalendarSelection;
791
+ exports.useDateRange = useDateRange;
792
+ exports.useMonthGrid = useMonthGrid;
793
+ exports.viewDayCount = viewDayCount;
794
+ exports.weekDaysCount = weekDaysCount;