feat: branch hover card pops out in front with backdrop dim
On hover, the card: - Grows slightly wider and floats directly over the base card - Shows detail content (tried, result, reason, steps) - Dims everything behind it with a fixed black/30 scrim - Heavy drop shadow for depth (0 8px 32px rgba(0,0,0,0.5)) - Status-colored border maintained Active branch still shows detail inline (no hover needed). Cards below don't shift — the expanded version is position: absolute. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useState } from 'react'
|
||||||
import { CircleDot, CheckCircle2, XCircle, Circle, RotateCcw } from 'lucide-react'
|
import { CircleDot, CheckCircle2, XCircle, Circle, RotateCcw } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { BranchResponse } from '@/types/branching'
|
import type { BranchResponse } from '@/types/branching'
|
||||||
@@ -60,22 +60,20 @@ interface BranchNodeProps {
|
|||||||
|
|
||||||
export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps) {
|
export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps) {
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const cardRef = useRef<HTMLDivElement>(null)
|
|
||||||
const config = STATUS_CONFIG[branch.status]
|
const config = STATUS_CONFIG[branch.status]
|
||||||
const Icon = config.icon
|
const Icon = config.icon
|
||||||
|
|
||||||
const hasDetail = branch.context_summary || branch.status_reason
|
const hasDetail = branch.context_summary || branch.status_reason
|
||||||
const showPopover = isHovered && hasDetail && !isActive
|
const showExpanded = isHovered && hasDetail && !isActive
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={cardRef}
|
|
||||||
style={{ marginLeft: `${depth * 12}px` }}
|
style={{ marginLeft: `${depth * 12}px` }}
|
||||||
className="relative"
|
className="relative"
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
{/* Card */}
|
{/* Base card — always visible */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onClick(branch.id)}
|
onClick={() => onClick(branch.id)}
|
||||||
@@ -86,28 +84,9 @@ export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps
|
|||||||
: 'bg-card/60 border-default hover:bg-card hover:border-hover',
|
: 'bg-card/60 border-default hover:bg-card hover:border-hover',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header row */}
|
<CardHeader icon={Icon} config={config} branch={branch} isActive={isActive} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Inline detail for ACTIVE branch only */}
|
{/* Inline detail for active branch */}
|
||||||
{isActive && hasDetail && (
|
{isActive && hasDetail && (
|
||||||
<div className="mt-2 pt-2 border-t border-default/50 space-y-1">
|
<div className="mt-2 pt-2 border-t border-default/50 space-y-1">
|
||||||
<BranchDetail branch={branch} />
|
<BranchDetail branch={branch} />
|
||||||
@@ -115,29 +94,74 @@ export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Floating popover for HOVERED non-active branches */}
|
{/* Expanded card — floats directly on top of the base card */}
|
||||||
{showPopover && (
|
{showExpanded && (
|
||||||
<div
|
<>
|
||||||
className={cn(
|
{/* Scrim behind to dim everything */}
|
||||||
'absolute z-50 left-full top-0 ml-2 w-64',
|
<div className="fixed inset-0 z-40 bg-black/30" />
|
||||||
'bg-card border rounded-lg p-3 space-y-1',
|
|
||||||
'shadow-[0_4px_20px_rgba(0,0,0,0.4)]',
|
{/* Expanded card positioned over the original */}
|
||||||
'animate-in fade-in-0 zoom-in-95 duration-150',
|
<div
|
||||||
config.borderClass
|
className={cn(
|
||||||
)}
|
'absolute z-50 inset-x-0 top-0',
|
||||||
>
|
'bg-card border rounded-lg p-2.5',
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
'shadow-[0_8px_32px_rgba(0,0,0,0.5)]',
|
||||||
<Icon size={12} className={config.textClass} />
|
config.borderClass,
|
||||||
<span className="text-xs font-medium text-heading truncate">{branch.label}</span>
|
)}
|
||||||
|
style={{
|
||||||
|
/* Grow slightly wider than the base card */
|
||||||
|
marginLeft: -8,
|
||||||
|
marginRight: -8,
|
||||||
|
width: 'calc(100% + 16px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader icon={Icon} config={config} branch={branch} isActive={false} />
|
||||||
|
<div className="mt-2 pt-2 border-t border-default/50 space-y-1">
|
||||||
|
<BranchDetail branch={branch} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BranchDetail branch={branch} />
|
</>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shared detail content used by both inline (active) and popover (hover) */
|
/** Card header row — shared between base and expanded states */
|
||||||
|
function CardHeader({
|
||||||
|
icon: Icon,
|
||||||
|
config,
|
||||||
|
branch,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
|
icon: React.ElementType
|
||||||
|
config: StatusConfig
|
||||||
|
branch: BranchResponse
|
||||||
|
isActive: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detail content — shared between inline (active) and expanded (hover) */
|
||||||
function BranchDetail({ branch }: { branch: BranchResponse }) {
|
function BranchDetail({ branch }: { branch: BranchResponse }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user