@opentui-ui/toast 0.0.1
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/README.md +660 -0
- package/dist/icons-DXx_5j_z.mjs +181 -0
- package/dist/icons-DXx_5j_z.mjs.map +1 -0
- package/dist/icons.d.mts +116 -0
- package/dist/icons.d.mts.map +1 -0
- package/dist/icons.mjs +3 -0
- package/dist/index.d.mts +53 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/react.d.mts +13 -0
- package/dist/react.d.mts.map +1 -0
- package/dist/react.mjs +14 -0
- package/dist/react.mjs.map +1 -0
- package/dist/state-CoqnQZsz.d.mts +157 -0
- package/dist/state-CoqnQZsz.d.mts.map +1 -0
- package/dist/themes.d.mts +69 -0
- package/dist/themes.d.mts.map +1 -0
- package/dist/themes.mjs +118 -0
- package/dist/themes.mjs.map +1 -0
- package/dist/toaster-CQ5RySDh.mjs +1118 -0
- package/dist/toaster-CQ5RySDh.mjs.map +1 -0
- package/dist/types-BnTc6iEw.d.mts +342 -0
- package/dist/types-BnTc6iEw.d.mts.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { c as getTypeIcon, l as isAction, n as DEFAULT_ICONS, o as getLoadingIcon, s as getSpinnerConfig } from "./icons-DXx_5j_z.mjs";
|
|
2
|
+
import { BoxRenderable, TextAttributes, TextRenderable, parseColor } from "@opentui/core";
|
|
3
|
+
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
/**
|
|
6
|
+
* Common toast duration presets (in milliseconds)
|
|
7
|
+
*
|
|
8
|
+
* Use these for consistent, readable duration values across your app.
|
|
9
|
+
*
|
|
10
|
+
* | Preset | Duration | Use Case |
|
|
11
|
+
* |--------------|------------|---------------------------|
|
|
12
|
+
* | `SHORT` | 2000ms | Brief confirmations |
|
|
13
|
+
* | `DEFAULT` | 4000ms | Standard notifications |
|
|
14
|
+
* | `LONG` | 6000ms | Important messages |
|
|
15
|
+
* | `EXTENDED` | 10000ms | Critical information |
|
|
16
|
+
* | `PERSISTENT` | Infinity | Requires manual dismissal |
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { toast, TOAST_DURATION } from '@opentui-ui/toast';
|
|
21
|
+
*
|
|
22
|
+
* // Quick confirmation
|
|
23
|
+
* toast.success('Copied!', { duration: TOAST_DURATION.SHORT });
|
|
24
|
+
*
|
|
25
|
+
* // Important warning
|
|
26
|
+
* toast.warning('Check your settings', { duration: TOAST_DURATION.LONG });
|
|
27
|
+
*
|
|
28
|
+
* // Critical error that requires acknowledgment
|
|
29
|
+
* toast.error('Connection lost', { duration: TOAST_DURATION.PERSISTENT });
|
|
30
|
+
*
|
|
31
|
+
* // Set as default for all toasts
|
|
32
|
+
* const toaster = new ToasterRenderable(ctx, {
|
|
33
|
+
* toastOptions: { duration: TOAST_DURATION.LONG },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
const TOAST_DURATION = {
|
|
38
|
+
SHORT: 2e3,
|
|
39
|
+
DEFAULT: 4e3,
|
|
40
|
+
LONG: 6e3,
|
|
41
|
+
EXTENDED: 1e4,
|
|
42
|
+
PERSISTENT: Infinity
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Time to wait before unmounting a dismissed toast (ms)
|
|
46
|
+
* This allows for any exit effects
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
const TIME_BEFORE_UNMOUNT = 200;
|
|
51
|
+
/**
|
|
52
|
+
* Default toast width (in terminal columns)
|
|
53
|
+
*
|
|
54
|
+
* @internal - Users should set maxWidth in ToasterOptions instead
|
|
55
|
+
*/
|
|
56
|
+
const TOAST_WIDTH = 60;
|
|
57
|
+
/**
|
|
58
|
+
* Default offset from screen edges
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
const DEFAULT_OFFSET = {
|
|
63
|
+
top: 1,
|
|
64
|
+
right: 2,
|
|
65
|
+
bottom: 1,
|
|
66
|
+
left: 2
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Default base style for all toasts
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
const DEFAULT_STYLE = {
|
|
74
|
+
border: true,
|
|
75
|
+
borderStyle: "single",
|
|
76
|
+
borderColor: "#333333",
|
|
77
|
+
minHeight: 3,
|
|
78
|
+
paddingX: 1,
|
|
79
|
+
paddingY: 0,
|
|
80
|
+
backgroundColor: "#1a1a1a",
|
|
81
|
+
foregroundColor: "#ffffff",
|
|
82
|
+
mutedColor: "#6b7280"
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Default toast options including base style and per-type overrides
|
|
86
|
+
*
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
const DEFAULT_TOAST_OPTIONS = {
|
|
90
|
+
style: DEFAULT_STYLE,
|
|
91
|
+
duration: TOAST_DURATION.DEFAULT,
|
|
92
|
+
default: { style: { borderColor: "#333333" } },
|
|
93
|
+
success: { style: { borderColor: "#22c55e" } },
|
|
94
|
+
error: { style: { borderColor: "#ef4444" } },
|
|
95
|
+
warning: { style: { borderColor: "#f59e0b" } },
|
|
96
|
+
info: { style: { borderColor: "#3b82f6" } },
|
|
97
|
+
loading: { style: { borderColor: "#6b7280" } }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/state.ts
|
|
102
|
+
let toastsCounter = 1;
|
|
103
|
+
/**
|
|
104
|
+
* Check if data is an HTTP Response object
|
|
105
|
+
*/
|
|
106
|
+
function isHttpResponse(data) {
|
|
107
|
+
return data !== null && typeof data === "object" && "ok" in data && typeof data.ok === "boolean" && "status" in data && typeof data.status === "number";
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Observer class implementing the pub/sub pattern for toast state management.
|
|
111
|
+
* This is the core of the Sonner-compatible API.
|
|
112
|
+
*/
|
|
113
|
+
var Observer = class {
|
|
114
|
+
subscribers = [];
|
|
115
|
+
toasts = [];
|
|
116
|
+
dismissedToasts = /* @__PURE__ */ new Set();
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to toast state changes
|
|
119
|
+
*/
|
|
120
|
+
subscribe = (subscriber) => {
|
|
121
|
+
this.subscribers.push(subscriber);
|
|
122
|
+
return () => {
|
|
123
|
+
const index = this.subscribers.indexOf(subscriber);
|
|
124
|
+
if (index > -1) this.subscribers.splice(index, 1);
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Publish a toast to all subscribers
|
|
129
|
+
*/
|
|
130
|
+
publish = (data) => {
|
|
131
|
+
for (const subscriber of this.subscribers) subscriber(data);
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Add a new toast
|
|
135
|
+
*/
|
|
136
|
+
addToast = (data) => {
|
|
137
|
+
this.publish(data);
|
|
138
|
+
this.toasts = [...this.toasts, data];
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Create a toast (internal method)
|
|
142
|
+
*/
|
|
143
|
+
create = (data) => {
|
|
144
|
+
const { message, ...rest } = data;
|
|
145
|
+
const id = typeof data.id === "number" || data.id && data.id.length > 0 ? data.id : toastsCounter++;
|
|
146
|
+
const alreadyExists = this.toasts.find((toast$1) => toast$1.id === id);
|
|
147
|
+
const dismissible = data.dismissible === void 0 ? true : data.dismissible;
|
|
148
|
+
if (this.dismissedToasts.has(id)) this.dismissedToasts.delete(id);
|
|
149
|
+
if (alreadyExists) this.toasts = this.toasts.map((toast$1) => {
|
|
150
|
+
if (toast$1.id === id) {
|
|
151
|
+
this.publish({
|
|
152
|
+
...toast$1,
|
|
153
|
+
...data,
|
|
154
|
+
id,
|
|
155
|
+
title: message
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
...toast$1,
|
|
159
|
+
...data,
|
|
160
|
+
id,
|
|
161
|
+
dismissible,
|
|
162
|
+
title: message
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return toast$1;
|
|
166
|
+
});
|
|
167
|
+
else this.addToast({
|
|
168
|
+
title: message,
|
|
169
|
+
...rest,
|
|
170
|
+
dismissible,
|
|
171
|
+
id,
|
|
172
|
+
type: data.type ?? "default"
|
|
173
|
+
});
|
|
174
|
+
return id;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Dismiss a toast by ID, or all toasts if no ID provided
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* // Dismiss a specific toast
|
|
182
|
+
* const id = toast('Hello');
|
|
183
|
+
* toast.dismiss(id);
|
|
184
|
+
*
|
|
185
|
+
* // Dismiss all toasts
|
|
186
|
+
* toast.dismiss();
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
dismiss = (id) => {
|
|
190
|
+
if (id !== void 0) {
|
|
191
|
+
this.dismissedToasts.add(id);
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
for (const subscriber of this.subscribers) subscriber({
|
|
194
|
+
id,
|
|
195
|
+
dismiss: true
|
|
196
|
+
});
|
|
197
|
+
}, 0);
|
|
198
|
+
} else for (const toast$1 of this.toasts) for (const subscriber of this.subscribers) subscriber({
|
|
199
|
+
id: toast$1.id,
|
|
200
|
+
dismiss: true
|
|
201
|
+
});
|
|
202
|
+
return id;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Create a basic message toast
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* toast.message('Hello World');
|
|
210
|
+
* toast.message('With description', { description: 'More details here' });
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
message = (message, data) => {
|
|
214
|
+
return this.create({
|
|
215
|
+
...data,
|
|
216
|
+
message,
|
|
217
|
+
type: "default"
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Create an error toast
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* toast.error('Something went wrong');
|
|
226
|
+
* toast.error('Failed to save', { description: 'Please try again' });
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
error = (message, data) => {
|
|
230
|
+
return this.create({
|
|
231
|
+
...data,
|
|
232
|
+
message,
|
|
233
|
+
type: "error"
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Create a success toast
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```ts
|
|
241
|
+
* toast.success('Operation completed!');
|
|
242
|
+
* toast.success('File uploaded', { description: 'document.pdf saved' });
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
success = (message, data) => {
|
|
246
|
+
return this.create({
|
|
247
|
+
...data,
|
|
248
|
+
message,
|
|
249
|
+
type: "success"
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Create an info toast
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* toast.info('Did you know?');
|
|
258
|
+
* toast.info('Tip', { description: 'Press Ctrl+S to save' });
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
info = (message, data) => {
|
|
262
|
+
return this.create({
|
|
263
|
+
...data,
|
|
264
|
+
message,
|
|
265
|
+
type: "info"
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Create a warning toast
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* toast.warning('Be careful!');
|
|
274
|
+
* toast.warning('Unsaved changes', { description: 'Your work may be lost' });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
warning = (message, data) => {
|
|
278
|
+
return this.create({
|
|
279
|
+
...data,
|
|
280
|
+
message,
|
|
281
|
+
type: "warning"
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* Create a loading toast with an animated spinner
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* // Basic loading toast
|
|
290
|
+
* const id = toast.loading('Processing...');
|
|
291
|
+
*
|
|
292
|
+
* // Update to success when done
|
|
293
|
+
* toast.success('Done!', { id });
|
|
294
|
+
*
|
|
295
|
+
* // Or update to error on failure
|
|
296
|
+
* toast.error('Failed', { id });
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
loading = (message, data) => {
|
|
300
|
+
return this.create({
|
|
301
|
+
...data,
|
|
302
|
+
message,
|
|
303
|
+
type: "loading"
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Create a promise toast that auto-updates based on promise state
|
|
308
|
+
*
|
|
309
|
+
* Automatically shows loading, success, or error states based on the promise result.
|
|
310
|
+
* Handles HTTP Response objects with non-2xx status codes as errors.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* // Basic promise toast
|
|
315
|
+
* toast.promise(fetch('/api/data'), {
|
|
316
|
+
* loading: 'Fetching data...',
|
|
317
|
+
* success: 'Data loaded!',
|
|
318
|
+
* error: 'Failed to load data',
|
|
319
|
+
* });
|
|
320
|
+
*
|
|
321
|
+
* // With dynamic messages based on result
|
|
322
|
+
* toast.promise(saveUser(data), {
|
|
323
|
+
* loading: 'Saving user...',
|
|
324
|
+
* success: (user) => `${user.name} saved!`,
|
|
325
|
+
* error: (err) => `Error: ${err.message}`,
|
|
326
|
+
* });
|
|
327
|
+
*
|
|
328
|
+
* // Access the underlying promise result
|
|
329
|
+
* const result = toast.promise(fetchData(), { ... });
|
|
330
|
+
* const data = await result.unwrap();
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
promise = (promise, data) => {
|
|
334
|
+
if (!data) return;
|
|
335
|
+
let id;
|
|
336
|
+
if (data.loading !== void 0) id = this.create({
|
|
337
|
+
...data,
|
|
338
|
+
type: "loading",
|
|
339
|
+
message: data.loading,
|
|
340
|
+
description: typeof data.description !== "function" ? data.description : void 0
|
|
341
|
+
});
|
|
342
|
+
const p = promise instanceof Function ? promise() : promise;
|
|
343
|
+
let shouldDismiss = id !== void 0;
|
|
344
|
+
let result;
|
|
345
|
+
const originalPromise = p.then(async (response) => {
|
|
346
|
+
result = ["resolve", response];
|
|
347
|
+
if (isHttpResponse(response) && !response.ok) {
|
|
348
|
+
shouldDismiss = false;
|
|
349
|
+
const promiseData = typeof data.error === "function" ? await data.error(`HTTP error! status: ${response.status}`) : data.error;
|
|
350
|
+
const description = typeof data.description === "function" ? await data.description(`HTTP error! status: ${response.status}`) : data.description;
|
|
351
|
+
const toastSettings = typeof promiseData === "object" && promiseData !== null ? promiseData : { message: promiseData };
|
|
352
|
+
this.create({
|
|
353
|
+
id,
|
|
354
|
+
type: "error",
|
|
355
|
+
description,
|
|
356
|
+
...toastSettings
|
|
357
|
+
});
|
|
358
|
+
} else if (response instanceof Error) {
|
|
359
|
+
shouldDismiss = false;
|
|
360
|
+
const promiseData = typeof data.error === "function" ? await data.error(response) : data.error;
|
|
361
|
+
const description = typeof data.description === "function" ? await data.description(response) : data.description;
|
|
362
|
+
const toastSettings = typeof promiseData === "object" && promiseData !== null ? promiseData : { message: promiseData };
|
|
363
|
+
this.create({
|
|
364
|
+
id,
|
|
365
|
+
type: "error",
|
|
366
|
+
description,
|
|
367
|
+
...toastSettings
|
|
368
|
+
});
|
|
369
|
+
} else if (data.success !== void 0) {
|
|
370
|
+
shouldDismiss = false;
|
|
371
|
+
const promiseData = typeof data.success === "function" ? await data.success(response) : data.success;
|
|
372
|
+
const description = typeof data.description === "function" ? await data.description(response) : data.description;
|
|
373
|
+
const toastSettings = typeof promiseData === "object" && promiseData !== null ? promiseData : { message: promiseData };
|
|
374
|
+
this.create({
|
|
375
|
+
id,
|
|
376
|
+
type: "success",
|
|
377
|
+
description,
|
|
378
|
+
...toastSettings
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}).catch(async (error) => {
|
|
382
|
+
result = ["reject", error];
|
|
383
|
+
if (data.error !== void 0) {
|
|
384
|
+
shouldDismiss = false;
|
|
385
|
+
const promiseData = typeof data.error === "function" ? await data.error(error) : data.error;
|
|
386
|
+
const description = typeof data.description === "function" ? await data.description(error) : data.description;
|
|
387
|
+
const toastSettings = typeof promiseData === "object" && promiseData !== null ? promiseData : { message: promiseData };
|
|
388
|
+
this.create({
|
|
389
|
+
id,
|
|
390
|
+
type: "error",
|
|
391
|
+
description,
|
|
392
|
+
...toastSettings
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}).finally(() => {
|
|
396
|
+
if (shouldDismiss) {
|
|
397
|
+
this.dismiss(id);
|
|
398
|
+
id = void 0;
|
|
399
|
+
}
|
|
400
|
+
data.finally?.();
|
|
401
|
+
});
|
|
402
|
+
const unwrap = () => new Promise((resolve, reject) => originalPromise.then(() => result[0] === "reject" ? reject(result[1]) : resolve(result[1])).catch(reject));
|
|
403
|
+
if (typeof id !== "string" && typeof id !== "number") return { unwrap };
|
|
404
|
+
return Object.assign(id, { unwrap });
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Get all active (non-dismissed) toasts
|
|
408
|
+
*/
|
|
409
|
+
getActiveToasts = () => {
|
|
410
|
+
return this.toasts.filter((toast$1) => !this.dismissedToasts.has(toast$1.id));
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* Global toast state singleton
|
|
415
|
+
*/
|
|
416
|
+
const ToastState = new Observer();
|
|
417
|
+
/**
|
|
418
|
+
* Basic toast function - delegates to ToastState.message() for consistent behavior
|
|
419
|
+
*/
|
|
420
|
+
const toastFunction = (message, data) => ToastState.message(message, data);
|
|
421
|
+
/**
|
|
422
|
+
* Get toast history (all toasts ever created, including dismissed)
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```ts
|
|
426
|
+
* const history = toast.getHistory();
|
|
427
|
+
* console.log(`Total toasts shown: ${history.length}`);
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
const getHistory = () => ToastState.toasts;
|
|
431
|
+
/**
|
|
432
|
+
* Get currently active (visible) toasts
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* const active = toast.getToasts();
|
|
437
|
+
* console.log(`Currently showing ${active.length} toasts`);
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
const getToasts = () => ToastState.getActiveToasts();
|
|
441
|
+
/**
|
|
442
|
+
* The main toast API - a function with methods attached
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```ts
|
|
446
|
+
* // Basic usage
|
|
447
|
+
* toast('Hello World');
|
|
448
|
+
*
|
|
449
|
+
* // With variants
|
|
450
|
+
* toast.success('Operation completed');
|
|
451
|
+
* toast.error('Something went wrong');
|
|
452
|
+
* toast.warning('Be careful');
|
|
453
|
+
* toast.info('Did you know?');
|
|
454
|
+
* toast.loading('Processing...');
|
|
455
|
+
*
|
|
456
|
+
* // Promise toast
|
|
457
|
+
* toast.promise(fetchData(), {
|
|
458
|
+
* loading: 'Loading...',
|
|
459
|
+
* success: 'Data loaded!',
|
|
460
|
+
* error: 'Failed to load',
|
|
461
|
+
* });
|
|
462
|
+
*
|
|
463
|
+
* // Dismiss
|
|
464
|
+
* const id = toast('Hello');
|
|
465
|
+
* toast.dismiss(id);
|
|
466
|
+
* toast.dismiss(); // dismiss all
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
const toast = Object.assign(toastFunction, {
|
|
470
|
+
success: ToastState.success,
|
|
471
|
+
info: ToastState.info,
|
|
472
|
+
warning: ToastState.warning,
|
|
473
|
+
error: ToastState.error,
|
|
474
|
+
message: ToastState.message,
|
|
475
|
+
promise: ToastState.promise,
|
|
476
|
+
dismiss: ToastState.dismiss,
|
|
477
|
+
loading: ToastState.loading
|
|
478
|
+
}, {
|
|
479
|
+
getHistory,
|
|
480
|
+
getToasts
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/utils/position.ts
|
|
485
|
+
/**
|
|
486
|
+
* Convert a Position and offset to BoxOptions for absolute positioning
|
|
487
|
+
*
|
|
488
|
+
* Handles all 6 position variants:
|
|
489
|
+
* - top-left, top-center, top-right
|
|
490
|
+
* - bottom-left, bottom-center, bottom-right
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* getPositionStyles("top-right", { top: 2, right: 3 })
|
|
495
|
+
* // => { position: "absolute", top: 2, right: 3, alignItems: "flex-end" }
|
|
496
|
+
*
|
|
497
|
+
* getPositionStyles("bottom-center", {})
|
|
498
|
+
* // => { position: "absolute", bottom: 1, left: 0, width: "100%", alignItems: "center" }
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
function getPositionStyles(position, offset = {}) {
|
|
502
|
+
const [y, x] = position.split("-");
|
|
503
|
+
const styles = { position: "absolute" };
|
|
504
|
+
if (y === "top") styles.top = offset.top ?? DEFAULT_OFFSET.top;
|
|
505
|
+
else styles.bottom = offset.bottom ?? DEFAULT_OFFSET.bottom;
|
|
506
|
+
if (x === "left") {
|
|
507
|
+
styles.left = offset.left ?? DEFAULT_OFFSET.left;
|
|
508
|
+
styles.alignItems = "flex-start";
|
|
509
|
+
} else if (x === "center") {
|
|
510
|
+
styles.left = 0;
|
|
511
|
+
styles.width = "100%";
|
|
512
|
+
styles.alignItems = "center";
|
|
513
|
+
} else {
|
|
514
|
+
styles.right = offset.right ?? DEFAULT_OFFSET.right;
|
|
515
|
+
styles.alignItems = "flex-end";
|
|
516
|
+
}
|
|
517
|
+
return styles;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Check if a position is horizontally centered
|
|
521
|
+
*/
|
|
522
|
+
function isCenteredPosition(position) {
|
|
523
|
+
return position.includes("center");
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Check if a position is at the top of the screen
|
|
527
|
+
*/
|
|
528
|
+
function isTopPosition(position) {
|
|
529
|
+
return position.startsWith("top");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/utils/style.ts
|
|
534
|
+
/**
|
|
535
|
+
* Style utilities for toast rendering
|
|
536
|
+
*
|
|
537
|
+
* Handles style merging and padding resolution.
|
|
538
|
+
*/
|
|
539
|
+
/**
|
|
540
|
+
* Resolve padding values with shorthand support
|
|
541
|
+
*
|
|
542
|
+
* Priority: specific > axis shorthand > uniform
|
|
543
|
+
* e.g., paddingLeft > paddingX > padding
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* ```ts
|
|
547
|
+
* resolvePadding({ padding: 1 })
|
|
548
|
+
* // => { top: 1, right: 1, bottom: 1, left: 1 }
|
|
549
|
+
*
|
|
550
|
+
* resolvePadding({ paddingX: 2, paddingY: 1 })
|
|
551
|
+
* // => { top: 1, right: 2, bottom: 1, left: 2 }
|
|
552
|
+
*
|
|
553
|
+
* resolvePadding({ padding: 1, paddingLeft: 3 })
|
|
554
|
+
* // => { top: 1, right: 1, bottom: 1, left: 3 }
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
function resolvePadding(style) {
|
|
558
|
+
const x = style.padding ?? style.paddingX ?? 0;
|
|
559
|
+
const y = style.padding ?? style.paddingY ?? 0;
|
|
560
|
+
return {
|
|
561
|
+
top: style.paddingTop ?? y,
|
|
562
|
+
right: style.paddingRight ?? x,
|
|
563
|
+
bottom: style.paddingBottom ?? y,
|
|
564
|
+
left: style.paddingLeft ?? x
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Merge multiple ToastStyle objects (later wins)
|
|
569
|
+
*
|
|
570
|
+
* Uses shallow Object.assign, so later styles completely
|
|
571
|
+
* override earlier values for the same property.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```ts
|
|
575
|
+
* mergeStyles(
|
|
576
|
+
* { borderColor: "red", padding: 1 },
|
|
577
|
+
* { borderColor: "blue" }
|
|
578
|
+
* )
|
|
579
|
+
* // => { borderColor: "blue", padding: 1 }
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
function mergeStyles(...styles) {
|
|
583
|
+
const result = {};
|
|
584
|
+
for (const style of styles) {
|
|
585
|
+
if (!style) continue;
|
|
586
|
+
Object.assign(result, style);
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Compute the final style for a toast by merging all style layers
|
|
592
|
+
*
|
|
593
|
+
* Merges styles in order of increasing specificity:
|
|
594
|
+
* 1. DEFAULT_TOAST_OPTIONS.style (sensible defaults)
|
|
595
|
+
* 2. DEFAULT_TOAST_OPTIONS[type].style (default type colors)
|
|
596
|
+
* 3. toastOptions.style (user's global style)
|
|
597
|
+
* 4. toastOptions[type].style (user's type-specific overrides)
|
|
598
|
+
* 5. toastStyle (per-toast inline style from toast() call)
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```ts
|
|
602
|
+
* computeToastStyle("success", { style: { paddingX: 2 }, success: { style: { borderColor: "green" } } })
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
function computeToastStyle(type, toastOptions, toastStyle) {
|
|
606
|
+
const defaultBaseStyle = DEFAULT_TOAST_OPTIONS.style;
|
|
607
|
+
const defaultTypeStyle = DEFAULT_TOAST_OPTIONS[type]?.style;
|
|
608
|
+
const userBaseStyle = toastOptions?.style;
|
|
609
|
+
const userTypeStyle = toastOptions?.[type]?.style;
|
|
610
|
+
const computedStyle = mergeStyles(defaultBaseStyle, defaultTypeStyle, userBaseStyle, userTypeStyle, toastStyle);
|
|
611
|
+
if (computedStyle.border === false) {
|
|
612
|
+
delete computedStyle.borderStyle;
|
|
613
|
+
delete computedStyle.borderColor;
|
|
614
|
+
}
|
|
615
|
+
return computedStyle;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Compute the duration for a toast
|
|
619
|
+
*
|
|
620
|
+
* Priority: toast.duration > toastOptions[type].duration > toastOptions.duration > DEFAULT
|
|
621
|
+
*/
|
|
622
|
+
function computeToastDuration(type, toastOptions, toastDuration) {
|
|
623
|
+
if (toastDuration !== void 0) return toastDuration;
|
|
624
|
+
const typeDuration = toastOptions?.[type]?.duration;
|
|
625
|
+
if (typeDuration !== void 0) return typeDuration;
|
|
626
|
+
if (toastOptions?.duration !== void 0) return toastOptions.duration;
|
|
627
|
+
return DEFAULT_TOAST_OPTIONS.duration;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/renderables/toast.ts
|
|
632
|
+
/**
|
|
633
|
+
* ToastRenderable - A single toast notification component
|
|
634
|
+
*
|
|
635
|
+
* Renders a toast with icon, title, description, action button, and close button.
|
|
636
|
+
*/
|
|
637
|
+
/**
|
|
638
|
+
* ToastRenderable - A single toast notification
|
|
639
|
+
*
|
|
640
|
+
* Renders a toast with:
|
|
641
|
+
* - Icon (based on type, with spinner animation for loading)
|
|
642
|
+
* - Title (bold text)
|
|
643
|
+
* - Description (optional, muted text)
|
|
644
|
+
* - Action button (optional)
|
|
645
|
+
* - Close button (optional)
|
|
646
|
+
*
|
|
647
|
+
* Supports:
|
|
648
|
+
* - Auto-dismiss with configurable duration
|
|
649
|
+
* - Pause/resume timer
|
|
650
|
+
* - Style updates when toast type changes
|
|
651
|
+
* - Spinner animation for loading toasts
|
|
652
|
+
*/
|
|
653
|
+
var ToastRenderable = class extends BoxRenderable {
|
|
654
|
+
_toast;
|
|
655
|
+
_icons;
|
|
656
|
+
_toastOptions;
|
|
657
|
+
_computedStyle;
|
|
658
|
+
_closeButton;
|
|
659
|
+
_onRemove;
|
|
660
|
+
_remainingTime;
|
|
661
|
+
_closeTimerStartTime = 0;
|
|
662
|
+
_lastCloseTimerStartTime = 0;
|
|
663
|
+
_timerHandle = null;
|
|
664
|
+
_paused = false;
|
|
665
|
+
_dismissed = false;
|
|
666
|
+
_spinnerHandle = null;
|
|
667
|
+
_spinnerFrameIndex = 0;
|
|
668
|
+
_spinnerConfig = null;
|
|
669
|
+
_iconText = null;
|
|
670
|
+
_contentBox = null;
|
|
671
|
+
_titleText = null;
|
|
672
|
+
_descriptionText = null;
|
|
673
|
+
_actionsBox = null;
|
|
674
|
+
constructor(ctx, options) {
|
|
675
|
+
const computedStyle = computeToastStyle(options.toast.type, options.toastOptions, options.toast.style);
|
|
676
|
+
const duration = computeToastDuration(options.toast.type, options.toastOptions, options.toast.duration);
|
|
677
|
+
const padding = resolvePadding(computedStyle);
|
|
678
|
+
super(ctx, {
|
|
679
|
+
id: `toast-${options.toast.id}`,
|
|
680
|
+
flexDirection: "row",
|
|
681
|
+
gap: 1,
|
|
682
|
+
border: computedStyle.border,
|
|
683
|
+
borderStyle: computedStyle.borderStyle,
|
|
684
|
+
borderColor: computedStyle.borderColor,
|
|
685
|
+
customBorderChars: computedStyle.customBorderChars,
|
|
686
|
+
backgroundColor: computedStyle.backgroundColor,
|
|
687
|
+
minHeight: computedStyle.minHeight,
|
|
688
|
+
maxWidth: computedStyle.maxWidth ?? TOAST_WIDTH,
|
|
689
|
+
minWidth: computedStyle.minWidth,
|
|
690
|
+
paddingTop: padding.top,
|
|
691
|
+
paddingRight: padding.right,
|
|
692
|
+
paddingBottom: padding.bottom,
|
|
693
|
+
paddingLeft: padding.left
|
|
694
|
+
});
|
|
695
|
+
this._toast = options.toast;
|
|
696
|
+
this._icons = options.icons === false ? false : {
|
|
697
|
+
...DEFAULT_ICONS,
|
|
698
|
+
...options.icons
|
|
699
|
+
};
|
|
700
|
+
this._toastOptions = options.toastOptions;
|
|
701
|
+
this._computedStyle = computedStyle;
|
|
702
|
+
this._closeButton = options.closeButton;
|
|
703
|
+
this._onRemove = options.onRemove;
|
|
704
|
+
this._remainingTime = duration;
|
|
705
|
+
this.setupContent();
|
|
706
|
+
if (this._remainingTime !== Infinity && this._toast.type !== "loading") this.startTimer();
|
|
707
|
+
if (this._toast.type === "loading") this.startSpinner();
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Set up the toast content (icon, title, description, actions)
|
|
711
|
+
*/
|
|
712
|
+
setupContent() {
|
|
713
|
+
const ctx = this.ctx;
|
|
714
|
+
const toast$1 = this._toast;
|
|
715
|
+
const style = this._computedStyle;
|
|
716
|
+
const icons = this._icons;
|
|
717
|
+
const iconColor = style.iconColor ?? style.borderColor;
|
|
718
|
+
const isLoading = toast$1.type === "loading";
|
|
719
|
+
if (isLoading && icons !== false) this._spinnerConfig = getSpinnerConfig(icons.loading);
|
|
720
|
+
const icon = toast$1.icon ?? (icons === false ? void 0 : isLoading ? getLoadingIcon(icons.loading) : getTypeIcon(toast$1.type, icons));
|
|
721
|
+
if (icon) {
|
|
722
|
+
this._iconText = new TextRenderable(ctx, {
|
|
723
|
+
id: `${this.id}-icon`,
|
|
724
|
+
content: icon,
|
|
725
|
+
fg: iconColor,
|
|
726
|
+
flexShrink: 0,
|
|
727
|
+
paddingTop: 0,
|
|
728
|
+
paddingBottom: 0
|
|
729
|
+
});
|
|
730
|
+
this.add(this._iconText);
|
|
731
|
+
}
|
|
732
|
+
this._contentBox = new BoxRenderable(ctx, {
|
|
733
|
+
id: `${this.id}-content`,
|
|
734
|
+
flexDirection: "column",
|
|
735
|
+
flexGrow: 1,
|
|
736
|
+
flexShrink: 1,
|
|
737
|
+
gap: 0
|
|
738
|
+
});
|
|
739
|
+
const title = typeof toast$1.title === "function" ? toast$1.title() : toast$1.title;
|
|
740
|
+
if (title) {
|
|
741
|
+
this._titleText = new TextRenderable(ctx, {
|
|
742
|
+
id: `${this.id}-title`,
|
|
743
|
+
content: title,
|
|
744
|
+
fg: style.foregroundColor,
|
|
745
|
+
attributes: TextAttributes.BOLD,
|
|
746
|
+
wrapMode: "word"
|
|
747
|
+
});
|
|
748
|
+
this._contentBox.add(this._titleText);
|
|
749
|
+
}
|
|
750
|
+
const description = typeof toast$1.description === "function" ? toast$1.description() : toast$1.description;
|
|
751
|
+
if (description) {
|
|
752
|
+
this._descriptionText = new TextRenderable(ctx, {
|
|
753
|
+
id: `${this.id}-description`,
|
|
754
|
+
content: description,
|
|
755
|
+
fg: style.mutedColor,
|
|
756
|
+
wrapMode: "word"
|
|
757
|
+
});
|
|
758
|
+
this._contentBox.add(this._descriptionText);
|
|
759
|
+
}
|
|
760
|
+
this.add(this._contentBox);
|
|
761
|
+
if (toast$1.action) {
|
|
762
|
+
this._actionsBox = new BoxRenderable(ctx, {
|
|
763
|
+
id: `${this.id}-actions`,
|
|
764
|
+
flexDirection: "row",
|
|
765
|
+
gap: 1,
|
|
766
|
+
flexShrink: 0,
|
|
767
|
+
alignItems: "center"
|
|
768
|
+
});
|
|
769
|
+
if (toast$1.action && isAction(toast$1.action)) {
|
|
770
|
+
const actionText = new TextRenderable(ctx, {
|
|
771
|
+
id: `${this.id}-action`,
|
|
772
|
+
content: `[${toast$1.action.label}]`,
|
|
773
|
+
fg: style.foregroundColor,
|
|
774
|
+
onMouseUp: () => toast$1.action?.onClick?.()
|
|
775
|
+
});
|
|
776
|
+
this._actionsBox.add(actionText);
|
|
777
|
+
}
|
|
778
|
+
this.add(this._actionsBox);
|
|
779
|
+
}
|
|
780
|
+
if ((toast$1.closeButton ?? this._closeButton) && toast$1.dismissible !== false) {
|
|
781
|
+
const closeIcon = icons === false ? "×" : icons.close;
|
|
782
|
+
const closeText = new TextRenderable(ctx, {
|
|
783
|
+
id: `${this.id}-close`,
|
|
784
|
+
content: closeIcon,
|
|
785
|
+
fg: style.mutedColor,
|
|
786
|
+
flexShrink: 0,
|
|
787
|
+
onMouseUp: () => this.dismiss()
|
|
788
|
+
});
|
|
789
|
+
this.add(closeText);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Start the auto-dismiss timer
|
|
794
|
+
*/
|
|
795
|
+
startTimer() {
|
|
796
|
+
if (this._remainingTime === Infinity) return;
|
|
797
|
+
this._closeTimerStartTime = Date.now();
|
|
798
|
+
this._timerHandle = setTimeout(() => {
|
|
799
|
+
this._toast.onAutoClose?.(this._toast);
|
|
800
|
+
this.dismiss();
|
|
801
|
+
}, this._remainingTime);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Pause the auto-dismiss timer
|
|
805
|
+
*
|
|
806
|
+
* Call this when the user is interacting with the toast
|
|
807
|
+
* (e.g., hovering over it in a mouse-enabled terminal)
|
|
808
|
+
*/
|
|
809
|
+
pause() {
|
|
810
|
+
if (this._paused || this._remainingTime === Infinity) return;
|
|
811
|
+
this._paused = true;
|
|
812
|
+
if (this._timerHandle) {
|
|
813
|
+
clearTimeout(this._timerHandle);
|
|
814
|
+
this._timerHandle = null;
|
|
815
|
+
}
|
|
816
|
+
if (this._lastCloseTimerStartTime < this._closeTimerStartTime) {
|
|
817
|
+
const elapsed = Date.now() - this._closeTimerStartTime;
|
|
818
|
+
this._remainingTime = Math.max(0, this._remainingTime - elapsed);
|
|
819
|
+
}
|
|
820
|
+
this._lastCloseTimerStartTime = Date.now();
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Resume the auto-dismiss timer
|
|
824
|
+
*
|
|
825
|
+
* Call this when the user stops interacting with the toast
|
|
826
|
+
*/
|
|
827
|
+
resume() {
|
|
828
|
+
if (!this._paused || this._remainingTime === Infinity) return;
|
|
829
|
+
this._paused = false;
|
|
830
|
+
this.startTimer();
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Start the spinner animation for loading toasts
|
|
834
|
+
*/
|
|
835
|
+
startSpinner() {
|
|
836
|
+
if (this._spinnerHandle || !this._spinnerConfig) return;
|
|
837
|
+
const { frames, interval } = this._spinnerConfig;
|
|
838
|
+
this._spinnerHandle = setInterval(() => {
|
|
839
|
+
this._spinnerFrameIndex = (this._spinnerFrameIndex + 1) % frames.length;
|
|
840
|
+
const frame = frames[this._spinnerFrameIndex];
|
|
841
|
+
if (this._iconText && frame) {
|
|
842
|
+
this._iconText.content = frame;
|
|
843
|
+
this.requestRender();
|
|
844
|
+
}
|
|
845
|
+
}, interval);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Stop the spinner animation
|
|
849
|
+
*/
|
|
850
|
+
stopSpinner() {
|
|
851
|
+
if (this._spinnerHandle) {
|
|
852
|
+
clearInterval(this._spinnerHandle);
|
|
853
|
+
this._spinnerHandle = null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Dismiss this toast
|
|
858
|
+
*
|
|
859
|
+
* Triggers the onDismiss callback and schedules removal.
|
|
860
|
+
*/
|
|
861
|
+
dismiss() {
|
|
862
|
+
if (this._dismissed) return;
|
|
863
|
+
this._dismissed = true;
|
|
864
|
+
if (this._timerHandle) {
|
|
865
|
+
clearTimeout(this._timerHandle);
|
|
866
|
+
this._timerHandle = null;
|
|
867
|
+
}
|
|
868
|
+
this.stopSpinner();
|
|
869
|
+
this._toast.onDismiss?.(this._toast);
|
|
870
|
+
setTimeout(() => {
|
|
871
|
+
this._onRemove?.(this._toast);
|
|
872
|
+
}, TIME_BEFORE_UNMOUNT);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Update the toast data
|
|
876
|
+
*
|
|
877
|
+
* Used for updating an existing toast (e.g., toast.success('done', { id: existingId }))
|
|
878
|
+
*/
|
|
879
|
+
updateToast(toast$1) {
|
|
880
|
+
this._toast = toast$1;
|
|
881
|
+
const computedStyle = computeToastStyle(toast$1.type, this._toastOptions, toast$1.style);
|
|
882
|
+
this._computedStyle = computedStyle;
|
|
883
|
+
if (computedStyle.borderColor) this.borderColor = computedStyle.borderColor;
|
|
884
|
+
if (computedStyle.customBorderChars) this.customBorderChars = computedStyle.customBorderChars;
|
|
885
|
+
const iconColor = computedStyle.iconColor ?? computedStyle.borderColor;
|
|
886
|
+
if (this._iconText) {
|
|
887
|
+
const icon = toast$1.icon ?? (this._icons === false ? void 0 : getTypeIcon(toast$1.type, this._icons));
|
|
888
|
+
if (icon) this._iconText.content = icon;
|
|
889
|
+
if (iconColor) this._iconText.fg = parseColor(iconColor);
|
|
890
|
+
}
|
|
891
|
+
if (this._titleText) {
|
|
892
|
+
const title = typeof toast$1.title === "function" ? toast$1.title() : toast$1.title;
|
|
893
|
+
if (title) this._titleText.content = title;
|
|
894
|
+
}
|
|
895
|
+
if (this._descriptionText) {
|
|
896
|
+
const description = typeof toast$1.description === "function" ? toast$1.description() : toast$1.description;
|
|
897
|
+
if (description) this._descriptionText.content = description;
|
|
898
|
+
}
|
|
899
|
+
const wasLoading = this._spinnerHandle !== null;
|
|
900
|
+
const isLoading = toast$1.type === "loading";
|
|
901
|
+
if (wasLoading && !isLoading) {
|
|
902
|
+
this.stopSpinner();
|
|
903
|
+
this._spinnerConfig = null;
|
|
904
|
+
} else if (!wasLoading && isLoading) {
|
|
905
|
+
if (this._icons !== false) this._spinnerConfig = getSpinnerConfig(this._icons.loading);
|
|
906
|
+
this.startSpinner();
|
|
907
|
+
}
|
|
908
|
+
if (toast$1.type !== "loading") {
|
|
909
|
+
if (this._timerHandle) clearTimeout(this._timerHandle);
|
|
910
|
+
this._remainingTime = computeToastDuration(toast$1.type, this._toastOptions, toast$1.duration);
|
|
911
|
+
if (this._remainingTime !== Infinity) this.startTimer();
|
|
912
|
+
}
|
|
913
|
+
this.requestRender();
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Get the toast data
|
|
917
|
+
*/
|
|
918
|
+
get toast() {
|
|
919
|
+
return this._toast;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Check if toast is dismissed
|
|
923
|
+
*/
|
|
924
|
+
get isDismissed() {
|
|
925
|
+
return this._dismissed;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Clean up on destroy
|
|
929
|
+
*/
|
|
930
|
+
destroy() {
|
|
931
|
+
if (this._timerHandle) {
|
|
932
|
+
clearTimeout(this._timerHandle);
|
|
933
|
+
this._timerHandle = null;
|
|
934
|
+
}
|
|
935
|
+
this.stopSpinner();
|
|
936
|
+
super.destroy();
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/renderables/toaster.ts
|
|
942
|
+
/**
|
|
943
|
+
* ToasterRenderable - Container for toast notifications
|
|
944
|
+
*
|
|
945
|
+
* Manages the display of multiple toasts, subscribes to ToastState,
|
|
946
|
+
* and handles positioning, stacking, and removal.
|
|
947
|
+
*/
|
|
948
|
+
/**
|
|
949
|
+
* ToasterRenderable - Container for toast notifications
|
|
950
|
+
*
|
|
951
|
+
* Features:
|
|
952
|
+
* - Subscribes to ToastState for automatic toast management
|
|
953
|
+
* - Supports 6 position variants (top/bottom + left/center/right)
|
|
954
|
+
* - Single or stack mode for multiple toasts
|
|
955
|
+
* - Configurable visible toast limit in stack mode
|
|
956
|
+
* - Automatic oldest toast removal when limit exceeded
|
|
957
|
+
*
|
|
958
|
+
* @example
|
|
959
|
+
* ```ts
|
|
960
|
+
* import { ToasterRenderable, toast } from '@opentui-ui/toast';
|
|
961
|
+
*
|
|
962
|
+
* // Basic usage - add to your app once
|
|
963
|
+
* const toaster = new ToasterRenderable(ctx);
|
|
964
|
+
* ctx.root.add(toaster);
|
|
965
|
+
*
|
|
966
|
+
* // Then show toasts from anywhere
|
|
967
|
+
* toast.success('Hello World!');
|
|
968
|
+
* ```
|
|
969
|
+
*
|
|
970
|
+
* @example
|
|
971
|
+
* ```ts
|
|
972
|
+
* // With full configuration
|
|
973
|
+
* const toaster = new ToasterRenderable(ctx, {
|
|
974
|
+
* position: 'top-right',
|
|
975
|
+
* stackingMode: 'stack',
|
|
976
|
+
* visibleToasts: 5,
|
|
977
|
+
* closeButton: true,
|
|
978
|
+
* gap: 1,
|
|
979
|
+
* toastOptions: {
|
|
980
|
+
* style: { backgroundColor: '#1a1a1a' },
|
|
981
|
+
* duration: 5000,
|
|
982
|
+
* success: { style: { borderColor: '#22c55e' } },
|
|
983
|
+
* error: { style: { borderColor: '#ef4444' } },
|
|
984
|
+
* },
|
|
985
|
+
* });
|
|
986
|
+
* ```
|
|
987
|
+
*
|
|
988
|
+
* @example
|
|
989
|
+
* ```ts
|
|
990
|
+
* // With a theme preset
|
|
991
|
+
* import { minimal } from '@opentui-ui/toast/themes';
|
|
992
|
+
*
|
|
993
|
+
* const toaster = new ToasterRenderable(ctx, {
|
|
994
|
+
* ...minimal,
|
|
995
|
+
* position: 'bottom-center',
|
|
996
|
+
* });
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
var ToasterRenderable = class extends BoxRenderable {
|
|
1000
|
+
_options;
|
|
1001
|
+
_toastRenderables = /* @__PURE__ */ new Map();
|
|
1002
|
+
_unsubscribe = null;
|
|
1003
|
+
constructor(ctx, options = {}) {
|
|
1004
|
+
const position = options.position ?? "bottom-right";
|
|
1005
|
+
const positionStyles = getPositionStyles(position, options.offset ?? {});
|
|
1006
|
+
const isCentered = isCenteredPosition(position);
|
|
1007
|
+
super(ctx, {
|
|
1008
|
+
id: "toaster",
|
|
1009
|
+
flexDirection: "column",
|
|
1010
|
+
gap: options.gap ?? 1,
|
|
1011
|
+
zIndex: 9999,
|
|
1012
|
+
...isCentered ? {} : { maxWidth: options.maxWidth ?? TOAST_WIDTH },
|
|
1013
|
+
...positionStyles
|
|
1014
|
+
});
|
|
1015
|
+
this._options = options;
|
|
1016
|
+
this.subscribe();
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Subscribe to toast state changes
|
|
1020
|
+
*/
|
|
1021
|
+
subscribe() {
|
|
1022
|
+
this._unsubscribe = ToastState.subscribe((toast$1) => {
|
|
1023
|
+
if ("dismiss" in toast$1 && toast$1.dismiss) this.removeToast(toast$1.id);
|
|
1024
|
+
else this.addOrUpdateToast(toast$1);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Add a new toast or update an existing one
|
|
1029
|
+
*/
|
|
1030
|
+
addOrUpdateToast(toast$1) {
|
|
1031
|
+
const existing = this._toastRenderables.get(toast$1.id);
|
|
1032
|
+
if (existing) {
|
|
1033
|
+
existing.updateToast(toast$1);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if ((this._options.stackingMode ?? "single") === "single") for (const [id] of this._toastRenderables) this.removeToast(id);
|
|
1037
|
+
else {
|
|
1038
|
+
const maxVisible = this._options.visibleToasts ?? 3;
|
|
1039
|
+
const currentCount = this._toastRenderables.size;
|
|
1040
|
+
if (currentCount >= maxVisible) {
|
|
1041
|
+
const toRemove = currentCount - maxVisible + 1;
|
|
1042
|
+
const ids = Array.from(this._toastRenderables.keys());
|
|
1043
|
+
for (let i = 0; i < toRemove; i++) {
|
|
1044
|
+
const id = ids[i];
|
|
1045
|
+
if (id !== void 0) this.removeToast(id);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const toastRenderable = new ToastRenderable(this.ctx, {
|
|
1050
|
+
toast: toast$1,
|
|
1051
|
+
icons: this._options.icons,
|
|
1052
|
+
toastOptions: this._options.toastOptions,
|
|
1053
|
+
closeButton: this._options.closeButton,
|
|
1054
|
+
onRemove: (t) => this.handleToastRemoved(t)
|
|
1055
|
+
});
|
|
1056
|
+
this._toastRenderables.set(toast$1.id, toastRenderable);
|
|
1057
|
+
if (isTopPosition(this._options.position ?? "bottom-right")) this.add(toastRenderable);
|
|
1058
|
+
else this.add(toastRenderable, 0);
|
|
1059
|
+
this.requestRender();
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Remove a toast by ID
|
|
1063
|
+
*/
|
|
1064
|
+
removeToast(id) {
|
|
1065
|
+
const toast$1 = this._toastRenderables.get(id);
|
|
1066
|
+
if (toast$1) toast$1.dismiss();
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Handle when a toast is fully removed
|
|
1070
|
+
*/
|
|
1071
|
+
handleToastRemoved(toast$1) {
|
|
1072
|
+
const renderable = this._toastRenderables.get(toast$1.id);
|
|
1073
|
+
if (renderable) {
|
|
1074
|
+
this._toastRenderables.delete(toast$1.id);
|
|
1075
|
+
this.remove(renderable.id);
|
|
1076
|
+
renderable.destroy();
|
|
1077
|
+
this.requestRender();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Get the current number of visible toasts
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* ```ts
|
|
1085
|
+
* if (toaster.toastCount > 0) {
|
|
1086
|
+
* console.log(`Showing ${toaster.toastCount} notifications`);
|
|
1087
|
+
* }
|
|
1088
|
+
* ```
|
|
1089
|
+
*/
|
|
1090
|
+
get toastCount() {
|
|
1091
|
+
return this._toastRenderables.size;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Dismiss all toasts managed by this toaster
|
|
1095
|
+
*
|
|
1096
|
+
* @example
|
|
1097
|
+
* ```ts
|
|
1098
|
+
* // Clear all notifications
|
|
1099
|
+
* toaster.dismissAll();
|
|
1100
|
+
* ```
|
|
1101
|
+
*/
|
|
1102
|
+
dismissAll() {
|
|
1103
|
+
for (const [id] of this._toastRenderables) this.removeToast(id);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Clean up on destroy
|
|
1107
|
+
*/
|
|
1108
|
+
destroy() {
|
|
1109
|
+
this._unsubscribe?.();
|
|
1110
|
+
for (const [, renderable] of this._toastRenderables) renderable.destroy();
|
|
1111
|
+
this._toastRenderables.clear();
|
|
1112
|
+
super.destroy();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
//#endregion
|
|
1117
|
+
export { toast as n, TOAST_DURATION as r, ToasterRenderable as t };
|
|
1118
|
+
//# sourceMappingURL=toaster-CQ5RySDh.mjs.map
|