react-native-toast-message-library 0.0.0-stage → 0.1.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.
@@ -0,0 +1,64 @@
1
+ const light = {
2
+ name: 'light',
3
+ colors: {
4
+ success: {
5
+ background: '#ECFDF3',
6
+ text: '#05603A',
7
+ border: '#ABEFC6',
8
+ icon: '#079455',
9
+ iconText: '#FFFFFF',
10
+ },
11
+ error: {
12
+ background: '#FEF3F2',
13
+ text: '#912018',
14
+ border: '#FECDCA',
15
+ icon: '#D92D20',
16
+ iconText: '#FFFFFF',
17
+ },
18
+ warning: {
19
+ background: '#FFFAEB',
20
+ text: '#93370D',
21
+ border: '#FEDF89',
22
+ icon: '#B54708',
23
+ iconText: '#FFFFFF',
24
+ },
25
+ info: {
26
+ background: '#EFF8FF',
27
+ text: '#175CD3',
28
+ border: '#B2DDFF',
29
+ icon: '#1570EF',
30
+ iconText: '#FFFFFF',
31
+ },
32
+ custom: {
33
+ background: '#FFFFFF',
34
+ text: '#101828',
35
+ border: '#E4E7EC',
36
+ icon: '#475467',
37
+ iconText: '#FFFFFF',
38
+ },
39
+ },
40
+ actionColor: undefined,
41
+ closeColor: undefined,
42
+ progressTrack: 'rgba(0,0,0,0.08)',
43
+ borderRadius: 14,
44
+ borderWidth: 1,
45
+ padding: 14,
46
+ spacing: 10,
47
+ iconSize: 24,
48
+ typography: {
49
+ titleSize: 15,
50
+ titleWeight: '700',
51
+ messageSize: 14,
52
+ messageWeight: '400',
53
+ fontFamily: undefined,
54
+ },
55
+ shadow: {
56
+ color: '#101828',
57
+ opacity: 0.14,
58
+ radius: 12,
59
+ offsetY: 4,
60
+ elevation: 6,
61
+ },
62
+ };
63
+
64
+ export default light;
@@ -0,0 +1,40 @@
1
+ export const TOAST_TYPES = ['success', 'error', 'warning', 'info', 'custom'];
2
+ export const POSITIONS = [
3
+ 'top',
4
+ 'bottom',
5
+ 'center',
6
+ 'top-left',
7
+ 'top-right',
8
+ 'bottom-left',
9
+ 'bottom-right',
10
+ ];
11
+ export const ANIMATIONS = ['fade', 'slide', 'zoom', 'none'];
12
+ export const DISMISS_REASONS = [
13
+ 'timeout',
14
+ 'manual',
15
+ 'swipe',
16
+ 'close',
17
+ 'action',
18
+ 'press',
19
+ 'hideAll',
20
+ ];
21
+
22
+ export const TYPE_LABELS = {
23
+ success: 'Success',
24
+ error: 'Error',
25
+ warning: 'Warning',
26
+ info: 'Info',
27
+ custom: '',
28
+ };
29
+
30
+ export const DEFAULT_CONFIG = {
31
+ position: 'top',
32
+ duration: 3000,
33
+ animation: 'slide',
34
+ animationDuration: 250,
35
+ maxVisible: 3,
36
+ closeButton: false,
37
+ swipeToDismiss: true,
38
+ progress: false,
39
+ haptics: false,
40
+ };
@@ -0,0 +1,46 @@
1
+ import { AccessibilityInfo } from 'react-native';
2
+
3
+ /**
4
+ * Throttled screen-reader announcements (used on iOS, which has no live
5
+ * regions). If several toasts appear at once, only the most recent message
6
+ * waiting is spoken after the minimum gap, so VoiceOver is not interrupted
7
+ * repeatedly.
8
+ */
9
+ export default function createAnnouncer(minGap = 1500) {
10
+ let lastAt = 0;
11
+ let pending = null;
12
+ let timer = null;
13
+
14
+ function speak(message) {
15
+ lastAt = Date.now();
16
+ AccessibilityInfo.announceForAccessibility(message);
17
+ }
18
+
19
+ return {
20
+ announce(message) {
21
+ const wait = lastAt + minGap - Date.now();
22
+ if (wait <= 0 && timer == null) {
23
+ speak(message);
24
+ return;
25
+ }
26
+ pending = message;
27
+ if (timer == null) {
28
+ timer = setTimeout(() => {
29
+ timer = null;
30
+ const next = pending;
31
+ pending = null;
32
+ if (next != null) {
33
+ speak(next);
34
+ }
35
+ }, Math.max(wait, 0));
36
+ }
37
+ },
38
+ cancel() {
39
+ if (timer != null) {
40
+ clearTimeout(timer);
41
+ }
42
+ timer = null;
43
+ pending = null;
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,10 @@
1
+ let counter = 0;
2
+
3
+ /**
4
+ * Returns an id that is unique for the lifetime of the JS runtime.
5
+ * A monotonically increasing counter is enough because ids never leave the app.
6
+ */
7
+ export default function generateToastId() {
8
+ counter += 1;
9
+ return `toast-${counter}`;
10
+ }
@@ -0,0 +1,42 @@
1
+ import { DEFAULT_CONFIG } from './constants';
2
+ import validateToastOptions from './validateToastOptions';
3
+
4
+ /**
5
+ * Converts the argument passed to `show()` (string or object) into a fully
6
+ * resolved options object by applying the provider defaults. The result is a
7
+ * new object; the caller's input is never mutated.
8
+ *
9
+ * @param {string|number|object} input
10
+ * @param {object} defaults provider-level defaults
11
+ */
12
+ export default function normalizeToastOptions(input, defaults = {}) {
13
+ const raw =
14
+ typeof input === 'string' || typeof input === 'number'
15
+ ? { message: String(input) }
16
+ : input;
17
+
18
+ validateToastOptions(raw);
19
+
20
+ const base = { ...DEFAULT_CONFIG, ...defaults };
21
+ const resolved = { ...raw };
22
+
23
+ resolved.type = raw.type || 'custom';
24
+ ['position', 'animation', 'animationDuration', 'duration'].forEach(key => {
25
+ resolved[key] = raw[key] != null ? raw[key] : base[key];
26
+ });
27
+ ['closeButton', 'swipeToDismiss', 'progress', 'haptics'].forEach(key => {
28
+ resolved[key] = raw[key] != null ? raw[key] : base[key];
29
+ });
30
+ resolved.dismissOnPress = raw.dismissOnPress === true;
31
+ resolved.dismissOnAction = raw.dismissOnAction !== false;
32
+ resolved.offsetX = raw.offsetX != null ? raw.offsetX : base.offsetX;
33
+ resolved.offsetY = raw.offsetY != null ? raw.offsetY : base.offsetY;
34
+ if (raw.message != null) {
35
+ resolved.message = String(raw.message);
36
+ }
37
+ if (raw.title != null) {
38
+ resolved.title = String(raw.title);
39
+ }
40
+
41
+ return resolved;
42
+ }
@@ -0,0 +1,34 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { Platform, StatusBar } from 'react-native';
3
+
4
+ // react-native-safe-area-context is an optional peer dependency. Metro treats a
5
+ // require() inside try/catch as optional, so the library works without it.
6
+ let ExternalInsetsContext = null;
7
+ try {
8
+ ExternalInsetsContext =
9
+ require('react-native-safe-area-context').SafeAreaInsetsContext;
10
+ } catch (error) {
11
+ ExternalInsetsContext = null;
12
+ }
13
+
14
+ const FallbackContext = createContext(null);
15
+ const InsetsContext = ExternalInsetsContext || FallbackContext;
16
+
17
+ function fallbackInsets() {
18
+ if (Platform.OS === 'android') {
19
+ return { top: StatusBar.currentHeight || 0, bottom: 0, left: 0, right: 0 };
20
+ }
21
+ // Conservative values for devices with a notch / Dynamic Island. Install
22
+ // react-native-safe-area-context (and add a SafeAreaProvider) for exact values.
23
+ return { top: 47, bottom: 34, left: 0, right: 0 };
24
+ }
25
+
26
+ /**
27
+ * Priority: explicit `override` prop > react-native-safe-area-context
28
+ * (when installed and a SafeAreaProvider is mounted) > platform fallback.
29
+ */
30
+ export default function useToastInsets(override) {
31
+ const external = useContext(InsetsContext);
32
+ const base = external || fallbackInsets();
33
+ return { top: 0, bottom: 0, left: 0, right: 0, ...base, ...override };
34
+ }
@@ -0,0 +1,127 @@
1
+ import { ANIMATIONS, POSITIONS, TOAST_TYPES } from './constants';
2
+
3
+ const PREFIX = '[react-native-toast-message-library]';
4
+
5
+ function fail(message) {
6
+ throw new TypeError(`${PREFIX} ${message}`);
7
+ }
8
+
9
+ function isPlainObject(value) {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+
13
+ function checkFunction(options, key) {
14
+ if (options[key] != null && typeof options[key] !== 'function') {
15
+ fail(`"${key}" must be a function, received ${typeof options[key]}.`);
16
+ }
17
+ }
18
+
19
+ function checkEnum(options, key, allowed) {
20
+ if (options[key] != null && !allowed.includes(options[key])) {
21
+ fail(
22
+ `Invalid ${key} "${String(
23
+ options[key],
24
+ )}". Expected one of: ${allowed.join(', ')}.`,
25
+ );
26
+ }
27
+ }
28
+
29
+ function checkNonNegativeNumber(options, key) {
30
+ const value = options[key];
31
+ if (value == null) {
32
+ return;
33
+ }
34
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
35
+ fail(`"${key}" must be a finite number >= 0, received ${String(value)}.`);
36
+ }
37
+ }
38
+
39
+ function checkBoolean(options, key) {
40
+ if (options[key] != null && typeof options[key] !== 'boolean') {
41
+ fail(`"${key}" must be a boolean, received ${typeof options[key]}.`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Validates an options object (after strings have been converted to objects).
47
+ * Throws a TypeError with an actionable message on the first problem found.
48
+ */
49
+ export default function validateToastOptions(options) {
50
+ if (!isPlainObject(options)) {
51
+ fail(
52
+ 'toast options must be a string or an object, e.g. toast.show({ message: "Saved" }).',
53
+ );
54
+ }
55
+
56
+ checkEnum(options, 'type', TOAST_TYPES);
57
+ checkEnum(options, 'position', POSITIONS);
58
+ checkEnum(options, 'animation', ANIMATIONS);
59
+ checkNonNegativeNumber(options, 'duration');
60
+ checkNonNegativeNumber(options, 'animationDuration');
61
+ checkNonNegativeNumber(options, 'offsetX');
62
+ checkNonNegativeNumber(options, 'offsetY');
63
+
64
+ if (
65
+ options.id != null &&
66
+ typeof options.id !== 'string' &&
67
+ typeof options.id !== 'number'
68
+ ) {
69
+ fail('"id" must be a string or a number.');
70
+ }
71
+
72
+ ['title', 'message'].forEach(key => {
73
+ const value = options[key];
74
+ if (
75
+ value != null &&
76
+ typeof value !== 'string' &&
77
+ typeof value !== 'number'
78
+ ) {
79
+ fail(`"${key}" must be a string or number, received ${typeof value}.`);
80
+ }
81
+ });
82
+
83
+ ['onPress', 'onDismiss', 'onAction', 'onShow', 'render'].forEach(key =>
84
+ checkFunction(options, key),
85
+ );
86
+
87
+ [
88
+ 'closeButton',
89
+ 'swipeToDismiss',
90
+ 'progress',
91
+ 'haptics',
92
+ 'dismissOnPress',
93
+ 'dismissOnAction',
94
+ ].forEach(key => checkBoolean(options, key));
95
+
96
+ if (options.action != null) {
97
+ if (!isPlainObject(options.action)) {
98
+ fail('"action" must be an object like { label: "Undo" }.');
99
+ }
100
+ if (
101
+ typeof options.action.label !== 'string' ||
102
+ options.action.label.trim() === ''
103
+ ) {
104
+ fail('"action.label" must be a non-empty string.');
105
+ }
106
+ }
107
+
108
+ if (
109
+ options.shadow != null &&
110
+ options.shadow !== false &&
111
+ !isPlainObject(options.shadow)
112
+ ) {
113
+ fail('"shadow" must be false or an object.');
114
+ }
115
+
116
+ const hasContent =
117
+ options.message != null ||
118
+ (options.title != null && options.title !== '') ||
119
+ typeof options.render === 'function';
120
+ if (!hasContent) {
121
+ fail(
122
+ 'a toast needs at least a "message", a "title" or a "render" function.',
123
+ );
124
+ }
125
+
126
+ return options;
127
+ }
@@ -0,0 +1,203 @@
1
+ import * as React from 'react';
2
+ import { StyleProp, TextStyle, ViewStyle } from 'react-native';
3
+
4
+ export type ToastType = 'success' | 'error' | 'warning' | 'info' | 'custom';
5
+ export type ToastPosition =
6
+ | 'top'
7
+ | 'bottom'
8
+ | 'center'
9
+ | 'top-left'
10
+ | 'top-right'
11
+ | 'bottom-left'
12
+ | 'bottom-right';
13
+ export type ToastAnimation = 'fade' | 'slide' | 'zoom' | 'none';
14
+ export type ToastDismissReason =
15
+ | 'timeout'
16
+ | 'manual'
17
+ | 'swipe'
18
+ | 'close'
19
+ | 'action'
20
+ | 'press'
21
+ | 'hideAll';
22
+
23
+ export type ToastId = string;
24
+
25
+ /** Argument passed to onPress / onAction and to `render`. */
26
+ export interface ToastCallbackInfo {
27
+ id: ToastId;
28
+ type: ToastType;
29
+ /** Dismisses this toast (reason "manual"). */
30
+ hide: () => boolean;
31
+ }
32
+
33
+ export interface ToastColors {
34
+ background: string;
35
+ text: string;
36
+ border: string;
37
+ icon: string;
38
+ }
39
+
40
+ export interface ToastRenderInfo extends ToastCallbackInfo {
41
+ colors: ToastColors;
42
+ theme: ToastTheme;
43
+ }
44
+
45
+ export interface ToastShadow {
46
+ color?: string;
47
+ opacity?: number;
48
+ radius?: number;
49
+ offsetY?: number;
50
+ elevation?: number;
51
+ }
52
+
53
+ export interface ToastAction {
54
+ label: string;
55
+ accessibilityLabel?: string;
56
+ }
57
+
58
+ export interface ToastOptions {
59
+ /** Reusing an id updates that toast in place. Generated when omitted. */
60
+ id?: string | number;
61
+ type?: ToastType;
62
+ /** Defaults to "Success" / "Error" / "Warning" / "Info". Use '' or null to hide. */
63
+ title?: string | number | null;
64
+ message?: string | number;
65
+ /** ms. 0 keeps the toast until dismissed. Default 3000. */
66
+ duration?: number;
67
+ position?: ToastPosition;
68
+ animation?: ToastAnimation;
69
+ animationDuration?: number;
70
+ /** Extra horizontal / vertical distance from the edge, added to the provider offset. */
71
+ offsetX?: number;
72
+ offsetY?: number;
73
+
74
+ /** false hides the icon; an element or a function renders a custom one. */
75
+ icon?:
76
+ | false
77
+ | React.ReactElement
78
+ | ((info: { type: ToastType; color: string; size: number }) => React.ReactNode);
79
+ /** Replaces the icon + title + message area. */
80
+ render?: (info: ToastRenderInfo) => React.ReactNode;
81
+
82
+ closeButton?: boolean;
83
+ closeAccessibilityLabel?: string;
84
+ swipeToDismiss?: boolean;
85
+ progress?: boolean;
86
+ haptics?: boolean;
87
+ action?: ToastAction;
88
+ dismissOnPress?: boolean;
89
+ /** Default true. */
90
+ dismissOnAction?: boolean;
91
+
92
+ backgroundColor?: string;
93
+ textColor?: string;
94
+ borderColor?: string;
95
+ iconColor?: string;
96
+ borderRadius?: number;
97
+ padding?: number;
98
+ /** false removes the shadow. */
99
+ shadow?: false | ToastShadow;
100
+ containerStyle?: StyleProp<ViewStyle>;
101
+ titleStyle?: StyleProp<TextStyle>;
102
+ messageStyle?: StyleProp<TextStyle>;
103
+ accessibilityLabel?: string;
104
+
105
+ /** Called when the toast becomes visible (after leaving the queue). */
106
+ onShow?: (id: ToastId) => void;
107
+ onPress?: (info: ToastCallbackInfo) => void;
108
+ onAction?: (info: ToastCallbackInfo) => void;
109
+ /** Called once, when dismissal starts, also for toasts cancelled while queued. */
110
+ onDismiss?: (reason: ToastDismissReason, id: ToastId) => void;
111
+ }
112
+
113
+ export type ToastShortcutOptions = Omit<ToastOptions, 'type' | 'message'> & {
114
+ message?: ToastOptions['message'];
115
+ };
116
+
117
+ export interface ToastApi {
118
+ /** Throws a TypeError on invalid options. Returns the toast id. */
119
+ show(options: ToastOptions | string): ToastId;
120
+ success(message: string | ToastShortcutOptions, options?: ToastShortcutOptions): ToastId;
121
+ error(message: string | ToastShortcutOptions, options?: ToastShortcutOptions): ToastId;
122
+ warning(message: string | ToastShortcutOptions, options?: ToastShortcutOptions): ToastId;
123
+ info(message: string | ToastShortcutOptions, options?: ToastShortcutOptions): ToastId;
124
+ /** true if a toast was dismissed (visible or queued). */
125
+ hide(id: ToastId | number): boolean;
126
+ /** Returns how many toasts were dismissed. */
127
+ hideAll(): number;
128
+ /** Without an id: is any toast visible? */
129
+ isVisible(id?: ToastId | number): boolean;
130
+ }
131
+
132
+ export interface ToastTypeColors {
133
+ background: string;
134
+ text: string;
135
+ border: string;
136
+ icon: string;
137
+ /** Colour of the glyph drawn inside the built-in icon badge. */
138
+ iconText: string;
139
+ }
140
+
141
+ export interface ToastTheme {
142
+ name: string;
143
+ colors: Record<ToastType, ToastTypeColors>;
144
+ actionColor?: string;
145
+ closeColor?: string;
146
+ progressTrack: string;
147
+ borderRadius: number;
148
+ borderWidth: number;
149
+ padding: number;
150
+ spacing: number;
151
+ iconSize: number;
152
+ typography: {
153
+ titleSize: number;
154
+ titleWeight: TextStyle['fontWeight'];
155
+ messageSize: number;
156
+ messageWeight: TextStyle['fontWeight'];
157
+ fontFamily?: string;
158
+ };
159
+ shadow: Required<ToastShadow>;
160
+ }
161
+
162
+ type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
163
+ export type ToastThemeOverrides = DeepPartial<ToastTheme> & { extends?: 'light' | 'dark' };
164
+
165
+ export interface Insets {
166
+ top?: number;
167
+ bottom?: number;
168
+ left?: number;
169
+ right?: number;
170
+ }
171
+
172
+ export interface ToastProviderProps {
173
+ children?: React.ReactNode;
174
+ /** 'system' follows the device colour scheme. Objects are merged over the light theme (or `extends`). */
175
+ theme?: 'light' | 'dark' | 'system' | ToastThemeOverrides;
176
+ position?: ToastPosition;
177
+ duration?: number;
178
+ animation?: ToastAnimation;
179
+ animationDuration?: number;
180
+ /** Max toasts on screen at once; the rest wait in a FIFO queue. Default 3. */
181
+ maxVisible?: number;
182
+ closeButton?: boolean;
183
+ swipeToDismiss?: boolean;
184
+ progress?: boolean;
185
+ haptics?: boolean;
186
+ /** Gap between stacked toasts. Defaults to the theme's spacing. */
187
+ spacing?: number;
188
+ offset?: { x?: number; y?: number };
189
+ /** Overrides detected safe-area insets. */
190
+ insets?: Insets;
191
+ /** Registers this provider for the imperative `Toast` API. */
192
+ global?: boolean;
193
+ /** Style of the wrapper View (default { flex: 1 }). */
194
+ style?: StyleProp<ViewStyle>;
195
+ }
196
+
197
+ export const ToastProvider: React.FC<ToastProviderProps>;
198
+ export function useToast(): ToastApi;
199
+ /** Imperative API. Requires <ToastProvider global>; throws if none is mounted. */
200
+ export const Toast: ToastApi;
201
+ export function createTheme(base?: 'light' | 'dark', overrides?: ToastThemeOverrides): ToastTheme;
202
+ export const lightTheme: ToastTheme;
203
+ export const darkTheme: ToastTheme;