feat: add BranchMap sidebar with BranchNode tree visualization

BranchNode renders a branch button with depth-based indent, status icon,
and status badge using the design system color palette. BranchMap builds
a tree from the flat branch list via buildTree/flattenTree and renders
all nodes with a GitBranch header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-24 09:51:30 +00:00
parent f5056d2e84
commit c0dfa7c230
2 changed files with 172 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
import { CircleDot, CheckCircle2, XCircle, Circle, RotateCcw } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { BranchResponse } from '@/types/branching'
type BranchStatus = BranchResponse['status']
interface StatusConfig {
icon: React.ElementType
textClass: string
badgeClass: string
label: string
}
const STATUS_CONFIG: Record<BranchStatus, StatusConfig> = {
active: {
icon: CircleDot,
textClass: 'text-accent',
badgeClass: 'bg-accent-dim text-accent-text',
label: 'Active',
},
solved: {
icon: CheckCircle2,
textClass: 'text-[#34d399]',
badgeClass: 'bg-[rgba(52,211,153,0.10)] text-[#34d399]',
label: 'Solved',
},
dead_end: {
icon: XCircle,
textClass: 'text-[#f87171]',
badgeClass: 'bg-[rgba(248,113,113,0.10)] text-[#f87171]',
label: 'Dead End',
},
untried: {
icon: Circle,
textClass: 'text-muted',
badgeClass: 'bg-[rgba(79,86,102,0.20)] text-muted',
label: 'Untried',
},
revived: {
icon: RotateCcw,
textClass: 'text-[#eab308]',
badgeClass: 'bg-[rgba(234,179,8,0.10)] text-[#eab308]',
label: 'Revived',
},
}
interface BranchNodeProps {
branch: BranchResponse
depth: number
isActive: boolean
onClick: (branchId: string) => void
}
export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps) {
const config = STATUS_CONFIG[branch.status]
const Icon = config.icon
return (
<button
type="button"
onClick={() => onClick(branch.id)}
style={{ paddingLeft: `${8 + depth * 16}px` }}
className={cn(
'w-full flex items-center gap-2 py-2 pr-3 text-left rounded-md transition-colors',
'hover:bg-elevated',
isActive && 'bg-accent-dim border-l-2 border-accent'
)}
>
<Icon
size={14}
className={cn('shrink-0', config.textClass)}
/>
<span
className={cn(
'flex-1 text-sm truncate',
isActive ? 'text-heading font-medium' : 'text-primary'
)}
>
{branch.label}
</span>
<span
className={cn(
'text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded-full shrink-0',
config.badgeClass
)}
>
{config.label}
</span>
</button>
)
}