react-native-toast-message-library 0.0.0-stage → 1.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/package.json CHANGED
@@ -1,6 +1,84 @@
1
1
  {
2
2
  "name": "react-native-toast-message-library",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Lightweight, customizable toast notifications for React Native: queue, themes, animations, actions, swipe-to-dismiss and accessibility. Pure JavaScript, no native modules.",
5
+ "main": "src/index.js",
6
+ "react-native": "src/index.js",
7
+ "types": "types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./types/index.d.ts",
11
+ "react-native": "./src/index.js",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "sideEffects": false,
17
+ "files": [
18
+ "src",
19
+ "types/index.d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "android": "react-native run-android",
26
+ "ios": "react-native run-ios",
27
+ "start": "react-native start",
28
+ "test": "jest",
29
+ "test:coverage": "jest --coverage",
30
+ "lint": "eslint src demo __tests__ App.js index.js --ext .js",
31
+ "typecheck": "tsc --noEmit -p types/tsconfig.json",
32
+ "validate": "npm run lint && npm run typecheck && npm test && npm pack --dry-run",
33
+ "prepublishOnly": "npm run lint && npm run typecheck && npm test"
34
+ },
35
+ "keywords": [
36
+ "react-native",
37
+ "toast",
38
+ "notification",
39
+ "snackbar",
40
+ "ios",
41
+ "android",
42
+ "toast-notification"
43
+ ],
44
+ "license": "MIT",
45
+ "author": "Denish Dabhi <dabhidenish57@gmail.com>",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "peerDependencies": {
50
+ "react": ">=18.0.0",
51
+ "react-native": ">=0.72.0",
52
+ "react-native-safe-area-context": ">=4.0.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "react-native-safe-area-context": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@babel/core": "^7.25.2",
61
+ "@babel/preset-env": "^7.25.3",
62
+ "@babel/runtime": "^7.25.0",
63
+ "@react-native-community/cli": "20.2.0",
64
+ "@react-native-community/cli-platform-android": "20.2.0",
65
+ "@react-native-community/cli-platform-ios": "20.2.0",
66
+ "@react-native/babel-preset": "0.88.0",
67
+ "@react-native/eslint-config": "0.88.0",
68
+ "@react-native/jest-preset": "0.88.0",
69
+ "@react-native/metro-config": "0.88.0",
70
+ "@testing-library/react-native": "^14.1.0",
71
+ "@types/react": "^19.2.0",
72
+ "eslint": "^8.19.0",
73
+ "jest": "^29.6.3",
74
+ "prettier": "2.8.8",
75
+ "react": "19.3.0",
76
+ "react-native": "0.88.0",
77
+ "react-native-safe-area-context": "^5.5.2",
78
+ "test-renderer": "^1.0.0",
79
+ "typescript": "^6.0.3"
80
+ },
81
+ "engines": {
82
+ "node": ">= 22.11.0"
83
+ }
84
+ }
@@ -0,0 +1,85 @@
1
+ import { Animated, Easing } from 'react-native';
2
+
3
+ const SLIDE_DISTANCE = 64;
4
+
5
+ /**
6
+ * Every animation is driven by a single Animated.Value `progress` that goes
7
+ * 0 (hidden) -> 1 (shown). Only opacity and transforms are animated, so the
8
+ * native driver can be used and no layout pass is needed per frame.
9
+ */
10
+
11
+ export function resolveAnimation(animation, reducedMotion) {
12
+ if (reducedMotion && (animation === 'slide' || animation === 'zoom')) {
13
+ // Movement and scaling are replaced by a plain cross-fade.
14
+ return 'fade';
15
+ }
16
+ return animation;
17
+ }
18
+
19
+ function slideOffset(position) {
20
+ if (position.startsWith('top')) {
21
+ return -SLIDE_DISTANCE;
22
+ }
23
+ if (position.startsWith('bottom')) {
24
+ return SLIDE_DISTANCE;
25
+ }
26
+ return SLIDE_DISTANCE / 2;
27
+ }
28
+
29
+ export function getAnimatedStyle(animation, progress, position) {
30
+ switch (animation) {
31
+ case 'fade':
32
+ return { opacity: progress };
33
+ case 'slide':
34
+ return {
35
+ opacity: progress,
36
+ transform: [
37
+ {
38
+ translateY: progress.interpolate({
39
+ inputRange: [0, 1],
40
+ outputRange: [slideOffset(position), 0],
41
+ }),
42
+ },
43
+ ],
44
+ };
45
+ case 'zoom':
46
+ return {
47
+ opacity: progress,
48
+ transform: [
49
+ {
50
+ scale: progress.interpolate({
51
+ inputRange: [0, 1],
52
+ outputRange: [0.85, 1],
53
+ }),
54
+ },
55
+ ],
56
+ };
57
+ default:
58
+ return {};
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Starts an enter (toValue 1) or exit (toValue 0) animation and returns the
64
+ * Animated handle so the caller can stop it. With animation 'none' or a zero
65
+ * duration the value is set immediately and `onEnd({ finished: true })` is
66
+ * called synchronously.
67
+ */
68
+ export function runToastAnimation(progress, toValue, options, onEnd) {
69
+ const { animation, duration } = options;
70
+ if (animation === 'none' || !(duration > 0)) {
71
+ progress.setValue(toValue);
72
+ if (onEnd) {
73
+ onEnd({ finished: true });
74
+ }
75
+ return null;
76
+ }
77
+ const handle = Animated.timing(progress, {
78
+ toValue,
79
+ duration,
80
+ easing: toValue === 1 ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
81
+ useNativeDriver: true,
82
+ });
83
+ handle.start(onEnd);
84
+ return handle;
85
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet, Text } from 'react-native';
3
+
4
+ export default function ToastAction({
5
+ label,
6
+ accessibilityLabel,
7
+ color,
8
+ onPress,
9
+ fontFamily,
10
+ }) {
11
+ return (
12
+ <Pressable
13
+ accessibilityRole="button"
14
+ accessibilityLabel={accessibilityLabel || label}
15
+ hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
16
+ onPress={onPress}
17
+ style={({ pressed }) => [styles.button, { opacity: pressed ? 0.6 : 1 }]}
18
+ >
19
+ <Text
20
+ maxFontSizeMultiplier={1.4}
21
+ style={[styles.label, { color }, fontFamily && { fontFamily }]}
22
+ >
23
+ {label}
24
+ </Text>
25
+ </Pressable>
26
+ );
27
+ }
28
+
29
+ const styles = StyleSheet.create({
30
+ button: {
31
+ minHeight: 36,
32
+ minWidth: 44,
33
+ paddingHorizontal: 10,
34
+ marginLeft: 8,
35
+ alignItems: 'center',
36
+ justifyContent: 'center',
37
+ },
38
+ label: { fontSize: 14, fontWeight: '700', textTransform: 'none' },
39
+ });
@@ -0,0 +1,109 @@
1
+ import React, { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
2
+ import { StyleSheet, View, useWindowDimensions } from 'react-native';
3
+ import useReducedMotion from '../hooks/useReducedMotion';
4
+ import { POSITIONS } from '../utils/constants';
5
+ import createAnnouncer from '../utils/createAnnouncer';
6
+ import useToastInsets from '../utils/safeArea';
7
+ import ToastItem from './ToastItem';
8
+
9
+ const SIDE_MARGIN = 16;
10
+ const MAX_WIDTH = 480;
11
+
12
+ function groupStyle(position, insets, offset) {
13
+ const horizontal = {
14
+ paddingLeft: SIDE_MARGIN + insets.left + offset.x,
15
+ paddingRight: SIDE_MARGIN + insets.right + offset.x,
16
+ };
17
+ const alignItems = position.endsWith('left')
18
+ ? 'flex-start'
19
+ : position.endsWith('right')
20
+ ? 'flex-end'
21
+ : 'center';
22
+ const base = {
23
+ position: 'absolute',
24
+ left: 0,
25
+ right: 0,
26
+ alignItems,
27
+ ...horizontal,
28
+ };
29
+ if (position.startsWith('top')) {
30
+ return { ...base, top: insets.top + offset.y };
31
+ }
32
+ if (position.startsWith('bottom')) {
33
+ return { ...base, bottom: insets.bottom + offset.y };
34
+ }
35
+ return { ...base, top: 0, bottom: 0, justifyContent: 'center' };
36
+ }
37
+
38
+ export default function ToastContainer({
39
+ manager,
40
+ theme,
41
+ spacing,
42
+ offset,
43
+ insets: insetsProp,
44
+ }) {
45
+ const snapshot = useSyncExternalStore(
46
+ manager.subscribe,
47
+ manager.getSnapshot,
48
+ manager.getSnapshot,
49
+ );
50
+ const reducedMotion = useReducedMotion();
51
+ const insets = useToastInsets(insetsProp);
52
+ const { width: windowWidth } = useWindowDimensions();
53
+ const announcer = useRef(null);
54
+ if (!announcer.current) {
55
+ announcer.current = createAnnouncer();
56
+ }
57
+ useEffect(() => () => announcer.current.cancel(), []);
58
+
59
+ const toastWidth = Math.min(
60
+ MAX_WIDTH,
61
+ windowWidth - 2 * SIDE_MARGIN - insets.left - insets.right - 2 * offset.x,
62
+ );
63
+
64
+ const groups = useMemo(() => {
65
+ const map = {};
66
+ snapshot.toasts.forEach(entry => {
67
+ const key = entry.options.position;
68
+ (map[key] = map[key] || []).push(entry);
69
+ });
70
+ return map;
71
+ }, [snapshot]);
72
+
73
+ return (
74
+ <View pointerEvents="box-none" style={styles.overlay}>
75
+ {POSITIONS.filter(position => groups[position]).map(position => (
76
+ <View
77
+ key={position}
78
+ pointerEvents="box-none"
79
+ style={groupStyle(position, insets, offset)}
80
+ >
81
+ {groups[position].map((entry, index) => (
82
+ <View
83
+ key={entry.id}
84
+ pointerEvents="box-none"
85
+ style={index === 0 ? null : { marginTop: spacing }}
86
+ >
87
+ <ToastItem
88
+ entry={entry}
89
+ manager={manager}
90
+ theme={theme}
91
+ reducedMotion={reducedMotion}
92
+ announce={announcer.current.announce}
93
+ width={toastWidth}
94
+ />
95
+ </View>
96
+ ))}
97
+ </View>
98
+ ))}
99
+ </View>
100
+ );
101
+ }
102
+
103
+ const styles = StyleSheet.create({
104
+ overlay: {
105
+ ...StyleSheet.absoluteFillObject,
106
+ zIndex: 9999,
107
+ elevation: 9999,
108
+ },
109
+ });
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+
4
+ const GLYPHS = {
5
+ success: '✓',
6
+ error: '✕',
7
+ warning: '!',
8
+ info: 'i',
9
+ };
10
+
11
+ /**
12
+ * Renders the leading icon. `icon` may be:
13
+ * - false -> nothing
14
+ * - a React element -> rendered as is
15
+ * - a function -> called with { type, color, size }
16
+ * - undefined -> built-in glyph for the type (none for "custom")
17
+ * The glyph is decorative; the toast's accessibility label carries the type.
18
+ */
19
+ export default function ToastIcon({ type, icon, color, glyphColor, size }) {
20
+ if (icon === false) {
21
+ return null;
22
+ }
23
+ if (typeof icon === 'function') {
24
+ return <View style={styles.wrapper}>{icon({ type, color, size })}</View>;
25
+ }
26
+ if (React.isValidElement(icon)) {
27
+ return <View style={styles.wrapper}>{icon}</View>;
28
+ }
29
+ const glyph = GLYPHS[type];
30
+ if (!glyph) {
31
+ return null;
32
+ }
33
+ return (
34
+ <View
35
+ accessible={false}
36
+ importantForAccessibility="no-hide-descendants"
37
+ style={[
38
+ styles.badge,
39
+ {
40
+ width: size,
41
+ height: size,
42
+ borderRadius: size / 2,
43
+ backgroundColor: color,
44
+ },
45
+ ]}
46
+ >
47
+ <Text
48
+ allowFontScaling={false}
49
+ style={[styles.glyph, { color: glyphColor, fontSize: size * 0.62 }]}
50
+ >
51
+ {glyph}
52
+ </Text>
53
+ </View>
54
+ );
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ wrapper: { marginRight: 12, justifyContent: 'center' },
59
+ badge: {
60
+ marginRight: 12,
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ },
64
+ glyph: { fontWeight: '800', textAlign: 'center', includeFontPadding: false },
65
+ });