react-native-bigger-calendar 0.1.0 → 0.2.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 CHANGED
@@ -21,13 +21,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ let date_fns = require("date-fns");
24
25
  let react = require("react");
25
26
  let react_native_reanimated = require("react-native-reanimated");
26
27
  react_native_reanimated = __toESM(react_native_reanimated);
27
- let date_fns = require("date-fns");
28
+ let _legendapp_list_react_native = require("@legendapp/list/react-native");
28
29
  let react_native = require("react-native");
29
30
  let react_jsx_runtime = require("react/jsx-runtime");
30
- let _legendapp_list_react_native = require("@legendapp/list/react-native");
31
31
  let react_native_gesture_handler = require("react-native-gesture-handler");
32
32
  //#region src/theme.ts
33
33
  const defaultTheme = {
@@ -87,18 +87,169 @@ const CalendarThemeContext = (0, react.createContext)(defaultTheme);
87
87
  const CalendarThemeProvider = CalendarThemeContext.Provider;
88
88
  const useCalendarTheme = () => (0, react.useContext)(CalendarThemeContext);
89
89
  //#endregion
90
+ //#region src/utils/dates.ts
91
+ /** The seven dates of the week containing `date`, starting on `weekStartsOn`. */
92
+ const getWeekDays = (date, weekStartsOn) => {
93
+ const start = (0, date_fns.startOfWeek)(date, { weekStartsOn });
94
+ return Array.from({ length: 7 }, (_, index) => (0, date_fns.addDays)(start, index));
95
+ };
96
+ /** How many day columns a time-grid mode shows. `custom` uses `numberOfDays`. */
97
+ const viewDayCount = (mode, numberOfDays = 1) => {
98
+ switch (mode) {
99
+ case "week": return 7;
100
+ case "3days": return 3;
101
+ case "custom": return Math.max(1, Math.floor(numberOfDays));
102
+ default: return 1;
103
+ }
104
+ };
105
+ /**
106
+ * Days in the inclusive span from `weekStartsOn` to `weekEndsOn` (1–7),
107
+ * wrapping when the end precedes the start (e.g. Sat→Wed). Mirrors
108
+ * react-native-big-calendar's `weekDaysCount`.
109
+ */
110
+ const weekDaysCount = (weekStartsOn, weekEndsOn) => {
111
+ if (weekEndsOn < weekStartsOn) {
112
+ let count = 1;
113
+ let i = weekStartsOn;
114
+ while (i !== weekEndsOn && count <= 7) {
115
+ i = (i + 1) % 7;
116
+ count++;
117
+ }
118
+ return count;
119
+ }
120
+ if (weekEndsOn > weekStartsOn) return weekEndsOn - weekStartsOn + 1;
121
+ return 1;
122
+ };
123
+ /**
124
+ * The day columns to render for a time-grid page. `week` spans the calendar week
125
+ * (honouring `weekStartsOn`). `custom` with a `weekEndsOn` spans the partial week
126
+ * from `weekStartsOn` to `weekEndsOn` (anchored to `date`'s week, paging by week);
127
+ * otherwise every mode shows `viewDayCount` consecutive days starting at `date`.
128
+ */
129
+ const getViewDays = (mode, date, weekStartsOn, numberOfDays = 1, isRTL = false, weekEndsOn) => {
130
+ let days;
131
+ if (mode === "week") days = getWeekDays(date, weekStartsOn);
132
+ else if (mode === "custom" && weekEndsOn != null) {
133
+ const subject = (0, date_fns.startOfDay)(date);
134
+ const offset = weekStartsOn - subject.getDay();
135
+ days = Array.from({ length: weekDaysCount(weekStartsOn, weekEndsOn) }, (_, index) => (0, date_fns.addDays)(subject, index + offset));
136
+ } else days = Array.from({ length: viewDayCount(mode, numberOfDays) }, (_, index) => (0, date_fns.addDays)((0, date_fns.startOfDay)(date), index));
137
+ return isRTL ? days.reverse() : days;
138
+ };
139
+ const isWeekend = (date) => {
140
+ const day = date.getDay();
141
+ return day === 0 || day === 6;
142
+ };
143
+ const getIsToday = (date) => (0, date_fns.isToday)(date);
144
+ const isSameCalendarDay = (a, b) => (0, date_fns.isSameDay)(a, b);
145
+ /** Minutes elapsed since midnight (0–1439). */
146
+ const minutesIntoDay = (date) => (0, date_fns.getHours)(date) * 60 + (0, date_fns.getMinutes)(date);
147
+ //#endregion
148
+ //#region src/components/Agenda.tsx
149
+ /**
150
+ * A vertical, day-grouped list of events (no time grid). Events are sorted by
151
+ * start, grouped under a date header per day. The consumer controls which
152
+ * events (and therefore which date range) are shown.
153
+ */
154
+ function Agenda({ events, locale, renderEvent, keyExtractor, onPressEvent, onLongPressEvent, onPressDay, activeDate, itemSeparatorComponent }) {
155
+ const theme = useCalendarTheme();
156
+ const RenderEventComponent = renderEvent;
157
+ const rows = (0, react.useMemo)(() => {
158
+ const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
159
+ const out = [];
160
+ let currentDay = null;
161
+ sorted.forEach((event, index) => {
162
+ if (!currentDay || !(0, date_fns.isSameDay)(event.start, currentDay)) {
163
+ currentDay = (0, date_fns.startOfDay)(event.start);
164
+ out.push({
165
+ kind: "header",
166
+ date: currentDay,
167
+ key: `h-${currentDay.toISOString()}`
168
+ });
169
+ }
170
+ out.push({
171
+ kind: "event",
172
+ event,
173
+ index,
174
+ key: `e-${keyExtractor(event, index)}`
175
+ });
176
+ });
177
+ return out;
178
+ }, [events, keyExtractor]);
179
+ const keyExtractorRow = (0, react.useCallback)((row) => row.key, []);
180
+ const renderItem = (0, react.useCallback)(({ item }) => {
181
+ if (item.kind === "header") {
182
+ const isHighlighted = activeDate ? (0, date_fns.isSameDay)(item.date, activeDate) : getIsToday(item.date);
183
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
184
+ style: [
185
+ theme.text.weekday,
186
+ styles$5.header,
187
+ { color: isHighlighted ? theme.colors.todayBackground : theme.colors.textMuted }
188
+ ],
189
+ onPress: onPressDay ? () => onPressDay(item.date) : void 0,
190
+ accessibilityRole: onPressDay ? "button" : "header",
191
+ children: (0, date_fns.format)(item.date, "EEEE, d LLLL", { locale })
192
+ });
193
+ }
194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
195
+ style: styles$5.eventRow,
196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenderEventComponent, {
197
+ event: item.event,
198
+ mode: "schedule",
199
+ onPress: () => onPressEvent(item.event),
200
+ onLongPress: onLongPressEvent ? () => onLongPressEvent(item.event) : void 0
201
+ })
202
+ });
203
+ }, [
204
+ theme,
205
+ locale,
206
+ activeDate,
207
+ onPressDay,
208
+ onPressEvent,
209
+ onLongPressEvent,
210
+ RenderEventComponent
211
+ ]);
212
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react_native.LegendList, {
213
+ style: styles$5.list,
214
+ data: rows,
215
+ keyExtractor: keyExtractorRow,
216
+ renderItem,
217
+ ItemSeparatorComponent: itemSeparatorComponent ?? void 0,
218
+ recycleItems: false
219
+ });
220
+ }
221
+ const styles$5 = react_native.StyleSheet.create({
222
+ list: { flex: 1 },
223
+ header: {
224
+ paddingTop: 12,
225
+ paddingBottom: 4,
226
+ paddingHorizontal: 12
227
+ },
228
+ eventRow: {
229
+ paddingHorizontal: 12,
230
+ paddingVertical: 2
231
+ }
232
+ });
233
+ //#endregion
90
234
  //#region src/components/DefaultEvent.tsx
