@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "1.7.0",
3
+ "version": "2.3.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -45,7 +45,9 @@
45
45
  "react-native-reanimated": ">=4.0.0",
46
46
  "react-native-gesture-handler": ">=2.0.0",
47
47
  "react-native-worklets": ">=0.8.0",
48
- "react-native-safe-area-context": ">=4.0.0"
48
+ "react-native-safe-area-context": ">=4.0.0",
49
+ "@react-native-picker/picker": ">=2.0.0",
50
+ "@react-native-community/slider": ">=4.0.0"
49
51
  },
50
52
  "pnpm": {
51
53
  "overrides": {
@@ -60,6 +62,8 @@
60
62
  },
61
63
  "devDependencies": {
62
64
  "@gorhom/bottom-sheet": "^5.0.0",
65
+ "@react-native-picker/picker": "2.11.4",
66
+ "@react-native-community/slider": "^4.5.5",
63
67
  "@types/react": "^19.1.0",
64
68
  "expo-haptics": "~15.0.8",
65
69
  "expo-linear-gradient": "~15.0.8",
@@ -2,22 +2,36 @@ import React from 'react'
2
2
  import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
- export type AlertVariant = 'default' | 'destructive'
5
+ export type AlertBannerVariant = 'default' | 'destructive' | 'success'
6
6
 
7
- export interface AlertProps {
7
+ export interface AlertBannerProps {
8
8
  title?: string
9
9
  description?: string
10
- variant?: AlertVariant
10
+ variant?: AlertBannerVariant
11
11
  icon?: React.ReactNode
12
12
  style?: ViewStyle
13
13
  }
14
14
 
15
- export function Alert({ title, description, variant = 'default', icon, style }: AlertProps) {
15
+ export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
16
16
  const { colors } = useTheme()
17
17
 
18
- const borderColor = variant === 'destructive' ? colors.destructive : colors.border
19
- const titleColor = variant === 'destructive' ? colors.destructive : colors.foreground
20
- const descColor = variant === 'destructive' ? colors.destructive : colors.mutedForeground
18
+ const borderColor =
19
+ variant === 'destructive' ? colors.destructive
20
+ : variant === 'success' ? colors.success
21
+ : colors.border
22
+
23
+ const titleColor =
24
+ variant === 'destructive' ? colors.destructive
25
+ : variant === 'success' ? colors.success
26
+ : colors.foreground
27
+
28
+ const descColor =
29
+ variant === 'destructive' ? colors.destructive
30
+ : variant === 'success' ? colors.success
31
+ : colors.mutedForeground
32
+
33
+ const defaultIcon =
34
+ variant === 'destructive' ? '⚠' : variant === 'success' ? '✓' : 'ℹ'
21
35
 
22
36
  return (
23
37
  <View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
@@ -25,15 +39,13 @@ export function Alert({ title, description, variant = 'default', icon, style }:
25
39
  <View style={styles.icon}>{icon}</View>
26
40
  ) : (
27
41
  <View style={styles.icon}>
28
- <Text style={[styles.defaultIcon, { color: titleColor }]}>
29
- {variant === 'destructive' ? '⚠' : 'ℹ'}
30
- </Text>
42
+ <Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
31
43
  </View>
32
44
  )}
33
45
  <View style={styles.content}>
34
- {title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
46
+ {title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
35
47
  {description ? (
36
- <Text style={[styles.description, { color: descColor }]}>{description}</Text>
48
+ <Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
37
49
  ) : null}
38
50
  </View>
39
51
  </View>
@@ -0,0 +1,83 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export type AlertBannerVariant = 'default' | 'destructive' | 'success'
6
+
7
+ export interface AlertBannerProps {
8
+ title?: string
9
+ description?: string
10
+ variant?: AlertBannerVariant
11
+ icon?: React.ReactNode
12
+ style?: ViewStyle
13
+ }
14
+
15
+ export function AlertBanner({ title, description, variant = 'default', icon, style }: AlertBannerProps) {
16
+ const { colors } = useTheme()
17
+
18
+ const borderColor =
19
+ variant === 'destructive' ? colors.destructive
20
+ : variant === 'success' ? colors.success
21
+ : colors.border
22
+
23
+ const titleColor =
24
+ variant === 'destructive' ? colors.destructive
25
+ : variant === 'success' ? colors.success
26
+ : colors.foreground
27
+
28
+ const descColor =
29
+ variant === 'destructive' ? colors.destructive
30
+ : variant === 'success' ? colors.success
31
+ : colors.mutedForeground
32
+
33
+ const defaultIcon =
34
+ variant === 'destructive' ? '⚠' : variant === 'success' ? '✓' : 'ℹ'
35
+
36
+ return (
37
+ <View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
38
+ {icon ? (
39
+ <View style={styles.icon}>{icon}</View>
40
+ ) : (
41
+ <View style={styles.icon}>
42
+ <Text style={[styles.defaultIcon, { color: titleColor }]} allowFontScaling={true}>{defaultIcon}</Text>
43
+ </View>
44
+ )}
45
+ <View style={styles.content}>
46
+ {title ? <Text style={[styles.title, { color: titleColor }]} allowFontScaling={true}>{title}</Text> : null}
47
+ {description ? (
48
+ <Text style={[styles.description, { color: descColor }]} allowFontScaling={true}>{description}</Text>
49
+ ) : null}
50
+ </View>
51
+ </View>
52
+ )
53
+ }
54
+
55
+ const styles = StyleSheet.create({
56
+ container: {
57
+ flexDirection: 'row',
58
+ borderWidth: 1,
59
+ borderRadius: 8,
60
+ padding: 16,
61
+ gap: 12,
62
+ },
63
+ icon: {
64
+ marginTop: 2,
65
+ },
66
+ content: {
67
+ flex: 1,
68
+ gap: 4,
69
+ },
70
+ title: {
71
+ fontSize: 14,
72
+ fontWeight: '500',
73
+ lineHeight: 20,
74
+ },
75
+ description: {
76
+ fontSize: 14,
77
+ lineHeight: 20,
78
+ },
79
+ defaultIcon: {
80
+ fontSize: 18,
81
+ fontWeight: '700',
82
+ },
83
+ })
@@ -0,0 +1,2 @@
1
+ export { AlertBanner } from './AlertBanner'
2
+ export type { AlertBannerProps, AlertBannerVariant } from './AlertBanner'
@@ -52,6 +52,7 @@ export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
52
52
  ) : (
53
53
  <Text
54
54
  style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
55
+ allowFontScaling={true}
55
56
  >
56
57
  {fallback?.slice(0, 2).toUpperCase() ?? '?'}
57
58
  </Text>
@@ -1,16 +1,40 @@
1
1
  import React from 'react'
2
- import { View, Text, StyleSheet, ViewStyle } from 'react-native'
2
+ import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
3
3
  import { useTheme } from '../../theme'
4
4
 
5
5
  export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
6
+ export type BadgeSize = 'sm' | 'md' | 'lg'
6
7
 
7
8
  export interface BadgeProps {
8
- label: string
9
+ label?: string
10
+ /** Alternative to \`label\` — allows JSX children. */
11
+ children?: React.ReactNode
9
12
  variant?: BadgeVariant
13
+ size?: BadgeSize
14
+ /** Icon rendered before the label/children. */
15
+ icon?: React.ReactNode
10
16
  style?: ViewStyle
11
17
  }
12
18
 
13
- export function Badge({ label, variant = 'default', style }: BadgeProps) {
19
+ const sizePadding: Record<BadgeSize, ViewStyle> = {
20
+ sm: { paddingHorizontal: 8, paddingVertical: 2 },
21
+ md: { paddingHorizontal: 10, paddingVertical: 4 },
22
+ lg: { paddingHorizontal: 12, paddingVertical: 6 },
23
+ }
24
+
25
+ const sizeFontSize: Record<BadgeSize, TextStyle> = {
26
+ sm: { fontSize: 11 },
27
+ md: { fontSize: 13 },
28
+ lg: { fontSize: 15 },
29
+ }
30
+
31
+ const sizeIconGap: Record<BadgeSize, number> = {
32
+ sm: 4,
33
+ md: 6,
34
+ lg: 6,
35
+ }
36
+
37
+ export function Badge({ label, children, variant = 'default', size = 'md', icon, style }: BadgeProps) {
14
38
  const { colors } = useTheme()
15
39
 
16
40
  const containerStyle: ViewStyle = {
@@ -27,9 +51,18 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
27
51
  outline: colors.foreground,
28
52
  }[variant]
29
53
 
54
+ const content = children ?? label
55
+
30
56
  return (
31
- <View style={[styles.container, containerStyle, style]}>
32
- <Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>{label}</Text>
57
+ <View style={[styles.container, containerStyle, sizePadding[size], { gap: sizeIconGap[size] }, style]}>
58
+ {icon ? <View style={styles.iconContainer}>{icon}</View> : null}
59
+ {typeof content === 'string' ? (
60
+ <Text style={[styles.label, { color: textColor }, sizeFontSize[size]]} allowFontScaling={true}>
61
+ {content}
62
+ </Text>
63
+ ) : (
64
+ content
65
+ )}
33
66
  </View>
34
67
  )
35
68
  }
@@ -37,12 +70,15 @@ export function Badge({ label, variant = 'default', style }: BadgeProps) {
37
70
  const styles = StyleSheet.create({
38
71
  container: {
39
72
  borderRadius: 8,
40
- paddingHorizontal: 10,
41
- paddingVertical: 4,
42
73
  alignSelf: 'flex-start',
74
+ flexDirection: 'row',
75
+ alignItems: 'center',
76
+ },
77
+ iconContainer: {
78
+ justifyContent: 'center',
79
+ alignItems: 'center',
43
80
  },
44
81
  label: {
45
- fontSize: 13,
46
82
  fontWeight: '500',
47
83
  },
48
84
  })
@@ -8,11 +8,14 @@ import {
8
8
  TouchableOpacityProps,
9
9
  ViewStyle,
10
10
  TextStyle,
11
+ Platform,
11
12
  } from 'react-native'
13
+
14
+ const nativeDriver = Platform.OS !== 'web'
12
15
  import * as Haptics from 'expo-haptics'
13
16
  import { useTheme } from '../../theme'
14
17
 
15
- export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
18
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'
16
19
  export type ButtonSize = 'sm' | 'md' | 'lg'
17
20
 
18
21
  export interface ButtonProps extends TouchableOpacityProps {
@@ -67,14 +70,14 @@ export function Button({
67
70
  if (isDisabled) return
68
71
  Animated.spring(scale, {
69
72
  toValue: 0.95,
70
- useNativeDriver: true,
73
+ useNativeDriver: nativeDriver,
71
74
  speed: 40,
72
75
  bounciness: 0,
73
76
  }).start()
74
77
  }
75
78
 
76
79
  const handlePressOut = () => {
77
- Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
80
+ Animated.spring(scale, { toValue: 1, useNativeDriver: nativeDriver, speed: 40, bounciness: 4 }).start()
78
81
  }
79
82
 
80
83
  const handlePress: TouchableOpacityProps['onPress'] = (e) => {
@@ -87,6 +90,7 @@ export function Button({
87
90
  secondary: { backgroundColor: colors.secondary },
88
91
  outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
89
92
  ghost: { backgroundColor: 'transparent' },
93
+ destructive: { backgroundColor: colors.destructive },
90
94
  }[variant]
91
95
 
92
96
  const labelVariantStyle: TextStyle = {
@@ -94,10 +98,13 @@ export function Button({
94
98
  secondary: { color: colors.secondaryForeground },
95
99
  outline: { color: colors.foreground },
96
100
  ghost: { color: colors.foreground },
101
+ destructive: { color: colors.destructiveForeground },
97
102
  }[variant]
98
103
 
99
104
  const spinnerColor =
100
- variant === 'primary' || variant === 'secondary' ? colors.primaryForeground : colors.foreground
105
+ variant === 'destructive' ? colors.destructiveForeground
106
+ : variant === 'primary' || variant === 'secondary' ? colors.primaryForeground
107
+ : colors.foreground
101
108
 
102
109
  return (
103
110
  <Animated.View style={[fullWidth && styles.fullWidth, { transform: [{ scale }] }]}>
@@ -143,7 +150,7 @@ const styles = StyleSheet.create({
143
150
  width: '100%',
144
151
  },
145
152
  disabled: {
146
- opacity: 0.45,
153
+ opacity: 0.5,
147
154
  },
148
155
  label: {
149
156
  fontWeight: '600',
@@ -1,9 +1,18 @@
1
- import React from 'react'
2
- import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
1
+ import React, { useRef } from 'react'
2
+ import { View, Text, TouchableOpacity, Animated, StyleSheet, ViewStyle, TextStyle, Platform } from 'react-native'
3
+ import * as Haptics from 'expo-haptics'
3
4
  import { useTheme } from '../../theme'
4
5
 
6
+ const nativeDriver = Platform.OS !== 'web'
7
+
8
+ export type CardVariant = 'elevated' | 'outlined' | 'filled'
9
+
5
10
  export interface CardProps {
6
11
  children: React.ReactNode
12
+ /** Visual style variant. `'elevated'` (default) has shadow, `'outlined'` has border only, `'filled'` uses accent background. */
13
+ variant?: CardVariant
14
+ /** Makes the card tappable. Adds press animation and haptic feedback. */
15
+ onPress?: () => void
7
16
  style?: ViewStyle
8
17
  }
9
18
 
@@ -32,15 +41,83 @@ export interface CardFooterProps {
32
41
  style?: ViewStyle
33
42
  }
34
43
 
35
- export function Card({ children, style }: CardProps) {
44
+ export function Card({ children, variant = 'elevated', onPress, style }: CardProps) {
36
45
  const { colors } = useTheme()
37
- return (
38
- <View
39
- style={[styles.card, { backgroundColor: colors.card, borderColor: colors.border }, style]}
40
- >
46
+ const scale = useRef(new Animated.Value(1)).current
47
+
48
+ const handlePressIn = () => {
49
+ if (!onPress) return
50
+ Animated.spring(scale, {
51
+ toValue: 0.98,
52
+ useNativeDriver: nativeDriver,
53
+ speed: 40,
54
+ bounciness: 0,
55
+ }).start()
56
+ }
57
+
58
+ const handlePressOut = () => {
59
+ if (!onPress) return
60
+ Animated.spring(scale, {
61
+ toValue: 1,
62
+ useNativeDriver: nativeDriver,
63
+ speed: 40,
64
+ bounciness: 4,
65
+ }).start()
66
+ }
67
+
68
+ const handlePress = () => {
69
+ if (!onPress) return
70
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
71
+ onPress()
72
+ }
73
+
74
+ const variantStyle: ViewStyle = {
75
+ elevated: {
76
+ backgroundColor: colors.card,
77
+ borderColor: colors.border,
78
+ shadowColor: '#000',
79
+ shadowOffset: { width: 0, height: 1 },
80
+ shadowOpacity: 0.05,
81
+ shadowRadius: 2,
82
+ elevation: 1,
83
+ },
84
+ outlined: {
85
+ backgroundColor: colors.card,
86
+ borderColor: colors.border,
87
+ shadowOpacity: 0,
88
+ elevation: 0,
89
+ },
90
+ filled: {
91
+ backgroundColor: colors.accent,
92
+ borderColor: colors.border,
93
+ shadowOpacity: 0,
94
+ elevation: 0,
95
+ },
96
+ }[variant]
97
+
98
+ const cardContent = (
99
+ <View style={[styles.card, variantStyle, style]}>
41
100
  {children}
42
101
  </View>
43
102
  )
103
+
104
+ if (onPress) {
105
+ return (
106
+ <Animated.View style={{ transform: [{ scale }] }}>
107
+ <TouchableOpacity
108
+ onPress={handlePress}
109
+ onPressIn={handlePressIn}
110
+ onPressOut={handlePressOut}
111
+ activeOpacity={1}
112
+ touchSoundDisabled={true}
113
+ >
114
+ {cardContent}
115
+ </TouchableOpacity>
116
+ </Animated.View>
117
+ )
118
+ }
119
+
120
+ return cardContent
44
121
  }
45
122
 
46
123
  export function CardHeader({ children, style }: CardHeaderProps) {
@@ -49,13 +126,13 @@ export function CardHeader({ children, style }: CardHeaderProps) {
49
126
 
50
127
  export function CardTitle({ children, style }: CardTitleProps) {
51
128
  const { colors } = useTheme()
52
- return <Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
129
+ return <Text style={[styles.title, { color: colors.cardForeground }, style]} allowFontScaling={true}>{children}</Text>
53
130
  }
54
131
 
55
132
  export function CardDescription({ children, style }: CardDescriptionProps) {
56
133
  const { colors } = useTheme()
57
134
  return (
58
- <Text style={[styles.description, { color: colors.mutedForeground }, style]}>{children}</Text>
135
+ <Text style={[styles.description, { color: colors.mutedForeground }, style]} allowFontScaling={true}>{children}</Text>
59
136
  )
60
137
  }
61
138
 
@@ -0,0 +1,173 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import {
3
+ TouchableOpacity,
4
+ Animated,
5
+ View,
6
+ Text,
7
+ StyleSheet,
8
+ ViewStyle,
9
+ Platform,
10
+ Easing,
11
+ } from 'react-native'
12
+ import * as Haptics from 'expo-haptics'
13
+ import { useTheme } from '../../theme'
14
+
15
+ const nativeDriver = Platform.OS !== 'web'
16
+
17
+ export interface ChipProps {
18
+ label: string
19
+ selected?: boolean
20
+ onPress?: () => void
21
+ style?: ViewStyle
22
+ }
23
+
24
+ export interface ChipOption {
25
+ label: string
26
+ value: string | number
27
+ }
28
+
29
+ export interface ChipGroupProps {
30
+ options: ChipOption[]
31
+ value?: string | number | (string | number)[]
32
+ onValueChange?: (value: string | number | (string | number)[]) => void
33
+ /** When true, allows selecting multiple chips. `value` and `onValueChange` will use arrays. */
34
+ multiSelect?: boolean
35
+ style?: ViewStyle
36
+ }
37
+
38
+ export function Chip({ label, selected = false, onPress, style }: ChipProps) {
39
+ const { colors } = useTheme()
40
+ const scale = useRef(new Animated.Value(1)).current
41
+ const pressAnim = useRef(new Animated.Value(selected ? 1 : 0)).current
42
+
43
+ useEffect(() => {
44
+ Animated.timing(pressAnim, {
45
+ toValue: selected ? 1 : 0,
46
+ duration: 150,
47
+ easing: Easing.out(Easing.ease),
48
+ useNativeDriver: false,
49
+ }).start()
50
+ }, [selected, pressAnim])
51
+
52
+ const handlePressIn = () => {
53
+ Animated.spring(scale, {
54
+ toValue: 0.95,
55
+ useNativeDriver: nativeDriver,
56
+ speed: 40,
57
+ bounciness: 0,
58
+ }).start()
59
+ }
60
+
61
+ const handlePressOut = () => {
62
+ Animated.spring(scale, {
63
+ toValue: 1,
64
+ useNativeDriver: nativeDriver,
65
+ speed: 40,
66
+ bounciness: 4,
67
+ }).start()
68
+ }
69
+
70
+ const handlePress = () => {
71
+ Haptics.selectionAsync()
72
+ onPress?.()
73
+ }
74
+
75
+ const backgroundColor = pressAnim.interpolate({
76
+ inputRange: [0, 1],
77
+ outputRange: [colors.secondary, colors.primary],
78
+ })
79
+
80
+ const textColor = pressAnim.interpolate({
81
+ inputRange: [0, 1],
82
+ outputRange: [colors.foreground, colors.primaryForeground],
83
+ })
84
+
85
+ const borderColor = pressAnim.interpolate({
86
+ inputRange: [0, 1],
87
+ outputRange: [colors.border, colors.primary],
88
+ })
89
+
90
+ return (
91
+ <Animated.View style={[styles.wrapper, { transform: [{ scale }] }, style]}>
92
+ <TouchableOpacity
93
+ onPress={handlePress}
94
+ onPressIn={handlePressIn}
95
+ onPressOut={handlePressOut}
96
+ activeOpacity={1}
97
+ touchSoundDisabled={true}
98
+ >
99
+ <Animated.View style={[styles.chip, { backgroundColor, borderColor }]}>
100
+ <Animated.Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>
101
+ {label}
102
+ </Animated.Text>
103
+ </Animated.View>
104
+ </TouchableOpacity>
105
+ </Animated.View>
106
+ )
107
+ }
108
+
109
+ export function ChipGroup({ options, value, onValueChange, multiSelect = false, style }: ChipGroupProps) {
110
+ const handlePress = (optionValue: string | number) => {
111
+ if (!multiSelect) {
112
+ onValueChange?.(optionValue)
113
+ return
114
+ }
115
+
116
+ // Multiselect logic
117
+ const currentArray = Array.isArray(value) ? value : value ? [value] : []
118
+ const isSelected = currentArray.includes(optionValue)
119
+
120
+ let newArray: (string | number)[]
121
+ if (isSelected) {
122
+ // Remove from selection
123
+ newArray = currentArray.filter((v) => v !== optionValue)
124
+ } else {
125
+ // Add to selection
126
+ newArray = [...currentArray, optionValue]
127
+ }
128
+
129
+ onValueChange?.(newArray)
130
+ }
131
+
132
+ const isSelected = (optionValue: string | number): boolean => {
133
+ if (Array.isArray(value)) {
134
+ return value.includes(optionValue)
135
+ }
136
+ return optionValue === value
137
+ }
138
+
139
+ return (
140
+ <View style={[styles.group, style]}>
141
+ {options.map((opt) => (
142
+ <Chip
143
+ key={opt.value}
144
+ label={opt.label}
145
+ selected={isSelected(opt.value)}
146
+ onPress={() => handlePress(opt.value)}
147
+ />
148
+ ))}
149
+ </View>
150
+ )
151
+ }
152
+
153
+ const styles = StyleSheet.create({
154
+ wrapper: {},
155
+ chip: {
156
+ borderRadius: 999,
157
+ paddingHorizontal: 14,
158
+ paddingVertical: 8,
159
+ borderWidth: 1.5,
160
+ alignItems: 'center',
161
+ justifyContent: 'center',
162
+ },
163
+ label: {
164
+ fontSize: 14,
165
+ fontWeight: '500',
166
+ lineHeight: 20,
167
+ },
168
+ group: {
169
+ flexDirection: 'row',
170
+ flexWrap: 'wrap',
171
+ gap: 8,
172
+ },
173
+ })
@@ -0,0 +1,2 @@
1
+ export { Chip, ChipGroup } from './Chip'
2
+ export type { ChipProps, ChipOption, ChipGroupProps } from './Chip'