'use client' import type React from 'react' import { useId, useEffect } from 'react' import { toast as sonnerToast, Toaster } from 'sonner' import { X, Info, CircleCheckBig, TriangleAlert, CircleAlert, } from 'lucide-react' import Block from '../Primitives/Block/Block' import Text from '../Text/Text' import PrimitiveButton from '../Primitives/PrimitiveButton/PrimitiveButton' import { type AddToastOptions, type CustomToastProps, SnackbarVariant, SnackbarPosition, SnackbarProps, SnackbarIconProps, } from './types' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { dismissNonPersistentToasts, SnackbarHiddenToastStyles, useVisibleToastCount, } from '../../utils/snackbar-shared' import { SnackbarTokens } from './snackbar.tokens' const SnackbarIcon: React.FC = ({ variant }) => { const snackbarTokens = useResponsiveTokens('SNACKBAR') const props = { color: snackbarTokens.infoIcon.color[variant], size: snackbarTokens.infoIcon.height, 'aria-hidden': 'true' as const, } if (variant == SnackbarVariant.SUCCESS) return else if (variant == SnackbarVariant.WARNING) return else if (variant == SnackbarVariant.ERROR) return return } export const StyledToast: React.FC = ({ header, description, variant, onClose, actionButton, toastId, wrap = false, ...props }) => { const snackbarTokens = useResponsiveTokens('SNACKBAR') const baseId = useId() const role = variant === SnackbarVariant.ERROR || variant === SnackbarVariant.WARNING ? 'alert' : 'status' const idPrefix = toastId ? `snackbar-${toastId}` : baseId const headerId = `${idPrefix}-header` const descriptionId = description ? `${idPrefix}-description` : undefined return ( {header} {description && ( {description} )} {actionButton && ( { actionButton.onClick() if (actionButton.autoDismiss !== false && toastId) { sonnerToast.dismiss(toastId) } }} > {actionButton.label} )} ) } export const addSnackbar = ({ header, description, variant = SnackbarVariant.INFO, onClose, actionButton, duration, position, wrap = false, }: AddToastOptions) => { // Determine if position includes "center" for proper alignment const isCenter = position?.includes('center') return sonnerToast.custom( (t) => ( { sonnerToast.dismiss(t) onClose?.() }} actionButton={actionButton} toastId={t} wrap={wrap} /> ), { duration, position, unstyled: true, style: { display: 'flex', justifyContent: 'center', width: isCenter ? '100%' : 'fit-content', maxWidth: 'calc(100vw - 32px)', margin: 0, padding: 0, background: 'transparent', border: 'none', boxShadow: 'none', }, } ) } const Snackbar: React.FC = ({ position = SnackbarPosition.BOTTOM_RIGHT, dismissOnClickAway = false, }) => { const isCenter = position?.includes('center') // Persistent toasts must stay reachable here too: sonner shares one toast // store across v1 and v2, so an app mounting only this container would // otherwise let a persistent toast be evicted into an unclickable state // that neither the timer nor click-away can clear. const visibleToasts = useVisibleToastCount() // Handle click away to dismiss all snackbars useEffect(() => { if (!dismissOnClickAway) { return } const handleClickAway = (event: MouseEvent) => { const target = event.target as Node // Check if click is on a snackbar element const clickedSnackbar = (target as Element)?.closest( '[data-snackbar]' ) // If click is not on a snackbar, dismiss all snackbars except // persistent ones. v1 and v2 share a single sonner toast store, so // dismissing everything here would also close SnackbarV2 toasts // created with duration: Infinity. if (!clickedSnackbar) { dismissNonPersistentToasts() } } document.addEventListener('mousedown', handleClickAway) return () => { document.removeEventListener('mousedown', handleClickAway) } }, [dismissOnClickAway]) return ( <> ) } export default Snackbar