react-native-popify 0.1.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.
@@ -0,0 +1,302 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ Animated,
8
+ Platform,
9
+ PanResponder,
10
+ } from 'react-native';
11
+
12
+ const TOAST_CONFIG = {
13
+ success: { color: '#00C853', icon: '✓', bg: '#F1FBF4', darkBg: '#152E1B' },
14
+ error: { color: '#FF3D71', icon: '✕', bg: '#FFF2F2', darkBg: '#2E1520' },
15
+ warning: { color: '#FFAA00', icon: '!', bg: '#FFFBF0', darkBg: '#2E2815' },
16
+ info: { color: '#3366FF', icon: 'i', bg: '#F0F4FF', darkBg: '#151E2E' },
17
+ };
18
+
19
+ const PopifyToast = ({
20
+ visible = false,
21
+ type = 'info',
22
+ message = '',
23
+ title,
24
+ position = 'top', // 'top' | 'bottom'
25
+ duration = 3000,
26
+ onClose,
27
+ showProgress = true,
28
+ showIcon = true,
29
+ theme = 'light',
30
+ style,
31
+ customIcon,
32
+ }) => {
33
+ const config = TOAST_CONFIG[type] || TOAST_CONFIG.info;
34
+ const isDark = theme === 'dark';
35
+
36
+ const translateY = useRef(
37
+ new Animated.Value(position === 'top' ? -150 : 150)
38
+ ).current;
39
+ const opacity = useRef(new Animated.Value(0)).current;
40
+ const scaleX = useRef(new Animated.Value(0.9)).current;
41
+ const progressAnim = useRef(new Animated.Value(1)).current;
42
+ const timerRef = useRef(null);
43
+
44
+ // Swipe to dismiss
45
+ const panResponder = useRef(
46
+ PanResponder.create({
47
+ onMoveShouldSetPanResponder: (_, gs) =>
48
+ Math.abs(gs.dy) > 10 &&
49
+ ((position === 'top' && gs.dy < 0) ||
50
+ (position === 'bottom' && gs.dy > 0)),
51
+ onPanResponderMove: (_, gs) => {
52
+ translateY.setValue(gs.dy);
53
+ },
54
+ onPanResponderRelease: (_, gs) => {
55
+ if (Math.abs(gs.dy) > 40) {
56
+ dismiss();
57
+ } else {
58
+ Animated.spring(translateY, {
59
+ toValue: 0,
60
+ useNativeDriver: true,
61
+ tension: 60,
62
+ friction: 10,
63
+ }).start();
64
+ }
65
+ },
66
+ })
67
+ ).current;
68
+
69
+ const dismiss = useCallback(() => {
70
+ if (timerRef.current) clearTimeout(timerRef.current);
71
+ Animated.parallel([
72
+ Animated.timing(translateY, {
73
+ toValue: position === 'top' ? -150 : 150,
74
+ duration: 250,
75
+ useNativeDriver: true,
76
+ }),
77
+ Animated.timing(opacity, {
78
+ toValue: 0,
79
+ duration: 200,
80
+ useNativeDriver: true,
81
+ }),
82
+ ]).start(() => onClose && onClose());
83
+ }, [position, onClose, translateY, opacity]);
84
+
85
+ const animateIn = useCallback(() => {
86
+ translateY.setValue(position === 'top' ? -150 : 150);
87
+ opacity.setValue(0);
88
+ scaleX.setValue(0.9);
89
+ progressAnim.setValue(1);
90
+
91
+ Animated.parallel([
92
+ Animated.spring(translateY, {
93
+ toValue: 0,
94
+ useNativeDriver: true,
95
+ tension: 50,
96
+ friction: 8,
97
+ }),
98
+ Animated.timing(opacity, {
99
+ toValue: 1,
100
+ duration: 250,
101
+ useNativeDriver: true,
102
+ }),
103
+ Animated.spring(scaleX, {
104
+ toValue: 1,
105
+ useNativeDriver: true,
106
+ tension: 50,
107
+ friction: 8,
108
+ }),
109
+ ]).start();
110
+
111
+ if (showProgress && duration > 0) {
112
+ Animated.timing(progressAnim, {
113
+ toValue: 0,
114
+ duration: duration,
115
+ useNativeDriver: false,
116
+ }).start();
117
+ }
118
+
119
+ if (duration > 0) {
120
+ timerRef.current = setTimeout(dismiss, duration);
121
+ }
122
+ }, [
123
+ position,
124
+ duration,
125
+ showProgress,
126
+ dismiss,
127
+ translateY,
128
+ opacity,
129
+ scaleX,
130
+ progressAnim,
131
+ ]);
132
+
133
+ useEffect(() => {
134
+ if (visible) {
135
+ animateIn();
136
+ } else {
137
+ dismiss();
138
+ }
139
+ return () => {
140
+ if (timerRef.current) clearTimeout(timerRef.current);
141
+ };
142
+ }, [visible, animateIn, dismiss]);
143
+
144
+ if (!visible) return null;
145
+
146
+ const bgColor = isDark ? config.darkBg : '#FFFFFF';
147
+ const textColor = isDark ? '#F0F0F5' : '#1A1A2E';
148
+ const subColor = isDark ? '#A0A0B0' : '#6B6B80';
149
+
150
+ return (
151
+ <Animated.View
152
+ {...panResponder.panHandlers}
153
+ style={[
154
+ styles.container,
155
+ position === 'top' ? styles.top : styles.bottom,
156
+ {
157
+ backgroundColor: bgColor,
158
+ opacity,
159
+ transform: [{ translateY }, { scaleX }],
160
+ borderLeftColor: config.color,
161
+ ...Platform.select({
162
+ ios: {
163
+ shadowColor: '#000',
164
+ shadowOffset: { width: 0, height: 8 },
165
+ shadowOpacity: 0.15,
166
+ shadowRadius: 16,
167
+ },
168
+ android: { elevation: 12 },
169
+ }),
170
+ },
171
+ isDark && styles.containerDark,
172
+ style,
173
+ ]}
174
+ >
175
+ <View style={styles.row}>
176
+ {/* Icon */}
177
+ {showIcon && (
178
+ <View style={[styles.iconCircle, { backgroundColor: config.color }]}>
179
+ {customIcon || <Text style={styles.iconText}>{config.icon}</Text>}
180
+ </View>
181
+ )}
182
+
183
+ {/* Content */}
184
+ <View style={styles.content}>
185
+ {title && (
186
+ <Text style={[styles.title, { color: textColor }]}>{title}</Text>
187
+ )}
188
+ <Text
189
+ style={[styles.message, { color: title ? subColor : textColor }]}
190
+ numberOfLines={2}
191
+ >
192
+ {message}
193
+ </Text>
194
+ </View>
195
+
196
+ {/* Close */}
197
+ <TouchableOpacity
198
+ style={styles.closeBtn}
199
+ onPress={dismiss}
200
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
201
+ >
202
+ <Text style={[styles.closeBtnText, { color: subColor }]}>✕</Text>
203
+ </TouchableOpacity>
204
+ </View>
205
+
206
+ {/* Progress Bar */}
207
+ {showProgress && duration > 0 && (
208
+ <View style={styles.progressWrap}>
209
+ <Animated.View
210
+ style={[
211
+ styles.progressBar,
212
+ {
213
+ backgroundColor: config.color,
214
+ width: progressAnim.interpolate({
215
+ inputRange: [0, 1],
216
+ outputRange: ['0%', '100%'],
217
+ }),
218
+ },
219
+ ]}
220
+ />
221
+ </View>
222
+ )}
223
+ </Animated.View>
224
+ );
225
+ };
226
+
227
+ const styles = StyleSheet.create({
228
+ container: {
229
+ position: 'absolute',
230
+ left: 16,
231
+ right: 16,
232
+ maxWidth: 500,
233
+ alignSelf: 'center',
234
+ borderRadius: 18,
235
+ overflow: 'hidden',
236
+ borderLeftWidth: 5,
237
+ },
238
+ top: {
239
+ top: Platform.OS === 'ios' ? 60 : 40,
240
+ },
241
+ bottom: {
242
+ bottom: Platform.OS === 'ios' ? 50 : 30,
243
+ },
244
+ row: {
245
+ flexDirection: 'row',
246
+ alignItems: 'center',
247
+ padding: 14,
248
+ paddingRight: 10,
249
+ },
250
+ iconCircle: {
251
+ width: 38,
252
+ height: 38,
253
+ borderRadius: 19,
254
+ justifyContent: 'center',
255
+ alignItems: 'center',
256
+ marginRight: 12,
257
+ },
258
+ iconText: {
259
+ color: '#FFF',
260
+ fontSize: 18,
261
+ fontWeight: '900',
262
+ },
263
+ content: {
264
+ flex: 1,
265
+ },
266
+ title: {
267
+ fontSize: 15,
268
+ fontWeight: '800',
269
+ marginBottom: 2,
270
+ letterSpacing: 0.2,
271
+ },
272
+ message: {
273
+ fontSize: 14,
274
+ lineHeight: 19,
275
+ letterSpacing: 0.1,
276
+ },
277
+ closeBtn: {
278
+ width: 28,
279
+ height: 28,
280
+ borderRadius: 14,
281
+ justifyContent: 'center',
282
+ alignItems: 'center',
283
+ marginLeft: 8,
284
+ },
285
+ closeBtnText: {
286
+ fontSize: 14,
287
+ fontWeight: '700',
288
+ },
289
+ progressWrap: {
290
+ height: 3,
291
+ backgroundColor: 'rgba(0,0,0,0.05)',
292
+ },
293
+ progressBar: {
294
+ height: 3,
295
+ },
296
+ containerDark: {
297
+ borderWidth: 1,
298
+ borderColor: 'rgba(255,255,255,0.08)',
299
+ },
300
+ });
301
+
302
+ export default PopifyToast;
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as PopifyAlert } from './PopifyAlert.js';
2
+ export { default as PopifyToast } from './PopifyToast.js';