import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { Info, X } from 'lucide-react'; import { IconButton } from '@/components/ui/icon-button'; import { getScoreStrokeColor } from '@/lib/score-colors'; import { __, sprintf } from '../lib/i18n'; import { formatCriterionWeight, getVisibilityScoreCriteria, type VisibilityScoreBreakdownMap, type VisibilityScoreCriterion, } from '../lib/visibility-score-criteria'; const TOOLTIP_WIDTH = 224; const TOOLTIP_GAP = 8; const TOOLTIP_VIEWPORT_PAD = 8; interface VisibilityScoreBreakdownModalProps { breakdown: Partial; totalScore: number; totalVisits?: number; uniquePages?: number; rangeDays?: number; onClose: () => void; } function getTooltipCoords(trigger: HTMLElement): { top: number; left: number; placeAbove: boolean } { const rect = trigger.getBoundingClientRect(); const maxLeft = Math.max( TOOLTIP_VIEWPORT_PAD, window.innerWidth - TOOLTIP_WIDTH - TOOLTIP_VIEWPORT_PAD, ); const placeAbove = rect.top >= 96; return { top: placeAbove ? rect.top - TOOLTIP_GAP : rect.bottom + TOOLTIP_GAP, left: Math.min(Math.max(TOOLTIP_VIEWPORT_PAD, rect.left + rect.width / 2 - TOOLTIP_WIDTH / 2), maxLeft), placeAbove, }; } function CriterionInfoTooltip({ description, details, }: { description: string; details?: string; }) { const [open, setOpen] = useState(false); const [coords, setCoords] = useState({ top: 0, left: 0, placeAbove: true }); const triggerRef = useRef(null); const ariaLabel = details ? `${description} ${details}` : description; const show = () => { if (triggerRef.current) { setCoords(getTooltipCoords(triggerRef.current)); } setOpen(true); }; const hide = () => setOpen(false); useEffect(() => { if (!open) { return; } const sync = () => { if (triggerRef.current) { setCoords(getTooltipCoords(triggerRef.current)); } }; window.addEventListener('resize', sync); return () => window.removeEventListener('resize', sync); }, [open]); return ( { event.stopPropagation(); if (open) { hide(); } else { show(); } }} aria-label={ariaLabel} > {open && createPortal(

{description}

{details ?

{details}

: null}
, document.body, )}
); } function CriterionRow({ item }: { item: VisibilityScoreCriterion }) { const color = getScoreStrokeColor(item.score); return (
{item.label} {formatCriterionWeight(item.weight)}
{item.score}
); } export function VisibilityScoreBreakdownModal({ breakdown, totalScore, totalVisits, uniquePages, rangeDays, onClose, }: VisibilityScoreBreakdownModalProps) { const criteria = getVisibilityScoreCriteria(breakdown); const scoreColor = getScoreStrokeColor(totalScore); const hasSummary = typeof totalVisits === 'number' && typeof uniquePages === 'number' && typeof rangeDays === 'number'; useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onClose(); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [onClose]); return createPortal(
event.stopPropagation()} >

{__('Visibility Score Breakdown', 'llmagnet-llm-txt-generator')}

{__('How is the score calculated?', 'llmagnet-llm-txt-generator')}

{criteria.map((item) => ( ))}
{__('Total Score', 'llmagnet-llm-txt-generator')} {hasSummary && (

{sprintf( __('Based on %1$d visits across %2$d pages in the last %3$d days', 'llmagnet-llm-txt-generator'), totalVisits, uniquePages, rangeDays, )}

)}
{totalScore} /100
, document.body, ); }