react-native-bigger-calendar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # react-native-bigger-calendar
2
+
3
+ A generic, themeable **month / week / day** calendar for React Native.
4
+
5
+ - πŸ“† Three views β€” month grid, week and day time-grids
6
+ - 🀏 Pinch-to-zoom on the week/day grid (UI thread, no re-renders)
7
+ - ♾️ Virtualized, snap-paging months/weeks/days via [`@legendapp/list`](https://legendapp.com/open-source/list/)
8
+ - 🧩 Bring-your-own event type (`CalendarEvent<T>`) and a `renderEvent` escape hatch
9
+ - 🎨 Fully themeable, with sensible defaults (no styling library required)
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install react-native-bigger-calendar
15
+ ```
16
+
17
+ ### Peer dependencies
18
+
19
+ This library relies on the following being installed in your app:
20
+
21
+ ```sh
22
+ npm install react-native-reanimated react-native-gesture-handler @legendapp/list
23
+ ```
24
+
25
+ Make sure Reanimated and Gesture Handler are set up per their own docs (Babel
26
+ plugin, `GestureHandlerRootView` at the root of your app).
27
+
28
+ ## Usage
29
+
30
+ ```tsx
31
+ import { useState } from 'react';
32
+ import { Calendar, type CalendarEvent } from 'react-native-bigger-calendar';
33
+
34
+ type MyEvent = { id: string; color: string };
35
+
36
+ const events: CalendarEvent<MyEvent>[] = [
37
+ {
38
+ id: '1',
39
+ color: '#1F6FEB',
40
+ title: 'Lecture',
41
+ start: new Date(2026, 5, 19, 10, 0),
42
+ end: new Date(2026, 5, 19, 11, 30),
43
+ },
44
+ ];
45
+
46
+ export function MyCalendar() {
47
+ const [mode, setMode] = useState<'month' | 'week' | 'day'>('week');
48
+ const [date, setDate] = useState(new Date());
49
+
50
+ return (
51
+ <Calendar
52
+ mode={mode}
53
+ date={date}
54
+ events={events}
55
+ weekStartsOn={1}
56
+ onChangeDate={setDate}
57
+ onPressEvent={(event) => console.log(event.id)}
58
+ onPressDay={(day) => {
59
+ setDate(day);
60
+ setMode('day');
61
+ }}
62
+ />
63
+ );
64
+ }
65
+ ```
66
+
67
+ ### Custom events
68
+
69
+ The built-in renderer draws a simple titled box. Pass `renderEvent` β€” **a React
70
+ component**, not a callback β€” to take full control. Because it's rendered as a
71
+ component, it may use hooks. On the week/day grid you also receive `boxHeight`, a
72
+ Reanimated shared value tracking the live pixel height of the box (driven by
73
+ pinch-zoom), so you can reveal detail progressively without re-rendering:
74
+
75
+ ```tsx
76
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
77
+ import { Pressable, Text } from 'react-native';
78
+ import type { RenderEventArgs } from 'react-native-bigger-calendar';
79
+
80
+ // Define the component once (don't inline it, or it remounts every render).
81
+ function MyEvent({ event, boxHeight, onPress }: RenderEventArgs<MyEvent>) {
82
+ const detailStyle = useAnimatedStyle(() => ({
83
+ display: (boxHeight?.value ?? Infinity) >= 84 ? 'flex' : 'none',
84
+ }));
85
+ return (
86
+ <Pressable style={{ flex: 1, backgroundColor: event.color }} onPress={onPress}>
87
+ <Text>{event.title}</Text>
88
+ <Animated.View style={detailStyle}>
89
+ <Text>{event.start.toLocaleTimeString()}</Text>
90
+ </Animated.View>
91
+ </Pressable>
92
+ );
93
+ }
94
+
95
+ <Calendar /* ... */ renderEvent={MyEvent} />;
96
+ ```
97
+
98
+ ### Theming
99
+
100
+ ```tsx
101
+ <Calendar
102
+ // ...
103
+ theme={{
104
+ colors: { todayBackground: '#E5484D', nowIndicator: '#E5484D' },
105
+ text: { dayNumber: { fontSize: 24, fontWeight: '800' } },
106
+ }}
107
+ />
108
+ ```
109
+
110
+ See `CalendarTheme` for the full set of tokens. Anything you omit falls back to
111
+ `defaultTheme`.
112
+
113
+ ### Week/day grid options
114
+
115
+ ```tsx
116
+ <Calendar
117
+ mode="week"
118
+ // ...
119
+ minHour={7} // window the grid to 07:00–21:00
120
+ maxHour={21}
121
+ ampm // 12-hour hour labels ("7 AM")
122
+ onPressCell={(date) => createEventAt(date)} // tap empty space -> date+time
123
+ />
124
+ ```
125
+
126
+ - `minHour` / `maxHour` clamp the visible hours (defaults `0` / `24`); events and
127
+ the now-line outside the window are hidden, and the initial scroll is adjusted.
128
+ - `ampm` switches hour labels to 12-hour AM/PM (default 24h).
129
+ - `onPressCell(date)` fires when empty grid space is tapped, with the date+time
130
+ under the touch β€” handy for "create event". (Event taps still go to `onPressEvent`.)
131
+ - `freeSwipe` (default `false`) controls paging: by default one day/week/month
132
+ moves per swipe; set it to allow a fling to carry across several pages (still
133
+ snapping to a page boundary). Applies to all modes.
134
+
135
+ ## Components
136
+
137
+ `<Calendar>` is the batteries-included entry point. The building blocks it wraps
138
+ are also exported for advanced layouts:
139
+
140
+ | Export | Description |
141
+ | --- | --- |
142
+ | `Calendar` | Top-level component; switches between month/week/day. |
143
+ | `MonthView` | A single month grid. |
144
+ | `MonthPager` | Horizontally-paged, virtualized months. |
145
+ | `TimeGrid` | Paged, pinch-zoomable week/day time-grid. |
146
+ | `DefaultEvent` | The built-in event renderer. |
147
+ | `useCalendarTheme` | Read the active theme inside a custom renderer. |
148
+
149
+ ## Notes & limitations
150
+
151
+ - **Multi-day events** are supported: pass one event and it appears on every day
152
+ it spans. On the week/day grid each day shows the clipped segment (so a
153
+ 23:00β†’01:00 event renders 23:00–24:00, then 00:00–01:00), and `renderEvent`
154
+ receives `continuesBefore`/`continuesAfter` so you can draw continuation hints.
155
+ A dedicated all-day lane (and an explicit `allDay` flag) is not yet provided.
156
+ - **`weekStartsOn` defaults to `0` (Sunday).** Pass `1` for Monday-first.
157
+ - **Controlled `date`.** The calendar is controlled: echo `onChangeDate` back
158
+ into the `date` prop, or paging and the "today" realign won't track.
159
+ - **External `cellHeight`.** If you own `cellHeight`, drive zoom through the
160
+ pinch gesture. Programmatic writes outside a pinch won't propagate to
161
+ off-screen pages until the next gesture settles.
162
+ - **Stable props.** Pass stable `renderEvent`/`keyExtractor`/`on*` references
163
+ (module scope or `useCallback`) so the memoized inner views can skip renders.
164
+
165
+ ## Example app
166
+
167
+ A runnable Expo demo lives in [`example/`](./example) β€” month/week/day modes, a
168
+ multi-day event, drill-into-day on tap, and one-page paging.
169
+
170
+ ```sh
171
+ cd example
172
+ npm install
173
+ npx expo run:ios # or: npx expo run:android
174
+ ```
175
+
176
+ It consumes the library straight from `../src` (via the example's
177
+ `metro.config.js`), so edits to the package hot-reload into the demo. A custom
178
+ dev build is required (Reanimated worklets aren't available in Expo Go).
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,302 @@
1
+ import { ComponentType } from "react";
2
+ import { SharedValue, useSharedValue } from "react-native-reanimated";
3
+ import { TextStyle } from "react-native";
4
+
5
+ //#region src/theme.d.ts
6
+ /**
7
+ * The full set of colours, text styles and metrics the calendar paints with.
8
+ * Supply a `Partial<CalendarTheme>` to `<Calendar theme={...} />`; missing keys
9
+ * fall back to {@link defaultTheme}, so you only override what you care about.
10
+ */
11
+ interface CalendarTheme {
12
+ colors: {
13
+ /** Hour lines, day separators and month-cell borders. */gridLine: string; /** Background tint behind weekend columns/cells. */
14
+ weekendBackground: string; /** Fill of the "today" badge (and any today highlight). */
15
+ todayBackground: string; /** Text on top of the today badge. */
16
+ todayText: string; /** The current-time indicator line on the week/day grid. */
17
+ nowIndicator: string; /** Default text colour (day numbers, weekday labels). */
18
+ text: string; /** Muted text (hour labels, "+N more"). */
19
+ textMuted: string; /** Dimmed text for days outside the current month. */
20
+ textDisabled: string; /** Background of the built-in default event box. */
21
+ eventBackground: string; /** Text colour inside the built-in default event box. */
22
+ eventText: string;
23
+ };
24
+ text: {
25
+ /** Large day number in the week/day header. */dayNumber: TextStyle; /** Short weekday label ("Mon") in headers. */
26
+ weekday: TextStyle; /** Date number inside a month cell. */
27
+ dateCell: TextStyle; /** Hour labels down the left of the time grid. */
28
+ hourLabel: TextStyle; /** The "+N more" overflow label in month cells. */
29
+ more: TextStyle; /** Title inside the built-in default event box. */
30
+ eventTitle: TextStyle;
31
+ };
32
+ /** Corner radius of the today badge. Use a large value for a circle. */
33
+ todayBadgeRadius: number;
34
+ }
35
+ declare const defaultTheme: CalendarTheme;
36
+ /** Deep-merge a partial theme over {@link defaultTheme}. */
37
+ declare function mergeTheme(theme?: PartialCalendarTheme): CalendarTheme;
38
+ type PartialCalendarTheme = {
39
+ colors?: Partial<CalendarTheme['colors']>;
40
+ text?: Partial<CalendarTheme['text']>;
41
+ todayBadgeRadius?: number;
42
+ };
43
+ declare const CalendarThemeProvider: import("react").Provider<CalendarTheme>;
44
+ declare const useCalendarTheme: () => CalendarTheme;
45
+ //#endregion
46
+ //#region src/types.d.ts
47
+ type CalendarMode = 'day' | 'week' | 'month';
48
+ /**
49
+ * The minimal shape every calendar event must have. Layout (positioning,
50
+ * overlap resolution, paging) only ever reads `start`/`end`; `title` is used by
51
+ * the built-in default renderer. Anything else lives in your own type and is
52
+ * threaded through untouched via the `T` generic.
53
+ */
54
+ interface ICalendarEvent {
55
+ start: Date;
56
+ end: Date;
57
+ title?: string;
58
+ }
59
+ /**
60
+ * An event carrying arbitrary extra fields `T` alongside the required shape.
61
+ * `ICalendarEvent` is authoritative: keys it reserves (`start`/`end`/`title`)
62
+ * cannot be re-typed by `T`.
63
+ */
64
+ type CalendarEvent<T = unknown> = ICalendarEvent & Omit<T, keyof ICalendarEvent>;
65
+ type RenderEventArgs<T = unknown> = {
66
+ event: CalendarEvent<T>;
67
+ mode: CalendarMode;
68
+ /**
69
+ * Live pixel height of the event box on the week/day grid, driven on the UI
70
+ * thread by pinch-to-zoom. Use it to reveal detail progressively as the box
71
+ * grows. `undefined` in month mode, where events render at a fixed size.
72
+ */
73
+ boxHeight?: SharedValue<number>;
74
+ /**
75
+ * On the week/day grid, true when this is a clipped segment of a multi-day
76
+ * event that started on an earlier day / continues onto a later day. Lets a
77
+ * renderer draw "continues" affordances. `undefined` in month mode.
78
+ */
79
+ continuesBefore?: boolean;
80
+ continuesAfter?: boolean;
81
+ onPress: () => void;
82
+ };
83
+ /**
84
+ * A component that renders a single event. It is rendered as a real component
85
+ * (not called as a function), so it may safely use hooks β€” including Reanimated
86
+ * hooks driven by `boxHeight`. Render an element that fills its container
87
+ * (`flex: 1`); the calendar positions and sizes the wrapping box for you.
88
+ */
89
+ type RenderEvent<T = unknown> = ComponentType<RenderEventArgs<T>>;
90
+ /** Build a stable key for an event. Defaults to start-time + index. */
91
+ type EventKeyExtractor<T = unknown> = (event: CalendarEvent<T>, index: number) => string;
92
+ /** Sunday = 0 … Saturday = 6, matching `Date.prototype.getDay()`. */
93
+ type WeekStartsOn = 0 | 1 | 2 | 3 | 4 | 5 | 6;
94
+ //#endregion
95
+ //#region src/components/Calendar.d.ts
96
+ type CalendarProps<T> = {
97
+ events: CalendarEvent<T>[];
98
+ mode: CalendarMode;
99
+ date: Date;
100
+ onChangeDate: (date: Date) => void;
101
+ onPressEvent: (event: CalendarEvent<T>) => void; /** Tap a day cell (month mode) β€” e.g. drill into the day view. */
102
+ onPressDay?: (date: Date) => void; /** Tap the "+N more" overflow label in a month cell. */
103
+ onPressMore?: (events: CalendarEvent<T>[], date: Date) => void; /** Tap empty space on the week/day grid; receives the date+time pressed. */
104
+ onPressCell?: (date: Date) => void; /** Max events shown per month cell before they collapse into "+N more". */
105
+ maxVisibleEventCount?: number; /** First day of the week. Sunday = 0 (default) … Saturday = 6. */
106
+ weekStartsOn?: WeekStartsOn; /** Replace the built-in event box. Return a `flex: 1` element. */
107
+ renderEvent?: RenderEvent<T>; /** Stable key per event. Defaults to start-time + index. */
108
+ keyExtractor?: EventKeyExtractor<T>; /** Partial theme merged over the defaults. */
109
+ theme?: PartialCalendarTheme; /** Externally-owned per-hour row height (week/day). Created internally if omitted. */
110
+ cellHeight?: ReturnType<typeof useSharedValue<number>>; /** Initial per-hour row height in px (week/day). Default 64. */
111
+ hourHeight?: number;
112
+ minHourHeight?: number;
113
+ maxHourHeight?: number;
114
+ hourColumnWidth?: number; /** First hour shown on the week/day grid (0–23). Default 0. */
115
+ minHour?: number; /** Last hour shown on the week/day grid, exclusive (1–24). Default 24. */
116
+ maxHour?: number; /** Show hour labels in 12-hour AM/PM form. Default false (24h). */
117
+ ampm?: boolean; /** Initial vertical scroll, in minutes from midnight (week/day). */
118
+ scrollOffsetMinutes?: number; /** Show the current-time line on the week/day grid. Default true. */
119
+ showNowIndicator?: boolean; /** BCP-47 locale for weekday labels. Defaults to the device locale. */
120
+ locale?: string;
121
+ /**
122
+ * Allow a fling to carry across several pages before snapping. Default false:
123
+ * one day/week/month per swipe.
124
+ */
125
+ freeSwipe?: boolean; /** Custom header above the week/day grid. Receives the visible days. */
126
+ renderTimeGridHeader?: (days: Date[]) => React.ReactNode;
127
+ };
128
+ declare function Calendar<T>({
129
+ events,
130
+ mode,
131
+ date,
132
+ onChangeDate,
133
+ onPressEvent,
134
+ onPressDay,
135
+ onPressMore,
136
+ onPressCell,
137
+ maxVisibleEventCount,
138
+ weekStartsOn,
139
+ renderEvent,
140
+ keyExtractor,
141
+ theme,
142
+ cellHeight: cellHeightProp,
143
+ hourHeight,
144
+ minHourHeight,
145
+ maxHourHeight,
146
+ hourColumnWidth,
147
+ minHour,
148
+ maxHour,
149
+ ampm,
150
+ scrollOffsetMinutes,
151
+ showNowIndicator,
152
+ locale,
153
+ freeSwipe,
154
+ renderTimeGridHeader
155
+ }: CalendarProps<T>): import("react").JSX.Element;
156
+ //#endregion
157
+ //#region src/components/MonthView.d.ts
158
+ type MonthViewProps<T> = {
159
+ date: Date;
160
+ events: CalendarEvent<T>[];
161
+ maxVisibleEventCount: number;
162
+ weekStartsOn: WeekStartsOn;
163
+ renderEvent: RenderEvent<T>;
164
+ keyExtractor: EventKeyExtractor<T>;
165
+ onPressDay?: (date: Date) => void;
166
+ onPressEvent: (event: CalendarEvent<T>) => void;
167
+ onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
168
+ };
169
+ declare function MonthViewInner<T>({
170
+ date,
171
+ events,
172
+ maxVisibleEventCount,
173
+ weekStartsOn,
174
+ renderEvent,
175
+ keyExtractor,
176
+ onPressDay,
177
+ onPressEvent,
178
+ onPressMore
179
+ }: MonthViewProps<T>): import("react").JSX.Element;
180
+ declare const MonthView: typeof MonthViewInner;
181
+ //#endregion
182
+ //#region src/components/MonthPager.d.ts
183
+ type MonthPagerProps<T> = {
184
+ date: Date;
185
+ events: CalendarEvent<T>[];
186
+ maxVisibleEventCount: number;
187
+ weekStartsOn: WeekStartsOn;
188
+ renderEvent: RenderEvent<T>;
189
+ keyExtractor: EventKeyExtractor<T>;
190
+ onPressDay?: (date: Date) => void;
191
+ onPressEvent: (event: CalendarEvent<T>) => void;
192
+ onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
193
+ onChangeDate: (date: Date) => void;
194
+ freeSwipe?: boolean;
195
+ };
196
+ declare function MonthPagerInner<T>({
197
+ date,
198
+ events,
199
+ maxVisibleEventCount,
200
+ weekStartsOn,
201
+ renderEvent,
202
+ keyExtractor,
203
+ onPressDay,
204
+ onPressEvent,
205
+ onPressMore,
206
+ onChangeDate,
207
+ freeSwipe
208
+ }: MonthPagerProps<T>): import("react").JSX.Element;
209
+ declare const MonthPager: typeof MonthPagerInner;
210
+ //#endregion
211
+ //#region src/components/TimeGrid.d.ts
212
+ declare const DEFAULT_HOUR_HEIGHT = 64;
213
+ type TimeGridProps<T> = {
214
+ mode: 'day' | 'week';
215
+ date: Date;
216
+ events: CalendarEvent<T>[];
217
+ cellHeight: SharedValue<number>; /** Initial per-hour row height in px; seeds scroll/zoom without reading the shared value during render. */
218
+ hourHeight?: number;
219
+ weekStartsOn: WeekStartsOn;
220
+ renderEvent: RenderEvent<T>;
221
+ keyExtractor: EventKeyExtractor<T>;
222
+ scrollOffsetMinutes?: number;
223
+ hourColumnWidth?: number; /** First hour shown (0–23). Default 0. */
224
+ minHour?: number; /** Last hour shown, exclusive (1–24). Default 24. */
225
+ maxHour?: number; /** Show hour labels in 12-hour AM/PM form. Default false (24h). */
226
+ ampm?: boolean;
227
+ minHourHeight?: number;
228
+ maxHourHeight?: number;
229
+ showNowIndicator?: boolean;
230
+ locale?: string;
231
+ freeSwipe?: boolean;
232
+ onPressEvent: (event: CalendarEvent<T>) => void;
233
+ onPressCell?: (date: Date) => void;
234
+ onChangeDate: (date: Date) => void; /** Optional header above the grid (e.g. weekday labels). Rendered full-width. */
235
+ renderHeader?: (days: Date[]) => React.ReactNode;
236
+ };
237
+ declare function TimeGridInner<T>({
238
+ mode,
239
+ date,
240
+ events,
241
+ cellHeight,
242
+ hourHeight,
243
+ weekStartsOn,
244
+ renderEvent,
245
+ keyExtractor,
246
+ scrollOffsetMinutes,
247
+ hourColumnWidth,
248
+ minHour,
249
+ maxHour,
250
+ ampm,
251
+ minHourHeight,
252
+ maxHourHeight,
253
+ showNowIndicator,
254
+ locale,
255
+ freeSwipe,
256
+ onPressEvent,
257
+ onPressCell,
258
+ onChangeDate,
259
+ renderHeader
260
+ }: TimeGridProps<T>): import("react").JSX.Element;
261
+ declare const TimeGrid: typeof TimeGridInner;
262
+ //#endregion
263
+ //#region src/components/DefaultEvent.d.ts
264
+ /**
265
+ * The built-in event renderer: a filled, rounded box showing the event title
266
+ * and (on the day/week grid) its time range. Pass your own `renderEvent` to
267
+ * `<Calendar>` to replace it entirely.
268
+ */
269
+ declare function DefaultEvent<T>({
270
+ event,
271
+ mode,
272
+ onPress
273
+ }: RenderEventArgs<T>): import("react").JSX.Element;
274
+ //#endregion
275
+ //#region src/utils/dates.d.ts
276
+ /** The seven dates of the week containing `date`, starting on `weekStartsOn`. */
277
+ declare const getWeekDays: (date: Date, weekStartsOn: WeekStartsOn) => Date[];
278
+ declare const isWeekend: (date: Date) => boolean;
279
+ declare const getIsToday: (date: Date) => boolean;
280
+ declare const isSameCalendarDay: (a: Date, b: Date) => boolean;
281
+ /** Minutes elapsed since midnight (0–1439). */
282
+ declare const minutesIntoDay: (date: Date) => number;
283
+ //#endregion
284
+ //#region src/utils/layout.d.ts
285
+ type PositionedEvent<T> = {
286
+ event: CalendarEvent<T>; /** Hours from midnight to the event's segment start on this day (fractional). */
287
+ startHours: number; /** Segment duration in hours on this day (clamped to a small minimum). */
288
+ durationHours: number; /** Zero-based column index within its overlap cluster. */
289
+ column: number; /** Total columns in this event's overlap cluster. */
290
+ columns: number; /** True when the segment is clipped because the event continues before/after this day. */
291
+ continuesBefore: boolean;
292
+ continuesAfter: boolean;
293
+ };
294
+ /**
295
+ * Lay out a single day's events: events that overlap in time are split into
296
+ * side-by-side columns. Multi-day events are clipped to the portion that falls
297
+ * on `day` (e.g. a 23:00β†’01:00 event renders 23:00–24:00 on the start day and
298
+ * 00:00–01:00 on the next). Pure β€” safe to call per render, never per frame.
299
+ */
300
+ declare function layoutDayEvents<T>(events: CalendarEvent<T>[], day: Date): PositionedEvent<T>[];
301
+ //#endregion
302
+ export { Calendar, type CalendarEvent, type CalendarMode, type CalendarProps, type CalendarTheme, CalendarThemeProvider, DEFAULT_HOUR_HEIGHT, DefaultEvent, type EventKeyExtractor, type ICalendarEvent, MonthPager, type MonthPagerProps, MonthView, type MonthViewProps, type PartialCalendarTheme, type PositionedEvent, type RenderEvent, type RenderEventArgs, TimeGrid, type TimeGridProps, type WeekStartsOn, defaultTheme, getIsToday, getWeekDays, isSameCalendarDay, isWeekend, layoutDayEvents, mergeTheme, minutesIntoDay, useCalendarTheme };