import type { ShowToastRequest } from "@yaakapp-internal/plugins"; import { Icon, type IconProps, VStack } from "@yaakapp-internal/ui"; import classNames from "classnames"; import * as m from "motion/react-m"; import type { ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useKey } from "react-use"; import { IconButton } from "./IconButton"; export interface ToastProps { children: ReactNode; open: boolean; onClose: () => void; className?: string; timeout: number | null; action?: (args: { hide: () => void }) => ReactNode; icon?: ShowToastRequest["icon"] | null; color?: ShowToastRequest["color"]; // Grow with the content (up to the viewport) instead of scrolling internally // past the default max height dynamicHeight?: boolean; // Hide the close button, for toasts that render their own dismiss action. // Escape still closes the toast hideDismiss?: boolean; } const ICONS: Record, IconProps["icon"] | null> = { custom: null, danger: "alert_triangle", info: "info", notice: "alert_triangle", primary: "info", secondary: "info", success: "check_circle", warning: "alert_triangle", }; export function Toast({ children, open, onClose, timeout, action, icon, color, dynamicHeight, hideDismiss, }: ToastProps) { const onCloseRef = useRef(onClose); const timeoutRef = useRef | null>(null); const [autoHideCanceled, setAutoHideCanceled] = useState(false); useEffect(() => { onCloseRef.current = onClose; }, [onClose]); const cancelAutoHide = useCallback(() => { if (timeoutRef.current == null) return; clearTimeout(timeoutRef.current); timeoutRef.current = null; setAutoHideCanceled(true); }, []); useEffect(() => { if (!open || timeout == null || autoHideCanceled) return; timeoutRef.current = setTimeout(() => { timeoutRef.current = null; onCloseRef.current(); }, timeout); return () => { if (timeoutRef.current == null) return; clearTimeout(timeoutRef.current); timeoutRef.current = null; }; }, [autoHideCanceled, open, timeout]); useKey( "Escape", () => { if (!open) return; onClose(); }, {}, [open], ); const toastIcon = icon === null ? null : (icon ?? (color && color in ICONS && ICONS[color])); return (
{toastIcon && }
{children}
{action?.({ hide: onClose })}
{!hideDismiss && ( )} {timeout != null && !autoHideCanceled && (
)}
); }