@octanejs/sonner 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,6 @@
1
+ import { toast as toastState } from './state';
2
+ import type { ToasterProps, ToastT } from './types';
3
+
4
+ export const toast: typeof toastState;
5
+ export function Toaster(props: ToasterProps): any;
6
+ export function useSonner(): { toasts: ToastT[] };
package/src/state.ts ADDED
@@ -0,0 +1,292 @@
1
+ // Ported from sonner@2.0.7 src/state.ts
2
+ // (https://github.com/emilkowalski/sonner/tree/v2.0.7).
3
+ // The observer and promise state machine are renderer-independent. Octane's
4
+ // isValidElement replaces React.isValidElement for renderable result checks.
5
+ import { isValidElement } from 'octane';
6
+ import type {
7
+ ExternalToast,
8
+ PromiseData,
9
+ PromiseIExtendedResult,
10
+ PromiseT,
11
+ ToastContent,
12
+ ToastElement,
13
+ ToastT,
14
+ ToastToDismiss,
15
+ ToastTypes,
16
+ } from './types';
17
+
18
+ let toastsCounter = 1;
19
+
20
+ const getToastId = (id?: number | string): number | string =>
21
+ typeof id === 'number' || (typeof id === 'string' && id.length > 0) ? id : toastsCounter++;
22
+
23
+ type Title = (() => ToastContent) | ToastContent;
24
+ type PublishedToast = ToastT | ToastToDismiss;
25
+ type PromiseToastResult<ToastData> =
26
+ | (string & { unwrap: () => Promise<ToastData> })
27
+ | (number & { unwrap: () => Promise<ToastData> })
28
+ | { unwrap: () => Promise<ToastData> };
29
+
30
+ class Observer {
31
+ subscribers: Array<(toast: PublishedToast) => void>;
32
+ toasts: Array<PublishedToast>;
33
+ dismissedToasts: Set<string | number>;
34
+
35
+ constructor() {
36
+ this.subscribers = [];
37
+ this.toasts = [];
38
+ this.dismissedToasts = new Set();
39
+ }
40
+
41
+ // Arrow properties preserve the Observer receiver when methods are attached
42
+ // directly to the callable toast function.
43
+ subscribe = (subscriber: (toast: PublishedToast) => void): (() => void) => {
44
+ this.subscribers.push(subscriber);
45
+
46
+ return () => {
47
+ const index = this.subscribers.indexOf(subscriber);
48
+ if (index !== -1) this.subscribers.splice(index, 1);
49
+ };
50
+ };
51
+
52
+ publish = (data: ToastT): void => {
53
+ this.subscribers.forEach((subscriber) => subscriber(data));
54
+ };
55
+
56
+ addToast = (data: ToastT): void => {
57
+ this.dismissedToasts.delete(data.id);
58
+ this.publish(data);
59
+ this.toasts = [...this.toasts, data];
60
+ };
61
+
62
+ create = (
63
+ data: ExternalToast & {
64
+ message?: Title;
65
+ type?: ToastTypes;
66
+ promise?: PromiseT;
67
+ jsx?: ToastElement;
68
+ },
69
+ ): number | string => {
70
+ const { message, ...rest } = data;
71
+ const id = getToastId(data.id);
72
+ const alreadyExists = this.toasts.find((toast) => toast.id === id);
73
+ const dismissible = data.dismissible === undefined ? true : data.dismissible;
74
+
75
+ if (this.dismissedToasts.has(id)) {
76
+ this.dismissedToasts.delete(id);
77
+ }
78
+
79
+ if (alreadyExists && !('dismiss' in alreadyExists)) {
80
+ this.toasts = this.toasts.map((toast) => {
81
+ if (toast.id === id && !('dismiss' in toast)) {
82
+ this.publish({ ...toast, ...data, id, title: message });
83
+ return {
84
+ ...toast,
85
+ ...data,
86
+ id,
87
+ dismissible,
88
+ title: message,
89
+ };
90
+ }
91
+
92
+ return toast;
93
+ });
94
+ } else {
95
+ this.addToast({ title: message, ...rest, dismissible, id });
96
+ }
97
+
98
+ return id;
99
+ };
100
+
101
+ dismiss = (id?: number | string): number | string => {
102
+ if (id !== undefined) {
103
+ this.dismissedToasts.add(id);
104
+ const publishDismiss = (): void => {
105
+ this.subscribers.forEach((subscriber) => subscriber({ id, dismiss: true }));
106
+ };
107
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(publishDismiss);
108
+ else publishDismiss();
109
+ } else {
110
+ this.toasts.forEach((toast) => {
111
+ this.dismissedToasts.add(toast.id);
112
+ this.subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true }));
113
+ });
114
+ }
115
+
116
+ // Sonner's public 2.0.7 declaration returns the id even though a global
117
+ // dismiss has no id at runtime. Preserve that published type contract.
118
+ return id as number | string;
119
+ };
120
+
121
+ message = (message: Title, data?: ExternalToast): number | string =>
122
+ this.create({ ...data, message });
123
+
124
+ error = (message: Title, data?: ExternalToast): number | string =>
125
+ this.create({ ...data, message, type: 'error' });
126
+
127
+ success = (message: Title, data?: ExternalToast): number | string =>
128
+ this.create({ ...data, type: 'success', message });
129
+
130
+ info = (message: Title, data?: ExternalToast): number | string =>
131
+ this.create({ ...data, type: 'info', message });
132
+
133
+ warning = (message: Title, data?: ExternalToast): number | string =>
134
+ this.create({ ...data, type: 'warning', message });
135
+
136
+ loading = (message: Title, data?: ExternalToast): number | string =>
137
+ this.create({ ...data, type: 'loading', message });
138
+
139
+ promise = <ToastData>(
140
+ promise: PromiseT<ToastData>,
141
+ data?: PromiseData<ToastData>,
142
+ ): PromiseToastResult<ToastData> => {
143
+ if (!data) return undefined as unknown as PromiseToastResult<ToastData>;
144
+
145
+ let id: string | number | undefined;
146
+ if (data.loading !== undefined) {
147
+ id = this.create({
148
+ ...data,
149
+ promise,
150
+ type: 'loading',
151
+ message: data.loading,
152
+ description: typeof data.description !== 'function' ? data.description : undefined,
153
+ });
154
+ }
155
+
156
+ const pending = Promise.resolve(promise instanceof Function ? promise() : promise);
157
+ let shouldDismiss = id !== undefined;
158
+ let result!: ['resolve', ToastData] | ['reject', unknown];
159
+
160
+ const originalPromise = pending
161
+ .then(async (response) => {
162
+ result = ['resolve', response];
163
+ if (isValidElement(response)) {
164
+ shouldDismiss = false;
165
+ this.create({ id, type: 'default', message: response });
166
+ } else if (isHttpResponse(response) && !response.ok) {
167
+ shouldDismiss = false;
168
+ const error = `HTTP error! status: ${response.status}`;
169
+ const promiseData =
170
+ typeof data.error === 'function' ? await data.error(error) : data.error;
171
+ const description =
172
+ typeof data.description === 'function'
173
+ ? await data.description(error)
174
+ : data.description;
175
+ const isExtendedResult =
176
+ typeof promiseData === 'object' && promiseData !== null && !isValidElement(promiseData);
177
+ const toastSettings: PromiseIExtendedResult = isExtendedResult
178
+ ? (promiseData as PromiseIExtendedResult)
179
+ : { message: promiseData };
180
+ this.create({ id, type: 'error', description, ...toastSettings });
181
+ } else if (response instanceof Error) {
182
+ shouldDismiss = false;
183
+ const promiseData =
184
+ typeof data.error === 'function' ? await data.error(response) : data.error;
185
+ const description =
186
+ typeof data.description === 'function'
187
+ ? await data.description(response)
188
+ : data.description;
189
+ const isExtendedResult =
190
+ typeof promiseData === 'object' && promiseData !== null && !isValidElement(promiseData);
191
+ const toastSettings: PromiseIExtendedResult = isExtendedResult
192
+ ? (promiseData as PromiseIExtendedResult)
193
+ : { message: promiseData };
194
+ this.create({ id, type: 'error', description, ...toastSettings });
195
+ } else if (data.success !== undefined) {
196
+ shouldDismiss = false;
197
+ const promiseData =
198
+ typeof data.success === 'function' ? await data.success(response) : data.success;
199
+ const description =
200
+ typeof data.description === 'function'
201
+ ? await data.description(response)
202
+ : data.description;
203
+ const isExtendedResult =
204
+ typeof promiseData === 'object' && promiseData !== null && !isValidElement(promiseData);
205
+ const toastSettings: PromiseIExtendedResult = isExtendedResult
206
+ ? (promiseData as PromiseIExtendedResult)
207
+ : { message: promiseData };
208
+ this.create({ id, type: 'success', description, ...toastSettings });
209
+ }
210
+ })
211
+ .catch(async (error) => {
212
+ result = ['reject', error];
213
+ if (data.error !== undefined) {
214
+ shouldDismiss = false;
215
+ const promiseData =
216
+ typeof data.error === 'function' ? await data.error(error) : data.error;
217
+ const description =
218
+ typeof data.description === 'function'
219
+ ? await data.description(error)
220
+ : data.description;
221
+ const isExtendedResult =
222
+ typeof promiseData === 'object' && promiseData !== null && !isValidElement(promiseData);
223
+ const toastSettings: PromiseIExtendedResult = isExtendedResult
224
+ ? (promiseData as PromiseIExtendedResult)
225
+ : { message: promiseData };
226
+ this.create({ id, type: 'error', description, ...toastSettings });
227
+ }
228
+ })
229
+ .finally(() => {
230
+ if (shouldDismiss) {
231
+ this.dismiss(id);
232
+ id = undefined;
233
+ }
234
+
235
+ data.finally?.();
236
+ });
237
+
238
+ const unwrap = (): Promise<ToastData> =>
239
+ new Promise((resolve, reject) => {
240
+ originalPromise
241
+ .then(() => (result[0] === 'reject' ? reject(result[1]) : resolve(result[1])))
242
+ .catch(reject);
243
+ });
244
+
245
+ if (typeof id !== 'string' && typeof id !== 'number') return { unwrap };
246
+ return Object.assign(id, { unwrap }) as PromiseToastResult<ToastData>;
247
+ };
248
+
249
+ custom = (jsx: (id: number | string) => ToastElement, data?: ExternalToast): number | string => {
250
+ const id = getToastId(data?.id);
251
+ this.create({ jsx: jsx(id), ...data, id });
252
+ return id;
253
+ };
254
+
255
+ getActiveToasts = (): PublishedToast[] =>
256
+ this.toasts.filter((toast) => !this.dismissedToasts.has(toast.id));
257
+ }
258
+
259
+ export const ToastState = new Observer();
260
+
261
+ const toastFunction = (message: Title, data?: ExternalToast): number | string => {
262
+ return ToastState.create({ ...data, message });
263
+ };
264
+
265
+ const isHttpResponse = (data: any): data is Response =>
266
+ Boolean(
267
+ data &&
268
+ typeof data === 'object' &&
269
+ 'ok' in data &&
270
+ typeof data.ok === 'boolean' &&
271
+ 'status' in data &&
272
+ typeof data.status === 'number',
273
+ );
274
+
275
+ const getHistory = (): PublishedToast[] => ToastState.toasts;
276
+ const getToasts = (): PublishedToast[] => ToastState.getActiveToasts();
277
+
278
+ export const toast = Object.assign(
279
+ toastFunction,
280
+ {
281
+ success: ToastState.success,
282
+ info: ToastState.info,
283
+ warning: ToastState.warning,
284
+ error: ToastState.error,
285
+ custom: ToastState.custom,
286
+ message: ToastState.message,
287
+ promise: ToastState.promise,
288
+ dismiss: ToastState.dismiss,
289
+ loading: ToastState.loading,
290
+ },
291
+ { getHistory, getToasts },
292
+ );