feat: restyle branch sidebar — real cards, scale animation, visible labels

BranchMap header:
- GitBranch icon: text-muted → text-accent-text
- "Branch Map" label: text-muted → text-muted-foreground
- Count: text-muted → text-muted-foreground with font-medium

BranchNode cards:
- Now proper cards: bg-card/60 with 1px border-default, rounded-lg, p-2.5
- Active card: accent-tinted border, subtle glow
- Hover animation: scale(1.02) + shadow — card lifts toward the user
  instead of expanding downward
- Detail section: uses scale-y transform from origin-top, feels like
  the card is growing rather than content sliding down
- Status-aware border colors (accent/success/danger/warning)
- Detail labels ("Tried:", "Result:") now text-foreground font-medium
  instead of invisible text-muted
- Depth indentation via marginLeft instead of paddingLeft

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-24 23:09:59 +00:00
parent 55d24118e0
commit 0f70cca905
2 changed files with 92 additions and 79 deletions

View File

@@ -12,12 +12,10 @@ function buildTree(branches: BranchResponse[]): BranchTreeNode[] {
const map = new Map<string, BranchTreeNode>() const map = new Map<string, BranchTreeNode>()
const roots: BranchTreeNode[] = [] const roots: BranchTreeNode[] = []
// First pass: create nodes
for (const branch of branches) { for (const branch of branches) {
map.set(branch.id, { branch, depth: 0, children: [] }) map.set(branch.id, { branch, depth: 0, children: [] })
} }
// Second pass: assign parents / roots
for (const branch of branches) { for (const branch of branches) {
const node = map.get(branch.id)! const node = map.get(branch.id)!
if (branch.parent_branch_id && map.has(branch.parent_branch_id)) { if (branch.parent_branch_id && map.has(branch.parent_branch_id)) {
@@ -54,27 +52,29 @@ export function BranchMap({ branches, activeBranchId, onSelectBranch }: BranchMa
const flat = flattenTree(roots) const flat = flattenTree(roots)
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-1.5 px-2 pb-2"> <div className="flex items-center gap-1.5 px-2 pb-1">
<GitBranch size={14} className="text-muted" /> <GitBranch size={14} className="text-accent-text" />
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted"> <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
Branch Map Branch Map
</span> </span>
<span className="ml-auto text-[10px] text-muted">{branches.length}</span> <span className="ml-auto text-[10px] font-medium text-muted-foreground">{branches.length}</span>
</div> </div>
{flat.length === 0 ? ( {flat.length === 0 ? (
<p className="px-2 text-xs text-muted">No branches yet.</p> <p className="px-2 text-xs text-muted-foreground">No branches yet.</p>
) : ( ) : (
flat.map(({ branch, depth }) => ( <div className="flex flex-col gap-1.5">
<BranchNode {flat.map(({ branch, depth }) => (
key={branch.id} <BranchNode
branch={branch} key={branch.id}
depth={depth} branch={branch}
isActive={branch.id === activeBranchId} depth={depth}
onClick={onSelectBranch} isActive={branch.id === activeBranchId}
/> onClick={onSelectBranch}
)) />
))}
</div>
)} )}
</div> </div>
) )

View File

@@ -9,6 +9,7 @@ interface StatusConfig {
icon: React.ElementType icon: React.ElementType
textClass: string textClass: string
badgeClass: string badgeClass: string
borderClass: string
label: string label: string
} }
@@ -17,30 +18,35 @@ const STATUS_CONFIG: Record<BranchStatus, StatusConfig> = {
icon: CircleDot, icon: CircleDot,
textClass: 'text-accent', textClass: 'text-accent',
badgeClass: 'bg-accent-dim text-accent-text', badgeClass: 'bg-accent-dim text-accent-text',
borderClass: 'border-accent/40',
label: 'Active', label: 'Active',
}, },
solved: { solved: {
icon: CheckCircle2, icon: CheckCircle2,
textClass: 'text-success', textClass: 'text-success',
badgeClass: 'bg-success-dim text-success', badgeClass: 'bg-success-dim text-success',
borderClass: 'border-success/30',
label: 'Solved', label: 'Solved',
}, },
dead_end: { dead_end: {
icon: XCircle, icon: XCircle,
textClass: 'text-danger', textClass: 'text-danger',
badgeClass: 'bg-danger-dim text-danger', badgeClass: 'bg-danger-dim text-danger',
borderClass: 'border-danger/30',
label: 'Dead End', label: 'Dead End',
}, },
untried: { untried: {
icon: Circle, icon: Circle,
textClass: 'text-muted', textClass: 'text-muted-foreground',
badgeClass: 'bg-elevated text-muted', badgeClass: 'bg-elevated text-muted-foreground',
borderClass: 'border-default',
label: 'Untried', label: 'Untried',
}, },
revived: { revived: {
icon: RotateCcw, icon: RotateCcw,
textClass: 'text-warning', textClass: 'text-warning',
badgeClass: 'bg-warning-dim text-warning', badgeClass: 'bg-warning-dim text-warning',
borderClass: 'border-warning/30',
label: 'Revived', label: 'Revived',
}, },
} }
@@ -60,79 +66,86 @@ export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps
return ( return (
<div <div
style={{ marginLeft: `${depth * 12}px` }}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
<button <button
type="button" type="button"
onClick={() => onClick(branch.id)} onClick={() => onClick(branch.id)}
style={{ paddingLeft: `${8 + depth * 16}px` }}
className={cn( className={cn(
'w-full flex items-center gap-2 py-2 pr-3 text-left rounded-md transition-all duration-150', 'w-full text-left rounded-lg border p-2.5 transition-all duration-200 ease-out',
'hover:bg-elevated', 'origin-center',
isActive && 'bg-accent-dim border-l-2 border-accent' isActive
? cn('bg-card border-accent/50 shadow-[0_0_0_1px_rgba(249,115,22,0.15)]', config.borderClass)
: 'bg-card/60 border-default hover:bg-card hover:border-hover',
showDetail && !isActive && 'scale-[1.02] shadow-[0_2px_8px_rgba(0,0,0,0.25)]',
)} )}
> >
<Icon {/* Header row */}
size={14} <div className="flex items-center gap-2">
className={cn('shrink-0', config.textClass)} <Icon
/> size={14}
<span className={cn('shrink-0', config.textClass)}
className={cn( />
'flex-1 text-sm truncate', <span
isActive ? 'text-heading font-medium' : 'text-primary' className={cn(
)} 'flex-1 text-sm truncate',
> isActive ? 'text-heading font-medium' : 'text-foreground'
{branch.label} )}
</span> >
<span {branch.label}
className={cn( </span>
'text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded-full shrink-0', <span
config.badgeClass 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> {config.label}
</span>
</div>
{/* Expandable detail panel */} {/* Detail section — animates from scale-y-0 to scale-y-100 */}
<div <div
className={cn( className={cn(
'overflow-hidden transition-all duration-200 ease-out', 'transition-all duration-200 ease-out origin-top',
showDetail ? 'max-h-[120px] opacity-100' : 'max-h-0 opacity-0' showDetail
)} ? 'max-h-[120px] opacity-100 scale-y-100 mt-2 pt-2 border-t border-default/50'
style={{ paddingLeft: `${24 + depth * 16}px` }} : 'max-h-0 opacity-0 scale-y-0 mt-0 pt-0'
>
<div className="py-1.5 pr-3 space-y-1">
{branch.context_summary ? (
<>
{branch.context_summary.tried.length > 0 && (
<p className="text-[11px] text-muted-foreground leading-snug">
<span className="text-muted">Tried:</span>{' '}
{branch.context_summary.tried.join(', ')}
</p>
)}
{branch.context_summary.concluded && (
<p className="text-[11px] text-muted-foreground leading-snug">
<span className="text-muted">Result:</span>{' '}
{branch.context_summary.concluded}
</p>
)}
</>
) : (
<p className="text-[11px] text-muted italic">No activity yet</p>
)} )}
{branch.status_reason && ( >
<p className="text-[11px] text-muted-foreground leading-snug"> <div className="space-y-1">
<span className="text-muted">Reason:</span>{' '} {branch.context_summary ? (
{branch.status_reason} <>
</p> {branch.context_summary.tried.length > 0 && (
)} <p className="text-[11px] text-muted-foreground leading-snug">
<div className="flex items-center gap-3 text-[10px] text-muted"> <span className="text-foreground font-medium">Tried:</span>{' '}
<span>{branch.step_count} step{branch.step_count !== 1 ? 's' : ''}</span> {branch.context_summary.tried.join(', ')}
</p>
)}
{branch.context_summary.concluded && (
<p className="text-[11px] text-muted-foreground leading-snug">
<span className="text-foreground font-medium">Result:</span>{' '}
{branch.context_summary.concluded}
</p>
)}
</>
) : (
<p className="text-[11px] text-muted-foreground italic">No activity yet</p>
)}
{branch.status_reason && (
<p className="text-[11px] text-muted-foreground leading-snug">
<span className="text-foreground font-medium">Reason:</span>{' '}
{branch.status_reason}
</p>
)}
<div className="flex items-center gap-3 text-[10px] text-muted-foreground pt-0.5">
<span>{branch.step_count} step{branch.step_count !== 1 ? 's' : ''}</span>
</div>
</div> </div>
</div> </div>
</div> </button>
</div> </div>
) )
} }