Three reviewer concerns land here: - Felix #2: magic numbers 0.7 and 300 belong in named constants - Sara #6: the position function had 4 branches and 2 thresholds with zero tests - Leonie FINDING-05: at 320px viewport the flip-left could push the card past the right edge — needed a viewport clamp Move the function to src/lib/utils/hoverCardPosition.ts as a pure (rect, viewport) → {top, left} mapping, with named exports CARD_WIDTH_PX, CARD_HEIGHT_PX, CARD_GAP_PX, BOTTOM_BAND_RATIO, RIGHT_FLIP_THRESHOLD_PX. Add a viewport clamp so left + CARD_WIDTH never exceeds the right edge. Ten unit tests cover default placement, flip-up (both triggers), flip-left, flip-right-edge clamp, and scroll offset. TranscriptionReadView passes the current window viewport in on each call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
/**
|
|
* 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;
|
|
scrollX: number;
|
|
scrollY: 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 + vp.scrollY),
|
|
left: Math.max(0, left + vp.scrollX)
|
|
};
|
|
}
|