@delightstack/components 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,967 @@
1
+ <script lang="ts" module>
2
+ import { DelightError } from '@delightstack/utilities';
3
+
4
+ type Position =
5
+ | 'top-left'
6
+ | 'top-center'
7
+ | 'top-right'
8
+ | 'bottom-left'
9
+ | 'bottom-center'
10
+ | 'bottom-right';
11
+
12
+ export interface ToastOptions {
13
+ /** Optional secondary line shown beneath the title. */
14
+ description?: string;
15
+ /** Auto-dismiss delay in milliseconds (overrides the Toaster default) */
16
+ duration?: number;
17
+ /** Whether the toast shows a close button */
18
+ dismissible?: boolean;
19
+ /** Style the toast as a success message */
20
+ success?: boolean;
21
+ /** Style the toast as a warning message */
22
+ warning?: boolean;
23
+ /** Style the toast as an error message */
24
+ error?: boolean;
25
+ /** Style the toast as an informational message */
26
+ info?: boolean;
27
+ /** An action button shown in the toast */
28
+ action?: { label: string; onclick: () => void };
29
+ /** Whether the toast stays until manually dismissed (no auto-dismiss) */
30
+ persistent?: boolean;
31
+ /** Custom toast id — reusing an id updates the existing toast in place */
32
+ id?: string;
33
+ }
34
+
35
+ type Variant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'loading';
36
+
37
+ interface ToastEntry {
38
+ id: string;
39
+ message: string;
40
+ description?: string;
41
+ variant: Variant;
42
+ options: ToastOptions;
43
+ created_at: number;
44
+ duration: number;
45
+ remaining: number;
46
+ dismissed: boolean;
47
+ height: number;
48
+ }
49
+
50
+ let toasts = $state<ToastEntry[]>([]);
51
+ let counter = 0;
52
+
53
+ // Default auto-dismiss duration. Kept in sync with the primary <Toaster>'s
54
+ // `duration` prop (see the election effect below) so the prop actually works.
55
+ let default_duration = 4000;
56
+
57
+ // Single-instance election. Every <Toaster /> shares this one `toasts` store,
58
+ // so only the first-mounted instance ("primary") may render the stack and run
59
+ // the timers. Mounting <Toaster /> more than once (e.g. one per docs demo)
60
+ // then never duplicates the UI or multiplies the auto-dismiss countdown.
61
+ // `registered` is a plain (non-reactive) list used only to elect the next
62
+ // primary on unmount; `primary_token` is the reactive bit that flips renders.
63
+ let registered: number[] = [];
64
+ let primary_token = $state<number | null>(null);
65
+ let election_counter = 0;
66
+
67
+ function generateId(): string {
68
+ return `toast-${++counter}-${Date.now()}`;
69
+ }
70
+
71
+ function variantFromOptions(options?: ToastOptions): Variant {
72
+ if (options?.error) return 'error';
73
+ if (options?.warning) return 'warning';
74
+ if (options?.success) return 'success';
75
+ if (options?.info) return 'info';
76
+ return 'default';
77
+ }
78
+
79
+ function addToast(message: string, variant: Variant, options?: ToastOptions): string {
80
+ const id = options?.id ?? generateId();
81
+ const base_duration = options?.duration ?? default_duration;
82
+ const effective_duration = options?.action ? base_duration + 2000 : base_duration;
83
+
84
+ const existing_index = toasts.findIndex((t) => t.id === id);
85
+ if (existing_index !== -1) {
86
+ toasts[existing_index].message = message;
87
+ toasts[existing_index].description =
88
+ options?.description ?? toasts[existing_index].description;
89
+ toasts[existing_index].variant = variant;
90
+ toasts[existing_index].options = { ...toasts[existing_index].options, ...options };
91
+ toasts[existing_index].duration = effective_duration;
92
+ toasts[existing_index].remaining = effective_duration;
93
+ toasts[existing_index].dismissed = false;
94
+ return id;
95
+ }
96
+
97
+ const entry: ToastEntry = {
98
+ id,
99
+ message,
100
+ description: options?.description,
101
+ variant,
102
+ options: {
103
+ dismissible: true,
104
+ persistent: false,
105
+ ...options,
106
+ },
107
+ created_at: Date.now(),
108
+ duration: effective_duration,
109
+ remaining: effective_duration,
110
+ dismissed: false,
111
+ height: 0,
112
+ };
113
+
114
+ toasts.push(entry);
115
+ return id;
116
+ }
117
+
118
+ function removeToast(id: string): void {
119
+ const index = toasts.findIndex((t) => t.id === id);
120
+ if (index !== -1) {
121
+ toasts[index].dismissed = true;
122
+ }
123
+ }
124
+
125
+ /** Remove a toast immediately, skipping the standard exit animation. */
126
+ function destroyToast(id: string): void {
127
+ toasts = toasts.filter((t) => t.id !== id);
128
+ }
129
+
130
+ /** Show a toast notification. Returns the toast ID for later dismissal. */
131
+ export function toast(message: string, options?: ToastOptions): string {
132
+ return addToast(message, variantFromOptions(options), options);
133
+ }
134
+
135
+ toast.success = function success(message: string, options?: ToastOptions): string {
136
+ return addToast(message, 'success', { ...options, success: true });
137
+ };
138
+
139
+ toast.error = function error(message: string, options?: ToastOptions): string {
140
+ return addToast(message, 'error', { ...options, error: true });
141
+ };
142
+
143
+ toast.warning = function warning(message: string, options?: ToastOptions): string {
144
+ return addToast(message, 'warning', { ...options, warning: true });
145
+ };
146
+
147
+ toast.info = function info(message: string, options?: ToastOptions): string {
148
+ return addToast(message, 'info', { ...options, info: true });
149
+ };
150
+
151
+ toast.loading = function loading(message: string, options?: ToastOptions): string {
152
+ return addToast(message, 'loading', { ...options, persistent: true });
153
+ };
154
+
155
+ toast.promise = async function promise<T>(
156
+ p: Promise<T>,
157
+ messages: {
158
+ loading: string;
159
+ success: string | ((result: T) => string);
160
+ error: string | ((err: Error) => string);
161
+ },
162
+ options?: ToastOptions,
163
+ ): Promise<T> {
164
+ const id = options?.id ?? generateId();
165
+ addToast(messages.loading, 'loading', { ...options, id, persistent: true });
166
+
167
+ try {
168
+ const result = await p;
169
+ const msg =
170
+ typeof messages.success === 'function'
171
+ ? messages.success(result)
172
+ : messages.success;
173
+ addToast(msg, 'success', { ...options, id, persistent: false, success: true });
174
+ return result;
175
+ } catch (err) {
176
+ const msg =
177
+ typeof messages.error === 'function'
178
+ ? messages.error(err instanceof Error ? err : new DelightError(String(err)))
179
+ : messages.error;
180
+ addToast(msg, 'error', { ...options, id, persistent: false, error: true });
181
+ throw err;
182
+ }
183
+ };
184
+
185
+ toast.dismiss = function dismiss(id?: string): void {
186
+ if (id) {
187
+ removeToast(id);
188
+ } else {
189
+ for (const t of toasts) t.dismissed = true;
190
+ }
191
+ };
192
+ </script>
193
+
194
+ <script lang="ts">
195
+ import { onMount, onDestroy } from 'svelte';
196
+ import { portal } from '../actions/Portal.svelte';
197
+ import Button from '../actions/Button.svelte';
198
+ import Progress from './Progress.svelte';
199
+
200
+ const propId = $props.id();
201
+ let {
202
+ /** Where toasts appear on the screen */
203
+ position = 'bottom-right' as Position,
204
+
205
+ /** Maximum number of visible toasts before the rest are queued behind */
206
+ max_visible = 3,
207
+
208
+ /** Gap between toasts when the stack is expanded (px) */
209
+ gap = 14,
210
+
211
+ /** Toast width in pixels */
212
+ width = 356,
213
+
214
+ /** Default auto-dismiss duration in milliseconds */
215
+ duration = 4000,
216
+
217
+ /** Use saturated, variant-colored toast backgrounds (sonner "rich colors"). */
218
+ rich_colors = false,
219
+
220
+ /** Element ID */
221
+ id = propId,
222
+
223
+ /** Additional CSS classes */
224
+ class: class_name = '',
225
+ } = $props();
226
+
227
+ // --- Single-instance election --------------------------------------------
228
+ // Register/deregister in lifecycle hooks (not an $effect) so we never read and
229
+ // write the same reactive value during tracking — that would self-invalidate
230
+ // and loop forever. Only `primary_token` is reactive, so flipping it re-renders.
231
+ const my_token = ++election_counter;
232
+ onMount(() => {
233
+ registered.push(my_token);
234
+ if (primary_token === null) primary_token = my_token;
235
+ });
236
+ onDestroy(() => {
237
+ clearTimeout(collapse_timer);
238
+ const i = registered.indexOf(my_token);
239
+ if (i !== -1) registered.splice(i, 1);
240
+ if (primary_token === my_token) primary_token = registered[0] ?? null;
241
+ });
242
+ const is_primary = $derived(primary_token === my_token);
243
+
244
+ // Keep the shared default duration in sync with the primary Toaster.
245
+ $effect(() => {
246
+ if (is_primary) default_duration = duration;
247
+ });
248
+
249
+ let expanded = $state(false);
250
+ let collapse_timer: ReturnType<typeof setTimeout> | undefined;
251
+ let toaster_el: HTMLDivElement | undefined = $state();
252
+
253
+ const is_top = $derived(position.startsWith('top'));
254
+ const is_center = $derived(position.endsWith('center'));
255
+ const align = $derived(
256
+ position.endsWith('left') ? 'left' : position.endsWith('right') ? 'right' : 'center',
257
+ );
258
+
259
+ // Active (non-dismissed) toasts, newest last. The newest is the "front".
260
+ const active_toasts = $derived(toasts.filter((t) => !t.dismissed));
261
+ // Visible set: the most recent `max_visible`. Render dismissed ones too so
262
+ // their exit animation can play.
263
+ const rendered = $derived.by(() => {
264
+ const recent = active_toasts.slice(-max_visible);
265
+ const recentIds = new Set(recent.map((t) => t.id));
266
+ return toasts.filter((t) => recentIds.has(t.id) || t.dismissed);
267
+ });
268
+
269
+ // 0 = front, 1 = one behind, etc. Dismissed toasts keep their last position.
270
+ function frontDistance(t: ToastEntry): number {
271
+ const idx = active_toasts.indexOf(t);
272
+ if (idx === -1) return 0;
273
+ return active_toasts.length - 1 - idx;
274
+ }
275
+
276
+ // Cleanup fully dismissed toasts after their exit animation.
277
+ $effect(() => {
278
+ if (!is_primary) return;
279
+ const hasDismissed = toasts.some((t) => t.dismissed);
280
+ if (hasDismissed) {
281
+ const timeout = setTimeout(() => {
282
+ toasts = toasts.filter((t) => !t.dismissed);
283
+ }, 320);
284
+ return () => clearTimeout(timeout);
285
+ }
286
+ });
287
+
288
+ // Auto-dismiss countdown. Runs only on the primary instance (so multiple
289
+ // mounted Toasters never multiply the rate) and pauses while the stack is
290
+ // hovered or a toast is being dragged.
291
+ $effect(() => {
292
+ if (!is_primary) return;
293
+ if (active_toasts.length === 0) return;
294
+ let raf_id: number;
295
+ let last_time = performance.now();
296
+ function tick(now: number) {
297
+ const delta = now - last_time;
298
+ last_time = now;
299
+ const paused = expanded || swipe_id !== null;
300
+ if (!paused) {
301
+ for (const t of toasts) {
302
+ if (t.dismissed || t.options.persistent || t.variant === 'loading') continue;
303
+ t.remaining -= delta;
304
+ if (t.remaining <= 0) t.dismissed = true;
305
+ }
306
+ }
307
+ raf_id = requestAnimationFrame(tick);
308
+ }
309
+ raf_id = requestAnimationFrame(tick);
310
+ return () => cancelAnimationFrame(raf_id);
311
+ });
312
+
313
+ // Escape dismisses the front toast.
314
+ $effect(() => {
315
+ if (!is_primary) return;
316
+ function onKeydown(e: KeyboardEvent) {
317
+ if (e.key === 'Escape') {
318
+ const active = active_toasts.filter((t) => t.options.dismissible !== false);
319
+ if (active.length > 0) active[active.length - 1].dismissed = true;
320
+ }
321
+ }
322
+ document.addEventListener('keydown', onKeydown);
323
+ return () => document.removeEventListener('keydown', onKeydown);
324
+ });
325
+
326
+ function getRole(t: ToastEntry): string {
327
+ return t.variant === 'warning' || t.variant === 'error' ? 'alert' : 'status';
328
+ }
329
+
330
+ function measure(node: HTMLElement, t: ToastEntry) {
331
+ const update = () => {
332
+ const h = node.offsetHeight;
333
+ if (h && t.height !== h) t.height = h;
334
+ };
335
+ update();
336
+ const ro = new ResizeObserver(update);
337
+ ro.observe(node);
338
+ return { destroy: () => ro.disconnect() };
339
+ }
340
+
341
+ // Cumulative offset for the expanded stack: sum of heights+gap of the toasts
342
+ // in front of this one (the newer toasts between it and the anchor edge).
343
+ function expandedOffset(t: ToastEntry): number {
344
+ let offset = 0;
345
+ const idx = active_toasts.indexOf(t);
346
+ if (idx === -1) return 0;
347
+ for (let i = idx + 1; i < active_toasts.length; i++) {
348
+ offset += (active_toasts[i].height || 64) + gap;
349
+ }
350
+ return offset;
351
+ }
352
+
353
+ // --- Swipe-to-dismiss (pointer based — mouse + touch, like sonner) --------
354
+ let swipe_id = $state<string | null>(null);
355
+ let swipe_delta = $state(0);
356
+ let swipe_settling = $state(false);
357
+ let swipe_settle_ms = $state(300);
358
+ let swipe_ease = $state('cubic-bezier(0.22, 1, 0.36, 1)');
359
+ let swipe_axis: 'X' | 'Y' = 'X';
360
+ let swipe_start = 0;
361
+ let last_pos = 0;
362
+ let last_time = 0;
363
+ let velocity = 0;
364
+ let settle_timer: ReturnType<typeof setTimeout> | undefined;
365
+
366
+ function onPointerDown(e: PointerEvent, t: ToastEntry) {
367
+ if (t.options.dismissible === false || swipe_settling) return;
368
+ // Don't start a drag from interactive children (close / action buttons).
369
+ if ((e.target as HTMLElement).closest('button, a')) return;
370
+ // Only start from the toast card itself, not its surrounding hover-halo.
371
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
372
+ if (
373
+ e.clientX < rect.left ||
374
+ e.clientX > rect.right ||
375
+ e.clientY < rect.top ||
376
+ e.clientY > rect.bottom
377
+ )
378
+ return;
379
+ // Horizontal stacks swipe left/right; centered stacks swipe up/down.
380
+ // Either direction dismisses (more forgiving than edge-only).
381
+ swipe_axis = is_center ? 'Y' : 'X';
382
+ swipe_id = t.id;
383
+ swipe_delta = 0;
384
+ swipe_settling = false;
385
+ swipe_start = swipe_axis === 'Y' ? e.clientY : e.clientX;
386
+ last_pos = swipe_start;
387
+ last_time = performance.now();
388
+ velocity = 0;
389
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
390
+ }
391
+
392
+ function onPointerMove(e: PointerEvent) {
393
+ if (!swipe_id || swipe_settling) return;
394
+ const pos = swipe_axis === 'Y' ? e.clientY : e.clientX;
395
+ const now = performance.now();
396
+ const dt = now - last_time;
397
+ if (dt > 0) velocity = (pos - last_pos) / dt; // signed px/ms
398
+ last_pos = pos;
399
+ last_time = now;
400
+ swipe_delta = pos - swipe_start; // bidirectional, no rubber-banding
401
+ }
402
+
403
+ function onPointerUp(e: PointerEvent) {
404
+ if (!swipe_id) return;
405
+ const id = swipe_id;
406
+ try {
407
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
408
+ } catch {
409
+ /* pointer already released */
410
+ }
411
+
412
+ const threshold = swipe_axis === 'Y' ? 40 : width * 0.25;
413
+ const dismiss = Math.abs(swipe_delta) > threshold || Math.abs(velocity) > 0.45;
414
+
415
+ // A pure tap (no movement) needs no settle animation.
416
+ if (!dismiss && swipe_delta === 0) {
417
+ swipe_id = null;
418
+ return;
419
+ }
420
+
421
+ swipe_settling = true;
422
+ clearTimeout(settle_timer);
423
+
424
+ if (dismiss) {
425
+ // Throw it off-screen in the swipe direction, continuing its momentum.
426
+ const dir = swipe_delta !== 0 ? Math.sign(swipe_delta) : Math.sign(velocity) || 1;
427
+ const fly = (swipe_axis === 'Y' ? 240 : width * 1.4) * dir;
428
+ const speed = Math.max(Math.abs(velocity), 0.6); // px/ms
429
+ const remaining = Math.max(0, Math.abs(fly - swipe_delta));
430
+ swipe_settle_ms = Math.min(380, Math.max(140, remaining / speed));
431
+ swipe_ease = 'cubic-bezier(0.32, 0.72, 0, 1)';
432
+ swipe_delta = fly;
433
+ settle_timer = setTimeout(() => {
434
+ destroyToast(id);
435
+ swipe_id = null;
436
+ swipe_settling = false;
437
+ }, swipe_settle_ms);
438
+ } else {
439
+ // Cancelled — spring back into place.
440
+ swipe_settle_ms = 320;
441
+ swipe_ease = 'cubic-bezier(0.34, 1.4, 0.5, 1)';
442
+ swipe_delta = 0;
443
+ settle_timer = setTimeout(() => {
444
+ swipe_id = null;
445
+ swipe_settling = false;
446
+ }, swipe_settle_ms);
447
+ }
448
+ }
449
+
450
+ // Full transform for a toast: stack position + (optional) live swipe offset.
451
+ function toastStyle(t: ToastEntry): string {
452
+ const dist = frontDistance(t);
453
+ const dir = is_top ? 1 : -1;
454
+ let ty: number;
455
+ let scale: number;
456
+ let opacity: number;
457
+ if (expanded) {
458
+ ty = expandedOffset(t) * dir;
459
+ scale = 1;
460
+ opacity = 1;
461
+ } else {
462
+ ty = 16 * dist * dir;
463
+ scale = Math.max(0.9, 1 - dist * 0.06);
464
+ opacity = dist >= max_visible ? 0 : 1;
465
+ }
466
+ const z = 1000 - dist;
467
+
468
+ if (swipe_id === t.id) {
469
+ const fade_dim = swipe_axis === 'Y' ? 120 : width * 0.6;
470
+ opacity *= Math.max(0, 1 - Math.abs(swipe_delta) / fade_dim);
471
+ const sx = swipe_axis === 'X' ? swipe_delta : 0;
472
+ const sy = swipe_axis === 'Y' ? swipe_delta : 0;
473
+ const transition = swipe_settling
474
+ ? `transform ${swipe_settle_ms}ms ${swipe_ease}, opacity ${swipe_settle_ms}ms ease`
475
+ : 'transform 0s, opacity 0s';
476
+ return `transform: translate(${sx}px, ${sy}px) translateY(${ty}px) scale(${scale}); opacity: ${opacity}; z-index: ${z}; transition: ${transition};`;
477
+ }
478
+
479
+ return `transform: translateY(${ty}px) scale(${scale}); opacity: ${opacity}; z-index: ${z};`;
480
+ }
481
+ </script>
482
+
483
+ {#if is_primary && rendered.length > 0}
484
+ <div
485
+ class={['toaster', position, `align-${align}`, class_name].filter(Boolean).join(' ')}
486
+ class:expanded
487
+ class:is-top={is_top}
488
+ class:rich={rich_colors}
489
+ style:--toast-width="{width}px"
490
+ style:--toast-gap="{gap}px"
491
+ {id}
492
+ use:portal={'body'}
493
+ bind:this={toaster_el}
494
+ role="region"
495
+ aria-label="Notifications"
496
+ onmouseenter={() => {
497
+ clearTimeout(collapse_timer);
498
+ expanded = true;
499
+ }}
500
+ onmouseleave={() => {
501
+ if (swipe_id) return;
502
+ // Grace period before collapsing. Dismissing a toast removes the one
503
+ // under the cursor (it stops capturing pointer events), which fires
504
+ // mouseleave — without this delay the stack would snap shut between
505
+ // clicks, forcing a re-hover to dismiss the next one. Moving to the
506
+ // next toast within the window cancels the collapse.
507
+ clearTimeout(collapse_timer);
508
+ collapse_timer = setTimeout(() => (expanded = false), 500);
509
+ }}>
510
+ {#each rendered as t (t.id)}
511
+ <div
512
+ class="toast"
513
+ class:success={t.variant === 'success'}
514
+ class:error={t.variant === 'error'}
515
+ class:warning={t.variant === 'warning'}
516
+ class:info={t.variant === 'info'}
517
+ class:loading={t.variant === 'loading'}
518
+ class:dismissed={t.dismissed}
519
+ class:front={frontDistance(t) === 0}
520
+ role={getRole(t)}
521
+ aria-live={t.variant === 'warning' || t.variant === 'error'
522
+ ? 'assertive'
523
+ : 'polite'}
524
+ use:measure={t}
525
+ style={toastStyle(t)}
526
+ style:touch-action={is_center ? 'pan-x' : 'pan-y'}
527
+ onpointerdown={(e) => onPointerDown(e, t)}
528
+ onpointermove={onPointerMove}
529
+ onpointerup={onPointerUp}
530
+ onpointercancel={onPointerUp}>
531
+ <div class="inner">
532
+ <span class="icon">
533
+ {#if t.variant === 'success'}
534
+ <svg
535
+ viewBox="0 0 24 24"
536
+ width="20"
537
+ height="20"
538
+ fill="none"
539
+ stroke="currentColor"
540
+ stroke-width="2"
541
+ stroke-linecap="round"
542
+ stroke-linejoin="round">
543
+ <path d="M20 6L9 17l-5-5" />
544
+ </svg>
545
+ {:else if t.variant === 'error'}
546
+ <svg
547
+ viewBox="0 0 24 24"
548
+ width="20"
549
+ height="20"
550
+ fill="none"
551
+ stroke="currentColor"
552
+ stroke-width="2"
553
+ stroke-linecap="round"
554
+ stroke-linejoin="round">
555
+ <circle cx="12" cy="12" r="10" />
556
+ <line x1="15" y1="9" x2="9" y2="15" />
557
+ <line x1="9" y1="9" x2="15" y2="15" />
558
+ </svg>
559
+ {:else if t.variant === 'warning'}
560
+ <svg
561
+ viewBox="0 0 24 24"
562
+ width="20"
563
+ height="20"
564
+ fill="none"
565
+ stroke="currentColor"
566
+ stroke-width="2"
567
+ stroke-linecap="round"
568
+ stroke-linejoin="round">
569
+ <path
570
+ d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
571
+ <line x1="12" y1="9" x2="12" y2="13" />
572
+ <line x1="12" y1="17" x2="12.01" y2="17" />
573
+ </svg>
574
+ {:else if t.variant === 'loading'}
575
+ <Progress size="00" color="currentColor" />
576
+ {:else}
577
+ <svg
578
+ viewBox="0 0 24 24"
579
+ width="20"
580
+ height="20"
581
+ fill="none"
582
+ stroke="currentColor"
583
+ stroke-width="2"
584
+ stroke-linecap="round"
585
+ stroke-linejoin="round">
586
+ <circle cx="12" cy="12" r="10" />
587
+ <line x1="12" y1="16" x2="12" y2="12" />
588
+ <line x1="12" y1="8" x2="12.01" y2="8" />
589
+ </svg>
590
+ {/if}
591
+ </span>
592
+
593
+ <div class="content">
594
+ <div class="title">{t.message}</div>
595
+ {#if t.description}
596
+ <div class="description">{t.description}</div>
597
+ {/if}
598
+ </div>
599
+
600
+ {#if t.options.action}
601
+ <div class="action">
602
+ <Button
603
+ dense
604
+ size="0"
605
+ onclick={() => {
606
+ t.options.action?.onclick();
607
+ removeToast(t.id);
608
+ }}>
609
+ {t.options.action.label}
610
+ </Button>
611
+ </div>
612
+ {/if}
613
+ </div>
614
+
615
+ {#if t.options.dismissible !== false}
616
+ <div class="close">
617
+ <Button
618
+ icon
619
+ size="0"
620
+ transparent
621
+ dense
622
+ aria-label="Dismiss notification"
623
+ onclick={() => removeToast(t.id)}>
624
+ <svg
625
+ viewBox="0 0 24 24"
626
+ fill="none"
627
+ stroke="currentColor"
628
+ stroke-width="2.2"
629
+ stroke-linecap="round"
630
+ stroke-linejoin="round">
631
+ <line x1="18" y1="6" x2="6" y2="18" />
632
+ <line x1="6" y1="6" x2="18" y2="18" />
633
+ </svg>
634
+ </Button>
635
+ </div>
636
+ {/if}
637
+ </div>
638
+ {/each}
639
+ </div>
640
+ {/if}
641
+
642
+ <style>
643
+ .toaster {
644
+ position: fixed;
645
+ z-index: var(--layer-toast, 600);
646
+ width: var(--toast-width, 356px);
647
+ max-width: calc(100vw - 2rem);
648
+ pointer-events: none;
649
+ --toast-bg: light-dark(#fff, #1c1c1f);
650
+ --toast-fg: light-dark(#18181b, #f4f4f5);
651
+ --toast-border: light-dark(rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.1));
652
+
653
+ &.bottom-right {
654
+ bottom: 1rem;
655
+ right: 1rem;
656
+ }
657
+ &.bottom-left {
658
+ bottom: 1rem;
659
+ left: 1rem;
660
+ }
661
+ &.bottom-center {
662
+ bottom: 1rem;
663
+ left: 50%;
664
+ transform: translateX(-50%);
665
+ }
666
+ &.top-right {
667
+ top: 1rem;
668
+ right: 1rem;
669
+ }
670
+ &.top-left {
671
+ top: 1rem;
672
+ left: 1rem;
673
+ }
674
+ &.top-center {
675
+ top: 1rem;
676
+ left: 50%;
677
+ transform: translateX(-50%);
678
+ }
679
+ }
680
+
681
+ /* Each toast is absolutely positioned within the toaster and offset by JS
682
+ * (collapsed stack vs. expanded list), giving the sonner pile-up effect. */
683
+ .toast {
684
+ position: absolute;
685
+ left: 0;
686
+ right: 0;
687
+ pointer-events: auto;
688
+ width: 100%;
689
+ border-radius: var(--radius-lg, 12px);
690
+ @supports (corner-shape: squircle) {
691
+ corner-shape: squircle;
692
+ border-radius: calc(var(--radius-lg, 12px) * var(--squircle-ratio, 2));
693
+ }
694
+ background-color: var(--toast-bg);
695
+ color: var(--toast-fg);
696
+ border: 1px solid var(--toast-border);
697
+ box-shadow:
698
+ 0 4px 12px rgb(0 0 0 / 0.1),
699
+ 0 2px 4px rgb(0 0 0 / 0.06);
700
+ cursor: default;
701
+ /* Stacked <-> expanded reflow: fast + strong ease-out (quintOut). */
702
+ transition:
703
+ transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
704
+ opacity 300ms ease,
705
+ box-shadow 250ms ease;
706
+ /* `backwards` (not `both`): the enter keyframe only fills BEFORE the run,
707
+ * so once it finishes it releases the transform back to the base value and
708
+ * the stacked<->expanded transition can animate it. A lingering `both`
709
+ * fill would keep overriding transform and make expand/collapse jump. */
710
+ animation: toast-enter 350ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
711
+ transform-origin: center top;
712
+ }
713
+ .toaster:is(.bottom-right, .bottom-left, .bottom-center) .toast {
714
+ bottom: 0;
715
+ transform-origin: center bottom;
716
+ --enter-from: 100%;
717
+ }
718
+ .toaster:is(.top-right, .top-left, .top-center) .toast {
719
+ top: 0;
720
+ --enter-from: -100%;
721
+ }
722
+
723
+ /* Slightly lift the stack while expanded for a sense of depth. */
724
+ .toaster.expanded .toast {
725
+ box-shadow:
726
+ 0 8px 24px rgb(0 0 0 / 0.14),
727
+ 0 3px 8px rgb(0 0 0 / 0.08);
728
+
729
+ /* While expanded, each toast carries a transparent hit-area halo extending
730
+ * ~22px beyond it on every side (sits behind the card via z-index:-1 so it
731
+ * never blocks the buttons). Adjacent halos overlap, so (a) moving the pointer
732
+ * between fanned-out toasts keeps the stack open, and (b) you must move a
733
+ * comfortable margin past the list before it collapses back to a stack. */
734
+ &::before {
735
+ content: '';
736
+ position: absolute;
737
+ inset: calc(-1 * var(--toast-hover-pad, 22px));
738
+ z-index: -1;
739
+ }
740
+ }
741
+
742
+ .inner {
743
+ display: flex;
744
+ /* Center so a taller sibling (the action Button) doesn't leave the
745
+ * single-line title pinned to the top of the row. */
746
+ align-items: center;
747
+ gap: 0.75rem;
748
+ padding: 1rem 1rem;
749
+
750
+ /* Multi-line toasts (title + description) keep the existing pattern:
751
+ * top-align so the icon sits on the title's first line. */
752
+ &:has(.description) {
753
+ align-items: flex-start;
754
+ }
755
+ }
756
+
757
+ .icon {
758
+ display: flex;
759
+ align-items: center;
760
+ justify-content: center;
761
+ flex-shrink: 0;
762
+ width: 20px;
763
+ height: 20px;
764
+
765
+ /* Optical nudge onto the first text line — only when top-aligned. */
766
+ .inner:has(.description) & {
767
+ margin-top: 0.05rem;
768
+ }
769
+
770
+ .toast.success & {
771
+ color: var(--color-success, #16a34a);
772
+ }
773
+ .toast.error & {
774
+ color: var(--color-error, #dc2626);
775
+ }
776
+ .toast.warning & {
777
+ color: var(--color-warning, #d97706);
778
+ }
779
+ .toast.info & {
780
+ color: var(--color-action, #3b82f6);
781
+ }
782
+ .toast.loading & {
783
+ color: var(--color-action, #3b82f6);
784
+ }
785
+
786
+ /* The success check pops in with a spring scale while its stroke draws
787
+ * itself on — most visible when a promise toast's spinner flips to the
788
+ * confirmation, instead of the check just blinking into place. */
789
+ .toast.success & svg {
790
+ animation: toast-check-pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
791
+
792
+ path {
793
+ /* Dash length >= the tick's path length (~23px) so `from` hides it fully */
794
+ stroke-dasharray: 24;
795
+ animation: toast-check-draw 350ms cubic-bezier(0.22, 1, 0.36, 1) 80ms backwards;
796
+ }
797
+ }
798
+ }
799
+ @keyframes toast-check-pop {
800
+ from {
801
+ transform: scale(0.3);
802
+ opacity: 0;
803
+ }
804
+ }
805
+ @keyframes toast-check-draw {
806
+ from {
807
+ stroke-dashoffset: 24;
808
+ }
809
+ }
810
+
811
+ .content {
812
+ flex: 1;
813
+ min-width: 0;
814
+ display: flex;
815
+ flex-direction: column;
816
+ gap: 0.2rem;
817
+ }
818
+ .title {
819
+ font-size: 0.875rem;
820
+ font-weight: 600;
821
+ line-height: 1.4;
822
+ word-break: break-word;
823
+ }
824
+ .description {
825
+ font-size: 0.8125rem;
826
+ line-height: 1.4;
827
+ opacity: 0.75;
828
+ word-break: break-word;
829
+ }
830
+
831
+ .action {
832
+ flex-shrink: 0;
833
+ align-self: center;
834
+ }
835
+
836
+ .close {
837
+ position: absolute;
838
+ top: 0;
839
+ left: 0;
840
+ font-size: 7px; /* scales the icon Button (4em) to 28px, host-independent */
841
+ transform: translate(-35%, -35%);
842
+ border-radius: 50%;
843
+ background: var(--toast-bg);
844
+ box-shadow: 0 0 0 1px var(--toast-border);
845
+ opacity: 0;
846
+ transition: opacity 150ms ease;
847
+
848
+ /* Trim the × glyph a touch smaller than the dense default (60%) so the
849
+ button box stays a comfortable tap target without an oversized icon. */
850
+ :global(svg) {
851
+ width: 52%;
852
+ height: 52%;
853
+ }
854
+
855
+ .toaster.expanded .toast &,
856
+ .toast.front &,
857
+ .toast:hover & {
858
+ opacity: 1;
859
+ }
860
+
861
+ .toaster.align-right & {
862
+ left: auto;
863
+ right: 0;
864
+ transform: translate(35%, -35%);
865
+ }
866
+ }
867
+
868
+ /* Subtle variant tint (default mode): faint wash + colored border. */
869
+ .toast.success {
870
+ --toast-accent: var(--color-success, #16a34a);
871
+ }
872
+ .toast.error {
873
+ --toast-accent: var(--color-error, #dc2626);
874
+ }
875
+ .toast.warning {
876
+ --toast-accent: var(--color-warning, #d97706);
877
+ }
878
+ .toast.info {
879
+ --toast-accent: var(--color-action, #3b82f6);
880
+ }
881
+ .toast:is(.success, .error, .warning, .info) {
882
+ background-color: color-mix(in oklch, var(--toast-accent) 6%, var(--toast-bg));
883
+ border-color: color-mix(in oklch, var(--toast-accent) 28%, var(--toast-border));
884
+ box-shadow:
885
+ 0 4px 12px rgb(0 0 0 / 0.1),
886
+ 0 2px 4px rgb(0 0 0 / 0.06),
887
+ inset 0 0 0 1px color-mix(in oklch, var(--toast-accent) 10%, transparent);
888
+ }
889
+ .toaster.expanded .toast:is(.success, .error, .warning, .info) {
890
+ box-shadow:
891
+ 0 8px 24px rgb(0 0 0 / 0.14),
892
+ 0 3px 8px rgb(0 0 0 / 0.08),
893
+ inset 0 0 0 1px color-mix(in oklch, var(--toast-accent) 10%, transparent);
894
+ }
895
+
896
+ /* Rich colors — saturated variant surfaces (overrides the subtle tint). */
897
+ .toaster.rich .toast {
898
+ &.success {
899
+ --toast-bg: light-dark(#ecfdf5, #052e1a);
900
+ --toast-fg: light-dark(#065f46, #6ee7b7);
901
+ --toast-border: light-dark(#a7f3d0, #065f46);
902
+ }
903
+ &.error {
904
+ --toast-bg: light-dark(#fef2f2, #2d0a0a);
905
+ --toast-fg: light-dark(#991b1b, #fca5a5);
906
+ --toast-border: light-dark(#fecaca, #7f1d1d);
907
+ }
908
+ &.warning {
909
+ --toast-bg: light-dark(#fffbeb, #2b1c00);
910
+ --toast-fg: light-dark(#92400e, #fcd34d);
911
+ --toast-border: light-dark(#fde68a, #78350f);
912
+ }
913
+ &.info {
914
+ --toast-bg: light-dark(#eff6ff, #0a1b2e);
915
+ --toast-fg: light-dark(#1e40af, #93c5fd);
916
+ --toast-border: light-dark(#bfdbfe, #1e3a8a);
917
+ }
918
+ &:is(.success, .error, .warning, .info) {
919
+ background-color: var(--toast-bg);
920
+ border-color: var(--toast-border);
921
+ box-shadow:
922
+ 0 4px 12px rgb(0 0 0 / 0.1),
923
+ 0 2px 4px rgb(0 0 0 / 0.06);
924
+ }
925
+ .icon {
926
+ color: currentColor;
927
+ }
928
+ }
929
+
930
+ .toast.dismissed {
931
+ animation: toast-exit 320ms cubic-bezier(0.22, 1, 0.36, 1) both;
932
+ pointer-events: none;
933
+ }
934
+
935
+ @keyframes toast-enter {
936
+ from {
937
+ opacity: 0;
938
+ transform: translateY(var(--enter-from, 100%)) scale(0.9);
939
+ }
940
+ }
941
+
942
+ @keyframes toast-exit {
943
+ to {
944
+ opacity: 0;
945
+ transform: translateY(var(--enter-from, 100%)) scale(0.9);
946
+ }
947
+ }
948
+
949
+ @media (prefers-reduced-motion: reduce) {
950
+ .toast {
951
+ animation-duration: 1ms !important;
952
+ transition-duration: 1ms !important;
953
+ }
954
+ .toast.success .icon svg,
955
+ .toast.success .icon svg path {
956
+ animation: none;
957
+ }
958
+ .toast.dismissed {
959
+ animation: toast-exit-reduced 150ms ease both;
960
+ }
961
+ @keyframes toast-exit-reduced {
962
+ to {
963
+ opacity: 0;
964
+ }
965
+ }
966
+ }
967
+ </style>