UNPKG

react-native-feather-toast

Version:

A lightweight, customizable toast notification library for React Native.

403 lines (395 loc) 11.5 kB
// src/Toast.tsx import React2 from "react"; // src/styles.ts import { StyleSheet, Platform, StatusBar } from "react-native"; var styles = StyleSheet.create({ rootWrapper: { ...StyleSheet.absoluteFillObject, elevation: 999999, zIndex: 999999 }, toastWrapper: { ...StyleSheet.absoluteFillObject, backgroundColor: "transparent" }, toastContainer: { position: "absolute", left: 16, right: 16, paddingVertical: 15, paddingHorizontal: 10, borderRadius: 12, minHeight: 46, marginTop: Platform.OS === "ios" ? 50 : StatusBar.currentHeight, ...Platform.select({ ios: { shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 12 }, android: { elevation: 8 } }) }, toastContent: { flexDirection: "row", alignItems: "center" }, iconContainer: { marginRight: 12 }, textContainer: { flex: 1 }, toastText: { color: "#fff", fontSize: 14, fontWeight: "600", letterSpacing: 0.2 }, descriptionText: { color: "rgba(255, 255, 255, 0.9)", fontSize: 12, marginTop: 4, lineHeight: 20 }, dismissButton: { padding: 3, marginLeft: 8, borderRadius: 20, backgroundColor: "rgba(255, 255, 255, 0.1)" } }); // src/toastConfig.ts var getToastConfig = (type = "info") => { switch (type) { case "success": return { backgroundColor: "#10B981", icon: "check-circle", iconColor: "#ffffff" }; case "error": return { backgroundColor: "#EF4444", icon: "x-circle", iconColor: "#ffffff" }; case "warning": return { backgroundColor: "#F59E0B", icon: "alert-circle", iconColor: "#ffffff" }; default: return { backgroundColor: "#3B82F6", icon: "info", iconColor: "#ffffff" }; } }; // src/icons.tsx import React from "react"; import { Svg, Path, Circle } from "react-native-svg"; var defaultProps = { size: 18, color: "#FFFFFF" }; var Icons = { info: ({ size = defaultProps.size, color = defaultProps.color }) => /* @__PURE__ */ React.createElement(Svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none" }, /* @__PURE__ */ React.createElement(Circle, { cx: "12", cy: "12", r: "10", stroke: color, strokeWidth: "2" }), /* @__PURE__ */ React.createElement( Path, { d: "M12 8h.01", stroke: color, strokeWidth: "2", strokeLinecap: "round" } ), /* @__PURE__ */ React.createElement( Path, { d: "M11 12h1v4h1", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" } )), success: ({ size = defaultProps.size, color = defaultProps.color }) => /* @__PURE__ */ React.createElement(Svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none" }, /* @__PURE__ */ React.createElement(Circle, { cx: "12", cy: "12", r: "10", stroke: color, strokeWidth: "2" }), /* @__PURE__ */ React.createElement( Path, { d: "M8 12l3 3 5-5", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" } )), error: ({ size = defaultProps.size, color = defaultProps.color }) => /* @__PURE__ */ React.createElement(Svg, { width: size, height: size, viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement( Path, { fillRule: "evenodd", clipRule: "evenodd", d: "M8.445.609a1.1 1.1 0 00-1.89 0L.161 11.337A1.1 1.1 0 001.106 13h12.788a1.1 1.1 0 00.945-1.663L8.445.609zm-1.03.512a.1.1 0 01.17 0l6.395 10.728a.1.1 0 01-.086.151H1.106a.1.1 0 01-.086-.151L7.414 1.12zm-.588 3.365a.674.674 0 111.346 0L8.02 8.487a.52.52 0 01-1.038 0l-.154-4zm1.423 5.99a.75.75 0 11-1.5 0 .75.75 0 011.5 0z", fill: color } )), warning: ({ size = defaultProps.size, color = defaultProps.color }) => /* @__PURE__ */ React.createElement(Svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none" }, /* @__PURE__ */ React.createElement(Circle, { cx: "12", cy: "12", r: "10", stroke: color, strokeWidth: "2" }), /* @__PURE__ */ React.createElement(Path, { d: "M12 8v4", stroke: color, strokeWidth: "2", strokeLinecap: "round" }), /* @__PURE__ */ React.createElement( Path, { d: "M12 16h.01", stroke: color, strokeWidth: "2", strokeLinecap: "round" } )), close: ({ size = defaultProps.size, color = defaultProps.color }) => /* @__PURE__ */ React.createElement(Svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none" }, /* @__PURE__ */ React.createElement( Path, { d: "M18 6L6 18", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" } ), /* @__PURE__ */ React.createElement( Path, { d: "M6 6L18 18", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" } )) }; // src/Toast.tsx import { Animated, Text, View, TouchableOpacity } from "react-native"; var Toast = ({ title, description, type, fadeAnim, slideAnim, onDismiss }) => { const config = getToastConfig(type); const Icon = Icons[type] || Icons.info; return /* @__PURE__ */ React2.createElement( Animated.View, { style: [ styles.toastContainer, { opacity: fadeAnim, transform: [{ translateY: slideAnim }], backgroundColor: config.backgroundColor } ] }, /* @__PURE__ */ React2.createElement(View, { style: styles.toastContent }, /* @__PURE__ */ React2.createElement(View, { style: styles.iconContainer }, /* @__PURE__ */ React2.createElement(Icon, { color: config.iconColor, size: 18 })), /* @__PURE__ */ React2.createElement(View, { style: styles.textContainer }, /* @__PURE__ */ React2.createElement(Text, { style: styles.toastText }, title), description && /* @__PURE__ */ React2.createElement(Text, { style: styles.descriptionText }, description)), /* @__PURE__ */ React2.createElement(TouchableOpacity, { onPress: onDismiss, style: styles.dismissButton }, /* @__PURE__ */ React2.createElement(Icons.close, { color: config.iconColor, size: 14 }))) ); }; // src/ToastRoot.tsx import React4, { useEffect as useEffect2 } from "react"; import { View as View2, KeyboardAvoidingView, Platform as Platform3 } from "react-native"; // src/useToast.tsx import { useCallback, useRef, useState, useEffect } from "react"; import { Animated as Animated2, Dimensions, Platform as Platform2, StatusBar as StatusBar2 } from "react-native"; var { height, width } = Dimensions.get("window"); var useToast = () => { const fadeAnim = useRef(new Animated2.Value(0)).current; const slideAnim = useRef(new Animated2.Value(-100)).current; const timeoutRef = useRef(); const [toastTitle, setToastTitle] = useState(""); const [toastDescription, setToastDescription] = useState(""); const [toastType, setToastType] = useState("info"); const [isVisible, setIsVisible] = useState(false); const handleDismiss = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } Animated2.parallel([ Animated2.timing(fadeAnim, { toValue: 0, duration: 200, useNativeDriver: true }), Animated2.timing(slideAnim, { toValue: -100, duration: 200, useNativeDriver: true }) ]).start(() => { setToastTitle(""); setToastDescription(""); setIsVisible(false); }); }, [fadeAnim, slideAnim]); const getToastPosition = useCallback((position) => { if (position === "top") { return Platform2.OS === "ios" ? 50 : 40; } const statusBarHeight = Platform2.OS === "android" ? StatusBar2.currentHeight || 0 : 0; const homeIndicatorHeight = Platform2.OS === "ios" && height >= 812 ? 34 : 0; const safePadding = 16; return height - (statusBarHeight + homeIndicatorHeight + safePadding + 100); }, []); const handleShowToast = useCallback( ({ title, description, type = "info", duration = 3e3, position = "top" }) => { setToastTitle(title); setToastDescription(description || ""); setToastType(type); setIsVisible(true); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } fadeAnim.setValue(0); slideAnim.setValue(position === "top" ? -100 : height); const targetPosition = getToastPosition(position); Animated2.parallel([ Animated2.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true }), Animated2.spring(slideAnim, { toValue: targetPosition, speed: 12, bounciness: 8, useNativeDriver: true }) ]).start(); timeoutRef.current = setTimeout(() => { Animated2.parallel([ Animated2.timing(fadeAnim, { toValue: 0, duration: 300, useNativeDriver: true }), Animated2.timing(slideAnim, { toValue: position === "top" ? -100 : height, duration: 300, useNativeDriver: true }) ]).start(() => { setToastTitle(""); setToastDescription(""); setIsVisible(false); }); }, duration); }, [getToastPosition] ); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return { toastTitle, toastDescription, toastType, isVisible, fadeAnim, slideAnim, handleShowToast, handleDismiss }; }; // src/ToastRoot.tsx var ToastEmitter = class _ToastEmitter { static instance; listeners = []; constructor() { } static getInstance() { if (!_ToastEmitter.instance) { _ToastEmitter.instance = new _ToastEmitter(); } return _ToastEmitter.instance; } addListener(callback) { this.listeners.push(callback); return () => { this.listeners = this.listeners.filter( (listener) => listener !== callback ); }; } emit(config) { this.listeners.forEach((listener) => listener(config)); } }; function showToast(config) { ToastEmitter.getInstance().emit(config); } var ToastRoot = () => { const { toastTitle, toastDescription, toastType, isVisible, fadeAnim, slideAnim, handleShowToast, handleDismiss } = useToast(); useEffect2(() => { const unsubscribe = ToastEmitter.getInstance().addListener(handleShowToast); return () => unsubscribe(); }, [handleShowToast]); if (!isVisible) return null; return /* @__PURE__ */ React4.createElement(View2, { style: [styles.rootWrapper], pointerEvents: "box-none" }, /* @__PURE__ */ React4.createElement( KeyboardAvoidingView, { style: styles.toastWrapper, pointerEvents: "box-none", behavior: Platform3.OS === "ios" ? "padding" : void 0 }, /* @__PURE__ */ React4.createElement( Toast, { title: toastTitle, description: toastDescription, type: toastType, fadeAnim, slideAnim, onDismiss: handleDismiss } ) )); }; export { Toast, ToastRoot, showToast };