@towsila/expo-ui-library 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 (104) hide show
  1. package/README.md +142 -0
  2. package/package.json +55 -0
  3. package/src/assets/customIcons/arrow.svg +3 -0
  4. package/src/assets/customIcons/arrow_back.svg +3 -0
  5. package/src/assets/customIcons/ban.svg +4 -0
  6. package/src/assets/customIcons/checkmark.svg +4 -0
  7. package/src/assets/customIcons/close.svg +5 -0
  8. package/src/assets/customIcons/dark_mode.svg +3 -0
  9. package/src/assets/customIcons/dark_suspended.svg +67 -0
  10. package/src/assets/customIcons/document.svg +4 -0
  11. package/src/assets/customIcons/email.svg +3 -0
  12. package/src/assets/customIcons/eye.svg +5 -0
  13. package/src/assets/customIcons/eye_off.svg +7 -0
  14. package/src/assets/customIcons/humberger.svg +5 -0
  15. package/src/assets/customIcons/information.svg +6 -0
  16. package/src/assets/customIcons/light_mode.svg +18 -0
  17. package/src/assets/customIcons/light_suspended.svg +67 -0
  18. package/src/assets/customIcons/lock_closed.svg +4 -0
  19. package/src/assets/customIcons/logout.svg +4 -0
  20. package/src/assets/customIcons/person.svg +4 -0
  21. package/src/assets/customIcons/phone.svg +3 -0
  22. package/src/assets/customIcons/phone_dark.svg +36 -0
  23. package/src/assets/customIcons/phone_light.svg +36 -0
  24. package/src/assets/customIcons/shield.svg +4 -0
  25. package/src/assets/customIcons/trash.svg +9 -0
  26. package/src/assets/customIcons/warning.svg +5 -0
  27. package/src/assets/icons/google.svg +9 -0
  28. package/src/assets/images/bg_dark.svg +5 -0
  29. package/src/assets/images/bg_dark_v1.svg +5 -0
  30. package/src/assets/images/bg_light.svg +5 -0
  31. package/src/assets/images/bg_light_v1.svg +5 -0
  32. package/src/assets/images/en.jpg +0 -0
  33. package/src/assets/images/fr.jpg +0 -0
  34. package/src/components/icons/ban.tsx +77 -0
  35. package/src/components/icons/chatIcon.tsx +19 -0
  36. package/src/components/icons/homeIcon.tsx +25 -0
  37. package/src/components/icons/profileIcon.tsx +29 -0
  38. package/src/components/icons/settingsIcon.tsx +22 -0
  39. package/src/components/reusable/templatePage.tsx +94 -0
  40. package/src/components/reusable/templateSubPage.tsx +66 -0
  41. package/src/components/screens/AccountStatusScreen.tsx +102 -0
  42. package/src/components/screens/AddReportScreen.tsx +135 -0
  43. package/src/components/screens/LegalScreen.tsx +41 -0
  44. package/src/components/screens/VerificationCodeScreen.tsx +66 -0
  45. package/src/components/screens/aboutScreen.tsx +139 -0
  46. package/src/components/screens/forgotPasswordScreen.tsx +60 -0
  47. package/src/components/screens/loginScreen.tsx +165 -0
  48. package/src/components/screens/onBoarding.tsx +178 -0
  49. package/src/components/screens/resetPassword.tsx +115 -0
  50. package/src/components/screens/settingScreen.tsx +85 -0
  51. package/src/components/screens/signUpScreen.tsx +119 -0
  52. package/src/components/screens/splashScreen.tsx +170 -0
  53. package/src/components/ui/OTPInput.tsx +73 -0
  54. package/src/components/ui/PageTopBar.tsx +41 -0
  55. package/src/components/ui/asidebar.tsx +151 -0
  56. package/src/components/ui/badge.tsx +21 -0
  57. package/src/components/ui/button.tsx +58 -0
  58. package/src/components/ui/customPopup.tsx +176 -0
  59. package/src/components/ui/dynamicForm.tsx +144 -0
  60. package/src/components/ui/error.tsx +23 -0
  61. package/src/components/ui/input.tsx +121 -0
  62. package/src/components/ui/languageSelect.tsx +99 -0
  63. package/src/components/ui/pageIndicator.tsx +58 -0
  64. package/src/components/ui/passwordRequirmentCard.tsx +55 -0
  65. package/src/components/ui/sharpButton.tsx +67 -0
  66. package/src/components/ui/sharpContainer.tsx +180 -0
  67. package/src/components/ui/sharpDynamicForm.tsx +148 -0
  68. package/src/components/ui/sharpInput.tsx +148 -0
  69. package/src/components/ui/sharpOtpInput.tsx +228 -0
  70. package/src/components/ui/sharpSelect.tsx +155 -0
  71. package/src/components/ui/successDialog.tsx +97 -0
  72. package/src/components/ui/termsPopup.tsx +56 -0
  73. package/src/components/ui/themeToggle.tsx +98 -0
  74. package/src/components/ui/toast.tsx +163 -0
  75. package/src/components/ui/warningModel.tsx +132 -0
  76. package/src/constants/language.ts +15 -0
  77. package/src/context/LanguageContext.tsx +49 -0
  78. package/src/context/LocationPermissionProvider.tsx +106 -0
  79. package/src/context/ThemeContext.tsx +70 -0
  80. package/src/hooks/useCountdown.ts +21 -0
  81. package/src/hooks/useForgotPasswordForm.ts +46 -0
  82. package/src/hooks/useForm.ts +52 -0
  83. package/src/hooks/useLoginForm.ts +25 -0
  84. package/src/hooks/useResetPasswordForm.ts +75 -0
  85. package/src/hooks/useSignup.tsx +34 -0
  86. package/src/hooks/useSubmitReport.ts +47 -0
  87. package/src/hooks/useUpdateUser.tsx +81 -0
  88. package/src/icons/index.ts +36 -0
  89. package/src/index.ts +62 -0
  90. package/src/service/createApi.ts +57 -0
  91. package/src/service/sendEmail.ts +11 -0
  92. package/src/tokens/themes.ts +19 -0
  93. package/src/tokens/typography.ts +16 -0
  94. package/src/types/formFields.ts +13 -0
  95. package/src/types/icon.ts +6 -0
  96. package/src/types/language.ts +7 -0
  97. package/src/types/menu.ts +9 -0
  98. package/src/types/settingsFields.tsx +9 -0
  99. package/src/types/svg.d.ts +6 -0
  100. package/src/types/validateForm.ts +8 -0
  101. package/src/utils/helpers/groupSettings.ts +21 -0
  102. package/src/utils/inputValidator/customValidator.ts +36 -0
  103. package/src/utils/inputValidator/generalTerms.ts +19 -0
  104. package/src/utils/inputValidator/registerValidator.ts +31 -0