91
235
  /**
92
236
  * The built-in event renderer: a filled, rounded box showing the event title
93
237
  * and (on the day/week grid) its time range. Pass your own `renderEvent` to
94
238
  * `<Calendar>` to replace it entirely.
95
239
  */
96
- function DefaultEvent({ event, mode, onPress }) {
240
+ function DefaultEvent({ event, mode, isAllDay, ampm = false, showTime = true, cellStyle, onPress, onLongPress }) {
97
241
  const theme = useCalendarTheme();
98
- const showTime = mode !== "month";
242
+ const timeFormat = ampm ? "h:mm a" : "HH:mm";
243
+ const shouldShowTime = mode !== "month" && !isAllDay && showTime;
99
244
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.TouchableOpacity, {
100
- style: [styles$3.box, { backgroundColor: theme.colors.eventBackground }],
245
+ style: [
246
+ styles$4.box,
247
+ { backgroundColor: theme.colors.eventBackground },
248
+ event.disabled && styles$4.disabled,
249
+ cellStyle
250
+ ],
101
251
  onPress,
252
+ onLongPress,
102
253
  activeOpacity: .7,
103
254
  accessibilityRole: "button",
104
255
  accessibilityLabel: event.title,
@@ -108,15 +259,15 @@ function DefaultEvent({ event, mode, onPress }) {
108
259
  ellipsizeMode: "tail",
109
260
  allowFontScaling: false,
110
261
  children: event.title
111
- }) : null, showTime ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
112
- style: [styles$3.time, { color: theme.colors.eventText }],
262
+ }) : null, shouldShowTime ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
263
+ style: [styles$4.time, { color: theme.colors.eventText }],
113
264
  numberOfLines: 1,
114
265
  allowFontScaling: false,
115
- children: `${(0, date_fns.format)(event.start, "HH:mm")} - ${(0, date_fns.format)(event.end, "HH:mm")}`
266
+ children: `${(0, date_fns.format)(event.start, timeFormat)} - ${(0, date_fns.format)(event.end, timeFormat)}`
116
267
  }) : null]
117
268
  });
118
269
  }
119
- const styles$3 = react_native.StyleSheet.create({
270
+ const styles$4 = react_native.StyleSheet.create({
120
271
  box: {
121
272
  flex: 1,
122
273
  borderRadius: 6,
@@ -124,24 +275,10 @@ const styles$3 = react_native.StyleSheet.create({
124
275
  paddingHorizontal: 4,
125
276
  overflow: "hidden"
126
277
  },
127
- time: { fontSize: 11 }
278
+ time: { fontSize: 11 },
279
+ disabled: { opacity: .5 }
128
280
  });
129
281
  //#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
282
  //#region src/utils/layout.ts
146
283
  const MINUTES_PER_HOUR$1 = 60;
147
284
  const MIN_DURATION_HOURS = .25;
@@ -154,7 +291,7 @@ const MIN_DURATION_HOURS = .25;
154
291
  function layoutDayEvents(events, day) {
155
292
  const dayStart = (0, date_fns.startOfDay)(day);
156
293
  const nextDayStart = (0, date_fns.addDays)(dayStart, 1);
157
- const segments = events.filter((event) => event.start < nextDayStart && event.end > dayStart).map((event) => {
294
+ const segments = events.filter((event) => !isAllDayEvent(event)).filter((event) => event.start < nextDayStart && event.end > dayStart).map((event) => {
158
295
  const segStart = (0, date_fns.max)([event.start, dayStart]);
159
296
  const segEnd = (0, date_fns.min)([event.end, nextDayStart]);
160
297
  return {
@@ -198,6 +335,16 @@ function layoutDayEvents(events, day) {
198
335
  if (cluster.length > 0) flushCluster();
199
336
  return positioned;
200
337
  }
338
+ const atMidnight = (date) => date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0;
339
+ /**
340
+ * Whether an event belongs in the all-day lane. An explicit `allDay` flag wins;
341
+ * otherwise it's inferred when the event spans whole days (both `start` and
342
+ * `end` land on midnight, e.g. an iCal-style all-day event). Pure.
343
+ */
344
+ function isAllDayEvent(event) {
345
+ if (typeof event.allDay === "boolean") return event.allDay;
346
+ return event.end > event.start && atMidnight(event.start) && atMidnight(event.end);
347
+ }
201
348
  /**
202
349
  * The `startOfDay` ISO keys of every calendar day an event touches (inclusive).
203
350
  * An event ending exactly at midnight does not count the following day. Used to
@@ -217,15 +364,23 @@ const chunkIntoWeeks = (days) => {
217
364
  for (let index = 0; index < days.length; index += 7) weeks.push(days.slice(index, index + 7));
218
365
  return weeks;
219
366
  };
220
- function MonthViewInner({ date, events, maxVisibleEventCount, weekStartsOn, renderEvent, keyExtractor, onPressDay, onPressEvent, onPressMore }) {
367
+ function MonthViewInner({ date, events, maxVisibleEventCount, weekStartsOn, locale, sortedMonthView = true, moreLabel = "{moreCount} More", showAdjacentMonths = true, disableMonthEventCellPress = false, isRTL = false, showSixWeeks = false, activeDate, calendarCellStyle, renderEvent, keyExtractor, onPressDay, onLongPressDay, onPressEvent, onLongPressEvent, onPressMore }) {
221
368
  const theme = useCalendarTheme();
222
369
  const RenderEventComponent = renderEvent;
223
370
  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 })
371
+ const start = (0, date_fns.startOfWeek)((0, date_fns.startOfMonth)(date), { weekStartsOn });
372
+ const naturalEnd = (0, date_fns.endOfWeek)((0, date_fns.endOfMonth)(date), { weekStartsOn });
373
+ const chunked = chunkIntoWeeks((0, date_fns.eachDayOfInterval)({
374
+ start,
375
+ end: showSixWeeks ? (0, date_fns.addDays)(start, 41) : naturalEnd
227
376
  }));
228
- }, [date, weekStartsOn]);
377
+ return isRTL ? chunked.map((week) => [...week].reverse()) : chunked;
378
+ }, [
379
+ date,
380
+ weekStartsOn,
381
+ isRTL,
382
+ showSixWeeks
383
+ ]);
229
384
  const eventsByDay = (0, react.useMemo)(() => {
230
385
  const map = /* @__PURE__ */ new Map();
231
386
  for (const event of events) for (const key of eventDayKeys(event)) {
@@ -233,29 +388,38 @@ function MonthViewInner({ date, events, maxVisibleEventCount, weekStartsOn, rend
233
388
  if (existing) existing.push(event);
234
389
  else map.set(key, [event]);
235
390
  }
391
+ if (sortedMonthView) for (const list of map.values()) list.sort((a, b) => a.start.getTime() - b.start.getTime());
236
392
  return map;
237
- }, [events]);
393
+ }, [events, sortedMonthView]);
238
394
  const renderDay = (day) => {
239
- const dayEvents = eventsByDay.get((0, date_fns.startOfDay)(day).toISOString()) ?? [];
240
395
  const isCurrentMonth = (0, date_fns.isSameMonth)(day, date);
396
+ if (!isCurrentMonth && !showAdjacentMonths) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { style: [
397
+ styles$3.dayCell,
398
+ { borderColor: theme.colors.gridLine },
399
+ isWeekend(day) && { backgroundColor: theme.colors.weekendBackground }
400
+ ] }, day.toISOString());
401
+ const dayEvents = eventsByDay.get((0, date_fns.startOfDay)(day).toISOString()) ?? [];
241
402
  const isToday = getIsToday(day);
403
+ const isHighlighted = activeDate ? isSameCalendarDay(day, activeDate) : isToday;
242
404
  const hiddenCount = dayEvents.length - maxVisibleEventCount;
243
- const dateColor = isToday ? theme.colors.todayText : isCurrentMonth ? theme.colors.text : theme.colors.textDisabled;
405
+ const dateColor = isHighlighted ? theme.colors.todayText : isCurrentMonth ? theme.colors.text : theme.colors.textDisabled;
244
406
  const eventCount = dayEvents.length;
245
- const accessibilityLabel = `${(0, date_fns.format)(day, "EEEE, d LLLL yyyy")}${isToday ? ", today" : ""}, ${eventCount} ${eventCount === 1 ? "event" : "events"}`;
407
+ const accessibilityLabel = `${(0, date_fns.format)(day, "EEEE, d LLLL yyyy", { locale })}${isToday ? ", today" : ""}, ${eventCount} ${eventCount === 1 ? "event" : "events"}`;
246
408
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.TouchableOpacity, {
247
409
  style: [
248
- styles$2.dayCell,
410
+ styles$3.dayCell,
249
411
  { borderColor: theme.colors.gridLine },
250
- isWeekend(day) && { backgroundColor: theme.colors.weekendBackground }
412
+ isWeekend(day) && { backgroundColor: theme.colors.weekendBackground },
413
+ calendarCellStyle?.(day)
251
414
  ],
252
415
  onPress: onPressDay ? () => onPressDay(day) : void 0,
253
- disabled: !onPressDay,
416
+ onLongPress: onLongPressDay ? () => onLongPressDay(day) : void 0,
417
+ disabled: !onPressDay && !onLongPressDay,
254
418
  accessibilityRole: onPressDay ? "button" : void 0,
255
419
  accessibilityLabel,
256
420
  children: [
257
421
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
258
- style: [styles$2.dateBadge, isToday && {
422
+ style: [styles$3.dateBadge, isHighlighted && {
259
423
  backgroundColor: theme.colors.todayBackground,
260
424
  borderRadius: theme.todayBadgeRadius
261
425
  }],
@@ -266,38 +430,40 @@ function MonthViewInner({ date, events, maxVisibleEventCount, weekStartsOn, rend
266
430
  })
267
431
  }),
268
432
  dayEvents.slice(0, maxVisibleEventCount).map((event, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
269
- style: styles$2.monthEvent,
433
+ style: styles$3.monthEvent,
270
434
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenderEventComponent, {
271
435
  event,
272
436
  mode: "month",
273
- onPress: () => onPressEvent(event)
437
+ isAllDay: isAllDayEvent(event),
438
+ onPress: disableMonthEventCellPress ? () => {} : () => onPressEvent(event),
439
+ onLongPress: disableMonthEventCellPress || !onLongPressEvent ? void 0 : () => onLongPressEvent(event)
274
440
  })
275
441
  }, keyExtractor(event, index))),
276
442
  hiddenCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
277
443
  style: [
278
444
  theme.text.more,
279
- styles$2.moreLabel,
445
+ styles$3.moreLabel,
280
446
  { color: theme.colors.textMuted }
281
447
  ],
282
448
  onPress: onPressMore ? () => onPressMore(dayEvents, day) : void 0,
283
449
  accessibilityRole: "button",
284
450
  accessibilityLabel: `Show ${hiddenCount} more events`,
285
451
  allowFontScaling: false,
286
- children: `${hiddenCount} More`
452
+ children: moreLabel.replace("{moreCount}", String(hiddenCount))
287
453
  }) : null
288
454
  ]
289
455
  }, day.toISOString());
290
456
  };
291
457
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
292
- style: styles$2.container,
458
+ style: styles$3.container,
293
459
  children: weeks.map((week) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
294
- style: styles$2.weekRow,
460
+ style: styles$3.weekRow,
295
461
  children: week.map((day) => renderDay(day))
296
462
  }, week[0].toISOString()))
