UNPKG

react-toast-kit

Version:

A modern, accessible toast notification system for React applications with automatic CSS injection

1,085 lines (957 loc) 41.8 kB
import { JSX } from 'react'; import { create } from 'zustand'; export type ToastPosition = | 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left'; export type ToastVariant = 'success' | 'error' | 'info' | 'warning' | 'default' | 'custom' | 'loading'; export type ToastTheme = 'light' | 'dark' | 'system'; export type ToastAnimation = 'slide' | 'fade' | 'bounce' | 'flip' | 'zoom' | 'none'; export type ToastStyle = 'solid' | 'gradient' | 'glass' | 'shimmer' | 'pill' | 'neon' | 'retro' | 'confetti'; // Enhanced progress bar styles export type ProgressBarStyle = | 'default' // Standard progress bar | 'fancy' // Similar to react-toastify | 'gradient-wave' // Animated gradient that moves with the progress | 'pulse' // Pulsating progress bar | 'particles' // Small particles moving along the progress bar | 'liquid' // Fluid-like animation | 'three-d' // Progress bar with depth effect (renamed from '3d') | 'dashed' // Dashed/dotted line progress | 'glow' // Glowing effect that follows progress | 'rainbow' // Rainbow color animation | 'data-flow' // Digital data flow animation | 'step-progress'; // Step-by-step completion indicators // Enhanced interface for custom animations export interface CustomAnimation { initial: Record<string, unknown>; animate: Record<string, unknown>; exit: Record<string, unknown>; transition?: Record<string, unknown>; } // Plugin interface for extensibility export interface ToastPlugin { name: string; description?: string; beforeCreate?: (options: ToastOptions) => ToastOptions; afterCreate?: (toast: Toast) => void; beforeRemove?: (toast: Toast) => boolean; afterRemove?: (toast: Toast) => void; } export interface ToastOptions { id?: string; title?: string; description?: string; variant?: ToastVariant; duration?: number; position?: ToastPosition; dismissible?: boolean; pauseOnHover?: boolean; dismissOnClick?: boolean; theme?: ToastTheme; icon?: JSX.Element; component?: JSX.Element; onDismiss?: (id: string) => void; // Styling options className?: string; style?: React.CSSProperties; animation?: ToastAnimation; customAnimation?: CustomAnimation; // New: Support for custom animations visualStyle?: ToastStyle; progressBarStyle?: ProgressBarStyle; progressBarColor?: string; // Custom color for progress bar progressBarPosition?: 'top' | 'bottom' | 'left' | 'right'; // Position of progress bar progressBarThickness?: number; // Thickness of progress bar in pixels floating?: boolean; emoji?: string; rippleEffect?: boolean; // New: Advanced options stagger?: number; // Delay between multiple toasts swipeToDismiss?: boolean; // Enable swipe to dismiss on mobile priority?: 'low' | 'normal' | 'high'; // Toast priority progressAnimation?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring'; } // Strict version for better type safety export interface StrictToastOptions extends Omit<ToastOptions, 'variant' | 'position'> { variant: ToastVariant; position: ToastPosition; } export interface Toast extends Required<Pick<ToastOptions, 'id' | 'variant' | 'position' | 'duration' | 'pauseOnHover' | 'dismissible' | 'dismissOnClick' | 'theme'>> { title?: string; description?: string; icon?: JSX.Element; component?: JSX.Element; createdAt: number; onDismiss?: (id: string) => void; // Additional fields from options className?: string; style?: React.CSSProperties; animation?: ToastAnimation; customAnimation?: CustomAnimation; visualStyle?: ToastStyle; progressBarStyle?: ProgressBarStyle; progressBarColor?: string; progressBarPosition?: 'top' | 'bottom' | 'left' | 'right'; progressBarThickness?: number; floating?: boolean; emoji?: string; rippleEffect?: boolean; stagger?: number; swipeToDismiss?: boolean; priority?: 'low' | 'normal' | 'high'; progressAnimation?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring'; // Internal properties iconString?: string; updating?: boolean; timerId?: number; // Track timer for cleanup } interface ToastState { toasts: Toast[]; maxToasts: number; theme: ToastTheme; effectiveTheme: 'light' | 'dark'; pausedToasts: Set<string>; activeTimers: Map<string, number>; // Track active timers plugins: ToastPlugin[]; // Registered plugins addToast: (toast: Toast) => string; removeToast: (id: string) => void; updateToast: (id: string, toast: Partial<Toast>) => void; pauseToast: (id: string) => void; resumeToast: (id: string) => void; clearAllToasts: () => void; setTheme: (theme: ToastTheme) => void; setMaxToasts: (max: number) => void; updateEffectiveTheme: () => void; cleanup: () => void; // Cleanup function registerPlugin: (plugin: ToastPlugin) => void; unregisterPlugin: (name: string) => void; } // Default values const DEFAULT_DURATION = 4000; const DEFAULT_POSITION: ToastPosition = 'top-right'; const DEFAULT_THEME: ToastTheme = 'system'; const DEFAULT_MAX_TOASTS = 3; const DEFAULT_ANIMATION: ToastAnimation = 'slide'; const DEFAULT_STYLE: ToastStyle = 'solid'; // const DEFAULT_PROGRESS_STYLE: ProgressBarStyle = 'default'; // const DEFAULT_PROGRESS_POSITION = 'bottom'; // const DEFAULT_PROGRESS_THICKNESS = 3; // const DEFAULT_PROGRESS_ANIMATION = 'linear'; // Enhanced system theme detection with better error handling and fallback const getSystemTheme = (): 'light' | 'dark' => { // Handle SSR if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return 'light'; // Safe default for SSR } try { // Try the standard media query first const standardQuery = window.matchMedia('(prefers-color-scheme: dark)'); if (standardQuery.matches) return 'dark'; // Try alternative methods for older browsers or special cases // iOS/Safari specific detection const iosDarkQuery = window.matchMedia('(-apple-system-interface-style: dark)'); if (iosDarkQuery.matches) return 'dark'; // Check for dark mode classes on html/body (common pattern) const htmlEl = document.documentElement; const bodyEl = document.body; if ( htmlEl?.classList.contains('dark') || bodyEl?.classList.contains('dark') || htmlEl?.classList.contains('theme-dark') || bodyEl?.classList.contains('theme-dark') || htmlEl?.dataset.theme === 'dark' || bodyEl?.dataset.theme === 'dark' ) { return 'dark'; } // Check for CSS variables that might indicate dark mode if (typeof window.getComputedStyle === 'function') { const bodyStyles = window.getComputedStyle(document.body); const htmlStyles = window.getComputedStyle(document.documentElement); const bgColor = bodyStyles.backgroundColor || htmlStyles.backgroundColor || ''; // Simple heuristic: if background is dark, probably dark mode if (bgColor) { const rgb = bgColor.match(/\d+/g)?.map(Number); if (rgb && rgb.length >= 3) { // Calculate luminance - if low, likely dark mode const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255; if (luminance < 0.5) return 'dark'; } } } // Default to light mode if all detection methods fail return 'light'; } catch (error) { console.warn('React Toast Kit: Error detecting system theme, falling back to light mode', error); return 'light'; } }; // Enhanced config value retrieval with validation const getConfigValue = <T,>(key: string, defaultValue: T): T => { if (typeof window === 'undefined') return defaultValue; try { const config = (window as any).__TOAST_CONFIG__; if (config && typeof config === 'object' && key in config) { return config[key]; } } catch (error) { console.warn(`React Toast Kit: Failed to get config value for ${key}`, error); } return defaultValue; }; // Configuration validation // const validateConfig = (config: Record<string, unknown>): Record<string, unknown> => { // if (config.maxToasts && typeof config.maxToasts === 'number' && config.maxToasts < 1) { // console.warn('React Toast Kit: maxToasts should be at least 1, setting to 1'); // config.maxToasts = 1; // } // if (config.defaultDuration && typeof config.defaultDuration === 'number' && config.defaultDuration < 0) { // console.warn('React Toast Kit: defaultDuration should be positive, setting to 4000ms'); // config.defaultDuration = 4000; // } // return config; // }; export const useToastStore = create<ToastState>((set, get) => ({ toasts: [], maxToasts: DEFAULT_MAX_TOASTS, theme: DEFAULT_THEME, effectiveTheme: getSystemTheme(), pausedToasts: new Set<string>(), activeTimers: new Map<string, number>(), plugins: [], addToast: (toast) => { try { const { toasts, maxToasts, plugins, activeTimers, pausedToasts } = get(); // Create a complete Toast object with all required fields // before passing to plugins (this fixes the TypeScript error) let processedToast: Toast = toast; // Run beforeCreate plugins plugins.forEach(plugin => { if (plugin.beforeCreate) { // Process the toast and ensure it still has all required properties const result = plugin.beforeCreate(processedToast); // Safely merge styles let mergedStyle: React.CSSProperties | undefined; if (result.style || processedToast.style) { mergedStyle = { ...(processedToast.style || {}), ...(result.style || {}) }; } // Create a new toast object with merged properties processedToast = { id: result.id || processedToast.id, variant: result.variant || processedToast.variant, position: result.position || processedToast.position, duration: result.duration !== undefined ? result.duration : processedToast.duration, pauseOnHover: result.pauseOnHover !== undefined ? result.pauseOnHover : processedToast.pauseOnHover, dismissible: result.dismissible !== undefined ? result.dismissible : processedToast.dismissible, dismissOnClick: result.dismissOnClick !== undefined ? result.dismissOnClick : processedToast.dismissOnClick, theme: result.theme || processedToast.theme, createdAt: processedToast.createdAt, title: result.title || processedToast.title, description: result.description || processedToast.description, icon: result.icon || processedToast.icon, component: result.component || processedToast.component, onDismiss: result.onDismiss || processedToast.onDismiss, className: result.className || processedToast.className, style: mergedStyle, animation: result.animation || processedToast.animation, customAnimation: result.customAnimation || processedToast.customAnimation, visualStyle: result.visualStyle || processedToast.visualStyle, progressBarStyle: result.progressBarStyle || processedToast.progressBarStyle, progressBarColor: result.progressBarColor || processedToast.progressBarColor, progressBarPosition: result.progressBarPosition || processedToast.progressBarPosition, progressBarThickness: result.progressBarThickness || processedToast.progressBarThickness, floating: result.floating !== undefined ? result.floating : processedToast.floating, emoji: result.emoji || processedToast.emoji, rippleEffect: result.rippleEffect !== undefined ? result.rippleEffect : processedToast.rippleEffect, stagger: result.stagger !== undefined ? result.stagger : processedToast.stagger, swipeToDismiss: result.swipeToDismiss !== undefined ? result.swipeToDismiss : processedToast.swipeToDismiss, priority: result.priority || processedToast.priority, progressAnimation: result.progressAnimation || processedToast.progressAnimation, iconString: processedToast.iconString, updating: processedToast.updating, timerId: processedToast.timerId, }; } }); // Priority-based insertion let insertIndex = toasts.length; if (processedToast.priority === 'high') { insertIndex = 0; } else if (processedToast.priority === 'normal') { // Find the first low priority toast const lowPriorityIndex = toasts.findIndex(t => t.priority === 'low'); if (lowPriorityIndex !== -1) { insertIndex = lowPriorityIndex; } } // Remove oldest if at capacity if (toasts.length >= maxToasts) { // Find the oldest toast that isn't the current one const oldestToast = [...toasts] .sort((a, b) => a.createdAt - b.createdAt) .find(t => t.id !== processedToast.id); if (oldestToast) { get().removeToast(oldestToast.id); } } // Add toast to state const updatedToasts = [...toasts]; updatedToasts.splice(insertIndex, 0, processedToast); set({ toasts: updatedToasts }); // Set up timer for auto-dismiss if duration is set and toast is not paused if (processedToast.duration && processedToast.duration > 0 && !pausedToasts.has(processedToast.id)) { // Clear any existing timer for this toast const existingTimer = activeTimers.get(processedToast.id); if (existingTimer) { clearTimeout(existingTimer); } // Create new timer const timerId = window.setTimeout(() => { get().removeToast(processedToast.id); }, processedToast.duration) as unknown as number; // Store timer reference set(state => ({ activeTimers: new Map(state.activeTimers).set(processedToast.id, timerId) })); } // Run afterCreate plugins plugins.forEach(plugin => { if (plugin.afterCreate) { plugin.afterCreate(processedToast); } }); // Add stagger delay if specified if (processedToast.stagger && processedToast.stagger > 0) { setTimeout(() => { // Returning ID after stagger }, processedToast.stagger); } return processedToast.id; } catch (error) { console.error('React Toast Kit: Failed to add toast', error); return ''; } }, removeToast: (id) => { try { const { toasts, plugins, activeTimers } = get(); const toast = toasts.find(t => t.id === id); if (!toast) return; // Run beforeRemove plugins let shouldRemove = true; plugins.forEach(plugin => { if (plugin.beforeRemove && plugin.beforeRemove(toast) === false) { shouldRemove = false; } }); if (!shouldRemove) return; // Clear timer if exists const timerId = activeTimers.get(id); if (timerId) { clearTimeout(timerId); } // Call onDismiss handler if (toast.onDismiss) { toast.onDismiss(id); } set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id), pausedToasts: new Set([...state.pausedToasts].filter(i => i !== id)), activeTimers: new Map([...state.activeTimers].filter(([key]) => key !== id)) })); // Run afterRemove plugins plugins.forEach(plugin => { if (plugin.afterRemove) { plugin.afterRemove(toast); } }); } catch (error) { console.error('React Toast Kit: Failed to remove toast', error); } }, updateToast: (id, updatedToast) => { try { const { activeTimers } = get(); // Update the toast in state set((state) => ({ toasts: state.toasts.map((t) => { if (t.id === id) { const updated = { ...t, ...updatedToast, updating: true }; // If duration changed and there's an active timer, reset it if (updatedToast.duration !== undefined && updated.duration !== t.duration) { // Clear existing timer const existingTimer = activeTimers.get(id); if (existingTimer) { clearTimeout(existingTimer); } // Set up new timer if duration is positive and toast isn't paused if (updated.duration > 0 && !state.pausedToasts.has(id)) { const newTimerId = window.setTimeout(() => { get().removeToast(id); }, updated.duration) as unknown as number; // Update the timer map after the state update completes setTimeout(() => { set(state => ({ activeTimers: new Map(state.activeTimers).set(id, newTimerId) })); }, 0); } } return updated; } return t; }) })); // Clear updating flag after animation setTimeout(() => { set((state) => ({ toasts: state.toasts.map((t) => t.id === id ? { ...t, updating: false } : t ) })); }, 100); } catch (error) { console.error('React Toast Kit: Failed to update toast', error); } }, pauseToast: (id) => { const { activeTimers } = get(); // Clear the existing timer if there is one const existingTimer = activeTimers.get(id); if (existingTimer) { clearTimeout(existingTimer); // Update active timers map to remove this timer set(state => { const newTimers = new Map(state.activeTimers); newTimers.delete(id); return { activeTimers: newTimers }; }); } set((state) => ({ pausedToasts: new Set(state.pausedToasts).add(id) })); }, resumeToast: (id) => { const { toasts } = get(); // Remove from paused set set((state) => { const newPausedToasts = new Set(state.pausedToasts); newPausedToasts.delete(id); return { pausedToasts: newPausedToasts }; }); // Restart the timer if toast has a duration const toast = toasts.find(t => t.id === id); if (toast && toast.duration && toast.duration > 0) { // Calculate remaining time based on creation time and elapsed time const elapsed = Date.now() - toast.createdAt; const remaining = Math.max(0, toast.duration - elapsed); // Create new timer with remaining time const timerId = window.setTimeout(() => { get().removeToast(id); }, remaining) as unknown as number; // Store timer reference set(state => ({ activeTimers: new Map(state.activeTimers).set(id, timerId) })); } }, clearAllToasts: () => { try { const { toasts, activeTimers } = get(); // Clear all timers activeTimers.forEach(timerId => clearTimeout(timerId)); // Call onDismiss handlers toasts.forEach(toast => { if (toast.onDismiss) { toast.onDismiss(toast.id); } }); set({ toasts: [], pausedToasts: new Set(), activeTimers: new Map() }); } catch (error) { console.error('React Toast Kit: Failed to clear all toasts', error); } }, setTheme: (theme) => { set({ theme }); get().updateEffectiveTheme(); }, setMaxToasts: (max) => { const validatedMax = Math.max(1, max); // Ensure at least 1 set({ maxToasts: validatedMax }); }, updateEffectiveTheme: () => { const { theme } = get(); let newEffectiveTheme: 'light' | 'dark'; if (theme === 'system') { newEffectiveTheme = getSystemTheme(); } else { newEffectiveTheme = theme as 'light' | 'dark'; } set({ effectiveTheme: newEffectiveTheme }); }, cleanup: () => { try { const { activeTimers, toasts } = get(); // Clear all active timers activeTimers.forEach(timerId => clearTimeout(timerId)); // Call onDismiss for all toasts toasts.forEach(toast => { if (toast.onDismiss) { toast.onDismiss(toast.id); } }); set({ toasts: [], pausedToasts: new Set(), activeTimers: new Map() }); } catch (error) { console.error('React Toast Kit: Failed to cleanup', error); } }, registerPlugin: (plugin) => { try { set((state) => ({ plugins: [...state.plugins.filter(p => p.name !== plugin.name), plugin] })); } catch (error) { console.error('React Toast Kit: Failed to register plugin', error); } }, unregisterPlugin: (name) => { try { set((state) => ({ plugins: state.plugins.filter(p => p.name !== name) })); } catch (error) { console.error('React Toast Kit: Failed to unregister plugin', error); } } })); // Setup media query listener in a safe way that doesn't interfere with React hooks let mediaQuerySetup = false; const setupMediaQueryListener = () => { if (mediaQuerySetup || typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return; } try { const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); // Modern browsers if (typeof darkModeQuery.addEventListener === 'function') { darkModeQuery.addEventListener('change', () => { const { theme } = useToastStore.getState(); if (theme === 'system') { useToastStore.getState().updateEffectiveTheme(); } }); } // Older browsers else if (typeof darkModeQuery.addListener === 'function') { darkModeQuery.addListener(() => { const { theme } = useToastStore.getState(); if (theme === 'system') { useToastStore.getState().updateEffectiveTheme(); } }); } // Handle iOS Safari try { const iosDarkQuery = window.matchMedia('(-apple-system-interface-style: dark)'); if (typeof iosDarkQuery.addEventListener === 'function') { iosDarkQuery.addEventListener('change', () => { const { theme } = useToastStore.getState(); if (theme === 'system') { useToastStore.getState().updateEffectiveTheme(); } }); } } catch (e) { // iOS query not supported, ignore } // Watch for class changes on html/body if (typeof MutationObserver === 'function') { const htmlObserver = new MutationObserver(() => { const { theme } = useToastStore.getState(); if (theme === 'system') { useToastStore.getState().updateEffectiveTheme(); } }); htmlObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); const bodyObserver = new MutationObserver(() => { const { theme } = useToastStore.getState(); if (theme === 'system') { useToastStore.getState().updateEffectiveTheme(); } }); if (document.body) { bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); } } mediaQuerySetup = true; } catch (error) { console.warn('React Toast Kit: Error setting up theme change listeners', error); } }; // Only setup when needed, not at module level if (typeof window !== 'undefined' && typeof document !== 'undefined') { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupMediaQueryListener); } else { setupMediaQueryListener(); } // Initialize effective theme useToastStore.getState().updateEffectiveTheme(); } const generateId = () => Math.random().toString(36).substring(2, 9); // Enhanced icon strings with better SVG structure const iconStrings = { success: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Success"> <path d="M10 18.3333C14.6024 18.3333 18.3334 14.6023 18.3334 9.99996C18.3334 5.39759 14.6024 1.66663 10 1.66663C5.39765 1.66663 1.66669 5.39759 1.66669 9.99996C1.66669 14.6023 5.39765 18.3333 10 18.3333Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.66669 10L9.16669 12.5L13.3334 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>`, error: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Error"> <path d="M10 18.3333C14.6024 18.3333 18.3334 14.6023 18.3334 9.99996C18.3334 5.39759 14.6024 1.66663 10 1.66663C5.39765 1.66663 1.66669 5.39759 1.66669 9.99996C1.66669 14.6023 5.39765 18.3333 10 18.3333Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12.5 7.5L7.5 12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M7.5 7.5L12.5 12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>`, warning: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Warning"> <path d="M8.57465 3.21667L1.51632 15C1.37079 15.2589 1.29379 15.5503 1.29298 15.8469C1.29216 16.1434 1.36756 16.4353 1.51175 16.6951C1.65593 16.9548 1.86359 17.1738 2.11656 17.3309C2.36954 17.4879 2.65908 17.5778 2.95548 17.5917H17.0721C17.3685 17.5778 17.6581 17.4879 17.9111 17.3309C18.164 17.1738 18.3717 16.9548 18.5159 16.6951C18.6601 16.4353 18.7355 16.1434 18.7347 15.8469C18.7339 15.5503 18.6569 15.2589 18.5113 15L11.453 3.21667C11.3018 2.96735 11.0893 2.7609 10.8353 2.61224C10.5813 2.46357 10.294 2.3779 10.0005 2.3646C9.7069 2.37798 9.41956 2.46374 9.16556 2.61247C8.91156 2.76121 8.69907 2.96773 8.54798 3.21708L8.57465 3.21667Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 7.5V10.8333" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 14.1667H10.0083" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>`, info: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Info"> <path d="M10 18.3333C14.6024 18.3333 18.3334 14.6023 18.3334 9.99996C18.3334 5.39759 14.6024 1.66663 10 1.66663C5.39765 1.66663 1.66669 5.39759 1.66669 9.99996C1.66669 14.6023 5.39765 18.3333 10 18.3333Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 13.3333V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 6.66663H10.0083" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>`, default: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Notification"> <path d="M18.3334 10C18.3334 14.6024 14.6024 18.3333 10 18.3333C5.39765 18.3333 1.66669 14.6024 1.66669 10C1.66669 5.39763 5.39765 1.66667 10 1.66667C14.6024 1.66667 18.3334 5.39763 18.3334 10Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 6.66667V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 13.3333H10.0083" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>`, loading: `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="animate-spin" role="img" aria-label="Loading"> <path d="M10 3.33337V5.00004" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 15V16.6667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4.1084 4.1084L5.2834 5.28257" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M14.7166 14.7167L15.8916 15.8917" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3.33337 10H5.00004" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M15 10H16.6667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4.1084 15.8917L5.2834 14.7167" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M14.7166 5.28257L15.8916 4.1084" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>`, }; // Enhanced toast creation with better error handling const createToast = (options: ToastOptions | string): string => { try { let toastOptions: ToastOptions; if (typeof options === 'string') { toastOptions = { description: options, // Apply defaults from provider duration: getConfigValue('defaultDuration', DEFAULT_DURATION), dismissible: getConfigValue('defaultDismissible', true), pauseOnHover: getConfigValue('defaultPauseOnHover', true), dismissOnClick: getConfigValue('defaultDismissOnClick', false), animation: getConfigValue('defaultAnimation', DEFAULT_ANIMATION), visualStyle: getConfigValue('defaultStyle', DEFAULT_STYLE), progressBarStyle: getConfigValue('defaultProgressBarStyle', undefined), progressBarColor: getConfigValue('defaultProgressBarColor', undefined), progressBarPosition: getConfigValue('defaultProgressBarPosition', 'bottom'), progressBarThickness: getConfigValue('defaultProgressBarThickness', 3), progressAnimation: getConfigValue('defaultProgressAnimation', 'linear'), floating: getConfigValue('defaultFloating', false), rippleEffect: getConfigValue('defaultRippleEffect', false), swipeToDismiss: getConfigValue('defaultSwipeToDismiss', false), priority: getConfigValue('defaultPriority', 'normal'), stagger: getConfigValue('defaultStagger', 0), }; } else { toastOptions = { // Apply defaults first, then override with provided options duration: getConfigValue('defaultDuration', DEFAULT_DURATION), dismissible: getConfigValue('defaultDismissible', true), pauseOnHover: getConfigValue('defaultPauseOnHover', true), dismissOnClick: getConfigValue('defaultDismissOnClick', false), animation: getConfigValue('defaultAnimation', DEFAULT_ANIMATION), visualStyle: getConfigValue('defaultStyle', DEFAULT_STYLE), progressBarStyle: getConfigValue('defaultProgressBarStyle', undefined), progressBarColor: getConfigValue('defaultProgressBarColor', undefined), progressBarPosition: getConfigValue('defaultProgressBarPosition', 'bottom'), progressBarThickness: getConfigValue('defaultProgressBarThickness', 3), progressAnimation: getConfigValue('defaultProgressAnimation', 'linear'), floating: getConfigValue('defaultFloating', false), rippleEffect: getConfigValue('defaultRippleEffect', false), swipeToDismiss: getConfigValue('defaultSwipeToDismiss', false), priority: getConfigValue('defaultPriority', 'normal'), stagger: getConfigValue('defaultStagger', 0), ...options, // User options override defaults }; } // Apply global styles if configured const globalClassName = getConfigValue('globalClassName', undefined); const globalStyle = getConfigValue('globalStyle', undefined); if (globalClassName) { toastOptions.className = toastOptions.className ? `${globalClassName} ${toastOptions.className}` : globalClassName; } if (globalStyle && typeof globalStyle === 'object') { toastOptions.style = { ...(globalStyle as React.CSSProperties), ...(toastOptions.style || {}), }; } const toast: Toast = { id: toastOptions.id || generateId(), variant: toastOptions.variant || 'default', position: toastOptions.position || DEFAULT_POSITION, duration: toastOptions.duration !== undefined ? toastOptions.duration : DEFAULT_DURATION, pauseOnHover: toastOptions.pauseOnHover !== undefined ? toastOptions.pauseOnHover : true, dismissible: toastOptions.dismissible !== undefined ? toastOptions.dismissible : true, dismissOnClick: toastOptions.dismissOnClick !== undefined ? toastOptions.dismissOnClick : false, theme: toastOptions.theme || DEFAULT_THEME, title: toastOptions.title, description: toastOptions.description, icon: toastOptions.icon, component: toastOptions.component, createdAt: Date.now(), onDismiss: toastOptions.onDismiss, className: toastOptions.className, style: toastOptions.style, animation: toastOptions.animation, customAnimation: toastOptions.customAnimation, visualStyle: toastOptions.visualStyle, progressBarStyle: toastOptions.progressBarStyle, progressBarColor: toastOptions.progressBarColor, progressBarPosition: toastOptions.progressBarPosition, progressBarThickness: toastOptions.progressBarThickness, floating: toastOptions.floating, emoji: toastOptions.emoji, rippleEffect: toastOptions.rippleEffect, stagger: toastOptions.stagger, swipeToDismiss: toastOptions.swipeToDismiss, priority: toastOptions.priority, progressAnimation: toastOptions.progressAnimation, }; // Add icon string if variant has one and no custom icon provided if (!toast.icon && !toast.emoji && toast.variant && toast.variant in iconStrings) { toast.iconString = iconStrings[toast.variant as keyof typeof iconStrings]; } return useToastStore.getState().addToast(toast); } catch (error) { console.error('React Toast Kit: Failed to create toast', error); return ''; } }; // Enhanced type definitions for function overloads type ToastHandler = { (message: string): string; (options: ToastOptions): string; success: (options: ToastOptions | string) => string; error: (options: ToastOptions | string) => string; warning: (options: ToastOptions | string) => string; info: (options: ToastOptions | string) => string; loading: (options: ToastOptions | string) => string; custom: (component: JSX.Element, options?: Omit<ToastOptions, 'component'>) => string; promise: <T>( promise: Promise<T>, options: { loading: ToastOptions | string; success: ((data: T) => ToastOptions | string) | ToastOptions | string; error: ((error: unknown) => ToastOptions | string) | ToastOptions | string; } ) => Promise<T>; update: (id: string, options: Partial<ToastOptions>) => void; dismiss: (id?: string) => void; clearAll: () => void; }; const createVariantToast = (variant: ToastVariant) => (options: ToastOptions | string): string => { const toastOptions = typeof options === 'string' ? { description: options } : options; return createToast({ ...toastOptions, variant }); }; export const toast = ((options: ToastOptions | string) => createToast(options)) as ToastHandler; toast.success = createVariantToast('success'); toast.error = createVariantToast('error'); toast.warning = createVariantToast('warning'); toast.info = createVariantToast('info'); toast.loading = createVariantToast('loading'); toast.custom = (component: JSX.Element, options: Omit<ToastOptions, 'component'> = {}) => { return createToast({ ...options, component, variant: 'custom' }); }; // Enhanced promise handler with better error handling toast.promise = async <T>( promise: Promise<T>, options: { loading: ToastOptions | string; success: ((data: T) => ToastOptions | string) | ToastOptions | string; error: ((error: unknown) => ToastOptions | string) | ToastOptions | string; } ): Promise<T> => { const id = createToast({ ...((typeof options.loading === 'string') ? { description: options.loading } : options.loading), variant: 'loading', duration: 0, }); try { const data = await promise; let successOptions: ToastOptions | string; if (typeof options.success === 'function') { successOptions = options.success(data); } else { successOptions = options.success; } toast.update( id, typeof successOptions === 'string' ? { description: successOptions, variant: 'success', duration: DEFAULT_DURATION } : { ...successOptions, variant: 'success', duration: successOptions.duration || DEFAULT_DURATION } ); return data; } catch (error) { let errorOptions: ToastOptions | string; if (typeof options.error === 'function') { errorOptions = options.error(error); } else { errorOptions = options.error; } toast.update( id, typeof errorOptions === 'string' ? { description: errorOptions, variant: 'error', duration: DEFAULT_DURATION } : { ...errorOptions, variant: 'error', duration: errorOptions.duration || DEFAULT_DURATION } ); throw error; } }; toast.update = (id: string, options: Partial<ToastOptions>) => { try { const existingToast = useToastStore.getState().toasts.find(t => t.id === id); if (existingToast && existingToast.variant === 'loading' && options.variant && options.variant !== 'loading' && !options.duration) { options.duration = DEFAULT_DURATION; } useToastStore.getState().updateToast(id, options as Partial<Toast>); } catch (error) { console.error('React Toast Kit: Failed to update toast', error); } }; toast.dismiss = (id?: string) => { try { if (id) { useToastStore.getState().removeToast(id); } else { useToastStore.getState().clearAllToasts(); } } catch (error) { console.error('React Toast Kit: Failed to dismiss toast', error); } }; toast.clearAll = () => { try { useToastStore.getState().clearAllToasts(); } catch (error) { console.error('React Toast Kit: Failed to clear all toasts', error); } }; // Development tools (only available in development) type DevTools = { getActiveToasts: () => Toast[]; clearAll: () => void; debugInfo: () => void; getStore: () => ToastState; simulateError: () => void; simulateSuccess: () => void; }; // Development tools (only available in development) const devTools = typeof process !== 'undefined' && process.env.NODE_ENV === 'development' ? { getActiveToasts: () => useToastStore.getState().toasts, clearAll: () => useToastStore.getState().clearAllToasts(), debugInfo: () => { const state = useToastStore.getState(); console.group('React Toast Kit Debug Info'); console.table(state.toasts.map(t => ({ id: t.id, variant: t.variant, position: t.position, duration: t.duration, priority: t.priority, createdAt: new Date(t.createdAt).toLocaleTimeString() }))); console.log('Theme:', state.theme, '| Effective:', state.effectiveTheme); console.log('Paused toasts:', Array.from(state.pausedToasts)); console.log('Active timers:', state.activeTimers.size); console.log('Registered plugins:', state.plugins.map(p => p.name)); console.groupEnd(); }, getStore: () => useToastStore.getState(), simulateError: () => { toast.error('This is a test error toast for debugging'); }, simulateSuccess: () => { toast.success('This is a test success toast for debugging'); } } as DevTools : {} as DevTools; // Plugin registration helpers export const registerPlugin = (plugin: ToastPlugin) => { useToastStore.getState().registerPlugin(plugin); }; export const unregisterPlugin = (name: string) => { useToastStore.getState().unregisterPlugin(name); }; // Cleanup function for applications export const cleanup = () => { useToastStore.getState().cleanup(); }; // DevTools specific API - only imported when using the devtools entry point export const toastDevTools = { /** * Show the DevTools panel */ show: () => { if (typeof window !== 'undefined') { const event = new CustomEvent('react-toast-kit:devtools:show'); window.dispatchEvent(event); } }, /** * Hide the DevTools panel */ hide: () => { if (typeof window !== 'undefined') { const event = new CustomEvent('react-toast-kit:devtools:hide'); window.dispatchEvent(event); } }, /** * Toggle the DevTools panel visibility */ toggle: () => { if (typeof window !== 'undefined') { const event = new CustomEvent('react-toast-kit:devtools:toggle'); window.dispatchEvent(event); } }, /** * Get all active toasts */ getToasts: () => useToastStore.getState().toasts, // Include the development tools but only in development ...devTools // Note: We removed the duplicate clearAll property since it's already in the devTools object };