File size: 4,611 Bytes
5feba25 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | import { useState, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { CheckCircle, Circle, Info, X } from 'lucide-react'
/**
* MetricItem Component
*
* Handles individual health metric with isolated hover state
* Uses React Portal to render popover outside the scrollable sidebar
* Ensures popover floats over the chat area without being clipped
*/
const MetricItem = ({ feature, displayLabel, definition, isCollected }) => {
const [showHint, setShowHint] = useState(false)
const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 })
const iconRef = useRef(null)
// Calculate popover position based on icon location
useEffect(() => {
if (!showHint || !iconRef.current) return
const iconRect = iconRef.current.getBoundingClientRect()
// Position popover to the right of the icon, aligned with top
// Add some spacing (16px) to the right
const left = iconRect.right + 16
const top = iconRect.top - 8 // Slight vertical centering
setPopoverPosition({ top, left })
}, [showHint])
// Handle mouse leaving the metric item
const handleMouseLeave = () => {
setShowHint(false)
}
// Handle mouse entering the metric item
const handleMouseEnter = () => {
setShowHint(true)
}
// Close hint when clicking X button
const handleCloseHint = (e) => {
e.stopPropagation()
setShowHint(false)
}
return (
<>
{/* Metric Item Row */}
<div
className={`flex items-center justify-between gap-2 text-xs p-2 rounded transition-all duration-150 ${
isCollected
? 'text-green-700 bg-green-50 hover:bg-green-100'
: 'text-gray-400 hover:bg-gray-100'
}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Status icon + Label */}
<div className="flex items-center gap-2 flex-1 min-w-0">
{isCollected ? (
<CheckCircle size={16} className="flex-shrink-0" />
) : (
<Circle size={16} className="flex-shrink-0" />
)}
<span className="truncate">{displayLabel}</span>
</div>
{/* Info Icon Button */}
<button
ref={iconRef}
onClick={() => setShowHint(!showHint)}
className="p-1 rounded-full text-gray-400 hover:text-gray-600 hover:bg-gray-200 transition-all duration-150 cursor-help flex-shrink-0"
title="Click for definition"
aria-label={`Information about ${displayLabel}`}
>
<Info size={14} />
</button>
</div>
{/* Popover - Rendered via Portal (outside sidebar) with Glassmorphism */}
{showHint &&
createPortal(
<div
className="fixed z-9999 w-80 rounded-2xl shadow-lg p-4 text-xs leading-relaxed animate-in fade-in duration-200 pointer-events-auto backdrop-blur-md"
style={{
top: `${popoverPosition.top}px`,
left: `${popoverPosition.left}px`,
backgroundColor: 'rgba(220, 252, 231, 0.75)',
borderColor: 'rgba(134, 239, 172, 0.5)',
borderWidth: '1px',
WebkitBackdropFilter: 'blur(12px)',
}}
onMouseEnter={() => setShowHint(true)}
onMouseLeave={() => setShowHint(false)}
>
{/* Header with feature name and close button */}
<div className="flex items-start justify-between gap-3 mb-3">
<span className="font-semibold text-green-900 text-sm">{displayLabel}</span>
<button
onClick={handleCloseHint}
className="flex-shrink-0 text-green-700 hover:text-green-900 transition-colors p-0.5 hover:bg-green-200/40 rounded-lg"
aria-label="Close popover"
>
<X size={16} />
</button>
</div>
{/* Definition text */}
<p className="text-green-900 leading-relaxed mb-2 font-medium">{definition}</p>
{/* Arrow pointer - points back to info icon (matches popover styling) */}
<div
className="absolute w-3 h-3 transform rotate-45"
style={{
right: '-6px',
top: `${8}px`,
backgroundColor: 'rgba(220, 252, 231, 0.85)',
borderTop: '1px solid rgba(134, 239, 172, 0.4)',
borderLeft: '1px solid rgba(134, 239, 172, 0.4)',
}}
/>
</div>,
document.body
)}
</>
)
}
export default MetricItem
|