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/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "react-native-bread",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
7
7
  "types": "lib/typescript/index.d.ts",
8
8
  "react-native": "lib/module/index.js",
9
- "files": ["lib", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", "!**/*.map"],
9
+ "files": [
10
+ "lib",
11
+ "src",
12
+ "!**/__tests__",
13
+ "!**/__fixtures__",
14
+ "!**/__mocks__",
15
+ "!**/*.map",
16
+ "README.md"
17
+ ],
10
18
  "sideEffects": false,
11
19
  "repository": {
12
20
  "type": "git",
@@ -31,21 +39,55 @@
31
39
  "reanimated"
32
40
  ],
33
41
  "scripts": {
42
+ "prepack": "cp ../README.md ./README.md",
43
+ "postpack": "rm ./README.md",
34
44
  "build": "bob build",
35
45
  "typecheck": "tsc --noEmit",
36
46
  "clean": "rm -rf lib",
37
- "prepare": "bob build"
47
+ "prepare": "bob build",
48
+ "release": "release-it"
49
+ },
50
+ "publishConfig": {
51
+ "registry": "https://registry.npmjs.org/"
52
+ },
53
+ "release-it": {
54
+ "git": {
55
+ "commitMessage": "chore: release v${version}",
56
+ "tagName": "v${version}"
57
+ },
58
+ "npm": {
59
+ "publish": true,
60
+ "skipChecks": true,
61
+ "publishArgs": [
62
+ "--provenance --access public"
63
+ ]
64
+ },
65
+ "github": {
66
+ "release": true
67
+ },
68
+ "hooks": {
69
+ "before:init": "bun run build"
70
+ },
71
+ "plugins": {
72
+ "@release-it/conventional-changelog": {
73
+ "preset": "conventionalcommits",
74
+ "infile": "../CHANGELOG.md"
75
+ }
76
+ }
38
77
  },
39
78
  "devDependencies": {
79
+ "@release-it/conventional-changelog": "^10.0.5",
80
+ "@types/react": "^19.1.0",
40
81
  "react": "19.1.0",
41
82
  "react-native": "0.81.5",
42
83
  "react-native-builder-bob": "^0.35.2",
43
84
  "react-native-gesture-handler": "~2.28.0",
44
85
  "react-native-reanimated": "~4.2.1",
45
- "react-native-safe-area-context": "~5.4.0",
86
+ "react-native-safe-area-context": "~5.6.0",
46
87
  "react-native-screens": "~4.16.0",
47
88
  "react-native-svg": "15.12.1",
48
89
  "react-native-worklets": "0.7.2",
90
+ "release-it": "^19.2.4",
49
91
  "terser": "^5.44.1",
50
92
  "typescript": "~5.9.2"
51
93
  },
