varsel 0.1.0

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