"use client"; import { useState, useMemo, useEffect, useRef } from "react"; import type { GraphNode } from "@/lib/types"; import { STATUS_LABELS, STATUS_COLORS, TYPE_ICONS } from "@/lib/types"; import { formatRelativeTime } from "@/lib/utils"; interface Props { nodes: GraphNode[]; isOpen: boolean; onClose: () => void; onNodeClick: (nodeId: string) => void; } /** * Shared task list item component used in both desktop and mobile views. * Renders a single task with status dot, metadata, and status badge. */ function TaskListItem({ node, onClick }: { node: GraphNode; onClick: () => void }) { return ( ); } /** * Full slide-in sidebar panel for tasks/issues. * Includes search bar, status filter chips, and task list. * Same pattern as ActivityPanel / AllCommentsPanel / NodeDetail. */ export function TasksPanel({ nodes, isOpen, onClose, onNodeClick, }: Props) { const [statusFilter, setStatusFilter] = useState("all"); const [search, setSearch] = useState(""); const panelRef = useRef(null); // Reset search and filter state when panel closes (beads-map-ipz) useEffect(() => { if (!isOpen) { setSearch(''); setStatusFilter('all'); } }, [isOpen]); // Keyboard navigation: Escape to close (beads-map-eri) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { onClose(); } }; if (isOpen) { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); } }, [isOpen, onClose]); const filteredNodes = useMemo(() => { let filtered = nodes; if (statusFilter === "open") { filtered = filtered.filter(n => n.status === "open" || n.status === "in_progress" || n.status === "blocked"); } else if (statusFilter === "in_progress") { filtered = filtered.filter(n => n.status === "in_progress"); } else if (statusFilter === "closed") { filtered = filtered.filter(n => n.status === "closed" || n.status === "deferred"); } if (search.trim()) { const term = search.toLowerCase(); filtered = filtered.filter(n => n.id.toLowerCase().includes(term) || n.title.toLowerCase().includes(term) || (n.assignee || "").toLowerCase().includes(term) ); } return filtered.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); }, [nodes, statusFilter, search]); return ( <> {/* Desktop sidebar */} {/* Mobile drawer */}
{/* Drag handle */}
{/* Header */}

Tasks

{nodes.length > 0 && ( {nodes.length} )}
{/* Filter chips */}
{[ ["all", "All"], ["open", "Open"], ["in_progress", "Active"], ["closed", "Closed"], ].map(([value, label]) => ( ))}
{/* Task list */}
{filteredNodes.length === 0 ? (
No tasks matching filters
) : (
{filteredNodes.map((node) => ( onNodeClick(node.id)} /> ))}
)}
); }