@@ -0,0 +1,23 @@
1
+ import { Easing } from "react-native-reanimated";
2
+
3
+ export const ICON_SIZE = 28;
4
+ export const POOL_SIZE = 5;
5
+
6
+ export const ENTRY_DURATION = 400;
7
+ export const EXIT_DURATION = 350;
8
+ export const STACK_TRANSITION_DURATION = 300;
9
+ export const SPRING_BACK_DURATION = 650;
10
+ export const ICON_ANIMATION_DURATION = 350;
11
+
12
+ export const ENTRY_OFFSET = 80;
13
+ export const EXIT_OFFSET = 100;
14
+ export const SWIPE_EXIT_OFFSET = 200;
15
+ export const MAX_DRAG_CLAMP = 180;
16
+ export const MAX_DRAG_RESISTANCE = 60;
17
+ export const DISMISS_THRESHOLD = 40;
18
+ export const DISMISS_VELOCITY_THRESHOLD = 300;
19
+
20
+ export const STACK_OFFSET_PER_ITEM = 10;
21
+ export const STACK_SCALE_PER_ITEM = 0.05;
22
+
23
+ export const EASING = Easing.bezier(0.25, 0.1, 0.25, 1.0);
@@ -0,0 +1,10 @@
1
+ import Svg, { Path, type SvgProps } from "react-native-svg";
2
+
3
+ export const CloseIcon = (props: SvgProps) => (
4
+ <Svg viewBox="0 0 24 24" width={24} height={24} fill="none" {...props}>
5
+ <Path
6
+ fill={props.fill ?? "#8993A4"}
7
+ d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"
8
+ />
9
+ </Svg>
10
+ );
@@ -0,0 +1,16 @@
1
+ import Svg, { Path, type SvgProps } from "react-native-svg";
2
+
3
+ export const GreenCheck = (props: SvgProps) => (
4
+ <Svg viewBox="0 0 30 31" width={30} height={31} fill="none" {...props}>
5
+ <Path
6
+ fill={props.fill ?? "#28B770"}
7
+ fillRule="evenodd"
8
+ d="m19.866 13.152-5.772 5.773a.933.933 0 0 1-1.326 0L9.88 16.039a.938.938 0 0 1 1.325-1.327l2.225 2.224 5.109-5.11a.938.938 0 1 1 1.326 1.326Zm.28-9.652H9.602C5.654 3.5 3 6.276 3 10.409v9.935c0 4.131 2.654 6.906 6.602 6.906h10.543c3.95 0 6.605-2.775 6.605-6.906v-9.935c0-4.133-2.654-6.909-6.604-6.909Z"
9
+ clipRule="evenodd"
10
+ />
11
+ <Path
12
+ fill="#fff"
13
+ d="m19.866 13.152-5.772 5.773a.933.933 0 0 1-1.326 0L9.88 16.039a.938.938 0 0 1 1.325-1.327l2.225 2.224 5.109-5.11a.938.938 0 1 1 1.326 1.326Z"
14
+ />
15
+ </Svg>
16
+ );
@@ -0,0 +1,12 @@
1
+ import Svg, { Path, type SvgProps } from "react-native-svg";
2
+
3
+ export const InfoIcon = (props: SvgProps) => (
4
+ <Svg viewBox="0 0 24 24" width={24} height={24} fill="none" {...props}>
5
+ <Path
6
+ fill={props.fill ?? "#EDBE43"}
7
+ fillRule="evenodd"
8
+ d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm1 15h-2v-6h2v6Zm0-8h-2V7h2v2Z"
9
+ clipRule="evenodd"
10
+ />
11
+ </Svg>
12
+ );
@@ -0,0 +1,16 @@
1
+ import Svg, { Path, type SvgProps } from "react-native-svg";
2
+
3
+ export const RedX = (props: SvgProps) => (
4
+ <Svg viewBox="0 0 24 24" width={24} height={24} fill="none" {...props}>
5
+ <Path
6
+ fill={props.fill ?? "#F05964"}
7
+ fillRule="evenodd"
8
+ d="M15.58 15.572a.935.935 0 0 1-1.326 0l-2.258-2.258-2.251 2.252a.938.938 0 0 1-1.326-1.325l2.251-2.252-2.252-2.254A.936.936 0 1 1 9.742 8.41l2.253 2.252 2.252-2.25a.939.939 0 0 1 1.325 1.325l-2.25 2.252 2.257 2.257a.938.938 0 0 1 0 1.326ZM17.271.126H6.727C2.777.125.125 2.9.125 7.032v9.936c0 4.13 2.652 6.907 6.603 6.907H17.27c3.95 0 6.605-2.776 6.605-6.907V7.032c0-4.132-2.654-6.907-6.604-6.907Z"
9
+ clipRule="evenodd"
10
+ />
11
+ <Path
12
+ fill="#fff"
13
+ d="M15.58 15.572a.935.935 0 0 1-1.326 0l-2.258-2.258-2.251 2.252a.938.938 0 0 1-1.326-1.325l2.251-2.252-2.252-2.254A.936.936 0 1 1 9.742 8.41l2.253 2.252 2.252-2.25a.939.939 0 0 1 1.325 1.325l-2.25 2.252 2.257 2.257a.938.938 0 0 1 0 1.326Z"
14
+ />
15
+ </Svg>
16
+ );
@@ -0,0 +1,4 @@
1
+ export { CloseIcon } from "./CloseIcon";
2
+ export { GreenCheck } from "./GreenCheck";
3
+ export { InfoIcon } from "./InfoIcon";
4
+ export { RedX } from "./RedX";
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Main exports
2
+
3
+ // Icons (for customization)
4
+ export { CloseIcon, GreenCheck, InfoIcon, RedX } from "./icons";
5
+ export { ToastContainer } from "./toast";
6
+ export { toast } from "./toast-api";
7
+ export { BreadLoaf, ToastPortal } from "./toast-provider";
8
+
9
+ // Store (for advanced usage)
10
+ export { toastStore } from "./toast-store";
11
+ // Types
12
+ export type {
13
+ CustomContentProps,
14
+ CustomContentRenderFn,
15
+ ErrorMessageInput,
16
+ IconProps,
17
+ IconRenderFn,
18
+ MessageInput,
19
+ PromiseMessages,
20
+ PromiseResult,
21
+ Toast,
22
+ ToastConfig,
23
+ ToastOptions,
24
+ ToastPosition,
25
+ ToastState,
26
+ ToastType,
27
+ ToastTypeColors,
28
+ } from "./types";
package/src/pool.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { makeMutable, type SharedValue } from "react-native-reanimated";
2
+ import { POOL_SIZE } from "./constants";
3
+
4
+ export interface AnimSlot {
5
+ progress: SharedValue<number>;
6
+ translationY: SharedValue<number>;
7
+ stackIndex: SharedValue<number>;
8
+ }
9
+
10
+ export interface SlotTracker {
11
+ wasExiting: boolean;
12
+ prevIndex: number;
13
+ initialized: boolean;
14
+ }
15
+
16
+ export const animationPool: AnimSlot[] = Array.from({ length: POOL_SIZE }, () => ({
17
+ progress: makeMutable(0),
18
+ translationY: makeMutable(0),
19
+ stackIndex: makeMutable(0),
20
+ }));
21
+
22
+ export const slotTrackers: SlotTracker[] = Array.from({ length: POOL_SIZE }, () => ({
23
+ wasExiting: false,
24
+ prevIndex: 0,
25
+ initialized: false,
26
+ }));
27
+
28
+ const slotAssignments = new Map<string, number>();
29
+ const usedSlots = new Set<number>();
30
+
31
+ export const getSlotIndex = (toastId: string): number => {
32
+ if (slotAssignments.has(toastId)) {
33
+ return slotAssignments.get(toastId) ?? 0;
34
+ }
35
+ for (let i = 0; i < POOL_SIZE; i++) {
36
+ if (!usedSlots.has(i)) {
37
+ slotAssignments.set(toastId, i);
38
+ usedSlots.add(i);
39
+ slotTrackers[i].initialized = false;
40
+ slotTrackers[i].wasExiting = false;
41
+ slotTrackers[i].prevIndex = 0;
42
+ return i;
43
+ }
44
+ }
45
+ return 0;
46
+ };
47
+
48
+ export const releaseSlot = (toastId: string) => {
49
+ const idx = slotAssignments.get(toastId);
50
+ if (idx !== undefined) {
51
+ usedSlots.delete(idx);
52
+ slotAssignments.delete(toastId);
53
+ slotTrackers[idx].initialized = false;
54
+ slotTrackers[idx].wasExiting = false;
55
+ slotTrackers[idx].prevIndex = 0;
56
+ }
57
+ };
@@ -0,0 +1,247 @@
1
+ import type { ReactNode } from "react";
2
+ import { toastStore } from "./toast-store";
3
+ import type {
4
+ CustomContentRenderFn,
5
+ ErrorMessageInput,
6
+ MessageInput,
7
+ PromiseMessages,
8
+ PromiseResult,
9
+ ToastOptions,
10
+ ToastType,
11
+ } from "./types";
12
+
13
+ type DescriptionOrOptions = string | ToastOptions;
14
+
15
+ const _toast = (title: string, description?: string, type?: ToastType, duration?: number) => {
16
+ toastStore.show(title, description, type, duration);
17
+ };
18
+
19
+ const parseDescriptionOrOptions = (
20
+ arg?: DescriptionOrOptions
21
+ ): { description?: string; duration?: number; options?: ToastOptions } => {
22
+ if (!arg) return {};
23
+ if (typeof arg === "string") return { description: arg };
24
+ return {
25
+ description: arg.description,
26
+ duration: arg.duration,
27
+ options: arg,
28
+ };
29
+ };
30
+
31
+ const parseMessage = (input: MessageInput): { title: string; description?: string; duration?: number } =>
32
+ typeof input === "string" ? { title: input } : input;
33
+
34
+ const parseErrorMessage = (
35
+ input: ErrorMessageInput,
36
+ error: Error
37
+ ): { title: string; description?: string; duration?: number } => {
38
+ if (typeof input === "function") {
39
+ return parseMessage(input(error));
40
+ }
41
+ return parseMessage(input);
42
+ };
43
+
44
+ const promiseToast = async <T>(promise: Promise<T>, messages: PromiseMessages): Promise<PromiseResult<T>> => {
45
+ const loadingCfg = parseMessage(messages.loading);
46
+
47
+ const toastId = toastStore.show(
48
+ loadingCfg.title,
49
+ loadingCfg.description,
50
+ "loading",
51
+ loadingCfg.duration ?? 60 * 60 * 1000
52
+ );
53
+
54
+ try {
55
+ const result = await promise;
56
+
57
+ const successCfg = parseMessage(messages.success);
58
+ toastStore.updateToast(toastId, {
59
+ title: successCfg.title,
60
+ description: successCfg.description,
61
+ type: "success",
62
+ duration: successCfg.duration ?? 4000,
63
+ });
64
+
65
+ return { data: result, success: true };
66
+ } catch (err) {
67
+ const error = err instanceof Error ? err : new Error(String(err));
68
+ const errorCfg = parseErrorMessage(messages.error, error);
69
+ toastStore.updateToast(toastId, {
70
+ title: errorCfg.title,
71
+ description: errorCfg.description,
72
+ type: "error",
73
+ duration: errorCfg.duration ?? 4000,
74
+ });
75
+
76
+ return { error, success: false };
77
+ }
78
+ };
79
+
80
+ interface CustomToastOptions extends Omit<ToastOptions, "customContent" | "icon" | "titleStyle" | "descriptionStyle"> {
81
+ type?: ToastType;
82
+ }
83
+
84
+ type BaseToastFn = ((title: string, description?: string, type?: ToastType, duration?: number) => void) & {
85
+ /**
86
+ * Show a fully custom toast where you control all the content.
87
+ * The content you provide fills the entire toast container and receives entry/exit animations.
88
+ * @param content - ReactNode or render function that receives { id, dismiss, type, isExiting }
89
+ * @param options - Optional configuration for duration, dismissible, style, etc.
90
+ * @returns The toast ID
91
+ * @example
92
+ * ```ts
93
+ * // Simple custom content
94
+ * toast.custom(<MyCustomToast />);
95
+ *
96
+ * // With render function for dismiss access
97
+ * toast.custom(({ dismiss }) => (
98
+ * <View style={{ flexDirection: 'row', alignItems: 'center' }}>
99
+ * <Text>Custom toast!</Text>
100
+ * <Button title="Close" onPress={dismiss} />
101
+ * </View>
102
+ * ));
103
+ *
104
+ * // With options
105
+ * toast.custom(<MyToast />, { duration: 5000, dismissible: false });
106
+ * ```
107
+ */
108
+ custom: (content: ReactNode | CustomContentRenderFn, options?: CustomToastOptions) => string;
109
+ /**
110
+ * Show a success toast with a green checkmark icon.
111
+ * @param title - The toast title
112
+ * @param descriptionOrOptions - Description string OR options object with description, duration, icon, style, etc.
113
+ * @param duration - Duration in ms (default: 4000). Ignored if options object is passed.
114
+ * @example
115
+ * ```ts
116
+ * // Simple usage
117
+ * toast.success("Saved!", "Your changes have been saved");
118
+ *
119
+ * // With options
120
+ * toast.success("Saved!", {
121
+ * description: "Your changes have been saved",
122
+ * duration: 5000,
123
+ * icon: <CustomIcon />,
124
+ * style: { borderRadius: 8 },
125
+ * });
126
+ * ```
127
+ */
128
+ success: (title: string, descriptionOrOptions?: DescriptionOrOptions, duration?: number) => void;
129
+ /**
130
+ * Show an error toast with a red X icon.
131
+ * @param title - The toast title
132
+ * @param descriptionOrOptions - Description string OR options object
133
+ * @param duration - Duration in ms (default: 4000). Ignored if options object is passed.
134
+ * @example
135
+ * ```ts
136
+ * toast.error("Failed", "Something went wrong");
137
+ * toast.error("Failed", { description: "Something went wrong", icon: <CustomErrorIcon /> });
138
+ * ```
139
+ */
140
+ error: (title: string, descriptionOrOptions?: DescriptionOrOptions, duration?: number) => void;
141
+ /**
142
+ * Show an info toast with a blue info icon.
143
+ * @param title - The toast title
144
+ * @param descriptionOrOptions - Description string OR options object
145
+ * @param duration - Duration in ms (default: 4000). Ignored if options object is passed.
146
+ * @example
147
+ * ```ts
148
+ * toast.info("Tip", "Swipe up to dismiss");
149
+ * toast.info("Tip", { description: "Swipe up to dismiss", style: { backgroundColor: '#f0f9ff' } });
150
+ * ```
151
+ */
152
+ info: (title: string, descriptionOrOptions?: DescriptionOrOptions, duration?: number) => void;
153
+ /**
154
+ * Show a loading toast that automatically transitions to success or error
155
+ * based on the promise result. Great for async operations like API calls.
156
+ * @param promise - The promise to track
157
+ * @param messages - Configuration for loading, success, and error states
158
+ * @returns Promise result with `{ data, success: true }` or `{ error, success: false }`
159
+ * @example
160
+ * ```ts
161
+ * toast.promise(fetchUser(id), {
162
+ * loading: { title: "Loading...", description: "Fetching user data" },
163
+ * success: { title: "Done!", description: "User loaded" },
164
+ * error: (err) => ({ title: "Error", description: err.message }),
165
+ * });
166
+ * ```
167
+ */
168
+ promise: <T>(promise: Promise<T>, messages: PromiseMessages) => Promise<PromiseResult<T>>;
169
+ /**
170
+ * Dismiss a specific toast by its ID.
171
+ * @param id - The toast ID to dismiss
172
+ * @example
173
+ * ```ts
174
+ * toast.dismiss("toast-123");
175
+ * ```
176
+ */
177
+ dismiss: (id: string) => void;
178
+ /**
179
+ * Dismiss all visible toasts immediately.
180
+ * @example
181
+ * ```ts
182
+ * toast.dismissAll();
183
+ * ```
184
+ */
185
+ dismissAll: () => void;
186
+ };
187
+
188
+ const toastFn = _toast as unknown as BaseToastFn;
189
+
190
+ toastFn.custom = (content: ReactNode | CustomContentRenderFn, options?: CustomToastOptions) => {
191
+ const type = options?.type ?? "info";
192
+ return toastStore.show("", undefined, type, options?.duration, {
193
+ ...options,
194
+ customContent: content,
195
+ });
196
+ };
197
+
198
+ toastFn.success = (title: string, descriptionOrOptions?: DescriptionOrOptions, duration?: number) => {
199
+ const { description, duration: optDuration, options } = parseDescriptionOrOptions(descriptionOrOptions);
200
+ toastStore.show(title, description, "success", duration ?? optDuration, options);
201
+ };
202
+
203
+ toastFn.error = (title: string, descriptionOrOptions?: DescriptionOrOptions, duration?: number) => {
204
+ const { description, duration: optDuration, options } = parseDescriptionOrOptions(descriptionOrOptions);
205
+ toastStore.show(title, description, "error", duration ?? optDuration, options);
206
+ };
207
+
208
+ toastFn.info = (title: string, descriptionOrOptions?: DescriptionOrOptions, duration?: number) => {
209
+ const { description, duration: optDuration, options } = parseDescriptionOrOptions(descriptionOrOptions);
210
+ toastStore.show(title, description, "info", duration ?? optDuration, options);
211
+ };
212
+
213
+ toastFn.promise = promiseToast;
214
+
215
+ toastFn.dismiss = (id: string) => {
216
+ toastStore.hide(id);
217
+ };
218
+
219
+ toastFn.dismissAll = () => {
220
+ toastStore.hideAll();
221
+ };
222
+
223
+ /**
224
+ * Toast API for showing notifications.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * import { toast } from 'react-native-bread';
229
+ *
230
+ * // Basic toasts
231
+ * toast.success("Saved!", "Your changes have been saved");
232
+ * toast.error("Error", "Something went wrong");
233
+ * toast.info("Tip", "Swipe up to dismiss");
234
+ *
235
+ * // Promise toast (loading → success/error)
236
+ * toast.promise(apiCall(), {
237
+ * loading: { title: "Loading..." },
238
+ * success: { title: "Done!" },
239
+ * error: (err) => ({ title: "Failed", description: err.message }),
240
+ * });
241
+ *
242
+ * // Dismiss toasts
243
+ * toast.dismiss(id);
244
+ * toast.dismissAll();
245
+ * ```
246
+ */
247
+ export const toast = toastFn;
@@ -0,0 +1,55 @@
1
+ import { memo, type ReactNode, useEffect } from "react";
2
+ import { ActivityIndicator } from "react-native";
3
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
4
+ import { ICON_ANIMATION_DURATION, ICON_SIZE } from "./constants";
5
+ import { GreenCheck, InfoIcon, RedX } from "./icons";
6
+ import type { IconRenderFn, ToastType } from "./types";
7
+
8
+ export const resolveIcon = (
9
+ type: ToastType,
10
+ color: string,
11
+ custom?: ReactNode | IconRenderFn,
12
+ config?: IconRenderFn
13
+ ) => {
14
+ if (custom) return typeof custom === "function" ? custom({ color, size: ICON_SIZE }) : custom;
15
+ if (config) return config({ color, size: ICON_SIZE });
16
+ switch (type) {
17
+ case "success":
18
+ return <GreenCheck width={36} height={36} fill={color} />;
19
+ case "error":
20
+ return <RedX width={ICON_SIZE} height={ICON_SIZE} fill={color} />;
21
+ case "loading":
22
+ return <ActivityIndicator size={ICON_SIZE} color={color} />;
23
+ case "info":
24
+ return <InfoIcon width={ICON_SIZE} height={ICON_SIZE} fill={color} />;
25
+ default:
26
+ return <GreenCheck width={36} height={36} fill={color} />;
27
+ }
28
+ };
29
+
30
+ export const AnimatedIcon = memo(
31
+ ({
32
+ type,
33
+ color,
34
+ custom,
35
+ config,
36
+ }: {
37
+ type: ToastType;
38
+ color: string;
39
+ custom?: ReactNode | IconRenderFn;
40
+ config?: IconRenderFn;
41
+ }) => {
42
+ const progress = useSharedValue(0);
43
+
44
+ useEffect(() => {
45
+ progress.set(withTiming(1, { duration: ICON_ANIMATION_DURATION, easing: Easing.out(Easing.back(1.5)) }));
46
+ }, [progress]);
47
+
48
+ const style = useAnimatedStyle(() => ({
49
+ opacity: progress.value,
50
+ transform: [{ scale: 0.7 + progress.value * 0.3 }],
51
+ }));
52
+
53
+ return <Animated.View style={style}>{resolveIcon(type, color, custom, config)}</Animated.View>;
54
+ }
55
+ );
@@ -0,0 +1,127 @@
1
+ import { useEffect } from "react";
2
+ import { Platform, StyleSheet, View } from "react-native";
3
+ import { FullWindowOverlay } from "react-native-screens";
4
+ import { ToastContainer } from "./toast";
5
+ import { toastStore } from "./toast-store";
6
+ import type { ToastConfig } from "./types";
7
+
8
+ function ToastContent() {
9
+ return (
10
+ <View style={styles.container} pointerEvents="box-none">
11
+ <ToastContainer />
12
+ </View>
13
+ );
14
+ }
15
+
16
+ interface BreadLoafProps {
17
+ /**
18
+ * Configuration for customizing toast behavior and appearance.
19
+ * All properties are optional and will be merged with defaults.
20
+ *
21
+ * @property position - Where toasts appear: `'top'` (default) or `'bottom'`
22
+ * @property offset - Extra spacing from screen edge in pixels (default: `0`)
23
+ * @property stacking - Show multiple toasts stacked (default: `true`). When `false`, only one toast shows at a time
24
+ * @property defaultDuration - Default display time in ms (default: `4000`)
25
+ * @property colors - Customize colors per toast type (`success`, `error`, `info`, `loading`)
26
+ * @property toastStyle - Style overrides for the toast container (borderRadius, shadow, padding, etc.)
27
+ * @property titleStyle - Style overrides for the title text
28
+ * @property descriptionStyle - Style overrides for the description text
29
+ */
30
+ config?: ToastConfig;
31
+ }
32
+
33
+ /**
34
+ * Toast component that enables toast notifications in your app.
35
+ * Add `<BreadLoaf />` to your root layout to start showing toasts.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * import { BreadLoaf } from 'react-native-bread';
40
+ *
41
+ * // Basic usage - add to your root layout
42
+ * export default function RootLayout() {
43
+ * return (
44
+ * <>
45
+ * <Stack />
46
+ * <BreadLoaf />
47
+ * </>
48
+ * );
49
+ * }
50
+ *
51
+ * // With configuration
52
+ * <BreadLoaf
53
+ * config={{
54
+ * position: 'bottom',
55
+ * stacking: false,
56
+ * defaultDuration: 5000,
57
+ * colors: {
58
+ * success: { accent: '#22c55e', background: '#f0fdf4' },
59
+ * error: { accent: '#ef4444', background: '#fef2f2' },
60
+ * },
61
+ * toastStyle: { borderRadius: 12 },
62
+ * }}
63
+ * />
64
+ * ```
65
+ */
66
+ export function BreadLoaf({ config }: BreadLoafProps) {
67
+ useEffect(() => {
68
+ toastStore.setConfig(config);
69
+ return () => {
70
+ toastStore.setConfig(undefined);
71
+ };
72
+ }, [config]);
73
+
74
+ // iOS: use FullWindowOverlay to render above native modals
75
+ if (Platform.OS === "ios") {
76
+ return (
77
+ <FullWindowOverlay>
78
+ <ToastContent />
79
+ </FullWindowOverlay>
80
+ );
81
+ }
82
+
83
+ return <ToastContent />;
84
+ }
85
+
86
+ /**
87
+ * Lightweight toast renderer for use inside modal screens.
88
+ *
89
+ * On Android, native modals render above the root React view, so toasts from
90
+ * the main `<BreadLoaf />` won't be visible. Add `<ToastPortal />` inside your
91
+ * modal layouts to show toasts above modal content.
92
+ *
93
+ * This component only renders on Android - it returns `null` on iOS where
94
+ * `<BreadLoaf />` already handles modal overlay via `FullWindowOverlay`.
95
+ *
96
+ * This component only renders toasts - it does not accept configuration.
97
+ * All styling/behavior is inherited from your root `<BreadLoaf />` config.
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // app/(modal)/_layout.tsx
102
+ * import { Stack } from 'expo-router';
103
+ * import { ToastPortal } from 'react-native-bread';
104
+ *
105
+ * export default function ModalLayout() {
106
+ * return (
107
+ * <>
108
+ * <Stack screenOptions={{ headerShown: false }} />
109
+ * <ToastPortal />
110
+ * </>
111
+ * );
112
+ * }
113
+ * ```
114
+ */
115
+ export function ToastPortal() {
116
+ if (Platform.OS !== "android") {
117
+ return null;
118
+ }
119
+ return <ToastContent />;
120
+ }
121
+
122
+ const styles = StyleSheet.create({
123
+ container: {
124
+ ...StyleSheet.absoluteFillObject,
125
+ zIndex: 9999,
126
+ },
127
+ });