Spaces:
Sleeping
Sleeping
| /** | |
| * TXAToast - Liquid Glass Toast Notification System | |
| * Hỗ trợ đa vị trí, bypass CSP qua Trusted Types & SVG Inline | |
| */ | |
| class TXAToastSystem { | |
| constructor() { | |
| this.containers = {}; | |
| this.policy = null; | |
| this.initPolicy(); | |
| this.injectStyles(); | |
| } | |
| initPolicy() { | |
| if (typeof window !== 'undefined' && window.trustedTypes) { | |
| try { | |
| this.policy = window.trustedTypes.createPolicy('txatoast-policy', { | |
| createHTML: (string) => string | |
| }); | |
| } catch (e) { | |
| this.policy = window.trustedTypes.defaultPolicy || { createHTML: (string) => string }; | |
| } | |
| } | |
| } | |
| getContainer(position = 'top-right') { | |
| if (typeof document === 'undefined') return null; | |
| const id = `txatoast-container-${position}`; | |
| let container = document.getElementById(id); | |
| if (!container) { | |
| container = document.createElement('div'); | |
| container.id = id; | |
| const posStyles = { | |
| 'top-right': 'top: 24px; right: 24px;', | |
| 'top-left': 'top: 24px; left: 24px;', | |
| 'bottom-right': 'bottom: 24px; right: 24px;', | |
| 'bottom-left': 'bottom: 24px; left: 24px;', | |
| 'top-center': 'top: 24px; left: 50%; transform: translateX(-50%); align-items: center;', | |
| 'bottom-center': 'bottom: 24px; left: 50%; transform: translateX(-50%); align-items: center;' | |
| }; | |
| container.style.cssText = ` | |
| position: fixed; | |
| z-index: 2147483647; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| pointer-events: none; | |
| max-width: 380px; | |
| width: 100%; | |
| ${posStyles[position] || posStyles['top-right']} | |
| `; | |
| document.body.appendChild(container); | |
| } | |
| return container; | |
| } | |
| injectStyles() { | |
| if (typeof document === 'undefined' || document.getElementById('txatoast-styles')) return; | |
| const style = document.createElement('style'); | |
| style.id = 'txatoast-styles'; | |
| style.textContent = ` | |
| .txa-toast { | |
| pointer-events: auto; | |
| position: relative; | |
| padding: 16px 22px; | |
| border-radius: 24px; | |
| color: #ffffff; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| font-size: 14px; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 14px; | |
| overflow: hidden; | |
| background: rgba(255, 255, 255, 0.02); | |
| backdrop-filter: blur(30px); | |
| -webkit-backdrop-filter: blur(30px); | |
| border: 1px solid rgba(255, 255, 255, 0.12); | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5), inset 0 1px 2px rgba(255, 255, 255, 0.2); | |
| opacity: 0; | |
| transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| /* Position-based Animations */ | |
| [id*="-right"] .txa-toast { transform: translateX(120%) scale(0.9); } | |
| [id*="-left"] .txa-toast { transform: translateX(-120%) scale(0.9); } | |
| [id*="-center"] .txa-toast { transform: translateY(50px) scale(0.9); } | |
| [id*="top-center"] .txa-toast { transform: translateY(-50px) scale(0.9); } | |
| .txa-toast.txa-show { transform: translate(0) scale(1); opacity: 1; } | |
| .txa-toast.txa-hide { opacity: 0; scale: 0.85; } | |
| .txa-toast::before { | |
| content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; | |
| background: radial-gradient(circle, rgba(255,255,255,0.12) 0%, transparent 60%); pointer-events: none; mix-blend-mode: overlay; | |
| } | |
| .txa-toast-success { border-color: rgba(16, 185, 129, 0.4); box-shadow: 0 12px 35px rgba(16, 185, 129, 0.2); } | |
| .txa-toast-success .txa-icon { color: #10b981; } | |
| .txa-toast-error { border-color: rgba(239, 68, 68, 0.4); box-shadow: 0 12px 35px rgba(239, 68, 68, 0.2); } | |
| .txa-toast-error .txa-icon { color: #ef4444; } | |
| .txa-toast-info { border-color: rgba(59, 130, 246, 0.4); box-shadow: 0 12px 35px rgba(59, 130, 246, 0.2); } | |
| .txa-toast-info .txa-icon { color: #3b82f6; } | |
| .txa-toast-warning { border-color: rgba(245, 158, 11, 0.4); box-shadow: 0 12px 35px rgba(245, 158, 11, 0.2); } | |
| .txa-toast-warning .txa-icon { color: #f59e0b; } | |
| .txa-content-wrapper { display: flex; align-items: center; gap: 12px; flex-grow: 1; } | |
| .txa-icon { display: flex; align-items: center; justify-content: center; flex-shrink: 0; } | |
| .txa-message { line-height: 1.4; text-shadow: 0 1px 2px rgba(0,0,0,0.6); } | |
| .txa-close { | |
| background: transparent; border: none; color: rgba(255, 255, 255, 0.4); | |
| cursor: pointer; padding: 6px; display: flex; align-items: center; | |
| justify-content: center; transition: all 0.2s; border-radius: 50%; | |
| } | |
| .txa-close:hover { color: #ffffff; background: rgba(255,255,255,0.1); transform: rotate(90deg); } | |
| .txa-progress-bar { | |
| position: absolute; bottom: 0; left: 0; height: 3px; width: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent); | |
| transform-origin: left; transform: scaleX(1); | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| show(message, type = 'info', duration = 3500, position = 'top-right') { | |
| const container = this.getContainer(position); | |
| if (!container) return; | |
| const svgIcons = { | |
| success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" 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>`, | |
| error: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" 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>`, | |
| info: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" 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>`, | |
| warning: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" 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>` | |
| }; | |
| const toast = document.createElement('div'); | |
| toast.className = `txa-toast txa-toast-${type}`; | |
| const rawHTML = ` | |
| <div class="txa-content-wrapper"> | |
| <div class="txa-icon">${svgIcons[type] || svgIcons.info}</div> | |
| <div class="txa-message">${message}</div> | |
| </div> | |
| <button class="txa-close" aria-label="Close"> | |
| <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> | |
| </button> | |
| <div class="txa-progress-bar"></div> | |
| `; | |
| if (this.policy) { | |
| toast.innerHTML = this.policy.createHTML(rawHTML); | |
| } else { | |
| toast.innerHTML = rawHTML; | |
| } | |
| container.appendChild(toast); | |
| setTimeout(() => { toast.classList.add('txa-show'); }, 10); | |
| const progressBar = toast.querySelector('.txa-progress-bar'); | |
| if (progressBar) { | |
| progressBar.style.transition = `transform ${duration}ms linear`; | |
| progressBar.style.transform = 'scaleX(0)'; | |
| } | |
| const closeToast = () => { | |
| toast.classList.remove('txa-show'); | |
| toast.classList.add('txa-hide'); | |
| setTimeout(() => { toast.remove(); }, 500); | |
| }; | |
| toast.querySelector('.txa-close').onclick = closeToast; | |
| setTimeout(closeToast, duration); | |
| } | |
| success(msg, duration, pos) { this.show(msg, 'success', duration, pos); } | |
| error(msg, duration, pos) { this.show(msg, 'error', duration, pos); } | |
| info(msg, duration, pos) { this.show(msg, 'info', duration, pos); } | |
| warning(msg, duration, pos) { this.show(msg, 'warning', duration, pos); } | |
| } | |
| const txatoastInstance = new TXAToastSystem(); | |
| if (typeof window !== 'undefined') { | |
| window.txatoast = txatoastInstance; | |
| document.addEventListener('astro:page-load', () => { | |
| txatoastInstance.injectStyles(); | |
| }); | |
| } |