|
| 1 | +<script> |
| 2 | + import { onMount, onDestroy } from 'svelte'; |
| 3 | + import { onToastChange, getToasts, removeToast } from '../../lib/toast.js'; |
| 4 | +
|
| 5 | + let toasts = $state(getToasts()); |
| 6 | + let unsubscribe = null; |
| 7 | +
|
| 8 | + onMount(() => { |
| 9 | + unsubscribe = onToastChange((list) => { toasts = list; }); |
| 10 | + }); |
| 11 | + onDestroy(() => { if (unsubscribe) unsubscribe(); }); |
| 12 | +
|
| 13 | + const icons = { |
| 14 | + success: 'M4.5 12.75l6 6 9-13.5', |
| 15 | + error: 'M6 18L18 6M6 6l12 12', |
| 16 | + warning: 'M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z', |
| 17 | + info: 'M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z', |
| 18 | + }; |
| 19 | +
|
| 20 | + const styles = { |
| 21 | + success: { bg: 'bg-emerald-50', border: 'border-emerald-200', icon: 'text-emerald-500', text: 'text-emerald-800' }, |
| 22 | + error: { bg: 'bg-red-50', border: 'border-red-200', icon: 'text-red-500', text: 'text-red-800' }, |
| 23 | + warning: { bg: 'bg-amber-50', border: 'border-amber-200', icon: 'text-amber-500', text: 'text-amber-800' }, |
| 24 | + info: { bg: 'bg-blue-50', border: 'border-blue-200', icon: 'text-blue-500', text: 'text-blue-800' }, |
| 25 | + }; |
| 26 | +</script> |
| 27 | + |
| 28 | +{#if toasts.length > 0} |
| 29 | + <div class="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none" style="max-width: 380px;"> |
| 30 | + {#each toasts as t (t.id)} |
| 31 | + {@const s = styles[t.type] || styles.info} |
| 32 | + <div class="pointer-events-auto flex items-start gap-2.5 px-4 py-3 rounded-xl border shadow-lg backdrop-blur-sm animate-toast-in {s.bg} {s.border}"> |
| 33 | + <svg class="w-4 h-4 flex-shrink-0 mt-0.5 {s.icon}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> |
| 34 | + <path stroke-linecap="round" stroke-linejoin="round" d={icons[t.type] || icons.info} /> |
| 35 | + </svg> |
| 36 | + <span class="flex-1 text-[12px] leading-relaxed {s.text}">{t.message}</span> |
| 37 | + <button |
| 38 | + class="flex-shrink-0 p-0.5 rounded hover:bg-black/5 transition-colors cursor-pointer {s.text} opacity-50 hover:opacity-100" |
| 39 | + onclick={() => removeToast(t.id)} |
| 40 | + aria-label="Close" |
| 41 | + > |
| 42 | + <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> |
| 43 | + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> |
| 44 | + </svg> |
| 45 | + </button> |
| 46 | + </div> |
| 47 | + {/each} |
| 48 | + </div> |
| 49 | +{/if} |
| 50 | + |
| 51 | +<style> |
| 52 | + @keyframes toastIn { |
| 53 | + from { opacity: 0; transform: translateX(20px); } |
| 54 | + to { opacity: 1; transform: translateX(0); } |
| 55 | + } |
| 56 | + :global(.animate-toast-in) { |
| 57 | + animation: toastIn 0.25s ease-out; |
| 58 | + } |
| 59 | +</style> |
0 commit comments