UNPKG

ng-angular-popup

Version:

A modern, lightweight, and customizable toast notification library for Angular 18-22 with signals, zoneless support, and standalone components

731 lines (712 loc) 60.5 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, signal, Injectable, input, ElementRef, Directive, computed, effect, ChangeDetectionStrategy, Component, NgModule, makeEnvironmentProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; import * as i2 from '@angular/forms'; import { FormsModule } from '@angular/forms'; /** * Represents a toast notification message with various configuration options. */ class ToastMessage { /** * Creates a new toast message instance. * * @param message The content of the toast message * @param type The visual type/style of the toast * @param title Optional title for the toast message * @param duration Time in milliseconds before auto-dismissal (0 for no auto-dismiss) * @param showProgress Whether to show the progress countdown bar * @param dismissible Whether the toast can be manually dismissed * @param showIcon Whether to show the icon */ constructor(message, type, title, duration = 3000, showProgress = true, dismissible = true, showIcon = true) { this.message = message; this.type = type; this.title = title; this.duration = duration; this.showProgress = showProgress; this.dismissible = dismissible; this.showIcon = showIcon; this.id = Date.now() + Math.random(); this.createdAt = Date.now(); // If duration is 0, disable progress bar as it won't auto-dismiss if (duration === 0) { this.showProgress = false; } } } var ToastType; (function (ToastType) { ToastType["PRIMARY"] = "toast-primary"; ToastType["SECONDARY"] = "toast-secondary"; ToastType["SUCCESS"] = "toast-success"; ToastType["INFO"] = "toast-info"; ToastType["WARNING"] = "toast-warning"; ToastType["DANGER"] = "toast-danger"; })(ToastType || (ToastType = {})); /** * Default configuration for ng-toast */ const DEFAULT_TOAST_CONFIG = { duration: 3000, position: 'bottom-right', maxToasts: 5, width: 350, showProgress: true, dismissible: true, showIcon: true, enableAnimations: true, }; /** * Injection token for ng-toast configuration */ const NG_TOAST_CONFIG = new InjectionToken('NG_TOAST_CONFIG', { providedIn: 'root', factory: () => DEFAULT_TOAST_CONFIG, }); /** * Service for displaying toast messages. */ class NgToastService { /** Default duration for toast messages in milliseconds */ #defaultDuration; /** Maximum number of toasts to show at once */ #maxToasts; /** * Constructs a new NgToastService instance. */ constructor() { /** Injected configuration */ this.config = inject(NG_TOAST_CONFIG); /** * Signal that holds the current toast messages */ this.toastMessages = signal([], ...(ngDevMode ? [{ debugName: "toastMessages" }] : [])); this.#defaultDuration = this.config.duration ?? 3000; this.#maxToasts = this.config.maxToasts ?? 5; } /** * Get the global configuration */ getConfig() { return this.config; } /** * Displays a toast message. * @param message The message to display. * @param type The type of the toast message. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ toast(message, type, title, duration, showProgress, dismissible, showIcon) { // Use config defaults if not provided const finalDuration = duration ?? this.#defaultDuration; const finalShowProgress = showProgress ?? this.config.showProgress ?? true; const finalDismissible = dismissible ?? this.config.dismissible ?? true; const finalShowIcon = showIcon ?? this.config.showIcon ?? true; // Create new toast message const newToast = new ToastMessage(message, type, title, finalDuration, finalShowProgress, finalDismissible, finalShowIcon); // Add to messages, limiting to max number of toasts this.toastMessages.update(messages => { const updatedMessages = [...messages, newToast]; // If we have more than max toasts, remove the oldest ones return updatedMessages.length > this.#maxToasts ? updatedMessages.slice(updatedMessages.length - this.#maxToasts) : updatedMessages; }); // Auto-remove the toast after the duration (if duration > 0) if (finalDuration > 0) { setTimeout(() => { this.removeWithAnimation(newToast.id); }, finalDuration); } } /** * Displays a success toast message. * @param message The message to display. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ success(message, title, duration, showProgress, dismissible, showIcon) { this.toast(message, ToastType.SUCCESS, title, duration, showProgress, dismissible, showIcon); } /** * Displays an info toast message. * @param message The message to display. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ info(message, title, duration, showProgress, dismissible, showIcon) { this.toast(message, ToastType.INFO, title, duration, showProgress, dismissible, showIcon); } /** * Displays a warning toast message. * @param message The message to display. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ warning(message, title, duration, showProgress, dismissible, showIcon) { this.toast(message, ToastType.WARNING, title, duration, showProgress, dismissible, showIcon); } /** * Displays a danger/error toast message. * @param message The message to display. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ danger(message, title, duration, showProgress, dismissible, showIcon) { this.toast(message, ToastType.DANGER, title, duration, showProgress, dismissible, showIcon); } /** * Displays a primary toast message. * @param message The message to display. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ primary(message, title, duration, showProgress, dismissible, showIcon) { this.toast(message, ToastType.PRIMARY, title, duration, showProgress, dismissible, showIcon); } /** * Displays a secondary toast message. * @param message The message to display. * @param title The optional title of the toast message. * @param duration The duration in milliseconds for which the toast message should be displayed. Defaults to the default duration. * @param showProgress Whether to show the progress bar. Defaults to true. * @param dismissible Whether the toast can be manually dismissed. Defaults to true. */ secondary(message, title, duration, showProgress, dismissible, showIcon) { this.toast(message, ToastType.SECONDARY, title, duration, showProgress, dismissible, showIcon); } /** * Set callback for before remove (used by component for animations) */ setBeforeRemoveCallback(callback) { this.beforeRemoveCallback = callback; } /** * Removes a toast message from the list * @param messageId The ID of the message to remove */ removeToast(messageId) { this.toastMessages.update(messages => messages.filter(message => message.id !== messageId)); } /** * Removes a toast with animation */ removeWithAnimation(messageId) { if (this.beforeRemoveCallback) { this.beforeRemoveCallback(messageId); } else { this.removeToast(messageId); } } /** * Removes all toast messages */ clearAll() { this.toastMessages.set([]); } /** * Updates the progress bars by triggering a signal update * This is used by the component to refresh progress bars */ updateProgress() { // Force a signal update by creating a new array with the same messages this.toastMessages.update(messages => [...messages]); } /** * Sets the maximum number of toasts to display at once * @param max The maximum number of toasts */ setMaxToasts(max) { if (max > 0) { this.#maxToasts = max; // If we already have more than max toasts, remove the oldest ones const currentToasts = this.toastMessages(); if (currentToasts.length > max) { this.toastMessages.set(currentToasts.slice(currentToasts.length - max)); } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Toast position values as string literals */ const TOAST_POSITIONS = { TOP_LEFT: 'toaster-top-left', TOP_CENTER: 'toaster-top-center', TOP_RIGHT: 'toaster-top-right', BOTTOM_LEFT: 'toaster-bottom-left', BOTTOM_CENTER: 'toaster-bottom-center', BOTTOM_RIGHT: 'toaster-bottom-right' }; /** * @deprecated Use TOAST_POSITIONS and ToastPosition instead */ const ToasterPosition = TOAST_POSITIONS; /** * Directive that renders appropriate SVG icons for different toast types */ class ToastIconDirective { constructor() { /** Input signal for the toast type */ this.type = input.required(...(ngDevMode ? [{ debugName: "type", alias: 'toastIcon' }] : [{ alias: 'toastIcon' }])); /** Element reference for DOM manipulation */ this.#el = inject(ElementRef); } /** Element reference for DOM manipulation */ #el; ngOnInit() { this.setIcon(); } /** * Sets the appropriate SVG icon based on toast type */ setIcon() { let svgContent; switch (this.type()) { case 'toast-success': svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> <polyline points="22 4 12 14.01 9 11.01"></polyline> </svg>`; break; case 'toast-danger': svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="15" y1="9" x2="9" y2="15"></line> <line x1="9" y1="9" x2="15" y2="15"></line> </svg>`; break; case 'toast-info': svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="16" x2="12" y2="12"></line> <line x1="12" y1="8" x2="12.01" y2="8"></line> </svg>`; break; case 'toast-warning': svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path> <line x1="12" y1="9" x2="12" y2="13"></line> <line x1="12" y1="17" x2="12.01" y2="17"></line> </svg>`; break; case 'toast-primary': svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path> <path d="M13.73 21a2 2 0 0 1-3.46 0"></path> </svg>`; break; case 'toast-secondary': svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <path d="M8 14s1.5 2 4 2 4-2 4-2"></path> <line x1="9" y1="9" x2="9.01" y2="9"></line> <line x1="15" y1="9" x2="15.01" y2="9"></line> </svg>`; break; default: svgContent = ` <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12.01" y2="16"></line> </svg>`; } this.#el.nativeElement.innerHTML = svgContent; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: ToastIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.3", type: ToastIconDirective, isStandalone: true, selector: "[toastIcon]", inputs: { type: { classPropertyName: "type", publicName: "toastIcon", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: ToastIconDirective, decorators: [{ type: Directive, args: [{ selector: '[toastIcon]', standalone: true }] }] }); class NgToastComponent { /** * Check if a toast is leaving */ isLeaving(messageId) { return this.leavingToasts().has(messageId); } constructor(toastService) { this.toastService = toastService; this.config = inject(NG_TOAST_CONFIG); /** * Input signal for the position of the toast container */ this.position = input(this.config.position ?? TOAST_POSITIONS.BOTTOM_RIGHT, ...(ngDevMode ? [{ debugName: "position" }] : [])); /** * Input signal for the width of the toast container in pixels */ this.width = input(this.config.width ?? 350, ...(ngDevMode ? [{ debugName: "width" }] : [])); /** * Input signal for the minimum width of the toast container in pixels * If not provided, min-width will not be applied */ this.minWidth = input(this.config.minWidth, ...(ngDevMode ? [{ debugName: "minWidth" }] : [])); /** * Signal to track progress update intervals */ this.progressInterval = signal(null, ...(ngDevMode ? [{ debugName: "progressInterval" }] : [])); /** * Signal to track toasts that are leaving (for exit animation) */ this.leavingToasts = signal(new Set(), ...(ngDevMode ? [{ debugName: "leavingToasts" }] : [])); /** * Computed signal that gets the messages from the service */ this.messages = computed(() => this.toastService.toastMessages(), ...(ngDevMode ? [{ debugName: "messages" }] : [])); // Register animation callback with service this.toastService.setBeforeRemoveCallback((messageId) => { this.triggerLeaveAnimation(messageId); }); // Create an effect to handle message positioning effect(() => { // This effect will run whenever the position signal changes // We don't need to do anything here as the position is handled in the template this.position(); }); // Create an effect to handle progress bar updates effect(() => { const messages = this.messages(); // Clear existing interval if no messages with progress bars if (messages.length === 0 || !messages.some(m => m.showProgress)) { this.clearProgressInterval(); return; } // Start progress interval if not already running if (this.progressInterval() === null) { const intervalId = window.setInterval(() => { // Force update to trigger change detection for progress bars this.toastService.updateProgress(); }, 100); this.progressInterval.set(intervalId); } }); } ngOnInit() { // Initialize any required state } /** * Calculates the progress width percentage for a toast message * @param message The toast message * @returns The progress width as a percentage (0-100) */ getProgressWidth(message) { if (message.duration <= 0) return 0; const elapsed = Date.now() - message.createdAt; const remaining = Math.max(0, message.duration - elapsed); return (remaining / message.duration) * 100; } /** * Gets the appropriate color for the progress bar based on toast type * @param message The toast message * @returns CSS color value for the progress bar */ getProgressColor(message) { const colorMap = { 'toast-primary': '#4f46e5', 'toast-secondary': '#475569', 'toast-success': '#10b981', 'toast-info': '#06b6d4', 'toast-warning': '#f59e0b', 'toast-danger': '#ef4444' }; return colorMap[message.type] || 'rgba(0, 0, 0, 0.2)'; } /** * Clears the progress update interval */ clearProgressInterval() { const intervalId = this.progressInterval(); if (intervalId !== null) { window.clearInterval(intervalId); this.progressInterval.set(null); } } /** * Trigger leave animation for a toast */ triggerLeaveAnimation(messageId) { // Mark as leaving to trigger exit animation this.leavingToasts.update(set => new Set(set).add(messageId)); // Wait for animation to complete before removing setTimeout(() => { this.toastService.removeToast(messageId); this.leavingToasts.update(set => { const newSet = new Set(set); newSet.delete(messageId); return newSet; }); }, 400); // Match toastLeave animation duration } /** * Removes a toast message with animation * @param message The message to remove */ remove(message) { if (message.dismissible) { this.triggerLeaveAnimation(message.id); } } ngOnDestroy() { // Clean up any resources this.clearProgressInterval(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastComponent, deps: [{ token: NgToastService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: NgToastComponent, isStandalone: true, selector: "ng-toast", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [style.max-width.px]=\"width()\" [style.min-width.px]=\"minWidth()\" class=\"toaster\" [class]=\"position()\">\n\n @for (message of messages(); track message.id) {\n <div class=\"toast-message\"\n [class.toast-enter]=\"!isLeaving(message.id)\"\n [class.toast-leave]=\"isLeaving(message.id)\"\n [class]=\"message.type\">\n <div class=\"flex-start-center gap-3\">\n @if (message.showIcon) {\n <div class=\"toast-icon-wrapper\">\n <span toastIcon=\"{{message.type}}\" class=\"toast-icon\"></span>\n </div>\n }\n <div class=\"flex-col\">\n @if (message.title && message.title !== '') {\n <span class=\"msg-title\">{{message.title}}</span>\n }\n <span class=\"msg-summary\">{{message.message}}</span>\n </div>\n </div>\n\n @if(message.dismissible) {\n <button (click)=\"remove(message)\" class=\"cross-icon\" aria-label=\"Close toast\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z\" fill=\"currentColor\"></path>\n </svg>\n </button>\n }\n\n <!-- Progress bar for auto-dismiss countdown -->\n @if (message.showProgress) {\n <div class=\"toast-progress\" [style]=\"{'width': getProgressWidth(message) + '%', 'background-color': getProgressColor(message)}\"></div>\n }\n </div>\n }\n</div>\n", styles: ["@keyframes toastEnter{0%{opacity:0;transform:scale(.3) translateY(-20px)}50%{opacity:1;transform:scale(1.05) translateY(5px)}70%{transform:scale(.95) translateY(-2px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes toastLeave{0%{opacity:1;transform:scale(1) translateY(0)}30%{transform:scale(1.05) translateY(-5px)}to{opacity:0;transform:scale(.7) translateY(10px)}}.toaster{position:fixed;z-index:9999;min-width:280px;max-width:360px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.toaster .toast-message{padding:.75rem 1rem;margin-bottom:.5rem;border-radius:8px;background:#fff;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;display:flex;justify-content:space-between;align-items:flex-start;word-break:break-word;transition:all .3s ease;border:1px solid rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);position:relative;overflow:hidden}.toaster .toast-message.toast-enter{animation:toastEnter .5s cubic-bezier(.68,-.55,.265,1.55) forwards}.toaster .toast-message.toast-leave{animation:toastLeave .4s cubic-bezier(.68,-.55,.265,1.55) forwards}.toaster .toast-message:hover{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d;transform:translateY(-2px)}.toaster .toast-message.toast-primary{border-left:6px solid #4338ca;background-color:#eef2ff;box-shadow:0 4px 12px #4338ca26}.toaster .toast-message.toast-primary .toast-icon{color:#4338ca}.toaster .toast-message.toast-primary .toast-title{color:#4338ca;font-weight:600}.toaster .toast-message.toast-primary .toast-content{color:#111827}.toaster .toast-message.toast-secondary{border-left:6px solid #374151;background-color:#f1f5f9;box-shadow:0 4px 12px #37415126}.toaster .toast-message.toast-secondary .toast-icon{color:#374151}.toaster .toast-message.toast-secondary .toast-title{color:#374151;font-weight:600}.toaster .toast-message.toast-secondary .toast-content{color:#111827}.toaster .toast-message.toast-success{border-left:6px solid #047857;background-color:#ecfdf5;box-shadow:0 4px 12px #04785726}.toaster .toast-message.toast-success .toast-icon{color:#047857}.toaster .toast-message.toast-success .toast-title{color:#047857;font-weight:600}.toaster .toast-message.toast-success .toast-content{color:#111827}.toaster .toast-message.toast-info{border-left:6px solid #0369a1;background-color:#ecfeff;box-shadow:0 4px 12px #0369a126}.toaster .toast-message.toast-info .toast-icon{color:#0369a1}.toaster .toast-message.toast-info .toast-title{color:#0369a1;font-weight:600}.toaster .toast-message.toast-info .toast-content{color:#111827}.toaster .toast-message.toast-warning{border-left:6px solid #b45309;background-color:#fffbeb;box-shadow:0 4px 12px #b4530926}.toaster .toast-message.toast-warning .toast-icon{color:#b45309}.toaster .toast-message.toast-warning .toast-title{color:#b45309;font-weight:600}.toaster .toast-message.toast-warning .toast-content{color:#111827}.toaster .toast-message.toast-danger{border-left:6px solid #b91c1c;background-color:#fef2f2;box-shadow:0 4px 12px #b91c1c26}.toaster .toast-message.toast-danger .toast-icon{color:#b91c1c}.toaster .toast-message.toast-danger .toast-title{color:#b91c1c;font-weight:600}.toaster .toast-message.toast-danger .toast-content{color:#111827}.toaster .toast-message .msg-title{font-size:.875rem;color:#111827;font-weight:600;margin-bottom:.25rem;line-height:1.3;letter-spacing:-.01em}.toaster .toast-message .msg-summary{font-size:.8125rem;color:#111827;font-weight:400;line-height:1.4;letter-spacing:0}.toaster .toast-message .cross-icon{background:transparent;border:none;outline:none;color:#111827;cursor:pointer;padding:4px;margin:-4px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all .3s ease;opacity:.7;flex-shrink:0}.toaster .toast-message .cross-icon svg{width:16px;height:16px}.toaster .toast-message .cross-icon:hover{opacity:1;background-color:#0000001a}.toaster.toaster-top-left{margin:1rem;top:0;left:0}.toaster.toaster-top-center{margin-top:1rem;top:0;left:50%;transform:translate(-50%)}.toaster.toaster-top-right{margin:1rem;top:0;right:0}.toaster.toaster-bottom-left{margin:1rem;bottom:0;left:0}.toaster.toaster-bottom-center{margin-bottom:1rem;bottom:0;left:50%;transform:translate(-50%)}.toaster.toaster-bottom-right{margin:1rem;bottom:0;right:0}@media (max-width: 480px){.toaster{min-width:calc(100vw - 2rem);max-width:calc(100vw - 2rem);margin:.5rem}.toaster .toast-message{padding:.625rem .75rem;margin-bottom:.5rem}}.flex-start-center{display:flex;align-items:center;justify-content:flex-start}.flex-col{display:flex;flex-direction:column}.gap-3{gap:.625rem}.toast-icon-wrapper{display:flex;align-items:center;justify-content:center;flex-shrink:0}.toast-icon-wrapper .toast-icon{width:20px;height:20px;font-size:.875rem}.toast-progress{position:absolute;bottom:0;left:0;height:4px;background-color:currentColor;opacity:.3;border-radius:0 0 0 8px;transition:width linear}.toast-primary .toast-progress{background-color:#4338ca}.toast-secondary .toast-progress{background-color:#374151}.toast-success .toast-progress{background-color:#047857}.toast-info .toast-progress{background-color:#0369a1}.toast-warning .toast-progress{background-color:#b45309}.toast-danger .toast-progress{background-color:#b91c1c}@media (max-width: 576px){.toaster{min-width:calc(100% - 1.5rem);max-width:calc(100% - 1.5rem)}.toaster.toaster-top-left,.toaster.toaster-top-right,.toaster.toaster-bottom-left,.toaster.toaster-bottom-right{left:0;right:0;margin-left:.75rem;margin-right:.75rem;transform:none}.toaster.toaster-top-center,.toaster.toaster-bottom-center{width:calc(100% - 1.5rem)}.toaster .toast-message{padding:.625rem .75rem}}@media (min-width: 577px) and (max-width: 768px){.toaster{min-width:320px;max-width:380px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: ToastIconDirective, selector: "[toastIcon]", inputs: ["toastIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastComponent, decorators: [{ type: Component, args: [{ selector: 'ng-toast', standalone: true, imports: [CommonModule, ToastIconDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [style.max-width.px]=\"width()\" [style.min-width.px]=\"minWidth()\" class=\"toaster\" [class]=\"position()\">\n\n @for (message of messages(); track message.id) {\n <div class=\"toast-message\"\n [class.toast-enter]=\"!isLeaving(message.id)\"\n [class.toast-leave]=\"isLeaving(message.id)\"\n [class]=\"message.type\">\n <div class=\"flex-start-center gap-3\">\n @if (message.showIcon) {\n <div class=\"toast-icon-wrapper\">\n <span toastIcon=\"{{message.type}}\" class=\"toast-icon\"></span>\n </div>\n }\n <div class=\"flex-col\">\n @if (message.title && message.title !== '') {\n <span class=\"msg-title\">{{message.title}}</span>\n }\n <span class=\"msg-summary\">{{message.message}}</span>\n </div>\n </div>\n\n @if(message.dismissible) {\n <button (click)=\"remove(message)\" class=\"cross-icon\" aria-label=\"Close toast\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z\" fill=\"currentColor\"></path>\n </svg>\n </button>\n }\n\n <!-- Progress bar for auto-dismiss countdown -->\n @if (message.showProgress) {\n <div class=\"toast-progress\" [style]=\"{'width': getProgressWidth(message) + '%', 'background-color': getProgressColor(message)}\"></div>\n }\n </div>\n }\n</div>\n", styles: ["@keyframes toastEnter{0%{opacity:0;transform:scale(.3) translateY(-20px)}50%{opacity:1;transform:scale(1.05) translateY(5px)}70%{transform:scale(.95) translateY(-2px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes toastLeave{0%{opacity:1;transform:scale(1) translateY(0)}30%{transform:scale(1.05) translateY(-5px)}to{opacity:0;transform:scale(.7) translateY(10px)}}.toaster{position:fixed;z-index:9999;min-width:280px;max-width:360px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.toaster .toast-message{padding:.75rem 1rem;margin-bottom:.5rem;border-radius:8px;background:#fff;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;display:flex;justify-content:space-between;align-items:flex-start;word-break:break-word;transition:all .3s ease;border:1px solid rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);position:relative;overflow:hidden}.toaster .toast-message.toast-enter{animation:toastEnter .5s cubic-bezier(.68,-.55,.265,1.55) forwards}.toaster .toast-message.toast-leave{animation:toastLeave .4s cubic-bezier(.68,-.55,.265,1.55) forwards}.toaster .toast-message:hover{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d;transform:translateY(-2px)}.toaster .toast-message.toast-primary{border-left:6px solid #4338ca;background-color:#eef2ff;box-shadow:0 4px 12px #4338ca26}.toaster .toast-message.toast-primary .toast-icon{color:#4338ca}.toaster .toast-message.toast-primary .toast-title{color:#4338ca;font-weight:600}.toaster .toast-message.toast-primary .toast-content{color:#111827}.toaster .toast-message.toast-secondary{border-left:6px solid #374151;background-color:#f1f5f9;box-shadow:0 4px 12px #37415126}.toaster .toast-message.toast-secondary .toast-icon{color:#374151}.toaster .toast-message.toast-secondary .toast-title{color:#374151;font-weight:600}.toaster .toast-message.toast-secondary .toast-content{color:#111827}.toaster .toast-message.toast-success{border-left:6px solid #047857;background-color:#ecfdf5;box-shadow:0 4px 12px #04785726}.toaster .toast-message.toast-success .toast-icon{color:#047857}.toaster .toast-message.toast-success .toast-title{color:#047857;font-weight:600}.toaster .toast-message.toast-success .toast-content{color:#111827}.toaster .toast-message.toast-info{border-left:6px solid #0369a1;background-color:#ecfeff;box-shadow:0 4px 12px #0369a126}.toaster .toast-message.toast-info .toast-icon{color:#0369a1}.toaster .toast-message.toast-info .toast-title{color:#0369a1;font-weight:600}.toaster .toast-message.toast-info .toast-content{color:#111827}.toaster .toast-message.toast-warning{border-left:6px solid #b45309;background-color:#fffbeb;box-shadow:0 4px 12px #b4530926}.toaster .toast-message.toast-warning .toast-icon{color:#b45309}.toaster .toast-message.toast-warning .toast-title{color:#b45309;font-weight:600}.toaster .toast-message.toast-warning .toast-content{color:#111827}.toaster .toast-message.toast-danger{border-left:6px solid #b91c1c;background-color:#fef2f2;box-shadow:0 4px 12px #b91c1c26}.toaster .toast-message.toast-danger .toast-icon{color:#b91c1c}.toaster .toast-message.toast-danger .toast-title{color:#b91c1c;font-weight:600}.toaster .toast-message.toast-danger .toast-content{color:#111827}.toaster .toast-message .msg-title{font-size:.875rem;color:#111827;font-weight:600;margin-bottom:.25rem;line-height:1.3;letter-spacing:-.01em}.toaster .toast-message .msg-summary{font-size:.8125rem;color:#111827;font-weight:400;line-height:1.4;letter-spacing:0}.toaster .toast-message .cross-icon{background:transparent;border:none;outline:none;color:#111827;cursor:pointer;padding:4px;margin:-4px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all .3s ease;opacity:.7;flex-shrink:0}.toaster .toast-message .cross-icon svg{width:16px;height:16px}.toaster .toast-message .cross-icon:hover{opacity:1;background-color:#0000001a}.toaster.toaster-top-left{margin:1rem;top:0;left:0}.toaster.toaster-top-center{margin-top:1rem;top:0;left:50%;transform:translate(-50%)}.toaster.toaster-top-right{margin:1rem;top:0;right:0}.toaster.toaster-bottom-left{margin:1rem;bottom:0;left:0}.toaster.toaster-bottom-center{margin-bottom:1rem;bottom:0;left:50%;transform:translate(-50%)}.toaster.toaster-bottom-right{margin:1rem;bottom:0;right:0}@media (max-width: 480px){.toaster{min-width:calc(100vw - 2rem);max-width:calc(100vw - 2rem);margin:.5rem}.toaster .toast-message{padding:.625rem .75rem;margin-bottom:.5rem}}.flex-start-center{display:flex;align-items:center;justify-content:flex-start}.flex-col{display:flex;flex-direction:column}.gap-3{gap:.625rem}.toast-icon-wrapper{display:flex;align-items:center;justify-content:center;flex-shrink:0}.toast-icon-wrapper .toast-icon{width:20px;height:20px;font-size:.875rem}.toast-progress{position:absolute;bottom:0;left:0;height:4px;background-color:currentColor;opacity:.3;border-radius:0 0 0 8px;transition:width linear}.toast-primary .toast-progress{background-color:#4338ca}.toast-secondary .toast-progress{background-color:#374151}.toast-success .toast-progress{background-color:#047857}.toast-info .toast-progress{background-color:#0369a1}.toast-warning .toast-progress{background-color:#b45309}.toast-danger .toast-progress{background-color:#b91c1c}@media (max-width: 576px){.toaster{min-width:calc(100% - 1.5rem);max-width:calc(100% - 1.5rem)}.toaster.toaster-top-left,.toaster.toaster-top-right,.toaster.toaster-bottom-left,.toaster.toaster-bottom-right{left:0;right:0;margin-left:.75rem;margin-right:.75rem;transform:none}.toaster.toaster-top-center,.toaster.toaster-bottom-center{width:calc(100% - 1.5rem)}.toaster .toast-message{padding:.625rem .75rem}}@media (min-width: 577px) and (max-width: 768px){.toaster{min-width:320px;max-width:380px}}\n"] }] }], ctorParameters: () => [{ type: NgToastService }] }); /** * @deprecated Use standalone components and provideNgToast() instead * This module is kept for backward compatibility with Angular versions < 14 * * Example usage with standalone components: * ```typescript * // In your app.config.ts * export const appConfig: ApplicationConfig = { * providers: [ * provideNgToast(), * // other providers... * ] * }; * * // In your component * @Component({ * // ... * imports: [NgToastComponent], * // ... * }) * export class YourComponent {} * ``` */ class NgToastModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.3", ngImport: i0, type: NgToastModule, imports: [NgToastComponent, ToastIconDirective], exports: [NgToastComponent, ToastIconDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastModule, imports: [NgToastComponent] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastModule, decorators: [{ type: NgModule, args: [{ imports: [NgToastComponent, ToastIconDirective], exports: [NgToastComponent, ToastIconDirective], }] }] }); /** * Provides the NgToast service for standalone applications with optional configuration * * @param config Optional global configuration for toast notifications * * @example * ```typescript * // Basic usage * export const appConfig: ApplicationConfig = { * providers: [ * provideNgToast(), * ] * }; * * // With custom configuration * export const appConfig: ApplicationConfig = { * providers: [ * provideNgToast({ * duration: 5000, * position: 'top-right', * maxToasts: 3, * showIcon: true, * showProgress: true, * dismissible: true, * enableAnimations: true * }), * ] * }; * ``` */ function provideNgToast(config) { return makeEnvironmentProviders([ { provide: NG_TOAST_CONFIG, useValue: { ...DEFAULT_TOAST_CONFIG, ...config } }, NgToastService ]); } /** * Demo component showcasing the ng-toast library with Angular v20 and signals */ class NgToastDemoComponent { constructor(toastService) { this.toastService = toastService; // Make positions available in template this.TOAST_POSITIONS = TOAST_POSITIONS; this.currentPosition = TOAST_POSITIONS.TOP_RIGHT; // Toast options this.duration = 3000; this.showProgress = true; this.dismissible = true; this.customTitle = ''; this.customMessage = 'This is a custom toast message that showcases our revamped UI with progress bars.'; // Available toast types for custom message this.toastTypes = Object.values(ToastType); } showSuccess() { this.toastService.success('Operation completed successfully!', this.customTitle || 'Success', this.duration, this.showProgress, this.dismissible); } showInfo() { this.toastService.info('Here is some important information.', this.customTitle || 'Information', this.duration, this.showProgress, this.dismissible); } showWarning() { this.toastService.warning('Please proceed with caution.', this.customTitle || 'Warning', this.duration, this.showProgress, this.dismissible); } showDanger() { this.toastService.danger('An error has occurred!', this.customTitle || 'Error', this.duration, this.showProgress, this.dismissible); } showPrimary() { this.toastService.primary('This is a primary notification.', this.customTitle || 'Primary', this.duration, this.showProgress, this.dismissible); } showSecondary() { this.toastService.secondary('This is a secondary notification.', this.customTitle || 'Secondary', this.duration, this.showProgress, this.dismissible); } showCustomToast(type) { if (!this.customMessage) { this.customMessage = 'This is a custom toast message'; } this.toastService.toast(this.customMessage, type, this.customTitle || type, this.duration, this.showProgress, this.dismissible); } clearAllToasts() { this.toastService.clearAll(); } showMultipleToasts() { // Show multiple toasts to demonstrate max toasts limit const types = [ToastType.SUCCESS, ToastType.INFO, ToastType.WARNING, ToastType.DANGER, ToastType.PRIMARY, ToastType.SECONDARY]; types.forEach((type, index) => { setTimeout(() => { this.toastService.toast(`This is toast #${index + 1} of ${types.length}`, type, `Multiple Toast ${index + 1}`, this.duration, this.showProgress, this.dismissible); }, index * 300); // Stagger the toasts }); } setPosition(position) { this.currentPosition = position; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: NgToastDemoComponent, deps: [{ token: NgToastService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: NgToastDemoComponent, isStandalone: true, selector: "ng-toast-demo", ngImport: i0, template: ` <div class="demo-container"> <h1>NgToast Demo</h1> <p>A modern, lightweight toast notification library for Angular v20 with signals</p> <div class="demo-card"> <h2>Toast Types</h2> <div class="button-group"> <button class="success-btn" (click)="showSuccess()">Success</button> <button class="info-btn" (click)="showInfo()">Info</button> <button class="warning-btn" (click)="showWarning()">Warning</button> <button class="danger-btn" (click)="showDanger()">Danger</button> <button class="primary-btn" (click)="showPrimary()">Primary</button> <button class="secondary-btn" (click)="showSecondary()">Secondary</button> </div> </div> <div class="demo-card"> <h2>Toast Options</h2> <div class="options-grid"> <div class="option-group"> <label>Duration (ms):</label> <input type="number" [(ngModel)]="duration" min="0" max="10000" step="500"> <span class="hint">Set to 0 for persistent toast</span> </div> <div class="option-group"> <label>Show Progress Bar:</label> <div class="toggle-switch"> <input type="checkbox" id="progress-toggle" [(ngModel)]="showProgress"> <label for="progress-toggle"></label> </div> </div> <div class="option-group"> <label>Dismissible:</label> <div class="toggle-switch"> <input type="checkbox" id="dismiss-toggle" [(ngModel)]="dismissible"> <label for="dismiss-toggle"></label> </div> </div> <div class="option-group"> <label>Custom Title:</label> <input type="text" [(ngModel)]="customTitle" placeholder="Optional title"> </div> </div> </div> <div class="demo-card"> <h2>Toast Position</h2> <div class="position-grid"> <button [class.active]="currentPosition === TOAST_POSITIONS.TOP_LEFT" (click)="setPosition(TOAST_POSITIONS.TOP_LEFT)">Top Left</button> <button [class.active]="currentPosition === TOAST_POSITIONS.TOP_CENTER" (click)="setPosition(TOAST_POSITIONS.TOP_CENTER)">Top Center</button> <button [class.active]="currentPosition === TOAST_POSITIONS.TOP_RIGHT" (click)="setPosition(TOAST_POSITIONS.TOP_RIGHT)">Top Right</button> <button [class.active]="currentPosition === TOAST_POSITIONS.BOTTOM_LEFT" (click)="setPosition(TOAST_POSITIONS.BOTTOM_LEFT)">Bottom Left</button> <button [class.active]="currentPosition === TOAST_POSITIONS.BOTTOM_CENTER" (click)="setPosition(TOAST_POSITIONS.BOTTOM_CENTER)">Bottom Center</button> <button [class.active]="currentPosition === TOAST_POSITIONS.BOTTOM_RIGHT" (click)="setPosition(TOAST_POSITIONS.BOTTOM_RIGHT)">Bottom Right</button> </div> </div> <div class="demo-card"> <h2>Custom Message</h2> <div class="custom-message-container"> <textarea [(ngModel)]="customMessage" placeholder="Enter your custom message here"></textarea> <div class="button-group"> @for (type of toastTypes; track type) { <button [class]="type.toLowerCase() + '-btn'" (click)="showCustomToast(type)"> {{ type }} </button> } </div> </div> </div> <div class="demo-card"> <h2>Actions</h2> <div class="button-group"> <button class="clear-btn" (click)="clearAllToasts()">Clear All Toasts</button> <button class="max-btn" (click)="showMultipleToasts()">Show Multiple Toasts</button> </div> </div> <!-- The toast component --> <ng-toast [position]="currentPosition" [width]="350"></ng-toast> </div> `, isInline: true, styles: [".demo-container{font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;max-width:800px;margin:0 auto;padding:20px;background-color:#f9fafb;color:#111827}h1{color:#111827;margin-bottom:10px;font-weight:600;text-align:center}h2{color:#374151;font-size:1.2rem;margin-bottom:15px;font-weight:600}p{color:#6b7280;margin-bottom:20px;text-align:center}.demo-card{background-color:#fff;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 1px 3px #0000001a}.button-group{display:flex;gap:10px;flex-wrap:wrap}button{padding:10px 15px;border:none;border-radius:6px;cursor:pointer;font-weight:500;transition:all .2s ease;box-shadow:0 1px 2px #0000000d}button:hover{opacity:.9;transform:translateY(-2px);box-shadow:0 4px 6px #0000001a}.success-btn{background-color:#22c55e;color:#fff}.info-btn{background-color:#3b82f6;color:#fff}.warning-btn{background-color:#f59e0b;color:#fff}.danger-btn{background-color: