@liwe3/webcomponents 1.0.0 → 1.0.14

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/Toast.ts ADDED
@@ -0,0 +1,770 @@
1
+ /**
2
+ * Toast Web Component
3
+ * A customizable toast notification system with multiple types, icons, buttons, and auto-dismiss
4
+ */
5
+
6
+ export type ToastType = 'info' | 'warning' | 'error' | 'success';
7
+
8
+ export type ToastButton = {
9
+ label: string;
10
+ onClick: () => void;
11
+ };
12
+
13
+ export type ToastConfig = {
14
+ title: string;
15
+ text: string;
16
+ type?: ToastType;
17
+ icon?: string; // URL to icon/image
18
+ buttons?: ToastButton[];
19
+ closable?: boolean; // Show close X button
20
+ duration?: number; // Auto-dismiss after x milliseconds (0 = no auto-dismiss, default: 5000ms)
21
+ onClose?: () => void;
22
+ };
23
+
24
+ export class ToastElement extends HTMLElement {
25
+ declare shadowRoot: ShadowRoot;
26
+ private config: ToastConfig = {
27
+ title: '',
28
+ text: '',
29
+ type: 'info',
30
+ closable: true,
31
+ duration: 5000
32
+ };
33
+ private autoCloseTimer?: number;
34
+ private remainingTime: number = 0;
35
+ private pauseTime: number = 0;
36
+ private progressBar?: HTMLElement;
37
+
38
+ constructor () {
39
+ super();
40
+ this.attachShadow( { mode: 'open' } );
41
+ }
42
+
43
+ static get observedAttributes (): string[] {
44
+ return [ 'title', 'text', 'type', 'icon', 'closable', 'duration', 'buttons' ];
45
+ }
46
+
47
+ attributeChangedCallback ( _name: string, oldValue: string | null, newValue: string | null ): void {
48
+ if ( oldValue !== newValue ) {
49
+ this.render();
50
+ }
51
+ }
52
+
53
+ connectedCallback (): void {
54
+ this.render();
55
+ this.startAutoCloseTimer();
56
+ }
57
+
58
+ disconnectedCallback (): void {
59
+ this.clearAutoCloseTimer();
60
+ }
61
+
62
+ get title (): string {
63
+ return this.getAttribute( 'title' ) || this.config.title;
64
+ }
65
+
66
+ set title ( value: string ) {
67
+ this.setAttribute( 'title', value );
68
+ this.config.title = value;
69
+ }
70
+
71
+ get text (): string {
72
+ return this.getAttribute( 'text' ) || this.config.text;
73
+ }
74
+
75
+ set text ( value: string ) {
76
+ this.setAttribute( 'text', value );
77
+ this.config.text = value;
78
+ }
79
+
80
+ get type (): ToastType {
81
+ const attr = this.getAttribute( 'type' );
82
+ return ( attr as ToastType ) || this.config.type || 'info';
83
+ }
84
+
85
+ set type ( value: ToastType ) {
86
+ this.setAttribute( 'type', value );
87
+ this.config.type = value;
88
+ }
89
+
90
+ get icon (): string | undefined {
91
+ return this.getAttribute( 'icon' ) || this.config.icon;
92
+ }
93
+
94
+ set icon ( value: string | undefined ) {
95
+ if ( value ) {
96
+ this.setAttribute( 'icon', value );
97
+ this.config.icon = value;
98
+ } else {
99
+ this.removeAttribute( 'icon' );
100
+ this.config.icon = undefined;
101
+ }
102
+ }
103
+
104
+ get closable (): boolean {
105
+ if ( this.hasAttribute( 'closable' ) ) {
106
+ return this.getAttribute( 'closable' ) !== 'false';
107
+ }
108
+ return this.config.closable !== false;
109
+ }
110
+
111
+ set closable ( value: boolean ) {
112
+ if ( value ) {
113
+ this.setAttribute( 'closable', 'true' );
114
+ } else {
115
+ this.setAttribute( 'closable', 'false' );
116
+ }
117
+ this.config.closable = value;
118
+ }
119
+
120
+ get duration (): number {
121
+ const attr = this.getAttribute( 'duration' );
122
+ if ( attr ) {
123
+ return parseInt( attr, 10 );
124
+ }
125
+ return this.config.duration ?? 5000;
126
+ }
127
+
128
+ set duration ( value: number ) {
129
+ this.setAttribute( 'duration', value.toString() );
130
+ this.config.duration = value;
131
+ }
132
+
133
+ get buttons (): ToastButton[] {
134
+ const attr = this.getAttribute( 'buttons' );
135
+ if ( attr ) {
136
+ try {
137
+ return JSON.parse( attr );
138
+ } catch ( e ) {
139
+ console.error( 'Invalid buttons format:', e );
140
+ return [];
141
+ }
142
+ }
143
+ return this.config.buttons || [];
144
+ }
145
+
146
+ set buttons ( value: ToastButton[] ) {
147
+ this.setAttribute( 'buttons', JSON.stringify( value ) );
148
+ this.config.buttons = value;
149
+ }
150
+
151
+ /**
152
+ * Shows the toast with the given configuration
153
+ */
154
+ show ( config: ToastConfig ): void {
155
+ this.config = { ...this.config, ...config };
156
+
157
+ // If buttons are present, force duration to 0 (user must interact to close)
158
+ if ( config.buttons && config.buttons.length > 0 ) {
159
+ this.config.duration = 0;
160
+ }
161
+
162
+ // Sync config to attributes
163
+ this.title = config.title;
164
+ this.text = config.text;
165
+ if ( config.type ) this.type = config.type;
166
+ if ( config.icon !== undefined ) this.icon = config.icon;
167
+ if ( config.closable !== undefined ) this.closable = config.closable;
168
+ if ( config.buttons && config.buttons.length > 0 ) {
169
+ // Force duration to 0 when buttons are present
170
+ this.duration = 0;
171
+ } else if ( config.duration !== undefined ) {
172
+ this.duration = config.duration;
173
+ }
174
+ if ( config.buttons ) this.buttons = config.buttons;
175
+
176
+ this.render();
177
+ this.startAutoCloseTimer();
178
+ }
179
+
180
+ /**
181
+ * Closes the toast
182
+ */
183
+ close (): void {
184
+ this.clearAutoCloseTimer();
185
+
186
+ // Add closing animation
187
+ const container = this.shadowRoot.querySelector( '.toast-container' ) as HTMLElement;
188
+ if ( container ) {
189
+ // Use requestAnimationFrame to ensure smooth animation
190
+ requestAnimationFrame( () => {
191
+ container.classList.add( 'closing' );
192
+ } );
193
+
194
+ // Listen for animation end event for smoother transition
195
+ const handleAnimationEnd = () => {
196
+ container.removeEventListener( 'animationend', handleAnimationEnd );
197
+
198
+ // Animate the host element collapsing (height and margin to 0)
199
+ const hostElement = this as unknown as HTMLElement;
200
+ const currentHeight = hostElement.offsetHeight;
201
+
202
+ // Set explicit height for animation
203
+ hostElement.style.height = `${ currentHeight }px`;
204
+ hostElement.style.marginBottom = '12px';
205
+
206
+ // Force reflow
207
+ void hostElement.offsetHeight;
208
+
209
+ // Animate to 0
210
+ hostElement.style.height = '0px';
211
+ hostElement.style.marginBottom = '0px';
212
+ hostElement.style.opacity = '0';
213
+
214
+ // Wait for transition to complete, then remove
215
+ setTimeout( () => {
216
+ this.dispatchEvent( new CustomEvent( 'close' ) );
217
+ if ( this.config.onClose ) {
218
+ this.config.onClose();
219
+ }
220
+ this.remove();
221
+ }, 300 );
222
+ };
223
+
224
+ container.addEventListener( 'animationend', handleAnimationEnd );
225
+
226
+ // Fallback timeout in case animationend doesn't fire
227
+ setTimeout( () => {
228
+ if ( this.isConnected ) {
229
+ handleAnimationEnd();
230
+ }
231
+ }, 350 );
232
+ } else {
233
+ this.dispatchEvent( new CustomEvent( 'close' ) );
234
+ if ( this.config.onClose ) {
235
+ this.config.onClose();
236
+ }
237
+ this.remove();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Starts the auto-close timer if duration is set
243
+ */
244
+ private startAutoCloseTimer (): void {
245
+ this.clearAutoCloseTimer();
246
+
247
+ if ( this.duration > 0 ) {
248
+ this.remainingTime = this.duration;
249
+ this.pauseTime = Date.now();
250
+ this.autoCloseTimer = window.setTimeout( () => {
251
+ this.close();
252
+ }, this.duration );
253
+
254
+ // Start progress bar animation
255
+ this.startProgressBarAnimation();
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Pauses the auto-close timer
261
+ */
262
+ private pauseAutoCloseTimer (): void {
263
+ if ( this.autoCloseTimer && this.duration > 0 ) {
264
+ clearTimeout( this.autoCloseTimer );
265
+ this.autoCloseTimer = undefined;
266
+ this.remainingTime -= Date.now() - this.pauseTime;
267
+
268
+ // Pause progress bar animation
269
+ this.pauseProgressBarAnimation();
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Resumes the auto-close timer
275
+ */
276
+ private resumeAutoCloseTimer (): void {
277
+ if ( !this.autoCloseTimer && this.remainingTime > 0 ) {
278
+ this.pauseTime = Date.now();
279
+ this.autoCloseTimer = window.setTimeout( () => {
280
+ this.close();
281
+ }, this.remainingTime );
282
+
283
+ // Resume progress bar animation
284
+ this.resumeProgressBarAnimation();
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Clears the auto-close timer
290
+ */
291
+ private clearAutoCloseTimer (): void {
292
+ if ( this.autoCloseTimer ) {
293
+ clearTimeout( this.autoCloseTimer );
294
+ this.autoCloseTimer = undefined;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Starts the progress bar animation
300
+ */
301
+ private startProgressBarAnimation (): void {
302
+ if ( !this.progressBar || this.duration <= 0 ) return;
303
+
304
+ // Reset and start the animation
305
+ this.progressBar.style.animation = 'none';
306
+ // Force a reflow to reset the animation
307
+ void this.progressBar.offsetWidth;
308
+ this.progressBar.style.animation = `shrinkProgress ${ this.duration }ms linear forwards`;
309
+ }
310
+
311
+ /**
312
+ * Pauses the progress bar animation
313
+ */
314
+ private pauseProgressBarAnimation (): void {
315
+ if ( !this.progressBar ) return;
316
+
317
+ // Get the current computed width as a percentage of the container
318
+ const computedStyle = window.getComputedStyle( this.progressBar );
319
+ const currentWidth = computedStyle.width;
320
+ const containerWidth = this.progressBar.parentElement?.offsetWidth || 1;
321
+ const widthPercent = ( parseFloat( currentWidth ) / containerWidth ) * 100;
322
+
323
+ // Stop the animation and set the width directly
324
+ this.progressBar.style.animation = 'none';
325
+ this.progressBar.style.width = `${ widthPercent }%`;
326
+ }
327
+
328
+ /**
329
+ * Resumes the progress bar animation
330
+ */
331
+ private resumeProgressBarAnimation (): void {
332
+ if ( !this.progressBar || this.remainingTime <= 0 ) return;
333
+
334
+ // Get current width as starting point
335
+ const computedStyle = window.getComputedStyle( this.progressBar );
336
+ const currentWidth = computedStyle.width;
337
+ const containerWidth = this.progressBar.parentElement?.offsetWidth || 1;
338
+ const currentPercent = ( parseFloat( currentWidth ) / containerWidth ) * 100;
339
+
340
+ // Calculate the duration based on the remaining percentage and remaining time
341
+ // The animation should take exactly remainingTime to go from currentPercent to 0
342
+ const adjustedDuration = this.remainingTime;
343
+
344
+ // Create a new keyframe animation from current position to 0
345
+ const animationName = `shrinkProgress-${ Date.now() }`;
346
+ const styleSheet = this.shadowRoot.styleSheets[ 0 ];
347
+ const keyframes = `
348
+ @keyframes ${ animationName } {
349
+ from {
350
+ width: ${ currentPercent }%;
351
+ }
352
+ to {
353
+ width: 0%;
354
+ }
355
+ }
356
+ `;
357
+
358
+ // Add the new keyframe rule
359
+ if ( styleSheet ) {
360
+ styleSheet.insertRule( keyframes, styleSheet.cssRules.length );
361
+ }
362
+
363
+ // Apply the animation
364
+ this.progressBar.style.animation = `${ animationName } ${ adjustedDuration }ms linear forwards`;
365
+ }
366
+
367
+ /**
368
+ * Gets the color scheme for the toast type
369
+ */
370
+ private getTypeColors (): { background: string; border: string; icon: string } {
371
+ const type = this.type;
372
+
373
+ switch ( type ) {
374
+ case 'success':
375
+ return {
376
+ background: 'var(--toast-success-background, #d4edda)',
377
+ border: 'var(--toast-success-border, #c3e6cb)',
378
+ icon: 'var(--toast-success-icon, #155724)'
379
+ };
380
+ case 'error':
381
+ return {
382
+ background: 'var(--toast-error-background, #f8d7da)',
383
+ border: 'var(--toast-error-border, #f5c6cb)',
384
+ icon: 'var(--toast-error-icon, #721c24)'
385
+ };
386
+ case 'warning':
387
+ return {
388
+ background: 'var(--toast-warning-background, #fff3cd)',
389
+ border: 'var(--toast-warning-border, #ffeaa7)',
390
+ icon: 'var(--toast-warning-icon, #856404)'
391
+ };
392
+ case 'info':
393
+ default:
394
+ return {
395
+ background: 'var(--toast-info-background, #d1ecf1)',
396
+ border: 'var(--toast-info-border, #bee5eb)',
397
+ icon: 'var(--toast-info-icon, #0c5460)'
398
+ };
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Gets the default icon for the toast type
404
+ */
405
+ private getDefaultIcon (): string {
406
+ const type = this.type;
407
+
408
+ switch ( type ) {
409
+ case 'success':
410
+ return '✓';
411
+ case 'error':
412
+ return '✕';
413
+ case 'warning':
414
+ return '⚠';
415
+ case 'info':
416
+ default:
417
+ return 'ℹ';
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Binds all event listeners
423
+ */
424
+ private bindEvents (): void {
425
+ // Handle close button click and button clicks
426
+ this.shadowRoot.addEventListener( 'click', ( e ) => {
427
+ const target = e.target as HTMLElement;
428
+
429
+ if ( target.closest( '.close-button' ) ) {
430
+ this.close();
431
+ } else if ( target.closest( '.toast-button' ) ) {
432
+ const buttonIndex = ( target.closest( '.toast-button' ) as HTMLElement ).dataset.index;
433
+ if ( buttonIndex !== undefined ) {
434
+ const button = this.buttons[ parseInt( buttonIndex, 10 ) ];
435
+ if ( button && button.onClick ) {
436
+ button.onClick();
437
+ }
438
+ }
439
+ }
440
+ } );
441
+
442
+ // Handle mouse enter/leave to pause/resume timer
443
+ const container = this.shadowRoot.querySelector( '.toast-container' );
444
+ if ( container ) {
445
+ container.addEventListener( 'mouseenter', () => {
446
+ this.pauseAutoCloseTimer();
447
+ } );
448
+
449
+ container.addEventListener( 'mouseleave', () => {
450
+ this.resumeAutoCloseTimer();
451
+ } );
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Renders the component
457
+ */
458
+ private render (): void {
459
+ const colors = this.getTypeColors();
460
+ const iconContent = this.icon
461
+ ? `<img src="${ this.icon }" alt="Toast icon" class="toast-icon-img" />`
462
+ : `<span class="toast-icon-default">${ this.getDefaultIcon() }</span>`;
463
+
464
+ this.shadowRoot.innerHTML = `
465
+ <style>
466
+ :host {
467
+ display: block;
468
+ font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
469
+ font-size: var(--font-size, 14px);
470
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
471
+ }
472
+
473
+ .toast-container {
474
+ display: flex;
475
+ flex-direction: column;
476
+ min-width: 300px;
477
+ max-width: 500px;
478
+ padding: 16px;
479
+ background: ${ colors.background };
480
+ border: 1px solid ${ colors.border };
481
+ border-radius: var(--toast-border-radius, 8px);
482
+ box-shadow: var(--toast-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));
483
+ animation: slideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
484
+ position: relative;
485
+ will-change: transform, opacity;
486
+ }
487
+
488
+ .toast-container.closing {
489
+ animation: slideOut 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
490
+ }
491
+
492
+ @keyframes slideIn {
493
+ from {
494
+ opacity: 0;
495
+ transform: translateY(-20px) scale(0.95);
496
+ }
497
+ to {
498
+ opacity: 1;
499
+ transform: translateY(0) scale(1);
500
+ }
501
+ }
502
+
503
+ @keyframes slideOut {
504
+ from {
505
+ opacity: 1;
506
+ transform: translateY(0) scale(1);
507
+ }
508
+ to {
509
+ opacity: 0;
510
+ transform: translateY(-20px) scale(0.95);
511
+ }
512
+ }
513
+
514
+ .toast-header {
515
+ display: flex;
516
+ align-items: flex-start;
517
+ gap: 12px;
518
+ margin-bottom: 8px;
519
+ }
520
+
521
+ .toast-icon {
522
+ flex-shrink: 0;
523
+ width: 24px;
524
+ height: 24px;
525
+ display: flex;
526
+ align-items: center;
527
+ justify-content: center;
528
+ color: ${ colors.icon };
529
+ }
530
+
531
+ .toast-icon-img {
532
+ width: 100%;
533
+ height: 100%;
534
+ object-fit: contain;
535
+ }
536
+
537
+ .toast-icon-default {
538
+ font-size: 20px;
539
+ font-weight: bold;
540
+ }
541
+
542
+ .toast-content {
543
+ flex: 1;
544
+ min-width: 0;
545
+ }
546
+
547
+ .toast-title {
548
+ font-weight: 600;
549
+ font-size: 16px;
550
+ margin: 0 0 4px 0;
551
+ color: var(--toast-title-color, #333);
552
+ }
553
+
554
+ .toast-text {
555
+ margin: 0;
556
+ color: var(--toast-text-color, #555);
557
+ line-height: 1.5;
558
+ word-wrap: break-word;
559
+ }
560
+
561
+ .close-button {
562
+ position: absolute;
563
+ top: 8px;
564
+ right: 8px;
565
+ width: 24px;
566
+ height: 24px;
567
+ display: flex;
568
+ align-items: center;
569
+ justify-content: center;
570
+ background: transparent;
571
+ border: none;
572
+ cursor: pointer;
573
+ font-size: 20px;
574
+ color: var(--toast-close-color, #666);
575
+ border-radius: 4px;
576
+ transition: background-color 0.2s, color 0.2s;
577
+ padding: 0;
578
+ }
579
+
580
+ .close-button:hover {
581
+ background-color: var(--toast-close-hover-background, rgba(0, 0, 0, 0.1));
582
+ color: var(--toast-close-hover-color, #333);
583
+ }
584
+
585
+ .toast-buttons {
586
+ display: flex;
587
+ gap: 8px;
588
+ justify-content: flex-end;
589
+ margin-top: 12px;
590
+ padding-top: 12px;
591
+ border-top: 1px solid var(--toast-button-border, rgba(0, 0, 0, 0.1));
592
+ }
593
+
594
+ .toast-button {
595
+ padding: 6px 16px;
596
+ border: 1px solid var(--toast-button-border-color, #ccc);
597
+ border-radius: var(--toast-button-border-radius, 4px);
598
+ background: var(--toast-button-background, white);
599
+ color: var(--toast-button-color, #333);
600
+ font-size: 14px;
601
+ cursor: pointer;
602
+ transition: background-color 0.2s, border-color 0.2s;
603
+ font-family: inherit;
604
+ }
605
+
606
+ .toast-button:hover {
607
+ background-color: var(--toast-button-hover-background, #f8f9fa);
608
+ border-color: var(--toast-button-hover-border-color, #999);
609
+ }
610
+
611
+ .toast-button:active {
612
+ background-color: var(--toast-button-active-background, #e9ecef);
613
+ }
614
+
615
+ .toast-progress-bar {
616
+ position: absolute;
617
+ bottom: 0;
618
+ left: 0;
619
+ height: 4px;
620
+ width: 100%;
621
+ background: var(--toast-progress-bar-color, rgba(0, 0, 0, 0.3));
622
+ border-bottom-left-radius: var(--toast-border-radius, 8px);
623
+ border-bottom-right-radius: var(--toast-border-radius, 8px);
624
+ transform-origin: left;
625
+ }
626
+
627
+ @keyframes shrinkProgress {
628
+ from {
629
+ width: 100%;
630
+ }
631
+ to {
632
+ width: 0%;
633
+ }
634
+ }
635
+ </style>
636
+
637
+ <div class="toast-container">
638
+ ${ this.closable ? '<button class="close-button" aria-label="Close">×</button>' : '' }
639
+
640
+ <div class="toast-header">
641
+ <div class="toast-icon">
642
+ ${ iconContent }
643
+ </div>
644
+ <div class="toast-content">
645
+ <h4 class="toast-title">${ this.title }</h4>
646
+ <p class="toast-text">${ this.text }</p>
647
+ </div>
648
+ </div>
649
+
650
+ ${ this.buttons.length > 0 ? `
651
+ <div class="toast-buttons">
652
+ ${ this.buttons.map( ( button, index ) => `
653
+ <button class="toast-button" data-index="${ index }">
654
+ ${ button.label }
655
+ </button>
656
+ `).join( '' ) }
657
+ </div>
658
+ ` : '' }
659
+
660
+ ${ this.duration > 0 ? '<div class="toast-progress-bar"></div>' : '' }
661
+ </div>
662
+ `;
663
+
664
+ // Store reference to progress bar
665
+ this.progressBar = this.shadowRoot.querySelector( '.toast-progress-bar' ) as HTMLElement;
666
+
667
+ this.bindEvents();
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Conditionally defines the custom element if in a browser environment.
673
+ */
674
+ const defineToast = ( tagName: string = 'liwe3-toast' ): void => {
675
+ if ( typeof window !== 'undefined' && !window.customElements.get( tagName ) ) {
676
+ customElements.define( tagName, ToastElement );
677
+ }
678
+ };
679
+
680
+ // Auto-register with default tag name
681
+ defineToast();
682
+
683
+ /**
684
+ * Default container ID for toast notifications
685
+ */
686
+ const DEFAULT_CONTAINER_ID = 'liwe3-toast-container';
687
+
688
+ /**
689
+ * Creates or gets the toast container element
690
+ */
691
+ const getToastContainer = (): HTMLElement => {
692
+ let container = document.getElementById( DEFAULT_CONTAINER_ID );
693
+
694
+ if ( !container ) {
695
+ container = document.createElement( 'div' );
696
+ container.id = DEFAULT_CONTAINER_ID;
697
+ container.style.position = 'fixed';
698
+ container.style.top = '20px';
699
+ container.style.right = '20px';
700
+ container.style.zIndex = '99999';
701
+ container.style.display = 'flex';
702
+ container.style.flexDirection = 'column';
703
+ container.style.maxWidth = '400px';
704
+ container.style.pointerEvents = 'none';
705
+
706
+ // Add media query styles for mobile and smooth transitions
707
+ const style = document.createElement( 'style' );
708
+ style.textContent = `
709
+ #${DEFAULT_CONTAINER_ID} > * {
710
+ margin-bottom: 12px;
711
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
712
+ overflow: hidden;
713
+ }
714
+
715
+ #${DEFAULT_CONTAINER_ID} > *:last-child {
716
+ margin-bottom: 0;
717
+ }
718
+
719
+ @media (max-width: 768px) {
720
+ #${DEFAULT_CONTAINER_ID} {
721
+ left: 20px !important;
722
+ right: 20px !important;
723
+ max-width: none !important;
724
+ }
725
+ }
726
+ `;
727
+ document.head.appendChild( style );
728
+
729
+ document.body.appendChild( container );
730
+ }
731
+
732
+ return container;
733
+ };
734
+
735
+ /**
736
+ * Shows a toast notification with the given configuration.
737
+ * This is the recommended way to display toasts.
738
+ *
739
+ * @param config - The toast configuration
740
+ * @returns The toast element instance
741
+ *
742
+ * @example
743
+ * ```typescript
744
+ * import { toastAdd } from '@liwe3/webcomponents';
745
+ *
746
+ * toastAdd({
747
+ * title: 'Success!',
748
+ * text: 'Your changes have been saved.',
749
+ * type: 'success',
750
+ * duration: 5000
751
+ * });
752
+ * ```
753
+ */
754
+ const toastAdd = ( config: ToastConfig ): ToastElement => {
755
+ const container = getToastContainer();
756
+ const toast = document.createElement( 'liwe3-toast' ) as ToastElement;
757
+
758
+ // Allow pointer events on individual toasts
759
+ toast.style.pointerEvents = 'auto';
760
+
761
+ // Show the toast with the provided config
762
+ toast.show( config );
763
+
764
+ // Add to container
765
+ container.appendChild( toast );
766
+
767
+ return toast;
768
+ };
769
+
770
+ export { defineToast, toastAdd };