File size: 2,126 Bytes
cf47645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9104321
cf47645
 
 
 
 
 
9104321
cf47645
 
 
 
 
 
9104321
cf47645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9104321
 
 
 
 
 
cf47645
 
 
 
 
 
9104321
 
 
cf47645
 
 
 
 
 
 
 
 
9104321
cf47645
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<script lang="ts">
	import { onDestroy, type Snippet } from "svelte";
	import { computePosition, autoUpdate } from "@floating-ui/dom";
	import { fly } from "svelte/transition";

	interface Props {
		children: Snippet<[{ addToast: typeof addToast; trigger: typeof trigger }]>;
		closeDelay?: number;
	}
	const { children, closeDelay = 2000 }: Props = $props();

	const id = $props.id();

	const trigger = {
		id,
	} as const;

	type Toast = {
		content: string;
		variant: "info" | "danger";
		id: string;
	};

	let toasts = $state<Toast[]>([]);
	let timeouts: ReturnType<typeof window.setTimeout>[] = [];

	function addToast(content: string, variant?: Toast["variant"]) {
		const id = crypto.randomUUID();
		const timeout = setTimeout(() => {
			toasts = toasts.filter(t => t.id !== id);
			timeouts = timeouts.filter(t => t !== timeout);
		}, closeDelay);

		toasts.push({ content, id, variant: variant ?? "info" });
		timeouts.push(timeout);
	}

	onDestroy(() => {
		timeouts.forEach(t => clearTimeout(t));
	});

	function float(node: HTMLElement) {
		const triggerEl = document.getElementById(trigger.id);
		if (!triggerEl) return;

		const compute = () =>
			computePosition(triggerEl, node, {
				placement: "top",
				strategy: "absolute",
			}).then(({ x, y }) => {
				Object.assign(node.style, {
					left: `${x}px`,
					top: `${y - 8}px`,
				});
			});

		return {
			destroy: autoUpdate(triggerEl, node, compute),
		};
	}

	const classMap: Record<Toast["variant"], string> = {
		info: "border border-blue-400 bg-gradient-to-b from-blue-500 to-blue-600",

		danger: "border border-red-400 bg-gradient-to-b from-red-500 to-red-600",
	};
</script>

{@render children({ trigger, addToast })}

{#each toasts as toast (toast.id)}
	<div
		data-local-toast
		data-variant={toast.variant}
		class="rounded-full px-2 py-1 text-xs {classMap[toast.variant]}"
		in:fly={{ y: 10 }}
		out:fly={{ y: -4 }}
		use:float
	>
		{toast.content}
	</div>
{/each}

<style>
	[data-local-toast] {
		/* Float on top of the UI */
		position: absolute;

		/* Avoid layout interference */
		width: max-content;
		top: 0;
		left: 0;
	}
</style>