@retray-dev/ui-kit 1.0.0 → 1.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "1.0.0",
3
+ "version": "1.5.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -21,7 +21,13 @@
21
21
  "build": "tsup",
22
22
  "dev": "tsup --watch",
23
23
  "typecheck": "tsc --noEmit",
24
- "release": "pnpm typecheck && pnpm build && npm publish --access public"
24
+ "lint": "eslint src",
25
+ "lint:fix": "eslint src --fix",
26
+ "format": "prettier --write src",
27
+ "format:check": "prettier --check src",
28
+ "lint:all": "pnpm lint && pnpm --filter retray-ui-kit-example lint",
29
+ "format:all": "pnpm format && pnpm --filter retray-ui-kit-example format",
30
+ "deploy": "pnpm typecheck && pnpm build && npm publish --access public"
25
31
  },
26
32
  "keywords": [
27
33
  "react-native",
@@ -34,6 +40,7 @@
34
40
  "react": ">=17",
35
41
  "react-native": ">=0.70",
36
42
  "expo-haptics": ">=14.0.0",
43
+ "expo-linear-gradient": ">=13.0.0",
37
44
  "@gorhom/bottom-sheet": ">=5.0.0",
38
45
  "react-native-reanimated": ">=4.0.0",
39
46
  "react-native-gesture-handler": ">=2.0.0",
@@ -47,18 +54,28 @@
47
54
  "react-native": "0.81.5",
48
55
  "react-native-worklets": "0.5.1"
49
56
  },
50
- "onlyBuiltDependencies": ["esbuild"]
57
+ "onlyBuiltDependencies": [
58
+ "esbuild"
59
+ ]
51
60
  },
52
61
  "devDependencies": {
53
62
  "@gorhom/bottom-sheet": "^5.0.0",
54
63
  "@types/react": "^19.1.0",
55
64
  "expo-haptics": "~15.0.8",
65
+ "expo-linear-gradient": "~14.1.5",
56
66
  "react": "18.2.0",
57
67
  "react-native": "0.74.0",
58
68
  "react-native-gesture-handler": "~2.28.0",
59
69
  "react-native-reanimated": "~4.1.1",
60
70
  "react-native-worklets": "~0.5.0",
61
71
  "react-native-safe-area-context": "~5.6.2",
72
+ "eslint": "^9.0.0",
73
+ "@eslint/js": "^9.0.0",
74
+ "typescript-eslint": "^8.0.0",
75
+ "eslint-plugin-react": "^7.37.0",
76
+ "eslint-plugin-react-hooks": "^5.0.0",
77
+ "eslint-config-prettier": "^10.0.0",
78
+ "prettier": "^3.0.0",
62
79
  "tsup": "^8.0.0",
63
80
  "typescript": "^5.4.0"
64
81
  }
@@ -4,11 +4,16 @@ import {
4
4
  Text,
5
5
  TouchableOpacity,
6
6
  Animated,
7
- Easing,
8
7
  StyleSheet,
9
8
  LayoutChangeEvent,
10
9
  ViewStyle,
11
10
  } from 'react-native'
11
+ import ReanimatedAnimated, {
12
+ useSharedValue,
13
+ useAnimatedStyle,
14
+ withTiming,
15
+ Easing,
16
+ } from 'react-native-reanimated'
12
17
  import * as Haptics from 'expo-haptics'
13
18
  import { useTheme } from '../../theme'
14
19
 
