react-native-bread 0.6.1 → 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.
@@ -0,0 +1,254 @@
1
+ import type { Toast, ToastConfig, ToastOptions, ToastState, ToastTheme, ToastType, ToastTypeColors } from "./types";
2
+
3
+ export type Listener = (state: ToastState) => void;
4
+
5
+ const EXIT_DURATION = 350;
6
+
7
+ const DEFAULT_THEME: ToastTheme = {
8
+ position: "top",
9
+ offset: 0,
10
+ rtl: false,
11
+ stacking: true,
12
+ maxStack: 3,
13
+ dismissible: true,
14
+ showCloseButton: true,
15
+ colors: {
16
+ success: { accent: "#28B770", background: "#FFFFFF" },
17
+ error: { accent: "#F05964", background: "#FFFFFF" },
18
+ info: { accent: "#EDBE43", background: "#FFFFFF" },
19
+ loading: { accent: "#232323", background: "#FFFFFF" },
20
+ },
21
+ icons: {},
22
+ toastStyle: {},
23
+ titleStyle: {},
24
+ descriptionStyle: {},
25
+ defaultDuration: 4000,
26
+ };
27
+
28
+ function mergeConfig(config: ToastConfig | undefined): ToastTheme {
29
+ if (!config) return DEFAULT_THEME;
30
+
31
+ const mergedColors = { ...DEFAULT_THEME.colors };
32
+ if (config.colors) {
33
+ for (const type of Object.keys(config.colors) as ToastType[]) {
34
+ const userColors = config.colors[type];
35
+ if (userColors) {
36
+ mergedColors[type] = {
37
+ ...DEFAULT_THEME.colors[type],
38
+ ...userColors,
39
+ } as ToastTypeColors;
40
+ }
41
+ }
42
+ }
43
+
44
+ return {
45
+ position: config.position ?? DEFAULT_THEME.position,
46
+ offset: config.offset ?? DEFAULT_THEME.offset,
47
+ rtl: config.rtl ?? DEFAULT_THEME.rtl,
48
+ stacking: config.stacking ?? DEFAULT_THEME.stacking,
49
+ maxStack: config.maxStack ?? DEFAULT_THEME.maxStack,
50
+ dismissible: config.dismissible ?? DEFAULT_THEME.dismissible,
51
+ showCloseButton: config.showCloseButton ?? DEFAULT_THEME.showCloseButton,
52
+ colors: mergedColors,
53
+ icons: { ...DEFAULT_THEME.icons, ...config.icons },
54
+ toastStyle: { ...DEFAULT_THEME.toastStyle, ...config.toastStyle },
55
+ titleStyle: { ...DEFAULT_THEME.titleStyle, ...config.titleStyle },
56
+ descriptionStyle: { ...DEFAULT_THEME.descriptionStyle, ...config.descriptionStyle },
57
+ defaultDuration: config.defaultDuration ?? DEFAULT_THEME.defaultDuration,
58
+ };
59
+ }
60
+
61
+ class ToastStore {
62
+ private state: ToastState = {
63
+ visibleToasts: [],
64
+ };
65
+
66
+ private theme: ToastTheme = DEFAULT_THEME;
67
+
68
+ private listeners = new Set<Listener>();
69
+ private toastIdCounter = 0;
70
+ private timeouts = new Map<string, ReturnType<typeof setTimeout>>();
71
+
72
+ subscribe = (listener: Listener) => {
73
+ this.listeners.add(listener);
74
+ return () => {
75
+ this.listeners.delete(listener);
76
+ };
77
+ };
78
+
79
+ private emit() {
80
+ for (const listener of this.listeners) {
81
+ listener(this.state);
82
+ }
83
+ }
84
+
85
+ private setState(partial: Partial<ToastState>) {
86
+ this.state = { ...this.state, ...partial };
87
+ this.emit();
88
+ }
89
+
90
+ getState = () => this.state;
91
+
92
+ getTheme = () => this.theme;
93
+
94
+ setConfig = (config: ToastConfig | undefined) => {
95
+ this.theme = mergeConfig(config);
96
+ };
97
+
98
+ show = (
99
+ title: string,
100
+ description?: string,
101
+ type: ToastType = "success",
102
+ duration?: number,
103
+ options?: ToastOptions
104
+ ): string => {
105
+ const actualDuration = duration ?? options?.duration ?? this.theme.defaultDuration;
106
+ const maxToasts = this.theme.stacking ? this.theme.maxStack : 1;
107
+
108
+ const id = `toast-${++this.toastIdCounter}`;
109
+ const newToast: Toast = {
110
+ id,
111
+ title,
112
+ description: description ?? options?.description,
113
+ type,
114
+ duration: actualDuration,
115
+ createdAt: Date.now(),
116
+ isExiting: false,
117
+ options,
118
+ };
119
+
120
+ const { visibleToasts } = this.state;
121
+ const activeToasts = visibleToasts.filter(t => !t.isExiting);
122
+
123
+ if (activeToasts.length >= maxToasts) {
124
+ const toastsToRemove = activeToasts.slice(maxToasts - 1);
125
+
126
+ for (const toast of toastsToRemove) {
127
+ const timeout = this.timeouts.get(toast.id);
128
+ if (timeout) {
129
+ clearTimeout(timeout);
130
+ this.timeouts.delete(toast.id);
131
+ }
132
+ }
133
+
134
+ const removeIds = new Set(toastsToRemove.map(t => t.id));
135
+
136
+ if (this.theme.stacking) {
137
+ this.setState({
138
+ visibleToasts: visibleToasts.filter(t => !removeIds.has(t.id)),
139
+ });
140
+ } else {
141
+ this.setState({
142
+ visibleToasts: visibleToasts.map(t => (removeIds.has(t.id) ? { ...t, isExiting: true } : t)),
143
+ });
144
+
145
+ setTimeout(() => {
146
+ for (const toast of toastsToRemove) {
147
+ this.removeToast(toast.id);
148
+ }
149
+ this.addToast(newToast, actualDuration);
150
+ }, EXIT_DURATION - 220);
151
+
152
+ return id;
153
+ }
154
+ }
155
+
156
+ this.addToast(newToast, actualDuration);
157
+
158
+ return id;
159
+ };
160
+
161
+ private addToast(toast: Toast, duration: number) {
162
+ this.setState({
163
+ visibleToasts: [toast, ...this.state.visibleToasts.filter(t => !t.isExiting)],
164
+ });
165
+
166
+ this.scheduleTimeout(toast.id, duration, 0);
167
+ this.rescheduleAllTimeouts();
168
+ }
169
+
170
+ private scheduleTimeout(id: string, baseDuration: number, index: number) {
171
+ const existingTimeout = this.timeouts.get(id);
172
+ if (existingTimeout) {
173
+ clearTimeout(existingTimeout);
174
+ }
175
+
176
+ const duration = baseDuration * (index + 1);
177
+
178
+ const timeout = setTimeout(() => {
179
+ this.hide(id);
180
+ }, duration);
181
+
182
+ this.timeouts.set(id, timeout);
183
+ }
184
+
185
+ private rescheduleAllTimeouts() {
186
+ const { visibleToasts } = this.state;
187
+
188
+ visibleToasts.forEach((toast, index) => {
189
+ if (toast.isExiting || index === 0) return;
190
+
191
+ this.scheduleTimeout(toast.id, toast.duration, index);
192
+ });
193
+ }
194
+
195
+ hide = (id: string) => {
196
+ const { visibleToasts } = this.state;
197
+ const toast = visibleToasts.find(t => t.id === id);
198
+ if (!toast || toast.isExiting) return;
199
+
200
+ const timeout = this.timeouts.get(id);
201
+ if (timeout) {
202
+ clearTimeout(timeout);
203
+ this.timeouts.delete(id);
204
+ }
205
+
206
+ this.setState({
207
+ visibleToasts: visibleToasts.map(t => (t.id === id ? { ...t, isExiting: true } : t)),
208
+ });
209
+
210
+ setTimeout(() => {
211
+ this.removeToast(id);
212
+ }, EXIT_DURATION);
213
+ };
214
+
215
+ private removeToast(id: string) {
216
+ const timeout = this.timeouts.get(id);
217
+ if (timeout) {
218
+ clearTimeout(timeout);
219
+ this.timeouts.delete(id);
220
+ }
221
+
222
+ this.setState({
223
+ visibleToasts: this.state.visibleToasts.filter(t => t.id !== id),
224
+ });
225
+
226
+ this.rescheduleAllTimeouts();
227
+ }
228
+
229
+ updateToast = (id: string, data: Partial<Omit<Toast, "id" | "createdAt">>) => {
230
+ const { visibleToasts } = this.state;
231
+ const index = visibleToasts.findIndex(t => t.id === id);
232
+ if (index === -1) return;
233
+
234
+ this.setState({
235
+ visibleToasts: visibleToasts.map(t => (t.id === id ? { ...t, ...data } : t)),
236
+ });
237
+
238
+ if (data.duration !== undefined) {
239
+ this.scheduleTimeout(id, data.duration, index);
240
+ }
241
+ };
242
+
243
+ hideAll = () => {
244
+ for (const timeout of this.timeouts.values()) {
245
+ clearTimeout(timeout);
246
+ }
247
+ this.timeouts.clear();
248
+ this.setState({ visibleToasts: [] });
249
+ };
250
+ }
251
+
252
+ export const toastStore = new ToastStore();
253
+
254
+ export type { Toast, ToastState, ToastType };
package/src/toast.tsx ADDED
@@ -0,0 +1,398 @@
1
+ import { memo, useCallback, useEffect, useState } from "react";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
+ import Animated, { interpolate, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
5
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
6
+ import { scheduleOnRN } from "react-native-worklets";
7
+ import {
8
+ DISMISS_THRESHOLD,
9
+ DISMISS_VELOCITY_THRESHOLD,
10
+ EASING,
11
+ ENTRY_DURATION,
12
+ ENTRY_OFFSET,
13
+ EXIT_DURATION,
14
+ EXIT_OFFSET,
15
+ ICON_ANIMATION_DURATION,
16
+ MAX_DRAG_CLAMP,
17
+ MAX_DRAG_RESISTANCE,
18
+ SPRING_BACK_DURATION,
19
+ STACK_OFFSET_PER_ITEM,
20
+ STACK_SCALE_PER_ITEM,
21
+ STACK_TRANSITION_DURATION,
22
+ SWIPE_EXIT_OFFSET,
23
+ } from "./constants";
24
+ import { CloseIcon } from "./icons";
25
+ import { type AnimSlot, animationPool, getSlotIndex, releaseSlot, slotTrackers } from "./pool";
26
+ import { AnimatedIcon, resolveIcon } from "./toast-icons";
27
+ import { toastStore } from "./toast-store";
28
+ import type { CustomContentRenderFn, ToastItemProps, TopToastRef } from "./types";
29
+ import { useToastState } from "./use-toast-state";
30
+
31
+ export const ToastContainer = () => {
32
+ const { top, bottom } = useSafeAreaInsets();
33
+ const { visibleToasts, theme, toastsWithIndex, isBottom, topToastMutable, isBottomMutable, isDismissibleMutable } =
34
+ useToastState();
35
+
36
+ const shouldDismiss = useSharedValue(false);
37
+
38
+ const panGesture = Gesture.Pan()
39
+ .onStart(() => {
40
+ "worklet";
41
+ shouldDismiss.set(false);
42
+ })
43
+ .onUpdate(event => {
44
+ "worklet";
45
+ if (!isDismissibleMutable.value) return;
46
+ const ref = topToastMutable.value;
47
+ if (!ref) return;
48
+
49
+ const { slot } = ref;
50
+ const bottom = isBottomMutable.value;
51
+ const rawY = event.translationY;
52
+ const dismissDrag = bottom ? rawY : -rawY;
53
+ const resistDrag = bottom ? -rawY : rawY;
54
+
55
+ if (dismissDrag > 0) {
56
+ const clampedY = bottom ? Math.min(rawY, MAX_DRAG_CLAMP) : Math.max(rawY, -MAX_DRAG_CLAMP);
57
+ slot.translationY.set(clampedY);
58
+
59
+ const shouldTriggerDismiss =
60
+ dismissDrag > DISMISS_THRESHOLD ||
61
+ (bottom ? event.velocityY > DISMISS_VELOCITY_THRESHOLD : event.velocityY < -DISMISS_VELOCITY_THRESHOLD);
62
+ shouldDismiss.set(shouldTriggerDismiss);
63
+ } else {
64
+ const exponentialDrag = MAX_DRAG_RESISTANCE * (1 - Math.exp(-resistDrag / 250));
65
+ slot.translationY.set(
66
+ bottom ? -Math.min(exponentialDrag, MAX_DRAG_RESISTANCE) : Math.min(exponentialDrag, MAX_DRAG_RESISTANCE)
67
+ );
68
+ shouldDismiss.set(false);
69
+ }
70
+ })
71
+ .onEnd(() => {
72
+ "worklet";
73
+ if (!isDismissibleMutable.value) return;
74
+ const ref = topToastMutable.value;
75
+ if (!ref) return;
76
+
77
+ const { slot } = ref;
78
+ const bottom = isBottomMutable.value;
79
+ if (shouldDismiss.value) {
80
+ slot.progress.set(withTiming(0, { duration: EXIT_DURATION, easing: EASING }));
81
+ const exitOffset = bottom ? SWIPE_EXIT_OFFSET : -SWIPE_EXIT_OFFSET;
82
+ slot.translationY.set(
83
+ withTiming(slot.translationY.value + exitOffset, { duration: EXIT_DURATION, easing: EASING })
84
+ );
85
+ scheduleOnRN(ref.dismiss);
86
+ } else {
87
+ slot.translationY.set(withTiming(0, { duration: SPRING_BACK_DURATION, easing: EASING }));
88
+ }
89
+ });
90
+
91
+ const registerTopToast = useCallback(
92
+ (values: TopToastRef | null) => {
93
+ topToastMutable.set(values);
94
+ },
95
+ [topToastMutable]
96
+ );
97
+
98
+ if (visibleToasts.length === 0) return null;
99
+
100
+ const inset = isBottom ? bottom : top;
101
+ const positionStyle = isBottom ? { bottom: inset + theme.offset + 2 } : { top: inset + theme.offset + 2 };
102
+
103
+ return (
104
+ <GestureDetector gesture={panGesture}>
105
+ <View style={[styles.container, positionStyle]} pointerEvents="box-none">
106
+ {toastsWithIndex.map(({ toast, index }) => (
107
+ <MemoizedToastItem
108
+ key={toast.id}
109
+ toast={toast}
110
+ index={index}
111
+ theme={theme}
112
+ position={theme.position}
113
+ isTopToast={index === 0}
114
+ registerTopToast={registerTopToast}
115
+ />
116
+ ))}
117
+ </View>
118
+ </GestureDetector>
119
+ );
120
+ };
121
+
122
+ const ToastItem = ({ toast, index, theme, position, isTopToast, registerTopToast }: ToastItemProps) => {
123
+ const [slotIdx] = useState(() => getSlotIndex(toast.id));
124
+ const slot = animationPool[slotIdx];
125
+ const tracker = slotTrackers[slotIdx];
126
+
127
+ const isBottom = position === "bottom";
128
+ const entryFromY = isBottom ? ENTRY_OFFSET : -ENTRY_OFFSET;
129
+ const exitToY = isBottom ? EXIT_OFFSET : -EXIT_OFFSET;
130
+
131
+ const [wasLoading, setWasLoading] = useState(toast.type === "loading");
132
+ const [showIcon, setShowIcon] = useState(false);
133
+
134
+ // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect
135
+ useEffect(() => {
136
+ slot.progress.set(0);
137
+ slot.translationY.set(0);
138
+ slot.stackIndex.set(index);
139
+ slot.progress.set(withTiming(1, { duration: ENTRY_DURATION, easing: EASING }));
140
+
141
+ const iconTimeout = setTimeout(() => setShowIcon(true), 50);
142
+
143
+ return () => {
144
+ clearTimeout(iconTimeout);
145
+ releaseSlot(toast.id);
146
+ };
147
+ }, []);
148
+
149
+ const dismissToast = useCallback(() => {
150
+ toastStore.hide(toast.id);
151
+ }, [toast.id]);
152
+
153
+ useEffect(() => {
154
+ let loadingTimeout: ReturnType<typeof setTimeout> | null = null;
155
+
156
+ if (toast.isExiting && !tracker.wasExiting) {
157
+ tracker.wasExiting = true;
158
+ slot.progress.set(withTiming(0, { duration: EXIT_DURATION, easing: EASING }));
159
+ slot.translationY.set(withTiming(exitToY, { duration: EXIT_DURATION, easing: EASING }));
160
+ }
161
+
162
+ if (tracker.initialized && index !== tracker.prevIndex) {
163
+ slot.stackIndex.set(withTiming(index, { duration: STACK_TRANSITION_DURATION, easing: EASING }));
164
+ }
165
+ tracker.prevIndex = index;
166
+ tracker.initialized = true;
167
+
168
+ if (toast.type === "loading") {
169
+ setWasLoading(true);
170
+ } else if (wasLoading) {
171
+ loadingTimeout = setTimeout(() => setWasLoading(false), ICON_ANIMATION_DURATION + 50);
172
+ }
173
+
174
+ if (isTopToast) {
175
+ registerTopToast({ slot: slot as AnimSlot, dismiss: dismissToast });
176
+ }
177
+
178
+ return () => {
179
+ if (loadingTimeout) clearTimeout(loadingTimeout);
180
+ if (isTopToast) registerTopToast(null);
181
+ };
182
+ }, [
183
+ toast.isExiting,
184
+ index,
185
+ slot,
186
+ tracker,
187
+ exitToY,
188
+ toast.type,
189
+ wasLoading,
190
+ isTopToast,
191
+ registerTopToast,
192
+ dismissToast,
193
+ ]);
194
+
195
+ const shouldAnimateIcon = wasLoading && toast.type !== "loading";
196
+
197
+ const animatedStyle = useAnimatedStyle(() => {
198
+ const baseTranslateY = interpolate(slot.progress.value, [0, 1], [entryFromY, 0]);
199
+ const stackOffsetY = isBottom
200
+ ? slot.stackIndex.value * STACK_OFFSET_PER_ITEM
201
+ : slot.stackIndex.value * -STACK_OFFSET_PER_ITEM;
202
+ const stackScale = 1 - slot.stackIndex.value * STACK_SCALE_PER_ITEM;
203
+
204
+ const finalTranslateY = baseTranslateY + slot.translationY.value + stackOffsetY;
205
+
206
+ const progressOpacity = interpolate(slot.progress.value, [0, 1], [0, 1]);
207
+ const dismissDirection = isBottom ? slot.translationY.value : -slot.translationY.value;
208
+ const dragOpacity = dismissDirection > 0 ? interpolate(dismissDirection, [0, 130], [1, 0], "clamp") : 1;
209
+ const opacity = progressOpacity * dragOpacity;
210
+
211
+ const dragScale = interpolate(Math.abs(slot.translationY.value), [0, 50], [1, 0.98], "clamp");
212
+ const scale = stackScale * dragScale;
213
+
214
+ return {
215
+ transform: [{ translateY: finalTranslateY }, { scale }],
216
+ opacity,
217
+ zIndex: 1000 - Math.round(slot.stackIndex.value),
218
+ };
219
+ });
220
+
221
+ const { options } = toast;
222
+ const colors = theme.colors[toast.type];
223
+
224
+ if (options?.customContent !== undefined) {
225
+ const content = options.customContent;
226
+ return (
227
+ <Animated.View
228
+ style={[
229
+ styles.toast,
230
+ styles.customContentToast,
231
+ isBottom ? styles.toastBottom : styles.toastTop,
232
+ { backgroundColor: colors.background },
233
+ theme.toastStyle,
234
+ options.style,
235
+ animatedStyle,
236
+ ]}
237
+ >
238
+ {typeof content === "function"
239
+ ? (content as CustomContentRenderFn)({
240
+ id: toast.id,
241
+ dismiss: dismissToast,
242
+ type: toast.type,
243
+ isExiting: !!toast.isExiting,
244
+ })
245
+ : content}
246
+ </Animated.View>
247
+ );
248
+ }
249
+
250
+ const shouldShowCloseButton = toast.type !== "loading" && (options?.showCloseButton ?? theme.showCloseButton);
251
+
252
+ return (
253
+ <Animated.View
254
+ style={[
255
+ styles.toast,
256
+ isBottom ? styles.toastBottom : styles.toastTop,
257
+ { backgroundColor: colors.background },
258
+ theme.rtl && styles.rtl,
259
+ theme.toastStyle,
260
+ options?.style,
261
+ animatedStyle,
262
+ ]}
263
+ >
264
+ <View style={styles.iconContainer}>
265
+ {showIcon &&
266
+ (shouldAnimateIcon ? (
267
+ <AnimatedIcon
268
+ key={toast.type}
269
+ type={toast.type}
270
+ color={colors.accent}
271
+ custom={options?.icon}
272
+ config={theme.icons[toast.type]}
273
+ />
274
+ ) : (
275
+ resolveIcon(toast.type, colors.accent, options?.icon, theme.icons[toast.type])
276
+ ))}
277
+ </View>
278
+ <View style={styles.textContainer}>
279
+ <Text
280
+ maxFontSizeMultiplier={1.35}
281
+ allowFontScaling={false}
282
+ style={[
283
+ styles.title,
284
+ { color: colors.accent },
285
+ theme.rtl && { textAlign: "right" },
286
+ theme.titleStyle,
287
+ options?.titleStyle,
288
+ ]}
289
+ >
290
+ {toast.title}
291
+ </Text>
292
+ {toast.description && (
293
+ <Text
294
+ allowFontScaling={false}
295
+ maxFontSizeMultiplier={1.35}
296
+ style={[
297
+ styles.description,
298
+ theme.rtl && { textAlign: "right" },
299
+ theme.descriptionStyle,
300
+ options?.descriptionStyle,
301
+ ]}
302
+ >
303
+ {toast.description}
304
+ </Text>
305
+ )}
306
+ </View>
307
+ {shouldShowCloseButton && (
308
+ <Pressable style={styles.closeButton} onPress={dismissToast} hitSlop={12}>
309
+ <CloseIcon width={20} height={20} />
310
+ </Pressable>
311
+ )}
312
+ </Animated.View>
313
+ );
314
+ };
315
+
316
+ const MemoizedToastItem = memo(ToastItem, (prev, next) => {
317
+ return (
318
+ prev.toast.id === next.toast.id &&
319
+ prev.toast.type === next.toast.type &&
320
+ prev.toast.title === next.toast.title &&
321
+ prev.toast.description === next.toast.description &&
322
+ prev.toast.isExiting === next.toast.isExiting &&
323
+ prev.index === next.index &&
324
+ prev.position === next.position &&
325
+ prev.theme === next.theme &&
326
+ prev.isTopToast === next.isTopToast
327
+ );
328
+ });
329
+
330
+ const styles = StyleSheet.create({
331
+ container: {
332
+ position: "absolute",
333
+ left: 16,
334
+ right: 16,
335
+ zIndex: 1000,
336
+ },
337
+ toast: {
338
+ flexDirection: "row",
339
+ alignItems: "center",
340
+ gap: 12,
341
+ minHeight: 36,
342
+ borderRadius: 20,
343
+ borderCurve: "continuous",
344
+ position: "absolute",
345
+ left: 0,
346
+ right: 0,
347
+ paddingHorizontal: 12,
348
+ paddingVertical: 10,
349
+ shadowColor: "#000",
350
+ shadowOffset: { width: 0, height: 8 },
351
+ shadowOpacity: 0.05,
352
+ shadowRadius: 24,
353
+ elevation: 8,
354
+ },
355
+ customContentToast: {
356
+ padding: 0,
357
+ paddingHorizontal: 0,
358
+ paddingVertical: 0,
359
+ overflow: "hidden",
360
+ },
361
+ rtl: {
362
+ flexDirection: "row-reverse",
363
+ },
364
+ toastTop: {
365
+ top: 0,
366
+ },
367
+ toastBottom: {
368
+ bottom: 0,
369
+ },
370
+ iconContainer: {
371
+ width: 48,
372
+ height: 48,
373
+ alignItems: "center",
374
+ justifyContent: "center",
375
+ marginLeft: 8,
376
+ },
377
+ textContainer: {
378
+ flex: 1,
379
+ gap: 1,
380
+ justifyContent: "center",
381
+ },
382
+ title: {
383
+ fontSize: 14,
384
+ fontWeight: "700",
385
+ lineHeight: 20,
386
+ },
387
+ description: {
388
+ color: "#6B7280",
389
+ fontSize: 12,
390
+ fontWeight: "500",
391
+ lineHeight: 16,
392
+ },
393
+ closeButton: {
394
+ padding: 4,
395
+ alignItems: "center",
396
+ justifyContent: "center",
397
+ },
398
+ });