297
463
  });
298
464
  }
299
465
  const MonthView = (0, react.memo)(MonthViewInner);
300
- const styles$2 = react_native.StyleSheet.create({
466
+ const styles$3 = react_native.StyleSheet.create({
301
467
  container: { flex: 1 },
302
468
  weekRow: {
303
469
  flex: 1,
@@ -325,7 +491,7 @@ const styles$2 = react_native.StyleSheet.create({
325
491
  //#region src/components/MonthPager.tsx
326
492
  const PAGE_WINDOW$1 = 60;
327
493
  const PAGE_VIEWABILITY$1 = { itemVisiblePercentThreshold: 90 };
328
- function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, renderEvent, keyExtractor, onPressDay, onPressEvent, onPressMore, onChangeDate, freeSwipe = false }) {
494
+ function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, locale, sortedMonthView, moreLabel, showAdjacentMonths, disableMonthEventCellPress, isRTL, calendarCellStyle, renderEvent, keyExtractor, onPressDay, onLongPressDay, onPressEvent, onLongPressEvent, onPressMore, onChangeDate, freeSwipe = false, swipeEnabled = true, showSixWeeks = false, activeDate, renderHeaderForMonthView }) {
329
495
  const { width, height } = (0, react_native.useWindowDimensions)();
330
496
  const listRef = (0, react.useRef)(null);
331
497
  const [pageHeight, setPageHeight] = (0, react.useState)(height);
@@ -348,6 +514,14 @@ function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, ren
348
514
  animated: false
349
515
  });
350
516
  }, [activeIndex]);
517
+ const weekDays = (0, react.useMemo)(() => {
518
+ const days = getWeekDays(anchor, weekStartsOn);
519
+ return isRTL ? days.reverse() : days;
520
+ }, [
521
+ anchor,
522
+ weekStartsOn,
523
+ isRTL
524
+ ]);
351
525
  const snapToIndices = (0, react.useMemo)(() => monthDates.map((_, index) => index), [monthDates]);
352
526
  const keyExtractorList = (0, react.useCallback)((item) => item.toISOString(), []);
353
527
  const getFixedItemSize = (0, react.useCallback)(() => width, [width]);
@@ -361,10 +535,21 @@ function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, ren
361
535
  events,
362
536
  maxVisibleEventCount,
363
537
  weekStartsOn,
538
+ locale,
539
+ sortedMonthView,
540
+ moreLabel,
541
+ showAdjacentMonths,
542
+ disableMonthEventCellPress,
543
+ isRTL,
544
+ showSixWeeks,
545
+ activeDate,
546
+ calendarCellStyle,
364
547
  renderEvent,
365
548
  keyExtractor,
366
549
  onPressDay,
550
+ onLongPressDay,
367
551
  onPressEvent,
552
+ onLongPressEvent,
368
553
  onPressMore
369
554
  })
