@lazerlen/legend-calendar 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.eslintrc.js +29 -0
  2. package/.turbo/turbo-build.log +19 -0
  3. package/.turbo/turbo-lint.log +14 -0
  4. package/.turbo/turbo-test.log +261 -0
  5. package/.turbo/turbo-typecheck.log +1 -0
  6. package/CHANGELOG.md +127 -0
  7. package/dist/index.d.mts +679 -0
  8. package/dist/index.d.ts +679 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +47 -0
  14. package/src/components/Calendar.stories.tsx +226 -0
  15. package/src/components/Calendar.tsx +224 -0
  16. package/src/components/CalendarItemDay.tsx +385 -0
  17. package/src/components/CalendarItemEmpty.tsx +30 -0
  18. package/src/components/CalendarItemWeekName.tsx +67 -0
  19. package/src/components/CalendarList.stories.tsx +326 -0
  20. package/src/components/CalendarList.tsx +373 -0
  21. package/src/components/CalendarRowMonth.tsx +62 -0
  22. package/src/components/CalendarRowWeek.tsx +46 -0
  23. package/src/components/CalendarThemeProvider.tsx +43 -0
  24. package/src/components/HStack.tsx +67 -0
  25. package/src/components/VStack.tsx +67 -0
  26. package/src/components/index.ts +108 -0
  27. package/src/developer/decorators.tsx +54 -0
  28. package/src/developer/loggginHandler.tsx +6 -0
  29. package/src/developer/useRenderCount.ts +7 -0
  30. package/src/helpers/dates.test.ts +567 -0
  31. package/src/helpers/dates.ts +163 -0
  32. package/src/helpers/functions.ts +327 -0
  33. package/src/helpers/numbers.ts +11 -0
  34. package/src/helpers/strings.ts +2 -0
  35. package/src/helpers/tokens.ts +71 -0
  36. package/src/helpers/types.ts +3 -0
  37. package/src/hooks/useCalendar.test.ts +381 -0
  38. package/src/hooks/useCalendar.ts +351 -0
  39. package/src/hooks/useCalendarList.test.ts +382 -0
  40. package/src/hooks/useCalendarList.tsx +291 -0
  41. package/src/hooks/useDateRange.test.ts +128 -0
  42. package/src/hooks/useDateRange.ts +94 -0
  43. package/src/hooks/useOptimizedDayMetadata.test.ts +582 -0
  44. package/src/hooks/useOptimizedDayMetadata.ts +93 -0
  45. package/src/hooks/useTheme.ts +14 -0
  46. package/src/index.ts +24 -0
  47. package/tsconfig.json +13 -0
  48. package/tsup.config.ts +15 -0
