Files
fn_registry/frontend/functions/ui/toast.tsx
T
egutierrez 35bcb63300 feat: nuevos componentes UI — accordion, avatar, breadcrumb, checkbox, command, dropdown, pagination, popover, radio, sheet, select, switch, textarea, toast
Componentes React accesibles basados en Radix UI con soporte de temas via CSS variables. Incluye barrel export en index.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:32 +02:00

171 lines
5.5 KiB
TypeScript

"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { XIcon, CheckCircle2Icon, AlertCircleIcon, AlertTriangleIcon, InfoIcon } from "lucide-react"
import { cn } from "../core/cn"
const toastVariants = cva(
"group/toast pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-xl border p-4 pr-8 shadow-lg transition-all",
{
variants: {
variant: {
default: "bg-background text-foreground border-border",
success: "bg-background border-l-4 border-l-green-500 border-border text-foreground",
error: "bg-background border-l-4 border-l-destructive border-border text-foreground",
warning: "bg-background border-l-4 border-l-yellow-500 border-border text-foreground",
info: "bg-background border-l-4 border-l-blue-500 border-border text-foreground",
},
},
defaultVariants: { variant: "default" },
}
)
const variantIcons: Record<string, React.ReactNode> = {
success: <CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-green-500" />,
error: <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />,
warning: <AlertTriangleIcon className="mt-0.5 size-4 shrink-0 text-yellow-500" />,
info: <InfoIcon className="mt-0.5 size-4 shrink-0 text-blue-500" />,
}
interface ToastProps
extends React.ComponentPropsWithoutRef<"div">,
VariantProps<typeof toastVariants> {
title?: string
description?: string
action?: React.ReactNode
onClose?: () => void
}
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
({ className, variant = "default", title, description, action, onClose, children, ...props }, ref) => {
return (
<div
ref={ref}
data-slot="toast"
data-variant={variant}
role="alert"
aria-live="polite"
className={cn(toastVariants({ variant }), className)}
{...props}
>
{variant && variant !== "default" && variantIcons[variant]}
<div className="flex flex-1 flex-col gap-1">
{title && (
<div data-slot="toast-title" className="text-sm font-semibold leading-none">
{title}
</div>
)}
{description && (
<div data-slot="toast-description" className="text-sm text-muted-foreground">
{description}
</div>
)}
{children}
{action && <div data-slot="toast-action" className="mt-2">{action}</div>}
</div>
{onClose && (
<button
data-slot="toast-close"
onClick={onClose}
className="absolute top-2 right-2 inline-flex size-5 items-center justify-center rounded-md text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label="Close"
>
<XIcon className="size-3.5" />
</button>
)}
</div>
)
}
)
Toast.displayName = "Toast"
interface ToastViewportProps extends React.ComponentPropsWithoutRef<"div"> {
position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"
}
const positionClasses: Record<NonNullable<ToastViewportProps["position"]>, string> = {
"top-left": "top-4 left-4",
"top-center": "top-4 left-1/2 -translate-x-1/2",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-center": "bottom-4 left-1/2 -translate-x-1/2",
"bottom-right": "bottom-4 right-4",
}
function ToastViewport({ className, position = "bottom-right", ...props }: ToastViewportProps) {
return (
<div
data-slot="toast-viewport"
className={cn(
"fixed z-[100] flex max-h-screen w-full max-w-sm flex-col gap-2 p-4",
positionClasses[position],
className
)}
{...props}
/>
)
}
type ToastEntry = ToastProps & {
id: string
duration?: number
}
interface ToastProviderContextValue {
toasts: ToastEntry[]
toast: (props: Omit<ToastEntry, "id">) => string
dismiss: (id: string) => void
dismissAll: () => void
}
const ToastContext = React.createContext<ToastProviderContextValue | null>(null)
function ToastProvider({ children, position = "bottom-right" }: { children: React.ReactNode; position?: ToastViewportProps["position"] }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([])
const dismiss = React.useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, [])
const dismissAll = React.useCallback(() => {
setToasts([])
}, [])
const toast = React.useCallback(
(props: Omit<ToastEntry, "id">) => {
const id = Math.random().toString(36).slice(2)
const duration = props.duration ?? 5000
setToasts((prev) => [...prev, { ...props, id }])
if (duration > 0) {
setTimeout(() => dismiss(id), duration)
}
return id
},
[dismiss]
)
return (
<ToastContext.Provider value={{ toasts, toast, dismiss, dismissAll }}>
{children}
<ToastViewport position={position}>
{toasts.map((t) => {
const { id, duration: _duration, ...rest } = t
return (
<Toast key={id} {...rest} onClose={() => dismiss(id)} />
)
})}
</ToastViewport>
</ToastContext.Provider>
)
}
function useToast() {
const ctx = React.useContext(ToastContext)
if (!ctx) throw new Error("useToast must be used within ToastProvider")
return ctx
}
export { Toast, ToastProvider, ToastViewport, toastVariants, useToast }
export type { ToastEntry, ToastProps, ToastViewportProps }