@towsila/expo-ui-library 3.3.16 → 3.5.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.
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Text, View } from "react-native";
|
|
2
|
+
import AnimatedSuccessIcon from "../ui/AnimatedSuccessIcon";
|
|
3
|
+
import { useAppTheme } from "../../context/ThemeContext";
|
|
4
|
+
import { useLanguage } from "../../context/LanguageContext";
|
|
5
|
+
import ScreenHeader from "../ui/PageTopBar";
|
|
6
|
+
import SharpButton from "../ui/sharpButton";
|
|
7
|
+
import { Fonts, textSize } from "../../tokens/typography";
|
|
8
|
+
|
|
9
|
+
interface SuccessScreenProps {
|
|
10
|
+
title?: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
buttonText: string;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
onNext: () => void;
|
|
16
|
+
footer?:React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function SuccessScreen({
|
|
20
|
+
title = "Password Changed",
|
|
21
|
+
subtitle = "",
|
|
22
|
+
description,
|
|
23
|
+
buttonText,
|
|
24
|
+
children,
|
|
25
|
+
onNext,
|
|
26
|
+
footer,
|
|
27
|
+
}: SuccessScreenProps) {
|
|
28
|
+
const { colors } = useAppTheme();
|
|
29
|
+
const {isRTL} = useLanguage();
|
|
30
|
+
return (
|
|
31
|
+
<View style={{ flex: 1, paddingHorizontal: 32, paddingVertical: 60 }}>
|
|
32
|
+
<ScreenHeader title={title} description={subtitle} />
|
|
33
|
+
<View style={{ flex: 1, marginTop: 30, alignItems: isRTL ? "flex-start" : "flex-end", gap: 20 }}>
|
|
34
|
+
<View style={{ backgroundColor: colors.primary + "30", padding: 20, borderRadius: 20 }}>
|
|
35
|
+
<AnimatedSuccessIcon color={colors.primary} size={50} strokeWidth={2} />
|
|
36
|
+
</View>
|
|
37
|
+
<Text style={{ color: colors.text.secondary, textAlign: isRTL ? "left" : "right", fontFamily: Fonts.montserrat.regular, fontSize: textSize.md }}>
|
|
38
|
+
{description}
|
|
39
|
+
</Text>
|
|
40
|
+
{children}
|
|
41
|
+
</View>
|
|
42
|
+
<SharpButton text={buttonText} onPress={onNext} bgColor={colors.primary}/>
|
|
43
|
+
{footer && (
|
|
44
|
+
<View style={{marginTop: 20}}/>
|
|
45
|
+
)}
|
|
46
|
+
{footer}
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Animated, Easing, View } from "react-native";
|
|
3
|
+
import Svg, { Circle, Path } from "react-native-svg";
|
|
4
|
+
|
|
5
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
6
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
7
|
+
|
|
8
|
+
interface AnimatedSuccessIconProps {
|
|
9
|
+
size?: number;
|
|
10
|
+
color?: string; // stroke color for both the circle and the checkmark
|
|
11
|
+
strokeWidth?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CHECK_PATH = "M18 33 L27 42 L46 20";
|
|
15
|
+
const CHECK_PATH_LENGTH = 40;
|
|
16
|
+
const CIRCLE_RADIUS = 28;
|
|
17
|
+
const CIRCLE_CIRCUMFERENCE = 2 * Math.PI * CIRCLE_RADIUS;
|
|
18
|
+
|
|
19
|
+
export default function AnimatedSuccessIcon({
|
|
20
|
+
size = 120,
|
|
21
|
+
color = "#22C55E",
|
|
22
|
+
strokeWidth = 5,
|
|
23
|
+
}: AnimatedSuccessIconProps) {
|
|
24
|
+
const circleProgress = useRef(new Animated.Value(0)).current;
|
|
25
|
+
const checkProgress = useRef(new Animated.Value(0)).current;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
Animated.sequence([
|
|
29
|
+
// Stage 1 — circle outline draws itself in, starting from the top
|
|
30
|
+
Animated.timing(circleProgress, {
|
|
31
|
+
toValue: 1,
|
|
32
|
+
duration: 600,
|
|
33
|
+
easing: Easing.out(Easing.ease),
|
|
34
|
+
useNativeDriver: false, // strokeDashoffset can't run on the native driver
|
|
35
|
+
}),
|
|
36
|
+
// Stage 2 — checkmark draws itself, only after the circle completes
|
|
37
|
+
Animated.timing(checkProgress, {
|
|
38
|
+
toValue: 1,
|
|
39
|
+
duration: 400,
|
|
40
|
+
easing: Easing.out(Easing.ease),
|
|
41
|
+
useNativeDriver: false,
|
|
42
|
+
}),
|
|
43
|
+
]).start();
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const circleDashoffset = circleProgress.interpolate({
|
|
47
|
+
inputRange: [0, 1],
|
|
48
|
+
outputRange: [CIRCLE_CIRCUMFERENCE, 0],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const checkDashoffset = checkProgress.interpolate({
|
|
52
|
+
inputRange: [0, 1],
|
|
53
|
+
outputRange: [CHECK_PATH_LENGTH, 0],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}>
|
|
58
|
+
<Svg width={size} height={size} viewBox="0 0 64 64">
|
|
59
|
+
<AnimatedCircle
|
|
60
|
+
cx={32}
|
|
61
|
+
cy={32}
|
|
62
|
+
r={CIRCLE_RADIUS}
|
|
63
|
+
stroke={color}
|
|
64
|
+
strokeWidth={strokeWidth}
|
|
65
|
+
fill="none"
|
|
66
|
+
strokeLinecap="round"
|
|
67
|
+
strokeDasharray={CIRCLE_CIRCUMFERENCE}
|
|
68
|
+
strokeDashoffset={circleDashoffset}
|
|
69
|
+
rotation="-90" // starts the draw from the top (12 o'clock) instead of the default 3 o'clock
|
|
70
|
+
origin="32, 32"
|
|
71
|
+
/>
|
|
72
|
+
<AnimatedPath
|
|
73
|
+
d={CHECK_PATH}
|
|
74
|
+
stroke={color}
|
|
75
|
+
strokeWidth={strokeWidth}
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
fill="none"
|
|
79
|
+
strokeDasharray={CHECK_PATH_LENGTH}
|
|
80
|
+
strokeDashoffset={checkDashoffset}
|
|
81
|
+
/>
|
|
82
|
+
</Svg>
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
ScrollViewProps,
|
|
6
|
+
View,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { useAppTheme } from "../../context/ThemeContext";
|
|
9
|
+
import { useLanguage } from "../../context/LanguageContext";
|
|
10
|
+
|
|
11
|
+
interface Props extends ScrollViewProps {
|
|
12
|
+
trackWidth?: number;
|
|
13
|
+
trackLength?: number;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const IDLE_OPACITY = 0.45;
|
|
18
|
+
|
|
19
|
+
export const ScrollWithIndicator: React.FC<Props> = ({
|
|
20
|
+
children,
|
|
21
|
+
trackWidth = 5,
|
|
22
|
+
trackLength = 200,
|
|
23
|
+
style,
|
|
24
|
+
contentContainerStyle,
|
|
25
|
+
...rest
|
|
26
|
+
}) => {
|
|
27
|
+
const { colors } = useAppTheme();
|
|
28
|
+
const { isRTL } = useLanguage();
|
|
29
|
+
|
|
30
|
+
const scrollY = useRef(new Animated.Value(0)).current;
|
|
31
|
+
const fade = useRef(new Animated.Value(IDLE_OPACITY)).current;
|
|
32
|
+
const awake = useRef(false);
|
|
33
|
+
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
const [viewH, setViewH] = useState(0);
|
|
36
|
+
const [contentH, setContentH] = useState(0);
|
|
37
|
+
|
|
38
|
+
const scrollable = viewH > 0 && contentH > viewH + 1;
|
|
39
|
+
|
|
40
|
+
// short fixed-length track instead of full height
|
|
41
|
+
const trackH = Math.min(trackLength, viewH);
|
|
42
|
+
const thumbH = scrollable
|
|
43
|
+
? Math.min(trackH * 0.7, Math.max(24, (viewH / contentH) * trackH))
|
|
44
|
+
: 0;
|
|
45
|
+
const maxScroll = Math.max(contentH - viewH, 1);
|
|
46
|
+
const maxThumb = Math.max(trackH - thumbH, 1);
|
|
47
|
+
|
|
48
|
+
const translateY = scrollY.interpolate({
|
|
49
|
+
inputRange: [0, maxScroll],
|
|
50
|
+
outputRange: [0, maxThumb],
|
|
51
|
+
extrapolate: "clamp",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// brighter while scrolling, softer when idle
|
|
55
|
+
const wake = () => {
|
|
56
|
+
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
57
|
+
if (!awake.current) {
|
|
58
|
+
awake.current = true;
|
|
59
|
+
Animated.timing(fade, { toValue: 1, duration: 120, useNativeDriver: true }).start();
|
|
60
|
+
}
|
|
61
|
+
hideTimer.current = setTimeout(() => {
|
|
62
|
+
awake.current = false;
|
|
63
|
+
Animated.timing(fade, { toValue: IDLE_OPACITY, duration: 400, useNativeDriver: true }).start();
|
|
64
|
+
}, 900);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<View style={[{ flex: 1, flexDirection: isRTL ? "row" : "row-reverse" }, style]}>
|
|
75
|
+
<Animated.ScrollView
|
|
76
|
+
{...rest}
|
|
77
|
+
style={{ flex: 1 }}
|
|
78
|
+
contentContainerStyle={contentContainerStyle}
|
|
79
|
+
showsVerticalScrollIndicator={false}
|
|
80
|
+
scrollEventThrottle={10}
|
|
81
|
+
onLayout={(e: LayoutChangeEvent) => setViewH(e.nativeEvent.layout.height)}
|
|
82
|
+
onContentSizeChange={(_, h) => setContentH(h)}
|
|
83
|
+
onScroll={Animated.event(
|
|
84
|
+
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
|
85
|
+
{ useNativeDriver: true, listener: wake }
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</Animated.ScrollView>
|
|
90
|
+
|
|
91
|
+
{scrollable && (
|
|
92
|
+
<Animated.View
|
|
93
|
+
pointerEvents="none"
|
|
94
|
+
style={[
|
|
95
|
+
{
|
|
96
|
+
width: trackWidth,
|
|
97
|
+
height: trackH,
|
|
98
|
+
alignSelf: "center",
|
|
99
|
+
opacity: fade,
|
|
100
|
+
},
|
|
101
|
+
isRTL ? { marginLeft: 15 } : { marginRight: 15 },
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
{/* soft track */}
|
|
105
|
+
<View
|
|
106
|
+
style={{
|
|
107
|
+
position: "absolute",
|
|
108
|
+
top: 0,
|
|
109
|
+
bottom: 0,
|
|
110
|
+
width: trackWidth,
|
|
111
|
+
borderRadius: trackWidth / 2,
|
|
112
|
+
backgroundColor: colors.primary,
|
|
113
|
+
opacity: 0.15,
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
{/* thumb */}
|
|
117
|
+
<Animated.View
|
|
118
|
+
style={{
|
|
119
|
+
width: trackWidth,
|
|
120
|
+
height: thumbH,
|
|
121
|
+
borderRadius: trackWidth / 2,
|
|
122
|
+
backgroundColor: colors.primary,
|
|
123
|
+
transform: [{ translateY }],
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</Animated.View>
|
|
127
|
+
)}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -65,4 +65,7 @@ export {default as RatingStars} from "./components/ui/rating";
|
|
|
65
65
|
export { getFcmToken } from "./hooks/useFcmToken";
|
|
66
66
|
export {useArrivalDetection} from "./hooks/useArrivalDetection";
|
|
67
67
|
export {default as Toggle} from "./components/ui/toggle";
|
|
68
|
-
export {default as BackIcon} from "./components/ui/backIcon";
|
|
68
|
+
export {default as BackIcon} from "./components/ui/backIcon";
|
|
69
|
+
export {default as SuccessScreen} from "./components/screens/sucessScreen";
|
|
70
|
+
export {default as AnimatedSuccessIcon} from "./components/ui/AnimatedSuccessIcon";
|
|
71
|
+
export {ScrollWithIndicator} from "./components/ui/scrollIndicator"
|
|
File without changes
|