import { useEffect, useRef, useState, type CSSProperties, type ReactNode, type RefObject, } from "react"; import { XIcon } from "lucide-react"; import { cn } from "~/lib/utils"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "../ui/alert"; import { Button } from "../ui/button"; const DISMISS_TRANSITION_MS = 220; const frontExitStyle = { opacity: 0, transform: "translate3d(0, 4rem, 0)", } satisfies CSSProperties; const stackedExitStyle = { opacity: 0, transform: "translate3d(0, 7rem, 0)", } satisfies CSSProperties; const restingStyle = { opacity: 1, transform: "translate3d(0, 0, 0)", } satisfies CSSProperties; const exitTransitionStyle = { transition: `transform ${DISMISS_TRANSITION_MS}ms ease-in, opacity ${DISMISS_TRANSITION_MS}ms ease-in`, willChange: "transform, opacity", } satisfies CSSProperties; export interface ComposerBannerStackItem { readonly id: string; readonly variant: "error" | "info" | "success" | "warning"; readonly icon: ReactNode; readonly title: ReactNode; readonly description?: ReactNode; readonly actions?: ReactNode; readonly dismissLabel?: string; readonly onDismiss?: () => void; } function useDismissTimeoutCleanup(timeoutRef: RefObject | null>) { useEffect(() => { return () => clearDismissTimeout(timeoutRef); }, [timeoutRef]); } function clearDismissTimeout(timeoutRef: RefObject | null>) { const timeout = timeoutRef.current; if (timeout) { clearTimeout(timeout); } } interface ComposerBannerStackProps { readonly className?: string; readonly items: ReadonlyArray; } export function ComposerBannerStack({ className, items }: ComposerBannerStackProps) { const [exitingItemId, setExitingItemId] = useState(null); const dismissTimeoutRef = useRef | null>(null); const activeExitingItemId = exitingItemId && items.some((item) => item.id === exitingItemId) ? exitingItemId : null; useDismissTimeoutCleanup(dismissTimeoutRef); if (items.length === 0) { return null; } const frontItem = items[0]; if (!frontItem) { return null; } const stackedItems = items.slice(1); const hasStack = stackedItems.length > 0; const showCollapsedStackCap = hasStack && activeExitingItemId !== frontItem.id; const requestDismiss = (item: ComposerBannerStackItem) => { if (!item.onDismiss || activeExitingItemId) { return; } setExitingItemId(item.id); if (dismissTimeoutRef.current) { clearTimeout(dismissTimeoutRef.current); } dismissTimeoutRef.current = setTimeout(() => { dismissTimeoutRef.current = null; item.onDismiss?.(); }, DISMISS_TRANSITION_MS); }; return (
{showCollapsedStackCap ? (
); } function ComposerBannerStackAlert({ item, exiting, onDismissRequest, }: { readonly item: ComposerBannerStackItem; readonly exiting: boolean; readonly onDismissRequest: () => void; }) { const dismissOnly = item.onDismiss && !item.actions; return ( {item.icon} {item.title} {item.description ? {item.description} : null} {item.actions || item.onDismiss ? ( {item.actions} {item.onDismiss ? ( ) : null} ) : null} ); }