sonner-vanilla 0.1.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,702 @@
1
+ /**
2
+ * Toaster component - DOM manipulation and rendering
3
+ * Vanilla JS port of sonner's index.tsx
4
+ */
5
+ import { CloseIcon, createLoader, getAsset } from './assets';
6
+ import { ToastState } from './state';
7
+ import { isAction, } from './types';
8
+ // Constants
9
+ const VISIBLE_TOASTS_AMOUNT = 3;
10
+ const VIEWPORT_OFFSET = '24px';
11
+ const MOBILE_VIEWPORT_OFFSET = '16px';
12
+ const TOAST_LIFETIME = 4000;
13
+ const TOAST_WIDTH = 356;
14
+ const GAP = 14;
15
+ const SWIPE_THRESHOLD = 45;
16
+ const TIME_BEFORE_UNMOUNT = 200;
17
+ function cn(...classes) {
18
+ return classes.filter(Boolean).join(' ');
19
+ }
20
+ function getDefaultSwipeDirections(position) {
21
+ const [y, x] = position.split('-');
22
+ const directions = [];
23
+ if (y)
24
+ directions.push(y);
25
+ if (x)
26
+ directions.push(x);
27
+ return directions;
28
+ }
29
+ function getDocumentDirection() {
30
+ if (typeof window === 'undefined' || typeof document === 'undefined')
31
+ return 'ltr';
32
+ const dirAttribute = document.documentElement.getAttribute('dir');
33
+ if (dirAttribute === 'auto' || !dirAttribute) {
34
+ return window.getComputedStyle(document.documentElement).direction;
35
+ }
36
+ return dirAttribute;
37
+ }
38
+ function assignOffset(defaultOffset, mobileOffset) {
39
+ const styles = {};
40
+ [defaultOffset, mobileOffset].forEach((offset, index) => {
41
+ const isMobile = index === 1;
42
+ const prefix = isMobile ? '--mobile-offset' : '--offset';
43
+ const defaultValue = isMobile ? MOBILE_VIEWPORT_OFFSET : VIEWPORT_OFFSET;
44
+ function assignAll(offset) {
45
+ ['top', 'right', 'bottom', 'left'].forEach((key) => {
46
+ styles[`${prefix}-${key}`] = typeof offset === 'number' ? `${offset}px` : offset;
47
+ });
48
+ }
49
+ if (typeof offset === 'number' || typeof offset === 'string') {
50
+ assignAll(offset);
51
+ }
52
+ else if (typeof offset === 'object' && offset !== null) {
53
+ ['top', 'right', 'bottom', 'left'].forEach((key) => {
54
+ const k = key;
55
+ if (offset[k] === undefined) {
56
+ styles[`${prefix}-${key}`] = defaultValue;
57
+ }
58
+ else {
59
+ styles[`${prefix}-${key}`] = typeof offset[k] === 'number' ? `${offset[k]}px` : String(offset[k]);
60
+ }
61
+ });
62
+ }
63
+ else {
64
+ assignAll(defaultValue);
65
+ }
66
+ });
67
+ return styles;
68
+ }
69
+ export class Toaster {
70
+ constructor(options = {}) {
71
+ this.container = null;
72
+ this.listEl = null;
73
+ this.toastInstances = new Map();
74
+ this.heights = [];
75
+ this.expanded = false;
76
+ this.interacting = false;
77
+ this.unsubscribe = null;
78
+ this.boundListHandlers = null;
79
+ this.actualTheme = 'light';
80
+ this.isDocumentHidden = false;
81
+ this.handleKeyDown = (event) => {
82
+ const { hotkey } = this.options;
83
+ const isHotkeyPressed = hotkey.length > 0 &&
84
+ hotkey.every((key) => event[key] || event.code === key);
85
+ if (isHotkeyPressed) {
86
+ this.setExpanded(true);
87
+ this.listEl?.focus();
88
+ }
89
+ if (event.code === 'Escape' && this.listEl?.contains(document.activeElement)) {
90
+ this.setExpanded(false);
91
+ }
92
+ };
93
+ this.handleVisibilityChange = () => {
94
+ this.isDocumentHidden = document.hidden;
95
+ // Pause/resume timers for all toasts
96
+ this.toastInstances.forEach((instance) => {
97
+ if (this.isDocumentHidden) {
98
+ this.pauseTimer(instance);
99
+ }
100
+ else {
101
+ this.startTimer(instance);
102
+ }
103
+ });
104
+ };
105
+ this.options = {
106
+ id: options.id ?? '',
107
+ invert: options.invert ?? false,
108
+ theme: options.theme ?? 'light',
109
+ position: options.position ?? 'bottom-right',
110
+ hotkey: options.hotkey ?? ['altKey', 'KeyT'],
111
+ richColors: options.richColors ?? false,
112
+ expand: options.expand ?? false,
113
+ duration: options.duration ?? TOAST_LIFETIME,
114
+ gap: options.gap ?? GAP,
115
+ visibleToasts: options.visibleToasts ?? VISIBLE_TOASTS_AMOUNT,
116
+ closeButton: options.closeButton ?? false,
117
+ className: options.className ?? '',
118
+ style: options.style ?? {},
119
+ offset: options.offset ?? VIEWPORT_OFFSET,
120
+ mobileOffset: options.mobileOffset ?? MOBILE_VIEWPORT_OFFSET,
121
+ dir: options.dir ?? getDocumentDirection(),
122
+ swipeDirections: options.swipeDirections ?? undefined,
123
+ containerAriaLabel: options.containerAriaLabel ?? 'Notifications',
124
+ toastOptions: options.toastOptions ?? {},
125
+ };
126
+ }
127
+ mount(target = document.body) {
128
+ const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
129
+ if (!targetEl) {
130
+ console.error('Toaster: target element not found');
131
+ return;
132
+ }
133
+ this.setupTheme();
134
+ this.createContainer();
135
+ targetEl.appendChild(this.container);
136
+ this.setupEventListeners();
137
+ this.subscribeToState();
138
+ }
139
+ unmount() {
140
+ document.removeEventListener('keydown', this.handleKeyDown);
141
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
142
+ // Remove list event listeners
143
+ if (this.listEl && this.boundListHandlers) {
144
+ this.listEl.removeEventListener('mouseenter', this.boundListHandlers.mouseenter);
145
+ this.listEl.removeEventListener('mousemove', this.boundListHandlers.mousemove);
146
+ this.listEl.removeEventListener('mouseleave', this.boundListHandlers.mouseleave);
147
+ this.listEl.removeEventListener('pointerdown', this.boundListHandlers.pointerdown);
148
+ this.listEl.removeEventListener('pointerup', this.boundListHandlers.pointerup);
149
+ }
150
+ this.boundListHandlers = null;
151
+ this.unsubscribe?.();
152
+ this.container?.remove();
153
+ this.container = null;
154
+ this.listEl = null;
155
+ this.toastInstances.clear();
156
+ }
157
+ setupTheme() {
158
+ if (this.options.theme !== 'system') {
159
+ this.actualTheme = this.options.theme;
160
+ return;
161
+ }
162
+ if (typeof window !== 'undefined' && window.matchMedia) {
163
+ const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
164
+ this.actualTheme = darkQuery.matches ? 'dark' : 'light';
165
+ darkQuery.addEventListener('change', (e) => {
166
+ this.actualTheme = e.matches ? 'dark' : 'light';
167
+ this.listEl?.setAttribute('data-sonner-theme', this.actualTheme);
168
+ });
169
+ }
170
+ }
171
+ createContainer() {
172
+ const [y, x] = this.options.position.split('-');
173
+ // Create section wrapper
174
+ this.container = document.createElement('section');
175
+ const hotkeyLabel = this.options.hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '');
176
+ this.container.setAttribute('aria-label', `${this.options.containerAriaLabel} ${hotkeyLabel}`);
177
+ this.container.setAttribute('tabindex', '-1');
178
+ this.container.setAttribute('aria-live', 'polite');
179
+ this.container.setAttribute('aria-relevant', 'additions text');
180
+ this.container.setAttribute('aria-atomic', 'false');
181
+ // Create ordered list
182
+ this.listEl = document.createElement('ol');
183
+ this.listEl.setAttribute('data-sonner-toaster', '');
184
+ this.listEl.setAttribute('data-sonner-theme', this.actualTheme);
185
+ this.listEl.setAttribute('data-y-position', y);
186
+ this.listEl.setAttribute('data-x-position', x);
187
+ this.listEl.setAttribute('dir', this.options.dir === 'auto' ? getDocumentDirection() : this.options.dir);
188
+ this.listEl.setAttribute('tabindex', '-1');
189
+ if (this.options.className) {
190
+ this.listEl.className = this.options.className;
191
+ }
192
+ // Apply styles - CSS custom properties must use setProperty
193
+ this.listEl.style.setProperty('--front-toast-height', '0px');
194
+ this.listEl.style.setProperty('--width', `${TOAST_WIDTH}px`);
195
+ this.listEl.style.setProperty('--gap', `${this.options.gap}px`);
196
+ // Apply offset styles (CSS custom properties)
197
+ const offsetStyles = assignOffset(this.options.offset, this.options.mobileOffset);
198
+ Object.entries(offsetStyles).forEach(([key, value]) => {
199
+ this.listEl.style.setProperty(key, value);
200
+ });
201
+ // Apply any additional user styles
202
+ Object.assign(this.listEl.style, this.options.style);
203
+ this.container.appendChild(this.listEl);
204
+ }
205
+ setupEventListeners() {
206
+ // Hotkey listener
207
+ document.addEventListener('keydown', this.handleKeyDown);
208
+ // Document visibility
209
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
210
+ // List interactions - store references for cleanup
211
+ this.boundListHandlers = {
212
+ mouseenter: () => this.setExpanded(true),
213
+ mousemove: () => this.setExpanded(true),
214
+ mouseleave: () => {
215
+ if (!this.interacting)
216
+ this.setExpanded(false);
217
+ },
218
+ pointerdown: (e) => {
219
+ const target = e.target;
220
+ if (target.dataset.dismissible !== 'false') {
221
+ this.interacting = true;
222
+ }
223
+ },
224
+ pointerup: () => {
225
+ this.interacting = false;
226
+ },
227
+ };
228
+ this.listEl?.addEventListener('mouseenter', this.boundListHandlers.mouseenter);
229
+ this.listEl?.addEventListener('mousemove', this.boundListHandlers.mousemove);
230
+ this.listEl?.addEventListener('mouseleave', this.boundListHandlers.mouseleave);
231
+ this.listEl?.addEventListener('pointerdown', this.boundListHandlers.pointerdown);
232
+ this.listEl?.addEventListener('pointerup', this.boundListHandlers.pointerup);
233
+ }
234
+ setExpanded(expanded) {
235
+ this.expanded = expanded;
236
+ this.toastInstances.forEach((instance) => {
237
+ instance.element.dataset.expanded = String(expanded || this.options.expand);
238
+ });
239
+ // Update positions to recalculate heights for expanded/collapsed state
240
+ this.updatePositions();
241
+ }
242
+ subscribeToState() {
243
+ this.unsubscribe = ToastState.subscribe((toast) => {
244
+ if (toast.dismiss) {
245
+ this.dismissToast(toast.id);
246
+ }
247
+ else {
248
+ const toastData = toast;
249
+ // Filter by toasterId - only show toasts that match this toaster's id
250
+ // If toaster has an id, only show toasts with matching toasterId
251
+ // If toaster has no id, only show toasts without a toasterId
252
+ if (this.options.id) {
253
+ if (toastData.toasterId !== this.options.id)
254
+ return;
255
+ }
256
+ else {
257
+ if (toastData.toasterId)
258
+ return;
259
+ }
260
+ this.addOrUpdateToast(toastData);
261
+ }
262
+ });
263
+ }
264
+ addOrUpdateToast(toast) {
265
+ const existing = this.toastInstances.get(toast.id);
266
+ if (existing) {
267
+ // Update existing toast
268
+ this.updateToastElement(existing, toast);
269
+ }
270
+ else {
271
+ // Create new toast
272
+ this.createToast(toast);
273
+ }
274
+ }
275
+ createToast(toast) {
276
+ const [y, x] = (toast.position || this.options.position).split('-');
277
+ const duration = toast.duration ?? this.options.toastOptions?.duration ?? this.options.duration;
278
+ const dismissible = toast.dismissible !== false;
279
+ const closeButton = toast.closeButton ?? this.options.toastOptions?.closeButton ?? this.options.closeButton;
280
+ const toastType = toast.type;
281
+ // Create list item
282
+ const li = document.createElement('li');
283
+ li.setAttribute('tabindex', '0');
284
+ li.setAttribute('data-sonner-toast', '');
285
+ li.setAttribute('data-styled', String(!toast.custom && !toast.unstyled && !this.options.toastOptions?.unstyled));
286
+ li.setAttribute('data-mounted', 'false');
287
+ li.setAttribute('data-promise', String(Boolean(toast.promise)));
288
+ li.setAttribute('data-swiped', 'false');
289
+ li.setAttribute('data-removed', 'false');
290
+ li.setAttribute('data-visible', 'true');
291
+ li.setAttribute('data-y-position', y);
292
+ li.setAttribute('data-x-position', x);
293
+ li.setAttribute('data-front', 'true');
294
+ li.setAttribute('data-swiping', 'false');
295
+ li.setAttribute('data-dismissible', String(dismissible));
296
+ li.setAttribute('data-type', toastType || '');
297
+ li.setAttribute('data-invert', String(toast.invert ?? this.options.invert));
298
+ li.setAttribute('data-swipe-out', 'false');
299
+ li.setAttribute('data-expanded', String(this.expanded || this.options.expand));
300
+ li.setAttribute('data-rich-colors', String(toast.richColors ?? this.options.richColors));
301
+ if (toast.testId) {
302
+ li.setAttribute('data-testid', toast.testId);
303
+ }
304
+ li.className = cn(this.options.toastOptions?.className, toast.className, this.options.toastOptions?.classNames?.toast, toast.classNames?.toast, this.options.toastOptions?.classNames?.[toastType], toast.classNames?.[toastType]);
305
+ // Build toast content
306
+ this.buildToastContent(li, toast, closeButton);
307
+ // Create instance
308
+ const instance = {
309
+ toast,
310
+ element: li,
311
+ mounted: false,
312
+ removed: false,
313
+ height: 0,
314
+ offset: 0,
315
+ remainingTime: duration,
316
+ closeTimerStart: 0,
317
+ swiping: false,
318
+ swipeDirection: null,
319
+ pointerStart: null,
320
+ dragStartTime: null,
321
+ isSwiped: false,
322
+ };
323
+ // Setup pointer events for swipe
324
+ this.setupSwipeHandlers(instance, dismissible);
325
+ // Add to DOM
326
+ this.listEl?.prepend(li);
327
+ this.toastInstances.set(toast.id, instance);
328
+ // Measure height after adding to DOM
329
+ requestAnimationFrame(() => {
330
+ const height = li.getBoundingClientRect().height;
331
+ instance.height = height;
332
+ this.heights.unshift({ toastId: toast.id, height, position: toast.position || this.options.position });
333
+ // Set mounted and update positions
334
+ li.dataset.mounted = 'true';
335
+ this.updatePositions();
336
+ // Start close timer
337
+ if (toastType !== 'loading' && toast.promise === undefined && duration !== Infinity) {
338
+ this.startTimer(instance);
339
+ }
340
+ });
341
+ }
342
+ buildToastContent(li, toast, closeButton) {
343
+ const toastType = toast.type;
344
+ // Handle custom content - replaces the default structure
345
+ if (toast.custom) {
346
+ let customElement;
347
+ if (typeof toast.custom === 'string') {
348
+ // HTML string
349
+ const wrapper = document.createElement('div');
350
+ wrapper.innerHTML = toast.custom;
351
+ customElement = wrapper.firstElementChild || wrapper;
352
+ }
353
+ else if (typeof toast.custom === 'function') {
354
+ // Builder function
355
+ customElement = toast.custom(toast.id);
356
+ }
357
+ else {
358
+ // HTMLElement
359
+ customElement = toast.custom;
360
+ }
361
+ li.appendChild(customElement);
362
+ return;
363
+ }
364
+ // Close button
365
+ if (closeButton && toastType !== 'loading') {
366
+ const closeBtn = document.createElement('button');
367
+ closeBtn.setAttribute('aria-label', 'Close toast');
368
+ closeBtn.setAttribute('data-close-button', '');
369
+ closeBtn.className = cn(this.options.toastOptions?.classNames?.closeButton, toast.classNames?.closeButton);
370
+ closeBtn.innerHTML = CloseIcon;
371
+ closeBtn.addEventListener('click', () => {
372
+ if (toast.dismissible !== false) {
373
+ this.removeToast(toast);
374
+ toast.onDismiss?.(toast);
375
+ }
376
+ });
377
+ li.appendChild(closeBtn);
378
+ }
379
+ // Icon
380
+ if (toastType || toast.icon) {
381
+ const iconWrapper = document.createElement('div');
382
+ iconWrapper.setAttribute('data-icon', '');
383
+ iconWrapper.className = cn(this.options.toastOptions?.classNames?.icon, toast.classNames?.icon);
384
+ if (toast.type === 'loading' && !toast.icon) {
385
+ const loader = createLoader(true, cn(this.options.toastOptions?.classNames?.loader, toast.classNames?.loader));
386
+ iconWrapper.appendChild(loader);
387
+ }
388
+ else if (toast.icon) {
389
+ if (typeof toast.icon === 'string') {
390
+ iconWrapper.innerHTML = toast.icon;
391
+ }
392
+ else {
393
+ iconWrapper.appendChild(toast.icon);
394
+ }
395
+ }
396
+ else {
397
+ const asset = getAsset(toastType);
398
+ if (asset) {
399
+ iconWrapper.innerHTML = asset;
400
+ }
401
+ }
402
+ li.appendChild(iconWrapper);
403
+ }
404
+ // Content
405
+ const content = document.createElement('div');
406
+ content.setAttribute('data-content', '');
407
+ content.className = cn(this.options.toastOptions?.classNames?.content, toast.classNames?.content);
408
+ // Title
409
+ const title = document.createElement('div');
410
+ title.setAttribute('data-title', '');
411
+ title.className = cn(this.options.toastOptions?.classNames?.title, toast.classNames?.title);
412
+ title.textContent = toast.title || '';
413
+ content.appendChild(title);
414
+ // Description
415
+ if (toast.description) {
416
+ const desc = document.createElement('div');
417
+ desc.setAttribute('data-description', '');
418
+ desc.className = cn(this.options.toastOptions?.descriptionClassName, toast.descriptionClassName, this.options.toastOptions?.classNames?.description, toast.classNames?.description);
419
+ desc.textContent = toast.description;
420
+ content.appendChild(desc);
421
+ }
422
+ li.appendChild(content);
423
+ // Cancel button
424
+ if (toast.cancel && isAction(toast.cancel)) {
425
+ const cancelBtn = document.createElement('button');
426
+ cancelBtn.setAttribute('data-button', '');
427
+ cancelBtn.setAttribute('data-cancel', '');
428
+ cancelBtn.className = cn(this.options.toastOptions?.classNames?.cancelButton, toast.classNames?.cancelButton);
429
+ cancelBtn.textContent = toast.cancel.label;
430
+ Object.assign(cancelBtn.style, toast.cancelButtonStyle || this.options.toastOptions?.cancelButtonStyle);
431
+ cancelBtn.addEventListener('click', (e) => {
432
+ if (toast.dismissible !== false) {
433
+ toast.cancel.onClick(e);
434
+ this.removeToast(toast);
435
+ }
436
+ });
437
+ li.appendChild(cancelBtn);
438
+ }
439
+ // Action button
440
+ if (toast.action && isAction(toast.action)) {
441
+ const actionBtn = document.createElement('button');
442
+ actionBtn.setAttribute('data-button', '');
443
+ actionBtn.setAttribute('data-action', '');
444
+ actionBtn.className = cn(this.options.toastOptions?.classNames?.actionButton, toast.classNames?.actionButton);
445
+ actionBtn.textContent = toast.action.label;
446
+ Object.assign(actionBtn.style, toast.actionButtonStyle || this.options.toastOptions?.actionButtonStyle);
447
+ actionBtn.addEventListener('click', (e) => {
448
+ toast.action.onClick(e);
449
+ if (!e.defaultPrevented) {
450
+ this.removeToast(toast);
451
+ }
452
+ });
453
+ li.appendChild(actionBtn);
454
+ }
455
+ }
456
+ setupSwipeHandlers(instance, dismissible) {
457
+ const { element: li, toast } = instance;
458
+ const position = toast.position || this.options.position;
459
+ const swipeDirections = this.options.swipeDirections ?? getDefaultSwipeDirections(position);
460
+ li.addEventListener('pointerdown', (e) => {
461
+ if (e.button === 2)
462
+ return; // Right click
463
+ if (toast.type === 'loading' || !dismissible)
464
+ return;
465
+ instance.dragStartTime = new Date();
466
+ instance.pointerStart = { x: e.clientX, y: e.clientY };
467
+ e.target.setPointerCapture(e.pointerId);
468
+ if (e.target.tagName !== 'BUTTON') {
469
+ instance.swiping = true;
470
+ li.dataset.swiping = 'true';
471
+ }
472
+ });
473
+ li.addEventListener('pointermove', (e) => {
474
+ if (!instance.pointerStart || !dismissible)
475
+ return;
476
+ const isHighlighted = (window.getSelection()?.toString().length ?? 0) > 0;
477
+ if (isHighlighted)
478
+ return;
479
+ const yDelta = e.clientY - instance.pointerStart.y;
480
+ const xDelta = e.clientX - instance.pointerStart.x;
481
+ // Determine swipe direction
482
+ if (!instance.swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
483
+ instance.swipeDirection = Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y';
484
+ }
485
+ let swipeAmount = { x: 0, y: 0 };
486
+ const getDampening = (delta) => {
487
+ const factor = Math.abs(delta) / 20;
488
+ return 1 / (1.5 + factor);
489
+ };
490
+ if (instance.swipeDirection === 'y') {
491
+ if (swipeDirections.includes('top') || swipeDirections.includes('bottom')) {
492
+ if ((swipeDirections.includes('top') && yDelta < 0) || (swipeDirections.includes('bottom') && yDelta > 0)) {
493
+ swipeAmount.y = yDelta;
494
+ }
495
+ else {
496
+ const dampenedDelta = yDelta * getDampening(yDelta);
497
+ swipeAmount.y = Math.abs(dampenedDelta) < Math.abs(yDelta) ? dampenedDelta : yDelta;
498
+ }
499
+ }
500
+ }
501
+ else if (instance.swipeDirection === 'x') {
502
+ if (swipeDirections.includes('left') || swipeDirections.includes('right')) {
503
+ if ((swipeDirections.includes('left') && xDelta < 0) || (swipeDirections.includes('right') && xDelta > 0)) {
504
+ swipeAmount.x = xDelta;
505
+ }
506
+ else {
507
+ const dampenedDelta = xDelta * getDampening(xDelta);
508
+ swipeAmount.x = Math.abs(dampenedDelta) < Math.abs(xDelta) ? dampenedDelta : xDelta;
509
+ }
510
+ }
511
+ }
512
+ if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
513
+ instance.isSwiped = true;
514
+ li.dataset.swiped = 'true';
515
+ }
516
+ li.style.setProperty('--swipe-amount-x', `${swipeAmount.x}px`);
517
+ li.style.setProperty('--swipe-amount-y', `${swipeAmount.y}px`);
518
+ });
519
+ li.addEventListener('pointerup', () => {
520
+ if (!dismissible)
521
+ return;
522
+ const swipeAmountX = parseFloat(li.style.getPropertyValue('--swipe-amount-x') || '0');
523
+ const swipeAmountY = parseFloat(li.style.getPropertyValue('--swipe-amount-y') || '0');
524
+ const timeTaken = instance.dragStartTime ? new Date().getTime() - instance.dragStartTime.getTime() : 1000;
525
+ const swipeAmount = instance.swipeDirection === 'x' ? swipeAmountX : swipeAmountY;
526
+ const velocity = Math.abs(swipeAmount) / timeTaken;
527
+ if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
528
+ toast.onDismiss?.(toast);
529
+ if (instance.swipeDirection === 'x') {
530
+ li.dataset.swipeDirection = swipeAmountX > 0 ? 'right' : 'left';
531
+ }
532
+ else {
533
+ li.dataset.swipeDirection = swipeAmountY > 0 ? 'down' : 'up';
534
+ }
535
+ li.dataset.swipeOut = 'true';
536
+ this.removeToast(toast);
537
+ }
538
+ else {
539
+ li.style.setProperty('--swipe-amount-x', '0px');
540
+ li.style.setProperty('--swipe-amount-y', '0px');
541
+ }
542
+ instance.isSwiped = false;
543
+ instance.swiping = false;
544
+ instance.swipeDirection = null;
545
+ instance.pointerStart = null;
546
+ li.dataset.swiped = 'false';
547
+ li.dataset.swiping = 'false';
548
+ });
549
+ }
550
+ updateToastElement(instance, toast) {
551
+ const { element: li } = instance;
552
+ instance.toast = toast;
553
+ // Update type
554
+ li.dataset.type = toast.type || '';
555
+ // Update title
556
+ const titleEl = li.querySelector('[data-title]');
557
+ if (titleEl) {
558
+ titleEl.textContent = toast.title || '';
559
+ }
560
+ // Update description
561
+ let descEl = li.querySelector('[data-description]');
562
+ if (toast.description) {
563
+ if (!descEl) {
564
+ descEl = document.createElement('div');
565
+ descEl.setAttribute('data-description', '');
566
+ li.querySelector('[data-content]')?.appendChild(descEl);
567
+ }
568
+ descEl.textContent = toast.description;
569
+ }
570
+ else if (descEl) {
571
+ descEl.remove();
572
+ }
573
+ // Update icon if type changed
574
+ const iconEl = li.querySelector('[data-icon]');
575
+ if (iconEl && toast.type && toast.type !== 'loading') {
576
+ const asset = getAsset(toast.type);
577
+ if (asset) {
578
+ iconEl.innerHTML = asset;
579
+ }
580
+ }
581
+ // Restart timer for non-loading types
582
+ if (toast.type !== 'loading') {
583
+ instance.remainingTime = toast.duration ?? this.options.duration;
584
+ this.startTimer(instance);
585
+ }
586
+ }
587
+ startTimer(instance) {
588
+ const { toast } = instance;
589
+ if (toast.promise && toast.type === 'loading')
590
+ return;
591
+ if (toast.duration === Infinity || toast.type === 'loading')
592
+ return;
593
+ if (this.expanded || this.interacting || this.isDocumentHidden)
594
+ return;
595
+ clearTimeout(instance.closeTimeout);
596
+ instance.closeTimerStart = Date.now();
597
+ instance.closeTimeout = setTimeout(() => {
598
+ toast.onAutoClose?.(toast);
599
+ this.removeToast(toast);
600
+ }, instance.remainingTime);
601
+ }
602
+ pauseTimer(instance) {
603
+ if (instance.closeTimerStart > 0) {
604
+ const elapsed = Date.now() - instance.closeTimerStart;
605
+ instance.remainingTime = Math.max(0, instance.remainingTime - elapsed);
606
+ }
607
+ clearTimeout(instance.closeTimeout);
608
+ }
609
+ dismissToast(id) {
610
+ const instance = this.toastInstances.get(id);
611
+ if (instance) {
612
+ instance.element.dataset.removed = 'true';
613
+ this.removeToast(instance.toast);
614
+ }
615
+ }
616
+ removeToast(toast) {
617
+ const instance = this.toastInstances.get(toast.id);
618
+ if (!instance || instance.removed)
619
+ return;
620
+ instance.removed = true;
621
+ instance.element.dataset.removed = 'true';
622
+ clearTimeout(instance.closeTimeout);
623
+ // Remove from heights
624
+ this.heights = this.heights.filter((h) => h.toastId !== toast.id);
625
+ setTimeout(() => {
626
+ instance.element.remove();
627
+ this.toastInstances.delete(toast.id);
628
+ ToastState.dismiss(toast.id);
629
+ this.updatePositions();
630
+ }, TIME_BEFORE_UNMOUNT);
631
+ this.updatePositions();
632
+ }
633
+ updatePositions() {
634
+ const { visibleToasts, gap, expand } = this.options;
635
+ // Use heights array order (newest first) to determine positioning
636
+ const orderedToasts = this.heights
637
+ .map((h) => this.toastInstances.get(h.toastId))
638
+ .filter((instance) => instance !== undefined && !instance.removed);
639
+ const newFrontHeight = orderedToasts.length > 0 ? orderedToasts[0].height : 0;
640
+ const isExpanded = this.expanded || expand;
641
+ // FIRST PASS: Lock current heights to enable transitions
642
+ // This captures the current rendered height before we change anything
643
+ // We do NOT change data-front here to avoid triggering opacity changes
644
+ orderedToasts.forEach((instance, index) => {
645
+ const { element: li } = instance;
646
+ const isFront = index === 0;
647
+ if (!isFront && !isExpanded) {
648
+ // Get current computed height and set it explicitly
649
+ // This gives the transition a "from" value
650
+ const currentHeight = li.getBoundingClientRect().height;
651
+ if (currentHeight > 0) {
652
+ li.style.height = `${currentHeight}px`;
653
+ }
654
+ }
655
+ });
656
+ // Force a reflow so the browser registers the current heights
657
+ // before we change them to new values
658
+ if (orderedToasts.length > 1) {
659
+ void this.listEl?.offsetHeight;
660
+ }
661
+ // SECOND PASS: Update heights and positions (but not data-front yet)
662
+ let heightBefore = 0;
663
+ orderedToasts.forEach((instance, index) => {
664
+ const { element: li } = instance;
665
+ const isFront = index === 0;
666
+ const isVisible = index < visibleToasts;
667
+ li.dataset.index = String(index);
668
+ li.dataset.visible = String(isVisible);
669
+ const offset = index * gap + heightBefore;
670
+ instance.offset = offset;
671
+ li.style.setProperty('--index', String(index));
672
+ li.style.setProperty('--toasts-before', String(index));
673
+ li.style.setProperty('--z-index', String(orderedToasts.length - index));
674
+ li.style.setProperty('--offset', `${offset}px`);
675
+ li.style.setProperty('--initial-height', `${instance.height}px`);
676
+ // Set explicit height to enable smooth CSS transitions
677
+ if (isExpanded) {
678
+ li.style.height = `${instance.height}px`;
679
+ }
680
+ else if (isFront) {
681
+ li.style.height = '';
682
+ }
683
+ else {
684
+ li.style.height = `${newFrontHeight}px`;
685
+ }
686
+ heightBefore += instance.height;
687
+ });
688
+ // Update front toast height on the container
689
+ if (this.listEl && orderedToasts.length > 0) {
690
+ this.listEl.style.setProperty('--front-toast-height', `${newFrontHeight}px`);
691
+ }
692
+ // THIRD PASS: Update data-front
693
+ orderedToasts.forEach((instance, index) => {
694
+ instance.element.dataset.front = String(index === 0);
695
+ });
696
+ }
697
+ }
698
+ // Factory function for easier usage
699
+ export function createToaster(options) {
700
+ return new Toaster(options);
701
+ }
702
+ //# sourceMappingURL=toaster.js.map