rn-toastify 1.0.11 → 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 -43
- 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 +5 -28
- package/src/components/EmojiToast.js +142 -38
- package/src/components/ErrorToast.js +23 -52
- package/src/components/InfoToast.js +23 -0
- package/src/components/LoadingToast.js +24 -33
- package/src/components/ProgressBar.js +67 -0
- package/src/components/SuccessToast.js +23 -51
- 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
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
25
|
-
backgroundColor: 'white',
|
|
26
|
-
},
|
|
27
|
-
text: {
|
|
28
|
-
fontSize: hp(2.2),
|
|
29
|
-
color: 'black',
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
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,51 +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
|
-
</View>
|
|
25
|
-
);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const styles = StyleSheet.create({
|
|
29
|
-
container: {
|
|
30
|
-
width: wp(87),
|
|
31
|
-
height: hp(6.8),
|
|
32
|
-
paddingHorizontal: wp(4),
|
|
33
|
-
borderRadius: wp(4),
|
|
34
|
-
// backgroundColor: '#d2f7d2',
|
|
35
|
-
backgroundColor: '#F7F7FC',
|
|
36
|
-
alignItems: 'center',
|
|
37
|
-
flexDirection: 'row',
|
|
38
|
-
},
|
|
39
|
-
text: {
|
|
40
|
-
fontSize: hp(2.3),
|
|
41
|
-
color: 'black',
|
|
42
|
-
fontWeight: '500',
|
|
43
|
-
paddingHorizontal: wp(3)
|
|
44
|
-
},
|
|
45
|
-
lottie: {
|
|
46
|
-
width: wp(8),
|
|
47
|
-
height: hp(4),
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
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);
|
|
@@ -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
|
+
withSequence,
|
|
9
|
+
Easing,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WarningIcon — Animated exclamation mark with subtle shake.
|
|
14
|
+
* Bar + dot that fade in, followed by a quick shake.
|
|
15
|
+
*/
|
|
16
|
+
const WarningIcon = ({ size = 22, color = '#D97706', animated = true }) => {
|
|
17
|
+
const opacity = useSharedValue(animated ? 0 : 1);
|
|
18
|
+
const shake = useSharedValue(0);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (animated) {
|
|
22
|
+
opacity.value = withDelay(
|
|
23
|
+
100,
|
|
24
|
+
withTiming(1, { duration: 350, easing: Easing.out(Easing.cubic) })
|
|
25
|
+
);
|
|
26
|
+
shake.value = withDelay(
|
|
27
|
+
350,
|
|
28
|
+
withSequence(
|
|
29
|
+
withTiming(-2.5, { duration: 40 }),
|
|
30
|
+
withTiming(2.5, { duration: 40 }),
|
|
31
|
+
withTiming(-1.5, { duration: 40 }),
|
|
32
|
+
withTiming(1.5, { duration: 40 }),
|
|
33
|
+
withTiming(0, { duration: 40 })
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}, [animated]);
|
|
38
|
+
|
|
39
|
+
const contentStyle = useAnimatedStyle(() => ({
|
|
40
|
+
opacity: opacity.value,
|
|
41
|
+
transform: [{ translateX: shake.value }],
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const barW = Math.max(2, size * 0.12);
|
|
45
|
+
const barH = size * 0.35;
|
|
46
|
+
const dotSize = Math.max(3, size * 0.16);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
50
|
+
<Animated.View style={[styles.content, contentStyle]}>
|
|
51
|
+
<View
|
|
52
|
+
style={{
|
|
53
|
+
width: barW,
|
|
54
|
+
height: barH,
|
|
55
|
+
backgroundColor: color,
|
|
56
|
+
borderRadius: barW,
|
|
57
|
+
marginBottom: size * 0.08,
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
<View
|
|
61
|
+
style={{
|
|
62
|
+
width: dotSize,
|
|
63
|
+
height: dotSize,
|
|
64
|
+
borderRadius: dotSize / 2,
|
|
65
|
+
backgroundColor: color,
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
</Animated.View>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
container: {
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'center',
|
|
77
|
+
},
|
|
78
|
+
content: {
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
justifyContent: 'center',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export default React.memo(WarningIcon);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as CheckIcon } from './CheckIcon';
|
|
2
|
+
export { default as CrossIcon } from './CrossIcon';
|
|
3
|
+
export { default as InfoIcon } from './InfoIcon';
|
|
4
|
+
export { default as WarningIcon } from './WarningIcon';
|
|
5
|
+
export { default as LoadingSpinner } from './LoadingSpinner';
|