@retray-dev/ui-kit 0.1.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.
Files changed (56) hide show
  1. package/COMPONENTS.md +710 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -0
  4. package/dist/index.d.mts +345 -4
  5. package/dist/index.d.ts +345 -4
  6. package/dist/index.js +1644 -58
  7. package/dist/index.mjs +1590 -58
  8. package/package.json +44 -5
  9. package/src/components/Accordion/Accordion.tsx +173 -0
  10. package/src/components/Accordion/index.ts +2 -0
  11. package/src/components/Alert/Alert.tsx +59 -0
  12. package/src/components/Alert/index.ts +2 -0
  13. package/src/components/Avatar/Avatar.tsx +71 -0
  14. package/src/components/Avatar/index.ts +2 -0
  15. package/src/components/Badge/Badge.tsx +48 -0
  16. package/src/components/Badge/index.ts +2 -0
  17. package/src/components/Button/Button.tsx +94 -45
  18. package/src/components/Card/Card.tsx +103 -0
  19. package/src/components/Card/index.ts +9 -0
  20. package/src/components/Checkbox/Checkbox.tsx +98 -0
  21. package/src/components/Checkbox/index.ts +2 -0
  22. package/src/components/EmptyState/EmptyState.tsx +67 -0
  23. package/src/components/EmptyState/index.ts +2 -0
  24. package/src/components/Input/Input.tsx +28 -35
  25. package/src/components/Progress/Progress.tsx +52 -0
  26. package/src/components/Progress/index.ts +2 -0
  27. package/src/components/RadioGroup/RadioGroup.tsx +132 -0
  28. package/src/components/RadioGroup/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +232 -0
  30. package/src/components/Select/index.ts +2 -0
  31. package/src/components/Separator/Separator.tsx +33 -0
  32. package/src/components/Separator/index.ts +2 -0
  33. package/src/components/Sheet/Sheet.tsx +115 -0
  34. package/src/components/Sheet/index.ts +2 -0
  35. package/src/components/Skeleton/Skeleton.tsx +63 -0
  36. package/src/components/Skeleton/index.ts +2 -0
  37. package/src/components/Slider/Slider.tsx +143 -0
  38. package/src/components/Slider/index.ts +2 -0
  39. package/src/components/Spinner/Spinner.tsx +21 -0
  40. package/src/components/Spinner/index.ts +2 -0
  41. package/src/components/Switch/Switch.tsx +86 -0
  42. package/src/components/Switch/index.ts +2 -0
  43. package/src/components/Tabs/Tabs.tsx +196 -0
  44. package/src/components/Tabs/index.ts +2 -0
  45. package/src/components/Text/Text.tsx +10 -4
  46. package/src/components/Textarea/Textarea.tsx +89 -0
  47. package/src/components/Textarea/index.ts +2 -0
  48. package/src/components/Toast/Toast.tsx +200 -0
  49. package/src/components/Toast/index.ts +2 -0
  50. package/src/components/Toggle/Toggle.tsx +92 -0
  51. package/src/components/Toggle/index.ts +2 -0
  52. package/src/index.ts +26 -0
  53. package/src/theme/ThemeProvider.tsx +47 -0
  54. package/src/theme/colors.ts +45 -0
  55. package/src/theme/index.ts +4 -0
  56. package/src/theme/types.ts +33 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "0.1.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",
@@ -14,13 +14,20 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
- "src"
17
+ "src",
18
+ "COMPONENTS.md"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "tsup",
21
22
  "dev": "tsup --watch",
22
23
  "typecheck": "tsc --noEmit",
23
- "release": "npm run typecheck && npm run 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"
24
31
  },
