react-compact-toast
Version:
A tiny, compact, and fully customizable toast notification library.
1 lines • 18.6 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["eventManager: EventManager","timers: TimeoutId[]","toast","combinedStyle: React.CSSProperties","Toast"],"sources":["../src/constants/index.ts","../src/types/index.ts","../src/core/event-manager.ts","../src/core/toast.ts","../src/hooks/use-toast-container.ts","../src/hooks/use-toast.ts","../src/components/toast.tsx","../src/utils/get-toast-offset-style.ts","../src/components/toast-container.tsx"],"sourcesContent":["export const TOAST_DEFAULT_POSITION = 'bottomCenter';\nexport const TOAST_MAX_COUNT = 6;\n","export type ToastPosition =\n | 'bottomCenter'\n | 'bottomLeft'\n | 'bottomRight'\n | 'topCenter'\n | 'topLeft'\n | 'topRight';\n\nexport type ToastProps = {\n toastId: string;\n text: string;\n icon?: React.JSX.Element | string | 'default';\n highlightText?: string;\n highlightColor?: string;\n autoClose?: false | number;\n closeOnClick?: boolean;\n position?: ToastPosition;\n offset?: number | string;\n className?: string;\n containerStyle?: React.CSSProperties;\n};\n\nexport const enum ToastEvent {\n Add,\n Delete,\n Update,\n}\n\nexport type EventCallbacks = {\n [ToastEvent.Add]: (props: ToastProps) => void;\n [ToastEvent.Delete]: (id: string) => void;\n [ToastEvent.Update]: (id: string, text: string) => void;\n};\n\nexport type TimeoutId = ReturnType<typeof setTimeout>;\n\nexport interface EventManager {\n list: Map<ToastEvent, EventCallbacks[keyof EventCallbacks][]>;\n emitQueue: Map<ToastEvent, TimeoutId[]>;\n activeToastCount: number;\n\n on<E extends ToastEvent>(event: E, callback: EventCallbacks[E]): EventManager;\n off<E extends ToastEvent>(\n event: E,\n callback?: EventCallbacks[E]\n ): EventManager;\n cancelEmit(event: ToastEvent): EventManager;\n emit<E extends ToastEvent>(\n event: E,\n ...args: Parameters<EventCallbacks[E]>\n ): void;\n}\n","import { TOAST_MAX_COUNT } from '../constants';\nimport {\n EventManager,\n ToastEvent,\n EventCallbacks,\n TimeoutId,\n ToastProps,\n} from '../types';\n\nexport const eventManager: EventManager = {\n list: new Map(),\n emitQueue: new Map(),\n activeToastCount: 0,\n\n on<E extends ToastEvent>(event: E, callback: EventCallbacks[E]) {\n if (!this.list.has(event)) {\n this.list.set(event, []);\n }\n this.list\n .get(event)!\n .push(callback as EventCallbacks[keyof EventCallbacks]);\n return this;\n },\n\n off<E extends ToastEvent>(event: E, callback?: EventCallbacks[E]) {\n if (callback && this.list.has(event)) {\n const callbacks = this.list.get(event)!;\n const filteredCallbacks = callbacks.filter((cb) => cb !== callback);\n this.list.set(event, filteredCallbacks);\n } else if (!callback) {\n this.list.delete(event);\n }\n return this;\n },\n\n emit<E extends ToastEvent>(event: E, ...args: Parameters<EventCallbacks[E]>) {\n if (!this.list.has(event)) return;\n\n const callbacks = this.list.get(event)!;\n const timers: TimeoutId[] = [];\n\n if (event === ToastEvent.Add && this.activeToastCount >= TOAST_MAX_COUNT) {\n return;\n }\n\n callbacks.forEach((callback) => {\n const timer = setTimeout(() => {\n switch (event) {\n case ToastEvent.Add:\n (callback as EventCallbacks[ToastEvent.Add])(\n ...(args as [ToastProps])\n );\n this.activeToastCount += 1;\n break;\n case ToastEvent.Delete:\n (callback as EventCallbacks[ToastEvent.Delete])(\n ...(args as [string])\n );\n this.activeToastCount -= 1;\n break;\n case ToastEvent.Update:\n (callback as EventCallbacks[ToastEvent.Update])(\n ...(args as [string, string])\n );\n break;\n }\n }, 0);\n timers.push(timer);\n });\n\n if (timers.length > 0) {\n this.emitQueue.set(event, [\n ...(this.emitQueue.get(event) || []),\n ...timers,\n ]);\n }\n },\n\n cancelEmit(event: ToastEvent) {\n if (this.emitQueue.has(event)) {\n const timers = this.emitQueue.get(event)!;\n timers.forEach(clearTimeout);\n this.emitQueue.delete(event);\n }\n return this;\n },\n};\n","import { eventManager } from './event-manager';\nimport { ToastEvent, ToastProps } from '../types';\n\ntype ToastOptions = Omit<ToastProps, 'toastId'>;\n\nconst emitAddToast = (toastProps: ToastOptions) => {\n const id = crypto.randomUUID();\n\n eventManager.emit(ToastEvent.Add, {\n ...toastProps,\n toastId: id,\n });\n return id;\n};\n\n/**\n * Creates and displays a toast notification.\n *\n * @param {ToastOptions | string} options - Toast configuration options or a simple text message\n * - If a string is provided, it will be converted to `{ text: options }`\n * - If an object is provided, it should contain toast configuration properties:\n * @param {string} options.text - The text content to display in the toast\n * @param {React.JSX.Element | string | 'default'} [options.icon] - Icon to display in the toast:\n * - JSX Element: Custom React component\n * - string: Text/emoji icon\n * - 'default': Default icon\n * - undefined: No icon\n * @param {string} [options.highlightText] - Text to highlight with a different color\n * @param {string} [options.highlightColor] - Custom color for the highlighted text (CSS color value)\n * @param {false | number} [options.autoClose=3000] - Auto-close behavior:\n * - `false`: Toast will not close automatically\n * - `number`: Time in milliseconds before the toast closes automatically\n * @param {boolean} [options.closeOnClick=true] - Whether the toast should close when clicked\n * @param {ToastPosition} [options.position='bottomCenter'] - Position where the toast appears:\n * - 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight'\n * @param {string} [options.className] - Custom CSS classes to apply to the toast\n * @param {string | number} [options.offset] - Custom offset from screen edge:\n * - `string`: CSS value like '50px', '2rem', '5vh'\n * - `number`: Pixel value (e.g., 50 becomes '50px')\n * @param {React.CSSProperties} [options.containerStyle] - Custom inline styles to apply to the toast container\n * @returns {string} The unique ID of the created toast\n *\n * @example\n * // Simple text toast\n * toast('Hello, world!');\n *\n * @example\n * // Toast with custom options\n * toast({\n * text: 'Custom notification',\n * icon: '🚀',\n * highlightText: 'Custom',\n * highlightColor: '#ff6b6b',\n * autoClose: 5000,\n * closeOnClick: true,\n * position: 'topRight',\n * className: 'bg-blue-500 text-white rounded-lg',\n * offset: 50, // 50px from screen edge\n * containerStyle: { right: '100px', top: '50px' }, // Custom container positioning\n * });\n *\n * @example\n * // Toast with custom offset\n * toast({\n * text: 'Far from edge',\n * position: 'topCenter',\n * offset: '100px', // 100px from top\n * });\n */\nexport const toast = (options: ToastOptions | string): string => {\n const toastOptions: ToastOptions =\n typeof options === 'string' ? { text: options } : options;\n\n return emitAddToast(toastOptions);\n};\n","import { useCallback, useEffect, useState } from 'react';\n\nimport { TOAST_DEFAULT_POSITION } from '../constants';\nimport { eventManager, ToastEvent, ToastPosition, ToastProps } from '../core';\n\ntype ToastPositionGroup = {\n toasts: ToastProps[];\n containerStyle?: React.CSSProperties;\n};\n\nexport const useToastContainer = () => {\n const [toastList, setToastList] = useState(new Map<string, ToastProps>());\n\n const addToast = useCallback((props: ToastProps) => {\n setToastList((prev) => {\n const newMap = new Map(prev);\n newMap.set(props.toastId, props);\n return newMap;\n });\n }, []);\n\n const deleteToast = useCallback((id: string) => {\n setToastList((prev) => {\n if (!prev.has(id)) return prev;\n\n const newMap = new Map(prev);\n newMap.delete(id);\n return newMap;\n });\n }, []);\n\n const updateToast = useCallback((id: string, text: string) => {\n setToastList((prev) => {\n if (!prev.has(id)) return prev;\n\n const newMap = new Map(prev);\n const toast = prev.get(id)!;\n newMap.set(id, { ...toast, text });\n return newMap;\n });\n }, []);\n\n useEffect(() => {\n eventManager.on(ToastEvent.Add, addToast);\n eventManager.on(ToastEvent.Delete, deleteToast);\n eventManager.on(ToastEvent.Update, updateToast);\n\n return () => {\n eventManager.off(ToastEvent.Add, addToast);\n eventManager.off(ToastEvent.Delete, deleteToast);\n eventManager.off(ToastEvent.Update, updateToast);\n\n eventManager.cancelEmit(ToastEvent.Add);\n eventManager.cancelEmit(ToastEvent.Delete);\n eventManager.cancelEmit(ToastEvent.Update);\n };\n }, [addToast, deleteToast, updateToast]);\n\n const getToastPositionGroupToRender = useCallback(() => {\n const positionGroup = new Map<ToastPosition, ToastPositionGroup>();\n\n toastList.forEach((toast) => {\n const position = toast.position || TOAST_DEFAULT_POSITION;\n\n if (!positionGroup.has(position)) {\n positionGroup.set(position, {\n toasts: [],\n containerStyle: toast.containerStyle,\n });\n } else {\n const existing = positionGroup.get(position)!;\n if (toast.containerStyle && !existing.containerStyle) {\n existing.containerStyle = toast.containerStyle;\n }\n }\n positionGroup.get(position)!.toasts.push(toast);\n });\n\n return positionGroup;\n }, [toastList]);\n\n return { getToastPositionGroupToRender };\n};\n","import { useState, useEffect } from 'react';\n\nimport { eventManager, ToastEvent } from '../core';\n\nexport const useToast = (\n toastId: string,\n autoClose?: number | false,\n closeOnClick = true\n) => {\n const [isExiting, setIsExiting] = useState(false);\n\n useEffect(() => {\n if (autoClose) {\n const timer = setTimeout(() => setIsExiting(true), autoClose);\n return () => clearTimeout(timer);\n }\n }, [autoClose]);\n\n const handleAnimationEnd = () => {\n if (isExiting) {\n eventManager.emit(ToastEvent.Delete, toastId);\n }\n };\n\n const handleClick = () => {\n if (closeOnClick) setIsExiting(true);\n };\n\n return { isExiting, handleAnimationEnd, handleClick };\n};\n","import { TOAST_DEFAULT_POSITION } from '../constants';\nimport { ToastProps } from '../core';\nimport { useToast } from '../hooks';\n\nimport '../styles.css';\n\nconst Toast = ({\n toastId,\n text,\n icon,\n highlightText,\n highlightColor,\n autoClose = 3000,\n closeOnClick = true,\n position = TOAST_DEFAULT_POSITION,\n offset,\n className,\n}: ToastProps) => {\n const { isExiting, handleAnimationEnd, handleClick } = useToast(\n toastId,\n autoClose,\n closeOnClick\n );\n\n const isTopPosition = position.startsWith('top');\n\n // Prevent unused variable warning\n void offset;\n\n const renderIcon = () => {\n switch (icon) {\n case 'default':\n return icon;\n case undefined:\n return null;\n default:\n return icon;\n }\n };\n\n const getToastClasses = () => {\n let classes = 'toast';\n\n if (!className) {\n classes += ' toast-default-style toast-default-size';\n }\n\n if (isTopPosition) {\n classes += ' toast-top-position';\n }\n\n if (isExiting) {\n classes += isTopPosition ? ' toast-exit-top' : ' toast-exit-bottom';\n } else {\n classes += isTopPosition ? ' toast-enter-top' : ' toast-enter-bottom';\n }\n\n if (className) {\n classes += ` ${className}`;\n }\n\n return classes;\n };\n\n return (\n <div\n className={getToastClasses()}\n onClick={handleClick}\n onAnimationEnd={handleAnimationEnd}\n >\n <div className=\"toast-content\">\n {renderIcon()}\n <p className=\"toast-text\">\n <span\n className=\"toast-highlight-text\"\n style={highlightColor ? { color: highlightColor } : undefined}\n >\n {highlightText}\n </span>\n {text}\n </p>\n </div>\n </div>\n );\n};\n\nexport default Toast;\n","import { ToastProps } from '../types';\n\n/**\n * Generate inline styles for toast container based on offset configuration\n * @param toasts Array of toast props for the position group\n * @param position Toast position (e.g., 'topCenter', 'bottomLeft')\n * @returns Inline style object for the container\n */\nexport const getToastOffsetStyle = (\n toasts: ToastProps[],\n position: string\n): React.CSSProperties => {\n const firstToast = toasts[0];\n\n if (firstToast?.offset === undefined || firstToast?.offset === null)\n return {};\n\n const offset =\n typeof firstToast.offset === 'number'\n ? `${firstToast.offset}px`\n : firstToast.offset;\n\n if (position.startsWith('top')) {\n return { top: offset };\n } else if (position.startsWith('bottom')) {\n return { bottom: offset };\n }\n return {};\n};\n","import { memo, useMemo, Fragment } from 'react';\n\nimport Toast from './toast';\nimport { useToastContainer } from '../hooks/use-toast-container';\nimport { getToastOffsetStyle } from '../utils/get-toast-offset-style';\nimport '../styles.css';\n\nconst ToastContainer = () => {\n const { getToastPositionGroupToRender } = useToastContainer();\n const positionGroup = useMemo(\n () => getToastPositionGroupToRender(),\n [getToastPositionGroupToRender]\n );\n\n return (\n <Fragment>\n {Array.from(positionGroup).map(\n ([position, { toasts, containerStyle }]) => {\n const className = `toast-container toast-position-${position}`;\n\n const combinedStyle: React.CSSProperties = {\n ...getToastOffsetStyle(toasts, position),\n ...containerStyle,\n };\n\n return (\n <div key={position} className={className} style={combinedStyle}>\n {toasts.map((toastProps) => (\n <Toast key={toastProps.toastId} {...toastProps} />\n ))}\n </div>\n );\n }\n )}\n </Fragment>\n );\n};\n\nexport default memo(ToastContainer);\n"],"mappings":"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"}