@retray-dev/ui-kit 0.1.0 → 1.0.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/COMPONENTS.md +654 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.d.mts +309 -3
- package/dist/index.d.ts +309 -3
- package/dist/index.js +1477 -57
- package/dist/index.mjs +1424 -57
- package/package.json +27 -5
- package/src/components/Accordion/Accordion.tsx +161 -0
- package/src/components/Accordion/index.ts +2 -0
- package/src/components/Alert/Alert.tsx +57 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/Avatar/Avatar.tsx +67 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.tsx +48 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Button/Button.tsx +78 -45
- package/src/components/Card/Card.tsx +109 -0
- package/src/components/Card/index.ts +9 -0
- package/src/components/Checkbox/Checkbox.tsx +70 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +69 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/Input/Input.tsx +26 -41
- package/src/components/Progress/Progress.tsx +53 -0
- package/src/components/Progress/index.ts +2 -0
- package/src/components/RadioGroup/RadioGroup.tsx +105 -0
- package/src/components/RadioGroup/index.ts +2 -0
- package/src/components/Select/Select.tsx +185 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Separator/Separator.tsx +33 -0
- package/src/components/Separator/index.ts +2 -0
- package/src/components/Sheet/Sheet.tsx +108 -0
- package/src/components/Sheet/index.ts +2 -0
- package/src/components/Skeleton/Skeleton.tsx +40 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.tsx +142 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Spinner/Spinner.tsx +27 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Switch/Switch.tsx +82 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Tabs/Tabs.tsx +145 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Text/Text.tsx +10 -4
- package/src/components/Textarea/Textarea.tsx +70 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Toast/Toast.tsx +164 -0
- package/src/components/Toast/index.ts +2 -0
- package/src/components/Toggle/Toggle.tsx +80 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/index.ts +26 -0
- package/src/theme/ThemeProvider.tsx +47 -0
- package/src/theme/colors.ts +41 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/types.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retray-dev/ui-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.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,14 @@
|
|
|
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": "
|
|
24
|
+
"release": "pnpm typecheck && pnpm build && npm publish --access public"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"react-native",
|
|
@@ -31,12 +32,33 @@
|
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"react": ">=17",
|
|
34
|
-
"react-native": ">=0.70"
|
|
35
|
+
"react-native": ">=0.70",
|
|
36
|
+
"expo-haptics": ">=14.0.0",
|
|
37
|
+
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
38
|
+
"react-native-reanimated": ">=4.0.0",
|
|
39
|
+
"react-native-gesture-handler": ">=2.0.0",
|
|
40
|
+
"react-native-worklets": ">=0.5.0",
|
|
41
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
42
|
+
},
|
|
43
|
+
"pnpm": {
|
|
44
|
+
"overrides": {
|
|
45
|
+
"fast-xml-parser": "^5.5.7",
|
|
46
|
+
"react": "19.1.0",
|
|
47
|
+
"react-native": "0.81.5",
|
|
48
|
+
"react-native-worklets": "0.5.1"
|
|
49
|
+
},
|
|
50
|
+
"onlyBuiltDependencies": ["esbuild"]
|
|
35
51
|
},
|
|
36
52
|
"devDependencies": {
|
|
37
|
-
"@
|
|
53
|
+
"@gorhom/bottom-sheet": "^5.0.0",
|
|
54
|
+
"@types/react": "^19.1.0",
|
|
55
|
+
"expo-haptics": "~15.0.8",
|
|
38
56
|
"react": "18.2.0",
|
|
39
57
|
"react-native": "0.74.0",
|
|
58
|
+
"react-native-gesture-handler": "~2.28.0",
|
|
59
|
+
"react-native-reanimated": "~4.1.1",
|
|
60
|
+
"react-native-worklets": "~0.5.0",
|
|
61
|
+
"react-native-safe-area-context": "~5.6.2",
|
|
40
62
|
"tsup": "^8.0.0",
|
|
41
63
|
"typescript": "^5.4.0"
|
|
42
64
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
Animated,
|
|
7
|
+
Easing,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
LayoutChangeEvent,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import * as Haptics from 'expo-haptics'
|
|
13
|
+
import { useTheme } from '../../theme'
|
|
14
|
+
|
|
15
|
+
export interface AccordionItem {
|
|
16
|
+
value: string
|
|
17
|
+
trigger: string
|
|
18
|
+
content: React.ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AccordionProps {
|
|
22
|
+
items: AccordionItem[]
|
|
23
|
+
/**
|
|
24
|
+
* - `'single'` (default): only one item can be open at a time. Opening another closes the current one.
|
|
25
|
+
* - `'multiple'`: any number of items can be open simultaneously.
|
|
26
|
+
*/
|
|
27
|
+
type?: 'single' | 'multiple'
|
|
28
|
+
/** Item value(s) that should be open on first render. */
|
|
29
|
+
defaultValue?: string | string[]
|
|
30
|
+
style?: ViewStyle
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function AccordionItemComponent({
|
|
34
|
+
item,
|
|
35
|
+
isOpen,
|
|
36
|
+
onToggle,
|
|
37
|
+
}: {
|
|
38
|
+
item: AccordionItem
|
|
39
|
+
isOpen: boolean
|
|
40
|
+
onToggle: () => void
|
|
41
|
+
}) {
|
|
42
|
+
const { colors } = useTheme()
|
|
43
|
+
const animatedHeight = useRef(new Animated.Value(0)).current
|
|
44
|
+
const animatedRotation = useRef(new Animated.Value(0)).current
|
|
45
|
+
const contentHeight = useRef(0)
|
|
46
|
+
|
|
47
|
+
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()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
toggle(isOpen)
|
|
66
|
+
}, [isOpen])
|
|
67
|
+
|
|
68
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
69
|
+
if (contentHeight.current === 0) {
|
|
70
|
+
contentHeight.current = e.nativeEvent.layout.height
|
|
71
|
+
if (isOpen) animatedHeight.setValue(contentHeight.current)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rotate = animatedRotation.interpolate({
|
|
76
|
+
inputRange: [0, 1],
|
|
77
|
+
outputRange: ['0deg', '180deg'],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<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 }] }]}
|
|
92
|
+
>
|
|
93
|
+
▾
|
|
94
|
+
</Animated.Text>
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
|
|
97
|
+
<Animated.View style={[styles.contentWrapper, { height: animatedHeight, overflow: 'hidden' }]}>
|
|
98
|
+
<View style={styles.content} onLayout={onLayout}>
|
|
99
|
+
{item.content}
|
|
100
|
+
</View>
|
|
101
|
+
</Animated.View>
|
|
102
|
+
</View>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function Accordion({ items, type = 'single', defaultValue, style }: AccordionProps) {
|
|
107
|
+
const [openValues, setOpenValues] = useState<string[]>(() => {
|
|
108
|
+
if (!defaultValue) return []
|
|
109
|
+
return Array.isArray(defaultValue) ? defaultValue : [defaultValue]
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const toggle = (value: string) => {
|
|
113
|
+
if (type === 'single') {
|
|
114
|
+
setOpenValues((prev) => (prev.includes(value) ? [] : [value]))
|
|
115
|
+
} else {
|
|
116
|
+
setOpenValues((prev) =>
|
|
117
|
+
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={style}>
|
|
124
|
+
{items.map((item) => (
|
|
125
|
+
<AccordionItemComponent
|
|
126
|
+
key={item.value}
|
|
127
|
+
item={item}
|
|
128
|
+
isOpen={openValues.includes(item.value)}
|
|
129
|
+
onToggle={() => toggle(item.value)}
|
|
130
|
+
/>
|
|
131
|
+
))}
|
|
132
|
+
</View>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const styles = StyleSheet.create({
|
|
137
|
+
item: {
|
|
138
|
+
borderBottomWidth: 1,
|
|
139
|
+
},
|
|
140
|
+
trigger: {
|
|
141
|
+
flexDirection: 'row',
|
|
142
|
+
justifyContent: 'space-between',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
paddingVertical: 16,
|
|
145
|
+
},
|
|
146
|
+
triggerText: {
|
|
147
|
+
fontSize: 15,
|
|
148
|
+
fontWeight: '500',
|
|
149
|
+
flex: 1,
|
|
150
|
+
},
|
|
151
|
+
chevron: {
|
|
152
|
+
fontSize: 16,
|
|
153
|
+
marginLeft: 8,
|
|
154
|
+
},
|
|
155
|
+
contentWrapper: {},
|
|
156
|
+
content: {
|
|
157
|
+
paddingBottom: 16,
|
|
158
|
+
position: 'absolute',
|
|
159
|
+
width: '100%',
|
|
160
|
+
},
|
|
161
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
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 ? <Text style={[styles.description, { color: descColor }]}>{description}</Text> : null}
|
|
28
|
+
</View>
|
|
29
|
+
</View>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const styles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
flexDirection: 'row',
|
|
36
|
+
borderWidth: 1,
|
|
37
|
+
borderRadius: 8,
|
|
38
|
+
padding: 16,
|
|
39
|
+
gap: 12,
|
|
40
|
+
},
|
|
41
|
+
icon: {
|
|
42
|
+
marginTop: 2,
|
|
43
|
+
},
|
|
44
|
+
content: {
|
|
45
|
+
flex: 1,
|
|
46
|
+
gap: 4,
|
|
47
|
+
},
|
|
48
|
+
title: {
|
|
49
|
+
fontSize: 14,
|
|
50
|
+
fontWeight: '500',
|
|
51
|
+
lineHeight: 20,
|
|
52
|
+
},
|
|
53
|
+
description: {
|
|
54
|
+
fontSize: 14,
|
|
55
|
+
lineHeight: 20,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
src?: string
|
|
9
|
+
fallback?: string
|
|
10
|
+
size?: AvatarSize
|
|
11
|
+
style?: ViewStyle
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sizeMap: Record<AvatarSize, number> = {
|
|
15
|
+
sm: 24,
|
|
16
|
+
md: 32,
|
|
17
|
+
lg: 48,
|
|
18
|
+
xl: 64,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fontSizeMap: Record<AvatarSize, number> = {
|
|
22
|
+
sm: 10,
|
|
23
|
+
md: 13,
|
|
24
|
+
lg: 18,
|
|
25
|
+
xl: 24,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Avatar({ src, fallback, size = 'md', style }: AvatarProps) {
|
|
29
|
+
const { colors } = useTheme()
|
|
30
|
+
const [imageError, setImageError] = useState(false)
|
|
31
|
+
const dimension = sizeMap[size]
|
|
32
|
+
const showFallback = !src || imageError
|
|
33
|
+
|
|
34
|
+
const containerStyle: ViewStyle = {
|
|
35
|
+
width: dimension,
|
|
36
|
+
height: dimension,
|
|
37
|
+
borderRadius: dimension / 2,
|
|
38
|
+
backgroundColor: colors.muted,
|
|
39
|
+
overflow: 'hidden',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={[styles.base, containerStyle, style]}>
|
|
44
|
+
{!showFallback ? (
|
|
45
|
+
<Image
|
|
46
|
+
source={{ uri: src }}
|
|
47
|
+
style={{ width: dimension, height: dimension }}
|
|
48
|
+
onError={() => setImageError(true)}
|
|
49
|
+
/>
|
|
50
|
+
) : (
|
|
51
|
+
<Text style={[styles.fallback, { color: colors.mutedForeground, fontSize: fontSizeMap[size] }]}>
|
|
52
|
+
{fallback?.slice(0, 2).toUpperCase() ?? '?'}
|
|
53
|
+
</Text>
|
|
54
|
+
)}
|
|
55
|
+
</View>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
base: {
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
},
|
|
64
|
+
fallback: {
|
|
65
|
+
fontWeight: '500',
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -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 }]}>{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
|
+
})
|
|
@@ -1,49 +1,45 @@
|
|
|
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
31
|
}
|
|
22
32
|
|
|
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' },
|
|
28
|
-
}
|
|
29
|
-
|
|
30
33
|
const containerSizeStyles: Record<ButtonSize, ViewStyle> = {
|
|
31
|
-
sm: { paddingHorizontal:
|
|
32
|
-
md: { paddingHorizontal:
|
|
33
|
-
lg: { paddingHorizontal:
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const labelVariantStyles: Record<ButtonVariant, TextStyle> = {
|
|
37
|
-
primary: { color: '#fff' },
|
|
38
|
-
secondary: { color: '#fff' },
|
|
39
|
-
outline: { color: '#000' },
|
|
40
|
-
ghost: { color: '#000' },
|
|
34
|
+
sm: { paddingHorizontal: 16, paddingVertical: 10 },
|
|
35
|
+
md: { paddingHorizontal: 20, paddingVertical: 14 },
|
|
36
|
+
lg: { paddingHorizontal: 28, paddingVertical: 18 },
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
const labelSizeStyles: Record<ButtonSize, TextStyle> = {
|
|
44
|
-
sm: { fontSize:
|
|
45
|
-
md: { fontSize:
|
|
46
|
-
lg: { fontSize:
|
|
40
|
+
sm: { fontSize: 14 },
|
|
41
|
+
md: { fontSize: 16 },
|
|
42
|
+
lg: { fontSize: 18 },
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
export function Button({
|
|
@@ -54,35 +50,72 @@ export function Button({
|
|
|
54
50
|
fullWidth = false,
|
|
55
51
|
disabled,
|
|
56
52
|
style,
|
|
53
|
+
onPress,
|
|
57
54
|
...props
|
|
58
55
|
}: ButtonProps) {
|
|
56
|
+
const { colors } = useTheme()
|
|
59
57
|
const isDisabled = disabled || loading
|
|
58
|
+
const scale = useRef(new Animated.Value(1)).current
|
|
59
|
+
|
|
60
|
+
const handlePressIn = () => {
|
|
61
|
+
if (isDisabled) return
|
|
62
|
+
Animated.spring(scale, { toValue: 0.97, useNativeDriver: true, speed: 40, bounciness: 0 }).start()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handlePressOut = () => {
|
|
66
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, speed: 40, bounciness: 2 }).start()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handlePress: TouchableOpacityProps['onPress'] = (e) => {
|
|
70
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
|
|
71
|
+
onPress?.(e)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const containerVariantStyle: ViewStyle = {
|
|
75
|
+
primary: { backgroundColor: colors.primary },
|
|
76
|
+
secondary: { backgroundColor: colors.secondary },
|
|
77
|
+
outline: { backgroundColor: 'transparent', borderWidth: 1.5, borderColor: colors.border },
|
|
78
|
+
ghost: { backgroundColor: 'transparent' },
|
|
79
|
+
}[variant]
|
|
80
|
+
|
|
81
|
+
const labelVariantStyle: TextStyle = {
|
|
82
|
+
primary: { color: colors.primaryForeground },
|
|
83
|
+
secondary: { color: colors.secondaryForeground },
|
|
84
|
+
outline: { color: colors.foreground },
|
|
85
|
+
ghost: { color: colors.foreground },
|
|
86
|
+
}[variant]
|
|
87
|
+
|
|
88
|
+
const spinnerColor = variant === 'primary' || variant === 'secondary'
|
|
89
|
+
? colors.primaryForeground
|
|
90
|
+
: colors.foreground
|
|
60
91
|
|
|
61
92
|
return (
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{label}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
<Animated.View style={[fullWidth && styles.fullWidth, { transform: [{ scale }] }]}>
|
|
94
|
+
<TouchableOpacity
|
|
95
|
+
style={[
|
|
96
|
+
styles.base,
|
|
97
|
+
containerVariantStyle,
|
|
98
|
+
containerSizeStyles[size],
|
|
99
|
+
fullWidth && styles.fullWidth,
|
|
100
|
+
isDisabled && styles.disabled,
|
|
101
|
+
style,
|
|
102
|
+
]}
|
|
103
|
+
disabled={isDisabled}
|
|
104
|
+
activeOpacity={1}
|
|
105
|
+
onPress={handlePress}
|
|
106
|
+
onPressIn={handlePressIn}
|
|
107
|
+
onPressOut={handlePressOut}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
{loading ? (
|
|
111
|
+
<ActivityIndicator size="small" color={spinnerColor} />
|
|
112
|
+
) : (
|
|
113
|
+
<Text style={[styles.label, labelVariantStyle, labelSizeStyles[size]]}>
|
|
114
|
+
{label}
|
|
115
|
+
</Text>
|
|
116
|
+
)}
|
|
117
|
+
</TouchableOpacity>
|
|
118
|
+
</Animated.View>
|
|
86
119
|
)
|
|
87
120
|
}
|
|
88
121
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
|
|
5
|
+
export interface CardProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
style?: ViewStyle
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CardHeaderProps {
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
style?: ViewStyle
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CardTitleProps {
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
style?: TextStyle
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CardDescriptionProps {
|
|
21
|
+
children: React.ReactNode
|
|
22
|
+
style?: TextStyle
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CardContentProps {
|
|
26
|
+
children: React.ReactNode
|
|
27
|
+
style?: ViewStyle
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CardFooterProps {
|
|
31
|
+
children: React.ReactNode
|
|
32
|
+
style?: ViewStyle
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Card({ children, style }: CardProps) {
|
|
36
|
+
const { colors } = useTheme()
|
|
37
|
+
return (
|
|
38
|
+
<View
|
|
39
|
+
style={[
|
|
40
|
+
styles.card,
|
|
41
|
+
{ backgroundColor: colors.card, borderColor: colors.border },
|
|
42
|
+
style,
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</View>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function CardHeader({ children, style }: CardHeaderProps) {
|
|
51
|
+
return <View style={[styles.header, style]}>{children}</View>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function CardTitle({ children, style }: CardTitleProps) {
|
|
55
|
+
const { colors } = useTheme()
|
|
56
|
+
return (
|
|
57
|
+
<Text style={[styles.title, { color: colors.cardForeground }, style]}>{children}</Text>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function CardDescription({ children, style }: CardDescriptionProps) {
|
|
62
|
+
const { colors } = useTheme()
|
|
63
|
+
return (
|
|
64
|
+
<Text style={[styles.description, { color: colors.mutedForeground }, style]}>{children}</Text>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function CardContent({ children, style }: CardContentProps) {
|
|
69
|
+
return <View style={[styles.content, style]}>{children}</View>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function CardFooter({ children, style }: CardFooterProps) {
|
|
73
|
+
return <View style={[styles.footer, style]}>{children}</View>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
card: {
|
|
78
|
+
borderRadius: 12,
|
|
79
|
+
borderWidth: 1,
|
|
80
|
+
shadowColor: '#000',
|
|
81
|
+
shadowOffset: { width: 0, height: 1 },
|
|
82
|
+
shadowOpacity: 0.05,
|
|
83
|
+
shadowRadius: 2,
|
|
84
|
+
elevation: 1,
|
|
85
|
+
},
|
|
86
|
+
header: {
|
|
87
|
+
padding: 24,
|
|
88
|
+
paddingBottom: 0,
|
|
89
|
+
gap: 6,
|
|
90
|
+
},
|
|
91
|
+
title: {
|
|
92
|
+
fontSize: 18,
|
|
93
|
+
fontWeight: '600',
|
|
94
|
+
lineHeight: 24,
|
|
95
|
+
},
|
|
96
|
+
description: {
|
|
97
|
+
fontSize: 14,
|
|
98
|
+
lineHeight: 20,
|
|
99
|
+
},
|
|
100
|
+
content: {
|
|
101
|
+
padding: 24,
|
|
102
|
+
},
|
|
103
|
+
footer: {
|
|
104
|
+
padding: 24,
|
|
105
|
+
paddingTop: 0,
|
|
106
|
+
flexDirection: 'row',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
},
|
|
109
|
+
})
|