25
32
  "keywords": [
26
33
  "react-native",
@@ -31,12 +38,44 @@
31
38
  "license": "MIT",
32
39
  "peerDependencies": {
33
40
  "react": ">=17",
34
- "react-native": ">=0.70"
41
+ "react-native": ">=0.70",
42
+ "expo-haptics": ">=14.0.0",
43
+ "expo-linear-gradient": ">=13.0.0",
44
+ "@gorhom/bottom-sheet": ">=5.0.0",
45
+ "react-native-reanimated": ">=4.0.0",
46
+ "react-native-gesture-handler": ">=2.0.0",
47
+ "react-native-worklets": ">=0.5.0",
48
+ "react-native-safe-area-context": ">=4.0.0"
49
+ },
50
+ "pnpm": {
51
+ "overrides": {
52
+ "fast-xml-parser": "^5.5.7",
53
+ "react": "19.1.0",
54
+ "react-native": "0.81.5",
55
+ "react-native-worklets": "0.5.1"
56
+ },
57
+ "onlyBuiltDependencies": [
58
+ "esbuild"
59
+ ]
35
60
  },
36
61
  "devDependencies": {
37
- "@types/react": "^18.3.0",
62
+ "@gorhom/bottom-sheet": "^5.0.0",
63
+ "@types/react": "^19.1.0",
64
+ "expo-haptics": "~15.0.8",
65
+ "expo-linear-gradient": "~14.1.5",
38
66
  "react": "18.2.0",
39
67
  "react-native": "0.74.0",
68
+ "react-native-gesture-handler": "~2.28.0",
69
+ "react-native-reanimated": "~4.1.1",
70
+ "react-native-worklets": "~0.5.0",
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",
40
79
  "tsup": "^8.0.0",
41
80
  "typescript": "^5.4.0"
42
81
  }
@@ -0,0 +1,173 @@
1
+ import React, { useState, useRef } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ Animated,
7
+ StyleSheet,
8
+ LayoutChangeEvent,
9
+ ViewStyle,
10
+ } from 'react-native'
11
+ import ReanimatedAnimated, {
12
+ useSharedValue,
13
+ useAnimatedStyle,
14
+ withTiming,
15
+ Easing,
16
+ } from 'react-native-reanimated'
17
+ import * as Haptics from 'expo-haptics'
18
+ import { useTheme } from '../../theme'
19
+
20
+ export interface AccordionItem {
21
+ value: string
22
+ trigger: string
23
+ content: React.ReactNode
24
+ }
25
+
26
+ export interface AccordionProps {
27
+ items: AccordionItem[]
28
+ /**
29
+ * - `'single'` (default): only one item can be open at a time. Opening another closes the current one.
30
+ * - `'multiple'`: any number of items can be open simultaneously.
31
+ */
32
+ type?: 'single' | 'multiple'
33
+ /** Item value(s) that should be open on first render. */
34
+ defaultValue?: string | string[]
35
+ style?: ViewStyle
36
+ }
37
+
38
+ function AccordionItemComponent({
39
+ item,
40
+ isOpen,
41
+ onToggle,
42
+ }: {
43
+ item: AccordionItem
44
+ isOpen: boolean
45
+ onToggle: () => void
46
+ }) {
47
+ const { colors } = useTheme()
48
+ const animatedHeight = useSharedValue(0)
49
+ const animatedRotation = useSharedValue(0)
50
+ const contentHeight = useRef(0)
51
+ const scale = useRef(new Animated.Value(1)).current
52
+
53
+ const toggle = (open: boolean) => {
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 })
57
+ }
58
+
59
+ React.useEffect(() => {
60
+ toggle(isOpen)
61
+ }, [isOpen])
62
+
63
+ const onLayout = (e: LayoutChangeEvent) => {
64
+ if (contentHeight.current === 0) {
65
+ contentHeight.current = e.nativeEvent.layout.height
66
+ if (isOpen) animatedHeight.value = contentHeight.current
67
+ }
68
+ }
69
+
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
+ }
86
+
87
+ return (
88
+ <View style={[styles.item, { borderBottomColor: colors.border }]}>
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}
100
+ >
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>
109
+
110
+ <ReanimatedAnimated.View style={heightStyle}>
111
+ <View style={styles.content} onLayout={onLayout}>
112
+ {item.content}
113
+ </View>
114
+ </ReanimatedAnimated.View>
115
+ </View>
116
+ )
117
+ }
118
+
119
+ export function Accordion({ items, type = 'single', defaultValue, style }: AccordionProps) {
120
+ const [openValues, setOpenValues] = useState<string[]>(() => {
121
+ if (!defaultValue) return []
122
+ return Array.isArray(defaultValue) ? defaultValue : [defaultValue]
123
+ })
124
+
125
+ const toggle = (value: string) => {
126
+ if (type === 'single') {
127
+ setOpenValues((prev) => (prev.includes(value) ? [] : [value]))
128
+ } else {
129
+ setOpenValues((prev) =>
130
+ prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
131
+ )
132
+ }
133
+ }
134
+
135
+ return (
136
+ <View style={style}>
137
+ {items.map((item) => (
138
+ <AccordionItemComponent
139
+ key={item.value}
140
+ item={item}
141
+ isOpen={openValues.includes(item.value)}
142
+ onToggle={() => toggle(item.value)}
143
+ />
144
+ ))}
145
+ </View>
146
+ )
147
+ }
148
+
149
+ const styles = StyleSheet.create({
150
+ item: {
151
+ borderBottomWidth: 1,
152
+ },
153
+ trigger: {
154
+ flexDirection: 'row',
155
+ justifyContent: 'space-between',
156
+ alignItems: 'center',
157
+ paddingVertical: 16,
158
+ },
159
+ triggerText: {
160
+ fontSize: 15,
161
+ fontWeight: '500',
162
+ flex: 1,
163
+ },
164
+ chevron: {
165
+ fontSize: 16,
166
+ marginLeft: 8,
167
+ },
168
+ content: {
169
+ paddingBottom: 16,
170
+ position: 'absolute',
171
+ width: '100%',
172
+ },
173
+ })
@@ -0,0 +1,2 @@
1
+ export { Accordion } from './Accordion'
2
+ export type { AccordionProps, AccordionItem } from './Accordion'
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export type AlertVariant = 'default' | 'destructive'
6
+
7
+ export interface AlertProps {
8
+ title?: string
9
+ description?: string
10
+ variant?: AlertVariant
11
+ icon?: React.ReactNode
12
+ style?: ViewStyle
13
+ }
14
+
15
+ export function Alert({ title, description, variant = 'default', icon, style }: AlertProps) {
16
+ const { colors } = useTheme()
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
21
+
22
+ return (
23
+ <View style={[styles.container, { backgroundColor: colors.card, borderColor }, style]}>
24
+ {icon ? <View style={styles.icon}>{icon}</View> : null}
25
+ <View style={styles.content}>
26
+ {title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
27
+ {description ? (
28
+ <Text style={[styles.description, { color: descColor }]}>{description}</Text>
29
+ ) : null}
30
+ </View>
31
+ </View>
32
+ )
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ container: {
37
+ flexDirection: 'row',
38
+ borderWidth: 1,
39
+ borderRadius: 8,
40
+ padding: 16,
41
+ gap: 12,
42
+ },
43
+ icon: {
44
+ marginTop: 2,
45
+ },
46
+ content: {
47
+ flex: 1,
48
+ gap: 4,
49
+ },
50
+ title: {
51
+ fontSize: 14,
52
+ fontWeight: '500',
53
+ lineHeight: 20,
54
+ },
55
+ description: {
56
+ fontSize: 14,
57
+ lineHeight: 20,
58
+ },
59
+ })
@@ -0,0 +1,2 @@
1
+ export { Alert } from './Alert'
2
+ export type { AlertProps, AlertVariant } from './Alert'
@@ -0,0 +1,71 @@
1
+ import React, { useState } from 'react'
2
+ import { View, Text, Image, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl'
6
+
7
+ export interface AvatarProps {
8
+ /** Remote image URI. Falls back to `fallback` initials on error or when omitted. */
9
+ src?: string
10
+ /** Up to 2 characters shown when the image is unavailable. Auto-uppercased. Defaults to `'?'`. */
11
+ fallback?: string
12
+ size?: AvatarSize
13
+ style?: ViewStyle
14
+ }
15
+
16
+ const sizeMap: Record<AvatarSize, number> = {
17
+ sm: 24,
18
+ md: 32,
19
+ lg: 48,
20
+ xl: 64,
21
+ }
22
+
23
+ const fontSizeMap: Record<AvatarSize, number> = {
24
+ sm: 10,
25
+ md: 13,
26
+ lg: 18,
27
+ xl: 24,
28
+ }
29
+
30
+ export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
31
+ const { colors } = useTheme()
32
+ const [imageError, setImageError] = useState(false)
33
+ const dimension = sizeMap[size]
34
+ const showFallback = !src || imageError
35
+
36
+ const containerStyle: ViewStyle = {
37
+ width: dimension,
38
+ height: dimension,
39
+ borderRadius: dimension / 2,
40
+ backgroundColor: colors.muted,
41
+ overflow: 'hidden',
42
+ }
43
+
44
+ return (
45
+ <View style={[styles.base, containerStyle, style]}>
46
+ {!showFallback ? (
47
+ <Image
48
+ source={{ uri: src }}
49
+ style={{ width: dimension, height: dimension }}
50
+ onError={() => setImageError(true)}
51
+ />
52
+ ) : (
53
+ <Text
54
+ style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}
55
+ >
56
+ {fallback?.slice(0, 2).toUpperCase() ?? '?'}
57
+ </Text>
58
+ )}
59
+ </View>
60
+ )
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ base: {
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ },
68
+ fallback: {
69
+ fontWeight: '500',
70
+ },
71
+ })
@@ -0,0 +1,2 @@
1
+ export { Avatar } from './Avatar'
2
+ export type { AvatarProps, AvatarSize } from './Avatar'
@@ -0,0 +1,48 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
+ import { useTheme } from '../../theme'
4
+
5
+ export type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline'
6
+
7
+ export interface BadgeProps {
8
+ label: string
9
+ variant?: BadgeVariant
10
+ style?: ViewStyle
11
+ }
12
+
13
+ export function Badge({ label, variant = 'default', style }: BadgeProps) {
14
+ const { colors } = useTheme()
15
+
16
+ const containerStyle: ViewStyle = {
17
+ default: { backgroundColor: colors.primary },
18
+ secondary: { backgroundColor: colors.secondary },
19
+ destructive: { backgroundColor: colors.destructive },
20
+ outline: { backgroundColor: 'transparent', borderWidth: 1, borderColor: colors.border },
21
+ }[variant]
22
+
23
+ const textColor = {
24
+ default: colors.primaryForeground,
25
+ secondary: colors.secondaryForeground,
26
+ destructive: colors.destructiveForeground,
27
+ outline: colors.foreground,
28
+ }[variant]
29
+
30
+ return (
31
+ <View style={[styles.container, containerStyle, style]}>
32
+ <Text style={[styles.label, { color: textColor }]} allowFontScaling={true}>{label}</Text>
33
+ </View>
34
+ )
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ container: {
39
+ borderRadius: 6,
40
+ paddingHorizontal: 8,
41
+ paddingVertical: 2,
42
+ alignSelf: 'flex-start',
43
+ },
44
+ label: {
45
+ fontSize: 12,
46
+ fontWeight: '500',
47
+ },
48
+ })
@@ -0,0 +1,2 @@
1
+ export { Badge } from './Badge'
2
+ export type { BadgeProps, BadgeVariant } from './Badge'
@@ -1,49 +1,49 @@
1
- import React from 'react'
1
+ import React, { useRef } from 'react'
2
2
  import {
3
3
  TouchableOpacity,
4
4
  Text,
5
+ Animated,
5
6
  ActivityIndicator,
6
7
  StyleSheet,
7
8
  TouchableOpacityProps,
8
9
  ViewStyle,
9
10
  TextStyle,
10
11
  } from 'react-native'
