/** * ToastManager - Sistema de Notificações Visuais * Responsável por exibir mensagens de feedback ao usuário de forma elegante e não-intrusiva */ class ToastManager { constructor() { this.container = null; this.toasts = new Map(); this.config = { position: 'top-right', maxToasts: 5, duration: { success: 3000, error: 5000, warning: 4000, info: 3000 } }; this.init(); } /** * Inicializa o ToastManager criando o container */ init() { this.createContainer(); console.log('🍞 ToastManager inicializado'); } /** * Cria o container de toasts no DOM */ createContainer() { // Remover container existente se houver const existingContainer = document.querySelector('.toast-container'); if (existingContainer) { existingContainer.remove(); } // Criar novo container this.container = document.createElement('div'); this.container.className = `toast-container toast-${this.config.position}`; // Adicionar estilos CSS this.addStyles(); // Adicionar ao body document.body.appendChild(this.container); } /** * Adiciona estilos CSS para os toasts */ addStyles() { // Verificar se os estilos já existem if (document.querySelector('#toast-styles')) { return; } const styles = document.createElement('style'); styles.id = 'toast-styles'; styles.textContent = ` .toast-container { position: fixed; z-index: 10000; pointer-events: none; display: flex; flex-direction: column; gap: 8px; max-width: 400px; width: 100%; } .toast-top-right { top: 20px; right: 20px; } .toast-top-left { top: 20px; left: 20px; } .toast-bottom-right { bottom: 20px; right: 20px; } .toast-bottom-left { bottom: 20px; left: 20px; } .toast-top-center { top: 20px; left: 50%; transform: translateX(-50%); } .toast-bottom-center { bottom: 20px; left: 50%; transform: translateX(-50%); } .toast { background: var(--color-bg-2); color: var(--color-text); border-radius: 8px; padding: 16px 20px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border-left: 4px solid var(--color-primary); display: flex; align-items: flex-start; gap: 12px; animation: toast-in 0.3s ease-out; pointer-events: auto; position: relative; overflow: hidden; } .toast-success { border-left-color: #10b981; background: linear-gradient(135deg, #10b98120 0%, transparent 100%); } .toast-error { border-left-color: #ef4444; background: linear-gradient(135deg, #ef444420 0%, transparent 100%); } .toast-warning { border-left-color: #f59e0b; background: linear-gradient(135deg, #f59e0b20 0%, transparent 100%); } .toast-info { border-left-color: #3b82f6; background: linear-gradient(135deg, #3b82f620 0%, transparent 100%); } .toast-icon { flex-shrink: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 16px; } .toast-content { flex: 1; min-width: 0; } .toast-title { font-weight: 600; margin-bottom: 4px; font-size: 14px; } .toast-message { font-size: 13px; line-height: 1.4; opacity: 0.9; } .toast-close { flex-shrink: 0; width: 20px; height: 20px; border: none; background: transparent; color: var(--color-text); cursor: pointer; opacity: 0.6; transition: opacity 0.2s; display: flex; align-items: center; justify-content: center; font-size: 16px; padding: 0; } .toast-close:hover { opacity: 1; } .toast-progress { position: absolute; bottom: 0; left: 0; height: 2px; background: var(--color-primary); opacity: 0.3; animation: toast-progress linear forwards; } .toast-success .toast-progress { background: #10b981; } .toast-error .toast-progress { background: #ef4444; } .toast-warning .toast-progress { background: #f59e0b; } .toast-info .toast-progress { background: #3b82f6; } .toast.removing { animation: toast-out 0.3s ease-in forwards; } @keyframes toast-in { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } @keyframes toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } } @keyframes toast-progress { from { width: 100%; } to { width: 0%; } } /* Responsividade */ @media (max-width: 480px) { .toast-container { left: 10px !important; right: 10px !important; max-width: none; } .toast { padding: 14px 16px; } } `; document.head.appendChild(styles); } /** * Mostra um toast genérico * @param {string} message - Mensagem a exibir * @param {string} type - Tipo do toast (success, error, warning, info) * @param {number} duration - Duração em milissegundos * @param {Object} options - Opções adicionais */ show(message, type = 'info', duration = null, options = {}) { // Limitar número de toasts simultâneos if (this.toasts.size >= this.config.maxToasts) { // Remover o toast mais antigo const oldestToast = this.toasts.values().next().value; if (oldestToast) { this.removeToast(oldestToast.id); } } const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const toastDuration = duration || this.config.duration[type] || 3000; const toast = this.createToastElement(toastId, message, type, options); this.container.appendChild(toast); const toastData = { id: toastId, element: toast, type: type, message: message, startTime: Date.now(), duration: toastDuration }; this.toasts.set(toastId, toastData); // Configurar remoção automática if (toastDuration > 0) { setTimeout(() => { this.removeToast(toastId); }, toastDuration); // Configurar barra de progresso const progressBar = toast.querySelector('.toast-progress'); if (progressBar) { progressBar.style.animationDuration = `${toastDuration}ms`; } } console.log(`🍞 Toast ${type}: ${message}`); return toastId; } /** * Cria o elemento HTML do toast * @param {string} id - ID do toast * @param {string} message - Mensagem * @param {string} type - Tipo do toast * @param {Object} options - Opções adicionais * @returns {HTMLElement} Elemento do toast */ createToastElement(id, message, type, options = {}) { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.id = id; // Ícone baseado no tipo const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; const icon = options.icon || icons[type] || '💬'; const title = options.title || this.getTitleByType(type); toast.innerHTML = `