/** * Pure positioning logic for the person-mention hover card. * * Pulled out of TranscriptionReadView so the four placement branches * (default, flip-up, flip-left, both) plus the viewport clamp are unit-testable * without DOM. Sara's PR-B2 review #6 (no test for computeCardPosition) and * Leonie's FINDING-05 (320px overflow) both land here. */ /** Width of the rendered hover card. Mirrored in PersonHoverCard.svelte's CSS. */ export const CARD_WIDTH_PX = 320; /** Min-height of the rendered hover card. Mirrored in PersonHoverCard.svelte's CSS. */ export const CARD_HEIGHT_PX = 180; /** Gap between the mention rect and the card so they do not touch. */ export const CARD_GAP_PX = 6; /** * Mentions in the bottom 30% of the viewport flip the card up by default, * even if it would numerically fit below — keeping the eye-line stable * is more important than minimal travel (Leonie #5329). */ export const BOTTOM_BAND_RATIO = 0.7; /** * Mentions within this distance of the right viewport edge flip the card * left so it stays fully visible. */ export const RIGHT_FLIP_THRESHOLD_PX = 300; export type Viewport = { viewportWidth: number; viewportHeight: number; }; export type CardPosition = { top: number; left: number }; /** * Compute absolute-positioned top/left for the hover card, given a rect for * the mention anchor and the current viewport. Output is in document * coordinates (already includes scroll offsets). */ export function computeHoverCardPosition(rect: DOMRect, vp: Viewport): CardPosition { let top = rect.bottom + CARD_GAP_PX; let left = rect.left; const overflowsBottom = vp.viewportHeight - rect.bottom < CARD_HEIGHT_PX + CARD_GAP_PX; const inBottomBand = rect.top > vp.viewportHeight * BOTTOM_BAND_RATIO; if (overflowsBottom || inBottomBand) { top = rect.top - CARD_HEIGHT_PX - CARD_GAP_PX; } if (vp.viewportWidth - rect.left < RIGHT_FLIP_THRESHOLD_PX) { left = rect.right - CARD_WIDTH_PX; } // Clamp left so the card never extends past the right viewport edge // (FINDING-05: at 320px viewport the flip would otherwise produce a // negative left or right-side overflow). left = Math.min(left, vp.viewportWidth - CARD_WIDTH_PX - CARD_GAP_PX); return { top: Math.max(0, top), left: Math.max(0, left) }; }