"use client"; import { CircleCheck, Info, type LucideIcon, OctagonAlert, TriangleAlert, X, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type ComponentPropsWithoutRef, type ReactNode, createContext, useContext, useState, } from "react"; import { EASE_OUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type AlertVariant = "info" | "success" | "warning" | "destructive"; // Each tone is mixed toward --foreground: darker in light mode, lighter in // dark mode, so icon and title tint keep contrast on either background while // still coming from the theme tokens. const VARIANT_CLASS: Record = { info: { root: "border-[color-mix(in_oklab,var(--chart-1)_30%,transparent)] bg-[color-mix(in_oklab,var(--chart-1)_8%,var(--background))]", icon: "text-[color-mix(in_oklab,var(--chart-1)_75%,var(--foreground))]", }, success: { root: "border-[color-mix(in_oklab,var(--success)_32%,transparent)] bg-[color-mix(in_oklab,var(--success)_9%,var(--background))]", icon: "text-[color-mix(in_oklab,var(--success)_62%,var(--foreground))]", }, warning: { root: "border-[color-mix(in_oklab,var(--warning)_40%,transparent)] bg-[color-mix(in_oklab,var(--warning)_10%,var(--background))]", icon: "text-[color-mix(in_oklab,var(--warning)_55%,var(--foreground))]", }, destructive: { root: "border-[color-mix(in_oklab,var(--destructive)_35%,transparent)] bg-[color-mix(in_oklab,var(--destructive)_8%,var(--background))]", icon: "text-[color-mix(in_oklab,var(--destructive)_75%,var(--foreground))]", }, }; const VARIANT_ICON: Record = { info: Info, success: CircleCheck, warning: TriangleAlert, destructive: OctagonAlert, }; const StackContext = createContext(false); export interface AlertProps extends Omit, "title" | "role"> { variant?: AlertVariant; title?: ReactNode; description?: ReactNode; /** Replace the variant icon; `null` hides it. */ icon?: ReactNode | null; /** Buttons rendered under the text (e.g. library `Button size="sm"`). */ actions?: ReactNode; /** Show a dismiss (X) button. */ dismissible?: boolean; dismissLabel?: string; /** Controlled visibility. Leave undefined to let the Alert hide itself on dismiss. */ open?: boolean; /** Called when the dismiss button is pressed. */ onDismiss?: () => void; } /** * Inline banner for info / success / warning / destructive messages. * Info and success announce politely (`role="status"`); warning and * destructive interrupt (`role="alert"`). Dismissing fades the banner and * collapses its height so content below closes the gap. */ export function Alert({ variant = "info", title, description, icon, actions, dismissible = false, dismissLabel = "Dismiss", open: openProp, onDismiss, className, children, ...rest }: AlertProps) { const reduce = useReducedMotion() ?? false; const inStack = useContext(StackContext); const [internalOpen, setInternalOpen] = useState(true); const open = openProp ?? internalOpen; const Icon = VARIANT_ICON[variant]; const tone = VARIANT_CLASS[variant]; const urgent = variant === "warning" || variant === "destructive"; const dismiss = () => { if (openProp === undefined) setInternalOpen(false); onDismiss?.(); }; // Height is animated on purpose: the collapse is the component's job, so the // content below slides up instead of jumping. Under reduced motion the // banner only fades, then the height snaps once it is invisible. const collapsed = reduce ? { opacity: 0, height: 0, transition: { opacity: { duration: 0.15 }, height: { delay: 0.15, duration: 0 } }, } : { opacity: 0, height: 0, transition: { opacity: { duration: 0.14, ease: EASE_OUT }, height: { duration: 0.24, ease: EASE_OUT }, }, }; return ( {open ? (
{icon === null ? null : ( )}
{title ? (

{title}

) : null} {description ? (
{description}
) : null} {children} {actions ?
{actions}
: null}
{dismissible ? ( ) : null}
) : null}
); } export interface AlertStackProps extends ComponentPropsWithoutRef<"div"> {} /** * Vertical list of Alerts whose spacing collapses together with a dismissed * alert (the gap lives inside each alert's collapsing wrapper, not in `gap`). */ export function AlertStack({ className, children, ...rest }: AlertStackProps) { return (
{children}
); }