varsel 0.4.0 → 0.5.1

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