beautiful-snackbar 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.
Files changed (79) hide show
  1. package/README.md +233 -0
  2. package/example/.claude/settings.json +5 -0
  3. package/example/.vscode/extensions.json +1 -0
  4. package/example/.vscode/settings.json +7 -0
  5. package/example/AGENTS.md +3 -0
  6. package/example/CLAUDE.md +1 -0
  7. package/example/app.json +44 -0
  8. package/example/assets/expo.icon/Assets/expo-symbol 2.svg +3 -0
  9. package/example/assets/expo.icon/Assets/grid.png +0 -0
  10. package/example/assets/expo.icon/icon.json +40 -0
  11. package/example/assets/images/android-icon-background.png +0 -0
  12. package/example/assets/images/android-icon-foreground.png +0 -0
  13. package/example/assets/images/android-icon-monochrome.png +0 -0
  14. package/example/assets/images/expo-badge-white.png +0 -0
  15. package/example/assets/images/expo-badge.png +0 -0
  16. package/example/assets/images/expo-logo.png +0 -0
  17. package/example/assets/images/favicon.png +0 -0
  18. package/example/assets/images/icon.png +0 -0
  19. package/example/assets/images/logo-glow.png +0 -0
  20. package/example/assets/images/react-logo.png +0 -0
  21. package/example/assets/images/react-logo@2x.png +0 -0
  22. package/example/assets/images/react-logo@3x.png +0 -0
  23. package/example/assets/images/splash-icon.png +0 -0
  24. package/example/assets/images/tabIcons/explore.png +0 -0
  25. package/example/assets/images/tabIcons/explore@2x.png +0 -0
  26. package/example/assets/images/tabIcons/explore@3x.png +0 -0
  27. package/example/assets/images/tabIcons/home.png +0 -0
  28. package/example/assets/images/tabIcons/home@2x.png +0 -0
  29. package/example/assets/images/tabIcons/home@3x.png +0 -0
  30. package/example/assets/images/tutorial-web.png +0 -0
  31. package/example/metro.config.js +24 -0
  32. package/example/package.json +46 -0
  33. package/example/scripts/reset-project.js +114 -0
  34. package/example/src/app/_layout.tsx +63 -0
  35. package/example/src/app/explore.tsx +181 -0
  36. package/example/src/app/index.tsx +641 -0
  37. package/example/src/components/animated-icon.module.css +6 -0
  38. package/example/src/components/animated-icon.tsx +132 -0
  39. package/example/src/components/animated-icon.web.tsx +108 -0
  40. package/example/src/components/app-tabs.tsx +33 -0
  41. package/example/src/components/app-tabs.web.tsx +116 -0
  42. package/example/src/components/external-link.tsx +25 -0
  43. package/example/src/components/hint-row.tsx +35 -0
  44. package/example/src/components/themed-text.tsx +73 -0
  45. package/example/src/components/themed-view.tsx +16 -0
  46. package/example/src/components/ui/collapsible.tsx +65 -0
  47. package/example/src/components/web-badge.tsx +44 -0
  48. package/example/src/constants/theme.ts +66 -0
  49. package/example/src/global.css +9 -0
  50. package/example/src/hooks/use-color-scheme.ts +1 -0
  51. package/example/src/hooks/use-color-scheme.web.ts +21 -0
  52. package/example/src/hooks/use-theme.ts +14 -0
  53. package/example/tsconfig.json +35 -0
  54. package/lib/components/ActionableSnackbar.d.ts +7 -0
  55. package/lib/components/ActionableSnackbar.js +96 -0
  56. package/lib/components/BeautifulSnackbar.d.ts +11 -0
  57. package/lib/components/BeautifulSnackbar.js +189 -0
  58. package/lib/components/StandardSnackbar.d.ts +7 -0
  59. package/lib/components/StandardSnackbar.js +65 -0
  60. package/lib/constants.d.ts +7 -0
  61. package/lib/constants.js +20 -0
  62. package/lib/index.d.ts +5 -0
  63. package/lib/index.js +11 -0
  64. package/lib/manager.d.ts +58 -0
  65. package/lib/manager.js +107 -0
  66. package/lib/types.d.ts +25 -0
  67. package/lib/types.js +2 -0
  68. package/lib/useSnackbarAnimation.d.ts +14 -0
  69. package/lib/useSnackbarAnimation.js +118 -0
  70. package/package.json +33 -0
  71. package/src/components/ActionableSnackbar.tsx +109 -0
  72. package/src/components/BeautifulSnackbar.tsx +203 -0
  73. package/src/components/StandardSnackbar.tsx +70 -0
  74. package/src/constants.ts +20 -0
  75. package/src/index.ts +5 -0
  76. package/src/manager.ts +151 -0
  77. package/src/types.ts +27 -0
  78. package/src/useSnackbarAnimation.ts +145 -0
  79. package/tsconfig.json +23 -0
