react-native-bread 0.1.0 → 0.1.2

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 (69) hide show
  1. package/README.md +102 -84
  2. package/lib/commonjs/icons/CloseIcon.js +1 -22
  3. package/lib/commonjs/icons/GreenCheck.js +1 -27
  4. package/lib/commonjs/icons/InfoIcon.js +1 -24
  5. package/lib/commonjs/icons/RedX.js +1 -27
  6. package/lib/commonjs/icons/index.js +1 -34
  7. package/lib/commonjs/index.js +1 -59
  8. package/lib/commonjs/toast-api.js +1 -127
  9. package/lib/commonjs/toast-provider.js +1 -71
  10. package/lib/commonjs/toast-store.js +1 -278
  11. package/lib/commonjs/toast.js +1 -445
  12. package/lib/commonjs/types.js +1 -6
  13. package/lib/module/icons/CloseIcon.js +1 -16
  14. package/lib/module/icons/GreenCheck.js +1 -21
  15. package/lib/module/icons/InfoIcon.js +1 -18
  16. package/lib/module/icons/RedX.js +1 -21
  17. package/lib/module/icons/index.js +1 -7
  18. package/lib/module/index.js +1 -14
  19. package/lib/module/toast-api.js +1 -124
  20. package/lib/module/toast-provider.js +1 -67
  21. package/lib/module/toast-store.js +1 -274
  22. package/lib/module/toast.js +1 -439
  23. package/lib/module/types.js +1 -4
  24. package/lib/typescript/toast-provider.d.ts +13 -12
  25. package/package.json +7 -6
  26. package/lib/commonjs/icons/CloseIcon.js.map +0 -1
  27. package/lib/commonjs/icons/GreenCheck.js.map +0 -1
  28. package/lib/commonjs/icons/InfoIcon.js.map +0 -1
  29. package/lib/commonjs/icons/RedX.js.map +0 -1
  30. package/lib/commonjs/icons/index.js.map +0 -1
  31. package/lib/commonjs/index.js.map +0 -1
  32. package/lib/commonjs/toast-api.js.map +0 -1
  33. package/lib/commonjs/toast-provider.js.map +0 -1
  34. package/lib/commonjs/toast-store.js.map +0 -1
  35. package/lib/commonjs/toast.js.map +0 -1
  36. package/lib/commonjs/types.js.map +0 -1
  37. package/lib/module/icons/CloseIcon.js.map +0 -1
  38. package/lib/module/icons/GreenCheck.js.map +0 -1
  39. package/lib/module/icons/InfoIcon.js.map +0 -1
  40. package/lib/module/icons/RedX.js.map +0 -1
  41. package/lib/module/icons/index.js.map +0 -1
  42. package/lib/module/index.js.map +0 -1
  43. package/lib/module/toast-api.js.map +0 -1
  44. package/lib/module/toast-provider.js.map +0 -1
  45. package/lib/module/toast-store.js.map +0 -1
  46. package/lib/module/toast.js.map +0 -1
  47. package/lib/module/types.js.map +0 -1
  48. package/lib/typescript/icons/CloseIcon.d.ts.map +0 -1
  49. package/lib/typescript/icons/GreenCheck.d.ts.map +0 -1
  50. package/lib/typescript/icons/InfoIcon.d.ts.map +0 -1
  51. package/lib/typescript/icons/RedX.d.ts.map +0 -1
  52. package/lib/typescript/icons/index.d.ts.map +0 -1
  53. package/lib/typescript/index.d.ts.map +0 -1
  54. package/lib/typescript/toast-api.d.ts.map +0 -1
  55. package/lib/typescript/toast-provider.d.ts.map +0 -1
  56. package/lib/typescript/toast-store.d.ts.map +0 -1
  57. package/lib/typescript/toast.d.ts.map +0 -1
  58. package/lib/typescript/types.d.ts.map +0 -1
  59. package/src/icons/CloseIcon.tsx +0 -10
  60. package/src/icons/GreenCheck.tsx +0 -16
  61. package/src/icons/InfoIcon.tsx +0 -12
  62. package/src/icons/RedX.tsx +0 -16
  63. package/src/icons/index.ts +0 -4
  64. package/src/index.ts +0 -26
  65. package/src/toast-api.ts +0 -213
  66. package/src/toast-provider.tsx +0 -81
  67. package/src/toast-store.ts +0 -270
  68. package/src/toast.tsx +0 -417
  69. package/src/types.ts +0 -121
