import * as React from 'react' import { createPortal } from 'react-dom' import { Toaster as SonnerToaster } from 'sonner' import type { ToasterProps as SonnerToasterProps } from 'sonner' import { getThemeFromDocument } from './theme-from-document' export { toast } from 'sonner' export type ToasterProps = Omit & { theme?: 'light' | 'dark' /** 可选:Portal 根节点 data-style,主库可传入 useThemeStyle 的值 */ dataStyle?: string /** 可选:Portal 根节点 data-theme,主库可传入 useThemeStyle 的值 */ dataTheme?: string } /** 图标/文字用最小 token 间距;按钮与容器边缘、按钮之间用 spacing-2 */ const TOAST_INNER_GAP = 'calc(var(--spacing-px) * 2)' const TOAST_EDGE_PADDING = 'var(--spacing-2)' const TOAST_BUTTON_GAP = 'var(--spacing-2)' /** 强制 toast 四边内边距 + 根节点 text-sm 变量 + 非首个按钮左间距;选择器带 data-styled 以覆盖 sonner */ const TOAST_PADDING_TOP_BOTTOM = 'var(--spacing-3)' /** 根节点用设计系统 text-sm 变量;[data-content] 撑满中间;非首个按钮左间距 */ const TOAST_LAYOUT_FIX_CSS = `[data-sonner-toaster] [data-sonner-toast][data-styled="true"] { padding-left: ${TOAST_EDGE_PADDING} !important; padding-right: ${TOAST_EDGE_PADDING} !important; padding-top: ${TOAST_PADDING_TOP_BOTTOM} !important; padding-bottom: ${TOAST_PADDING_TOP_BOTTOM} !important; font-size: var(--font-size-sm) !important; line-height: var(--font-size-sm--line-height) !important; } [data-sonner-toaster] [data-sonner-toast] [data-content] { flex: 1 !important; min-width: 0 !important; } [data-sonner-toaster] [data-sonner-toast] [data-button]:not(:first-of-type) { margin-left: ${TOAST_BUTTON_GAP} !important; }` const defaultToastOptions: NonNullable = { className: 'text-sm', style: { padding: `${TOAST_PADDING_TOP_BOTTOM} ${TOAST_EDGE_PADDING}`, gap: TOAST_INNER_GAP, background: 'var(--color-bg-container)', borderColor: 'var(--color-border-tertiary)', color: 'var(--color-text)', }, descriptionClassName: 'text-xs text-text-secondary', actionButtonStyle: { borderRadius: 'var(--radius)', paddingLeft: 'var(--spacing-2)', paddingRight: 'var(--spacing-2)', height: 'var(--spacing-7)', fontSize: 'var(--font-size-xs)', }, cancelButtonStyle: { borderRadius: 'var(--radius)', paddingLeft: 'var(--spacing-2)', paddingRight: 'var(--spacing-2)', height: 'var(--spacing-7)', fontSize: 'var(--font-size-xs)', }, } const SONNER_GAP = 14 const SONNER_VISIBLE_TOASTS = 3 /** toaster 距视口右侧默认 24px,改为 8px 使按钮到屏幕边缘不会过大 */ const DEFAULT_OFFSET = { right: 8, left: 8, top: 24, bottom: 24 } export function Toaster({ theme, toastOptions, dataStyle, dataTheme, icons, gap = SONNER_GAP, visibleToasts = SONNER_VISIBLE_TOASTS, offset = DEFAULT_OFFSET, ...props }: ToasterProps) { const fromDocument = getThemeFromDocument() const dataProps = dataStyle !== undefined || dataTheme !== undefined ? { 'data-style': dataStyle, 'data-theme': dataTheme } : fromDocument const resolvedTheme = theme ?? (typeof document !== 'undefined' && document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light') const content = ( <>