@croacroa/react-native-template 2.0.1 → 3.2.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 (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -0
  10. package/README.md +446 -399
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -0
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -175
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -1,416 +1,416 @@
1
- import { View, Text, TouchableOpacity } from "react-native";
2
- import { Ionicons } from "@expo/vector-icons";
3
- import { useTheme } from "@/hooks/useTheme";
4
- import { cn } from "@/utils/cn";
5
-
6
- type BadgeVariant =
7
- | "default"
8
- | "primary"
9
- | "secondary"
10
- | "success"
11
- | "warning"
12
- | "error"
13
- | "info";
14
-
15
- type BadgeSize = "sm" | "md" | "lg";
16
-
17
- interface BadgeProps {
18
- /**
19
- * Badge content
20
- */
21
- children: React.ReactNode;
22
-
23
- /**
24
- * Visual variant
25
- */
26
- variant?: BadgeVariant;
27
-
28
- /**
29
- * Size variant
30
- */
31
- size?: BadgeSize;
32
-
33
- /**
34
- * Icon name (Ionicons)
35
- */
36
- icon?: keyof typeof Ionicons.glyphMap;
37
-
38
- /**
39
- * Whether the badge is outlined
40
- */
41
- outlined?: boolean;
42
-
43
- /**
44
- * Whether to make it a pill shape
45
- */
46
- pill?: boolean;
47
-
48
- /**
49
- * Additional class name
50
- */
51
- className?: string;
52
- }
53
-
54
- const sizeStyles: Record<
55
- BadgeSize,
56
- { container: string; text: string; icon: number }
57
- > = {
58
- sm: { container: "px-2 py-0.5", text: "text-xs", icon: 12 },
59
- md: { container: "px-2.5 py-1", text: "text-sm", icon: 14 },
60
- lg: { container: "px-3 py-1.5", text: "text-base", icon: 16 },
61
- };
62
-
63
- const variantStyles: Record<
64
- BadgeVariant,
65
- {
66
- bg: string;
67
- bgDark: string;
68
- text: string;
69
- textDark: string;
70
- border: string;
71
- borderDark: string;
72
- }
73
- > = {
74
- default: {
75
- bg: "bg-gray-100",
76
- bgDark: "bg-gray-800",
77
- text: "text-gray-700",
78
- textDark: "text-gray-300",
79
- border: "border-gray-300",
80
- borderDark: "border-gray-600",
81
- },
82
- primary: {
83
- bg: "bg-primary-100",
84
- bgDark: "bg-primary-900/30",
85
- text: "text-primary-700",
86
- textDark: "text-primary-400",
87
- border: "border-primary-300",
88
- borderDark: "border-primary-700",
89
- },
90
- secondary: {
91
- bg: "bg-slate-100",
92
- bgDark: "bg-slate-800",
93
- text: "text-slate-700",
94
- textDark: "text-slate-300",
95
- border: "border-slate-300",
96
- borderDark: "border-slate-600",
97
- },
98
- success: {
99
- bg: "bg-green-100",
100
- bgDark: "bg-green-900/30",
101
- text: "text-green-700",
102
- textDark: "text-green-400",
103
- border: "border-green-300",
104
- borderDark: "border-green-700",
105
- },
106
- warning: {
107
- bg: "bg-yellow-100",
108
- bgDark: "bg-yellow-900/30",
109
- text: "text-yellow-700",
110
- textDark: "text-yellow-400",
111
- border: "border-yellow-300",
112
- borderDark: "border-yellow-700",
113
- },
114
- error: {
115
- bg: "bg-red-100",
116
- bgDark: "bg-red-900/30",
117
- text: "text-red-700",
118
- textDark: "text-red-400",
119
- border: "border-red-300",
120
- borderDark: "border-red-700",
121
- },
122
- info: {
123
- bg: "bg-blue-100",
124
- bgDark: "bg-blue-900/30",
125
- text: "text-blue-700",
126
- textDark: "text-blue-400",
127
- border: "border-blue-300",
128
- borderDark: "border-blue-700",
129
- },
130
- };
131
-
132
- export function Badge({
133
- children,
134
- variant = "default",
135
- size = "md",
136
- icon,
137
- outlined = false,
138
- pill = false,
139
- className,
140
- }: BadgeProps) {
141
- const { isDark } = useTheme();
142
- const sizeStyle = sizeStyles[size];
143
- const variantStyle = variantStyles[variant];
144
-
145
- return (
146
- <View
147
- className={cn(
148
- "flex-row items-center self-start",
149
- sizeStyle.container,
150
- pill ? "rounded-full" : "rounded-md",
151
- outlined
152
- ? cn(
153
- "border bg-transparent",
154
- isDark ? variantStyle.borderDark : variantStyle.border
155
- )
156
- : isDark
157
- ? variantStyle.bgDark
158
- : variantStyle.bg,
159
- className
160
- )}
161
- >
162
- {icon && (
163
- <Ionicons
164
- name={icon}
165
- size={sizeStyle.icon}
166
- color={
167
- isDark
168
- ? getTextColor(variantStyle.textDark)
169
- : getTextColor(variantStyle.text)
170
- }
171
- style={{ marginRight: 4 }}
172
- />
173
- )}
174
- <Text
175
- className={cn(
176
- sizeStyle.text,
177
- "font-medium",
178
- isDark ? variantStyle.textDark : variantStyle.text
179
- )}
180
- >
181
- {children}
182
- </Text>
183
- </View>
184
- );
185
- }
186
-
187
- // Helper to extract color from class name
188
- function getTextColor(className: string): string {
189
- const colorMap: Record<string, string> = {
190
- "text-gray-700": "#374151",
191
- "text-gray-300": "#d1d5db",
192
- "text-primary-700": "#047857",
193
- "text-primary-400": "#34d399",
194
- "text-slate-700": "#334155",
195
- "text-slate-300": "#cbd5e1",
196
- "text-green-700": "#15803d",
197
- "text-green-400": "#4ade80",
198
- "text-yellow-700": "#a16207",
199
- "text-yellow-400": "#facc15",
200
- "text-red-700": "#b91c1c",
201
- "text-red-400": "#f87171",
202
- "text-blue-700": "#1d4ed8",
203
- "text-blue-400": "#60a5fa",
204
- };
205
- return colorMap[className] || "#6b7280";
206
- }
207
-
208
- /**
209
- * Chip component (interactive badge)
210
- */
211
- interface ChipProps extends Omit<BadgeProps, "children"> {
212
- /**
213
- * Chip label
214
- */
215
- label: string;
216
-
217
- /**
218
- * Called when the chip is pressed
219
- */
220
- onPress?: () => void;
221
-
222
- /**
223
- * Whether the chip is selected
224
- */
225
- selected?: boolean;
226
-
227
- /**
228
- * Whether to show remove button
229
- */
230
- removable?: boolean;
231
-
232
- /**
233
- * Called when remove button is pressed
234
- */
235
- onRemove?: () => void;
236
-
237
- /**
238
- * Whether the chip is disabled
239
- */
240
- disabled?: boolean;
241
- }
242
-
243
- export function Chip({
244
- label,
245
- variant = "default",
246
- size = "md",
247
- icon,
248
- onPress,
249
- selected = false,
250
- removable = false,
251
- onRemove,
252
- disabled = false,
253
- pill = true,
254
- className,
255
- }: ChipProps) {
256
- const { isDark } = useTheme();
257
- const sizeStyle = sizeStyles[size];
258
- const variantStyle = selected
259
- ? variantStyles.primary
260
- : variantStyles[variant];
261
-
262
- const content = (
263
- <View
264
- className={cn(
265
- "flex-row items-center",
266
- sizeStyle.container,
267
- pill ? "rounded-full" : "rounded-md",
268
- selected
269
- ? isDark
270
- ? "bg-primary-600"
271
- : "bg-primary-500"
272
- : isDark
273
- ? variantStyle.bgDark
274
- : variantStyle.bg,
275
- disabled && "opacity-50",
276
- className
277
- )}
278
- >
279
- {icon && (
280
- <Ionicons
281
- name={icon}
282
- size={sizeStyle.icon}
283
- color={
284
- selected
285
- ? "#ffffff"
286
- : isDark
287
- ? getTextColor(variantStyle.textDark)
288
- : getTextColor(variantStyle.text)
289
- }
290
- style={{ marginRight: 4 }}
291
- />
292
- )}
293
- <Text
294
- className={cn(
295
- sizeStyle.text,
296
- "font-medium",
297
- selected
298
- ? "text-white"
299
- : isDark
300
- ? variantStyle.textDark
301
- : variantStyle.text
302
- )}
303
- >
304
- {label}
305
- </Text>
306
- {removable && (
307
- <TouchableOpacity
308
- onPress={onRemove}
309
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
310
- disabled={disabled}
311
- className="ml-1"
312
- >
313
- <Ionicons
314
- name="close-circle"
315
- size={sizeStyle.icon + 2}
316
- color={
317
- selected
318
- ? "#ffffff"
319
- : isDark
320
- ? getTextColor(variantStyle.textDark)
321
- : getTextColor(variantStyle.text)
322
- }
323
- />
324
- </TouchableOpacity>
325
- )}
326
- </View>
327
- );
328
-
329
- if (onPress) {
330
- return (
331
- <TouchableOpacity
332
- onPress={onPress}
333
- disabled={disabled}
334
- activeOpacity={0.7}
335
- >
336
- {content}
337
- </TouchableOpacity>
338
- );
339
- }
340
-
341
- return content;
342
- }
343
-
344
- /**
345
- * Count Badge (for notifications, cart items, etc.)
346
- */
347
- interface CountBadgeProps {
348
- /**
349
- * Count to display
350
- */
351
- count: number;
352
-
353
- /**
354
- * Maximum count to show before showing "99+"
355
- */
356
- max?: number;
357
-
358
- /**
359
- * Variant color
360
- */
361
- variant?: "primary" | "error" | "warning";
362
-
363
- /**
364
- * Size variant
365
- */
366
- size?: "sm" | "md";
367
-
368
- /**
369
- * Additional class name
370
- */
371
- className?: string;
372
- }
373
-
374
- export function CountBadge({
375
- count,
376
- max = 99,
377
- variant = "error",
378
- size = "sm",
379
- className,
380
- }: CountBadgeProps) {
381
- if (count <= 0) return null;
382
-
383
- const displayCount = count > max ? `${max}+` : count.toString();
384
-
385
- const bgColors = {
386
- primary: "bg-primary-500",
387
- error: "bg-red-500",
388
- warning: "bg-yellow-500",
389
- };
390
-
391
- const sizes = {
392
- sm: { min: 18, text: "text-xs", px: 4 },
393
- md: { min: 22, text: "text-sm", px: 6 },
394
- };
395
-
396
- const sizeConfig = sizes[size];
397
-
398
- return (
399
- <View
400
- className={cn(
401
- "items-center justify-center rounded-full",
402
- bgColors[variant],
403
- className
404
- )}
405
- style={{
406
- minWidth: sizeConfig.min,
407
- height: sizeConfig.min,
408
- paddingHorizontal: sizeConfig.px,
409
- }}
410
- >
411
- <Text className={cn(sizeConfig.text, "font-bold text-white")}>
412
- {displayCount}
413
- </Text>
414
- </View>
415
- );
416
- }
1
+ import { View, Text, TouchableOpacity } from "react-native";
2
+ import { Ionicons } from "@expo/vector-icons";
3
+ import { useTheme } from "@/hooks/useTheme";
4
+ import { cn } from "@/utils/cn";
5
+
6
+ type BadgeVariant =
7
+ | "default"
8
+ | "primary"
9
+ | "secondary"
10
+ | "success"
11
+ | "warning"
12
+ | "error"
13
+ | "info";
14
+
15
+ type BadgeSize = "sm" | "md" | "lg";
16
+
17
+ interface BadgeProps {
18
+ /**
19
+ * Badge content
20
+ */
21
+ children: React.ReactNode;
22
+
23
+ /**
24
+ * Visual variant
25
+ */
26
+ variant?: BadgeVariant;
27
+
28
+ /**
29
+ * Size variant
30
+ */
31
+ size?: BadgeSize;
32
+
33
+ /**
34
+ * Icon name (Ionicons)
35
+ */
36
+ icon?: keyof typeof Ionicons.glyphMap;
37
+
38
+ /**
39
+ * Whether the badge is outlined
40
+ */
41
+ outlined?: boolean;
42
+
43
+ /**
44
+ * Whether to make it a pill shape
45
+ */
46
+ pill?: boolean;
47
+
48
+ /**
49
+ * Additional class name
50
+ */
51
+ className?: string;
52
+ }
53
+
54
+ const sizeStyles: Record<
55
+ BadgeSize,
56
+ { container: string; text: string; icon: number }
57
+ > = {
58
+ sm: { container: "px-2 py-0.5", text: "text-xs", icon: 12 },
59
+ md: { container: "px-2.5 py-1", text: "text-sm", icon: 14 },
60
+ lg: { container: "px-3 py-1.5", text: "text-base", icon: 16 },
61
+ };
62
+
63
+ const variantStyles: Record<
64
+ BadgeVariant,
65
+ {
66
+ bg: string;
67
+ bgDark: string;
68
+ text: string;
69
+ textDark: string;
70
+ border: string;
71
+ borderDark: string;
72
+ }
73
+ > = {
74
+ default: {
75
+ bg: "bg-gray-100",
76
+ bgDark: "bg-gray-800",
77
+ text: "text-gray-700",
78
+ textDark: "text-gray-300",
79
+ border: "border-gray-300",
80
+ borderDark: "border-gray-600",
81
+ },
82
+ primary: {
83
+ bg: "bg-primary-100",
84
+ bgDark: "bg-primary-900/30",
85
+ text: "text-primary-700",
86
+ textDark: "text-primary-400",
87
+ border: "border-primary-300",
88
+ borderDark: "border-primary-700",
89
+ },
90
+ secondary: {
91
+ bg: "bg-slate-100",
92
+ bgDark: "bg-slate-800",
93
+ text: "text-slate-700",
94
+ textDark: "text-slate-300",
95
+ border: "border-slate-300",
96
+ borderDark: "border-slate-600",
97
+ },
98
+ success: {
99
+ bg: "bg-green-100",
100
+ bgDark: "bg-green-900/30",
101
+ text: "text-green-700",
102
+ textDark: "text-green-400",
103
+ border: "border-green-300",
104
+ borderDark: "border-green-700",
105
+ },
106
+ warning: {
107
+ bg: "bg-yellow-100",
108
+ bgDark: "bg-yellow-900/30",
109
+ text: "text-yellow-700",
110
+ textDark: "text-yellow-400",
111
+ border: "border-yellow-300",
112
+ borderDark: "border-yellow-700",
113
+ },
114
+ error: {
115
+ bg: "bg-red-100",
116
+ bgDark: "bg-red-900/30",
117
+ text: "text-red-700",
118
+ textDark: "text-red-400",
119
+ border: "border-red-300",
120
+ borderDark: "border-red-700",
121
+ },
122
+ info: {
123
+ bg: "bg-blue-100",
124
+ bgDark: "bg-blue-900/30",
125
+ text: "text-blue-700",
126
+ textDark: "text-blue-400",
127
+ border: "border-blue-300",
128
+ borderDark: "border-blue-700",
129
+ },
130
+ };
131
+
132
+ export function Badge({
133
+ children,
134
+ variant = "default",
135
+ size = "md",
136
+ icon,
137
+ outlined = false,
138
+ pill = false,
139
+ className,
140
+ }: BadgeProps) {
141
+ const { isDark } = useTheme();
142
+ const sizeStyle = sizeStyles[size];
143
+ const variantStyle = variantStyles[variant];
144
+
145
+ return (
146
+ <View
147
+ className={cn(
148
+ "flex-row items-center self-start",
149
+ sizeStyle.container,
150
+ pill ? "rounded-full" : "rounded-md",
151
+ outlined
152
+ ? cn(
153
+ "border bg-transparent",
154
+ isDark ? variantStyle.borderDark : variantStyle.border
155
+ )
156
+ : isDark
157
+ ? variantStyle.bgDark
158
+ : variantStyle.bg,
159
+ className
160
+ )}
161
+ >
162
+ {icon && (
163
+ <Ionicons
164
+ name={icon}
165
+ size={sizeStyle.icon}
166
+ color={
167
+ isDark
168
+ ? getTextColor(variantStyle.textDark)
169
+ : getTextColor(variantStyle.text)
170
+ }
171
+ style={{ marginRight: 4 }}
172
+ />
173
+ )}
174
+ <Text
175
+ className={cn(
176
+ sizeStyle.text,
177
+ "font-medium",
178
+ isDark ? variantStyle.textDark : variantStyle.text
179
+ )}
180
+ >
181
+ {children}
182
+ </Text>
183
+ </View>
184
+ );
185
+ }
186
+
187
+ // Helper to extract color from class name
188
+ function getTextColor(className: string): string {
189
+ const colorMap: Record<string, string> = {
190
+ "text-gray-700": "#374151",
191
+ "text-gray-300": "#d1d5db",
192
+ "text-primary-700": "#047857",
193
+ "text-primary-400": "#34d399",
194
+ "text-slate-700": "#334155",
195
+ "text-slate-300": "#cbd5e1",
196
+ "text-green-700": "#15803d",
197
+ "text-green-400": "#4ade80",
198
+ "text-yellow-700": "#a16207",
199
+ "text-yellow-400": "#facc15",
200
+ "text-red-700": "#b91c1c",
201
+ "text-red-400": "#f87171",
202
+ "text-blue-700": "#1d4ed8",
203
+ "text-blue-400": "#60a5fa",
204
+ };
205
+ return colorMap[className] || "#6b7280";
206
+ }
207
+
208
+ /**
209
+ * Chip component (interactive badge)
210
+ */
211
+ interface ChipProps extends Omit<BadgeProps, "children"> {
212
+ /**
213
+ * Chip label
214
+ */
215
+ label: string;
216
+
217
+ /**
218
+ * Called when the chip is pressed
219
+ */
220
+ onPress?: () => void;
221
+
222
+ /**
223
+ * Whether the chip is selected
224
+ */
225
+ selected?: boolean;
226
+
227
+ /**
228
+ * Whether to show remove button
229
+ */
230
+ removable?: boolean;
231
+
232
+ /**
233
+ * Called when remove button is pressed
234
+ */
235
+ onRemove?: () => void;
236
+
237
+ /**
238
+ * Whether the chip is disabled
239
+ */
240
+ disabled?: boolean;
241
+ }
242
+
243
+ export function Chip({
244
+ label,
245
+ variant = "default",
246
+ size = "md",
247
+ icon,
248
+ onPress,
249
+ selected = false,
250
+ removable = false,
251
+ onRemove,
252
+ disabled = false,
253
+ pill = true,
254
+ className,
255
+ }: ChipProps) {
256
+ const { isDark } = useTheme();
257
+ const sizeStyle = sizeStyles[size];
258
+ const variantStyle = selected
259
+ ? variantStyles.primary
260
+ : variantStyles[variant];
261
+
262
+ const content = (
263
+ <View
264
+ className={cn(
265
+ "flex-row items-center",
266
+ sizeStyle.container,
267
+ pill ? "rounded-full" : "rounded-md",
268
+ selected
269
+ ? isDark
270
+ ? "bg-primary-600"
271
+ : "bg-primary-500"
272
+ : isDark
273
+ ? variantStyle.bgDark
274
+ : variantStyle.bg,
275
+ disabled && "opacity-50",
276
+ className
277
+ )}
278
+ >
279
+ {icon && (
280
+ <Ionicons
281
+ name={icon}
282
+ size={sizeStyle.icon}
283
+ color={
284
+ selected
285
+ ? "#ffffff"
286
+ : isDark
287
+ ? getTextColor(variantStyle.textDark)
288
+ : getTextColor(variantStyle.text)
289
+ }
290
+ style={{ marginRight: 4 }}
291
+ />
292
+ )}
293
+ <Text
294
+ className={cn(
295
+ sizeStyle.text,
296
+ "font-medium",
297
+ selected
298
+ ? "text-white"
299
+ : isDark
300
+ ? variantStyle.textDark
301
+ : variantStyle.text
302
+ )}
303
+ >
304
+ {label}
305
+ </Text>
306
+ {removable && (
307
+ <TouchableOpacity
308
+ onPress={onRemove}
309
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
310
+ disabled={disabled}
311
+ className="ml-1"
312
+ >
313
+ <Ionicons
314
+ name="close-circle"
315
+ size={sizeStyle.icon + 2}
316
+ color={
317
+ selected
318
+ ? "#ffffff"
319
+ : isDark
320
+ ? getTextColor(variantStyle.textDark)
321
+ : getTextColor(variantStyle.text)
322
+ }
323
+ />
324
+ </TouchableOpacity>
325
+ )}
326
+ </View>
327
+ );
328
+
329
+ if (onPress) {
330
+ return (
331
+ <TouchableOpacity
332
+ onPress={onPress}
333
+ disabled={disabled}
334
+ activeOpacity={0.7}
335
+ >
336
+ {content}
337
+ </TouchableOpacity>
338
+ );
339
+ }
340
+
341
+ return content;
342
+ }
343
+
344
+ /**
345
+ * Count Badge (for notifications, cart items, etc.)
346
+ */
347
+ interface CountBadgeProps {
348
+ /**
349
+ * Count to display
350
+ */
351
+ count: number;
352
+
353
+ /**
354
+ * Maximum count to show before showing "99+"
355
+ */
356
+ max?: number;
357
+
358
+ /**
359
+ * Variant color
360
+ */
361
+ variant?: "primary" | "error" | "warning";
362
+
363
+ /**
364
+ * Size variant
365
+ */
366
+ size?: "sm" | "md";
367
+
368
+ /**
369
+ * Additional class name
370
+ */
371
+ className?: string;
372
+ }
373
+
374
+ export function CountBadge({
375
+ count,
376
+ max = 99,
377
+ variant = "error",
378
+ size = "sm",
379
+ className,
380
+ }: CountBadgeProps) {
381
+ if (count <= 0) return null;
382
+
383
+ const displayCount = count > max ? `${max}+` : count.toString();
384
+
385
+ const bgColors = {
386
+ primary: "bg-primary-500",
387
+ error: "bg-red-500",
388
+ warning: "bg-yellow-500",
389
+ };
390
+
391
+ const sizes = {
392
+ sm: { min: 18, text: "text-xs", px: 4 },
393
+ md: { min: 22, text: "text-sm", px: 6 },
394
+ };
395
+
396
+ const sizeConfig = sizes[size];
397
+
398
+ return (
399
+ <View
400
+ className={cn(
401
+ "items-center justify-center rounded-full",
402
+ bgColors[variant],
403
+ className
404
+ )}
405
+ style={{
406
+ minWidth: sizeConfig.min,
407
+ height: sizeConfig.min,
408
+ paddingHorizontal: sizeConfig.px,
409
+ }}
410
+ >
411
+ <Text className={cn(sizeConfig.text, "font-bold text-white")}>
412
+ {displayCount}
413
+ </Text>
414
+ </View>
415
+ );
416
+ }