@dreamstack-us/kaal 0.0.1 → 0.0.2
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 +47 -13
- package/lib/module/components/CalendarGrid/CalendarGrid.js.map +1 -1
- package/lib/module/components/CalendarGrid/CalendarGrid.styles.js +22 -17
- package/lib/module/components/CalendarGrid/CalendarGrid.styles.js.map +1 -1
- package/lib/module/components/CalendarGrid/CalendarGrid.web.js +203 -0
- package/lib/module/components/CalendarGrid/CalendarGrid.web.js.map +1 -0
- package/lib/module/components/CalendarGrid/DayCell.js +64 -52
- package/lib/module/components/CalendarGrid/DayCell.js.map +1 -1
- package/lib/module/components/CalendarGrid/DayCell.web.js +112 -0
- package/lib/module/components/CalendarGrid/DayCell.web.js.map +1 -0
- package/lib/module/components/CalendarGrid/index.js +1 -1
- package/lib/module/components/CalendarGrid/index.js.map +1 -1
- package/lib/module/components/DatePicker/DatePicker.android.js +17 -8
- package/lib/module/components/DatePicker/DatePicker.android.js.map +1 -1
- package/lib/module/components/DatePicker/DatePicker.ios.js +17 -8
- package/lib/module/components/DatePicker/DatePicker.ios.js.map +1 -1
- package/lib/module/components/DatePicker/DatePicker.js.map +1 -1
- package/lib/module/components/DatePicker/DatePicker.styles.js +19 -20
- package/lib/module/components/DatePicker/DatePicker.styles.js.map +1 -1
- package/lib/module/components/DatePicker/DatePicker.web.js +26 -12
- package/lib/module/components/DatePicker/DatePicker.web.js.map +1 -1
- package/lib/module/components/TimePicker/ClockFace.js +27 -7
- package/lib/module/components/TimePicker/ClockFace.js.map +1 -1
- package/lib/module/components/TimePicker/ClockFace.web.js +253 -0
- package/lib/module/components/TimePicker/ClockFace.web.js.map +1 -0
- package/lib/module/components/TimePicker/MaterialTimePicker.js +68 -16
- package/lib/module/components/TimePicker/MaterialTimePicker.js.map +1 -1
- package/lib/module/components/TimePicker/MaterialTimePicker.web.js +231 -0
- package/lib/module/components/TimePicker/MaterialTimePicker.web.js.map +1 -0
- package/lib/module/components/TimePicker/TimePicker.android.js +13 -6
- package/lib/module/components/TimePicker/TimePicker.android.js.map +1 -1
- package/lib/module/components/TimePicker/TimePicker.ios.js +14 -7
- package/lib/module/components/TimePicker/TimePicker.ios.js.map +1 -1
- package/lib/module/components/TimePicker/TimePicker.styles.js +53 -45
- package/lib/module/components/TimePicker/TimePicker.styles.js.map +1 -1
- package/lib/module/components/TimePicker/TimePicker.web.js +24 -12
- package/lib/module/components/TimePicker/TimePicker.web.js.map +1 -1
- package/lib/module/components/TimePicker/TimeWheelPicker.js +45 -10
- package/lib/module/components/TimePicker/TimeWheelPicker.js.map +1 -1
- package/lib/module/components/TimePicker/TimeWheelPicker.web.js +339 -0
- package/lib/module/components/TimePicker/TimeWheelPicker.web.js.map +1 -0
- package/lib/module/components/TimePicker/index.js +3 -3
- package/lib/module/components/TimePicker/index.js.map +1 -1
- package/lib/module/components/WheelPicker/WheelPicker.js +21 -2
- package/lib/module/components/WheelPicker/WheelPicker.js.map +1 -1
- package/lib/module/components/WheelPicker/WheelPicker.styles.js +13 -8
- package/lib/module/components/WheelPicker/WheelPicker.styles.js.map +1 -1
- package/lib/module/components/WheelPicker/WheelPicker.web.js +146 -57
- package/lib/module/components/WheelPicker/WheelPicker.web.js.map +1 -1
- package/lib/module/context/ThemeOverrideContext.js +34 -0
- package/lib/module/context/ThemeOverrideContext.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/validation.js +74 -34
- package/lib/module/utils/validation.js.map +1 -1
- package/lib/typescript/components/CalendarGrid/CalendarGrid.d.ts +9 -0
- package/lib/typescript/components/CalendarGrid/CalendarGrid.d.ts.map +1 -1
- package/lib/typescript/components/CalendarGrid/CalendarGrid.styles.d.ts +12 -10
- package/lib/typescript/components/CalendarGrid/CalendarGrid.styles.d.ts.map +1 -1
- package/lib/typescript/components/CalendarGrid/CalendarGrid.web.d.ts +21 -0
- package/lib/typescript/components/CalendarGrid/CalendarGrid.web.d.ts.map +1 -0
- package/lib/typescript/components/CalendarGrid/DayCell.d.ts.map +1 -1
- package/lib/typescript/components/CalendarGrid/DayCell.web.d.ts +12 -0
- package/lib/typescript/components/CalendarGrid/DayCell.web.d.ts.map +1 -0
- package/lib/typescript/components/DatePicker/DatePicker.android.d.ts.map +1 -1
- package/lib/typescript/components/DatePicker/DatePicker.d.ts +12 -1
- package/lib/typescript/components/DatePicker/DatePicker.d.ts.map +1 -1
- package/lib/typescript/components/DatePicker/DatePicker.ios.d.ts.map +1 -1
- package/lib/typescript/components/DatePicker/DatePicker.styles.d.ts +12 -13
- package/lib/typescript/components/DatePicker/DatePicker.styles.d.ts.map +1 -1
- package/lib/typescript/components/DatePicker/DatePicker.web.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/ClockFace.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/ClockFace.web.d.ts +12 -0
- package/lib/typescript/components/TimePicker/ClockFace.web.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/MaterialTimePicker.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/MaterialTimePicker.web.d.ts +12 -0
- package/lib/typescript/components/TimePicker/MaterialTimePicker.web.d.ts.map +1 -0
- package/lib/typescript/components/TimePicker/TimePicker.android.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/TimePicker.ios.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/TimePicker.styles.d.ts +29 -25
- package/lib/typescript/components/TimePicker/TimePicker.styles.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/TimePicker.web.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/TimeWheelPicker.d.ts.map +1 -1
- package/lib/typescript/components/TimePicker/TimeWheelPicker.web.d.ts +11 -0
- package/lib/typescript/components/TimePicker/TimeWheelPicker.web.d.ts.map +1 -0
- package/lib/typescript/components/WheelPicker/WheelPicker.d.ts +14 -1
- package/lib/typescript/components/WheelPicker/WheelPicker.d.ts.map +1 -1
- package/lib/typescript/components/WheelPicker/WheelPicker.styles.d.ts +9 -7
- package/lib/typescript/components/WheelPicker/WheelPicker.styles.d.ts.map +1 -1
- package/lib/typescript/components/WheelPicker/WheelPicker.web.d.ts.map +1 -1
- package/lib/typescript/context/ThemeOverrideContext.d.ts +23 -0
- package/lib/typescript/context/ThemeOverrideContext.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/types/datepicker.d.ts +41 -0
- package/lib/typescript/types/datepicker.d.ts.map +1 -1
- package/lib/typescript/types/timepicker.d.ts +62 -0
- package/lib/typescript/types/timepicker.d.ts.map +1 -1
- package/lib/typescript/utils/validation.d.ts +47 -27
- package/lib/typescript/utils/validation.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/components/CalendarGrid/CalendarGrid.styles.ts +21 -17
- package/src/components/CalendarGrid/CalendarGrid.tsx +98 -12
- package/src/components/CalendarGrid/CalendarGrid.web.tsx +305 -0
- package/src/components/CalendarGrid/DayCell.tsx +78 -59
- package/src/components/CalendarGrid/DayCell.web.tsx +129 -0
- package/src/components/DatePicker/DatePicker.android.tsx +14 -8
- package/src/components/DatePicker/DatePicker.ios.tsx +14 -8
- package/src/components/DatePicker/DatePicker.styles.ts +18 -22
- package/src/components/DatePicker/DatePicker.tsx +12 -1
- package/src/components/DatePicker/DatePicker.web.tsx +21 -13
- package/src/components/TimePicker/ClockFace.tsx +34 -8
- package/src/components/TimePicker/ClockFace.web.tsx +303 -0
- package/src/components/TimePicker/MaterialTimePicker.tsx +144 -13
- package/src/components/TimePicker/MaterialTimePicker.web.tsx +271 -0
- package/src/components/TimePicker/TimePicker.android.tsx +9 -1
- package/src/components/TimePicker/TimePicker.ios.tsx +10 -6
- package/src/components/TimePicker/TimePicker.styles.ts +52 -45
- package/src/components/TimePicker/TimePicker.web.tsx +17 -7
- package/src/components/TimePicker/TimeWheelPicker.tsx +60 -6
- package/src/components/TimePicker/TimeWheelPicker.web.tsx +401 -0
- package/src/components/WheelPicker/WheelPicker.styles.ts +12 -8
- package/src/components/WheelPicker/WheelPicker.tsx +24 -2
- package/src/components/WheelPicker/WheelPicker.web.tsx +153 -57
- package/src/context/ThemeOverrideContext.tsx +38 -0
- package/src/index.ts +11 -0
- package/src/types/datepicker.ts +44 -0
- package/src/types/timepicker.ts +74 -0
- package/src/utils/validation.ts +111 -55
- package/lib/module/unistyles.js +0 -9
- package/lib/module/unistyles.js.map +0 -1
- package/lib/typescript/unistyles.d.ts +0 -3
- package/lib/typescript/unistyles.d.ts.map +0 -1
- package/src/unistyles.ts +0 -6
|
@@ -1,35 +1,31 @@
|
|
|
1
|
-
import { Platform } from 'react-native';
|
|
2
|
-
import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
|
|
1
|
+
import { Platform, StyleSheet } from 'react-native';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Default styles for DatePicker using plain React Native StyleSheet.
|
|
5
|
+
* Colors use dark theme defaults - consumers override via themeOverrides prop.
|
|
6
|
+
*/
|
|
7
|
+
export const styles = StyleSheet.create({
|
|
5
8
|
container: {
|
|
6
|
-
backgroundColor:
|
|
7
|
-
borderRadius:
|
|
9
|
+
backgroundColor: '#1E1E1E',
|
|
10
|
+
borderRadius: 16,
|
|
8
11
|
overflow: 'hidden',
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
large: {
|
|
18
|
-
padding: theme.spacing(6),
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
},
|
|
12
|
+
padding: 16,
|
|
13
|
+
},
|
|
14
|
+
containerCompact: {
|
|
15
|
+
padding: 8,
|
|
16
|
+
},
|
|
17
|
+
containerLarge: {
|
|
18
|
+
padding: 24,
|
|
22
19
|
},
|
|
23
20
|
backdrop: Platform.select({
|
|
24
21
|
web: {
|
|
22
|
+
// @ts-ignore - web-only properties
|
|
25
23
|
backdropFilter: 'blur(20px) saturate(180%)',
|
|
26
24
|
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
|
27
25
|
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
|
28
26
|
},
|
|
29
27
|
default: {
|
|
30
|
-
backgroundColor:
|
|
28
|
+
backgroundColor: '#2C2C2E',
|
|
31
29
|
},
|
|
32
30
|
}),
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
export type DatePickerVariants = UnistylesVariants<typeof styles>;
|
|
31
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import type { DatePickerMode } from '../../types';
|
|
2
|
+
import type { DatePickerMode, DatePickerThemeOverrides } from '../../types';
|
|
3
3
|
|
|
4
4
|
export interface KaalDatePickerProps {
|
|
5
5
|
value: Date;
|
|
@@ -11,6 +11,17 @@ export interface KaalDatePickerProps {
|
|
|
11
11
|
maxDate?: Date;
|
|
12
12
|
disabledDates?: Date[];
|
|
13
13
|
locale?: string;
|
|
14
|
+
/**
|
|
15
|
+
* First day of the week: 0 = Sunday, 1 = Monday
|
|
16
|
+
* @default 0 (Sunday)
|
|
17
|
+
*
|
|
18
|
+
* TODO: This is a temporary solution. In the future, we need to add full
|
|
19
|
+
* locale support to handle different calendar formats, layouts, and
|
|
20
|
+
* localized day/month names across different regions.
|
|
21
|
+
*/
|
|
22
|
+
weekStartsOn?: 0 | 1;
|
|
23
|
+
/** Custom theme overrides for styling without matching Kaal's theme structure */
|
|
24
|
+
themeOverrides?: DatePickerThemeOverrides;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
// Platform-specific implementations are handled by Metro's file resolution
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
+
import { ThemeOverrideProvider } from '../../context/ThemeOverrideContext';
|
|
2
3
|
import { CalendarGrid } from '../CalendarGrid';
|
|
3
4
|
import { WheelPicker } from '../WheelPicker';
|
|
4
5
|
import type { KaalDatePickerProps } from './DatePicker';
|
|
@@ -11,26 +12,33 @@ export const DatePicker: React.FC<KaalDatePickerProps> = ({
|
|
|
11
12
|
minDate,
|
|
12
13
|
maxDate,
|
|
13
14
|
disabledDates,
|
|
15
|
+
themeOverrides,
|
|
16
|
+
weekStartsOn = 0,
|
|
14
17
|
}) => {
|
|
15
18
|
if (theme === 'ios' && variant === 'wheel') {
|
|
16
19
|
return (
|
|
17
|
-
<
|
|
20
|
+
<ThemeOverrideProvider value={{ datePicker: themeOverrides }}>
|
|
21
|
+
<WheelPicker
|
|
22
|
+
value={value}
|
|
23
|
+
onChange={onChange}
|
|
24
|
+
minDate={minDate}
|
|
25
|
+
maxDate={maxDate}
|
|
26
|
+
/>
|
|
27
|
+
</ThemeOverrideProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ThemeOverrideProvider value={{ datePicker: themeOverrides }}>
|
|
33
|
+
<CalendarGrid
|
|
18
34
|
value={value}
|
|
19
35
|
onChange={onChange}
|
|
20
36
|
minDate={minDate}
|
|
21
37
|
maxDate={maxDate}
|
|
38
|
+
disabledDates={disabledDates}
|
|
39
|
+
themeMode={theme === 'native' ? 'ios' : theme}
|
|
40
|
+
weekStartsOn={weekStartsOn}
|
|
22
41
|
/>
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<CalendarGrid
|
|
28
|
-
value={value}
|
|
29
|
-
onChange={onChange}
|
|
30
|
-
minDate={minDate}
|
|
31
|
-
maxDate={maxDate}
|
|
32
|
-
disabledDates={disabledDates}
|
|
33
|
-
themeMode={theme === 'native' ? 'ios' : theme}
|
|
34
|
-
/>
|
|
42
|
+
</ThemeOverrideProvider>
|
|
35
43
|
);
|
|
36
44
|
};
|
|
@@ -4,8 +4,9 @@ import { View } from 'react-native';
|
|
|
4
4
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
5
5
|
import { runOnJS } from 'react-native-reanimated';
|
|
6
6
|
import Svg, { Circle, G, Line, Text as SvgText } from 'react-native-svg';
|
|
7
|
+
import { useTimePickerOverrides } from '../../context/ThemeOverrideContext';
|
|
7
8
|
import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
|
|
8
|
-
import type { ClockMode,
|
|
9
|
+
import type { ClockMode, TimeValue } from '../../types/timepicker';
|
|
9
10
|
import { styles } from './TimePicker.styles';
|
|
10
11
|
|
|
11
12
|
// Clock dimensions
|
|
@@ -19,6 +20,15 @@ const CENTER_DOT_RADIUS = 4;
|
|
|
19
20
|
const HOURS_12 = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
20
21
|
const MINUTE_LABELS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
21
22
|
|
|
23
|
+
// Default colors (dark theme)
|
|
24
|
+
const DEFAULT_COLORS = {
|
|
25
|
+
clockBackground: '#3F384C',
|
|
26
|
+
handColor: '#4DA6FF',
|
|
27
|
+
selectionDotColor: '#4DA6FF',
|
|
28
|
+
textColor: '#E6E1E5',
|
|
29
|
+
textSelectedColor: '#FFFFFF',
|
|
30
|
+
};
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Converts clock position (0-11 for hours, 0-59 for minutes) to angle in degrees
|
|
24
34
|
* 12 o'clock is at -90 degrees (top)
|
|
@@ -54,8 +64,24 @@ interface ClockFaceProps {
|
|
|
54
64
|
|
|
55
65
|
export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
56
66
|
({ value, onChange, mode, onModeChange, is24Hour = false }) => {
|
|
67
|
+
const overrides = useTimePickerOverrides();
|
|
57
68
|
const { hour: hour12, period } = to12Hour(value.hours);
|
|
58
69
|
|
|
70
|
+
// Build colors from overrides
|
|
71
|
+
const colors = useMemo(
|
|
72
|
+
() => ({
|
|
73
|
+
clockBackground:
|
|
74
|
+
overrides?.clockBackground ?? DEFAULT_COLORS.clockBackground,
|
|
75
|
+
handColor: overrides?.clockHandColor ?? DEFAULT_COLORS.handColor,
|
|
76
|
+
selectionDotColor:
|
|
77
|
+
overrides?.clockSelectionColor ?? DEFAULT_COLORS.selectionDotColor,
|
|
78
|
+
textColor: overrides?.clockTextColor ?? DEFAULT_COLORS.textColor,
|
|
79
|
+
textSelectedColor:
|
|
80
|
+
overrides?.clockTextSelectedColor ?? DEFAULT_COLORS.textSelectedColor,
|
|
81
|
+
}),
|
|
82
|
+
[overrides],
|
|
83
|
+
);
|
|
84
|
+
|
|
59
85
|
// Calculate hand end position based on current value
|
|
60
86
|
const handAngle = useMemo(() => {
|
|
61
87
|
if (mode === 'hours') {
|
|
@@ -149,7 +175,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
|
149
175
|
alignmentBaseline="central"
|
|
150
176
|
fontSize={14}
|
|
151
177
|
fontWeight={isSelected ? '500' : '400'}
|
|
152
|
-
fill={isSelected ?
|
|
178
|
+
fill={isSelected ? colors.textSelectedColor : colors.textColor}
|
|
153
179
|
>
|
|
154
180
|
{hour}
|
|
155
181
|
</SvgText>
|
|
@@ -171,13 +197,13 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
|
171
197
|
alignmentBaseline="central"
|
|
172
198
|
fontSize={14}
|
|
173
199
|
fontWeight={isSelected ? '500' : '400'}
|
|
174
|
-
fill={isSelected ?
|
|
200
|
+
fill={isSelected ? colors.textSelectedColor : colors.textColor}
|
|
175
201
|
>
|
|
176
202
|
{minute.toString().padStart(2, '0')}
|
|
177
203
|
</SvgText>
|
|
178
204
|
);
|
|
179
205
|
});
|
|
180
|
-
}, [mode, hour12, value.minutes]);
|
|
206
|
+
}, [mode, hour12, value.minutes, colors]);
|
|
181
207
|
|
|
182
208
|
return (
|
|
183
209
|
<View style={styles.clockContainer}>
|
|
@@ -192,7 +218,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
|
192
218
|
cx={CLOCK_CENTER}
|
|
193
219
|
cy={CLOCK_CENTER}
|
|
194
220
|
r={CLOCK_SIZE / 2 - 4}
|
|
195
|
-
fill=
|
|
221
|
+
fill={colors.clockBackground}
|
|
196
222
|
/>
|
|
197
223
|
|
|
198
224
|
{/* Selection dot (behind numbers) */}
|
|
@@ -200,7 +226,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
|
200
226
|
cx={handEndPos.x}
|
|
201
227
|
cy={handEndPos.y}
|
|
202
228
|
r={SELECTION_DOT_RADIUS}
|
|
203
|
-
fill=
|
|
229
|
+
fill={colors.selectionDotColor}
|
|
204
230
|
/>
|
|
205
231
|
|
|
206
232
|
{/* Clock hand */}
|
|
@@ -209,7 +235,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
|
209
235
|
y1={CLOCK_CENTER}
|
|
210
236
|
x2={handEndPos.x}
|
|
211
237
|
y2={handEndPos.y}
|
|
212
|
-
stroke=
|
|
238
|
+
stroke={colors.handColor}
|
|
213
239
|
strokeWidth={2}
|
|
214
240
|
/>
|
|
215
241
|
|
|
@@ -218,7 +244,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
|
218
244
|
cx={CLOCK_CENTER}
|
|
219
245
|
cy={CLOCK_CENTER}
|
|
220
246
|
r={CENTER_DOT_RADIUS}
|
|
221
|
-
fill=
|
|
247
|
+
fill={colors.handColor}
|
|
222
248
|
/>
|
|
223
249
|
|
|
224
250
|
{/* Numbers */}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import { memo, useCallback, useMemo, useRef } from 'react';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import { useTimePickerOverrides } from '../../context/ThemeOverrideContext';
|
|
6
|
+
import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
|
|
7
|
+
import type { ClockMode, TimeValue } from '../../types/timepicker';
|
|
8
|
+
import { styles } from './TimePicker.styles';
|
|
9
|
+
|
|
10
|
+
// Clock dimensions
|
|
11
|
+
const CLOCK_SIZE = 256;
|
|
12
|
+
const CLOCK_CENTER = CLOCK_SIZE / 2;
|
|
13
|
+
const OUTER_RADIUS = 96;
|
|
14
|
+
const SELECTION_DOT_RADIUS = 20;
|
|
15
|
+
const CENTER_DOT_RADIUS = 4;
|
|
16
|
+
|
|
17
|
+
// Hour positions (12 at top, clockwise)
|
|
18
|
+
const HOURS_12 = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
19
|
+
const MINUTE_LABELS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
20
|
+
|
|
21
|
+
// Default colors (dark theme)
|
|
22
|
+
const DEFAULT_COLORS = {
|
|
23
|
+
clockBackground: '#3F384C',
|
|
24
|
+
handColor: '#4DA6FF',
|
|
25
|
+
selectionDotColor: '#4DA6FF',
|
|
26
|
+
textColor: '#E6E1E5',
|
|
27
|
+
textSelectedColor: '#FFFFFF',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Converts clock position (0-11 for hours, 0-59 for minutes) to angle in degrees
|
|
32
|
+
* 12 o'clock is at -90 degrees (top)
|
|
33
|
+
*/
|
|
34
|
+
const positionToAngle = (position: number, isMinutes = false): number => {
|
|
35
|
+
if (isMinutes) {
|
|
36
|
+
return position * 6 - 90; // 360/60 = 6 degrees per minute
|
|
37
|
+
}
|
|
38
|
+
return position * 30 - 90; // 360/12 = 30 degrees per position
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets x,y coordinates on circle from angle and radius
|
|
43
|
+
*/
|
|
44
|
+
const getPointOnCircle = (
|
|
45
|
+
angleDegrees: number,
|
|
46
|
+
radius: number,
|
|
47
|
+
): { x: number; y: number } => {
|
|
48
|
+
const rad = (angleDegrees * Math.PI) / 180;
|
|
49
|
+
return {
|
|
50
|
+
x: CLOCK_CENTER + radius * Math.cos(rad),
|
|
51
|
+
y: CLOCK_CENTER + radius * Math.sin(rad),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
interface ClockFaceProps {
|
|
56
|
+
value: TimeValue;
|
|
57
|
+
onChange: (time: TimeValue) => void;
|
|
58
|
+
mode: ClockMode;
|
|
59
|
+
onModeChange?: (mode: ClockMode) => void;
|
|
60
|
+
is24Hour?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const ClockFace: React.FC<ClockFaceProps> = memo(
|
|
64
|
+
({ value, onChange, mode, onModeChange, is24Hour = false }) => {
|
|
65
|
+
const overrides = useTimePickerOverrides();
|
|
66
|
+
const svgRef = useRef<SVGSVGElement>(null);
|
|
67
|
+
const isDragging = useRef(false);
|
|
68
|
+
const { hour: hour12, period } = to12Hour(value.hours);
|
|
69
|
+
|
|
70
|
+
// Build colors from overrides
|
|
71
|
+
const colors = useMemo(
|
|
72
|
+
() => ({
|
|
73
|
+
clockBackground:
|
|
74
|
+
overrides?.clockBackground ?? DEFAULT_COLORS.clockBackground,
|
|
75
|
+
handColor: overrides?.clockHandColor ?? DEFAULT_COLORS.handColor,
|
|
76
|
+
selectionDotColor:
|
|
77
|
+
overrides?.clockSelectionColor ?? DEFAULT_COLORS.selectionDotColor,
|
|
78
|
+
textColor: overrides?.clockTextColor ?? DEFAULT_COLORS.textColor,
|
|
79
|
+
textSelectedColor:
|
|
80
|
+
overrides?.clockTextSelectedColor ?? DEFAULT_COLORS.textSelectedColor,
|
|
81
|
+
}),
|
|
82
|
+
[overrides],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Calculate hand end position based on current value
|
|
86
|
+
const handAngle = useMemo(() => {
|
|
87
|
+
if (mode === 'hours') {
|
|
88
|
+
const hourIndex = HOURS_12.indexOf(hour12);
|
|
89
|
+
return positionToAngle(hourIndex >= 0 ? hourIndex : 0);
|
|
90
|
+
}
|
|
91
|
+
return positionToAngle(value.minutes, true);
|
|
92
|
+
}, [mode, hour12, value.minutes]);
|
|
93
|
+
|
|
94
|
+
const handEndPos = getPointOnCircle(handAngle, OUTER_RADIUS);
|
|
95
|
+
|
|
96
|
+
const handleInteraction = useCallback(
|
|
97
|
+
(clientX: number, clientY: number) => {
|
|
98
|
+
if (!svgRef.current) return;
|
|
99
|
+
|
|
100
|
+
const rect = svgRef.current.getBoundingClientRect();
|
|
101
|
+
const x = clientX - rect.left;
|
|
102
|
+
const y = clientY - rect.top;
|
|
103
|
+
|
|
104
|
+
// Convert touch coordinates to angle
|
|
105
|
+
const dx = x - CLOCK_CENTER;
|
|
106
|
+
const dy = y - CLOCK_CENTER;
|
|
107
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
108
|
+
|
|
109
|
+
// Normalize angle to 0-360, with 0 at top (12 o'clock)
|
|
110
|
+
const normalized = (((angle + 90) % 360) + 360) % 360;
|
|
111
|
+
|
|
112
|
+
if (mode === 'hours') {
|
|
113
|
+
// Round to nearest hour position
|
|
114
|
+
const position = Math.round(normalized / 30) % 12;
|
|
115
|
+
const hour = HOURS_12[position];
|
|
116
|
+
if (hour !== undefined) {
|
|
117
|
+
const newHours = to24Hour(hour, period);
|
|
118
|
+
onChange({ hours: newHours, minutes: value.minutes });
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// Round to nearest minute
|
|
122
|
+
const minutes = Math.round(normalized / 6) % 60;
|
|
123
|
+
onChange({ hours: value.hours, minutes });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[mode, period, value, onChange],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const handleInteractionEnd = useCallback(() => {
|
|
130
|
+
// Switch to minutes mode after selecting hours
|
|
131
|
+
if (mode === 'hours' && onModeChange) {
|
|
132
|
+
// Small delay for visual feedback
|
|
133
|
+
setTimeout(() => onModeChange('minutes'), 200);
|
|
134
|
+
}
|
|
135
|
+
}, [mode, onModeChange]);
|
|
136
|
+
|
|
137
|
+
const handleMouseDown = useCallback(
|
|
138
|
+
(e: React.MouseEvent<SVGSVGElement>) => {
|
|
139
|
+
isDragging.current = true;
|
|
140
|
+
handleInteraction(e.clientX, e.clientY);
|
|
141
|
+
},
|
|
142
|
+
[handleInteraction],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const handleMouseMove = useCallback(
|
|
146
|
+
(e: React.MouseEvent<SVGSVGElement>) => {
|
|
147
|
+
if (!isDragging.current) return;
|
|
148
|
+
handleInteraction(e.clientX, e.clientY);
|
|
149
|
+
},
|
|
150
|
+
[handleInteraction],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const handleMouseUp = useCallback(() => {
|
|
154
|
+
if (isDragging.current) {
|
|
155
|
+
isDragging.current = false;
|
|
156
|
+
handleInteractionEnd();
|
|
157
|
+
}
|
|
158
|
+
}, [handleInteractionEnd]);
|
|
159
|
+
|
|
160
|
+
const handleTouchStart = useCallback(
|
|
161
|
+
(e: React.TouchEvent<SVGSVGElement>) => {
|
|
162
|
+
isDragging.current = true;
|
|
163
|
+
const touch = e.touches[0];
|
|
164
|
+
if (touch) {
|
|
165
|
+
handleInteraction(touch.clientX, touch.clientY);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
[handleInteraction],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const handleTouchMove = useCallback(
|
|
172
|
+
(e: React.TouchEvent<SVGSVGElement>) => {
|
|
173
|
+
if (!isDragging.current) return;
|
|
174
|
+
const touch = e.touches[0];
|
|
175
|
+
if (touch) {
|
|
176
|
+
handleInteraction(touch.clientX, touch.clientY);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[handleInteraction],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const handleTouchEnd = useCallback(() => {
|
|
183
|
+
if (isDragging.current) {
|
|
184
|
+
isDragging.current = false;
|
|
185
|
+
handleInteractionEnd();
|
|
186
|
+
}
|
|
187
|
+
}, [handleInteractionEnd]);
|
|
188
|
+
|
|
189
|
+
// Render clock numbers
|
|
190
|
+
const numbers = useMemo(() => {
|
|
191
|
+
if (mode === 'hours') {
|
|
192
|
+
return HOURS_12.map((hour, index) => {
|
|
193
|
+
const angle = positionToAngle(index);
|
|
194
|
+
const pos = getPointOnCircle(angle, OUTER_RADIUS);
|
|
195
|
+
const isSelected = hour12 === hour;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<text
|
|
199
|
+
key={hour}
|
|
200
|
+
x={pos.x}
|
|
201
|
+
y={pos.y}
|
|
202
|
+
textAnchor="middle"
|
|
203
|
+
dominantBaseline="central"
|
|
204
|
+
fontSize={14}
|
|
205
|
+
fontWeight={isSelected ? '500' : '400'}
|
|
206
|
+
fill={isSelected ? colors.textSelectedColor : colors.textColor}
|
|
207
|
+
style={{ userSelect: 'none' }}
|
|
208
|
+
>
|
|
209
|
+
{hour}
|
|
210
|
+
</text>
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return MINUTE_LABELS.map((minute, index) => {
|
|
216
|
+
const angle = positionToAngle(index);
|
|
217
|
+
const pos = getPointOnCircle(angle, OUTER_RADIUS);
|
|
218
|
+
const isSelected = value.minutes === minute;
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<text
|
|
222
|
+
key={minute}
|
|
223
|
+
x={pos.x}
|
|
224
|
+
y={pos.y}
|
|
225
|
+
textAnchor="middle"
|
|
226
|
+
dominantBaseline="central"
|
|
227
|
+
fontSize={14}
|
|
228
|
+
fontWeight={isSelected ? '500' : '400'}
|
|
229
|
+
fill={isSelected ? colors.textSelectedColor : colors.textColor}
|
|
230
|
+
style={{ userSelect: 'none' }}
|
|
231
|
+
>
|
|
232
|
+
{minute.toString().padStart(2, '0')}
|
|
233
|
+
</text>
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
}, [mode, hour12, value.minutes, colors]);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<View style={styles.clockContainer}>
|
|
240
|
+
<svg
|
|
241
|
+
ref={svgRef}
|
|
242
|
+
width={CLOCK_SIZE}
|
|
243
|
+
height={CLOCK_SIZE}
|
|
244
|
+
viewBox={`0 0 ${CLOCK_SIZE} ${CLOCK_SIZE}`}
|
|
245
|
+
onMouseDown={handleMouseDown}
|
|
246
|
+
onMouseMove={handleMouseMove}
|
|
247
|
+
onMouseUp={handleMouseUp}
|
|
248
|
+
onMouseLeave={handleMouseUp}
|
|
249
|
+
onTouchStart={handleTouchStart}
|
|
250
|
+
onTouchMove={handleTouchMove}
|
|
251
|
+
onTouchEnd={handleTouchEnd}
|
|
252
|
+
style={{ cursor: 'pointer', touchAction: 'none' }}
|
|
253
|
+
role="img"
|
|
254
|
+
aria-label={`Clock face for selecting ${mode === 'hours' ? 'hours' : 'minutes'}`}
|
|
255
|
+
>
|
|
256
|
+
<title>
|
|
257
|
+
{mode === 'hours'
|
|
258
|
+
? 'Hour selection clock'
|
|
259
|
+
: 'Minute selection clock'}
|
|
260
|
+
</title>
|
|
261
|
+
{/* Background circle */}
|
|
262
|
+
<circle
|
|
263
|
+
cx={CLOCK_CENTER}
|
|
264
|
+
cy={CLOCK_CENTER}
|
|
265
|
+
r={CLOCK_SIZE / 2 - 4}
|
|
266
|
+
fill={colors.clockBackground}
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
{/* Selection dot (behind numbers) */}
|
|
270
|
+
<circle
|
|
271
|
+
cx={handEndPos.x}
|
|
272
|
+
cy={handEndPos.y}
|
|
273
|
+
r={SELECTION_DOT_RADIUS}
|
|
274
|
+
fill={colors.selectionDotColor}
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
{/* Clock hand */}
|
|
278
|
+
<line
|
|
279
|
+
x1={CLOCK_CENTER}
|
|
280
|
+
y1={CLOCK_CENTER}
|
|
281
|
+
x2={handEndPos.x}
|
|
282
|
+
y2={handEndPos.y}
|
|
283
|
+
stroke={colors.handColor}
|
|
284
|
+
strokeWidth={2}
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
{/* Center dot */}
|
|
288
|
+
<circle
|
|
289
|
+
cx={CLOCK_CENTER}
|
|
290
|
+
cy={CLOCK_CENTER}
|
|
291
|
+
r={CENTER_DOT_RADIUS}
|
|
292
|
+
fill={colors.handColor}
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
{/* Numbers */}
|
|
296
|
+
<g>{numbers}</g>
|
|
297
|
+
</svg>
|
|
298
|
+
</View>
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
ClockFace.displayName = 'ClockFace';
|