/** * NotificationRow - Linear Inbox-style notification row. * Compact, high-density layout with hover actions. */ import { useState } from 'react'; import { getTriggerColorDef } from '@shared/constants/triggerColors'; import { formatDistanceToNow } from 'date-fns'; import { ArrowRight, Bot, Check, Trash2, Users } from 'lucide-react'; import type { DetectedError } from '@renderer/types/data'; interface NotificationRowProps { error: DetectedError; onRowClick: () => void; onArchive: () => void; onDelete: () => void; } /** * Truncates a string to a maximum length, adding ellipsis if truncated. */ function truncateMessage(message: string, maxLength: number = 100): string { if (message.length <= maxLength) return message; return message.slice(0, maxLength).trim() + '...'; } export const NotificationRow = ({ error, onRowClick, onArchive, onDelete, }: Readonly): React.JSX.Element => { const [isHovered, setIsHovered] = useState(false); const isUnread = !error.isRead; const projectName = error.context?.projectName || 'Unknown Project'; const relativeTime = formatDistanceToNow(new Date(error.timestamp), { addSuffix: true, }); const truncatedMessage = truncateMessage(error.message); const colorDef = getTriggerColorDef(error.triggerColor); const displayName = error.triggerName ?? error.source; const isTeamNotification = error.category === 'team' || error.sessionId?.startsWith('team:'); const handleArchiveClick = (e: React.MouseEvent): void => { e.stopPropagation(); onArchive(); }; const handleDeleteClick = (e: React.MouseEvent): void => { e.stopPropagation(); onDelete(); }; const handleNavigateClick = (e: React.MouseEvent): void => { e.stopPropagation(); onRowClick(); }; return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRowClick(); } }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className="flex h-full cursor-pointer items-center gap-3 border-b px-4 transition-colors" style={{ borderColor: 'var(--color-border)', backgroundColor: isHovered ? 'var(--color-surface-raised)' : undefined, opacity: isUnread ? 1 : 0.5, }} > {/* Color Dot — always visible, opacity indicates read state */}
{/* Content */}
{/* Title Row */}
{displayName} · {projectName} {isTeamNotification && !error.subagentId && ( team )} {error.subagentId && ( subagent )}
{/* Description */}

{truncatedMessage}

{/* Right Side: Time or Hover Actions */}
{isHovered ? ( ) : ( {relativeTime} )}
); }; /** * HoverActions - Action buttons shown on hover. */ interface HoverActionsProps { isUnread: boolean; onArchiveClick: (e: React.MouseEvent) => void; onDeleteClick: (e: React.MouseEvent) => void; onNavigateClick: (e: React.MouseEvent) => void; } const HoverActions = ({ isUnread, onArchiveClick, onDeleteClick, onNavigateClick, }: HoverActionsProps): React.JSX.Element => { const [hoveredButton, setHoveredButton] = useState(null); const getButtonStyle = (buttonId: string, isDelete = false): React.CSSProperties => ({ color: hoveredButton === buttonId ? isDelete ? 'var(--tool-result-error-text)' : 'var(--color-text)' : 'var(--color-text-muted)', backgroundColor: hoveredButton === buttonId ? 'var(--color-border-emphasis)' : undefined, }); return ( <> {/* Archive Button (mark as read) */} {isUnread && ( )} {/* Delete Button */} {/* Navigate Button */} ); };