@croacroa/react-native-template 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +187 -184
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/CHANGELOG.md +106 -106
- package/CONTRIBUTING.md +377 -377
- package/README.md +399 -399
- package/__tests__/components/snapshots.test.tsx +131 -0
- package/__tests__/integration/auth-api.test.tsx +227 -0
- package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
- package/app/(public)/onboarding.tsx +5 -5
- package/app.config.ts +45 -2
- package/assets/images/.gitkeep +7 -7
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/SuspenseBoundary.tsx +357 -0
- package/components/providers/index.ts +21 -0
- package/components/ui/Avatar.tsx +316 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Select.tsx +240 -240
- package/components/ui/VirtualizedList.tsx +285 -0
- package/components/ui/index.ts +23 -18
- package/constants/config.ts +97 -54
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/hooks/index.ts +27 -25
- package/hooks/useApi.ts +102 -5
- package/hooks/useAuth.tsx +82 -0
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useMFA.ts +499 -0
- package/hooks/useNotifications.ts +39 -0
- package/hooks/useOffline.ts +60 -6
- package/hooks/usePerformance.ts +434 -434
- package/hooks/useTheme.tsx +76 -0
- package/hooks/useUpdates.ts +358 -358
- package/i18n/index.ts +194 -77
- package/i18n/locales/ar.json +101 -0
- package/i18n/locales/de.json +101 -0
- package/i18n/locales/en.json +101 -101
- package/i18n/locales/es.json +101 -0
- package/i18n/locales/fr.json +101 -101
- package/jest.config.js +4 -4
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -0
- package/maestro/flows/mfa-setup.yaml +86 -0
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -0
- package/maestro/flows/offline-sync.yaml +128 -0
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +175 -170
- package/services/analytics.ts +428 -428
- package/services/api.ts +340 -340
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +626 -0
- package/services/index.ts +54 -22
- package/services/security.ts +286 -0
- package/tailwind.config.js +47 -47
- package/utils/accessibility.ts +446 -446
- package/utils/index.ts +52 -43
- package/utils/validation.ts +2 -1
- package/utils/withAccessibility.tsx +272 -0
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
import { TouchableOpacity, View, Text } from "react-native";
|
|
2
|
-
import Animated, {
|
|
3
|
-
useAnimatedStyle,
|
|
4
|
-
withSpring,
|
|
5
|
-
withTiming,
|
|
6
|
-
interpolateColor,
|
|
7
|
-
} from "react-native-reanimated";
|
|
8
|
-
import { Ionicons } from "@expo/vector-icons";
|
|
9
|
-
import { useTheme } from "@/hooks/useTheme";
|
|
10
|
-
import { cn } from "@/utils/cn";
|
|
11
|
-
|
|
12
|
-
interface CheckboxProps {
|
|
13
|
-
/**
|
|
14
|
-
* Whether the checkbox is checked
|
|
15
|
-
*/
|
|
16
|
-
checked: boolean;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Callback when the checkbox is toggled
|
|
20
|
-
*/
|
|
21
|
-
onChange: (checked: boolean) => void;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Label text
|
|
25
|
-
*/
|
|
26
|
-
label?: string;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Description text (below label)
|
|
30
|
-
*/
|
|
31
|
-
description?: string;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Whether the checkbox is disabled
|
|
35
|
-
*/
|
|
36
|
-
disabled?: boolean;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Size variant
|
|
40
|
-
*/
|
|
41
|
-
size?: "sm" | "md" | "lg";
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Additional class name
|
|
45
|
-
*/
|
|
46
|
-
className?: string;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Error message
|
|
50
|
-
*/
|
|
51
|
-
error?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const AnimatedView = Animated.createAnimatedComponent(View);
|
|
55
|
-
|
|
56
|
-
const sizes = {
|
|
57
|
-
sm: { box: 18, icon: 12, label: "text-sm" },
|
|
58
|
-
md: { box: 22, icon: 16, label: "text-base" },
|
|
59
|
-
lg: { box: 26, icon: 20, label: "text-lg" },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export function Checkbox({
|
|
63
|
-
checked,
|
|
64
|
-
onChange,
|
|
65
|
-
label,
|
|
66
|
-
description,
|
|
67
|
-
disabled = false,
|
|
68
|
-
size = "md",
|
|
69
|
-
className,
|
|
70
|
-
error,
|
|
71
|
-
}: CheckboxProps) {
|
|
72
|
-
const { isDark } = useTheme();
|
|
73
|
-
const sizeConfig = sizes[size];
|
|
74
|
-
|
|
75
|
-
const animatedBoxStyle = useAnimatedStyle(() => {
|
|
76
|
-
const backgroundColor = interpolateColor(
|
|
77
|
-
checked ? 1 : 0,
|
|
78
|
-
[0, 1],
|
|
79
|
-
["transparent", "#10b981"]
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const borderColor = interpolateColor(
|
|
83
|
-
checked ? 1 : 0,
|
|
84
|
-
[0, 1],
|
|
85
|
-
[error ? "#ef4444" : isDark ? "#475569" : "#cbd5e1", "#10b981"]
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
backgroundColor: withTiming(backgroundColor, { duration: 150 }),
|
|
90
|
-
borderColor: withTiming(borderColor, { duration: 150 }),
|
|
91
|
-
transform: [
|
|
92
|
-
{
|
|
93
|
-
scale: withSpring(checked ? 1 : 0.95, {
|
|
94
|
-
damping: 15,
|
|
95
|
-
stiffness: 400,
|
|
96
|
-
}),
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
};
|
|
100
|
-
}, [checked, isDark, error]);
|
|
101
|
-
|
|
102
|
-
const animatedCheckStyle = useAnimatedStyle(() => {
|
|
103
|
-
return {
|
|
104
|
-
opacity: withTiming(checked ? 1 : 0, { duration: 150 }),
|
|
105
|
-
transform: [
|
|
106
|
-
{
|
|
107
|
-
scale: withSpring(checked ? 1 : 0.5, {
|
|
108
|
-
damping: 15,
|
|
109
|
-
stiffness: 400,
|
|
110
|
-
}),
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
}, [checked]);
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<TouchableOpacity
|
|
118
|
-
onPress={() => !disabled && onChange(!checked)}
|
|
119
|
-
disabled={disabled}
|
|
120
|
-
activeOpacity={0.7}
|
|
121
|
-
className={cn(
|
|
122
|
-
"flex-row items-start",
|
|
123
|
-
disabled && "opacity-50",
|
|
124
|
-
className
|
|
125
|
-
)}
|
|
126
|
-
>
|
|
127
|
-
<AnimatedView
|
|
128
|
-
style={[
|
|
129
|
-
animatedBoxStyle,
|
|
130
|
-
{
|
|
131
|
-
width: sizeConfig.box,
|
|
132
|
-
height: sizeConfig.box,
|
|
133
|
-
borderWidth: 2,
|
|
134
|
-
borderRadius: 6,
|
|
135
|
-
justifyContent: "center",
|
|
136
|
-
alignItems: "center",
|
|
137
|
-
marginTop: 2,
|
|
138
|
-
},
|
|
139
|
-
]}
|
|
140
|
-
>
|
|
141
|
-
<Animated.View style={animatedCheckStyle}>
|
|
142
|
-
<Ionicons name="checkmark" size={sizeConfig.icon} color="white" />
|
|
143
|
-
</Animated.View>
|
|
144
|
-
</AnimatedView>
|
|
145
|
-
|
|
146
|
-
{(label || description) && (
|
|
147
|
-
<View className="flex-1 ml-3">
|
|
148
|
-
{label && (
|
|
149
|
-
<Text
|
|
150
|
-
className={cn(
|
|
151
|
-
sizeConfig.label,
|
|
152
|
-
isDark ? "text-text-dark" : "text-text-light",
|
|
153
|
-
disabled && "opacity-70"
|
|
154
|
-
)}
|
|
155
|
-
>
|
|
156
|
-
{label}
|
|
157
|
-
</Text>
|
|
158
|
-
)}
|
|
159
|
-
{description && (
|
|
160
|
-
<Text
|
|
161
|
-
className={cn(
|
|
162
|
-
"text-sm mt-0.5",
|
|
163
|
-
isDark ? "text-muted-dark" : "text-muted-light"
|
|
164
|
-
)}
|
|
165
|
-
>
|
|
166
|
-
{description}
|
|
167
|
-
</Text>
|
|
168
|
-
)}
|
|
169
|
-
{error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
|
|
170
|
-
</View>
|
|
171
|
-
)}
|
|
172
|
-
</TouchableOpacity>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Checkbox Group component for multiple checkboxes
|
|
178
|
-
*/
|
|
179
|
-
interface CheckboxGroupProps<T extends string> {
|
|
180
|
-
/**
|
|
181
|
-
* Available options
|
|
182
|
-
*/
|
|
183
|
-
options: {
|
|
184
|
-
value: T;
|
|
185
|
-
label: string;
|
|
186
|
-
description?: string;
|
|
187
|
-
disabled?: boolean;
|
|
188
|
-
}[];
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Currently selected values
|
|
192
|
-
*/
|
|
193
|
-
value: T[];
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Callback when selection changes
|
|
197
|
-
*/
|
|
198
|
-
onChange: (value: T[]) => void;
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Group label
|
|
202
|
-
*/
|
|
203
|
-
label?: string;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Size variant
|
|
207
|
-
*/
|
|
208
|
-
size?: "sm" | "md" | "lg";
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Additional class name
|
|
212
|
-
*/
|
|
213
|
-
className?: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function CheckboxGroup<T extends string>({
|
|
217
|
-
options,
|
|
218
|
-
value,
|
|
219
|
-
onChange,
|
|
220
|
-
label,
|
|
221
|
-
size = "md",
|
|
222
|
-
className,
|
|
223
|
-
}: CheckboxGroupProps<T>) {
|
|
224
|
-
const { isDark } = useTheme();
|
|
225
|
-
|
|
226
|
-
const handleToggle = (optionValue: T) => {
|
|
227
|
-
if (value.includes(optionValue)) {
|
|
228
|
-
onChange(value.filter((v) => v !== optionValue));
|
|
229
|
-
} else {
|
|
230
|
-
onChange([...value, optionValue]);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
return (
|
|
235
|
-
<View className={className}>
|
|
236
|
-
{label && (
|
|
237
|
-
<Text
|
|
238
|
-
className={cn(
|
|
239
|
-
"text-sm font-medium mb-3",
|
|
240
|
-
isDark ? "text-text-dark" : "text-text-light"
|
|
241
|
-
)}
|
|
242
|
-
>
|
|
243
|
-
{label}
|
|
244
|
-
</Text>
|
|
245
|
-
)}
|
|
246
|
-
<View className="gap-3">
|
|
247
|
-
{options.map((option) => (
|
|
248
|
-
<Checkbox
|
|
249
|
-
key={option.value}
|
|
250
|
-
checked={value.includes(option.value)}
|
|
251
|
-
onChange={() => handleToggle(option.value)}
|
|
252
|
-
label={option.label}
|
|
253
|
-
description={option.description}
|
|
254
|
-
disabled={option.disabled}
|
|
255
|
-
size={size}
|
|
256
|
-
/>
|
|
257
|
-
))}
|
|
258
|
-
</View>
|
|
259
|
-
</View>
|
|
260
|
-
);
|
|
261
|
-
}
|
|
1
|
+
import { TouchableOpacity, View, Text } from "react-native";
|
|
2
|
+
import Animated, {
|
|
3
|
+
useAnimatedStyle,
|
|
4
|
+
withSpring,
|
|
5
|
+
withTiming,
|
|
6
|
+
interpolateColor,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
9
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
10
|
+
import { cn } from "@/utils/cn";
|
|
11
|
+
|
|
12
|
+
interface CheckboxProps {
|
|
13
|
+
/**
|
|
14
|
+
* Whether the checkbox is checked
|
|
15
|
+
*/
|
|
16
|
+
checked: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Callback when the checkbox is toggled
|
|
20
|
+
*/
|
|
21
|
+
onChange: (checked: boolean) => void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Label text
|
|
25
|
+
*/
|
|
26
|
+
label?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Description text (below label)
|
|
30
|
+
*/
|
|
31
|
+
description?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the checkbox is disabled
|
|
35
|
+
*/
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Size variant
|
|
40
|
+
*/
|
|
41
|
+
size?: "sm" | "md" | "lg";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Additional class name
|
|
45
|
+
*/
|
|
46
|
+
className?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Error message
|
|
50
|
+
*/
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const AnimatedView = Animated.createAnimatedComponent(View);
|
|
55
|
+
|
|
56
|
+
const sizes = {
|
|
57
|
+
sm: { box: 18, icon: 12, label: "text-sm" },
|
|
58
|
+
md: { box: 22, icon: 16, label: "text-base" },
|
|
59
|
+
lg: { box: 26, icon: 20, label: "text-lg" },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function Checkbox({
|
|
63
|
+
checked,
|
|
64
|
+
onChange,
|
|
65
|
+
label,
|
|
66
|
+
description,
|
|
67
|
+
disabled = false,
|
|
68
|
+
size = "md",
|
|
69
|
+
className,
|
|
70
|
+
error,
|
|
71
|
+
}: CheckboxProps) {
|
|
72
|
+
const { isDark } = useTheme();
|
|
73
|
+
const sizeConfig = sizes[size];
|
|
74
|
+
|
|
75
|
+
const animatedBoxStyle = useAnimatedStyle(() => {
|
|
76
|
+
const backgroundColor = interpolateColor(
|
|
77
|
+
checked ? 1 : 0,
|
|
78
|
+
[0, 1],
|
|
79
|
+
["transparent", "#10b981"]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const borderColor = interpolateColor(
|
|
83
|
+
checked ? 1 : 0,
|
|
84
|
+
[0, 1],
|
|
85
|
+
[error ? "#ef4444" : isDark ? "#475569" : "#cbd5e1", "#10b981"]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
backgroundColor: withTiming(backgroundColor, { duration: 150 }),
|
|
90
|
+
borderColor: withTiming(borderColor, { duration: 150 }),
|
|
91
|
+
transform: [
|
|
92
|
+
{
|
|
93
|
+
scale: withSpring(checked ? 1 : 0.95, {
|
|
94
|
+
damping: 15,
|
|
95
|
+
stiffness: 400,
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}, [checked, isDark, error]);
|
|
101
|
+
|
|
102
|
+
const animatedCheckStyle = useAnimatedStyle(() => {
|
|
103
|
+
return {
|
|
104
|
+
opacity: withTiming(checked ? 1 : 0, { duration: 150 }),
|
|
105
|
+
transform: [
|
|
106
|
+
{
|
|
107
|
+
scale: withSpring(checked ? 1 : 0.5, {
|
|
108
|
+
damping: 15,
|
|
109
|
+
stiffness: 400,
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}, [checked]);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<TouchableOpacity
|
|
118
|
+
onPress={() => !disabled && onChange(!checked)}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
activeOpacity={0.7}
|
|
121
|
+
className={cn(
|
|
122
|
+
"flex-row items-start",
|
|
123
|
+
disabled && "opacity-50",
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
<AnimatedView
|
|
128
|
+
style={[
|
|
129
|
+
animatedBoxStyle,
|
|
130
|
+
{
|
|
131
|
+
width: sizeConfig.box,
|
|
132
|
+
height: sizeConfig.box,
|
|
133
|
+
borderWidth: 2,
|
|
134
|
+
borderRadius: 6,
|
|
135
|
+
justifyContent: "center",
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
marginTop: 2,
|
|
138
|
+
},
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
<Animated.View style={animatedCheckStyle}>
|
|
142
|
+
<Ionicons name="checkmark" size={sizeConfig.icon} color="white" />
|
|
143
|
+
</Animated.View>
|
|
144
|
+
</AnimatedView>
|
|
145
|
+
|
|
146
|
+
{(label || description) && (
|
|
147
|
+
<View className="flex-1 ml-3">
|
|
148
|
+
{label && (
|
|
149
|
+
<Text
|
|
150
|
+
className={cn(
|
|
151
|
+
sizeConfig.label,
|
|
152
|
+
isDark ? "text-text-dark" : "text-text-light",
|
|
153
|
+
disabled && "opacity-70"
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{label}
|
|
157
|
+
</Text>
|
|
158
|
+
)}
|
|
159
|
+
{description && (
|
|
160
|
+
<Text
|
|
161
|
+
className={cn(
|
|
162
|
+
"text-sm mt-0.5",
|
|
163
|
+
isDark ? "text-muted-dark" : "text-muted-light"
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{description}
|
|
167
|
+
</Text>
|
|
168
|
+
)}
|
|
169
|
+
{error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
|
|
170
|
+
</View>
|
|
171
|
+
)}
|
|
172
|
+
</TouchableOpacity>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Checkbox Group component for multiple checkboxes
|
|
178
|
+
*/
|
|
179
|
+
interface CheckboxGroupProps<T extends string> {
|
|
180
|
+
/**
|
|
181
|
+
* Available options
|
|
182
|
+
*/
|
|
183
|
+
options: {
|
|
184
|
+
value: T;
|
|
185
|
+
label: string;
|
|
186
|
+
description?: string;
|
|
187
|
+
disabled?: boolean;
|
|
188
|
+
}[];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Currently selected values
|
|
192
|
+
*/
|
|
193
|
+
value: T[];
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Callback when selection changes
|
|
197
|
+
*/
|
|
198
|
+
onChange: (value: T[]) => void;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Group label
|
|
202
|
+
*/
|
|
203
|
+
label?: string;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Size variant
|
|
207
|
+
*/
|
|
208
|
+
size?: "sm" | "md" | "lg";
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Additional class name
|
|
212
|
+
*/
|
|
213
|
+
className?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function CheckboxGroup<T extends string>({
|
|
217
|
+
options,
|
|
218
|
+
value,
|
|
219
|
+
onChange,
|
|
220
|
+
label,
|
|
221
|
+
size = "md",
|
|
222
|
+
className,
|
|
223
|
+
}: CheckboxGroupProps<T>) {
|
|
224
|
+
const { isDark } = useTheme();
|
|
225
|
+
|
|
226
|
+
const handleToggle = (optionValue: T) => {
|
|
227
|
+
if (value.includes(optionValue)) {
|
|
228
|
+
onChange(value.filter((v) => v !== optionValue));
|
|
229
|
+
} else {
|
|
230
|
+
onChange([...value, optionValue]);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<View className={className}>
|
|
236
|
+
{label && (
|
|
237
|
+
<Text
|
|
238
|
+
className={cn(
|
|
239
|
+
"text-sm font-medium mb-3",
|
|
240
|
+
isDark ? "text-text-dark" : "text-text-light"
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{label}
|
|
244
|
+
</Text>
|
|
245
|
+
)}
|
|
246
|
+
<View className="gap-3">
|
|
247
|
+
{options.map((option) => (
|
|
248
|
+
<Checkbox
|
|
249
|
+
key={option.value}
|
|
250
|
+
checked={value.includes(option.value)}
|
|
251
|
+
onChange={() => handleToggle(option.value)}
|
|
252
|
+
label={option.label}
|
|
253
|
+
description={option.description}
|
|
254
|
+
disabled={option.disabled}
|
|
255
|
+
size={size}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</View>
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
}
|