react-native-sooner 1.0.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.
Files changed (80) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +376 -0
  3. package/lib/module/constants.js +40 -0
  4. package/lib/module/constants.js.map +1 -0
  5. package/lib/module/context.js +25 -0
  6. package/lib/module/context.js.map +1 -0
  7. package/lib/module/easings.js +9 -0
  8. package/lib/module/easings.js.map +1 -0
  9. package/lib/module/gestures.js +119 -0
  10. package/lib/module/gestures.js.map +1 -0
  11. package/lib/module/hooks.js +9 -0
  12. package/lib/module/hooks.js.map +1 -0
  13. package/lib/module/icons.js +332 -0
  14. package/lib/module/icons.js.map +1 -0
  15. package/lib/module/index.js +13 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/module/state.js +200 -0
  19. package/lib/module/state.js.map +1 -0
  20. package/lib/module/theme.js +189 -0
  21. package/lib/module/theme.js.map +1 -0
  22. package/lib/module/toast.js +362 -0
  23. package/lib/module/toast.js.map +1 -0
  24. package/lib/module/toaster.js +198 -0
  25. package/lib/module/toaster.js.map +1 -0
  26. package/lib/module/types.js +4 -0
  27. package/lib/module/types.js.map +1 -0
  28. package/lib/module/use-app-state.js +13 -0
  29. package/lib/module/use-app-state.js.map +1 -0
  30. package/lib/module/use-pauseable-timer.js +18 -0
  31. package/lib/module/use-pauseable-timer.js.map +1 -0
  32. package/lib/module/use-toast-state.js +37 -0
  33. package/lib/module/use-toast-state.js.map +1 -0
  34. package/lib/typescript/package.json +1 -0
  35. package/lib/typescript/src/constants.d.ts +32 -0
  36. package/lib/typescript/src/constants.d.ts.map +1 -0
  37. package/lib/typescript/src/context.d.ts +20 -0
  38. package/lib/typescript/src/context.d.ts.map +1 -0
  39. package/lib/typescript/src/easings.d.ts +6 -0
  40. package/lib/typescript/src/easings.d.ts.map +1 -0
  41. package/lib/typescript/src/gestures.d.ts +17 -0
  42. package/lib/typescript/src/gestures.d.ts.map +1 -0
  43. package/lib/typescript/src/hooks.d.ts +5 -0
  44. package/lib/typescript/src/hooks.d.ts.map +1 -0
  45. package/lib/typescript/src/icons.d.ts +15 -0
  46. package/lib/typescript/src/icons.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +12 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -0
  49. package/lib/typescript/src/state.d.ts +66 -0
  50. package/lib/typescript/src/state.d.ts.map +1 -0
  51. package/lib/typescript/src/theme.d.ts +163 -0
  52. package/lib/typescript/src/theme.d.ts.map +1 -0
  53. package/lib/typescript/src/toast.d.ts +3 -0
  54. package/lib/typescript/src/toast.d.ts.map +1 -0
  55. package/lib/typescript/src/toaster.d.ts +3 -0
  56. package/lib/typescript/src/toaster.d.ts.map +1 -0
  57. package/lib/typescript/src/types.d.ts +264 -0
  58. package/lib/typescript/src/types.d.ts.map +1 -0
  59. package/lib/typescript/src/use-app-state.d.ts +3 -0
  60. package/lib/typescript/src/use-app-state.d.ts.map +1 -0
  61. package/lib/typescript/src/use-pauseable-timer.d.ts +2 -0
  62. package/lib/typescript/src/use-pauseable-timer.d.ts.map +1 -0
  63. package/lib/typescript/src/use-toast-state.d.ts +7 -0
  64. package/lib/typescript/src/use-toast-state.d.ts.map +1 -0
  65. package/package.json +152 -0
  66. package/src/constants.ts +44 -0
  67. package/src/context.tsx +38 -0
  68. package/src/easings.ts +7 -0
  69. package/src/gestures.tsx +135 -0
  70. package/src/hooks.ts +3 -0
  71. package/src/icons.tsx +227 -0
  72. package/src/index.tsx +48 -0
  73. package/src/state.ts +262 -0
  74. package/src/theme.ts +170 -0
  75. package/src/toast.tsx +429 -0
  76. package/src/toaster.tsx +221 -0
  77. package/src/types.ts +311 -0
  78. package/src/use-app-state.ts +15 -0
  79. package/src/use-pauseable-timer.ts +24 -0
  80. package/src/use-toast-state.ts +43 -0
