1// Toast service - unified toast notifications 2// Usage: showToast("Message", "success") or import { showToast } from "services/toast" 3 4const icons = { 5 success: '<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>', 6 error: '<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>', 7 warning: '<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>', 8 info: '<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>' 9} 10 11const styles = { 12 success: 'bg-white dark:bg-gray-800 border-green-200 dark:border-green-800', 13 error: 'bg-white dark:bg-gray-800 border-red-200 dark:border-red-800', 14 warning: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600', 15 info: 'bg-white dark:bg-gray-800 border-blue-200 dark:border-blue-800' 16} 17 18export function showToast(message, type = 'success', duration = 4000, { icon = null } = {}) { 19 // Check if a dialog is open (dialogs use browser's top layer, ignoring all z-index) 20 // If so, render toast inside the dialog so it's visible above the modal 21 const openDialog = document.querySelector('dialog[open]') 22 let container 23 24 if (openDialog) { 25 // Get or create toast container inside the dialog 26 container = openDialog.querySelector('.dialog-toast-container') 27 if (!container) { 28 container = document.createElement('div') 29 container.className = 'dialog-toast-container fixed top-4 right-4 flex flex-col gap-2 pointer-events-none' 30 openDialog.appendChild(container) 31 } 32 } else { 33 container = document.getElementById('toast-container') 34 } 35 36 if (!container) return 37 38 const toast = document.createElement('div') 39 toast.className = `flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg pointer-events-auto transform transition-all duration-300 translate-x-full opacity-0 ${styles[type] || styles.info}` 40 toast.innerHTML = ` 41 ${icon || icons[type] || icons.info} 42 <span class="text-sm font-medium text-gray-900 dark:text-gray-100">${message}</span> 43 <button onclick="this.parentElement.remove()" class="ml-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"> 44 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> 45 </button> 46 ` 47 48 container.appendChild(toast) 49 requestAnimationFrame(() => { 50 toast.classList.remove('translate-x-full', 'opacity-0') 51 }) 52 53 setTimeout(() => { 54 toast.classList.add('translate-x-full', 'opacity-0') 55 setTimeout(() => toast.remove(), 300) 56 }, duration) 57} 58 59// Linked toast - clickable, navigates to URL on click 60export function showLinkedToast(message, type = 'info', url = null, duration = 8000, { image = null } = {}) { 61 const openDialog = document.querySelector('dialog[open]') 62 let container 63 64 if (openDialog) { 65 container = openDialog.querySelector('.dialog-toast-container') 66 if (!container) { 67 container = document.createElement('div') 68 container.className = 'dialog-toast-container fixed top-4 right-4 flex flex-col gap-2 pointer-events-none' 69 openDialog.appendChild(container) 70 } 71 } else { 72 container = document.getElementById('toast-container') 73 } 74 75 if (!container) return 76 77 const toast = document.createElement('div') 78 toast.className = `flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg pointer-events-auto transform transition-all duration-300 translate-x-full opacity-0 ${url ? 'cursor-pointer hover:shadow-xl' : ''} ${styles[type] || styles.info}` 79 const leading = image 80 ? `<img src="${image}" alt="" class="w-9 h-9 rounded object-cover shrink-0">` 81 : (icons[type] || icons.info) 82 toast.innerHTML = ` 83 ${leading} 84 <span class="text-sm font-medium text-gray-900 dark:text-gray-100 flex-1">${message}</span> 85 ${url ? '<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>' : ''} 86 <button class="ml-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 shrink-0" data-toast-dismiss> 87 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> 88 </button> 89 ` 90 91 // Dismiss button 92 toast.querySelector('[data-toast-dismiss]').addEventListener('click', (e) => { 93 e.stopPropagation() 94 toast.classList.add('translate-x-full', 'opacity-0') 95 setTimeout(() => toast.remove(), 300) 96 }) 97 98 // Click navigates to URL 99 if (url) { 100 toast.addEventListener('click', (e) => { 101 if (e.target.closest('[data-toast-dismiss]')) return 102 window.Turbo ? window.Turbo.visit(url) : (window.location.href = url) 103 toast.remove() 104 }) 105 } 106 107 container.appendChild(toast) 108 requestAnimationFrame(() => { 109 toast.classList.remove('translate-x-full', 'opacity-0') 110 }) 111 112 setTimeout(() => { 113 if (toast.parentElement) { 114 toast.classList.add('translate-x-full', 'opacity-0') 115 setTimeout(() => toast.remove(), 300) 116 } 117 }, duration) 118 119 return toast 120} 121 122// Attach to window for inline script usage (flash messages) 123window.showToast = showToast 124window.showLinkedToast = showLinkedToast 125 126export default { showToast, showLinkedToast };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.