import type React from 'react' import { useId, useEffect, forwardRef, ReactElement } 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 SnackbarV2ToastOptions, type SnackbarV2ToastProps, SnackbarV2Variant, SnackbarV2Position, SnackbarV2Props, SnackbarV2IconProps, } from './snackbarV2.types' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { DEFAULT_VISIBLE_TOASTS, dismissNonPersistentToasts, SnackbarHiddenToastStyles, useVisibleToastCount, } from '../../utils/snackbar-shared' import { SnackbarV2TokensType } from './snackbarV2.tokens' import { addPxToValue } from '../../global-utils/GlobalUtils' import { filterBlockedProps } from '../../utils/prop-helpers' /** * `duration: 0` looks like it should disable auto-dismiss, and the v1 docs used * to recommend it. It does the opposite: `0` is falsy, so sonner falls through * to its 4000ms default and the toast silently disappears. Types cannot catch * this because `0` is a valid `number`, so warn at runtime in development. * * Warned once per session, matching `useResponsiveTokens`' deprecation notice, * so a toast fired in a render loop cannot flood the console. */ let warnedAboutZeroDuration = false const warnAboutZeroDuration = () => { if (warnedAboutZeroDuration) return warnedAboutZeroDuration = true console.warn( '[Blend] addSnackbarV2: duration: 0 does not make a toast persistent. ' + '0 is falsy, so it falls through to the 4000ms default. ' + 'Use duration: Infinity instead.' ) } const SnackbarV2Icon: React.FC = ({ variant }) => { const snackbarTokens = useResponsiveTokens('SNACKBARV2') const iconColor = snackbarTokens.slot.color[variant] const iconSize = snackbarTokens.slot.height const props = { color: iconColor, size: iconSize, 'aria-hidden': 'true' as const, } if (variant === SnackbarV2Variant.SUCCESS) return else if (variant === SnackbarV2Variant.WARNING) return else if (variant === SnackbarV2Variant.ERROR) return return } const SlotContainer = ({ slot, snackbarTokens, }: { slot?: ReactElement snackbarTokens: SnackbarV2TokensType }) => { if (!slot) return null return ( ) } const ActionButton = ({ action, variant, snackbarTokens, toastId, }: { action: { label: string onClick: () => void autoDismiss?: boolean } variant: SnackbarV2Variant snackbarTokens: SnackbarV2TokensType toastId?: string | number }) => { const actionTokens = snackbarTokens.mainContainer.content.actionContainer.primaryAction const color = actionTokens.color[variant] return ( { action.onClick() if (action.autoDismiss !== false && toastId) { sonnerToast.dismiss(toastId) } }} tabIndex={0} aria-label={action.label} border="none" backgroundColor="transparent" cursor="pointer" color={color} fontSize={actionTokens.fontSize} fontWeight={actionTokens.fontWeight} lineHeight={addPxToValue(actionTokens.lineHeight)} width="fit-content" whiteSpace="nowrap" > {action.label} ) } const ActionsContainer = ({ actionButton, variant, snackbarTokens, toastId, }: { actionButton?: { label: string onClick: () => void autoDismiss?: boolean } variant: SnackbarV2Variant snackbarTokens: SnackbarV2TokensType toastId?: string | number }) => { if (!actionButton) return null return ( ) } const TextContainer = ({ header, description, variant, snackbarTokens, headerId, descriptionId, }: { header: string description?: string variant: SnackbarV2Variant snackbarTokens: SnackbarV2TokensType headerId: string descriptionId?: string }) => { return ( {header} {description && ( {description} )} ) } const CloseButton = ({ onClose, variant, snackbarTokens, }: { onClose?: () => void variant: SnackbarV2Variant snackbarTokens: SnackbarV2TokensType }) => { const closeButtonColor = snackbarTokens.mainContainer.closeButton.color[variant] const closeButtonHeight = snackbarTokens.mainContainer.closeButton.height return ( ) } export const StyledToast: React.FC = ({ header, description, variant, slot, onClose, actionButton, toastId, width, maxWidth, minWidth, ...rest }) => { const snackbarTokens = useResponsiveTokens('SNACKBARV2') const filteredProps = filterBlockedProps(rest) const baseId = useId() const role = variant === SnackbarV2Variant.ERROR || variant === SnackbarV2Variant.WARNING ? 'alert' : 'status' const idPrefix = toastId ? `snackbar-${toastId}` : baseId const headerId = `${idPrefix}-header` const descriptionId = description ? `${idPrefix}-description` : undefined const defaultSlot = const iconSlot = slot ?? defaultSlot return ( ) } export const addSnackbarV2 = ({ header, description, variant = SnackbarV2Variant.INFO, slot, onClose, actionButton, duration, position, width, maxWidth, minWidth, }: SnackbarV2ToastOptions) => { const isCenter = position?.includes('center') if (process.env.NODE_ENV !== 'production' && duration === 0) { warnAboutZeroDuration() } return sonnerToast.custom( (t) => ( { sonnerToast.dismiss(t) onClose?.() }} actionButton={actionButton} toastId={t} width={width} maxWidth={maxWidth} minWidth={minWidth} /> ), { duration, position, unstyled: true, style: { display: 'flex', justifyContent: 'center', width: isCenter ? '100%' : 'fit-content', margin: 0, padding: 0, background: 'transparent', border: 'none', boxShadow: 'none', }, } ) } const SnackbarV2 = forwardRef( ( { position = SnackbarV2Position.BOTTOM_RIGHT, dismissOnClickAway = false, visibleToasts = DEFAULT_VISIBLE_TOASTS, containerAriaLabel, hotkey, }, ref ) => { const isCenter = position?.includes('center') const resolvedVisibleToasts = useVisibleToastCount(visibleToasts) useEffect(() => { if (!dismissOnClickAway) { return } const handleClickAway = (event: MouseEvent) => { const target = event.target as Node const clickedSnackbar = (target as Element)?.closest( '[data-snackbar]' ) if (!clickedSnackbar) { dismissNonPersistentToasts() } } document.addEventListener('mousedown', handleClickAway) return () => { document.removeEventListener('mousedown', handleClickAway) } }, [dismissOnClickAway]) return ( ) } ) SnackbarV2.displayName = 'SnackbarV2' export default SnackbarV2