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/src/index.tsx ADDED
@@ -0,0 +1,30 @@
1
+ export { Calendar, type CalendarProps } from './components/Calendar';
2
+ export { MonthView, type MonthViewProps } from './components/MonthView';
3
+ export { MonthPager, type MonthPagerProps } from './components/MonthPager';
4
+ export { TimeGrid, type TimeGridProps, DEFAULT_HOUR_HEIGHT } from './components/TimeGrid';
5
+ export { DefaultEvent } from './components/DefaultEvent';
6
+ export {
7
+ type CalendarTheme,
8
+ type PartialCalendarTheme,
9
+ defaultTheme,
10
+ mergeTheme,
11
+ CalendarThemeProvider,
12
+ useCalendarTheme,
13
+ } from './theme';
14
+ export type {
15
+ CalendarEvent,
16
+ CalendarMode,
17
+ EventKeyExtractor,
18
+ ICalendarEvent,
19
+ RenderEvent,
20
+ RenderEventArgs,
21
+ WeekStartsOn,
22
+ } from './types';
23
+ export {
24
+ getWeekDays,
25
+ getIsToday,
26
+ isWeekend,
27
+ isSameCalendarDay,
28
+ minutesIntoDay,
29
+ } from './utils/dates';
30
+ export { layoutDayEvents, type PositionedEvent } from './utils/layout';
package/src/theme.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * The full set of colours, text styles and metrics the calendar paints with.
6
+ * Supply a `Partial<CalendarTheme>` to `<Calendar theme={...} />`; missing keys
7
+ * fall back to {@link defaultTheme}, so you only override what you care about.
8
+ */
9
+ export interface CalendarTheme {
10
+ colors: {
11
+ /** Hour lines, day separators and month-cell borders. */
12
+ gridLine: string;
13
+ /** Background tint behind weekend columns/cells. */
14
+ weekendBackground: string;
15
+ /** Fill of the "today" badge (and any today highlight). */
16
+ todayBackground: string;
17
+ /** Text on top of the today badge. */
18
+ todayText: string;
19
+ /** The current-time indicator line on the week/day grid. */
20
+ nowIndicator: string;
21
+ /** Default text colour (day numbers, weekday labels). */
22
+ text: string;
23
+ /** Muted text (hour labels, "+N more"). */
24
+ textMuted: string;
25
+ /** Dimmed text for days outside the current month. */
26
+ textDisabled: string;
27
+ /** Background of the built-in default event box. */
28
+ eventBackground: string;
29
+ /** Text colour inside the built-in default event box. */
30
+ eventText: string;
31
+ };
32
+ text: {
33
+ /** Large day number in the week/day header. */
34
+ dayNumber: TextStyle;
35
+ /** Short weekday label ("Mon") in headers. */
36
+ weekday: TextStyle;
37
+ /** Date number inside a month cell. */
38
+ dateCell: TextStyle;
39
+ /** Hour labels down the left of the time grid. */
40
+ hourLabel: TextStyle;
41
+ /** The "+N more" overflow label in month cells. */
42
+ more: TextStyle;
43
+ /** Title inside the built-in default event box. */
44
+ eventTitle: TextStyle;
45
+ };
46
+ /** Corner radius of the today badge. Use a large value for a circle. */
47
+ todayBadgeRadius: number;
48
+ }
49
+
50
+ export const defaultTheme: CalendarTheme = {
51
+ colors: {
52
+ gridLine: '#E2E4E9',
53
+ weekendBackground: '#F6F7F9',
54
+ todayBackground: '#1F6FEB',
55
+ todayText: '#FFFFFF',
56
+ nowIndicator: '#E5484D',
57
+ text: '#1A1B1E',
58
+ textMuted: '#6B7280',
59
+ textDisabled: '#B5B9C0',
60
+ eventBackground: '#DCE7FF',
61
+ eventText: '#1A1B1E',
62
+ },
63
+ text: {
64
+ dayNumber: { fontSize: 22, fontWeight: '700' },
65
+ weekday: { fontSize: 13, fontWeight: '700' },
66
+ dateCell: { fontSize: 13, fontWeight: '700' },
67
+ hourLabel: { fontSize: 12 },
68
+ more: { fontSize: 11, fontWeight: '700' },
69
+ eventTitle: { fontSize: 12, fontWeight: '700' },
70
+ },
71
+ todayBadgeRadius: 999,
72
+ };
73
+
74
+ /** Deep-merge a partial theme over {@link defaultTheme}. */
75
+ export function mergeTheme(theme?: PartialCalendarTheme): CalendarTheme {
76
+ if (!theme) return defaultTheme;
77
+ return {
78
+ colors: { ...defaultTheme.colors, ...theme.colors },
79
+ text: { ...defaultTheme.text, ...theme.text },
80
+ todayBadgeRadius: theme.todayBadgeRadius ?? defaultTheme.todayBadgeRadius,
81
+ };
82
+ }
83
+
84
+ export type PartialCalendarTheme = {
85
+ colors?: Partial<CalendarTheme['colors']>;
86
+ text?: Partial<CalendarTheme['text']>;
87
+ todayBadgeRadius?: number;
88
+ };
89
+
90
+ const CalendarThemeContext = createContext<CalendarTheme>(defaultTheme);
91
+
92
+ export const CalendarThemeProvider = CalendarThemeContext.Provider;
93
+
94
+ export const useCalendarTheme = () => useContext(CalendarThemeContext);
package/src/types.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
4
+ export type CalendarMode = 'day' | 'week' | 'month';
5
+
6
+ /**
7
+ * The minimal shape every calendar event must have. Layout (positioning,
8
+ * overlap resolution, paging) only ever reads `start`/`end`; `title` is used by
9
+ * the built-in default renderer. Anything else lives in your own type and is
10
+ * threaded through untouched via the `T` generic.
11
+ */
12
+ export interface ICalendarEvent {
13
+ start: Date;
14
+ end: Date;
15
+ title?: string;
16
+ }
17
+
18
+ /**
19
+ * An event carrying arbitrary extra fields `T` alongside the required shape.
20
+ * `ICalendarEvent` is authoritative: keys it reserves (`start`/`end`/`title`)
21
+ * cannot be re-typed by `T`.
22
+ */
23
+ export type CalendarEvent<T = unknown> = ICalendarEvent & Omit<T, keyof ICalendarEvent>;
24
+
25
+ export type RenderEventArgs<T = unknown> = {
26
+ event: CalendarEvent<T>;
27
+ mode: CalendarMode;
28
+ /**
29
+ * Live pixel height of the event box on the week/day grid, driven on the UI
30
+ * thread by pinch-to-zoom. Use it to reveal detail progressively as the box
31
+ * grows. `undefined` in month mode, where events render at a fixed size.
32
+ */
33
+ boxHeight?: SharedValue<number>;
34
+ /**
35
+ * On the week/day grid, true when this is a clipped segment of a multi-day
36
+ * event that started on an earlier day / continues onto a later day. Lets a
37
+ * renderer draw "continues" affordances. `undefined` in month mode.
38
+ */
39
+ continuesBefore?: boolean;
40
+ continuesAfter?: boolean;
41
+ onPress: () => void;
42
+ };
43
+
44
+ /**
45
+ * A component that renders a single event. It is rendered as a real component
46
+ * (not called as a function), so it may safely use hooks — including Reanimated
47
+ * hooks driven by `boxHeight`. Render an element that fills its container
48
+ * (`flex: 1`); the calendar positions and sizes the wrapping box for you.
49
+ */
50
+ export type RenderEvent<T = unknown> = ComponentType<RenderEventArgs<T>>;
51
+
52
+ /** Build a stable key for an event. Defaults to start-time + index. */
53
+ export type EventKeyExtractor<T = unknown> = (event: CalendarEvent<T>, index: number) => string;
54
+
55
+ /** Sunday = 0 … Saturday = 6, matching `Date.prototype.getDay()`. */
56
+ export type WeekStartsOn = 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -0,0 +1,20 @@
1
+ import { addDays, getHours, getMinutes, isSameDay, isToday, startOfWeek } from 'date-fns';
2
+ import type { WeekStartsOn } from '../types';
3
+
4
+ /** The seven dates of the week containing `date`, starting on `weekStartsOn`. */
5
+ export const getWeekDays = (date: Date, weekStartsOn: WeekStartsOn): Date[] => {
6
+ const start = startOfWeek(date, { weekStartsOn });
7
+ return Array.from({ length: 7 }, (_, index) => addDays(start, index));
8
+ };
9
+
10
+ export const isWeekend = (date: Date): boolean => {
11
+ const day = date.getDay();
12
+ return day === 0 || day === 6;
13
+ };
14
+
15
+ export const getIsToday = (date: Date): boolean => isToday(date);
16
+
17
+ export const isSameCalendarDay = (a: Date, b: Date): boolean => isSameDay(a, b);
18
+
19
+ /** Minutes elapsed since midnight (0–1439). */
20
+ export const minutesIntoDay = (date: Date): number => getHours(date) * 60 + getMinutes(date);
@@ -0,0 +1,119 @@
1
+ import { addDays, differenceInMinutes, max as maxDate, min as minDate, startOfDay } from 'date-fns';
2
+ import type { CalendarEvent } from '../types';
3
+
4
+ const MINUTES_PER_HOUR = 60;
5
+ // Minimum duration (in hours) a positioned event is given, so a zero/negative
6
+ // span still occupies a sliver rather than collapsing to nothing.
7
+ const MIN_DURATION_HOURS = 0.25;
8
+
9
+ export type PositionedEvent<T> = {
10
+ event: CalendarEvent<T>;
11
+ /** Hours from midnight to the event's segment start on this day (fractional). */
12
+ startHours: number;
13
+ /** Segment duration in hours on this day (clamped to a small minimum). */
14
+ durationHours: number;
15
+ /** Zero-based column index within its overlap cluster. */
16
+ column: number;
17
+ /** Total columns in this event's overlap cluster. */
18
+ columns: number;
19
+ /** True when the segment is clipped because the event continues before/after this day. */
20
+ continuesBefore: boolean;
21
+ continuesAfter: boolean;
22
+ };
23
+
24
+ type Segment<T> = {
25
+ event: CalendarEvent<T>;
26
+ start: number;
27
+ end: number;
28
+ continuesBefore: boolean;
29
+ continuesAfter: boolean;
30
+ };
31
+
32
+ /**
33
+ * Lay out a single day's events: events that overlap in time are split into
34
+ * side-by-side columns. Multi-day events are clipped to the portion that falls
35
+ * on `day` (e.g. a 23:00→01:00 event renders 23:00–24:00 on the start day and
36
+ * 00:00–01:00 on the next). Pure — safe to call per render, never per frame.
37
+ */
38
+ export function layoutDayEvents<T>(
39
+ events: CalendarEvent<T>[],
40
+ day: Date,
41
+ ): PositionedEvent<T>[] {
42
+ const dayStart = startOfDay(day);
43
+ const nextDayStart = addDays(dayStart, 1);
44
+
45
+ const segments: Segment<T>[] = events
46
+ // Overlaps this day if it starts before the day ends and ends after it begins.
47
+ .filter((event) => event.start < nextDayStart && event.end > dayStart)
48
+ .map((event) => {
49
+ const segStart = maxDate([event.start, dayStart]);
50
+ const segEnd = minDate([event.end, nextDayStart]);
51
+ return {
52
+ event,
53
+ start: differenceInMinutes(segStart, dayStart) / MINUTES_PER_HOUR,
54
+ end: differenceInMinutes(segEnd, dayStart) / MINUTES_PER_HOUR,
55
+ continuesBefore: event.start < dayStart,
56
+ continuesAfter: event.end > nextDayStart,
57
+ };
58
+ })
59
+ .sort((a, b) => a.start - b.start);
60
+
61
+ const positioned: PositionedEvent<T>[] = [];
62
+ let cluster: Segment<T>[] = [];
63
+ let clusterEnd = Number.NEGATIVE_INFINITY;
64
+
65
+ const flushCluster = () => {
66
+ const columnEnds: number[] = [];
67
+ const columnOf = new Map<Segment<T>, number>();
68
+ for (const seg of cluster) {
69
+ let column = columnEnds.findIndex((end) => end <= seg.start);
70
+ if (column === -1) {
71
+ column = columnEnds.length;
72
+ columnEnds.push(seg.end);
73
+ } else {
74
+ columnEnds[column] = seg.end;
75
+ }
76
+ columnOf.set(seg, column);
77
+ }
78
+ for (const seg of cluster) {
79
+ positioned.push({
80
+ event: seg.event,
81
+ startHours: seg.start,
82
+ durationHours: Math.max(seg.end - seg.start, MIN_DURATION_HOURS),
83
+ column: columnOf.get(seg) ?? 0,
84
+ columns: columnEnds.length,
85
+ continuesBefore: seg.continuesBefore,
86
+ continuesAfter: seg.continuesAfter,
87
+ });
88
+ }
89
+ cluster = [];
90
+ };
91
+
92
+ for (const seg of segments) {
93
+ if (cluster.length > 0 && seg.start >= clusterEnd) flushCluster();
94
+ cluster.push(seg);
95
+ clusterEnd = Math.max(clusterEnd, seg.end);
96
+ }
97
+ if (cluster.length > 0) flushCluster();
98
+
99
+ return positioned;
100
+ }
101
+
102
+ /**
103
+ * The `startOfDay` ISO keys of every calendar day an event touches (inclusive).
104
+ * An event ending exactly at midnight does not count the following day. Used to
105
+ * index events by day for the month grid. Pure.
106
+ */
107
+ export function eventDayKeys<T>(event: CalendarEvent<T>): string[] {
108
+ const first = startOfDay(event.start);
109
+ // The last instant the event occupies; an end of exactly midnight belongs to
110
+ // the previous day.
111
+ const lastInstant = event.end > event.start ? new Date(event.end.getTime() - 1) : event.start;
112
+ const last = startOfDay(lastInstant);
113
+
114
+ const keys: string[] = [];
115
+ for (let cursor = first; cursor <= last; cursor = addDays(cursor, 1)) {
116
+ keys.push(cursor.toISOString());
117
+ }
118
+ return keys;
119
+ }