@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,170 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const rangeCalendarStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
width: 256,
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
header: {
|
|
9
|
+
flexDirection: 'row',
|
|
10
|
+
justifyContent: 'space-between',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
marginBottom: theme.spacing?.md || 16,
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
headerButton: {
|
|
16
|
+
minWidth: 32,
|
|
17
|
+
minHeight: 32,
|
|
18
|
+
paddingHorizontal: theme.spacing?.xs || 8,
|
|
19
|
+
paddingVertical: theme.spacing?.xs || 4,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
headerTitle: {
|
|
23
|
+
flexDirection: 'row',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: theme.spacing?.xs || 8,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
monthYearButton: {
|
|
29
|
+
paddingHorizontal: theme.spacing?.sm || 8,
|
|
30
|
+
paddingVertical: theme.spacing?.xs || 4,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
weekdayHeader: {
|
|
34
|
+
display: 'grid',
|
|
35
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
36
|
+
gap: 2,
|
|
37
|
+
marginBottom: theme.spacing?.xs || 8,
|
|
38
|
+
|
|
39
|
+
// Native fallback
|
|
40
|
+
_native: {
|
|
41
|
+
flexDirection: 'row',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
weekdayCell: {
|
|
46
|
+
display: 'flex',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
paddingVertical: theme.spacing?.xs || 4,
|
|
50
|
+
|
|
51
|
+
// Native fallback
|
|
52
|
+
_native: {
|
|
53
|
+
flex: 1,
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
paddingVertical: theme.spacing?.xs || 4,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
weekdayText: {
|
|
60
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
61
|
+
fontWeight: '500',
|
|
62
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
calendarGrid: {
|
|
66
|
+
display: 'grid',
|
|
67
|
+
gridTemplateColumns: 'repeat(7, 1fr)',
|
|
68
|
+
gap: 2,
|
|
69
|
+
marginBottom: theme.spacing?.xs || 8,
|
|
70
|
+
|
|
71
|
+
// Native fallback
|
|
72
|
+
_native: {
|
|
73
|
+
flexDirection: 'row',
|
|
74
|
+
flexWrap: 'wrap',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
dayCell: {
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
aspectRatio: '1',
|
|
83
|
+
minHeight: 32,
|
|
84
|
+
|
|
85
|
+
// Native specific sizing
|
|
86
|
+
_native: {
|
|
87
|
+
width: '14.28%', // 100% / 7 days
|
|
88
|
+
aspectRatio: 1,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
dayButton: {
|
|
95
|
+
width: '100%',
|
|
96
|
+
height: '100%',
|
|
97
|
+
maxWidth: 36,
|
|
98
|
+
maxHeight: 36,
|
|
99
|
+
minWidth: 28,
|
|
100
|
+
minHeight: 28,
|
|
101
|
+
padding: 0,
|
|
102
|
+
borderRadius: theme.borderRadius?.sm || 6,
|
|
103
|
+
fontSize: theme.typography?.sizes?.small || 13,
|
|
104
|
+
|
|
105
|
+
variants: {
|
|
106
|
+
inRange: {
|
|
107
|
+
true: {
|
|
108
|
+
backgroundColor: theme.colors?.accent?.primary + '20' || '#3b82f620',
|
|
109
|
+
borderRadius: 0,
|
|
110
|
+
},
|
|
111
|
+
false: {},
|
|
112
|
+
},
|
|
113
|
+
isStart: {
|
|
114
|
+
true: {
|
|
115
|
+
backgroundColor: theme.colors?.accent?.primary || '#3b82f6',
|
|
116
|
+
borderTopLeftRadius: theme.borderRadius?.sm || 6,
|
|
117
|
+
borderBottomLeftRadius: theme.borderRadius?.sm || 6,
|
|
118
|
+
},
|
|
119
|
+
false: {},
|
|
120
|
+
},
|
|
121
|
+
isEnd: {
|
|
122
|
+
true: {
|
|
123
|
+
backgroundColor: theme.colors?.accent?.primary || '#3b82f6',
|
|
124
|
+
borderTopRightRadius: theme.borderRadius?.sm || 6,
|
|
125
|
+
borderBottomRightRadius: theme.borderRadius?.sm || 6,
|
|
126
|
+
},
|
|
127
|
+
false: {},
|
|
128
|
+
},
|
|
129
|
+
isSelected: {
|
|
130
|
+
true: {
|
|
131
|
+
backgroundColor: theme.colors?.accent?.primary || '#3b82f6',
|
|
132
|
+
fontWeight: '600',
|
|
133
|
+
},
|
|
134
|
+
false: {
|
|
135
|
+
fontWeight: '400',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Native specific styling
|
|
141
|
+
_native: {
|
|
142
|
+
width: 28,
|
|
143
|
+
height: 28,
|
|
144
|
+
minWidth: 28,
|
|
145
|
+
minHeight: 28,
|
|
146
|
+
borderRadius: theme.borderRadius?.sm || 6,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
rangePresets: {
|
|
151
|
+
paddingTop: theme.spacing?.sm || 12,
|
|
152
|
+
borderTopWidth: 1,
|
|
153
|
+
borderTopColor: theme.colors?.border?.secondary || '#f3f4f6',
|
|
154
|
+
gap: theme.spacing?.xs || 8,
|
|
155
|
+
|
|
156
|
+
// Web specific border
|
|
157
|
+
_web: {
|
|
158
|
+
borderTop: `1px solid ${theme.colors?.border?.secondary || '#f3f4f6'}`,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
presetButton: {
|
|
163
|
+
width: '100%',
|
|
164
|
+
justifyContent: 'flex-start',
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
clearButton: {
|
|
168
|
+
width: '100%',
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { View, Text, Button } from '@idealyst/components';
|
|
3
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
4
|
+
import { RangeCalendarProps, DateRange } from './types';
|
|
5
|
+
import { rangeCalendarStyles } from './RangeCalendar.styles';
|
|
6
|
+
|
|
7
|
+
export const RangeCalendar: React.FC<RangeCalendarProps> = ({
|
|
8
|
+
value = {},
|
|
9
|
+
onChange,
|
|
10
|
+
minDate,
|
|
11
|
+
maxDate,
|
|
12
|
+
disabled = false,
|
|
13
|
+
currentMonth: controlledCurrentMonth,
|
|
14
|
+
onMonthChange,
|
|
15
|
+
allowSameDay = true,
|
|
16
|
+
maxDays,
|
|
17
|
+
style,
|
|
18
|
+
testID,
|
|
19
|
+
}) => {
|
|
20
|
+
const [internalCurrentMonth, setInternalCurrentMonth] = useState(
|
|
21
|
+
controlledCurrentMonth || value?.startDate || new Date()
|
|
22
|
+
);
|
|
23
|
+
const [selectingEnd, setSelectingEnd] = useState(false);
|
|
24
|
+
|
|
25
|
+
const currentMonth = controlledCurrentMonth || internalCurrentMonth;
|
|
26
|
+
|
|
27
|
+
const handleMonthChange = (newMonth: Date) => {
|
|
28
|
+
if (onMonthChange) {
|
|
29
|
+
onMonthChange(newMonth);
|
|
30
|
+
} else {
|
|
31
|
+
setInternalCurrentMonth(newMonth);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const { monthStart, monthEnd, daysInMonth, startingDayOfWeek } = useMemo(() => {
|
|
36
|
+
const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
|
|
37
|
+
const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0);
|
|
38
|
+
const daysInMonth = monthEnd.getDate();
|
|
39
|
+
const startingDayOfWeek = monthStart.getDay();
|
|
40
|
+
|
|
41
|
+
return { monthStart, monthEnd, daysInMonth, startingDayOfWeek };
|
|
42
|
+
}, [currentMonth]);
|
|
43
|
+
|
|
44
|
+
const isDateDisabled = (date: Date): boolean => {
|
|
45
|
+
if (disabled) return true;
|
|
46
|
+
if (minDate && date < minDate) return true;
|
|
47
|
+
if (maxDate && date > maxDate) return true;
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const isDateInRange = (date: Date): boolean => {
|
|
52
|
+
const { startDate, endDate } = value || {};
|
|
53
|
+
if (!startDate || !endDate) return false;
|
|
54
|
+
return date > startDate && date < endDate;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isDateRangeStart = (date: Date): boolean => {
|
|
58
|
+
const { startDate } = value || {};
|
|
59
|
+
if (!startDate) return false;
|
|
60
|
+
return (
|
|
61
|
+
date.getDate() === startDate.getDate() &&
|
|
62
|
+
date.getMonth() === startDate.getMonth() &&
|
|
63
|
+
date.getFullYear() === startDate.getFullYear()
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const isDateRangeEnd = (date: Date): boolean => {
|
|
68
|
+
const { endDate } = value || {};
|
|
69
|
+
if (!endDate) return false;
|
|
70
|
+
return (
|
|
71
|
+
date.getDate() === endDate.getDate() &&
|
|
72
|
+
date.getMonth() === endDate.getMonth() &&
|
|
73
|
+
date.getFullYear() === endDate.getFullYear()
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isDateSelected = (date: Date): boolean => {
|
|
78
|
+
return isDateRangeStart(date) || isDateRangeEnd(date);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleDateClick = (date: Date) => {
|
|
82
|
+
if (isDateDisabled(date)) return;
|
|
83
|
+
|
|
84
|
+
const { startDate, endDate } = value || {};
|
|
85
|
+
|
|
86
|
+
// If no range is selected or we're starting fresh
|
|
87
|
+
if (!startDate || (startDate && endDate)) {
|
|
88
|
+
onChange({ startDate: date, endDate: undefined });
|
|
89
|
+
setSelectingEnd(true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If we have a start date but no end date
|
|
94
|
+
if (startDate && !endDate) {
|
|
95
|
+
let newStartDate = startDate;
|
|
96
|
+
let newEndDate = date;
|
|
97
|
+
|
|
98
|
+
// Swap if end date is before start date
|
|
99
|
+
if (date < startDate) {
|
|
100
|
+
newStartDate = date;
|
|
101
|
+
newEndDate = startDate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if same day selection is allowed
|
|
105
|
+
if (!allowSameDay && newStartDate.getTime() === newEndDate.getTime()) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check max days constraint
|
|
110
|
+
if (maxDays) {
|
|
111
|
+
const daysDiff = Math.ceil((newEndDate.getTime() - newStartDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
112
|
+
if (daysDiff > maxDays) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onChange({ startDate: newStartDate, endDate: newEndDate });
|
|
118
|
+
setSelectingEnd(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const goToPreviousMonth = () => {
|
|
123
|
+
const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
|
|
124
|
+
handleMonthChange(newMonth);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const goToNextMonth = () => {
|
|
128
|
+
const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
|
|
129
|
+
handleMonthChange(newMonth);
|
|
130
|
+
};
|
|
131
|
+
|
|
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);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const clearRange = () => {
|
|
142
|
+
onChange({});
|
|
143
|
+
setSelectingEnd(false);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
147
|
+
|
|
148
|
+
// Create calendar grid
|
|
149
|
+
const calendarDays = [];
|
|
150
|
+
|
|
151
|
+
// Add empty cells for days before month starts
|
|
152
|
+
for (let i = 0; i < startingDayOfWeek; i++) {
|
|
153
|
+
calendarDays.push(null);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add days of the month
|
|
157
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
158
|
+
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
|
|
159
|
+
calendarDays.push(date);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
rangeCalendarStyles.useVariants({});
|
|
163
|
+
|
|
164
|
+
const containerProps = getWebProps([rangeCalendarStyles.container, style]);
|
|
165
|
+
const headerProps = getWebProps([rangeCalendarStyles.header]);
|
|
166
|
+
const headerTitleProps = getWebProps([rangeCalendarStyles.headerTitle]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div {...containerProps} data-testid={testID}>
|
|
170
|
+
{/* Header */}
|
|
171
|
+
<div {...headerProps}>
|
|
172
|
+
<Button
|
|
173
|
+
variant="text"
|
|
174
|
+
size="small"
|
|
175
|
+
onPress={goToPreviousMonth}
|
|
176
|
+
disabled={disabled}
|
|
177
|
+
style={getWebProps([rangeCalendarStyles.headerButton]).style}
|
|
178
|
+
>
|
|
179
|
+
←
|
|
180
|
+
</Button>
|
|
181
|
+
|
|
182
|
+
<div {...headerTitleProps}>
|
|
183
|
+
<Text weight="semibold">{monthName}</Text>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<Button
|
|
187
|
+
variant="text"
|
|
188
|
+
size="small"
|
|
189
|
+
onPress={goToNextMonth}
|
|
190
|
+
disabled={disabled}
|
|
191
|
+
style={getWebProps([rangeCalendarStyles.headerButton]).style}
|
|
192
|
+
>
|
|
193
|
+
→
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Weekday headers */}
|
|
198
|
+
<div {...getWebProps([rangeCalendarStyles.weekdayHeader])}>
|
|
199
|
+
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((day) => (
|
|
200
|
+
<div key={day} {...getWebProps([rangeCalendarStyles.weekdayCell])}>
|
|
201
|
+
<div {...getWebProps([rangeCalendarStyles.weekdayText])}>
|
|
202
|
+
{day}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Calendar grid */}
|
|
209
|
+
<div {...getWebProps([rangeCalendarStyles.calendarGrid])}>
|
|
210
|
+
{calendarDays.map((date, index) => (
|
|
211
|
+
<div key={index} {...getWebProps([rangeCalendarStyles.dayCell])}>
|
|
212
|
+
{date && (
|
|
213
|
+
<Button
|
|
214
|
+
variant="text"
|
|
215
|
+
disabled={isDateDisabled(date)}
|
|
216
|
+
onPress={() => handleDateClick(date)}
|
|
217
|
+
size="small"
|
|
218
|
+
style={{
|
|
219
|
+
...getWebProps([rangeCalendarStyles.dayButton]).style,
|
|
220
|
+
backgroundColor: isDateSelected(date)
|
|
221
|
+
? '#3b82f6'
|
|
222
|
+
: isDateInRange(date)
|
|
223
|
+
? '#3b82f620'
|
|
224
|
+
: 'transparent',
|
|
225
|
+
color: isDateSelected(date) ? 'white' : 'black',
|
|
226
|
+
fontWeight: isDateSelected(date) ? '600' : '400',
|
|
227
|
+
borderRadius: isDateRangeStart(date)
|
|
228
|
+
? '6px 0 0 6px'
|
|
229
|
+
: isDateRangeEnd(date)
|
|
230
|
+
? '0 6px 6px 0'
|
|
231
|
+
: isDateInRange(date)
|
|
232
|
+
? '0'
|
|
233
|
+
: '6px',
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{date.getDate()}
|
|
237
|
+
</Button>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{/* Range presets */}
|
|
244
|
+
<div {...getWebProps([rangeCalendarStyles.rangePresets])}>
|
|
245
|
+
<Button
|
|
246
|
+
variant="text"
|
|
247
|
+
size="small"
|
|
248
|
+
onPress={() => handlePresetRange(7)}
|
|
249
|
+
disabled={disabled}
|
|
250
|
+
style={getWebProps([rangeCalendarStyles.presetButton]).style}
|
|
251
|
+
>
|
|
252
|
+
Next 7 days
|
|
253
|
+
</Button>
|
|
254
|
+
<Button
|
|
255
|
+
variant="text"
|
|
256
|
+
size="small"
|
|
257
|
+
onPress={() => handlePresetRange(30)}
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
style={getWebProps([rangeCalendarStyles.presetButton]).style}
|
|
260
|
+
>
|
|
261
|
+
Next 30 days
|
|
262
|
+
</Button>
|
|
263
|
+
<Button
|
|
264
|
+
variant="outlined"
|
|
265
|
+
size="small"
|
|
266
|
+
onPress={clearRange}
|
|
267
|
+
disabled={disabled}
|
|
268
|
+
style={getWebProps([rangeCalendarStyles.clearButton]).style}
|
|
269
|
+
>
|
|
270
|
+
Clear Range
|
|
271
|
+
</Button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface DateRange {
|
|
5
|
+
/** Start date of the range */
|
|
6
|
+
startDate?: Date;
|
|
7
|
+
|
|
8
|
+
/** End date of the range */
|
|
9
|
+
endDate?: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DateRangePickerProps {
|
|
13
|
+
/** Current selected date range */
|
|
14
|
+
value?: DateRange;
|
|
15
|
+
|
|
16
|
+
/** Called when date range changes */
|
|
17
|
+
onChange: (range: DateRange | null) => void;
|
|
18
|
+
|
|
19
|
+
/** Minimum selectable date */
|
|
20
|
+
minDate?: Date;
|
|
21
|
+
|
|
22
|
+
/** Maximum selectable date */
|
|
23
|
+
maxDate?: Date;
|
|
24
|
+
|
|
25
|
+
/** Disabled state */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Placeholder text when no range is selected */
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
|
|
31
|
+
/** Label for the picker */
|
|
32
|
+
label?: string;
|
|
33
|
+
|
|
34
|
+
/** Error message to display */
|
|
35
|
+
error?: string;
|
|
36
|
+
|
|
37
|
+
/** Helper text */
|
|
38
|
+
helperText?: string;
|
|
39
|
+
|
|
40
|
+
/** Date format for display (default: 'MM/dd/yyyy') */
|
|
41
|
+
format?: string;
|
|
42
|
+
|
|
43
|
+
/** Locale for date formatting */
|
|
44
|
+
locale?: string;
|
|
45
|
+
|
|
46
|
+
/** Size variant */
|
|
47
|
+
size?: 'small' | 'medium' | 'large';
|
|
48
|
+
|
|
49
|
+
/** Visual variant */
|
|
50
|
+
variant?: 'outlined' | 'filled';
|
|
51
|
+
|
|
52
|
+
/** Allow same day selection for start and end */
|
|
53
|
+
allowSameDay?: boolean;
|
|
54
|
+
|
|
55
|
+
/** Maximum number of days in range */
|
|
56
|
+
maxDays?: number;
|
|
57
|
+
|
|
58
|
+
/** Custom styles */
|
|
59
|
+
style?: ViewStyle;
|
|
60
|
+
|
|
61
|
+
/** Test ID for testing */
|
|
62
|
+
testID?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RangeCalendarProps {
|
|
66
|
+
/** Current selected date range */
|
|
67
|
+
value?: DateRange;
|
|
68
|
+
|
|
69
|
+
/** Called when range is selected */
|
|
70
|
+
onChange: (range: DateRange) => void;
|
|
71
|
+
|
|
72
|
+
/** Minimum selectable date */
|
|
73
|
+
minDate?: Date;
|
|
74
|
+
|
|
75
|
+
/** Maximum selectable date */
|
|
76
|
+
maxDate?: Date;
|
|
77
|
+
|
|
78
|
+
/** Disabled state */
|
|
79
|
+
disabled?: boolean;
|
|
80
|
+
|
|
81
|
+
/** Current month being viewed */
|
|
82
|
+
currentMonth?: Date;
|
|
83
|
+
|
|
84
|
+
/** Called when month changes */
|
|
85
|
+
onMonthChange?: (month: Date) => void;
|
|
86
|
+
|
|
87
|
+
/** Allow same day selection */
|
|
88
|
+
allowSameDay?: boolean;
|
|
89
|
+
|
|
90
|
+
/** Maximum number of days in range */
|
|
91
|
+
maxDays?: number;
|
|
92
|
+
|
|
93
|
+
/** Custom styles */
|
|
94
|
+
style?: ViewStyle;
|
|
95
|
+
|
|
96
|
+
/** Test ID for testing */
|
|
97
|
+
testID?: string;
|
|
98
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from '@idealyst/components';
|
|
3
|
+
import { DateTimePickerProps } from './types';
|
|
4
|
+
import { Calendar } from '../DatePicker/Calendar.native';
|
|
5
|
+
import { TimePicker } from './TimePicker.native';
|
|
6
|
+
import { dateTimePickerStyles } from './DateTimePicker.styles';
|
|
7
|
+
|
|
8
|
+
const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
minDate,
|
|
12
|
+
maxDate,
|
|
13
|
+
disabled = false,
|
|
14
|
+
timeMode = '12h',
|
|
15
|
+
showSeconds = false,
|
|
16
|
+
timeStep = 1,
|
|
17
|
+
style,
|
|
18
|
+
testID,
|
|
19
|
+
}) => {
|
|
20
|
+
|
|
21
|
+
const handleDateChange = (newDate: Date) => {
|
|
22
|
+
if (value) {
|
|
23
|
+
// Preserve the time from current value
|
|
24
|
+
const updatedDate = new Date(newDate);
|
|
25
|
+
updatedDate.setHours(value.getHours(), value.getMinutes(), value.getSeconds());
|
|
26
|
+
onChange(updatedDate);
|
|
27
|
+
} else {
|
|
28
|
+
onChange(newDate);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleTimeChange = (newTime: Date) => {
|
|
33
|
+
if (value) {
|
|
34
|
+
// Update time while preserving the date
|
|
35
|
+
const updatedDate = new Date(value);
|
|
36
|
+
updatedDate.setHours(newTime.getHours(), newTime.getMinutes(), newTime.getSeconds());
|
|
37
|
+
onChange(updatedDate);
|
|
38
|
+
} else {
|
|
39
|
+
// If no date is selected, use today's date with the new time
|
|
40
|
+
const today = new Date();
|
|
41
|
+
today.setHours(newTime.getHours(), newTime.getMinutes(), newTime.getSeconds());
|
|
42
|
+
onChange(today);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
dateTimePickerStyles.useVariants({});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={[dateTimePickerStyles.container, style]} testID={testID}>
|
|
50
|
+
{/* Date Section */}
|
|
51
|
+
<View style={dateTimePickerStyles.section}>
|
|
52
|
+
<Text style={dateTimePickerStyles.sectionLabel}>Date</Text>
|
|
53
|
+
<View style={dateTimePickerStyles.sectionContent}>
|
|
54
|
+
<Calendar
|
|
55
|
+
value={value}
|
|
56
|
+
onChange={handleDateChange}
|
|
57
|
+
minDate={minDate}
|
|
58
|
+
maxDate={maxDate}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
/>
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
|
|
64
|
+
{/* Time Section */}
|
|
65
|
+
<View style={dateTimePickerStyles.section}>
|
|
66
|
+
<Text style={dateTimePickerStyles.sectionLabel}>Time</Text>
|
|
67
|
+
<View style={dateTimePickerStyles.sectionContent}>
|
|
68
|
+
<TimePicker
|
|
69
|
+
value={value || new Date()}
|
|
70
|
+
onChange={handleTimeChange}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
mode={timeMode}
|
|
73
|
+
showSeconds={showSeconds}
|
|
74
|
+
step={timeStep}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
</View>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default DateTimePicker;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const dateTimePickerStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
gap: theme.spacing?.md || 16,
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
label: {
|
|
9
|
+
fontSize: theme.typography?.sizes?.small || 14,
|
|
10
|
+
fontWeight: '600',
|
|
11
|
+
color: theme.colors?.text?.primary || '#111827',
|
|
12
|
+
marginBottom: theme.spacing?.xs || 4,
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
picker: {
|
|
16
|
+
gap: theme.spacing?.md || 16,
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
selectedDateTimeHeader: {
|
|
20
|
+
padding: theme.spacing?.sm || 12,
|
|
21
|
+
backgroundColor: theme.colors?.surface?.secondary || '#f9fafb',
|
|
22
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
23
|
+
borderWidth: 1,
|
|
24
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
25
|
+
gap: theme.spacing?.xs || 4,
|
|
26
|
+
|
|
27
|
+
_web: {
|
|
28
|
+
border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
selectedDateTimeLabel: {
|
|
33
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
34
|
+
fontWeight: '500',
|
|
35
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
selectedDateTimeValue: {
|
|
39
|
+
fontSize: theme.typography?.sizes?.medium || 16,
|
|
40
|
+
fontWeight: '600',
|
|
41
|
+
color: theme.colors?.text?.primary || '#111827',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
section: {
|
|
45
|
+
gap: theme.spacing?.xs || 8,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
sectionLabel: {
|
|
49
|
+
fontSize: theme.typography?.sizes?.small || 14,
|
|
50
|
+
fontWeight: '600',
|
|
51
|
+
color: theme.colors?.text?.primary || '#111827',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
sectionContent: {
|
|
55
|
+
padding: theme.spacing?.sm || 12,
|
|
56
|
+
borderWidth: 1,
|
|
57
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
58
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
59
|
+
backgroundColor: theme.colors?.surface?.primary || '#ffffff',
|
|
60
|
+
|
|
61
|
+
_web: {
|
|
62
|
+
border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
errorText: {
|
|
67
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
68
|
+
color: theme.colors?.semantic?.error || '#dc2626',
|
|
69
|
+
marginTop: theme.spacing?.xs || 4,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
helperText: {
|
|
73
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
74
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
75
|
+
marginTop: theme.spacing?.xs || 4,
|
|
76
|
+
},
|
|
77
|
+
}));
|