| |
| |
| |
| |
|
|
| export class NotificationSystem { |
| constructor() { |
| this.container = null; |
| this.queue = []; |
| this.activeToasts = new Set(); |
| this.maxToasts = 3; |
| this.init(); |
| } |
|
|
| |
| |
| |
| init() { |
| if (!this.container) { |
| this.container = document.createElement('div'); |
| this.container.id = 'notification-container'; |
| this.container.className = 'notification-container'; |
| this.container.setAttribute('aria-live', 'polite'); |
| this.container.setAttribute('aria-atomic', 'true'); |
| document.body.appendChild(this.container); |
| } |
| } |
|
|
| |
| |
| |
| |
| show(options = {}) { |
| const defaults = { |
| type: 'info', |
| title: '', |
| message: '', |
| duration: 4000, |
| closable: true, |
| icon: null, |
| action: null, |
| position: 'top-right' |
| }; |
|
|
| const config = { ...defaults, ...options }; |
|
|
| |
| if (this.activeToasts.size >= this.maxToasts) { |
| this.queue.push(config); |
| return; |
| } |
|
|
| this.createToast(config); |
| } |
|
|
| |
| |
| |
| |
| createToast(config) { |
| const toast = document.createElement('div'); |
| toast.className = `notification notification-${config.type}`; |
| toast.setAttribute('role', 'alert'); |
|
|
| |
| const icon = this.getIcon(config.type, config.icon); |
|
|
| |
| const content = ` |
| <div class="notification-icon">${icon}</div> |
| <div class="notification-content"> |
| ${config.title ? `<div class="notification-title">${config.title}</div>` : ''} |
| <div class="notification-message">${config.message}</div> |
| ${config.action ? ` |
| <button class="notification-action" onclick="${config.action.onClick}"> |
| ${config.action.label} |
| </button> |
| ` : ''} |
| </div> |
| ${config.closable ? ` |
| <button class="notification-close" aria-label="Close notification"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="18" y1="6" x2="6" y2="18"></line> |
| <line x1="6" y1="6" x2="18" y2="18"></line> |
| </svg> |
| </button> |
| ` : ''} |
| `; |
|
|
| toast.innerHTML = content; |
|
|
| |
| if (config.duration > 0) { |
| const progress = document.createElement('div'); |
| progress.className = 'notification-progress'; |
| progress.style.animationDuration = `${config.duration}ms`; |
| toast.appendChild(progress); |
| } |
|
|
| |
| this.container.appendChild(toast); |
| this.activeToasts.add(toast); |
|
|
| |
| requestAnimationFrame(() => { |
| toast.classList.add('notification-show'); |
| }); |
|
|
| |
| if (config.closable) { |
| const closeBtn = toast.querySelector('.notification-close'); |
| closeBtn.addEventListener('click', () => this.removeToast(toast)); |
| } |
|
|
| |
| if (config.duration > 0) { |
| setTimeout(() => this.removeToast(toast), config.duration); |
| } |
|
|
| |
| toast.addEventListener('mouseenter', () => { |
| const progress = toast.querySelector('.notification-progress'); |
| if (progress) progress.style.animationPlayState = 'paused'; |
| }); |
|
|
| toast.addEventListener('mouseleave', () => { |
| const progress = toast.querySelector('.notification-progress'); |
| if (progress) progress.style.animationPlayState = 'running'; |
| }); |
| } |
|
|
| |
| |
| |
| |
| removeToast(toast) { |
| if (!toast || !this.activeToasts.has(toast)) return; |
|
|
| toast.classList.remove('notification-show'); |
| toast.classList.add('notification-hide'); |
|
|
| setTimeout(() => { |
| if (toast.parentNode) { |
| toast.parentNode.removeChild(toast); |
| } |
| this.activeToasts.delete(toast); |
|
|
| |
| if (this.queue.length > 0) { |
| const next = this.queue.shift(); |
| this.createToast(next); |
| } |
| }, 300); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| getIcon(type, customIcon) { |
| if (customIcon) return customIcon; |
|
|
| const icons = { |
| success: ` |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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> |
| `, |
| error: ` |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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> |
| `, |
| warning: ` |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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> |
| `, |
| info: ` |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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> |
| ` |
| }; |
|
|
| return icons[type] || icons.info; |
| } |
|
|
| |
| |
| |
| success(message, title = 'Success', options = {}) { |
| this.show({ type: 'success', message, title, ...options }); |
| } |
|
|
| error(message, title = 'Error', options = {}) { |
| this.show({ type: 'error', message, title, ...options }); |
| } |
|
|
| warning(message, title = 'Warning', options = {}) { |
| this.show({ type: 'warning', message, title, ...options }); |
| } |
|
|
| info(message, title = 'Info', options = {}) { |
| this.show({ type: 'info', message, title, ...options }); |
| } |
|
|
| |
| |
| |
| clearAll() { |
| this.activeToasts.forEach(toast => this.removeToast(toast)); |
| this.queue = []; |
| } |
|
|
| |
| |
| |
| static injectStyles() { |
| if (document.querySelector('#notification-system-styles')) return; |
|
|
| const style = document.createElement('style'); |
| style.id = 'notification-system-styles'; |
| style.textContent = ` |
| .notification-container { |
| position: fixed; |
| top: 70px; |
| right: 20px; |
| z-index: 10000; |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| max-width: 400px; |
| pointer-events: none; |
| } |
| |
| .notification { |
| display: flex; |
| align-items: flex-start; |
| gap: 12px; |
| padding: 16px; |
| background: white; |
| border: 1px solid rgba(20, 184, 166, 0.15); |
| border-radius: 12px; |
| box-shadow: 0 8px 24px rgba(13, 115, 119, 0.12); |
| pointer-events: all; |
| opacity: 0; |
| transform: translateX(100%); |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .notification-show { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| |
| .notification-hide { |
| opacity: 0; |
| transform: translateX(100%); |
| } |
| |
| .notification-icon { |
| flex-shrink: 0; |
| width: 20px; |
| height: 20px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .notification-success { |
| border-left: 4px solid #10b981; |
| } |
| |
| .notification-success .notification-icon { |
| color: #10b981; |
| } |
| |
| .notification-error { |
| border-left: 4px solid #ef4444; |
| } |
| |
| .notification-error .notification-icon { |
| color: #ef4444; |
| } |
| |
| .notification-warning { |
| border-left: 4px solid #f59e0b; |
| } |
| |
| .notification-warning .notification-icon { |
| color: #f59e0b; |
| } |
| |
| .notification-info { |
| border-left: 4px solid #22d3ee; |
| } |
| |
| .notification-info .notification-icon { |
| color: #22d3ee; |
| } |
| |
| .notification-content { |
| flex: 1; |
| min-width: 0; |
| } |
| |
| .notification-title { |
| font-size: 14px; |
| font-weight: 600; |
| color: #0f2926; |
| margin-bottom: 4px; |
| } |
| |
| .notification-message { |
| font-size: 13px; |
| color: #2a5f5a; |
| line-height: 1.5; |
| } |
| |
| .notification-action { |
| margin-top: 8px; |
| padding: 4px 12px; |
| background: linear-gradient(135deg, #2dd4bf, #22d3ee); |
| color: white; |
| border: none; |
| border-radius: 6px; |
| font-size: 12px; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| |
| .notification-action:hover { |
| transform: translateY(-1px); |
| box-shadow: 0 4px 12px rgba(20, 184, 166, 0.3); |
| } |
| |
| .notification-close { |
| flex-shrink: 0; |
| width: 24px; |
| height: 24px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: transparent; |
| border: none; |
| color: #6bb8ae; |
| cursor: pointer; |
| border-radius: 6px; |
| transition: all 0.2s; |
| } |
| |
| .notification-close:hover { |
| background: rgba(20, 184, 166, 0.1); |
| color: #14b8a6; |
| } |
| |
| .notification-progress { |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| height: 3px; |
| background: linear-gradient(90deg, #2dd4bf, #22d3ee); |
| animation: notificationProgress linear forwards; |
| } |
| |
| @keyframes notificationProgress { |
| from { width: 100%; } |
| to { width: 0%; } |
| } |
| |
| @media (max-width: 768px) { |
| .notification-container { |
| left: 12px; |
| right: 12px; |
| max-width: none; |
| } |
| |
| .notification { |
| width: 100%; |
| } |
| } |
| |
| [data-theme="dark"] .notification { |
| background: rgba(19, 46, 42, 0.95); |
| border-color: rgba(45, 212, 191, 0.25); |
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); |
| } |
| |
| [data-theme="dark"] .notification-title { |
| color: #f0fdfa; |
| } |
| |
| [data-theme="dark"] .notification-message { |
| color: #99f6e4; |
| } |
| |
| [data-theme="dark"] .notification-close { |
| color: #5eead4; |
| } |
| |
| [data-theme="dark"] .notification-close:hover { |
| background: rgba(45, 212, 191, 0.15); |
| color: #2dd4bf; |
| } |
| `; |
| document.head.appendChild(style); |
| } |
| } |
|
|
| |
| NotificationSystem.injectStyles(); |
| const notifications = new NotificationSystem(); |
|
|
| |
| export default notifications; |
| export { notifications }; |
|
|