@@ -0,0 +1,121 @@
1
+ import React, { useState, ComponentProps } from "react";
2
+ import { StyleProp, Text, TextInput, TextInputProps, View, ViewProps } from "react-native";
3
+ import { useAppTheme } from "../../context/ThemeContext";
4
+ import { textSize, Fonts } from "../../tokens/typography";
5
+ import { Ionicons } from "@expo/vector-icons";
6
+ import ErrorMessage from "./error";
7
+ interface InputProps extends TextInputProps {
8
+ value: string;
9
+ onChangeText: (text: string) => void;
10
+ placeholder: string;
11
+ iconLeft?: ComponentProps<typeof Ionicons>["name"];
12
+ iconRight?: ComponentProps<typeof Ionicons>["name"];
13
+ onPressIconRight?: () => void;
14
+ error?: string;
15
+ label?:string;
16
+ onClearError: () => void;
17
+ style?: StyleProp<ViewProps>;
18
+ labelFontFamily?: string;
19
+ textFontFamily?: string;
20
+ }
21
+
22
+ export default function Input({
23
+ value,
24
+ onChangeText,
25
+ placeholder,
26
+ style,
27
+ iconLeft,
28
+ iconRight,
29
+ onPressIconRight,
30
+ error,
31
+ label,
32
+ onClearError,
33
+ labelFontFamily = Fonts.montserrat.semiBold,
34
+ textFontFamily = Fonts.montserrat.regular,
35
+ ...rest
36
+ }: InputProps) {
37
+ const { colors } = useAppTheme();
38
+ const [focused, setFocused] = useState(false);
39
+ return (
40
+ <View style={[style, {marginBottom: 4}]}>
41
+ {label && (
42
+ <Text style={{
43
+ color: colors.text.primary,
44
+ fontSize: textSize.xs,
45
+ fontFamily: labelFontFamily,
46
+ marginBottom: 6,
47
+ }}>
48
+ {label}
49
+ </Text>
50
+ )}
51
+ <View style={{position: "relative"}}>
52
+ {/* icon left */}
53
+ {iconLeft && (
54
+ <Ionicons
55
+ name={iconLeft}
56
+ size={18}
57
+ color={error ? colors.semantic.error : focused ? colors.primary : colors.text.secondary}
58
+ style={{
59
+ position: "absolute",
60
+ left: 14,
61
+ top: "50%",
62
+ transform: [{ translateY: -9 }],
63
+ zIndex: 1,
64
+ }}
65
+ />
66
+ )}
67
+ {/* input */}
68
+ <TextInput
69
+ value={value}
70
+ onChangeText={(text) => {
71
+ onChangeText(text);
72
+ if (error) onClearError?.();
73
+ }}
74
+ placeholder={placeholder}
75
+ placeholderTextColor={colors.text.secondary}
76
+ onFocus={() => {
77
+ setFocused(true);
78
+ if (error) onClearError?.();
79
+ }}
80
+ onBlur={() => setFocused(false)}
81
+ style={{
82
+ height: 50,
83
+ borderRadius: 10,
84
+ fontSize: textSize.sm,
85
+ backgroundColor: colors.background.main,
86
+ color: colors.text.primary,
87
+ fontFamily: textFontFamily,
88
+ paddingLeft: iconLeft ? 42 : 16,
89
+ paddingRight: iconRight ? 42 : 16,
90
+ borderColor: error
91
+ ? colors.semantic.error
92
+ : focused
93
+ ? colors.primary
94
+ : colors.semantic.border,
95
+ borderWidth: error ? 1 : focused ? 1.5 : 0.5,
96
+ }}
97
+ {...rest}
98
+ />
99
+ {/* icon right */}
100
+ {iconRight && (
101
+ <Ionicons
102
+ name={iconRight}
103
+ size={18}
104
+ color={colors.text.secondary}
105
+ onPress={onPressIconRight}
106
+ style={{
107
+ position: "absolute",
108
+ right: 14,
109
+ top: "50%",
110
+ transform: [{ translateY: -9 }],
111
+ zIndex: 1,
112
+ }}
113
+ />
114
+ )}
115
+ </View>
116
+ {error && (
117
+ <ErrorMessage message={error}/>
118
+ )}
119
+ </View>
120
+ );
121
+ }
@@ -0,0 +1,99 @@
1
+ import { useState } from "react";
2
+ import { Image, Text, TouchableOpacity, View } from "react-native";
3
+ import { Ionicons } from "@expo/vector-icons";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+ import { Fonts, textSize } from "../../tokens/typography";
6
+ import { LANGUAGES } from "../../constants/language";
7
+ import { Language } from "../../context/LanguageContext";
8
+
9
+ interface LanguageSelectProps {
10
+ value: Language;
11
+ onChange: (code: Language) => void;
12
+ }
13
+
14
+ export default function LanguageSelect({ value, onChange }: LanguageSelectProps) {
15
+ const { colors } = useAppTheme();
16
+ const [open, setOpen] = useState(false);
17
+
18
+ const selected = LANGUAGES.find((l) => l.code === value) ?? LANGUAGES[0];
19
+
20
+ return (
21
+ <View style={{ position: "relative", zIndex: open ? 100 : 1 }}>
22
+ {/* Selected value / trigger */}
23
+ <TouchableOpacity
24
+ onPress={() => setOpen((prev) => !prev)}
25
+ style={{
26
+ flexDirection: "row",
27
+ alignItems: "center",
28
+ justifyContent: "space-between",
29
+ }}
30
+ >
31
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10}}>
32
+ <Image source={selected.logo} style={{ width: 20, height: 20, borderRadius: 10 }} />
33
+ <Text style={{
34
+ fontFamily: Fonts.montserrat.regular,
35
+ fontSize: textSize.sm,
36
+ color: colors.text.primary,
37
+ }}>
38
+ {selected.code}
39
+ </Text>
40
+ </View>
41
+ <Ionicons
42
+ name={open ? "chevron-up" : "chevron-down"}
43
+ size={18}
44
+ color={colors.text.secondary}
45
+ />
46
+ </TouchableOpacity>
47
+
48
+ {/* Expanded options — absolutely positioned so it floats over content below instead of pushing it down */}
49
+ {open && (
50
+ <View style={{
51
+ position: "absolute",
52
+ top: "100%",
53
+ left: -40,
54
+ right: 0,
55
+ marginTop: 10,
56
+ backgroundColor: colors.background.card,
57
+ borderRadius: 10,
58
+ overflow: "hidden",
59
+ elevation: 6, // Android shadow
60
+ shadowColor: "#000", // iOS shadow
61
+ shadowOffset: { width: 0, height: 2 },
62
+ shadowOpacity: 0.15,
63
+ shadowRadius: 6,
64
+ width:100
65
+ }}>
66
+ {LANGUAGES.map((lang) => (
67
+ <TouchableOpacity
68
+ key={lang.code}
69
+ onPress={() => {
70
+ onChange(lang.code);
71
+ setOpen(false);
72
+ }}
73
+ style={{
74
+ flexDirection: "row",
75
+ alignItems: "center",
76
+ gap: 6,
77
+ paddingVertical: 14,
78
+ paddingHorizontal: 14,
79
+ }}
80
+ >
81
+ <Image source={lang.logo} style={{ width: 20, height: 20, borderRadius: 10 }} />
82
+ <Text style={{
83
+ fontFamily: Fonts.montserrat.regular,
84
+ fontSize: textSize.sm,
85
+ color: colors.text.primary,
86
+ flex: 1,
87
+ }}>
88
+ {lang.code}
89
+ </Text>
90
+ {lang.code === value && (
91
+ <Ionicons name="checkmark" size={20} color={colors.primary} />
92
+ )}
93
+ </TouchableOpacity>
94
+ ))}
95
+ </View>
96
+ )}
97
+ </View>
98
+ );
99
+ }
@@ -0,0 +1,58 @@
1
+ import { View, StyleSheet } from "react-native";
2
+ import Svg, { Circle } from "react-native-svg";
3
+
4
+ type Props = {
5
+ total?: number;
6
+ current?: number;
7
+ borderColor: string;
8
+ color?: string;
9
+ defaultColor:string;
10
+ };
11
+
12
+ const SIZE = 15;
13
+ const CENTER = SIZE / 2;
14
+
15
+ export default function PageIndicator({
16
+ total = 3,
17
+ current = 1,
18
+ borderColor = "black",
19
+ color = "white",
20
+ defaultColor,
21
+ }: Props) {
22
+ return (
23
+ <View style={styles.container}>
24
+ {Array.from({ length: total }).map((_, index) => (
25
+ <Svg
26
+ key={index}
27
+ width={SIZE}
28
+ height={SIZE}
29
+ viewBox={`0 0 ${SIZE} ${SIZE}`}
30
+ >
31
+ {/* Outer circle (border) */}
32
+ <Circle
33
+ cx={CENTER}
34
+ cy={CENTER}
35
+ r={CENTER - 0.5}
36
+ fill={borderColor}
37
+ />
38
+
39
+ {/* Inner circle */}
40
+ <Circle
41
+ cx={CENTER - 1} // Move slightly right -> thinner left border
42
+ cy={CENTER}
43
+ r={CENTER - 3}
44
+ fill={current === index ? color : defaultColor}
45
+ />
46
+ </Svg>
47
+ ))}
48
+ </View>
49
+ );
50
+ }
51
+
52
+ const styles = StyleSheet.create({
53
+ container: {
54
+ flexDirection: "row",
55
+ justifyContent: "center",
56
+ gap: 8,
57
+ },
58
+ });
@@ -0,0 +1,55 @@
1
+ import { View, Text } from "react-native";
2
+ import { Ionicons } from "@expo/vector-icons";
3
+ import { useAppTheme } from "../../context/ThemeContext";
4
+ import { textSize, Fonts } from "../../tokens/typography";
5
+
6
+ interface PasswordRequirementsCardProps {
7
+ checks: boolean[]; // ← array of booleans instead of named object
8
+ cardItems: string[];
9
+ cardTitle?: string;
10
+ }
11
+
12
+ export default function PasswordRequirementsCard({
13
+ checks,
14
+ cardItems,
15
+ cardTitle,
16
+ }: PasswordRequirementsCardProps) {
17
+ const { colors } = useAppTheme();
18
+
19
+ return (
20
+ <View style={{
21
+ backgroundColor: colors.background.card,
22
+ borderRadius: 14,
23
+ padding: 16,
24
+ gap: 10,
25
+ }}>
26
+ {cardTitle && (
27
+ <Text style={{
28
+ fontSize: textSize.sm,
29
+ fontFamily: Fonts.montserrat.semiBold,
30
+ color: colors.text.primary,
31
+ marginBottom: 4,
32
+ }}>
33
+ {cardTitle}
34
+ </Text>
35
+ )}
36
+
37
+ {cardItems.map((item, index) => (
38
+ <View key={index} style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
39
+ <Ionicons
40
+ name={checks[index] ? "checkmark-circle" : "ellipse-outline"}
41
+ size={16}
42
+ color={checks[index] ? colors.semantic.success : colors.text.secondary}
43
+ />
44
+ <Text style={{
45
+ fontSize: textSize.sm,
46
+ fontFamily: Fonts.montserrat.regular,
47
+ color: checks[index] ? colors.text.primary : colors.text.secondary,
48
+ }}>
49
+ {item}
50
+ </Text>
51
+ </View>
52
+ ))}
53
+ </View>
54
+ );
55
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, View, LayoutChangeEvent } from "react-native";
3
+ import { Fonts, textSize } from "../../tokens/typography";
4
+ import { useAppTheme } from "../../context/ThemeContext";
5
+ import SharpContainer from "./sharpContainer"
6
+
7
+ type ButtonProps = {
8
+ text: string;
9
+ bgColor?: string;
10
+ textColor?: string;
11
+ onPress?: () => void;
12
+ fontSize?: number;
13
+ fontFamily?: string;
14
+ height?: number;
15
+ loading?: boolean;
16
+ borderColor?: string;
17
+ };
18
+
19
+
20
+ export default function SharpButton({
21
+ text,
22
+ bgColor = "transparent",
23
+ textColor,
24
+ borderColor,
25
+ onPress,
26
+ fontSize = textSize.md,
27
+ fontFamily = Fonts.montserrat.semiBold,
28
+ height = 56,
29
+ loading,
30
+ }: ButtonProps) {
31
+ const { colors } = useAppTheme();
32
+
33
+ return (
34
+ <TouchableOpacity
35
+ onPress={onPress}
36
+ activeOpacity={0.8}
37
+ disabled={loading}
38
+ >
39
+ <SharpContainer height={height} bgColor={bgColor} borderColor={borderColor ?? colors.text.primary}>
40
+ <View style={styles.contentContainer}>
41
+ {loading ? (
42
+ <ActivityIndicator color={textColor ?? colors.text.primary} size="small" />
43
+ ) : (
44
+ <Text style={{
45
+ fontSize,
46
+ fontFamily: fontFamily,
47
+ color: textColor ?? colors.text.primary,
48
+ textAlign: "center",
49
+ }}>
50
+ {text}
51
+ </Text>
52
+ )}
53
+ </View>
54
+ </SharpContainer>
55
+ </TouchableOpacity>
56
+ );
57
+ }
58
+
59
+ const styles = StyleSheet.create({
60
+ contentContainer: {
61
+ ...StyleSheet.absoluteFill,
62
+ justifyContent: "center",
63
+ alignItems: "center",
64
+ zIndex: 2,
65
+ paddingHorizontal: 24,
66
+ },
67
+ });
@@ -0,0 +1,180 @@
1
+ import React, { useState } from "react";
2
+ import { LayoutChangeEvent, StyleSheet, View } from "react-native";
3
+ import Svg, { Path, Defs, LinearGradient as SvgGradient, Stop } from "react-native-svg";
4
+
5
+ interface SharpElementProps{
6
+ height: number;
7
+ bgColor: string;
8
+ borderColor: string;
9
+ children: React.ReactNode;
10
+ }
11
+ export default function SharpElement({height, bgColor, borderColor, children}:SharpElementProps){
12
+ const [dims, setDims] = useState({ w: 300, h: height });
13
+
14
+ const onLayout = (e: LayoutChangeEvent) => {
15
+ const { width, height: h } = e.nativeEvent.layout;
16
+ setDims({ w: width, h });
17
+ };
18
+
19
+ const W = dims.w - 8;
20
+ const H = dims.h;
21
+
22
+ // anchors — all relative to real W and H
23
+ const skewX = 10; // horizontal skew offset (fixed, looks good at any width)
24
+ const skewYT = H * 0.05; // top edge vertical skew at right side
25
+ const skewYB = H * 0.95; // bottom edge vertical skew at right side
26
+ const crT = H * 0.25; // top-right corner end
27
+ const crB = H * 0.75; // bottom-right corner start
28
+
29
+ // main shape
30
+ const bg = `
31
+ M ${skewX},0
32
+ L ${W - skewX},${skewYT}
33
+ L ${W},${crT}
34
+ L ${W},${crB}
35
+ L ${W - skewX},${skewYB}
36
+ L ${skewX},${H}
37
+ L 0,${crB}
38
+ L 0,${crT}
39
+ Z
40
+ `;
41
+
42
+ return(
43
+ <View
44
+ onLayout={onLayout}
45
+ style={{ width: "100%", height, justifyContent: "center" }}
46
+ >
47
+ <View style={StyleSheet.absoluteFill}>
48
+ <Svg
49
+ width="100%"
50
+ height="100%"
51
+ viewBox={`0 0 ${W} ${H}`}
52
+ >
53
+ <Defs>
54
+ {/* Button fill gradient */}
55
+ <SvgGradient id="btnGrad" x1="0%" y1="0%" x2="100%" y2="0%">
56
+ <Stop offset="0%" stopColor={bgColor} stopOpacity="0.65" />
57
+ <Stop offset="70%" stopColor={bgColor} stopOpacity="1" />
58
+ </SvgGradient>
59
+
60
+ {/* Border highlight gradient */}
61
+ <SvgGradient id="highlightGrad" x1="0%" y1="0%" x2="100%" y2="100%">
62
+ <Stop offset="0%" stopColor={borderColor} stopOpacity="0.65" />
63
+ <Stop offset="60%" stopColor={bgColor} stopOpacity="1" />
64
+ </SvgGradient>
65
+ </Defs>
66
+
67
+ {/* ── Background ── */}
68
+ <Path d={bg} fill="url(#btnGrad)" />
69
+
70
+ {/* ── Shadow: top + right ── */}
71
+ <Path
72
+ d={`
73
+ M ${skewX},-1.6
74
+ L ${W - skewX},${skewYT+3}
75
+ L ${W-skewX},${crT+5}
76
+ L ${W},${crB-16}
77
+ `}
78
+ fill="none"
79
+ stroke="url(#highlightGrad)"
80
+ strokeWidth={6}
81
+ strokeLinejoin="miter"
82
+ transform="translate(2 3)"
83
+ />
84
+
85
+ {/* ── Shadow: bottom ── */}
86
+ <Path
87
+ d={`
88
+ M ${W-skewX},${crB}
89
+ L ${skewX+50},${H-0.5}
90
+ `}
91
+
92
+
93
+ fill="none"
94
+ stroke="url(#highlightGrad)"
95
+ strokeWidth={6}
96
+ strokeLinejoin="miter"
97
+ transform="translate(2 3)"
98
+ />
99
+
100
+ {/* ── Shadow: left ── */}
101
+ <Path
102
+ d={`
103
+ M ${skewX+0.2},-1.4
104
+ L -0.5,${crT-2}
105
+ `}
106
+ fill="none"
107
+ stroke={borderColor+"99"}
108
+ strokeWidth={2.7}
109
+ strokeLinejoin="miter"
110
+ transform="translate(2 3)"
111
+ />
112
+ <Path
113
+ d={`
114
+ M -1,${crT-3}
115
+ L -1,${H-20}
116
+ L -0.5,${H-20}
117
+ L 1,${crT-2}
118
+ Z
119
+ `}
120
+ fill={borderColor+"99"}
121
+ transform="translate(2 3)"
122
+ />
123
+
124
+ {/* ── Border: top + right ── */}
125
+ <Path
126
+ d={`
127
+ M ${skewX},-0.5
128
+ L ${W - skewX},${skewYT}
129
+ L ${W},${crT}
130
+ L ${W},${crB}
131
+ `}
132
+ fill="none"
133
+ stroke={borderColor}
134
+ strokeWidth={6}
135
+ strokeLinejoin="miter"
136
+ />
137
+
138
+ {/* ── Border: bottom ── */}
139
+ <Path
140
+ d={`
141
+ M ${W+1.},${crB-2.2}
142
+ L ${W - skewX},${skewYB}
143
+ L ${skewX+50},${H+3}
144
+ `}
145
+
146
+ fill="none"
147
+ stroke={borderColor}
148
+ strokeWidth={6}
149
+ strokeLinejoin="miter"
150
+ />
151
+
152
+ {/* ── Border: left ── */}
153
+
154
+ <Path
155
+ d={`
156
+ M ${skewX},0
157
+ L 0,${crT}
158
+ `}
159
+ fill="none"
160
+ stroke={borderColor}
161
+ strokeWidth={2.5}
162
+ strokeLinejoin="miter"
163
+ />
164
+ <Path
165
+ d={`
166
+ M -1,${crT-1}
167
+ L 0,${H-14}
168
+ L 1,${H-14}
169
+ L 1,${crT+1}
170
+ Z
171
+ `}
172
+ fill={borderColor}
173
+ />
174
+ </Svg>
175
+ </View>
176
+ {children}
177
+ </View>
178
+ )
179
+
180
+ }