12
+ import * as Haptics from 'expo-haptics'
13
+ import { useTheme } from '../../theme'
11
14
 
12
15
  export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
13
16
  export type ButtonSize = 'sm' | 'md' | 'lg'
14
17
 
15
18
  export interface ButtonProps extends TouchableOpacityProps {
16
19
  label: string
20
+ /**
21
+ * - `primary`: filled with `primary` token — main CTA
22
+ * - `secondary`: filled with `secondary` token — less prominent
23
+ * - `outline`: transparent with border — alternative actions
24
+ * - `ghost`: fully transparent — in-context or low-emphasis actions
25
+ */
17
26
  variant?: ButtonVariant
18
27
  size?: ButtonSize
28
+ /** Replaces the label with a spinner and forces `disabled`. */
19
29
  loading?: boolean
20
30
  fullWidth?: boolean
21
- }
22
-
23
- const containerVariantStyles: Record<ButtonVariant, ViewStyle> = {
24
- primary: { backgroundColor: '#000' },
25
- secondary: { backgroundColor: '#6B7280' },
26
- outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: '#000' },
27
- ghost: { backgroundColor: 'transparent' },
31
+ /** Icon rendered alongside the label. */
32
+ icon?: React.ReactNode
33
+ /** Side the icon appears on. Defaults to `'left'`. */
34
+ iconPosition?: 'left' | 'right'
28
35
  }
