@idealyst/datepicker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +88 -0
  2. package/package.json +77 -0
  3. package/src/DatePicker/Calendar.native.tsx +159 -0
  4. package/src/DatePicker/Calendar.styles.tsx +224 -0
  5. package/src/DatePicker/Calendar.tsx +154 -0
  6. package/src/DatePicker/DatePicker.native.tsx +33 -0
  7. package/src/DatePicker/DatePicker.styles.tsx +69 -0
  8. package/src/DatePicker/DatePicker.web.tsx +31 -0
  9. package/src/DatePicker/index.native.ts +3 -0
  10. package/src/DatePicker/index.ts +3 -0
  11. package/src/DatePicker/types.ts +78 -0
  12. package/src/DateRangePicker/DateRangePicker.native.tsx +39 -0
  13. package/src/DateRangePicker/DateRangePicker.styles.tsx +83 -0
  14. package/src/DateRangePicker/DateRangePicker.web.tsx +40 -0
  15. package/src/DateRangePicker/RangeCalendar.native.tsx +267 -0
  16. package/src/DateRangePicker/RangeCalendar.styles.tsx +170 -0
  17. package/src/DateRangePicker/RangeCalendar.web.tsx +275 -0
  18. package/src/DateRangePicker/index.native.ts +3 -0
  19. package/src/DateRangePicker/index.ts +3 -0
  20. package/src/DateRangePicker/types.ts +98 -0
  21. package/src/DateTimePicker/DateTimePicker.native.tsx +82 -0
  22. package/src/DateTimePicker/DateTimePicker.styles.tsx +77 -0
  23. package/src/DateTimePicker/DateTimePicker.tsx +79 -0
  24. package/src/DateTimePicker/TimePicker.native.tsx +204 -0
  25. package/src/DateTimePicker/TimePicker.styles.tsx +116 -0
  26. package/src/DateTimePicker/TimePicker.tsx +406 -0
  27. package/src/DateTimePicker/index.native.ts +3 -0
  28. package/src/DateTimePicker/index.ts +3 -0
  29. package/src/DateTimePicker/types.ts +84 -0
  30. package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +213 -0
  31. package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +95 -0
  32. package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +141 -0
  33. package/src/DateTimeRangePicker/index.native.ts +2 -0
  34. package/src/DateTimeRangePicker/index.ts +2 -0
  35. package/src/DateTimeRangePicker/types.ts +72 -0
  36. package/src/examples/DatePickerExamples.tsx +274 -0
  37. package/src/examples/index.ts +1 -0
  38. package/src/index.native.ts +16 -0
  39. package/src/index.ts +16 -0
  40. package/src/primitives/CalendarGrid/CalendarGrid.styles.tsx +62 -0
  41. package/src/primitives/CalendarGrid/CalendarGrid.tsx +138 -0
  42. package/src/primitives/CalendarGrid/index.ts +1 -0
  43. package/src/primitives/CalendarHeader/CalendarHeader.styles.tsx +25 -0
  44. package/src/primitives/CalendarHeader/CalendarHeader.tsx +69 -0
  45. package/src/primitives/CalendarHeader/index.ts +1 -0
  46. package/src/primitives/CalendarOverlay/CalendarOverlay.styles.tsx +81 -0
  47. package/src/primitives/CalendarOverlay/CalendarOverlay.tsx +130 -0
  48. package/src/primitives/CalendarOverlay/index.ts +1 -0
  49. package/src/primitives/Wrapper/Wrapper.web.tsx +33 -0
  50. package/src/primitives/Wrapper/index.ts +1 -0
