react-native-bread 0.6.0 → 0.7.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/README.md +24 -5
- package/lib/commonjs/toast-icons.js +2 -2
- package/lib/commonjs/toast-provider.js +7 -2
- package/lib/commonjs/toast.js +33 -33
- package/lib/commonjs/use-toast-state.js +11 -11
- package/lib/module/toast-icons.js +2 -2
- package/lib/module/toast-provider.js +7 -2
- package/lib/module/toast.js +34 -34
- package/lib/module/use-toast-state.js +12 -12
- package/lib/typescript/toast-provider.d.ts +5 -3
- package/lib/typescript/types.d.ts +9 -10
- package/lib/typescript/use-toast-state.d.ts +3 -3
- package/package.json +46 -4
- package/src/constants.ts +23 -0
- package/src/icons/CloseIcon.tsx +10 -0
- package/src/icons/GreenCheck.tsx +16 -0
- package/src/icons/InfoIcon.tsx +12 -0
- package/src/icons/RedX.tsx +16 -0
- package/src/icons/index.ts +4 -0
- package/src/index.ts +28 -0
- package/src/pool.ts +57 -0
- package/src/toast-api.ts +247 -0
- package/src/toast-icons.tsx +55 -0
- package/src/toast-provider.tsx +127 -0
- package/src/toast-store.ts +254 -0
- package/src/toast.tsx +398 -0
- package/src/types.ts +166 -0
- package/src/use-toast-state.ts +78 -0
package/README.md
CHANGED
|
@@ -16,9 +16,10 @@ An extremely lightweight, opinionated toast component for React Native.
|
|
|
16
16
|
- Promise handling with automatic loading → success/error states
|
|
17
17
|
- Toast stacking with configurable limits
|
|
18
18
|
- **Works above modals** - automatic on iOS, simple setup on Android
|
|
19
|
-
- **RTL support** -
|
|
19
|
+
- **RTL support** - code-level RTL for when you're not using native RTL (`I18nManager`)
|
|
20
20
|
- Completely customizable - colors, icons, styles, animations
|
|
21
21
|
- Full Expo compatibility
|
|
22
|
+
- **React Compiler compatible** - uses `.set()` / `.value` API for shared values
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
|
|
@@ -181,7 +182,7 @@ Customize all toasts globally via the `config` prop on `<BreadLoaf />`:
|
|
|
181
182
|
<BreadLoaf
|
|
182
183
|
config={{
|
|
183
184
|
position: 'bottom',
|
|
184
|
-
rtl: false, //
|
|
185
|
+
rtl: false, // Code-level RTL — not needed if using native RTL (I18nManager)
|
|
185
186
|
stacking: true,
|
|
186
187
|
maxStack: 3,
|
|
187
188
|
defaultDuration: 4000,
|
|
@@ -221,21 +222,39 @@ Available options include:
|
|
|
221
222
|
|
|
222
223
|
Toasts automatically appear above native modals on **iOS**.
|
|
223
224
|
|
|
224
|
-
On **Android**,
|
|
225
|
+
On **Android**, you have two options:
|
|
226
|
+
|
|
227
|
+
### Option 1: Use a Contained Modal
|
|
228
|
+
|
|
229
|
+
The simplest fix is to use `containedModal` presentation instead of `modal`. On Android, `modal` and `containedModal` look nearly identical, so this is an easy swap:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<Stack.Screen
|
|
233
|
+
name="(modal)"
|
|
234
|
+
options={{ presentation: Platform.OS === "android" ? "containedModal" : "modal" }}
|
|
235
|
+
/>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This renders the modal within the React hierarchy on Android, so toasts from your root `<BreadLoaf />` remain visible.
|
|
239
|
+
|
|
240
|
+
### Option 2: Use ToastPortal
|
|
241
|
+
|
|
242
|
+
If you need native modals, add `<ToastPortal />` inside your modal layouts:
|
|
225
243
|
|
|
226
244
|
```tsx
|
|
227
245
|
// app/(modal)/_layout.tsx
|
|
228
246
|
import { Stack } from "expo-router";
|
|
229
|
-
import { Platform } from "react-native";
|
|
230
247
|
import { ToastPortal } from "react-native-bread";
|
|
231
248
|
|
|
232
249
|
export default function ModalLayout() {
|
|
233
250
|
return (
|
|
234
251
|
<>
|
|
235
252
|
<Stack screenOptions={{ headerShown: false }} />
|
|
236
|
-
|
|
253
|
+
<ToastPortal />
|
|
237
254
|
</>
|
|
238
255
|
);
|
|
239
256
|
}
|
|
240
257
|
```
|
|
241
258
|
|
|
259
|
+
The `ToastPortal` component only renders on Android - it returns `null` on iOS, so no platform check is needed.
|
|
260
|
+
|
|
@@ -61,10 +61,10 @@ const AnimatedIcon = exports.AnimatedIcon = /*#__PURE__*/(0, _react.memo)(({
|
|
|
61
61
|
}) => {
|
|
62
62
|
const progress = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
63
63
|
(0, _react.useEffect)(() => {
|
|
64
|
-
progress.
|
|
64
|
+
progress.set((0, _reactNativeReanimated.withTiming)(1, {
|
|
65
65
|
duration: _constants.ICON_ANIMATION_DURATION,
|
|
66
66
|
easing: _reactNativeReanimated.Easing.out(_reactNativeReanimated.Easing.back(1.5))
|
|
67
|
-
});
|
|
67
|
+
}));
|
|
68
68
|
}, [progress]);
|
|
69
69
|
const style = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
70
70
|
opacity: progress.value,
|
|
@@ -77,6 +77,9 @@ function BreadLoaf({
|
|
|
77
77
|
* the main `<BreadLoaf />` won't be visible. Add `<ToastPortal />` inside your
|
|
78
78
|
* modal layouts to show toasts above modal content.
|
|
79
79
|
*
|
|
80
|
+
* This component only renders on Android - it returns `null` on iOS where
|
|
81
|
+
* `<BreadLoaf />` already handles modal overlay via `FullWindowOverlay`.
|
|
82
|
+
*
|
|
80
83
|
* This component only renders toasts - it does not accept configuration.
|
|
81
84
|
* All styling/behavior is inherited from your root `<BreadLoaf />` config.
|
|
82
85
|
*
|
|
@@ -85,19 +88,21 @@ function BreadLoaf({
|
|
|
85
88
|
* // app/(modal)/_layout.tsx
|
|
86
89
|
* import { Stack } from 'expo-router';
|
|
87
90
|
* import { ToastPortal } from 'react-native-bread';
|
|
88
|
-
* import { Platform } from 'react-native';
|
|
89
91
|
*
|
|
90
92
|
* export default function ModalLayout() {
|
|
91
93
|
* return (
|
|
92
94
|
* <>
|
|
93
95
|
* <Stack screenOptions={{ headerShown: false }} />
|
|
94
|
-
*
|
|
96
|
+
* <ToastPortal />
|
|
95
97
|
* </>
|
|
96
98
|
* );
|
|
97
99
|
* }
|
|
98
100
|
* ```
|
|
99
101
|
*/
|
|
100
102
|
function ToastPortal() {
|
|
103
|
+
if (_reactNative.Platform.OS !== "android") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
101
106
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(ToastContent, {});
|
|
102
107
|
}
|
|
103
108
|
const styles = _reactNative.StyleSheet.create({
|
package/lib/commonjs/toast.js
CHANGED
|
@@ -28,69 +28,69 @@ const ToastContainer = () => {
|
|
|
28
28
|
theme,
|
|
29
29
|
toastsWithIndex,
|
|
30
30
|
isBottom,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
topToastMutable,
|
|
32
|
+
isBottomMutable,
|
|
33
|
+
isDismissibleMutable
|
|
34
34
|
} = (0, _useToastState.useToastState)();
|
|
35
35
|
const shouldDismiss = (0, _reactNativeReanimated.useSharedValue)(false);
|
|
36
|
-
const panGesture =
|
|
36
|
+
const panGesture = _reactNativeGestureHandler.Gesture.Pan().onStart(() => {
|
|
37
37
|
"worklet";
|
|
38
38
|
|
|
39
39
|
shouldDismiss.set(false);
|
|
40
40
|
}).onUpdate(event => {
|
|
41
41
|
"worklet";
|
|
42
42
|
|
|
43
|
-
if (!
|
|
44
|
-
const ref =
|
|
43
|
+
if (!isDismissibleMutable.value) return;
|
|
44
|
+
const ref = topToastMutable.value;
|
|
45
45
|
if (!ref) return;
|
|
46
46
|
const {
|
|
47
47
|
slot
|
|
48
48
|
} = ref;
|
|
49
|
-
const bottom =
|
|
49
|
+
const bottom = isBottomMutable.value;
|
|
50
50
|
const rawY = event.translationY;
|
|
51
51
|
const dismissDrag = bottom ? rawY : -rawY;
|
|
52
52
|
const resistDrag = bottom ? -rawY : rawY;
|
|
53
53
|
if (dismissDrag > 0) {
|
|
54
54
|
const clampedY = bottom ? Math.min(rawY, _constants.MAX_DRAG_CLAMP) : Math.max(rawY, -_constants.MAX_DRAG_CLAMP);
|
|
55
|
-
slot.translationY.
|
|
55
|
+
slot.translationY.set(clampedY);
|
|
56
56
|
const shouldTriggerDismiss = dismissDrag > _constants.DISMISS_THRESHOLD || (bottom ? event.velocityY > _constants.DISMISS_VELOCITY_THRESHOLD : event.velocityY < -_constants.DISMISS_VELOCITY_THRESHOLD);
|
|
57
57
|
shouldDismiss.set(shouldTriggerDismiss);
|
|
58
58
|
} else {
|
|
59
59
|
const exponentialDrag = _constants.MAX_DRAG_RESISTANCE * (1 - Math.exp(-resistDrag / 250));
|
|
60
|
-
slot.translationY.
|
|
61
|
-
shouldDismiss.
|
|
60
|
+
slot.translationY.set(bottom ? -Math.min(exponentialDrag, _constants.MAX_DRAG_RESISTANCE) : Math.min(exponentialDrag, _constants.MAX_DRAG_RESISTANCE));
|
|
61
|
+
shouldDismiss.set(false);
|
|
62
62
|
}
|
|
63
63
|
}).onEnd(() => {
|
|
64
64
|
"worklet";
|
|
65
65
|
|
|
66
|
-
if (!
|
|
67
|
-
const ref =
|
|
66
|
+
if (!isDismissibleMutable.value) return;
|
|
67
|
+
const ref = topToastMutable.value;
|
|
68
68
|
if (!ref) return;
|
|
69
69
|
const {
|
|
70
70
|
slot
|
|
71
71
|
} = ref;
|
|
72
|
-
const bottom =
|
|
72
|
+
const bottom = isBottomMutable.value;
|
|
73
73
|
if (shouldDismiss.value) {
|
|
74
|
-
slot.progress.
|
|
74
|
+
slot.progress.set((0, _reactNativeReanimated.withTiming)(0, {
|
|
75
75
|
duration: _constants.EXIT_DURATION,
|
|
76
76
|
easing: _constants.EASING
|
|
77
|
-
});
|
|
77
|
+
}));
|
|
78
78
|
const exitOffset = bottom ? _constants.SWIPE_EXIT_OFFSET : -_constants.SWIPE_EXIT_OFFSET;
|
|
79
|
-
slot.translationY.
|
|
79
|
+
slot.translationY.set((0, _reactNativeReanimated.withTiming)(slot.translationY.value + exitOffset, {
|
|
80
80
|
duration: _constants.EXIT_DURATION,
|
|
81
81
|
easing: _constants.EASING
|
|
82
|
-
});
|
|
82
|
+
}));
|
|
83
83
|
(0, _reactNativeWorklets.scheduleOnRN)(ref.dismiss);
|
|
84
84
|
} else {
|
|
85
|
-
slot.translationY.
|
|
85
|
+
slot.translationY.set((0, _reactNativeReanimated.withTiming)(0, {
|
|
86
86
|
duration: _constants.SPRING_BACK_DURATION,
|
|
87
87
|
easing: _constants.EASING
|
|
88
|
-
});
|
|
88
|
+
}));
|
|
89
89
|
}
|
|
90
|
-
})
|
|
90
|
+
});
|
|
91
91
|
const registerTopToast = (0, _react.useCallback)(values => {
|
|
92
|
-
|
|
93
|
-
}, [
|
|
92
|
+
topToastMutable.set(values);
|
|
93
|
+
}, [topToastMutable]);
|
|
94
94
|
if (visibleToasts.length === 0) return null;
|
|
95
95
|
const inset = isBottom ? bottom : top;
|
|
96
96
|
const positionStyle = isBottom ? {
|
|
@@ -137,13 +137,13 @@ const ToastItem = ({
|
|
|
137
137
|
|
|
138
138
|
// biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect
|
|
139
139
|
(0, _react.useEffect)(() => {
|
|
140
|
-
slot.progress.
|
|
141
|
-
slot.translationY.
|
|
142
|
-
slot.stackIndex.
|
|
143
|
-
slot.progress.
|
|
140
|
+
slot.progress.set(0);
|
|
141
|
+
slot.translationY.set(0);
|
|
142
|
+
slot.stackIndex.set(index);
|
|
143
|
+
slot.progress.set((0, _reactNativeReanimated.withTiming)(1, {
|
|
144
144
|
duration: _constants.ENTRY_DURATION,
|
|
145
145
|
easing: _constants.EASING
|
|
146
|
-
});
|
|
146
|
+
}));
|
|
147
147
|
const iconTimeout = setTimeout(() => setShowIcon(true), 50);
|
|
148
148
|
return () => {
|
|
149
149
|
clearTimeout(iconTimeout);
|
|
@@ -157,20 +157,20 @@ const ToastItem = ({
|
|
|
157
157
|
let loadingTimeout = null;
|
|
158
158
|
if (toast.isExiting && !tracker.wasExiting) {
|
|
159
159
|
tracker.wasExiting = true;
|
|
160
|
-
slot.progress.
|
|
160
|
+
slot.progress.set((0, _reactNativeReanimated.withTiming)(0, {
|
|
161
161
|
duration: _constants.EXIT_DURATION,
|
|
162
162
|
easing: _constants.EASING
|
|
163
|
-
});
|
|
164
|
-
slot.translationY.
|
|
163
|
+
}));
|
|
164
|
+
slot.translationY.set((0, _reactNativeReanimated.withTiming)(exitToY, {
|
|
165
165
|
duration: _constants.EXIT_DURATION,
|
|
166
166
|
easing: _constants.EASING
|
|
167
|
-
});
|
|
167
|
+
}));
|
|
168
168
|
}
|
|
169
169
|
if (tracker.initialized && index !== tracker.prevIndex) {
|
|
170
|
-
slot.stackIndex.
|
|
170
|
+
slot.stackIndex.set((0, _reactNativeReanimated.withTiming)(index, {
|
|
171
171
|
duration: _constants.STACK_TRANSITION_DURATION,
|
|
172
172
|
easing: _constants.EASING
|
|
173
|
-
});
|
|
173
|
+
}));
|
|
174
174
|
}
|
|
175
175
|
tracker.prevIndex = index;
|
|
176
176
|
tracker.initialized = true;
|
|
@@ -10,9 +10,9 @@ var _toastStore = require("./toast-store.js");
|
|
|
10
10
|
const useToastState = () => {
|
|
11
11
|
const [visibleToasts, setVisibleToasts] = (0, _react.useState)([]);
|
|
12
12
|
const [theme, setTheme] = (0, _react.useState)(() => _toastStore.toastStore.getTheme());
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const [topToastMutable] = (0, _react.useState)(() => (0, _reactNativeReanimated.makeMutable)(null));
|
|
14
|
+
const [isBottomMutable] = (0, _react.useState)(() => (0, _reactNativeReanimated.makeMutable)(theme.position === "bottom"));
|
|
15
|
+
const [isDismissibleMutable] = (0, _react.useState)(() => (0, _reactNativeReanimated.makeMutable)(true));
|
|
16
16
|
const isBottom = theme.position === "bottom";
|
|
17
17
|
const topToast = visibleToasts.find(t => !t.isExiting);
|
|
18
18
|
const isTopDismissible = topToast?.options?.dismissible ?? theme.dismissible;
|
|
@@ -21,8 +21,8 @@ const useToastState = () => {
|
|
|
21
21
|
const initialTheme = _toastStore.toastStore.getTheme();
|
|
22
22
|
setVisibleToasts(initialToasts);
|
|
23
23
|
const initialTopToast = initialToasts.find(t => !t.isExiting);
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
isBottomMutable.set(initialTheme.position === "bottom");
|
|
25
|
+
isDismissibleMutable.set(initialTopToast?.options?.dismissible ?? initialTheme.dismissible);
|
|
26
26
|
let pendingToasts = null;
|
|
27
27
|
let rafId = null;
|
|
28
28
|
const unsubscribe = _toastStore.toastStore.subscribe(state => {
|
|
@@ -38,13 +38,13 @@ const useToastState = () => {
|
|
|
38
38
|
rafId = null;
|
|
39
39
|
setTheme(prev => prev === currentTheme ? prev : currentTheme);
|
|
40
40
|
const topToast = currentToasts.find(t => !t.isExiting);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
isBottomMutable.set(currentTheme.position === "bottom");
|
|
42
|
+
isDismissibleMutable.set(topToast?.options?.dismissible ?? currentTheme.dismissible);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
return unsubscribe;
|
|
47
|
-
}, []);
|
|
47
|
+
}, [isBottomMutable, isDismissibleMutable]);
|
|
48
48
|
const toastsWithIndex = (0, _react.useMemo)(() => {
|
|
49
49
|
const indices = new Map();
|
|
50
50
|
let visualIndex = 0;
|
|
@@ -63,9 +63,9 @@ const useToastState = () => {
|
|
|
63
63
|
toastsWithIndex,
|
|
64
64
|
isBottom,
|
|
65
65
|
isTopDismissible,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
topToastMutable,
|
|
67
|
+
isBottomMutable,
|
|
68
|
+
isDismissibleMutable
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
71
|
exports.useToastState = useToastState;
|
|
@@ -55,10 +55,10 @@ export const AnimatedIcon = /*#__PURE__*/memo(({
|
|
|
55
55
|
}) => {
|
|
56
56
|
const progress = useSharedValue(0);
|
|
57
57
|
useEffect(() => {
|
|
58
|
-
progress.
|
|
58
|
+
progress.set(withTiming(1, {
|
|
59
59
|
duration: ICON_ANIMATION_DURATION,
|
|
60
60
|
easing: Easing.out(Easing.back(1.5))
|
|
61
|
-
});
|
|
61
|
+
}));
|
|
62
62
|
}, [progress]);
|
|
63
63
|
const style = useAnimatedStyle(() => ({
|
|
64
64
|
opacity: progress.value,
|
|
@@ -72,6 +72,9 @@ export function BreadLoaf({
|
|
|
72
72
|
* the main `<BreadLoaf />` won't be visible. Add `<ToastPortal />` inside your
|
|
73
73
|
* modal layouts to show toasts above modal content.
|
|
74
74
|
*
|
|
75
|
+
* This component only renders on Android - it returns `null` on iOS where
|
|
76
|
+
* `<BreadLoaf />` already handles modal overlay via `FullWindowOverlay`.
|
|
77
|
+
*
|
|
75
78
|
* This component only renders toasts - it does not accept configuration.
|
|
76
79
|
* All styling/behavior is inherited from your root `<BreadLoaf />` config.
|
|
77
80
|
*
|
|
@@ -80,19 +83,21 @@ export function BreadLoaf({
|
|
|
80
83
|
* // app/(modal)/_layout.tsx
|
|
81
84
|
* import { Stack } from 'expo-router';
|
|
82
85
|
* import { ToastPortal } from 'react-native-bread';
|
|
83
|
-
* import { Platform } from 'react-native';
|
|
84
86
|
*
|
|
85
87
|
* export default function ModalLayout() {
|
|
86
88
|
* return (
|
|
87
89
|
* <>
|
|
88
90
|
* <Stack screenOptions={{ headerShown: false }} />
|
|
89
|
-
*
|
|
91
|
+
* <ToastPortal />
|
|
90
92
|
* </>
|
|
91
93
|
* );
|
|
92
94
|
* }
|
|
93
95
|
* ```
|
|
94
96
|
*/
|
|
95
97
|
export function ToastPortal() {
|
|
98
|
+
if (Platform.OS !== "android") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
96
101
|
return /*#__PURE__*/_jsx(ToastContent, {});
|
|
97
102
|
}
|
|
98
103
|
const styles = StyleSheet.create({
|
package/lib/module/toast.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { memo, useCallback, useEffect,
|
|
3
|
+
import { memo, useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
5
5
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
6
6
|
import Animated, { interpolate, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
@@ -23,69 +23,69 @@ export const ToastContainer = () => {
|
|
|
23
23
|
theme,
|
|
24
24
|
toastsWithIndex,
|
|
25
25
|
isBottom,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
topToastMutable,
|
|
27
|
+
isBottomMutable,
|
|
28
|
+
isDismissibleMutable
|
|
29
29
|
} = useToastState();
|
|
30
30
|
const shouldDismiss = useSharedValue(false);
|
|
31
|
-
const panGesture =
|
|
31
|
+
const panGesture = Gesture.Pan().onStart(() => {
|
|
32
32
|
"worklet";
|
|
33
33
|
|
|
34
34
|
shouldDismiss.set(false);
|
|
35
35
|
}).onUpdate(event => {
|
|
36
36
|
"worklet";
|
|
37
37
|
|
|
38
|
-
if (!
|
|
39
|
-
const ref =
|
|
38
|
+
if (!isDismissibleMutable.value) return;
|
|
39
|
+
const ref = topToastMutable.value;
|
|
40
40
|
if (!ref) return;
|
|
41
41
|
const {
|
|
42
42
|
slot
|
|
43
43
|
} = ref;
|
|
44
|
-
const bottom =
|
|
44
|
+
const bottom = isBottomMutable.value;
|
|
45
45
|
const rawY = event.translationY;
|
|
46
46
|
const dismissDrag = bottom ? rawY : -rawY;
|
|
47
47
|
const resistDrag = bottom ? -rawY : rawY;
|
|
48
48
|
if (dismissDrag > 0) {
|
|
49
49
|
const clampedY = bottom ? Math.min(rawY, MAX_DRAG_CLAMP) : Math.max(rawY, -MAX_DRAG_CLAMP);
|
|
50
|
-
slot.translationY.
|
|
50
|
+
slot.translationY.set(clampedY);
|
|
51
51
|
const shouldTriggerDismiss = dismissDrag > DISMISS_THRESHOLD || (bottom ? event.velocityY > DISMISS_VELOCITY_THRESHOLD : event.velocityY < -DISMISS_VELOCITY_THRESHOLD);
|
|
52
52
|
shouldDismiss.set(shouldTriggerDismiss);
|
|
53
53
|
} else {
|
|
54
54
|
const exponentialDrag = MAX_DRAG_RESISTANCE * (1 - Math.exp(-resistDrag / 250));
|
|
55
|
-
slot.translationY.
|
|
56
|
-
shouldDismiss.
|
|
55
|
+
slot.translationY.set(bottom ? -Math.min(exponentialDrag, MAX_DRAG_RESISTANCE) : Math.min(exponentialDrag, MAX_DRAG_RESISTANCE));
|
|
56
|
+
shouldDismiss.set(false);
|
|
57
57
|
}
|
|
58
58
|
}).onEnd(() => {
|
|
59
59
|
"worklet";
|
|
60
60
|
|
|
61
|
-
if (!
|
|
62
|
-
const ref =
|
|
61
|
+
if (!isDismissibleMutable.value) return;
|
|
62
|
+
const ref = topToastMutable.value;
|
|
63
63
|
if (!ref) return;
|
|
64
64
|
const {
|
|
65
65
|
slot
|
|
66
66
|
} = ref;
|
|
67
|
-
const bottom =
|
|
67
|
+
const bottom = isBottomMutable.value;
|
|
68
68
|
if (shouldDismiss.value) {
|
|
69
|
-
slot.progress.
|
|
69
|
+
slot.progress.set(withTiming(0, {
|
|
70
70
|
duration: EXIT_DURATION,
|
|
71
71
|
easing: EASING
|
|
72
|
-
});
|
|
72
|
+
}));
|
|
73
73
|
const exitOffset = bottom ? SWIPE_EXIT_OFFSET : -SWIPE_EXIT_OFFSET;
|
|
74
|
-
slot.translationY.
|
|
74
|
+
slot.translationY.set(withTiming(slot.translationY.value + exitOffset, {
|
|
75
75
|
duration: EXIT_DURATION,
|
|
76
76
|
easing: EASING
|
|
77
|
-
});
|
|
77
|
+
}));
|
|
78
78
|
scheduleOnRN(ref.dismiss);
|
|
79
79
|
} else {
|
|
80
|
-
slot.translationY.
|
|
80
|
+
slot.translationY.set(withTiming(0, {
|
|
81
81
|
duration: SPRING_BACK_DURATION,
|
|
82
82
|
easing: EASING
|
|
83
|
-
});
|
|
83
|
+
}));
|
|
84
84
|
}
|
|
85
|
-
})
|
|
85
|
+
});
|
|
86
86
|
const registerTopToast = useCallback(values => {
|
|
87
|
-
|
|
88
|
-
}, [
|
|
87
|
+
topToastMutable.set(values);
|
|
88
|
+
}, [topToastMutable]);
|
|
89
89
|
if (visibleToasts.length === 0) return null;
|
|
90
90
|
const inset = isBottom ? bottom : top;
|
|
91
91
|
const positionStyle = isBottom ? {
|
|
@@ -131,13 +131,13 @@ const ToastItem = ({
|
|
|
131
131
|
|
|
132
132
|
// biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect
|
|
133
133
|
useEffect(() => {
|
|
134
|
-
slot.progress.
|
|
135
|
-
slot.translationY.
|
|
136
|
-
slot.stackIndex.
|
|
137
|
-
slot.progress.
|
|
134
|
+
slot.progress.set(0);
|
|
135
|
+
slot.translationY.set(0);
|
|
136
|
+
slot.stackIndex.set(index);
|
|
137
|
+
slot.progress.set(withTiming(1, {
|
|
138
138
|
duration: ENTRY_DURATION,
|
|
139
139
|
easing: EASING
|
|
140
|
-
});
|
|
140
|
+
}));
|
|
141
141
|
const iconTimeout = setTimeout(() => setShowIcon(true), 50);
|
|
142
142
|
return () => {
|
|
143
143
|
clearTimeout(iconTimeout);
|
|
@@ -151,20 +151,20 @@ const ToastItem = ({
|
|
|
151
151
|
let loadingTimeout = null;
|
|
152
152
|
if (toast.isExiting && !tracker.wasExiting) {
|
|
153
153
|
tracker.wasExiting = true;
|
|
154
|
-
slot.progress.
|
|
154
|
+
slot.progress.set(withTiming(0, {
|
|
155
155
|
duration: EXIT_DURATION,
|
|
156
156
|
easing: EASING
|
|
157
|
-
});
|
|
158
|
-
slot.translationY.
|
|
157
|
+
}));
|
|
158
|
+
slot.translationY.set(withTiming(exitToY, {
|
|
159
159
|
duration: EXIT_DURATION,
|
|
160
160
|
easing: EASING
|
|
161
|
-
});
|
|
161
|
+
}));
|
|
162
162
|
}
|
|
163
163
|
if (tracker.initialized && index !== tracker.prevIndex) {
|
|
164
|
-
slot.stackIndex.
|
|
164
|
+
slot.stackIndex.set(withTiming(index, {
|
|
165
165
|
duration: STACK_TRANSITION_DURATION,
|
|
166
166
|
easing: EASING
|
|
167
|
-
});
|
|
167
|
+
}));
|
|
168
168
|
}
|
|
169
169
|
tracker.prevIndex = index;
|
|
170
170
|
tracker.initialized = true;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo,
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { makeMutable } from "react-native-reanimated";
|
|
5
5
|
import { toastStore } from "./toast-store.js";
|
|
6
6
|
export const useToastState = () => {
|
|
7
7
|
const [visibleToasts, setVisibleToasts] = useState([]);
|
|
8
8
|
const [theme, setTheme] = useState(() => toastStore.getTheme());
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
9
|
+
const [topToastMutable] = useState(() => makeMutable(null));
|
|
10
|
+
const [isBottomMutable] = useState(() => makeMutable(theme.position === "bottom"));
|
|
11
|
+
const [isDismissibleMutable] = useState(() => makeMutable(true));
|
|
12
12
|
const isBottom = theme.position === "bottom";
|
|
13
13
|
const topToast = visibleToasts.find(t => !t.isExiting);
|
|
14
14
|
const isTopDismissible = topToast?.options?.dismissible ?? theme.dismissible;
|
|
@@ -17,8 +17,8 @@ export const useToastState = () => {
|
|
|
17
17
|
const initialTheme = toastStore.getTheme();
|
|
18
18
|
setVisibleToasts(initialToasts);
|
|
19
19
|
const initialTopToast = initialToasts.find(t => !t.isExiting);
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
isBottomMutable.set(initialTheme.position === "bottom");
|
|
21
|
+
isDismissibleMutable.set(initialTopToast?.options?.dismissible ?? initialTheme.dismissible);
|
|
22
22
|
let pendingToasts = null;
|
|
23
23
|
let rafId = null;
|
|
24
24
|
const unsubscribe = toastStore.subscribe(state => {
|
|
@@ -34,13 +34,13 @@ export const useToastState = () => {
|
|
|
34
34
|
rafId = null;
|
|
35
35
|
setTheme(prev => prev === currentTheme ? prev : currentTheme);
|
|
36
36
|
const topToast = currentToasts.find(t => !t.isExiting);
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
isBottomMutable.set(currentTheme.position === "bottom");
|
|
38
|
+
isDismissibleMutable.set(topToast?.options?.dismissible ?? currentTheme.dismissible);
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
return unsubscribe;
|
|
43
|
-
}, []);
|
|
43
|
+
}, [isBottomMutable, isDismissibleMutable]);
|
|
44
44
|
const toastsWithIndex = useMemo(() => {
|
|
45
45
|
const indices = new Map();
|
|
46
46
|
let visualIndex = 0;
|
|
@@ -59,9 +59,9 @@ export const useToastState = () => {
|
|
|
59
59
|
toastsWithIndex,
|
|
60
60
|
isBottom,
|
|
61
61
|
isTopDismissible,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
topToastMutable,
|
|
63
|
+
isBottomMutable,
|
|
64
|
+
isDismissibleMutable
|
|
65
65
|
};
|
|
66
66
|
};
|
|
67
67
|
//# sourceMappingURL=use-toast-state.js.map
|
|
@@ -56,6 +56,9 @@ export declare function BreadLoaf({ config }: BreadLoafProps): import("react/jsx
|
|
|
56
56
|
* the main `<BreadLoaf />` won't be visible. Add `<ToastPortal />` inside your
|
|
57
57
|
* modal layouts to show toasts above modal content.
|
|
58
58
|
*
|
|
59
|
+
* This component only renders on Android - it returns `null` on iOS where
|
|
60
|
+
* `<BreadLoaf />` already handles modal overlay via `FullWindowOverlay`.
|
|
61
|
+
*
|
|
59
62
|
* This component only renders toasts - it does not accept configuration.
|
|
60
63
|
* All styling/behavior is inherited from your root `<BreadLoaf />` config.
|
|
61
64
|
*
|
|
@@ -64,18 +67,17 @@ export declare function BreadLoaf({ config }: BreadLoafProps): import("react/jsx
|
|
|
64
67
|
* // app/(modal)/_layout.tsx
|
|
65
68
|
* import { Stack } from 'expo-router';
|
|
66
69
|
* import { ToastPortal } from 'react-native-bread';
|
|
67
|
-
* import { Platform } from 'react-native';
|
|
68
70
|
*
|
|
69
71
|
* export default function ModalLayout() {
|
|
70
72
|
* return (
|
|
71
73
|
* <>
|
|
72
74
|
* <Stack screenOptions={{ headerShown: false }} />
|
|
73
|
-
*
|
|
75
|
+
* <ToastPortal />
|
|
74
76
|
* </>
|
|
75
77
|
* );
|
|
76
78
|
* }
|
|
77
79
|
* ```
|
|
78
80
|
*/
|
|
79
|
-
export declare function ToastPortal(): import("react/jsx-runtime").JSX.Element;
|
|
81
|
+
export declare function ToastPortal(): import("react/jsx-runtime").JSX.Element | null;
|
|
80
82
|
export {};
|
|
81
83
|
//# sourceMappingURL=toast-provider.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { TextStyle, ViewStyle } from "react-native";
|
|
3
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
4
|
export type ToastType = "success" | "error" | "info" | "loading";
|
|
4
5
|
export type ToastPosition = "top" | "bottom";
|
|
5
6
|
export interface ToastTypeColors {
|
|
@@ -35,7 +36,11 @@ export interface ToastTheme {
|
|
|
35
36
|
position: ToastPosition;
|
|
36
37
|
/** Extra offset from safe area edge (in addition to safe area insets) */
|
|
37
38
|
offset: number;
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Enable right-to-left layout at the code level (reverses icon/text order and text alignment).
|
|
41
|
+
* Only needed when you handle RTL in JavaScript — native RTL (e.g. via `I18nManager.forceRTL`)
|
|
42
|
+
* already flips the entire layout automatically, so this option is unnecessary in that case.
|
|
43
|
+
*/
|
|
39
44
|
rtl: boolean;
|
|
40
45
|
/** Whether to show multiple toasts stacked (default: true). When false, only one toast shows at a time. */
|
|
41
46
|
stacking: boolean;
|
|
@@ -121,15 +126,9 @@ export interface PromiseResult<T> {
|
|
|
121
126
|
}
|
|
122
127
|
export interface TopToastRef {
|
|
123
128
|
slot: {
|
|
124
|
-
progress:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
translationY: {
|
|
128
|
-
value: number;
|
|
129
|
-
};
|
|
130
|
-
stackIndex: {
|
|
131
|
-
value: number;
|
|
132
|
-
};
|
|
129
|
+
progress: SharedValue<number>;
|
|
130
|
+
translationY: SharedValue<number>;
|
|
131
|
+
stackIndex: SharedValue<number>;
|
|
133
132
|
};
|
|
134
133
|
dismiss: () => void;
|
|
135
134
|
}
|
|
@@ -9,8 +9,8 @@ export declare const useToastState: () => {
|
|
|
9
9
|
}[];
|
|
10
10
|
isBottom: boolean;
|
|
11
11
|
isTopDismissible: boolean;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
topToastMutable: import("react-native-reanimated/lib/typescript/commonTypes").Mutable<TopToastRef | null>;
|
|
13
|
+
isBottomMutable: import("react-native-reanimated/lib/typescript/commonTypes").Mutable<boolean>;
|
|
14
|
+
isDismissibleMutable: import("react-native-reanimated/lib/typescript/commonTypes").Mutable<boolean>;
|
|
15
15
|
};
|
|
16
16
|
//# sourceMappingURL=use-toast-state.d.ts.map
|