import { useState, type CSSProperties, type ReactNode } from "react"; import type { Attachment } from "../../../types/post"; import { SINGLE_IMAGE_FALLBACK_HEIGHT_CLASS, SINGLE_IMAGE_MAX_HEIGHT, } from "../../../constants/media"; import { BubbleImage } from "../BubbleImage"; /** * Shared frame that sizes an image to its real aspect ratio. The image always * fills the full width (never any left/right black bars). Behaviour by * orientation: * - Landscape/square: height capped at SINGLE_IMAGE_MAX_HEIGHT; images taller * than the cap are cropped top/bottom (object-cover). * - Portrait (height > width): no height cap — the frame grows to the real * ratio so the whole image shows, uncropped and without side bars (the * bubble is correspondingly tall). * Used by single-image bubbles and 2-image albums. * * The ratio is taken from backend width/height when present (no layout shift), * then refined from the loaded image's intrinsic size so it works even when the * backend omits those fields. See `constants/media.ts`. */ export function AdaptiveImageFrame({ attachment, src, fallbackSrc, onOpen, ariaLabel, children, }: { attachment: Attachment; /** Display source; defaults to the attachment's full url. */ src?: string; fallbackSrc?: (string | undefined)[]; onOpen: () => void; ariaLabel: string; /** Overlays rendered on top of the image (download pill, "+N", etc.). */ children?: ReactNode; }) { const [ratio, setRatio] = useState( attachment.width && attachment.height ? attachment.width / attachment.height : undefined, ); const isPortrait = ratio !== undefined && ratio < 1; let frameStyle: CSSProperties | undefined; if (ratio !== undefined) { frameStyle = isPortrait ? // Portrait: follow the real ratio with no cap → full image, full width, // no crop, no side bars (the frame is tall). { aspectRatio: String(ratio) } : // Landscape/square: real ratio, capped height, cropped top/bottom past it. { aspectRatio: String(ratio), maxHeight: SINGLE_IMAGE_MAX_HEIGHT }; } return (
{children}
); }