Files
vega/src/components/shared/ImageLightbox.tsx
T
Jure 94f610c2d3 fix: article reader scroll lock + click-to-zoom for body images
WebKitGTK collapses body <img> elements to 0 height during re-decode,
shrinking scrollHeight and clamping scrollTop — articles with body images
locked partway down. Give every .prose-article img a fixed 16:9
aspect-ratio + object-fit: contain so the layout box survives the collapse
and scrollHeight stays constant. Body images now click-to-open the existing
ImageLightbox (with cursor: zoom-in affordance), and the lightbox image
itself now closes on click instead of swallowing it.
2026-05-24 20:07:06 +02:00

85 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useCallback } from "react";
interface ImageLightboxProps {
images: string[];
index: number;
onClose: () => void;
onNavigate: (index: number) => void;
}
export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLightboxProps) {
const hasPrev = index > 0;
const hasNext = index < images.length - 1;
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === "Escape") {
e.stopImmediatePropagation();
onClose();
}
if (e.key === "ArrowLeft" && hasPrev) onNavigate(index - 1);
if (e.key === "ArrowRight" && hasNext) onNavigate(index + 1);
}, [onClose, onNavigate, index, hasPrev, hasNext]);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-label="Image viewer"
onClick={onClose}
>
{/* Close button */}
<button
onClick={onClose}
aria-label="Close image viewer"
className="absolute top-4 right-4 text-white/70 hover:text-white text-[20px] w-8 h-8 flex items-center justify-center transition-colors z-10"
>
</button>
{/* Counter */}
{images.length > 1 && (
<div className="absolute top-4 left-4 text-white/50 text-[12px] z-10">
{index + 1} / {images.length}
</div>
)}
{/* Prev arrow */}
{hasPrev && (
<button
onClick={(e) => { e.stopPropagation(); onNavigate(index - 1); }}
aria-label="Previous image"
className="absolute left-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-white text-[28px] w-10 h-10 flex items-center justify-center transition-colors z-10"
>
</button>
)}
{/* Image — click to close (same gesture as clicking the backdrop) */}
<img
src={images[index]}
alt={`Image ${index + 1} of ${images.length}`}
className="max-w-[90vw] max-h-[90vh] object-contain select-none cursor-zoom-out"
onClick={onClose}
draggable={false}
/>
{/* Next arrow */}
{hasNext && (
<button
onClick={(e) => { e.stopPropagation(); onNavigate(index + 1); }}
aria-label="Next image"
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-white text-[28px] w-10 h-10 flex items-center justify-center transition-colors z-10"
>
</button>
)}
</div>
);
}