@idealyst/datepicker 1.0.0 → 1.0.41

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 (36) hide show
  1. package/package.json +6 -2
  2. package/src/DatePicker/Calendar.native.tsx +180 -78
  3. package/src/DatePicker/Calendar.styles.tsx +73 -70
  4. package/src/DatePicker/DatePicker.native.tsx +24 -6
  5. package/src/DatePicker/DatePicker.styles.tsx +18 -11
  6. package/src/DatePicker/DatePicker.web.tsx +1 -1
  7. package/src/DatePicker/index.ts +1 -1
  8. package/src/DateRangePicker/RangeCalendar.native.tsx +143 -55
  9. package/src/DateRangePicker/RangeCalendar.styles.tsx +65 -39
  10. package/src/DateRangePicker/RangeCalendar.web.tsx +169 -60
  11. package/src/DateRangePicker/types.ts +9 -0
  12. package/src/DateTimePicker/DateTimePicker.native.tsx +11 -69
  13. package/src/DateTimePicker/DateTimePicker.tsx +12 -70
  14. package/src/DateTimePicker/DateTimePickerBase.tsx +241 -0
  15. package/src/DateTimePicker/TimePicker.native.tsx +9 -196
  16. package/src/DateTimePicker/TimePicker.styles.tsx +4 -2
  17. package/src/DateTimePicker/TimePicker.tsx +9 -401
  18. package/src/DateTimePicker/TimePickerBase.tsx +232 -0
  19. package/src/DateTimePicker/primitives/ClockFace.native.tsx +195 -0
  20. package/src/DateTimePicker/primitives/ClockFace.web.tsx +168 -0
  21. package/src/DateTimePicker/primitives/TimeInput.native.tsx +53 -0
  22. package/src/DateTimePicker/primitives/TimeInput.web.tsx +66 -0
  23. package/src/DateTimePicker/primitives/index.native.ts +2 -0
  24. package/src/DateTimePicker/primitives/index.ts +2 -0
  25. package/src/DateTimePicker/primitives/index.web.ts +2 -0
  26. package/src/DateTimePicker/types.ts +0 -4
  27. package/src/DateTimePicker/utils/dimensions.native.ts +9 -0
  28. package/src/DateTimePicker/utils/dimensions.ts +9 -0
  29. package/src/DateTimePicker/utils/dimensions.web.ts +33 -0
  30. package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +10 -199
  31. package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +3 -0
  32. package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +11 -131
  33. package/src/DateTimeRangePicker/DateTimeRangePickerBase.tsx +391 -0
  34. package/src/DateTimeRangePicker/types.ts +0 -2
  35. package/src/examples/DatePickerExamples.tsx +42 -118
  36. /package/src/DatePicker/{Calendar.tsx → Calendar.web.tsx} +0 -0
@@ -3,10 +3,14 @@ import { View, Text, Button } from '@idealyst/components';
3
3
  import { TouchableOpacity } from 'react-native';
4
4
  import { RangeCalendarProps, DateRange } from './types';
5
5
  import { rangeCalendarStyles } from './RangeCalendar.styles';
6
+ import { CalendarOverlay } from '../primitives/CalendarOverlay';
6
7
 
7
8
  export const RangeCalendar: React.FC<RangeCalendarProps> = ({
8
9
  value = {},
9
10
  onChange,
11
+ onDateSelected,
12
+ showTimes = false,
13
+ timeMode = '12h',
10
14
  minDate,
11
15
  maxDate,
12
16
  disabled = false,
@@ -21,6 +25,7 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
21
25
  controlledCurrentMonth || value?.startDate || new Date()
22
26
  );
23
27
  const [selectingEnd, setSelectingEnd] = useState(false);
28
+ const [overlayMode, setOverlayMode] = useState<'month' | 'year' | null>(null);
24
29
 
25
30
  const currentMonth = controlledCurrentMonth || internalCurrentMonth;
26
31
 
@@ -78,6 +83,34 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
78
83
  return isDateRangeStart(date) || isDateRangeEnd(date);
79
84
  };
