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/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-bread",
|
|
3
|
-
"version": "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": [
|
|
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.
|
|
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
|
},
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
);
|
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
|
+
};
|
package/src/toast-api.ts
ADDED
|
@@ -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
|
+
});
|