@super-calendar/native 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.
@@ -0,0 +1,1030 @@
1
+ import { addMonths, differenceInCalendarMonths, format, isSameMonth, startOfDay, startOfMonth } from "date-fns";
2
+ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
+ import { CalendarSelectionProvider, buildMonthWeeks, compareDayEvents, darkColors, dayBadgeKind, daySelectionState, eventAccessibilityLabel, eventTimeLabel, getIsToday, getWeekDays, groupEventsByDay, isAllDayEvent, isDateSelectable, isSameCalendarDay, isWeekend, lightColors, monthEventCapacity, monthVisibleCount, rangeBandKind, titleEllipsizeMode, titleNumberOfLines, useCalendarSelection } from "@super-calendar/core";
4
+ import { LegendList } from "@legendapp/list/react-native";
5
+ import { Platform, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from "react-native";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
8
+ //#region src/theme.ts
9
+ const defaultTheme = {
10
+ colors: lightColors,
11
+ text: {
12
+ dayNumber: {
13
+ fontSize: 22,
14
+ fontWeight: "700"
15
+ },
16
+ weekday: {
17
+ fontSize: 13,
18
+ fontWeight: "700"
19
+ },
20
+ dateCell: {
21
+ fontSize: 13,
22
+ fontWeight: "700"
23
+ },
24
+ hourLabel: { fontSize: 10 },
25
+ more: {
26
+ fontSize: 11,
27
+ fontWeight: "700"
28
+ },
29
+ eventTitle: {
30
+ fontSize: 12,
31
+ fontWeight: "700",
32
+ lineHeight: 16
33
+ }
34
+ },
35
+ todayBadgeRadius: 999,
36
+ rangeBandHeight: 22
37
+ };
38
+ /**
39
+ * A ready-made dark palette. Pass it straight to `<Calendar theme={darkTheme} />`,
40
+ * or switch on the system scheme with React Native's `useColorScheme()`. Shares
41
+ * {@link defaultTheme}'s typography and metrics; only the colours change.
42
+ */
43
+ const darkTheme = {
44
+ colors: darkColors,
45
+ text: defaultTheme.text,
46
+ todayBadgeRadius: defaultTheme.todayBadgeRadius,
47
+ rangeBandHeight: defaultTheme.rangeBandHeight
48
+ };
49
+ /** Deep-merge a partial theme over {@link defaultTheme}. */
50
+ function mergeTheme(theme) {
51
+ if (!theme) return defaultTheme;
52
+ return {
53
+ colors: {
54
+ ...defaultTheme.colors,
55
+ ...theme.colors
56
+ },
57
+ text: {
58
+ ...defaultTheme.text,
59
+ ...theme.text
60
+ },
61
+ todayBadgeRadius: theme.todayBadgeRadius ?? defaultTheme.todayBadgeRadius,
62
+ rangeBandHeight: theme.rangeBandHeight ?? defaultTheme.rangeBandHeight
63
+ };
64
+ }
65
+ const CalendarThemeContext = createContext(defaultTheme);
66
+ const CalendarThemeProvider = CalendarThemeContext.Provider;
67
+ const useCalendarTheme = () => useContext(CalendarThemeContext);
68
+ //#endregion
69
+ //#region src/utils/useWebPagerKeys.ts
70
+ const getDocument = () => globalThis.document;
71
+ const isTextEntry = (element) => {
72
+ const tag = element?.tagName;
73
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element?.isContentEditable === true;
74
+ };
75
+ /**
76
+ * Web-only: page the calendar with the ← / → arrow keys, standing in for the
77
+ * horizontal swipe that the grid disables on web. No-op off web or when
78
+ * `enabled` is false. `onPage(delta)` receives -1 (previous) or +1 (next).
79
+ */
80
+ function useWebPagerKeys(enabled, onPage) {
81
+ useEffect(() => {
82
+ if (Platform.OS !== "web" || !enabled) return;
83
+ const doc = getDocument();
84
+ if (!doc) return;
85
+ const handler = (event) => {
86
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
87
+ if (isTextEntry(doc.activeElement)) return;
88
+ if (event.key === "ArrowRight") {
89
+ event.preventDefault();
90
+ onPage(1);
91
+ } else if (event.key === "ArrowLeft") {
92
+ event.preventDefault();
93
+ onPage(-1);
94
+ }
95
+ };
96
+ doc.addEventListener("keydown", handler);
97
+ return () => doc.removeEventListener("keydown", handler);
98
+ }, [enabled, onPage]);
99
+ }
100
+ //#endregion
101
+ //#region src/components/MonthView.tsx
102
+ const isWeb$2 = Platform.OS === "web";
103
+ const DAY_CELL_PADDING_TOP = 4;
104
+ const DATE_BADGE_HEIGHT = 24;
105
+ const BAND_CENTER_Y = 16;
106
+ const CELL_ROW_GAP = 2;
107
+ const CHIP_PADDING_V = 2;
108
+ const FALLBACK_VISIBLE_COUNT = 3;
109
+ const numericStyle = (value, fallback) => typeof value === "number" ? value : fallback;
110
+ function MonthViewInner({ date, events, maxVisibleEventCount, weekStartsOn, locale, sortedMonthView = true, moreLabel = "{moreCount} More", showAdjacentMonths = true, disableMonthEventCellPress = false, isRTL = false, showSixWeeks = false, showTitle = true, showWeekdays = true, activeDate, selectedDates: selectedDatesProp, selectedRange: selectedRangeProp, fillCellOnSelection = false, minDate: minDateProp, maxDate: maxDateProp, isDateDisabled: isDateDisabledProp, calendarCellStyle, renderEvent, keyExtractor, onPressDay, onLongPressDay, onPressEvent, onLongPressEvent, onPressMore, renderCustomDateForMonth, onDayPointerDown, onDayPointerEnter }) {
111
+ const theme = useCalendarTheme();
112
+ const selection = useCalendarSelection();
113
+ const selectedDates = selectedDatesProp ?? selection.selectedDates;
114
+ const selectedRange = selectedRangeProp ?? selection.selectedRange;
115
+ const minDate = minDateProp ?? selection.minDate;
116
+ const maxDate = maxDateProp ?? selection.maxDate;
117
+ const isDateDisabled = isDateDisabledProp ?? selection.isDateDisabled;
118
+ const RenderEventComponent = renderEvent;
119
+ const [gridHeight, setGridHeight] = useState(0);
120
+ const [hoveredKey, setHoveredKey] = useState(null);
121
+ const [pressedKey, setPressedKey] = useState(null);
122
+ const weeks = useMemo(() => buildMonthWeeks(date, weekStartsOn, {
123
+ showSixWeeks,
124
+ isRTL
125
+ }), [
126
+ date,
127
+ weekStartsOn,
128
+ isRTL,
129
+ showSixWeeks
130
+ ]);
131
+ const weekdayLabels = useMemo(() => {
132
+ const days = getWeekDays(date, weekStartsOn);
133
+ return isRTL ? days.reverse() : days;
134
+ }, [
135
+ date,
136
+ weekStartsOn,
137
+ isRTL
138
+ ]);
139
+ const capacity = useMemo(() => {
140
+ if (maxVisibleEventCount != null) return {
141
+ full: maxVisibleEventCount,
142
+ withMore: maxVisibleEventCount
143
+ };
144
+ if (gridHeight <= 0 || weeks.length === 0) return {
145
+ full: FALLBACK_VISIBLE_COUNT,
146
+ withMore: FALLBACK_VISIBLE_COUNT
147
+ };
148
+ const rowHeight = gridHeight / weeks.length;
149
+ const fontSize = numericStyle(theme.text.eventTitle.fontSize, 12);
150
+ const chipRowHeight = numericStyle(theme.text.eventTitle.lineHeight, Math.ceil(fontSize * 1.3)) + CHIP_PADDING_V * 2 + CELL_ROW_GAP;
151
+ const moreFontSize = numericStyle(theme.text.more.fontSize, 11);
152
+ const moreRowHeight = Math.ceil(moreFontSize * 1.3) + CELL_ROW_GAP;
153
+ return monthEventCapacity(rowHeight - DAY_CELL_PADDING_TOP - DATE_BADGE_HEIGHT, chipRowHeight, moreRowHeight);
154
+ }, [
155
+ maxVisibleEventCount,
156
+ gridHeight,
157
+ weeks.length,
158
+ theme
159
+ ]);
160
+ const eventsByDay = useMemo(() => {
161
+ const map = groupEventsByDay(events);
162
+ if (sortedMonthView) for (const list of map.values()) list.sort(compareDayEvents);
163
+ return map;
164
+ }, [events, sortedMonthView]);
165
+ const showGrid = events.length > 0;
166
+ const renderDay = (day) => {
167
+ const isCurrentMonth = isSameMonth(day, date);
168
+ if (!isCurrentMonth && !showAdjacentMonths) return /* @__PURE__ */ jsx(View, { style: [styles$3.dayCell, showGrid && {
169
+ borderTopWidth: StyleSheet.hairlineWidth,
170
+ borderRightWidth: StyleSheet.hairlineWidth,
171
+ borderColor: theme.colors.gridLine
172
+ }] }, day.toISOString());
173
+ const dayEvents = eventsByDay.get(startOfDay(day).toISOString()) ?? [];
174
+ const isToday = getIsToday(day);
175
+ const isHighlighted = activeDate ? isSameCalendarDay(day, activeDate) : isToday;
176
+ const { isDisabled, isSelected, isInRange, isRangeStart, isRangeEnd } = daySelectionState(day, {
177
+ selectedDates,
178
+ selectedRange
179
+ }, {
180
+ minDate,
181
+ maxDate,
182
+ isDateDisabled
183
+ });
184
+ const visibleCount = monthVisibleCount(dayEvents.length, capacity);
185
+ const hiddenCount = dayEvents.length - visibleCount;
186
+ const isFilledBadge = dayBadgeKind({ isSelected }, isHighlighted) !== "none";
187
+ const hasBand = rangeBandKind({
188
+ isInRange,
189
+ isRangeStart,
190
+ isRangeEnd
191
+ }, fillCellOnSelection) !== "none";
192
+ const dayKey = day.toISOString();
193
+ const isHovered = isWeb$2 && !isDisabled && !showGrid && hoveredKey === dayKey;
194
+ const dateColor = isDisabled ? theme.colors.textDisabled : isFilledBadge ? isHighlighted ? theme.colors.todayText : theme.colors.selectedText : isCurrentMonth ? theme.colors.text : theme.colors.textDisabled;
195
+ const handlePressDay = isDisabled || !onPressDay ? void 0 : () => onPressDay(day);
196
+ const handleLongPressDay = isDisabled || !onLongPressDay ? void 0 : () => onLongPressDay(day);
197
+ const eventCount = dayEvents.length;
198
+ const accessibilityLabel = `${format(day, "EEEE, d LLLL yyyy", { locale })}${isToday ? ", today" : ""}${isSelected ? ", selected" : ""}${isDisabled ? ", unavailable" : ""}, ${eventCount} ${eventCount === 1 ? "event" : "events"}`;
199
+ return /* @__PURE__ */ jsxs(TouchableOpacity, {
200
+ style: [
201
+ styles$3.dayCell,
202
+ showGrid && styles$3.dayCellEvents,
203
+ showGrid && {
204
+ borderTopWidth: StyleSheet.hairlineWidth,
205
+ borderRightWidth: StyleSheet.hairlineWidth,
206
+ borderColor: theme.colors.gridLine
207
+ },
208
+ isWeekend(day) && { backgroundColor: theme.colors.weekendBackground },
209
+ calendarCellStyle?.(day)
210
+ ],
211
+ activeOpacity: showGrid ? .2 : 1,
212
+ ...showGrid ? null : {
213
+ onPressIn: () => setPressedKey(dayKey),
214
+ onPressOut: () => setPressedKey((k) => k === dayKey ? null : k)
215
+ },
216
+ onPress: handlePressDay,
217
+ onLongPress: handleLongPressDay,
218
+ disabled: isDisabled || !onPressDay && !onLongPressDay,
219
+ ...isWeb$2 && !isDisabled ? {
220
+ onPointerEnter: () => {
221
+ if (!showGrid) setHoveredKey(dayKey);
222
+ if (onDayPointerDown) onDayPointerEnter?.(day);
223
+ },
224
+ onPointerLeave: () => {
225
+ if (!showGrid) setHoveredKey((k) => k === dayKey ? null : k);
226
+ },
227
+ ...onDayPointerDown ? { onPointerDown: () => onDayPointerDown(day) } : {}
228
+ } : null,
229
+ role: "cell",
230
+ ...isWeb$2 && showGrid ? { focusable: false } : null,
231
+ accessibilityLabel,
232
+ children: [
233
+ hasBand ? /* @__PURE__ */ jsx(View, {
234
+ testID: "month-range-band",
235
+ style: [
236
+ styles$3.rangeBand,
237
+ { pointerEvents: "none" },
238
+ { backgroundColor: theme.colors.rangeBackground },
239
+ fillCellOnSelection ? {
240
+ top: 0,
241
+ bottom: 0
242
+ } : {
243
+ top: BAND_CENTER_Y - theme.rangeBandHeight / 2,
244
+ height: theme.rangeBandHeight
245
+ },
246
+ !fillCellOnSelection && isRangeStart && {
247
+ left: "50%",
248
+ marginLeft: -24 / 2,
249
+ borderTopLeftRadius: theme.rangeBandHeight / 2,
250
+ borderBottomLeftRadius: theme.rangeBandHeight / 2
251
+ },
252
+ !fillCellOnSelection && isRangeEnd && {
253
+ right: "50%",
254
+ marginRight: -24 / 2,
255
+ borderTopRightRadius: theme.rangeBandHeight / 2,
256
+ borderBottomRightRadius: theme.rangeBandHeight / 2
257
+ }
258
+ ]
259
+ }) : null,
260
+ renderCustomDateForMonth ? renderCustomDateForMonth(day) : /* @__PURE__ */ jsx(View, {
261
+ style: [
262
+ styles$3.dateBadge,
263
+ showGrid && styles$3.dateBadgeEvents,
264
+ isFilledBadge && {
265
+ backgroundColor: isHighlighted ? theme.colors.todayBackground : theme.colors.selectedBackground,
266
+ borderRadius: theme.todayBadgeRadius
267
+ },
268
+ isHovered && !isFilledBadge && {
269
+ backgroundColor: theme.colors.hoverBackground,
270
+ borderRadius: theme.todayBadgeRadius
271
+ },
272
+ !showGrid && pressedKey === dayKey && { opacity: .2 }
273
+ ],
274
+ children: /* @__PURE__ */ jsx(Text, {
275
+ style: [theme.text.dateCell, { color: dateColor }],
276
+ allowFontScaling: false,
277
+ children: format(day, "d")
278
+ })
279
+ }),
280
+ dayEvents.slice(0, visibleCount).map((event, index) => /* @__PURE__ */ jsx(View, {
281
+ style: styles$3.monthEvent,
282
+ children: /* @__PURE__ */ jsx(RenderEventComponent, {
283
+ event,
284
+ mode: "month",
285
+ isAllDay: isAllDayEvent(event),
286
+ onPress: disableMonthEventCellPress ? () => {} : () => onPressEvent(event),
287
+ onLongPress: disableMonthEventCellPress || !onLongPressEvent ? void 0 : () => onLongPressEvent(event)
288
+ })
289
+ }, keyExtractor(event, index))),
290
+ hiddenCount > 0 ? /* @__PURE__ */ jsx(Text, {
291
+ style: [
292
+ theme.text.more,
293
+ styles$3.moreLabel,
294
+ { color: theme.colors.textMuted }
295
+ ],
296
+ onPress: onPressMore ? () => onPressMore(dayEvents, day) : void 0,
297
+ accessibilityRole: "button",
298
+ accessibilityLabel: `Show ${hiddenCount} more events`,
299
+ allowFontScaling: false,
300
+ children: moreLabel.replace("{moreCount}", String(hiddenCount))
301
+ }) : null
302
+ ]
303
+ }, day.toISOString());
304
+ };
305
+ const handleLayout = (event) => {
306
+ const next = event.nativeEvent.layout.height;
307
+ setGridHeight((prev) => prev === next ? prev : next);
308
+ };
309
+ return /* @__PURE__ */ jsxs(View, {
310
+ style: styles$3.root,
311
+ children: [
312
+ showTitle ? /* @__PURE__ */ jsx(Text, {
313
+ style: [styles$3.title, { color: theme.colors.text }],
314
+ allowFontScaling: false,
315
+ children: format(date, "MMMM yyyy", locale ? { locale } : void 0)
316
+ }) : null,
317
+ showWeekdays ? /* @__PURE__ */ jsx(View, {
318
+ style: styles$3.weekdayHeader,
319
+ children: weekdayLabels.map((day) => /* @__PURE__ */ jsx(Text, {
320
+ style: [
321
+ theme.text.weekday,
322
+ styles$3.weekdayLabel,
323
+ { color: theme.colors.textMuted }
324
+ ],
325
+ allowFontScaling: false,
326
+ children: format(day, "EEE", { locale })
327
+ }, day.toISOString()))
328
+ }) : null,
329
+ /* @__PURE__ */ jsx(View, {
330
+ style: styles$3.container,
331
+ onLayout: handleLayout,
332
+ children: weeks.map((week) => /* @__PURE__ */ jsx(View, {
333
+ style: styles$3.weekRow,
334
+ children: week.map((day) => renderDay(day))
335
+ }, week[0].toISOString()))
336
+ })
337
+ ]
338
+ });
339
+ }
340
+ const MonthView = memo(MonthViewInner);
341
+ const styles$3 = StyleSheet.create({
342
+ root: { flex: 1 },
343
+ title: {
344
+ fontSize: 17,
345
+ fontWeight: "700",
346
+ paddingTop: 10,
347
+ paddingHorizontal: 14,
348
+ paddingBottom: 6
349
+ },
350
+ weekdayHeader: {
351
+ flexDirection: "row",
352
+ paddingBottom: 4
353
+ },
354
+ weekdayLabel: {
355
+ flex: 1,
356
+ textAlign: "center"
357
+ },
358
+ container: { flex: 1 },
359
+ weekRow: {
360
+ flex: 1,
361
+ flexDirection: "row"
362
+ },
363
+ dayCell: {
364
+ flex: 1,
365
+ alignItems: "center",
366
+ paddingTop: 4,
367
+ gap: 2,
368
+ overflow: "hidden"
369
+ },
370
+ dayCellEvents: { alignItems: "stretch" },
371
+ dateBadge: {
372
+ justifyContent: "center",
373
+ alignItems: "center",
374
+ height: 24,
375
+ width: 24
376
+ },
377
+ dateBadgeEvents: {
378
+ alignSelf: "flex-end",
379
+ marginRight: 4
380
+ },
381
+ rangeBand: {
382
+ position: "absolute",
383
+ left: 0,
384
+ right: 0
385
+ },
386
+ monthEvent: { marginHorizontal: 4 },
387
+ moreLabel: {
388
+ marginTop: 2,
389
+ marginHorizontal: 4
390
+ }
391
+ });
392
+ //#endregion
393
+ //#region src/components/MonthPager.tsx
394
+ const isWeb$1 = Platform.OS === "web";
395
+ const PAGE_WINDOW$1 = 60;
396
+ const PAGE_VIEWABILITY = { itemVisiblePercentThreshold: 90 };
397
+ function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, locale, sortedMonthView, moreLabel, showAdjacentMonths, disableMonthEventCellPress, isRTL, calendarCellStyle, renderEvent, keyExtractor, onPressDay, onLongPressDay, onPressEvent, onLongPressEvent, onPressMore, onChangeDate, freeSwipe = false, swipeEnabled = true, showSixWeeks = false, activeDate, renderHeaderForMonthView, renderCustomDateForMonth }) {
398
+ const theme = useCalendarTheme();
399
+ const { width, height } = useWindowDimensions();
400
+ const listRef = useRef(null);
401
+ const containerRef = useRef(null);
402
+ useEffect(() => {
403
+ if (!isWeb$1) return;
404
+ const root = containerRef.current;
405
+ if (!root) return;
406
+ const raf = requestAnimationFrame(() => {
407
+ for (const el of root.querySelectorAll("*")) {
408
+ if (el.scrollWidth <= el.clientWidth + 20 || el.clientWidth <= 100) continue;
409
+ const overflowX = getComputedStyle(el).overflowX;
410
+ if (overflowX === "auto" || overflowX === "scroll") {
411
+ el.style.overflowX = "hidden";
412
+ el.style.touchAction = "pan-y";
413
+ }
414
+ }
415
+ });
416
+ return () => cancelAnimationFrame(raf);
417
+ }, []);
418
+ const [pageHeight, setPageHeight] = useState(height);
419
+ const [containerWidth, setContainerWidth] = useState(width);
420
+ const [anchorDate] = useState(date);
421
+ const anchor = useMemo(() => startOfMonth(anchorDate), [anchorDate]);
422
+ const monthDates = useMemo(() => Array.from({ length: 121 }, (_, i) => addMonths(anchor, i - PAGE_WINDOW$1)), [anchor]);
423
+ const activeIndex = useCallback((target) => differenceInCalendarMonths(startOfMonth(target), anchor) + PAGE_WINDOW$1, [anchor])(date);
424
+ const viewedIndexRef = useRef(activeIndex);
425
+ const pendingScrollIndexRef = useRef(null);
426
+ const handleViewableItemsChanged = useCallback((info) => {
427
+ if (isWeb$1) return;
428
+ const settled = info.viewableItems.find((token) => token.isViewable);
429
+ if (settled?.index == null) return;
430
+ if (pendingScrollIndexRef.current != null) {
431
+ if (settled.index === pendingScrollIndexRef.current) {
432
+ pendingScrollIndexRef.current = null;
433
+ viewedIndexRef.current = settled.index;
434
+ }
435
+ return;
436
+ }
437
+ if (settled.index === viewedIndexRef.current) return;
438
+ viewedIndexRef.current = settled.index;
439
+ if (settled.item) onChangeDate(settled.item);
440
+ }, [onChangeDate]);
441
+ useEffect(() => {
442
+ if (activeIndex === viewedIndexRef.current) return;
443
+ viewedIndexRef.current = activeIndex;
444
+ pendingScrollIndexRef.current = activeIndex;
445
+ listRef.current?.scrollToIndex({
446
+ index: activeIndex,
447
+ animated: false
448
+ });
449
+ }, [activeIndex]);
450
+ useWebPagerKeys(swipeEnabled, useCallback((delta) => {
451
+ const target = monthDates[activeIndex + delta];
452
+ if (target) onChangeDate(target);
453
+ }, [
454
+ monthDates,
455
+ activeIndex,
456
+ onChangeDate
457
+ ]));
458
+ const weekDays = useMemo(() => {
459
+ const days = getWeekDays(anchor, weekStartsOn);
460
+ return isRTL ? days.reverse() : days;
461
+ }, [
462
+ anchor,
463
+ weekStartsOn,
464
+ isRTL
465
+ ]);
466
+ const snapToIndices = useMemo(() => monthDates.map((_, index) => index), [monthDates]);
467
+ const keyExtractorList = useCallback((item) => item.toISOString(), []);
468
+ const getFixedItemSize = useCallback(() => containerWidth, [containerWidth]);
469
+ const renderItem = useCallback(({ item }) => /* @__PURE__ */ jsx(View, {
470
+ style: {
471
+ width: containerWidth,
472
+ height: pageHeight
473
+ },
474
+ children: /* @__PURE__ */ jsx(MonthView, {
475
+ date: item,
476
+ events,
477
+ showTitle: false,
478
+ showWeekdays: false,
479
+ maxVisibleEventCount,
480
+ weekStartsOn,
481
+ locale,
482
+ sortedMonthView,
483
+ moreLabel,
484
+ showAdjacentMonths,
485
+ disableMonthEventCellPress,
486
+ isRTL,
487
+ showSixWeeks,
488
+ activeDate,
489
+ calendarCellStyle,
490
+ renderEvent,
491
+ keyExtractor,
492
+ onPressDay,
493
+ onLongPressDay,
494
+ onPressEvent,
495
+ onLongPressEvent,
496
+ onPressMore,
497
+ renderCustomDateForMonth
498
+ })
499
+ }), [
500
+ containerWidth,
501
+ pageHeight,
502
+ events,
503
+ maxVisibleEventCount,
504
+ weekStartsOn,
505
+ locale,
506
+ sortedMonthView,
507
+ moreLabel,
508
+ showAdjacentMonths,
509
+ disableMonthEventCellPress,
510
+ isRTL,
511
+ showSixWeeks,
512
+ activeDate,
513
+ calendarCellStyle,
514
+ renderEvent,
515
+ keyExtractor,
516
+ onPressDay,
517
+ onLongPressDay,
518
+ onPressEvent,
519
+ onLongPressEvent,
520
+ onPressMore,
521
+ renderCustomDateForMonth
522
+ ]);
523
+ return /* @__PURE__ */ jsxs(View, {
524
+ ref: containerRef,
525
+ style: styles$2.container,
526
+ children: [
527
+ /* @__PURE__ */ jsx(Text, {
528
+ style: [styles$2.monthTitle, { color: theme.colors.text }],
529
+ allowFontScaling: false,
530
+ children: format(date, "MMMM yyyy", locale ? { locale } : void 0)
531
+ }),
532
+ renderHeaderForMonthView ? renderHeaderForMonthView(weekDays) : /* @__PURE__ */ jsx(MonthWeekdayHeader, {
533
+ weekDays,
534
+ locale
535
+ }),
536
+ /* @__PURE__ */ jsx(View, {
537
+ style: styles$2.pager,
538
+ onLayout: (event) => {
539
+ setPageHeight(event.nativeEvent.layout.height);
540
+ setContainerWidth(event.nativeEvent.layout.width);
541
+ },
542
+ children: /* @__PURE__ */ jsx(LegendList, {
543
+ ref: listRef,
544
+ style: isWeb$1 ? [styles$2.pagerList, styles$2.webNoScroll] : styles$2.pagerList,
545
+ data: monthDates,
546
+ horizontal: true,
547
+ recycleItems: false,
548
+ keyExtractor: keyExtractorList,
549
+ getFixedItemSize,
550
+ ...isWeb$1 ? null : {
551
+ scrollEnabled: swipeEnabled,
552
+ pagingEnabled: !freeSwipe,
553
+ snapToIndices: freeSwipe ? snapToIndices : void 0
554
+ },
555
+ initialScrollIndex: activeIndex,
556
+ showsHorizontalScrollIndicator: false,
557
+ viewabilityConfig: PAGE_VIEWABILITY,
558
+ onViewableItemsChanged: handleViewableItemsChanged,
559
+ renderItem
560
+ }, pageHeight)
561
+ })
562
+ ]
563
+ });
564
+ }
565
+ const MonthPager = memo(MonthPagerInner);
566
+ const MonthWeekdayHeader = ({ weekDays, locale }) => {
567
+ const theme = useCalendarTheme();
568
+ return /* @__PURE__ */ jsx(View, {
569
+ style: styles$2.weekdayHeader,
570
+ children: weekDays.map((day) => /* @__PURE__ */ jsx(Text, {
571
+ style: [
572
+ theme.text.weekday,
573
+ styles$2.weekdayLabel,
574
+ { color: theme.colors.textMuted }
575
+ ],
576
+ allowFontScaling: false,
577
+ children: format(day, "EEE", { locale })
578
+ }, day.toISOString()))
579
+ });
580
+ };
581
+ const styles$2 = StyleSheet.create({
582
+ container: { flex: 1 },
583
+ pager: { flex: 1 },
584
+ pagerList: { flex: 1 },
585
+ webNoScroll: { overflow: "hidden" },
586
+ monthTitle: {
587
+ fontSize: 17,
588
+ fontWeight: "700",
589
+ paddingTop: 10,
590
+ paddingHorizontal: 14,
591
+ paddingBottom: 6
592
+ },
593
+ weekdayHeader: {
594
+ flexDirection: "row",
595
+ paddingBottom: 4
596
+ },
597
+ weekdayLabel: {
598
+ flex: 1,
599
+ textAlign: "center"
600
+ }
601
+ });
602
+ //#endregion
603
+ //#region src/components/DefaultMonthEvent.tsx
604
+ /**
605
+ * A lightweight event chip for the month grid and the date picker: the same
606
+ * titled box as {@link DefaultEvent} but with no Reanimated dependency (it drops
607
+ * the pinch-zoom time reveal, which only applies to the week/day grid). It's the
608
+ * default renderer for `MonthList`, so the `/picker` entry point stays free of
609
+ * Reanimated.
610
+ */
611
+ function DefaultMonthEvent({ event, mode, isAllDay, ampm = false, showTime = true, ellipsizeTitle = false, allDayLabel, cellStyle, onPress, onLongPress }) {
612
+ const theme = useCalendarTheme();
613
+ const isAllDayEvent = isAllDay ?? false;
614
+ const timeLabel = eventTimeLabel({
615
+ mode,
616
+ isAllDay: isAllDayEvent,
617
+ start: event.start,
618
+ end: event.end,
619
+ ampm,
620
+ showTime,
621
+ allDayLabel
622
+ });
623
+ const ellipsizeMode = titleEllipsizeMode(ellipsizeTitle);
624
+ const accessibilityLabel = eventAccessibilityLabel({
625
+ title: event.title,
626
+ isAllDay: isAllDayEvent,
627
+ start: event.start,
628
+ end: event.end,
629
+ ampm,
630
+ allDayLabel
631
+ });
632
+ return /* @__PURE__ */ jsxs(TouchableOpacity, {
633
+ style: [
634
+ styles$1.box,
635
+ { backgroundColor: theme.colors.eventBackground },
636
+ event.disabled && styles$1.disabled,
637
+ cellStyle
638
+ ],
639
+ onPress,
640
+ onLongPress,
641
+ activeOpacity: .7,
642
+ accessibilityRole: "button",
643
+ accessibilityLabel,
644
+ accessibilityState: { disabled: event.disabled ?? false },
645
+ children: [event.title ? /* @__PURE__ */ jsx(Text, {
646
+ style: [
647
+ theme.text.eventTitle,
648
+ styles$1.title,
649
+ { color: theme.colors.eventText }
650
+ ],
651
+ numberOfLines: titleNumberOfLines(mode, isAllDayEvent),
652
+ ellipsizeMode,
653
+ allowFontScaling: false,
654
+ children: event.title
655
+ }) : null, timeLabel ? /* @__PURE__ */ jsx(View, { children: /* @__PURE__ */ jsx(Text, {
656
+ style: [styles$1.time, { color: theme.colors.eventText }],
657
+ numberOfLines: 2,
658
+ ellipsizeMode,
659
+ allowFontScaling: false,
660
+ children: timeLabel
661
+ }) }) : null]
662
+ });
663
+ }
664
+ const styles$1 = StyleSheet.create({
665
+ box: {
666
+ flexGrow: 1,
667
+ flexShrink: 1,
668
+ flexBasis: "auto",
669
+ borderRadius: 6,
670
+ paddingVertical: 2,
671
+ paddingHorizontal: 4,
672
+ overflow: "hidden"
673
+ },
674
+ title: { flexShrink: 1 },
675
+ time: { fontSize: 11 },
676
+ disabled: { opacity: .5 }
677
+ });
678
+ //#endregion
679
+ //#region src/components/MonthList.tsx
680
+ const isWeb = Platform.OS === "web";
681
+ const PAGE_WINDOW = 60;
682
+ const VIEWABILITY = { itemVisiblePercentThreshold: 60 };
683
+ const DEFAULT_MONTH_HEADER_HEIGHT = 44;
684
+ const DEFAULT_WEEK_ROW_HEIGHT = 56;
685
+ const DEFAULT_EVENT_WEEK_ROW_HEIGHT = 96;
686
+ const LONG_PRESS_MS = 500;
687
+ const AUTOSCROLL_EDGE_PX = 64;
688
+ const AUTOSCROLL_STEP_PX = 14;
689
+ const AUTOSCROLL_INTERVAL_MS = 16;
690
+ const NO_EVENTS = [];
691
+ const noop = () => {};
692
+ const defaultKeyExtractor = (event) => `${event.start.toISOString()}|${event.end.toISOString()}|${event.title ?? ""}`;
693
+ function MonthListInner({ date, events = NO_EVENTS, weekStartsOn, weekRowHeight: weekRowHeightProp, monthHeaderHeight = DEFAULT_MONTH_HEADER_HEIGHT, maxVisibleEventCount, locale, sortedMonthView, moreLabel, showAdjacentMonths = false, disableMonthEventCellPress, isRTL, activeDate, selectedDates, selectedRange, fillCellOnSelection, minDate, maxDate, isDateDisabled, calendarCellStyle, renderEvent = DefaultMonthEvent, keyExtractor = defaultKeyExtractor, onPressDay, onLongPressDay, onPressEvent = noop, onLongPressEvent, onPressMore, onSelectDrag, onChangeVisibleMonth, renderMonthHeader }) {
694
+ const theme = useCalendarTheme();
695
+ const listRef = useRef(null);
696
+ const weekRowHeight = weekRowHeightProp ?? (events.length > 0 ? DEFAULT_EVENT_WEEK_ROW_HEIGHT : DEFAULT_WEEK_ROW_HEIGHT);
697
+ const [firstViewableIndex, setFirstViewableIndex] = useState(0);
698
+ const [anchorDate] = useState(date);
699
+ const anchor = useMemo(() => startOfMonth(anchorDate), [anchorDate]);
700
+ const monthDates = useMemo(() => Array.from({ length: 121 }, (_, i) => addMonths(anchor, i - PAGE_WINDOW)), [anchor]);
701
+ const initialIndex = useMemo(() => differenceInCalendarMonths(startOfMonth(date), anchor) + PAGE_WINDOW, [date, anchor]);
702
+ const weekDays = useMemo(() => {
703
+ const days = getWeekDays(anchor, weekStartsOn);
704
+ return isRTL ? days.reverse() : days;
705
+ }, [
706
+ anchor,
707
+ weekStartsOn,
708
+ isRTL
709
+ ]);
710
+ const selection = useMemo(() => ({
711
+ selectedDates,
712
+ selectedRange,
713
+ minDate,
714
+ maxDate,
715
+ isDateDisabled
716
+ }), [
717
+ selectedDates,
718
+ selectedRange,
719
+ minDate,
720
+ maxDate,
721
+ isDateDisabled
722
+ ]);
723
+ const keyExtractorList = useCallback((item) => item.toISOString(), []);
724
+ const monthWeeks = useMemo(() => monthDates.map((month) => buildMonthWeeks(month, weekStartsOn, { isRTL })), [
725
+ monthDates,
726
+ weekStartsOn,
727
+ isRTL
728
+ ]);
729
+ const blockHeightAt = useCallback((index) => monthHeaderHeight + monthWeeks[index].length * weekRowHeight, [
730
+ monthWeeks,
731
+ weekRowHeight,
732
+ monthHeaderHeight
733
+ ]);
734
+ const getFixedItemSize = useCallback((_item, index) => blockHeightAt(index), [blockHeightAt]);
735
+ const offsets = useMemo(() => {
736
+ const out = [0];
737
+ for (let i = 0; i < monthWeeks.length; i++) out.push(out[i] + blockHeightAt(i));
738
+ return out;
739
+ }, [monthWeeks, blockHeightAt]);
740
+ const dragStartRef = useRef(null);
741
+ const dragMovedRef = useRef(false);
742
+ const pointerDownRef = useRef(false);
743
+ const scrollYRef = useRef(0);
744
+ const viewportRef = useRef({
745
+ width: 0,
746
+ height: 0
747
+ });
748
+ const lastPanRef = useRef({
749
+ x: 0,
750
+ y: 0
751
+ });
752
+ const autoScrollRef = useRef(null);
753
+ const isSelectable = useCallback((day) => isDateSelectable(day, {
754
+ minDate,
755
+ maxDate,
756
+ isDateDisabled
757
+ }), [
758
+ minDate,
759
+ maxDate,
760
+ isDateDisabled
761
+ ]);
762
+ const dayAtContent = useCallback((x, contentY) => {
763
+ const width = viewportRef.current.width;
764
+ if (width <= 0) return null;
765
+ const clampedY = Math.min(Math.max(contentY, 0), offsets[offsets.length - 1] - 1);
766
+ let index = 0;
767
+ while (index < monthWeeks.length - 1 && offsets[index + 1] <= clampedY) index++;
768
+ const localY = clampedY - offsets[index] - monthHeaderHeight;
769
+ if (localY < 0) return null;
770
+ const weeks = monthWeeks[index];
771
+ const row = Math.min(weeks.length - 1, Math.max(0, Math.floor(localY / weekRowHeight)));
772
+ const col = Math.min(6, Math.max(0, Math.floor(x / (width / 7))));
773
+ const day = weeks[row]?.[col];
774
+ if (!day) return null;
775
+ if (!showAdjacentMonths && !isSameMonth(day, monthDates[index])) return null;
776
+ return isSelectable(day) ? day : null;
777
+ }, [
778
+ offsets,
779
+ monthWeeks,
780
+ monthDates,
781
+ weekRowHeight,
782
+ monthHeaderHeight,
783
+ showAdjacentMonths,
784
+ isSelectable
785
+ ]);
786
+ const extendTo = useCallback((day) => {
787
+ const start = dragStartRef.current;
788
+ if (!start || !day) return;
789
+ if (day.getTime() !== start.getTime()) dragMovedRef.current = true;
790
+ onSelectDrag?.(start, day);
791
+ }, [onSelectDrag]);
792
+ const stopAutoScroll = useCallback(() => {
793
+ if (autoScrollRef.current) {
794
+ clearInterval(autoScrollRef.current.id);
795
+ autoScrollRef.current = null;
796
+ }
797
+ }, []);
798
+ const startAutoScroll = useCallback((dir) => {
799
+ if (autoScrollRef.current?.dir === dir) return;
800
+ stopAutoScroll();
801
+ const max = Math.max(0, offsets[offsets.length - 1] - viewportRef.current.height);
802
+ autoScrollRef.current = {
803
+ id: setInterval(() => {
804
+ const next = Math.min(max, Math.max(0, scrollYRef.current + dir * AUTOSCROLL_STEP_PX));
805
+ scrollYRef.current = next;
806
+ listRef.current?.scrollToOffset({
807
+ offset: next,
808
+ animated: false
809
+ });
810
+ extendTo(dayAtContent(lastPanRef.current.x, next + lastPanRef.current.y));
811
+ }, AUTOSCROLL_INTERVAL_MS),
812
+ dir
813
+ };
814
+ }, [
815
+ offsets,
816
+ stopAutoScroll,
817
+ extendTo,
818
+ dayAtContent
819
+ ]);
820
+ const dragGesture = useMemo(() => {
821
+ if (!onSelectDrag) return void 0;
822
+ return Gesture.Pan().activateAfterLongPress(LONG_PRESS_MS).runOnJS(true).onStart((event) => {
823
+ lastPanRef.current = {
824
+ x: event.x,
825
+ y: event.y
826
+ };
827
+ dragMovedRef.current = false;
828
+ dragStartRef.current = dayAtContent(event.x, scrollYRef.current + event.y);
829
+ }).onUpdate((event) => {
830
+ lastPanRef.current = {
831
+ x: event.x,
832
+ y: event.y
833
+ };
834
+ const height = viewportRef.current.height;
835
+ if (event.y < AUTOSCROLL_EDGE_PX) startAutoScroll(-1);
836
+ else if (height > 0 && event.y > height - AUTOSCROLL_EDGE_PX) startAutoScroll(1);
837
+ else stopAutoScroll();
838
+ extendTo(dayAtContent(event.x, scrollYRef.current + event.y));
839
+ }).onFinalize(() => {
840
+ stopAutoScroll();
841
+ dragStartRef.current = null;
842
+ });
843
+ }, [
844
+ onSelectDrag,
845
+ dayAtContent,
846
+ extendTo,
847
+ startAutoScroll,
848
+ stopAutoScroll
849
+ ]);
850
+ const onDayPointerDown = useCallback((day) => {
851
+ pointerDownRef.current = true;
852
+ dragMovedRef.current = false;
853
+ dragStartRef.current = isSelectable(day) ? day : null;
854
+ }, [isSelectable]);
855
+ const onDayPointerEnter = useCallback((day) => {
856
+ if (pointerDownRef.current && dragStartRef.current && isSelectable(day)) extendTo(day);
857
+ }, [extendTo, isSelectable]);
858
+ useEffect(() => {
859
+ if (!isWeb || !onSelectDrag) return;
860
+ const end = () => {
861
+ pointerDownRef.current = false;
862
+ dragStartRef.current = null;
863
+ };
864
+ const target = globalThis;
865
+ target.addEventListener?.("pointerup", end);
866
+ return () => target.removeEventListener?.("pointerup", end);
867
+ }, [onSelectDrag]);
868
+ const handlePressDay = useCallback((day) => {
869
+ if (dragMovedRef.current) {
870
+ dragMovedRef.current = false;
871
+ return;
872
+ }
873
+ onPressDay?.(day);
874
+ }, [onPressDay]);
875
+ const handleScroll = useCallback((event) => {
876
+ scrollYRef.current = event.nativeEvent.contentOffset.y;
877
+ }, []);
878
+ const handleViewableItemsChanged = useCallback((info) => {
879
+ const settled = info.viewableItems.find((token) => token.isViewable);
880
+ if (settled?.item && onChangeVisibleMonth) onChangeVisibleMonth(settled.item);
881
+ const topIndex = settled?.index;
882
+ if (isWeb && typeof topIndex === "number") setFirstViewableIndex((prev) => prev === topIndex ? prev : topIndex);
883
+ }, [onChangeVisibleMonth]);
884
+ const dragEnabled = onSelectDrag != null;
885
+ const renderItem = useCallback(({ item, index }) => /* @__PURE__ */ jsxs(MonthBlock, {
886
+ height: blockHeightAt(index),
887
+ inert: isWeb && events.length > 0 && index < firstViewableIndex,
888
+ children: [/* @__PURE__ */ jsx(View, {
889
+ style: [styles.monthHeader, { height: monthHeaderHeight }],
890
+ children: renderMonthHeader ? renderMonthHeader(item) : /* @__PURE__ */ jsx(Text, {
891
+ style: [styles.monthTitle, { color: theme.colors.text }],
892
+ children: format(item, "LLLL yyyy", { locale })
893
+ })
894
+ }), /* @__PURE__ */ jsx(View, {
895
+ style: styles.grid,
896
+ children: /* @__PURE__ */ jsx(MonthView, {
897
+ date: item,
898
+ events,
899
+ showTitle: false,
900
+ showWeekdays: false,
901
+ maxVisibleEventCount,
902
+ weekStartsOn,
903
+ locale,
904
+ sortedMonthView,
905
+ moreLabel,
906
+ showAdjacentMonths,
907
+ disableMonthEventCellPress,
908
+ isRTL,
909
+ activeDate,
910
+ fillCellOnSelection,
911
+ calendarCellStyle,
912
+ renderEvent,
913
+ keyExtractor,
914
+ onPressDay: dragEnabled ? handlePressDay : onPressDay,
915
+ onLongPressDay,
916
+ onPressEvent,
917
+ onLongPressEvent,
918
+ onPressMore,
919
+ onDayPointerDown: isWeb && dragEnabled ? onDayPointerDown : void 0,
920
+ onDayPointerEnter: isWeb && dragEnabled ? onDayPointerEnter : void 0
921
+ })
922
+ })]
923
+ }), [
924
+ blockHeightAt,
925
+ firstViewableIndex,
926
+ monthHeaderHeight,
927
+ renderMonthHeader,
928
+ theme,
929
+ locale,
930
+ events,
931
+ maxVisibleEventCount,
932
+ weekStartsOn,
933
+ sortedMonthView,
934
+ moreLabel,
935
+ showAdjacentMonths,
936
+ disableMonthEventCellPress,
937
+ isRTL,
938
+ activeDate,
939
+ fillCellOnSelection,
940
+ calendarCellStyle,
941
+ renderEvent,
942
+ keyExtractor,
943
+ dragEnabled,
944
+ handlePressDay,
945
+ onPressDay,
946
+ onLongPressDay,
947
+ onPressEvent,
948
+ onLongPressEvent,
949
+ onPressMore,
950
+ onDayPointerDown,
951
+ onDayPointerEnter
952
+ ]);
953
+ const list = /* @__PURE__ */ jsx(LegendList, {
954
+ ref: listRef,
955
+ style: styles.list,
956
+ data: monthDates,
957
+ recycleItems: false,
958
+ extraData: firstViewableIndex,
959
+ keyExtractor: keyExtractorList,
960
+ getFixedItemSize,
961
+ initialScrollIndex: initialIndex,
962
+ showsVerticalScrollIndicator: false,
963
+ scrollEventThrottle: 16,
964
+ onScroll: handleScroll,
965
+ viewabilityConfig: VIEWABILITY,
966
+ onViewableItemsChanged: handleViewableItemsChanged,
967
+ onLayout: (event) => {
968
+ viewportRef.current = {
969
+ width: event.nativeEvent.layout.width,
970
+ height: event.nativeEvent.layout.height
971
+ };
972
+ },
973
+ renderItem
974
+ });
975
+ return /* @__PURE__ */ jsx(CalendarSelectionProvider, {
976
+ value: selection,
977
+ children: /* @__PURE__ */ jsxs(View, {
978
+ style: styles.container,
979
+ children: [/* @__PURE__ */ jsx(View, {
980
+ style: [styles.weekdayHeader, { borderBottomColor: theme.colors.gridLine }],
981
+ children: weekDays.map((day) => /* @__PURE__ */ jsx(Text, {
982
+ style: [styles.weekdayLabel, { color: theme.colors.textMuted }],
983
+ allowFontScaling: false,
984
+ children: format(day, "EEE", { locale })
985
+ }, day.toISOString()))
986
+ }), dragGesture && !isWeb ? /* @__PURE__ */ jsx(GestureDetector, {
987
+ gesture: dragGesture,
988
+ children: list
989
+ }) : list]
990
+ })
991
+ });
992
+ }
993
+ function MonthBlock({ height, inert, children }) {
994
+ const ref = useRef(null);
995
+ useEffect(() => {
996
+ if (!isWeb) return;
997
+ const node = ref.current;
998
+ if (node) node.inert = inert;
999
+ }, [inert]);
1000
+ return /* @__PURE__ */ jsx(View, {
1001
+ ref,
1002
+ style: { height },
1003
+ children
1004
+ });
1005
+ }
1006
+ const MonthList = memo(MonthListInner);
1007
+ const styles = StyleSheet.create({
1008
+ container: { flex: 1 },
1009
+ list: { flex: 1 },
1010
+ monthHeader: { justifyContent: "center" },
1011
+ monthTitle: {
1012
+ paddingHorizontal: 8,
1013
+ fontSize: 17,
1014
+ fontWeight: "700"
1015
+ },
1016
+ grid: { flex: 1 },
1017
+ weekdayHeader: {
1018
+ flexDirection: "row",
1019
+ paddingVertical: 8,
1020
+ borderBottomWidth: StyleSheet.hairlineWidth
1021
+ },
1022
+ weekdayLabel: {
1023
+ flex: 1,
1024
+ textAlign: "center",
1025
+ fontSize: 12,
1026
+ fontWeight: "600"
1027
+ }
1028
+ });
1029
+ //#endregion
1030
+ export { useWebPagerKeys as a, defaultTheme as c, MonthView as i, mergeTheme as l, DefaultMonthEvent as n, CalendarThemeProvider as o, MonthPager as r, darkTheme as s, MonthList as t, useCalendarTheme as u };