'use client'; import type { BeadIssue } from '../../lib/types'; /** Props for an individual task card in the grid. */ /** Details for a blocker task shown on the card. */ export interface BlockerDetail { id: string; title: string; status: BeadIssue['status']; priority: BeadIssue['priority']; epicTitle?: string; } /** Props for an individual task card in the grid. */ interface TaskCardProps { /** The issue data for this card. */ issue: BeadIssue; /** Whether this card is the currently selected task. */ selected: boolean; /** List of issues blocking this task. */ blockers: BlockerDetail[]; /** List of issues this task blocks. */ blocking: BlockerDetail[]; /** Whether this task is actionable (unblocked). */ isActionable: boolean; /** Callback fired when the user clicks this card (or a blocker). */ onSelect: (id: string, shouldOpenDrawer?: boolean) => void; } /** Props for the TaskCardGrid component. */ interface TaskCardGridProps { /** List of tasks to display in the grid. */ tasks: BeadIssue[]; /** ID of the currently selected task, or null. */ selectedId: string | null; /** Map of issue ID to detailed blocker info. */ blockerDetailsMap: Map; /** Map of issue ID to detailed downstream blocking info. */ blocksDetailsMap: Map; /** Set of actionable (unblocked) task IDs. */ actionableIds: Set; /** Callback fired when the user selects a task. */ onSelect: (id: string, shouldOpenDrawer?: boolean) => void; } /** * Returns the Tailwind background color class for a status dot indicator. * Mirrors the statusDot function from the original monolith. */ function statusDot(status: BeadIssue['status']): string { switch (status) { case 'open': return 'bg-emerald-400'; case 'in_progress': return 'bg-amber-400'; case 'blocked': return 'bg-rose-500'; case 'deferred': return 'bg-slate-400'; case 'closed': return 'bg-slate-400'; case 'pinned': return 'bg-violet-400'; case 'hooked': return 'bg-orange-400'; default: return 'bg-zinc-500'; } } /** * Returns status-tinted gradient background for Aero Chrome styling. */ function statusGradient(status: BeadIssue['status']): string { switch (status) { case 'open': return 'bg-[linear-gradient(145deg,rgba(34,45,42,0.92)_0%,rgba(24,32,30,0.88)_50%,rgba(18,28,26,0.9)_100%)]'; case 'in_progress': return 'bg-[linear-gradient(145deg,rgba(42,40,32,0.92)_0%,rgba(32,30,24,0.88)_50%,rgba(26,24,18,0.9)_100%)]'; case 'blocked': return 'bg-[linear-gradient(145deg,rgba(60,24,30,0.95)_0%,rgba(45,18,24,0.9)_50%,rgba(32,12,16,0.92)_100%)]'; case 'closed': return 'bg-[linear-gradient(145deg,rgba(28,30,34,0.75)_0%,rgba(22,24,28,0.72)_50%,rgba(18,20,24,0.75)_100%)] opacity-75'; case 'deferred': return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]'; default: return 'bg-[linear-gradient(145deg,rgba(38,40,48,0.92)_0%,rgba(28,30,36,0.88)_50%,rgba(22,24,30,0.9)_100%)]'; } } /** * Returns status-colored border for Aero Chrome styling. */ function statusBorder(status: BeadIssue['status']): string { switch (status) { case 'open': return 'border-emerald-500/20'; case 'in_progress': return 'border-amber-500/20'; case 'blocked': return 'border-rose-500/20'; case 'closed': return 'border-rose-500/30'; case 'deferred': return 'border-slate-500/20'; default: return 'border-white/[0.06]'; } } /** * Returns title text color class - greyed out for closed status. */ function titleColorClass(status: BeadIssue['status']): string { return status === 'closed' ? 'text-text-muted/70' : 'text-text-strong'; } /** * Returns a human-friendly label and text color class for a status. */ function statusBadge(status: BeadIssue['status'], isActionable: boolean, hasBlockers: boolean): { label: string; textColor: string; bgColor: string } { // Actual blocked status always shows as Blocked in red if (status === 'blocked') { return { label: 'Blocked', textColor: 'text-rose-400', bgColor: 'bg-rose-400/10' }; } // If effectively blocked (has open blockers), show Blocked (unless closed/done) if (hasBlockers && status !== 'closed' && status !== 'in_progress') { return { label: 'Blocked', textColor: 'text-rose-400', bgColor: 'bg-rose-400/10' }; } switch (status) { case 'in_progress': return { label: 'In Progress', textColor: 'text-amber-400', bgColor: 'bg-amber-400/10' }; case 'closed': return { label: 'Done', textColor: 'text-emerald-400', bgColor: 'bg-emerald-400/10' }; case 'deferred': return { label: 'Deferred', textColor: 'text-slate-400', bgColor: 'bg-slate-400/10' }; case 'open': // Open with no blockers -> Ready return { label: 'Ready', textColor: 'text-cyan-400', bgColor: 'bg-cyan-400/10' }; default: return { label: status, textColor: 'text-zinc-400', bgColor: 'bg-zinc-400/10' }; } } /** * A single task card displaying the issue ID, title, priority, type, assignee, * and detailed blocker list (interactive). */ function TaskCard({ issue, selected, blockers, blocking, isActionable, onSelect }: TaskCardProps) { const hasBlockers = blockers.length > 0; // Note: blockers list only contains OPEN blockers (computed in page) const badge = statusBadge(issue.status, isActionable, hasBlockers); const projectName = (issue as BeadIssue & { project?: { name?: string } }).project?.name ?? null; // Determine effective status: in_progress always shows as in_progress, blocked always blocked, otherwise check blockers const effectiveStatus: BeadIssue['status'] = issue.status === 'in_progress' ? 'in_progress' : issue.status === 'blocked' ? 'blocked' : hasBlockers ? 'blocked' : issue.status; return (
onSelect(issue.id, false)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(issue.id, false); } }} className={`group relative flex w-full flex-col rounded-xl border ${statusBorder(effectiveStatus)} ${statusGradient(effectiveStatus)} px-4 py-4 text-left transition duration-200 shadow-[0_4px_24px_rgba(0,0,0,0.35),inset_0_1px_0_rgba(255,255,255,0.06)] ${selected ? 'ring-1 ring-amber-200/30 shadow-[0_0_20px_rgba(251,191,36,0.15)]' : 'hover:shadow-[0_8px_30px_rgba(0,0,0,0.4)]' }`} > {/* Expand / Open Drawer Button */}
{issue.id} {/* Status Badge */} {badge.label}
{projectName ? (
project: {projectName}
) : null}

