import type { Card, Column, DocPage } from '../../lib/kanbanApi'; import type { CustomPage } from '../../lib/pagesApi'; import { ReferenceLinks } from './ReferenceLinks'; import type { UseOrchestrator } from './useOrchestrator'; const COLUMN_DOT: Record = { done: 'var(--green)', 'in-progress': 'var(--accent)', todo: 'var(--red)', backlog: 'var(--muted)', }; /** * Compact, click-to-open kanban card. * * The full detail (agent orchestration, comments, references, tags) lives in * the CardModal opened by `onOpen`. This card is a dense preview: status, id, * title, description, references, and a prominent live indicator while an agent * or Bevy playtest is running on the card's worktree. */ interface KanbanCardProps { card: Card; pages: DocPage[]; customPages: CustomPage[]; orch: UseOrchestrator; onOpen: () => void; } export function KanbanCard({ card, pages, customPages, orch, onOpen }: KanbanCardProps) { const agentRunning = orch.isRunning(card.id); const run = orch.runForCard(card.id); const bevyRunning = Boolean(run && orch.bevyIsRunning(run.id)); const active = agentRunning || bevyRunning; return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(); } }} title="Open card details" > {/* Header: id + category + counts */}
{card.id} {card.category} {active && ( )} {!active && ( {card.comments.length > 0 && `💬 ${card.comments.length} `} {card.tags.length > 0 && `🏷️ ${card.tags.length} `} {card.references.length > 0 && `🔗 ${card.references.length}`} )}

{card.title}

{card.description}

{/* References (compact) */}
{/* Footer: files + notes + tags (read-only chips here) */} {(card.files || card.notes || card.tags.length > 0) && (
{card.files && (
📁 {card.files}
)} {card.notes &&
💡 {card.notes}
} {card.tags.length > 0 && (
{card.tags.map((tag) => ( 🏷️ {tag} ))}
)}
)}
); } /** Live activity badge: pulses while an agent or Bevy run is in flight. */ function RunningBadge({ agent, bevy }: { agent: boolean; bevy: boolean }) { const color = agent ? 'var(--accent)' : 'var(--purple)'; const label = agent ? 'Agent' : 'Bevy'; return ( {label} running ); } function pulseDotStyle(color: string): React.CSSProperties { return { width: '6px', height: '6px', borderRadius: '50%', background: color, boxShadow: `0 0 6px ${color}`, animation: 'vn-pulse 1.1s ease-in-out infinite', }; } // --- styles ---------------------------------------------------------------- const cardStyle: React.CSSProperties = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', padding: 'var(--sp-3)', cursor: 'pointer', transition: 'all 0.15s ease', }; const badgeStyle: React.CSSProperties = { fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)', background: 'var(--surface-raised)', padding: '2px 6px', borderRadius: 'var(--radius-sm)', }; const categoryStyle: React.CSSProperties = { fontSize: '0.7rem', color: 'var(--fg-dim)', background: 'rgba(100,100,100,0.1)', padding: '2px 6px', borderRadius: 'var(--radius-sm)', }; const titleStyle: React.CSSProperties = { margin: '0 0 var(--sp-1) 0', fontSize: '0.9rem', fontWeight: 500, color: 'var(--fg)', }; const descStyle: React.CSSProperties = { margin: 0, fontSize: '0.8rem', color: 'var(--fg-dim)', lineHeight: 1.4, }; const noteStyle: React.CSSProperties = { fontSize: '0.7rem', color: 'var(--accent)', padding: '4px 8px', background: 'rgba(240,160,48,0.08)', borderRadius: 'var(--radius-sm)', borderLeft: '2px solid var(--accent)', }; const tagStyle: React.CSSProperties = { fontSize: '0.65rem', padding: '2px 6px', background: 'rgba(100,150,255,0.1)', border: '1px solid rgba(100,150,255,0.3)', borderRadius: 'var(--radius-sm)', };