Files
Arkie-Library-Frontend/src/components/messageStream/bubbles/AdaptiveImageFrame.tsx

86 lines
2.7 KiB
TypeScript
Raw Normal View History

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<number | undefined>(
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 (
<div
className={`relative w-full overflow-hidden bg-black ${
ratio ? "" : SINGLE_IMAGE_FALLBACK_HEIGHT_CLASS
}`}
style={frameStyle}
>
<button
type="button"
onClick={onOpen}
className="block h-full w-full"
aria-label={ariaLabel}
>
<BubbleImage
src={src ?? attachment.url}
fallbackSrc={fallbackSrc}
loading="lazy"
className="h-full w-full object-cover"
onNaturalSize={(w, h) => {
if (w && h) setRatio(w / h);
}}
/>
</button>
{children}
</div>
);
}