@retray-dev/ui-kit 1.7.0 → 2.3.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 (40) hide show
  1. package/COMPONENTS.md +258 -54
  2. package/README.md +6 -5
  3. package/dist/index.d.mts +113 -44
  4. package/dist/index.d.ts +113 -44
  5. package/dist/index.js +802 -324
  6. package/dist/index.mjs +794 -323
  7. package/package.json +6 -2
  8. package/src/components/Alert/Alert.tsx +24 -12
  9. package/src/components/AlertBanner/AlertBanner.tsx +83 -0
  10. package/src/components/AlertBanner/index.ts +2 -0
  11. package/src/components/Avatar/Avatar.tsx +1 -0
  12. package/src/components/Badge/Badge.tsx +44 -8
  13. package/src/components/Button/Button.tsx +12 -5
  14. package/src/components/Card/Card.tsx +86 -9
  15. package/src/components/Chip/Chip.tsx +173 -0
  16. package/src/components/Chip/index.ts +2 -0
  17. package/src/components/ConfirmDialog/ConfirmDialog.tsx +87 -0
  18. package/src/components/ConfirmDialog/index.ts +2 -0
  19. package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +4 -2
  20. package/src/components/CurrencyInput/CurrencyInput.tsx +9 -1
  21. package/src/components/EmptyState/EmptyState.tsx +42 -7
  22. package/src/components/Input/Input.tsx +102 -21
  23. package/src/components/LabelValue/LabelValue.tsx +47 -0
  24. package/src/components/LabelValue/index.ts +2 -0
  25. package/src/components/ListItem/ListItem.tsx +121 -0
  26. package/src/components/ListItem/index.ts +2 -0
  27. package/src/components/MonthPicker/MonthPicker.tsx +92 -0
  28. package/src/components/MonthPicker/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +189 -125
  30. package/src/components/Slider/Slider.tsx +64 -100
  31. package/src/components/Switch/Switch.tsx +25 -21
  32. package/src/components/Textarea/Textarea.tsx +12 -2
  33. package/src/components/Toggle/Toggle.tsx +13 -6
  34. package/src/index.ts +8 -2
  35. package/src/theme/ThemeProvider.tsx +11 -8
  36. package/src/theme/colors.ts +19 -18
  37. package/src/theme/types.ts +2 -0
  38. package/src/components/Alert/index.ts +0 -2
  39. package/src/components/CurrencyInputLarge/CurrencyInputLarge.tsx +0 -66
  40. package/src/components/CurrencyInputLarge/index.ts +0 -1
@@ -0,0 +1,87 @@
1
+ import React from 'react'
2
+ import { Modal, View, Text, StyleSheet, TouchableOpacity } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+ import { Button } from '../Button'
5
+
6
+ export interface ConfirmDialogProps {
7
+ visible: boolean
8
+ title: string
9
+ description?: string
10
+ confirmLabel?: string
11
+ cancelLabel?: string
12
+ confirmVariant?: 'primary' | 'destructive'
13
+ onConfirm: () => void
14
+ onCancel: () => void
15
+ }
16
+
17
+ export function ConfirmDialog({
18
+ visible,
19
+ title,
20
+ description,
21
+ confirmLabel = 'Confirm',
22
+ cancelLabel = 'Cancel',
23
+ confirmVariant = 'primary',
24
+ onConfirm,
25
+ onCancel,
26
+ }: ConfirmDialogProps) {
27
+ const { colors } = useTheme()
28
+
29
+ return (
30
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={onCancel}>
31
+ <TouchableOpacity style={styles.overlay} activeOpacity={1} onPress={onCancel}>
32
+ <View
33
+ style={[styles.dialog, { backgroundColor: colors.card }]}
34
+ onStartShouldSetResponder={() => true}
35
+ >
36
+ <Text style={[styles.title, { color: colors.cardForeground }]} allowFontScaling={true}>
37
+ {title}
38
+ </Text>
39
+ {description ? (
40
+ <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>
41
+ {description}
42
+ </Text>
43
+ ) : null}
44
+ <View style={styles.actions}>
45
+ <Button label={cancelLabel} variant="outline" fullWidth onPress={onCancel} />
46
+ <Button label={confirmLabel} variant={confirmVariant} fullWidth onPress={onConfirm} />
47
+ </View>
48
+ </View>
49
+ </TouchableOpacity>
50
+ </Modal>
51
+ )
52
+ }
53
+
54
+ const styles = StyleSheet.create({
55
+ overlay: {
56
+ flex: 1,
57
+ backgroundColor: 'rgba(0,0,0,0.5)',
58
+ justifyContent: 'center',
59
+ alignItems: 'center',
60
+ padding: 24,
61
+ },
62
+ dialog: {
63
+ width: '100%',
64
+ maxWidth: 400,
65
+ borderRadius: 16,
66
+ padding: 24,
67
+ gap: 12,
68
+ shadowColor: '#000',
69
+ shadowOffset: { width: 0, height: 8 },
70
+ shadowOpacity: 0.15,
71
+ shadowRadius: 16,
72
+ elevation: 8,
73
+ },
74
+ title: {
75
+ fontSize: 18,
76
+ fontWeight: '600',
77
+ lineHeight: 26,
78
+ },
79
+ description: {
80
+ fontSize: 15,
81
+ lineHeight: 22,
82
+ },
83
+ actions: {
84
+ gap: 10,
85
+ marginTop: 8,
86
+ },
87
+ })
@@ -0,0 +1,2 @@
1
+ export { ConfirmDialog } from './ConfirmDialog'
2
+ export type { ConfirmDialogProps } from './ConfirmDialog'
@@ -8,6 +8,8 @@ export interface CurrencyDisplayProps {
8
8
  prefix?: string
9
9
  /** When true, shows two decimal places separated by a comma (e.g. `$25.000,00`). Defaults to `false`. */
10
10
  showDecimals?: boolean
11
+ /** Override the color of the formatted text. Defaults to the `foreground` theme token. */
12
+ textColor?: string
11
13
  style?: ViewStyle
12
14
  }