@@ -40,25 +45,15 @@ function AccordionItemComponent({
40
45
  onToggle: () => void
41
46
  }) {
42
47
  const { colors } = useTheme()
43
- const animatedHeight = useRef(new Animated.Value(0)).current
44
- const animatedRotation = useRef(new Animated.Value(0)).current
48
+ const animatedHeight = useSharedValue(0)
49
+ const animatedRotation = useSharedValue(0)
45
50
  const contentHeight = useRef(0)
51
+ const scale = useRef(new Animated.Value(1)).current
46
52
 
47
53
  const toggle = (open: boolean) => {
48
- Animated.parallel([
49
- Animated.timing(animatedHeight, {
50
- toValue: open ? contentHeight.current : 0,
51
- duration: 220,
52
- easing: open ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
53
- useNativeDriver: false,
54
- }),
55
- Animated.timing(animatedRotation, {
56
- toValue: open ? 1 : 0,
57
- duration: 220,
58
- easing: open ? Easing.out(Easing.ease) : Easing.in(Easing.ease),
59
- useNativeDriver: true,
60
- }),
61
- ]).start()
54
+ const easing = open ? Easing.out(Easing.ease) : Easing.in(Easing.ease)
55
+ animatedHeight.value = withTiming(open ? contentHeight.current : 0, { duration: 220, easing })
56
+ animatedRotation.value = withTiming(open ? 1 : 0, { duration: 220, easing })
62
57
  }
63
58
 
64
59
  React.useEffect(() => {
@@ -68,37 +63,55 @@ function AccordionItemComponent({
68
63
  const onLayout = (e: LayoutChangeEvent) => {
69
64
  if (contentHeight.current === 0) {
70
65
  contentHeight.current = e.nativeEvent.layout.height
71
- if (isOpen) animatedHeight.setValue(contentHeight.current)
66
+ if (isOpen) animatedHeight.value = contentHeight.current
72
67
  }
73
68
  }
74
69
 
75
- const rotate = animatedRotation.interpolate({
76
- inputRange: [0, 1],
77
- outputRange: ['0deg', '180deg'],
78
- })
70
+ const heightStyle = useAnimatedStyle(() => ({
71
+ height: animatedHeight.value,
72
+ overflow: 'hidden',
73
+ }))
74
+
75
+ const rotationStyle = useAnimatedStyle(() => ({
76
+ transform: [{ rotate: `${animatedRotation.value * 180}deg` }],
77
+ }))
78
+
79
+ const handlePressIn = () => {
80
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
81
+ }
82
+
83
+ const handlePressOut = () => {
84
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
85
+ }
79
86
 
80
87
  return (
81
88
  <View style={[styles.item, { borderBottomColor: colors.border }]}>
82
- <TouchableOpacity
83
- style={styles.trigger}
84
- onPress={() => { Haptics.selectionAsync(); onToggle() }}
85
- activeOpacity={0.7}
86
- >
87
- <Text style={[styles.triggerText, { color: colors.foreground }]}>
88
- {item.trigger}
89
- </Text>
90
- <Animated.Text
91
- style={[styles.chevron, { color: colors.foreground, transform: [{ rotate }] }]}
89
+ <Animated.View style={{ transform: [{ scale }] }}>
90
+ <TouchableOpacity
91
+ style={styles.trigger}
92
+ onPress={() => {
93
+ Haptics.selectionAsync()
94
+ onToggle()
95
+ }}
96
+ onPressIn={handlePressIn}
97
+ onPressOut={handlePressOut}
98
+ activeOpacity={1}
99
+ touchSoundDisabled={true}
92
100
  >
93
-
94
- </Animated.Text>
95
- </TouchableOpacity>
101
+ <Text style={[styles.triggerText, { color: colors.foreground }]}>{item.trigger}</Text>
102
+ <ReanimatedAnimated.Text
103
+ style={[styles.chevron, { color: colors.foreground }, rotationStyle]}
104
+ >
105
+
106
+ </ReanimatedAnimated.Text>
107
+ </TouchableOpacity>
108
+ </Animated.View>
96
109
 
97
- <Animated.View style={[styles.contentWrapper, { height: animatedHeight, overflow: 'hidden' }]}>
110
+ <ReanimatedAnimated.View style={heightStyle}>
98
111
  <View style={styles.content} onLayout={onLayout}>
99
112
  {item.content}
100
113
  </View>
101
- </Animated.View>
114
+ </ReanimatedAnimated.View>
102
115
  </View>
103
116
  )
104
117
  }
@@ -152,7 +165,6 @@ const styles = StyleSheet.create({
152
165
  fontSize: 16,
153
166
  marginLeft: 8,
154
167
  },
155
- contentWrapper: {},
156
168
  content: {
157
169
  paddingBottom: 16,
158
170
  position: 'absolute',
@@ -24,7 +24,9 @@ export function Alert({ title, description, variant = 'default', icon, style }:
24
24
  {icon ? <View style={styles.icon}>{icon}</View> : null}
25
25
  <View style={styles.content}>
26
26
  {title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
27
- {description ? <Text style={[styles.description, { color: descColor }]}>{description}</Text> : null}
27
+ {description ? (
28
+ <Text style={[styles.description, { color: descColor }]}>{description}</Text>
29
+ ) : null}
28
30
  </View>
29
31
  </View>
30
32
  )
@@ -5,7 +5,9 @@ import { useTheme } from '../../theme'
5
5
  export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl'
6
6
 
7
7
  export interface AvatarProps {
8
+ /** Remote image URI. Falls back to `fallback` initials on error or when omitted. */
8
9
  src?: string
10
+ /** Up to 2 characters shown when the image is unavailable. Auto-uppercased. Defaults to `'?'`. */
9
11
  fallback?: string
10
12
  size?: AvatarSize
11
13
  style?: ViewStyle
@@ -48,7 +50,9 @@ export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
48
50
  onError={() => setImageError(true)}
49
51
  />
50
52
  ) : (
51
- <Text style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}>
53
+ <Text
54
+ style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
55
+ >
52
56
  {fallback?.slice(0, 2).toUpperCase() ?? '?'}
53
57
  </Text>
54
58
  )}
@@ -29,7 +29,7 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
29
29
 
30
30
  return (
31
31
  <View style={[styles.container, containerStyle, style]}>
32
- <Text style={[styles.label, { color: textColor }]}>{label}</Text>
32
+ <Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>{label}</Text>
33
33
  </View>
34
34
  )
35
35
  }
@@ -28,6 +28,10 @@ export interface ButtonProps extends TouchableOpacityProps {
28
28
  /** Replaces the label with a spinner and forces `disabled`. */
29
29
  loading?: boolean
30
30
  fullWidth?: boolean
31
+ /** Icon rendered alongside the label. */
32
+ icon?: React.ReactNode
33
+ /** Side the icon appears on. Defaults to `'left'`. */
34
+ iconPosition?: 'left' | 'right'
31
35
  }
32
36
 
33
37
  const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
@@ -48,6 +52,8 @@ export function Button({
48
52
  size = 'md',
49
53
  loading = false,
50
54
  fullWidth = false,
55
+ icon,
56
+ iconPosition = 'left',
51
57
  disabled,
52
58
  style,
53
59
  onPress,
@@ -59,11 +65,16 @@ export function Button({
59
65
 
60
66
  const handlePressIn = () => {
61
67
  if (isDisabled) return
62
- Animated.spring(scale, { toValue: 0.97, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
68
+ Animated.spring(scale, {
69
+ toValue: 0.95,
70
+ useNativeDriver: true,
71
+ speed: 40,
72
+ bounciness: 0,
73
+ }).start()
63
74
  }
64
75
 
65
76
  const handlePressOut = () => {
66
- Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 2 }).start()
77
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
67
78
  }
68
79
 
69
80
  const handlePress: TouchableOpacityProps['onPress'] = (e) => {
@@ -85,9 +96,8 @@ export function Button({
85
96
  ghost: { color: colors.foreground },
86
97
  }[variant]
87
98
 
88
- const spinnerColor = variant === 'primary' || variant === 'secondary'
89
- ? colors.primaryForeground
90
- : colors.foreground
99
+ const spinnerColor =
100
+ variant === 'primary' || variant === 'secondary' ? colors.primaryForeground : colors.foreground
91
101
 
92
102
  return (
93
103
  <Animated.View style={[fullWidth && styles.fullWidth, { transform: [{ scale }] }]}>
@@ -102,6 +112,7 @@ export function Button({
102
112
  ]}
103
113
  disabled={isDisabled}
104
114
  activeOpacity={1}
115
+ touchSoundDisabled={true}
105
116
  onPress={handlePress}
106
117
  onPressIn={handlePressIn}
107
118
  onPressOut={handlePressOut}
@@ -110,9 +121,11 @@ export function Button({
110
121
  {loading ? (
111
122
  <ActivityIndicator size="small" color={spinnerColor} />
112
123
  ) : (
113
- <Text style={[styles.label, labelVariantStyle, labelSizeStyles[size]]}>
114
- {label}
115
- </Text>
124
+ <>
125
+ {icon && iconPosition === 'left' && <>{icon}</>}
126
+ <Text style={[styles.label, labelVariantStyle, labelSizeStyles[size], icon ? styles.labelWithIcon : undefined]}>{label}</Text>
127
+ {icon && iconPosition === 'right' && <>{icon}</>}
128
+ </>
116
129
  )}
117
130
  </TouchableOpacity>
118
131
  </Animated.View>
@@ -135,4 +148,7 @@ const styles = StyleSheet.create({
135
148
  label: {
136
149
  fontWeight: '600',
137
150
  },
151
+ labelWithIcon: {
152
+ marginHorizontal: 6,
153
+ },
138
154
  })
@@ -36,11 +36,7 @@ export function Card({ children, style }: CardProps) {
36
36
  const { colors } = useTheme()
37
37
  return (
38
38
  <View
39
- style={[
40
- styles.card,
41
- { backgroundColor: colors.card, borderColor: colors.border },
42
- style,
43
- ]}
39
+ style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }, style]}
44
40
  >
45
41
  {children}
46
42
  </View>
@@ -53,9 +49,7 @@ export function CardHeader({ children, style }: CardHeaderProps) {
53
49
 
54
50
  export function CardTitle({ children, style }: CardTitleProps) {
55
51
  const { colors } = useTheme()
56
- return (
57
- <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
58
- )
52
+ return <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
59
53
  }
60
54
 
61
55
  export function CardDescription({ children, style }: CardDescriptionProps) {
@@ -1,5 +1,5 @@
1
- import React from 'react'
2
- import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native'
1
+ import React, { useRef } from 'react'
2
+ import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
3
3
  import * as Haptics from 'expo-haptics'
4
4
  import { useTheme } from '../../theme'
5
5
 
@@ -11,15 +11,38 @@ export interface CheckboxProps {
11
11
  style?: ViewStyle
12
12
  }
13
13
 
14
- export function Checkbox({ checked = false, onCheckedChange, label, disabled, style }: CheckboxProps) {
14
+ export function Checkbox({
15
+ checked = false,
16
+ onCheckedChange,
17
+ label,
18
+ disabled,
19
+ style,
20
+ }: CheckboxProps) {
15
21
  const { colors } = useTheme()
22
+ const scale = useRef(new Animated.Value(1)).current
23
+
24
+ const handlePressIn = () => {
25
+ if (disabled) return
26
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
27
+ }
28
+
29
+ const handlePressOut = () => {
30
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
31
+ }
16
32
 
17
33
  return (
34
+ <Animated.View style={{ transform: [{ scale }] }}>
18
35
  <TouchableOpacity
19
36
  style={[styles.row, style]}
20
- onPress={() => { Haptics.selectionAsync(); onCheckedChange?.(!checked) }}
37
+ onPress={() => {
38
+ Haptics.selectionAsync()
39
+ onCheckedChange?.(!checked)
40
+ }}
41
+ onPressIn={handlePressIn}
42
+ onPressOut={handlePressOut}
21
43
  disabled={disabled}
22
- activeOpacity={0.7}
44
+ activeOpacity={1}
45
+ touchSoundDisabled={true}
23
46
  >
24
47
  <View
25
48
  style={[
@@ -31,14 +54,19 @@ export function Checkbox({ checked = false, onCheckedChange, label, disabled, st
31
54
  },
32
55
  ]}
33
56
  >
34
- {checked ? <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} /> : null}
57
+ {checked ? (
58
+ <View style={[styles.checkmark, { borderColor: colors.primaryForeground }]} />
59
+ ) : null}
35
60
  </View>
36
61
  {label ? (
37
- <Text style={[styles.label, { color: disabled ? colors.mutedForeground : colors.foreground }]}>
62
+ <Text
63
+ style={[styles.label, { color: disabled ? colors.mutedForeground : colors.foreground }]}
64
+ >
38
65
  {label}
39
66
  </Text>
40
67
  ) : null}
41
68
  </TouchableOpacity>
69
+ </Animated.View>
42
70
  )
43
71
  }
44
72
 
@@ -16,9 +16,7 @@ export function EmptyState({ icon, title, description, action, style }: EmptySta
16
16
  return (
17
17
  <View style={[styles.container, { borderColor: colors.border }, style]}>
18
18
  {icon ? (
19
- <View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>
20
- {icon}
21
- </View>
19
+ <View style={[styles.iconWrapper, { backgroundColor: colors.muted }]}>{icon}</View>
22
20
  ) : null}
23
21
  <View style={styles.textWrapper}>
24
22
  <Text style={[styles.title, { color: colors.foreground }]}>{title}</Text>
@@ -1,22 +1,24 @@
1
1
  import React, { useState } from 'react'
2
- import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native'
2
+ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
5
  export interface InputProps extends TextInputProps {
6
6
  label?: string
7
+ /** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
7
8
  error?: string
9
+ /** Helper text shown below the input when there is no error. */
8
10
  hint?: string
11
+ /** Style for the outer container `View`. Use `style` (from `TextInputProps`) to style the `TextInput` itself. */
12
+ containerStyle?: ViewStyle
9
13
  }
10
14
 
11
- export function Input({ label, error, hint, style, onFocus, onBlur, ...props }: InputProps) {
15
+ export function Input({ label, error, hint, containerStyle, style, onFocus, onBlur, ...props }: InputProps) {
12
16
  const { colors } = useTheme()
13
17
  const [focused, setFocused] = useState(false)
14
18
 
15
19
  return (
16
- <View style={styles.container}>
17
- {label ? (
18
- <Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
19
- ) : null}
20
+ <View style={[styles.container, containerStyle]}>
21
+ {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
20
22
  <TextInput
21
23
  style={[
22
24
  styles.input,
@@ -27,17 +29,23 @@ export function Input({ label, error, hint, style, onFocus, onBlur, ...props }:
27
29
  },
28
30
  style,
29
31
  ]}
30
- onFocus={(e) => { setFocused(true); onFocus?.(e) }}
31
- onBlur={(e) => { setFocused(false); onBlur?.(e) }}
32
+ onFocus={(e) => {
33
+ setFocused(true)
34
+ onFocus?.(e)
35
+ }}
36
+ onBlur={(e) => {
37
+ setFocused(false)
38
+ onBlur?.(e)
39
+ }}
32
40
  placeholderTextColor={colors.mutedForeground}
33
41
  allowFontScaling={true}
34
42
  {...props}
35
43
  />
36
44
  {error ? (
37
- <Text style={[styles.helperText, { color: colors.destructive }]}>{error}</Text>
45
+ <Text style={[styles.helperText, { color: colors.destructive }]} allowFontScaling={true}>{error}</Text>
38
46
  ) : null}
39
47
  {!error && hint ? (
40
- <Text style={[styles.helperText, { color: colors.mutedForeground }]}>{hint}</Text>
48
+ <Text style={[styles.helperText, { color: colors.mutedForeground }]} allowFontScaling={true}>{hint}</Text>
41
49
  ) : null}
42
50
  </View>
43
51
  )
@@ -3,7 +3,9 @@ import { View, Animated, StyleSheet, ViewStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
5
  export interface ProgressProps {
6
+ /** Current progress value. Clamped to `[0, max]`. Defaults to `0`. */
6
7
  value?: number
8
+ /** Maximum value. Defaults to `100`. */
7
9
  max?: number
8
10
  style?: ViewStyle
9
11
  }
@@ -30,10 +32,7 @@ export function Progress({ value = 0, max = 100, style }: ProgressProps) {
30
32
  onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
31
33
  >
32
34
  <Animated.View
33
- style={[
34
- styles.indicator,
35
- { width: animatedWidth, backgroundColor: colors.primary },
36
- ]}
35
+ style={[styles.indicator, { width: animatedWidth, backgroundColor: colors.primary }]}
37
36
  />
38
37
  </View>
39
38
  )
@@ -1,5 +1,5 @@
1
- import React from 'react'
2
- import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native'
1
+ import React, { useRef } from 'react'
2
+ import { TouchableOpacity, Animated, View, Text, StyleSheet, ViewStyle } from 'react-native'
3
3
  import * as Haptics from 'expo-haptics'
4
4
  import { useTheme } from '../../theme'
5
5
 
@@ -17,6 +17,67 @@ export interface RadioGroupProps {
17
17
  style?: ViewStyle
18
18
  }
19
19
 
20
+ function RadioItem({
21
+ option,
22
+ selected,
23
+ onSelect,
24
+ }: {
25
+ option: RadioOption
26
+ selected: boolean
27
+ onSelect: () => void
28
+ }) {
29
+ const { colors } = useTheme()
30
+ const scale = useRef(new Animated.Value(1)).current
31
+
32
+ const handlePressIn = () => {
33
+ if (option.disabled) return
34
+ Animated.spring(scale, { toValue: 0.95, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
35
+ }
36
+
37
+ const handlePressOut = () => {
38
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
39
+ }
40
+
41
+ return (
42
+ <Animated.View style={{ transform: [{ scale }] }}>
43
+ <TouchableOpacity
44
+ style={styles.row}
45
+ onPress={() => {
46
+ if (!option.disabled) {
47
+ Haptics.selectionAsync()
48
+ onSelect()
49
+ }
50
+ }}
51
+ onPressIn={handlePressIn}
52
+ onPressOut={handlePressOut}
53
+ activeOpacity={1}
54
+ touchSoundDisabled={true}
55
+ disabled={option.disabled}
56
+ >
57
+ <View
58
+ style={[
59
+ styles.radio,
60
+ {
61
+ borderColor: selected ? colors.primary : colors.border,
62
+ opacity: option.disabled ? 0.45 : 1,
63
+ },
64
+ ]}
65
+ >
66
+ {selected ? <View style={[styles.dot, { backgroundColor: colors.primary }]} /> : null}
67
+ </View>
68
+ <Text
69
+ style={[
70
+ styles.label,
71
+ { color: option.disabled ? colors.mutedForeground : colors.foreground },
72
+ ]}
73
+ >
74
+ {option.label}
75
+ </Text>
76
+ </TouchableOpacity>
77
+ </Animated.View>
78
+ )
79
+ }
80
+
20
81
  export function RadioGroup({
21
82
  options,
22
83
  value,
@@ -24,50 +85,16 @@ export function RadioGroup({
24
85
  orientation = 'vertical',
25
86
  style,
26
87
  }: RadioGroupProps) {
27
- const { colors } = useTheme()
28
-
29
88
  return (
30
- <View
31
- style={[
32
- styles.container,
33
- orientation === 'horizontal' && styles.horizontal,
34
- style,
35
- ]}
36
- >
37
- {options.map((option) => {
38
- const selected = option.value === value
39
- return (
40
- <TouchableOpacity
41
- key={option.value}
42
- style={styles.row}
43
- onPress={() => { if (!option.disabled) { Haptics.selectionAsync(); onValueChange?.(option.value) } }}
44
- activeOpacity={0.7}
45
- disabled={option.disabled}
46
- >
47
- <View
48
- style={[
49
- styles.radio,
50
- {
51
- borderColor: selected ? colors.primary : colors.border,
52
- opacity: option.disabled ? 0.45 : 1,
53
- },
54
- ]}
55
- >
56
- {selected ? (
57
- <View style={[styles.dot, { backgroundColor: colors.primary }]} />
58
- ) : null}
59
- </View>
60
- <Text
61
- style={[
62
- styles.label,
63
- { color: option.disabled ? colors.mutedForeground : colors.foreground },
64
- ]}
65
- >
66
- {option.label}
67
- </Text>
68
- </TouchableOpacity>
69
- )
70
- })}
89
+ <View style={[styles.container, orientation === 'horizontal' && styles.horizontal, style]}>
90
+ {options.map((option) => (
91
+ <RadioItem
92
+ key={option.value}
93
+ option={option}
94
+ selected={option.value === value}
95
+ onSelect={() => onValueChange?.(option.value)}
96
+ />
97
+ ))}
71
98
  </View>
72
99
  )
73
100
  }