@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.
- package/README.md +88 -0
- package/package.json +77 -0
- package/src/DatePicker/Calendar.native.tsx +159 -0
- package/src/DatePicker/Calendar.styles.tsx +224 -0
- package/src/DatePicker/Calendar.tsx +154 -0
- package/src/DatePicker/DatePicker.native.tsx +33 -0
- package/src/DatePicker/DatePicker.styles.tsx +69 -0
- package/src/DatePicker/DatePicker.web.tsx +31 -0
- package/src/DatePicker/index.native.ts +3 -0
- package/src/DatePicker/index.ts +3 -0
- package/src/DatePicker/types.ts +78 -0
- package/src/DateRangePicker/DateRangePicker.native.tsx +39 -0
- package/src/DateRangePicker/DateRangePicker.styles.tsx +83 -0
- package/src/DateRangePicker/DateRangePicker.web.tsx +40 -0
- package/src/DateRangePicker/RangeCalendar.native.tsx +267 -0
- package/src/DateRangePicker/RangeCalendar.styles.tsx +170 -0
- package/src/DateRangePicker/RangeCalendar.web.tsx +275 -0
- package/src/DateRangePicker/index.native.ts +3 -0
- package/src/DateRangePicker/index.ts +3 -0
- package/src/DateRangePicker/types.ts +98 -0
- package/src/DateTimePicker/DateTimePicker.native.tsx +82 -0
- package/src/DateTimePicker/DateTimePicker.styles.tsx +77 -0
- package/src/DateTimePicker/DateTimePicker.tsx +79 -0
- package/src/DateTimePicker/TimePicker.native.tsx +204 -0
- package/src/DateTimePicker/TimePicker.styles.tsx +116 -0
- package/src/DateTimePicker/TimePicker.tsx +406 -0
- package/src/DateTimePicker/index.native.ts +3 -0
- package/src/DateTimePicker/index.ts +3 -0
- package/src/DateTimePicker/types.ts +84 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +213 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +95 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +141 -0
- package/src/DateTimeRangePicker/index.native.ts +2 -0
- package/src/DateTimeRangePicker/index.ts +2 -0
- package/src/DateTimeRangePicker/types.ts +72 -0
- package/src/examples/DatePickerExamples.tsx +274 -0
- package/src/examples/index.ts +1 -0
- package/src/index.native.ts +16 -0
- package/src/index.ts +16 -0
- package/src/primitives/CalendarGrid/CalendarGrid.styles.tsx +62 -0
- package/src/primitives/CalendarGrid/CalendarGrid.tsx +138 -0
- package/src/primitives/CalendarGrid/index.ts +1 -0
- package/src/primitives/CalendarHeader/CalendarHeader.styles.tsx +25 -0
- package/src/primitives/CalendarHeader/CalendarHeader.tsx +69 -0
- package/src/primitives/CalendarHeader/index.ts +1 -0
- package/src/primitives/CalendarOverlay/CalendarOverlay.styles.tsx +81 -0
- package/src/primitives/CalendarOverlay/CalendarOverlay.tsx +130 -0
- package/src/primitives/CalendarOverlay/index.ts +1 -0
- package/src/primitives/Wrapper/Wrapper.web.tsx +33 -0
- package/src/primitives/Wrapper/index.ts +1 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Screen, View, Text, Button } from '@idealyst/components';
|
|
3
|
+
import { DatePicker } from '../DatePicker';
|
|
4
|
+
import { DateTimePicker } from '../DateTimePicker';
|
|
5
|
+
import { DateRangePicker, DateRange } from '../DateRangePicker';
|
|
6
|
+
import { DateTimeRangePicker, DateTimeRange } from '../DateTimeRangePicker';
|
|
7
|
+
|
|
8
|
+
export const DatePickerExamples = () => {
|
|
9
|
+
const [basicDate, setBasicDate] = useState<Date | null>(null);
|
|
10
|
+
const [rangeDate, setRangeDate] = useState<Date | null>(null);
|
|
11
|
+
const [disabledDate, setDisabledDate] = useState<Date | null>(new Date());
|
|
12
|
+
const [dateTime, setDateTime] = useState<Date | null>(null);
|
|
13
|
+
const [dateTime24h, setDateTime24h] = useState<Date | null>(null);
|
|
14
|
+
const [dateRange, setDateRange] = useState<DateRange | null>(null);
|
|
15
|
+
const [restrictedRange, setRestrictedRange] = useState<DateRange | null>(null);
|
|
16
|
+
const [dateTimeRange, setDateTimeRange] = useState<DateTimeRange | null>(null);
|
|
17
|
+
|
|
18
|
+
const tomorrow = new Date();
|
|
19
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
20
|
+
|
|
21
|
+
const nextMonth = new Date();
|
|
22
|
+
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Screen background="primary" padding="lg">
|
|
26
|
+
<View spacing="none">
|
|
27
|
+
<Text size="large" weight="bold" align="center">
|
|
28
|
+
DatePicker Examples
|
|
29
|
+
</Text>
|
|
30
|
+
|
|
31
|
+
{/* Basic DatePicker */}
|
|
32
|
+
<View spacing="md">
|
|
33
|
+
<Text size="medium" weight="semibold">Basic DatePicker</Text>
|
|
34
|
+
<DatePicker
|
|
35
|
+
value={basicDate}
|
|
36
|
+
onChange={setBasicDate}
|
|
37
|
+
label="Select Date"
|
|
38
|
+
placeholder="Choose a date"
|
|
39
|
+
helperText="Pick any date"
|
|
40
|
+
/>
|
|
41
|
+
{basicDate && (
|
|
42
|
+
<Text size="small" color="secondary">
|
|
43
|
+
Selected: {basicDate.toDateString()}
|
|
44
|
+
</Text>
|
|
45
|
+
)}
|
|
46
|
+
</View>
|
|
47
|
+
|
|
48
|
+
{/* Date Range Restricted */}
|
|
49
|
+
<View spacing="md">
|
|
50
|
+
<Text size="medium" weight="semibold">Date Range Restricted</Text>
|
|
51
|
+
<DatePicker
|
|
52
|
+
value={rangeDate}
|
|
53
|
+
onChange={setRangeDate}
|
|
54
|
+
label="Future Dates Only"
|
|
55
|
+
placeholder="Select future date"
|
|
56
|
+
minDate={tomorrow}
|
|
57
|
+
maxDate={nextMonth}
|
|
58
|
+
helperText="Only dates between tomorrow and next month"
|
|
59
|
+
/>
|
|
60
|
+
{rangeDate && (
|
|
61
|
+
<Text size="small" color="secondary">
|
|
62
|
+
Selected: {rangeDate.toDateString()}
|
|
63
|
+
</Text>
|
|
64
|
+
)}
|
|
65
|
+
</View>
|
|
66
|
+
|
|
67
|
+
{/* Disabled DatePicker */}
|
|
68
|
+
<View spacing="md">
|
|
69
|
+
<Text size="medium" weight="semibold">Disabled DatePicker</Text>
|
|
70
|
+
<DatePicker
|
|
71
|
+
value={disabledDate}
|
|
72
|
+
onChange={setDisabledDate}
|
|
73
|
+
label="Disabled"
|
|
74
|
+
placeholder="Cannot select"
|
|
75
|
+
disabled
|
|
76
|
+
helperText="This picker is disabled"
|
|
77
|
+
/>
|
|
78
|
+
</View>
|
|
79
|
+
|
|
80
|
+
{/* Size Variants */}
|
|
81
|
+
<View spacing="md">
|
|
82
|
+
<Text size="medium" weight="semibold">Size Variants</Text>
|
|
83
|
+
<View spacing="sm">
|
|
84
|
+
<DatePicker
|
|
85
|
+
value={null}
|
|
86
|
+
onChange={() => {}}
|
|
87
|
+
label="Small"
|
|
88
|
+
placeholder="Small size"
|
|
89
|
+
size="small"
|
|
90
|
+
/>
|
|
91
|
+
<DatePicker
|
|
92
|
+
value={null}
|
|
93
|
+
onChange={() => {}}
|
|
94
|
+
label="Medium"
|
|
95
|
+
placeholder="Medium size"
|
|
96
|
+
size="medium"
|
|
97
|
+
/>
|
|
98
|
+
<DatePicker
|
|
99
|
+
value={null}
|
|
100
|
+
onChange={() => {}}
|
|
101
|
+
label="Large"
|
|
102
|
+
placeholder="Large size"
|
|
103
|
+
size="large"
|
|
104
|
+
/>
|
|
105
|
+
</View>
|
|
106
|
+
</View>
|
|
107
|
+
|
|
108
|
+
{/* Actions */}
|
|
109
|
+
<View spacing="md">
|
|
110
|
+
<Text size="medium" weight="semibold">Actions</Text>
|
|
111
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
112
|
+
<Button
|
|
113
|
+
variant="outlined"
|
|
114
|
+
onPress={() => setBasicDate(new Date())}
|
|
115
|
+
>
|
|
116
|
+
Set Today
|
|
117
|
+
</Button>
|
|
118
|
+
<Button
|
|
119
|
+
variant="outlined"
|
|
120
|
+
onPress={() => {
|
|
121
|
+
setBasicDate(null);
|
|
122
|
+
setRangeDate(null);
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
Clear All
|
|
126
|
+
</Button>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
{/* DateTime Picker Examples */}
|
|
131
|
+
<View spacing="md">
|
|
132
|
+
<Text size="medium" weight="semibold">DateTimePicker Examples</Text>
|
|
133
|
+
|
|
134
|
+
{/* Basic DateTime */}
|
|
135
|
+
<View spacing="sm">
|
|
136
|
+
<Text size="small" weight="medium">12-hour format</Text>
|
|
137
|
+
<DateTimePicker
|
|
138
|
+
value={dateTime}
|
|
139
|
+
onChange={setDateTime}
|
|
140
|
+
label="Select Date & Time"
|
|
141
|
+
placeholder="Choose date and time"
|
|
142
|
+
helperText="12-hour format with AM/PM"
|
|
143
|
+
/>
|
|
144
|
+
{dateTime && (
|
|
145
|
+
<Text size="small" color="secondary">
|
|
146
|
+
Selected: {dateTime.toLocaleString()}
|
|
147
|
+
</Text>
|
|
148
|
+
)}
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
{/* 24-hour format */}
|
|
152
|
+
<View spacing="sm">
|
|
153
|
+
<Text size="small" weight="medium">24-hour format with seconds</Text>
|
|
154
|
+
<DateTimePicker
|
|
155
|
+
value={dateTime24h}
|
|
156
|
+
onChange={setDateTime24h}
|
|
157
|
+
label="24-hour with seconds"
|
|
158
|
+
placeholder="Choose date and time"
|
|
159
|
+
timeMode="24h"
|
|
160
|
+
showSeconds={true}
|
|
161
|
+
timeStep={5}
|
|
162
|
+
helperText="24-hour format with seconds, 5-minute steps"
|
|
163
|
+
/>
|
|
164
|
+
{dateTime24h && (
|
|
165
|
+
<Text size="small" color="secondary">
|
|
166
|
+
Selected: {dateTime24h.toLocaleString()}
|
|
167
|
+
</Text>
|
|
168
|
+
)}
|
|
169
|
+
</View>
|
|
170
|
+
</View>
|
|
171
|
+
|
|
172
|
+
{/* Date Range Picker Examples */}
|
|
173
|
+
<View spacing="md">
|
|
174
|
+
<Text size="medium" weight="semibold">DateRangePicker Examples</Text>
|
|
175
|
+
|
|
176
|
+
{/* Basic Range */}
|
|
177
|
+
<View spacing="sm">
|
|
178
|
+
<Text size="small" weight="medium">Basic range selection</Text>
|
|
179
|
+
<DateRangePicker
|
|
180
|
+
value={dateRange}
|
|
181
|
+
onChange={setDateRange}
|
|
182
|
+
label="Select Date Range"
|
|
183
|
+
placeholder="Choose start and end dates"
|
|
184
|
+
helperText="Click dates to select a range"
|
|
185
|
+
/>
|
|
186
|
+
{dateRange?.startDate && dateRange?.endDate && (
|
|
187
|
+
<Text size="small" color="secondary">
|
|
188
|
+
Range: {dateRange.startDate.toDateString()} to {dateRange.endDate.toDateString()}
|
|
189
|
+
</Text>
|
|
190
|
+
)}
|
|
191
|
+
</View>
|
|
192
|
+
|
|
193
|
+
{/* Restricted Range */}
|
|
194
|
+
<View spacing="sm">
|
|
195
|
+
<Text size="small" weight="medium">Max 14 days, future dates only</Text>
|
|
196
|
+
<DateRangePicker
|
|
197
|
+
value={restrictedRange}
|
|
198
|
+
onChange={setRestrictedRange}
|
|
199
|
+
label="Restricted Range"
|
|
200
|
+
placeholder="Maximum 14 days"
|
|
201
|
+
minDate={tomorrow}
|
|
202
|
+
maxDays={14}
|
|
203
|
+
helperText="Future dates only, max 14 days"
|
|
204
|
+
/>
|
|
205
|
+
{restrictedRange?.startDate && restrictedRange?.endDate && (
|
|
206
|
+
<Text size="small" color="secondary">
|
|
207
|
+
Range: {restrictedRange.startDate.toDateString()} to {restrictedRange.endDate.toDateString()}
|
|
208
|
+
</Text>
|
|
209
|
+
)}
|
|
210
|
+
</View>
|
|
211
|
+
</View>
|
|
212
|
+
|
|
213
|
+
{/* Date Time Range Picker Examples */}
|
|
214
|
+
<View spacing="md">
|
|
215
|
+
<Text size="medium" weight="semibold">DateTimeRangePicker Examples</Text>
|
|
216
|
+
|
|
217
|
+
{/* Basic DateTime Range */}
|
|
218
|
+
<View spacing="sm">
|
|
219
|
+
<Text size="small" weight="medium">Date and time range selection</Text>
|
|
220
|
+
<DateTimeRangePicker
|
|
221
|
+
value={dateTimeRange}
|
|
222
|
+
onChange={setDateTimeRange}
|
|
223
|
+
label="Select Date & Time Range"
|
|
224
|
+
placeholder="Choose date/time range"
|
|
225
|
+
helperText="Select date range first, then adjust times"
|
|
226
|
+
/>
|
|
227
|
+
{dateTimeRange?.startDate && dateTimeRange?.endDate && (
|
|
228
|
+
<Text size="small" color="secondary">
|
|
229
|
+
Range: {dateTimeRange.startDate.toLocaleString()} to {dateTimeRange.endDate.toLocaleString()}
|
|
230
|
+
</Text>
|
|
231
|
+
)}
|
|
232
|
+
</View>
|
|
233
|
+
</View>
|
|
234
|
+
|
|
235
|
+
{/* Features Description */}
|
|
236
|
+
<View spacing="md">
|
|
237
|
+
<Text size="medium" weight="semibold">Features</Text>
|
|
238
|
+
<View spacing="sm">
|
|
239
|
+
<Text size="small" color="secondary">
|
|
240
|
+
• Cross-platform calendar picker
|
|
241
|
+
</Text>
|
|
242
|
+
<Text size="small" color="secondary">
|
|
243
|
+
• Date and time selection
|
|
244
|
+
</Text>
|
|
245
|
+
<Text size="small" color="secondary">
|
|
246
|
+
• Date range selection
|
|
247
|
+
</Text>
|
|
248
|
+
<Text size="small" color="secondary">
|
|
249
|
+
• Date/time range selection
|
|
250
|
+
</Text>
|
|
251
|
+
<Text size="small" color="secondary">
|
|
252
|
+
• 12/24 hour time formats
|
|
253
|
+
</Text>
|
|
254
|
+
<Text size="small" color="secondary">
|
|
255
|
+
• Min/max date restrictions
|
|
256
|
+
</Text>
|
|
257
|
+
<Text size="small" color="secondary">
|
|
258
|
+
• Multiple size variants
|
|
259
|
+
</Text>
|
|
260
|
+
<Text size="small" color="secondary">
|
|
261
|
+
• Accessible and keyboard navigable
|
|
262
|
+
</Text>
|
|
263
|
+
<Text size="small" color="secondary">
|
|
264
|
+
• Theme-aware styling
|
|
265
|
+
</Text>
|
|
266
|
+
<Text size="small" color="secondary">
|
|
267
|
+
• Customizable date/time formats
|
|
268
|
+
</Text>
|
|
269
|
+
</View>
|
|
270
|
+
</View>
|
|
271
|
+
</View>
|
|
272
|
+
</Screen>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DatePickerExamples } from './DatePickerExamples';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Date Picker Components
|
|
2
|
+
export { DatePicker, Calendar } from './DatePicker/index.native';
|
|
3
|
+
export type { DatePickerProps, CalendarProps } from './DatePicker';
|
|
4
|
+
|
|
5
|
+
// Date Time Picker Components
|
|
6
|
+
export { DateTimePicker, TimePicker } from './DateTimePicker/index.native';
|
|
7
|
+
export type { DateTimePickerProps, TimePickerProps } from './DateTimePicker';
|
|
8
|
+
|
|
9
|
+
// Date Range Picker Components
|
|
10
|
+
export { DateRangePicker, RangeCalendar } from './DateRangePicker/index.native';
|
|
11
|
+
export type { DateRangePickerProps, RangeCalendarProps, DateRange } from './DateRangePicker';
|
|
12
|
+
|
|
13
|
+
// Date Time Range Picker Components
|
|
14
|
+
export { DateTimeRangePicker } from './DateTimeRangePicker/index.native';
|
|
15
|
+
export type { DateTimeRangePickerProps, DateTimeRange } from './DateTimeRangePicker';
|
|
16
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Date Picker Components
|
|
2
|
+
export { DatePicker, Calendar } from './DatePicker';
|
|
3
|
+
export type { DatePickerProps, CalendarProps } from './DatePicker';
|
|
4
|
+
|
|
5
|
+
// Date Time Picker Components
|
|
6
|
+
export { DateTimePicker, TimePicker } from './DateTimePicker';
|
|
7
|
+
export type { DateTimePickerProps, TimePickerProps } from './DateTimePicker';
|
|
8
|
+
|
|
9
|
+
// Date Range Picker Components
|
|
10
|
+
export { DateRangePicker, RangeCalendar } from './DateRangePicker';
|
|
11
|
+
export type { DateRangePickerProps, RangeCalendarProps, DateRange } from './DateRangePicker';
|
|
12
|
+
|
|
13
|
+
// Date Time Range Picker Components
|
|
14
|
+
export { DateTimeRangePicker } from './DateTimeRangePicker';
|
|
15
|
+
export type { DateTimeRangePickerProps, DateTimeRange } from './DateTimeRangePicker';
|
|
16
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const calendarGridStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
weekdayHeader: {
|
|
5
|
+
display: 'grid',
|
|
6
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
7
|
+
gap: 2,
|
|
8
|
+
marginBottom: 8,
|
|
9
|
+
|
|
10
|
+
_web: {
|
|
11
|
+
display: 'grid',
|
|
12
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
weekdayCell: {
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
justifyContent: 'center',
|
|
18
|
+
paddingVertical: 4,
|
|
19
|
+
|
|
20
|
+
_web: {
|
|
21
|
+
display: 'flex',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
weekdayText: {
|
|
27
|
+
fontSize: 12,
|
|
28
|
+
fontWeight: '500',
|
|
29
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
30
|
+
},
|
|
31
|
+
calendarGrid: {
|
|
32
|
+
gap: 2,
|
|
33
|
+
marginBottom: 8,
|
|
34
|
+
height: 192,
|
|
35
|
+
|
|
36
|
+
_web: {
|
|
37
|
+
display: 'grid',
|
|
38
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
dayCell: {
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
minHeight: 0,
|
|
45
|
+
|
|
46
|
+
_web: {
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
dayButton: {
|
|
53
|
+
width: '100%',
|
|
54
|
+
height: '100%',
|
|
55
|
+
maxWidth: 36,
|
|
56
|
+
minWidth: 24,
|
|
57
|
+
minHeight: 24,
|
|
58
|
+
padding: 0,
|
|
59
|
+
borderRadius: 4,
|
|
60
|
+
fontSize: 13,
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Text, Button, View } from '@idealyst/components';
|
|
3
|
+
import { calendarGridStyles } from './CalendarGrid.styles';
|
|
4
|
+
|
|
5
|
+
interface CalendarGridProps {
|
|
6
|
+
currentMonth: Date;
|
|
7
|
+
selectedDate?: Date;
|
|
8
|
+
onDateSelect: (date: Date) => void;
|
|
9
|
+
onMonthChange?: (month: Date) => void;
|
|
10
|
+
minDate?: Date;
|
|
11
|
+
maxDate?: Date;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export const CalendarGrid: React.FC<CalendarGridProps> = ({
|
|
17
|
+
currentMonth,
|
|
18
|
+
selectedDate,
|
|
19
|
+
onDateSelect,
|
|
20
|
+
onMonthChange,
|
|
21
|
+
minDate,
|
|
22
|
+
maxDate,
|
|
23
|
+
disabled = false,
|
|
24
|
+
}) => {
|
|
25
|
+
const { calendarDays, startingDayOfWeek, daysInMonth, weekCount } = useMemo(() => {
|
|
26
|
+
const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
|
|
27
|
+
const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0);
|
|
28
|
+
const daysInMonth = monthEnd.getDate();
|
|
29
|
+
const startingDayOfWeek = monthStart.getDay();
|
|
30
|
+
|
|
31
|
+
const calendarDays = [];
|
|
32
|
+
|
|
33
|
+
// Add previous month days to fill start of week
|
|
34
|
+
const prevMonthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 0);
|
|
35
|
+
const prevMonthDaysToShow = startingDayOfWeek;
|
|
36
|
+
for (let i = prevMonthDaysToShow - 1; i >= 0; i--) {
|
|
37
|
+
const day = prevMonthEnd.getDate() - i;
|
|
38
|
+
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, day);
|
|
39
|
+
calendarDays.push({ date, isCurrentMonth: false });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add days of the current month
|
|
43
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
44
|
+
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
|
|
45
|
+
calendarDays.push({ date, isCurrentMonth: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add next month days only to complete partial weeks, never add full weeks of next month
|
|
49
|
+
const currentLength = calendarDays.length;
|
|
50
|
+
const weeksNeeded = Math.ceil(currentLength / 7);
|
|
51
|
+
const maxDays = weeksNeeded * 7;
|
|
52
|
+
|
|
53
|
+
// Only add next month days to fill partial week
|
|
54
|
+
const remainingDays = maxDays - currentLength;
|
|
55
|
+
for (let day = 1; day <= remainingDays; day++) {
|
|
56
|
+
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, day);
|
|
57
|
+
calendarDays.push({ date, isCurrentMonth: false });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const weekCount = Math.ceil(calendarDays.length / 7);
|
|
61
|
+
return { calendarDays, startingDayOfWeek, daysInMonth, weekCount };
|
|
62
|
+
}, [currentMonth]);
|
|
63
|
+
|
|
64
|
+
const isDateDisabled = (date: Date): boolean => {
|
|
65
|
+
if (disabled) return true;
|
|
66
|
+
if (minDate && date < minDate) return true;
|
|
67
|
+
if (maxDate && date > maxDate) return true;
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isDateSelected = (date: Date): boolean => {
|
|
72
|
+
if (!selectedDate) return false;
|
|
73
|
+
return (
|
|
74
|
+
date.getDate() === selectedDate.getDate() &&
|
|
75
|
+
date.getMonth() === selectedDate.getMonth() &&
|
|
76
|
+
date.getFullYear() === selectedDate.getFullYear()
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const isCurrentMonth = (date: Date): boolean => {
|
|
81
|
+
return (
|
|
82
|
+
date.getMonth() === currentMonth.getMonth() &&
|
|
83
|
+
date.getFullYear() === currentMonth.getFullYear()
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleDateSelect = (date: Date) => {
|
|
88
|
+
// If selecting a date from previous/next month, change the current month first
|
|
89
|
+
if (!isCurrentMonth(date) && onMonthChange) {
|
|
90
|
+
const newMonth = new Date(date.getFullYear(), date.getMonth(), 1);
|
|
91
|
+
onMonthChange(newMonth);
|
|
92
|
+
}
|
|
93
|
+
onDateSelect(date);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
{/* Weekday headers */}
|
|
99
|
+
<View style={calendarGridStyles.weekdayHeader}>
|
|
100
|
+
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((day) => (
|
|
101
|
+
<View key={day} style={calendarGridStyles.weekdayCell}>
|
|
102
|
+
<Text style={calendarGridStyles.weekdayText}>
|
|
103
|
+
{day}
|
|
104
|
+
</Text>
|
|
105
|
+
</View>
|
|
106
|
+
))}
|
|
107
|
+
</View>
|
|
108
|
+
|
|
109
|
+
{/* Calendar grid */}
|
|
110
|
+
<View
|
|
111
|
+
style={[calendarGridStyles.calendarGrid, { gridTemplateRows: `repeat(${weekCount}, 1fr)` }]}
|
|
112
|
+
>
|
|
113
|
+
{calendarDays.map((dayData, index) => {
|
|
114
|
+
const { date, isCurrentMonth } = dayData;
|
|
115
|
+
const buttonStyle = [
|
|
116
|
+
calendarGridStyles.dayButton,
|
|
117
|
+
{ opacity: isCurrentMonth ? 1 : 0.4 }
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View key={index} style={calendarGridStyles.dayCell}>
|
|
122
|
+
<Button
|
|
123
|
+
variant={isDateSelected(date) ? 'contained' : 'text'}
|
|
124
|
+
intent={isDateSelected(date) ? 'primary' : 'neutral'}
|
|
125
|
+
disabled={isDateDisabled(date)}
|
|
126
|
+
onPress={() => handleDateSelect(date)}
|
|
127
|
+
size="small"
|
|
128
|
+
style={buttonStyle}
|
|
129
|
+
>
|
|
130
|
+
{date.getDate()}
|
|
131
|
+
</Button>
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</View>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CalendarGrid } from './CalendarGrid';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const calendarHeaderStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
flexDirection: 'row',
|
|
6
|
+
justifyContent: 'space-between',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
marginBottom: 16,
|
|
9
|
+
},
|
|
10
|
+
centerSection: {
|
|
11
|
+
flexDirection: 'row',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
gap: 8,
|
|
14
|
+
},
|
|
15
|
+
navButton: {
|
|
16
|
+
minWidth: 32,
|
|
17
|
+
minHeight: 32,
|
|
18
|
+
paddingHorizontal: 8,
|
|
19
|
+
paddingVertical: 4,
|
|
20
|
+
},
|
|
21
|
+
titleButton: {
|
|
22
|
+
paddingHorizontal: 8,
|
|
23
|
+
paddingVertical: 4,
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Button, View } from '@idealyst/components';
|
|
3
|
+
import { calendarHeaderStyles } from './CalendarHeader.styles';
|
|
4
|
+
|
|
5
|
+
interface CalendarHeaderProps {
|
|
6
|
+
currentMonth: Date;
|
|
7
|
+
onPreviousMonth: () => void;
|
|
8
|
+
onNextMonth: () => void;
|
|
9
|
+
onMonthClick: () => void;
|
|
10
|
+
onYearClick: () => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export const CalendarHeader: React.FC<CalendarHeaderProps> = ({
|
|
16
|
+
currentMonth,
|
|
17
|
+
onPreviousMonth,
|
|
18
|
+
onNextMonth,
|
|
19
|
+
onMonthClick,
|
|
20
|
+
onYearClick,
|
|
21
|
+
disabled = false,
|
|
22
|
+
}) => {
|
|
23
|
+
const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long' });
|
|
24
|
+
const year = currentMonth.getFullYear();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={calendarHeaderStyles.container}>
|
|
28
|
+
<Button
|
|
29
|
+
variant="text"
|
|
30
|
+
size="small"
|
|
31
|
+
onPress={onPreviousMonth}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
style={calendarHeaderStyles.navButton}
|
|
34
|
+
>
|
|
35
|
+
←
|
|
36
|
+
</Button>
|
|
37
|
+
|
|
38
|
+
<View style={calendarHeaderStyles.centerSection}>
|
|
39
|
+
<Button
|
|
40
|
+
variant="text"
|
|
41
|
+
onPress={onMonthClick}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
style={calendarHeaderStyles.titleButton}
|
|
44
|
+
>
|
|
45
|
+
<Text weight="semibold">{monthName}</Text>
|
|
46
|
+
</Button>
|
|
47
|
+
|
|
48
|
+
<Button
|
|
49
|
+
variant="text"
|
|
50
|
+
onPress={onYearClick}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
style={calendarHeaderStyles.titleButton}
|
|
53
|
+
>
|
|
54
|
+
<Text weight="semibold">{year}</Text>
|
|
55
|
+
</Button>
|
|
56
|
+
</View>
|
|
57
|
+
|
|
58
|
+
<Button
|
|
59
|
+
variant="text"
|
|
60
|
+
size="small"
|
|
61
|
+
onPress={onNextMonth}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
style={calendarHeaderStyles.navButton}
|
|
64
|
+
>
|
|
65
|
+
→
|
|
66
|
+
</Button>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CalendarHeader } from './CalendarHeader';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const calendarOverlayStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
position: 'absolute',
|
|
6
|
+
top: 40,
|
|
7
|
+
left: 0,
|
|
8
|
+
right: 0,
|
|
9
|
+
backgroundColor: theme.colors?.surface?.primary || 'white',
|
|
10
|
+
borderRadius: 8,
|
|
11
|
+
padding: 12,
|
|
12
|
+
borderWidth: 1,
|
|
13
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
14
|
+
zIndex: 10,
|
|
15
|
+
|
|
16
|
+
_web: {
|
|
17
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
header: {
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
justifyContent: 'space-between',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
marginBottom: 12,
|
|
25
|
+
},
|
|
26
|
+
title: {
|
|
27
|
+
fontSize: 14,
|
|
28
|
+
fontWeight: '600',
|
|
29
|
+
margin: 0,
|
|
30
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
31
|
+
},
|
|
32
|
+
closeButton: {
|
|
33
|
+
minWidth: 24,
|
|
34
|
+
minHeight: 24,
|
|
35
|
+
padding: 2,
|
|
36
|
+
fontSize: 12,
|
|
37
|
+
},
|
|
38
|
+
monthGrid: {
|
|
39
|
+
gap: 6,
|
|
40
|
+
|
|
41
|
+
_web: {
|
|
42
|
+
display: 'grid',
|
|
43
|
+
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
yearContainer: {
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
gap: 8,
|
|
49
|
+
},
|
|
50
|
+
yearNavigation: {
|
|
51
|
+
flexDirection: 'row',
|
|
52
|
+
justifyContent: 'space-between',
|
|
53
|
+
alignItems: 'center',
|
|
54
|
+
},
|
|
55
|
+
yearGrid: {
|
|
56
|
+
gap: 6,
|
|
57
|
+
|
|
58
|
+
_web: {
|
|
59
|
+
display: 'grid',
|
|
60
|
+
gridTemplateColumns: 'repeat(4, 1fr)',
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
yearNavButton: {
|
|
64
|
+
minWidth: 32,
|
|
65
|
+
minHeight: 24,
|
|
66
|
+
paddingHorizontal: 8,
|
|
67
|
+
paddingVertical: 4,
|
|
68
|
+
fontSize: 12,
|
|
69
|
+
},
|
|
70
|
+
yearRangeText: {
|
|
71
|
+
fontSize: 13,
|
|
72
|
+
fontWeight: '500',
|
|
73
|
+
color: theme.colors?.text?.primary || '#374151',
|
|
74
|
+
},
|
|
75
|
+
gridButton: {
|
|
76
|
+
minHeight: 28,
|
|
77
|
+
paddingHorizontal: 8,
|
|
78
|
+
paddingVertical: 4,
|
|
79
|
+
fontSize: 12,
|
|
80
|
+
},
|
|
81
|
+
}));
|