| import { useState, useRef, useEffect } from 'react' |
| import { createPortal } from 'react-dom' |
| import { CheckCircle, Circle, Info, X } from 'lucide-react' |
|
|
| |
| |
| |
| |
| |
| |
| |
| const MetricItem = ({ feature, displayLabel, definition, isCollected }) => { |
| const [showHint, setShowHint] = useState(false) |
| const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 }) |
| const iconRef = useRef(null) |
|
|
| |
| useEffect(() => { |
| if (!showHint || !iconRef.current) return |
|
|
| const iconRect = iconRef.current.getBoundingClientRect() |
|
|
| |
| |
| const left = iconRect.right + 16 |
| const top = iconRect.top - 8 |
|
|
| setPopoverPosition({ top, left }) |
| }, [showHint]) |
|
|
| |
| const handleMouseLeave = () => { |
| setShowHint(false) |
| } |
|
|
| |
| const handleMouseEnter = () => { |
| setShowHint(true) |
| } |
|
|
| |
| 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 |
|
|