370
555
  }), [
@@ -373,44 +558,131 @@ function MonthPagerInner({ date, events, maxVisibleEventCount, weekStartsOn, ren
373
558
  events,
374
559
  maxVisibleEventCount,
375
560
  weekStartsOn,
561
+ locale,
562
+ sortedMonthView,
563
+ moreLabel,
564
+ showAdjacentMonths,
565
+ disableMonthEventCellPress,
566
+ isRTL,
567
+ showSixWeeks,
568
+ activeDate,
569
+ calendarCellStyle,
376
570
  renderEvent,
377
571
  keyExtractor,
378
572
  onPressDay,
573
+ onLongPressDay,
379
574
  onPressEvent,
575
+ onLongPressEvent,
380
576
  onPressMore
381
577
  ]);
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)
578
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
579
+ style: styles$2.container,
580
+ children: [renderHeaderForMonthView ? renderHeaderForMonthView(weekDays) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthWeekdayHeader, {
581
+ weekDays,
582
+ locale
583
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
584
+ style: styles$2.pager,
585
+ onLayout: (event) => setPageHeight(event.nativeEvent.layout.height),
586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react_native.LegendList, {
587
+ ref: listRef,
588
+ style: styles$2.pagerList,
589
+ data: monthDates,
590
+ horizontal: true,
591
+ recycleItems: false,
592
+ keyExtractor: keyExtractorList,
593
+ getFixedItemSize,
594
+ scrollEnabled: swipeEnabled,
595
+ pagingEnabled: !freeSwipe,
596
+ snapToIndices: freeSwipe ? snapToIndices : void 0,
597
+ initialScrollIndex: activeIndex,
598
+ showsHorizontalScrollIndicator: false,
599
+ viewabilityConfig: PAGE_VIEWABILITY$1,
600
+ onViewableItemsChanged: handleViewableItemsChanged,
601
+ renderItem
602
+ }, pageHeight)
603
+ })]
401
604
  });
402
605
  }
403
606
  const MonthPager = (0, react.memo)(MonthPagerInner);
404
- const styles$1 = react_native.StyleSheet.create({
607
+ const MonthWeekdayHeader = ({ weekDays, locale }) => {
608
+ const theme = useCalendarTheme();
609
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
610
+ style: styles$2.weekdayHeader,
611
+ children: weekDays.map((day) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
612
+ style: [
613
+ theme.text.weekday,
614
+ styles$2.weekdayLabel,
615
+ { color: theme.colors.textMuted }
616
+ ],
617
+ allowFontScaling: false,
618
+ children: (0, date_fns.format)(day, "EEE", { locale })
619
+ }, day.toISOString()))
620
+ });
621
+ };
622
+ const styles$2 = react_native.StyleSheet.create({
623
+ container: { flex: 1 },
405
624
  pager: { flex: 1 },
406
- pagerList: { flex: 1 }
625
+ pagerList: { flex: 1 },
626
+ weekdayHeader: {
627
+ flexDirection: "row",
628
+ paddingBottom: 4
629
+ },
630
+ weekdayLabel: {
631
+ flex: 1,
632
+ textAlign: "center"
633
+ }
634
+ });
635
+ //#endregion
636
+ //#region src/components/AllDayLane.tsx
637
+ /**
638
+ * The all-day lane that sits above the scrolling time grid. All-day events are
639
+ * excluded from the timed columns (see `layoutDayEvents`) and shown here,
640
+ * stacked under their day(s). Renders nothing when no day has an all-day event,
641
+ * so timed-only calendars are unaffected.
642
+ */
643
+ function AllDayLane({ days, events, mode, hourColumnWidth, dayWidth, renderEvent, keyExtractor, onPressEvent, onLongPressEvent }) {
644
+ const theme = useCalendarTheme();
645
+ const RenderEventComponent = renderEvent;
646
+ const allDay = events.filter(isAllDayEvent);
647
+ const perDay = days.map((day) => {
648
+ const start = (0, date_fns.startOfDay)(day);
649
+ const next = (0, date_fns.addDays)(start, 1);
650
+ return allDay.filter((event) => event.start < next && event.end > start);
651
+ });
652
+ if (perDay.every((list) => list.length === 0)) return null;
653
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
654
+ style: [styles$1.lane, { borderBottomColor: theme.colors.gridLine }],
655
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { style: { width: hourColumnWidth } }), days.map((day, dayIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
656
+ style: [styles$1.column, { width: dayWidth }],
657
+ children: perDay[dayIndex].map((event, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
658
+ style: styles$1.chip,
659
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RenderEventComponent, {
660
+ event,
661
+ mode,
662
+ isAllDay: true,
663
+ onPress: () => onPressEvent(event),
664
+ onLongPress: onLongPressEvent ? () => onLongPressEvent(event) : void 0
665
+ })
666
+ }, keyExtractor(event, index)))
667
+ }, day.toISOString()))]
668
+ });
669
+ }
670
+ const styles$1 = react_native.StyleSheet.create({
671
+ lane: {
672
+ flexDirection: "row",
673
+ borderBottomWidth: react_native.StyleSheet.hairlineWidth,
674
+ paddingBottom: 2
675
+ },
676
+ column: {
677
+ paddingHorizontal: 1,
678
+ gap: 2
679
+ },
680
+ chip: { minHeight: 18 }
407
681
  });
408
682
  //#endregion
409
683
  //#region src/components/TimeGrid.tsx
410
684
  const MINUTES_PER_HOUR = 60;
411
685
  const HOURS_PER_DAY = 24;
412
- const DAY_VIEW_STEP = 1;
413
- const WEEK_VIEW_STEP = 7;
414
686
  const PAGE_WINDOW = 180;
415
687
  const PAGE_VIEWABILITY = { itemVisiblePercentThreshold: 90 };
416
688
  const DEFAULT_HOUR_HEIGHT = 64;
@@ -435,7 +707,7 @@ function formatHourLabel(hour, ampm) {
435
707
  const period = hour < 12 ? "AM" : "PM";
436
708
  return `${hour % 12 === 0 ? 12 : hour % 12} ${period}`;
437
709
  }
438
- function AnimatedEventBox({ positioned, cellHeight, minHour, left, width, mode, renderEvent, onPress }) {
710
+ function AnimatedEventBox({ positioned, cellHeight, minHour, left, width, mode, renderEvent, onPress, onLongPress }) {
439
711
  const RenderEventComponent = renderEvent;
440
712
  const boxHeight = (0, react_native_reanimated.useDerivedValue)(() => Math.max(positioned.durationHours * cellHeight.value, MIN_EVENT_HEIGHT), [positioned.durationHours]);
441
713
  const boxStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({
@@ -447,6 +719,7 @@ function AnimatedEventBox({ positioned, cellHeight, minHour, left, width, mode,
447
719
  minHour
448
720
  ]);
449
721
  const handlePress = () => onPress(positioned.event);
722
+ const handleLongPress = onLongPress ? () => onLongPress(positioned.event) : void 0;
450
723
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
451
724
  style: [
452
725
  styles.eventBox,
@@ -462,17 +735,21 @@ function AnimatedEventBox({ positioned, cellHeight, minHour, left, width, mode,
462
735
  boxHeight,
463
736
  continuesBefore: positioned.continuesBefore,
464
737
  continuesAfter: positioned.continuesAfter,
465
- onPress: handlePress
738
+ onPress: handlePress,
739
+ onLongPress: handleLongPress
466
740
  })
467
741
  });
468
742
  }
469
- const HourRow = ({ hour, minHour, cellHeight, hourColumnWidth, label }) => {
743
+ const HourRow = ({ hour, minHour, cellHeight, hourColumnWidth, label, ampm, hourComponent }) => {
470
744
  const theme = useCalendarTheme();
471
745
  const animatedStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({ top: (hour - minHour) * cellHeight.value }), [hour, minHour]);
472
746
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native_reanimated.default.View, {
473
747
  style: [styles.hourRow, animatedStyle],
474
748
  pointerEvents: "none",
475
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
749
+ children: [hourComponent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
750
+ style: { width: hourColumnWidth },
751
+ children: hourComponent(hour, ampm)
752
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
476
753
  style: [
477
754
  theme.text.hourLabel,
478
755
  styles.hourLabel,
@@ -486,6 +763,25 @@ const HourRow = ({ hour, minHour, cellHeight, hourColumnWidth, label }) => {
486
763
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { style: [styles.hourLine, { backgroundColor: theme.colors.gridLine }] })]
487
764
  });
488
765
  };
766
+ const TimeslotLine = ({ hour, minHour, fraction, cellHeight, hourColumnWidth }) => {
767
+ const theme = useCalendarTheme();
768
+ const animatedStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({ top: (hour - minHour + fraction) * cellHeight.value }), [
769
+ hour,
770
+ minHour,
771
+ fraction
772
+ ]);
773
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
774
+ style: [
775
+ styles.timeslotLine,
776
+ {
777
+ left: hourColumnWidth,
778
+ backgroundColor: theme.colors.gridLine
779
+ },
780
+ animatedStyle
781
+ ],
782
+ pointerEvents: "none"
783
+ });
784
+ };
489
785
  const NowIndicator = ({ cellHeight, nowHours, minHour, left, color }) => {
490
786
  const animatedStyle = (0, react_native_reanimated.useAnimatedStyle)(() => ({ top: (nowHours - minHour) * cellHeight.value }), [nowHours, minHour]);
491
787
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
@@ -500,7 +796,7 @@ const NowIndicator = ({ cellHeight, nowHours, minHour, left, color }) => {
500
796
  pointerEvents: "none"
501
797
  });
