Thomas G. Lopes
toasts
5500bfc
<script lang="ts">
import { fly } from "svelte/transition";
import { toaster } from "./toaster.svelte.js";
import { Progress } from "melt/components";
import Close from "~icons/carbon/close";
import { omit } from "$lib/utils/object.js";
import { session } from "$lib/state/session.svelte.js";
import { AnimationFrames } from "runed";
let toastHeights = $state<number[]>([]);
new AnimationFrames(() => {
const rootEl = document.getElementById(toaster.root.id);
if (!rootEl) return;
const toastEls = Array.from(rootEl.querySelectorAll("[data-melt-toaster-toast-content]"));
toastHeights = toastEls.map(el => el.clientHeight);
// console.log(toastHeights);
});
const isComparing = $derived(session.project.conversations.length > 1);
const GAP = 8;
function getToastStyle(i: number) {
// Remember, the order is reversed! Meaning i=0 was the first toast, so its the last
// we want to show.
const n = toaster.toasts.length - i - 1;
if (n === 0) return "";
const reversedHeights = toastHeights.toReversed();
const yHover = -1 * reversedHeights.slice(0, n).reduce((a, b) => a + b + GAP, 0);
const y = -n * 10;
return `
--y-hover: ${yHover}px;
--y: ${y}px;
`;
}
function getRootStyle() {
const heightHover = toastHeights.reduce((a, b) => a + b + GAP, 0);
return `
--h-hover: ${heightHover}px;
`;
}
</script>
<div
{...omit(toaster.root, "popover")}
class={["absolute right-2 bottom-23 flex w-[300px] flex-col ", !isComparing && "md:right-0"]}
style:--toasts={toaster.toasts.length}
style={getRootStyle()}
>
{#each toaster.toasts as toast, i (toast.id)}
<div
class="flex w-full flex-col justify-center rounded-xl bg-white px-4 py-4 text-left transition dark:bg-gray-800"
{...toast.content}
style:--n={toaster.toasts.length - i}
in:fly={{ y: 20, opacity: 0 }}
out:fly={{ y: 20 }}
style={getToastStyle(i)}
>
<h3 {...toast.title} class="text-sm font-semibold whitespace-nowrap text-gray-700 dark:text-gray-300">
{toast.data.title}
</h3>
{#if toast.data.description}
<p {...toast.description} class="max-w-[200px] text-xs text-gray-700 dark:text-gray-300">
{toast.data.description}
</p>
{/if}
<button
{...toast.close}
aria-label="dismiss toast"
class="absolute top-2 right-2 bg-transparent text-gray-300 hover:text-gray-400 dark:hover:text-gray-100"
>
<Close class="size-4" />
</button>
{#if toast.closeDelay !== 0}
<div class="absolute right-4 bottom-4 h-[4px] w-[30px] overflow-hidden rounded-full">
<Progress value={toast.percentage}>
{#snippet children(progress)}
<div {...progress.root} class="relative h-full w-full overflow-hidden bg-gray-200 dark:bg-gray-950">
<div
{...progress.progress}
class="h-full w-full -translate-x-(--progress)"
class:bg-green-400={toast.data.variant === "success"}
class:bg-orange-400={toast.data.variant === "warning"}
class:bg-red-500={toast.data.variant === "error"}
></div>
</div>
{/snippet}
</Progress>
</div>
{/if}
</div>
{/each}
</div>
<style>
:global([popover]) {
inset: unset;
}
[data-melt-toaster-root] {
--gap: 0.75rem;
--hover-offset: 0rem;
/* --toast-height: 4.5rem; */
--hidden-offset: 0.75rem;
--hidden-toasts: calc(var(--toasts) - 1);
overflow: visible;
gap: 0;
background: unset;
padding: 0;
border: none;
height: var(--h);
}
[data-melt-toaster-root]:hover {
height: var(--h-hover);
}
[data-melt-toaster-toast-content] {
position: absolute;
pointer-events: auto;
bottom: 0;
left: 0;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
transform-origin: 50% 0%;
transition: all 350ms ease;
translate: 0 var(--y);
}
:global(.dark [data-melt-toaster-toast-content]) {
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.25);
}
[data-melt-toaster-toast-content]:nth-last-child(n + 4) {
z-index: 1;
scale: 0.925;
opacity: 0;
}
[data-melt-toaster-toast-content]:nth-last-child(-n + 3) {
z-index: 2;
scale: 0.95;
}
[data-melt-toaster-toast-content]:nth-last-child(-n + 2) {
z-index: 3;
scale: 0.975;
}
[data-melt-toaster-toast-content]:nth-last-child(-n + 1) {
z-index: 4;
scale: 1;
}
[data-melt-toaster-root]:hover [data-melt-toaster-toast-content] {
scale: 1;
opacity: 1;
translate: 0 var(--y-hover);
}
</style>