@@ -1,270 +0,0 @@
1
- import type { Toast, ToastConfig, ToastOptions, ToastState, ToastTheme, ToastType, ToastTypeColors } from "./types";
2
-
3
- export type Listener = (state: ToastState) => void;
4
-
5
- const EXIT_DURATION = 350;
6
-
7
- /** Default theme values */
8
- const DEFAULT_THEME: ToastTheme = {
9
- position: "top",
10
- offset: 0,
11
- stacking: true,
12
- maxStack: 3,
13
- dismissible: true,
14
- showCloseButton: true,
15
- colors: {
16
- success: { accent: "#28B770", background: "#FFFFFF" },
17
- error: { accent: "#F05964", background: "#FFFFFF" },
18
- info: { accent: "#EDBE43", background: "#FFFFFF" },
19
- loading: { accent: "#232323", background: "#FFFFFF" },
20
- },
21
- icons: {},
22
- toastStyle: {},
23
- titleStyle: {},
24
- descriptionStyle: {},
25
- defaultDuration: 4000,
26
- };
27
-
28
- /** Deep merge user config with defaults */
29
- function mergeConfig(config: ToastConfig | undefined): ToastTheme {
30
- if (!config) return DEFAULT_THEME;
31
-
32
- const mergedColors = { ...DEFAULT_THEME.colors };
33
- if (config.colors) {
34
- for (const type of Object.keys(config.colors) as ToastType[]) {
35
- const userColors = config.colors[type];
36
- if (userColors) {
37
- mergedColors[type] = {
38
- ...DEFAULT_THEME.colors[type],
39
- ...userColors,
40
- } as ToastTypeColors;
41
- }
42
- }
43
- }
44
-
45
- return {
46
- position: config.position ?? DEFAULT_THEME.position,
47
- offset: config.offset ?? DEFAULT_THEME.offset,
48
- stacking: config.stacking ?? DEFAULT_THEME.stacking,
49
- maxStack: config.maxStack ?? DEFAULT_THEME.maxStack,
50
- dismissible: config.dismissible ?? DEFAULT_THEME.dismissible,
51
- showCloseButton: config.showCloseButton ?? DEFAULT_THEME.showCloseButton,
52
- colors: mergedColors,
53
- icons: { ...DEFAULT_THEME.icons, ...config.icons },
54
- toastStyle: { ...DEFAULT_THEME.toastStyle, ...config.toastStyle },
55
- titleStyle: { ...DEFAULT_THEME.titleStyle, ...config.titleStyle },
56
- descriptionStyle: { ...DEFAULT_THEME.descriptionStyle, ...config.descriptionStyle },
57
- defaultDuration: config.defaultDuration ?? DEFAULT_THEME.defaultDuration,
58
- };
59
- }
60
-
61
- class ToastStore {
62
- private state: ToastState = {
63
- visibleToasts: [],
64
- };
65
-
66
- private theme: ToastTheme = DEFAULT_THEME;
67
-
68
- private listeners = new Set<Listener>();
69
- private toastIdCounter = 0;
70
- private timeouts = new Map<string, ReturnType<typeof setTimeout>>();
71
-
72
- subscribe = (listener: Listener) => {
73
- this.listeners.add(listener);
74
- return () => {
75
- this.listeners.delete(listener);
76
- };
77
- };
78
-
79
- private emit() {
80
- for (const listener of this.listeners) {
81
- listener(this.state);
82
- }
83
- }
84
-
85
- private setState(partial: Partial<ToastState>) {
86
- this.state = { ...this.state, ...partial };
87
- this.emit();
88
- }
89
-
90
- getState = () => this.state;
91
-
92
- getTheme = () => this.theme;
93
-
94
- setConfig = (config: ToastConfig | undefined) => {
95
- this.theme = mergeConfig(config);
96
- };
97
-
98
- show = (
99
- title: string,
100
- description?: string,
101
- type: ToastType = "success",
102
- duration?: number,
103
- options?: ToastOptions
104
- ): string => {
105
- const actualDuration = duration ?? options?.duration ?? this.theme.defaultDuration;
106
- const maxToasts = this.theme.stacking ? this.theme.maxStack : 1;
107
-
108
- const id = `toast-${++this.toastIdCounter}`;
109
- const newToast: Toast = {
110
- id,
111
- title,
112
- description: description ?? options?.description,
113
- type,
114
- duration: actualDuration,
115
- createdAt: Date.now(),
116
- isExiting: false,
117
- options,
118
- };
119
-
120
- const { visibleToasts } = this.state;
121
-
122
- // Get only non-exiting toasts for count
123
- const activeToasts = visibleToasts.filter(t => !t.isExiting);
124
-
125
- if (activeToasts.length >= maxToasts) {
126
- const toastsToRemove = activeToasts.slice(maxToasts - 1);
127
-
128
- for (const toast of toastsToRemove) {
129
- // Clear auto-dismiss timeout
130
- const timeout = this.timeouts.get(toast.id);
131
- if (timeout) {
132
- clearTimeout(timeout);
133
- this.timeouts.delete(toast.id);
134
- }
135
- }
136
-
137
- const removeIds = new Set(toastsToRemove.map(t => t.id));
138
-
139
- if (this.theme.stacking) {
140
- // When stacking is ON: remove old toasts from state immediately (no animation for stack overflow)
141
- this.setState({
142
- visibleToasts: visibleToasts.filter(t => !removeIds.has(t.id)),
143
- });
144
- } else {
145
- // When stacking is OFF: animate out the old toast, wait, then show new one
146
- this.setState({
147
- visibleToasts: visibleToasts.map(t => (removeIds.has(t.id) ? { ...t, isExiting: true } : t)),
148
- });
149
-
150
- // Delay showing the new toast until the old one has animated out
151
- setTimeout(() => {
152
- for (const toast of toastsToRemove) {
153
- this.removeToast(toast.id);
154
- }
155
- this.addToast(newToast, actualDuration);
156
- }, EXIT_DURATION - 220);
157
-
158
- return id;
159
- }
160
- }
161
-
162
- // Add new toast immediately (stacking ON or no existing toasts)
163
- this.addToast(newToast, actualDuration);
164
-
165
- return id;
166
- };
167
-
168
- private addToast(toast: Toast, duration: number) {
169
- this.setState({
170
- visibleToasts: [toast, ...this.state.visibleToasts.filter(t => !t.isExiting)],
171
- });
172
-
173
- // Schedule auto-dismiss with duration multiplier based on position
174
- this.scheduleTimeout(toast.id, duration, 0);
175
-
176
- // Reschedule timeouts for other toasts based on their new positions
177
- this.rescheduleAllTimeouts();
178
- }
179
-
180
- private scheduleTimeout(id: string, baseDuration: number, index: number) {
181
- const existingTimeout = this.timeouts.get(id);
182
- if (existingTimeout) {
183
- clearTimeout(existingTimeout);
184
- }
185
-
186
- // Duration multiplier: index 0 = 1x, index 1 = 2x, index 2 = 3x
187
- const duration = baseDuration * (index + 1);
188
-
189
- const timeout = setTimeout(() => {
190
- this.hide(id);
191
- }, duration);
192
-
193
- this.timeouts.set(id, timeout);
194
- }
195
-
196
- private rescheduleAllTimeouts() {
197
- const { visibleToasts } = this.state;
198
-
199
- visibleToasts.forEach((toast, index) => {
200
- // Skip if already exiting or index 0 (just scheduled)
201
- if (toast.isExiting || index === 0) return;
202
-
203
- this.scheduleTimeout(toast.id, toast.duration, index);
204
- });
205
- }
206
-
207
- hide = (id: string) => {
208
- const { visibleToasts } = this.state;
209
- const toast = visibleToasts.find(t => t.id === id);
210
- if (!toast || toast.isExiting) return;
211
-
212
- // Clear the auto-dismiss timeout
213
- const timeout = this.timeouts.get(id);
214
- if (timeout) {
215
- clearTimeout(timeout);
216
- this.timeouts.delete(id);
217
- }
218
-
219
- // Mark as exiting (triggers exit animation in component)
220
- this.setState({
221
- visibleToasts: visibleToasts.map(t => (t.id === id ? { ...t, isExiting: true } : t)),
222
- });
223
-
224
- // After exit animation, actually remove the toast
225
- setTimeout(() => {
226
- this.removeToast(id);
227
- }, EXIT_DURATION);
228
- };
229
-
230
- private removeToast(id: string) {
231
- const timeout = this.timeouts.get(id);
232
- if (timeout) {
233
- clearTimeout(timeout);
234
- this.timeouts.delete(id);
235
- }
236
-
237
- this.setState({
238
- visibleToasts: this.state.visibleToasts.filter(t => t.id !== id),
239
- });
240
-
241
- // Reschedule remaining toasts with updated positions
242
- this.rescheduleAllTimeouts();
243
- }
244
-
245
- updateToast = (id: string, data: Partial<Omit<Toast, "id" | "createdAt">>) => {
246
- const { visibleToasts } = this.state;
247
- const index = visibleToasts.findIndex(t => t.id === id);
248
- if (index === -1) return;
249
-
250
- this.setState({
251
- visibleToasts: visibleToasts.map(t => (t.id === id ? { ...t, ...data } : t)),
252
- });
253
-
254
- if (data.duration !== undefined) {
255
- this.scheduleTimeout(id, data.duration, index);
256
- }
257
- };
258
-
259
- hideAll = () => {
260
- for (const timeout of this.timeouts.values()) {
261
- clearTimeout(timeout);
262
- }
263
- this.timeouts.clear();
264
- this.setState({ visibleToasts: [] });
265
- };
266
- }
267
-
268
- export const toastStore = new ToastStore();
269
-
270
- export type { Toast, ToastState, ToastType };
package/src/toast.tsx DELETED
@@ -1,417 +0,0 @@
1
- import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
3
- import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
- import Animated, {
5
- Easing,
6
- interpolate,
7
- interpolateColor,
8
- useAnimatedStyle,
9
- useSharedValue,
10
- withTiming,
11
- } from "react-native-reanimated";
12
- import { useSafeAreaInsets } from "react-native-safe-area-context";
13
- import { scheduleOnRN } from "react-native-worklets";
14
- import { CloseIcon, GreenCheck, InfoIcon, RedX } from "./icons";
15
- import { type Toast as ToastData, type ToastState, type ToastType, toastStore } from "./toast-store";
16
- import type { IconRenderFn, ToastPosition, ToastTheme } from "./types";
17
-
18
- const ICON_SIZE = 28;
19
-
20
- /** Default icon for each toast type */
21
- const DefaultIcon = ({ type, accentColor }: { type: ToastType; accentColor: string }) => {
22
- switch (type) {
23
- case "success":
24
- return <GreenCheck width={36} height={36} fill={accentColor} />;
25
- case "error":
26
- return <RedX width={ICON_SIZE} height={ICON_SIZE} fill={accentColor} />;
27
- case "loading":
28
- return <ActivityIndicator size={ICON_SIZE} color={accentColor} />;
29
- case "info":
30
- return <InfoIcon width={ICON_SIZE} height={ICON_SIZE} fill={accentColor} />;
31
- default:
32
- return <GreenCheck width={36} height={36} fill={accentColor} />;
33
- }
34
- };
35
-
36
- /** Resolves the icon to render - checks per-toast, then config, then default */
37
- const resolveIcon = (
38
- type: ToastType,
39
- accentColor: string,
40
- customIcon?: ReactNode | IconRenderFn,
41
- configIcon?: IconRenderFn
42
- ): ReactNode => {
43
- // Per-toast custom icon takes priority
44
- if (customIcon) {
45
- if (typeof customIcon === "function") {
46
- return customIcon({ color: accentColor, size: ICON_SIZE });
47
- }
48
- return customIcon;
49
- }
50
-
51
- // Config-level custom icon
52
- if (configIcon) {
53
- return configIcon({ color: accentColor, size: ICON_SIZE });
54
- }
55
-
56
- // Default icon
57
- return <DefaultIcon type={type} accentColor={accentColor} />;
58
- };
59
-
60
- interface AnimatedIconProps {
61
- type: ToastType;
62
- accentColor: string;
63
- customIcon?: ReactNode | IconRenderFn;
64
- configIcon?: IconRenderFn;
65
- }
66
-
67
- const AnimatedIcon = ({ type, accentColor, customIcon, configIcon }: AnimatedIconProps) => {
68
- const progress = useSharedValue(0);
69
-
70
- useEffect(() => {
71
- progress.value = withTiming(1, { duration: 350, easing: Easing.out(Easing.back(1.5)) });
72
- }, [progress]);
73
-
74
- const style = useAnimatedStyle(() => ({
75
- opacity: progress.value,
76
- transform: [{ scale: 0.7 + progress.value * 0.3 }],
77
- }));
78
-
79
- return <Animated.View style={style}>{resolveIcon(type, accentColor, customIcon, configIcon)}</Animated.View>;
80
- };
81
-
82
- // singleton instance
83
- export const ToastContainer = () => {
84
- const [visibleToasts, setVisibleToasts] = useState<ToastData[]>([]);
85
- const [theme, setTheme] = useState<ToastTheme>(() => toastStore.getTheme());
86
- const { top, bottom } = useSafeAreaInsets();
87
-
88
- useEffect(() => {
89
- const initialState = toastStore.getState();
90
- setVisibleToasts(initialState.visibleToasts);
91
- setTheme(toastStore.getTheme());
92
-
93
- return toastStore.subscribe((state: ToastState) => {
94
- setVisibleToasts(state.visibleToasts);
95
- setTheme(toastStore.getTheme());
96
- });
97
- }, []);
98
-
99
- // Calculate visual index for each toast (exiting toasts don't count)
100
- const getVisualIndex = useCallback(
101
- (toastId: string) => {
102
- let visualIndex = 0;
103
- for (const t of visibleToasts) {
104
- if (t.id === toastId) break;
105
- if (!t.isExiting) visualIndex++;
106
- }
107
- return visualIndex;
108
- },
109
- [visibleToasts]
110
- );
111
-
112
- // Memoize the reversed array to avoid recreating on each render
113
- const reversedToasts = useMemo(() => [...visibleToasts].reverse(), [visibleToasts]);
114
-
115
- if (visibleToasts.length === 0) {
116
- return null;
117
- }
118
-
119
- const isBottom = theme.position === "bottom";
120
- const inset = isBottom ? bottom : top;
121
- const positionStyle = isBottom ? { bottom: inset + theme.offset + 2 } : { top: inset + theme.offset + 2 };
122
-
123
- return (
124
- <View style={[styles.container, positionStyle]} pointerEvents="box-none">
125
- {reversedToasts.map(toast => {
126
- const index = toast.isExiting ? -1 : getVisualIndex(toast.id);
127
- return <MemoizedToastItem key={toast.id} toast={toast} index={index} theme={theme} position={theme.position} />;
128
- })}
129
- </View>
130
- );
131
- };
132
-
133
- interface ToastItemProps {
134
- toast: ToastData;
135
- index: number;
136
- theme: ToastTheme;
137
- position: ToastPosition;
138
- }
139
-
140
- const EASING = Easing.bezier(0.25, 0.1, 0.25, 1.0);
141
- const ToY = 0;
142
- const Duration = 400;
143
- const ExitDuration = 350;
144
- const MaxDragDown = 60;
145
-
146
- const ToastItem = ({ toast, index, theme, position }: ToastItemProps) => {
147
- const progress = useSharedValue(0);
148
- const translationY = useSharedValue(0);
149
- const isBeingDragged = useSharedValue(false);
150
- const shouldDismiss = useSharedValue(false);
151
-
152
- // Position-based animation values
153
- const isBottom = position === "bottom";
154
- const entryFromY = isBottom ? 80 : -80;
155
- const exitToY = isBottom ? 100 : -100;
156
-
157
- // Stack position animation
158
- const stackIndex = useSharedValue(index);
159
-
160
- // Title color animation on variant change
161
- const colorProgress = useSharedValue(1);
162
- const fromColor = useSharedValue(theme.colors[toast.type].accent);
163
- const toColor = useSharedValue(theme.colors[toast.type].accent);
164
-
165
- // Refs for tracking previous values to avoid unnecessary animations
166
- const lastHandledType = useRef(toast.type);
167
- const prevIndex = useRef(index);
168
- const hasEntered = useRef(false);
169
-
170
- // Combined animation effect for entry, exit, color transitions, and stack position
171
- useEffect(() => {
172
- // Entry animation (only once on mount)
173
- if (!hasEntered.current && !toast.isExiting) {
174
- progress.value = withTiming(1, { duration: Duration, easing: EASING });
175
- hasEntered.current = true;
176
- }
177
-
178
- // Exit animation when isExiting becomes true
179
- if (toast.isExiting) {
180
- progress.value = withTiming(0, { duration: ExitDuration, easing: EASING });
181
- translationY.value = withTiming(exitToY, { duration: ExitDuration, easing: EASING });
182
- }
183
-
184
- // Color transition when type changes
185
- if (toast.type !== lastHandledType.current) {
186
- fromColor.value = theme.colors[lastHandledType.current].accent;
187
- toColor.value = theme.colors[toast.type].accent;
188
- lastHandledType.current = toast.type;
189
- colorProgress.value = 0;
190
- colorProgress.value = withTiming(1, { duration: 300, easing: EASING });
191
- }
192
-
193
- // Stack position animation when index changes
194
- if (index >= 0 && prevIndex.current !== index) {
195
- stackIndex.value = withTiming(index, { duration: 300, easing: EASING });
196
- prevIndex.current = index;
197
- }
198
- }, [
199
- toast.isExiting,
200
- toast.type,
201
- index,
202
- progress,
203
- translationY,
204
- fromColor,
205
- toColor,
206
- colorProgress,
207
- stackIndex,
208
- exitToY,
209
- theme.colors,
210
- ]);
211
-
212
- const titleColorStyle = useAnimatedStyle(() => ({
213
- color: interpolateColor(colorProgress.value, [0, 1], [fromColor.value, toColor.value]),
214
- }));
215
-
216
- const dismissToast = useCallback(() => {
217
- toastStore.hide(toast.id);
218
- }, [toast.id]);
219
-
220
- const panGesture = Gesture.Pan()
221
- .onStart(() => {
222
- "worklet";
223
- isBeingDragged.value = true;
224
- shouldDismiss.value = false;
225
- })
226
- .onUpdate(event => {
227
- "worklet";
228
- const rawY = event.translationY;
229
- // For top: negative Y = dismiss direction, positive Y = resistance
230
- // For bottom: positive Y = dismiss direction, negative Y = resistance
231
- const dismissDrag = isBottom ? rawY : -rawY;
232
- const resistDrag = isBottom ? -rawY : rawY;
233
-
234
- if (dismissDrag > 0) {
235
- // Moving toward dismiss direction
236
- const clampedY = isBottom ? Math.min(rawY, 180) : Math.max(rawY, -180);
237
- translationY.value = clampedY;
238
- if (dismissDrag > 40 || (isBottom ? event.velocityY > 300 : event.velocityY < -300)) {
239
- shouldDismiss.value = true;
240
- }
241
- } else {
242
- // Moving away from edge - apply resistance
243
- const exponentialDrag = MaxDragDown * (1 - Math.exp(-resistDrag / 250));
244
- translationY.value = isBottom
245
- ? -Math.min(exponentialDrag, MaxDragDown)
246
- : Math.min(exponentialDrag, MaxDragDown);
247
- shouldDismiss.value = false;
248
- }
249
- })
250
- .onEnd(() => {
251
- "worklet";
252
- isBeingDragged.value = false;
253
-
254
- if (shouldDismiss.value) {
255
- progress.value = withTiming(0, {
256
- duration: ExitDuration,
257
- easing: EASING,
258
- });
259
- const exitOffset = isBottom ? 200 : -200;
260
- translationY.value = withTiming(translationY.value + exitOffset, {
261
- duration: ExitDuration,
262
- easing: EASING,
263
- });
264
- scheduleOnRN(dismissToast);
265
- } else {
266
- translationY.value = withTiming(0, {
267
- duration: 650,
268
- easing: EASING,
269
- });
270
- }
271
- });
272
-
273
- const animatedStyle = useAnimatedStyle(() => {
274
- const baseTranslateY = interpolate(progress.value, [0, 1], [entryFromY, ToY]);
275
-
276
- // Stack offset: each toast behind moves away from edge (up for top, down for bottom)
277
- const stackOffsetY = isBottom ? stackIndex.value * 10 : stackIndex.value * -10;
278
-
279
- // Stack scale: each toast behind scales down by 0.05
280
- const stackScale = 1 - stackIndex.value * 0.05;
281
-
282
- const finalTranslateY = baseTranslateY + translationY.value + stackOffsetY;
283
-
284
- const progressOpacity = interpolate(progress.value, [0, 1], [0, 1]);
285
- // For top: dragging up (negative) fades out. For bottom: dragging down (positive) fades out
286
- const dismissDirection = isBottom ? translationY.value : -translationY.value;
287
- const dragOpacity = dismissDirection > 0 ? interpolate(dismissDirection, [0, 130], [1, 0], "clamp") : 1;
288
- const opacity = progressOpacity * dragOpacity;
289
-
290
- const dragScale = interpolate(Math.abs(translationY.value), [0, 50], [1, 0.98], "clamp");
291
- const scale = stackScale * dragScale;
292
-
293
- return {
294
- transform: [{ translateY: finalTranslateY }, { scale }],
295
- opacity,
296
- zIndex: 1000 - stackIndex.value,
297
- };
298
- });
299
-
300
- const accentColor = theme.colors[toast.type].accent;
301
- const backgroundColor = theme.colors[toast.type].background;
302
- const verticalAnchor = isBottom ? { bottom: 0 } : { top: 0 };
303
-
304
- // Per-toast overrides from options
305
- const { options } = toast;
306
- const customIcon = options?.icon;
307
- const configIcon = theme.icons[toast.type];
308
-
309
- // Resolve dismissible and showCloseButton (per-toast overrides config)
310
- const isDismissible = options?.dismissible ?? theme.dismissible;
311
- const shouldShowCloseButton = toast.type !== "loading" && (options?.showCloseButton ?? theme.showCloseButton);
312
-
313
- // Enable/disable gesture based on dismissible setting
314
- const gesture = isDismissible ? panGesture : Gesture.Pan().enabled(false);
315
-
316
- return (
317
- <GestureDetector gesture={gesture}>
318
- <Animated.View
319
- style={[styles.toast, verticalAnchor, { backgroundColor }, theme.toastStyle, options?.style, animatedStyle]}
320
- >
321
- <View style={styles.content}>
322
- <View style={styles.icon}>
323
- <AnimatedIcon
324
- key={toast.type}
325
- type={toast.type}
326
- accentColor={accentColor}
327
- customIcon={customIcon}
328
- configIcon={configIcon}
329
- />
330
- </View>
331
- <View style={styles.textContainer}>
332
- <Animated.Text
333
- maxFontSizeMultiplier={1.35}
334
- allowFontScaling={false}
335
- style={[styles.title, theme.titleStyle, options?.titleStyle, titleColorStyle]}
336
- >
337
- {toast.title}
338
- </Animated.Text>
339
- {toast.description && (
340
- <Text
341
- allowFontScaling={false}
342
- maxFontSizeMultiplier={1.35}
343
- style={[styles.description, theme.descriptionStyle, options?.descriptionStyle]}
344
- >
345
- {toast.description}
346
- </Text>
347
- )}
348
- </View>
349
- {shouldShowCloseButton && (
350
- <Pressable style={styles.closeButton} onPress={dismissToast} hitSlop={12}>
351
- <CloseIcon width={20} height={20} />
352
- </Pressable>
353
- )}
354
- </View>
355
- </Animated.View>
356
- </GestureDetector>
357
- );
358
- };
359
-
360
- const MemoizedToastItem = memo(ToastItem);
361
-
362
- const styles = StyleSheet.create({
363
- container: {
364
- position: "absolute",
365
- left: 16,
366
- right: 16,
367
- zIndex: 1000,
368
- },
369
- closeButton: {
370
- padding: 4,
371
- alignItems: "center",
372
- justifyContent: "center",
373
- },
374
- icon: {
375
- width: 48,
376
- height: 48,
377
- alignItems: "center",
378
- justifyContent: "center",
379
- marginLeft: 8,
380
- },
381
- content: {
382
- alignItems: "center",
383
- flexDirection: "row",
384
- gap: 12,
385
- minHeight: 36,
386
- },
387
- description: {
388
- color: "#6B7280",
389
- fontSize: 12,
390
- fontWeight: "500",
391
- lineHeight: 16,
392
- },
393
- textContainer: {
394
- flex: 1,
395
- gap: 1,
396
- justifyContent: "center",
397
- },
398
- title: {
399
- fontSize: 14,
400
- fontWeight: "700",
401
- lineHeight: 20,
402
- },
403
- toast: {
404
- borderRadius: 20,
405
- borderCurve: "continuous",
406
- position: "absolute",
407
- left: 0,
408
- right: 0,
409
- paddingHorizontal: 12,
410
- paddingVertical: 10,
411
- shadowColor: "#000",
412
- shadowOffset: { width: 0, height: 8 },
413
- shadowOpacity: 0.05,
414
- shadowRadius: 24,
415
- elevation: 8,
416
- },
417
- });