13
15
 
@@ -24,13 +26,13 @@ function formatValue(value: number | string, prefix: string, showDecimals: boole
24
26
  return `${sign}${prefix}${intPart}`
25
27
  }
26
28
 
27
- export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, style }: CurrencyDisplayProps) {
29
+ export function CurrencyDisplay({ value, prefix = '$', showDecimals = false, textColor, style }: CurrencyDisplayProps) {
28
30
  const { colors } = useTheme()
29
31
  const formatted = formatValue(value, prefix, showDecimals)
30
32
 
31
33
  return (
32
34
  <View style={[styles.container, style]}>
33
- <Text style={[styles.amount, { color: colors.foreground }]} allowFontScaling={true}>
35
+ <Text style={[styles.amount, { color: textColor ?? colors.foreground }]} allowFontScaling={true}>
34
36
  {formatted}
35
37
  </Text>
36
38
  </View>
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { ViewStyle } from 'react-native'
2
+ import { ViewStyle, TextStyle } from 'react-native'
3
3
  import { Input } from '../Input'
4
4
 
5
5
  export interface CurrencyInputProps {
@@ -11,6 +11,8 @@ export interface CurrencyInputProps {
11
11
  prefix?: string
12
12
  /** Character used to separate groups of three digits. Defaults to `'.'`. */
13
13
  thousandsSeparator?: '.' | ','
14
+ /** Font size variant. `'large'` renders at 36pt, `'default'` at 17pt. */
15
+ size?: 'default' | 'large'
14
16
  label?: string
15
17
  /** Red helper text; also changes input border to destructive color. */
16
18
  error?: string
@@ -18,6 +20,7 @@ export interface CurrencyInputProps {
18
20
  placeholder?: string
19
21
  editable?: boolean
20
22
  containerStyle?: ViewStyle
23
+ style?: TextStyle
21
24
  }
22
25
 
23
26
  function formatCurrency(raw: string, separator: '.' | ','): string {
@@ -32,12 +35,14 @@ export function CurrencyInput({
32
35
  onChangeValue,
33
36
  prefix = '$',
34
37
  thousandsSeparator = '.',
38
+ size = 'default',
35
39
  label,
36
40
  error,
37
41
  hint,
38
42
  placeholder,
39
43
  editable,
40
44
  containerStyle,
45
+ style,
41
46
  }: CurrencyInputProps) {
42
47
  const handleChange = (text: string) => {
43
48
  const withoutPrefix = prefix && text.startsWith(prefix) ? text.slice(prefix.length) : text
@@ -49,6 +54,8 @@ export function CurrencyInput({
49
54
  onChangeValue?.(isNaN(raw) ? 0 : raw)
50
55
  }
51
56
 
57
+ const inputStyle: TextStyle = size === 'large' ? { fontSize: 36 } : {}
58
+
52
59
  return (
53
60
  <Input
54
61
  value={value}
@@ -60,6 +67,7 @@ export function CurrencyInput({
60
67
  placeholder={placeholder ?? `${prefix}0`}
61
68
  editable={editable}
62
69
  containerStyle={containerStyle}
70
+ style={[inputStyle, style]}
63
71
  />
64
72
  )
65
73
  }
@@ -7,24 +7,47 @@ export interface EmptyStateProps {
7
7
  title: string
8
8
  description?: string
9
9
  action?: React.ReactNode
10
+ /** `compact` hides description/action and uses tighter spacing and a smaller icon. */
11
+ size?: 'default' | 'compact'
10
12
  style?: ViewStyle
11
13
  }
12
14
 
13
- export function EmptyState({ icon, title, description, action, style }: EmptyStateProps) {
15
+ export function EmptyState({ icon, title, description, action, size = 'default', style }: EmptyStateProps) {
14
16
  const { colors } = useTheme()
17
+ const isCompact = size === 'compact'
15
18
 
16
19
  return (
17
- <View style={[styles.container, { borderColor: colors.border }, style]}>
20
+ <View
21
+ style={[
22
+ styles.container,
23
+ isCompact && styles.containerCompact,
24
+ { borderColor: colors.border },
25
+ style,
26
+ ]}
27
+ >
18
28
  {icon ? (
19
- <View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>{icon}</View>
29
+ <View
30
+ style={[
31
+ styles.iconWrapper,
32
+ isCompact && styles.iconWrapperCompact,
33
+ { backgroundColor: colors.muted },
34
+ ]}
35
+ >
36
+ {icon}
37
+ </View>
20
38
  ) : null}
21
39
  <View style={styles.textWrapper}>
22
- <Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
23
- {description ? (
24
- <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>
40
+ <Text
41
+ style={[styles.title, isCompact && styles.titleCompact, { color: colors.foreground }]}
42
+ allowFontScaling={true}
43
+ >
44
+ {title}
45
+ </Text>
46
+ {description && !isCompact ? (
47
+ <Text style={[styles.description, { color: colors.mutedForeground }]} allowFontScaling={true}>{description}</Text>
25
48
  ) : null}
26
49
  </View>
27
- {action ? <View style={styles.action}>{action}</View> : null}
50
+ {action && !isCompact ? <View style={styles.action}>{action}</View> : null}
28
51
  </View>
29
52
  )
30
53
  }
@@ -39,6 +62,10 @@ const styles = StyleSheet.create({
39
62
  padding: 32,
40
63
  gap: 16,
41
64
  },
65
+ containerCompact: {
66
+ padding: 20,
67
+ gap: 10,
68
+ },
42
69
  iconWrapper: {
43
70
  width: 48,
44
71
  height: 48,
@@ -46,6 +73,11 @@ const styles = StyleSheet.create({
46
73
  alignItems: 'center',
47
74
  justifyContent: 'center',
48
75
  },
76
+ iconWrapperCompact: {
77
+ width: 36,
78
+ height: 36,
79
+ borderRadius: 8,
80
+ },
49
81
  textWrapper: {
50
82
  alignItems: 'center',
51
83
  gap: 8,
@@ -56,6 +88,9 @@ const styles = StyleSheet.create({
56
88
  fontWeight: '500',
57
89
  textAlign: 'center',
58
90
  },
91
+ titleCompact: {
92
+ fontSize: 15,
93
+ },
59
94
  description: {
60
95
  fontSize: 14,
61
96
  lineHeight: 20,
@@ -1,46 +1,104 @@
1
1
  import React, { useState } from 'react'
2
- import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle, TouchableOpacity, Platform } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
+ const webInputResetStyle: any =
6
+ Platform.OS === 'web'
7
+ ? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
8
+ : {}
9
+
5
10
  export interface InputProps extends TextInputProps {
6
11
  label?: string
7
12
  /** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
8
13
  error?: string
9
14
  /** Helper text shown below the input when there is no error. */
10
15
  hint?: string
11
- /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
16
+ /** Text or component rendered before the input text. */
17
+ prefix?: React.ReactNode
18
+ /** Text or component rendered after the input text. */
19
+ suffix?: React.ReactNode
20
+ /** Style applied to prefix text if prefix is a string. */
21
+ prefixStyle?: TextStyle
22
+ /** Style applied to suffix text if suffix is a string. */
23
+ suffixStyle?: TextStyle
24
+ /** Input type. When set to \`'password'\`, shows a toggle button to reveal/hide text. */
25
+ type?: 'text' | 'password'
26
+ /** Style for the outer container \`View\`. Use \`style\` (from \`TextInputProps\`) to style the \`TextInput\` itself. */
12
27
  containerStyle?: ViewStyle
13
28
  }
14
29
 
15
- export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
30
+ export function Input({ label, error, hint, prefix, suffix, prefixStyle, suffixStyle, type = 'text', containerStyle, style, onFocus, onBlur, secureTextEntry, ...props }: InputProps) {
16
31
  const { colors } = useTheme()
17
32
  const [focused, setFocused] = useState(false)
33
+ const [showPassword, setShowPassword] = useState(false)
34
+
35
+ const isPassword = type === 'password'
36
+ const effectiveSecure = isPassword ? !showPassword : secureTextEntry
37
+
38
+ // If type is password and no suffix is provided, add the toggle button
39
+ const effectiveSuffix = isPassword && !suffix ? (
40
+ <TouchableOpacity onPress={() => setShowPassword(!showPassword)} style={styles.passwordToggle} activeOpacity={0.6}>
41
+ <Text style={[styles.suffixText, { color: colors.mutedForeground }]}>{showPassword ? '👁' : '👁‍🗨'}</Text>
42
+ </TouchableOpacity>
43
+ ) : suffix
18
44
 
19
45
  return (
20
46
  <View style={[styles.container, containerStyle]}>
21
47
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
22
- <TextInput
48
+ <View
23
49
  style={[
24
- styles.input,
50
+ styles.inputWrapper,
25
51
  {
26
- borderColor: error ? colors.destructive : focused ? colors.ring : colors.border,
27
- color: colors.foreground,
52
+ borderColor: error
53
+ ? colors.destructive
54
+ : focused
55
+ ? (colors.ring ?? colors.primary)
56
+ : colors.border,
28
57
  backgroundColor: colors.background,
29
58
  },
30
- style,
31
59
  ]}
32
- onFocus={(e) => {
33
- setFocused(true)
34
- onFocus?.(e)
35
- }}
36
- onBlur={(e) => {
37
- setFocused(false)
38
- onBlur?.(e)
39
- }}
40
- placeholderTextColor={colors.mutedForeground}
41
- allowFontScaling={true}
42
- {...props}
43
- />
60
+ >
61
+ {prefix ? (
62
+ typeof prefix === 'string' ? (
63
+ <Text style={[styles.prefixText, { color: colors.mutedForeground }, prefixStyle]} allowFontScaling={true}>
64
+ {prefix}
65
+ </Text>
66
+ ) : (
67
+ <View style={styles.prefixContainer}>{prefix}</View>
68
+ )
69
+ ) : null}
70
+ <TextInput
71
+ style={[
72
+ styles.input,
73
+ {
74
+ color: colors.foreground,
75
+ },
76
+ webInputResetStyle,
77
+ style,
78
+ ]}
79
+ onFocus={(e) => {
80
+ setFocused(true)
81
+ onFocus?.(e)
82
+ }}
83
+ onBlur={(e) => {
84
+ setFocused(false)
85
+ onBlur?.(e)
86
+ }}
87
+ placeholderTextColor={colors.mutedForeground}
88
+ allowFontScaling={true}
89
+ secureTextEntry={effectiveSecure}
90
+ {...props}
91
+ />
92
+ {effectiveSuffix ? (
93
+ typeof effectiveSuffix === 'string' ? (
94
+ <Text style={[styles.suffixText, { color: colors.mutedForeground }, suffixStyle]} allowFontScaling={true}>
95
+ {effectiveSuffix}
96
+ </Text>
97
+ ) : (
98
+ <View style={styles.suffixContainer}>{effectiveSuffix}</View>
99
+ )
100
+ ) : null}
101
+ </View>
44
102
  {error ? (
45
103
  <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
46
104
  ) : null}
@@ -60,12 +118,35 @@ const styles = StyleSheet.create({
60
118
  fontWeight: '500',
61
119
  marginBottom: 6,
62
120
  },
63
- input: {
121
+ inputWrapper: {
122
+ flexDirection: 'row',
123
+ alignItems: 'center',
64
124
  borderWidth: 1.5,
65
125
  borderRadius: 14,
66
126
  paddingHorizontal: 20,
67
127
  paddingVertical: 16,
128
+ },
129
+ input: {
130
+ flex: 1,
68
131
  fontSize: 17,
132
+ paddingVertical: 0,
133
+ },
134
+ prefixContainer: {
135
+ marginRight: 8,
136
+ },
137
+ prefixText: {
138
+ fontSize: 17,
139
+ marginRight: 8,
140
+ },
141
+ suffixContainer: {
142
+ marginLeft: 8,
143
+ },
144
+ suffixText: {
145
+ fontSize: 17,
146
+ marginLeft: 8,
147
+ },
148
+ passwordToggle: {
149
+ padding: 4,
69
150
  },
70
151
  helperText: {
71
152
  fontSize: 13,
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export interface LabelValueProps {
6
+ label: string
7
+ value: string | React.ReactNode
8
+ style?: ViewStyle
9
+ }
10
+
11
+ export function LabelValue({ label, value, style }: LabelValueProps) {
12
+ const { colors } = useTheme()
13
+
14
+ return (
15
+ <View style={[styles.container, style]}>
16
+ <Text style={[styles.label, { color: colors.mutedForeground }]} allowFontScaling={true}>
17
+ {label}
18
+ </Text>
19
+ {typeof value === 'string' ? (
20
+ <Text style={[styles.value, { color: colors.foreground }]} allowFontScaling={true}>
21
+ {value}
22
+ </Text>
23
+ ) : (
24
+ value
25
+ )}
26
+ </View>
27
+ )
28
+ }
29
+
30
+ const styles = StyleSheet.create({
31
+ container: {
32
+ flexDirection: 'row',
33
+ justifyContent: 'space-between',
34
+ alignItems: 'center',
35
+ gap: 12,
36
+ },
37
+ label: {
38
+ fontSize: 13,
39
+ lineHeight: 18,
40
+ },
41
+ value: {
42
+ fontSize: 15,
43
+ fontWeight: '500',
44
+ lineHeight: 22,
45
+ textAlign: 'right',
46
+ },
47
+ })
@@ -0,0 +1,2 @@
1
+ export { LabelValue } from './LabelValue'
2
+ export type { LabelValueProps } from './LabelValue'
@@ -0,0 +1,121 @@
1
+ import React, { useRef } from 'react'
2
+ import {
3
+ TouchableOpacity,
4
+ Animated,
5
+ View,
6
+ Text,
7
+ StyleSheet,
8
+ ViewStyle,
9
+ Platform,
10
+ } from 'react-native'
11
+ import * as Haptics from 'expo-haptics'
12
+ import { useTheme } from '../../theme'
13
+
14
+ const nativeDriver = Platform.OS !== 'web'
15
+
16
+ export interface ListItemProps {
17
+ icon?: React.ReactNode
18
+ title: string
19
+ subtitle?: string
20
+ trailing?: string | React.ReactNode
21
+ onPress?: () => void
22
+ disabled?: boolean
23
+ style?: ViewStyle
24
+ }
25
+
26
+ export function ListItem({ icon, title, subtitle, trailing, onPress, disabled, style }: ListItemProps) {
27
+ const { colors } = useTheme()
28
+ const scale = useRef(new Animated.Value(1)).current
29
+
30
+ const handlePressIn = () => {
31
+ if (!onPress || disabled) return
32
+ Animated.spring(scale, {
33
+ toValue: 0.97,
34
+ useNativeDriver: nativeDriver,
35
+ speed: 40,
36
+ bounciness: 0,
37
+ }).start()
38
+ }
39
+
40
+ const handlePressOut = () => {
41
+ Animated.spring(scale, {
42
+ toValue: 1,
43
+ useNativeDriver: nativeDriver,
44
+ speed: 40,
45
+ bounciness: 4,
46
+ }).start()
47
+ }
48
+
49
+ const handlePress = () => {
50
+ Haptics.selectionAsync()
51
+ onPress?.()
52
+ }
53
+
54
+ return (
55
+ <Animated.View style={[{ transform: [{ scale }] }, disabled && styles.disabled]}>
56
+ <TouchableOpacity
57
+ style={[styles.container, style]}
58
+ onPress={onPress ? handlePress : undefined}
59
+ onPressIn={handlePressIn}
60
+ onPressOut={handlePressOut}
61
+ disabled={disabled}
62
+ activeOpacity={1}
63
+ touchSoundDisabled={true}
64
+ >
65
+ {icon ? <View style={styles.iconWrapper}>{icon}</View> : null}
66
+ <View style={styles.content}>
67
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
68
+ {title}
69
+ </Text>
70
+ {subtitle ? (
71
+ <Text style={[styles.subtitle, { color: colors.mutedForeground }]} allowFontScaling={true}>
72
+ {subtitle}
73
+ </Text>
74
+ ) : null}
75
+ </View>
76
+ {trailing !== undefined ? (
77
+ typeof trailing === 'string' ? (
78
+ <Text style={[styles.trailing, { color: colors.mutedForeground }]} allowFontScaling={true}>
79
+ {trailing}
80
+ </Text>
81
+ ) : (
82
+ trailing
83
+ )
84
+ ) : null}
85
+ </TouchableOpacity>
86
+ </Animated.View>
87
+ )
88
+ }
89
+
90
+ const styles = StyleSheet.create({
91
+ container: {
92
+ flexDirection: 'row',
93
+ alignItems: 'center',
94
+ paddingHorizontal: 16,
95
+ paddingVertical: 14,
96
+ gap: 12,
97
+ },
98
+ iconWrapper: {
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ },
102
+ content: {
103
+ flex: 1,
104
+ gap: 3,
105
+ },
106
+ title: {
107
+ fontSize: 16,
108
+ fontWeight: '500',
109
+ lineHeight: 22,
110
+ },
111
+ subtitle: {
112
+ fontSize: 13,
113
+ lineHeight: 18,
114
+ },
115
+ trailing: {
116
+ fontSize: 15,
117
+ },
118
+ disabled: {
119
+ opacity: 0.45,
120
+ },
121
+ })
@@ -0,0 +1,2 @@
1
+ export { ListItem } from './ListItem'
2
+ export type { ListItemProps } from './ListItem'
@@ -0,0 +1,92 @@
1
+ import React from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
4
+ import { useTheme } from '../../theme'
5
+
6
+ const MONTH_NAMES = [
7
+ 'January', 'February', 'March', 'April', 'May', 'June',
8
+ 'July', 'August', 'September', 'October', 'November', 'December',
9
+ ]
10
+
11
+ export interface MonthPickerValue {
12
+ /** Month number 1–12 */
13
+ month: number
14
+ year: number
15
+ }
16
+
17
+ export interface MonthPickerProps {
18
+ value: MonthPickerValue
19
+ onChange: (value: MonthPickerValue) => void
20
+ style?: ViewStyle
21
+ }
22
+
23
+ export function MonthPicker({ value, onChange, style }: MonthPickerProps) {
24
+ const { colors } = useTheme()
25
+
26
+ const handlePrev = () => {
27
+ Haptics.selectionAsync()
28
+ if (value.month === 1) {
29
+ onChange({ month: 12, year: value.year - 1 })
30
+ } else {
31
+ onChange({ month: value.month - 1, year: value.year })
32
+ }
33
+ }
34
+
35
+ const handleNext = () => {
36
+ Haptics.selectionAsync()
37
+ if (value.month === 12) {
38
+ onChange({ month: 1, year: value.year + 1 })
39
+ } else {
40
+ onChange({ month: value.month + 1, year: value.year })
41
+ }
42
+ }
43
+
44
+ return (
45
+ <View style={[styles.container, style]}>
46
+ <TouchableOpacity
47
+ style={styles.arrow}
48
+ onPress={handlePrev}
49
+ activeOpacity={0.6}
50
+ touchSoundDisabled={true}
51
+ >
52
+ <Text style={[styles.arrowText, { color: colors.foreground }]} allowFontScaling={true}>‹</Text>
53
+ </TouchableOpacity>
54
+ <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>
55
+ {MONTH_NAMES[value.month - 1]} {value.year}
56
+ </Text>
57
+ <TouchableOpacity
58
+ style={styles.arrow}
59
+ onPress={handleNext}
60
+ activeOpacity={0.6}
61
+ touchSoundDisabled={true}
62
+ >
63
+ <Text style={[styles.arrowText, { color: colors.foreground }]} allowFontScaling={true}>›</Text>
64
+ </TouchableOpacity>
65
+ </View>
66
+ )
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ container: {
71
+ flexDirection: 'row',
72
+ alignItems: 'center',
73
+ justifyContent: 'space-between',
74
+ },
75
+ arrow: {
76
+ width: 44,
77
+ height: 44,
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ },
81
+ arrowText: {
82
+ fontSize: 24,
83
+ lineHeight: 30,
84
+ },
85
+ label: {
86
+ fontSize: 17,
87
+ fontWeight: '500',
88
+ lineHeight: 24,
89
+ textAlign: 'center',
90
+ minWidth: 160,
91
+ },
92
+ })
@@ -0,0 +1,2 @@
1
+ export { MonthPicker } from './MonthPicker'
2
+ export type { MonthPickerProps, MonthPickerValue } from './MonthPicker'