package/src/state.ts ADDED
@@ -0,0 +1,262 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ DISMISSED_CACHE_CLEANUP_THRESHOLD,
4
+ DISMISSED_CACHE_MAX_SIZE,
5
+ } from "./constants";
6
+ import type {
7
+ ExternalToast,
8
+ PromiseData,
9
+ ToastDismissSubscriber,
10
+ ToastStateSubscriber,
11
+ ToastT,
12
+ ToastType,
13
+ ToastUpdateSubscriber,
14
+ UpdateToastOptions,
15
+ } from "./types";
16
+
17
+ let toastCount = 0;
18
+
19
+ function generateId(): string {
20
+ toastCount = (toastCount + 1) % Number.MAX_SAFE_INTEGER;
21
+ return `sooner-${toastCount}-${Date.now()}`;
22
+ }
23
+
24
+ class ToastStateManager {
25
+ private toasts: ToastT[] = [];
26
+ private subscribers: Set<ToastStateSubscriber> = new Set();
27
+ private dismissSubscribers: Set<ToastDismissSubscriber> = new Set();
28
+ private updateSubscribers: Set<ToastUpdateSubscriber> = new Set();
29
+ private dismissedToasts: Map<string | number, number> = new Map();
30
+
31
+ subscribe(callback: ToastStateSubscriber): () => void {
32
+ this.subscribers.add(callback);
33
+ return () => this.subscribers.delete(callback);
34
+ }
35
+
36
+ subscribeToDismiss(callback: ToastDismissSubscriber): () => void {
37
+ this.dismissSubscribers.add(callback);
38
+ return () => this.dismissSubscribers.delete(callback);
39
+ }
40
+
41
+ subscribeToUpdate(callback: ToastUpdateSubscriber): () => void {
42
+ this.updateSubscribers.add(callback);
43
+ return () => this.updateSubscribers.delete(callback);
44
+ }
45
+
46
+ private publish(toast: ToastT): void {
47
+ this.subscribers.forEach((subscriber) => subscriber(toast));
48
+ }
49
+
50
+ private publishDismiss(toastId: string | number | undefined): void {
51
+ this.dismissSubscribers.forEach((subscriber) => subscriber(toastId));
52
+ }
53
+
54
+ private publishUpdate(toast: ToastT): void {
55
+ this.updateSubscribers.forEach((subscriber) => subscriber(toast));
56
+ }
57
+
58
+ private cleanupDismissedCache(): void {
59
+ if (this.dismissedToasts.size <= DISMISSED_CACHE_MAX_SIZE) {
60
+ return;
61
+ }
62
+
63
+ // Sort by timestamp (oldest first) and remove oldest entries
64
+ const entries = Array.from(this.dismissedToasts.entries());
65
+ entries.sort((a, b) => a[1] - b[1]);
66
+
67
+ const toRemove = entries.slice(0, DISMISSED_CACHE_CLEANUP_THRESHOLD);
68
+ toRemove.forEach(([id]) => this.dismissedToasts.delete(id));
69
+ }
70
+
71
+ create(
72
+ title: string | ReactNode,
73
+ type: ToastType = "default",
74
+ options?: ExternalToast
75
+ ): string | number {
76
+ const id = options?.id ?? generateId();
77
+
78
+ // Skip if this toast was recently dismissed (prevents re-creation loops)
79
+ if (this.dismissedToasts.has(id)) {
80
+ return id;
81
+ }
82
+
83
+ const toast: ToastT = {
84
+ ...options,
85
+ id,
86
+ type,
87
+ title,
88
+ createdAt: Date.now(),
89
+ };
90
+
91
+ // Update existing toast or add new one
92
+ const existingIndex = this.toasts.findIndex((t) => t.id === id);
93
+ if (existingIndex > -1) {
94
+ this.toasts[existingIndex] = toast;
95
+ this.publishUpdate(toast);
96
+ } else {
97
+ this.toasts.push(toast);
98
+ this.publish(toast);
99
+ }
100
+
101
+ return id;
102
+ }
103
+
104
+ update(toastId: string | number, options: UpdateToastOptions): boolean {
105
+ const index = this.toasts.findIndex((t) => t.id === toastId);
106
+ if (index === -1) {
107
+ return false;
108
+ }
109
+
110
+ const existingToast = this.toasts[index]!;
111
+ const updatedToast: ToastT = {
112
+ ...existingToast,
113
+ ...options,
114
+ id: toastId, // Ensure ID isn't changed
115
+ type: options.type ?? existingToast.type, // Ensure type is always defined
116
+ title: options.title ?? existingToast.title, // Ensure title is always defined
117
+ createdAt: existingToast.createdAt, // Preserve original timestamp
118
+ };
119
+
120
+ this.toasts[index] = updatedToast;
121
+ this.publishUpdate(updatedToast);
122
+ return true;
123
+ }
124
+
125
+ dismiss(toastId?: string | number): void {
126
+ const now = Date.now();
127
+
128
+ if (toastId === undefined) {
129
+ for (const toast of this.toasts) {
130
+ this.dismissedToasts.set(toast.id, now);
131
+ toast.onDismiss?.(toast);
132
+ }
133
+ this.toasts = [];
134
+ } else {
135
+ const toast = this.toasts.find((t) => t.id === toastId);
136
+ if (toast) {
137
+ toast.onDismiss?.(toast);
138
+ }
139
+ this.dismissedToasts.set(toastId, now);
140
+ this.toasts = this.toasts.filter((t) => t.id !== toastId);
141
+ }
142
+
143
+ this.publishDismiss(toastId);
144
+ this.cleanupDismissedCache();
145
+ }
146
+
147
+ getToasts(): ToastT[] {
148
+ return [...this.toasts];
149
+ }
150
+
151
+ getToast(toastId: string | number): ToastT | undefined {
152
+ return this.toasts.find((t) => t.id === toastId);
153
+ }
154
+
155
+ isActive(toastId: string | number): boolean {
156
+ return this.toasts.some((t) => t.id === toastId);
157
+ }
158
+
159
+ async promise<T>(
160
+ promise: Promise<T> | (() => Promise<T>),
161
+ data: PromiseData<T>,
162
+ options?: ExternalToast
163
+ ): Promise<T> {
164
+ const id = this.create(data.loading, "loading", {
165
+ ...options,
166
+ duration: Number.POSITIVE_INFINITY,
167
+ dismissible: false,
168
+ });
169
+
170
+ const promiseToExecute = typeof promise === "function" ? promise() : promise;
171
+
172
+ try {
173
+ const result = await promiseToExecute;
174
+ const successMessage =
175
+ typeof data.success === "function" ? data.success(result) : data.success;
176
+
177
+ this.update(id, {
178
+ type: "success",
179
+ title: successMessage,
180
+ dismissible: true,
181
+ duration: options?.duration,
182
+ });
183
+
184
+ return result;
185
+ } catch (error) {
186
+ const errorMessage =
187
+ typeof data.error === "function" ? data.error(error) : data.error;
188
+
189
+ this.update(id, {
190
+ type: "error",
191
+ title: errorMessage,
192
+ dismissible: true,
193
+ duration: options?.duration,
194
+ });
195
+
196
+ throw error;
197
+ } finally {
198
+ data.finally?.();
199
+ }
200
+ }
201
+
202
+ clearDismissedHistory(): void {
203
+ this.dismissedToasts.clear();
204
+ }
205
+ }
206
+
207
+ export const ToastState = new ToastStateManager();
208
+
209
+ function createToast(type: ToastType) {
210
+ return (title: string | ReactNode, options?: ExternalToast) =>
211
+ ToastState.create(title, type, options);
212
+ }
213
+
214
+ /**
215
+ * Main toast API
216
+ *
217
+ * @example
218
+ * // Basic usage
219
+ * toast('Hello World');
220
+ *
221
+ * // With variants
222
+ * toast.success('Success!');
223
+ * toast.error('Error!');
224
+ *
225
+ * // With options
226
+ * toast('Message', { duration: 5000 });
227
+ *
228
+ * // Update existing toast
229
+ * const id = toast.loading('Loading...');
230
+ * toast.update(id, { title: 'Done!', type: 'success' });
231
+ *
232
+ * // Promise toast
233
+ * toast.promise(fetchData(), {
234
+ * loading: 'Loading...',
235
+ * success: 'Loaded!',
236
+ * error: 'Failed',
237
+ * });
238
+ */
239
+ export const toast = Object.assign(
240
+ (title: string | ReactNode, options?: ExternalToast) =>
241
+ ToastState.create(title, "default", options),
242
+ {
243
+ success: createToast("success"),
244
+ error: createToast("error"),
245
+ warning: createToast("warning"),
246
+ info: createToast("info"),
247
+ loading: createToast("loading"),
248
+ promise: <T>(
249
+ promise: Promise<T> | (() => Promise<T>),
250
+ data: PromiseData<T>,
251
+ options?: ExternalToast
252
+ ) => ToastState.promise(promise, data, options),
253
+ update: (toastId: string | number, options: UpdateToastOptions) =>
254
+ ToastState.update(toastId, options),
255
+ dismiss: (toastId?: string | number) => ToastState.dismiss(toastId),
256
+ getToasts: () => ToastState.getToasts(),
257
+ getToast: (toastId: string | number) => ToastState.getToast(toastId),
258
+ isActive: (toastId: string | number) => ToastState.isActive(toastId),
259
+ custom: (title: string | ReactNode, options?: ExternalToast) =>
260
+ ToastState.create(title, "default", options),
261
+ }
262
+ );
package/src/theme.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { Appearance, StyleSheet } from "react-native";
2
+ import type { Theme, ToastType } from "./types";
3
+
4
+ export function resolveTheme(theme: Theme): "light" | "dark" {
5
+ if (theme === "system") {
6
+ const scheme = Appearance.getColorScheme();
7
+ return scheme === "dark" ? "dark" : "light";
8
+ }
9
+ return theme;
10
+ }
11
+
12
+ export const colors = {
13
+ light: {
14
+ background: "#ffffff",
15
+ foreground: "#0f172a",
16
+ description: "#64748b",
17
+ border: "#e2e8f0",
18
+ success: "#22c55e",
19
+ error: "#ef4444",
20
+ warning: "#f59e0b",
21
+ info: "#3b82f6",
22
+ loading: "#6b7280",
23
+ },
24
+ dark: {
25
+ background: "#1e293b",
26
+ foreground: "#f8fafc",
27
+ description: "#94a3b8",
28
+ border: "#334155",
29
+ success: "#4ade80",
30
+ error: "#f87171",
31
+ warning: "#fbbf24",
32
+ info: "#60a5fa",
33
+ loading: "#9ca3af",
34
+ },
35
+ } as const;
36
+
37
+ export const richColors = {
38
+ light: {
39
+ success: { background: "#dcfce7", foreground: "#166534", border: "#bbf7d0" },
40
+ error: { background: "#fee2e2", foreground: "#991b1b", border: "#fecaca" },
41
+ warning: { background: "#fef3c7", foreground: "#92400e", border: "#fde68a" },
42
+ info: { background: "#dbeafe", foreground: "#1e40af", border: "#bfdbfe" },
43
+ },
44
+ dark: {
45
+ success: { background: "#166534", foreground: "#dcfce7", border: "#22c55e" },
46
+ error: { background: "#991b1b", foreground: "#fee2e2", border: "#ef4444" },
47
+ warning: { background: "#92400e", foreground: "#fef3c7", border: "#f59e0b" },
48
+ info: { background: "#1e40af", foreground: "#dbeafe", border: "#3b82f6" },
49
+ },
50
+ } as const;
51
+
52
+ export function getIconColor(
53
+ type: ToastType,
54
+ theme: "light" | "dark",
55
+ rich: boolean
56
+ ): string {
57
+ if (type === "default" || type === "loading") {
58
+ return colors[theme].loading;
59
+ }
60
+
61
+ if (rich) {
62
+ const richColor = richColors[theme][type as keyof typeof richColors.light];
63
+ return richColor?.foreground ?? colors[theme][type];
64
+ }
65
+
66
+ return colors[theme][type];
67
+ }
68
+
69
+ export function getToastColors(
70
+ type: ToastType,
71
+ theme: "light" | "dark",
72
+ rich: boolean
73
+ ) {
74
+ const baseColors = colors[theme];
75
+
76
+ if (rich && type !== "default" && type !== "loading") {
77
+ const richColor = richColors[theme][type as keyof typeof richColors.light];
78
+ if (richColor) {
79
+ return {
80
+ background: richColor.background,
81
+ foreground: richColor.foreground,
82
+ description: richColor.foreground,
83
+ border: richColor.border,
84
+ };
85
+ }
86
+ }
87
+
88
+ return {
89
+ background: baseColors.background,
90
+ foreground: baseColors.foreground,
91
+ description: baseColors.description,
92
+ border: baseColors.border,
93
+ };
94
+ }
95
+
96
+ export const baseStyles = StyleSheet.create({
97
+ container: {
98
+ flexDirection: "row",
99
+ alignItems: "center",
100
+ paddingHorizontal: 16,
101
+ paddingVertical: 14,
102
+ borderRadius: 12,
103
+ borderWidth: 1,
104
+ shadowColor: "#000",
105
+ shadowOffset: { width: 0, height: 2 },
106
+ shadowOpacity: 0.1,
107
+ shadowRadius: 8,
108
+ elevation: 4,
109
+ minHeight: 52,
110
+ maxWidth: "100%",
111
+ },
112
+ content: {
113
+ flex: 1,
114
+ marginLeft: 12,
115
+ },
116
+ contentNoIcon: {
117
+ marginLeft: 0,
118
+ },
119
+ title: {
120
+ fontSize: 14,
121
+ fontWeight: "500",
122
+ lineHeight: 20,
123
+ },
124
+ description: {
125
+ fontSize: 13,
126
+ lineHeight: 18,
127
+ marginTop: 2,
128
+ },
129
+ actionsContainer: {
130
+ flexDirection: "row",
131
+ alignItems: "center",
132
+ marginLeft: 12,
133
+ gap: 8,
134
+ },
135
+ actionButton: {
136
+ paddingHorizontal: 12,
137
+ paddingVertical: 6,
138
+ borderRadius: 6,
139
+ backgroundColor: "#0f172a",
140
+ },
141
+ actionButtonText: {
142
+ fontSize: 13,
143
+ fontWeight: "500",
144
+ color: "#ffffff",
145
+ },
146
+ cancelButton: {
147
+ paddingHorizontal: 12,
148
+ paddingVertical: 6,
149
+ borderRadius: 6,
150
+ backgroundColor: "transparent",
151
+ borderWidth: 1,
152
+ borderColor: "#e2e8f0",
153
+ },
154
+ cancelButtonText: {
155
+ fontSize: 13,
156
+ fontWeight: "500",
157
+ color: "#64748b",
158
+ },
159
+ closeButton: {
160
+ marginLeft: 8,
161
+ padding: 4,
162
+ borderRadius: 4,
163
+ },
164
+ iconContainer: {
165
+ width: 20,
166
+ height: 20,
167
+ justifyContent: "center",
168
+ alignItems: "center",
169
+ },
170
+ });