@croacroa/react-native-template 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 (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +1,307 @@
1
+ import { forwardRef, useCallback, useMemo, ReactNode } from "react";
2
+ import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
3
+ import GorhomBottomSheet, {
4
+ BottomSheetBackdrop,
5
+ BottomSheetView,
6
+ BottomSheetScrollView,
7
+ BottomSheetBackdropProps,
8
+ } from "@gorhom/bottom-sheet";
9
+ import { Ionicons } from "@expo/vector-icons";
10
+ import { useTheme } from "@/hooks/useTheme";
11
+ import { cn } from "@/utils/cn";
12
+
13
+ /**
14
+ * Hook to control BottomSheet imperatively
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * function MyComponent() {
19
+ * const { ref, open, close, snapTo } = useBottomSheet();
20
+ *
21
+ * return (
22
+ * <>
23
+ * <Button onPress={() => open()}>Open Sheet</Button>
24
+ * <BottomSheet ref={ref}>
25
+ * <Text>Content</Text>
26
+ * </BottomSheet>
27
+ * </>
28
+ * );
29
+ * }
30
+ * ```
31
+ */
32
+ import { useRef } from "react";
33
+
34
+ interface BottomSheetProps {
35
+ /**
36
+ * Content to render inside the bottom sheet
37
+ */
38
+ children: ReactNode;
39
+
40
+ /**
41
+ * Snap points for the bottom sheet (e.g., ['25%', '50%', '90%'])
42
+ */
43
+ snapPoints?: (string | number)[];
44
+
45
+ /**
46
+ * Initial snap point index
47
+ */
48
+ index?: number;
49
+
50
+ /**
51
+ * Title displayed in the header
52
+ */
53
+ title?: string;
54
+
55
+ /**
56
+ * Show close button in header
57
+ */
58
+ showCloseButton?: boolean;
59
+
60
+ /**
61
+ * Called when the sheet is closed
62
+ */
63
+ onClose?: () => void;
64
+
65
+ /**
66
+ * Called when the sheet index changes
67
+ */
68
+ onChange?: (index: number) => void;
69
+
70
+ /**
71
+ * Whether to enable backdrop
72
+ */
73
+ enableBackdrop?: boolean;
74
+
75
+ /**
76
+ * Whether to close on backdrop press
77
+ */
78
+ closeOnBackdropPress?: boolean;
79
+
80
+ /**
81
+ * Whether content is scrollable
82
+ */
83
+ scrollable?: boolean;
84
+
85
+ /**
86
+ * Whether to enable handle
87
+ */
88
+ enableHandle?: boolean;
89
+
90
+ /**
91
+ * Additional styles for the container
92
+ */
93
+ containerClassName?: string;
94
+
95
+ /**
96
+ * Additional styles for the content
97
+ */
98
+ contentClassName?: string;
99
+ }
100
+
101
+ export type BottomSheetRef = GorhomBottomSheet;
102
+
103
+ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>(
104
+ (
105
+ {
106
+ children,
107
+ snapPoints: customSnapPoints,
108
+ index = -1,
109
+ title,
110
+ showCloseButton = true,
111
+ onClose,
112
+ onChange,
113
+ enableBackdrop = true,
114
+ closeOnBackdropPress = true,
115
+ scrollable = false,
116
+ enableHandle = true,
117
+ containerClassName,
118
+ contentClassName,
119
+ },
120
+ ref
121
+ ) => {
122
+ const { isDark } = useTheme();
123
+
124
+ // Default snap points
125
+ const snapPoints = useMemo(
126
+ () => customSnapPoints || ["50%", "90%"],
127
+ [customSnapPoints]
128
+ );
129
+
130
+ // Handle sheet changes
131
+ const handleSheetChanges = useCallback(
132
+ (sheetIndex: number) => {
133
+ onChange?.(sheetIndex);
134
+ if (sheetIndex === -1) {
135
+ onClose?.();
136
+ }
137
+ },
138
+ [onChange, onClose]
139
+ );
140
+
141
+ // Handle close button press
142
+ const handleClose = useCallback(() => {
143
+ if (ref && "current" in ref && ref.current) {
144
+ ref.current.close();
145
+ }
146
+ onClose?.();
147
+ }, [ref, onClose]);
148
+
149
+ // Backdrop component
150
+ const renderBackdrop = useCallback(
151
+ (props: BottomSheetBackdropProps) => (
152
+ <BottomSheetBackdrop
153
+ {...props}
154
+ disappearsOnIndex={-1}
155
+ appearsOnIndex={0}
156
+ pressBehavior={closeOnBackdropPress ? "close" : "none"}
157
+ opacity={0.5}
158
+ />
159
+ ),
160
+ [closeOnBackdropPress]
161
+ );
162
+
163
+ // Handle component
164
+ const renderHandle = useCallback(() => {
165
+ if (!enableHandle) return null;
166
+
167
+ return (
168
+ <View className="items-center pt-2 pb-1">
169
+ <View
170
+ className={cn(
171
+ "w-10 h-1 rounded-full",
172
+ isDark ? "bg-gray-600" : "bg-gray-300"
173
+ )}
174
+ />
175
+ </View>
176
+ );
177
+ }, [enableHandle, isDark]);
178
+
179
+ const ContentWrapper = scrollable ? BottomSheetScrollView : BottomSheetView;
180
+
181
+ return (
182
+ <GorhomBottomSheet
183
+ ref={ref}
184
+ index={index}
185
+ snapPoints={snapPoints}
186
+ onChange={handleSheetChanges}
187
+ backdropComponent={enableBackdrop ? renderBackdrop : null}
188
+ handleComponent={renderHandle}
189
+ enablePanDownToClose
190
+ backgroundStyle={[
191
+ styles.background,
192
+ { backgroundColor: isDark ? "#1e293b" : "#ffffff" },
193
+ ]}
194
+ style={styles.sheet}
195
+ >
196
+ <View
197
+ className={cn(
198
+ "flex-1",
199
+ isDark ? "bg-surface-dark" : "bg-white",
200
+ containerClassName
201
+ )}
202
+ >
203
+ {/* Header */}
204
+ {(title || showCloseButton) && (
205
+ <View
206
+ className={cn(
207
+ "flex-row items-center justify-between px-4 py-3 border-b",
208
+ isDark ? "border-gray-700" : "border-gray-100"
209
+ )}
210
+ >
211
+ <Text
212
+ className={cn(
213
+ "text-lg font-semibold",
214
+ isDark ? "text-text-dark" : "text-text-light"
215
+ )}
216
+ >
217
+ {title || ""}
218
+ </Text>
219
+ {showCloseButton && (
220
+ <TouchableOpacity
221
+ onPress={handleClose}
222
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
223
+ className="p-1"
224
+ >
225
+ <Ionicons
226
+ name="close"
227
+ size={24}
228
+ color={isDark ? "#f8fafc" : "#0f172a"}
229
+ />
230
+ </TouchableOpacity>
231
+ )}
232
+ </View>
233
+ )}
234
+
235
+ {/* Content */}
236
+ <ContentWrapper
237
+ style={styles.contentContainer}
238
+ contentContainerStyle={[
239
+ styles.content,
240
+ scrollable && styles.scrollContent,
241
+ ]}
242
+ >
243
+ <View className={cn("flex-1", contentClassName)}>{children}</View>
244
+ </ContentWrapper>
245
+ </View>
246
+ </GorhomBottomSheet>
247
+ );
248
+ }
249
+ );
250
+
251
+ BottomSheet.displayName = "BottomSheet";
252
+
253
+ const styles = StyleSheet.create({
254
+ sheet: {
255
+ shadowColor: "#000",
256
+ shadowOffset: { width: 0, height: -4 },
257
+ shadowOpacity: 0.1,
258
+ shadowRadius: 8,
259
+ elevation: 5,
260
+ },
261
+ background: {
262
+ borderTopLeftRadius: 24,
263
+ borderTopRightRadius: 24,
264
+ },
265
+ contentContainer: {
266
+ flex: 1,
267
+ },
268
+ content: {
269
+ flex: 1,
270
+ },
271
+ scrollContent: {
272
+ paddingBottom: 40,
273
+ },
274
+ });
275
+
276
+ export function useBottomSheet() {
277
+ const ref = useRef<BottomSheetRef>(null);
278
+
279
+ const open = useCallback((snapIndex = 0) => {
280
+ ref.current?.snapToIndex(snapIndex);
281
+ }, []);
282
+
283
+ const close = useCallback(() => {
284
+ ref.current?.close();
285
+ }, []);
286
+
287
+ const snapTo = useCallback((index: number) => {
288
+ ref.current?.snapToIndex(index);
289
+ }, []);
290
+
291
+ const expand = useCallback(() => {
292
+ ref.current?.expand();
293
+ }, []);
294
+
295
+ const collapse = useCallback(() => {
296
+ ref.current?.collapse();
297
+ }, []);
298
+
299
+ return {
300
+ ref,
301
+ open,
302
+ close,
303
+ snapTo,
304
+ expand,
305
+ collapse,
306
+ };
307
+ }
@@ -0,0 +1,115 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { View } from "react-native";
3
+ import { Ionicons } from "@expo/vector-icons";
4
+ import { Button } from "./Button";
5
+
6
+ const meta: Meta<typeof Button> = {
7
+ title: "UI/Button",
8
+ component: Button,
9
+ argTypes: {
10
+ variant: {
11
+ control: "select",
12
+ options: ["primary", "secondary", "outline", "ghost", "danger"],
13
+ },
14
+ size: {
15
+ control: "select",
16
+ options: ["sm", "md", "lg"],
17
+ },
18
+ isLoading: {
19
+ control: "boolean",
20
+ },
21
+ disabled: {
22
+ control: "boolean",
23
+ },
24
+ },
25
+ args: {
26
+ children: "Button",
27
+ variant: "primary",
28
+ size: "md",
29
+ isLoading: false,
30
+ disabled: false,
31
+ },
32
+ };
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof Button>;
36
+
37
+ export const Primary: Story = {
38
+ args: {
39
+ variant: "primary",
40
+ children: "Primary Button",
41
+ },
42
+ };
43
+
44
+ export const Secondary: Story = {
45
+ args: {
46
+ variant: "secondary",
47
+ children: "Secondary Button",
48
+ },
49
+ };
50
+
51
+ export const Outline: Story = {
52
+ args: {
53
+ variant: "outline",
54
+ children: "Outline Button",
55
+ },
56
+ };
57
+
58
+ export const Ghost: Story = {
59
+ args: {
60
+ variant: "ghost",
61
+ children: "Ghost Button",
62
+ },
63
+ };
64
+
65
+ export const Danger: Story = {
66
+ args: {
67
+ variant: "danger",
68
+ children: "Danger Button",
69
+ },
70
+ };
71
+
72
+ export const Loading: Story = {
73
+ args: {
74
+ isLoading: true,
75
+ children: "Loading",
76
+ },
77
+ };
78
+
79
+ export const Disabled: Story = {
80
+ args: {
81
+ disabled: true,
82
+ children: "Disabled",
83
+ },
84
+ };
85
+
86
+ export const Sizes: Story = {
87
+ render: () => (
88
+ <View style={{ gap: 12 }}>
89
+ <Button size="sm">Small</Button>
90
+ <Button size="md">Medium</Button>
91
+ <Button size="lg">Large</Button>
92
+ </View>
93
+ ),
94
+ };
95
+
96
+ export const WithIcon: Story = {
97
+ render: () => (
98
+ <Button>
99
+ <Ionicons name="add" size={20} color="#fff" style={{ marginRight: 8 }} />
100
+ Add Item
101
+ </Button>
102
+ ),
103
+ };
104
+
105
+ export const AllVariants: Story = {
106
+ render: () => (
107
+ <View style={{ gap: 12 }}>
108
+ <Button variant="primary">Primary</Button>
109
+ <Button variant="secondary">Secondary</Button>
110
+ <Button variant="outline">Outline</Button>
111
+ <Button variant="ghost">Ghost</Button>
112
+ <Button variant="danger">Danger</Button>
113
+ </View>
114
+ ),
115
+ };
@@ -0,0 +1,104 @@
1
+ import { forwardRef } from "react";
2
+ import {
3
+ Pressable,
4
+ Text,
5
+ ActivityIndicator,
6
+ PressableProps,
7
+ View,
8
+ } from "react-native";
9
+ import { cn } from "@/utils/cn";
10
+
11
+ type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
12
+ type ButtonSize = "sm" | "md" | "lg";
13
+
14
+ interface ButtonProps extends PressableProps {
15
+ variant?: ButtonVariant;
16
+ size?: ButtonSize;
17
+ isLoading?: boolean;
18
+ className?: string;
19
+ textClassName?: string;
20
+ children: React.ReactNode;
21
+ }
22
+
23
+ const variantStyles: Record<ButtonVariant, string> = {
24
+ primary: "bg-primary-600 active:bg-primary-700",
25
+ secondary: "bg-gray-200 dark:bg-gray-700 active:bg-gray-300 dark:active:bg-gray-600",
26
+ outline: "border-2 border-gray-300 dark:border-gray-600 bg-transparent active:bg-gray-100 dark:active:bg-gray-800",
27
+ ghost: "bg-transparent active:bg-gray-100 dark:active:bg-gray-800",
28
+ danger: "bg-red-600 active:bg-red-700",
29
+ };
30
+
31
+ const variantTextStyles: Record<ButtonVariant, string> = {
32
+ primary: "text-white",
33
+ secondary: "text-text-light dark:text-text-dark",
34
+ outline: "text-text-light dark:text-text-dark",
35
+ ghost: "text-primary-600 dark:text-primary-400",
36
+ danger: "text-white",
37
+ };
38
+
39
+ const sizeStyles: Record<ButtonSize, string> = {
40
+ sm: "px-3 py-2",
41
+ md: "px-4 py-3",
42
+ lg: "px-6 py-4",
43
+ };
44
+
45
+ const textSizeStyles: Record<ButtonSize, string> = {
46
+ sm: "text-sm",
47
+ md: "text-base",
48
+ lg: "text-lg",
49
+ };
50
+
51
+ export const Button = forwardRef<View, ButtonProps>(
52
+ (
53
+ {
54
+ variant = "primary",
55
+ size = "md",
56
+ isLoading = false,
57
+ disabled,
58
+ className,
59
+ textClassName,
60
+ children,
61
+ ...props
62
+ },
63
+ ref
64
+ ) => {
65
+ const isDisabled = disabled || isLoading;
66
+
67
+ return (
68
+ <Pressable
69
+ ref={ref}
70
+ disabled={isDisabled}
71
+ className={cn(
72
+ "flex-row items-center justify-center rounded-xl",
73
+ variantStyles[variant],
74
+ sizeStyles[size],
75
+ isDisabled && "opacity-50",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ {isLoading ? (
81
+ <ActivityIndicator
82
+ color={variant === "primary" || variant === "danger" ? "#ffffff" : "#3b82f6"}
83
+ size="small"
84
+ />
85
+ ) : typeof children === "string" ? (
86
+ <Text
87
+ className={cn(
88
+ "font-semibold",
89
+ variantTextStyles[variant],
90
+ textSizeStyles[size],
91
+ textClassName
92
+ )}
93
+ >
94
+ {children}
95
+ </Text>
96
+ ) : (
97
+ children
98
+ )}
99
+ </Pressable>
100
+ );
101
+ }
102
+ );
103
+
104
+ Button.displayName = "Button";
@@ -0,0 +1,84 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { View, Text } from "react-native";
3
+ import { Card } from "./Card";
4
+
5
+ const meta: Meta<typeof Card> = {
6
+ title: "UI/Card",
7
+ component: Card,
8
+ argTypes: {
9
+ variant: {
10
+ control: "select",
11
+ options: ["default", "elevated", "outlined"],
12
+ },
13
+ },
14
+ args: {
15
+ variant: "default",
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof Card>;
21
+
22
+ export const Default: Story = {
23
+ render: (args) => (
24
+ <Card {...args} className="p-4">
25
+ <Text>Default Card Content</Text>
26
+ </Card>
27
+ ),
28
+ };
29
+
30
+ export const Elevated: Story = {
31
+ render: () => (
32
+ <Card variant="elevated" className="p-4">
33
+ <Text>Elevated Card with Shadow</Text>
34
+ </Card>
35
+ ),
36
+ };
37
+
38
+ export const Outlined: Story = {
39
+ render: () => (
40
+ <Card variant="outlined" className="p-4">
41
+ <Text>Outlined Card with Border</Text>
42
+ </Card>
43
+ ),
44
+ };
45
+
46
+ export const AllVariants: Story = {
47
+ render: () => (
48
+ <View style={{ gap: 16 }}>
49
+ <Card variant="default" className="p-4">
50
+ <Text style={{ fontWeight: "bold" }}>Default</Text>
51
+ <Text>Background color, no border</Text>
52
+ </Card>
53
+ <Card variant="elevated" className="p-4">
54
+ <Text style={{ fontWeight: "bold" }}>Elevated</Text>
55
+ <Text>Background color with shadow</Text>
56
+ </Card>
57
+ <Card variant="outlined" className="p-4">
58
+ <Text style={{ fontWeight: "bold" }}>Outlined</Text>
59
+ <Text>Transparent with border</Text>
60
+ </Card>
61
+ </View>
62
+ ),
63
+ };
64
+
65
+ export const ComplexContent: Story = {
66
+ render: () => (
67
+ <Card className="p-4">
68
+ <View style={{ gap: 8 }}>
69
+ <Text style={{ fontSize: 18, fontWeight: "bold" }}>Card Title</Text>
70
+ <Text style={{ color: "#64748b" }}>
71
+ This is a more complex card with multiple elements inside.
72
+ </Text>
73
+ <View
74
+ style={{
75
+ height: 100,
76
+ backgroundColor: "#f1f5f9",
77
+ borderRadius: 8,
78
+ marginTop: 8,
79
+ }}
80
+ />
81
+ </View>
82
+ </Card>
83
+ ),
84
+ };
@@ -0,0 +1,32 @@
1
+ import { View, ViewProps } from "react-native";
2
+ import { cn } from "@/utils/cn";
3
+
4
+ interface CardProps extends ViewProps {
5
+ variant?: "default" | "elevated" | "outlined";
6
+ className?: string;
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ export function Card({
11
+ variant = "default",
12
+ className,
13
+ children,
14
+ ...props
15
+ }: CardProps) {
16
+ return (
17
+ <View
18
+ className={cn(
19
+ "rounded-xl",
20
+ variant === "default" && "bg-surface-light dark:bg-surface-dark",
21
+ variant === "elevated" &&
22
+ "bg-surface-light shadow-lg dark:bg-surface-dark",
23
+ variant === "outlined" &&
24
+ "border-2 border-gray-200 bg-transparent dark:border-gray-700",
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </View>
31
+ );
32
+ }