502
798
  };
503
- function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, committedCellHeight, scrollY, isActive, scrollOffsetMinutes, weekStartsOn, hourColumnWidth, minHour, maxHour, ampm, minHourHeight, maxHourHeight, showNowIndicator, renderEvent, keyExtractor, onPressEvent, onPressCell }) {
799
+ function TimetablePageInner({ mode, numberOfDays, date, events, cellHeight, hourHeight, committedCellHeight, scrollY, isActive, scrollOffsetMinutes, weekStartsOn, weekEndsOn, hourColumnWidth, minHour, maxHour, ampm, timeslots, isRTL, showVerticalScrollIndicator, verticalScrollEnabled, hourComponent, calendarCellStyle, minHourHeight, maxHourHeight, showNowIndicator, renderEvent, keyExtractor, onPressEvent, onLongPressEvent, onPressCell, onLongPressCell }) {
504
800
  const theme = useCalendarTheme();
505
801
  const { width } = (0, react_native.useWindowDimensions)();
506
802
  const scrollRef = (0, react_native_reanimated.useAnimatedRef)();
@@ -511,24 +807,34 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
511
807
  (0, react_native_reanimated.useAnimatedReaction)(() => scrollY.value, (current, previous) => {
512
808
  if (!isActive && current !== previous) (0, react_native_reanimated.scrollTo)(scrollRef, 0, current, false);
513
809
  });
514
- const days = (0, react.useMemo)(() => mode === "week" ? getWeekDays(date, weekStartsOn) : [date], [
810
+ const days = (0, react.useMemo)(() => getViewDays(mode, date, weekStartsOn, numberOfDays, isRTL, weekEndsOn), [
515
811
  mode,
516
812
  date,
517
- weekStartsOn
813
+ weekStartsOn,
814
+ numberOfDays,
815
+ isRTL,
816
+ weekEndsOn
518
817
  ]);
519
818
  const dayWidth = (width - hourColumnWidth) / days.length;
520
819
  const dayLeft = (dayIndex) => hourColumnWidth + dayIndex * dayWidth;
521
820
  const dayLayouts = (0, react.useMemo)(() => days.map((day) => layoutDayEvents(events, day)), [days, events]);
522
- const handleBackgroundPress = (event) => {
523
- if (!onPressCell) return;
821
+ const cellDateFromTouch = (event) => {
524
822
  const { locationX, locationY } = event.nativeEvent;
525
823
  const day = days[days.length === 1 ? 0 : Math.floor(locationX / dayWidth)];
526
- if (!day) return;
824
+ if (!day) return null;
527
825
  const minutes = Math.round((minHour + locationY / heightSource.value) * MINUTES_PER_HOUR);
528
826
  const pressed = new Date(day);
529
827
  pressed.setHours(0, 0, 0, 0);
530
828
  pressed.setMinutes(minutes);
531
- onPressCell(pressed);
829
+ return pressed;
830
+ };
831
+ const handleBackgroundPress = (event) => {
832
+ const date = onPressCell && cellDateFromTouch(event);
833
+ if (date) onPressCell?.(date);
834
+ };
835
+ const handleBackgroundLongPress = (event) => {
836
+ const date = onLongPressCell && cellDateFromTouch(event);
837
+ if (date) onLongPressCell?.(date);
532
838
  };
533
839
  const hoursRange = (0, react.useMemo)(() => Array.from({ length: maxHour - minHour }, (_, index) => minHour + index), [minHour, maxHour]);
534
840
  const now = useNow(showNowIndicator && isActive);
@@ -557,13 +863,24 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
557
863
  minHourHeight,
558
864
  maxHourHeight
559
865
  ]);
560
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
866
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
561
867
  style: styles.container,
562
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_gesture_handler.GestureDetector, {
868
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AllDayLane, {
869
+ days,
870
+ events,
871
+ mode,
872
+ hourColumnWidth,
873
+ dayWidth,
874
+ renderEvent,
875
+ keyExtractor,
876
+ onPressEvent,
877
+ onLongPressEvent
878
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_gesture_handler.GestureDetector, {
563
879
  gesture: zoomGesture,
564
880
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.ScrollView, {
565
881
  ref: scrollRef,
566
- showsVerticalScrollIndicator: true,
882
+ showsVerticalScrollIndicator: showVerticalScrollIndicator,
883
+ scrollEnabled: verticalScrollEnabled,
567
884
  onScroll: scrollHandler,
568
885
  scrollEventThrottle: 16,
569
886
  contentContainerStyle: { paddingTop: HOUR_LABEL_TOP_INSET },
@@ -574,9 +891,10 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
574
891
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native_reanimated.default.View, {
575
892
  style: [styles.content, fullHeightStyle],
576
893
  children: [
577
- onPressCell ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Pressable, {
894
+ onPressCell || onLongPressCell ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Pressable, {
578
895
  style: [styles.cellPressLayer, { left: hourColumnWidth }],
579
- onPress: handleBackgroundPress,
896
+ onPress: onPressCell ? handleBackgroundPress : void 0,
897
+ onLongPress: onLongPressCell ? handleBackgroundLongPress : void 0,
580
898
  importantForAccessibility: "no",
581
899
  accessibilityElementsHidden: true
582
900
  }) : null,
@@ -592,6 +910,21 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
592
910
  ],
593
911
  pointerEvents: "none"
594
912
  }, `weekend-${day.toISOString()}`) : null),
913
+ calendarCellStyle ? days.map((day, dayIndex) => {
914
+ const cellStyle = calendarCellStyle(day);
915
+ return cellStyle ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
916
+ style: [
917
+ styles.weekendColumn,
918
+ {
919
+ left: dayLeft(dayIndex),
920
+ width: dayWidth
921
+ },
922
+ cellStyle,
923
+ fullHeightStyle
924
+ ],
925
+ pointerEvents: "none"
926
+ }, `cell-${day.toISOString()}`) : null;
927
+ }) : null,
595
928
  days.map((day, dayIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_reanimated.default.View, {
596
929
  style: [
597
930
  styles.daySeparator,
@@ -606,8 +939,17 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
606
939
  minHour,
607
940
  cellHeight: heightSource,
608
941
  hourColumnWidth,
609
- label: formatHourLabel(hour, ampm)
942
+ label: formatHourLabel(hour, ampm),
943
+ ampm,
944
+ hourComponent
610
945
  }, hour)),
946
+ timeslots > 1 ? hoursRange.flatMap((hour) => Array.from({ length: timeslots - 1 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeslotLine, {
947
+ hour,
948
+ minHour,
949
+ fraction: (i + 1) / timeslots,
950
+ cellHeight: heightSource,
951
+ hourColumnWidth
952
+ }, `slot-${hour}-${i}`))) : null,
611
953
  dayLayouts.flatMap((layout, dayIndex) => layout.filter((p) => p.startHours < maxHour && p.startHours + p.durationHours > minHour).map((positioned, eventIndex) => {
612
954
  const columnWidth = dayWidth / positioned.columns;
613
955
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedEventBox, {
@@ -618,7 +960,8 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
618
960
  width: columnWidth,
619
961
  mode,
620
962
  renderEvent,
621
- onPress: onPressEvent
963
+ onPress: onPressEvent,
964
+ onLongPress: onLongPressEvent
622
965
  }, keyExtractor(positioned.event, eventIndex));
623
966
  })),
624
967
  showNowIndicator && nowDayIndex >= 0 && nowInWindow ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NowIndicator, {
@@ -631,40 +974,48 @@ function TimetablePageInner({ mode, date, events, cellHeight, hourHeight, commit
631
974
  ]
632
975
  })
633
976
  })
634
- })
977
+ })]
635
978
  });
