oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useNavigate } from '@tanstack/react-router'
import { Bell, Check, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
import {
  type Notification,
  type NotificationSeverity,
  useNotificationStore,
} from '@/stores/notifications'

/** Color map for severity. */
const severityDot: Record<NotificationSeverity, string> = {
  info: 'bg-info',
  warning: 'bg-warning',
  error: 'bg-error',
  success: 'bg-success',
}

/**
 * i18n-aware relative time formatter.
 * Defined outside the component to avoid re-creation, but requires `t`.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function timeAgo(iso: string, t: (...args: any[]) => any): string {
  const diff = Date.now() - new Date(iso).getTime()
  if (diff < 60_000) return t('common.justNow', 'just now')
  if (diff < 3_600_000) return t('common.minutesAgo', { count: Math.floor(diff / 60_000) })
  if (diff < 86_400_000) return t('common.hoursAgo', { count: Math.floor(diff / 3_600_000) })
  return t('common.daysAgo', { count: Math.floor(diff / 86_400_000) })
}

/**
 * Global notification bell displayed in the header.
 * Shows unread count badge and a dropdown panel with recent notifications.
 */
export function NotificationBell() {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)
  const panelRef = useRef<HTMLDivElement>(null)
  const navigate = useNavigate()

  const notifications = useNotificationStore((s) => s.notifications)
  const unreadCount = useNotificationStore((s) => s.unreadCount)
  const markRead = useNotificationStore((s) => s.markRead)
  const markAllRead = useNotificationStore((s) => s.markAllRead)
  const dismiss = useNotificationStore((s) => s.dismiss)

  // Close on outside click
  useEffect(() => {
    if (!open) return
    const handler = (e: MouseEvent) => {
      if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
        setOpen(false)
      }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [open])

  const handleClick = (n: Notification) => {
    markRead(n.id)
    setOpen(false)
    if (n.link) {
      navigate({ to: n.link })
    }
  }

  return (
    <div className="relative" ref={panelRef}>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="relative rounded-md p-2 hover:bg-accent/50 transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
        aria-label={`${t('notifications.openNotifications')}${unreadCount > 0 ? t('common.unreadCount', { count: unreadCount }) : ''}`}
      >
        <Bell className="h-4 w-4" />
        {unreadCount > 0 && (
          <span className="absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-2xs font-bold text-destructive-foreground animate-scale-in">
            {unreadCount > 99 ? '99+' : unreadCount}
          </span>
        )}
      </button>

      {open && (
        <div className="absolute right-0 top-full mt-2 z-[var(--z-popover,200)] w-80 max-w-[calc(100vw-2rem)] rounded-xl border bg-popover text-popover-foreground shadow-xl animate-scale-in">
          {/* Header */}
          <div className="flex items-center justify-between border-b px-3 py-2">
            <span className="text-sm font-medium">{t('notifications.title')}</span>
            {unreadCount > 0 && (
              <Button variant="ghost" size="sm" className="h-6 text-xs" onClick={markAllRead}>
                <Check className="h-3 w-3 mr-1" /> {t('notifications.markAllRead')}
              </Button>
            )}
          </div>

          {/* Body */}
          <ScrollArea className="max-h-80">
            {notifications.length === 0 ? (
              <div className="p-4 text-center text-sm text-muted-foreground">
                {t('notifications.noNotifications')}
              </div>
            ) : (
              <div className="divide-y">
                {notifications.map((n) => (
                  // biome-ignore lint/a11y/useSemanticElements: contains nested dismiss button; div is correct
                  <div
                    key={n.id}
                    className={cn(
                      'group flex gap-2 px-3 py-2.5 transition-all cursor-pointer hover:bg-accent/50',
                      !n.read && 'bg-accent/20',
                    )}
                    onClick={() => handleClick(n)}
                    role="button"
                    tabIndex={0}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter') handleClick(n)
                    }}
                  >
                    <div
                      className={cn(
                        'mt-0.5 h-2 w-2 shrink-0 rounded-full',
                        severityDot[n.severity],
                      )}
                    />
                    <div className="flex-1 min-w-0">
                      <p className="text-sm font-medium leading-tight truncate">{n.title}</p>
                      {n.message && (
                        <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
                          {n.message}
                        </p>
                      )}
                      <p className="text-2xs text-muted-foreground/60 mt-1">
                        {timeAgo(n.timestamp, t)}
                      </p>
                    </div>
                    <button
                      type="button"
                      onClick={(e) => {
                        e.stopPropagation()
                        dismiss(n.id)
                      }}
                      className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity rounded p-0.5 hover:bg-muted"
                      aria-label={t('common.dismiss')}
                    >
                      <X className="h-3 w-3" />
                    </button>
                  </div>
                ))}
              </div>
            )}
          </ScrollArea>
        </div>
      )}
    </div>
  )
}