@idealyst/datepicker 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 (50) hide show
  1. package/README.md +88 -0
  2. package/package.json +77 -0
  3. package/src/DatePicker/Calendar.native.tsx +159 -0
  4. package/src/DatePicker/Calendar.styles.tsx +224 -0
  5. package/src/DatePicker/Calendar.tsx +154 -0
  6. package/src/DatePicker/DatePicker.native.tsx +33 -0
  7. package/src/DatePicker/DatePicker.styles.tsx +69 -0
  8. package/src/DatePicker/DatePicker.web.tsx +31 -0
  9. package/src/DatePicker/index.native.ts +3 -0
  10. package/src/DatePicker/index.ts +3 -0
  11. package/src/DatePicker/types.ts +78 -0
  12. package/src/DateRangePicker/DateRangePicker.native.tsx +39 -0
  13. package/src/DateRangePicker/DateRangePicker.styles.tsx +83 -0
  14. package/src/DateRangePicker/DateRangePicker.web.tsx +40 -0
  15. package/src/DateRangePicker/RangeCalendar.native.tsx +267 -0
  16. package/src/DateRangePicker/RangeCalendar.styles.tsx +170 -0
  17. package/src/DateRangePicker/RangeCalendar.web.tsx +275 -0
  18. package/src/DateRangePicker/index.native.ts +3 -0
  19. package/src/DateRangePicker/index.ts +3 -0
  20. package/src/DateRangePicker/types.ts +98 -0
  21. package/src/DateTimePicker/DateTimePicker.native.tsx +82 -0
  22. package/src/DateTimePicker/DateTimePicker.styles.tsx +77 -0
  23. package/src/DateTimePicker/DateTimePicker.tsx +79 -0
  24. package/src/DateTimePicker/TimePicker.native.tsx +204 -0
  25. package/src/DateTimePicker/TimePicker.styles.tsx +116 -0
  26. package/src/DateTimePicker/TimePicker.tsx +406 -0
  27. package/src/DateTimePicker/index.native.ts +3 -0
  28. package/src/DateTimePicker/index.ts +3 -0
  29. package/src/DateTimePicker/types.ts +84 -0
  30. package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +213 -0
  31. package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +95 -0
  32. package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +141 -0
  33. package/src/DateTimeRangePicker/index.native.ts +2 -0
  34. package/src/DateTimeRangePicker/index.ts +2 -0
  35. package/src/DateTimeRangePicker/types.ts +72 -0
  36. package/src/examples/DatePickerExamples.tsx +274 -0
  37. package/src/examples/index.ts +1 -0
  38. package/src/index.native.ts +16 -0
  39. package/src/index.ts +16 -0
  40. package/src/primitives/CalendarGrid/CalendarGrid.styles.tsx +62 -0
  41. package/src/primitives/CalendarGrid/CalendarGrid.tsx +138 -0
  42. package/src/primitives/CalendarGrid/index.ts +1 -0
  43. package/src/primitives/CalendarHeader/CalendarHeader.styles.tsx +25 -0
  44. package/src/primitives/CalendarHeader/CalendarHeader.tsx +69 -0
  45. package/src/primitives/CalendarHeader/index.ts +1 -0
  46. package/src/primitives/CalendarOverlay/CalendarOverlay.styles.tsx +81 -0
  47. package/src/primitives/CalendarOverlay/CalendarOverlay.tsx +130 -0
  48. package/src/primitives/CalendarOverlay/index.ts +1 -0
  49. package/src/primitives/Wrapper/Wrapper.web.tsx +33 -0
  50. package/src/primitives/Wrapper/index.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @idealyst/datepicker
