varsel 0.5.4 → 0.6.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.
@@ -1,760 +1,821 @@
1
1
  <script lang="ts">
2
- /**
3
- * @component
4
- * @description
5
- * Individual toast component. Handles its own enter/exit animations,
6
- * swipe-to-dismiss gestures, auto-closing timer, and rendering of content.
7
- */
8
- import { onDestroy, onMount } from "svelte";
9
- import {
10
- ANIMATION_CONFIG,
11
- FOCUSABLE_SELECTORS,
12
- POSITION_CONFIGS,
13
- SWIPE_DISMISS_THRESHOLD,
14
- SWIPE_DISMISS_VELOCITY,
15
- SWIPE_EXIT_DISTANCE,
16
- cn,
17
- getDefaultSwipeDirections,
18
- toastContainerVariants,
19
- toastContentVariants,
20
- toastState,
21
- type PositionedToast,
22
- type ToastPosition,
23
- type SwipeAxis,
24
- type SwipeDirection,
25
- } from "./internals";
26
- import {
27
- hasVariantIcon,
28
- variantIconMap,
29
- } from "./variant-icons";
30
-
31
- let {
32
- toast,
33
- onRemove,
34
- isGroupHovered = false,
35
- expandedOffset = 0,
36
- expandedGap = ANIMATION_CONFIG.EXPANDED_GAP,
37
- collapsedOffset = undefined,
38
- hiddenCollapsedOffset = undefined,
39
- onHeightChange = undefined,
40
- onGroupHoverEnter = undefined,
41
- onGroupHoldChange = undefined,
42
- defaultDuration = 5000,
43
- defaultShowClose = true,
44
- pauseOnHover = true,
45
- offset = undefined,
46
- expand = true,
47
- visibleToasts = ANIMATION_CONFIG.MAX_VISIBLE_TOASTS
48
- }: {
49
- toast: PositionedToast;
50
- onRemove: (id: string) => void;
51
- isGroupHovered?: boolean;
52
- expandedOffset?: number;
53
- expandedGap?: number;
54
- collapsedOffset?: number;
55
- hiddenCollapsedOffset?: number;
56
- onHeightChange?: (id: string, height: number) => void;
57
- onGroupHoverEnter?: () => void;
58
- onGroupHoldChange?: (holding: boolean) => void;
59
- defaultDuration?: number;
60
- defaultShowClose?: boolean;
61
- pauseOnHover?: boolean;
62
- offset?: number | string;
63
- expand?: boolean;
64
- visibleToasts?: number;
65
- } = $props();
66
-
67
- let id = $derived(toast.id);
68
- let title = $derived(toast.title);
69
- let description = $derived(toast.description);
70
- let variant = $derived(toast.variant || "default");
71
- let duration = $derived(toast.duration ?? defaultDuration);
72
- let action = $derived(toast.action);
73
- let isLoading = $derived(toast.isLoading || false);
74
- let index = $derived(toast.index);
75
- let renderIndex = $derived(toast.renderIndex);
76
- let shouldClose = $derived(toast.shouldClose);
77
- let position = $derived(toast.position || "bottom-center");
78
- let className = $derived(toast.className || "");
79
- let onAutoClose = $derived(toast.onAutoClose);
80
- let onDismiss = $derived(toast.onDismiss);
81
- let showClose = $derived(toast.showClose ?? defaultShowClose);
82
-
83
- let toastRef = $state<HTMLDivElement | null>(null);
84
- let isItemHovered = $state(false);
85
- let isSwiping = $state(false);
86
- let swipeDismissDirection = $state<SwipeDirection | null>(null);
87
- let animationState = $state<"entering" | "entered" | "exiting" | "stacking">(
88
- "entering",
89
- );
90
-
91
- let timeoutRef: ReturnType<typeof setTimeout> | null = null;
92
- let timerStartRef: number | null = null;
93
- let remainingTime = $state<number | null>(Number.NaN);
94
- let enterAnimationFrame: number | null = null;
95
- let focusTimeout: ReturnType<typeof setTimeout> | null = null;
96
- let pointerStart: { x: number; y: number } | null = null;
97
- let dragStartTime: number | null = null;
98
- let swipeAxis: SwipeAxis | null = null;
99
- let lastSwipe = { x: 0, y: 0 };
100
- let mounted = $state(false);
101
- let prevShouldClose = false;
102
- let previousDuration: number | undefined;
103
- let isExiting = $state(false);
104
- let exitAnimationComplete = false;
105
- let hasAnimatedIn = $state(false);
106
- let isPointerHeld = false;
107
- type SpinnerState = "hidden" | "loading" | "finishing";
108
- let spinnerState = $state<SpinnerState>("hidden");
109
- let spinnerFinishTimer: ReturnType<typeof setTimeout> | null = null;
110
- let hasShownSpinner = $state(false);
111
-
112
- $effect(() => {
113
- if (isLoading) {
114
- hasShownSpinner = true;
115
- }
116
- });
117
-
118
- $effect(() => {
119
- if (isLoading) {
120
- spinnerState = "loading";
121
- } else if (spinnerState === "loading") {
122
- spinnerState = "finishing";
123
- }
124
- });
125
-
126
- $effect(() => {
127
- if (spinnerState === "finishing") {
128
- if (!spinnerFinishTimer) {
129
- spinnerFinishTimer = setTimeout(() => {
130
- spinnerState = "hidden";
131
- spinnerFinishTimer = null;
132
- }, 420);
133
- }
134
- } else if (spinnerFinishTimer) {
135
- clearTimeout(spinnerFinishTimer);
136
- spinnerFinishTimer = null;
137
- }
138
- });
139
-
140
- let shouldRenderSpinner = $derived(spinnerState !== "hidden");
141
-
142
- const handleSpinnerAnimationEnd = (event: AnimationEvent) => {
143
- if (event.animationName !== "vs-spinner-finish") return;
144
- spinnerState = "hidden";
145
- };
146
-
147
- let iconConfig = $derived(
148
- hasVariantIcon(variant) ? variantIconMap[variant] : undefined,
149
- );
150
- let showStatusIcon = $derived(isLoading || Boolean(iconConfig));
151
-
152
- let iconStateClass = $derived(
153
- !iconConfig
154
- ? undefined
155
- : !hasShownSpinner
156
- ? "vs-icon--static"
157
- : isLoading
158
- ? "vs-icon--waiting"
159
- : "vs-icon--pop",
160
- );
161
-
162
- const clearSwipeRefs = () => {
163
- pointerStart = null;
164
- dragStartTime = null;
165
- swipeAxis = null;
166
- lastSwipe = { x: 0, y: 0 };
167
- };
168
-
169
- const getFocusableElements = () => {
170
- if (!toastRef) return [];
171
- return Array.from(
172
- toastRef.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS),
2
+ /**
3
+ * @component
4
+ * @description
5
+ * Individual toast component. Handles its own enter/exit animations,
6
+ * swipe-to-dismiss gestures, auto-closing timer, and rendering of content.
7
+ */
8
+ import { onDestroy, onMount } from "svelte";
9
+ import {
10
+ ANIMATION_CONFIG,
11
+ FOCUSABLE_SELECTORS,
12
+ focusManager,
13
+ isFocusVisible,
14
+ POSITION_CONFIGS,
15
+ SWIPE_DISMISS_THRESHOLD,
16
+ SWIPE_DISMISS_VELOCITY,
17
+ SWIPE_EXIT_DISTANCE,
18
+ SWIPE_REVERSE_CANCEL_THRESHOLD,
19
+ cn,
20
+ getDefaultSwipeDirections,
21
+ toastContainerVariants,
22
+ toastContentVariants,
23
+ toastState,
24
+ type PositionedToast,
25
+ type ToastPosition,
26
+ type SwipeAxis,
27
+ type SwipeDirection,
28
+ } from "./internals";
29
+ import { hasVariantIcon, variantIconMap } from "./variant-icons";
30
+ import type { IconElement } from "./variant-icons";
31
+
32
+ let {
33
+ toast,
34
+ onRemove,
35
+ isGroupHovered = false,
36
+ expandedOffset = 0,
37
+ expandedGap = ANIMATION_CONFIG.EXPANDED_GAP,
38
+ collapsedOffset = undefined,
39
+ hiddenCollapsedOffset = undefined,
40
+ onHeightChange = undefined,
41
+ onGroupHoverEnter = undefined,
42
+ onGroupHoldChange = undefined,
43
+ defaultDuration = 5000,
44
+ defaultShowClose = true,
45
+ pauseOnHover = true,
46
+ offset = undefined,
47
+ expand = true,
48
+ visibleToasts = ANIMATION_CONFIG.MAX_VISIBLE_TOASTS,
49
+ isWindowFocused = true,
50
+ }: {
51
+ toast: PositionedToast;
52
+ onRemove: (id: string) => void;
53
+ isGroupHovered?: boolean;
54
+ expandedOffset?: number;
55
+ expandedGap?: number;
56
+ collapsedOffset?: number;
57
+ hiddenCollapsedOffset?: number;
58
+ onHeightChange?: (id: string, height: number) => void;
59
+ onGroupHoverEnter?: () => void;
60
+ onGroupHoldChange?: (holding: boolean) => void;
61
+ defaultDuration?: number;
62
+ defaultShowClose?: boolean;
63
+ pauseOnHover?: boolean;
64
+ offset?: number | string;
65
+ expand?: boolean;
66
+ visibleToasts?: number;
67
+ isWindowFocused?: boolean;
68
+ } = $props();
69
+
70
+ let id = $derived(toast.id);
71
+ let title = $derived(toast.title);
72
+ let description = $derived(toast.description);
73
+ let variant = $derived(toast.variant || "default");
74
+ let duration = $derived(toast.duration ?? defaultDuration);
75
+ let action = $derived(toast.action);
76
+ let isLoading = $derived(toast.isLoading || false);
77
+ let index = $derived(toast.index);
78
+ let renderIndex = $derived(toast.renderIndex);
79
+ let shouldClose = $derived(toast.shouldClose);
80
+ let position = $derived(toast.position || "bottom-center");
81
+ let className = $derived(toast.className || "");
82
+ let onAutoClose = $derived(toast.onAutoClose);
83
+ let onDismiss = $derived(toast.onDismiss);
84
+ let showClose = $derived(toast.showClose ?? defaultShowClose);
85
+ let priority = $derived(toast.priority ?? "low");
86
+
87
+ let toastRef = $state<HTMLDivElement | null>(null);
88
+ let isItemHovered = $state(false);
89
+ let isItemFocusVisible = $state(false);
90
+ let isSwiping = $state(false);
91
+ let swipeDismissDirection = $state<SwipeDirection | null>(null);
92
+ let animationState = $state<"entering" | "entered" | "exiting" | "stacking">(
93
+ "entering",
173
94
  );
174
- };
175
-
176
- type CloseReason = "auto" | "dismiss" | null;
177
- let closeReason: CloseReason = null;
178
-
179
- const handleTransitionEnd = (event: TransitionEvent) => {
180
- if (event.target !== toastRef) return;
181
- if (event.propertyName !== "opacity" && event.propertyName !== "transform")
182
- return;
183
- if (animationState !== "exiting") return;
184
- if (exitAnimationComplete) return;
185
-
186
- exitAnimationComplete = true;
187
- if (closeReason === "auto") {
188
- onAutoClose?.();
189
- } else if (closeReason === "dismiss") {
190
- onDismiss?.();
191
- }
192
- onRemove(id);
193
- };
194
-
195
- const handleClose = (reason: Exclude<CloseReason, null> = "dismiss") => {
196
- if (!toastRef || isExiting) return;
197
-
198
- isExiting = true;
199
- exitAnimationComplete = false;
200
- closeReason = reason;
201
-
202
- toastState.update(id, { shouldClose: true });
203
-
204
- if (enterAnimationFrame) {
205
- cancelAnimationFrame(enterAnimationFrame);
206
- enterAnimationFrame = null;
207
- }
208
-
209
- if (timeoutRef) {
210
- clearTimeout(timeoutRef);
211
- timeoutRef = null;
212
- }
213
-
214
- animationState = "exiting";
215
- toastState.update(id, { shouldClose: true, isLeaving: true });
216
- };
217
-
218
- $effect(() => {
219
- const desiredClose = Boolean(shouldClose);
220
- if (desiredClose && !prevShouldClose) {
221
- handleClose();
222
- }
223
- prevShouldClose = desiredClose;
224
- });
225
-
226
- onMount(() => {
227
- mounted = true;
228
- return () => {
229
- mounted = false;
95
+
96
+ let timeoutRef: ReturnType<typeof setTimeout> | null = null;
97
+ let timerStartRef: number | null = null;
98
+ let remainingTime = $state<number | null>(Number.NaN);
99
+ let enterAnimationFrame: number | null = null;
100
+ let focusTimeout: ReturnType<typeof setTimeout> | null = null;
101
+ let pointerStart: { x: number; y: number } | null = null;
102
+ let dragStartTime: number | null = null;
103
+ let swipeAxis: SwipeAxis | null = null;
104
+ let lastSwipe = { x: 0, y: 0 };
105
+ let maxSwipeDisplacement = 0;
106
+ let swipeCancelled = false;
107
+ let mounted = $state(false);
108
+ let prevShouldClose = false;
109
+ let previousDuration: number | undefined;
110
+ let isExiting = $state(false);
111
+ let exitAnimationComplete = false;
112
+ let hasAnimatedIn = $state(false);
113
+ let isPointerHeld = false;
114
+ type SpinnerState = "hidden" | "loading" | "finishing";
115
+ let spinnerState = $state<SpinnerState>("hidden");
116
+ let spinnerFinishTimer: ReturnType<typeof setTimeout> | null = null;
117
+ let hasShownSpinner = $state(false);
118
+
119
+ type LineElement = Extract<IconElement, { tag: "line" }>;
120
+
121
+ $effect(() => {
122
+ if (isLoading) {
123
+ hasShownSpinner = true;
124
+ }
125
+ });
126
+
127
+ $effect(() => {
128
+ if (isLoading) {
129
+ spinnerState = "loading";
130
+ } else if (spinnerState === "loading") {
131
+ spinnerState = "finishing";
132
+ }
133
+ });
134
+
135
+ $effect(() => {
136
+ if (spinnerState === "finishing") {
137
+ if (!spinnerFinishTimer) {
138
+ spinnerFinishTimer = setTimeout(() => {
139
+ spinnerState = "hidden";
140
+ spinnerFinishTimer = null;
141
+ }, 420);
142
+ }
143
+ } else if (spinnerFinishTimer) {
144
+ clearTimeout(spinnerFinishTimer);
145
+ spinnerFinishTimer = null;
146
+ }
147
+ });
148
+
149
+ let shouldRenderSpinner = $derived(spinnerState !== "hidden");
150
+
151
+ const handleSpinnerAnimationEnd = (event: AnimationEvent) => {
152
+ if (event.animationName !== "vs-spinner-finish") return;
153
+ spinnerState = "hidden";
154
+ };
155
+
156
+ let iconConfig = $derived(
157
+ hasVariantIcon(variant) ? variantIconMap[variant] : undefined,
158
+ );
159
+ let showStatusIcon = $derived(isLoading || Boolean(iconConfig));
160
+
161
+ let iconStateClass = $derived(
162
+ !iconConfig
163
+ ? undefined
164
+ : !hasShownSpinner
165
+ ? "vs-icon--static"
166
+ : isLoading
167
+ ? "vs-icon--waiting"
168
+ : "vs-icon--pop",
169
+ );
170
+
171
+ const clearSwipeRefs = () => {
172
+ pointerStart = null;
173
+ dragStartTime = null;
174
+ swipeAxis = null;
175
+ lastSwipe = { x: 0, y: 0 };
176
+ maxSwipeDisplacement = 0;
177
+ swipeCancelled = false;
178
+ };
179
+
180
+ const getFocusableElements = () => {
181
+ if (!toastRef) return [];
182
+ return Array.from(toastRef.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS));
183
+ };
184
+
185
+ type CloseReason = "auto" | "dismiss" | null;
186
+ let closeReason: CloseReason = null;
187
+
188
+ const handleTransitionEnd = (event: TransitionEvent) => {
189
+ if (event.target !== toastRef) return;
190
+ if (event.propertyName !== "opacity" && event.propertyName !== "transform") return;
191
+ if (animationState !== "exiting") return;
192
+ if (exitAnimationComplete) return;
193
+
194
+ exitAnimationComplete = true;
195
+ if (closeReason === "auto") {
196
+ onAutoClose?.();
197
+ } else if (closeReason === "dismiss") {
198
+ onDismiss?.();
199
+ }
200
+ onRemove(id);
201
+ };
202
+
203
+ const shiftFocusOnDismiss = () => {
204
+ if (!toastRef || typeof document === "undefined") return;
205
+ const active = document.activeElement;
206
+ if (!(active instanceof HTMLElement)) return;
207
+ if (!toastRef.contains(active)) return;
208
+
209
+ const siblings = Array.from(
210
+ document.querySelectorAll<HTMLElement>("[data-toast-id]"),
211
+ ).filter(
212
+ (el) =>
213
+ el !== toastRef &&
214
+ el.getAttribute("data-limited") !== "true" &&
215
+ !el.hasAttribute("inert"),
216
+ );
217
+ const next = siblings[0];
218
+ if (next) {
219
+ const focusable = next.querySelector<HTMLElement>(FOCUSABLE_SELECTORS);
220
+ const target = focusable ?? next;
221
+ try {
222
+ target.focus({ preventScroll: true });
223
+ } catch {
224
+ target.focus();
225
+ }
226
+ return;
227
+ }
228
+
229
+ if (focusManager.isClaimed()) {
230
+ focusManager.restoreFocusToPrevElement();
231
+ } else {
232
+ active.blur();
233
+ }
234
+ };
235
+
236
+ const handleClose = (reason: Exclude<CloseReason, null> = "dismiss") => {
237
+ if (!toastRef || isExiting) return;
238
+
239
+ shiftFocusOnDismiss();
240
+
241
+ isExiting = true;
242
+ exitAnimationComplete = false;
243
+ closeReason = reason;
244
+
245
+ toastState.update(id, { shouldClose: true });
246
+
247
+ if (enterAnimationFrame) {
248
+ cancelAnimationFrame(enterAnimationFrame);
249
+ enterAnimationFrame = null;
250
+ }
251
+
252
+ if (timeoutRef) {
253
+ clearTimeout(timeoutRef);
254
+ timeoutRef = null;
255
+ }
256
+
257
+ animationState = "exiting";
258
+ toastState.update(id, { shouldClose: true, isLeaving: true });
259
+ };
260
+
261
+ $effect(() => {
262
+ const desiredClose = Boolean(shouldClose);
263
+ if (desiredClose && !prevShouldClose) {
264
+ handleClose();
265
+ }
266
+ prevShouldClose = desiredClose;
267
+ });
268
+
269
+ onMount(() => {
270
+ mounted = true;
271
+ return () => {
272
+ mounted = false;
273
+ if (enterAnimationFrame) cancelAnimationFrame(enterAnimationFrame);
274
+ if (timeoutRef) clearTimeout(timeoutRef);
275
+ if (focusTimeout) clearTimeout(focusTimeout);
276
+ };
277
+ });
278
+
279
+ onDestroy(() => {
230
280
  if (enterAnimationFrame) cancelAnimationFrame(enterAnimationFrame);
231
281
  if (timeoutRef) clearTimeout(timeoutRef);
232
282
  if (focusTimeout) clearTimeout(focusTimeout);
233
- };
234
- });
235
-
236
- onDestroy(() => {
237
- if (enterAnimationFrame) cancelAnimationFrame(enterAnimationFrame);
238
- if (timeoutRef) clearTimeout(timeoutRef);
239
- if (focusTimeout) clearTimeout(focusTimeout);
240
- if (spinnerFinishTimer) {
241
- clearTimeout(spinnerFinishTimer);
242
- spinnerFinishTimer = null;
243
- }
244
- if (isPointerHeld) {
245
- isPointerHeld = false;
246
- onGroupHoldChange?.(false);
247
- }
248
- });
249
-
250
- $effect(() => {
251
- if (mounted && duration !== previousDuration) {
252
- remainingTime = duration;
253
- previousDuration = duration;
254
- }
255
- });
256
-
257
- $effect(() => {
258
- if (mounted) {
259
- if (!toastRef || !onHeightChange) {
260
- // do nothing
261
- } else {
262
- const el = toastRef;
263
- const notify = () => onHeightChange?.(id, el.offsetHeight);
264
- const ro = new ResizeObserver(() => notify());
265
- ro.observe(el);
266
- notify();
267
- return () => ro.disconnect();
268
- }
269
- }
270
- });
271
-
272
- const setFocusToToast = () => {
273
- if (!toastRef) return;
274
- const focusableElements = getFocusableElements();
275
- const firstFocusable = focusableElements[0];
276
- if (firstFocusable) {
277
- firstFocusable.focus();
278
- return;
279
- }
280
- toastRef.focus();
281
- };
282
-
283
- $effect(() => {
284
- if (mounted && toastRef && !isExiting) {
285
- if (!hasAnimatedIn && isLatest) {
286
- hasAnimatedIn = true;
287
- animationState = "entering";
288
- if (enterAnimationFrame) {
289
- cancelAnimationFrame(enterAnimationFrame);
283
+ if (spinnerFinishTimer) {
284
+ clearTimeout(spinnerFinishTimer);
285
+ spinnerFinishTimer = null;
286
+ }
287
+ if (isPointerHeld) {
288
+ isPointerHeld = false;
289
+ onGroupHoldChange?.(false);
290
+ }
291
+ });
292
+
293
+ $effect(() => {
294
+ if (mounted && duration !== previousDuration) {
295
+ remainingTime = duration;
296
+ previousDuration = duration;
297
+ }
298
+ });
299
+
300
+ $effect(() => {
301
+ if (mounted) {
302
+ if (!toastRef || !onHeightChange) {
303
+ // do nothing
304
+ } else {
305
+ const el = toastRef;
306
+ const notify = () => onHeightChange?.(id, el.offsetHeight);
307
+ const ro = new ResizeObserver(() => notify());
308
+ ro.observe(el);
309
+ notify();
310
+ return () => ro.disconnect();
290
311
  }
291
- enterAnimationFrame = requestAnimationFrame(() => {
312
+ }
313
+ });
314
+
315
+ const setFocusToToast = () => {
316
+ if (!toastRef) return;
317
+ const focusableElements = getFocusableElements();
318
+ const firstFocusable = focusableElements[0];
319
+ if (firstFocusable) {
320
+ firstFocusable.focus();
321
+ return;
322
+ }
323
+ toastRef.focus();
324
+ };
325
+
326
+ $effect(() => {
327
+ if (mounted && toastRef && !isExiting) {
328
+ if (!hasAnimatedIn && isLatest) {
329
+ hasAnimatedIn = true;
330
+ animationState = "entering";
331
+ if (enterAnimationFrame) {
332
+ cancelAnimationFrame(enterAnimationFrame);
333
+ }
292
334
  enterAnimationFrame = requestAnimationFrame(() => {
293
- animationState = "entered";
294
- if (action) {
295
- if (focusTimeout) clearTimeout(focusTimeout);
296
- focusTimeout = setTimeout(
297
- () => setFocusToToast(),
298
- ANIMATION_CONFIG.ENTER_DURATION * 1000,
299
- );
300
- }
335
+ enterAnimationFrame = requestAnimationFrame(() => {
336
+ animationState = "entered";
337
+ if (action) {
338
+ if (focusTimeout) clearTimeout(focusTimeout);
339
+ focusTimeout = setTimeout(
340
+ () => setFocusToToast(),
341
+ ANIMATION_CONFIG.ENTER_DURATION * 1000,
342
+ );
343
+ }
344
+ });
301
345
  });
302
- });
303
- } else if (hasAnimatedIn) {
304
- if (animationState !== "stacking" || index > 0) {
346
+ } else if (hasAnimatedIn) {
347
+ if (animationState !== "stacking" || index > 0) {
348
+ animationState = "stacking";
349
+ }
350
+ } else {
305
351
  animationState = "stacking";
306
352
  }
307
- } else {
308
- animationState = "stacking";
309
353
  }
310
- }
311
- });
312
-
313
- $effect(() => {
314
- if (mounted) {
315
- if (shouldClose || !hasAnimatedIn || duration <= 0) {
316
- if (timeoutRef) {
317
- clearTimeout(timeoutRef);
318
- timeoutRef = null;
319
- }
320
- timerStartRef = null;
321
- } else {
322
- if (remainingTime == null || Number.isNaN(remainingTime)) {
323
- remainingTime = duration;
324
- }
354
+ });
325
355
 
326
- const isHovering = isGroupHovered || isItemHovered;
327
- const isPaused =
328
- (pauseOnHover && isHovering) || isSwiping || hiddenByStacking;
329
-
330
- if (isPaused) {
356
+ $effect(() => {
357
+ if (mounted) {
358
+ if (shouldClose || !hasAnimatedIn || duration <= 0) {
331
359
  if (timeoutRef) {
332
360
  clearTimeout(timeoutRef);
333
361
  timeoutRef = null;
334
362
  }
335
- if (timerStartRef !== null) {
336
- const elapsed = Date.now() - timerStartRef;
337
- remainingTime = Math.max(0, (remainingTime ?? duration) - elapsed);
338
- timerStartRef = null;
339
- }
363
+ timerStartRef = null;
340
364
  } else {
341
- if (!timeoutRef) {
342
- const ms = Math.max(0, remainingTime ?? duration);
343
- if (!Number.isFinite(ms)) {
344
- // Do not set timeout for infinite duration
345
- } else if (ms === 0) {
346
- handleClose("auto");
347
- } else {
348
- timerStartRef = Date.now();
349
- timeoutRef = setTimeout(() => {
365
+ if (remainingTime == null || Number.isNaN(remainingTime)) {
366
+ remainingTime = duration;
367
+ }
368
+
369
+ const isHovering = isGroupHovered || isItemHovered;
370
+ const isPaused =
371
+ (pauseOnHover && isHovering) ||
372
+ isItemFocusVisible ||
373
+ isSwiping ||
374
+ hiddenByStacking ||
375
+ !isWindowFocused;
376
+
377
+ if (isPaused) {
378
+ if (timeoutRef) {
379
+ clearTimeout(timeoutRef);
380
+ timeoutRef = null;
381
+ }
382
+ if (timerStartRef !== null) {
383
+ const elapsed = Date.now() - timerStartRef;
384
+ remainingTime = Math.max(0, (remainingTime ?? duration) - elapsed);
385
+ timerStartRef = null;
386
+ }
387
+ } else {
388
+ if (!timeoutRef) {
389
+ const ms = Math.max(0, remainingTime ?? duration);
390
+ if (!Number.isFinite(ms)) {
391
+ // Do not set timeout for infinite duration
392
+ } else if (ms === 0) {
350
393
  handleClose("auto");
351
- }, ms);
394
+ } else {
395
+ timerStartRef = Date.now();
396
+ timeoutRef = setTimeout(() => {
397
+ handleClose("auto");
398
+ }, ms);
399
+ }
352
400
  }
353
401
  }
354
402
  }
355
403
  }
356
- }
357
- });
358
-
359
- $effect(() => {
360
- if (mounted && toastRef && !isSwiping && !swipeDismissDirection) {
361
- toastRef.style.setProperty("--swipe-translate-x", "0px");
362
- toastRef.style.setProperty("--swipe-translate-y", "0px");
363
- }
364
- });
365
-
366
- let swipeDirections = $derived(
367
- showClose ? getDefaultSwipeDirections(position) : [],
368
- );
369
-
370
- const handlePointerDown = (event: PointerEvent) => {
371
- if (!showClose) return;
372
- if (event.pointerType === "mouse" && event.button !== 0) return;
373
- if (event.button === 2) return;
374
- if (isExiting) return;
375
-
376
- const target = event.target as HTMLElement;
377
- if (target.closest("button, a, input, textarea, select")) {
378
- return;
379
- }
380
-
381
- clearSwipeRefs();
382
- pointerStart = { x: event.clientX, y: event.clientY };
383
- dragStartTime = Date.now();
384
- if (toastRef) {
385
- toastRef.style.setProperty("--swipe-translate-x", "0px");
386
- toastRef.style.setProperty("--swipe-translate-y", "0px");
387
- }
388
- swipeDismissDirection = null;
389
- isSwiping = true;
390
- if (!isPointerHeld) {
391
- isPointerHeld = true;
392
- onGroupHoldChange?.(true);
393
- }
394
- const currentTarget = event.currentTarget as HTMLElement | null;
395
- currentTarget?.setPointerCapture(event.pointerId);
396
- };
397
-
398
- const handlePointerMove = (event: PointerEvent) => {
399
- if (!showClose) return;
400
- if (!pointerStart) return;
401
- if (isExiting) return;
402
-
403
- if (event.pointerType === "touch") {
404
- event.preventDefault();
405
- }
406
-
407
- const xDelta = event.clientX - pointerStart.x;
408
- const yDelta = event.clientY - pointerStart.y;
409
-
410
- let axis = swipeAxis;
411
- if (!axis) {
412
- if (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1) {
413
- axis = Math.abs(xDelta) > Math.abs(yDelta) ? "x" : "y";
414
- swipeAxis = axis;
415
- } else {
404
+ });
405
+
406
+ $effect(() => {
407
+ if (mounted && toastRef && !isSwiping && !swipeDismissDirection) {
408
+ toastRef.style.setProperty("--swipe-translate-x", "0px");
409
+ toastRef.style.setProperty("--swipe-translate-y", "0px");
410
+ }
411
+ });
412
+
413
+ let swipeDirections = $derived(showClose ? getDefaultSwipeDirections(position) : []);
414
+
415
+ const handlePointerDown = (event: PointerEvent) => {
416
+ if (!showClose) return;
417
+ if (event.pointerType === "mouse" && event.button !== 0) return;
418
+ if (event.button === 2) return;
419
+ if (isExiting) return;
420
+
421
+ const target = event.target as HTMLElement;
422
+ if (target.closest("button, a, input, textarea, select")) {
416
423
  return;
417
424
  }
418
- }
419
425
 
420
- const dampen = (delta: number) => {
421
- const factor = Math.abs(delta) / 20;
422
- return delta * (1 / (1.5 + factor));
426
+ clearSwipeRefs();
427
+ pointerStart = { x: event.clientX, y: event.clientY };
428
+ dragStartTime = Date.now();
429
+ if (toastRef) {
430
+ toastRef.style.setProperty("--swipe-translate-x", "0px");
431
+ toastRef.style.setProperty("--swipe-translate-y", "0px");
432
+ }
433
+ swipeDismissDirection = null;
434
+ isSwiping = true;
435
+ if (!isPointerHeld) {
436
+ isPointerHeld = true;
437
+ onGroupHoldChange?.(true);
438
+ }
439
+ const currentTarget = event.currentTarget as HTMLElement | null;
440
+ currentTarget?.setPointerCapture(event.pointerId);
423
441
  };
424
442
 
425
- let nextX = 0;
426
- let nextY = 0;
427
-
428
- if (axis === "x") {
429
- const allowLeft = swipeDirections.includes("left");
430
- const allowRight = swipeDirections.includes("right");
431
- if (!allowLeft && !allowRight) {
432
- swipeAxis = "y";
433
- axis = "y";
434
- } else if ((allowLeft && xDelta < 0) || (allowRight && xDelta > 0)) {
435
- nextX = xDelta;
436
- } else {
437
- nextX = dampen(xDelta);
438
- }
439
- }
440
-
441
- if (axis === "y") {
442
- const allowTop = swipeDirections.includes("top");
443
- const allowBottom = swipeDirections.includes("bottom");
444
- if (!allowTop && !allowBottom) {
445
- swipeAxis = "x";
446
- axis = "x";
447
- } else if ((allowTop && yDelta < 0) || (allowBottom && yDelta > 0)) {
448
- nextY = yDelta;
449
- } else {
450
- nextY = dampen(yDelta);
443
+ const handlePointerMove = (event: PointerEvent) => {
444
+ if (!showClose) return;
445
+ if (!pointerStart) return;
446
+ if (isExiting) return;
447
+
448
+ if (event.pointerType === "touch") {
449
+ event.preventDefault();
451
450
  }
452
- }
453
451
 
454
- lastSwipe = { x: nextX, y: nextY };
455
- if (toastRef) {
456
- toastRef.style.setProperty("--swipe-translate-x", `${nextX}px`);
457
- toastRef.style.setProperty("--swipe-translate-y", `${nextY}px`);
458
- }
459
- };
452
+ const xDelta = event.clientX - pointerStart.x;
453
+ const yDelta = event.clientY - pointerStart.y;
460
454
 
461
- const handlePointerUp = (event: PointerEvent) => {
462
- if (!showClose) return;
463
- const currentTarget = event.currentTarget as HTMLElement | null;
464
- if (currentTarget?.hasPointerCapture(event.pointerId)) {
465
- currentTarget.releasePointerCapture(event.pointerId);
466
- }
455
+ let axis = swipeAxis;
456
+ if (!axis) {
457
+ if (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1) {
458
+ axis = Math.abs(xDelta) > Math.abs(yDelta) ? "x" : "y";
459
+ swipeAxis = axis;
460
+ } else {
461
+ return;
462
+ }
463
+ }
467
464
 
468
- if (!pointerStart) {
469
- swipeDismissDirection = null;
470
- isSwiping = false;
471
- clearSwipeRefs();
472
- return;
473
- }
474
-
475
- const elapsed = dragStartTime ? Date.now() - dragStartTime : 0;
476
-
477
- const axis = swipeAxis;
478
- const { x, y } = lastSwipe;
479
- let dismissed = false;
480
-
481
- if (axis) {
482
- const distance = axis === "x" ? x : y;
483
- const velocity = elapsed > 0 ? Math.abs(distance) / elapsed : 0;
484
- const meetsThreshold =
485
- Math.abs(distance) >= SWIPE_DISMISS_THRESHOLD ||
486
- velocity > SWIPE_DISMISS_VELOCITY;
487
-
488
- if (meetsThreshold && Math.abs(distance) > 0) {
489
- let direction: SwipeDirection;
490
- if (axis === "x") {
491
- direction = distance > 0 ? "right" : "left";
465
+ const dampen = (delta: number) => {
466
+ const factor = Math.abs(delta) / 20;
467
+ return delta * (1 / (1.5 + factor));
468
+ };
469
+
470
+ let nextX = 0;
471
+ let nextY = 0;
472
+
473
+ if (axis === "x") {
474
+ const allowLeft = swipeDirections.includes("left");
475
+ const allowRight = swipeDirections.includes("right");
476
+ if (!allowLeft && !allowRight) {
477
+ swipeAxis = "y";
478
+ axis = "y";
479
+ } else if ((allowLeft && xDelta < 0) || (allowRight && xDelta > 0)) {
480
+ nextX = xDelta;
492
481
  } else {
493
- direction = distance > 0 ? "bottom" : "top";
482
+ nextX = dampen(xDelta);
494
483
  }
484
+ }
485
+
486
+ if (axis === "y") {
487
+ const allowTop = swipeDirections.includes("top");
488
+ const allowBottom = swipeDirections.includes("bottom");
489
+ if (!allowTop && !allowBottom) {
490
+ swipeAxis = "x";
491
+ } else if ((allowTop && yDelta < 0) || (allowBottom && yDelta > 0)) {
492
+ nextY = yDelta;
493
+ } else {
494
+ nextY = dampen(yDelta);
495
+ }
496
+ }
497
+
498
+ lastSwipe = { x: nextX, y: nextY };
499
+
500
+ const displacement = axis === "x" ? Math.abs(nextX) : Math.abs(nextY);
501
+ if (displacement > maxSwipeDisplacement) {
502
+ maxSwipeDisplacement = displacement;
503
+ }
504
+ if (displacement >= SWIPE_DISMISS_THRESHOLD) {
505
+ swipeCancelled = false;
506
+ } else if (maxSwipeDisplacement - displacement >= SWIPE_REVERSE_CANCEL_THRESHOLD) {
507
+ swipeCancelled = true;
508
+ }
509
+
510
+ if (toastRef) {
511
+ toastRef.style.setProperty("--swipe-translate-x", `${nextX}px`);
512
+ toastRef.style.setProperty("--swipe-translate-y", `${nextY}px`);
513
+ }
514
+ };
515
+
516
+ const handlePointerUp = (event: PointerEvent) => {
517
+ if (!showClose) return;
518
+ const currentTarget = event.currentTarget as HTMLElement | null;
519
+ if (currentTarget?.hasPointerCapture(event.pointerId)) {
520
+ currentTarget.releasePointerCapture(event.pointerId);
521
+ }
522
+
523
+ if (!pointerStart) {
524
+ swipeDismissDirection = null;
525
+ isSwiping = false;
526
+ clearSwipeRefs();
527
+ return;
528
+ }
529
+
530
+ const elapsed = dragStartTime ? Date.now() - dragStartTime : 0;
495
531
 
496
- if (swipeDirections.includes(direction)) {
497
- swipeDismissDirection = direction;
498
- dismissed = true;
499
- handleClose();
532
+ const axis = swipeAxis;
533
+ const { x, y } = lastSwipe;
534
+ let dismissed = false;
535
+
536
+ if (axis && !swipeCancelled) {
537
+ const distance = axis === "x" ? x : y;
538
+ const velocity = elapsed > 0 ? Math.abs(distance) / elapsed : 0;
539
+ const meetsThreshold =
540
+ Math.abs(distance) >= SWIPE_DISMISS_THRESHOLD ||
541
+ velocity > SWIPE_DISMISS_VELOCITY;
542
+
543
+ if (meetsThreshold && Math.abs(distance) > 0) {
544
+ let direction: SwipeDirection;
545
+ if (axis === "x") {
546
+ direction = distance > 0 ? "right" : "left";
547
+ } else {
548
+ direction = distance > 0 ? "bottom" : "top";
549
+ }
550
+
551
+ if (swipeDirections.includes(direction)) {
552
+ swipeDismissDirection = direction;
553
+ dismissed = true;
554
+ handleClose();
555
+ }
500
556
  }
501
557
  }
502
- }
503
558
 
504
- if (!dismissed) {
559
+ if (!dismissed) {
560
+ swipeDismissDirection = null;
561
+ }
562
+
563
+ isSwiping = false;
564
+ clearSwipeRefs();
565
+ if (isPointerHeld) {
566
+ isPointerHeld = false;
567
+ onGroupHoldChange?.(false);
568
+ }
569
+ };
570
+
571
+ const handlePointerCancel = (event: PointerEvent) => {
572
+ if (!showClose) return;
573
+ const currentTarget = event.currentTarget as HTMLElement | null;
574
+ if (currentTarget?.hasPointerCapture(event.pointerId)) {
575
+ currentTarget.releasePointerCapture(event.pointerId);
576
+ }
505
577
  swipeDismissDirection = null;
506
- }
507
-
508
- isSwiping = false;
509
- clearSwipeRefs();
510
- if (isPointerHeld) {
511
- isPointerHeld = false;
512
- onGroupHoldChange?.(false);
513
- }
514
- };
515
-
516
- const handlePointerCancel = (event: PointerEvent) => {
517
- if (!showClose) return;
518
- const currentTarget = event.currentTarget as HTMLElement | null;
519
- if (currentTarget?.hasPointerCapture(event.pointerId)) {
520
- currentTarget.releasePointerCapture(event.pointerId);
521
- }
522
- swipeDismissDirection = null;
523
- isSwiping = false;
524
- clearSwipeRefs();
525
- if (isPointerHeld) {
526
- isPointerHeld = false;
527
- onGroupHoldChange?.(false);
528
- }
529
- };
530
-
531
- const zIndexBase = Number(ANIMATION_CONFIG.Z_INDEX_BASE);
532
-
533
- let isTopPosition = $derived(position?.startsWith("top-") ?? false);
534
- let maxVisibleIndex = $derived(
535
- Math.max(0, visibleToasts - 1),
536
- );
537
- let visibleIndex = $derived(Math.min(index, maxVisibleIndex));
538
- let defaultCollapsedOffset = $derived(
539
- isTopPosition
540
- ? index * ANIMATION_CONFIG.STACK_OFFSET
541
- : -(index * ANIMATION_CONFIG.STACK_OFFSET),
542
- );
543
- let resolvedCollapsedOffset = $derived(
544
- typeof collapsedOffset === "number" && Number.isFinite(collapsedOffset)
545
- ? collapsedOffset
546
- : defaultCollapsedOffset,
547
- );
548
- let resolvedHiddenCollapsedOffset = $derived(
549
- typeof hiddenCollapsedOffset === "number" &&
550
- Number.isFinite(hiddenCollapsedOffset)
551
- ? hiddenCollapsedOffset
552
- : resolvedCollapsedOffset,
553
- );
554
- let scale = $derived(
555
- Math.max(ANIMATION_CONFIG.MIN_SCALE, 1 - index * ANIMATION_CONFIG.SCALE_FACTOR),
556
- );
557
- let visibleScale = $derived(
558
- Math.max(
559
- ANIMATION_CONFIG.MIN_SCALE,
560
- 1 - visibleIndex * ANIMATION_CONFIG.SCALE_FACTOR,
561
- ),
562
- );
563
- let zIndex = $derived(zIndexBase - renderIndex);
564
- let stackHidden = $derived(index >= visibleToasts);
565
- let hiddenByStacking = $derived(stackHidden && animationState !== "exiting");
566
- let isStackLeader = $derived(index === 0);
567
- let isLatest = $derived(isStackLeader && !shouldClose);
568
- type PositionConfig = (typeof POSITION_CONFIGS)[ToastPosition];
569
- let config = $derived(
570
- POSITION_CONFIGS[(position ?? "bottom-center") as ToastPosition],
571
- );
572
-
573
- let transformStyle = $derived.by(() => {
574
- const baseOffsetY = stackHidden
575
- ? resolvedHiddenCollapsedOffset
576
- : resolvedCollapsedOffset;
577
- const promotionOffset =
578
- typeof expandedGap === "number"
579
- ? expandedGap
580
- : ANIMATION_CONFIG.EXPANDED_GAP;
581
- const expandedTranslateY = isTopPosition ? expandedOffset : -expandedOffset;
582
- const hiddenExpandedTranslateY = expandedTranslateY - promotionOffset;
583
-
584
- let translateX = 0;
585
- let translateY = baseOffsetY;
586
- let scaleValue = stackHidden
587
- ? visibleIndex === 0
588
- ? 1
589
- : visibleScale
590
- : isStackLeader
591
- ? 1
592
- : scale;
593
- let opacityValue = stackHidden ? 0 : 1;
594
-
595
- if (stackHidden) {
596
- if (expand && isGroupHovered && animationState !== "exiting") {
597
- translateX = 0;
598
- translateY = hiddenExpandedTranslateY;
599
- scaleValue = 1;
578
+ isSwiping = false;
579
+ clearSwipeRefs();
580
+ if (isPointerHeld) {
581
+ isPointerHeld = false;
582
+ onGroupHoldChange?.(false);
600
583
  }
601
- } else if (expand && isGroupHovered && animationState !== "exiting") {
602
- translateX = 0;
603
- translateY = expandedTranslateY;
604
- scaleValue = 1;
605
- opacityValue = 1;
606
- } else {
607
- switch (animationState) {
608
- case "entering":
609
- translateX = config.animateIn.x;
610
- translateY = config.animateIn.y;
611
- scaleValue = 1;
612
- opacityValue = 0;
613
- break;
614
- case "entered":
584
+ };
585
+
586
+ const zIndexBase = Number(ANIMATION_CONFIG.Z_INDEX_BASE);
587
+
588
+ let isTopPosition = $derived(position?.startsWith("top-") ?? false);
589
+ let maxVisibleIndex = $derived(Math.max(0, visibleToasts - 1));
590
+ let visibleIndex = $derived(Math.min(index, maxVisibleIndex));
591
+ let defaultCollapsedOffset = $derived(
592
+ isTopPosition
593
+ ? index * ANIMATION_CONFIG.STACK_OFFSET
594
+ : -(index * ANIMATION_CONFIG.STACK_OFFSET),
595
+ );
596
+ let resolvedCollapsedOffset = $derived(
597
+ typeof collapsedOffset === "number" && Number.isFinite(collapsedOffset)
598
+ ? collapsedOffset
599
+ : defaultCollapsedOffset,
600
+ );
601
+ let resolvedHiddenCollapsedOffset = $derived(
602
+ typeof hiddenCollapsedOffset === "number" && Number.isFinite(hiddenCollapsedOffset)
603
+ ? hiddenCollapsedOffset
604
+ : resolvedCollapsedOffset,
605
+ );
606
+ let scale = $derived(
607
+ Math.max(ANIMATION_CONFIG.MIN_SCALE, 1 - index * ANIMATION_CONFIG.SCALE_FACTOR),
608
+ );
609
+ let visibleScale = $derived(
610
+ Math.max(
611
+ ANIMATION_CONFIG.MIN_SCALE,
612
+ 1 - visibleIndex * ANIMATION_CONFIG.SCALE_FACTOR,
613
+ ),
614
+ );
615
+ let zIndex = $derived(zIndexBase - renderIndex);
616
+ let stackHidden = $derived(index >= visibleToasts);
617
+ let hiddenByStacking = $derived(stackHidden && animationState !== "exiting");
618
+ let isStackLeader = $derived(index === 0);
619
+ let isLatest = $derived(isStackLeader && !shouldClose);
620
+ let config = $derived(
621
+ POSITION_CONFIGS[(position ?? "bottom-center") as ToastPosition],
622
+ );
623
+
624
+ let transformStyle = $derived.by(() => {
625
+ const baseOffsetY = stackHidden
626
+ ? resolvedHiddenCollapsedOffset
627
+ : resolvedCollapsedOffset;
628
+ const promotionOffset =
629
+ typeof expandedGap === "number" ? expandedGap : ANIMATION_CONFIG.EXPANDED_GAP;
630
+ const expandedTranslateY = isTopPosition ? expandedOffset : -expandedOffset;
631
+ const hiddenExpandedTranslateY = expandedTranslateY - promotionOffset;
632
+
633
+ let translateX = 0;
634
+ let translateY = baseOffsetY;
635
+ let scaleValue = stackHidden
636
+ ? visibleIndex === 0
637
+ ? 1
638
+ : visibleScale
639
+ : isStackLeader
640
+ ? 1
641
+ : scale;
642
+ let opacityValue = stackHidden ? 0 : 1;
643
+
644
+ if (stackHidden) {
645
+ if (expand && isGroupHovered && animationState !== "exiting") {
615
646
  translateX = 0;
616
- translateY = baseOffsetY;
647
+ translateY = hiddenExpandedTranslateY;
617
648
  scaleValue = 1;
618
- opacityValue = 1;
619
- break;
620
- case "exiting": {
621
- scaleValue = 1;
622
- opacityValue = 0;
623
- if (swipeDismissDirection) {
624
- switch (swipeDismissDirection) {
625
- case "left":
626
- translateX = -SWIPE_EXIT_DISTANCE;
627
- translateY = 0;
628
- break;
629
- case "right":
630
- translateX = SWIPE_EXIT_DISTANCE;
631
- translateY = 0;
632
- break;
633
- case "top":
634
- translateX = 0;
635
- translateY = -SWIPE_EXIT_DISTANCE;
636
- break;
637
- case "bottom":
638
- translateX = 0;
639
- translateY = SWIPE_EXIT_DISTANCE;
640
- break;
641
- default:
642
- translateX = config.animateOut.x;
643
- translateY = config.animateOut.y;
644
- break;
649
+ }
650
+ } else if (expand && isGroupHovered && animationState !== "exiting") {
651
+ translateX = 0;
652
+ translateY = expandedTranslateY;
653
+ scaleValue = 1;
654
+ opacityValue = 1;
655
+ } else {
656
+ switch (animationState) {
657
+ case "entering":
658
+ translateX = config.animateIn.x;
659
+ translateY = config.animateIn.y;
660
+ scaleValue = 1;
661
+ opacityValue = 0;
662
+ break;
663
+ case "entered":
664
+ translateX = 0;
665
+ translateY = baseOffsetY;
666
+ scaleValue = 1;
667
+ opacityValue = 1;
668
+ break;
669
+ case "exiting": {
670
+ scaleValue = 1;
671
+ opacityValue = 0;
672
+ if (swipeDismissDirection) {
673
+ switch (swipeDismissDirection) {
674
+ case "left":
675
+ translateX = -SWIPE_EXIT_DISTANCE;
676
+ translateY = 0;
677
+ break;
678
+ case "right":
679
+ translateX = SWIPE_EXIT_DISTANCE;
680
+ translateY = 0;
681
+ break;
682
+ case "top":
683
+ translateX = 0;
684
+ translateY = -SWIPE_EXIT_DISTANCE;
685
+ break;
686
+ case "bottom":
687
+ translateX = 0;
688
+ translateY = SWIPE_EXIT_DISTANCE;
689
+ break;
690
+ default:
691
+ translateX = config.animateOut.x;
692
+ translateY = config.animateOut.y;
693
+ break;
694
+ }
695
+ } else {
696
+ translateX = config.animateOut.x;
697
+ translateY = config.animateOut.y;
645
698
  }
646
- } else {
647
- translateX = config.animateOut.x;
648
- translateY = config.animateOut.y;
699
+ break;
649
700
  }
650
- break;
701
+ default:
702
+ translateX = 0;
703
+ translateY = baseOffsetY;
704
+ scaleValue = isStackLeader ? 1 : scale;
705
+ opacityValue = stackHidden ? 0 : 1;
706
+ break;
651
707
  }
708
+ }
709
+
710
+ const transform = `translate(calc(${translateX}px + var(--swipe-translate-x, 0px)), calc(${translateY}px + var(--swipe-translate-y, 0px))) scale(${scaleValue})`;
711
+
712
+ return {
713
+ transform,
714
+ opacity: opacityValue,
715
+ };
716
+ });
717
+
718
+ let transitionDuration = $derived.by(() => {
719
+ switch (animationState) {
720
+ case "entering":
721
+ case "entered":
722
+ return `${ANIMATION_CONFIG.ENTER_DURATION}s`;
723
+ case "exiting":
724
+ return `${ANIMATION_CONFIG.EXIT_DURATION}s`;
652
725
  default:
653
- translateX = 0;
654
- translateY = baseOffsetY;
655
- scaleValue = isStackLeader ? 1 : scale;
656
- opacityValue = stackHidden ? 0 : 1;
657
- break;
726
+ return `${ANIMATION_CONFIG.STACK_DURATION}s`;
658
727
  }
659
- }
728
+ });
729
+
730
+ let transitionTimingFunction = $derived(
731
+ animationState === "exiting"
732
+ ? ANIMATION_CONFIG.EASING_EXIT
733
+ : ANIMATION_CONFIG.EASING_DEFAULT,
734
+ );
660
735
 
661
- const transform = `translate(calc(${translateX}px + var(--swipe-translate-x, 0px)), calc(${translateY}px + var(--swipe-translate-y, 0px))) scale(${scaleValue})`;
736
+ let canSwipe = $derived(showClose && swipeDirections.length > 0);
737
+ let swipeCursorClass = $derived(
738
+ canSwipe ? (isSwiping ? "cursor-grabbing" : "cursor-grab") : undefined,
739
+ );
740
+
741
+ let titleId = $derived(title ? `${id}-title` : undefined);
742
+ let descriptionId = $derived(description ? `${id}-desc` : undefined);
743
+ let dialogRole = $derived(
744
+ variant === "destructive" || priority === "high" ? "alertdialog" : "dialog",
745
+ );
746
+ let livePoliteness = $derived(
747
+ (variant === "destructive" || priority === "high" ? "assertive" : "polite") as
748
+ | "assertive"
749
+ | "polite",
750
+ );
751
+
752
+ let offsetStyle = $derived.by(() => {
753
+ if (offset === undefined) return undefined;
754
+ const val = typeof offset === "number" ? `${offset}px` : offset;
755
+ if (position.startsWith("top")) return `top: ${val};`;
756
+ if (position.startsWith("bottom")) return `bottom: ${val};`;
757
+ return undefined;
758
+ });
759
+
760
+ const handleBlurCapture = (event: FocusEvent) => {
761
+ const next = event.relatedTarget as Node | null;
762
+ if (!toastRef || !next || !toastRef.contains(next)) {
763
+ isItemFocusVisible = false;
764
+ }
765
+ };
662
766
 
663
- return {
664
- transform,
665
- opacity: opacityValue,
767
+ const handleFocusCapture = (event: FocusEvent) => {
768
+ const target = event.target as Element | null;
769
+ if (target && isFocusVisible(target)) {
770
+ isItemFocusVisible = true;
771
+ }
666
772
  };
667
- });
668
-
669
- let transitionDuration = $derived.by(() => {
670
- switch (animationState) {
671
- case "entering":
672
- case "entered":
673
- return `${ANIMATION_CONFIG.ENTER_DURATION}s`;
674
- case "exiting":
675
- return `${ANIMATION_CONFIG.EXIT_DURATION}s`;
676
- default:
677
- return `${ANIMATION_CONFIG.STACK_DURATION}s`;
678
- }
679
- });
680
-
681
- let transitionTimingFunction = $derived(
682
- animationState === "exiting"
683
- ? ANIMATION_CONFIG.EASING_EXIT
684
- : ANIMATION_CONFIG.EASING_DEFAULT,
685
- );
686
-
687
- let canSwipe = $derived(showClose && swipeDirections.length > 0);
688
- let swipeCursorClass = $derived(
689
- canSwipe ? (isSwiping ? "cursor-grabbing" : "cursor-grab") : undefined,
690
- );
691
-
692
- let titleId = $derived(title ? `${id}-title` : undefined);
693
- let descriptionId = $derived(description ? `${id}-desc` : undefined);
694
- let liveRole = $derived(variant === "destructive" ? "alert" : "status");
695
- let livePoliteness = $derived(
696
- (variant === "destructive" ? "assertive" : "polite") as "assertive" | "polite",
697
- );
698
-
699
- let offsetStyle = $derived.by(() => {
700
- if (offset === undefined) return undefined;
701
- const val = typeof offset === "number" ? `${offset}px` : offset;
702
- if (position.startsWith("top")) return `top: ${val};`;
703
- if (position.startsWith("bottom")) return `bottom: ${val};`;
704
- return undefined;
705
- });
706
-
707
- const handleBlurCapture = (event: FocusEvent) => {
708
- const next = event.relatedTarget as Node | null;
709
- if (!toastRef || !next || !toastRef.contains(next)) {
710
- isItemHovered = false;
711
- }
712
- };
713
773
  </script>
714
774
 
715
775
  <div
716
- bind:this={toastRef}
717
- class={cn(
718
- toastContainerVariants({ position, variant }),
719
- className,
720
- swipeCursorClass,
721
- stackHidden && "pointer-events-none",
722
- )}
723
- style:transform-origin={position?.startsWith("top-")
724
- ? "center top"
725
- : "center bottom"}
726
- style:z-index={zIndex}
727
- style:transition={isSwiping
728
- ? `transform 0s linear, opacity ${transitionDuration} ${transitionTimingFunction}`
729
- : `transform ${transitionDuration} ${transitionTimingFunction}, opacity ${transitionDuration} ${transitionTimingFunction}`}
730
- style:transform={transformStyle.transform}
731
- style:opacity={transformStyle.opacity}
732
- style={offsetStyle}
733
- role={stackHidden ? undefined : liveRole}
734
- aria-live={stackHidden ? undefined : livePoliteness}
735
- aria-atomic={stackHidden ? undefined : "true"}
736
- aria-describedby={stackHidden ? undefined : descriptionId}
737
- aria-hidden={stackHidden ? true : undefined}
738
- tabindex="-1"
739
- ontransitionend={handleTransitionEnd}
740
- data-toast-id={id}
776
+ bind:this={toastRef}
777
+ class={cn(
778
+ toastContainerVariants({ position, variant }),
779
+ className,
780
+ swipeCursorClass,
781
+ stackHidden && "pointer-events-none",
782
+ )}
783
+ style:transform-origin={position?.startsWith("top-") ? "center top" : "center bottom"}
784
+ style:z-index={zIndex}
785
+ style:transition={isSwiping
786
+ ? `transform 0s linear, opacity ${transitionDuration} ${transitionTimingFunction}`
787
+ : `transform ${transitionDuration} ${transitionTimingFunction}, opacity ${transitionDuration} ${transitionTimingFunction}`}
788
+ style:transform={transformStyle.transform}
789
+ style:opacity={transformStyle.opacity}
790
+ style={offsetStyle}
791
+ role={stackHidden ? undefined : dialogRole}
792
+ aria-live={stackHidden ? undefined : livePoliteness}
793
+ aria-atomic={stackHidden ? undefined : "true"}
794
+ aria-modal={stackHidden ? undefined : "false"}
795
+ aria-labelledby={stackHidden ? undefined : titleId}
796
+ aria-describedby={stackHidden ? undefined : descriptionId}
797
+ aria-hidden={stackHidden ? true : undefined}
798
+ tabindex="-1"
799
+ ontransitionend={handleTransitionEnd}
800
+ data-toast-id={id}
801
+ data-priority={priority}
741
802
  >
742
- <div
743
- role="presentation"
744
- class={cn(swipeCursorClass, "touch-none")}
745
- aria-busy={isLoading ? "true" : undefined}
746
- onpointerdown={handlePointerDown}
747
- onpointermove={handlePointerMove}
748
- onpointerup={handlePointerUp}
749
- onpointercancel={handlePointerCancel}
750
- onmouseenter={() => {
751
- isItemHovered = true;
752
- onGroupHoverEnter?.();
753
- }}
754
- onmouseleave={() => (isItemHovered = false)}
755
- onfocuscapture={() => (isItemHovered = true)}
756
- onblurcapture={handleBlurCapture}
757
- >
803
+ <div
804
+ role="presentation"
805
+ class={cn(swipeCursorClass, "touch-none")}
806
+ aria-busy={isLoading ? "true" : undefined}
807
+ onpointerdown={handlePointerDown}
808
+ onpointermove={handlePointerMove}
809
+ onpointerup={handlePointerUp}
810
+ onpointercancel={handlePointerCancel}
811
+ onmouseenter={() => {
812
+ isItemHovered = true;
813
+ onGroupHoverEnter?.();
814
+ }}
815
+ onmouseleave={() => (isItemHovered = false)}
816
+ onfocuscapture={handleFocusCapture}
817
+ onblurcapture={handleBlurCapture}
818
+ >
758
819
  {#if toast.component}
759
820
  {@const Component = toast.component}
760
821
  <Component {id} {toast} {...toast.componentProps} />
@@ -765,22 +826,22 @@ const handleBlurCapture = (event: FocusEvent) => {
765
826
  type="button"
766
827
  onclick={() => handleClose()}
767
828
  class={cn(
768
- "absolute top-2 end-2 cursor-pointer rounded-vs-sm p-1 text-vs-foreground/45 hover:bg-vs-popover-muted hover:text-vs-foreground/70 transition-[background-color,color,box-shadow] ease-vs-button duration-100 focus-visible:ring-1 focus-visible:ring-vs-ring/50 focus-visible:outline-none",
829
+ "rounded-vs-sm text-vs-foreground/45 hover:bg-vs-popover-muted hover:text-vs-foreground/70 ease-vs-button focus-visible:ring-vs-ring/50 absolute end-2 top-2 cursor-pointer p-1 transition-[background-color,color,box-shadow] duration-100 focus-visible:ring-1 focus-visible:outline-none",
769
830
  )}
770
831
  aria-label="Close toast"
771
832
  >
772
833
  <svg
773
834
  aria-hidden="true"
774
835
  class="h-4 w-4"
775
- viewBox="0 0 24 24"
836
+ viewBox="0 0 18 18"
776
837
  fill="none"
777
838
  stroke="currentColor"
778
- stroke-width="2"
839
+ stroke-width="1.5"
779
840
  stroke-linecap="round"
780
841
  stroke-linejoin="round"
781
842
  >
782
- <line x1="18" x2="6" y1="6" y2="18" />
783
- <line x1="6" x2="18" y1="6" y2="18" />
843
+ <line x1="14" y1="4" x2="4" y2="14" />
844
+ <line x1="4" y1="4" x2="14" y2="14" />
784
845
  </svg>
785
846
  </button>
786
847
  {/if}
@@ -788,7 +849,9 @@ const handleBlurCapture = (event: FocusEvent) => {
788
849
  <div class="p-4 pe-8">
789
850
  <div class="flex gap-3">
790
851
  {#if showStatusIcon}
791
- <span class="relative inline-flex h-4 w-4 shrink-0 items-center justify-center">
852
+ <span
853
+ class="relative inline-flex h-4 w-4 shrink-0 items-center justify-center"
854
+ >
792
855
  {#if shouldRenderSpinner}
793
856
  <span
794
857
  class={cn(
@@ -803,21 +866,45 @@ const handleBlurCapture = (event: FocusEvent) => {
803
866
  onanimationend={handleSpinnerAnimationEnd}
804
867
  >
805
868
  <svg
806
- viewBox="0 0 256 256"
869
+ viewBox="0 0 18 18"
807
870
  fill="none"
808
871
  stroke="currentColor"
809
- stroke-width="24"
872
+ stroke-width="1.5"
810
873
  stroke-linecap="round"
811
874
  stroke-linejoin="round"
812
875
  >
813
- <line x1="128" y1="32" x2="128" y2="64" />
814
- <line x1="195.9" y1="60.1" x2="173.3" y2="82.7" />
815
- <line x1="224" y1="128" x2="192" y2="128" />
816
- <line x1="195.9" y1="195.9" x2="173.3" y2="173.3" />
817
- <line x1="128" y1="224" x2="128" y2="192" />
818
- <line x1="60.1" y1="195.9" x2="82.7" y2="173.3" />
819
- <line x1="32" y1="128" x2="64" y2="128" />
820
- <line x1="60.1" y1="60.1" x2="82.7" y2="82.7" />
876
+ <line x1="9" y1="1.75" x2="9" y2="4.25" />
877
+ <line
878
+ x1="14.127"
879
+ y1="3.873"
880
+ x2="12.359"
881
+ y2="5.641"
882
+ opacity="0.88"
883
+ />
884
+ <line x1="16.25" y1="9" x2="13.75" y2="9" opacity="0.75" />
885
+ <line
886
+ x1="14.127"
887
+ y1="14.127"
888
+ x2="12.359"
889
+ y2="12.359"
890
+ opacity="0.63"
891
+ />
892
+ <line x1="9" y1="16.25" x2="9" y2="13.75" opacity="0.5" />
893
+ <line
894
+ x1="3.873"
895
+ y1="14.127"
896
+ x2="5.641"
897
+ y2="12.359"
898
+ opacity="0.38"
899
+ />
900
+ <line x1="1.75" y1="9" x2="4.25" y2="9" opacity="0.25" />
901
+ <line
902
+ x1="3.873"
903
+ y1="3.873"
904
+ x2="5.641"
905
+ y2="5.641"
906
+ opacity="0.13"
907
+ />
821
908
  </svg>
822
909
  </span>
823
910
  {/if}
@@ -833,26 +920,29 @@ const handleBlurCapture = (event: FocusEvent) => {
833
920
  viewBox={iconConfig.viewBox}
834
921
  fill="none"
835
922
  stroke="currentColor"
836
- stroke-width="2"
923
+ stroke-width="1.5"
837
924
  stroke-linecap="round"
838
925
  stroke-linejoin="round"
839
926
  >
840
927
  {#each iconConfig.elements as element, elementIndex (elementIndex)}
841
928
  {#if element.tag === "path"}
842
- <path d={element.d} />
929
+ {#if element.fill}
930
+ <path d={element.d} fill="currentColor" stroke="none" />
931
+ {:else}
932
+ <path d={element.d} />
933
+ {/if}
843
934
  {:else if element.tag === "line"}
935
+ {@const lineEl = element as LineElement}
936
+
844
937
  <line
845
- x1={element.x1}
846
- y1={element.y1}
847
- x2={element.x2}
848
- y2={element.y2}
849
- />
850
- {:else if element.tag === "circle"}
851
- <circle
852
- cx={element.cx}
853
- cy={element.cy}
854
- r={element.r}
938
+ x1={lineEl.x1}
939
+ y1={lineEl.y1}
940
+ x2={lineEl.x2}
941
+ y2={lineEl.y2}
942
+ opacity={lineEl.opacity ?? undefined}
855
943
  />
944
+ {:else if element.tag === "polyline"}
945
+ <polyline points={element.points} />
856
946
  {/if}
857
947
  {/each}
858
948
  </svg>
@@ -872,7 +962,7 @@ const handleBlurCapture = (event: FocusEvent) => {
872
962
  {#if description}
873
963
  <div
874
964
  id={descriptionId}
875
- class="text-sm leading-snug text-vs-foreground/70 text-balance select-none"
965
+ class="text-vs-foreground/70 text-sm leading-snug text-balance select-none"
876
966
  >
877
967
  {description}
878
968
  </div>
@@ -885,7 +975,7 @@ const handleBlurCapture = (event: FocusEvent) => {
885
975
  action.onClick();
886
976
  handleClose();
887
977
  }}
888
- class="relative inline-flex cursor-pointer items-center justify-center rounded-vs-md px-3 py-1.5 text-sm font-medium bg-vs-foreground text-vs-popover shadow-vs-button transition-[background-color,color,box-shadow] ease-vs-button duration-100 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-vs-ring-offset/50 focus-visible:outline-none focus-visible:ring-vs-ring/50"
978
+ class="rounded-vs-md bg-vs-foreground hover:bg-vs-foreground/90 text-vs-popover shadow-vs-button ease-vs-button focus-visible:ring-offset-vs-ring-offset/50 focus-visible:ring-vs-ring/50 border-vs-foreground relative inline-flex cursor-pointer items-center justify-center border px-3 py-1.5 text-sm font-medium transition-[background-color,color,box-shadow] duration-100 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-none"
889
979
  >
890
980
  {action.label}
891
981
  </button>
@@ -896,5 +986,5 @@ const handleBlurCapture = (event: FocusEvent) => {
896
986
  </div>
897
987
  </div>
898
988
  {/if}
899
- </div>
989
+ </div>
900
990
  </div>