@idealyst/datepicker 1.1.3 → 1.1.5

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 (80) hide show
  1. package/package.json +6 -5
  2. package/src/DateInput.native.tsx +155 -0
  3. package/src/DateInput.tsx +2 -0
  4. package/src/DateInput.web.tsx +146 -0
  5. package/src/DatePicker.tsx +276 -0
  6. package/src/DateTimePicker.tsx +89 -0
  7. package/src/TimeInput.native.tsx +175 -0
  8. package/src/TimeInput.tsx +2 -0
  9. package/src/TimeInput.web.tsx +171 -0
  10. package/src/TimePicker.tsx +106 -0
  11. package/src/examples/DatePickerExamples.tsx +113 -149
  12. package/src/examples/index.ts +1 -1
  13. package/src/index.native.ts +15 -20
  14. package/src/index.ts +14 -19
  15. package/src/styles.ts +127 -0
  16. package/src/types.ts +56 -0
  17. package/src/DateInput/DateInput.native.tsx +0 -61
  18. package/src/DateInput/DateInput.styles.tsx +0 -26
  19. package/src/DateInput/DateInput.web.tsx +0 -61
  20. package/src/DateInput/DateInputBase.tsx +0 -228
  21. package/src/DateInput/index.native.ts +0 -2
  22. package/src/DateInput/index.ts +0 -2
  23. package/src/DateInput/types.ts +0 -60
  24. package/src/DatePicker/Calendar.native.tsx +0 -261
  25. package/src/DatePicker/Calendar.styles.tsx +0 -230
  26. package/src/DatePicker/Calendar.web.tsx +0 -159
  27. package/src/DatePicker/DatePicker.native.tsx +0 -51
  28. package/src/DatePicker/DatePicker.styles.tsx +0 -76
  29. package/src/DatePicker/DatePicker.web.tsx +0 -31
  30. package/src/DatePicker/index.native.ts +0 -3
  31. package/src/DatePicker/index.ts +0 -3
  32. package/src/DatePicker/types.ts +0 -78
  33. package/src/DateRangePicker/DateRangePicker.native.tsx +0 -39
  34. package/src/DateRangePicker/DateRangePicker.styles.tsx +0 -83
  35. package/src/DateRangePicker/DateRangePicker.web.tsx +0 -40
  36. package/src/DateRangePicker/RangeCalendar.native.tsx +0 -355
  37. package/src/DateRangePicker/RangeCalendar.styles.tsx +0 -200
  38. package/src/DateRangePicker/RangeCalendar.web.tsx +0 -384
  39. package/src/DateRangePicker/index.native.ts +0 -3
  40. package/src/DateRangePicker/index.ts +0 -3
  41. package/src/DateRangePicker/types.ts +0 -107
  42. package/src/DateTimePicker/DateTimePicker.native.tsx +0 -24
  43. package/src/DateTimePicker/DateTimePicker.styles.tsx +0 -104
  44. package/src/DateTimePicker/DateTimePicker.tsx +0 -21
  45. package/src/DateTimePicker/DateTimePickerBase.tsx +0 -185
  46. package/src/DateTimePicker/TimePicker.native.tsx +0 -17
  47. package/src/DateTimePicker/TimePicker.styles.tsx +0 -115
  48. package/src/DateTimePicker/TimePicker.tsx +0 -14
  49. package/src/DateTimePicker/TimePickerBase.tsx +0 -232
  50. package/src/DateTimePicker/index.native.ts +0 -3
  51. package/src/DateTimePicker/index.ts +0 -3
  52. package/src/DateTimePicker/primitives/ClockFace.native.tsx +0 -195
  53. package/src/DateTimePicker/primitives/ClockFace.web.tsx +0 -168
  54. package/src/DateTimePicker/primitives/TimeInput.native.tsx +0 -53
  55. package/src/DateTimePicker/primitives/TimeInput.web.tsx +0 -66
  56. package/src/DateTimePicker/primitives/index.native.ts +0 -2
  57. package/src/DateTimePicker/primitives/index.ts +0 -2
  58. package/src/DateTimePicker/primitives/index.web.ts +0 -2
  59. package/src/DateTimePicker/types.ts +0 -80
  60. package/src/DateTimePicker/utils/dimensions.native.ts +0 -9
  61. package/src/DateTimePicker/utils/dimensions.ts +0 -9
  62. package/src/DateTimePicker/utils/dimensions.web.ts +0 -33
  63. package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +0 -24
  64. package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +0 -118
  65. package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +0 -21
  66. package/src/DateTimeRangePicker/DateTimeRangePickerBase.tsx +0 -327
  67. package/src/DateTimeRangePicker/index.native.ts +0 -2
  68. package/src/DateTimeRangePicker/index.ts +0 -2
  69. package/src/DateTimeRangePicker/types.ts +0 -70
  70. package/src/primitives/CalendarGrid/CalendarGrid.styles.tsx +0 -51
  71. package/src/primitives/CalendarGrid/CalendarGrid.tsx +0 -146
  72. package/src/primitives/CalendarGrid/index.ts +0 -1
  73. package/src/primitives/CalendarHeader/CalendarHeader.styles.tsx +0 -25
  74. package/src/primitives/CalendarHeader/CalendarHeader.tsx +0 -69
  75. package/src/primitives/CalendarHeader/index.ts +0 -1
  76. package/src/primitives/CalendarOverlay/CalendarOverlay.styles.tsx +0 -86
  77. package/src/primitives/CalendarOverlay/CalendarOverlay.tsx +0 -136
  78. package/src/primitives/CalendarOverlay/index.ts +0 -1
  79. package/src/primitives/Wrapper/Wrapper.web.tsx +0 -33
  80. package/src/primitives/Wrapper/index.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/datepicker",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Cross-platform date and time picker components for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/datepicker#readme",
