56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
export type NotificationTone = "info" | "success" | "danger";
|
|
|
|
export type NotificationItem = {
|
|
id: string;
|
|
title: string;
|
|
message?: string;
|
|
tone: NotificationTone;
|
|
closing: boolean;
|
|
};
|
|
|
|
type NotificationsToasterProps = {
|
|
items: NotificationItem[];
|
|
onDismiss: (id: string) => void;
|
|
};
|
|
|
|
function toneClasses(tone: NotificationTone) {
|
|
if (tone === "success") return "border-emerald-400/40 text-emerald-200";
|
|
if (tone === "danger") return "border-red-400/50 text-red-200";
|
|
return "border-accent-weak text-[color:var(--color-text)]";
|
|
}
|
|
|
|
export default function NotificationsToaster({ items, onDismiss }: NotificationsToasterProps) {
|
|
if (!items.length) return null;
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-[60] flex w-80 flex-col-reverse gap-2 pointer-events-none">
|
|
{items.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className={`pointer-events-auto rounded-xl border bg-panel px-3 py-2 text-sm shadow-lg transition-all duration-300 ${toneClasses(item.tone)} ${item.closing ? "opacity-0 translate-y-2" : "opacity-100"}`}
|
|
role="status"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div>
|
|
<div className="font-semibold">{item.title}</div>
|
|
{item.message ? <div className="text-xs text-soft">{item.message}</div> : null}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="rounded-md border border-accent-weak px-1.5 py-0.5 text-xs text-soft hover:border-accent"
|
|
onClick={() => onDismiss(item.id)}
|
|
aria-label="Dismiss notification"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|