@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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- 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>
|