@beyondcorp/beyond-ui-native 0.1.1 → 0.1.3

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 (83) hide show
  1. package/dist/{chunk-2B6HTSGM.mjs → chunk-RNHEGFYR.mjs} +16 -2
  2. package/dist/chunk-RNHEGFYR.mjs.map +1 -0
  3. package/dist/index.d.mts +2 -145
  4. package/dist/index.d.ts +2 -145
  5. package/dist/index.js +37 -8
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +23 -8
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/preset-BmUHLoAa.d.mts +274 -0
  10. package/dist/preset-BmUHLoAa.d.ts +274 -0
  11. package/dist/preset.d.mts +1 -117
  12. package/dist/preset.d.ts +1 -117
  13. package/dist/preset.js +19 -3
  14. package/dist/preset.js.map +1 -1
  15. package/dist/preset.mjs +5 -3
  16. package/package.json +11 -5
  17. package/src/components/accordion/accordion.tsx +245 -0
  18. package/src/components/accordion/index.ts +1 -0
  19. package/src/components/alert/alert.tsx +118 -0
  20. package/src/components/alert/index.ts +1 -0
  21. package/src/components/avatar/avatar.tsx +125 -0
  22. package/src/components/avatar/index.ts +1 -0
  23. package/src/components/badge/badge.tsx +89 -0
  24. package/src/components/badge/index.ts +1 -0
  25. package/src/components/bottom-sheet/bottom-sheet.tsx +336 -0
  26. package/src/components/bottom-sheet/index.ts +1 -0
  27. package/src/components/button/button.tsx +172 -0
  28. package/src/components/button/index.ts +1 -0
  29. package/src/components/card/card.tsx +121 -0
  30. package/src/components/card/index.ts +1 -0
  31. package/src/components/checkbox/checkbox.tsx +100 -0
  32. package/src/components/checkbox/index.ts +1 -0
  33. package/src/components/data-table/data-table.tsx +277 -0
  34. package/src/components/data-table/index.ts +1 -0
  35. package/src/components/dialog/dialog.tsx +278 -0
  36. package/src/components/dialog/index.ts +1 -0
  37. package/src/components/input/index.ts +1 -0
  38. package/src/components/input/input.tsx +130 -0
  39. package/src/components/radio/index.ts +1 -0
  40. package/src/components/radio/radio.tsx +117 -0
  41. package/src/components/select/index.ts +1 -0
  42. package/src/components/select/select.tsx +375 -0
  43. package/src/components/separator/index.ts +1 -0
  44. package/src/components/separator/separator.tsx +28 -0
  45. package/src/components/skeleton/index.ts +1 -0
  46. package/src/components/skeleton/skeleton.tsx +43 -0
  47. package/src/components/spinner/index.ts +1 -0
  48. package/src/components/spinner/spinner.tsx +46 -0
  49. package/src/components/stats-card/index.ts +1 -0
  50. package/src/components/stats-card/stats-card.tsx +92 -0
  51. package/src/components/switch/index.ts +1 -0
  52. package/src/components/switch/switch.tsx +80 -0
  53. package/src/components/tabs/index.ts +1 -0
  54. package/src/components/tabs/tabs.tsx +172 -0
  55. package/src/components/text/heading.tsx +50 -0
  56. package/src/components/text/index.ts +2 -0
  57. package/src/components/text/text.tsx +62 -0
  58. package/src/components/textarea/index.ts +1 -0
  59. package/src/components/textarea/textarea.tsx +87 -0
  60. package/src/components/toast/index.ts +2 -0
  61. package/src/components/toast/toast.tsx +136 -0
  62. package/src/components/toast/types.ts +32 -0
  63. package/src/index.ts +42 -0
  64. package/src/primitives/box.tsx +25 -0
  65. package/src/primitives/index.ts +2 -0
  66. package/src/primitives/pressable.tsx +49 -0
  67. package/src/providers/BeyondUIProvider.tsx +100 -0
  68. package/src/templates/auth/ForgotPasswordTemplate.tsx +132 -0
  69. package/src/templates/auth/SignInTemplate.tsx +213 -0
  70. package/src/templates/auth/SignUpTemplate.tsx +233 -0
  71. package/src/templates/auth/index.ts +3 -0
  72. package/src/templates/dashboard/DashboardTemplate.tsx +205 -0
  73. package/src/templates/dashboard/index.ts +1 -0
  74. package/src/templates/index.ts +3 -0
  75. package/src/templates/settings/SettingsTemplate.tsx +226 -0
  76. package/src/templates/settings/index.ts +1 -0
  77. package/src/theme/preset.ts +45 -0
  78. package/src/theme/tokens.ts +144 -0
  79. package/src/types/nativewind-env.d.ts +26 -0
  80. package/src/utilities/accessibility.ts +43 -0
  81. package/src/utilities/cn.ts +9 -0
  82. package/src/utilities/index.ts +2 -0
  83. package/dist/chunk-2B6HTSGM.mjs.map +0 -1
