react-native-bigger-calendar 0.1.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,930 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let react = require("react");
25
+ let react_native_reanimated = require("react-native-reanimated");
26
+ react_native_reanimated = __toESM(react_native_reanimated);
27
+ let date_fns = require("date-fns");
28
+ let react_native = require("react-native");
29
+ let react_jsx_runtime = require("react/jsx-runtime");
30
+ let _legendapp_list_react_native = require("@legendapp/list/react-native");
31
+ let react_native_gesture_handler = require("react-native-gesture-handler");
32
+ //#region src/theme.ts
33
+ const defaultTheme = {
34
+ colors: {
35
+ gridLine: "#E2E4E9",
36
+ weekendBackground: "#F6F7F9",
37
+ todayBackground: "#1F6FEB",
38
+ todayText: "#FFFFFF",
39
+ nowIndicator: "#E5484D",
40
+ text: "#1A1B1E",
41
+ textMuted: "#6B7280",
42
+ textDisabled: "#B5B9C0",
43
+ eventBackground: "#DCE7FF",
44
+ eventText: "#1A1B1E"
45
+ },
46
+ text: {
47
+ dayNumber: {
48
+ fontSize: 22,
49
+ fontWeight: "700"
50
+ },
51
+ weekday: {
52
+ fontSize: 13,
53
+ fontWeight: "700"
54
+ },
55
+ dateCell: {
56
+ fontSize: 13,
57
+ fontWeight: "700"
58
+ },
59
+ hourLabel: { fontSize: 12 },
60
+ more: {
61
+ fontSize: 11,
62
+ fontWeight: "700"
63
+ },
64
+ eventTitle: {
65
+ fontSize: 12,
66
+ fontWeight: "700"
67
+ }
68
+ },
69
+ todayBadgeRadius: 999
70
+ };
71
+ /** Deep-merge a partial theme over {@link defaultTheme}. */
72
+ function mergeTheme(theme) {
73
+ if (!theme) return defaultTheme;
74
+ return {
75
+ colors: {
76
+ ...defaultTheme.colors,
77
+ ...theme.colors
78
+ },
79
+ text: {
80
+ ...defaultTheme.text,
81
+ ...theme.text
82
+ },
83
+ todayBadgeRadius: theme.todayBadgeRadius ?? defaultTheme.todayBadgeRadius
84
+ };
85
+ }
86
+ const CalendarThemeContext = (0, react.createContext)(defaultTheme);
87
+ const CalendarThemeProvider = CalendarThemeContext.Provider;
88
+ const useCalendarTheme = () => (0, react.useContext)(CalendarThemeContext);
89
+ //#endregion
90
+ //#region src/components/DefaultEvent.tsx
91
+ /**
92
+ * The built-in event renderer: a filled, rounded box showing the event title
93
+ * and (on the day/week grid) its time range. Pass your own `renderEvent` to
94
+ * `<Calendar>` to replace it entirely.
95
+ */
96
+ function DefaultEvent({ event, mode, onPress }) {
97
+ const theme = useCalendarTheme();
98
+ const showTime = mode !== "month";
99
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.TouchableOpacity, {
100
+ style: [styles$3.box, { backgroundColor: theme.colors.eventBackground }],
101
+ onPress,
102
+ activeOpacity: .7,
103
+ accessibilityRole: "button",
104
+ accessibilityLabel: event.title,
105
+ children: [event.title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
106
+ style: [theme.text.eventTitle, { color: theme.colors.eventText }],
107
+ numberOfLines: mode === "day" ? void 0 : 1,
108
+ ellipsizeMode: "tail",
109
+ allowFontScaling: false,
110
+ children: event.title
111
+ }) : null, showTime ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
112
+ style: [styles$3.time, { color: theme.colors.eventText }],
113
+ numberOfLines: 1,
114
+ allowFontScaling: false,
115
+ children: `${(0, date_fns.format)(event.start, "HH:mm")} - ${(0, date_fns.format)(event.end, "HH:mm")}`
116
+ }) : null]
117
+ });
118
+ }
119
+ const styles$3 = react_native.StyleSheet.create({
120
+ box: {
121
+ flex: 1,
122
+ borderRadius: 6,
123
+ paddingVertical: 2,
124
+ paddingHorizontal: 4,
125
+ overflow: "hidden"
126
+ },
127
+ time: { fontSize: 11 }
128
+ });
129
+ //#endregion
130
+ //#region src/utils/dates.ts
131
+ /** The seven dates of the week containing `date`, starting on `weekStartsOn`. */
132
+ const getWeekDays = (date, weekStartsOn) => {
133
+ const start = (0, date_fns.startOfWeek)(date, { weekStartsOn });
134
+ return Array.from({ length: 7 }, (_, index) => (0, date_fns.addDays)(start, index));
135
+ };
136
+ const isWeekend = (date) => {
137
+ const day = date.getDay();
138
+ return day === 0 || day === 6;
139
+ };
140
+ const getIsToday = (date) => (0, date_fns.isToday)(date);
141
+ const isSameCalendarDay = (a, b) => (0, date_fns.isSameDay)(a, b);
142
+ /** Minutes elapsed since midnight (0–1439). */
143
+ const minutesIntoDay = (date) => (0, date_fns.getHours)(date) * 60 + (0, date_fns.getMinutes)(date);
144
+ //#endregion
145
+ //#region src/utils/layout.ts
146
+ const MINUTES_PER_HOUR$1 = 60;
147
+ const MIN_DURATION_HOURS = .25;
148
+ /**
149
+ * Lay out a single day's events: events that overlap in time are split into
150
+ * side-by-side columns. Multi-day events are clipped to the portion that falls
151
+ * on `day` (e.g. a 23:00→01:00 event renders 23:00–24:00 on the start day and
152
+ * 00:00–01:00 on the next). Pure — safe to call per render, never per frame.
153
+ */
154
+ function layoutDayEvents(events, day) {
155
+ const dayStart = (0, date_fns.startOfDay)(day);
156
+ const nextDayStart = (0, date_fns.addDays)(dayStart, 1);
157
+ const segments = events.filter((event) => event.start < nextDayStart && event.end > dayStart).map((event) => {
158
+ const segStart = (0, date_fns.max)([event.start, dayStart]);
159
+ const segEnd = (0, date_fns.min)([event.end, nextDayStart]);
160
+ return {
161
+ event,
162
+ start: (0, date_fns.differenceInMinutes)(segStart, dayStart) / MINUTES_PER_HOUR$1,
163
+ end: (0, date_fns.differenceInMinutes)(segEnd, dayStart) / MINUTES_PER_HOUR$1,
164
+ continuesBefore: event.start < dayStart,
165
+ continuesAfter: event.end > nextDayStart
166
+ };
167
+ }).sort((a, b) => a.start - b.start);
168
+ const positioned = [];
169
+ let cluster = [];
170
+ let clusterEnd = Number.NEGATIVE_INFINITY;
171
+ const flushCluster = () => {
172
+ const columnEnds = [];
173
+ const columnOf = /* @__PURE__ */ new Map();
174
+ for (const seg of cluster) {
175
+ let column = columnEnds.findIndex((end) => end <= seg.start);
176
+ if (column === -1) {
177
+ column = columnEnds.length;
178
+ columnEnds.push(seg.end);
179
+ } else columnEnds[column] = seg.end;
180
+ columnOf.set(seg, column);
181
+ }
182
+ for (const seg of cluster) positioned.push({
183
+ event: seg.event,
184
+ startHours: seg.start,
185
+ durationHours: Math.max(seg.end - seg.start, MIN_DURATION_HOURS),
186
+ column: columnOf.get(seg) ?? 0,
187
+ columns: columnEnds.length,
188
+ continuesBefore: seg.continuesBefore,
189
+ continuesAfter: seg.continuesAfter
190
+ });
191
+ cluster = [];
192
+ };
193
+ for (const seg of segments) {
194
+ if (cluster.length > 0 && seg.start >= clusterEnd) flushCluster();
195
+ cluster.push(seg);
196
+ clusterEnd = Math.max(clusterEnd, seg.end);
197
+ }
198
+ if (cluster.length > 0) flushCluster();
199
+ return positioned;
200
+ }
201
+ /**
202
+ * The `startOfDay` ISO keys of every calendar day an event touches (inclusive).
203
+ * An event ending exactly at midnight does not count the following day. Used to
204
+ * index events by day for the month grid. Pure.
205
+ */
206
+ function eventDayKeys(event) {
207
+ const first = (0, date_fns.startOfDay)(event.start);
208
+ const last = (0, date_fns.startOfDay)(event.end > event.start ? /* @__PURE__ */ new Date(event.end.getTime() - 1) : event.start);
209
+ const keys = [];
210
+ for (let cursor = first; cursor <= last; cursor = (0, date_fns.addDays)(cursor, 1)) keys.push(cursor.toISOString());
211
+ return keys;
212
+ }
213
+ //#endregion
214
+ //#region src/components/MonthView.tsx
215
+ const chunkIntoWeeks = (days) => {
216
+ const weeks = [];
217
+ for (let index = 0; index < days.length; index += 7) weeks.push(days.slice(index, index + 7));
218
+ return weeks;
219
+ };
220
+ function MonthViewInner({ date, events, maxVisibleEventCount, weekStartsOn, renderEvent, keyExtractor, onPressDay, onPressEvent, onPressMore }) {
221
+ const theme = useCalendarTheme();
222
+ const RenderEventComponent = renderEvent;
223
+ const weeks = (0, react.useMemo)(() => {
224
+ return chunkIntoWeeks((0, date_fns.eachDayOfInterval)({
225
+ start: (0, date_fns.startOfWeek)((0, date_fns.startOfMonth)(date), { weekStartsOn }),
226
+ end: (0, date_fns.endOfWeek)((0, date_fns.endOfMonth)(date), { weekStartsOn })
227
+ }));
228
+ }, [date, weekStartsOn]);
229
+ const eventsByDay = (0, react.useMemo)(() => {
230
+ const map = /* @__PURE__ */ new Map();
231
+ for (const event of events) for (const key of eventDayKeys(event)) {
232
+ const existing = map.get(key);
233
+ if (existing) existing.push(event);
234
+ else map.set(key, [event]);
235
+ }
236
+ return map;
237
+ }, [events]);
238
+ const renderDay = (day) => {
239
+ const dayEvents = eventsByDay.get((0, date_fns.startOfDay)(day).toISOString()) ?? [];
240
+ const isCurrentMonth = (0, date_fns.isSameMonth)(day, date);
241
+ const isToday = getIsToday(day);
242
+ const hiddenCount = dayEvents.length - maxVisibleEventCount;
243
+ const dateColor = isToday ? theme.colors.todayText : isCurrentMonth ? theme.colors.text : theme.colors.textDisabled;
244
+ const eventCount = dayEvents.length;
245
+ const accessibilityLabel = `${(0, date_fns.format)(day, "EEEE, d LLLL yyyy")}${isToday ? ", today" : ""}, ${eventCount} ${eventCount === 1 ? "event" : "events"}`;
246
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.TouchableOpacity, {
247
+ style: [
248
+ styles$2.dayCell,
249
+ { borderColor: theme.colors.gridLine },
250
+ isWeekend(day) && { backgroundColor: theme.colors.weekendBackground }
251
+ ],
252
+ onPress: onPressDay ? () => onPressDay(day) : void 0,
253
+ disabled: !onPressDay,
254
+ accessibilityRole: onPressDay ? "button" : void 0,
255
+ accessibilityLabel,
256
+ children: [
257
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
258
+ style: [styles$2.dateBadge, isToday && {
259
+ backgroundColor: theme.colors.todayBackground,
260
+ borderRadius: theme.todayBadgeRadius
261
+ }],
262
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
263
+ style: [theme.text.dateCell, { color: dateColor }],
264
+ allowFontScaling: false,
265
+ children: (0, date_fns.format)(day, "d")
266
+ })
267
+ }),
268
+ dayEvents.slice(0, maxVisibleEventCount).map((event, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
269
+ style: styles$2.monthEvent,
270
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenderEventComponent, {
271
+ event,
272
+ mode: "month",
273
+ onPress: () => onPressEvent(event)
274
+ })
275
+ }, keyExtractor(event, index))),
276
+ hiddenCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
277
+ style: [
278
+ theme.text.more,
279
+ styles$2.moreLabel,
280
+ { color: theme.colors.textMuted }
281
+ ],
282
+ onPress: onPressMore ? () => onPressMore(dayEvents, day) : void 0,
283
+ accessibilityRole: "button",
284
+ accessibilityLabel: `Show ${hiddenCount} more events`,
285
+ allowFontScaling: false,
286
+ children: `${hiddenCount} More`
287
+ }) : null
288
+ ]
289
+ }, day.toISOString());
290
+ };
291
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
292
+ style: styles$2.container,
293
+ children: weeks.map((week) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
294
+ style: styles$2.weekRow,
295
+ children: week.map((day) => renderDay(day))
296
+ }, week[0].toISOString()))
297
+ });
298
+ }
299
+ const MonthView = (0, react.memo)(MonthViewInner);
300
+ const styles$2 = react_native.StyleSheet.create({
301
+ container: { flex: 1 },
302
+ weekRow: {
303
+ flex: 1,
304
+ flexDirection: "row"
305
+ },
306
+ dayCell: {
307
+ flex: 1,
308
+ alignItems: "center",
309
+ paddingTop: 4,
310
+ gap: 2,
311
+ overflow: "hidden",
312
+ borderTopWidth: react_native.StyleSheet.hairlineWidth,
313
+ borderRightWidth: react_native.StyleSheet.hairlineWidth
314
+ },
315
+ dateBadge: {
316
+ justifyContent: "center",
317
+ alignItems: "center",
318
+ height: 24,
319
+ width: 24
320
+ },
321
+ monthEvent: { width: "92%" },
322
+ moreLabel: { marginTop: 2 }
323
+ });
324
+ //#endregion
325
+ //#region src/components/MonthPager.tsx
326
+ const PAGE_WINDOW$1 = 60;
327
+ const PAGE_VIEWABILITY$1 = { itemVisiblePercentThreshold: 90 };
328
+ function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, renderEvent, keyExtractor, onPressDay, onPressEvent, onPressMore, onChangeDate, freeSwipe = false }) {
329
+ const { width, height } = (0, react_native.useWindowDimensions)();
330
+ const listRef = (0, react.useRef)(null);
331
+ const [pageHeight, setPageHeight] = (0, react.useState)(height);
332
+ const [anchorDate] = (0, react.useState)(date);
333
+ const anchor = (0, react.useMemo)(() => (0, date_fns.startOfMonth)(anchorDate), [anchorDate]);
334
+ const monthDates = (0, react.useMemo)(() => Array.from({ length: 121 }, (_, i) => (0, date_fns.addMonths)(anchor, i - PAGE_WINDOW$1)), [anchor]);
335
+ const activeIndex = (0, react.useCallback)((target) => (0, date_fns.differenceInCalendarMonths)((0, date_fns.startOfMonth)(target), anchor) + PAGE_WINDOW$1, [anchor])(date);
336
+ const viewedIndexRef = (0, react.useRef)(activeIndex);
337
+ const handleViewableItemsChanged = (0, react.useCallback)((info) => {
338
+ const settled = info.viewableItems.find((token) => token.isViewable);
339
+ if (settled?.index == null || settled.index === viewedIndexRef.current) return;
340
+ viewedIndexRef.current = settled.index;
341
+ if (settled.item) onChangeDate(settled.item);
342
+ }, [onChangeDate]);
343
+ (0, react.useEffect)(() => {
344
+ if (activeIndex === viewedIndexRef.current) return;
345
+ viewedIndexRef.current = activeIndex;
346
+ listRef.current?.scrollToIndex({
347
+ index: activeIndex,
348
+ animated: false
349
+ });
350
+ }, [activeIndex]);
351
+ const snapToIndices = (0, react.useMemo)(() => monthDates.map((_, index) => index), [monthDates]);
352
+ const keyExtractorList = (0, react.useCallback)((item) => item.toISOString(), []);
353
+ const getFixedItemSize = (0, react.useCallback)(() => width, [width]);
354
+ const renderItem = (0, react.useCallback)(({ item }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
355
+ style: {
356
+ width,
357
+ height: pageHeight
358
+ },
359
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthView, {
360
+ date: item,
361
+ events,
362
+ maxVisibleEventCount,
363
+ weekStartsOn,
364
+ renderEvent,
365
+ keyExtractor,
366
+ onPressDay,
367
+ onPressEvent,
368
+ onPressMore
369
+ })
370
+ }), [
371
+ width,
372
+ pageHeight,
373
+ events,
374
+ maxVisibleEventCount,
375
+ weekStartsOn,
376
+ renderEvent,
377
+ keyExtractor,
378
+ onPressDay,
379
+ onPressEvent,
380
+ onPressMore
381
+ ]);
382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
383
+ style: styles$1.pager,
384
+ onLayout: (event) => setPageHeight(event.nativeEvent.layout.height),
385
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react_native.LegendList, {
386
+ ref: listRef,
387
+ style: styles$1.pagerList,
388
+ data: monthDates,
389
+ horizontal: true,
390
+ recycleItems: false,
391
+ keyExtractor: keyExtractorList,
392
+ getFixedItemSize,
393
+ pagingEnabled: !freeSwipe,
394
+ snapToIndices: freeSwipe ? snapToIndices : void 0,
395
+ initialScrollIndex: activeIndex,
396
+ showsHorizontalScrollIndicator: false,
397
+ viewabilityConfig: PAGE_VIEWABILITY$1,
398
+ onViewableItemsChanged: handleViewableItemsChanged,
399
+ renderItem
400
+ }, pageHeight)
401
+ });
402
+ }
403
+ const MonthPager = (0, react.memo)(MonthPagerInner);
404
+ const styles$1 = react_native.StyleSheet.create({
405
+ pager: { flex: 1 },
406
+ pagerList: { flex: 1 }
407
+ });
408
+ //#endregion
409
+ //#region src/components/TimeGrid.tsx
410
+ const MINUTES_PER_HOUR = 60;
411
+ const HOURS_PER_DAY = 24;
412
+ const DAY_VIEW_STEP = 1;
413
+ const WEEK_VIEW_STEP = 7;
414
+ const PAGE_WINDOW = 180;
415
+ const PAGE_VIEWABILITY = { itemVisiblePercentThreshold: 90 };
416
+ const DEFAULT_HOUR_HEIGHT = 64;
417
+ const DEFAULT_MIN_HOUR_HEIGHT = 32;
418
+ const DEFAULT_MAX_HOUR_HEIGHT = 160;
419
+ const DEFAULT_HOUR_COLUMN_WIDTH = 50;
420
+ const MIN_EVENT_HEIGHT = 32;
421
+ const HOUR_LABEL_TOP_INSET = 12;
422
+ const NOW_TICK_MS = 6e4;
423
+ function useNow(enabled) {
424
+ const [now, setNow] = (0, react.useState)(() => /* @__PURE__ */ new Date());
425
+ (0, react.useEffect)(() => {
426
+ if (!enabled) return;
427
+ setNow(/* @__PURE__ */ new Date());
428
+ const id = setInterval(() => setNow(/* @__PURE__ */ new Date()), NOW_TICK_MS);
429
+ return () => clearInterval(id);
430
+ }, [enabled]);
431
+ return now;
432
+ }
433
+ function formatHourLabel(hour, ampm) {
434
+ if (!ampm) return String(hour);
435
+ const period = hour < 12 ? "AM" : "PM";
436
+ return `${hour % 12 === 0 ? 12 : hour % 12} ${period}`;
437
+ }
438
+ function AnimatedEventBox({ positioned, cellHeight, minHour, left, width, mode, renderEvent, onPress }) {
439
+ const RenderEventComponent = renderEvent;
440
+ const boxHeight = (0, react_native_reanimated.useDerivedValue)(() => Math.max(positioned.durationHours * cellHeight.value, MIN_EVENT_HEIGHT), [positioned.durationHours]);
441
+ const boxStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({
442
+ top: (positioned.startHours - minHour) * cellHeight.value,
443
+ height: boxHeight.value
444
+ }), [
445
+ positioned.startHours,
446
+ positioned.durationHours,
447
+ minHour
448
+ ]);
449
+ const handlePress = () => onPress(positioned.event);
450
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
451
+ style: [
452
+ styles.eventBox,
453
+ {
454
+ left,
455
+ width
456
+ },
457
+ boxStyle
458
+ ],
459
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenderEventComponent, {
460
+ event: positioned.event,
461
+ mode,
462
+ boxHeight,
463
+ continuesBefore: positioned.continuesBefore,
464
+ continuesAfter: positioned.continuesAfter,
465
+ onPress: handlePress
466
+ })
467
+ });
468
+ }
469
+ const HourRow = ({ hour, minHour, cellHeight, hourColumnWidth, label }) => {
470
+ const theme = useCalendarTheme();
471
+ const animatedStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({ top: (hour - minHour) * cellHeight.value }), [hour, minHour]);
472
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native_reanimated.default.View, {
473
+ style: [styles.hourRow, animatedStyle],
474
+ pointerEvents: "none",
475
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
476
+ style: [
477
+ theme.text.hourLabel,
478
+ styles.hourLabel,
479
+ {
480
+ width: hourColumnWidth,
481
+ color: theme.colors.textMuted
482
+ }
483
+ ],
484
+ allowFontScaling: false,
485
+ children: label
486
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { style: [styles.hourLine, { backgroundColor: theme.colors.gridLine }] })]
487
+ });
488
+ };
489
+ const NowIndicator = ({ cellHeight, nowHours, minHour, left, color }) => {
490
+ const animatedStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({ top: (nowHours - minHour) * cellHeight.value }), [nowHours, minHour]);
491
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
492
+ style: [
493
+ styles.nowIndicator,
494
+ {
495
+ left,
496
+ backgroundColor: color
497
+ },
498
+ animatedStyle
499
+ ],
500
+ pointerEvents: "none"
501
+ });
502
+ };
503
+ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, committedCellHeight, scrollY, isActive, scrollOffsetMinutes, weekStartsOn, hourColumnWidth, minHour, maxHour, ampm, minHourHeight, maxHourHeight, showNowIndicator, renderEvent, keyExtractor, onPressEvent, onPressCell }) {
504
+ const theme = useCalendarTheme();
505
+ const { width } = (0, react_native.useWindowDimensions)();
506
+ const scrollRef = (0, react_native_reanimated.useAnimatedRef)();
507
+ const heightSource = isActive ? cellHeight : committedCellHeight;
508
+ const scrollHandler = (0, react_native_reanimated.useAnimatedScrollHandler)((event) => {
509
+ if (isActive) scrollY.value = event.contentOffset.y;
510
+ });
511
+ (0, react_native_reanimated.useAnimatedReaction)(() => scrollY.value, (current, previous) => {
512
+ if (!isActive && current !== previous) (0, react_native_reanimated.scrollTo)(scrollRef, 0, current, false);
513
+ });
514
+ const days = (0, react.useMemo)(() => mode === "week" ? getWeekDays(date, weekStartsOn) : [date], [
515
+ mode,
516
+ date,
517
+ weekStartsOn
518
+ ]);
519
+ const dayWidth = (width - hourColumnWidth) / days.length;
520
+ const dayLeft = (dayIndex) => hourColumnWidth + dayIndex * dayWidth;
521
+ const dayLayouts = (0, react.useMemo)(() => days.map((day) => layoutDayEvents(events, day)), [days, events]);
522
+ const handleBackgroundPress = (event) => {
523
+ if (!onPressCell) return;
524
+ const { locationX, locationY } = event.nativeEvent;
525
+ const day = days[days.length === 1 ? 0 : Math.floor(locationX / dayWidth)];
526
+ if (!day) return;
527
+ const minutes = Math.round((minHour + locationY / heightSource.value) * MINUTES_PER_HOUR);
528
+ const pressed = new Date(day);
529
+ pressed.setHours(0, 0, 0, 0);
530
+ pressed.setMinutes(minutes);
531
+ onPressCell(pressed);
532
+ };
533
+ const hoursRange = (0, react.useMemo)(() => Array.from({ length: maxHour - minHour }, (_, index) => minHour + index), [minHour, maxHour]);
534
+ const now = useNow(showNowIndicator && isActive);
535
+ const nowDayIndex = days.findIndex((day) => getIsToday(day));
536
+ const nowHours = ((0, date_fns.getHours)(now) * MINUTES_PER_HOUR + (0, date_fns.getMinutes)(now)) / MINUTES_PER_HOUR;
537
+ const nowInWindow = nowHours >= minHour && nowHours <= maxHour;
538
+ const fullHeightStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({ height: (maxHour - minHour) * heightSource.value }), [
539
+ minHour,
540
+ maxHour,
541
+ heightSource
542
+ ]);
543
+ const pinchStartCellHeight = (0, react_native_reanimated.useSharedValue)(hourHeight);
544
+ const zoomGesture = (0, react.useMemo)(() => {
545
+ const pinch = react_native_gesture_handler.Gesture.Pinch().onStart(() => {
546
+ pinchStartCellHeight.value = cellHeight.value;
547
+ }).onUpdate((event) => {
548
+ cellHeight.value = Math.min(maxHourHeight, Math.max(minHourHeight, pinchStartCellHeight.value * event.scale));
549
+ }).onEnd(() => {
550
+ committedCellHeight.value = cellHeight.value;
551
+ });
552
+ return react_native_gesture_handler.Gesture.Simultaneous(pinch, react_native_gesture_handler.Gesture.Native());
553
+ }, [
554
+ cellHeight,
555
+ committedCellHeight,
556
+ pinchStartCellHeight,
557
+ minHourHeight,
558
+ maxHourHeight
559
+ ]);
560
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
561
+ style: styles.container,
562
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_gesture_handler.GestureDetector, {
563
+ gesture: zoomGesture,
564
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.ScrollView, {
565
+ ref: scrollRef,
566
+ showsVerticalScrollIndicator: true,
567
+ onScroll: scrollHandler,
568
+ scrollEventThrottle: 16,
569
+ contentContainerStyle: { paddingTop: HOUR_LABEL_TOP_INSET },
570
+ contentOffset: {
571
+ x: 0,
572
+ y: Math.max(0, scrollOffsetMinutes / MINUTES_PER_HOUR - minHour) * hourHeight
573
+ },
574
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native_reanimated.default.View, {
575
+ style: [styles.content, fullHeightStyle],
576
+ children: [
577
+ onPressCell ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Pressable, {
578
+ style: [styles.cellPressLayer, { left: hourColumnWidth }],
579
+ onPress: handleBackgroundPress,
580
+ importantForAccessibility: "no",
581
+ accessibilityElementsHidden: true
582
+ }) : null,
583
+ days.map((day, dayIndex) => isWeekend(day) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
584
+ style: [
585
+ styles.weekendColumn,
586
+ { backgroundColor: theme.colors.weekendBackground },
587
+ {
588
+ left: dayLeft(dayIndex),
589
+ width: dayWidth
590
+ },
591
+ fullHeightStyle
592
+ ],
593
+ pointerEvents: "none"
594
+ }, `weekend-${day.toISOString()}`) : null),
595
+ days.map((day, dayIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
596
+ style: [
597
+ styles.daySeparator,
598
+ { backgroundColor: theme.colors.gridLine },
599
+ { left: dayLeft(dayIndex) },
600
+ fullHeightStyle
601
+ ],
602
+ pointerEvents: "none"
603
+ }, `separator-${day.toISOString()}`)),
604
+ hoursRange.map((hour) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HourRow, {
605
+ hour,
606
+ minHour,
607
+ cellHeight: heightSource,
608
+ hourColumnWidth,
609
+ label: formatHourLabel(hour, ampm)
610
+ }, hour)),
611
+ dayLayouts.flatMap((layout, dayIndex) => layout.filter((p) => p.startHours < maxHour && p.startHours + p.durationHours > minHour).map((positioned, eventIndex) => {
612
+ const columnWidth = dayWidth / positioned.columns;
613
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedEventBox, {
614
+ positioned,
615
+ cellHeight: heightSource,
616
+ minHour,
617
+ left: dayLeft(dayIndex) + positioned.column * columnWidth,
618
+ width: columnWidth,
619
+ mode,
620
+ renderEvent,
621
+ onPress: onPressEvent
622
+ }, keyExtractor(positioned.event, eventIndex));
623
+ })),
624
+ showNowIndicator && nowDayIndex >= 0 && nowInWindow ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NowIndicator, {
625
+ cellHeight: heightSource,
626
+ nowHours,
627
+ minHour,
628
+ left: dayLeft(nowDayIndex),
629
+ color: theme.colors.nowIndicator
630
+ }) : null
631
+ ]
632
+ })
633
+ })
634
+ })
635
+ });
636
+ }
637
+ const TimetablePage = (0, react.memo)(TimetablePageInner);
638
+ function TimeGridInner({ mode, date, events, cellHeight, hourHeight = 64, weekStartsOn, renderEvent, keyExtractor, scrollOffsetMinutes = 0, hourColumnWidth = DEFAULT_HOUR_COLUMN_WIDTH, minHour = 0, maxHour = HOURS_PER_DAY, ampm = false, minHourHeight = DEFAULT_MIN_HOUR_HEIGHT, maxHourHeight = DEFAULT_MAX_HOUR_HEIGHT, showNowIndicator = true, locale, freeSwipe = false, onPressEvent, onPressCell, onChangeDate, renderHeader }) {
639
+ const clampedMinHour = Math.max(0, Math.min(minHour, HOURS_PER_DAY - 1));
640
+ const clampedMaxHour = Math.max(clampedMinHour + 1, Math.min(maxHour, HOURS_PER_DAY));
641
+ const { width, height } = (0, react_native.useWindowDimensions)();
642
+ const listRef = (0, react.useRef)(null);
643
+ const [pageHeight, setPageHeight] = (0, react.useState)(height);
644
+ const step = mode === "week" ? WEEK_VIEW_STEP : DAY_VIEW_STEP;
645
+ const scrollY = (0, react_native_reanimated.useSharedValue)(Math.max(0, scrollOffsetMinutes / MINUTES_PER_HOUR - clampedMinHour) * hourHeight);
646
+ const committedCellHeight = (0, react_native_reanimated.useSharedValue)(hourHeight);
647
+ const [anchorDate] = (0, react.useState)(date);
648
+ const anchor = (0, react.useMemo)(() => mode === "week" ? (0, date_fns.startOfWeek)(anchorDate, { weekStartsOn }) : (0, date_fns.startOfDay)(anchorDate), [
649
+ mode,
650
+ anchorDate,
651
+ weekStartsOn
652
+ ]);
653
+ const pageDates = (0, react.useMemo)(() => Array.from({ length: 361 }, (_, i) => (0, date_fns.addDays)(anchor, (i - PAGE_WINDOW) * step)), [anchor, step]);
654
+ const activeIndex = (0, react.useCallback)((target) => {
655
+ const aligned = mode === "week" ? (0, date_fns.startOfWeek)(target, { weekStartsOn }) : (0, date_fns.startOfDay)(target);
656
+ return Math.round((0, date_fns.differenceInCalendarDays)(aligned, anchor) / step) + PAGE_WINDOW;
657
+ }, [
658
+ anchor,
659
+ mode,
660
+ step,
661
+ weekStartsOn
662
+ ])(date);
663
+ const viewedIndexRef = (0, react.useRef)(activeIndex);
664
+ const headerDays = (0, react.useMemo)(() => mode === "week" ? getWeekDays(date, weekStartsOn) : [date], [
665
+ mode,
666
+ date,
667
+ weekStartsOn
668
+ ]);
669
+ const handleViewableItemsChanged = (0, react.useCallback)((info) => {
670
+ const settled = info.viewableItems.find((token) => token.isViewable);
671
+ if (settled?.index == null || settled.index === viewedIndexRef.current) return;
672
+ viewedIndexRef.current = settled.index;
673
+ if (settled.item) onChangeDate(settled.item);
674
+ }, [onChangeDate]);
675
+ (0, react.useEffect)(() => {
676
+ if (activeIndex === viewedIndexRef.current) return;
677
+ viewedIndexRef.current = activeIndex;
678
+ listRef.current?.scrollToIndex({
679
+ index: activeIndex,
680
+ animated: false
681
+ });
682
+ }, [activeIndex]);
683
+ const snapToIndices = (0, react.useMemo)(() => pageDates.map((_, index) => index), [pageDates]);
684
+ const keyExtractorList = (0, react.useCallback)((item) => item.toISOString(), []);
685
+ const getFixedItemSize = (0, react.useCallback)(() => width, [width]);
686
+ const renderItem = (0, react.useCallback)(({ item, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
687
+ style: {
688
+ width,
689
+ height: pageHeight
690
+ },
691
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimetablePage, {
692
+ mode,
693
+ date: item,
694
+ events,
695
+ cellHeight,
696
+ hourHeight,
697
+ committedCellHeight,
698
+ scrollY,
699
+ isActive: index === activeIndex,
700
+ scrollOffsetMinutes,
701
+ weekStartsOn,
702
+ hourColumnWidth,
703
+ minHour: clampedMinHour,
704
+ maxHour: clampedMaxHour,
705
+ ampm,
706
+ minHourHeight,
707
+ maxHourHeight,
708
+ showNowIndicator,
709
+ renderEvent,
710
+ keyExtractor,
711
+ onPressEvent,
712
+ onPressCell
713
+ })
714
+ }), [
715
+ width,
716
+ pageHeight,
717
+ mode,
718
+ events,
719
+ cellHeight,
720
+ hourHeight,
721
+ committedCellHeight,
722
+ scrollY,
723
+ activeIndex,
724
+ scrollOffsetMinutes,
725
+ weekStartsOn,
726
+ hourColumnWidth,
727
+ clampedMinHour,
728
+ clampedMaxHour,
729
+ ampm,
730
+ minHourHeight,
731
+ maxHourHeight,
732
+ showNowIndicator,
733
+ renderEvent,
734
+ keyExtractor,
735
+ onPressEvent,
736
+ onPressCell
737
+ ]);
738
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
739
+ style: styles.container,
740
+ children: [renderHeader ? renderHeader(headerDays) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultHeader, {
741
+ days: headerDays,
742
+ mode,
743
+ width,
744
+ hourColumnWidth,
745
+ locale
746
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
747
+ style: styles.pager,
748
+ onLayout: (event) => setPageHeight(event.nativeEvent.layout.height),
749
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react_native.LegendList, {
750
+ ref: listRef,
751
+ style: styles.pagerList,
752
+ data: pageDates,
753
+ horizontal: true,
754
+ recycleItems: false,
755
+ keyExtractor: keyExtractorList,
756
+ getFixedItemSize,
757
+ pagingEnabled: !freeSwipe,
758
+ snapToIndices: freeSwipe ? snapToIndices : void 0,
759
+ initialScrollIndex: activeIndex,
760
+ showsHorizontalScrollIndicator: false,
761
+ viewabilityConfig: PAGE_VIEWABILITY,
762
+ onViewableItemsChanged: handleViewableItemsChanged,
763
+ renderItem
764
+ }, pageHeight)
765
+ })]
766
+ });
767
+ }
768
+ const TimeGrid = (0, react.memo)(TimeGridInner);
769
+ const DefaultHeader = ({ days, mode, width, hourColumnWidth, locale }) => {
770
+ const dayWidth = mode === "week" ? (width - hourColumnWidth) / days.length : width;
771
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
772
+ style: styles.headerRow,
773
+ children: [mode === "week" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { style: { width: hourColumnWidth } }) : null, days.map((day) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DayHeader, {
774
+ day,
775
+ mode,
776
+ width: dayWidth,
777
+ locale
778
+ }, day.toISOString()))]
779
+ });
780
+ };
781
+ const DayHeader = ({ day, mode, width, locale }) => {
782
+ const theme = useCalendarTheme();
783
+ const isToday = getIsToday(day);
784
+ const badgeSize = mode === "day" ? 44 : 32;
785
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
786
+ style: [styles.dayHeader, {
787
+ width,
788
+ gap: mode === "day" ? 4 : 2
789
+ }],
790
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
791
+ style: [styles.dayHeaderBadge, isToday && {
792
+ backgroundColor: theme.colors.todayBackground,
793
+ borderRadius: 999,
794
+ width: badgeSize,
795
+ height: badgeSize
796
+ }],
797
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
798
+ style: [theme.text.dayNumber, { color: isToday ? theme.colors.todayText : theme.colors.text }],
799
+ allowFontScaling: false,
800
+ ...isToday && { accessibilityLabel: `Today, ${day.getDate()}` },
801
+ children: day.getDate()
802
+ })
803
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
804
+ style: [theme.text.weekday, { color: theme.colors.text }],
805
+ allowFontScaling: false,
806
+ children: day.toLocaleDateString(locale, { weekday: "short" })
807
+ })]
808
+ });
809
+ };
810
+ const styles = react_native.StyleSheet.create({
811
+ pager: { flex: 1 },
812
+ pagerList: { flex: 1 },
813
+ container: { flex: 1 },
814
+ headerRow: {
815
+ flexDirection: "row",
816
+ alignItems: "center",
817
+ paddingBottom: 8
818
+ },
819
+ dayHeader: { alignItems: "center" },
820
+ dayHeaderBadge: {
821
+ justifyContent: "center",
822
+ alignItems: "center"
823
+ },
824
+ content: {
825
+ width: "100%",
826
+ position: "relative"
827
+ },
828
+ cellPressLayer: {
829
+ position: "absolute",
830
+ top: 0,
831
+ bottom: 0,
832
+ right: 0
833
+ },
834
+ weekendColumn: {
835
+ position: "absolute",
836
+ top: 0
837
+ },
838
+ daySeparator: {
839
+ position: "absolute",
840
+ top: 0,
841
+ width: react_native.StyleSheet.hairlineWidth
842
+ },
843
+ hourRow: {
844
+ position: "absolute",
845
+ left: 0,
846
+ right: 0,
847
+ flexDirection: "row",
848
+ alignItems: "flex-start"
849
+ },
850
+ hourLabel: {
851
+ marginTop: -8,
852
+ textAlign: "center"
853
+ },
854
+ hourLine: {
855
+ flex: 1,
856
+ height: react_native.StyleSheet.hairlineWidth
857
+ },
858
+ eventBox: {
859
+ position: "absolute",
860
+ overflow: "hidden"
861
+ },
862
+ nowIndicator: {
863
+ position: "absolute",
864
+ right: 0,
865
+ height: 2
866
+ }
867
+ });
868
+ //#endregion
869
+ //#region src/components/Calendar.tsx
870
+ const defaultKeyExtractor = (event) => `${event.start.toISOString()}|${event.end.toISOString()}|${event.title ?? ""}`;
871
+ function Calendar({ events, mode, date, onChangeDate, onPressEvent, onPressDay, onPressMore, onPressCell, maxVisibleEventCount = 2, weekStartsOn = 0, renderEvent = DefaultEvent, keyExtractor = defaultKeyExtractor, theme, cellHeight: cellHeightProp, hourHeight = 64, minHourHeight, maxHourHeight, hourColumnWidth, minHour, maxHour, ampm, scrollOffsetMinutes, showNowIndicator, locale, freeSwipe, renderTimeGridHeader }) {
872
+ const mergedTheme = (0, react.useMemo)(() => mergeTheme(theme), [theme]);
873
+ const internalCellHeight = (0, react_native_reanimated.useSharedValue)(hourHeight);
874
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CalendarThemeProvider, {
875
+ value: mergedTheme,
876
+ children: mode === "month" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthPager, {
877
+ date,
878
+ events,
879
+ maxVisibleEventCount,
880
+ weekStartsOn,
881
+ renderEvent,
882
+ keyExtractor,
883
+ onPressDay,
884
+ onPressEvent,
885
+ onPressMore,
886
+ onChangeDate,
887
+ freeSwipe
888
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeGrid, {
889
+ mode,
890
+ date,
891
+ events,
892
+ cellHeight: cellHeightProp ?? internalCellHeight,
893
+ hourHeight,
894
+ weekStartsOn,
895
+ renderEvent,
896
+ keyExtractor,
897
+ scrollOffsetMinutes,
898
+ hourColumnWidth,
899
+ minHour,
900
+ maxHour,
901
+ ampm,
902
+ minHourHeight,
903
+ maxHourHeight,
904
+ showNowIndicator,
905
+ locale,
906
+ freeSwipe,
907
+ onPressEvent,
908
+ onPressCell,
909
+ onChangeDate,
910
+ renderHeader: renderTimeGridHeader
911
+ })
912
+ });
913
+ }
914
+ //#endregion
915
+ exports.Calendar = Calendar;
916
+ exports.CalendarThemeProvider = CalendarThemeProvider;
917
+ exports.DEFAULT_HOUR_HEIGHT = DEFAULT_HOUR_HEIGHT;
918
+ exports.DefaultEvent = DefaultEvent;
919
+ exports.MonthPager = MonthPager;
920
+ exports.MonthView = MonthView;
921
+ exports.TimeGrid = TimeGrid;
922
+ exports.defaultTheme = defaultTheme;
923
+ exports.getIsToday = getIsToday;
924
+ exports.getWeekDays = getWeekDays;
925
+ exports.isSameCalendarDay = isSameCalendarDay;
926
+ exports.isWeekend = isWeekend;
927
+ exports.layoutDayEvents = layoutDayEvents;
928
+ exports.mergeTheme = mergeTheme;
929
+ exports.minutesIntoDay = minutesIntoDay;
930
+ exports.useCalendarTheme = useCalendarTheme;