@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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/package.json +51 -0
- package/src/assets.tsrx +103 -0
- package/src/hooks.tsrx +18 -0
- package/src/index.ts +8 -0
- package/src/sonner.tsrx +831 -0
- package/src/sonner.tsrx.d.ts +6 -0
- package/src/state.ts +292 -0
- package/src/styles.css +756 -0
- package/src/types.ts +226 -0
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
|
+
);
|