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.
This commit is contained in:
Jure
2026-05-24 20:07:06 +02:00
parent d3d855e7f3
commit 94f610c2d3
3 changed files with 29 additions and 4 deletions
+22
View File
@@ -10,6 +10,7 @@ import { nip19 } from "@nostr-dev-kit/ndk";
import { useProfile } from "../../hooks/useProfile";
import { profileName } from "../../lib/utils";
import { ZapModal } from "../zap/ZapModal";
import { ImageLightbox } from "../shared/ImageLightbox";
// ── Types ────────────────────────────────────────────────────────────────────
@@ -138,6 +139,18 @@ export function ArticleView() {
const [progress, setProgress] = useState(0);
const [headings, setHeadings] = useState<TocHeading[]>([]);
const [activeId, setActiveId] = useState("");
const [lightbox, setLightbox] = useState<{ images: string[]; index: number } | null>(null);
const handleContentClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (!(target instanceof HTMLImageElement)) return;
const root = contentRef.current;
if (!root) return;
const imgs = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
const index = imgs.indexOf(target);
if (index < 0) return;
setLightbox({ images: imgs.map((img) => img.src), index });
}, []);
// Extract headings from rendered content and assign IDs
useEffect(() => {
@@ -398,8 +411,17 @@ export function ArticleView() {
<div
ref={contentRef}
className="prose-article"
onClick={handleContentClick}
dangerouslySetInnerHTML={{ __html: bodyHtml }}
/>
{lightbox && (
<ImageLightbox
images={lightbox.images}
index={lightbox.index}
onClose={() => setLightbox(null)}
onNavigate={(i) => setLightbox({ ...lightbox, index: i })}
/>
)}
{/* Footer */}
<div className="mt-10 pt-6 border-t border-border flex items-center justify-between">
+3 -3
View File
@@ -60,12 +60,12 @@ export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLight
</button>
)}
{/* Image */}
{/* 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"
onClick={(e) => e.stopPropagation()}
className="max-w-[90vw] max-h-[90vh] object-contain select-none cursor-zoom-out"
onClick={onClose}
draggable={false}
/>
+4 -1
View File
@@ -104,7 +104,10 @@ html.font-readable body {
.prose-article a { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; }
.prose-article hr { border: none; border-top: 1px solid var(--color-border); margin: 2em 0; }
.prose-article strong { color: var(--color-text); font-weight: 600; }
.prose-article img { max-width: 100%; border-radius: 2px; margin: 1em 0; }
/* WebKitGTK scroll-lock workaround: body <img> sometimes collapses to 0 height on re-decode,
shrinking scrollHeight and clamping the scroll. A fixed aspect-ratio reserves a layout box
that survives the collapse. object-fit: contain preserves the image's own ratio inside it. */
.prose-article img { display: block; width: 100%; max-width: 100%; aspect-ratio: 16 / 9; object-fit: contain; background: var(--color-bg-raised); border-radius: 2px; margin: 1em 0; cursor: zoom-in; }
/* View transition fade-in */
@keyframes fade-in {