@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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1543 @@
1
+ import { a as useWebPagerKeys, c as defaultTheme, i as MonthView, l as mergeTheme, n as DefaultMonthEvent, o as CalendarThemeProvider, r as MonthPager, s as darkTheme, t as MonthList, u as useCalendarTheme } from "./MonthList-CyrEJ_U6.mjs";
2
+ import { addDays, differenceInCalendarDays, endOfDay, endOfMonth, endOfWeek, format, getHours, getISOWeek, getMinutes, isSameDay, startOfDay, startOfMonth, startOfWeek } from "date-fns";
3
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import Animated, { runOnJS, scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, useAnimatedStyle, useDerivedValue, useSharedValue } from "react-native-reanimated";
5
+ import { buildMonthGrid, buildMonthWeeks, cellRangeFromDrag, closedHourBands, daySelectionState, eventAccessibilityLabel, eventChipLayout, eventTimeLabel, eventsInTimeZone, expandRecurringEvents, formatHour, getIsToday, getIsToday as getIsToday$1, getViewDays, getViewDays as getViewDays$1, getWeekDays, isAllDayEvent, isAllDayEvent as isAllDayEvent$1, isDateSelectable, isRangeEndpoint, isSameCalendarDay, isSameCalendarDay as isSameCalendarDay$1, isWeekend, isWeekend as isWeekend$1, isWithinDateRange, layoutDayEvents, layoutDayEvents as layoutDayEvents$1, minutesIntoDay, nextDateRange, resolveDraggedBounds, snapDeltaMinutes, titleEllipsizeMode, titleNumberOfLines, toZonedTime, useDateRange, useMonthGrid, viewDayCount } from "@super-calendar/core";
6
+ import { LegendList } from "@legendapp/list/react-native";
7
+ import { Platform, Pressable, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from "react-native";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
10
+ //#region src/components/Agenda.tsx
11
+ /**
12
+ * A vertical, day-grouped list of events (no time grid). Events are sorted by
13
+ * start, grouped under a date header per day. The consumer controls which
14
+ * events (and therefore which date range) are shown.
15
+ */
16
+ function Agenda({ events, locale, renderEvent, keyExtractor, onPressEvent, onLongPressEvent, onPressDay, activeDate, itemSeparatorComponent }) {
17
+ const theme = useCalendarTheme();
18
+ const RenderEventComponent = renderEvent;
19
+ const rows = useMemo(() => {
20
+ const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
21
+ const out = [];
22
+ let currentDay = null;
23
+ sorted.forEach((event, index) => {
24
+ if (!currentDay || !isSameDay(event.start, currentDay)) {
25
+ currentDay = startOfDay(event.start);
26
+ out.push({
27
+ kind: "header",
28
+ date: currentDay,
29
+ key: `h-${currentDay.toISOString()}`
30
+ });
31
+ }
32
+ out.push({
33
+ kind: "event",
34
+ event,
35
+ index,
36
+ key: `e-${keyExtractor(event, index)}`
37
+ });
38
+ });
39
+ return out;
40
+ }, [events, keyExtractor]);
41
+ const keyExtractorRow = useCallback((row) => row.key, []);
42
+ const renderItem = useCallback(({ item }) => {
43
+ if (item.kind === "header") {
44
+ const isHighlighted = activeDate ? isSameDay(item.date, activeDate) : getIsToday$1(item.date);
45
+ return /* @__PURE__ */ jsx(Text, {
46
+ style: [styles$3.header, { color: isHighlighted ? theme.colors.todayBackground : theme.colors.textMuted }],
47
+ onPress: onPressDay ? () => onPressDay(item.date) : void 0,
48
+ accessibilityRole: onPressDay ? "button" : "header",
49
+ children: format(item.date, "EEEE, d LLLL", { locale })
50
+ });
51
+ }
52
+ return /* @__PURE__ */ jsx(View, {
53
+ style: styles$3.eventRow,
54
+ children: /* @__PURE__ */ jsx(RenderEventComponent, {
55
+ event: item.event,
56
+ mode: "schedule",
57
+ isAllDay: isAllDayEvent$1(item.event),
58
+ onPress: () => onPressEvent(item.event),
59
+ onLongPress: onLongPressEvent ? () => onLongPressEvent(item.event) : void 0
60
+ })
61
+ });
62
+ }, [
63
+ theme,
64
+ locale,
65
+ activeDate,
66
+ onPressDay,
67
+ onPressEvent,
68
+ onLongPressEvent,
69
+ RenderEventComponent
70
+ ]);
71
+ if (rows.length === 0) return /* @__PURE__ */ jsx(Text, {
72
+ style: [styles$3.empty, { color: theme.colors.textMuted }],
73
+ children: "No events"
74
+ });
75
+ return /* @__PURE__ */ jsx(LegendList, {
76
+ style: styles$3.list,
77
+ data: rows,
78
+ keyExtractor: keyExtractorRow,
79
+ renderItem,
80
+ ItemSeparatorComponent: itemSeparatorComponent ?? void 0,
81
+ recycleItems: false
82
+ });
83
+ }
84
+ const styles$3 = StyleSheet.create({
85
+ list: { flex: 1 },
86
+ header: {
87
+ fontSize: 13,
88
+ fontWeight: "600",
89
+ paddingTop: 12,
90
+ paddingBottom: 4,
91
+ paddingHorizontal: 12
92
+ },
93
+ eventRow: {
94
+ paddingHorizontal: 12,
95
+ paddingVertical: 2
96
+ },
97
+ empty: {
98
+ fontSize: 14,
99
+ paddingVertical: 16,
100
+ paddingHorizontal: 12
101
+ }
102
+ });
103
+ //#endregion
104
+ //#region src/components/DefaultEvent.tsx
105
+ const BOX_PADDING_V = 2;
106
+ const TIME_LINE_HEIGHT = 30;
107
+ const FALLBACK_TITLE_LINE_HEIGHT = 16;
108
+ const numericStyle = (value, fallback) => typeof value === "number" ? value : fallback;
109
+ /**
110
+ * The built-in event renderer: a filled, rounded box showing the event title
111
+ * and (on the day/week grid, when the box is tall enough) its time range. The
112
+ * title fills the box in whole lines, never a half-cut last line, and clips
113
+ * without an ellipsis unless `ellipsizeTitle` is set. The time is secondary: it
114
+ * only shows once a full line is free beneath the title. Pass your own
115
+ * `renderEvent` to `<Calendar>` to replace it entirely.
116
+ */
117
+ function DefaultEvent({ event, mode, boxHeight, isAllDay, ampm = false, showTime = true, ellipsizeTitle = false, allDayLabel, cellStyle, onPress, onLongPress }) {
118
+ const theme = useCalendarTheme();
119
+ const isAllDayEvent = isAllDay ?? false;
120
+ const timeLabel = eventTimeLabel({
121
+ mode,
122
+ isAllDay: isAllDayEvent,
123
+ start: event.start,
124
+ end: event.end,
125
+ ampm,
126
+ showTime,
127
+ allDayLabel
128
+ });
129
+ const ellipsizeMode = titleEllipsizeMode(ellipsizeTitle);
130
+ const titleLineHeight = numericStyle(theme.text.eventTitle.lineHeight, FALLBACK_TITLE_LINE_HEIGHT);
131
+ const accessibilityLabel = eventAccessibilityLabel({
132
+ title: event.title,
133
+ isAllDay: isAllDayEvent,
134
+ start: event.start,
135
+ end: event.end,
136
+ ampm,
137
+ allDayLabel
138
+ });
139
+ const fixedTitleLines = titleNumberOfLines(mode, isAllDayEvent);
140
+ const isSchedule = mode === "schedule";
141
+ const layout = useDerivedValue(() => eventChipLayout({
142
+ boxHeightPx: boxHeight?.value,
143
+ mode,
144
+ hasTime: timeLabel != null,
145
+ titleLineHeightPx: titleLineHeight,
146
+ timeLineHeightPx: TIME_LINE_HEIGHT,
147
+ paddingYPx: BOX_PADDING_V
148
+ }), [
149
+ boxHeight,
150
+ mode,
151
+ timeLabel,
152
+ titleLineHeight
153
+ ]);
154
+ const titleClipStyle = useAnimatedStyle(() => {
155
+ const lines = layout.value.titleMaxLines;
156
+ return { maxHeight: lines > 0 ? lines * titleLineHeight : void 0 };
157
+ }, [layout, titleLineHeight]);
158
+ const timeStyle = useAnimatedStyle(() => {
159
+ return { display: layout.value.showTime ? "flex" : "none" };
160
+ }, [layout]);
161
+ return /* @__PURE__ */ jsxs(TouchableOpacity, {
162
+ style: [
163
+ styles$2.box,
164
+ isSchedule && styles$2.scheduleBox,
165
+ { backgroundColor: theme.colors.eventBackground },
166
+ event.disabled && styles$2.disabled,
167
+ cellStyle
168
+ ],
169
+ onPress,
170
+ onLongPress,
171
+ activeOpacity: .7,
172
+ accessibilityRole: "button",
173
+ accessibilityLabel,
174
+ accessibilityState: { disabled: event.disabled ?? false },
175
+ children: [event.title ? fixedTitleLines == null ? /* @__PURE__ */ jsx(Animated.View, {
176
+ style: [styles$2.titleClip, titleClipStyle],
177
+ children: /* @__PURE__ */ jsx(Text, {
178
+ style: [
179
+ theme.text.eventTitle,
180
+ isSchedule && styles$2.scheduleTitle,
181
+ { color: theme.colors.eventText }
182
+ ],
183
+ ellipsizeMode,
184
+ allowFontScaling: false,
185
+ children: event.title
186
+ })
187
+ }) : /* @__PURE__ */ jsx(Text, {
188
+ style: [
189
+ theme.text.eventTitle,
190
+ styles$2.title,
191
+ isSchedule && styles$2.scheduleTitle,
192
+ { color: theme.colors.eventText }
193
+ ],
194
+ numberOfLines: fixedTitleLines,
195
+ ellipsizeMode,
196
+ allowFontScaling: false,
197
+ children: event.title
198
+ }) : null, timeLabel ? /* @__PURE__ */ jsx(Animated.View, {
199
+ style: timeStyle,
200
+ children: /* @__PURE__ */ jsx(Text, {
201
+ style: [
202
+ styles$2.time,
203
+ isSchedule && styles$2.scheduleTime,
204
+ { color: theme.colors.eventText }
205
+ ],
206
+ numberOfLines: 2,
207
+ ellipsizeMode,
208
+ allowFontScaling: false,
209
+ children: timeLabel
210
+ })
211
+ }) : null]
212
+ });
213
+ }
214
+ const styles$2 = StyleSheet.create({
215
+ box: {
216
+ flexGrow: 1,
217
+ flexShrink: 1,
218
+ flexBasis: "auto",
219
+ borderRadius: 6,
220
+ paddingVertical: BOX_PADDING_V,
221
+ paddingHorizontal: 4,
222
+ overflow: "hidden"
223
+ },
224
+ scheduleBox: {
225
+ borderRadius: 8,
226
+ paddingVertical: 6,
227
+ paddingHorizontal: 10
228
+ },
229
+ scheduleTitle: {
230
+ fontSize: 14,
231
+ fontWeight: "600",
232
+ lineHeight: 18
233
+ },
234
+ scheduleTime: {
235
+ fontSize: 13,
236
+ lineHeight: 18,
237
+ opacity: .75
238
+ },
239
+ titleClip: { overflow: "hidden" },
240
+ title: { flexShrink: 1 },
241
+ time: {
242
+ fontSize: 11,
243
+ lineHeight: 15
244
+ },
245
+ disabled: { opacity: .5 }
246
+ });
247
+ //#endregion
248
+ //#region src/utils/useWebGridZoom.ts
249
+ const ZOOM_SENSITIVITY = .002;
250
+ /**
251
+ * Web-only: zoom the time grid with Ctrl/Cmd + scroll, standing in for the pinch
252
+ * gesture that web has no equivalent of. `target` is the grid's host node — on
253
+ * web a React Native View ref resolves to its DOM element. No-op off web, when
254
+ * `enabled` is false, or before the node mounts. Plain scroll is left untouched.
255
+ */
256
+ function useWebGridZoom(enabled, target, cellHeight, committedCellHeight, minHeight, maxHeight) {
257
+ useEffect(() => {
258
+ if (Platform.OS !== "web" || !enabled) return;
259
+ const node = target.current;
260
+ if (!node) return;
261
+ const handler = (event) => {
262
+ if (!event.ctrlKey && !event.metaKey) return;
263
+ event.preventDefault();
264
+ const next = Math.min(maxHeight, Math.max(minHeight, cellHeight.value * Math.exp(-event.deltaY * ZOOM_SENSITIVITY)));
265
+ cellHeight.value = next;
266
+ committedCellHeight.value = next;
267
+ };
268
+ node.addEventListener("wheel", handler, { passive: false });
269
+ return () => node.removeEventListener("wheel", handler);
270
+ }, [
271
+ enabled,
272
+ target,
273
+ cellHeight,
274
+ committedCellHeight,
275
+ minHeight,
276
+ maxHeight
277
+ ]);
278
+ }
279
+ //#endregion
280
+ //#region src/components/AllDayLane.tsx
281
+ /**
282
+ * The all-day lane that sits above the scrolling time grid. All-day events are
283
+ * excluded from the timed columns (see `layoutDayEvents`) and shown here,
284
+ * stacked under their day(s). Renders nothing when no day has an all-day event,
285
+ * so timed-only calendars are unaffected.
286
+ */
287
+ function AllDayLane({ days, events, mode, hourColumnWidth, dayWidth, renderEvent, keyExtractor, onPressEvent, onLongPressEvent }) {
288
+ const theme = useCalendarTheme();
289
+ const RenderEventComponent = renderEvent;
290
+ const allDay = events.filter(isAllDayEvent$1);
291
+ const perDay = days.map((day) => {
292
+ const start = startOfDay(day);
293
+ const next = addDays(start, 1);
294
+ return allDay.filter((event) => event.start < next && event.end > start);
295
+ });
296
+ if (perDay.every((list) => list.length === 0)) return null;
297
+ return /* @__PURE__ */ jsxs(View, {
298
+ style: [styles$1.lane, { borderBottomColor: theme.colors.gridLine }],
299
+ children: [/* @__PURE__ */ jsx(View, {
300
+ style: [styles$1.gutter, { width: hourColumnWidth }],
301
+ children: /* @__PURE__ */ jsx(Text, {
302
+ style: [styles$1.label, { color: theme.colors.textMuted }],
303
+ allowFontScaling: false,
304
+ children: "all-day"
305
+ })
306
+ }), days.map((day, dayIndex) => /* @__PURE__ */ jsx(View, {
307
+ style: [styles$1.column, { width: dayWidth }],
308
+ children: perDay[dayIndex].map((event, index) => /* @__PURE__ */ jsx(View, {
309
+ style: styles$1.chip,
310
+ children: /* @__PURE__ */ jsx(RenderEventComponent, {
311
+ event,
312
+ mode,
313
+ isAllDay: true,
314
+ onPress: () => onPressEvent(event),
315
+ onLongPress: onLongPressEvent ? () => onLongPressEvent(event) : void 0
316
+ })
317
+ }, keyExtractor(event, index)))
318
+ }, day.toISOString()))]
319
+ });
320
+ }
321
+ const styles$1 = StyleSheet.create({
322
+ lane: {
323
+ flexDirection: "row",
324
+ borderBottomWidth: StyleSheet.hairlineWidth
325
+ },
326
+ gutter: { justifyContent: "center" },
327
+ label: {
328
+ fontSize: 10,
329
+ textAlign: "right",
330
+ paddingRight: 6
331
+ },
332
+ column: {
333
+ paddingVertical: 2,
334
+ paddingHorizontal: 1,
335
+ gap: 2
336
+ },
337
+ chip: { minHeight: 18 }
338
+ });
339
+ //#endregion
340
+ //#region src/components/TimeGrid.tsx
341
+ const isWeb = Platform.OS === "web";
342
+ const MINUTES_PER_HOUR = 60;
343
+ const HOURS_PER_DAY = 24;
344
+ const MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY;
345
+ const PAGE_WINDOW = 180;
346
+ const PAGE_VIEWABILITY = { itemVisiblePercentThreshold: 90 };
347
+ const DEFAULT_HOUR_HEIGHT = 48;
348
+ const DEFAULT_MIN_HOUR_HEIGHT = 32;
349
+ const DEFAULT_MAX_HOUR_HEIGHT = 160;
350
+ const DEFAULT_HOUR_COLUMN_WIDTH = 56;
351
+ const MIN_EVENT_HEIGHT = 32;
352
+ const EVENT_GAP = 2;
353
+ const DRAG_ACTIVATE_MS = 300;
354
+ const MOVE_ACTIVATE_MS = 500;
355
+ const DRAG_ACTIVATE_PX = 8;
356
+ const RESIZE_HANDLE_HEIGHT = 14;
357
+ const DEFAULT_DRAG_STEP_MINUTES = 15;
358
+ const HOUR_LABEL_TOP_INSET = 12;
359
+ const NOW_TICK_MS = 6e4;
360
+ function useNow(enabled) {
361
+ const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
362
+ useEffect(() => {
363
+ if (!enabled) return;
364
+ setNow(/* @__PURE__ */ new Date());
365
+ const id = setInterval(() => setNow(/* @__PURE__ */ new Date()), NOW_TICK_MS);
366
+ return () => clearInterval(id);
367
+ }, [enabled]);
368
+ return now;
369
+ }
370
+ function AnimatedEventBox({ positioned, cellHeight, minHour, left, width, dayWidth, dayIndex, dayCount, mode, renderEvent, snapMinutes, onPress, onLongPress, onDragEvent, onDragStart, showDragHandle }) {
371
+ const RenderEventComponent = renderEvent;
372
+ const theme = useCalendarTheme();
373
+ const draggable = onDragEvent != null && !positioned.event.disabled;
374
+ const resizable = draggable && !positioned.continuesAfter;
375
+ const moveOffset = useSharedValue(0);
376
+ const moveOffsetX = useSharedValue(0);
377
+ const resizeDelta = useSharedValue(0);
378
+ const boxHeight = useDerivedValue(() => Math.max(positioned.durationHours * cellHeight.value + resizeDelta.value, MIN_EVENT_HEIGHT), [positioned.durationHours]);
379
+ const boxStyle = useAnimatedStyle(() => ({
380
+ top: (positioned.startHours - minHour) * cellHeight.value + moveOffset.value,
381
+ height: boxHeight.value,
382
+ transform: [{ translateX: moveOffsetX.value }]
383
+ }), [
384
+ positioned.startHours,
385
+ positioned.durationHours,
386
+ minHour
387
+ ]);
388
+ useEffect(() => {
389
+ moveOffset.value = 0;
390
+ moveOffsetX.value = 0;
391
+ resizeDelta.value = 0;
392
+ }, [
393
+ positioned.startHours,
394
+ positioned.durationHours,
395
+ moveOffset,
396
+ moveOffsetX,
397
+ resizeDelta
398
+ ]);
399
+ const latest = useRef({
400
+ event: positioned.event,
401
+ onDragEvent,
402
+ onDragStart
403
+ });
404
+ latest.current = {
405
+ event: positioned.event,
406
+ onDragEvent,
407
+ onDragStart
408
+ };
409
+ const snapBack = useCallback(() => {
410
+ moveOffset.value = 0;
411
+ moveOffsetX.value = 0;
412
+ resizeDelta.value = 0;
413
+ }, [
414
+ moveOffset,
415
+ moveOffsetX,
416
+ resizeDelta
417
+ ]);
418
+ const commitDrag = useCallback((deltaStartMin, deltaEndMin) => {
419
+ const { event, onDragEvent: handler } = latest.current;
420
+ if (!handler) return;
421
+ const next = resolveDraggedBounds(event.start, event.end, deltaStartMin, deltaEndMin, snapMinutes);
422
+ if (!next) {
423
+ snapBack();
424
+ return;
425
+ }
426
+ if (handler(event, next.start, next.end) === false) snapBack();
427
+ }, [snapMinutes, snapBack]);
428
+ const notifyDragStart = useCallback(() => {
429
+ latest.current.onDragStart?.(latest.current.event);
430
+ }, []);
431
+ const moveGesture = useMemo(() => {
432
+ const pan = Gesture.Pan().enabled(draggable).onStart(() => {
433
+ runOnJS(notifyDragStart)();
434
+ }).onUpdate((event) => {
435
+ moveOffset.value = event.translationY;
436
+ moveOffsetX.value = event.translationX;
437
+ }).onEnd((event) => {
438
+ const minuteDelta = snapDeltaMinutes(event.translationY, cellHeight.value, snapMinutes);
439
+ const rawDayDelta = dayWidth > 0 ? Math.round(event.translationX / dayWidth) : 0;
440
+ const dayDelta = Math.min(Math.max(dayIndex + rawDayDelta, 0), dayCount - 1) - dayIndex;
441
+ if (minuteDelta === 0 && dayDelta === 0) {
442
+ moveOffset.value = 0;
443
+ moveOffsetX.value = 0;
444
+ return;
445
+ }
446
+ moveOffset.value = minuteDelta / MINUTES_PER_HOUR * cellHeight.value;
447
+ moveOffsetX.value = dayDelta * dayWidth;
448
+ const totalDelta = minuteDelta + dayDelta * MINUTES_PER_DAY;
449
+ runOnJS(commitDrag)(totalDelta, totalDelta);
450
+ });
451
+ return isWeb ? pan.activeOffsetX([-8, DRAG_ACTIVATE_PX]).activeOffsetY([-8, DRAG_ACTIVATE_PX]) : pan.activateAfterLongPress(MOVE_ACTIVATE_MS);
452
+ }, [
453
+ draggable,
454
+ snapMinutes,
455
+ cellHeight,
456
+ moveOffset,
457
+ moveOffsetX,
458
+ dayWidth,
459
+ dayIndex,
460
+ dayCount,
461
+ commitDrag,
462
+ notifyDragStart
463
+ ]);
464
+ const resizeGesture = useMemo(() => Gesture.Pan().enabled(resizable).onStart(() => {
465
+ runOnJS(notifyDragStart)();
466
+ }).onUpdate((event) => {
467
+ resizeDelta.value = event.translationY;
468
+ }).onEnd((event) => {
469
+ const delta = snapDeltaMinutes(event.translationY, cellHeight.value, snapMinutes);
470
+ if (delta === 0) {
471
+ resizeDelta.value = 0;
472
+ return;
473
+ }
474
+ resizeDelta.value = delta / MINUTES_PER_HOUR * cellHeight.value;
475
+ runOnJS(commitDrag)(0, delta);
476
+ }), [
477
+ resizable,
478
+ snapMinutes,
479
+ cellHeight,
480
+ resizeDelta,
481
+ commitDrag,
482
+ notifyDragStart
483
+ ]);
484
+ const handlePress = () => onPress(positioned.event);
485
+ const handleLongPress = !draggable && onLongPress ? () => onLongPress(positioned.event) : void 0;
486
+ const box = /* @__PURE__ */ jsxs(Animated.View, {
487
+ style: [
488
+ styles.eventBox,
489
+ {
490
+ left,
491
+ width
492
+ },
493
+ boxStyle
494
+ ],
495
+ children: [/* @__PURE__ */ jsx(RenderEventComponent, {
496
+ event: positioned.event,
497
+ mode,
498
+ boxHeight,
499
+ continuesBefore: positioned.continuesBefore,
500
+ continuesAfter: positioned.continuesAfter,
501
+ onPress: handlePress,
502
+ onLongPress: handleLongPress
503
+ }), resizable ? /* @__PURE__ */ jsx(GestureDetector, {
504
+ gesture: resizeGesture,
505
+ children: /* @__PURE__ */ jsx(Animated.View, {
506
+ style: styles.resizeHandle,
507
+ children: showDragHandle ? /* @__PURE__ */ jsx(View, { style: [styles.resizeGrip, { backgroundColor: theme.colors.eventText }] }) : null
508
+ })
509
+ }) : null]
510
+ });
511
+ if (!draggable) return box;
512
+ return /* @__PURE__ */ jsx(GestureDetector, {
513
+ gesture: moveGesture,
514
+ children: box
515
+ });
516
+ }
517
+ const HourRow = ({ hour, minHour, cellHeight, hourColumnWidth, label, ampm, hourComponent }) => {
518
+ const theme = useCalendarTheme();
519
+ const animatedStyle = useAnimatedStyle(() => ({ top: (hour - minHour) * cellHeight.value }), [hour, minHour]);
520
+ return /* @__PURE__ */ jsxs(Animated.View, {
521
+ style: [
522
+ styles.hourRow,
523
+ styles.nonInteractive,
524
+ animatedStyle
525
+ ],
526
+ children: [hourComponent ? /* @__PURE__ */ jsx(View, {
527
+ style: { width: hourColumnWidth },
528
+ children: hourComponent(hour, ampm)
529
+ }) : /* @__PURE__ */ jsx(Text, {
530
+ style: [
531
+ theme.text.hourLabel,
532
+ styles.hourLabel,
533
+ {
534
+ width: hourColumnWidth,
535
+ color: theme.colors.textMuted
536
+ }
537
+ ],
538
+ allowFontScaling: false,
539
+ children: label
540
+ }), /* @__PURE__ */ jsx(View, { style: [styles.hourLine, { backgroundColor: theme.colors.gridLine }] })]
541
+ });
542
+ };
543
+ const TimeslotLine = ({ hour, minHour, fraction, cellHeight, hourColumnWidth }) => {
544
+ const theme = useCalendarTheme();
545
+ const animatedStyle = useAnimatedStyle(() => ({ top: (hour - minHour + fraction) * cellHeight.value }), [
546
+ hour,
547
+ minHour,
548
+ fraction
549
+ ]);
550
+ return /* @__PURE__ */ jsx(Animated.View, { style: [
551
+ styles.timeslotLine,
552
+ styles.nonInteractive,
553
+ {
554
+ left: hourColumnWidth,
555
+ backgroundColor: theme.colors.gridLine
556
+ },
557
+ animatedStyle
558
+ ] });
559
+ };
560
+ const NowIndicator = ({ cellHeight, nowHours, minHour, left, width, color }) => {
561
+ const animatedStyle = useAnimatedStyle(() => ({ top: (nowHours - minHour) * cellHeight.value }), [nowHours, minHour]);
562
+ return /* @__PURE__ */ jsx(Animated.View, { style: [
563
+ styles.nowIndicator,
564
+ styles.nonInteractive,
565
+ {
566
+ left,
567
+ width,
568
+ backgroundColor: color
569
+ },
570
+ animatedStyle
571
+ ] });
572
+ };
573
+ const ShadeBand = ({ cellHeight, startHour, endHour, minHour, left, width, color }) => {
574
+ const animatedStyle = useAnimatedStyle(() => ({
575
+ top: (startHour - minHour) * cellHeight.value,
576
+ height: (endHour - startHour) * cellHeight.value
577
+ }), [
578
+ startHour,
579
+ endHour,
580
+ minHour
581
+ ]);
582
+ return /* @__PURE__ */ jsx(Animated.View, {
583
+ testID: "business-hours-shade",
584
+ style: [
585
+ styles.shadeBand,
586
+ styles.nonInteractive,
587
+ {
588
+ left,
589
+ width,
590
+ backgroundColor: color
591
+ },
592
+ animatedStyle
593
+ ]
594
+ });
595
+ };
596
+ function TimetablePageInner({ mode, numberOfDays, date, events, cellHeight, hourHeight, committedCellHeight, scrollY, isActive, initialScrollY, onSettleOffset, weekStartsOn, weekEndsOn, width, hourColumnWidth, minHour, maxHour, ampm, timeslots, isRTL, showAllDayEventCell, showVerticalScrollIndicator, verticalScrollEnabled, hourComponent, calendarCellStyle, minHourHeight, maxHourHeight, showNowIndicator, businessHours, renderEvent, keyExtractor, snapMinutes, showDragHandle, onPressEvent, onLongPressEvent, onDragEvent, onDragStart, onPressCell, onLongPressCell, onCreateEvent }) {
597
+ const theme = useCalendarTheme();
598
+ const scrollRef = useAnimatedRef();
599
+ const heightSource = isActive ? cellHeight : committedCellHeight;
600
+ const isDragging = useSharedValue(false);
601
+ const isActiveShared = useSharedValue(isActive);
602
+ useEffect(() => {
603
+ isActiveShared.value = isActive;
604
+ }, [isActive, isActiveShared]);
605
+ const scrollHandler = useAnimatedScrollHandler({
606
+ onScroll: (event) => {
607
+ if (isActiveShared.value && isDragging.value) scrollY.value = event.contentOffset.y;
608
+ },
609
+ onBeginDrag: () => {
610
+ isDragging.value = true;
611
+ },
612
+ onEndDrag: (event) => {
613
+ isDragging.value = false;
614
+ if (!isWeb && isActiveShared.value) {
615
+ scrollY.value = event.contentOffset.y;
616
+ runOnJS(onSettleOffset)(event.contentOffset.y);
617
+ }
618
+ },
619
+ onMomentumEnd: (event) => {
620
+ isDragging.value = false;
621
+ if (!isWeb && isActiveShared.value) {
622
+ scrollY.value = event.contentOffset.y;
623
+ runOnJS(onSettleOffset)(event.contentOffset.y);
624
+ }
625
+ }
626
+ });
627
+ useAnimatedReaction(() => scrollY.value, (current, previous) => {
628
+ if (!isActiveShared.value && current !== previous) scrollTo(scrollRef, 0, current, false);
629
+ });
630
+ useAnimatedReaction(() => isActiveShared.value, (active, previous) => {
631
+ if (isWeb || !active || active === previous) return;
632
+ scrollTo(scrollRef, 0, scrollY.value, false);
633
+ });
634
+ const days = useMemo(() => getViewDays$1(mode, date, weekStartsOn, numberOfDays, isRTL, weekEndsOn), [
635
+ mode,
636
+ date,
637
+ weekStartsOn,
638
+ numberOfDays,
639
+ isRTL,
640
+ weekEndsOn
641
+ ]);
642
+ const dayWidth = (width - hourColumnWidth) / days.length;
643
+ const dayLeft = (dayIndex) => hourColumnWidth + dayIndex * dayWidth;
644
+ const dayLayouts = useMemo(() => days.map((day) => layoutDayEvents$1(events, day)), [days, events]);
645
+ const cellDateFromTouch = (event) => {
646
+ const { locationX, locationY } = event.nativeEvent;
647
+ const day = days[days.length === 1 ? 0 : Math.floor(locationX / dayWidth)];
648
+ if (!day) return null;
649
+ const minutes = Math.round((minHour + locationY / heightSource.value) * MINUTES_PER_HOUR);
650
+ const pressed = new Date(day);
651
+ pressed.setHours(0, 0, 0, 0);
652
+ pressed.setMinutes(minutes);
653
+ return pressed;
654
+ };
655
+ const handleBackgroundPress = (event) => {
656
+ const date = onPressCell && cellDateFromTouch(event);
657
+ if (date) onPressCell?.(date);
658
+ };
659
+ const handleBackgroundLongPress = (event) => {
660
+ const date = onLongPressCell && cellDateFromTouch(event);
661
+ if (date) onLongPressCell?.(date);
662
+ };
663
+ const hoursRange = useMemo(() => Array.from({ length: maxHour - minHour }, (_, index) => minHour + index), [minHour, maxHour]);
664
+ const now = useNow(showNowIndicator && isActive);
665
+ const nowDayIndex = days.findIndex((day) => getIsToday$1(day));
666
+ const nowHours = (getHours(now) * MINUTES_PER_HOUR + getMinutes(now)) / MINUTES_PER_HOUR;
667
+ const nowInWindow = nowHours >= minHour && nowHours <= maxHour;
668
+ const fullHeightStyle = useAnimatedStyle(() => ({ height: (maxHour - minHour) * heightSource.value }), [
669
+ minHour,
670
+ maxHour,
671
+ heightSource
672
+ ]);
673
+ const pinchStartCellHeight = useSharedValue(hourHeight);
674
+ const zoomGesture = useMemo(() => {
675
+ const pinch = Gesture.Pinch().onStart(() => {
676
+ pinchStartCellHeight.value = cellHeight.value;
677
+ }).onUpdate((event) => {
678
+ cellHeight.value = Math.min(maxHourHeight, Math.max(minHourHeight, pinchStartCellHeight.value * event.scale));
679
+ }).onEnd(() => {
680
+ committedCellHeight.value = cellHeight.value;
681
+ });
682
+ return Gesture.Simultaneous(pinch, Gesture.Native());
683
+ }, [
684
+ cellHeight,
685
+ committedCellHeight,
686
+ pinchStartCellHeight,
687
+ minHourHeight,
688
+ maxHourHeight
689
+ ]);
690
+ const createEnabled = onCreateEvent != null;
691
+ const createActive = useSharedValue(0);
692
+ const createTop = useSharedValue(0);
693
+ const createHeight = useSharedValue(0);
694
+ const createLeft = useSharedValue(0);
695
+ const createWidth = useSharedValue(0);
696
+ const createStartY = useSharedValue(0);
697
+ const createDayIndex = useSharedValue(0);
698
+ const createCancelled = useSharedValue(0);
699
+ const commitCreate = useCallback((startY, endY, dayIndex) => {
700
+ const day = days[dayIndex];
701
+ if (!day) return;
702
+ const range = cellRangeFromDrag(day, startY, endY, heightSource.value, minHour, snapMinutes);
703
+ if (range) onCreateEvent?.(range.start, range.end);
704
+ }, [
705
+ days,
706
+ heightSource,
707
+ minHour,
708
+ snapMinutes,
709
+ onCreateEvent
710
+ ]);
711
+ const tapCell = useCallback((x, y) => {
712
+ const day = days[days.length === 1 ? 0 : Math.floor(x / dayWidth)];
713
+ if (!day) return;
714
+ const minutes = Math.round((minHour + y / heightSource.value) * MINUTES_PER_HOUR);
715
+ const pressed = new Date(day);
716
+ pressed.setHours(0, 0, 0, 0);
717
+ pressed.setMinutes(minutes);
718
+ onPressCell?.(pressed);
719
+ }, [
720
+ days,
721
+ dayWidth,
722
+ minHour,
723
+ heightSource,
724
+ onPressCell
725
+ ]);
726
+ const createGesture = useMemo(() => {
727
+ const pan = Gesture.Pan().enabled(createEnabled).onStart((event) => {
728
+ const idx = days.length === 1 ? 0 : Math.floor(event.x / dayWidth);
729
+ createDayIndex.value = idx;
730
+ createStartY.value = event.y;
731
+ createLeft.value = hourColumnWidth + idx * dayWidth;
732
+ createWidth.value = dayWidth;
733
+ createTop.value = event.y;
734
+ createHeight.value = 0;
735
+ createActive.value = 1;
736
+ createCancelled.value = 0;
737
+ }).onUpdate((event) => {
738
+ if (createCancelled.value) return;
739
+ const stepPx = snapMinutes / MINUTES_PER_HOUR * heightSource.value;
740
+ const snap = (y) => stepPx > 0 ? Math.round(y / stepPx) * stepPx : y;
741
+ const startSnap = snap(createStartY.value);
742
+ const endSnap = snap(createStartY.value + event.translationY);
743
+ createTop.value = Math.min(startSnap, endSnap);
744
+ createHeight.value = Math.max(Math.abs(endSnap - startSnap), stepPx);
745
+ }).onEnd((event) => {
746
+ createActive.value = 0;
747
+ createHeight.value = 0;
748
+ if (createCancelled.value) return;
749
+ runOnJS(commitCreate)(createStartY.value, createStartY.value + event.translationY, createDayIndex.value);
750
+ });
751
+ return isWeb ? pan.activeOffsetY([-8, DRAG_ACTIVATE_PX]) : pan.activateAfterLongPress(DRAG_ACTIVATE_MS);
752
+ }, [
753
+ createEnabled,
754
+ days.length,
755
+ dayWidth,
756
+ hourColumnWidth,
757
+ heightSource,
758
+ snapMinutes,
759
+ commitCreate,
760
+ createActive,
761
+ createTop,
762
+ createHeight,
763
+ createLeft,
764
+ createWidth,
765
+ createStartY,
766
+ createDayIndex,
767
+ createCancelled
768
+ ]);
769
+ const backgroundGesture = useMemo(() => {
770
+ const tap = isWeb && onPressCell != null ? Gesture.Tap().onEnd((event) => {
771
+ runOnJS(tapCell)(event.x, event.y);
772
+ }) : null;
773
+ if (createEnabled && tap) return Gesture.Exclusive(createGesture, tap);
774
+ if (createEnabled) return createGesture;
775
+ return tap;
776
+ }, [
777
+ createEnabled,
778
+ createGesture,
779
+ onPressCell,
780
+ tapCell
781
+ ]);
782
+ useEffect(() => {
783
+ if (!isWeb || !createEnabled) return;
784
+ const doc = globalThis.document;
785
+ if (!doc) return;
786
+ const handler = (event) => {
787
+ if (event.key !== "Escape" || !createActive.value) return;
788
+ createCancelled.value = 1;
789
+ createActive.value = 0;
790
+ createHeight.value = 0;
791
+ };
792
+ doc.addEventListener("keydown", handler);
793
+ return () => doc.removeEventListener("keydown", handler);
794
+ }, [
795
+ createEnabled,
796
+ createActive,
797
+ createCancelled,
798
+ createHeight
799
+ ]);
800
+ const createGhostStyle = useAnimatedStyle(() => ({
801
+ top: createTop.value,
802
+ height: createHeight.value,
803
+ left: createLeft.value,
804
+ width: createWidth.value,
805
+ opacity: createActive.value
806
+ }));
807
+ const cellLayer = onPressCell || onLongPressCell || createEnabled ? /* @__PURE__ */ jsx(Pressable, {
808
+ style: [styles.cellPressLayer, { left: hourColumnWidth }],
809
+ onPress: onPressCell ? handleBackgroundPress : void 0,
810
+ onLongPress: !createEnabled && onLongPressCell ? handleBackgroundLongPress : void 0,
811
+ tabIndex: -1,
812
+ importantForAccessibility: "no",
813
+ accessibilityElementsHidden: true
814
+ }) : null;
815
+ return /* @__PURE__ */ jsxs(View, {
816
+ style: styles.container,
817
+ children: [showAllDayEventCell ? /* @__PURE__ */ jsx(AllDayLane, {
818
+ days,
819
+ events,
820
+ mode,
821
+ hourColumnWidth,
822
+ dayWidth,
823
+ renderEvent,
824
+ keyExtractor,
825
+ onPressEvent,
826
+ onLongPressEvent
827
+ }) : null, /* @__PURE__ */ jsx(GestureDetector, {
828
+ gesture: zoomGesture,
829
+ children: /* @__PURE__ */ jsx(Animated.ScrollView, {
830
+ ref: scrollRef,
831
+ showsVerticalScrollIndicator: showVerticalScrollIndicator,
832
+ scrollEnabled: verticalScrollEnabled,
833
+ onScroll: scrollHandler,
834
+ scrollEventThrottle: 16,
835
+ contentContainerStyle: { paddingTop: HOUR_LABEL_TOP_INSET },
836
+ contentOffset: {
837
+ x: 0,
838
+ y: initialScrollY
839
+ },
840
+ children: /* @__PURE__ */ jsxs(Animated.View, {
841
+ style: [styles.content, fullHeightStyle],
842
+ children: [
843
+ cellLayer && backgroundGesture ? /* @__PURE__ */ jsx(GestureDetector, {
844
+ gesture: backgroundGesture,
845
+ children: cellLayer
846
+ }) : cellLayer,
847
+ days.map((day, dayIndex) => isWeekend$1(day) ? /* @__PURE__ */ jsx(Animated.View, { style: [
848
+ styles.weekendColumn,
849
+ styles.nonInteractive,
850
+ { backgroundColor: theme.colors.weekendBackground },
851
+ {
852
+ left: dayLeft(dayIndex),
853
+ width: dayWidth
854
+ },
855
+ fullHeightStyle
856
+ ] }, `weekend-${day.toISOString()}`) : null),
857
+ calendarCellStyle ? days.map((day, dayIndex) => {
858
+ const cellStyle = calendarCellStyle(day);
859
+ return cellStyle ? /* @__PURE__ */ jsx(Animated.View, { style: [
860
+ styles.weekendColumn,
861
+ styles.nonInteractive,
862
+ {
863
+ left: dayLeft(dayIndex),
864
+ width: dayWidth
865
+ },
866
+ cellStyle,
867
+ fullHeightStyle
868
+ ] }, `cell-${day.toISOString()}`) : null;
869
+ }) : null,
870
+ businessHours ? days.flatMap((day, dayIndex) => closedHourBands(day, businessHours, minHour, maxHour).map((band, bandIndex) => /* @__PURE__ */ jsx(ShadeBand, {
871
+ cellHeight: heightSource,
872
+ startHour: band.start,
873
+ endHour: band.end,
874
+ minHour,
875
+ left: dayLeft(dayIndex),
876
+ width: dayWidth,
877
+ color: theme.colors.outsideHoursBackground
878
+ }, `closed-${day.toISOString()}-${bandIndex}`))) : null,
879
+ days.map((day, dayIndex) => /* @__PURE__ */ jsx(Animated.View, { style: [
880
+ styles.daySeparator,
881
+ styles.nonInteractive,
882
+ { backgroundColor: theme.colors.gridLine },
883
+ { left: dayLeft(dayIndex) },
884
+ fullHeightStyle
885
+ ] }, `separator-${day.toISOString()}`)),
886
+ hoursRange.map((hour) => /* @__PURE__ */ jsx(HourRow, {
887
+ hour,
888
+ minHour,
889
+ cellHeight: heightSource,
890
+ hourColumnWidth,
891
+ label: formatHour(hour, { ampm }),
892
+ ampm,
893
+ hourComponent
894
+ }, hour)),
895
+ timeslots > 1 ? hoursRange.flatMap((hour) => Array.from({ length: timeslots - 1 }, (_, i) => /* @__PURE__ */ jsx(TimeslotLine, {
896
+ hour,
897
+ minHour,
898
+ fraction: (i + 1) / timeslots,
899
+ cellHeight: heightSource,
900
+ hourColumnWidth
901
+ }, `slot-${hour}-${i}`))) : null,
902
+ dayLayouts.flatMap((layout, dayIndex) => layout.filter((p) => p.startHours < maxHour && p.startHours + p.durationHours > minHour).map((positioned, eventIndex) => {
903
+ const columnWidth = dayWidth / positioned.columns;
904
+ return /* @__PURE__ */ jsx(AnimatedEventBox, {
905
+ positioned,
906
+ cellHeight: heightSource,
907
+ minHour,
908
+ left: dayLeft(dayIndex) + positioned.column * columnWidth,
909
+ width: columnWidth,
910
+ dayWidth,
911
+ dayIndex,
912
+ dayCount: days.length,
913
+ mode,
914
+ renderEvent,
915
+ snapMinutes,
916
+ showDragHandle,
917
+ onPress: onPressEvent,
918
+ onLongPress: onLongPressEvent,
919
+ onDragEvent,
920
+ onDragStart
921
+ }, `${dayIndex}:${keyExtractor(positioned.event, eventIndex)}`);
922
+ })),
923
+ showNowIndicator && nowDayIndex >= 0 && nowInWindow ? /* @__PURE__ */ jsx(NowIndicator, {
924
+ cellHeight: heightSource,
925
+ nowHours,
926
+ minHour,
927
+ left: dayLeft(nowDayIndex),
928
+ width: dayWidth,
929
+ color: theme.colors.nowIndicator
930
+ }) : null,
931
+ createEnabled ? /* @__PURE__ */ jsx(Animated.View, { style: [
932
+ styles.createGhost,
933
+ { pointerEvents: "none" },
934
+ {
935
+ backgroundColor: theme.colors.eventBackground,
936
+ borderColor: theme.colors.todayBackground
937
+ },
938
+ createGhostStyle
939
+ ] }) : null
940
+ ]
941
+ })
942
+ })
943
+ })]
944
+ });
945
+ }
946
+ const TimetablePage = memo(TimetablePageInner);
947
+ function TimeGridInner({ mode, numberOfDays = 1, weekEndsOn, date, events, cellHeight, hourHeight = 48, weekStartsOn, renderEvent, keyExtractor, scrollOffsetMinutes = 0, hourColumnWidth: hourColumnWidthProp = DEFAULT_HOUR_COLUMN_WIDTH, hideHours = false, timeslots = 1, showAllDayEventCell = true, calendarCellStyle, businessHours, showWeekNumber = false, headerComponent, minHour = 0, maxHour = HOURS_PER_DAY, ampm = false, isRTL = false, minHourHeight = DEFAULT_MIN_HOUR_HEIGHT, maxHourHeight = DEFAULT_MAX_HOUR_HEIGHT, showNowIndicator = true, locale, freeSwipe = false, swipeEnabled = true, showVerticalScrollIndicator = true, verticalScrollEnabled = true, weekNumberPrefix = "W", hourComponent, activeDate, resetPageOnPressCell = false, dragStepMinutes = DEFAULT_DRAG_STEP_MINUTES, showDragHandle = true, onPressEvent, onLongPressEvent, onDragEvent, onDragStart, onPressCell, onLongPressCell, onCreateEvent, onPressDateHeader, onChangeDate, renderHeader }) {
948
+ const clampedMinHour = Math.max(0, Math.min(minHour, HOURS_PER_DAY - 1));
949
+ const clampedMaxHour = Math.max(clampedMinHour + 1, Math.min(maxHour, HOURS_PER_DAY));
950
+ const hourColumnWidth = hideHours ? 0 : hourColumnWidthProp;
951
+ const { width, height } = useWindowDimensions();
952
+ const listRef = useRef(null);
953
+ const containerRef = useRef(null);
954
+ const suppressCaptureUntilRef = useRef(0);
955
+ const [pageHeight, setPageHeight] = useState(height);
956
+ const [containerWidth, setContainerWidth] = useState(width);
957
+ const [measured, setMeasured] = useState(false);
958
+ const weekAnchored = mode === "week" || mode === "custom" && weekEndsOn != null;
959
+ const step = weekAnchored ? 7 : viewDayCount(mode, numberOfDays);
960
+ const seedDefaultY = Math.max(0, scrollOffsetMinutes / MINUTES_PER_HOUR - clampedMinHour) * hourHeight;
961
+ const scrollY = useSharedValue(seedDefaultY);
962
+ const offsetSeedRef = useRef(null);
963
+ const captureOffsetSeed = useCallback((y) => {
964
+ offsetSeedRef.current = y;
965
+ }, []);
966
+ const committedCellHeight = useSharedValue(hourHeight);
967
+ useWebGridZoom(isWeb, containerRef, cellHeight, committedCellHeight, minHourHeight, maxHourHeight);
968
+ const [anchorDate] = useState(date);
969
+ const anchor = useMemo(() => weekAnchored ? startOfWeek(anchorDate, { weekStartsOn }) : startOfDay(anchorDate), [
970
+ weekAnchored,
971
+ anchorDate,
972
+ weekStartsOn
973
+ ]);
974
+ const pageDates = useMemo(() => Array.from({ length: 361 }, (_, i) => addDays(anchor, (i - PAGE_WINDOW) * step)), [anchor, step]);
975
+ const activeIndex = useCallback((target) => {
976
+ const aligned = weekAnchored ? startOfWeek(target, { weekStartsOn }) : startOfDay(target);
977
+ return Math.floor(differenceInCalendarDays(aligned, anchor) / step) + PAGE_WINDOW;
978
+ }, [
979
+ anchor,
980
+ weekAnchored,
981
+ step,
982
+ weekStartsOn
983
+ ])(date);
984
+ const viewedIndexRef = useRef(activeIndex);
985
+ const pendingScrollIndexRef = useRef(null);
986
+ const headerDays = useMemo(() => getViewDays$1(mode, pageDates[activeIndex] ?? date, weekStartsOn, numberOfDays, isRTL, weekEndsOn), [
987
+ mode,
988
+ pageDates,
989
+ activeIndex,
990
+ date,
991
+ weekStartsOn,
992
+ numberOfDays,
993
+ isRTL,
994
+ weekEndsOn
995
+ ]);
996
+ const handleViewableItemsChanged = useCallback((info) => {
997
+ if (isWeb) return;
998
+ const settled = info.viewableItems.find((token) => token.isViewable);
999
+ if (settled?.index == null) return;
1000
+ if (pendingScrollIndexRef.current != null) {
1001
+ if (settled.index === pendingScrollIndexRef.current) {
1002
+ pendingScrollIndexRef.current = null;
1003
+ viewedIndexRef.current = settled.index;
1004
+ }
1005
+ return;
1006
+ }
1007
+ if (settled.index === viewedIndexRef.current) return;
1008
+ viewedIndexRef.current = settled.index;
1009
+ if (settled.item) onChangeDate(settled.item);
1010
+ }, [onChangeDate]);
1011
+ useEffect(() => {
1012
+ if (activeIndex === viewedIndexRef.current) return;
1013
+ viewedIndexRef.current = activeIndex;
1014
+ pendingScrollIndexRef.current = activeIndex;
1015
+ listRef.current?.scrollToIndex({
1016
+ index: activeIndex,
1017
+ animated: false
1018
+ });
1019
+ }, [activeIndex]);
1020
+ useEffect(() => {
1021
+ if (!isWeb) return;
1022
+ const root = containerRef.current;
1023
+ if (!root) return;
1024
+ const restoreVisiblePage = () => {
1025
+ const vw = (root.ownerDocument?.defaultView ?? globalThis).innerWidth;
1026
+ for (const el of root.querySelectorAll("*")) {
1027
+ const style = getComputedStyle(el);
1028
+ if (!((style.overflowY === "scroll" || style.overflowY === "auto") && el.scrollHeight > el.clientHeight + 20 && el.clientHeight > 100)) continue;
1029
+ const rect = el.getBoundingClientRect();
1030
+ if (rect.left > -50 && rect.right <= vw + 50) {
1031
+ el.scrollTop = scrollY.value;
1032
+ break;
1033
+ }
1034
+ }
1035
+ };
1036
+ suppressCaptureUntilRef.current = Date.now() + 400;
1037
+ let raf2 = 0;
1038
+ const raf1 = requestAnimationFrame(() => {
1039
+ restoreVisiblePage();
1040
+ raf2 = requestAnimationFrame(restoreVisiblePage);
1041
+ });
1042
+ return () => {
1043
+ cancelAnimationFrame(raf1);
1044
+ cancelAnimationFrame(raf2);
1045
+ };
1046
+ }, [activeIndex, pageHeight]);
1047
+ useEffect(() => {
1048
+ if (!isWeb) return;
1049
+ const root = containerRef.current;
1050
+ if (!root) return;
1051
+ const onScrollCapture = (event) => {
1052
+ if (Date.now() < suppressCaptureUntilRef.current) return;
1053
+ const el = event.target;
1054
+ if (!el || typeof el.scrollTop !== "number" || el.clientHeight <= 100) return;
1055
+ const rect = el.getBoundingClientRect();
1056
+ const vw = (root.ownerDocument?.defaultView ?? globalThis).innerWidth;
1057
+ if (rect.left <= -50 || rect.right > vw + 50) return;
1058
+ scrollY.value = el.scrollTop;
1059
+ offsetSeedRef.current = el.scrollTop;
1060
+ };
1061
+ root.addEventListener("scroll", onScrollCapture, true);
1062
+ return () => root.removeEventListener("scroll", onScrollCapture, true);
1063
+ }, []);
1064
+ useEffect(() => {
1065
+ if (!isWeb) return;
1066
+ const root = containerRef.current;
1067
+ if (!root) return;
1068
+ const lockHorizontal = () => {
1069
+ for (const el of root.querySelectorAll("*")) {
1070
+ if (el.scrollWidth <= el.clientWidth + 20 || el.clientWidth <= 100) continue;
1071
+ const overflowX = getComputedStyle(el).overflowX;
1072
+ if (overflowX === "auto" || overflowX === "scroll") {
1073
+ el.style.overflowX = "hidden";
1074
+ el.style.touchAction = "pan-y";
1075
+ }
1076
+ }
1077
+ };
1078
+ const raf = requestAnimationFrame(lockHorizontal);
1079
+ return () => cancelAnimationFrame(raf);
1080
+ }, [pageHeight]);
1081
+ useWebPagerKeys(swipeEnabled, useCallback((delta) => {
1082
+ const target = pageDates[activeIndex + delta];
1083
+ if (target) onChangeDate(target);
1084
+ }, [
1085
+ pageDates,
1086
+ activeIndex,
1087
+ onChangeDate
1088
+ ]));
1089
+ const handlePressCell = useMemo(() => {
1090
+ if (!onPressCell) return void 0;
1091
+ if (!resetPageOnPressCell) return onPressCell;
1092
+ return (cellDate) => {
1093
+ onPressCell(cellDate);
1094
+ listRef.current?.scrollToIndex({
1095
+ index: activeIndex,
1096
+ animated: true
1097
+ });
1098
+ };
1099
+ }, [
1100
+ onPressCell,
1101
+ resetPageOnPressCell,
1102
+ activeIndex
1103
+ ]);
1104
+ const snapToIndices = useMemo(() => pageDates.map((_, index) => index), [pageDates]);
1105
+ const keyExtractorList = useCallback((item) => item.toISOString(), []);
1106
+ const getFixedItemSize = useCallback(() => containerWidth, [containerWidth]);
1107
+ const renderItem = useCallback(({ item, index }) => /* @__PURE__ */ jsx(View, {
1108
+ style: {
1109
+ width: containerWidth,
1110
+ height: pageHeight
1111
+ },
1112
+ children: /* @__PURE__ */ jsx(TimetablePage, {
1113
+ mode,
1114
+ numberOfDays,
1115
+ date: item,
1116
+ width: containerWidth,
1117
+ events,
1118
+ cellHeight,
1119
+ hourHeight,
1120
+ committedCellHeight,
1121
+ scrollY,
1122
+ isActive: index === activeIndex,
1123
+ initialScrollY: offsetSeedRef.current ?? seedDefaultY,
1124
+ onSettleOffset: captureOffsetSeed,
1125
+ weekStartsOn,
1126
+ weekEndsOn,
1127
+ hourColumnWidth,
1128
+ minHour: clampedMinHour,
1129
+ maxHour: clampedMaxHour,
1130
+ ampm,
1131
+ timeslots,
1132
+ isRTL,
1133
+ showAllDayEventCell,
1134
+ showVerticalScrollIndicator,
1135
+ verticalScrollEnabled,
1136
+ hourComponent,
1137
+ calendarCellStyle,
1138
+ businessHours,
1139
+ minHourHeight,
1140
+ maxHourHeight,
1141
+ showNowIndicator,
1142
+ renderEvent,
1143
+ keyExtractor,
1144
+ snapMinutes: Math.max(1, dragStepMinutes),
1145
+ showDragHandle,
1146
+ onPressEvent,
1147
+ onLongPressEvent,
1148
+ onDragEvent,
1149
+ onDragStart,
1150
+ onPressCell: handlePressCell,
1151
+ onLongPressCell,
1152
+ onCreateEvent
1153
+ })
1154
+ }), [
1155
+ containerWidth,
1156
+ pageHeight,
1157
+ mode,
1158
+ numberOfDays,
1159
+ events,
1160
+ cellHeight,
1161
+ hourHeight,
1162
+ committedCellHeight,
1163
+ scrollY,
1164
+ activeIndex,
1165
+ seedDefaultY,
1166
+ captureOffsetSeed,
1167
+ weekStartsOn,
1168
+ weekEndsOn,
1169
+ hourColumnWidth,
1170
+ clampedMinHour,
1171
+ clampedMaxHour,
1172
+ ampm,
1173
+ timeslots,
1174
+ isRTL,
1175
+ showAllDayEventCell,
1176
+ showVerticalScrollIndicator,
1177
+ verticalScrollEnabled,
1178
+ hourComponent,
1179
+ calendarCellStyle,
1180
+ businessHours,
1181
+ minHourHeight,
1182
+ maxHourHeight,
1183
+ showNowIndicator,
1184
+ renderEvent,
1185
+ keyExtractor,
1186
+ dragStepMinutes,
1187
+ showDragHandle,
1188
+ onPressEvent,
1189
+ onLongPressEvent,
1190
+ onDragEvent,
1191
+ onDragStart,
1192
+ handlePressCell,
1193
+ onLongPressCell,
1194
+ onCreateEvent
1195
+ ]);
1196
+ const listExtraData = useMemo(() => ({
1197
+ events,
1198
+ activeIndex
1199
+ }), [events, activeIndex]);
1200
+ return /* @__PURE__ */ jsxs(View, {
1201
+ ref: containerRef,
1202
+ style: styles.container,
1203
+ children: [
1204
+ renderHeader ? renderHeader(headerDays) : /* @__PURE__ */ jsx(DefaultHeader, {
1205
+ days: headerDays,
1206
+ mode,
1207
+ width: containerWidth,
1208
+ hourColumnWidth,
1209
+ showWeekNumber,
1210
+ weekNumberPrefix,
1211
+ locale,
1212
+ activeDate,
1213
+ onPressDateHeader
1214
+ }),
1215
+ headerComponent,
1216
+ /* @__PURE__ */ jsx(View, {
1217
+ style: styles.pager,
1218
+ onLayout: (event) => {
1219
+ setPageHeight(event.nativeEvent.layout.height);
1220
+ setContainerWidth(event.nativeEvent.layout.width);
1221
+ setMeasured(true);
1222
+ },
1223
+ children: /* @__PURE__ */ jsx(LegendList, {
1224
+ ref: listRef,
1225
+ style: isWeb ? [styles.pagerList, styles.webNoScroll] : styles.pagerList,
1226
+ data: pageDates,
1227
+ extraData: listExtraData,
1228
+ horizontal: true,
1229
+ recycleItems: false,
1230
+ keyExtractor: keyExtractorList,
1231
+ getFixedItemSize,
1232
+ ...isWeb ? null : {
1233
+ scrollEnabled: swipeEnabled,
1234
+ pagingEnabled: !freeSwipe,
1235
+ snapToIndices: freeSwipe ? snapToIndices : void 0
1236
+ },
1237
+ initialScrollIndex: activeIndex,
1238
+ showsHorizontalScrollIndicator: false,
1239
+ viewabilityConfig: PAGE_VIEWABILITY,
1240
+ onViewableItemsChanged: handleViewableItemsChanged,
1241
+ renderItem
1242
+ }, measured ? "grid" : "grid-seed")
1243
+ })
1244
+ ]
1245
+ });
1246
+ }
1247
+ const TimeGrid = memo(TimeGridInner);
1248
+ const DefaultHeader = ({ days, mode, width, hourColumnWidth, showWeekNumber, weekNumberPrefix = "W", locale, activeDate, onPressDateHeader }) => {
1249
+ const theme = useCalendarTheme();
1250
+ const dayWidth = (width - hourColumnWidth) / days.length;
1251
+ return /* @__PURE__ */ jsxs(View, {
1252
+ style: [styles.headerRow, { borderBottomColor: theme.colors.gridLine }],
1253
+ children: [/* @__PURE__ */ jsx(View, {
1254
+ style: [styles.weekNumberGutter, { width: hourColumnWidth }],
1255
+ children: showWeekNumber && hourColumnWidth > 0 && days[0] ? /* @__PURE__ */ jsx(Text, {
1256
+ style: [theme.text.hourLabel, { color: theme.colors.textMuted }],
1257
+ allowFontScaling: false,
1258
+ children: `${weekNumberPrefix}${getISOWeek(days[0])}`
1259
+ }) : null
1260
+ }), days.map((day) => /* @__PURE__ */ jsx(DayHeader, {
1261
+ day,
1262
+ mode,
1263
+ width: dayWidth,
1264
+ locale,
1265
+ activeDate,
1266
+ onPressDateHeader
1267
+ }, day.toISOString()))]
1268
+ });
1269
+ };
1270
+ const DayHeader = ({ day, width, locale, activeDate, onPressDateHeader }) => {
1271
+ const theme = useCalendarTheme();
1272
+ const isToday = getIsToday$1(day);
1273
+ const isHighlighted = activeDate ? isSameCalendarDay$1(day, activeDate) : isToday;
1274
+ return /* @__PURE__ */ jsxs(Pressable, {
1275
+ style: [styles.dayHeader, { width }],
1276
+ onPress: onPressDateHeader ? () => onPressDateHeader(day) : void 0,
1277
+ disabled: !onPressDateHeader,
1278
+ accessibilityRole: onPressDateHeader ? "button" : void 0,
1279
+ children: [/* @__PURE__ */ jsx(Text, {
1280
+ style: [styles.dayHeaderWeekday, { color: theme.colors.textMuted }],
1281
+ allowFontScaling: false,
1282
+ children: format(day, "EEE", { locale })
1283
+ }), /* @__PURE__ */ jsx(View, {
1284
+ style: [styles.dayHeaderBadge, isHighlighted && { backgroundColor: theme.colors.todayBackground }],
1285
+ children: /* @__PURE__ */ jsx(Text, {
1286
+ style: [styles.dayHeaderNumber, { color: isHighlighted ? theme.colors.todayText : theme.colors.text }],
1287
+ allowFontScaling: false,
1288
+ ...isToday && { accessibilityLabel: `Today, ${day.getDate()}` },
1289
+ children: day.getDate()
1290
+ })
1291
+ })]
1292
+ });
1293
+ };
1294
+ const styles = StyleSheet.create({
1295
+ pager: { flex: 1 },
1296
+ pagerList: { flex: 1 },
1297
+ container: { flex: 1 },
1298
+ headerRow: {
1299
+ flexDirection: "row",
1300
+ alignItems: "center",
1301
+ borderBottomWidth: StyleSheet.hairlineWidth
1302
+ },
1303
+ weekNumberGutter: {
1304
+ alignItems: "center",
1305
+ justifyContent: "flex-end"
1306
+ },
1307
+ dayHeader: {
1308
+ alignItems: "center",
1309
+ gap: 2,
1310
+ paddingVertical: 6
1311
+ },
1312
+ dayHeaderWeekday: {
1313
+ fontSize: 11,
1314
+ fontWeight: "600"
1315
+ },
1316
+ dayHeaderBadge: {
1317
+ width: 28,
1318
+ height: 28,
1319
+ borderRadius: 999,
1320
+ justifyContent: "center",
1321
+ alignItems: "center"
1322
+ },
1323
+ dayHeaderNumber: {
1324
+ fontSize: 15,
1325
+ fontWeight: "600"
1326
+ },
1327
+ content: {
1328
+ width: "100%",
1329
+ position: "relative"
1330
+ },
1331
+ cellPressLayer: {
1332
+ position: "absolute",
1333
+ top: 0,
1334
+ bottom: 0,
1335
+ right: 0
1336
+ },
1337
+ createGhost: {
1338
+ position: "absolute",
1339
+ borderRadius: 6,
1340
+ borderWidth: StyleSheet.hairlineWidth,
1341
+ marginHorizontal: EVENT_GAP
1342
+ },
1343
+ weekendColumn: {
1344
+ position: "absolute",
1345
+ top: 0
1346
+ },
1347
+ shadeBand: { position: "absolute" },
1348
+ daySeparator: {
1349
+ position: "absolute",
1350
+ top: 0,
1351
+ width: StyleSheet.hairlineWidth
1352
+ },
1353
+ hourRow: {
1354
+ position: "absolute",
1355
+ left: 0,
1356
+ right: 0,
1357
+ flexDirection: "row",
1358
+ alignItems: "flex-start"
1359
+ },
1360
+ hourLabel: {
1361
+ marginTop: -6,
1362
+ textAlign: "right",
1363
+ paddingRight: 6
1364
+ },
1365
+ hourLine: {
1366
+ flex: 1,
1367
+ height: StyleSheet.hairlineWidth
1368
+ },
1369
+ timeslotLine: {
1370
+ position: "absolute",
1371
+ right: 0,
1372
+ height: StyleSheet.hairlineWidth,
1373
+ opacity: .5
1374
+ },
1375
+ resizeHandle: {
1376
+ position: "absolute",
1377
+ left: 0,
1378
+ right: 0,
1379
+ bottom: 0,
1380
+ height: RESIZE_HANDLE_HEIGHT,
1381
+ alignItems: "center",
1382
+ justifyContent: "center"
1383
+ },
1384
+ resizeGrip: {
1385
+ width: 24,
1386
+ height: 3,
1387
+ borderRadius: 2,
1388
+ opacity: .4
1389
+ },
1390
+ eventBox: {
1391
+ position: "absolute",
1392
+ overflow: "hidden",
1393
+ padding: EVENT_GAP
1394
+ },
1395
+ nowIndicator: {
1396
+ position: "absolute",
1397
+ height: 2
1398
+ },
1399
+ nonInteractive: { pointerEvents: "none" },
1400
+ webNoScroll: { overflow: "hidden" }
1401
+ });
1402
+ //#endregion
1403
+ //#region src/components/Calendar.tsx
1404
+ const defaultKeyExtractor = (event) => `${event.start.toISOString()}|${event.end.toISOString()}|${event.title ?? ""}`;
1405
+ function visibleRange(mode, date, weekStartsOn, numberOfDays, weekEndsOn) {
1406
+ if (mode === "month") return [startOfWeek(startOfMonth(date), { weekStartsOn }), endOfWeek(endOfMonth(date), { weekStartsOn })];
1407
+ const days = getViewDays$1(mode, date, weekStartsOn, numberOfDays, false, weekEndsOn);
1408
+ return [startOfDay(days[0]), endOfDay(days[days.length - 1])];
1409
+ }
1410
+ function Calendar({ events, mode, date, onChangeDate, onChangeDateRange, onPressEvent, onLongPressEvent, onDragEvent, onDragStart, dragStepMinutes, showDragHandle, onPressDay, onLongPressDay, onPressMore, onPressCell, resetPageOnPressCell, onLongPressCell, onCreateEvent, onPressDateHeader, maxVisibleEventCount, sortedMonthView, moreLabel, showAdjacentMonths, disableMonthEventCellPress, weekStartsOn = 0, numberOfDays, weekEndsOn, renderEvent = DefaultEvent, eventCellStyle, calendarCellStyle, businessHours, keyExtractor = defaultKeyExtractor, theme, cellHeight: cellHeightProp, hourHeight = 48, minHourHeight, maxHourHeight, hourColumnWidth, hideHours, timeslots, showAllDayEventCell, showWeekNumber, weekNumberPrefix, hourComponent, showSixWeeks, swipeEnabled, showVerticalScrollIndicator, verticalScrollEnabled, headerComponent, minHour, maxHour, ampm, showTime, ellipsizeTitle, allDayLabel, scrollOffsetMinutes, showNowIndicator, locale, activeDate, isRTL, freeSwipe, renderTimeGridHeader, renderHeaderForMonthView, renderCustomDateForMonth, itemSeparatorComponent }) {
1411
+ const mergedTheme = useMemo(() => mergeTheme(theme), [theme]);
1412
+ const internalCellHeight = useSharedValue(hourHeight);
1413
+ const cellHeight = cellHeightProp ?? internalCellHeight;
1414
+ const handlePressEvent = useCallback((event) => {
1415
+ if (!event.disabled) onPressEvent(event);
1416
+ }, [onPressEvent]);
1417
+ const handleLongPressEvent = useMemo(() => onLongPressEvent ? (event) => {
1418
+ if (!event.disabled) onLongPressEvent(event);
1419
+ } : void 0, [onLongPressEvent]);
1420
+ const handleChangeDate = useCallback((next) => {
1421
+ onChangeDate(next);
1422
+ onChangeDateRange?.(visibleRange(mode, next, weekStartsOn, numberOfDays ?? 1, weekEndsOn));
1423
+ }, [
1424
+ onChangeDate,
1425
+ onChangeDateRange,
1426
+ mode,
1427
+ weekStartsOn,
1428
+ numberOfDays,
1429
+ weekEndsOn
1430
+ ]);
1431
+ const resolvedRenderEvent = useMemo(() => {
1432
+ if (eventCellStyle == null && ampm == null && showTime == null && ellipsizeTitle == null && allDayLabel == null) return renderEvent;
1433
+ const Base = renderEvent;
1434
+ return function StyledEvent(props) {
1435
+ const cellStyle = typeof eventCellStyle === "function" ? eventCellStyle(props.event) : eventCellStyle;
1436
+ return /* @__PURE__ */ jsx(Base, {
1437
+ ...props,
1438
+ cellStyle,
1439
+ ampm,
1440
+ showTime,
1441
+ ellipsizeTitle,
1442
+ allDayLabel
1443
+ });
1444
+ };
1445
+ }, [
1446
+ renderEvent,
1447
+ eventCellStyle,
1448
+ ampm,
1449
+ showTime,
1450
+ ellipsizeTitle,
1451
+ allDayLabel
1452
+ ]);
1453
+ return /* @__PURE__ */ jsx(CalendarThemeProvider, {
1454
+ value: mergedTheme,
1455
+ children: mode === "month" ? /* @__PURE__ */ jsx(MonthPager, {
1456
+ date,
1457
+ events,
1458
+ maxVisibleEventCount,
1459
+ weekStartsOn,
1460
+ locale,
1461
+ sortedMonthView,
1462
+ moreLabel,
1463
+ showAdjacentMonths,
1464
+ disableMonthEventCellPress,
1465
+ isRTL,
1466
+ showSixWeeks,
1467
+ activeDate,
1468
+ renderHeaderForMonthView,
1469
+ renderCustomDateForMonth,
1470
+ calendarCellStyle,
1471
+ renderEvent: resolvedRenderEvent,
1472
+ keyExtractor,
1473
+ onPressDay,
1474
+ onLongPressDay,
1475
+ onPressEvent: handlePressEvent,
1476
+ onLongPressEvent: handleLongPressEvent,
1477
+ onPressMore,
1478
+ onChangeDate: handleChangeDate,
1479
+ freeSwipe,
1480
+ swipeEnabled
1481
+ }) : mode === "schedule" ? /* @__PURE__ */ jsx(Agenda, {
1482
+ events,
1483
+ locale,
1484
+ renderEvent: resolvedRenderEvent,
1485
+ keyExtractor,
1486
+ onPressEvent: handlePressEvent,
1487
+ onLongPressEvent: handleLongPressEvent,
1488
+ onPressDay,
1489
+ activeDate,
1490
+ itemSeparatorComponent
1491
+ }) : /* @__PURE__ */ jsx(TimeGrid, {
1492
+ mode,
1493
+ numberOfDays,
1494
+ weekEndsOn,
1495
+ date,
1496
+ events,
1497
+ cellHeight,
1498
+ hourHeight,
1499
+ weekStartsOn,
1500
+ renderEvent: resolvedRenderEvent,
1501
+ keyExtractor,
1502
+ scrollOffsetMinutes,
1503
+ hourColumnWidth,
1504
+ hideHours,
1505
+ timeslots,
1506
+ showAllDayEventCell,
1507
+ calendarCellStyle,
1508
+ businessHours,
1509
+ showWeekNumber,
1510
+ weekNumberPrefix,
1511
+ hourComponent,
1512
+ showVerticalScrollIndicator,
1513
+ verticalScrollEnabled,
1514
+ headerComponent,
1515
+ minHour,
1516
+ maxHour,
1517
+ ampm,
1518
+ minHourHeight,
1519
+ maxHourHeight,
1520
+ showNowIndicator,
1521
+ locale,
1522
+ activeDate,
1523
+ isRTL,
1524
+ freeSwipe,
1525
+ swipeEnabled,
1526
+ onPressEvent: handlePressEvent,
1527
+ onLongPressEvent: handleLongPressEvent,
1528
+ onDragEvent,
1529
+ showDragHandle,
1530
+ onDragStart,
1531
+ dragStepMinutes,
1532
+ onPressCell,
1533
+ resetPageOnPressCell,
1534
+ onLongPressCell,
1535
+ onCreateEvent,
1536
+ onPressDateHeader,
1537
+ onChangeDate: handleChangeDate,
1538
+ renderHeader: renderTimeGridHeader
1539
+ })
1540
+ });
1541
+ }
1542
+ //#endregion
1543
+ export { Agenda, Calendar, CalendarThemeProvider, DEFAULT_HOUR_HEIGHT, DefaultEvent, DefaultMonthEvent, MonthList, MonthPager, MonthView, TimeGrid, buildMonthGrid, buildMonthWeeks, darkTheme, daySelectionState, defaultTheme, eventsInTimeZone, expandRecurringEvents, getIsToday, getViewDays, getWeekDays, isAllDayEvent, isDateSelectable, isRangeEndpoint, isSameCalendarDay, isWeekend, isWithinDateRange, layoutDayEvents, mergeTheme, minutesIntoDay, nextDateRange, toZonedTime, useCalendarTheme, useDateRange, useMonthGrid };