636
979
  }
637
980
  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 }) {
981
+ function TimeGridInner({ mode, numberOfDays = 1, weekEndsOn, date, events, cellHeight, hourHeight = 64, weekStartsOn, renderEvent, keyExtractor, scrollOffsetMinutes = 0, hourColumnWidth: hourColumnWidthProp = DEFAULT_HOUR_COLUMN_WIDTH, hideHours = false, timeslots = 1, calendarCellStyle, 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, onPressEvent, onLongPressEvent, onPressCell, onLongPressCell, onPressDateHeader, onChangeDate, renderHeader }) {
639
982
  const clampedMinHour = Math.max(0, Math.min(minHour, HOURS_PER_DAY - 1));
640
983
  const clampedMaxHour = Math.max(clampedMinHour + 1, Math.min(maxHour, HOURS_PER_DAY));
984
+ const hourColumnWidth = hideHours ? 0 : hourColumnWidthProp;
641
985
  const { width, height } = (0, react_native.useWindowDimensions)();
642
986
  const listRef = (0, react.useRef)(null);
643
987
  const [pageHeight, setPageHeight] = (0, react.useState)(height);
644
- const step = mode === "week" ? WEEK_VIEW_STEP : DAY_VIEW_STEP;
988
+ const [measured, setMeasured] = (0, react.useState)(false);
989
+ const weekAnchored = mode === "week" || mode === "custom" && weekEndsOn != null;
990
+ const step = weekAnchored ? 7 : viewDayCount(mode, numberOfDays);
645
991
  const scrollY = (0, react_native_reanimated.useSharedValue)(Math.max(0, scrollOffsetMinutes / MINUTES_PER_HOUR - clampedMinHour) * hourHeight);
646
992
  const committedCellHeight = (0, react_native_reanimated.useSharedValue)(hourHeight);
647
993
  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,
994
+ const anchor = (0, react.useMemo)(() => weekAnchored ? (0, date_fns.startOfWeek)(anchorDate, { weekStartsOn }) : (0, date_fns.startOfDay)(anchorDate), [
995
+ weekAnchored,
650
996
  anchorDate,
651
997
  weekStartsOn
652
998
  ]);
653
999
  const pageDates = (0, react.useMemo)(() => Array.from({ length: 361 }, (_, i) => (0, date_fns.addDays)(anchor, (i - PAGE_WINDOW) * step)), [anchor, step]);
654
1000
  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;
1001
+ const aligned = weekAnchored ? (0, date_fns.startOfWeek)(target, { weekStartsOn }) : (0, date_fns.startOfDay)(target);
1002
+ return Math.floor((0, date_fns.differenceInCalendarDays)(aligned, anchor) / step) + PAGE_WINDOW;
657
1003
  }, [
658
1004
  anchor,
659
- mode,
1005
+ weekAnchored,
660
1006
  step,
661
1007
  weekStartsOn
662
1008
  ])(date);
663
1009
  const viewedIndexRef = (0, react.useRef)(activeIndex);
664
- const headerDays = (0, react.useMemo)(() => mode === "week" ? getWeekDays(date, weekStartsOn) : [date], [
1010
+ const headerDays = (0, react.useMemo)(() => getViewDays(mode, pageDates[activeIndex] ?? date, weekStartsOn, numberOfDays, isRTL, weekEndsOn), [
665
1011
  mode,
1012
+ pageDates,
1013
+ activeIndex,
666
1014
  date,
667
- weekStartsOn
1015
+ weekStartsOn,
1016
+ numberOfDays,
1017
+ isRTL,
1018
+ weekEndsOn
668
1019
  ]);
669
1020
  const handleViewableItemsChanged = (0, react.useCallback)((info) => {
670
1021
  const settled = info.viewableItems.find((token) => token.isViewable);
@@ -680,6 +1031,21 @@ function TimeGridInner({ mode, date, events, cellHeight, hourHeight = 64, weekSt
680
1031
  animated: false
681
1032
  });
682
1033
  }, [activeIndex]);
1034
+ const handlePressCell = (0, react.useMemo)(() => {
1035
+ if (!onPressCell) return void 0;
1036
+ if (!resetPageOnPressCell) return onPressCell;
1037
+ return (cellDate) => {
1038
+ onPressCell(cellDate);
1039
+ listRef.current?.scrollToIndex({
1040
+ index: activeIndex,
1041
+ animated: true
1042
+ });
1043
+ };
1044
+ }, [
1045
+ onPressCell,
1046
+ resetPageOnPressCell,
1047
+ activeIndex
1048
+ ]);
683
1049
  const snapToIndices = (0, react.useMemo)(() => pageDates.map((_, index) => index), [pageDates]);
684
1050
  const keyExtractorList = (0, react.useCallback)((item) => item.toISOString(), []);
685
1051
  const getFixedItemSize = (0, react.useCallback)(() => width, [width]);
@@ -690,6 +1056,7 @@ function TimeGridInner({ mode, date, events, cellHeight, hourHeight = 64, weekSt
690
1056
  },
691
1057
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimetablePage, {
692
1058
  mode,
1059
+ numberOfDays,
693
1060
  date: item,
694
1061
  events,
695
1062
  cellHeight,
@@ -699,22 +1066,32 @@ function TimeGridInner({ mode, date, events, cellHeight, hourHeight = 64, weekSt
699
1066
  isActive: index === activeIndex,
700
1067
  scrollOffsetMinutes,
701
1068
  weekStartsOn,
1069
+ weekEndsOn,
702
1070
  hourColumnWidth,
703
1071
  minHour: clampedMinHour,
704
1072
  maxHour: clampedMaxHour,
705
1073
  ampm,
1074
+ timeslots,
1075
+ isRTL,
1076
+ showVerticalScrollIndicator,
1077
+ verticalScrollEnabled,
1078
+ hourComponent,
1079
+ calendarCellStyle,
706
1080
  minHourHeight,
707
1081
  maxHourHeight,
708
1082
  showNowIndicator,
709
1083
  renderEvent,
710
1084
  keyExtractor,
711
1085
  onPressEvent,
712
- onPressCell
1086
+ onLongPressEvent,
1087
+ onPressCell: handlePressCell,
1088
+ onLongPressCell
713
1089
  })
714
1090
  }), [
715
1091
  width,
716
1092
  pageHeight,
717
1093
  mode,
1094
+ numberOfDays,
718
1095
  events,
719
1096
  cellHeight,
720
1097
  hourHeight,
@@ -723,79 +1100,114 @@ function TimeGridInner({ mode, date, events, cellHeight, hourHeight = 64, weekSt
723
1100
  activeIndex,
724
1101
  scrollOffsetMinutes,
725
1102
  weekStartsOn,
1103
+ weekEndsOn,
726
1104
  hourColumnWidth,
727
1105
  clampedMinHour,
728
1106
  clampedMaxHour,
729
1107
  ampm,
1108
+ timeslots,
1109
+ isRTL,
1110
+ showVerticalScrollIndicator,
1111
+ verticalScrollEnabled,
1112
+ hourComponent,
1113
+ calendarCellStyle,
730
1114
  minHourHeight,
731
1115
  maxHourHeight,
732
1116
  showNowIndicator,
733
1117
  renderEvent,
734
1118
  keyExtractor,
735
1119
  onPressEvent,
736
- onPressCell
1120
+ onLongPressEvent,
1121
+ handlePressCell,
1122
+ onLongPressCell
737
1123
  ]);
738
1124
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
739
1125
  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
- })]
1126
+ children: [
1127
+ renderHeader ? renderHeader(headerDays) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultHeader, {
1128
+ days: headerDays,
1129
+ mode,
1130
+ width,
1131
+ hourColumnWidth,
1132
+ showWeekNumber,
1133
+ weekNumberPrefix,
1134
+ locale,
1135
+ activeDate,
1136
+ onPressDateHeader
1137
+ }),
1138
+ headerComponent,
1139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1140
+ style: styles.pager,
1141
+ onLayout: (event) => {
1142
+ setPageHeight(event.nativeEvent.layout.height);
1143
+ setMeasured(true);
1144
+ },
1145
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_legendapp_list_react_native.LegendList, {
1146
+ ref: listRef,
1147
+ style: styles.pagerList,
1148
+ data: pageDates,
1149
+ horizontal: true,
1150
+ recycleItems: false,
1151
+ keyExtractor: keyExtractorList,
1152
+ getFixedItemSize,
1153
+ scrollEnabled: swipeEnabled,
1154
+ pagingEnabled: !freeSwipe,
1155
+ snapToIndices: freeSwipe ? snapToIndices : void 0,
1156
+ initialScrollIndex: activeIndex,
1157
+ showsHorizontalScrollIndicator: false,
1158
+ viewabilityConfig: PAGE_VIEWABILITY,
1159
+ onViewableItemsChanged: handleViewableItemsChanged,
1160
+ renderItem
1161
+ }, measured ? "grid" : "grid-seed")
1162
+ })
1163
+ ]
766
1164
  });