2
+
3
+ Cross-platform date and time picker components for React and React Native.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @idealyst/datepicker
9
+ # or
10
+ yarn add @idealyst/datepicker
11
+ ```
12
+
13
+ ## Peer Dependencies
14
+
15
+ ```bash
16
+ npm install @idealyst/components @idealyst/theme react-native-unistyles
17
+ ```
18
+
19
+ ## Components
20
+
21
+ ### DatePicker
22
+
23
+ A single date selection component with calendar popup.
24
+
25
+ ```tsx
26
+ import { DatePicker } from '@idealyst/datepicker';
27
+
28
+ function MyComponent() {
29
+ const [date, setDate] = useState<Date | null>(null);
30
+
31
+ return (
32
+ <DatePicker
33
+ value={date}
34
+ onChange={setDate}
35
+ label="Select Date"
36
+ placeholder="Choose a date"
37
+ />
38
+ );
39
+ }
40
+ ```
41
+
42
+ ### Coming Soon
43
+
44
+ - **DateTimePicker**: Select date and time
45
+ - **DateRangePicker**: Select a range of dates
46
+ - **DateTimeRangePicker**: Select a range of dates with times
47
+
48
+ ## Features
49
+
50
+ - ✅ Cross-platform (React & React Native)
51
+ - ✅ Theme integration with Unistyles
52
+ - ✅ Accessibility support
53
+ - ✅ Keyboard navigation
54
+ - ✅ Min/max date restrictions
55
+ - ✅ Multiple size variants
56
+ - ✅ Customizable date formats
57
+ - ✅ TypeScript support
58
+
59
+ ## API Reference
60
+
61
+ ### DatePicker Props
62
+
63
+ | Prop | Type | Default | Description |
64
+ |------|------|---------|-------------|
65
+ | `value` | `Date \| undefined` | - | Current selected date |
66
+ | `onChange` | `(date: Date \| null) => void` | - | Called when date changes |
67
+ | `minDate` | `Date` | - | Minimum selectable date |
68
+ | `maxDate` | `Date` | - | Maximum selectable date |
69
+ | `disabled` | `boolean` | `false` | Disabled state |
70
+ | `placeholder` | `string` | `'Select date'` | Placeholder text |
71
+ | `label` | `string` | - | Label for the picker |
72
+ | `error` | `string` | - | Error message |
73
+ | `helperText` | `string` | - | Helper text |
74
+ | `format` | `string` | `'MM/dd/yyyy'` | Date display format |
75
+ | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Size variant |
76
+ | `variant` | `'outlined' \| 'filled'` | `'outlined'` | Visual variant |
77
+
78
+ ## Examples
79
+
80
+ See the examples directory or import examples:
81
+
82
+ ```tsx
83
+ import { DatePickerExamples } from '@idealyst/datepicker/examples';
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@idealyst/datepicker",
3
+ "version": "1.0.0",
4
+ "description": "Cross-platform date and time picker components for React and React Native",
5
+ "documentation": "https://github.com/your-username/idealyst-framework/tree/main/packages/datepicker#readme",
6
+ "main": "src/index.ts",
7
+ "module": "src/index.ts",
8
+ "types": "src/index.ts",
9
+ "react-native": "src/index.native.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/your-username/idealyst-framework.git",
13
+ "directory": "packages/datepicker"
14
+ },
15
+ "author": "Your Name <your.email@example.com>",
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "react-native": "./src/index.native.ts",
23
+ "import": "./src/index.ts",
24
+ "require": "./src/index.ts",
25
+ "types": "./src/index.ts"
26
+ },
27
+ "./examples": {
28
+ "import": "./src/examples/index.ts",
29
+ "require": "./src/examples/index.ts",
30
+ "types": "./src/examples/index.ts"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "prepublishOnly": "echo 'Publishing TypeScript source directly'",
35
+ "publish:npm": "npm publish"
36
+ },
37
+ "peerDependencies": {
38
+ "@idealyst/components": "^1.0.40",
39
+ "@idealyst/theme": "^1.0.40",
40
+ "react": ">=16.8.0",
41
+ "react-native": ">=0.60.0",
42
+ "react-native-unistyles": "^3.0.4"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@idealyst/components": {
46
+ "optional": false
47
+ },
48
+ "@idealyst/theme": {
49
+ "optional": false
50
+ },
51
+ "react-native": {
52
+ "optional": true
53
+ },
54
+ "react-native-unistyles": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@types/react": "^19.1.0",
60
+ "typescript": "^5.0.0"
61
+ },
62
+ "files": [
63
+ "src",
64
+ "README.md"
65
+ ],
66
+ "keywords": [
67
+ "react",
68
+ "react-native",
69
+ "datepicker",
70
+ "timepicker",
71
+ "date-range",
72
+ "datetime",
73
+ "calendar",
74
+ "cross-platform",
75
+ "picker"
76
+ ]
77
+ }
@@ -0,0 +1,159 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { View, Text, Button } from '@idealyst/components';
3
+ import { TouchableOpacity } from 'react-native';
4
+ import { CalendarProps } from './types';
5
+ import { calendarStyles } from './Calendar.styles';
6
+
7
+ export const Calendar: React.FC<CalendarProps> = ({
8
+ value,
9
+ onChange,
10
+ minDate,
11
+ maxDate,
12
+ disabled = false,
13
+ currentMonth: controlledCurrentMonth,
14
+ onMonthChange,
15
+ style,
16
+ testID,
17
+ }) => {
18
+ const [internalCurrentMonth, setInternalCurrentMonth] = useState(
19
+ controlledCurrentMonth || value || new Date()
20
+ );
21
+
22
+ const currentMonth = controlledCurrentMonth || internalCurrentMonth;
23
+
24
+ const handleMonthChange = (newMonth: Date) => {
25
+ if (onMonthChange) {
26
+ onMonthChange(newMonth);
27
+ } else {
28
+ setInternalCurrentMonth(newMonth);
29
+ }
30
+ };
31
+
32
+ const { monthStart, monthEnd, daysInMonth, startingDayOfWeek } = useMemo(() => {
33
+ const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
34
+ const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0);
35
+ const daysInMonth = monthEnd.getDate();
36
+ const startingDayOfWeek = monthStart.getDay();
37
+
38
+ return { monthStart, monthEnd, daysInMonth, startingDayOfWeek };
39
+ }, [currentMonth]);
40
+
41
+ const isDateDisabled = (date: Date): boolean => {
42
+ if (disabled) return true;
43
+ if (minDate && date < minDate) return true;
44
+ if (maxDate && date > maxDate) return true;
45
+ return false;
46
+ };
47
+
48
+ const isDateSelected = (date: Date): boolean => {
49
+ if (!value) return false;
50
+ return (
51
+ date.getDate() === value.getDate() &&
52
+ date.getMonth() === value.getMonth() &&
53
+ date.getFullYear() === value.getFullYear()
54
+ );
55
+ };
56
+
57
+ const handleDateClick = (date: Date) => {
58
+ if (!isDateDisabled(date)) {
59
+ onChange(date);
60
+ }
61
+ };
62
+
63
+ const goToPreviousMonth = () => {
64
+ const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
65
+ handleMonthChange(newMonth);
66
+ };
67
+
68
+ const goToNextMonth = () => {
69
+ const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
70
+ handleMonthChange(newMonth);
71
+ };
72
+
73
+ const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
74
+
75
+ // Create calendar grid
76
+ const calendarDays = [];
77
+
78
+ // Add empty cells for days before month starts
79
+ for (let i = 0; i < startingDayOfWeek; i++) {
80
+ calendarDays.push(null);
81
+ }
82
+
83
+ // Add days of the month
84
+ for (let day = 1; day <= daysInMonth; day++) {
85
+ const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
86
+ calendarDays.push(date);
87
+ }
88
+
89
+ // Use Unistyles
90
+ calendarStyles.useVariants({});
91
+
92
+ return (
93
+ <View style={[calendarStyles.container, style]} testID={testID}>
94
+ {/* Header */}
95
+ <View style={calendarStyles.header}>
96
+ <Button
97
+ variant="text"
98
+ size="small"
99
+ onPress={goToPreviousMonth}
100
+ disabled={disabled}
101
+ style={calendarStyles.headerButton}
102
+ >
103
+
104
+ </Button>
105
+ <Text weight="semibold">{monthName}</Text>
106
+ <Button
107
+ variant="text"
108
+ size="small"
109
+ onPress={goToNextMonth}
110
+ disabled={disabled}
111
+ style={calendarStyles.headerButton}
112
+ >
113
+
114
+ </Button>
115
+ </View>
116
+
117
+ {/* Weekday headers */}
118
+ <View style={calendarStyles.weekdayHeader}>
119
+ {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((day) => (
120
+ <View key={day} style={calendarStyles.weekdayCell}>
121
+ <Text style={calendarStyles.weekdayText}>
122
+ {day}
123
+ </Text>
124
+ </View>
125
+ ))}
126
+ </View>
127
+
128
+ {/* Calendar grid */}
129
+ <View style={calendarStyles.calendarGrid}>
130
+ {calendarDays.map((date, index) => (
131
+ <View key={index} style={calendarStyles.dayCell}>
132
+ {date && (
133
+ <TouchableOpacity
134
+ onPress={() => handleDateClick(date)}
135
+ disabled={isDateDisabled(date)}
136
+ style={[
137
+ calendarStyles.dayButton,
138
+ {
139
+ backgroundColor: isDateSelected(date) ? '#3b82f6' : 'transparent',
140
+ opacity: isDateDisabled(date) ? 0.5 : 1,
141
+ }
142
+ ]}
143
+ >
144
+ <Text
145
+ style={{
146
+ color: isDateSelected(date) ? 'white' : 'black',
147
+ fontSize: 13,
148
+ }}
149
+ >
150
+ {date.getDate()}
151
+ </Text>
152
+ </TouchableOpacity>
153
+ )}
154
+ </View>
155
+ ))}
156
+ </View>
157
+ </View>
158
+ );
159
+ };
@@ -0,0 +1,224 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ export const calendarStyles = StyleSheet.create((theme) => ({
4
+ container: {
5
+ width: 256,
6
+ position: 'relative',
7
+ },
8
+
9
+ header: {
10
+ display: 'flex',
11
+ flexDirection: 'row',
12
+ justifyContent: 'space-between',
13
+ alignItems: 'center',
14
+ marginBottom: theme.spacing?.md || 16,
15
+ },
16
+
17
+ headerButton: {
18
+ minWidth: 32,
19
+ minHeight: 32,
20
+ paddingHorizontal: theme.spacing?.xs || 8,
21
+ paddingVertical: theme.spacing?.xs || 4,
22
+ },
23
+
24
+ headerTitle: {
25
+ display: 'flex',
26
+ flexDirection: 'row',
27
+ alignItems: 'center',
28
+ gap: theme.spacing?.xs || 8,
29
+ },
30
+
31
+ monthYearButton: {
32
+ paddingHorizontal: theme.spacing?.sm || 8,
33
+ paddingVertical: theme.spacing?.xs || 4,
34
+ },
35
+
36
+ pickerContainer: {
37
+ marginBottom: theme.spacing?.md || 16,
38
+ padding: theme.spacing?.sm || 12,
39
+ backgroundColor: theme.colors?.surface?.secondary || '#f9fafb',
40
+ borderRadius: theme.borderRadius?.md || 8,
41
+ border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
42
+
43
+ // Native specific styles
44
+ _native: {
45
+ borderWidth: 1,
46
+ borderColor: theme.colors?.border?.primary || '#e5e7eb',
47
+ },
48
+ },
49
+
50
+ monthPickerGrid: {
51
+ display: 'flex',
52
+ flexDirection: 'row',
53
+ flexWrap: 'wrap',
54
+ gap: 6,
55
+ width: '100%',
56
+ justifyContent: 'space-between',
57
+
58
+ // Native fallback
59
+ _native: {
60
+ flexDirection: 'row',
61
+ flexWrap: 'wrap',
62
+ justifyContent: 'space-between',
63
+ },
64
+ },
65
+
66
+ yearPickerGrid: {
67
+ display: 'flex',
68
+ flexDirection: 'row',
69
+ flexWrap: 'wrap',
70
+ gap: 4,
71
+ maxHeight: 200,
72
+ overflowY: 'auto',
73
+ width: '100%',
74
+ justifyContent: 'space-between',
75
+
76
+ // Native fallback
77
+ _native: {
78
+ flexDirection: 'row',
79
+ flexWrap: 'wrap',
80
+ justifyContent: 'space-between',
81
+ maxHeight: 200,
82
+ },
83
+ },
84
+
85
+ pickerButton: {
86
+ fontSize: theme.typography?.sizes?.small || 12,
87
+ paddingHorizontal: theme.spacing?.xs || 6,
88
+ paddingVertical: theme.spacing?.xs || 4,
89
+ minHeight: 32,
90
+ display: 'flex',
91
+ justifyContent: 'center',
92
+ alignItems: 'center',
93
+
94
+ // Month buttons: 3 per row with equal spacing
95
+ variants: {
96
+ monthButton: {
97
+ true: {
98
+ flex: '1 0 30%',
99
+ maxWidth: 'calc(33.333% - 4px)',
100
+ },
101
+ },
102
+ yearButton: {
103
+ true: {
104
+ flex: '1 0 22%',
105
+ maxWidth: 'calc(25% - 3px)',
106
+ },
107
+ },
108
+ },
109
+
110
+ // Native specific sizing
111
+ _native: {
112
+ width: '30%',
113
+ marginBottom: 4,
114
+ },
115
+ },
116
+
117
+ weekdayHeader: {
118
+ display: 'grid',
119
+ gridTemplateColumns: 'repeat(7, 1fr)',
120
+ gap: 2,
121
+ marginBottom: theme.spacing?.xs || 8,
122
+
123
+ // Native fallback
124
+ _native: {
125
+ flexDirection: 'row',
126
+ },
127
+ },
128
+
129
+ weekdayCell: {
130
+ display: 'flex',
131
+ alignItems: 'center',
132
+ justifyContent: 'center',
133
+ paddingVertical: theme.spacing?.xs || 4,
134
+
135
+ // Native fallback
136
+ _native: {
137
+ flex: 1,
138
+ alignItems: 'center',
139
+ paddingVertical: theme.spacing?.xs || 4,
140
+ },
141
+ },
142
+
143
+ weekdayText: {
144
+ fontSize: theme.typography?.sizes?.small || 12,
145
+ fontWeight: '500',
146
+ color: theme.colors?.text?.secondary || '#6b7280',
147
+ },
148
+
149
+ calendarGrid: {
150
+ display: 'grid',
151
+ gridTemplateColumns: 'repeat(7, 1fr)',
152
+ gap: 2,
153
+ marginBottom: theme.spacing?.xs || 8,
154
+
155
+ // Native fallback
156
+ _native: {
157
+ flexDirection: 'row',
158
+ flexWrap: 'wrap',
159
+ },
160
+ },
161
+
162
+ dayCell: {
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ justifyContent: 'center',
166
+ aspectRatio: '1',
167
+ minHeight: 32,
168
+
169
+ // Native specific sizing
170
+ _native: {
171
+ width: '14.28%', // 100% / 7 days
172
+ aspectRatio: 1,
173
+ alignItems: 'center',
174
+ justifyContent: 'center',
175
+ },
176
+ },
177
+
178
+ dayButton: {
179
+ width: '100%',
180
+ height: '100%',
181
+ maxWidth: 36,
182
+ maxHeight: 36,
183
+ minWidth: 28,
184
+ minHeight: 28,
185
+ padding: 0,
186
+ borderRadius: theme.borderRadius?.sm || 6,
187
+ fontSize: theme.typography?.sizes?.small || 13,
188
+
189
+ variants: {
190
+ selected: {
191
+ true: {
192
+ fontWeight: '600',
193
+ },
194
+ false: {
195
+ fontWeight: '400',
196
+ },
197
+ },
198
+ },
199
+
200
+ // Native specific styling
201
+ _native: {
202
+ width: 28,
203
+ height: 28,
204
+ minWidth: 28,
205
+ minHeight: 28,
206
+ borderRadius: theme.borderRadius?.sm || 6,
207
+ },
208
+ },
209
+
210
+ todaySection: {
211
+ paddingTop: theme.spacing?.sm || 12,
212
+ borderTopWidth: 1,
213
+ borderTopColor: theme.colors?.border?.secondary || '#f3f4f6',
214
+
215
+ // Web specific border
216
+ _web: {
217
+ borderTop: `1px solid ${theme.colors?.border?.secondary || '#f3f4f6'}`,
218
+ },
219
+ },
220
+
221
+ todayButton: {
222
+ width: '100%',
223
+ },
224
+ }));
@@ -0,0 +1,154 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { View } from '@idealyst/components';
3
+ import { CalendarProps } from './types';
4
+ import { CalendarHeader } from '../primitives/CalendarHeader';
5
+ import { CalendarGrid } from '../primitives/CalendarGrid';
6
+ import { CalendarOverlay } from '../primitives/CalendarOverlay';
7
+ import { calendarStyles } from './Calendar.styles';
8
+
9
+
10
+ export const Calendar: React.FC<CalendarProps> = ({
11
+ value,
12
+ onChange,
13
+ minDate,
14
+ maxDate,
15
+ disabled = false,
16
+ currentMonth: controlledCurrentMonth,
17
+ onMonthChange,
18
+ style,
19
+ testID,
20
+ }) => {
21
+ const [internalCurrentMonth, setInternalCurrentMonth] = useState(
22
+ controlledCurrentMonth || value || new Date()
23
+ );
24
+ const [overlayMode, setOverlayMode] = useState<'month' | 'year' | null>(null);
25
+ const containerRef = useRef<View>(null);
26
+
27
+ const currentMonth = controlledCurrentMonth || internalCurrentMonth;
28
+
29
+ // Close overlay when clicking outside
30
+ useEffect(() => {
31
+ const handleClickOutside = (event: MouseEvent) => {
32
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
33
+ setOverlayMode(null);
34
+ }
35
+ };
36
+
37
+ if (overlayMode) {
38
+ document.addEventListener('mousedown', handleClickOutside);
39
+ }
40
+
41
+ return () => {
42
+ document.removeEventListener('mousedown', handleClickOutside);
43
+ };
44
+ }, [overlayMode]);
45
+
46
+ const handleMonthChange = (newMonth: Date) => {
47
+ if (onMonthChange) {
48
+ onMonthChange(newMonth);
49
+ } else {
50
+ setInternalCurrentMonth(newMonth);
51
+ }
52
+ };
53
+
54
+ const isDateDisabled = (date: Date): boolean => {
55
+ if (disabled) return true;
56
+ if (minDate && date < minDate) return true;
57
+ if (maxDate && date > maxDate) return true;
58
+ return false;
59
+ };
60
+
61
+ const handleDateSelect = (date: Date) => {
62
+ if (!isDateDisabled(date)) {
63
+ onChange(date);
64
+ }
65
+ };
66
+
67
+ const goToPreviousMonth = () => {
68
+ const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
69
+ handleMonthChange(newMonth);
70
+ };
71
+
72
+ const goToNextMonth = () => {
73
+ const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
74
+ handleMonthChange(newMonth);
75
+ };
76
+
77
+ const createDateWithDayAdjustment = (year: number, month: number, day: number): Date => {
78
+ // Get the last day of the target month
79
+ const lastDayOfMonth = new Date(year, month + 1, 0).getDate();
80
+ // Use the smaller of the requested day or the last day of the month
81
+ const adjustedDay = Math.min(day, lastDayOfMonth);
82
+ return new Date(year, month, adjustedDay);
83
+ };
84
+
85
+ const handleMonthSelect = (monthIndex: number) => {
86
+ const newMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
87
+ handleMonthChange(newMonth);
88
+
89
+ // Update the selected date if one exists
90
+ if (value) {
91
+ const newDate = createDateWithDayAdjustment(
92
+ currentMonth.getFullYear(),
93
+ monthIndex,
94
+ value.getDate()
95
+ );
96
+ if (!isDateDisabled(newDate)) {
97
+ onChange(newDate);
98
+ }
99
+ }
100
+ };
101
+
102
+ const handleYearSelect = (year: number) => {
103
+ const newMonth = new Date(year, currentMonth.getMonth(), 1);
104
+ handleMonthChange(newMonth);
105
+
106
+ // Update the selected date if one exists
107
+ if (value) {
108
+ const newDate = createDateWithDayAdjustment(
109
+ year,
110
+ currentMonth.getMonth(),
111
+ value.getDate()
112
+ );
113
+ if (!isDateDisabled(newDate)) {
114
+ onChange(newDate);
115
+ }
116
+ }
117
+ };
118
+
119
+ return (
120
+ <View ref={containerRef} style={[calendarStyles.container, style]} data-testid={testID}>
121
+ <CalendarHeader
122
+ currentMonth={currentMonth}
123
+ onPreviousMonth={goToPreviousMonth}
124
+ onNextMonth={goToNextMonth}
125
+ onMonthClick={() => setOverlayMode('month')}
126
+ onYearClick={() => setOverlayMode('year')}
127
+ disabled={disabled}
128
+ />
129
+
130
+ <CalendarGrid
131
+ currentMonth={currentMonth}
132
+ selectedDate={value}
133
+ onDateSelect={handleDateSelect}
134
+ onMonthChange={handleMonthChange}
135
+ minDate={minDate}
136
+ maxDate={maxDate}
137
+ disabled={disabled}
138
+ />
139
+
140
+ {/* Overlay for month/year selection */}
141
+ {overlayMode && (
142
+ <CalendarOverlay
143
+ mode={overlayMode}
144
+ currentMonth={currentMonth.getMonth()}
145
+ currentYear={currentMonth.getFullYear()}
146
+ onMonthSelect={handleMonthSelect}
147
+ onYearSelect={handleYearSelect}
148
+ onClose={() => setOverlayMode(null)}
149
+ disabled={disabled}
150
+ />
151
+ )}
152
+ </View>
153
+ );
154
+ };