@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.
Files changed (56) hide show
  1. package/COMPONENTS.md +654 -0
  2. package/LICENSE +21 -0
  3. package/README.md +151 -0
  4. package/dist/index.d.mts +309 -3
  5. package/dist/index.d.ts +309 -3
  6. package/dist/index.js +1477 -57
  7. package/dist/index.mjs +1424 -57
  8. package/package.json +27 -5
  9. package/src/components/Accordion/Accordion.tsx +161 -0
  10. package/src/components/Accordion/index.ts +2 -0
  11. package/src/components/Alert/Alert.tsx +57 -0
  12. package/src/components/Alert/index.ts +2 -0
  13. package/src/components/Avatar/Avatar.tsx +67 -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 +78 -45
  18. package/src/components/Card/Card.tsx +109 -0
  19. package/src/components/Card/index.ts +9 -0
  20. package/src/components/Checkbox/Checkbox.tsx +70 -0
  21. package/src/components/Checkbox/index.ts +2 -0
  22. package/src/components/EmptyState/EmptyState.tsx +69 -0
  23. package/src/components/EmptyState/index.ts +2 -0
  24. package/src/components/Input/Input.tsx +26 -41
  25. package/src/components/Progress/Progress.tsx +53 -0
  26. package/src/components/Progress/index.ts +2 -0
  27. package/src/components/RadioGroup/RadioGroup.tsx +105 -0
  28. package/src/components/RadioGroup/index.ts +2 -0
  29. package/src/components/Select/Select.tsx +185 -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 +108 -0
  34. package/src/components/Sheet/index.ts +2 -0
  35. package/src/components/Skeleton/Skeleton.tsx +40 -0
  36. package/src/components/Skeleton/index.ts +2 -0
  37. package/src/components/Slider/Slider.tsx +142 -0
  38. package/src/components/Slider/index.ts +2 -0
  39. package/src/components/Spinner/Spinner.tsx +27 -0
  40. package/src/components/Spinner/index.ts +2 -0
  41. package/src/components/Switch/Switch.tsx +82 -0
  42. package/src/components/Switch/index.ts +2 -0
  43. package/src/components/Tabs/Tabs.tsx +145 -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 +70 -0
  47. package/src/components/Textarea/index.ts +2 -0
  48. package/src/components/Toast/Toast.tsx +164 -0
  49. package/src/components/Toast/index.ts +2 -0
  50. package/src/components/Toggle/Toggle.tsx +80 -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 +41 -0
  55. package/src/theme/index.ts +4 -0
  56. 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": "0.1.0",
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": "npm run typecheck && npm run build && npm publish --access public"
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
- "@types/react": "^18.3.0",
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,2 @@
1
+ export { Accordion } from './Accordion'
2
+ export type { AccordionProps, AccordionItem } from './Accordion'
@@ -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,2 @@
1
+ export { Alert } from './Alert'
2
+ export type { AlertProps, AlertVariant } from './Alert'
@@ -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,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 }]}>{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,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: 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' },
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: 13 },
45
- md: { fontSize: 15 },
46
- lg: { fontSize: 17 },
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
- <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>
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
+ })
@@ -0,0 +1,9 @@
1
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'
2
+ export type {
3
+ CardProps,
4
+ CardHeaderProps,
5
+ CardTitleProps,
6
+ CardDescriptionProps,
7
+ CardContentProps,
8
+ CardFooterProps,
9
+ } from './Card'