react-native-bread 0.5.2 → 0.6.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.
@@ -1 +1,345 @@
1
- "use strict";import{memo as t,useCallback as e,useEffect as o,useMemo as i,useState as n}from"react";import{Pressable as s,StyleSheet as a,Text as r,View as l}from"react-native";import{Gesture as c,GestureDetector as u}from"react-native-gesture-handler";import d,{interpolate as p,useAnimatedStyle as m,useSharedValue as g,withTiming as h}from"react-native-reanimated";import{useSafeAreaInsets as f}from"react-native-safe-area-context";import{scheduleOnRN as v}from"react-native-worklets";import{DISMISS_THRESHOLD as x,DISMISS_VELOCITY_THRESHOLD as y,EASING as T,ENTRY_DURATION as w,ENTRY_OFFSET as C,EXIT_DURATION as b,EXIT_OFFSET as Y,ICON_ANIMATION_DURATION as k,MAX_DRAG_CLAMP as I,MAX_DRAG_RESISTANCE as S,SPRING_BACK_DURATION as j,STACK_OFFSET_PER_ITEM as z,STACK_SCALE_PER_ITEM as B,STACK_TRANSITION_DURATION as E,SWIPE_EXIT_OFFSET as M}from"./constants.js";import{CloseIcon as H}from"./icons/index.js";import{animationPool as R,getSlotIndex as F,releaseSlot as V,slotTrackers as D}from"./pool.js";import{AnimatedIcon as W,resolveIcon as A}from"./toast-icons.js";import{toastStore as O}from"./toast-store.js";import{useToastState as P}from"./use-toast-state.js";import{jsx as L,jsxs as U}from"react/jsx-runtime";export const ToastContainer=()=>{const{top:t,bottom:o}=f(),{visibleToasts:n,theme:s,toastsWithIndex:a,isBottom:r,topToastRef:d,isBottomRef:p,isDismissibleRef:m}=P(),w=g(!1),C=i(()=>c.Pan().onStart(()=>{"worklet";w.set(!1)}).onUpdate(t=>{"worklet";if(!m.current.value)return;const e=d.current.value;if(!e)return;const{slot:o}=e,i=p.current.value,n=t.translationY,s=i?n:-n,a=i?-n:n;if(s>0){const e=i?Math.min(n,I):Math.max(n,-I);o.translationY.value=e;const a=s>x||(i?t.velocityY>y:t.velocityY<-y);w.set(a)}else{const t=S*(1-Math.exp(-a/250));o.translationY.value=i?-Math.min(t,S):Math.min(t,S),w.value=!1}}).onEnd(()=>{"worklet";if(!m.current.value)return;const t=d.current.value;if(!t)return;const{slot:e}=t,o=p.current.value;if(w.value){e.progress.value=h(0,{duration:b,easing:T});const i=o?M:-M;e.translationY.value=h(e.translationY.value+i,{duration:b,easing:T}),v(t.dismiss)}else e.translationY.value=h(0,{duration:j,easing:T})}),[w,m,d,p]),Y=e(t=>{d.current.value=t},[d]);if(0===n.length)return null;const k=r?o:t,z=r?{bottom:k+s.offset+2}:{top:k+s.offset+2};return L(u,{gesture:C,children:L(l,{style:[G.container,z],pointerEvents:"box-none",children:a.map(({toast:t,index:e})=>L(q,{toast:t,index:e,theme:s,position:s.position,isTopToast:0===e,registerTopToast:Y},t.id))})})};const q=t(({toast:t,index:i,theme:a,position:c,isTopToast:u,registerTopToast:g})=>{const[f]=n(()=>F(t.id)),v=R[f],x=D[f],y="bottom"===c,I=y?C:-C,S=y?Y:-Y,[j,M]=n("loading"===t.type),[P,q]=n(!1);o(()=>{v.progress.value=0,v.translationY.value=0,v.stackIndex.value=i,v.progress.value=h(1,{duration:w,easing:T});const e=setTimeout(()=>q(!0),50);return()=>{clearTimeout(e),V(t.id)}},[]);const J=e(()=>{O.hide(t.id)},[t.id]);o(()=>{let e=null;return t.isExiting&&!x.wasExiting&&(x.wasExiting=!0,v.progress.value=h(0,{duration:b,easing:T}),v.translationY.value=h(S,{duration:b,easing:T})),x.initialized&&i!==x.prevIndex&&(v.stackIndex.value=h(i,{duration:E,easing:T})),x.prevIndex=i,x.initialized=!0,"loading"===t.type?M(!0):j&&(e=setTimeout(()=>M(!1),k+50)),u&&g({slot:v,dismiss:J}),()=>{e&&clearTimeout(e),u&&g(null)}},[t.isExiting,i,v,x,S,t.type,j,u,g,J]);const K=j&&"loading"!==t.type,N=m(()=>{const t=p(v.progress.value,[0,1],[I,0]),e=y?v.stackIndex.value*z:v.stackIndex.value*-z,o=1-v.stackIndex.value*B,i=t+v.translationY.value+e,n=p(v.progress.value,[0,1],[0,1]),s=y?v.translationY.value:-v.translationY.value,a=n*(s>0?p(s,[0,130],[1,0],"clamp"):1);return{transform:[{translateY:i},{scale:o*p(Math.abs(v.translationY.value),[0,50],[1,.98],"clamp")}],opacity:a,zIndex:1e3-Math.round(v.stackIndex.value)}}),{options:Q}=t,X=a.colors[t.type];if(void 0!==Q?.customContent){const e=Q.customContent;return L(d.View,{style:[G.toast,G.customContentToast,y?G.toastBottom:G.toastTop,{backgroundColor:X.background},a.toastStyle,Q.style,N],children:"function"==typeof e?e({id:t.id,dismiss:J,type:t.type,isExiting:!!t.isExiting}):e})}const Z="loading"!==t.type&&(Q?.showCloseButton??a.showCloseButton);return U(d.View,{style:[G.toast,y?G.toastBottom:G.toastTop,{backgroundColor:X.background},a.rtl&&G.rtl,a.toastStyle,Q?.style,N],children:[L(l,{style:G.iconContainer,children:P&&(K?L(W,{type:t.type,color:X.accent,custom:Q?.icon,config:a.icons[t.type]},t.type):A(t.type,X.accent,Q?.icon,a.icons[t.type]))}),U(l,{style:G.textContainer,children:[L(r,{maxFontSizeMultiplier:1.35,allowFontScaling:!1,style:[G.title,{color:X.accent},a.rtl&&{textAlign:"right"},a.titleStyle,Q?.titleStyle],children:t.title}),t.description&&L(r,{allowFontScaling:!1,maxFontSizeMultiplier:1.35,style:[G.description,a.rtl&&{textAlign:"right"},a.descriptionStyle,Q?.descriptionStyle],children:t.description})]}),Z&&L(s,{style:G.closeButton,onPress:J,hitSlop:12,children:L(H,{width:20,height:20})})]})},(t,e)=>t.toast.id===e.toast.id&&t.toast.type===e.toast.type&&t.toast.title===e.toast.title&&t.toast.description===e.toast.description&&t.toast.isExiting===e.toast.isExiting&&t.index===e.index&&t.position===e.position&&t.theme===e.theme&&t.isTopToast===e.isTopToast),G=a.create({container:{position:"absolute",left:16,right:16,zIndex:1e3},toast:{flexDirection:"row",alignItems:"center",gap:12,minHeight:36,borderRadius:20,borderCurve:"continuous",position:"absolute",left:0,right:0,paddingHorizontal:12,paddingVertical:10,shadowColor:"#000",shadowOffset:{width:0,height:8},shadowOpacity:.05,shadowRadius:24,elevation:8},customContentToast:{padding:0,paddingHorizontal:0,paddingVertical:0,overflow:"hidden"},rtl:{flexDirection:"row-reverse"},toastTop:{top:0},toastBottom:{bottom:0},iconContainer:{width:48,height:48,alignItems:"center",justifyContent:"center",marginLeft:8},textContainer:{flex:1,gap:1,justifyContent:"center"},title:{fontSize:14,fontWeight:"700",lineHeight:20},description:{color:"#6B7280",fontSize:12,fontWeight:"500",lineHeight:16},closeButton:{padding:4,alignItems:"center",justifyContent:"center"}});
1
+ "use strict";
2
+
3
+ import { memo, useCallback, useEffect, useMemo, useState } from "react";
4
+ import { Pressable, StyleSheet, Text, View } from "react-native";
5
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
6
+ import Animated, { interpolate, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
7
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
8
+ import { scheduleOnRN } from "react-native-worklets";
9
+ import { DISMISS_THRESHOLD, DISMISS_VELOCITY_THRESHOLD, EASING, ENTRY_DURATION, ENTRY_OFFSET, EXIT_DURATION, EXIT_OFFSET, ICON_ANIMATION_DURATION, MAX_DRAG_CLAMP, MAX_DRAG_RESISTANCE, SPRING_BACK_DURATION, STACK_OFFSET_PER_ITEM, STACK_SCALE_PER_ITEM, STACK_TRANSITION_DURATION, SWIPE_EXIT_OFFSET } from "./constants.js";
10
+ import { CloseIcon } from "./icons/index.js";
11
+ import { animationPool, getSlotIndex, releaseSlot, slotTrackers } from "./pool.js";
12
+ import { AnimatedIcon, resolveIcon } from "./toast-icons.js";
13
+ import { toastStore } from "./toast-store.js";
14
+ import { useToastState } from "./use-toast-state.js";
15
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
+ export const ToastContainer = () => {
17
+ const {
18
+ top,
19
+ bottom
20
+ } = useSafeAreaInsets();
21
+ const {
22
+ visibleToasts,
23
+ theme,
24
+ toastsWithIndex,
25
+ isBottom,
26
+ topToastRef,
27
+ isBottomRef,
28
+ isDismissibleRef
29
+ } = useToastState();
30
+ const shouldDismiss = useSharedValue(false);
31
+ const panGesture = useMemo(() => Gesture.Pan().onStart(() => {
32
+ "worklet";
33
+
34
+ shouldDismiss.set(false);
35
+ }).onUpdate(event => {
36
+ "worklet";
37
+
38
+ if (!isDismissibleRef.current.value) return;
39
+ const ref = topToastRef.current.value;
40
+ if (!ref) return;
41
+ const {
42
+ slot
43
+ } = ref;
44
+ const bottom = isBottomRef.current.value;
45
+ const rawY = event.translationY;
46
+ const dismissDrag = bottom ? rawY : -rawY;
47
+ const resistDrag = bottom ? -rawY : rawY;
48
+ if (dismissDrag > 0) {
49
+ const clampedY = bottom ? Math.min(rawY, MAX_DRAG_CLAMP) : Math.max(rawY, -MAX_DRAG_CLAMP);
50
+ slot.translationY.value = clampedY;
51
+ const shouldTriggerDismiss = dismissDrag > DISMISS_THRESHOLD || (bottom ? event.velocityY > DISMISS_VELOCITY_THRESHOLD : event.velocityY < -DISMISS_VELOCITY_THRESHOLD);
52
+ shouldDismiss.set(shouldTriggerDismiss);
53
+ } else {
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;
57
+ }
58
+ }).onEnd(() => {
59
+ "worklet";
60
+
61
+ if (!isDismissibleRef.current.value) return;
62
+ const ref = topToastRef.current.value;
63
+ if (!ref) return;
64
+ const {
65
+ slot
66
+ } = ref;
67
+ const bottom = isBottomRef.current.value;
68
+ if (shouldDismiss.value) {
69
+ slot.progress.value = withTiming(0, {
70
+ duration: EXIT_DURATION,
71
+ easing: EASING
72
+ });
73
+ const exitOffset = bottom ? SWIPE_EXIT_OFFSET : -SWIPE_EXIT_OFFSET;
74
+ slot.translationY.value = withTiming(slot.translationY.value + exitOffset, {
75
+ duration: EXIT_DURATION,
76
+ easing: EASING
77
+ });
78
+ scheduleOnRN(ref.dismiss);
79
+ } else {
80
+ slot.translationY.value = withTiming(0, {
81
+ duration: SPRING_BACK_DURATION,
82
+ easing: EASING
83
+ });
84
+ }
85
+ }), [shouldDismiss, isDismissibleRef, topToastRef, isBottomRef]);
86
+ const registerTopToast = useCallback(values => {
87
+ topToastRef.current.value = values;
88
+ }, [topToastRef]);
89
+ if (visibleToasts.length === 0) return null;
90
+ const inset = isBottom ? bottom : top;
91
+ const positionStyle = isBottom ? {
92
+ bottom: inset + theme.offset + 2
93
+ } : {
94
+ top: inset + theme.offset + 2
95
+ };
96
+ return /*#__PURE__*/_jsx(GestureDetector, {
97
+ gesture: panGesture,
98
+ children: /*#__PURE__*/_jsx(View, {
99
+ style: [styles.container, positionStyle],
100
+ pointerEvents: "box-none",
101
+ children: toastsWithIndex.map(({
102
+ toast,
103
+ index
104
+ }) => /*#__PURE__*/_jsx(MemoizedToastItem, {
105
+ toast: toast,
106
+ index: index,
107
+ theme: theme,
108
+ position: theme.position,
109
+ isTopToast: index === 0,
110
+ registerTopToast: registerTopToast
111
+ }, toast.id))
112
+ })
113
+ });
114
+ };
115
+ const ToastItem = ({
116
+ toast,
117
+ index,
118
+ theme,
119
+ position,
120
+ isTopToast,
121
+ registerTopToast
122
+ }) => {
123
+ const [slotIdx] = useState(() => getSlotIndex(toast.id));
124
+ const slot = animationPool[slotIdx];
125
+ const tracker = slotTrackers[slotIdx];
126
+ const isBottom = position === "bottom";
127
+ const entryFromY = isBottom ? ENTRY_OFFSET : -ENTRY_OFFSET;
128
+ const exitToY = isBottom ? EXIT_OFFSET : -EXIT_OFFSET;
129
+ const [wasLoading, setWasLoading] = useState(toast.type === "loading");
130
+ const [showIcon, setShowIcon] = useState(false);
131
+
132
+ // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect
133
+ useEffect(() => {
134
+ slot.progress.value = 0;
135
+ slot.translationY.value = 0;
136
+ slot.stackIndex.value = index;
137
+ slot.progress.value = withTiming(1, {
138
+ duration: ENTRY_DURATION,
139
+ easing: EASING
140
+ });
141
+ const iconTimeout = setTimeout(() => setShowIcon(true), 50);
142
+ return () => {
143
+ clearTimeout(iconTimeout);
144
+ releaseSlot(toast.id);
145
+ };
146
+ }, []);
147
+ const dismissToast = useCallback(() => {
148
+ toastStore.hide(toast.id);
149
+ }, [toast.id]);
150
+ useEffect(() => {
151
+ let loadingTimeout = null;
152
+ if (toast.isExiting && !tracker.wasExiting) {
153
+ tracker.wasExiting = true;
154
+ slot.progress.value = withTiming(0, {
155
+ duration: EXIT_DURATION,
156
+ easing: EASING
157
+ });
158
+ slot.translationY.value = withTiming(exitToY, {
159
+ duration: EXIT_DURATION,
160
+ easing: EASING
161
+ });
162
+ }
163
+ if (tracker.initialized && index !== tracker.prevIndex) {
164
+ slot.stackIndex.value = withTiming(index, {
165
+ duration: STACK_TRANSITION_DURATION,
166
+ easing: EASING
167
+ });
168
+ }
169
+ tracker.prevIndex = index;
170
+ tracker.initialized = true;
171
+ if (toast.type === "loading") {
172
+ setWasLoading(true);
173
+ } else if (wasLoading) {
174
+ loadingTimeout = setTimeout(() => setWasLoading(false), ICON_ANIMATION_DURATION + 50);
175
+ }
176
+ if (isTopToast) {
177
+ registerTopToast({
178
+ slot: slot,
179
+ dismiss: dismissToast
180
+ });
181
+ }
182
+ return () => {
183
+ if (loadingTimeout) clearTimeout(loadingTimeout);
184
+ if (isTopToast) registerTopToast(null);
185
+ };
186
+ }, [toast.isExiting, index, slot, tracker, exitToY, toast.type, wasLoading, isTopToast, registerTopToast, dismissToast]);
187
+ const shouldAnimateIcon = wasLoading && toast.type !== "loading";
188
+ const animatedStyle = useAnimatedStyle(() => {
189
+ const baseTranslateY = interpolate(slot.progress.value, [0, 1], [entryFromY, 0]);
190
+ const stackOffsetY = isBottom ? slot.stackIndex.value * STACK_OFFSET_PER_ITEM : slot.stackIndex.value * -STACK_OFFSET_PER_ITEM;
191
+ const stackScale = 1 - slot.stackIndex.value * STACK_SCALE_PER_ITEM;
192
+ const finalTranslateY = baseTranslateY + slot.translationY.value + stackOffsetY;
193
+ const progressOpacity = interpolate(slot.progress.value, [0, 1], [0, 1]);
194
+ const dismissDirection = isBottom ? slot.translationY.value : -slot.translationY.value;
195
+ const dragOpacity = dismissDirection > 0 ? interpolate(dismissDirection, [0, 130], [1, 0], "clamp") : 1;
196
+ const opacity = progressOpacity * dragOpacity;
197
+ const dragScale = interpolate(Math.abs(slot.translationY.value), [0, 50], [1, 0.98], "clamp");
198
+ const scale = stackScale * dragScale;
199
+ return {
200
+ transform: [{
201
+ translateY: finalTranslateY
202
+ }, {
203
+ scale
204
+ }],
205
+ opacity,
206
+ zIndex: 1000 - Math.round(slot.stackIndex.value)
207
+ };
208
+ });
209
+ const {
210
+ options
211
+ } = toast;
212
+ const colors = theme.colors[toast.type];
213
+ if (options?.customContent !== undefined) {
214
+ const content = options.customContent;
215
+ return /*#__PURE__*/_jsx(Animated.View, {
216
+ style: [styles.toast, styles.customContentToast, isBottom ? styles.toastBottom : styles.toastTop, {
217
+ backgroundColor: colors.background
218
+ }, theme.toastStyle, options.style, animatedStyle],
219
+ children: typeof content === "function" ? content({
220
+ id: toast.id,
221
+ dismiss: dismissToast,
222
+ type: toast.type,
223
+ isExiting: !!toast.isExiting
224
+ }) : content
225
+ });
226
+ }
227
+ const shouldShowCloseButton = toast.type !== "loading" && (options?.showCloseButton ?? theme.showCloseButton);
228
+ return /*#__PURE__*/_jsxs(Animated.View, {
229
+ style: [styles.toast, isBottom ? styles.toastBottom : styles.toastTop, {
230
+ backgroundColor: colors.background
231
+ }, theme.rtl && styles.rtl, theme.toastStyle, options?.style, animatedStyle],
232
+ children: [/*#__PURE__*/_jsx(View, {
233
+ style: styles.iconContainer,
234
+ children: showIcon && (shouldAnimateIcon ? /*#__PURE__*/_jsx(AnimatedIcon, {
235
+ type: toast.type,
236
+ color: colors.accent,
237
+ custom: options?.icon,
238
+ config: theme.icons[toast.type]
239
+ }, toast.type) : resolveIcon(toast.type, colors.accent, options?.icon, theme.icons[toast.type]))
240
+ }), /*#__PURE__*/_jsxs(View, {
241
+ style: styles.textContainer,
242
+ children: [/*#__PURE__*/_jsx(Text, {
243
+ maxFontSizeMultiplier: 1.35,
244
+ allowFontScaling: false,
245
+ style: [styles.title, {
246
+ color: colors.accent
247
+ }, theme.rtl && {
248
+ textAlign: "right"
249
+ }, theme.titleStyle, options?.titleStyle],
250
+ children: toast.title
251
+ }), toast.description && /*#__PURE__*/_jsx(Text, {
252
+ allowFontScaling: false,
253
+ maxFontSizeMultiplier: 1.35,
254
+ style: [styles.description, theme.rtl && {
255
+ textAlign: "right"
256
+ }, theme.descriptionStyle, options?.descriptionStyle],
257
+ children: toast.description
258
+ })]
259
+ }), shouldShowCloseButton && /*#__PURE__*/_jsx(Pressable, {
260
+ style: styles.closeButton,
261
+ onPress: dismissToast,
262
+ hitSlop: 12,
263
+ children: /*#__PURE__*/_jsx(CloseIcon, {
264
+ width: 20,
265
+ height: 20
266
+ })
267
+ })]
268
+ });
269
+ };
270
+ const MemoizedToastItem = /*#__PURE__*/memo(ToastItem, (prev, next) => {
271
+ return prev.toast.id === next.toast.id && prev.toast.type === next.toast.type && prev.toast.title === next.toast.title && prev.toast.description === next.toast.description && prev.toast.isExiting === next.toast.isExiting && prev.index === next.index && prev.position === next.position && prev.theme === next.theme && prev.isTopToast === next.isTopToast;
272
+ });
273
+ const styles = StyleSheet.create({
274
+ container: {
275
+ position: "absolute",
276
+ left: 16,
277
+ right: 16,
278
+ zIndex: 1000
279
+ },
280
+ toast: {
281
+ flexDirection: "row",
282
+ alignItems: "center",
283
+ gap: 12,
284
+ minHeight: 36,
285
+ borderRadius: 20,
286
+ borderCurve: "continuous",
287
+ position: "absolute",
288
+ left: 0,
289
+ right: 0,
290
+ paddingHorizontal: 12,
291
+ paddingVertical: 10,
292
+ shadowColor: "#000",
293
+ shadowOffset: {
294
+ width: 0,
295
+ height: 8
296
+ },
297
+ shadowOpacity: 0.05,
298
+ shadowRadius: 24,
299
+ elevation: 8
300
+ },
301
+ customContentToast: {
302
+ padding: 0,
303
+ paddingHorizontal: 0,
304
+ paddingVertical: 0,
305
+ overflow: "hidden"
306
+ },
307
+ rtl: {
308
+ flexDirection: "row-reverse"
309
+ },
310
+ toastTop: {
311
+ top: 0
312
+ },
313
+ toastBottom: {
314
+ bottom: 0
315
+ },
316
+ iconContainer: {
317
+ width: 48,
318
+ height: 48,
319
+ alignItems: "center",
320
+ justifyContent: "center",
321
+ marginLeft: 8
322
+ },
323
+ textContainer: {
324
+ flex: 1,
325
+ gap: 1,
326
+ justifyContent: "center"
327
+ },
328
+ title: {
329
+ fontSize: 14,
330
+ fontWeight: "700",
331
+ lineHeight: 20
332
+ },
333
+ description: {
334
+ color: "#6B7280",
335
+ fontSize: 12,
336
+ fontWeight: "500",
337
+ lineHeight: 16
338
+ },
339
+ closeButton: {
340
+ padding: 4,
341
+ alignItems: "center",
342
+ justifyContent: "center"
343
+ }
344
+ });
345
+ //# sourceMappingURL=toast.js.map
@@ -1 +1,4 @@
1
- "use strict";export{};
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -1 +1,67 @@
1
- "use strict";import{useEffect as t,useMemo as i,useRef as s,useState as e}from"react";import{makeMutable as o}from"react-native-reanimated";import{toastStore as n}from"./toast-store.js";export const useToastState=()=>{const[r,l]=e([]),[m,a]=e(()=>n.getTheme()),u=s(o(null)),b=s(o("bottom"===m.position)),c=s(o(!0)),p="bottom"===m.position,d=r.find(t=>!t.isExiting),f=d?.options?.dismissible??m.dismissible;t(()=>{const t=n.getState().visibleToasts,i=n.getTheme();l(t);const s=t.find(t=>!t.isExiting);b.current.value="bottom"===i.position,c.current.value=s?.options?.dismissible??i.dismissible;let e=null,o=null;const r=n.subscribe(t=>{e=t.visibleToasts,null===o&&(o=requestAnimationFrame(()=>{const t=e??n.getState().visibleToasts,i=n.getTheme();e&&(l(e),e=null),o=null,a(t=>t===i?t:i);const s=t.find(t=>!t.isExiting);b.current.value="bottom"===i.position,c.current.value=s?.options?.dismissible??i.dismissible}))});return r},[]);const g=i(()=>{const t=new Map;let i=0;for(const s of r)t.set(s.id,s.isExiting?-1:i),s.isExiting||i++;return[...r].reverse().map(i=>({toast:i,index:t.get(i.id)??0}))},[r]);return{visibleToasts:r,theme:m,toastsWithIndex:g,isBottom:p,isTopDismissible:f,topToastRef:u,isBottomRef:b,isDismissibleRef:c}};
1
+ "use strict";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { makeMutable } from "react-native-reanimated";
5
+ import { toastStore } from "./toast-store.js";
6
+ export const useToastState = () => {
7
+ const [visibleToasts, setVisibleToasts] = useState([]);
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));
12
+ const isBottom = theme.position === "bottom";
13
+ const topToast = visibleToasts.find(t => !t.isExiting);
14
+ const isTopDismissible = topToast?.options?.dismissible ?? theme.dismissible;
15
+ useEffect(() => {
16
+ const initialToasts = toastStore.getState().visibleToasts;
17
+ const initialTheme = toastStore.getTheme();
18
+ setVisibleToasts(initialToasts);
19
+ const initialTopToast = initialToasts.find(t => !t.isExiting);
20
+ isBottomRef.current.value = initialTheme.position === "bottom";
21
+ isDismissibleRef.current.value = initialTopToast?.options?.dismissible ?? initialTheme.dismissible;
22
+ let pendingToasts = null;
23
+ let rafId = null;
24
+ const unsubscribe = toastStore.subscribe(state => {
25
+ pendingToasts = state.visibleToasts;
26
+ if (rafId === null) {
27
+ rafId = requestAnimationFrame(() => {
28
+ const currentToasts = pendingToasts ?? toastStore.getState().visibleToasts;
29
+ const currentTheme = toastStore.getTheme();
30
+ if (pendingToasts) {
31
+ setVisibleToasts(pendingToasts);
32
+ pendingToasts = null;
33
+ }
34
+ rafId = null;
35
+ setTheme(prev => prev === currentTheme ? prev : currentTheme);
36
+ const topToast = currentToasts.find(t => !t.isExiting);
37
+ isBottomRef.current.value = currentTheme.position === "bottom";
38
+ isDismissibleRef.current.value = topToast?.options?.dismissible ?? currentTheme.dismissible;
39
+ });
40
+ }
41
+ });
42
+ return unsubscribe;
43
+ }, []);
44
+ const toastsWithIndex = useMemo(() => {
45
+ const indices = new Map();
46
+ let visualIndex = 0;
47
+ for (const t of visibleToasts) {
48
+ indices.set(t.id, t.isExiting ? -1 : visualIndex);
49
+ if (!t.isExiting) visualIndex++;
50
+ }
51
+ return [...visibleToasts].reverse().map(t => ({
52
+ toast: t,
53
+ index: indices.get(t.id) ?? 0
54
+ }));
55
+ }, [visibleToasts]);
56
+ return {
57
+ visibleToasts,
58
+ theme,
59
+ toastsWithIndex,
60
+ isBottom,
61
+ isTopDismissible,
62
+ topToastRef,
63
+ isBottomRef,
64
+ isDismissibleRef
65
+ };
66
+ };
67
+ //# sourceMappingURL=use-toast-state.js.map
@@ -1,7 +1,7 @@
1
1
  export { CloseIcon, GreenCheck, InfoIcon, RedX } from "./icons";
