'use client' import type { KeyboardEvent, MouseEvent } from 'react' import { cn } from '../../../utils/cn' import type { RenderNotificationTile } from './types' import { useOptionalNotifications } from './notifications-context' import { NotificationTile } from './notification-tile' import type { Notification } from './types' export type NotificationPopupsPosition = | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' export interface NotificationPopupsProps { className?: string liveDurationMs?: number maxVisible?: number position?: NotificationPopupsPosition hideWhenDrawerOpen?: boolean renderTile?: RenderNotificationTile } const positionClasses: Record = { 'top-right': 'top-4 right-4', 'top-left': 'top-4 left-4', 'bottom-right': 'bottom-4 right-4 flex-col-reverse', 'bottom-left': 'bottom-4 left-4 flex-col-reverse', } export function NotificationPopups({ className, liveDurationMs = 4000, maxVisible = 4, position = 'top-right', hideWhenDrawerOpen = true, renderTile: renderTileProp, }: NotificationPopupsProps) { const ctx = useOptionalNotifications() if (!ctx) return null const { notifications, showPopups, isOpen, open, markRead, markSettled, renderTile: ctxRenderTile } = ctx const renderTile = renderTileProp ?? ctxRenderTile if (!showPopups) return null if (hideWhenDrawerOpen && isOpen) return null const now = Date.now() const live = notifications .filter((n) => !n.read && !n.settled && now - n.createdAt < liveDurationMs) .slice(0, maxVisible) if (live.length === 0) return null const activate = (notification: Notification) => { if (notification.onClick) { notification.onClick() markRead(notification.id) markSettled(notification.id) return } open() markSettled(notification.id) } const handleBodyClick = (notification: Notification) => (event: MouseEvent) => { // Nested interactive controls (X, check button) handle their own actions. if ((event.target as HTMLElement).closest('button')) return activate(notification) } const handleBodyKeyDown = (notification: Notification) => (event: KeyboardEvent) => { if ((event.target as HTMLElement).closest('button')) return if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() activate(notification) } return (
{live.map((n) => { const custom = renderTile?.(n, { onComplete: markRead, onSettle: markSettled, liveDurationMs }) if (custom) { return (
{custom}
) } return ( // biome-ignore lint/a11y/useSemanticElements: nested interactive elements forbid
) }