/** * 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 = `
${icon}
${title ? `
${title}
` : ''}
${this.escapeHtml(message)}
`; // Adicionar eventos de hover toast.addEventListener('mouseenter', () => { const progressBar = toast.querySelector('.toast-progress'); if (progressBar) { progressBar.style.animationPlayState = 'paused'; } }); toast.addEventListener('mouseleave', () => { const progressBar = toast.querySelector('.toast-progress'); if (progressBar) { progressBar.style.animationPlayState = 'running'; } }); // Permitir clique para fechar (exceto no botão de fechar) toast.addEventListener('click', (e) => { if (!e.target.classList.contains('toast-close')) { this.removeToast(id); } }); return toast; } /** * Obtém título baseado no tipo * @param {string} type - Tipo do toast * @returns {string} Título */ getTitleByType(type) { const titles = { success: 'Sucesso!', error: 'Erro!', warning: 'Atenção!', info: 'Informação' }; return titles[type] || ''; } /** * Remove um toast específico * @param {string} toastId - ID do toast a remover */ removeToast(toastId) { const toastData = this.toasts.get(toastId); if (!toastData) { return; } const toast = toastData.element; toast.classList.add('removing'); // Remover após animação setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } this.toasts.delete(toastId); }, 300); } /** * Remove todos os toasts ativos */ removeAllToasts() { const toastIds = Array.from(this.toasts.keys()); toastIds.forEach(id => this.removeToast(id)); } /** * Escapa HTML para prevenir XSS * @param {string} text - Texto a escapar * @returns {string} Texto escapado */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Métodos de conveniência /** * Mostra um toast de sucesso * @param {string} message - Mensagem * @param {number} duration - Duração em milissegundos * @param {Object} options - Opções adicionais */ success(message, duration = null, options = {}) { return this.show(message, 'success', duration, options); } /** * Mostra um toast de erro * @param {string} message - Mensagem * @param {number} duration - Duração em milissegundos * @param {Object} options - Opções adicionais */ error(message, duration = null, options = {}) { return this.show(message, 'error', duration, options); } /** * Mostra um toast de aviso * @param {string} message - Mensagem * @param {number} duration - Duração em milissegundos * @param {Object} options - Opções adicionais */ warning(message, duration = null, options = {}) { return this.show(message, 'warning', duration, options); } /** * Mostra um toast de informação * @param {string} message - Mensagem * @param {number} duration - Duração em milissegundos * @param {Object} options - Opções adicionais */ info(message, duration = null, options = {}) { return this.show(message, 'info', duration, options); } /** * Mostra um toast de loading (persistente) * @param {string} message - Mensagem * @param {Object} options - Opções adicionais */ loading(message = 'Carregando...', options = {}) { const toastId = this.show(message, 'info', 0, { icon: '⏳', title: 'Aguarde...', ...options }); // Remover barra de progresso para loading persistente const toast = this.toasts.get(toastId); if (toast) { const progressBar = toast.element.querySelector('.toast-progress'); if (progressBar) { progressBar.remove(); } } return toastId; } /** * Atualiza o conteúdo de um toast existente * @param {string} toastId - ID do toast * @param {string} message - Nova mensagem * @param {string} type - Novo tipo (opcional) */ updateToast(toastId, message, type = null) { const toastData = this.toasts.get(toastId); if (!toastData) { return false; } const toast = toastData.element; const messageElement = toast.querySelector('.toast-message'); if (messageElement) { messageElement.textContent = message; } if (type && type !== toastData.type) { toast.className = toast.className.replace(`toast-${toastData.type}`, `toast-${type}`); toastData.type = type; // Atualizar ícone const iconElement = toast.querySelector('.toast-icon'); if (iconElement) { const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; iconElement.textContent = icons[type] || '💬'; } } return true; } /** * Configura o ToastManager * @param {Object} config - Configurações */ setConfig(config) { this.config = { ...this.config, ...config }; // Recriar container se a posição mudou if (config.position) { this.createContainer(); } } /** * Obtém estatísticas dos toasts * @returns {Object} Estatísticas */ getStats() { return { activeToasts: this.toasts.size, maxToasts: this.config.maxToasts, position: this.config.position }; } } // Criar instância global window.toastManager = new ToastManager(); console.log('🍞 ToastManager inicializado com sucesso');