package/lib/manager.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.snackbar = exports.SnackbarManager = exports.SnackbarItem = void 0;
4
+ const react_native_1 = require("react-native");
5
+ class SnackbarItem {
6
+ constructor(options, id) {
7
+ this.onShowRequested = () => { };
8
+ this.onHideRequested = () => { };
9
+ this.id = id;
10
+ this.message = options.message;
11
+ this.messageStyle = options.messageStyle;
12
+ this.containerStyle = options.containerStyle;
13
+ this.actionLabel = options.actionLabel;
14
+ this.actionStyle = options.actionStyle;
15
+ this.onActionPress = options.onActionPress;
16
+ this.icon = options.icon;
17
+ this.duration = options.duration ?? 'short';
18
+ this.position = options.position;
19
+ this.animationType = options.animationType;
20
+ this.bottomOffset = options.bottomOffset;
21
+ this.topOffset = options.topOffset;
22
+ this.dismissOnNavigation = Boolean(options.dismissOnNavigation);
23
+ this.backgroundColor = options.backgroundColor;
24
+ this.textColor = options.textColor;
25
+ this.actionColor = options.actionColor;
26
+ this.type = options.type;
27
+ this.data = options.data;
28
+ }
29
+ }
30
+ exports.SnackbarItem = SnackbarItem;
31
+ class SnackbarManager {
32
+ constructor() {
33
+ this.configListeners = [];
34
+ this.activeItems = [];
35
+ this.counter = 0;
36
+ this.config = {
37
+ avoidKeyboard: true,
38
+ position: 'bottom',
39
+ animationType: 'slide',
40
+ bottomOffset: 24,
41
+ topOffset: react_native_1.Platform.OS === 'ios' ? 50 : 24,
42
+ };
43
+ }
44
+ registerListener(listener) {
45
+ this.stateListener = listener;
46
+ listener([...this.activeItems]);
47
+ }
48
+ unregisterListener() {
49
+ this.stateListener = undefined;
50
+ }
51
+ registerConfigListener(listener) {
52
+ this.configListeners.push(listener);
53
+ listener({ ...this.config });
54
+ return () => {
55
+ this.configListeners = this.configListeners.filter((l) => l !== listener);
56
+ };
57
+ }
58
+ setAvoidKeyboard(val) {
59
+ this.config.avoidKeyboard = val;
60
+ this.configListeners.forEach((listener) => listener({ ...this.config }));
61
+ }
62
+ getAvoidKeyboard() {
63
+ return this.config.avoidKeyboard;
64
+ }
65
+ setPosition(val) {
66
+ this.config.position = val;
67
+ this.configListeners.forEach((listener) => listener({ ...this.config }));
68
+ }
69
+ getPosition() {
70
+ return this.config.position;
71
+ }
72
+ setAnimationType(val) {
73
+ this.config.animationType = val;
74
+ this.configListeners.forEach((listener) => listener({ ...this.config }));
75
+ }
76
+ getAnimationType() {
77
+ return this.config.animationType;
78
+ }
79
+ setBottomOffset(val) {
80
+ this.config.bottomOffset = val;
81
+ this.configListeners.forEach((listener) => listener({ ...this.config }));
82
+ }
83
+ getBottomOffset() {
84
+ return this.config.bottomOffset;
85
+ }
86
+ setTopOffset(val) {
87
+ this.config.topOffset = val;
88
+ this.configListeners.forEach((listener) => listener({ ...this.config }));
89
+ }
90
+ getTopOffset() {
91
+ return this.config.topOffset;
92
+ }
93
+ show(options) {
94
+ this.counter++;
95
+ const id = `sb_${Date.now()}_${this.counter}`;
96
+ const item = new SnackbarItem(options, id);
97
+ this.activeItems.push(item);
98
+ this.stateListener?.([...this.activeItems]);
99
+ return item;
100
+ }
101
+ dismiss(item) {
102
+ this.activeItems = this.activeItems.filter(x => x.id !== item.id);
103
+ this.stateListener?.([...this.activeItems]);
104
+ }
105
+ }
106
+ exports.SnackbarManager = SnackbarManager;
107
+ exports.snackbar = new SnackbarManager();
package/lib/types.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { ViewStyle, TextStyle } from 'react-native';
3
+ export type SnackbarDuration = 'short' | 'medium' | 'long' | 'infinite';
4
+ export type SnackbarPosition = 'top' | 'bottom';
5
+ export type SnackbarAnimationType = 'slide' | 'fade' | 'scale';
6
+ export interface SnackbarOptions {
7
+ message: string;
8
+ messageStyle?: TextStyle;
9
+ containerStyle?: ViewStyle;
10
+ actionLabel?: string;
11
+ actionStyle?: TextStyle;
12
+ onActionPress?: () => void;
13
+ icon?: React.ReactNode;
14
+ duration?: SnackbarDuration;
15
+ position?: SnackbarPosition;
16
+ animationType?: SnackbarAnimationType;
17
+ bottomOffset?: number;
18
+ topOffset?: number;
19
+ dismissOnNavigation?: boolean;
20
+ backgroundColor?: string;
21
+ textColor?: string;
22
+ actionColor?: string;
23
+ type?: string;
24
+ data?: any;
25
+ }
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ import { Animated } from "react-native";
2
+ import { SnackbarItem } from './manager';
3
+ export declare const useSnackbarAnimation: (item: SnackbarItem) => {
4
+ animStyle: {
5
+ opacity: Animated.Value;
6
+ transform: ({
7
+ translateY: Animated.Value;
8
+ scale?: undefined;
9
+ } | {
10
+ scale: Animated.Value;
11
+ translateY?: undefined;
12
+ })[];
13
+ };
14
+ };
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSnackbarAnimation = void 0;
4
+ const react_1 = require("react");
5
+ const react_native_1 = require("react-native");
6
+ const manager_1 = require("./manager");
7
+ const constants_1 = require("./constants");
8
+ const useSnackbarAnimation = (item) => {
9
+ const durationMs = (0, constants_1.getDurationMs)(item.duration);
10
+ const position = item.position || manager_1.snackbar.getPosition();
11
+ const animType = item.animationType || manager_1.snackbar.getAnimationType();
12
+ const isTop = position === "top";
13
+ const offset = isTop
14
+ ? item.topOffset !== undefined
15
+ ? item.topOffset
16
+ : manager_1.snackbar.getTopOffset()
17
+ : item.bottomOffset !== undefined
18
+ ? item.bottomOffset
19
+ : manager_1.snackbar.getBottomOffset();
20
+ const startVal = isTop ? -120 : 120;
21
+ const endVal = isTop ? offset : -offset;
22
+ const opacity = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
23
+ const scale = (0, react_1.useRef)(new react_native_1.Animated.Value(animType === "scale" ? 0.8 : 1)).current;
24
+ const translateY = (0, react_1.useRef)(new react_native_1.Animated.Value(animType === "slide" ? startVal : endVal)).current;
25
+ const animStyle = {
26
+ opacity,
27
+ transform: [{ translateY }, { scale }],
28
+ };
29
+ const removeSelf = (0, react_1.useCallback)(() => {
30
+ manager_1.snackbar.dismiss(item);
31
+ }, [item]);
32
+ const animateIntro = (0, react_1.useCallback)(() => {
33
+ const animations = [
34
+ react_native_1.Animated.timing(opacity, {
35
+ toValue: 1,
36
+ duration: constants_1.TRANSITION_SPEED,
37
+ useNativeDriver: true,
38
+ }),
39
+ ];
40
+ if (animType === "slide") {
41
+ animations.push(react_native_1.Animated.timing(translateY, {
42
+ toValue: endVal,
43
+ duration: constants_1.TRANSITION_SPEED,
44
+ useNativeDriver: true,
45
+ }));
46
+ }
47
+ if (animType === "scale") {
48
+ animations.push(react_native_1.Animated.spring(scale, {
49
+ toValue: 1,
50
+ tension: 40,
51
+ friction: 6,
52
+ useNativeDriver: true,
53
+ }));
54
+ }
55
+ return react_native_1.Animated.parallel(animations);
56
+ }, [opacity, translateY, scale, animType, endVal]);
57
+ const animateOutro = (0, react_1.useCallback)(() => {
58
+ const animations = [
59
+ react_native_1.Animated.timing(opacity, {
60
+ toValue: 0,
61
+ duration: constants_1.TRANSITION_SPEED,
62
+ useNativeDriver: true,
63
+ }),
64
+ ];
65
+ if (animType === "slide") {
66
+ animations.push(react_native_1.Animated.timing(translateY, {
67
+ toValue: startVal,
68
+ duration: constants_1.TRANSITION_SPEED,
69
+ useNativeDriver: true,
70
+ }));
71
+ }
72
+ if (animType === "scale") {
73
+ animations.push(react_native_1.Animated.timing(scale, {
74
+ toValue: 0.8,
75
+ duration: constants_1.TRANSITION_SPEED,
76
+ useNativeDriver: true,
77
+ }));
78
+ }
79
+ return react_native_1.Animated.parallel(animations);
80
+ }, [opacity, translateY, scale, animType, startVal]);
81
+ const animateIntroAndOutro = (0, react_1.useCallback)(() => {
82
+ react_native_1.Animated.sequence([
83
+ animateIntro(),
84
+ react_native_1.Animated.delay(durationMs),
85
+ animateOutro(),
86
+ ]).start(({ finished }) => {
87
+ if (finished) {
88
+ removeSelf();
89
+ }
90
+ });
91
+ }, [animateIntro, animateOutro, durationMs, removeSelf]);
92
+ const animateShow = (0, react_1.useCallback)(() => {
93
+ animateIntro().start();
94
+ }, [animateIntro]);
95
+ const animateHide = (0, react_1.useCallback)(() => {
96
+ animateOutro().start();
97
+ }, [animateOutro]);
98
+ const animateDismiss = (0, react_1.useCallback)(() => {
99
+ animateOutro().start(({ finished }) => {
100
+ if (finished) {
101
+ removeSelf();
102
+ }
103
+ });
104
+ }, [animateOutro, removeSelf]);
105
+ (0, react_1.useEffect)(() => {
106
+ if (item.duration !== 'infinite') {
107
+ animateIntroAndOutro();
108
+ }
109
+ else {
110
+ item.onShowRequested = animateShow;
111
+ item.onHideRequested = animateHide;
112
+ animateShow();
113
+ }
114
+ item.onDismissRequested = animateDismiss;
115
+ }, [animateDismiss, animateHide, item, animateShow, animateIntroAndOutro]);
116
+ return { animStyle };
117
+ };
118
+ exports.useSnackbarAnimation = useSnackbarAnimation;
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "beautiful-snackbar",
3
+ "version": "1.0.0",
4
+ "description": "A beautiful, highly customizable, animated snackbar for React Native.",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Satwikk1/beautiful-snackbar.git"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc"
13
+ },
14
+ "private": false,
15
+ "keywords": [
16
+ "react-native",
17
+ "snackbar",
18
+ "toast",
19
+ "notification",
20
+ "animated"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "peerDependencies": {
25
+ "react": "*",
26
+ "react-native": "*",
27
+ "react-native-safe-area-context": "*"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.0.0",
31
+ "@types/react": "^18.0.0 || ^19.0.0"
32
+ }
33
+ }
@@ -0,0 +1,109 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, TouchableOpacity, View, Animated } from 'react-native';
3
+ import { SnackbarItem, snackbar } from '../manager';
4
+ import { useSnackbarAnimation } from '../useSnackbarAnimation';
5
+
6
+ interface ActionableSnackbarProps {
7
+ item: SnackbarItem;
8
+ }
9
+
10
+ export const ActionableSnackbar = ({ item }: ActionableSnackbarProps) => {
11
+ const { animStyle } = useSnackbarAnimation(item);
12
+
13
+ const customContainerStyle = [
14
+ styles.wrap,
15
+ item.backgroundColor ? { backgroundColor: item.backgroundColor } : null,
16
+ item.containerStyle,
17
+ ];
18
+
19
+ const customMessageStyle = [
20
+ styles.message,
21
+ item.textColor ? { color: item.textColor } : null,
22
+ item.messageStyle,
23
+ ];
24
+
25
+ const customActionStyle = [
26
+ styles.actionText,
27
+ item.actionColor ? { color: item.actionColor } : null,
28
+ item.actionStyle,
29
+ ];
30
+
31
+ const isTop = (item.position || snackbar.getPosition()) === 'top';
32
+ const positionStyle = isTop ? { top: 0 } : { bottom: 0 };
33
+
34
+ const handleActionPress = () => {
35
+ item.onActionPress?.();
36
+ item.onDismissRequested?.();
37
+ };
38
+
39
+ return (
40
+ <Animated.View style={[styles.outer, positionStyle, animStyle]}>
41
+ <View style={customContainerStyle}>
42
+ <TouchableOpacity activeOpacity={1} style={styles.contentArea}>
43
+ {item.icon && <View style={styles.iconWrap}>{item.icon}</View>}
44
+ <Text numberOfLines={2} style={customMessageStyle}>
45
+ {item.message}
46
+ </Text>
47
+ </TouchableOpacity>
48
+ {item.actionLabel && (
49
+ <TouchableOpacity
50
+ onPress={handleActionPress}
51
+ style={styles.actionBtn}
52
+ activeOpacity={0.7}
53
+ >
54
+ <Text style={customActionStyle}>
55
+ {item.actionLabel}
56
+ </Text>
57
+ </TouchableOpacity>
58
+ )}
59
+ </View>
60
+ </Animated.View>
61
+ );
62
+ };
63
+
64
+ const styles = StyleSheet.create({
65
+ outer: {
66
+ width: '100%',
67
+ position: 'absolute',
68
+ },
69
+ wrap: {
70
+ paddingVertical: 14,
71
+ paddingHorizontal: 16,
72
+ marginHorizontal: 16,
73
+ backgroundColor: '#1E293B',
74
+ flexDirection: 'row',
75
+ alignItems: 'center',
76
+ justifyContent: 'space-between',
77
+ borderRadius: 10,
78
+ shadowColor: '#000',
79
+ shadowOffset: { width: 0, height: 4 },
80
+ shadowOpacity: 0.15,
81
+ shadowRadius: 6,
82
+ elevation: 4,
83
+ },
84
+ contentArea: {
85
+ flexDirection: 'row',
86
+ alignItems: 'center',
87
+ flex: 1,
88
+ paddingRight: 12,
89
+ },
90
+ iconWrap: {
91
+ marginRight: 10,
92
+ justifyContent: 'center',
93
+ alignItems: 'center',
94
+ },
95
+ message: {
96
+ color: '#F8FAFC',
97
+ fontSize: 14,
98
+ fontWeight: '600',
99
+ flexShrink: 1,
100
+ },
101
+ actionBtn: {
102
+ justifyContent: 'center',
103
+ },
104
+ actionText: {
105
+ color: '#10B981', // Emerald 500
106
+ fontSize: 14,
107
+ fontWeight: '700',
108
+ },
109
+ });
@@ -0,0 +1,203 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { Animated, Keyboard, Platform, StyleSheet, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { SnackbarItem, snackbar } from '../manager';
5
+ import { StandardSnackbar } from './StandardSnackbar';
6
+ import { ActionableSnackbar } from './ActionableSnackbar';
7
+ import { useSnackbarAnimation } from '../useSnackbarAnimation';
8
+
9
+ export interface BeautifulSnackbarProps {
10
+ navigation?: any;
11
+ avoidKeyboard?: boolean;
12
+ templates?: Record<string, React.ComponentType<{ item: SnackbarItem; dismiss?: () => void }>>;
13
+ }
14
+
15
+ const CustomTemplateWrapper = ({
16
+ item,
17
+ Template,
18
+ }: {
19
+ item: SnackbarItem;
20
+ Template: React.ComponentType<{ item: SnackbarItem; dismiss?: () => void }>;
21
+ }) => {
22
+ const { animStyle } = useSnackbarAnimation(item);
23
+ const isTop = (item.position || snackbar.getPosition()) === 'top';
24
+ const positionStyle = isTop ? { top: 0 } : { bottom: 0 };
25
+ const dismiss = useCallback(() => {
26
+ item.onDismissRequested?.();
27
+ }, [item]);
28
+
29
+ return (
30
+ <Animated.View style={[styles.customOuter, positionStyle, animStyle]}>
31
+ <Template item={item} dismiss={dismiss} />
32
+ </Animated.View>
33
+ );
34
+ };
35
+
36
+ export const BeautifulSnackbar = ({
37
+ navigation,
38
+ avoidKeyboard: propAvoidKeyboard,
39
+ templates,
40
+ }: BeautifulSnackbarProps) => {
41
+ const [activeItems, setActiveItems] = useState<SnackbarItem[]>([]);
42
+ const [avoidKeyboard, setAvoidKeyboard] = useState(snackbar.getAvoidKeyboard());
43
+ const [globalPosition, setGlobalPosition] = useState(snackbar.getPosition());
44
+ const [keyboardOffset] = useState(() => new Animated.Value(0));
45
+
46
+ const insets = useSafeAreaInsets();
47
+ const bottomInset = insets.bottom;
48
+
49
+ useEffect(() => {
50
+ snackbar.registerListener(setActiveItems);
51
+ const unregisterConfig = snackbar.registerConfigListener((config) => {
52
+ setAvoidKeyboard(config.avoidKeyboard);
53
+ setGlobalPosition(config.position);
54
+ });
55
+
56
+ return () => {
57
+ snackbar.unregisterListener();
58
+ unregisterConfig();
59
+ };
60
+ }, []);
61
+
62
+ // Prioritize prop value if specified, otherwise fall back to manager configuration
63
+ const finalAvoidKeyboard = propAvoidKeyboard !== undefined ? propAvoidKeyboard : avoidKeyboard;
64
+
65
+ useEffect(() => {
66
+ if (!finalAvoidKeyboard) {
67
+ Animated.timing(keyboardOffset, {
68
+ toValue: 0,
69
+ duration: 200,
70
+ useNativeDriver: true,
71
+ }).start();
72
+ return;
73
+ }
74
+
75
+ const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
76
+ const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
77
+
78
+ const showSubscription = Keyboard.addListener(showEvent, (e: any) => {
79
+ const targetOffset = Math.max(0, e.endCoordinates.height - bottomInset);
80
+ Animated.timing(keyboardOffset, {
81
+ toValue: targetOffset,
82
+ duration: e.duration || 250,
83
+ useNativeDriver: true,
84
+ }).start();
85
+ });
86
+
87
+ const hideSubscription = Keyboard.addListener(hideEvent, (e: any) => {
88
+ Animated.timing(keyboardOffset, {
89
+ toValue: 0,
90
+ duration: e.duration || 250,
91
+ useNativeDriver: true,
92
+ }).start();
93
+ });
94
+
95
+ return () => {
96
+ showSubscription.remove();
97
+ hideSubscription.remove();
98
+ };
99
+ }, [finalAvoidKeyboard, keyboardOffset, bottomInset]);
100
+
101
+ const handleNavigationStateChange = useCallback(() => {
102
+ activeItems.forEach((item: SnackbarItem) => {
103
+ if (item.dismissOnNavigation) {
104
+ item.onDismissRequested?.();
105
+ }
106
+ });
107
+ }, [activeItems]);
108
+
109
+ useEffect(() => {
110
+ let cleanup: (() => void) | undefined;
111
+ if (navigation && typeof navigation.addListener === 'function') {
112
+ try {
113
+ const isReady = typeof navigation.isReady === 'function' ? navigation.isReady() : true;
114
+ if (isReady) {
115
+ cleanup = navigation.addListener('state', handleNavigationStateChange);
116
+ }
117
+ } catch (e) {
118
+ console.warn('BeautifulSnackbar failed to bind navigation listener:', e);
119
+ }
120
+ }
121
+ return cleanup;
122
+ }, [handleNavigationStateChange, navigation]);
123
+
124
+ const renderItem = (item: SnackbarItem) => {
125
+ // Custom template checks
126
+ if (item.type && templates && templates[item.type]) {
127
+ const TemplateComponent = templates[item.type];
128
+ return (
129
+ <CustomTemplateWrapper
130
+ key={item.id}
131
+ item={item}
132
+ Template={TemplateComponent}
133
+ />
134
+ );
135
+ }
136
+
137
+ if (item.actionLabel && item.onActionPress) {
138
+ return <ActionableSnackbar key={item.id} item={item} />;
139
+ }
140
+ return <StandardSnackbar key={item.id} item={item} />;
141
+ };
142
+
143
+ const topItems = activeItems.filter(
144
+ (item: SnackbarItem) => (item.position || globalPosition) === 'top'
145
+ );
146
+ const bottomItems = activeItems.filter(
147
+ (item: SnackbarItem) => (item.position || globalPosition) === 'bottom'
148
+ );
149
+
150
+ return (
151
+ <View style={styles.rootContainer} pointerEvents="box-none">
152
+ <View style={styles.topContainer} pointerEvents="box-none">
153
+ {topItems.map(renderItem)}
154
+ </View>
155
+ <Animated.View
156
+ style={[
157
+ styles.bottomContainer,
158
+ {
159
+ transform: [
160
+ {
161
+ translateY: keyboardOffset.interpolate({
162
+ inputRange: [0, 1000],
163
+ outputRange: [0, -1000],
164
+ }),
165
+ },
166
+ ],
167
+ },
168
+ ]}
169
+ pointerEvents="box-none"
170
+ >
171
+ {bottomItems.map(renderItem)}
172
+ </Animated.View>
173
+ </View>
174
+ );
175
+ };
176
+
177
+ const styles = StyleSheet.create({
178
+ rootContainer: {
179
+ position: 'absolute',
180
+ bottom: 0,
181
+ left: 0,
182
+ right: 0,
183
+ top: 0,
184
+ },
185
+ topContainer: {
186
+ position: 'absolute',
187
+ top: 0,
188
+ left: 0,
189
+ right: 0,
190
+ justifyContent: 'flex-start',
191
+ },
192
+ bottomContainer: {
193
+ position: 'absolute',
194
+ bottom: 0,
195
+ left: 0,
196
+ right: 0,
197
+ justifyContent: 'flex-end',
198
+ },
199
+ customOuter: {
200
+ width: '100%',
201
+ position: 'absolute',
202
+ },
203
+ });
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View, Animated } from 'react-native';
3
+ import { SnackbarItem, snackbar } from '../manager';
4
+ import { useSnackbarAnimation } from '../useSnackbarAnimation';
5
+
6
+ interface StandardSnackbarProps {
7
+ item: SnackbarItem;
8
+ }
9
+
10
+ export const StandardSnackbar = ({ item }: StandardSnackbarProps) => {
11
+ const { animStyle } = useSnackbarAnimation(item);
12
+
13
+ const customContainerStyle = [
14
+ styles.wrap,
15
+ item.backgroundColor ? { backgroundColor: item.backgroundColor } : null,
16
+ item.containerStyle,
17
+ ];
18
+
19
+ const customMessageStyle = [
20
+ styles.message,
21
+ item.textColor ? { color: item.textColor } : null,
22
+ item.messageStyle,
23
+ ];
24
+
25
+ const isTop = (item.position || snackbar.getPosition()) === 'top';
26
+ const positionStyle = isTop ? { top: 0 } : { bottom: 0 };
27
+
28
+ return (
29
+ <Animated.View style={[styles.outer, positionStyle, animStyle]}>
30
+ <View style={customContainerStyle}>
31
+ {item.icon && <View style={styles.iconWrap}>{item.icon}</View>}
32
+ <Text numberOfLines={2} style={customMessageStyle}>
33
+ {item.message}
34
+ </Text>
35
+ </View>
36
+ </Animated.View>
37
+ );
38
+ };
39
+
40
+ const styles = StyleSheet.create({
41
+ outer: {
42
+ width: '100%',
43
+ position: 'absolute',
44
+ },
45
+ wrap: {
46
+ paddingVertical: 14,
47
+ paddingHorizontal: 16,
48
+ marginHorizontal: 16,
49
+ backgroundColor: '#1E293B',
50
+ flexDirection: 'row',
51
+ alignItems: 'center',
52
+ borderRadius: 10,
53
+ shadowColor: '#000',
54
+ shadowOffset: { width: 0, height: 4 },
55
+ shadowOpacity: 0.15,
56
+ shadowRadius: 6,
57
+ elevation: 4,
58
+ },
59
+ iconWrap: {
60
+ marginRight: 10,
61
+ justifyContent: 'center',
62
+ alignItems: 'center',
63
+ },
64
+ message: {
65
+ color: '#F8FAFC',
66
+ fontSize: 14,
67
+ fontWeight: '600',
68
+ flexShrink: 1,
69
+ },
70
+ });