ga-toasts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/toasts.js ADDED
@@ -0,0 +1,1005 @@
1
+ /**
2
+ * Toast component JavaScript for Genie AI Plugin
3
+ * Vanilla JS implementation - No jQuery dependency
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ // Simple utility object for internal helpers (no global namespace)
10
+ const GaToastsUtils = {
11
+ generateId: function() {
12
+ return 'toast_' + Math.random().toString(36).substr(2, 9);
13
+ },
14
+ log: function(message, data) {
15
+ if (console && console.log) {
16
+ // console.log('[GaToasts]', message, data || '');
17
+ }
18
+ }
19
+ };
20
+
21
+ // Optional external logger hook
22
+ let GaToastsLogger = null;
23
+
24
+ function logEvent(eventName, payload) {
25
+ GaToastsUtils.log(eventName, payload);
26
+ if (typeof GaToastsLogger === 'function') {
27
+ try {
28
+ GaToastsLogger(eventName, payload);
29
+ } catch (e) {
30
+ // Swallow logger errors to avoid breaking the app
31
+ }
32
+ }
33
+ }
34
+
35
+ // Helper functions
36
+ const helpers = {
37
+ // Query selector wrapper
38
+ qs: function(selector, context) {
39
+ return (context || document).querySelector(selector);
40
+ },
41
+
42
+ // Query selector all wrapper
43
+ qsa: function(selector, context) {
44
+ return Array.from((context || document).querySelectorAll(selector));
45
+ },
46
+
47
+ // Add event listener with delegation
48
+ on: function(element, event, selector, handler) {
49
+ if (typeof selector === 'function') {
50
+ handler = selector;
51
+ element.addEventListener(event, handler);
52
+ } else {
53
+ element.addEventListener(event, function(e) {
54
+ const target = e.target.closest(selector);
55
+ if (target) {
56
+ handler.call(target, e);
57
+ }
58
+ });
59
+ }
60
+ },
61
+
62
+ // Remove event listener
63
+ off: function(element, event, handler) {
64
+ element.removeEventListener(event, handler);
65
+ },
66
+
67
+ // Add class
68
+ addClass: function(element, className) {
69
+ if (!element) return;
70
+ const classes = className.split(' ');
71
+ element.classList.add(...classes);
72
+ },
73
+
74
+ // Remove class
75
+ removeClass: function(element, className) {
76
+ if (!element) return;
77
+ const classes = className.split(' ');
78
+ element.classList.remove(...classes);
79
+ },
80
+
81
+ // Has class
82
+ hasClass: function(element, className) {
83
+ return element && element.classList.contains(className);
84
+ },
85
+
86
+ // Get/Set data attribute
87
+ data: function(element, key, value) {
88
+ if (value === undefined) {
89
+ return element.dataset[key];
90
+ }
91
+ element.dataset[key] = value;
92
+ },
93
+
94
+ // Get/Set attribute
95
+ attr: function(element, key, value) {
96
+ if (value === undefined) {
97
+ return element.getAttribute(key);
98
+ }
99
+ element.setAttribute(key, value);
100
+ },
101
+
102
+ // Remove element
103
+ remove: function(element) {
104
+ if (element && element.parentNode) {
105
+ element.parentNode.removeChild(element);
106
+ }
107
+ },
108
+
109
+ // Trigger custom event
110
+ trigger: function(element, eventName, detail) {
111
+ const event = new CustomEvent(eventName, {
112
+ detail: detail,
113
+ bubbles: true,
114
+ cancelable: true
115
+ });
116
+ element.dispatchEvent(event);
117
+ },
118
+
119
+ // Extend objects (simple deep merge)
120
+ extend: function() {
121
+ const extended = {};
122
+ const deep = arguments[0] === true;
123
+ const start = deep ? 1 : 0;
124
+
125
+ for (let i = start; i < arguments.length; i++) {
126
+ const obj = arguments[i];
127
+ if (!obj) continue;
128
+
129
+ for (let key in obj) {
130
+ if (obj.hasOwnProperty(key)) {
131
+ if (deep && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
132
+ extended[key] = this.extend(true, extended[key] || {}, obj[key]);
133
+ } else {
134
+ extended[key] = obj[key];
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return extended;
140
+ },
141
+
142
+ // Create element from HTML string
143
+ createElement: function(html) {
144
+ const template = document.createElement('template');
145
+ template.innerHTML = html.trim();
146
+ return template.content.firstChild;
147
+ }
148
+ };
149
+
150
+ // Toast component
151
+ const Toast = {
152
+ /**
153
+ * Initialize toasts
154
+ */
155
+ init: function () {
156
+ this.bindEvents();
157
+ this.initDefaultContainer();
158
+ },
159
+
160
+ /**
161
+ * Bind events
162
+ */
163
+ bindEvents: function () {
164
+ const self = this;
165
+
166
+ // Close toast triggers
167
+ helpers.on(document, 'click', '[data-ga-toast-close], .ga-toast-close', function (e) {
168
+ e.preventDefault();
169
+ const toastId = helpers.data(this, 'gaToastClose');
170
+ if (toastId) {
171
+ self.close(helpers.qs('#' + toastId));
172
+ } else {
173
+ self.close(this.closest('.ga-toast'));
174
+ }
175
+ });
176
+
177
+ // Auto-close toasts with pause on hover
178
+ helpers.on(document, 'ga:toast:shown', '.ga-toast', function (e) {
179
+ const toast = e.target;
180
+ const autoClose = parseInt(helpers.data(toast, 'gaAutoClose'));
181
+ const pauseOnHover = helpers.data(toast, 'gaPauseOnHover') === 'true';
182
+ const progress = helpers.qs('.ga-toast-progress', toast);
183
+
184
+ if (autoClose && autoClose > 0) {
185
+ let timeoutId;
186
+ let startTime = Date.now();
187
+ let remainingTime = autoClose;
188
+
189
+ const startCountdown = function() {
190
+ startTime = Date.now();
191
+ timeoutId = setTimeout(function () {
192
+ if (toast && !helpers.hasClass(toast, 'hide')) {
193
+ self.close(toast);
194
+ }
195
+ }, remainingTime);
196
+ };
197
+
198
+ // Start initial countdown
199
+ startCountdown();
200
+
201
+ // Pause on hover if enabled
202
+ if (pauseOnHover) {
203
+ toast.addEventListener('mouseenter', function() {
204
+ const elapsed = Date.now() - startTime;
205
+ remainingTime = Math.max(0, remainingTime - elapsed);
206
+ clearTimeout(timeoutId);
207
+ helpers.addClass(toast, 'ga-toast-paused');
208
+
209
+ // Pause progress bar animation
210
+ if (progress) {
211
+ const currentTransform = getComputedStyle(progress).transform;
212
+ progress.style.transition = 'none';
213
+ helpers.attr(progress, 'data-paused-transform', currentTransform);
214
+ }
215
+ });
216
+
217
+ toast.addEventListener('mouseleave', function() {
218
+ helpers.removeClass(toast, 'ga-toast-paused');
219
+
220
+ // Resume progress bar animation
221
+ if (progress) {
222
+ progress.style.transition = 'transform ' + remainingTime + 'ms linear';
223
+ progress.style.transform = 'scaleX(0)';
224
+ }
225
+
226
+ startCountdown();
227
+ });
228
+ }
229
+ }
230
+ });
231
+
232
+ // Handle toast click to close (optional)
233
+ helpers.on(document, 'click', '.ga-toast', function (e) {
234
+ if (helpers.attr(this, 'data-ga-click-to-close') === 'true') {
235
+ if (!e.target.closest('.ga-toast-actions, .ga-toast-close')) {
236
+ self.close(this);
237
+ }
238
+ }
239
+ });
240
+ },
241
+
242
+ /**
243
+ * Initialize default container
244
+ */
245
+ initDefaultContainer: function () {
246
+ if (helpers.qs('.ga-container')) {
247
+ // Create default container if it doesn't exist
248
+ this.getContainer('top-end');
249
+ }
250
+ },
251
+
252
+ /**
253
+ * Show toast
254
+ */
255
+ show: function (options) {
256
+ const defaults = {
257
+ id: 'ga-toast-' + GaToastsUtils.generateId(),
258
+ title: '',
259
+ message: '',
260
+ meta: '',
261
+ type: 'info',
262
+ duration: 5000,
263
+ closable: true,
264
+ position: 'top-end',
265
+ icon: null,
266
+ actions: [],
267
+ size: '',
268
+ variant: '', // '', 'filled', 'light'
269
+ animation: 'slide', // 'fade', 'slide', 'bounce', 'scale'
270
+ clickToClose: false,
271
+ swipeToClose: true,
272
+ // Media / visual
273
+ avatar: null,
274
+ avatarAlt: '',
275
+ unread: false,
276
+ truncateTitle: false,
277
+ progress: true,
278
+ progressBackground: true,
279
+ pauseOnHover: true,
280
+ // UI niceties
281
+ compact: false,
282
+ showStatus: false,
283
+ statusText: '',
284
+ autoIcon: true
285
+ };
286
+
287
+ // Merge instance defaults (set via setDefaults) with per-call options
288
+ const mergedOptions = this.getMergedOptions
289
+ ? this.getMergedOptions(options || {})
290
+ : (options || {});
291
+
292
+ const settings = helpers.extend({}, defaults, mergedOptions);
293
+
294
+ // Compact toasts: default to click-to-close if not explicitly set
295
+ if (settings.compact && mergedOptions && mergedOptions.clickToClose == null) {
296
+ settings.clickToClose = true;
297
+ }
298
+
299
+ const toast = this.create(settings);
300
+
301
+ // Enable swipe-to-close interaction if configured
302
+ if (settings.swipeToClose) {
303
+ this.attachSwipeHandlers(toast);
304
+ }
305
+ const container = this.getContainer(settings.position);
306
+
307
+ container.appendChild(toast);
308
+
309
+ // Trigger animation after a small delay to ensure DOM is ready
310
+ setTimeout(function () {
311
+ helpers.addClass(toast, 'show');
312
+ helpers.trigger(toast, 'ga:toast:shown');
313
+ }, 10);
314
+
315
+ logEvent('toast:shown', settings);
316
+
317
+ return toast;
318
+ },
319
+
320
+ /**
321
+ * Create toast element
322
+ */
323
+ create: function (options) {
324
+ // Ensure options is an object
325
+ options = options || {};
326
+
327
+ const classes = ['ga-toast', 'ga-toast-' + options.type];
328
+
329
+ // Add size class
330
+ if (options.size) {
331
+ classes.push('ga-toast-' + options.size);
332
+ }
333
+
334
+ // Add variant class
335
+ if (options.variant) {
336
+ classes.push('ga-toast-' + options.type + '-' + options.variant);
337
+ }
338
+
339
+ // Add animation class
340
+ if (options.animation) {
341
+ classes.push(options.animation);
342
+ }
343
+
344
+ // Compact density
345
+ if (options.compact) {
346
+ classes.push('ga-toast-compact');
347
+ }
348
+
349
+ // Add modern styling classes
350
+ classes.push('ga-toast-modern');
351
+
352
+ // Add glassmorphism effect
353
+ if (options.glassmorphism !== false) {
354
+ classes.push('ga-toast-glass');
355
+ }
356
+
357
+ const toast = document.createElement('div');
358
+ toast.className = classes.join(' ');
359
+ toast.id = options.id;
360
+
361
+ // Add click to close attribute
362
+ if (options.clickToClose) {
363
+ helpers.attr(toast, 'data-ga-click-to-close', 'true');
364
+ }
365
+
366
+ // Add pause on hover attribute
367
+ if (options.pauseOnHover) {
368
+ helpers.attr(toast, 'data-ga-pause-on-hover', 'true');
369
+ }
370
+
371
+ // Create content wrapper
372
+ const content = document.createElement('div');
373
+ content.className = 'ga-toast-content';
374
+
375
+ // Add header if title, closable, or icon exists
376
+ if (options.title || options.closable || options.icon) {
377
+ const header = document.createElement('div');
378
+ header.className = 'ga-toast-header';
379
+
380
+ // Left section (avatar + icon + title)
381
+ const left = document.createElement('div');
382
+ left.className = 'ga-toast-header-left';
383
+
384
+ // Optional avatar (URL string)
385
+ if (options.avatar) {
386
+ const avatar = document.createElement('img');
387
+ avatar.className = 'ga-toast-avatar';
388
+ avatar.src = options.avatar;
389
+ if (options.avatarAlt) {
390
+ avatar.alt = options.avatarAlt;
391
+ } else if (options.title) {
392
+ avatar.alt = options.title;
393
+ } else {
394
+ avatar.alt = 'Notification avatar';
395
+ }
396
+ left.appendChild(avatar);
397
+ }
398
+
399
+ // Auto icon support
400
+ let resolvedIcon = options.icon;
401
+ if (!resolvedIcon && options.autoIcon !== false) {
402
+ const type = options.type || 'info';
403
+ const iconMap = {
404
+ success: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>',
405
+ error: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>',
406
+ warning: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>',
407
+ info: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>',
408
+ primary: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path d="M4 4h12v12H4z" opacity="0.2"></path><path d="M5 5h10v10H5z"></path></svg>',
409
+ secondary: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><circle cx="10" cy="10" r="6"></circle></svg>'
410
+ };
411
+ resolvedIcon = iconMap[type] || null;
412
+ }
413
+
414
+ if (resolvedIcon) {
415
+ const iconDiv = document.createElement('div');
416
+ iconDiv.className = 'ga-toast-icon';
417
+ iconDiv.innerHTML = resolvedIcon;
418
+ left.appendChild(iconDiv);
419
+ }
420
+
421
+ // Optional unread dot
422
+ if (options.unread) {
423
+ const unreadDot = document.createElement('span');
424
+ unreadDot.className = 'ga-toast-unread-dot';
425
+ left.appendChild(unreadDot);
426
+ }
427
+
428
+ if (options.title) {
429
+ const title = document.createElement('h4');
430
+ title.className = 'ga-toast-title';
431
+ title.textContent = options.title;
432
+ if (options.truncateTitle) {
433
+ title.classList.add('ga-toast-title-truncate');
434
+ }
435
+ left.appendChild(title);
436
+ }
437
+
438
+ // Optional status label
439
+ if (options.showStatus && options.type) {
440
+ const status = document.createElement('span');
441
+ status.className = 'ga-toast-status';
442
+ const label = (options.statusText || options.type)
443
+ .toString()
444
+ .replace(/^\w/, c => c.toUpperCase());
445
+ status.textContent = label;
446
+ header.appendChild(status);
447
+ }
448
+
449
+ header.appendChild(left);
450
+
451
+ // Right section (close button)
452
+ if (options.closable) {
453
+ const closeBtn = document.createElement('button');
454
+ closeBtn.type = 'button';
455
+ closeBtn.className = 'ga-toast-close';
456
+ helpers.attr(closeBtn, 'aria-label', 'Close');
457
+ helpers.attr(closeBtn, 'data-ga-toast-close', '');
458
+ header.appendChild(closeBtn);
459
+ }
460
+
461
+ content.appendChild(header);
462
+ }
463
+
464
+ // Add body if there is message or meta
465
+ // For compact toasts we still render the content, but keep the layout
466
+ // tight via CSS (see `.ga-toast-compact .ga-toast-body`).
467
+ if (options.message || options.meta) {
468
+ const body = document.createElement('div');
469
+ body.className = 'ga-toast-body';
470
+
471
+ if (options.meta) {
472
+ const meta = document.createElement('div');
473
+ meta.className = 'ga-toast-meta';
474
+ meta.textContent = options.meta;
475
+ body.appendChild(meta);
476
+ }
477
+
478
+ if (options.message) {
479
+ const messageEl = document.createElement('div');
480
+ messageEl.className = 'ga-toast-message';
481
+ messageEl.innerHTML = options.message;
482
+ body.appendChild(messageEl);
483
+ }
484
+
485
+ content.appendChild(body);
486
+ }
487
+
488
+ // Add actions if they exist (skip for compact toasts)
489
+ if (!options.compact && options.actions && options.actions.length > 0) {
490
+ const actionsDiv = document.createElement('div');
491
+ actionsDiv.className = 'ga-toast-actions';
492
+ const self = this;
493
+
494
+ options.actions.forEach(function (action) {
495
+ const btnClasses = ['ga-btn', 'ga-btn-sm'];
496
+ if (action.class) {
497
+ btnClasses.push(action.class);
498
+ } else {
499
+ btnClasses.push('ga-btn-secondary');
500
+ }
501
+
502
+ const btn = document.createElement('button');
503
+ btn.type = 'button';
504
+ btn.className = btnClasses.join(' ');
505
+ btn.textContent = action.text;
506
+
507
+ if (action.click) {
508
+ btn.addEventListener('click', function (e) {
509
+ action.click(e, toast);
510
+ });
511
+ }
512
+
513
+ actionsDiv.appendChild(btn);
514
+ });
515
+
516
+ content.appendChild(actionsDiv);
517
+ }
518
+
519
+ toast.appendChild(content);
520
+
521
+ // Add progress bar if enabled
522
+ if (options.progress && options.duration && options.duration > 0 && options.progressPosition !== 'none') {
523
+ const progress = document.createElement('div');
524
+ progress.className = 'ga-toast-progress ga-toast-progress-' + options.type;
525
+ if (options.progressPosition === 'top') {
526
+ helpers.addClass(progress, 'ga-toast-progress-top');
527
+ }
528
+ progress.style.width = '100%';
529
+ progress.style.transform = 'scaleX(1)';
530
+ progress.style.transformOrigin = 'left center';
531
+ toast.appendChild(progress);
532
+
533
+ // Animate progress bar with smooth countdown
534
+ setTimeout(function () {
535
+ progress.style.transition = 'transform ' + options.duration + 'ms linear';
536
+ progress.style.transform = 'scaleX(0)';
537
+ }, 10);
538
+ }
539
+
540
+ // Add background fill with opacity if enabled
541
+ if (options.progressBackground && options.duration && options.duration > 0) {
542
+ const backgroundFill = document.createElement('div');
543
+ backgroundFill.className = 'ga-toast-background-fill ga-toast-bg-' + options.type;
544
+ toast.appendChild(backgroundFill);
545
+
546
+ // Animate background fill
547
+ setTimeout(function () {
548
+ backgroundFill.style.transition = 'opacity ' + options.duration + 'ms linear';
549
+ backgroundFill.style.opacity = '0';
550
+ }, 10);
551
+ }
552
+
553
+ // Segmented steps indicator for multi-step flows
554
+ if (options.steps && options.steps > 1) {
555
+ const stepsContainer = document.createElement('div');
556
+ stepsContainer.className = 'ga-toast-steps';
557
+ const activeIndex = Math.min(
558
+ options.steps,
559
+ Math.max(1, options.currentStep || 1)
560
+ );
561
+ for (let i = 1; i <= options.steps; i++) {
562
+ const stepEl = document.createElement('div');
563
+ stepEl.className = 'ga-toast-step';
564
+ if (i <= activeIndex) {
565
+ helpers.addClass(stepEl, 'ga-toast-step-active');
566
+ }
567
+ stepsContainer.appendChild(stepEl);
568
+ }
569
+ content.appendChild(stepsContainer);
570
+ }
571
+
572
+ // Add auto-close data
573
+ if (options.duration && options.duration > 0) {
574
+ helpers.data(toast, 'gaAutoClose', options.duration.toString());
575
+ }
576
+
577
+ return toast;
578
+ },
579
+
580
+ /**
581
+ * Attach swipe-to-close interaction (primarily for touch devices)
582
+ */
583
+ attachSwipeHandlers: function (toast) {
584
+ if (!toast) return;
585
+
586
+ const self = this;
587
+ let pointerDown = false;
588
+ let startX = 0;
589
+ let startY = 0;
590
+ let currentX = 0;
591
+ let startTime = 0;
592
+
593
+ const SWIPE_DISTANCE = 60;
594
+
595
+ function onPointerDown(e) {
596
+ if (e.pointerType === 'mouse' && e.button !== 0) return;
597
+ pointerDown = true;
598
+ startX = e.clientX;
599
+ startY = e.clientY;
600
+ currentX = startX;
601
+ startTime = Date.now();
602
+ toast.setPointerCapture && toast.setPointerCapture(e.pointerId);
603
+ }
604
+
605
+ function onPointerMove(e) {
606
+ if (!pointerDown) return;
607
+ currentX = e.clientX;
608
+ const dx = currentX - startX;
609
+ const dy = e.clientY - startY;
610
+
611
+ // Only treat mostly horizontal movement as swipe
612
+ if (Math.abs(dx) < 5 || Math.abs(dx) < Math.abs(dy)) {
613
+ return;
614
+ }
615
+
616
+ e.preventDefault();
617
+
618
+ const translateX = dx;
619
+ const opacity = Math.max(0.2, 1 - Math.abs(dx) / 200);
620
+ toast.style.transform = 'translateX(' + translateX + 'px)';
621
+ toast.style.opacity = String(opacity);
622
+ }
623
+
624
+ function onPointerUp(e) {
625
+ if (!pointerDown) return;
626
+ pointerDown = false;
627
+
628
+ const dx = currentX - startX;
629
+ const dy = e.clientY - startY;
630
+ const elapsed = Date.now() - startTime;
631
+
632
+ const isHorizontal = Math.abs(dx) > Math.abs(dy);
633
+ const passedDistance = Math.abs(dx) > SWIPE_DISTANCE;
634
+
635
+ // Reset visual state
636
+ toast.style.transform = '';
637
+ toast.style.opacity = '';
638
+
639
+ if (isHorizontal && passedDistance) {
640
+ self.close(toast);
641
+ }
642
+ }
643
+
644
+ toast.addEventListener('pointerdown', onPointerDown);
645
+ toast.addEventListener('pointermove', onPointerMove);
646
+ toast.addEventListener('pointerup', onPointerUp);
647
+ toast.addEventListener('pointercancel', onPointerUp);
648
+ },
649
+
650
+ /**
651
+ * Close toast
652
+ */
653
+ close: function (toast) {
654
+ if (!toast || helpers.hasClass(toast, 'hide')) {
655
+ return;
656
+ }
657
+
658
+ // Remove any active event listeners
659
+ const newToast = toast.cloneNode(true);
660
+ toast.parentNode.replaceChild(newToast, toast);
661
+ toast = newToast;
662
+
663
+ helpers.removeClass(toast, 'show');
664
+ helpers.addClass(toast, 'hide');
665
+
666
+ const removeDelay = 300; // Match CSS transition duration
667
+
668
+ setTimeout(function () {
669
+ if (toast) {
670
+ helpers.trigger(toast, 'ga:toast:closed');
671
+ helpers.remove(toast);
672
+ }
673
+ }, removeDelay);
674
+
675
+ logEvent('toast:closed', toast.id);
676
+ },
677
+
678
+ /**
679
+ * Close all toasts
680
+ */
681
+ closeAll: function () {
682
+ const self = this;
683
+ helpers.qsa('.ga-toast').forEach(function (toast) {
684
+ self.close(toast);
685
+ });
686
+ },
687
+
688
+ /**
689
+ * Get or create container by position
690
+ */
691
+ getContainer: function (position) {
692
+ const containerId = 'ga-toast-container-' + position;
693
+ let container = helpers.qs('#' + containerId);
694
+
695
+ if (!container) {
696
+ container = document.createElement('div');
697
+ container.id = containerId;
698
+ container.className = 'ga-toast-container ga-toast-container-' + position;
699
+
700
+ // Try to append to ga-container first, then body
701
+ const gaContainer = helpers.qs('.ga-container');
702
+ if (gaContainer) {
703
+ gaContainer.appendChild(container);
704
+ } else {
705
+ document.body.appendChild(container);
706
+ }
707
+ }
708
+
709
+ return container;
710
+ },
711
+
712
+ /**
713
+ * Show success toast
714
+ */
715
+ success: function (message, options) {
716
+ const settings = helpers.extend({}, options || {}, {
717
+ type: 'success',
718
+ message: message,
719
+ duration: (options && options.duration) || 5000,
720
+ icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>'
721
+ });
722
+ return this.show(settings);
723
+ },
724
+
725
+ /**
726
+ * Show error toast
727
+ */
728
+ error: function (message, options) {
729
+ console.log('error', options);
730
+ const settings = helpers.extend({}, options || {}, {
731
+ type: 'error',
732
+ message: message,
733
+ duration: (options && options.duration) || 8000,
734
+ icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>'
735
+ });
736
+ return this.show(settings);
737
+ },
738
+
739
+ /**
740
+ * Show warning toast
741
+ */
742
+ warning: function (message, options) {
743
+ const settings = helpers.extend({}, options || {}, {
744
+ type: 'warning',
745
+ message: message,
746
+ duration: (options && options.duration) || 6000,
747
+ icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>'
748
+ });
749
+ return this.show(settings);
750
+ },
751
+
752
+ /**
753
+ * Show info toast
754
+ */
755
+ info: function (message, options) {
756
+ const settings = helpers.extend({}, options || {}, {
757
+ type: 'info',
758
+ message: message,
759
+ duration: (options && options.duration) || 4000,
760
+ icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>'
761
+ });
762
+ return this.show(settings);
763
+ },
764
+
765
+ /**
766
+ * Show confirmation toast
767
+ */
768
+ confirm: function (message, options) {
769
+ const self = this;
770
+ const defaults = {
771
+ type: 'warning',
772
+ message: message,
773
+ duration: 0,
774
+ closable: false,
775
+ actions: [
776
+ {
777
+ text: 'Cancel',
778
+ class: 'ga-btn-secondary',
779
+ click: function (e, toast) {
780
+ self.close(toast);
781
+ if (options && options.onCancel) {
782
+ options.onCancel();
783
+ }
784
+ }
785
+ },
786
+ {
787
+ text: 'Confirm',
788
+ class: 'ga-btn-primary',
789
+ click: function (e, toast) {
790
+ self.close(toast);
791
+ if (options && options.onConfirm) {
792
+ options.onConfirm();
793
+ }
794
+ }
795
+ }
796
+ ]
797
+ };
798
+
799
+ const settings = helpers.extend(true, {}, defaults, options);
800
+ return this.show(settings);
801
+ },
802
+
803
+ /**
804
+ * Show loading toast
805
+ */
806
+ loading: function (message, options) {
807
+ const defaults = {
808
+ type: 'info',
809
+ message: message || 'Loading...',
810
+ closable: false,
811
+ duration: 0,
812
+ icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/><path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="ga-spin"/></svg>'
813
+ };
814
+
815
+ const settings = helpers.extend({}, defaults, options);
816
+ const toast = this.show(settings);
817
+ helpers.addClass(toast, 'ga-toast-loading');
818
+ return toast;
819
+ },
820
+
821
+ /**
822
+ * Update toast content
823
+ */
824
+ update: function (toastId, options) {
825
+ options = options || {};
826
+
827
+ const toast = helpers.qs('#' + toastId);
828
+ if (!toast) {
829
+ logEvent('toast:update:not-found', toastId);
830
+ return false;
831
+ }
832
+
833
+ if (options.title !== undefined) {
834
+ const title = helpers.qs('.ga-toast-title', toast);
835
+ if (title) {
836
+ title.textContent = options.title;
837
+ } else if (options.title) {
838
+ let header = helpers.qs('.ga-toast-header', toast);
839
+ if (!header) {
840
+ header = document.createElement('div');
841
+ header.className = 'ga-toast-header';
842
+ const content = helpers.qs('.ga-toast-content', toast);
843
+ content.insertBefore(header, content.firstChild);
844
+ }
845
+ const titleEl = document.createElement('h4');
846
+ titleEl.className = 'ga-toast-title';
847
+ titleEl.textContent = options.title;
848
+ header.insertBefore(titleEl, header.firstChild);
849
+ }
850
+ }
851
+
852
+ if (options.message !== undefined) {
853
+ const messageEl = helpers.qs('.ga-toast-body', toast);
854
+ if (messageEl) {
855
+ messageEl.innerHTML = options.message;
856
+ }
857
+ }
858
+
859
+ if (options.type) {
860
+ helpers.removeClass(toast, 'ga-toast-success ga-toast-error ga-toast-warning ga-toast-info ga-toast-primary ga-toast-secondary');
861
+ helpers.addClass(toast, 'ga-toast-' + options.type);
862
+ }
863
+
864
+ if (options.icon !== undefined) {
865
+ const icon = helpers.qs('.ga-toast-icon', toast);
866
+ if (options.icon) {
867
+ if (icon) {
868
+ icon.innerHTML = options.icon;
869
+ } else {
870
+ const iconDiv = document.createElement('div');
871
+ iconDiv.className = 'ga-toast-icon';
872
+ iconDiv.innerHTML = options.icon;
873
+ toast.insertBefore(iconDiv, toast.firstChild);
874
+ }
875
+ } else if (icon) {
876
+ helpers.remove(icon);
877
+ }
878
+ }
879
+
880
+ return true;
881
+ },
882
+
883
+ /**
884
+ * Get toast count
885
+ */
886
+ getCount: function (type) {
887
+ if (type) {
888
+ return helpers.qsa('.ga-toast.ga-toast-' + type).length;
889
+ }
890
+ return helpers.qsa('.ga-toast').length;
891
+ },
892
+
893
+ /**
894
+ * Clear all toasts
895
+ */
896
+ clear: function (type) {
897
+ const self = this;
898
+ if (type) {
899
+ helpers.qsa('.ga-toast.ga-toast-' + type).forEach(function (toast) {
900
+ self.close(toast);
901
+ });
902
+ } else {
903
+ this.closeAll();
904
+ }
905
+ },
906
+
907
+ /**
908
+ * Check if toast exists
909
+ */
910
+ exists: function (toastId) {
911
+ return !!helpers.qs('#' + toastId);
912
+ },
913
+
914
+ /**
915
+ * Get toast by ID
916
+ */
917
+ get: function (toastId) {
918
+ return helpers.qs('#' + toastId);
919
+ },
920
+
921
+ /**
922
+ * Set global defaults
923
+ */
924
+ setDefaults: function (defaults) {
925
+ this.globalDefaults = helpers.extend({}, this.globalDefaults || {}, defaults);
926
+ },
927
+
928
+ /**
929
+ * Get merged options with global defaults
930
+ */
931
+ getMergedOptions: function (options) {
932
+ return helpers.extend({}, this.globalDefaults || {}, options);
933
+ },
934
+
935
+ /**
936
+ * Set external logger callback
937
+ */
938
+ setLogger: function (logger) {
939
+ GaToastsLogger = typeof logger === 'function' ? logger : null;
940
+ },
941
+
942
+ /**
943
+ * Show modern toast with enhanced styling
944
+ */
945
+ modern: function (message, options) {
946
+ const defaults = {
947
+ message: message,
948
+ progress: true,
949
+ progressBackground: true,
950
+ pauseOnHover: true,
951
+ glassmorphism: true,
952
+ animation: 'slide',
953
+ size: 'md',
954
+ variant: 'light'
955
+ };
956
+
957
+ const settings = helpers.extend({}, defaults, options);
958
+ return this.show(settings);
959
+ },
960
+
961
+ /**
962
+ * Show notification with modern styling
963
+ */
964
+ notification: function (title, message, options) {
965
+ const defaults = {
966
+ title: title,
967
+ message: message,
968
+ type: 'info',
969
+ progress: true,
970
+ progressBackground: true,
971
+ pauseOnHover: true,
972
+ glassmorphism: true,
973
+ animation: 'slide',
974
+ size: 'md',
975
+ duration: 4000
976
+ };
977
+
978
+ const settings = helpers.extend({}, defaults, options);
979
+ return this.show(settings);
980
+ }
981
+ };
982
+
983
+ // Initialize when document is ready
984
+ if (document.readyState === 'loading') {
985
+ document.addEventListener('DOMContentLoaded', function() {
986
+ Toast.init();
987
+ });
988
+ } else {
989
+ Toast.init();
990
+ }
991
+
992
+ // Primary global export
993
+ var GaToasts = Toast;
994
+ window.GaToasts = GaToasts;
995
+
996
+ // Framework / module support (CommonJS, AMD)
997
+ if (typeof module !== 'undefined' && module.exports) {
998
+ module.exports = GaToasts;
999
+ } else if (typeof define === 'function' && define.amd) {
1000
+ define([], function () {
1001
+ return GaToasts;
1002
+ });
1003
+ }
1004
+
1005
+ })();