@@ -0,0 +1,406 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Button, View, Input } from '@idealyst/components';
3
+ import { TimePickerProps } from './types';
4
+ import { timePickerStyles } from './TimePicker.styles';
5
+
6
+ export const TimePicker: React.FC<TimePickerProps> = ({
7
+ value = new Date(),
8
+ onChange,
9
+ disabled = false,
10
+ mode = '12h',
11
+ showSeconds = false,
12
+ step = 1,
13
+ style,
14
+ testID,
15
+ }) => {
16
+ const [activeSelection, setActiveSelection] = useState<'hour' | 'minute'>('hour');
17
+ const [hourInputValue, setHourInputValue] = useState(String(value.getHours() > 12 && mode === '12h' ? value.getHours() - 12 : value.getHours()));
18
+ const [minuteInputValue, setMinuteInputValue] = useState(String(value.getMinutes()).padStart(2, '0'));
19
+ const [hourInputFocused, setHourInputFocused] = useState(false);
20
+ const [minuteInputFocused, setMinuteInputFocused] = useState(false);
21
+ const hourInputRef = useRef<HTMLInputElement | null>(null);
22
+ const minuteInputRef = useRef<HTMLInputElement | null>(null);
23
+ const hours = value.getHours();
24
+ const minutes = value.getMinutes();
25
+ const seconds = value.getSeconds();
26
+
27
+ const displayHours = mode === '12h' ? (hours === 0 ? 12 : hours > 12 ? hours - 12 : hours) : hours;
28
+ const ampm = mode === '12h' ? (hours >= 12 ? 'PM' : 'AM') : null;
29
+
30
+ // Sync input values when time changes from external sources (like clock clicks)
31
+ // Only update if the input is not currently focused
32
+ useEffect(() => {
33
+ if (!hourInputFocused) {
34
+ setHourInputValue(String(displayHours));
35
+ }
36
+ if (!minuteInputFocused) {
37
+ setMinuteInputValue(String(minutes).padStart(2, '0'));
38
+ }
39
+ }, [displayHours, minutes, hourInputFocused, minuteInputFocused]);
40
+
41
+ const updateTime = (newHours: number, newMinutes: number, newSeconds?: number) => {
42
+ const newDate = new Date(value);
43
+ newDate.setHours(newHours, newMinutes, newSeconds || 0);
44
+ onChange(newDate);
45
+ };
46
+
47
+ const handleHourChange = (delta: number) => {
48
+ let newHours = hours + delta;
49
+ if (mode === '12h') {
50
+ if (newHours < 0) newHours = 23;
51
+ if (newHours > 23) newHours = 0;
52
+ } else {
53
+ if (newHours < 0) newHours = 23;
54
+ if (newHours > 23) newHours = 0;
55
+ }
56
+ updateTime(newHours, minutes, seconds);
57
+ };
58
+
59
+ const handleMinuteChange = (delta: number) => {
60
+ let newMinutes = minutes + (delta * step);
61
+ let newHours = hours;
62
+
63
+ if (newMinutes < 0) {
64
+ newMinutes = 60 + newMinutes;
65
+ newHours = hours - 1;
66
+ if (newHours < 0) newHours = 23;
67
+ } else if (newMinutes >= 60) {
68
+ newMinutes = newMinutes - 60;
69
+ newHours = hours + 1;
70
+ if (newHours > 23) newHours = 0;
71
+ }
72
+
73
+ updateTime(newHours, newMinutes, seconds);
74
+ };
75
+
76
+ const handleSecondChange = (delta: number) => {
77
+ let newSeconds = seconds + delta;
78
+ let newMinutes = minutes;
79
+ let newHours = hours;
80
+
81
+ if (newSeconds < 0) {
82
+ newSeconds = 59;
83
+ newMinutes = minutes - 1;
84
+ if (newMinutes < 0) {
85
+ newMinutes = 59;
86
+ newHours = hours - 1;
87
+ if (newHours < 0) newHours = 23;
88
+ }
89
+ } else if (newSeconds >= 60) {
90
+ newSeconds = 0;
91
+ newMinutes = minutes + 1;
92
+ if (newMinutes >= 60) {
93
+ newMinutes = 0;
94
+ newHours = hours + 1;
95
+ if (newHours > 23) newHours = 0;
96
+ }
97
+ }
98
+
99
+ updateTime(newHours, newMinutes, newSeconds);
100
+ };
101
+
102
+ const toggleAmPm = () => {
103
+ if (mode === '12h') {
104
+ const newHours = hours >= 12 ? hours - 12 : hours + 12;
105
+ updateTime(newHours, minutes, seconds);
106
+ }
107
+ };
108
+
109
+ const handleHourClick = (hour: number) => {
110
+ let hour24 = hour;
111
+ if (mode === '12h') {
112
+ const isPM = hours >= 12;
113
+ if (hour === 12) hour24 = isPM ? 12 : 0;
114
+ else hour24 = isPM ? hour + 12 : hour;
115
+ }
116
+ updateTime(hour24, minutes, seconds);
117
+ setActiveSelection('minute');
118
+ };
119
+
120
+ const handleMinuteClick = (minute: number) => {
121
+ updateTime(hours, minute, seconds);
122
+ };
123
+
124
+ const renderClockFace = () => {
125
+ // Clock configuration
126
+ const CLOCK_SIZE = 180;
127
+ const CENTER = CLOCK_SIZE / 2;
128
+ const CLOCK_RADIUS = CENTER - 5;
129
+ const NUMBER_RADIUS = CENTER - 24;
130
+ const HOUR_HAND_LENGTH = CENTER - 44;
131
+ const MINUTE_HAND_LENGTH = CENTER - 36;
132
+ const CIRCLE_RADIUS = 15;
133
+
134
+ if (activeSelection === 'hour') {
135
+ return (
136
+ <View style={timePickerStyles.clockContainer}>
137
+ <svg width={CLOCK_SIZE} height={CLOCK_SIZE} style={timePickerStyles.clockSvg}>
138
+ {/* Clock face */}
139
+ <circle cx={CENTER} cy={CENTER} r={CLOCK_RADIUS} fill="#f9fafb" stroke="#e5e7eb" strokeWidth="2"/>
140
+
141
+ {/* Hour numbers - clickable */}
142
+ {[...Array(12)].map((_, i) => {
143
+ const hour = i === 0 ? 12 : i;
144
+ const angle = (i * 30) - 90;
145
+ const x = CENTER + NUMBER_RADIUS * Math.cos(angle * Math.PI / 180);
146
+ const y = CENTER + NUMBER_RADIUS * Math.sin(angle * Math.PI / 180);
147
+ const isSelected = (mode === '12h' ? displayHours : hours) === (hour === 12 ? 0 : hour);
148
+
149
+ return (
150
+ <g key={i} onClick={() => handleHourClick(hour)}>
151
+ <circle
152
+ cx={x}
153
+ cy={y}
154
+ r={CIRCLE_RADIUS}
155
+ fill={isSelected ? '#3b82f6' : 'transparent'}
156
+ stroke={isSelected ? '#3b82f6' : '#e5e7eb'}
157
+ strokeWidth="1"
158
+ style={{ cursor: 'pointer' }}
159
+ />
160
+ <text
161
+ x={x}
162
+ y={y + 4}
163
+ textAnchor="middle"
164
+ fontSize="14"
165
+ fill={isSelected ? '#ffffff' : '#374151'}
166
+ fontWeight="500"
167
+ style={{ cursor: 'pointer', userSelect: 'none', pointerEvents: 'none' }}
168
+ >
169
+ {hour}
170
+ </text>
171
+ </g>
172
+ );
173
+ })}
174
+
175
+ {/* Hour hand pointing to selected hour */}
176
+ {(() => {
177
+ const selectedHour = mode === '12h' ? displayHours : hours;
178
+ // Convert 12 to 0 for angle calculation, but keep others as-is
179
+ const hourFor12Clock = selectedHour === 12 ? 0 : selectedHour;
180
+ const hourAngle = (hourFor12Clock * 30) - 90;
181
+ const handX = CENTER + HOUR_HAND_LENGTH * Math.cos(hourAngle * Math.PI / 180);
182
+ const handY = CENTER + HOUR_HAND_LENGTH * Math.sin(hourAngle * Math.PI / 180);
183
+
184
+ return (
185
+ <line
186
+ x1={CENTER}
187
+ y1={CENTER}
188
+ x2={handX}
189
+ y2={handY}
190
+ stroke="#3b82f6"
191
+ strokeWidth="3"
192
+ strokeLinecap="round"
193
+ />
194
+ );
195
+ })()}
196
+
197
+ {/* Center dot */}
198
+ <circle cx={CENTER} cy={CENTER} r="4" fill="#3b82f6"/>
199
+ </svg>
200
+ </View>
201
+ );
202
+ } else {
203
+ return (
204
+ <View style={timePickerStyles.clockContainer}>
205
+ <svg width={CLOCK_SIZE} height={CLOCK_SIZE} style={timePickerStyles.clockSvg}>
206
+ {/* Clock face */}
207
+ <circle cx={CENTER} cy={CENTER} r={CLOCK_RADIUS} fill="#f9fafb" stroke="#e5e7eb" strokeWidth="2"/>
208
+
209
+ {/* Minute markers - every 5 minutes */}
210
+ {[...Array(12)].map((_, i) => {
211
+ const minute = i * 5;
212
+ const angle = (i * 30) - 90;
213
+ const x = CENTER + NUMBER_RADIUS * Math.cos(angle * Math.PI / 180);
214
+ const y = CENTER + NUMBER_RADIUS * Math.sin(angle * Math.PI / 180);
215
+ const isSelected = Math.floor(minutes / 5) * 5 === minute;
216
+
217
+ return (
218
+ <g key={i} onClick={() => handleMinuteClick(minute)}>
219
+ <circle
220
+ cx={x}
221
+ cy={y}
222
+ r={CIRCLE_RADIUS}
223
+ fill={isSelected ? '#3b82f6' : 'transparent'}
224
+ stroke={isSelected ? '#3b82f6' : '#e5e7eb'}
225
+ strokeWidth="1"
226
+ style={{ cursor: 'pointer' }}
227
+ />
228
+ <text
229
+ x={x}
230
+ y={y + 4}
231
+ textAnchor="middle"
232
+ fontSize="12"
233
+ fill={isSelected ? '#ffffff' : '#374151'}
234
+ fontWeight="500"
235
+ style={{ cursor: 'pointer', userSelect: 'none', pointerEvents: 'none' }}
236
+ >
237
+ {minute.toString().padStart(2, '0')}
238
+ </text>
239
+ </g>
240
+ );
241
+ })}
242
+
243
+ {/* Minute hand pointing to selected minute */}
244
+ {(() => {
245
+ const minuteAngle = (minutes * 6) - 90;
246
+ const handX = CENTER + MINUTE_HAND_LENGTH * Math.cos(minuteAngle * Math.PI / 180);
247
+ const handY = CENTER + MINUTE_HAND_LENGTH * Math.sin(minuteAngle * Math.PI / 180);
248
+
249
+ return (
250
+ <line
251
+ x1={CENTER}
252
+ y1={CENTER}
253
+ x2={handX}
254
+ y2={handY}
255
+ stroke="#3b82f6"
256
+ strokeWidth="2"
257
+ strokeLinecap="round"
258
+ />
259
+ );
260
+ })()}
261
+
262
+ {/* Center dot */}
263
+ <circle cx={CENTER} cy={CENTER} r="4" fill="#3b82f6"/>
264
+ </svg>
265
+ </View>
266
+ );
267
+ }
268
+ };
269
+
270
+
271
+ return (
272
+ <View style={[timePickerStyles.container, style]} data-testid={testID}>
273
+ {/* Tab Bar */}
274
+ <View style={timePickerStyles.tabBar}>
275
+ <Button
276
+ onPress={() => setActiveSelection('hour')}
277
+ style={[
278
+ timePickerStyles.tabButton,
279
+ activeSelection === 'hour' ? timePickerStyles.activeTab : timePickerStyles.inactiveTab
280
+ ]}
281
+ disabled={disabled}
282
+ >
283
+ Hour
284
+ </Button>
285
+ <Button
286
+ onPress={() => setActiveSelection('minute')}
287
+ style={[
288
+ timePickerStyles.tabButton,
289
+ activeSelection === 'minute' ? timePickerStyles.activeTab : timePickerStyles.inactiveTab
290
+ ]}
291
+ disabled={disabled}
292
+ >
293
+ Minute
294
+ </Button>
295
+ </View>
296
+
297
+ {/* Interactive Clock Face */}
298
+ {renderClockFace()}
299
+
300
+ {/* Time Input Row */}
301
+ <View style={timePickerStyles.timeInputRow}>
302
+ <Input
303
+ ref={hourInputRef}
304
+ variant="bare"
305
+ value={hourInputValue}
306
+ onChangeText={(value) => {
307
+ setHourInputValue(value);
308
+
309
+ // Smart focus switching: if user types 2 or higher, focus on minutes
310
+ const num = parseInt(value);
311
+ if (!isNaN(num) && num >= 2 && mode === '12h') {
312
+ // Wait a moment then focus minutes
313
+ setTimeout(() => {
314
+ setActiveSelection('minute');
315
+ setHourInputFocused(false);
316
+ setMinuteInputFocused(true);
317
+ minuteInputRef.current?.focus();
318
+ }, 100);
319
+ }
320
+
321
+ // Try to update time if value is valid
322
+ const hour = parseInt(value);
323
+ if (!isNaN(hour)) {
324
+ const maxHour = mode === '12h' ? 12 : 23;
325
+ const minHour = mode === '12h' ? 1 : 0;
326
+
327
+ if (hour >= minHour && hour <= maxHour) {
328
+ let hour24 = hour;
329
+ if (mode === '12h') {
330
+ const isPM = hours >= 12;
331
+ if (hour === 12) hour24 = isPM ? 12 : 0;
332
+ else hour24 = isPM ? hour + 12 : hour;
333
+ }
334
+ updateTime(hour24, minutes, seconds);
335
+ }
336
+ }
337
+ }}
338
+ onFocus={() => {
339
+ setActiveSelection('hour');
340
+ setHourInputFocused(true);
341
+ setHourInputValue(String(displayHours));
342
+ }}
343
+ onBlur={() => {
344
+ setHourInputFocused(false);
345
+ // Handle 0 -> 12 conversion for 12h mode
346
+ const hour = parseInt(hourInputValue);
347
+ if (hour === 0 && mode === '12h') {
348
+ const isPM = hours >= 12;
349
+ const hour24 = isPM ? 12 : 0;
350
+ updateTime(hour24, minutes, seconds);
351
+ }
352
+ setHourInputValue(String(displayHours));
353
+ }}
354
+ style={[
355
+ timePickerStyles.timeInput,
356
+ activeSelection === 'hour' ? timePickerStyles.activeInput : {}
357
+ ]}
358
+ disabled={disabled}
359
+ />
360
+ <View style={timePickerStyles.timeSeparator}>:</View>
361
+ <Input
362
+ ref={minuteInputRef}
363
+ variant="bare"
364
+ value={minuteInputValue}
365
+ onChangeText={(value) => {
366
+ setMinuteInputValue(value);
367
+
368
+ // Try to update time if value is valid
369
+ const minute = parseInt(value);
370
+ if (!isNaN(minute)) {
371
+ if (minute >= 0 && minute <= 59) {
372
+ updateTime(hours, minute, seconds);
373
+ }
374
+ }
375
+ }}
376
+ onFocus={() => {
377
+ setActiveSelection('minute');
378
+ setMinuteInputFocused(true);
379
+ setMinuteInputValue(String(minutes));
380
+ }}
381
+ onBlur={() => {
382
+ setMinuteInputFocused(false);
383
+ setMinuteInputValue(String(minutes).padStart(2, '0'));
384
+ }}
385
+ style={[
386
+ timePickerStyles.timeInput,
387
+ activeSelection === 'minute' ? timePickerStyles.activeInput : {}
388
+ ]}
389
+ disabled={disabled}
390
+ />
391
+
392
+ {mode === '12h' && ampm && (
393
+ <Button
394
+ variant="outlined"
395
+ size="small"
396
+ onPress={toggleAmPm}
397
+ disabled={disabled}
398
+ style={timePickerStyles.ampmButton}
399
+ >
400
+ {ampm}
401
+ </Button>
402
+ )}
403
+ </View>
404
+ </View>
405
+ );
406
+ };
@@ -0,0 +1,3 @@
1
+ export { default as DateTimePicker } from './DateTimePicker.native';
2
+ export { TimePicker } from './TimePicker.native';
3
+ export * from './types';
@@ -0,0 +1,3 @@
1
+ export { default as DateTimePicker } from './DateTimePicker';
2
+ export { TimePicker } from './TimePicker';
3
+ export * from './types';
@@ -0,0 +1,84 @@
1
+ import { ReactNode } from 'react';
2
+ import { ViewStyle } from 'react-native';
3
+
4
+ export interface DateTimePickerProps {
5
+ /** Current selected date and time */
6
+ value?: Date;
7
+
8
+ /** Called when date/time changes */
9
+ onChange: (date: Date | null) => void;
10
+
11
+ /** Minimum selectable date */
12
+ minDate?: Date;
13
+
14
+ /** Maximum selectable date */
15
+ maxDate?: Date;
16
+
17
+ /** Disabled state */
18
+ disabled?: boolean;
19
+
20
+ /** Placeholder text when no date is selected */
21
+ placeholder?: string;
22
+
23
+ /** Label for the picker */
24
+ label?: string;
25
+
26
+ /** Error message to display */
27
+ error?: string;
28
+
29
+ /** Helper text */
30
+ helperText?: string;
31
+
32
+ /** Date format for display (default: 'MM/dd/yyyy HH:mm') */
33
+ format?: string;
34
+
35
+ /** Locale for date formatting */
36
+ locale?: string;
37
+
38
+ /** Size variant */
39
+ size?: 'small' | 'medium' | 'large';
40
+
41
+ /** Visual variant */
42
+ variant?: 'outlined' | 'filled';
43
+
44
+ /** Time picker mode */
45
+ timeMode?: '12h' | '24h';
46
+
47
+ /** Show seconds in time picker */
48
+ showSeconds?: boolean;
49
+
50
+ /** Time step in minutes */
51
+ timeStep?: number;
52
+
53
+ /** Custom styles */
54
+ style?: ViewStyle;
55
+
56
+ /** Test ID for testing */
57
+ testID?: string;
58
+ }
59
+
60
+ export interface TimePickerProps {
61
+ /** Current selected time */
62
+ value?: Date;
63
+
64
+ /** Called when time is selected */
65
+ onChange: (time: Date) => void;
66
+
67
+ /** Disabled state */
68
+ disabled?: boolean;
69
+
70
+ /** Time picker mode */
71
+ mode?: '12h' | '24h';
72
+
73
+ /** Show seconds */
74
+ showSeconds?: boolean;
75
+
76
+ /** Time step in minutes */
77
+ step?: number;
78
+
79
+ /** Custom styles */
80
+ style?: ViewStyle;
81
+
82
+ /** Test ID for testing */
83
+ testID?: string;
84
+ }