react-native-feather-toast
Version:
A lightweight, customizable toast notification library for React Native.
403 lines (395 loc) • 11.5 kB
JavaScript
// 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
};