@dreamstack-us/kaal 0.0.1
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/lib/module/components/CalendarGrid/CalendarGrid.js +112 -0
- package/lib/module/components/CalendarGrid/CalendarGrid.js.map +1 -0
- package/lib/module/components/CalendarGrid/CalendarGrid.styles.js +46 -0
- package/lib/module/components/CalendarGrid/CalendarGrid.styles.js.map +1 -0
- package/lib/module/components/CalendarGrid/DayCell.js +96 -0
- package/lib/module/components/CalendarGrid/DayCell.js.map +1 -0
- package/lib/module/components/CalendarGrid/index.js +4 -0
- package/lib/module/components/CalendarGrid/index.js.map +1 -0
- package/lib/module/components/DatePicker/DatePicker.android.js +66 -0
- package/lib/module/components/DatePicker/DatePicker.android.js.map +1 -0
- package/lib/module/components/DatePicker/DatePicker.ios.js +74 -0
- package/lib/module/components/DatePicker/DatePicker.ios.js.map +1 -0
- package/lib/module/components/DatePicker/DatePicker.js +9 -0
- package/lib/module/components/DatePicker/DatePicker.js.map +1 -0
- package/lib/module/components/DatePicker/DatePicker.styles.js +35 -0
- package/lib/module/components/DatePicker/DatePicker.styles.js.map +1 -0
- package/lib/module/components/DatePicker/DatePicker.web.js +32 -0
- package/lib/module/components/DatePicker/DatePicker.web.js.map +1 -0
- package/lib/module/components/DatePicker/index.js +4 -0
- package/lib/module/components/DatePicker/index.js.map +1 -0
- package/lib/module/components/TimePicker/ClockFace.js +194 -0
- package/lib/module/components/TimePicker/ClockFace.js.map +1 -0
- package/lib/module/components/TimePicker/MaterialTimePicker.js +122 -0
- package/lib/module/components/TimePicker/MaterialTimePicker.js.map +1 -0
- package/lib/module/components/TimePicker/TimePicker.android.js +77 -0
- package/lib/module/components/TimePicker/TimePicker.android.js.map +1 -0
- package/lib/module/components/TimePicker/TimePicker.ios.js +83 -0
- package/lib/module/components/TimePicker/TimePicker.ios.js.map +1 -0
- package/lib/module/components/TimePicker/TimePicker.js +34 -0
- package/lib/module/components/TimePicker/TimePicker.js.map +1 -0
- package/lib/module/components/TimePicker/TimePicker.styles.js +180 -0
- package/lib/module/components/TimePicker/TimePicker.styles.js.map +1 -0
- package/lib/module/components/TimePicker/TimePicker.web.js +37 -0
- package/lib/module/components/TimePicker/TimePicker.web.js.map +1 -0
- package/lib/module/components/TimePicker/TimeWheelPicker.js +178 -0
- package/lib/module/components/TimePicker/TimeWheelPicker.js.map +1 -0
- package/lib/module/components/TimePicker/index.js +7 -0
- package/lib/module/components/TimePicker/index.js.map +1 -0
- package/lib/module/components/WheelPicker/WheelPicker.js +5 -0
- package/lib/module/components/WheelPicker/WheelPicker.js.map +1 -0
- package/lib/module/components/WheelPicker/WheelPicker.styles.js +41 -0
- package/lib/module/components/WheelPicker/WheelPicker.styles.js.map +1 -0
- package/lib/module/components/WheelPicker/WheelPicker.web.js +190 -0
- package/lib/module/components/WheelPicker/WheelPicker.web.js.map +1 -0
- package/lib/module/components/WheelPicker/index.js +4 -0
- package/lib/module/components/WheelPicker/index.js.map +1 -0
- package/lib/module/components/index.js +7 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/hooks/index.js +6 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useCalendar.js +44 -0
- package/lib/module/hooks/useCalendar.js.map +1 -0
- package/lib/module/hooks/useDatePicker.js +30 -0
- package/lib/module/hooks/useDatePicker.js.map +1 -0
- package/lib/module/hooks/useTimePicker.js +125 -0
- package/lib/module/hooks/useTimePicker.js.map +1 -0
- package/lib/module/index.js +22 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types/datepicker.js +2 -0
- package/lib/module/types/datepicker.js.map +1 -0
- package/lib/module/types/index.js +5 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/timepicker.js +2 -0
- package/lib/module/types/timepicker.js.map +1 -0
- package/lib/module/unistyles.js +9 -0
- package/lib/module/unistyles.js.map +1 -0
- package/lib/module/utils/date.js +205 -0
- package/lib/module/utils/date.js.map +1 -0
- package/lib/module/utils/index.js +5 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/validation.js +61 -0
- package/lib/module/utils/validation.js.map +1 -0
- package/lib/typescript/components/CalendarGrid/CalendarGrid.d.ts +12 -0
- package/lib/typescript/components/CalendarGrid/CalendarGrid.d.ts.map +1 -0
- package/lib/typescript/components/CalendarGrid/CalendarGrid.styles.d.ts +45 -0
- package/lib/typescript/components/CalendarGrid/CalendarGrid.styles.d.ts.map +1 -0
- package/lib/typescript/components/CalendarGrid/DayCell.d.ts +12 -0
- package/lib/typescript/components/CalendarGrid/DayCell.d.ts.map +1 -0
- package/lib/typescript/components/CalendarGrid/index.d.ts +2 -0
- package/lib/typescript/components/CalendarGrid/index.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/DatePicker.android.d.ts +4 -0
- package/lib/typescript/components/DatePicker/DatePicker.android.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/DatePicker.d.ts +15 -0
- package/lib/typescript/components/DatePicker/DatePicker.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/DatePicker.ios.d.ts +4 -0
- package/lib/typescript/components/DatePicker/DatePicker.ios.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/DatePicker.styles.d.ts +29 -0
- package/lib/typescript/components/DatePicker/DatePicker.styles.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/DatePicker.web.d.ts +4 -0
- package/lib/typescript/components/DatePicker/DatePicker.web.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/index.d.ts +3 -0
- package/lib/typescript/components/DatePicker/index.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/ClockFace.d.ts +12 -0
- package/lib/typescript/components/TimePicker/ClockFace.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/MaterialTimePicker.d.ts +12 -0
- package/lib/typescript/components/TimePicker/MaterialTimePicker.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimePicker.android.d.ts +4 -0
- package/lib/typescript/components/TimePicker/TimePicker.android.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimePicker.d.ts +29 -0
- package/lib/typescript/components/TimePicker/TimePicker.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimePicker.ios.d.ts +4 -0
- package/lib/typescript/components/TimePicker/TimePicker.ios.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimePicker.styles.d.ts +168 -0
- package/lib/typescript/components/TimePicker/TimePicker.styles.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimePicker.web.d.ts +10 -0
- package/lib/typescript/components/TimePicker/TimePicker.web.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimeWheelPicker.d.ts +11 -0
- package/lib/typescript/components/TimePicker/TimeWheelPicker.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/index.d.ts +6 -0
- package/lib/typescript/components/TimePicker/index.d.ts.map +1 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.d.ts +2 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.d.ts.map +1 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.styles.d.ts +40 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.styles.d.ts.map +1 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.web.d.ts +10 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.web.d.ts.map +1 -0
- package/lib/typescript/components/WheelPicker/index.d.ts +2 -0
- package/lib/typescript/components/WheelPicker/index.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +5 -0
- package/lib/typescript/components/index.d.ts.map +1 -0
- package/lib/typescript/hooks/index.d.ts +4 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useCalendar.d.ts +10 -0
- package/lib/typescript/hooks/useCalendar.d.ts.map +1 -0
- package/lib/typescript/hooks/useDatePicker.d.ts +15 -0
- package/lib/typescript/hooks/useDatePicker.d.ts.map +1 -0
- package/lib/typescript/hooks/useTimePicker.d.ts +52 -0
- package/lib/typescript/hooks/useTimePicker.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +12 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types/datepicker.d.ts +15 -0
- package/lib/typescript/types/datepicker.d.ts.map +1 -0
- package/lib/typescript/types/index.d.ts +3 -0
- package/lib/typescript/types/index.d.ts.map +1 -0
- package/lib/typescript/types/timepicker.d.ts +54 -0
- package/lib/typescript/types/timepicker.d.ts.map +1 -0
- package/lib/typescript/unistyles.d.ts +3 -0
- package/lib/typescript/unistyles.d.ts.map +1 -0
- package/lib/typescript/utils/date.d.ts +94 -0
- package/lib/typescript/utils/date.d.ts.map +1 -0
- package/lib/typescript/utils/index.d.ts +3 -0
- package/lib/typescript/utils/index.d.ts.map +1 -0
- package/lib/typescript/utils/validation.d.ts +40 -0
- package/lib/typescript/utils/validation.d.ts.map +1 -0
- package/package.json +101 -0
- package/src/components/CalendarGrid/CalendarGrid.styles.ts +44 -0
- package/src/components/CalendarGrid/CalendarGrid.tsx +151 -0
- package/src/components/CalendarGrid/DayCell.tsx +108 -0
- package/src/components/CalendarGrid/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.android.tsx +69 -0
- package/src/components/DatePicker/DatePicker.ios.tsx +78 -0
- package/src/components/DatePicker/DatePicker.styles.ts +35 -0
- package/src/components/DatePicker/DatePicker.tsx +21 -0
- package/src/components/DatePicker/DatePicker.web.tsx +36 -0
- package/src/components/DatePicker/index.ts +2 -0
- package/src/components/TimePicker/ClockFace.tsx +233 -0
- package/src/components/TimePicker/MaterialTimePicker.tsx +169 -0
- package/src/components/TimePicker/TimePicker.android.tsx +80 -0
- package/src/components/TimePicker/TimePicker.ios.tsx +88 -0
- package/src/components/TimePicker/TimePicker.styles.ts +209 -0
- package/src/components/TimePicker/TimePicker.tsx +35 -0
- package/src/components/TimePicker/TimePicker.web.tsx +35 -0
- package/src/components/TimePicker/TimeWheelPicker.tsx +211 -0
- package/src/components/TimePicker/index.ts +5 -0
- package/src/components/WheelPicker/WheelPicker.styles.ts +39 -0
- package/src/components/WheelPicker/WheelPicker.tsx +2 -0
- package/src/components/WheelPicker/WheelPicker.web.tsx +237 -0
- package/src/components/WheelPicker/index.ts +1 -0
- package/src/components/index.ts +11 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useCalendar.ts +59 -0
- package/src/hooks/useDatePicker.ts +40 -0
- package/src/hooks/useTimePicker.ts +152 -0
- package/src/index.ts +77 -0
- package/src/types/datepicker.ts +17 -0
- package/src/types/index.ts +2 -0
- package/src/types/timepicker.ts +59 -0
- package/src/unistyles.ts +6 -0
- package/src/utils/date.ts +217 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/validation.ts +76 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
5
|
+
import { runOnJS } from 'react-native-reanimated';
|
|
6
|
+
import Svg, { Circle, G, Line, Text as SvgText } from 'react-native-svg';
|
|
7
|
+
import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
|
|
8
|
+
import type { ClockMode, TimePeriod, TimeValue } from '../../types/timepicker';
|
|
9
|
+
import { styles } from './TimePicker.styles';
|
|
10
|
+
|
|
11
|
+
// Clock dimensions
|
|
12
|
+
const CLOCK_SIZE = 256;
|
|
13
|
+
const CLOCK_CENTER = CLOCK_SIZE / 2;
|
|
14
|
+
const OUTER_RADIUS = 96;
|
|
15
|
+
const SELECTION_DOT_RADIUS = 20;
|
|
16
|
+
const CENTER_DOT_RADIUS = 4;
|
|
17
|
+
|
|
18
|
+
// Hour positions (12 at top, clockwise)
|
|
19
|
+
const HOURS_12 = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
20
|
+
const MINUTE_LABELS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts clock position (0-11 for hours, 0-59 for minutes) to angle in degrees
|
|
24
|
+
* 12 o'clock is at -90 degrees (top)
|
|
25
|
+
*/
|
|
26
|
+
const positionToAngle = (position: number, isMinutes = false): number => {
|
|
27
|
+
if (isMinutes) {
|
|
28
|
+
return position * 6 - 90; // 360/60 = 6 degrees per minute
|
|
29
|
+
}
|
|
30
|
+
return position * 30 - 90; // 360/12 = 30 degrees per position
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Gets x,y coordinates on circle from angle and radius
|
|
35
|
+
*/
|
|
36
|
+
const getPointOnCircle = (
|
|
37
|
+
angleDegrees: number,
|
|
38
|
+
radius: number,
|
|
39
|
+
): { x: number; y: number } => {
|
|
40
|
+
const rad = (angleDegrees * Math.PI) / 180;
|
|
41
|
+
return {
|
|
42
|
+
x: CLOCK_CENTER + radius * Math.cos(rad),
|
|
43
|
+
y: CLOCK_CENTER + radius * Math.sin(rad),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface ClockFaceProps {
|
|
48
|
+
value: TimeValue;
|
|
49
|
+
onChange: (time: TimeValue) => void;
|
|
50
|
+
mode: ClockMode;
|
|
51
|
+
onModeChange?: (mode: ClockMode) => void;
|
|
52
|
+
is24Hour?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
56
|
+
({ value, onChange, mode, onModeChange, is24Hour = false }) => {
|
|
57
|
+
const { hour: hour12, period } = to12Hour(value.hours);
|
|
58
|
+
|
|
59
|
+
// Calculate hand end position based on current value
|
|
60
|
+
const handAngle = useMemo(() => {
|
|
61
|
+
if (mode === 'hours') {
|
|
62
|
+
const hourIndex = HOURS_12.indexOf(hour12);
|
|
63
|
+
return positionToAngle(hourIndex >= 0 ? hourIndex : 0);
|
|
64
|
+
}
|
|
65
|
+
return positionToAngle(value.minutes, true);
|
|
66
|
+
}, [mode, hour12, value.minutes]);
|
|
67
|
+
|
|
68
|
+
const handEndPos = getPointOnCircle(handAngle, OUTER_RADIUS);
|
|
69
|
+
|
|
70
|
+
const handleInteraction = useCallback(
|
|
71
|
+
(x: number, y: number) => {
|
|
72
|
+
// Convert touch coordinates to angle
|
|
73
|
+
const dx = x - CLOCK_CENTER;
|
|
74
|
+
const dy = y - CLOCK_CENTER;
|
|
75
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
76
|
+
|
|
77
|
+
// Normalize angle to 0-360, with 0 at top (12 o'clock)
|
|
78
|
+
const normalized = (((angle + 90) % 360) + 360) % 360;
|
|
79
|
+
|
|
80
|
+
if (mode === 'hours') {
|
|
81
|
+
// Round to nearest hour position
|
|
82
|
+
const position = Math.round(normalized / 30) % 12;
|
|
83
|
+
const hour = HOURS_12[position];
|
|
84
|
+
if (hour !== undefined) {
|
|
85
|
+
const newHours = to24Hour(hour, period);
|
|
86
|
+
onChange({ hours: newHours, minutes: value.minutes });
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
// Round to nearest minute
|
|
90
|
+
const minutes = Math.round(normalized / 6) % 60;
|
|
91
|
+
onChange({ hours: value.hours, minutes });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[mode, period, value, onChange],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const handleInteractionEnd = useCallback(() => {
|
|
98
|
+
// Switch to minutes mode after selecting hours
|
|
99
|
+
if (mode === 'hours' && onModeChange) {
|
|
100
|
+
// Small delay for visual feedback
|
|
101
|
+
setTimeout(() => onModeChange('minutes'), 200);
|
|
102
|
+
}
|
|
103
|
+
}, [mode, onModeChange]);
|
|
104
|
+
|
|
105
|
+
const panGesture = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
Gesture.Pan()
|
|
108
|
+
.onStart((e) => {
|
|
109
|
+
'worklet';
|
|
110
|
+
runOnJS(handleInteraction)(e.x, e.y);
|
|
111
|
+
})
|
|
112
|
+
.onUpdate((e) => {
|
|
113
|
+
'worklet';
|
|
114
|
+
runOnJS(handleInteraction)(e.x, e.y);
|
|
115
|
+
})
|
|
116
|
+
.onEnd(() => {
|
|
117
|
+
'worklet';
|
|
118
|
+
runOnJS(handleInteractionEnd)();
|
|
119
|
+
}),
|
|
120
|
+
[handleInteraction, handleInteractionEnd],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const tapGesture = useMemo(
|
|
124
|
+
() =>
|
|
125
|
+
Gesture.Tap().onEnd((e) => {
|
|
126
|
+
'worklet';
|
|
127
|
+
runOnJS(handleInteraction)(e.x, e.y);
|
|
128
|
+
runOnJS(handleInteractionEnd)();
|
|
129
|
+
}),
|
|
130
|
+
[handleInteraction, handleInteractionEnd],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const combinedGesture = Gesture.Race(panGesture, tapGesture);
|
|
134
|
+
|
|
135
|
+
// Render clock numbers
|
|
136
|
+
const numbers = useMemo(() => {
|
|
137
|
+
if (mode === 'hours') {
|
|
138
|
+
return HOURS_12.map((hour, index) => {
|
|
139
|
+
const angle = positionToAngle(index);
|
|
140
|
+
const pos = getPointOnCircle(angle, OUTER_RADIUS);
|
|
141
|
+
const isSelected = hour12 === hour;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<SvgText
|
|
145
|
+
key={hour}
|
|
146
|
+
x={pos.x}
|
|
147
|
+
y={pos.y}
|
|
148
|
+
textAnchor="middle"
|
|
149
|
+
alignmentBaseline="central"
|
|
150
|
+
fontSize={14}
|
|
151
|
+
fontWeight={isSelected ? '500' : '400'}
|
|
152
|
+
fill={isSelected ? '#FFFFFF' : '#E6E1E5'}
|
|
153
|
+
>
|
|
154
|
+
{hour}
|
|
155
|
+
</SvgText>
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return MINUTE_LABELS.map((minute, index) => {
|
|
161
|
+
const angle = positionToAngle(index);
|
|
162
|
+
const pos = getPointOnCircle(angle, OUTER_RADIUS);
|
|
163
|
+
const isSelected = value.minutes === minute;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<SvgText
|
|
167
|
+
key={minute}
|
|
168
|
+
x={pos.x}
|
|
169
|
+
y={pos.y}
|
|
170
|
+
textAnchor="middle"
|
|
171
|
+
alignmentBaseline="central"
|
|
172
|
+
fontSize={14}
|
|
173
|
+
fontWeight={isSelected ? '500' : '400'}
|
|
174
|
+
fill={isSelected ? '#FFFFFF' : '#E6E1E5'}
|
|
175
|
+
>
|
|
176
|
+
{minute.toString().padStart(2, '0')}
|
|
177
|
+
</SvgText>
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
}, [mode, hour12, value.minutes]);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<View style={styles.clockContainer}>
|
|
184
|
+
<GestureDetector gesture={combinedGesture}>
|
|
185
|
+
<Svg
|
|
186
|
+
width={CLOCK_SIZE}
|
|
187
|
+
height={CLOCK_SIZE}
|
|
188
|
+
viewBox={`0 0 ${CLOCK_SIZE} ${CLOCK_SIZE}`}
|
|
189
|
+
>
|
|
190
|
+
{/* Background circle */}
|
|
191
|
+
<Circle
|
|
192
|
+
cx={CLOCK_CENTER}
|
|
193
|
+
cy={CLOCK_CENTER}
|
|
194
|
+
r={CLOCK_SIZE / 2 - 4}
|
|
195
|
+
fill="#3F384C"
|
|
196
|
+
/>
|
|
197
|
+
|
|
198
|
+
{/* Selection dot (behind numbers) */}
|
|
199
|
+
<Circle
|
|
200
|
+
cx={handEndPos.x}
|
|
201
|
+
cy={handEndPos.y}
|
|
202
|
+
r={SELECTION_DOT_RADIUS}
|
|
203
|
+
fill="#4DA6FF"
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
{/* Clock hand */}
|
|
207
|
+
<Line
|
|
208
|
+
x1={CLOCK_CENTER}
|
|
209
|
+
y1={CLOCK_CENTER}
|
|
210
|
+
x2={handEndPos.x}
|
|
211
|
+
y2={handEndPos.y}
|
|
212
|
+
stroke="#4DA6FF"
|
|
213
|
+
strokeWidth={2}
|
|
214
|
+
/>
|
|
215
|
+
|
|
216
|
+
{/* Center dot */}
|
|
217
|
+
<Circle
|
|
218
|
+
cx={CLOCK_CENTER}
|
|
219
|
+
cy={CLOCK_CENTER}
|
|
220
|
+
r={CENTER_DOT_RADIUS}
|
|
221
|
+
fill="#4DA6FF"
|
|
222
|
+
/>
|
|
223
|
+
|
|
224
|
+
{/* Numbers */}
|
|
225
|
+
<G>{numbers}</G>
|
|
226
|
+
</Svg>
|
|
227
|
+
</GestureDetector>
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
ClockFace.displayName = 'ClockFace';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { memo, useCallback, useState } from 'react';
|
|
3
|
+
import { Pressable, Text, View } from 'react-native';
|
|
4
|
+
import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
|
|
5
|
+
import type { ClockMode, TimePeriod, TimeValue } from '../../types/timepicker';
|
|
6
|
+
import { ClockFace } from './ClockFace';
|
|
7
|
+
import { styles } from './TimePicker.styles';
|
|
8
|
+
|
|
9
|
+
interface MaterialTimePickerProps {
|
|
10
|
+
value: TimeValue;
|
|
11
|
+
onChange: (time: TimeValue) => void;
|
|
12
|
+
is24Hour?: boolean;
|
|
13
|
+
onCancel?: () => void;
|
|
14
|
+
onConfirm?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
|
|
18
|
+
({ value, onChange, is24Hour = false, onCancel, onConfirm }) => {
|
|
19
|
+
const [mode, setMode] = useState<ClockMode>('hours');
|
|
20
|
+
const { hour: hour12, period } = to12Hour(value.hours);
|
|
21
|
+
|
|
22
|
+
const handleHourPress = useCallback(() => {
|
|
23
|
+
setMode('hours');
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const handleMinutePress = useCallback(() => {
|
|
27
|
+
setMode('minutes');
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const handlePeriodChange = useCallback(
|
|
31
|
+
(newPeriod: TimePeriod) => {
|
|
32
|
+
const newHours = to24Hour(hour12, newPeriod);
|
|
33
|
+
onChange({ hours: newHours, minutes: value.minutes });
|
|
34
|
+
},
|
|
35
|
+
[hour12, value.minutes, onChange],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const handleModeChange = useCallback((newMode: ClockMode) => {
|
|
39
|
+
setMode(newMode);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
// Format display values
|
|
43
|
+
const displayHour = is24Hour
|
|
44
|
+
? value.hours.toString().padStart(2, '0')
|
|
45
|
+
: hour12.toString().padStart(2, '0');
|
|
46
|
+
const displayMinute = value.minutes.toString().padStart(2, '0');
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={styles.materialContainer}>
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<Text style={styles.materialHeader}>Select time</Text>
|
|
52
|
+
|
|
53
|
+
{/* Time Input Display */}
|
|
54
|
+
<View style={styles.timeInputContainer}>
|
|
55
|
+
<View style={styles.timeFieldsContainer}>
|
|
56
|
+
{/* Hour Field */}
|
|
57
|
+
<Pressable
|
|
58
|
+
onPress={handleHourPress}
|
|
59
|
+
style={[
|
|
60
|
+
styles.timeField,
|
|
61
|
+
mode === 'hours' && styles.timeFieldActive,
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
<Text
|
|
65
|
+
style={[
|
|
66
|
+
styles.timeFieldText,
|
|
67
|
+
mode === 'hours' && styles.timeFieldTextActive,
|
|
68
|
+
]}
|
|
69
|
+
>
|
|
70
|
+
{displayHour}
|
|
71
|
+
</Text>
|
|
72
|
+
</Pressable>
|
|
73
|
+
|
|
74
|
+
{/* Separator */}
|
|
75
|
+
<Text style={styles.timeSeparator}>:</Text>
|
|
76
|
+
|
|
77
|
+
{/* Minute Field */}
|
|
78
|
+
<Pressable
|
|
79
|
+
onPress={handleMinutePress}
|
|
80
|
+
style={[
|
|
81
|
+
styles.timeField,
|
|
82
|
+
mode === 'minutes' && styles.timeFieldActive,
|
|
83
|
+
]}
|
|
84
|
+
>
|
|
85
|
+
<Text
|
|
86
|
+
style={[
|
|
87
|
+
styles.timeFieldText,
|
|
88
|
+
mode === 'minutes' && styles.timeFieldTextActive,
|
|
89
|
+
]}
|
|
90
|
+
>
|
|
91
|
+
{displayMinute}
|
|
92
|
+
</Text>
|
|
93
|
+
</Pressable>
|
|
94
|
+
</View>
|
|
95
|
+
|
|
96
|
+
{/* AM/PM Toggle (only for 12-hour format) */}
|
|
97
|
+
{!is24Hour && (
|
|
98
|
+
<View style={styles.periodToggleContainer}>
|
|
99
|
+
<Pressable
|
|
100
|
+
onPress={() => handlePeriodChange('AM')}
|
|
101
|
+
style={[
|
|
102
|
+
styles.periodButton,
|
|
103
|
+
styles.periodButtonTop,
|
|
104
|
+
period === 'AM' && styles.periodButtonActive,
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
<Text
|
|
108
|
+
style={[
|
|
109
|
+
styles.periodButtonText,
|
|
110
|
+
period === 'AM' && styles.periodButtonTextActive,
|
|
111
|
+
]}
|
|
112
|
+
>
|
|
113
|
+
AM
|
|
114
|
+
</Text>
|
|
115
|
+
</Pressable>
|
|
116
|
+
<Pressable
|
|
117
|
+
onPress={() => handlePeriodChange('PM')}
|
|
118
|
+
style={[
|
|
119
|
+
styles.periodButton,
|
|
120
|
+
period === 'PM' && styles.periodButtonActive,
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
<Text
|
|
124
|
+
style={[
|
|
125
|
+
styles.periodButtonText,
|
|
126
|
+
period === 'PM' && styles.periodButtonTextActive,
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
PM
|
|
130
|
+
</Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
</View>
|
|
133
|
+
)}
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
{/* Clock Face */}
|
|
137
|
+
<ClockFace
|
|
138
|
+
value={value}
|
|
139
|
+
onChange={onChange}
|
|
140
|
+
mode={mode}
|
|
141
|
+
onModeChange={handleModeChange}
|
|
142
|
+
is24Hour={is24Hour}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
{/* Actions */}
|
|
146
|
+
<View style={styles.actionsContainer}>
|
|
147
|
+
<Pressable style={styles.keyboardButton}>
|
|
148
|
+
<Text style={styles.keyboardButtonText}>⌨️</Text>
|
|
149
|
+
</Pressable>
|
|
150
|
+
|
|
151
|
+
<View style={styles.actionButtonsContainer}>
|
|
152
|
+
{onCancel && (
|
|
153
|
+
<Pressable style={styles.actionButton} onPress={onCancel}>
|
|
154
|
+
<Text style={styles.actionButtonText}>Cancel</Text>
|
|
155
|
+
</Pressable>
|
|
156
|
+
)}
|
|
157
|
+
{onConfirm && (
|
|
158
|
+
<Pressable style={styles.actionButton} onPress={onConfirm}>
|
|
159
|
+
<Text style={styles.actionButtonText}>OK</Text>
|
|
160
|
+
</Pressable>
|
|
161
|
+
)}
|
|
162
|
+
</View>
|
|
163
|
+
</View>
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
MaterialTimePicker.displayName = 'MaterialTimePicker';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { Suspense, useCallback } from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
import type { TimePickerProps, TimeValue } from '../../types/timepicker';
|
|
4
|
+
import { MaterialTimePicker } from './MaterialTimePicker';
|
|
5
|
+
import { styles } from './TimePicker.styles';
|
|
6
|
+
|
|
7
|
+
interface ExpoTimePickerProps {
|
|
8
|
+
value: TimeValue;
|
|
9
|
+
onChange: (time: TimeValue) => void;
|
|
10
|
+
is24Hour?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// @ts-expect-error - React.lazy fallback returns null when @expo/ui unavailable
|
|
14
|
+
const ExpoTimePicker = React.lazy(async () => {
|
|
15
|
+
try {
|
|
16
|
+
// @ts-expect-error - @expo/ui types not available
|
|
17
|
+
const { DateTimePicker } = await import('@expo/ui/jetpack-compose');
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
default: ({ value, onChange, is24Hour }: ExpoTimePickerProps) => {
|
|
21
|
+
// Create a Date object for the picker
|
|
22
|
+
const date = new Date();
|
|
23
|
+
date.setHours(value.hours, value.minutes, 0, 0);
|
|
24
|
+
|
|
25
|
+
const handleChange = (newDate: Date) => {
|
|
26
|
+
onChange({
|
|
27
|
+
hours: newDate.getHours(),
|
|
28
|
+
minutes: newDate.getMinutes(),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<DateTimePicker
|
|
34
|
+
onDateSelected={handleChange}
|
|
35
|
+
displayedComponents="hourAndMinute"
|
|
36
|
+
initialDate={date.toISOString()}
|
|
37
|
+
variant="picker"
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
// Fallback when @expo/ui is not available
|
|
44
|
+
return { default: (_props: ExpoTimePickerProps) => null };
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const TimePicker: React.FC<TimePickerProps> = ({
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
theme = 'native',
|
|
52
|
+
is24Hour = false,
|
|
53
|
+
}) => {
|
|
54
|
+
const handleTimeChange = useCallback(
|
|
55
|
+
(time: TimeValue) => {
|
|
56
|
+
onChange(time);
|
|
57
|
+
},
|
|
58
|
+
[onChange],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Use native Android picker for 'native' theme
|
|
62
|
+
if (theme === 'native') {
|
|
63
|
+
return (
|
|
64
|
+
<View style={styles.container}>
|
|
65
|
+
<Suspense fallback={<ActivityIndicator />}>
|
|
66
|
+
<ExpoTimePicker
|
|
67
|
+
value={value}
|
|
68
|
+
onChange={handleTimeChange}
|
|
69
|
+
is24Hour={is24Hour}
|
|
70
|
+
/>
|
|
71
|
+
</Suspense>
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Use Material-style picker for 'android' or 'ios' themes
|
|
77
|
+
return (
|
|
78
|
+
<MaterialTimePicker value={value} onChange={onChange} is24Hour={is24Hour} />
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { Suspense, useCallback } from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
import type { TimePickerProps, TimeValue } from '../../types/timepicker';
|
|
4
|
+
import { styles } from './TimePicker.styles';
|
|
5
|
+
import { TimeWheelPicker } from './TimeWheelPicker';
|
|
6
|
+
|
|
7
|
+
interface ExpoTimePickerProps {
|
|
8
|
+
value: TimeValue;
|
|
9
|
+
onChange: (time: TimeValue) => void;
|
|
10
|
+
is24Hour?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// @ts-expect-error - React.lazy fallback returns null when @expo/ui unavailable
|
|
14
|
+
const ExpoTimePicker = React.lazy(async () => {
|
|
15
|
+
try {
|
|
16
|
+
// @ts-expect-error - @expo/ui types not available
|
|
17
|
+
const { DateTimePicker, Host } = await import('@expo/ui/swift-ui');
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
default: ({ value, onChange, is24Hour }: ExpoTimePickerProps) => {
|
|
21
|
+
// Create a Date object for the picker
|
|
22
|
+
const date = new Date();
|
|
23
|
+
date.setHours(value.hours, value.minutes, 0, 0);
|
|
24
|
+
|
|
25
|
+
const handleChange = (newDate: Date) => {
|
|
26
|
+
onChange({
|
|
27
|
+
hours: newDate.getHours(),
|
|
28
|
+
minutes: newDate.getMinutes(),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Host matchContents>
|
|
34
|
+
<DateTimePicker
|
|
35
|
+
onDateSelected={handleChange}
|
|
36
|
+
displayedComponents="hourAndMinute"
|
|
37
|
+
initialDate={date.toISOString()}
|
|
38
|
+
variant="wheel"
|
|
39
|
+
/>
|
|
40
|
+
</Host>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
// Fallback when @expo/ui is not available
|
|
46
|
+
return { default: (_props: ExpoTimePickerProps) => null };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const TimePicker: React.FC<TimePickerProps> = ({
|
|
51
|
+
value,
|
|
52
|
+
onChange,
|
|
53
|
+
theme = 'native',
|
|
54
|
+
is24Hour = false,
|
|
55
|
+
minuteInterval = 1,
|
|
56
|
+
}) => {
|
|
57
|
+
const handleTimeChange = useCallback(
|
|
58
|
+
(time: TimeValue) => {
|
|
59
|
+
onChange(time);
|
|
60
|
+
},
|
|
61
|
+
[onChange],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Use native iOS picker for 'native' or 'ios' theme
|
|
65
|
+
if (theme === 'native' || theme === 'ios') {
|
|
66
|
+
return (
|
|
67
|
+
<View style={styles.container}>
|
|
68
|
+
<Suspense fallback={<ActivityIndicator />}>
|
|
69
|
+
<ExpoTimePicker
|
|
70
|
+
value={value}
|
|
71
|
+
onChange={handleTimeChange}
|
|
72
|
+
is24Hour={is24Hour}
|
|
73
|
+
/>
|
|
74
|
+
</Suspense>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback to wheel picker for other themes
|
|
80
|
+
return (
|
|
81
|
+
<TimeWheelPicker
|
|
82
|
+
value={value}
|
|
83
|
+
onChange={onChange}
|
|
84
|
+
is24Hour={is24Hour}
|
|
85
|
+
minuteInterval={minuteInterval}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|