767
1165
  }
768
1166
  const TimeGrid = (0, react.memo)(TimeGridInner);
769
- const DefaultHeader = ({ days, mode, width, hourColumnWidth, locale }) => {
770
- const dayWidth = mode === "week" ? (width - hourColumnWidth) / days.length : width;
1167
+ const DefaultHeader = ({ days, mode, width, hourColumnWidth, showWeekNumber, weekNumberPrefix = "W", locale, activeDate, onPressDateHeader }) => {
1168
+ const theme = useCalendarTheme();
1169
+ const dayWidth = (width - hourColumnWidth) / days.length;
771
1170
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
772
1171
  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, {
1172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
1173
+ style: [styles.weekNumberGutter, { width: hourColumnWidth }],
1174
+ children: showWeekNumber && hourColumnWidth > 0 && days[0] ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
1175
+ style: [theme.text.hourLabel, { color: theme.colors.textMuted }],
1176
+ allowFontScaling: false,
1177
+ children: `${weekNumberPrefix}${(0, date_fns.getISOWeek)(days[0])}`
1178
+ }) : null
1179
+ }), days.map((day) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DayHeader, {
774
1180
  day,
775
1181
  mode,
776
1182
  width: dayWidth,
777
- locale
1183
+ locale,
1184
+ activeDate,
1185
+ onPressDateHeader
778
1186
  }, day.toISOString()))]
779
1187
  });
780
1188
  };
781
- const DayHeader = ({ day, mode, width, locale }) => {
1189
+ const DayHeader = ({ day, mode, width, locale, activeDate, onPressDateHeader }) => {
782
1190
  const theme = useCalendarTheme();
783
1191
  const isToday = getIsToday(day);
1192
+ const isHighlighted = activeDate ? isSameCalendarDay(day, activeDate) : isToday;
784
1193
  const badgeSize = mode === "day" ? 44 : 32;
785
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
1194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.Pressable, {
786
1195
  style: [styles.dayHeader, {
787
1196
  width,
788
1197
  gap: mode === "day" ? 4 : 2
789
1198
  }],
1199
+ onPress: onPressDateHeader ? () => onPressDateHeader(day) : void 0,
1200
+ disabled: !onPressDateHeader,
1201
+ accessibilityRole: onPressDateHeader ? "button" : void 0,
790
1202
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
791
- style: [styles.dayHeaderBadge, isToday && {
1203
+ style: [styles.dayHeaderBadge, isHighlighted && {
792
1204
  backgroundColor: theme.colors.todayBackground,
793
1205
  borderRadius: 999,
794
1206
  width: badgeSize,
795
1207
  height: badgeSize
796
1208
  }],
797
1209
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
798
- style: [theme.text.dayNumber, { color: isToday ? theme.colors.todayText : theme.colors.text }],
1210
+ style: [theme.text.dayNumber, { color: isHighlighted ? theme.colors.todayText : theme.colors.text }],
799
1211
  allowFontScaling: false,
800
1212
  ...isToday && { accessibilityLabel: `Today, ${day.getDate()}` },
801
1213
  children: day.getDate()
@@ -803,7 +1215,7 @@ const DayHeader = ({ day, mode, width, locale }) => {
803
1215
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
804
1216
  style: [theme.text.weekday, { color: theme.colors.text }],
805
1217
  allowFontScaling: false,
806
- children: day.toLocaleDateString(locale, { weekday: "short" })
1218
+ children: (0, date_fns.format)(day, "EEE", { locale })
807
1219
  })]
808
1220
  });
809
1221
  };
