From 94f610c2d3499f797b23d01d4ae57bfdc2fbb89d Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Sun, 24 May 2026 20:07:06 +0200 Subject: [PATCH] fix: article reader scroll lock + click-to-zoom for body images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebKitGTK collapses body 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. --- src/components/article/ArticleView.tsx | 22 ++++++++++++++++++++++ src/components/shared/ImageLightbox.tsx | 6 +++--- src/index.css | 5 ++++- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/article/ArticleView.tsx b/src/components/article/ArticleView.tsx index d471304..87b9fb8 100644 --- a/src/components/article/ArticleView.tsx +++ b/src/components/article/ArticleView.tsx @@ -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([]); const [activeId, setActiveId] = useState(""); + const [lightbox, setLightbox] = useState<{ images: string[]; index: number } | null>(null); + + const handleContentClick = useCallback((e: React.MouseEvent) => { + const target = e.target as HTMLElement; + if (!(target instanceof HTMLImageElement)) return; + const root = contentRef.current; + if (!root) return; + const imgs = Array.from(root.querySelectorAll("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() {
+ {lightbox && ( + setLightbox(null)} + onNavigate={(i) => setLightbox({ ...lightbox, index: i })} + /> + )} {/* Footer */}
diff --git a/src/components/shared/ImageLightbox.tsx b/src/components/shared/ImageLightbox.tsx index d82ecf0..fc9bbff 100644 --- a/src/components/shared/ImageLightbox.tsx +++ b/src/components/shared/ImageLightbox.tsx @@ -60,12 +60,12 @@ export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLight )} - {/* Image */} + {/* Image — click to close (same gesture as clicking the backdrop) */} {`Image e.stopPropagation()} + className="max-w-[90vw] max-h-[90vh] object-contain select-none cursor-zoom-out" + onClick={onClose} draggable={false} /> diff --git a/src/index.css b/src/index.css index 9b7a94a..7adefc0 100644 --- a/src/index.css +++ b/src/index.css @@ -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 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 {