Toast.svelte RawHistoryFolder
1<script lang="ts">
2 import { onMount, onDestroy } from 'svelte';
3
4 interface ToastProps {
5 id: number;
6 message: string;
7 duration?: number;
8 type?: 'info' | 'success' | 'error';
9 onDismiss: (id: number) => void;
10 }
11
12 let { id, message, duration = 2500, type = 'info', onDismiss }: ToastProps = $props();
13
14 let isExiting = $state(false);
15 let isDarkMode = $state(false);
16 let themeObserver: MutationObserver | undefined;
17 let timeoutId: number | undefined;
18
19 onMount(() => {
20 timeoutId = window.setTimeout(() => {
21 dismiss();
22 }, duration);
23
24 syncTheme();
25
26 if (typeof window !== 'undefined' && 'MutationObserver' in window) {
27 themeObserver = new MutationObserver(syncTheme);
28 themeObserver.observe(document.documentElement, {
29 attributes: true,
30 attributeFilter: ['class']
31 });
32 }
33 });
34
35 onDestroy(() => {
36 if (timeoutId) {
37 window.clearTimeout(timeoutId);
38 timeoutId = undefined;
39 }
40
41 if (themeObserver) {
42 themeObserver.disconnect();
43 themeObserver = undefined;
44 }
45 });
46
47 function dismiss() {
48 if (timeoutId) {
49 window.clearTimeout(timeoutId);
50 timeoutId = undefined;
51 }
52 isExiting = true;
53 // Allow time for exit animation before removing from parent
54 setTimeout(() => {
55 onDismiss(id);
56 }, 250);
57 }
58
59 function handleClick() {
60 dismiss();
61 }
62
63 function syncTheme() {
64 if (typeof document === 'undefined') {
65 return;
66 }
67
68 const prefersDark = document.documentElement.classList.contains('dark');
69
70 if (prefersDark) {
71 isDarkMode = true;
72 return;
73 }
74
75 try {
76 isDarkMode = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
77 } catch {
78 isDarkMode = false;
79 }
80 }
81</script>
82
83<button
84 class={`toast ${isDarkMode ? 'toast-dark' : 'toast-light'} toast-${type} ${isExiting ? 'toast-exit' : 'toast-enter'}`}
85 onclick={handleClick}
86 data-toast-id={id}
87 type="button"
88 aria-label="Dismiss notification"
89 title="Click to dismiss"
90>
91 <div class="toast-message">
92 {message || 'Notification'}
93 </div>
94</button>
95
96<style>
97 .toast {
98 background-color: var(--dynamic-surface);
99 border: 1px solid var(--dynamic-border);
100 border-radius: 0;
101 box-shadow: 0 8px 12px rgba(0, 0, 0, 0.08);
102 cursor: pointer;
103 display: inline-flex;
104 margin: 0;
105 pointer-events: auto;
106 padding: 0;
107 transition: transform 0.2s ease, opacity 0.2s ease;
108 }
109
110 .toast-enter {
111 opacity: 1;
112 transform: translateY(0);
113 }
114
115 .toast-exit {
116 opacity: 0;
117 transform: translateY(-16px);
118 }
119
120 .toast-message {
121 color: var(--dynamic-text);
122 font-size: 0.875rem;
123 font-weight: 600;
124 min-width: 9rem;
125 padding: 0.75rem 1rem;
126 text-align: center;
127 }
128
129 .toast-light.toast-info {
130 background-color: rgb(239 246 255);
131 border-color: rgb(191 219 254);
132 }
133
134 .toast-dark.toast-info {
135 background-color: rgb(59 130 246 / 0.2);
136 border-color: rgb(59 130 246);
137 }
138
139 .toast-light.toast-success {
140 background-color: rgb(240 253 244);
141 border-color: rgb(187 247 208);
142 }
143
144 .toast-dark.toast-success {
145 background-color: rgb(22 163 74 / 0.2);
146 border-color: rgb(22 163 74);
147 }
148
149 .toast-light.toast-error {
150 background-color: rgb(254 242 242);
151 border-color: rgb(254 202 202);
152 }
153
154 .toast-dark.toast-error {
155 background-color: rgb(220 38 38 / 0.2);
156 border-color: rgb(220 38 38);
157 }
158
159 .toast-light.toast-info .toast-message {
160 color: rgb(59 130 246);
161 }
162
163 .toast-dark.toast-info .toast-message {
164 color: rgb(191 219 254);
165 }
166
167 .toast-light.toast-success .toast-message {
168 color: rgb(22 163 74);
169 }
170
171 .toast-dark.toast-success .toast-message {
172 color: rgb(187 247 208);
173 }
174
175 .toast-light.toast-error .toast-message {
176 color: rgb(220 38 38);
177 }
178
179 .toast-dark.toast-error .toast-message {
180 color: rgb(254 202 202);
181 }
182</style>