2
2
  export { ToastContainer } from "./toast";
3
3
  export { toast } from "./toast-api";
4
- export { BreadLoaf } from "./toast-provider";
4
+ export { BreadLoaf, ToastPortal } from "./toast-provider";
5
5
  export { toastStore } from "./toast-store";
6
6
  export type { CustomContentProps, CustomContentRenderFn, ErrorMessageInput, IconProps, IconRenderFn, MessageInput, PromiseMessages, PromiseResult, Toast, ToastConfig, ToastOptions, ToastPosition, ToastState, ToastType, ToastTypeColors, } from "./types";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -49,5 +49,33 @@ interface BreadLoafProps {
49
49
  * ```
50
50
  */
51
51
  export declare function BreadLoaf({ config }: BreadLoafProps): import("react/jsx-runtime").JSX.Element;
52
+ /**
53
+ * Lightweight toast renderer for use inside modal screens.
54
+ *
55
+ * On Android, native modals render above the root React view, so toasts from
56
+ * the main `<BreadLoaf />` won't be visible. Add `<ToastPortal />` inside your
57
+ * modal layouts to show toasts above modal content.
58
+ *
59
+ * This component only renders toasts - it does not accept configuration.
60
+ * All styling/behavior is inherited from your root `<BreadLoaf />` config.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * // app/(modal)/_layout.tsx
65
+ * import { Stack } from 'expo-router';
66
+ * import { ToastPortal } from 'react-native-bread';
67
+ * import { Platform } from 'react-native';
68
+ *
69
+ * export default function ModalLayout() {
70
+ * return (
71
+ * <>
72
+ * <Stack screenOptions={{ headerShown: false }} />
73
+ * {Platform.OS === 'android' && <ToastPortal />}
74
+ * </>
75
+ * );
76
+ * }
77
+ * ```
78
+ */
79
+ export declare function ToastPortal(): import("react/jsx-runtime").JSX.Element;
52
80
  export {};
53
81
  //# sourceMappingURL=toast-provider.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-bread",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "A lightweight toast library for React Native with premium feeling animations and complex gesture support",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -32,10 +32,9 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "build": "bob build",
35
- "minify": "find lib -name '*.js' -not -path '*/node_modules/*' -exec terser {} -o {} -c directives=false -m --module \\;",
36
35
  "typecheck": "tsc --noEmit",
37
36
  "clean": "rm -rf lib",
38
- "prepare": "bob build && npm run minify"
37
+ "prepare": "bob build"
39
38
  },
40
39
  "devDependencies": {
41
40
  "react": "19.1.0",
@@ -44,6 +43,7 @@
44
43
  "react-native-gesture-handler": "~2.28.0",
45
44
  "react-native-reanimated": "~4.2.1",
46
45
  "react-native-safe-area-context": "~5.4.0",
46
+ "react-native-screens": "~4.16.0",
47
47
  "react-native-svg": "15.12.1",
48
48
  "react-native-worklets": "0.7.2",
49
49
  "terser": "^5.44.1",
@@ -55,6 +55,7 @@
55
55
  "react-native-gesture-handler": ">=2.25.0",
56
56
  "react-native-reanimated": ">=4.1.0",
57
57
  "react-native-safe-area-context": ">=5.0.0",
58
+ "react-native-screens": ">=4.0.0",
58
59
  "react-native-svg": ">=15.8.0",
59
60
  "react-native-worklets": ">=0.5.0"
60
61
  },
@@ -68,6 +69,9 @@
68
69
  "react-native-safe-area-context": {
69
70
  "optional": false
70
71
  },
72
+ "react-native-screens": {
73
+ "optional": false
74
+ },
71
75
  "react-native-svg": {
72
76
  "optional": false
73
77
  }