80
85
 
86
+ const formatTime = (date: Date): string => {
87
+ let hours = date.getHours();
88
+ const minutes = String(date.getMinutes()).padStart(2, '0');
89
+
90
+ if (timeMode === '12h') {
91
+ const ampm = hours >= 12 ? 'PM' : 'AM';
92
+ hours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
93
+ return `${hours}:${minutes}${ampm}`;
94
+ } else {
95
+ return `${String(hours).padStart(2, '0')}:${minutes}`;
96
+ }
97
+ };
98
+
99
+ const getDateTimeInfo = (date: Date) => {
100
+ if (!showTimes) return null;
101
+
102
+ const isStart = isDateRangeStart(date);
103
+ const isEnd = isDateRangeEnd(date);
104
+
105
+ if (isStart && value?.startDate) {
106
+ return { type: 'start', time: formatTime(value.startDate) };
107
+ }
108
+ if (isEnd && value?.endDate) {
109
+ return { type: 'end', time: formatTime(value.endDate) };
110
+ }
111
+ return null;
112
+ };
113
+
81
114
  const handleDateClick = (date: Date) => {
82
115
  if (isDateDisabled(date)) return;
83
116
 
@@ -87,6 +120,7 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
87
120
  if (!startDate || (startDate && endDate)) {
88
121
  onChange({ startDate: date, endDate: undefined });
89
122
  setSelectingEnd(true);
123
+ onDateSelected?.('start');
90
124
  return;
91
125
  }
92
126
 
@@ -116,6 +150,7 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
116
150
 
117
151
  onChange({ startDate: newStartDate, endDate: newEndDate });
118
152
  setSelectingEnd(false);
153
+ onDateSelected?.('end');
119
154
  }
120
155
  };
121
156
 
@@ -129,34 +164,56 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
129
164
  handleMonthChange(newMonth);
130
165
  };
131
166
 
132
- const handlePresetRange = (days: number) => {
133
- const startDate = new Date();
134
- const endDate = new Date();
135
- endDate.setDate(endDate.getDate() + days - 1);
136
-
137
- onChange({ startDate, endDate });
138
- setSelectingEnd(false);
167
+ const createDateWithDayAdjustment = (year: number, month: number, day: number): Date => {
168
+ // Get the last day of the target month
169
+ const lastDayOfMonth = new Date(year, month + 1, 0).getDate();
170
+ // Use the smaller of the requested day or the last day of the month
171
+ const adjustedDay = Math.min(day, lastDayOfMonth);
172
+ return new Date(year, month, adjustedDay);
139
173
  };
140
174
 
141
- const clearRange = () => {
142
- onChange({});
143
- setSelectingEnd(false);
175
+ const handleMonthSelect = (monthIndex: number) => {
176
+ const newMonth = new Date(currentMonth.getFullYear(), monthIndex, 1);
177
+ handleMonthChange(newMonth);
178
+ setOverlayMode(null);
179
+ };
180
+
181
+ const handleYearSelect = (year: number) => {
182
+ const newMonth = new Date(year, currentMonth.getMonth(), 1);
183
+ handleMonthChange(newMonth);
184
+ setOverlayMode(null);
144
185
  };
145
186
 
187
+
146
188
  const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
147
189
 
148
190
  // Create calendar grid
149
191
  const calendarDays = [];
150
192
 
151
- // Add empty cells for days before month starts
152
- for (let i = 0; i < startingDayOfWeek; i++) {
153
- calendarDays.push(null);
193
+ // Add previous month days to fill start of week
194
+ const prevMonthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 0);
195
+ const prevMonthDaysToShow = startingDayOfWeek;
196
+ for (let i = prevMonthDaysToShow - 1; i >= 0; i--) {
197
+ const day = prevMonthEnd.getDate() - i;
198
+ const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, day);
199
+ calendarDays.push({ date, isCurrentMonth: false });
154
200
  }
155
201
 
156
- // Add days of the month
202
+ // Add days of the current month
157
203
  for (let day = 1; day <= daysInMonth; day++) {
158
204
  const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
159
- calendarDays.push(date);
205
+ calendarDays.push({ date, isCurrentMonth: true });
206
+ }
207
+
208
+ // Add next month days to complete partial weeks
209
+ const currentLength = calendarDays.length;
210
+ const weeksNeeded = Math.ceil(currentLength / 7);
211
+ const totalCalendarDays = weeksNeeded * 7;
212
+
213
+ const remainingDays = totalCalendarDays - currentLength;
214
+ for (let day = 1; day <= remainingDays; day++) {
215
+ const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, day);
216
+ calendarDays.push({ date, isCurrentMonth: false });
160
217
  }
