@elik56a/react-native-pickers 0.0.0-stage → 0.1.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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -2
  3. package/lib/date/Calendar.d.ts +89 -0
  4. package/lib/date/Calendar.d.ts.map +1 -0
  5. package/lib/date/Calendar.js +250 -0
  6. package/lib/date/Calendar.js.map +1 -0
  7. package/lib/date/DatePicker.d.ts +14 -0
  8. package/lib/date/DatePicker.d.ts.map +1 -0
  9. package/lib/date/DatePicker.js +17 -0
  10. package/lib/date/DatePicker.js.map +1 -0
  11. package/lib/date/DateWheelPicker.d.ts +29 -0
  12. package/lib/date/DateWheelPicker.d.ts.map +1 -0
  13. package/lib/date/DateWheelPicker.js +83 -0
  14. package/lib/date/DateWheelPicker.js.map +1 -0
  15. package/lib/emoji/EmojiPicker.d.ts +47 -0
  16. package/lib/emoji/EmojiPicker.d.ts.map +1 -0
  17. package/lib/emoji/EmojiPicker.js +161 -0
  18. package/lib/emoji/EmojiPicker.js.map +1 -0
  19. package/lib/emoji/data.d.ts +2 -0
  20. package/lib/emoji/data.d.ts.map +1 -0
  21. package/lib/emoji/data.js +4 -0
  22. package/lib/emoji/data.js.map +1 -0
  23. package/lib/emoji/emojis.d.ts +23 -0
  24. package/lib/emoji/emojis.d.ts.map +1 -0
  25. package/lib/emoji/emojis.js +100 -0
  26. package/lib/emoji/emojis.js.map +1 -0
  27. package/lib/index.d.ts +17 -0
  28. package/lib/index.d.ts.map +1 -0
  29. package/lib/index.js +17 -0
  30. package/lib/index.js.map +1 -0
  31. package/lib/select/SelectList.d.ts +60 -0
  32. package/lib/select/SelectList.d.ts.map +1 -0
  33. package/lib/select/SelectList.js +91 -0
  34. package/lib/select/SelectList.js.map +1 -0
  35. package/lib/select/SelectPicker.d.ts +35 -0
  36. package/lib/select/SelectPicker.d.ts.map +1 -0
  37. package/lib/select/SelectPicker.js +64 -0
  38. package/lib/select/SelectPicker.js.map +1 -0
  39. package/lib/select/filter.d.ts +3 -0
  40. package/lib/select/filter.d.ts.map +1 -0
  41. package/lib/select/filter.js +13 -0
  42. package/lib/select/filter.js.map +1 -0
  43. package/lib/sheet/BottomSheet.d.ts +29 -0
  44. package/lib/sheet/BottomSheet.d.ts.map +1 -0
  45. package/lib/sheet/BottomSheet.js +89 -0
  46. package/lib/sheet/BottomSheet.js.map +1 -0
  47. package/lib/sheet/PickerSheet.d.ts +26 -0
  48. package/lib/sheet/PickerSheet.d.ts.map +1 -0
  49. package/lib/sheet/PickerSheet.js +27 -0
  50. package/lib/sheet/PickerSheet.js.map +1 -0
  51. package/lib/sheet/sheets.d.ts +48 -0
  52. package/lib/sheet/sheets.d.ts.map +1 -0
  53. package/lib/sheet/sheets.js +58 -0
  54. package/lib/sheet/sheets.js.map +1 -0
  55. package/lib/theme.d.ts +81 -0
  56. package/lib/theme.d.ts.map +1 -0
  57. package/lib/theme.js +96 -0
  58. package/lib/theme.js.map +1 -0
  59. package/lib/time/TimePicker.d.ts +41 -0
  60. package/lib/time/TimePicker.d.ts.map +1 -0
  61. package/lib/time/TimePicker.js +105 -0
  62. package/lib/time/TimePicker.js.map +1 -0
  63. package/lib/types.d.ts +9 -0
  64. package/lib/types.d.ts.map +1 -0
  65. package/lib/types.js +2 -0
  66. package/lib/types.js.map +1 -0
  67. package/lib/utils/OwnScroll.d.ts +10 -0
  68. package/lib/utils/OwnScroll.d.ts.map +1 -0
  69. package/lib/utils/OwnScroll.js +13 -0
  70. package/lib/utils/OwnScroll.js.map +1 -0
  71. package/lib/utils/date.d.ts +30 -0
  72. package/lib/utils/date.d.ts.map +1 -0
  73. package/lib/utils/date.js +115 -0
  74. package/lib/utils/date.js.map +1 -0
  75. package/lib/wheel/MultiWheelPicker.d.ts +26 -0
  76. package/lib/wheel/MultiWheelPicker.d.ts.map +1 -0
  77. package/lib/wheel/MultiWheelPicker.js +28 -0
  78. package/lib/wheel/MultiWheelPicker.js.map +1 -0
  79. package/lib/wheel/WheelPicker.d.ts +59 -0
  80. package/lib/wheel/WheelPicker.d.ts.map +1 -0
  81. package/lib/wheel/WheelPicker.js +257 -0
  82. package/lib/wheel/WheelPicker.js.map +1 -0
  83. package/lib/wheel/typeToSelect.d.ts +8 -0
  84. package/lib/wheel/typeToSelect.d.ts.map +1 -0
  85. package/lib/wheel/typeToSelect.js +10 -0
  86. package/lib/wheel/typeToSelect.js.map +1 -0
  87. package/package.json +81 -4
  88. package/src/date/Calendar.tsx +586 -0
  89. package/src/date/DatePicker.tsx +22 -0
  90. package/src/date/DateWheelPicker.tsx +149 -0
  91. package/src/emoji/EmojiPicker.tsx +410 -0
  92. package/src/emoji/data.ts +3 -0
  93. package/src/emoji/emojis.ts +127 -0
  94. package/src/index.ts +16 -0
  95. package/src/select/SelectList.tsx +266 -0
  96. package/src/select/SelectPicker.tsx +203 -0
  97. package/src/select/filter.ts +17 -0
  98. package/src/sheet/BottomSheet.tsx +199 -0
  99. package/src/sheet/PickerSheet.tsx +86 -0
  100. package/src/sheet/sheets.tsx +218 -0
  101. package/src/theme.tsx +160 -0
  102. package/src/time/TimePicker.tsx +199 -0
  103. package/src/types.ts +10 -0
  104. package/src/utils/OwnScroll.tsx +14 -0
  105. package/src/utils/date.ts +131 -0
  106. package/src/wheel/MultiWheelPicker.tsx +96 -0
  107. package/src/wheel/WheelPicker.tsx +534 -0
  108. package/src/wheel/typeToSelect.ts +12 -0
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Pressable, Text } from 'react-native';
3
+ import { BottomSheet } from './BottomSheet';
4
+ import type { BottomSheetProps } from './BottomSheet';
5
+ import { textStyle, usePickerTheme } from '../theme';
6
+ import type { ThemableProps } from '../types';
7
+
8
+ export interface SheetChromeProps extends ThemableProps {
9
+ visible: boolean;
10
+ onClose: () => void;
11
+ title?: React.ReactNode;
12
+ /** Default: 'Done' */
13
+ confirmLabel?: string;
14
+ /** Default: 'Cancel' */
15
+ cancelLabel?: string;
16
+ /** Show the cancel button. Default: true */
17
+ showCancel?: boolean;
18
+ /** Passed through to the underlying BottomSheet */
19
+ sheetProps?: Partial<Omit<BottomSheetProps, 'visible' | 'onClose' | 'children'>>;
20
+ }
21
+
22
+ export interface PickerSheetProps extends SheetChromeProps {
23
+ onConfirm?: () => void;
24
+ confirmDisabled?: boolean;
25
+ children: React.ReactNode;
26
+ }
27
+
28
+ /** BottomSheet with Cancel / Done header buttons. */
29
+ export function PickerSheet({
30
+ visible,
31
+ onClose,
32
+ onConfirm,
33
+ confirmDisabled,
34
+ title,
35
+ confirmLabel = 'Done',
36
+ cancelLabel = 'Cancel',
37
+ showCancel = true,
38
+ sheetProps,
39
+ theme: themeOverride,
40
+ children,
41
+ }: PickerSheetProps) {
42
+ const theme = usePickerTheme(themeOverride);
43
+ const btn = (label: string, onPress: () => void, primary: boolean, disabled?: boolean) => (
44
+ <Pressable
45
+ onPress={disabled ? undefined : onPress}
46
+ hitSlop={10}
47
+ accessibilityRole="button"
48
+ accessibilityState={{ disabled }}
49
+ style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1, paddingVertical: 6 })}
50
+ >
51
+ <Text
52
+ style={[
53
+ textStyle(theme, 'md', primary ? 'bold' : 'regular'),
54
+ { color: disabled ? theme.colors.textDisabled : primary ? theme.colors.primary : theme.colors.textSecondary },
55
+ ]}
56
+ >
57
+ {label}
58
+ </Text>
59
+ </Pressable>
60
+ );
61
+ return (
62
+ <BottomSheet
63
+ visible={visible}
64
+ onClose={onClose}
65
+ title={title}
66
+ theme={themeOverride}
67
+ headerLeft={showCancel ? btn(cancelLabel, onClose, false) : undefined}
68
+ headerRight={onConfirm ? btn(confirmLabel, onConfirm, true, confirmDisabled) : undefined}
69
+ {...sheetProps}
70
+ >
71
+ {children}
72
+ </BottomSheet>
73
+ );
74
+ }
75
+
76
+ /** Draft state that resets to `value` every time the sheet opens. */
77
+ export function useDraft<V>(value: V, visible: boolean): [V, (v: V) => void] {
78
+ const [draft, setDraft] = useState(value);
79
+ const wasVisible = useRef(visible);
80
+ useEffect(() => {
81
+ if (visible && !wasVisible.current) setDraft(value);
82
+ wasVisible.current = visible;
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ }, [visible]);
85
+ return [draft, setDraft];
86
+ }
@@ -0,0 +1,218 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { PickerSheet, useDraft } from './PickerSheet';
4
+ import type { SheetChromeProps } from './PickerSheet';
5
+ import { Calendar } from '../date/Calendar';
6
+ import type { CalendarBaseProps, DateRange } from '../date/Calendar';
7
+ import { DateWheelPicker } from '../date/DateWheelPicker';
8
+ import type { DateWheelPickerProps } from '../date/DateWheelPicker';
9
+ import { TimePicker } from '../time/TimePicker';
10
+ import type { TimePickerProps } from '../time/TimePicker';
11
+ import { WheelPicker } from '../wheel/WheelPicker';
12
+ import { EmojiPicker } from '../emoji/EmojiPicker';
13
+ import type { EmojiPickerProps } from '../emoji/EmojiPicker';
14
+ import type { WheelPickerProps } from '../wheel/WheelPicker';
15
+
16
+ const pad = (children: React.ReactNode, h = 16) => <View style={{ paddingHorizontal: h, paddingBottom: 8 }}>{children}</View>;
17
+
18
+ // ---------- Date ----------
19
+
20
+ type DateSheetSelection =
21
+ | { mode?: 'single'; value?: Date | null; onConfirm: (date: Date) => void }
22
+ | { mode: 'range'; value?: DateRange; onConfirm: (range: DateRange) => void; minRangeDays?: number; maxRangeDays?: number }
23
+ | { mode: 'multiple'; value?: Date[]; onConfirm: (dates: Date[]) => void; maxSelections?: number };
24
+
25
+ export type DatePickerSheetProps = SheetChromeProps &
26
+ Omit<CalendarBaseProps, 'theme'> &
27
+ DateSheetSelection & {
28
+ /** 'calendar' (default) or 'wheel' (single mode only) */
29
+ display?: 'calendar' | 'wheel';
30
+ wheelProps?: Partial<DateWheelPickerProps>;
31
+ /** Single mode: confirm as soon as a day is tapped. Default: false */
32
+ confirmOnSelect?: boolean;
33
+ };
34
+
35
+ export function DatePickerSheet(props: DatePickerSheetProps) {
36
+ const {
37
+ visible,
38
+ onClose,
39
+ title,
40
+ confirmLabel,
41
+ cancelLabel,
42
+ showCancel,
43
+ sheetProps,
44
+ theme,
45
+ display = 'calendar',
46
+ wheelProps,
47
+ confirmOnSelect,
48
+ mode = 'single',
49
+ value,
50
+ onConfirm,
51
+ ...calendarProps
52
+ } = props as DatePickerSheetProps & { value?: unknown; onConfirm: (v: unknown) => void };
53
+ const empty = mode === 'range' ? { start: null, end: null } : mode === 'multiple' ? [] : null;
54
+ const [draft, setDraft] = useDraft<unknown>(value ?? (display === 'wheel' ? new Date() : empty), visible);
55
+
56
+ const ready =
57
+ mode === 'single' ? draft instanceof Date : mode === 'range' ? !!(draft as DateRange).start && !!(draft as DateRange).end : true;
58
+ const confirm = () => {
59
+ if (!ready) return;
60
+ onConfirm(draft);
61
+ onClose();
62
+ };
63
+
64
+ return (
65
+ <PickerSheet
66
+ visible={visible}
67
+ onClose={onClose}
68
+ title={title}
69
+ confirmLabel={confirmLabel}
70
+ cancelLabel={cancelLabel}
71
+ showCancel={showCancel}
72
+ sheetProps={sheetProps}
73
+ theme={theme}
74
+ onConfirm={confirmOnSelect && mode === 'single' && display === 'calendar' ? undefined : confirm}
75
+ confirmDisabled={!ready}
76
+ >
77
+ {display === 'wheel'
78
+ ? pad(
79
+ <DateWheelPicker
80
+ {...wheelProps}
81
+ locale={calendarProps.locale}
82
+ minDate={calendarProps.minDate}
83
+ maxDate={calendarProps.maxDate}
84
+ theme={theme}
85
+ value={draft as Date}
86
+ onChange={setDraft}
87
+ />
88
+ )
89
+ : pad(
90
+ <Calendar
91
+ {...(calendarProps as CalendarBaseProps)}
92
+ theme={theme}
93
+ {...({
94
+ mode,
95
+ value: draft,
96
+ onChange: (v: unknown) => {
97
+ setDraft(v);
98
+ if (confirmOnSelect && mode === 'single') {
99
+ onConfirm(v);
100
+ onClose();
101
+ }
102
+ },
103
+ } as object)}
104
+ />,
105
+ 8
106
+ )}
107
+ </PickerSheet>
108
+ );
109
+ }
110
+
111
+ // ---------- Time ----------
112
+
113
+ export type TimePickerSheetProps = SheetChromeProps &
114
+ Omit<TimePickerProps, 'value' | 'defaultValue' | 'onChange' | 'theme'> & {
115
+ value?: Date;
116
+ onConfirm: (date: Date) => void;
117
+ };
118
+
119
+ export function TimePickerSheet({
120
+ visible,
121
+ onClose,
122
+ title,
123
+ confirmLabel,
124
+ cancelLabel,
125
+ showCancel,
126
+ sheetProps,
127
+ theme,
128
+ value,
129
+ onConfirm,
130
+ ...rest
131
+ }: TimePickerSheetProps) {
132
+ const [draft, setDraft] = useDraft(value ?? new Date(), visible);
133
+ return (
134
+ <PickerSheet
135
+ {...{ visible, onClose, title, confirmLabel, cancelLabel, showCancel, sheetProps, theme }}
136
+ onConfirm={() => {
137
+ onConfirm(draft);
138
+ onClose();
139
+ }}
140
+ >
141
+ {pad(<TimePicker {...rest} theme={theme} value={draft} onChange={setDraft} />)}
142
+ </PickerSheet>
143
+ );
144
+ }
145
+
146
+ // ---------- Generic wheel ----------
147
+
148
+ export type WheelPickerSheetProps<T> = SheetChromeProps &
149
+ Omit<WheelPickerProps<T>, 'value' | 'defaultValue' | 'onChange' | 'theme'> & {
150
+ value?: T;
151
+ onConfirm: (value: T) => void;
152
+ };
153
+
154
+ export function WheelPickerSheet<T>({
155
+ visible,
156
+ onClose,
157
+ title,
158
+ confirmLabel,
159
+ cancelLabel,
160
+ showCancel,
161
+ sheetProps,
162
+ theme,
163
+ value,
164
+ onConfirm,
165
+ ...rest
166
+ }: WheelPickerSheetProps<T>) {
167
+ const first = rest.items[0] as unknown;
168
+ const firstValue = (first && typeof first === 'object' && 'value' in (first as object) ? (first as { value: T }).value : first) as T;
169
+ const [draft, setDraft] = useDraft<T>(value ?? firstValue, visible);
170
+ return (
171
+ <PickerSheet
172
+ {...{ visible, onClose, title, confirmLabel, cancelLabel, showCancel, sheetProps, theme }}
173
+ onConfirm={() => {
174
+ onConfirm(draft);
175
+ onClose();
176
+ }}
177
+ >
178
+ {pad(<WheelPicker<T> {...rest} theme={theme} value={draft} onChange={setDraft} />)}
179
+ </PickerSheet>
180
+ );
181
+ }
182
+
183
+ // ---------- Emoji ----------
184
+
185
+ export type EmojiPickerSheetProps = SheetChromeProps &
186
+ Omit<EmojiPickerProps, 'theme'> & {
187
+ /** Close after picking an emoji. Default: true */
188
+ closeOnSelect?: boolean;
189
+ };
190
+
191
+ export function EmojiPickerSheet({
192
+ visible,
193
+ onClose,
194
+ title,
195
+ confirmLabel,
196
+ cancelLabel,
197
+ showCancel = false,
198
+ sheetProps,
199
+ theme,
200
+ closeOnSelect = true,
201
+ onSelect,
202
+ height = 420,
203
+ ...rest
204
+ }: EmojiPickerSheetProps) {
205
+ return (
206
+ <PickerSheet {...{ visible, onClose, title, confirmLabel, cancelLabel, showCancel, sheetProps, theme }}>
207
+ <EmojiPicker
208
+ {...rest}
209
+ height={height}
210
+ theme={theme}
211
+ onSelect={(ch, e) => {
212
+ onSelect(ch, e);
213
+ if (closeOnSelect) onClose();
214
+ }}
215
+ />
216
+ </PickerSheet>
217
+ );
218
+ }
package/src/theme.tsx ADDED
@@ -0,0 +1,160 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { useColorScheme } from 'react-native';
3
+ import type { TextStyle } from 'react-native';
4
+
5
+ export interface PickerTheme {
6
+ dark: boolean;
7
+ colors: {
8
+ /** Picker surface background */
9
+ background: string;
10
+ /** Secondary surfaces: search inputs, chips, hovered rows */
11
+ surface: string;
12
+ /** Bottom sheet background */
13
+ sheet: string;
14
+ text: string;
15
+ textSecondary: string;
16
+ textDisabled: string;
17
+ primary: string;
18
+ onPrimary: string;
19
+ /** Soft primary tint: range fill, selected row background */
20
+ primaryMuted: string;
21
+ border: string;
22
+ backdrop: string;
23
+ /** Wheel selection band */
24
+ indicator: string;
25
+ handle: string;
26
+ };
27
+ radius: { sm: number; md: number; lg: number; full: number; sheet: number };
28
+ spacing: { xs: number; sm: number; md: number; lg: number; xl: number };
29
+ typography: {
30
+ fontFamily?: string;
31
+ fontFamilyMedium?: string;
32
+ sizes: { xs: number; sm: number; md: number; lg: number; xl: number };
33
+ weights: { regular: TextStyle['fontWeight']; medium: TextStyle['fontWeight']; bold: TextStyle['fontWeight'] };
34
+ };
35
+ animation: { duration: number };
36
+ }
37
+
38
+ const shared = {
39
+ radius: { sm: 8, md: 12, lg: 16, full: 999, sheet: 24 },
40
+ spacing: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
41
+ typography: {
42
+ sizes: { xs: 11, sm: 13, md: 16, lg: 19, xl: 22 },
43
+ weights: { regular: '400', medium: '500', bold: '600' } as PickerTheme['typography']['weights'],
44
+ },
45
+ animation: { duration: 240 },
46
+ };
47
+
48
+ export const lightTheme: PickerTheme = {
49
+ dark: false,
50
+ colors: {
51
+ background: '#FFFFFF',
52
+ surface: '#F2F3F7',
53
+ sheet: '#FFFFFF',
54
+ text: '#14151A',
55
+ textSecondary: '#6B6F7B',
56
+ textDisabled: '#B9BCC6',
57
+ primary: '#4F46E5',
58
+ onPrimary: '#FFFFFF',
59
+ primaryMuted: 'rgba(79,70,229,0.12)',
60
+ border: '#E6E7EC',
61
+ backdrop: 'rgba(10,11,16,0.45)',
62
+ indicator: 'rgba(20,21,26,0.06)',
63
+ handle: '#D5D7DE',
64
+ },
65
+ ...shared,
66
+ };
67
+
68
+ export const darkTheme: PickerTheme = {
69
+ dark: true,
70
+ colors: {
71
+ background: '#16171C',
72
+ surface: '#22242B',
73
+ sheet: '#1B1C22',
74
+ text: '#F3F4F7',
75
+ textSecondary: '#9A9EAA',
76
+ textDisabled: '#4E515B',
77
+ primary: '#8B85FF',
78
+ onPrimary: '#0E0E14',
79
+ primaryMuted: 'rgba(139,133,255,0.18)',
80
+ border: '#2C2E36',
81
+ backdrop: 'rgba(0,0,0,0.6)',
82
+ indicator: 'rgba(255,255,255,0.07)',
83
+ handle: '#3A3D47',
84
+ },
85
+ ...shared,
86
+ };
87
+
88
+ export type DeepPartial<T> = {
89
+ [K in keyof T]?: T[K] extends object ? (T[K] extends Function ? T[K] : DeepPartial<T[K]>) : T[K];
90
+ };
91
+
92
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
93
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
94
+ }
95
+
96
+ export function mergeTheme<T>(base: T, override?: DeepPartial<T>): T {
97
+ if (!override) return base;
98
+ const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
99
+ for (const key of Object.keys(override)) {
100
+ const o = (override as Record<string, unknown>)[key];
101
+ const b = out[key];
102
+ out[key] = isPlainObject(o) && isPlainObject(b) ? mergeTheme(b, o as DeepPartial<typeof b>) : o;
103
+ }
104
+ return out as T;
105
+ }
106
+
107
+ /** Build a full theme from overrides on top of the light (or dark) base. */
108
+ export function createTheme(overrides: DeepPartial<PickerTheme>, base?: PickerTheme): PickerTheme {
109
+ return mergeTheme(base ?? (overrides.dark ? darkTheme : lightTheme), overrides);
110
+ }
111
+
112
+ interface ProviderValue {
113
+ light: PickerTheme;
114
+ dark: PickerTheme;
115
+ colorScheme: 'auto' | 'light' | 'dark';
116
+ }
117
+
118
+ const ThemeContext = createContext<ProviderValue | null>(null);
119
+
120
+ export interface PickersProviderProps {
121
+ children: React.ReactNode;
122
+ /** 'auto' follows the OS appearance. Default: 'auto' */
123
+ colorScheme?: 'auto' | 'light' | 'dark';
124
+ /** Overrides applied to the light theme (or a full theme) */
125
+ theme?: DeepPartial<PickerTheme>;
126
+ /** Overrides applied to the dark theme */
127
+ darkTheme?: DeepPartial<PickerTheme>;
128
+ }
129
+
130
+ export function PickersProvider({ children, colorScheme = 'auto', theme, darkTheme: dark }: PickersProviderProps) {
131
+ const value = useMemo<ProviderValue>(
132
+ () => ({
133
+ light: mergeTheme(lightTheme, theme),
134
+ dark: mergeTheme(darkTheme, dark ?? theme),
135
+ colorScheme,
136
+ }),
137
+ [theme, dark, colorScheme]
138
+ );
139
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
140
+ }
141
+
142
+ /** Resolves the active theme, applying a component-level override if given. */
143
+ export function usePickerTheme(override?: DeepPartial<PickerTheme>): PickerTheme {
144
+ const ctx = useContext(ThemeContext);
145
+ const system = useColorScheme();
146
+ const scheme = ctx?.colorScheme ?? 'auto';
147
+ const isDark = scheme === 'auto' ? system === 'dark' : scheme === 'dark';
148
+ const base = isDark ? ctx?.dark ?? darkTheme : ctx?.light ?? lightTheme;
149
+ return useMemo(() => mergeTheme(base, override), [base, override]);
150
+ }
151
+
152
+ /** Text style helper used across components. */
153
+ export function textStyle(theme: PickerTheme, size: keyof PickerTheme['typography']['sizes'], weight: 'regular' | 'medium' | 'bold' = 'regular'): TextStyle {
154
+ return {
155
+ fontSize: theme.typography.sizes[size],
156
+ fontWeight: theme.typography.weights[weight],
157
+ fontFamily: weight === 'regular' ? theme.typography.fontFamily : theme.typography.fontFamilyMedium ?? theme.typography.fontFamily,
158
+ color: theme.colors.text,
159
+ };
160
+ }
@@ -0,0 +1,199 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import { MultiWheelPicker } from '../wheel/MultiWheelPicker';
4
+ import type { MultiWheelSlot } from '../wheel/MultiWheelPicker';
5
+ import type { WheelColumn, } from '../wheel/MultiWheelPicker';
6
+ import type { WheelItem } from '../wheel/WheelPicker';
7
+ import { getDayPeriods, uses12HourClock } from '../utils/date';
8
+ import type { SlotStyles, ThemableProps } from '../types';
9
+
10
+ export interface TimePickerProps extends ThemableProps {
11
+ /** Selected time (the date part is preserved). */
12
+ value?: Date;
13
+ defaultValue?: Date;
14
+ onChange?: (date: Date) => void;
15
+ /** Fires on every tick while scrolling (for haptics). */
16
+ onValueChanging?: (date: Date) => void;
17
+ /** Default: 'auto' (from locale) */
18
+ format?: '12h' | '24h' | 'auto';
19
+ locale?: string;
20
+ /** Minute step: 1, 5, 15... Default: 1 */
21
+ minuteInterval?: number;
22
+ showSeconds?: boolean;
23
+ secondInterval?: number;
24
+ /** Earliest selectable time of day */
25
+ minTime?: Date;
26
+ /** Latest selectable time of day */
27
+ maxTime?: Date;
28
+ /** Infinite hour/minute wheels. Default: true */
29
+ loop?: boolean;
30
+ /** Zero-pad hours. Default: true for 24h, false for 12h */
31
+ padHours?: boolean;
32
+ /** Labels for AM/PM. Default: from locale */
33
+ dayPeriodLabels?: [string, string];
34
+ /** Put AM/PM before the hours (e.g. zh, ko). Default: false */
35
+ dayPeriodFirst?: boolean;
36
+ /** Separator between columns. Default: ':' */
37
+ separator?: React.ReactNode;
38
+ itemHeight?: number;
39
+ visibleItems?: number;
40
+ variant?: '3d' | 'flat';
41
+ style?: StyleProp<ViewStyle>;
42
+ styles?: SlotStyles<MultiWheelSlot>;
43
+ testID?: string;
44
+ }
45
+
46
+ const pad2 = (n: number) => String(n).padStart(2, '0');
47
+ const range = (count: number, step = 1) => Array.from({ length: Math.ceil(count / step) }, (_, i) => i * step);
48
+ const secondsOfDay = (d: Date) => d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
49
+
50
+ export function TimePicker({
51
+ value,
52
+ defaultValue,
53
+ onChange,
54
+ onValueChanging,
55
+ format = 'auto',
56
+ locale,
57
+ minuteInterval = 1,
58
+ showSeconds = false,
59
+ secondInterval = 1,
60
+ minTime,
61
+ maxTime,
62
+ loop = true,
63
+ padHours,
64
+ dayPeriodLabels,
65
+ dayPeriodFirst = false,
66
+ separator = ':',
67
+ itemHeight,
68
+ visibleItems,
69
+ variant,
70
+ style,
71
+ styles,
72
+ theme,
73
+ testID,
74
+ }: TimePickerProps) {
75
+ const [inner, setInner] = useState<Date>(() => value ?? defaultValue ?? new Date());
76
+ const date = value ?? inner;
77
+ const is12 = format === 'auto' ? uses12HourClock(locale) : format === '12h';
78
+ const periods = useMemo(() => dayPeriodLabels ?? getDayPeriods(locale), [dayPeriodLabels, locale]);
79
+ const padH = padHours ?? !is12;
80
+
81
+ const h24 = date.getHours();
82
+ const minute = Math.floor(date.getMinutes() / minuteInterval) * minuteInterval;
83
+ const second = Math.floor(date.getSeconds() / secondInterval) * secondInterval;
84
+ const isPM = h24 >= 12;
85
+
86
+ const minS = minTime ? secondsOfDay(minTime) : 0;
87
+ const maxS = maxTime ? secondsOfDay(maxTime) : 86399;
88
+
89
+ const build = useCallback(
90
+ (patch: { h24?: number; m?: number; s?: number }) => {
91
+ const d = new Date(date);
92
+ d.setHours(patch.h24 ?? h24, patch.m ?? minute, showSeconds ? patch.s ?? second : 0, 0);
93
+ return d;
94
+ },
95
+ [date, h24, minute, second, showSeconds]
96
+ );
97
+
98
+ const commit = useCallback(
99
+ (d: Date) => {
100
+ // Clamp to [minTime, maxTime]
101
+ const sec = secondsOfDay(d);
102
+ if (sec < minS || sec > maxS) {
103
+ const target = sec < minS ? minS : maxS;
104
+ d = new Date(d);
105
+ d.setHours(Math.floor(target / 3600), Math.floor((target % 3600) / 60), showSeconds ? target % 60 : 0, 0);
106
+ }
107
+ if (value === undefined) setInner(d);
108
+ onChange?.(d);
109
+ },
110
+ [maxS, minS, onChange, showSeconds, value]
111
+ );
112
+
113
+ const columns = useMemo(() => {
114
+ const cols: WheelColumn[] = [];
115
+ const hourOutOfRange = (h: number) => h * 3600 + 3599 < minS || h * 3600 > maxS;
116
+ const hourItems: WheelItem<number>[] = is12
117
+ ? range(12).map((i) => {
118
+ const display = i === 0 ? 12 : i;
119
+ const real = (isPM ? 12 : 0) + i;
120
+ return { value: i, label: padH ? pad2(display) : String(display), disabled: hourOutOfRange(real) };
121
+ })
122
+ : range(24).map((h) => ({ value: h, label: padH ? pad2(h) : String(h), disabled: hourOutOfRange(h) }));
123
+
124
+ const minuteItems: WheelItem<number>[] = range(60, minuteInterval).map((m) => {
125
+ const t = h24 * 3600 + m * 60;
126
+ return { value: m, label: pad2(m), disabled: t + 59 < minS || t > maxS };
127
+ });
128
+
129
+ const toH24 = (hv: number, pm: boolean) => (is12 ? (pm ? 12 : 0) + hv : hv);
130
+ const mk = (pick: (v: number) => Date) => ({
131
+ onChange: (v: number) => commit(pick(v)),
132
+ onValueChanging: onValueChanging ? (v: number) => onValueChanging(pick(v)) : undefined,
133
+ });
134
+
135
+ const periodCol: WheelColumn<number> = {
136
+ key: 'period',
137
+ items: [
138
+ { value: 0, label: periods[0], disabled: minS >= 12 * 3600 },
139
+ { value: 1, label: periods[1], disabled: maxS < 12 * 3600 },
140
+ ],
141
+ value: isPM ? 1 : 0,
142
+ flex: 1,
143
+ ...mk((v) => build({ h24: (h24 % 12) + (v ? 12 : 0) })),
144
+ };
145
+
146
+ if (is12 && dayPeriodFirst) cols.push(periodCol);
147
+ cols.push({
148
+ key: 'hour',
149
+ items: hourItems,
150
+ value: is12 ? h24 % 12 : h24,
151
+ loop,
152
+ flex: 1,
153
+ ...mk((v) => build({ h24: toH24(v, isPM) })),
154
+ });
155
+ cols.push({
156
+ key: 'minute',
157
+ items: minuteItems,
158
+ value: minute,
159
+ loop,
160
+ flex: 1,
161
+ ...mk((v) => build({ m: v })),
162
+ });
163
+ if (showSeconds) {
164
+ cols.push({
165
+ key: 'second',
166
+ items: range(60, secondInterval).map((sv) => ({ value: sv, label: pad2(sv) })),
167
+ value: second,
168
+ loop,
169
+ flex: 1,
170
+ ...mk((v) => build({ s: v })),
171
+ });
172
+ }
173
+ if (is12 && !dayPeriodFirst) cols.push(periodCol);
174
+ return cols;
175
+ }, [build, commit, dayPeriodFirst, h24, is12, isPM, loop, maxS, minS, minute, minuteInterval, onValueChanging, padH, periods, second, secondInterval, showSeconds]);
176
+
177
+ const sep = useCallback(
178
+ (left: number) => {
179
+ const leftKey = columns[left]?.key;
180
+ const rightKey = columns[left + 1]?.key;
181
+ return leftKey === 'period' || rightKey === 'period' ? null : separator;
182
+ },
183
+ [columns, separator]
184
+ );
185
+
186
+ return (
187
+ <MultiWheelPicker
188
+ columns={columns}
189
+ separator={sep}
190
+ itemHeight={itemHeight}
191
+ visibleItems={visibleItems}
192
+ variant={variant}
193
+ style={style}
194
+ styles={styles}
195
+ theme={theme}
196
+ testID={testID}
197
+ />
198
+ );
199
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { StyleProp, TextStyle } from 'react-native';
2
+ import type { DeepPartial, PickerTheme } from './theme';
3
+
4
+ /** Per-slot style overrides. Every component exposes its own slot names. */
5
+ export type SlotStyles<K extends string> = Partial<Record<K, StyleProp<TextStyle>>>;
6
+
7
+ export interface ThemableProps {
8
+ /** Component-level theme override, merged on top of the provider theme */
9
+ theme?: DeepPartial<PickerTheme>;
10
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { ScrollView } from 'react-native';
3
+
4
+ // Absent on react-native-web.
5
+ const ParentScrollContext = (ScrollView as unknown as { Context?: React.Context<unknown> }).Context;
6
+
7
+ /**
8
+ * Wraps a fixed-height list that scrolls on its own. Its windowing never depends on an outer
9
+ * ScrollView, so hiding that parent silences VirtualizedList's "should never be nested" warning
10
+ * (the context is only read for that warning).
11
+ */
12
+ export function OwnScroll({ children }: { children: React.ReactNode }) {
13
+ return ParentScrollContext ? <ParentScrollContext.Provider value={null}>{children}</ParentScrollContext.Provider> : <>{children}</>;
14
+ }