@@ -816,6 +1228,10 @@ const styles = react_native.StyleSheet.create({
816
1228
  alignItems: "center",
817
1229
  paddingBottom: 8
818
1230
  },
1231
+ weekNumberGutter: {
1232
+ alignItems: "center",
1233
+ justifyContent: "flex-end"
1234
+ },
819
1235
  dayHeader: { alignItems: "center" },
820
1236
  dayHeaderBadge: {
821
1237
  justifyContent: "center",
@@ -855,6 +1271,12 @@ const styles = react_native.StyleSheet.create({
855
1271
  flex: 1,
856
1272
  height: react_native.StyleSheet.hairlineWidth
857
1273
  },
1274
+ timeslotLine: {
1275
+ position: "absolute",
1276
+ right: 0,
1277
+ height: react_native.StyleSheet.hairlineWidth,
1278
+ opacity: .5
1279
+ },
858
1280
  eventBox: {
859
1281
  position: "absolute",
860
1282
  overflow: "hidden"
@@ -868,9 +1290,50 @@ const styles = react_native.StyleSheet.create({
868
1290
  //#endregion
869
1291
  //#region src/components/Calendar.tsx
870
1292
  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 }) {
1293
+ function visibleRange(mode, date, weekStartsOn, numberOfDays, weekEndsOn) {
1294
+ if (mode === "month") return [(0, date_fns.startOfWeek)((0, date_fns.startOfMonth)(date), { weekStartsOn }), (0, date_fns.endOfWeek)((0, date_fns.endOfMonth)(date), { weekStartsOn })];
1295
+ const days = getViewDays(mode, date, weekStartsOn, numberOfDays, false, weekEndsOn);
1296
+ return [(0, date_fns.startOfDay)(days[0]), (0, date_fns.endOfDay)(days[days.length - 1])];
1297
+ }
1298
+ function Calendar({ events, mode, date, onChangeDate, onChangeDateRange, onPressEvent, onLongPressEvent, onPressDay, onLongPressDay, onPressMore, onPressCell, resetPageOnPressCell, onLongPressCell, onPressDateHeader, maxVisibleEventCount = 2, sortedMonthView, moreLabel, showAdjacentMonths, disableMonthEventCellPress, weekStartsOn = 0, numberOfDays, weekEndsOn, renderEvent = DefaultEvent, eventCellStyle, calendarCellStyle, keyExtractor = defaultKeyExtractor, theme, cellHeight: cellHeightProp, hourHeight = 64, minHourHeight, maxHourHeight, hourColumnWidth, hideHours, timeslots, showWeekNumber, weekNumberPrefix, hourComponent, showSixWeeks, swipeEnabled, showVerticalScrollIndicator, verticalScrollEnabled, headerComponent, minHour, maxHour, ampm, showTime, scrollOffsetMinutes, showNowIndicator, locale, activeDate, isRTL, freeSwipe, renderTimeGridHeader, renderHeaderForMonthView, itemSeparatorComponent }) {
872
1299
  const mergedTheme = (0, react.useMemo)(() => mergeTheme(theme), [theme]);
873
1300
  const internalCellHeight = (0, react_native_reanimated.useSharedValue)(hourHeight);
1301
+ const cellHeight = cellHeightProp ?? internalCellHeight;
1302
+ const handlePressEvent = (0, react.useCallback)((event) => {
1303
+ if (!event.disabled) onPressEvent(event);
1304
+ }, [onPressEvent]);
1305
+ const handleLongPressEvent = (0, react.useMemo)(() => onLongPressEvent ? (event) => {
1306
+ if (!event.disabled) onLongPressEvent(event);
1307
+ } : void 0, [onLongPressEvent]);
1308
+ const handleChangeDate = (0, react.useCallback)((next) => {
1309
+ onChangeDate(next);
1310
+ onChangeDateRange?.(visibleRange(mode, next, weekStartsOn, numberOfDays ?? 1, weekEndsOn));
1311
+ }, [
1312
+ onChangeDate,
1313
+ onChangeDateRange,
1314
+ mode,
1315
+ weekStartsOn,
1316
+ numberOfDays,
1317
+ weekEndsOn
1318
+ ]);
1319
+ const resolvedRenderEvent = (0, react.useMemo)(() => {
1320
+ if (eventCellStyle == null && ampm == null && showTime == null) return renderEvent;
1321
+ const Base = renderEvent;
1322
+ return function StyledEvent(props) {
1323
+ const cellStyle = typeof eventCellStyle === "function" ? eventCellStyle(props.event) : eventCellStyle;
1324
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Base, {
1325
+ ...props,
1326
+ cellStyle,
1327
+ ampm,
1328
+ showTime
1329
+ });
1330
+ };
1331
+ }, [
1332
+ renderEvent,
1333
+ eventCellStyle,
1334
+ ampm,
1335
+ showTime
1336
+ ]);
874
1337
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CalendarThemeProvider, {
875
1338
  value: mergedTheme,
876
1339
  children: mode === "month" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MonthPager, {
@@ -878,24 +1341,58 @@ function Calendar({ events, mode, date, onChangeDate, onPressEvent, onPressDay,
878
1341
  events,
879
1342
  maxVisibleEventCount,
880
1343
  weekStartsOn,
881
- renderEvent,
1344
+ locale,
1345
+ sortedMonthView,
1346
+ moreLabel,
1347
+ showAdjacentMonths,
1348
+ disableMonthEventCellPress,
1349
+ isRTL,
1350
+ showSixWeeks,
1351
+ activeDate,
1352
+ renderHeaderForMonthView,
1353
+ calendarCellStyle,
1354
+ renderEvent: resolvedRenderEvent,
882
1355
  keyExtractor,
883
1356
  onPressDay,
884
- onPressEvent,
1357
+ onLongPressDay,
1358
+ onPressEvent: handlePressEvent,
1359
+ onLongPressEvent: handleLongPressEvent,
885
1360
  onPressMore,
886
- onChangeDate,
887
- freeSwipe
1361
+ onChangeDate: handleChangeDate,
1362
+ freeSwipe,
1363
+ swipeEnabled
1364
+ }) : mode === "schedule" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Agenda, {
1365
+ events,
1366
+ locale,
1367
+ renderEvent: resolvedRenderEvent,
1368
+ keyExtractor,
1369
+ onPressEvent: handlePressEvent,
1370
+ onLongPressEvent: handleLongPressEvent,
1371
+ onPressDay,
1372
+ activeDate,
1373
+ itemSeparatorComponent
888
1374
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeGrid, {
889
1375
  mode,
1376
+ numberOfDays,
1377
+ weekEndsOn,
890
1378
  date,
891
1379
  events,
892
- cellHeight: cellHeightProp ?? internalCellHeight,
1380
+ cellHeight,
893
1381
  hourHeight,
894
1382
  weekStartsOn,
895
- renderEvent,
1383
+ renderEvent: resolvedRenderEvent,
896
1384
  keyExtractor,
897
1385
  scrollOffsetMinutes,
898
1386
  hourColumnWidth,
1387
+ hideHours,
1388
+ timeslots,
1389
+ calendarCellStyle,
1390
+ showWeekNumber,
1391
+ weekNumberPrefix,
1392
+ hourComponent,
1393
+ showVerticalScrollIndicator,
1394
+ verticalScrollEnabled,
1395
+ headerComponent,
899
1396
  minHour,
900
1397
  maxHour,
901
1398
  ampm,
@@ -903,15 +1400,23 @@ function Calendar({ events, mode, date, onChangeDate, onPressEvent, onPressDay,
903
1400
  maxHourHeight,
904
1401
  showNowIndicator,
905
1402
  locale,
1403
+ activeDate,
1404
+ isRTL,
906
1405
  freeSwipe,
907
- onPressEvent,
1406
+ swipeEnabled,
1407
+ onPressEvent: handlePressEvent,
1408
+ onLongPressEvent: handleLongPressEvent,
908
1409
  onPressCell,
909
- onChangeDate,
1410
+ resetPageOnPressCell,
1411
+ onLongPressCell,
1412
+ onPressDateHeader,
1413
+ onChangeDate: handleChangeDate,
910
1414
  renderHeader: renderTimeGridHeader
911
1415
  })
912
1416
  });
913
1417
  }
914
1418
  //#endregion
1419
+ exports.Agenda = Agenda;
915
1420
  exports.Calendar = Calendar;
916
1421
  exports.CalendarThemeProvider = CalendarThemeProvider;
917
1422
  exports.DEFAULT_HOUR_HEIGHT = DEFAULT_HOUR_HEIGHT;
@@ -922,6 +1427,7 @@ exports.TimeGrid = TimeGrid;
922
1427
  exports.defaultTheme = defaultTheme;
923
1428
  exports.getIsToday = getIsToday;
924
1429
  exports.getWeekDays = getWeekDays;
1430
+ exports.isAllDayEvent = isAllDayEvent;
925
1431
  exports.isSameCalendarDay = isSameCalendarDay;
926
1432
  exports.isWeekend = isWeekend;
927
1433
  exports.layoutDayEvents = layoutDayEvents;