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:
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user