@@ -0,0 +1,375 @@
1
+ import * as React from "react";
2
+ import {
3
+ Modal,
4
+ View,
5
+ Pressable,
6
+ ScrollView,
7
+ TextInput,
8
+ type ViewProps,
9
+ } from "react-native";
10
+ import { cva, type VariantProps } from "class-variance-authority";
11
+ import { cn } from "../../utilities/cn";
12
+ import { Text } from "../text/text";
13
+
14
+ const selectTriggerVariants = cva(
15
+ "flex-row items-center justify-between rounded-xl border bg-card px-4 py-3 active:opacity-80",
16
+ {
17
+ variants: {
18
+ variant: {
19
+ default: "border-border",
20
+ error: "border-danger",
21
+ success: "border-success",
22
+ },
23
+ size: {
24
+ sm: "h-9 px-3 py-1.5",
25
+ md: "h-12 px-4 py-3",
26
+ lg: "h-14 px-5 py-4",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "md",
32
+ },
33
+ }
34
+ );
35
+
36
+ export interface SelectOption {
37
+ label: string;
38
+ value: string;
39
+ disabled?: boolean;
40
+ }
41
+
42
+ interface SelectContextValue {
43
+ open: boolean;
44
+ setOpen: (open: boolean) => void;
45
+ value?: string;
46
+ onSelect: (val: string) => void;
47
+ }
48
+
49
+ const SelectContext = React.createContext<SelectContextValue | null>(null);
50
+
51
+ function useSelectContext() {
52
+ const context = React.useContext(SelectContext);
53
+ if (!context) {
54
+ throw new Error("Select compound subcomponents must be used within a Select");
55
+ }
56
+ return context;
57
+ }
58
+
59
+ export interface SelectProps extends VariantProps<typeof selectTriggerVariants> {
60
+ options?: SelectOption[];
61
+ value?: string;
62
+ defaultValue?: string;
63
+ onValueChange?: (value: string) => void;
64
+ placeholder?: string;
65
+ label?: string;
66
+ helperText?: string;
67
+ error?: string;
68
+ disabled?: boolean;
69
+ searchable?: boolean;
70
+ className?: string;
71
+ children?: React.ReactNode;
72
+ }
73
+
74
+ export const Select: React.FC<SelectProps> = ({
75
+ options,
76
+ value: controlledValue,
77
+ defaultValue,
78
+ onValueChange,
79
+ placeholder = "Select an option...",
80
+ label,
81
+ helperText,
82
+ error,
83
+ disabled = false,
84
+ searchable = false,
85
+ variant,
86
+ size,
87
+ className,
88
+ children,
89
+ }) => {
90
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
91
+ const [open, setOpen] = React.useState(false);
92
+ const [searchQuery, setSearchQuery] = React.useState("");
93
+
94
+ const isControlled = controlledValue !== undefined;
95
+ const currentValue = isControlled ? controlledValue : uncontrolledValue;
96
+
97
+ const handleSelect = React.useCallback(
98
+ (newVal: string) => {
99
+ if (!isControlled) {
100
+ setUncontrolledValue(newVal);
101
+ }
102
+ onValueChange?.(newVal);
103
+ setOpen(false);
104
+ setSearchQuery("");
105
+ },
106
+ [isControlled, onValueChange]
107
+ );
108
+
109
+ const selectedOption = options?.find((o) => o.value === currentValue);
110
+ const displayLabel = selectedOption ? selectedOption.label : "";
111
+
112
+ const filteredOptions = React.useMemo(() => {
113
+ if (!options) return [];
114
+ if (!searchQuery) return options;
115
+ return options.filter((o) =>
116
+ o.label.toLowerCase().includes(searchQuery.toLowerCase())
117
+ );
118
+ }, [options, searchQuery]);
119
+
120
+ const activeVariant = error ? "error" : variant;
121
+
122
+ return (
123
+ <SelectContext.Provider
124
+ value={{
125
+ open,
126
+ setOpen,
127
+ value: currentValue,
128
+ onSelect: handleSelect,
129
+ }}
130
+ >
131
+ <View className={cn("w-full gap-1.5", className)}>
132
+ {label && (
133
+ <Text className="text-sm font-medium text-foreground">{label}</Text>
134
+ )}
135
+
136
+ {children ? (
137
+ children
138
+ ) : (
139
+ <Pressable
140
+ disabled={disabled}
141
+ onPress={() => setOpen(true)}
142
+ accessibilityRole="combobox"
143
+ accessibilityState={{ disabled, expanded: open }}
144
+ className={cn(
145
+ selectTriggerVariants({ variant: activeVariant, size }),
146
+ disabled && "opacity-50"
147
+ )}
148
+ >
149
+ <Text
150
+ className={cn(
151
+ "text-base",
152
+ displayLabel ? "text-foreground font-medium" : "text-muted"
153
+ )}
154
+ >
155
+ {displayLabel || placeholder}
156
+ </Text>
157
+ <Text className="text-muted text-xs">▼</Text>
158
+ </Pressable>
159
+ )}
160
+
161
+ {error ? (
162
+ <Text className="text-xs text-danger font-medium">{error}</Text>
163
+ ) : helperText ? (
164
+ <Text className="text-xs text-muted">{helperText}</Text>
165
+ ) : null}
166
+
167
+ {/* Modal Options Picker if options prop was passed */}
168
+ {options && (
169
+ <Modal
170
+ transparent
171
+ visible={open}
172
+ animationType="fade"
173
+ onRequestClose={() => setOpen(false)}
174
+ statusBarTranslucent
175
+ >
176
+ <View className="flex-1 justify-end bg-black/60">
177
+ <Pressable className="flex-1" onPress={() => setOpen(false)} />
178
+ <View className="w-full bg-card rounded-t-3xl border-t border-border p-6 max-h-[75%]">
179
+ <View className="flex-row items-center justify-between pb-4 border-b border-border mb-3">
180
+ <Text className="text-lg font-bold text-foreground">
181
+ {label || placeholder}
182
+ </Text>
183
+ <Pressable onPress={() => setOpen(false)} className="p-1">
184
+ <Text className="text-muted text-lg font-semibold">✕</Text>
185
+ </Pressable>
186
+ </View>
187
+
188
+ {searchable && (
189
+ <View className="mb-4">
190
+ <TextInput
191
+ value={searchQuery}
192
+ onChangeText={setSearchQuery}
193
+ placeholder="Search options..."
194
+ placeholderTextColor="#9ca3af"
195
+ className="h-11 px-4 rounded-xl border border-border bg-background text-foreground text-base"
196
+ />
197
+ </View>
198
+ )}
199
+
200
+ <ScrollView className="max-h-80" showsVerticalScrollIndicator={false}>
201
+ {filteredOptions.length === 0 ? (
202
+ <View className="py-8 items-center">
203
+ <Text className="text-muted text-sm">No options found</Text>
204
+ </View>
205
+ ) : (
206
+ filteredOptions.map((opt) => {
207
+ const isSelected = opt.value === currentValue;
208
+ return (
209
+ <Pressable
210
+ key={opt.value}
211
+ disabled={opt.disabled}
212
+ onPress={() => handleSelect(opt.value)}
213
+ className={cn(
214
+ "flex-row items-center justify-between py-3.5 px-3 rounded-xl active:bg-muted/10",
215
+ isSelected && "bg-primary/10",
216
+ opt.disabled && "opacity-40"
217
+ )}
218
+ >
219
+ <Text
220
+ className={cn(
221
+ "text-base",
222
+ isSelected
223
+ ? "text-primary font-semibold"
224
+ : "text-foreground font-normal"
225
+ )}
226
+ >
227
+ {opt.label}
228
+ </Text>
229
+ {isSelected && (
230
+ <Text className="text-primary font-bold text-base">✓</Text>
231
+ )}
232
+ </Pressable>
233
+ );
234
+ })
235
+ )}
236
+ </ScrollView>
237
+ </View>
238
+ </View>
239
+ </Modal>
240
+ )}
241
+ </View>
242
+ </SelectContext.Provider>
243
+ );
244
+ };
245
+ Select.displayName = "Select";
246
+
247
+ // --- Compound Subcomponents ---
248
+
249
+ export interface SelectTriggerProps extends ViewProps {
250
+ className?: string;
251
+ children: React.ReactNode;
252
+ }
253
+
254
+ export const SelectTrigger: React.FC<SelectTriggerProps> = ({
255
+ className,
256
+ children,
257
+ ...props
258
+ }) => {
259
+ const { setOpen, open } = useSelectContext();
260
+ return (
261
+ <Pressable
262
+ onPress={() => setOpen(true)}
263
+ accessibilityRole="combobox"
264
+ accessibilityState={{ expanded: open }}
265
+ className={cn(
266
+ "flex-row items-center justify-between rounded-xl border border-border bg-card px-4 py-3 active:opacity-80",
267
+ className
268
+ )}
269
+ {...props}
270
+ >
271
+ {children}
272
+ <Text className="text-muted text-xs">▼</Text>
273
+ </Pressable>
274
+ );
275
+ };
276
+ SelectTrigger.displayName = "SelectTrigger";
277
+
278
+ export interface SelectValueProps {
279
+ placeholder?: string;
280
+ className?: string;
281
+ }
282
+
283
+ export const SelectValue: React.FC<SelectValueProps> = ({
284
+ placeholder = "Select...",
285
+ className,
286
+ }) => {
287
+ const { value } = useSelectContext();
288
+ return (
289
+ <Text
290
+ className={cn(
291
+ "text-base",
292
+ value ? "text-foreground font-medium" : "text-muted",
293
+ className
294
+ )}
295
+ >
296
+ {value || placeholder}
297
+ </Text>
298
+ );
299
+ };
300
+ SelectValue.displayName = "SelectValue";
301
+
302
+ export interface SelectContentProps {
303
+ title?: string;
304
+ children: React.ReactNode;
305
+ }
306
+
307
+ export const SelectContent: React.FC<SelectContentProps> = ({ title, children }) => {
308
+ const { open, setOpen } = useSelectContext();
309
+ if (!open) return null;
310
+
311
+ return (
312
+ <Modal
313
+ transparent
314
+ visible={open}
315
+ animationType="fade"
316
+ onRequestClose={() => setOpen(false)}
317
+ statusBarTranslucent
318
+ >
319
+ <View className="flex-1 justify-end bg-black/60">
320
+ <Pressable className="flex-1" onPress={() => setOpen(false)} />
321
+ <View className="w-full bg-card rounded-t-3xl border-t border-border p-6 max-h-[75%]">
322
+ <View className="flex-row items-center justify-between pb-3 border-b border-border mb-3">
323
+ <Text className="text-lg font-bold text-foreground">{title || "Select"}</Text>
324
+ <Pressable onPress={() => setOpen(false)} className="p-1">
325
+ <Text className="text-muted text-lg font-semibold">✕</Text>
326
+ </Pressable>
327
+ </View>
328
+ <ScrollView className="max-h-80">{children}</ScrollView>
329
+ </View>
330
+ </View>
331
+ </Modal>
332
+ );
333
+ };
334
+ SelectContent.displayName = "SelectContent";
335
+
336
+ export interface SelectItemProps {
337
+ value: string;
338
+ className?: string;
339
+ children: React.ReactNode;
340
+ }
341
+
342
+ export const SelectItem: React.FC<SelectItemProps> = ({
343
+ value: itemValue,
344
+ className,
345
+ children,
346
+ }) => {
347
+ const { value: selectedValue, onSelect } = useSelectContext();
348
+ const isSelected = selectedValue === itemValue;
349
+
350
+ return (
351
+ <Pressable
352
+ onPress={() => onSelect(itemValue)}
353
+ className={cn(
354
+ "flex-row items-center justify-between py-3.5 px-3 rounded-xl active:bg-muted/10",
355
+ isSelected && "bg-primary/10",
356
+ className
357
+ )}
358
+ >
359
+ {typeof children === "string" ? (
360
+ <Text
361
+ className={cn(
362
+ "text-base",
363
+ isSelected ? "text-primary font-semibold" : "text-foreground font-normal"
364
+ )}
365
+ >
366
+ {children}
367
+ </Text>
368
+ ) : (
369
+ children
370
+ )}
371
+ {isSelected && <Text className="text-primary font-bold text-base">✓</Text>}
372
+ </Pressable>
373
+ );
374
+ };
375
+ SelectItem.displayName = "SelectItem";
@@ -0,0 +1 @@
1
+ export * from "./separator";
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { cn } from "../../utilities/cn";
4
+
5
+ export interface SeparatorProps extends ViewProps {
6
+ orientation?: "horizontal" | "vertical";
7
+ className?: string;
8
+ }
9
+
10
+ export function Separator({
11
+ orientation = "horizontal",
12
+ className,
13
+ ...props
14
+ }: SeparatorProps) {
15
+ return (
16
+ <View
17
+ accessibilityRole="none"
18
+ className={cn(
19
+ "bg-neutral-200 dark:bg-neutral-800",
20
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ Separator.displayName = "Separator";
@@ -0,0 +1 @@
1
+ export * from "./skeleton";
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { Animated, type ViewProps } from "react-native";
3
+ import { cn } from "../../utilities/cn";
4
+
5
+ export interface SkeletonProps extends ViewProps {
6
+ className?: string;
7
+ }
8
+
9
+ export function Skeleton({ className, style, ...props }: SkeletonProps) {
10
+ const pulseAnim = React.useRef(new Animated.Value(0.4)).current;
11
+
12
+ React.useEffect(() => {
13
+ const animation = Animated.loop(
14
+ Animated.sequence([
15
+ Animated.timing(pulseAnim, {
16
+ toValue: 1,
17
+ duration: 900,
18
+ useNativeDriver: true,
19
+ }),
20
+ Animated.timing(pulseAnim, {
21
+ toValue: 0.4,
22
+ duration: 900,
23
+ useNativeDriver: true,
24
+ }),
25
+ ])
26
+ );
27
+ animation.start();
28
+ return () => animation.stop();
29
+ }, [pulseAnim]);
30
+
31
+ return (
32
+ <Animated.View
33
+ style={[{ opacity: pulseAnim }, style]}
34
+ className={cn(
35
+ "rounded-xl bg-neutral-200 dark:bg-neutral-800",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ Skeleton.displayName = "Skeleton";
@@ -0,0 +1 @@
1
+ export * from "./spinner";
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { ActivityIndicator, View, type ViewProps } from "react-native";
3
+ import { cn } from "../../utilities/cn";
4
+
5
+ export type SpinnerColor =
6
+ | "primary"
7
+ | "secondary"
8
+ | "danger"
9
+ | "success"
10
+ | "warning"
11
+ | "white"
12
+ | "muted";
13
+
14
+ export interface SpinnerProps extends ViewProps {
15
+ size?: "sm" | "md" | "lg";
16
+ color?: SpinnerColor;
17
+ className?: string;
18
+ }
19
+
20
+ const colorMap: Record<SpinnerColor, string> = {
21
+ primary: "#2563eb",
22
+ secondary: "#475569",
23
+ danger: "#dc2626",
24
+ success: "#16a34a",
25
+ warning: "#d97706",
26
+ white: "#ffffff",
27
+ muted: "#94a3b8",
28
+ };
29
+
30
+ export function Spinner({
31
+ size = "md",
32
+ color = "primary",
33
+ className,
34
+ ...props
35
+ }: SpinnerProps) {
36
+ const nativeSize = size === "lg" ? "large" : "small";
37
+ const hexColor = colorMap[color] ?? colorMap.primary;
38
+
39
+ return (
40
+ <View className={cn("items-center justify-center", className)} {...props}>
41
+ <ActivityIndicator size={nativeSize} color={hexColor} />
42
+ </View>
43
+ );
44
+ }
45
+
46
+ Spinner.displayName = "Spinner";
@@ -0,0 +1 @@
1
+ export * from "./stats-card";
@@ -0,0 +1,92 @@
1
+ import * as React from "react";
2
+ import { View, Text, type ViewProps } from "react-native";
3
+ import { cn } from "../../utilities/cn";
4
+ import { Card, CardContent } from "../card/card";
5
+
6
+ export type TrendDirection = "up" | "down" | "neutral";
7
+
8
+ export interface StatsCardProps extends ViewProps {
9
+ title: string;
10
+ value: string | number;
11
+ trend?: {
12
+ direction: TrendDirection;
13
+ value: string | number;
14
+ label?: string;
15
+ };
16
+ icon?: React.ReactNode;
17
+ className?: string;
18
+ }
19
+
20
+ export function StatsCard({
21
+ title,
22
+ value,
23
+ trend,
24
+ icon,
25
+ className,
26
+ ...props
27
+ }: StatsCardProps) {
28
+ const isUp = trend?.direction === "up";
29
+ const isDown = trend?.direction === "down";
30
+
31
+ return (
32
+ <Card className={cn("w-full shadow-sm", className)} {...props}>
33
+ <CardContent className="p-4">
34
+ <View className="flex-row items-center justify-between mb-2">
35
+ <Text className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
36
+ {title}
37
+ </Text>
38
+ {icon ? (
39
+ <View className="h-9 w-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center">
40
+ {icon}
41
+ </View>
42
+ ) : null}
43
+ </View>
44
+
45
+ <Text className="text-2xl font-extrabold tracking-tight text-neutral-900 dark:text-neutral-50 mb-2">
46
+ {value}
47
+ </Text>
48
+
49
+ {trend ? (
50
+ <View className="flex-row items-center gap-1.5 flex-wrap">
51
+ <View
52
+ className={cn(
53
+ "flex-row items-center rounded-md px-1.5 py-0.5",
54
+ isUp && "bg-success-100 dark:bg-success-950/60",
55
+ isDown && "bg-danger-100 dark:bg-danger-950/60",
56
+ !isUp && !isDown && "bg-neutral-100 dark:bg-neutral-800"
57
+ )}
58
+ >
59
+ <Text
60
+ className={cn(
61
+ "text-xs font-bold mr-1",
62
+ isUp && "text-success-700 dark:text-success-300",
63
+ isDown && "text-danger-700 dark:text-danger-300",
64
+ !isUp && !isDown && "text-neutral-600 dark:text-neutral-400"
65
+ )}
66
+ >
67
+ {isUp ? "↑" : isDown ? "↓" : "—"}
68
+ </Text>
69
+ <Text
70
+ className={cn(
71
+ "text-xs font-semibold",
72
+ isUp && "text-success-700 dark:text-success-300",
73
+ isDown && "text-danger-700 dark:text-danger-300",
74
+ !isUp && !isDown && "text-neutral-600 dark:text-neutral-400"
75
+ )}
76
+ >
77
+ {trend.value}
78
+ </Text>
79
+ </View>
80
+ {trend.label ? (
81
+ <Text className="text-xs text-neutral-500 dark:text-neutral-400">
82
+ {trend.label}
83
+ </Text>
84
+ ) : null}
85
+ </View>
86
+ ) : null}
87
+ </CardContent>
88
+ </Card>
89
+ );
90
+ }
91
+
92
+ StatsCard.displayName = "StatsCard";
@@ -0,0 +1 @@
1
+ export * from "./switch";
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import {
3
+ Pressable,
4
+ View,
5
+ type GestureResponderEvent,
6
+ type PressableProps,
7
+ } from "react-native";
8
+ import { cn } from "../../utilities/cn";
9
+
10
+ export interface SwitchProps extends Omit<PressableProps, "onPress"> {
11
+ checked?: boolean;
12
+ defaultChecked?: boolean;
13
+ onCheckedChange?: (checked: boolean) => void;
14
+ disabled?: boolean;
15
+ className?: string;
16
+ thumbClassName?: string;
17
+ onPress?: (event: GestureResponderEvent) => void;
18
+ }
19
+
20
+ export const Switch = React.forwardRef<
21
+ React.ElementRef<typeof Pressable>,
22
+ SwitchProps
23
+ >(function Switch(
24
+ {
25
+ checked: controlledChecked,
26
+ defaultChecked = false,
27
+ onCheckedChange,
28
+ disabled = false,
29
+ className,
30
+ thumbClassName,
31
+ onPress,
32
+ ...props
33
+ },
34
+ ref
35
+ ) {
36
+ const [uncontrolledChecked, setUncontrolledChecked] = React.useState(defaultChecked);
37
+ const isControlled = controlledChecked !== undefined;
38
+ const isChecked = isControlled ? controlledChecked : uncontrolledChecked;
39
+
40
+ const handlePress = (e: GestureResponderEvent) => {
41
+ if (disabled) return;
42
+ const nextChecked = !isChecked;
43
+ if (!isControlled) {
44
+ setUncontrolledChecked(nextChecked);
45
+ }
46
+ onCheckedChange?.(nextChecked);
47
+ onPress?.(e);
48
+ };
49
+
50
+ return (
51
+ <Pressable
52
+ ref={ref}
53
+ role="switch"
54
+ accessibilityRole="switch"
55
+ accessibilityState={{ checked: isChecked, disabled: !!disabled }}
56
+ disabled={disabled}
57
+ onPress={handlePress}
58
+ className={cn(
59
+ "h-7 w-12 shrink-0 flex-row items-center rounded-full p-0.5 transition-colors",
60
+ isChecked
61
+ ? "bg-primary-600 dark:bg-primary-600"
62
+ : "bg-neutral-300 dark:bg-neutral-700",
63
+ disabled && "opacity-50",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ <View
69
+ role="presentation"
70
+ className={cn(
71
+ "h-6 w-6 rounded-full bg-white shadow-sm transition-transform",
72
+ isChecked ? "translate-x-5" : "translate-x-0",
73
+ thumbClassName
74
+ )}
75
+ />
76
+ </Pressable>
77
+ );
78
+ });
79
+
80
+ Switch.displayName = "Switch";
@@ -0,0 +1 @@
1
+ export * from "./tabs";