rn-toastify 1.0.12 → 2.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.
- package/LICENSE +21 -21
- package/README.MD +221 -190
- package/babel.config.js +5 -5
- package/docs/demo.gif +0 -0
- package/example/App.js +193 -0
- package/index.js +60 -34
- package/jest.config.js +14 -14
- package/jest.setup.js +1 -1
- package/package.json +86 -74
- package/src/Toast.js +194 -114
- package/src/__tests__/Toast.test.js +54 -54
- package/src/components/BaseToast.js +163 -0
- package/src/components/CustomToast.js +58 -0
- package/src/components/CustomeToast.js +4 -40
- package/src/components/EmojiToast.js +142 -57
- package/src/components/ErrorToast.js +23 -65
- package/src/components/InfoToast.js +23 -0
- package/src/components/LoadingToast.js +24 -55
- package/src/components/ProgressBar.js +67 -0
- package/src/components/SuccessToast.js +23 -65
- package/src/components/WarningToast.js +23 -0
- package/src/components/icons/CheckIcon.js +98 -0
- package/src/components/icons/CrossIcon.js +84 -0
- package/src/components/icons/InfoIcon.js +71 -0
- package/src/components/icons/LoadingSpinner.js +78 -0
- package/src/components/icons/WarningIcon.js +84 -0
- package/src/components/icons/index.js +5 -0
- package/src/context/ToastContainer.js +223 -144
- package/src/context/ToastManager.js +150 -36
- package/src/hooks/useToast.js +123 -49
- package/src/types.d.ts +172 -0
- package/src/utils/Pixel/Index.js +28 -28
- package/src/utils/theme.js +81 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BaseToast from './BaseToast';
|
|
3
|
+
import { InfoIcon } from './icons';
|
|
4
|
+
import { TOAST_COLORS, TOAST_DEFAULTS } from '../utils/theme';
|
|
5
|
+
|
|
6
|
+
const InfoToast = ({ title, message, theme = 'light', duration }) => {
|
|
7
|
+
const isDark = theme === 'dark';
|
|
8
|
+
const colors = TOAST_COLORS.info;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<BaseToast
|
|
12
|
+
icon={<InfoIcon size={TOAST_DEFAULTS.iconSize} color={isDark ? colors.iconDark : colors.icon} />}
|
|
13
|
+
title={title || null}
|
|
14
|
+
message={message || null}
|
|
15
|
+
accentColor={colors.accent}
|
|
16
|
+
toastType="info"
|
|
17
|
+
theme={theme}
|
|
18
|
+
duration={duration}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default React.memo(InfoToast);
|
|
@@ -1,55 +1,24 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const styles = StyleSheet.create({
|
|
27
|
-
toast: {
|
|
28
|
-
minHeight: hp(6.5),
|
|
29
|
-
paddingHorizontal: wp(4),
|
|
30
|
-
paddingVertical: hp(1.2),
|
|
31
|
-
borderRadius: wp(3),
|
|
32
|
-
backgroundColor: '#FFFFFF',
|
|
33
|
-
flexDirection: 'row',
|
|
34
|
-
alignItems: 'center',
|
|
35
|
-
// Shadow for iOS
|
|
36
|
-
shadowColor: '#000',
|
|
37
|
-
shadowOffset: {
|
|
38
|
-
width: 0,
|
|
39
|
-
height: 4,
|
|
40
|
-
},
|
|
41
|
-
shadowOpacity: 0.15,
|
|
42
|
-
shadowRadius: 8,
|
|
43
|
-
// Shadow for Android
|
|
44
|
-
elevation: 6,
|
|
45
|
-
},
|
|
46
|
-
text: {
|
|
47
|
-
fontSize: hp(1.85),
|
|
48
|
-
color: '#1F2937',
|
|
49
|
-
fontWeight: '500',
|
|
50
|
-
marginLeft: wp(3),
|
|
51
|
-
lineHeight: hp(2.4),
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export default LoadingToast;
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BaseToast from './BaseToast';
|
|
3
|
+
import { LoadingSpinner } from './icons';
|
|
4
|
+
import { TOAST_COLORS, TOAST_DEFAULTS } from '../utils/theme';
|
|
5
|
+
|
|
6
|
+
const LoadingToast = ({ title, message, theme = 'light', duration }) => {
|
|
7
|
+
const isDark = theme === 'dark';
|
|
8
|
+
const colors = TOAST_COLORS.loading;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<BaseToast
|
|
12
|
+
icon={<LoadingSpinner size={TOAST_DEFAULTS.iconSize - 2} color={isDark ? colors.iconDark : colors.icon} />}
|
|
13
|
+
title={title || null}
|
|
14
|
+
message={message || null}
|
|
15
|
+
accentColor={colors.accent}
|
|
16
|
+
toastType="loading"
|
|
17
|
+
theme={theme}
|
|
18
|
+
duration={duration}
|
|
19
|
+
showProgress={false}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default React.memo(LoadingToast);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withTiming,
|
|
7
|
+
withDelay,
|
|
8
|
+
Easing,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ProgressBar — flush edge-to-edge countdown indicator.
|
|
13
|
+
* Sits at the very bottom of the toast card, no padding.
|
|
14
|
+
*/
|
|
15
|
+
const ProgressBar = ({ duration, color = '#22C55E', trackColor = 'rgba(0,0,0,0.05)' }) => {
|
|
16
|
+
const progress = useSharedValue(1);
|
|
17
|
+
const barOpacity = useSharedValue(0);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (duration && duration !== Infinity) {
|
|
21
|
+
barOpacity.value = withDelay(200, withTiming(1, { duration: 250 }));
|
|
22
|
+
progress.value = withTiming(0, {
|
|
23
|
+
duration: duration,
|
|
24
|
+
easing: Easing.linear,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}, [duration]);
|
|
28
|
+
|
|
29
|
+
const barStyle = useAnimatedStyle(() => ({
|
|
30
|
+
width: `${progress.value * 100}%`,
|
|
31
|
+
opacity: barOpacity.value,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
if (!duration || duration === Infinity) return null;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Animated.View style={styles.track}>
|
|
38
|
+
<Animated.View style={[styles.trackBg, { backgroundColor: trackColor }]} />
|
|
39
|
+
<Animated.View
|
|
40
|
+
style={[
|
|
41
|
+
styles.bar,
|
|
42
|
+
{ backgroundColor: color },
|
|
43
|
+
barStyle,
|
|
44
|
+
]}
|
|
45
|
+
/>
|
|
46
|
+
</Animated.View>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const styles = StyleSheet.create({
|
|
51
|
+
track: {
|
|
52
|
+
height: 3,
|
|
53
|
+
width: '100%',
|
|
54
|
+
position: 'relative',
|
|
55
|
+
},
|
|
56
|
+
trackBg: {
|
|
57
|
+
...StyleSheet.absoluteFillObject,
|
|
58
|
+
},
|
|
59
|
+
bar: {
|
|
60
|
+
height: '100%',
|
|
61
|
+
position: 'absolute',
|
|
62
|
+
left: 0,
|
|
63
|
+
top: 0,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default React.memo(ProgressBar);
|
|
@@ -1,65 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{message}
|
|
25
|
-
</Text>
|
|
26
|
-
</View>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const styles = StyleSheet.create({
|
|
31
|
-
container: {
|
|
32
|
-
width: wp(87),
|
|
33
|
-
minHeight: hp(6.5),
|
|
34
|
-
paddingHorizontal: wp(4),
|
|
35
|
-
paddingVertical: hp(1.2),
|
|
36
|
-
borderRadius: wp(3),
|
|
37
|
-
backgroundColor: '#FFFFFF',
|
|
38
|
-
alignItems: 'center',
|
|
39
|
-
flexDirection: 'row',
|
|
40
|
-
// Shadow for iOS
|
|
41
|
-
shadowColor: '#000',
|
|
42
|
-
shadowOffset: {
|
|
43
|
-
width: 0,
|
|
44
|
-
height: 4,
|
|
45
|
-
},
|
|
46
|
-
shadowOpacity: 0.15,
|
|
47
|
-
shadowRadius: 8,
|
|
48
|
-
// Shadow for Android
|
|
49
|
-
elevation: 6,
|
|
50
|
-
},
|
|
51
|
-
text: {
|
|
52
|
-
fontSize: hp(1.85),
|
|
53
|
-
color: '#1F2937',
|
|
54
|
-
fontWeight: '500',
|
|
55
|
-
paddingHorizontal: wp(2.5),
|
|
56
|
-
flex: 1,
|
|
57
|
-
lineHeight: hp(2.4),
|
|
58
|
-
},
|
|
59
|
-
lottie: {
|
|
60
|
-
width: wp(7),
|
|
61
|
-
height: hp(3.5),
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export default SuccessToast;
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BaseToast from './BaseToast';
|
|
3
|
+
import { CheckIcon } from './icons';
|
|
4
|
+
import { TOAST_COLORS, TOAST_DEFAULTS } from '../utils/theme';
|
|
5
|
+
|
|
6
|
+
const SuccessToast = ({ title, message, theme = 'light', duration }) => {
|
|
7
|
+
const isDark = theme === 'dark';
|
|
8
|
+
const colors = TOAST_COLORS.success;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<BaseToast
|
|
12
|
+
icon={<CheckIcon size={TOAST_DEFAULTS.iconSize} color={isDark ? colors.iconDark : colors.icon} />}
|
|
13
|
+
title={title || null}
|
|
14
|
+
message={message || null}
|
|
15
|
+
accentColor={colors.accent}
|
|
16
|
+
toastType="success"
|
|
17
|
+
theme={theme}
|
|
18
|
+
duration={duration}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default React.memo(SuccessToast);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import BaseToast from './BaseToast';
|
|
3
|
+
import { WarningIcon } from './icons';
|
|
4
|
+
import { TOAST_COLORS, TOAST_DEFAULTS } from '../utils/theme';
|
|
5
|
+
|
|
6
|
+
const WarningToast = ({ title, message, theme = 'light', duration }) => {
|
|
7
|
+
const isDark = theme === 'dark';
|
|
8
|
+
const colors = TOAST_COLORS.warning;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<BaseToast
|
|
12
|
+
icon={<WarningIcon size={TOAST_DEFAULTS.iconSize} color={isDark ? colors.iconDark : colors.icon} />}
|
|
13
|
+
title={title || null}
|
|
14
|
+
message={message || null}
|
|
15
|
+
accentColor={colors.accent}
|
|
16
|
+
toastType="warning"
|
|
17
|
+
theme={theme}
|
|
18
|
+
duration={duration}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default React.memo(WarningToast);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withTiming,
|
|
7
|
+
withDelay,
|
|
8
|
+
withSpring,
|
|
9
|
+
Easing,
|
|
10
|
+
interpolate,
|
|
11
|
+
} from 'react-native-reanimated';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CheckIcon — Animated checkmark.
|
|
15
|
+
* Clean two-stroke design that draws in sequentially.
|
|
16
|
+
*/
|
|
17
|
+
const CheckIcon = ({ size = 22, color = '#16A34A', animated = true }) => {
|
|
18
|
+
const progress = useSharedValue(animated ? 0 : 1);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (animated) {
|
|
22
|
+
progress.value = withDelay(
|
|
23
|
+
100,
|
|
24
|
+
withTiming(1, { duration: 450, easing: Easing.out(Easing.cubic) })
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}, [animated]);
|
|
28
|
+
|
|
29
|
+
const stroke = Math.max(2, size * 0.12);
|
|
30
|
+
const shortLen = size * 0.3;
|
|
31
|
+
const longLen = size * 0.5;
|
|
32
|
+
|
|
33
|
+
const leftBarStyle = useAnimatedStyle(() => {
|
|
34
|
+
const p = interpolate(progress.value, [0, 0.45], [0, 1], 'clamp');
|
|
35
|
+
return {
|
|
36
|
+
transform: [{ rotate: '45deg' }, { scaleY: p }],
|
|
37
|
+
opacity: p,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const rightBarStyle = useAnimatedStyle(() => {
|
|
42
|
+
const p = interpolate(progress.value, [0.3, 1], [0, 1], 'clamp');
|
|
43
|
+
return {
|
|
44
|
+
transform: [{ rotate: '-45deg' }, { scaleY: p }],
|
|
45
|
+
opacity: p,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
51
|
+
<View style={[styles.checkWrap, { width: size * 0.6, height: size * 0.6 }]}>
|
|
52
|
+
<Animated.View
|
|
53
|
+
style={[
|
|
54
|
+
{
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
bottom: 0,
|
|
57
|
+
left: 0,
|
|
58
|
+
width: stroke,
|
|
59
|
+
height: shortLen,
|
|
60
|
+
backgroundColor: color,
|
|
61
|
+
borderRadius: stroke,
|
|
62
|
+
transformOrigin: 'bottom',
|
|
63
|
+
},
|
|
64
|
+
leftBarStyle,
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
<Animated.View
|
|
68
|
+
style={[
|
|
69
|
+
{
|
|
70
|
+
position: 'absolute',
|
|
71
|
+
bottom: 0,
|
|
72
|
+
left: stroke * 0.4,
|
|
73
|
+
width: stroke,
|
|
74
|
+
height: longLen,
|
|
75
|
+
backgroundColor: color,
|
|
76
|
+
borderRadius: stroke,
|
|
77
|
+
transformOrigin: 'bottom',
|
|
78
|
+
},
|
|
79
|
+
rightBarStyle,
|
|
80
|
+
]}
|
|
81
|
+
/>
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
container: {
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
},
|
|
92
|
+
checkWrap: {
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export default React.memo(CheckIcon);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withTiming,
|
|
7
|
+
withDelay,
|
|
8
|
+
Easing,
|
|
9
|
+
interpolate,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CrossIcon — Animated X mark.
|
|
14
|
+
* Two strokes that draw in sequentially with a cross shape.
|
|
15
|
+
*/
|
|
16
|
+
const CrossIcon = ({ size = 22, color = '#DC2626', animated = true }) => {
|
|
17
|
+
const progress = useSharedValue(animated ? 0 : 1);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (animated) {
|
|
21
|
+
progress.value = withDelay(
|
|
22
|
+
100,
|
|
23
|
+
withTiming(1, { duration: 400, easing: Easing.out(Easing.cubic) })
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}, [animated]);
|
|
27
|
+
|
|
28
|
+
const stroke = Math.max(2, size * 0.12);
|
|
29
|
+
const barLen = size * 0.5;
|
|
30
|
+
|
|
31
|
+
const bar1Style = useAnimatedStyle(() => {
|
|
32
|
+
const p = interpolate(progress.value, [0, 0.55], [0, 1], 'clamp');
|
|
33
|
+
return {
|
|
34
|
+
transform: [{ rotate: '45deg' }, { scaleY: p }],
|
|
35
|
+
opacity: p,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const bar2Style = useAnimatedStyle(() => {
|
|
40
|
+
const p = interpolate(progress.value, [0.3, 1], [0, 1], 'clamp');
|
|
41
|
+
return {
|
|
42
|
+
transform: [{ rotate: '-45deg' }, { scaleY: p }],
|
|
43
|
+
opacity: p,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
49
|
+
<Animated.View
|
|
50
|
+
style={[
|
|
51
|
+
{
|
|
52
|
+
position: 'absolute',
|
|
53
|
+
width: stroke,
|
|
54
|
+
height: barLen,
|
|
55
|
+
backgroundColor: color,
|
|
56
|
+
borderRadius: stroke,
|
|
57
|
+
},
|
|
58
|
+
bar1Style,
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
<Animated.View
|
|
62
|
+
style={[
|
|
63
|
+
{
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
width: stroke,
|
|
66
|
+
height: barLen,
|
|
67
|
+
backgroundColor: color,
|
|
68
|
+
borderRadius: stroke,
|
|
69
|
+
},
|
|
70
|
+
bar2Style,
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const styles = StyleSheet.create({
|
|
78
|
+
container: {
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
justifyContent: 'center',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export default React.memo(CrossIcon);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withTiming,
|
|
7
|
+
withDelay,
|
|
8
|
+
Easing,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* InfoIcon — Animated "i" information symbol.
|
|
13
|
+
* Dot + vertical bar that fade in.
|
|
14
|
+
*/
|
|
15
|
+
const InfoIcon = ({ size = 22, color = '#2563EB', animated = true }) => {
|
|
16
|
+
const opacity = useSharedValue(animated ? 0 : 1);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (animated) {
|
|
20
|
+
opacity.value = withDelay(
|
|
21
|
+
100,
|
|
22
|
+
withTiming(1, { duration: 350, easing: Easing.out(Easing.cubic) })
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}, [animated]);
|
|
26
|
+
|
|
27
|
+
const contentStyle = useAnimatedStyle(() => ({
|
|
28
|
+
opacity: opacity.value,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const dotSize = Math.max(3, size * 0.16);
|
|
32
|
+
const barW = Math.max(2, size * 0.12);
|
|
33
|
+
const barH = size * 0.35;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
37
|
+
<Animated.View style={[styles.content, contentStyle]}>
|
|
38
|
+
<View
|
|
39
|
+
style={{
|
|
40
|
+
width: dotSize,
|
|
41
|
+
height: dotSize,
|
|
42
|
+
borderRadius: dotSize / 2,
|
|
43
|
+
backgroundColor: color,
|
|
44
|
+
marginBottom: size * 0.08,
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
<View
|
|
48
|
+
style={{
|
|
49
|
+
width: barW,
|
|
50
|
+
height: barH,
|
|
51
|
+
backgroundColor: color,
|
|
52
|
+
borderRadius: barW,
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
</Animated.View>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
container: {
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
justifyContent: 'center',
|
|
64
|
+
},
|
|
65
|
+
content: {
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
justifyContent: 'center',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export default React.memo(InfoIcon);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withRepeat,
|
|
7
|
+
withTiming,
|
|
8
|
+
Easing,
|
|
9
|
+
} from 'react-native-reanimated';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* LoadingSpinner — Smooth spinning arc.
|
|
13
|
+
* Thin track with a spinning colored arc segment.
|
|
14
|
+
*/
|
|
15
|
+
const LoadingSpinner = ({ size = 22, color = '#7C3AED' }) => {
|
|
16
|
+
const rotation = useSharedValue(0);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
rotation.value = withRepeat(
|
|
20
|
+
withTiming(360, { duration: 850, easing: Easing.linear }),
|
|
21
|
+
-1,
|
|
22
|
+
false
|
|
23
|
+
);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const spinStyle = useAnimatedStyle(() => ({
|
|
27
|
+
transform: [{ rotate: `${rotation.value}deg` }],
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const arcW = Math.max(2, size * 0.11);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
34
|
+
<View
|
|
35
|
+
style={[
|
|
36
|
+
styles.track,
|
|
37
|
+
{
|
|
38
|
+
width: size,
|
|
39
|
+
height: size,
|
|
40
|
+
borderRadius: size / 2,
|
|
41
|
+
borderWidth: arcW,
|
|
42
|
+
borderColor: color + '20',
|
|
43
|
+
},
|
|
44
|
+
]}
|
|
45
|
+
/>
|
|
46
|
+
<Animated.View
|
|
47
|
+
style={[
|
|
48
|
+
styles.arc,
|
|
49
|
+
{
|
|
50
|
+
width: size,
|
|
51
|
+
height: size,
|
|
52
|
+
borderRadius: size / 2,
|
|
53
|
+
borderWidth: arcW,
|
|
54
|
+
borderColor: 'transparent',
|
|
55
|
+
borderTopColor: color,
|
|
56
|
+
borderRightColor: color + '50',
|
|
57
|
+
},
|
|
58
|
+
spinStyle,
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
container: {
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
},
|
|
70
|
+
track: {
|
|
71
|
+
position: 'absolute',
|
|
72
|
+
},
|
|
73
|
+
arc: {
|
|
74
|
+
position: 'absolute',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export default React.memo(LoadingSpinner);
|