6
6
  "readme": "README.md",
@@ -36,8 +36,8 @@
36
36
  "publish:npm": "npm publish"
37
37
  },
38
38
  "peerDependencies": {
39
- "@idealyst/components": "^1.1.3",
40
- "@idealyst/theme": "^1.1.3",
39
+ "@idealyst/components": "^1.1.5",
40
+ "@idealyst/theme": "^1.1.5",
41
41
  "react": ">=16.8.0",
42
42
  "react-native": ">=0.60.0",
43
43
  "react-native-svg": ">=13.0.0",
@@ -61,11 +61,12 @@
61
61
  }
62
62
  },
63
63
  "devDependencies": {
64
- "@idealyst/components": "^1.1.3",
65
- "@idealyst/theme": "^1.1.3",
64
+ "@idealyst/components": "^1.1.5",
65
+ "@idealyst/theme": "^1.1.5",
66
66
  "@types/react": "^19.1.0",
67
67
  "react": "^19.1.0",
68
68
  "react-native": "^0.80.1",
69
+ "react-native-svg": "^15.15.1",
69
70
  "react-native-unistyles": "^3.0.10",
70
71
  "typescript": "^5.0.0"
71
72
  },
@@ -0,0 +1,155 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Modal, TextInput as RNTextInput } from 'react-native';
3
+ import { View, Text, Button, Icon } from '@idealyst/components';
4
+ import { DatePicker } from './DatePicker';
5
+ import { datePickerStyles } from './styles';
6
+ import type { DateInputProps } from './types';
7
+
8
+ export const DateInput: React.FC<DateInputProps> = ({
9
+ value,
10
+ onChange,
11
+ label,
12
+ placeholder = 'MM/DD/YYYY',
13
+ minDate,
14
+ maxDate,
15
+ disabled = false,
16
+ error,
17
+ style,
18
+ }) => {
19
+ const [open, setOpen] = useState(false);
20
+ const [inputValue, setInputValue] = useState('');
21
+
22
+ // Format date to string
23
+ const formatDate = (date: Date | undefined): string => {
24
+ if (!date) return '';
25
+ const month = String(date.getMonth() + 1).padStart(2, '0');
26
+ const day = String(date.getDate()).padStart(2, '0');
27
+ const year = date.getFullYear();
28
+ return `${month}/${day}/${year}`;
29
+ };
30
+
31
+ // Parse string to date
32
+ const parseDate = (str: string): Date | null => {
33
+ const match = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
34
+ if (!match) return null;
35
+ const [, month, day, year] = match;
36
+ const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
37
+ if (isNaN(date.getTime())) return null;
38
+ return date;
39
+ };
40
+
41
+ // Sync input value with prop value
42
+ useEffect(() => {
43
+ setInputValue(formatDate(value ?? undefined));
44
+ }, [value]);
45
+
46
+ const handleInputChange = (newValue: string) => {
47
+ setInputValue(newValue);
48
+
49
+ // Try to parse and update if valid
50
+ const parsed = parseDate(newValue);
51
+ if (parsed) {
52
+ onChange(parsed);
53
+ }
54
+ };
55
+
56
+ const handleInputBlur = () => {
57
+ // On blur, reset to formatted value if invalid
58
+ const parsed = parseDate(inputValue);
59
+ if (!parsed && value) {
60
+ setInputValue(formatDate(value));
61
+ } else if (!parsed) {
62
+ setInputValue('');
63
+ onChange(null);
64
+ }
65
+ };
66
+
67
+ const handleDateSelect = (date: Date) => {
68
+ onChange(date);
69
+ setOpen(false);
70
+ };
71
+
72
+ return (
73
+ <View style={style}>
74
+ {label && (
75
+ <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
76
+ {label}
77
+ </Text>
78
+ )}
79
+ <View
80
+ style={{
81
+ flexDirection: 'row',
82
+ alignItems: 'center',
83
+ borderWidth: 1,
84
+ borderColor: error ? '#ef4444' : '#d1d5db',
85
+ borderRadius: 6,
86
+ backgroundColor: disabled ? '#f3f4f6' : 'white',
87
+ overflow: 'hidden',
88
+ }}
89
+ >
90
+ <RNTextInput
91
+ value={inputValue}
92
+ onChangeText={handleInputChange}
93
+ onBlur={handleInputBlur}
94
+ placeholder={placeholder}
95
+ editable={!disabled}
96
+ style={{
97
+ flex: 1,
98
+ padding: 8,
99
+ paddingHorizontal: 12,
100
+ fontSize: 14,
101
+ backgroundColor: 'transparent',
102
+ color: disabled ? '#9ca3af' : '#111827',
103
+ }}
104
+ />
105
+ <Button
106
+ type="text"
107
+ size="sm"
108
+ onPress={() => !disabled && setOpen(true)}
109
+ disabled={disabled}
110
+ style={{ marginRight: 4 }}
111
+ >
112
+ <Icon name="calendar" size={18} />
113
+ </Button>
114
+ </View>
115
+ {error && (
116
+ <Text typography="caption" style={{ marginTop: 4, color: '#ef4444' }}>
117
+ {error}
118
+ </Text>
119
+ )}
120
+
121
+ <Modal
122
+ visible={open}
123
+ transparent
124
+ animationType="fade"
125
+ onRequestClose={() => setOpen(false)}
126
+ >
127
+ <View
128
+ style={{
129
+ flex: 1,
130
+ justifyContent: 'center',
131
+ alignItems: 'center',
132
+ backgroundColor: 'rgba(0,0,0,0.5)',
133
+ }}
134
+ >
135
+ <View style={datePickerStyles.popoverContent}>
136
+ <DatePicker
137
+ value={value ?? undefined}
138
+ onChange={handleDateSelect}
139
+ minDate={minDate}
140
+ maxDate={maxDate}
141
+ disabled={disabled}
142
+ />
143
+ <Button
144
+ type="text"
145
+ onPress={() => setOpen(false)}
146
+ style={{ marginTop: 8 }}
147
+ >
148
+ Close
149
+ </Button>
150
+ </View>
151
+ </View>
152
+ </Modal>
153
+ </View>
154
+ );
155
+ };
@@ -0,0 +1,2 @@
1
+ // Platform-specific exports
2
+ export { DateInput } from './DateInput.web';
@@ -0,0 +1,146 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { View, Text, Button, Icon } from '@idealyst/components';
3
+ import { PositionedPortal } from '@idealyst/components/internal';
4
+ import { DatePicker } from './DatePicker';
5
+ import { datePickerStyles } from './styles';
6
+ import type { DateInputProps } from './types';
7
+
8
+ export const DateInput: React.FC<DateInputProps> = ({
9
+ value,
10
+ onChange,
11
+ label,
12
+ placeholder = 'MM/DD/YYYY',
13
+ minDate,
14
+ maxDate,
15
+ disabled = false,
16
+ error,
17
+ style,
18
+ }) => {
19
+ const [open, setOpen] = useState(false);
20
+ const [inputValue, setInputValue] = useState('');
21
+ const triggerRef = useRef<HTMLDivElement>(null);
22
+
23
+ // Format date to string
24
+ const formatDate = (date: Date | undefined): string => {
25
+ if (!date) return '';
26
+ const month = String(date.getMonth() + 1).padStart(2, '0');
27
+ const day = String(date.getDate()).padStart(2, '0');
28
+ const year = date.getFullYear();
29
+ return `${month}/${day}/${year}`;
30
+ };
31
+
32
+ // Parse string to date
33
+ const parseDate = (str: string): Date | null => {
34
+ const match = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
35
+ if (!match) return null;
36
+ const [, month, day, year] = match;
37
+ const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
38
+ if (isNaN(date.getTime())) return null;
39
+ return date;
40
+ };
41
+
42
+ // Sync input value with prop value
43
+ useEffect(() => {
44
+ setInputValue(formatDate(value ?? undefined));
45
+ }, [value]);
46
+
47
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
48
+ const newValue = e.target.value;
49
+ setInputValue(newValue);
50
+
51
+ // Try to parse and update if valid
52
+ const parsed = parseDate(newValue);
53
+ if (parsed) {
54
+ onChange(parsed);
55
+ }
56
+ };
57
+
58
+ const handleInputBlur = () => {
59
+ // On blur, reset to formatted value if invalid
60
+ const parsed = parseDate(inputValue);
61
+ if (!parsed && value) {
62
+ setInputValue(formatDate(value));
63
+ } else if (!parsed) {
64
+ setInputValue('');
65
+ onChange(null);
66
+ }
67
+ };
68
+
69
+ const handleDateSelect = (date: Date) => {
70
+ onChange(date);
71
+ setOpen(false);
72
+ };
73
+
74
+ return (
75
+ <View style={style}>
76
+ {label && (
77
+ <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
78
+ {label}
79
+ </Text>
80
+ )}
81
+ <div
82
+ ref={triggerRef}
83
+ style={{
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ border: `1px solid ${error ? '#ef4444' : '#d1d5db'}`,
87
+ borderRadius: 6,
88
+ backgroundColor: disabled ? '#f3f4f6' : 'white',
89
+ overflow: 'hidden',
90
+ }}
91
+ >
92
+ <input
93
+ type="text"
94
+ value={inputValue}
95
+ onChange={handleInputChange}
96
+ onBlur={handleInputBlur}
97
+ placeholder={placeholder}
98
+ disabled={disabled}
99
+ style={{
100
+ flex: 1,
101
+ padding: '8px 12px',
102
+ fontSize: 14,
103
+ border: 'none',
104
+ outline: 'none',
105
+ backgroundColor: 'transparent',
106
+ color: disabled ? '#9ca3af' : '#111827',
107
+ }}
108
+ />
109
+ <Button
110
+ type="text"
111
+ size="sm"
112
+ onPress={() => !disabled && setOpen(!open)}
113
+ disabled={disabled}
114
+ style={{ marginRight: 4 }}
115
+ >
116
+ <Icon name="calendar" size={18} />
117
+ </Button>
118
+ </div>
119
+ {error && (
120
+ <Text typography="caption" style={{ marginTop: 4, color: '#ef4444' }}>
121
+ {error}
122
+ </Text>
123
+ )}
124
+
125
+ <PositionedPortal
126
+ open={open}
127
+ anchor={triggerRef}
128
+ placement="bottom-start"
129
+ offset={4}
130
+ onClickOutside={() => setOpen(false)}
131
+ onEscapeKey={() => setOpen(false)}
132
+ zIndex={9999}
133
+ >
134
+ <View style={datePickerStyles.popoverContent}>
135
+ <DatePicker
136
+ value={value ?? undefined}
137
+ onChange={handleDateSelect}
138
+ minDate={minDate}
139
+ maxDate={maxDate}
140
+ disabled={disabled}
141
+ />
142
+ </View>
143
+ </PositionedPortal>
144
+ </View>
145
+ );
146
+ };
@@ -0,0 +1,276 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { View, Text, Button, Icon } from '@idealyst/components';
3
+ import { datePickerStyles } from './styles';
4
+ import type { DatePickerProps } from './types';
5
+
6
+ const WEEKDAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
7
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
8
+
9
+ type ViewMode = 'calendar' | 'months' | 'years';
10
+
11
+ export const DatePicker: React.FC<DatePickerProps> = ({
12
+ value,
13
+ onChange,
14
+ minDate,
15
+ maxDate,
16
+ disabled = false,
17
+ style,
18
+ }) => {
19
+ const [currentMonth, setCurrentMonth] = useState(() => value || new Date());
20
+ const [viewMode, setViewMode] = useState<ViewMode>('calendar');
21
+
22
+ datePickerStyles.useVariants({ disabled });
23
+
24
+ const { days, monthLabel, monthShort, year } = useMemo(() => {
25
+ const year = currentMonth.getFullYear();
26
+ const month = currentMonth.getMonth();
27
+ const firstDay = new Date(year, month, 1);
28
+ const lastDay = new Date(year, month + 1, 0);
29
+ const startPadding = firstDay.getDay();
30
+ const daysInMonth = lastDay.getDate();
31
+
32
+ const days: Array<{ date: Date; isCurrentMonth: boolean }> = [];
33
+
34
+ // Previous month padding
35
+ const prevMonthEnd = new Date(year, month, 0);
36
+ for (let i = startPadding - 1; i >= 0; i--) {
37
+ days.push({
38
+ date: new Date(year, month - 1, prevMonthEnd.getDate() - i),
39
+ isCurrentMonth: false,
40
+ });
41
+ }
42
+
43
+ // Current month
44
+ for (let day = 1; day <= daysInMonth; day++) {
45
+ days.push({ date: new Date(year, month, day), isCurrentMonth: true });
46
+ }
47
+
48
+ // Next month padding (fill to complete last week)
49
+ const remaining = 7 - (days.length % 7);
50
+ if (remaining < 7) {
51
+ for (let day = 1; day <= remaining; day++) {
52
+ days.push({ date: new Date(year, month + 1, day), isCurrentMonth: false });
53
+ }
54
+ }
55
+
56
+ const monthLabel = firstDay.toLocaleDateString('en-US', {
57
+ month: 'short',
58
+ year: 'numeric',
59
+ });
60
+
61
+ return { days, monthLabel, monthShort: MONTHS[month], year };
62
+ }, [currentMonth]);
63
+
64
+ const isSelected = (date: Date): boolean => {
65
+ if (!value) return false;
66
+ return (
67
+ date.getDate() === value.getDate() &&
68
+ date.getMonth() === value.getMonth() &&
69
+ date.getFullYear() === value.getFullYear()
70
+ );
71
+ };
72
+
73
+ const isToday = (date: Date): boolean => {
74
+ const today = new Date();
75
+ return (
76
+ date.getDate() === today.getDate() &&
77
+ date.getMonth() === today.getMonth() &&
78
+ date.getFullYear() === today.getFullYear()
79
+ );
80
+ };
81
+
82
+ const isDisabled = (date: Date): boolean => {
83
+ if (disabled) return true;
84
+ if (minDate && date < new Date(minDate.setHours(0, 0, 0, 0))) return true;
85
+ if (maxDate && date > new Date(maxDate.setHours(23, 59, 59, 999))) return true;
86
+ return false;
87
+ };
88
+
89
+ const goToPrevMonth = () => {
90
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1));
91
+ };
92
+
93
+ const goToNextMonth = () => {
94
+ setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1));
95
+ };
96
+
97
+ const handleDayPress = (date: Date) => {
98
+ if (!isDisabled(date)) {
99
+ onChange(date);
100
+ }
101
+ };
102
+
103
+ const handleMonthSelect = (monthIndex: number) => {
104
+ setCurrentMonth(new Date(currentMonth.getFullYear(), monthIndex, 1));
105
+ setViewMode('calendar');
106
+ };
107
+
108
+ const handleYearSelect = (selectedYear: number) => {
109
+ setCurrentMonth(new Date(selectedYear, currentMonth.getMonth(), 1));
110
+ setViewMode('months');
111
+ };
112
+
113
+ // Generate year range (10 years before and after current)
114
+ const yearRange = useMemo(() => {
115
+ const currentYear = currentMonth.getFullYear();
116
+ const startYear = Math.floor(currentYear / 10) * 10 - 10;
117
+ return Array.from({ length: 21 }, (_, i) => startYear + i);
118
+ }, [currentMonth]);
119
+
120
+ const goToPrevYearRange = () => {
121
+ setCurrentMonth(new Date(currentMonth.getFullYear() - 10, currentMonth.getMonth(), 1));
122
+ };
123
+
124
+ const goToNextYearRange = () => {
125
+ setCurrentMonth(new Date(currentMonth.getFullYear() + 10, currentMonth.getMonth(), 1));
126
+ };
127
+
128
+ // Render month selector
129
+ if (viewMode === 'months') {
130
+ return (
131
+ <View style={[datePickerStyles.calendar, style]}>
132
+ <View style={datePickerStyles.calendarHeader}>
133
+ <Button type="text" size="sm" onPress={() => setViewMode('calendar')} disabled={disabled}>
134
+ <Icon name="chevron-left" size={16} />
135
+ </Button>
136
+ <Button type="text" size="sm" onPress={() => setViewMode('years')} disabled={disabled}>
137
+ <Text typography="body2" weight="semibold">{year}</Text>
138
+ </Button>
139
+ <View style={{ width: 28 }} />
140
+ </View>
141
+ <View style={datePickerStyles.monthGrid}>
142
+ {MONTHS.map((month, index) => {
143
+ const isCurrentMonth = index === currentMonth.getMonth();
144
+ return (
145
+ <Button
146
+ key={month}
147
+ type={isCurrentMonth ? 'contained' : 'text'}
148
+ size="sm"
149
+ onPress={() => handleMonthSelect(index)}
150
+ disabled={disabled}
151
+ style={{ minWidth: 48, margin: 2 }}
152
+ >
153
+ <Text typography="caption" color={isCurrentMonth ? 'inverse' : 'primary'}>
154
+ {month}
155
+ </Text>
156
+ </Button>
157
+ );
158
+ })}
159
+ </View>
160
+ </View>
161
+ );
162
+ }
163
+
164
+ // Render year selector
165
+ if (viewMode === 'years') {
166
+ return (
167
+ <View style={[datePickerStyles.calendar, style]}>
168
+ <View style={datePickerStyles.calendarHeader}>
169
+ <Button type="text" size="sm" onPress={goToPrevYearRange} disabled={disabled}>
170
+ <Icon name="chevron-left" size={16} />
171
+ </Button>
172
+ <Text typography="body2" weight="semibold">
173
+ {yearRange[0]} - {yearRange[yearRange.length - 1]}
174
+ </Text>
175
+ <Button type="text" size="sm" onPress={goToNextYearRange} disabled={disabled}>
176
+ <Icon name="chevron-right" size={16} />
177
+ </Button>
178
+ </View>
179
+ <View style={datePickerStyles.yearGrid}>
180
+ {yearRange.map((yr) => {
181
+ const isCurrentYear = yr === currentMonth.getFullYear();
182
+ return (
183
+ <Button
184
+ key={yr}
185
+ type={isCurrentYear ? 'contained' : 'text'}
186
+ size="sm"
187
+ onPress={() => handleYearSelect(yr)}
188
+ disabled={disabled}
189
+ style={{ minWidth: 48, margin: 2 }}
190
+ >
191
+ <Text typography="caption" color={isCurrentYear ? 'inverse' : 'primary'}>
192
+ {yr}
193
+ </Text>
194
+ </Button>
195
+ );
196
+ })}
197
+ </View>
198
+ </View>
199
+ );
200
+ }
201
+
202
+ // Render calendar (default)
203
+ return (
204
+ <View style={[datePickerStyles.calendar, style]}>
205
+ {/* Header */}
206
+ <View style={datePickerStyles.calendarHeader}>
207
+ <Button type="text" size="sm" onPress={goToPrevMonth} disabled={disabled}>
208
+ <Icon name="chevron-left" size={16} />
209
+ </Button>
210
+ <Button type="text" size="sm" onPress={() => setViewMode('months')} disabled={disabled}>
211
+ <Text typography="body2" weight="semibold">
212
+ {monthShort} {year}
213
+ </Text>
214
+ </Button>
215
+ <Button type="text" size="sm" onPress={goToNextMonth} disabled={disabled}>
216
+ <Icon name="chevron-right" size={16} />
217
+ </Button>
218
+ </View>
219
+
220
+ {/* Weekday headers */}
221
+ <View style={datePickerStyles.weekdayRow}>
222
+ {WEEKDAYS.map((day, i) => (
223
+ <View key={i} style={datePickerStyles.weekdayCell}>
224
+ <Text typography="caption" color="secondary">
225
+ {day}
226
+ </Text>
227
+ </View>
228
+ ))}
229
+ </View>
230
+
231
+ {/* Calendar grid */}
232
+ <View style={datePickerStyles.calendarGrid}>
233
+ {days.map(({ date, isCurrentMonth }, index) => {
234
+ const selected = isSelected(date);
235
+ const today = isToday(date);
236
+ const dayDisabled = isDisabled(date);
237
+
238
+ return (
239
+ <View
240
+ key={index}
241
+ style={[
242
+ datePickerStyles.dayCell,
243
+ selected && {
244
+ backgroundColor: '#3b82f6',
245
+ borderRadius: 4,
246
+ },
247
+ !isCurrentMonth && { opacity: 0.3 },
248
+ today && !selected && {
249
+ borderWidth: 1,
250
+ borderColor: '#3b82f6',
251
+ borderRadius: 4,
252
+ },
253
+ dayDisabled && { opacity: 0.3 },
254
+ ]}
255
+ >
256
+ <Button
257
+ type="text"
258
+ size="sm"
259
+ onPress={() => handleDayPress(date)}
260
+ disabled={dayDisabled}
261
+ style={{ minWidth: 24, minHeight: 24, padding: 0 }}
262
+ >
263
+ <Text
264
+ typography="caption"
265
+ color={selected ? 'inverse' : 'primary'}
266
+ >
267
+ {date.getDate()}
268
+ </Text>
269
+ </Button>
270
+ </View>
271
+ );
272
+ })}
273
+ </View>
274
+ </View>
275
+ );
276
+ };
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import { View, Text } from '@idealyst/components';
3
+ import { DateInput } from './DateInput';
4
+ import { TimeInput } from './TimeInput';
5
+ import { datePickerStyles } from './styles';
6
+ import type { DateTimePickerProps } from './types';
7
+
8
+ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
9
+ value,
10
+ onChange,
11
+ label,
12
+ minDate,
13
+ maxDate,
14
+ timeMode = '12h',
15
+ minuteStep = 1,
16
+ disabled = false,
17
+ error,
18
+ style,
19
+ }) => {
20
+ const handleDateChange = (date: Date | null) => {
21
+ if (!date) {
22
+ onChange(null);
23
+ return;
24
+ }
25
+ // Preserve time from current value, or use noon as default
26
+ const hours = value?.getHours() ?? 12;
27
+ const minutes = value?.getMinutes() ?? 0;
28
+ const updated = new Date(date);
29
+ updated.setHours(hours, minutes, 0, 0);
30
+ onChange(updated);
31
+ };
32
+
33
+ const handleTimeChange = (time: Date | null) => {
34
+ if (!time) {
35
+ // Only clear time component, keep date if it exists
36
+ if (value) {
37
+ const updated = new Date(value);
38
+ updated.setHours(12, 0, 0, 0);
39
+ onChange(updated);
40
+ }
41
+ return;
42
+ }
43
+ // Preserve date from current value, or use today
44
+ const baseDate = value || new Date();
45
+ const updated = new Date(
46
+ baseDate.getFullYear(),
47
+ baseDate.getMonth(),
48
+ baseDate.getDate(),
49
+ time.getHours(),
50
+ time.getMinutes(),
51
+ 0,
52
+ 0
53
+ );
54
+ onChange(updated);
55
+ };
56
+
57
+ return (
58
+ <View style={style}>
59
+ {label && (
60
+ <Text typography="body2" weight="medium" style={{ marginBottom: 4 }}>
61
+ {label}
62
+ </Text>
63
+ )}
64
+ <View style={datePickerStyles.inputRow}>
65
+ <View style={{ flex: 1 }}>
66
+ <DateInput
67
+ value={value ?? undefined}
68
+ onChange={handleDateChange}
69
+ placeholder="MM/DD/YYYY"
70
+ minDate={minDate}
71
+ maxDate={maxDate}
72
+ disabled={disabled}
73
+ error={error}
74
+ />
75
+ </View>
76
+ <View style={{ flex: 1 }}>
77
+ <TimeInput
78
+ value={value ?? undefined}
79
+ onChange={handleTimeChange}
80
+ placeholder={timeMode === '24h' ? '14:30' : '2:30 PM'}
81
+ mode={timeMode}
82
+ minuteStep={minuteStep}
83
+ disabled={disabled}
84
+ />
85
+ </View>
86
+ </View>
87
+ </View>
88
+ );
89
+ };