{issue.title}

{/* Labels */} {issue.labels?.length > 0 ? (
{issue.labels.map((label) => ( {label} ))}
) : null} {/* "Unlocks" section for blockers */} {blockers.length > 0 ? (

Unlocks

{blockers.map((blocker) => (
{ e.stopPropagation(); onSelect(blocker.id, false); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); onSelect(blocker.id, false); } }} className="group relative flex flex-col gap-0.5 rounded border border-white/5 bg-white/5 px-2.5 py-2 hover:border-sky-400/30 hover:bg-white/10 transition-colors" > {/* Expand Button */}
{blocker.id} {blocker.title}
{blocker.epicTitle ? (
↳ {blocker.epicTitle}
) : null}
))}
) : null} {/* "Blocks" section (downstream) */} {blocking.length > 0 ? (
0 ? 'mt-2' : 'mt-auto'} w-full`}>

Blocks

{blocking.map((item) => (
{ e.stopPropagation(); onSelect(item.id, false); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); onSelect(item.id, false); } }} className="group relative flex flex-col gap-0.5 rounded border border-white/5 bg-white/5 px-2.5 py-2 hover:border-sky-400/30 hover:bg-white/10 transition-colors" > {/* Expand Button */}
{item.id} {item.title}
{item.epicTitle ? (
↳ {item.epicTitle}
) : null}
))}
) : null} {/* Footer Metadata: Assignee, Due Date */}
{/* Assignee */}
{issue.assignee ?? 'Unassigned'}
{/* Due Date (if exists) */} {issue.due_at ? (
{new Date(issue.due_at as string).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
) : null}
); } /** * Renders a responsive grid of task cards. * Uses auto-fill with minmax to prevent cards from being too narrow to read. */ export function TaskCardGrid({ tasks, selectedId, blockerDetailsMap, blocksDetailsMap, actionableIds, onSelect }: TaskCardGridProps) { // Show an empty state when no tasks exist in the selected epic if (tasks.length === 0) { return (

No tasks in this epic

); } return (
{tasks.map((task) => ( ))}
); }