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 roots: BranchTreeNode[] = []
// First pass: create nodes
for (const branch of branches) {
map.set(branch.id, { branch, depth: 0, children: [] })
}
// Second pass: assign parents / roots
for (const branch of branches) {
const node = map.get(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)
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1.5 px-2 pb-2">
<GitBranch size={14} className="text-muted" />
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1.5 px-2 pb-1">
<GitBranch size={14} className="text-accent-text" />
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
Branch Map
</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>
{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 }) => (
<BranchNode
key={branch.id}
branch={branch}
depth={depth}
isActive={branch.id === activeBranchId}
onClick={onSelectBranch}
/>
))
<div className="flex flex-col gap-1.5">
{flat.map(({ branch, depth }) => (
<BranchNode
key={branch.id}
branch={branch}
depth={depth}
isActive={branch.id === activeBranchId}
onClick={onSelectBranch}
/>
))}
</div>
)}
</div>
)

View File

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