'use client'
/**
* useToast Hook
* Standalone toast notification system with React context
* Migrated from @metabuilder/fakemui for broader usage.
*
* Features:
* - Multiple severity levels (success, error, warning, info)
* - Auto-hide with configurable duration
* - Maximum toast limit
* - Deduplication by key
* - Customizable anchor position
* - Built-in styled toast renderer
*
* @example
* // Wrap your app with ToastProvider
*
*
*
*
* // Use in components
* const { toast, success, error, warning, info, close, closeAll } = useToast()
*
* // Simple usage
* toast('Hello world')
*
* // With severity helpers
* success('Operation completed!')
* error('Something went wrong')
*
* // With full options
* toast({
* message: 'Custom toast',
* severity: 'warning',
* autoHideDuration: 3000,
* action:
* })
*/
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react'
// ============================================================================
// Types
// ============================================================================
export type ToastSeverity = 'success' | 'error' | 'warning' | 'info'
export interface ToastOptions {
/** Toast message content */
message: string
/** Severity level for styling */
severity?: ToastSeverity
/** Auto hide duration in ms (null to disable) */
autoHideDuration?: number | null
/** Action button content */
action?: React.ReactNode
/** Custom key for deduplication */
key?: string
/** Callback when toast closes */
onClose?: () => void
/** Anchor position */
anchorOrigin?: {
vertical: 'top' | 'bottom'
horizontal: 'left' | 'center' | 'right'
}
}
interface Toast extends Required> {
id: string
action?: React.ReactNode
onClose?: () => void
anchorOrigin: NonNullable
}
export interface ToastContextValue {
/** Show a toast notification */
toast: (options: ToastOptions | string) => string
/** Show a success toast */
success: (message: string, options?: Omit) => string
/** Show an error toast */
error: (message: string, options?: Omit) => string
/** Show a warning toast */
warning: (message: string, options?: Omit) => string
/** Show an info toast */
info: (message: string, options?: Omit) => string
/** Close a specific toast by ID */
close: (id: string) => void
/** Close all toasts */
closeAll: () => void
}
export interface ToastProviderProps {
children: React.ReactNode
/** Default auto hide duration in ms */
defaultAutoHideDuration?: number
/** Maximum number of toasts to show at once */
maxToasts?: number
/** Default anchor position */
defaultAnchorOrigin?: ToastOptions['anchorOrigin']
/** Custom toast renderer (optional) */
renderToast?: (toast: Toast, onClose: () => void) => React.ReactNode
}
// ============================================================================
// Context
// ============================================================================
const ToastContext = createContext(null)
let toastIdCounter = 0
const generateId = () => `toast-${++toastIdCounter}`
// ============================================================================
// Default Styled Toast Component
// ============================================================================
const severityColors: Record = {
success: { bg: '#d4edda', border: '#c3e6cb', text: '#155724' },
error: { bg: '#f8d7da', border: '#f5c6cb', text: '#721c24' },
warning: { bg: '#fff3cd', border: '#ffeeba', text: '#856404' },
info: { bg: '#d1ecf1', border: '#bee5eb', text: '#0c5460' },
}
const positionStyles: Record = {
'top-left': { top: 16, left: 16 },
'top-center': { top: 16, left: '50%', transform: 'translateX(-50%)' },
'top-right': { top: 16, right: 16 },
'bottom-left': { bottom: 16, left: 16 },
'bottom-center': { bottom: 16, left: '50%', transform: 'translateX(-50%)' },
'bottom-right': { bottom: 16, right: 16 },
}
interface DefaultToastProps {
toast: Toast
onClose: () => void
}
const DefaultToast: React.FC = ({ toast, onClose }) => {
const colors = severityColors[toast.severity]
const positionKey = `${toast.anchorOrigin.vertical}-${toast.anchorOrigin.horizontal}`
const position = positionStyles[positionKey] || positionStyles['bottom-left']
return (
{toast.message}
{toast.action &&
{toast.action}
}
)
}
// ============================================================================
// Provider Component
// ============================================================================
export const ToastProvider: React.FC = ({
children,
defaultAutoHideDuration = 5000,
maxToasts = 3,
defaultAnchorOrigin = { vertical: 'bottom', horizontal: 'left' },
renderToast,
}) => {
const [toasts, setToasts] = useState([])
const timersRef = useRef