@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.
@@ -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