29
36
 
30
37
  const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
31
- sm: { paddingHorizontal: 12, paddingVertical: 6 },
32
- md: { paddingHorizontal: 16, paddingVertical: 10 },
33
- lg: { paddingHorizontal: 24, paddingVertical: 14 },
34
- }
35
-
36
- const labelVariantStyles: Record<ButtonVariant, TextStyle> = {
37
- primary: { color: '#fff' },
38
- secondary: { color: '#fff' },
39
- outline: { color: '#000' },
40
- ghost: { color: '#000' },
38
+ sm: { paddingHorizontal: 16, paddingVertical: 10 },
39
+ md: { paddingHorizontal: 20, paddingVertical: 14 },
40
+ lg: { paddingHorizontal: 28, paddingVertical: 18 },
41
41
  }
42
42
 
43
43
  const labelSizeStyles: Record<ButtonSize, TextStyle> = {
44
- sm: { fontSize: 13 },
45
- md: { fontSize: 15 },
46
- lg: { fontSize: 17 },
44
+ sm: { fontSize: 14 },
45
+ md: { fontSize: 16 },
46
+ lg: { fontSize: 18 },
47
47
  }
48
48
 
49
49
  export function Button({
@@ -52,37 +52,83 @@ export function Button({
52
52
  size = 'md',
53
53
  loading = false,
54
54
  fullWidth = false,
55
+ icon,
56
+ iconPosition = 'left',
55
57
  disabled,
56
58
  style,
59
+ onPress,
57
60
  ...props
58
61
  }: ButtonProps) {
62
+ const { colors } = useTheme()
59
63
  const isDisabled = disabled || loading
64
+ const scale = useRef(new Animated.Value(1)).current
65
+
66
+ const handlePressIn = () => {
67
+ if (isDisabled) return
68
+ Animated.spring(scale, {
69
+ toValue: 0.95,
70
+ useNativeDriver: true,
71
+ speed: 40,
72
+ bounciness: 0,
73
+ }).start()
74
+ }
75
+
76
+ const handlePressOut = () => {
77
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 4 }).start()
78
+ }
79
+
80
+ const handlePress: TouchableOpacityProps['onPress'] = (e) => {
81
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
82
+ onPress?.(e)
83
+ }
84
+
85
+ const containerVariantStyle: ViewStyle = {
86
+ primary: { backgroundColor: colors.primary },
87
+ secondary: { backgroundColor: colors.secondary },
88
+ outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
89
+ ghost: { backgroundColor: 'transparent' },
90
+ }[variant]
91
+
92
+ const labelVariantStyle: TextStyle = {
93
+ primary: { color: colors.primaryForeground },
94
+ secondary: { color: colors.secondaryForeground },
95
+ outline: { color: colors.foreground },
96
+ ghost: { color: colors.foreground },
97
+ }[variant]
98
+
99
+ const spinnerColor =
100
+ variant === 'primary' || variant === 'secondary' ? colors.primaryForeground : colors.foreground
60
101
 
61
102
  return (
62
- <TouchableOpacity
63
- style={[
64
- styles.base,
65
- containerVariantStyles[variant],
66
- containerSizeStyles[size],
67
- fullWidth && styles.fullWidth,
68
- isDisabled && styles.disabled,
69
- style,
70
- ]}
71
- disabled={isDisabled}
72
- activeOpacity={0.75}
73
- {...props}
74
- >
75
- {loading ? (
76
- <ActivityIndicator
77
- size="small"
78
- color={variant === 'outline' || variant === 'ghost' ? '#000' : '#fff'}
79
- />
80
- ) : (
81
- <Text style={[styles.label, labelVariantStyles[variant], labelSizeStyles[size]]}>
82
- {label}
83
- </Text>
84
- )}
85
- </TouchableOpacity>
103
+ <Animated.View style={[fullWidth && styles.fullWidth, { transform: [{ scale }] }]}>
104
+ <TouchableOpacity
105
+ style={[
106
+ styles.base,
107
+ containerVariantStyle,
108
+ containerSizeStyles[size],
109
+ fullWidth && styles.fullWidth,
110
+ isDisabled && styles.disabled,
111
+ style,
112
+ ]}
113
+ disabled={isDisabled}
114
+ activeOpacity={1}
115
+ touchSoundDisabled={true}
116
+ onPress={handlePress}
117
+ onPressIn={handlePressIn}
118
+ onPressOut={handlePressOut}
119
+ {...props}
120
+ >
121
+ {loading ? (
122
+ <ActivityIndicator size="small" color={spinnerColor} />
123
+ ) : (
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
+ </>
129
+ )}
130
+ </TouchableOpacity>
131
+ </Animated.View>
86
132
  )
87
133
  }
88
134
 
@@ -102,4 +148,7 @@ const styles = StyleSheet.create({
102
148
  label: {
103
149
  fontWeight: '600',
104
150
  },
151
+ labelWithIcon: {
152
+ marginHorizontal: 6,
153
+ },
105
154
  })