@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.
- package/.eslintrc.js +29 -0
- package/.turbo/turbo-build.log +19 -0
- package/.turbo/turbo-lint.log +14 -0
- package/.turbo/turbo-test.log +261 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +127 -0
- package/dist/index.d.mts +679 -0
- package/dist/index.d.ts +679 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
- package/src/components/Calendar.stories.tsx +226 -0
- package/src/components/Calendar.tsx +224 -0
- package/src/components/CalendarItemDay.tsx +385 -0
- package/src/components/CalendarItemEmpty.tsx +30 -0
- package/src/components/CalendarItemWeekName.tsx +67 -0
- package/src/components/CalendarList.stories.tsx +326 -0
- package/src/components/CalendarList.tsx +373 -0
- package/src/components/CalendarRowMonth.tsx +62 -0
- package/src/components/CalendarRowWeek.tsx +46 -0
- package/src/components/CalendarThemeProvider.tsx +43 -0
- package/src/components/HStack.tsx +67 -0
- package/src/components/VStack.tsx +67 -0
- package/src/components/index.ts +108 -0
- package/src/developer/decorators.tsx +54 -0
- package/src/developer/loggginHandler.tsx +6 -0
- package/src/developer/useRenderCount.ts +7 -0
- package/src/helpers/dates.test.ts +567 -0
- package/src/helpers/dates.ts +163 -0
- package/src/helpers/functions.ts +327 -0
- package/src/helpers/numbers.ts +11 -0
- package/src/helpers/strings.ts +2 -0
- package/src/helpers/tokens.ts +71 -0
- package/src/helpers/types.ts +3 -0
- package/src/hooks/useCalendar.test.ts +381 -0
- package/src/hooks/useCalendar.ts +351 -0
- package/src/hooks/useCalendarList.test.ts +382 -0
- package/src/hooks/useCalendarList.tsx +291 -0
- package/src/hooks/useDateRange.test.ts +128 -0
- package/src/hooks/useDateRange.ts +94 -0
- package/src/hooks/useOptimizedDayMetadata.test.ts +582 -0
- package/src/hooks/useOptimizedDayMetadata.ts +93 -0
- package/src/hooks/useTheme.ts +14 -0
- package/src/index.ts +24 -0
- package/tsconfig.json +13 -0
- 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
|
+
};
|