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 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** - perfect for Arabic and other RTL languages
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, // Enable for RTL languages
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**, add `<ToastPortal />` inside your modal layouts:
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
- {Platform.OS === "android" && <ToastPortal />}
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.value = (0, _reactNativeReanimated.withTiming)(1, {
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
- * {Platform.OS === 'android' && <ToastPortal />}
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({
@@ -28,69 +28,69 @@ const ToastContainer = () => {
28
28
  theme,
29
29
  toastsWithIndex,
30
30
  isBottom,
31
- topToastRef,
32
- isBottomRef,
33
- isDismissibleRef
31
+ topToastMutable,
32
+ isBottomMutable,
33
+ isDismissibleMutable
34
34
  } = (0, _useToastState.useToastState)();
35
35
  const shouldDismiss = (0, _reactNativeReanimated.useSharedValue)(false);
36
- const panGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Pan().onStart(() => {
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 (!isDismissibleRef.current.value) return;
44
- const ref = topToastRef.current.value;
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 = isBottomRef.current.value;
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.value = clampedY;
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.value = bottom ? -Math.min(exponentialDrag, _constants.MAX_DRAG_RESISTANCE) : Math.min(exponentialDrag, _constants.MAX_DRAG_RESISTANCE);
61
- shouldDismiss.value = false;
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 (!isDismissibleRef.current.value) return;
67
- const ref = topToastRef.current.value;
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 = isBottomRef.current.value;
72
+ const bottom = isBottomMutable.value;
73
73
  if (shouldDismiss.value) {
74
- slot.progress.value = (0, _reactNativeReanimated.withTiming)(0, {
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.value = (0, _reactNativeReanimated.withTiming)(slot.translationY.value + exitOffset, {
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.value = (0, _reactNativeReanimated.withTiming)(0, {
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
- }), [shouldDismiss, isDismissibleRef, topToastRef, isBottomRef]);
90
+ });
91
91
  const registerTopToast = (0, _react.useCallback)(values => {
92
- topToastRef.current.value = values;
93
- }, [topToastRef]);
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.value = 0;
141
- slot.translationY.value = 0;
142
- slot.stackIndex.value = index;
143
- slot.progress.value = (0, _reactNativeReanimated.withTiming)(1, {
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.value = (0, _reactNativeReanimated.withTiming)(0, {
160
+ slot.progress.set((0, _reactNativeReanimated.withTiming)(0, {
161
161
  duration: _constants.EXIT_DURATION,
162
162
  easing: _constants.EASING
163
- });
164
- slot.translationY.value = (0, _reactNativeReanimated.withTiming)(exitToY, {
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.value = (0, _reactNativeReanimated.withTiming)(index, {
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 topToastRef = (0, _react.useRef)((0, _reactNativeReanimated.makeMutable)(null));
14
- const isBottomRef = (0, _react.useRef)((0, _reactNativeReanimated.makeMutable)(theme.position === "bottom"));
15
- const isDismissibleRef = (0, _react.useRef)((0, _reactNativeReanimated.makeMutable)(true));
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
- isBottomRef.current.value = initialTheme.position === "bottom";
25
- isDismissibleRef.current.value = initialTopToast?.options?.dismissible ?? initialTheme.dismissible;
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
- isBottomRef.current.value = currentTheme.position === "bottom";
42
- isDismissibleRef.current.value = topToast?.options?.dismissible ?? currentTheme.dismissible;
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
- topToastRef,
67
- isBottomRef,
68
- isDismissibleRef
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.value = withTiming(1, {
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
- * {Platform.OS === 'android' && <ToastPortal />}
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({
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { memo, useCallback, useEffect, useMemo, useState } from "react";
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
- topToastRef,
27
- isBottomRef,
28
- isDismissibleRef
26
+ topToastMutable,
27
+ isBottomMutable,
28
+ isDismissibleMutable
29
29
  } = useToastState();
30
30
  const shouldDismiss = useSharedValue(false);
31
- const panGesture = useMemo(() => Gesture.Pan().onStart(() => {
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 (!isDismissibleRef.current.value) return;
39
- const ref = topToastRef.current.value;
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 = isBottomRef.current.value;
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.value = clampedY;
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.value = bottom ? -Math.min(exponentialDrag, MAX_DRAG_RESISTANCE) : Math.min(exponentialDrag, MAX_DRAG_RESISTANCE);
56
- shouldDismiss.value = false;
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 (!isDismissibleRef.current.value) return;
62
- const ref = topToastRef.current.value;
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 = isBottomRef.current.value;
67
+ const bottom = isBottomMutable.value;
68
68
  if (shouldDismiss.value) {
69
- slot.progress.value = withTiming(0, {
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.value = withTiming(slot.translationY.value + exitOffset, {
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.value = withTiming(0, {
80
+ slot.translationY.set(withTiming(0, {
81
81
  duration: SPRING_BACK_DURATION,
82
82
  easing: EASING
83
- });
83
+ }));
84
84
  }
85
- }), [shouldDismiss, isDismissibleRef, topToastRef, isBottomRef]);
85
+ });
86
86
  const registerTopToast = useCallback(values => {
87
- topToastRef.current.value = values;
88
- }, [topToastRef]);
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.value = 0;
135
- slot.translationY.value = 0;
136
- slot.stackIndex.value = index;
137
- slot.progress.value = withTiming(1, {
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.value = withTiming(0, {
154
+ slot.progress.set(withTiming(0, {
155
155
  duration: EXIT_DURATION,
156
156
  easing: EASING
157
- });
158
- slot.translationY.value = withTiming(exitToY, {
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.value = withTiming(index, {
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, useRef, useState } from "react";
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 topToastRef = useRef(makeMutable(null));
10
- const isBottomRef = useRef(makeMutable(theme.position === "bottom"));
11
- const isDismissibleRef = useRef(makeMutable(true));
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
- isBottomRef.current.value = initialTheme.position === "bottom";
21
- isDismissibleRef.current.value = initialTopToast?.options?.dismissible ?? initialTheme.dismissible;
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
- isBottomRef.current.value = currentTheme.position === "bottom";
38
- isDismissibleRef.current.value = topToast?.options?.dismissible ?? currentTheme.dismissible;
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
- topToastRef,
63
- isBottomRef,
64
- isDismissibleRef
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
- * {Platform.OS === 'android' && <ToastPortal />}
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
- /** Enable right-to-left layout (reverses icon/text order) */
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
- value: number;
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
- topToastRef: import("react").RefObject<import("react-native-reanimated/lib/typescript/commonTypes").Mutable<TopToastRef | null>>;
13
- isBottomRef: import("react").RefObject<import("react-native-reanimated/lib/typescript/commonTypes").Mutable<boolean>>;
14
- isDismissibleRef: import("react").RefObject<import("react-native-reanimated/lib/typescript/commonTypes").Mutable<boolean>>;
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