161
218
 
162
219
  rangeCalendarStyles.useVariants({});
@@ -174,7 +231,26 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
174
231
  >
175
232
 
176
233
  </Button>
177
- <Text weight="semibold">{monthName}</Text>
234
+ <View style={{ flexDirection: 'row', gap: 8 }}>
235
+ <Button
236
+ variant="text"
237
+ size="small"
238
+ onPress={() => setOverlayMode('month')}
239
+ disabled={disabled}
240
+ style={{ padding: 4 }}
241
+ >
242
+ <Text weight="semibold">{currentMonth.toLocaleDateString('en-US', { month: 'long' })}</Text>
243
+ </Button>
244
+ <Button
245
+ variant="text"
246
+ size="small"
247
+ onPress={() => setOverlayMode('year')}
248
+ disabled={disabled}
249
+ style={{ padding: 4 }}
250
+ >
251
+ <Text weight="semibold">{currentMonth.getFullYear()}</Text>
252
+ </Button>
253
+ </View>
178
254
  <Button
179
255
  variant="text"
180
256
  size="small"
@@ -199,69 +275,81 @@ export const RangeCalendar: React.FC<RangeCalendarProps> = ({
199
275
 
200
276
  {/* Calendar grid */}
201
277
  <View style={rangeCalendarStyles.calendarGrid}>
202
- {calendarDays.map((date, index) => (
278
+ {calendarDays.map((dayInfo, index) => (
203
279
  <View key={index} style={rangeCalendarStyles.dayCell}>
204
- {date && (
280
+ {dayInfo && (
205
281
  <TouchableOpacity
206
- onPress={() => handleDateClick(date)}
207
- disabled={isDateDisabled(date)}
282
+ onPress={() => handleDateClick(dayInfo.date)}
283
+ disabled={isDateDisabled(dayInfo.date)}
208
284
  style={[
209
285
  rangeCalendarStyles.dayButton,
210
286
  {
211
- backgroundColor: isDateSelected(date)
287
+ backgroundColor: isDateSelected(dayInfo.date)
212
288
  ? '#3b82f6'
213
- : isDateInRange(date)
289
+ : isDateInRange(dayInfo.date)
214
290
  ? '#3b82f620'
215
291
  : 'transparent',
216
- opacity: isDateDisabled(date) ? 0.5 : 1,
292
+ opacity: isDateDisabled(dayInfo.date) ? 0.5 : dayInfo.isCurrentMonth ? 1 : 0.5,
293
+ flexDirection: 'column',
294
+ justifyContent: 'center',
295
+ alignItems: 'center',
296
+ position: 'relative',
297
+ flex: 1,
298
+ aspectRatio: 1,
217
299
  }
218
300
  ]}
219
301
  >
220
302
  <Text
221
303
  style={{
222
- color: isDateSelected(date) ? 'white' : 'black',
304
+ color: isDateSelected(dayInfo.date) ? 'white' : dayInfo.isCurrentMonth ? 'black' : '#9ca3af',
223
305
  fontSize: 13,
224
- fontWeight: isDateSelected(date) ? '600' : '400',
306
+ fontWeight: isDateSelected(dayInfo.date) ? '600' : '400',
307
+ textAlign: 'center',
225
308
  }}
226
309
  >
227
- {date.getDate()}
310
+ {dayInfo.date.getDate()}
228
311
  </Text>
312
+ {(() => {
313
+ const timeInfo = getDateTimeInfo(dayInfo.date);
314
+ if (timeInfo) {
315
+ return (
316
+ <Text
317
+ style={{
318
+ color: isDateSelected(dayInfo.date) ? 'white' : '#666',
319
+ fontSize: 7,
320
+ fontWeight: '500',
321
+ textAlign: 'center',
322
+ marginTop: 1,
323
+ width: '100%',
324
+ }}
325
+ numberOfLines={1}
326
+ ellipsizeMode="tail"
327
+ >
328
+ {timeInfo.time}
329
+ </Text>
330
+ );
331
+ }
332
+ return null;
333
+ })()}
229
334
  </TouchableOpacity>
230
335
  )}
231
336
  </View>
232
337
  ))}
233
338
  </View>
234
339
 
235
- {/* Range presets */}
236
- <View style={rangeCalendarStyles.rangePresets}>
237
- <Button
238
- variant="text"
239
- size="small"
240
- onPress={() => handlePresetRange(7)}
241
- disabled={disabled}
242
- style={rangeCalendarStyles.presetButton}
243
- >
244
- Next 7 days
245
- </Button>
246
- <Button
247
- variant="text"
248
- size="small"
249
- onPress={() => handlePresetRange(30)}
340
+ {/* Overlay for month/year selection */}
341
+ {overlayMode && (
342
+ <CalendarOverlay
343
+ mode={overlayMode}
344
+ currentMonth={currentMonth.getMonth()}
345
+ currentYear={currentMonth.getFullYear()}
346
+ onMonthSelect={handleMonthSelect}
347
+ onYearSelect={handleYearSelect}
348
+ onClose={() => setOverlayMode(null)}
250
349
  disabled={disabled}
251
- style={rangeCalendarStyles.presetButton}
252
- >
253
- Next 30 days
254
- </Button>
255
- <Button
256
- variant="outlined"
257
- size="small"
258
- onPress={clearRange}
259
- disabled={disabled}
260
- style={rangeCalendarStyles.clearButton}
261
- >
262
- Clear Range
263
- </Button>
264
- </View>
350
+ />
351
+ )}
352
+
265
353
  </View>
266
354
  );
267
355
  };
@@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native-unistyles';
3
3
  export const rangeCalendarStyles = StyleSheet.create((theme) => ({
4
4
  container: {
5
5
  width: 256,
6
+ position: 'relative',
6
7
  },
7
8
 
8
9
  header: {
@@ -10,6 +11,10 @@ export const rangeCalendarStyles = StyleSheet.create((theme) => ({
10
11
  justifyContent: 'space-between',
11
12
  alignItems: 'center',
12
13
  marginBottom: theme.spacing?.md || 16,
14
+
15
+ _web: {
16
+ display: 'flex',
17
+ },
13
18
  },
14
19
 
15
20
  headerButton: {
@@ -31,28 +36,34 @@ export const rangeCalendarStyles = StyleSheet.create((theme) => ({
31
36
  },
32
37
 
33
38
  weekdayHeader: {
34
- display: 'grid',
35
- gridTemplateColumns: 'repeat(7, 1fr)',
36
- gap: 2,
39
+ flexDirection: 'row',
37
40
  marginBottom: theme.spacing?.xs || 8,
38
41
 
39
- // Native fallback
40
- _native: {
41
- flexDirection: 'row',
42
+ // Web specific styles
43
+ _web: {
44
+ display: 'grid',
45
+ gridTemplateColumns: 'repeat(7, 1fr)',
46
+ gap: 2,
47
+ flexDirection: undefined,
42
48
  },
43
49
  },
44
50
 
45
51
  weekdayCell: {
46
- display: 'flex',
52
+ flex: 1,
53
+ width: 36,
54
+ minWidth: 36,
55
+ maxWidth: 36,
47
56
  alignItems: 'center',
48
57
  justifyContent: 'center',
49
58
  paddingVertical: theme.spacing?.xs || 4,
50
59
 
51
- // Native fallback
52
- _native: {
53
- flex: 1,
54
- alignItems: 'center',
55
- paddingVertical: theme.spacing?.xs || 4,
60
+ // Web specific styles
61
+ _web: {
62
+ display: 'flex',
63
+ flex: undefined,
64
+ width: '36px',
65
+ minWidth: '36px',
66
+ maxWidth: '36px',
56
67
  },
57
68
  },
58
69
 
@@ -63,44 +74,56 @@ export const rangeCalendarStyles = StyleSheet.create((theme) => ({
63
74
  },
64
75
 
65
76
  calendarGrid: {
66
- display: 'grid',
67
- gridTemplateColumns: 'repeat(7, 1fr)',
68
- gap: 2,
77
+ flexDirection: 'row',
78
+ flexWrap: 'wrap',
69
79
  marginBottom: theme.spacing?.xs || 8,
70
80
 
71
- // Native fallback
72
- _native: {
73
- flexDirection: 'row',
74
- flexWrap: 'wrap',
81
+ // Web specific styles
82
+ _web: {
83
+ display: 'grid',
84
+ gridTemplateColumns: 'repeat(7, 1fr)',
85
+ gap: 2,
86
+ flexDirection: undefined,
87
+ flexWrap: undefined,
75
88
  },
76
89
  },
77
90
 
78
91
  dayCell: {
79
- display: 'flex',
80
- alignItems: 'center',
81
- justifyContent: 'center',
82
- aspectRatio: '1',
83
- minHeight: 32,
92
+ width: '14.28%', // 100% / 7 days
93
+ minWidth: 36,
94
+ maxWidth: 36,
95
+ aspectRatio: 1,
96
+ alignItems: 'stretch',
97
+ justifyContent: 'stretch',
98
+ height: 36,
99
+ maxHeight: 36,
84
100
 
85
- // Native specific sizing
86
- _native: {
87
- width: '14.28%', // 100% / 7 days
88
- aspectRatio: 1,
89
- alignItems: 'center',
90
- justifyContent: 'center',
101
+ // Web specific styles
102
+ _web: {
103
+ display: 'flex',
104
+ width: '36px',
105
+ minWidth: '36px',
106
+ maxWidth: '36px',
107
+ height: '36px',
108
+ maxHeight: '36px',
91
109
  },
92
110
  },
93
111
 
94
112
  dayButton: {
113
+ flex: 1,
95
114
  width: '100%',
96
115
  height: '100%',
116
+ minWidth: 36,
97
117
  maxWidth: 36,
118
+ minHeight: 36,
98
119
  maxHeight: 36,
99
- minWidth: 28,
100
- minHeight: 28,
101
120
  padding: 0,
121
+ margin: 0,
102
122
  borderRadius: theme.borderRadius?.sm || 6,
103
123
  fontSize: theme.typography?.sizes?.small || 13,
124
+ alignItems: 'center',
125
+ justifyContent: 'center',
126
+ overflow: 'hidden',
104
127
 
105
128
  variants: {
106
129
  inRange: {
@@ -137,13 +160,16 @@ export const rangeCalendarStyles = StyleSheet.create((theme) => ({
137
160
  },
138
161
  },
139
162
 
140
- // Native specific styling
141
- _native: {
142
- width: 28,
143
- height: 28,
144
- minWidth: 28,
145
- minHeight: 28,
146
- borderRadius: theme.borderRadius?.sm || 6,
163
+ // Web specific styling
164
+ _web: {
165
+ width: '36px',
166
+ height: '36px',
167
+ minWidth: '36px',
168
+ maxWidth: '36px',
169
+ minHeight: '36px',
170
+ maxHeight: '36px',
171
+ display: 'flex',
172
+ overflow: 'hidden',
147
173
  },
148
174
  },
149
175