@@ -0,0 +1,224 @@
1
+ import { memo, useEffect } from "react";
2
+ import type { ColorSchemeName, PressableProps } from "react-native";
3
+
4
+ import type {
5
+ CalendarItemDayContainerProps,
6
+ CalendarItemDayProps,
7
+ } from "@/components/CalendarItemDay";
8
+ import {
9
+ CalendarItemDayContainer,
10
+ CalendarItemDayWithContainer,
11
+ } from "@/components/CalendarItemDay";
12
+ import type { CalendarItemEmptyProps } from "@/components/CalendarItemEmpty";
13
+ import { CalendarItemEmpty } from "@/components/CalendarItemEmpty";
14
+ import type { CalendarItemWeekNameProps } from "@/components/CalendarItemWeekName";
15
+ import { CalendarItemWeekName } from "@/components/CalendarItemWeekName";
16
+ import type { CalendarRowMonthProps } from "@/components/CalendarRowMonth";
17
+ import { CalendarRowMonth } from "@/components/CalendarRowMonth";
18
+ import type { CalendarRowWeekProps } from "@/components/CalendarRowWeek";
19
+ import { CalendarRowWeek } from "@/components/CalendarRowWeek";
20
+ import { CalendarThemeProvider } from "@/components/CalendarThemeProvider";
21
+ import { VStack } from "@/components/VStack";
22
+ import { uppercaseFirstLetter } from "@/helpers/strings";
23
+ import type { BaseTheme } from "@/helpers/tokens";
24
+ import type { UseCalendarParams } from "@/hooks/useCalendar";
25
+ import { useCalendar } from "@/hooks/useCalendar";
26
+ import { activeDateRangesEmitter } from "@/hooks/useOptimizedDayMetadata";
27
+
28
+ export type PressableLike = React.ComponentType<
29
+ Pick<PressableProps, "children" | "style" | "disabled"> & {
30
+ onPress: () => void;
31
+ }
32
+ >;
33
+
34
+ export interface CalendarTheme {
35
+ rowMonth?: CalendarRowMonthProps["theme"];
36
+ rowWeek?: CalendarRowWeekProps["theme"];
37
+ itemWeekName?: CalendarItemWeekNameProps["theme"];
38
+ itemEmpty?: CalendarItemEmptyProps["theme"];
39
+ itemDayContainer?: CalendarItemDayContainerProps["theme"];
40
+ /**
41
+ * The theme for the day. `base` is applied before any state, allowing you to
42
+ * set a base value once and use it for all states.
43
+ */
44
+ itemDay?: CalendarItemDayProps["theme"];
45
+ }
46
+
47
+ export type CalendarOnDayPress = (dateId: string) => void;
48
+
49
+ export interface CalendarProps extends UseCalendarParams {
50
+ /**
51
+ * A unique identifier for this calendar instance. This is useful if you
52
+ * need to render more than one calendar at once. This allows Legend Calendar
53
+ * to scope its state to the given instance.
54
+ *
55
+ * No need to get fancy with `uuid` or anything like that - a simple static
56
+ * string is enough.
57
+ *
58
+ * If not provided, Legend Calendar will use a default value which will hoist
59
+ * the state in a global scope.
60
+ */
61
+ calendarInstanceId?: string;
62
+ /**
63
+ * The spacing between each calendar row (the month header, the week days row,
64
+ * and the weeks row)
65
+ * @defaultValue 8
66
+ */
67
+ calendarRowVerticalSpacing?: number;
68
+ /**
69
+ * The spacing between each day in the weeks row.
70
+ * @defaultValue 8
71
+ */
72
+ calendarRowHorizontalSpacing?: number;
73
+ /**
74
+ * The height of each day cell.
75
+ * @defaultValue 32
76
+ */
77
+ calendarDayHeight?: number;
78
+ /**
79
+ * The height of the week day's header.
80
+ * @defaultValue calendarDayHeight
81
+ */
82
+ calendarWeekHeaderHeight?: number;
83
+ /**
84
+ * The height of the month header.
85
+ * @defaultValue 20
86
+ */
87
+ calendarMonthHeaderHeight?: number;
88
+ /**
89
+ * When set, Legend Calendar will use this color scheme instead of the system's
90
+ * value (`light|dark`). This is useful if your app doesn't support dark-mode,
91
+ * for example.
92
+ *
93
+ * We don't advise using this prop - ideally, your app should reflect the
94
+ * user's preferences.
95
+ * @defaultValue undefined
96
+ */
97
+ calendarColorScheme?: ColorSchemeName;
98
+ /**
99
+ * The callback to be called when a day is pressed.
100
+ */
101
+ onCalendarDayPress: CalendarOnDayPress;
102
+ /** Theme to customize the calendar component. */
103
+ theme?: CalendarTheme;
104
+ /** Optional component to replace the default <Pressable> component. */
105
+ CalendarPressableComponent?: PressableLike;
106
+ }
107
+
108
+ const BaseCalendar = memo(function BaseCalendar(props: CalendarProps) {
109
+ const {
110
+ calendarInstanceId,
111
+ calendarRowVerticalSpacing = 8,
112
+ calendarRowHorizontalSpacing = 8,
113
+ calendarDayHeight = 32,
114
+ calendarMonthHeaderHeight = 20,
115
+ calendarWeekHeaderHeight = calendarDayHeight,
116
+ onCalendarDayPress,
117
+ theme,
118
+ CalendarPressableComponent,
119
+ ...buildCalendarParams
120
+ } = props;
121
+
122
+ const { calendarRowMonth, weeksList, weekDaysList } =
123
+ useCalendar(buildCalendarParams);
124
+
125
+ return (
126
+ <VStack
127
+ alignItems="center"
128
+ spacing={calendarRowVerticalSpacing as keyof BaseTheme["spacing"]}
129
+ >
130
+ <CalendarRowMonth
131
+ height={calendarMonthHeaderHeight}
132
+ theme={theme?.rowMonth}
133
+ >
134
+ {uppercaseFirstLetter(calendarRowMonth)}
135
+ </CalendarRowMonth>
136
+ <CalendarRowWeek spacing={8} theme={theme?.rowWeek}>
137
+ {weekDaysList.map((weekDay, i) => (
138
+ <CalendarItemWeekName
139
+ height={calendarWeekHeaderHeight}
140
+ key={i}
141
+ theme={theme?.itemWeekName}
142
+ >
143
+ {weekDay}
144
+ </CalendarItemWeekName>
145
+ ))}
146
+ </CalendarRowWeek>
147
+ {weeksList.map((week, index) => (
148
+ <CalendarRowWeek key={index}>
149
+ {week.map((dayProps) => {
150
+ if (dayProps.isDifferentMonth) {
151
+ return (
152
+ <CalendarItemDayContainer
153
+ dayHeight={calendarDayHeight}
154
+ daySpacing={calendarRowHorizontalSpacing}
155
+ isStartOfWeek={dayProps.isStartOfWeek}
156
+ key={dayProps.id}
157
+ metadata={dayProps}
158
+ theme={theme?.itemDayContainer}
159
+ >
160
+ <CalendarItemEmpty
161
+ height={calendarDayHeight}
162
+ theme={theme?.itemEmpty}
163
+ />
164
+ </CalendarItemDayContainer>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <CalendarItemDayWithContainer
170
+ CalendarPressableComponent={CalendarPressableComponent}
171
+ calendarInstanceId={calendarInstanceId}
172
+ containerTheme={theme?.itemDayContainer}
173
+ dayHeight={calendarDayHeight}
174
+ daySpacing={calendarRowHorizontalSpacing}
175
+ key={dayProps.id}
176
+ metadata={dayProps}
177
+ onPress={onCalendarDayPress}
178
+ theme={theme?.itemDay}
179
+ >
180
+ {dayProps.displayLabel}
181
+ </CalendarItemDayWithContainer>
182
+ );
183
+ })}
184
+ </CalendarRowWeek>
185
+ ))}
186
+ </VStack>
187
+ );
188
+ });
189
+
190
+ export const Calendar = memo(function Calendar(props: CalendarProps) {
191
+ const {
192
+ calendarInstanceId,
193
+ calendarActiveDateRanges,
194
+ calendarMonthId,
195
+ calendarColorScheme,
196
+ ...otherProps
197
+ } = props;
198
+ useEffect(() => {
199
+ activeDateRangesEmitter.emit("onSetActiveDateRanges", {
200
+ instanceId: calendarInstanceId,
201
+ ranges: calendarActiveDateRanges ?? [],
202
+ });
203
+ /**
204
+ * While `calendarMonthId` is not used by the effect, we still need it in
205
+ * the dependency array since [LegendList uses recycling
206
+ * internally](https://legendapp.com/open-source/list/).
207
+ *
208
+ * This means `Calendar` can re-render with different props instead of
209
+ * getting re-mounted. Without it, we would see staled/invalid data, as
210
+ * reported by
211
+ * [#11](https://github.com/MarceloPrado/flash-calendar/issues/11).
212
+ */
213
+ }, [calendarActiveDateRanges, calendarInstanceId, calendarMonthId]);
214
+
215
+ return (
216
+ <CalendarThemeProvider colorScheme={calendarColorScheme}>
217
+ <BaseCalendar
218
+ {...otherProps}
219
+ calendarInstanceId={calendarInstanceId}
220
+ calendarMonthId={calendarMonthId}
221
+ />
222
+ </CalendarThemeProvider>
223
+ );
224
+ });
@@ -0,0 +1,385 @@
1
+ import type { ReactNode } from "react";
2
+ import { useCallback, useMemo } from "react";
3
+ import type { TextProps, TextStyle, ViewStyle } from "react-native";
4
+ import { Pressable, StyleSheet, Text, View } from "react-native";
5
+
6
+ import type { BaseTheme } from "@/helpers/tokens";
7
+ import type { CalendarDayMetadata } from "@/hooks/useCalendar";
8
+ import { useOptimizedDayMetadata } from "@/hooks/useOptimizedDayMetadata";
9
+ import { useTheme } from "@/hooks/useTheme";
10
+
11
+ import type { PressableLike } from "./Calendar";
12
+
13
+ // react-native-web/overrides.ts
14
+ declare module "react-native" {
15
+ interface PressableStateCallbackType {
16
+ hovered?: boolean;
17
+ focused?: boolean;
18
+ }
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ baseContainer: {
23
+ padding: 6,
24
+ alignItems: "center",
25
+ justifyContent: "center",
26
+ borderRadius: 16,
27
+ flex: 1,
28
+ },
29
+ baseContent: {
30
+ textAlign: "center",
31
+ },
32
+ });
33
+
34
+ export type DayState = "idle" | "active" | "today" | "disabled";
35
+
36
+ interface DayTheme {
37
+ container: Omit<ViewStyle, "borderRadius">;
38
+ content: TextStyle;
39
+ }
40
+ type CalendarItemDayTheme = Record<
41
+ DayState,
42
+ (params: {
43
+ isStartOfRange: boolean;
44
+ isEndOfRange: boolean;
45
+ isPressed: boolean;
46
+ isHovered?: boolean;
47
+ isFocused?: boolean;
48
+ }) => DayTheme
49
+ >;
50
+
51
+ const buildBaseStyles = (theme: BaseTheme): CalendarItemDayTheme => {
52
+ const baseContent = {
53
+ ...styles.baseContent,
54
+ color: theme.colors.content.primary,
55
+ };
56
+
57
+ return {
58
+ active: ({ isPressed, isHovered, isStartOfRange, isEndOfRange }) => {
59
+ const baseStyles: DayTheme & { container: ViewStyle } =
60
+ isPressed || isHovered
61
+ ? {
62
+ container: {
63
+ ...styles.baseContainer,
64
+ backgroundColor: theme.colors.background.tertiary,
65
+ },
66
+ content: {
67
+ ...baseContent,
68
+ color: theme.colors.content.primary,
69
+ },
70
+ }
71
+ : {
72
+ container: {
73
+ ...styles.baseContainer,
74
+ backgroundColor: theme.colors.background.inverse.primary,
75
+ },
76
+ content: {
77
+ ...baseContent,
78
+ color: theme.colors.content.inverse.primary,
79
+ },
80
+ };
81
+
82
+ baseStyles.container.borderRadius = 0;
83
+ if (isStartOfRange) {
84
+ baseStyles.container.borderTopLeftRadius = 16;
85
+ baseStyles.container.borderBottomLeftRadius = 16;
86
+ }
87
+ if (isEndOfRange) {
88
+ baseStyles.container.borderTopRightRadius = 16;
89
+ baseStyles.container.borderBottomRightRadius = 16;
90
+ }
91
+ if (!isStartOfRange && !isEndOfRange) {
92
+ baseStyles.container.borderRadius = 0;
93
+ }
94
+ return baseStyles;
95
+ },
96
+ disabled: () => ({
97
+ container: styles.baseContainer,
98
+ content: {
99
+ ...baseContent,
100
+ color: theme.colors.content.disabled,
101
+ },
102
+ }),
103
+ idle: ({ isPressed, isHovered }) => {
104
+ return isPressed || isHovered
105
+ ? {
106
+ container: {
107
+ ...styles.baseContainer,
108
+ backgroundColor: theme.colors.background.tertiary,
109
+ },
110
+ content: {
111
+ ...baseContent,
112
+ color: theme.colors.content.primary,
113
+ },
114
+ }
115
+ : {
116
+ container: styles.baseContainer,
117
+ content: baseContent,
118
+ };
119
+ },
120
+ today: ({ isPressed, isHovered }) => {
121
+ return isPressed || isHovered
122
+ ? {
123
+ container: {
124
+ ...styles.baseContainer,
125
+ backgroundColor: theme.colors.background.tertiaryPressed,
126
+ },
127
+ content: baseContent,
128
+ }
129
+ : {
130
+ container: {
131
+ ...styles.baseContainer,
132
+ borderColor: theme.colors.borders.default,
133
+ borderStyle: "solid",
134
+ borderWidth: 1,
135
+ },
136
+ content: baseContent,
137
+ };
138
+ },
139
+ };
140
+ };
141
+
142
+ export interface CalendarItemDayProps {
143
+ children: ReactNode;
144
+ onPress: (id: string) => void;
145
+ metadata: CalendarDayMetadata;
146
+ theme?: Partial<
147
+ Record<
148
+ DayState | "base",
149
+ (
150
+ params: CalendarDayMetadata & {
151
+ isPressed: boolean;
152
+ isHovered?: boolean;
153
+ isFocused?: boolean;
154
+ }
155
+ ) => Partial<DayTheme>
156
+ >
157
+ >;
158
+ /** The cell's height */
159
+ height: number;
160
+ /** Optional TextProps to spread to the <Text> component. */
161
+ textProps?: Omit<TextProps, "children" | "onPress">;
162
+ /** Optional component to replace the default <Pressable> component. */
163
+ CalendarPressableComponent?: PressableLike;
164
+ }
165
+
166
+ /**
167
+ * The base calendar item day component. This component is responsible for
168
+ * rendering each day cell, along with its event handlers.
169
+ *
170
+ * This is not meant to be used directly. Instead, use the
171
+ * `CalendarItemDayWithContainer`, since it also includes the spacing between
172
+ * each day.
173
+ */
174
+ export const CalendarItemDay = ({
175
+ onPress,
176
+ children,
177
+ theme,
178
+ height,
179
+ metadata,
180
+ textProps,
181
+ CalendarPressableComponent = Pressable as PressableLike,
182
+ }: CalendarItemDayProps) => {
183
+ const baseTheme = useTheme();
184
+ const baseStyles = useMemo(() => {
185
+ return buildBaseStyles(baseTheme);
186
+ }, [baseTheme]);
187
+
188
+ const handlePress = useCallback(() => {
189
+ onPress(metadata.id);
190
+ }, [metadata.id, onPress]);
191
+
192
+ return (
193
+ <CalendarPressableComponent
194
+ disabled={metadata.state === "disabled"}
195
+ onPress={handlePress}
196
+ style={({
197
+ pressed: isPressed,
198
+ hovered: isHovered,
199
+ focused: isFocused,
200
+ }) => {
201
+ const params = {
202
+ isPressed,
203
+ isHovered,
204
+ isFocused,
205
+ isEndOfRange: metadata.isEndOfRange ?? false,
206
+ isStartOfRange: metadata.isStartOfRange ?? false,
207
+ };
208
+ const { container } = baseStyles[metadata.state](params);
209
+ return {
210
+ ...container,
211
+ height,
212
+ ...theme?.base?.({ ...metadata, isPressed }).container,
213
+ ...theme?.[metadata.state]?.({ ...metadata, isPressed }).container,
214
+ };
215
+ }}
216
+ >
217
+ {({ pressed: isPressed, hovered: isHovered, focused: isFocused }) => {
218
+ const params = {
219
+ isPressed,
220
+ isHovered,
221
+ isFocused,
222
+ isEndOfRange: metadata.isEndOfRange ?? false,
223
+ isStartOfRange: metadata.isStartOfRange ?? false,
224
+ };
225
+ const { content } = baseStyles[metadata.state](params);
226
+ return (
227
+ <Text
228
+ {...textProps}
229
+ style={{
230
+ ...content,
231
+ ...(textProps?.style ?? ({} as object)),
232
+ ...theme?.base?.({ ...metadata, isPressed, isHovered, isFocused })
233
+ .content,
234
+ ...theme?.[metadata.state]?.({
235
+ ...metadata,
236
+ isPressed,
237
+ isHovered,
238
+ isFocused,
239
+ }).content,
240
+ }}
241
+ >
242
+ {children}
243
+ </Text>
244
+ );
245
+ }}
246
+ </CalendarPressableComponent>
247
+ );
248
+ };
249
+
250
+ interface CalendarItemDayContainerTheme {
251
+ /** An empty view that acts as a spacer between each day. The spacing is
252
+ * controlled by the `daySpacing` prop. */
253
+ spacer?: ViewStyle;
254
+ /** An absolute positioned filler to join the active days together in a single
255
+ * complete range. */
256
+ activeDayFiller?: ViewStyle | ((params: CalendarDayMetadata) => ViewStyle);
257
+ }
258
+
259
+ export interface CalendarItemDayContainerProps {
260
+ children: ReactNode;
261
+ isStartOfWeek: boolean;
262
+ /**
263
+ * If true, the active day filler/extension will be shown. The filler is used
264
+ * as a visual effect to join the active days together in a complete range.
265
+ */
266
+ shouldShowActiveDayFiller?: boolean;
267
+ theme?: CalendarItemDayContainerTheme;
268
+ /**
269
+ * The spacing between each day
270
+ */
271
+ daySpacing: number;
272
+ /** The day's height */
273
+ dayHeight: number;
274
+ /** The metadata for the day, extracted from the calendar's state. */
275
+ metadata?: CalendarDayMetadata;
276
+ }
277
+
278
+ export const CalendarItemDayContainer = ({
279
+ children,
280
+ isStartOfWeek,
281
+ shouldShowActiveDayFiller,
282
+ theme,
283
+ daySpacing,
284
+ dayHeight,
285
+ metadata,
286
+ }: CalendarItemDayContainerProps) => {
287
+ const baseTheme = useTheme();
288
+ const spacerStyles = useMemo<ViewStyle>(() => {
289
+ return {
290
+ position: "relative",
291
+ marginLeft: isStartOfWeek ? 0 : daySpacing,
292
+ flex: 1,
293
+ height: dayHeight,
294
+ ...theme?.spacer,
295
+ };
296
+ }, [dayHeight, daySpacing, isStartOfWeek, theme?.spacer]);
297
+
298
+ const activeDayFiller = useMemo<ViewStyle | null>(() => {
299
+ if (!shouldShowActiveDayFiller) {
300
+ return null;
301
+ }
302
+
303
+ return {
304
+ position: "absolute",
305
+ top: 0,
306
+ bottom: 0,
307
+ right: -(daySpacing + 1), // +1 to cover the 1px gap
308
+ width: daySpacing + 2, // +2 to cover the 1px gap (distributes evenly on both sides)
309
+ backgroundColor: baseTheme.colors.background.inverse.primary,
310
+ ...(typeof theme?.activeDayFiller === "function" && !!metadata
311
+ ? theme.activeDayFiller(metadata)
312
+ : theme?.activeDayFiller),
313
+ };
314
+ }, [
315
+ baseTheme.colors.background.inverse.primary,
316
+ daySpacing,
317
+ metadata,
318
+ shouldShowActiveDayFiller,
319
+ theme,
320
+ ]);
321
+
322
+ return (
323
+ <View style={spacerStyles}>
324
+ {children}
325
+ {activeDayFiller ? <View style={activeDayFiller} /> : null}
326
+ </View>
327
+ );
328
+ };
329
+
330
+ export interface CalendarItemDayWithContainerProps
331
+ extends Omit<CalendarItemDayProps, "height">,
332
+ Pick<CalendarItemDayContainerProps, "daySpacing" | "dayHeight"> {
333
+ containerTheme?: CalendarItemDayContainerTheme;
334
+ /**
335
+ * A unique identifier for this calendar instance. This is useful if you
336
+ * need to render more than one calendar at once. This allows Legend Calendar
337
+ * to scope its state to the given instance.
338
+ *
339
+ * No need to get fancy with `uuid` or anything like that - a simple static
340
+ * string is enough.
341
+ *
342
+ * If not provided, Legend Calendar will use a default value which will hoist
343
+ * the state in a global scope.
344
+ */
345
+ calendarInstanceId?: string;
346
+ }
347
+
348
+ export const CalendarItemDayWithContainer = ({
349
+ children,
350
+ metadata: baseMetadata,
351
+ onPress,
352
+ theme,
353
+ dayHeight,
354
+ daySpacing,
355
+ containerTheme,
356
+ calendarInstanceId,
357
+ CalendarPressableComponent,
358
+ }: CalendarItemDayWithContainerProps) => {
359
+ const metadata = useOptimizedDayMetadata(baseMetadata, calendarInstanceId);
360
+
361
+ return (
362
+ <CalendarItemDayContainer
363
+ dayHeight={dayHeight}
364
+ daySpacing={daySpacing}
365
+ isStartOfWeek={metadata.isStartOfWeek}
366
+ metadata={metadata}
367
+ shouldShowActiveDayFiller={
368
+ metadata.isRangeValid && !metadata.isEndOfWeek
369
+ ? !metadata.isEndOfRange
370
+ : false
371
+ }
372
+ theme={containerTheme}
373
+ >
374
+ <CalendarItemDay
375
+ CalendarPressableComponent={CalendarPressableComponent}
376
+ height={dayHeight}
377
+ metadata={metadata}
378
+ onPress={onPress}
379
+ theme={theme}
380
+ >
381
+ {children}
382
+ </CalendarItemDay>
383
+ </CalendarItemDayContainer>
384
+ );
385
+ };
@@ -0,0 +1,30 @@
1
+ import { memo, useMemo } from "react";
2
+ import type { ViewStyle } from "react-native";
3
+ import { View, StyleSheet } from "react-native";
4
+
5
+ const styles = StyleSheet.create({
6
+ container: {
7
+ padding: 6,
8
+ flex: 1,
9
+ },
10
+ });
11
+
12
+ export interface CalendarItemEmptyProps {
13
+ /** The height of the cell. Should be the same as `CalendarItemDay`. */
14
+ height: number;
15
+ /** The theme of the empty cell, useful for customizing the component. */
16
+ theme?: {
17
+ container?: ViewStyle;
18
+ };
19
+ }
20
+
21
+ export const CalendarItemEmpty = memo(function CalendarItemEmpty(
22
+ props: CalendarItemEmptyProps
23
+ ) {
24
+ const { height, theme } = props;
25
+ const containerStyles = useMemo(() => {
26
+ return [{ ...styles.container, height }, theme?.container];
27
+ }, [height, theme?.container]);
28
+
29
+ return <View style={containerStyles} />;
30
+ });
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+ import { useMemo } from "react";
3
+ import type { TextProps, TextStyle, ViewStyle } from "react-native";
4
+ import { StyleSheet, Text, View } from "react-native";
5
+
6
+ import { lightTheme } from "@/helpers/tokens";
7
+ import { useTheme } from "@/hooks/useTheme";
8
+
9
+ const styles = StyleSheet.create({
10
+ container: {
11
+ alignItems: "center",
12
+ flex: 1,
13
+ justifyContent: "center",
14
+ padding: lightTheme.spacing[6],
15
+ },
16
+ content: {},
17
+ });
18
+
19
+ interface CalendarItemWeekNameTheme {
20
+ container?: ViewStyle;
21
+ content?: TextStyle;
22
+ }
23
+
24
+ export interface CalendarItemWeekNameProps {
25
+ children: ReactNode;
26
+ /**
27
+ * The height of the week name, needed to correctly measure the calendar's
28
+ */
29
+ height: number;
30
+ /** The theme of the week name, useful for customizing the component. */
31
+ theme?: CalendarItemWeekNameTheme;
32
+ /** Optional TextProps to spread to the <Text> component. */
33
+ textProps?: Omit<TextProps, "children">;
34
+ }
35
+
36
+ export const CalendarItemWeekName = ({
37
+ children,
38
+ height,
39
+ theme,
40
+ textProps,
41
+ }: CalendarItemWeekNameProps) => {
42
+ const { colors } = useTheme();
43
+ const { containerStyles, contentStyles } = useMemo(() => {
44
+ const containerStyles = [styles.container, { height }, theme?.container];
45
+ const contentStyles = [
46
+ styles.content,
47
+ { color: colors.content.primary },
48
+ textProps?.style,
49
+ theme?.content,
50
+ ];
51
+ return { containerStyles, contentStyles };
52
+ }, [
53
+ colors.content.primary,
54
+ height,
55
+ theme?.container,
56
+ theme?.content,
57
+ textProps?.style,
58
+ ]);
59
+
60
+ return (
61
+ <View style={containerStyles}>
62
+ <Text {...textProps} style={contentStyles}>
63
+ {children}
64
+ </Text>
65
+ </View>
66
+ );
67
+ };