Components
Image Viewer
A full-screen look at an attached image, opened from the composer's thumb row and from images on a sent message. Click a thumbnail below to open it; arrow keys page, Escape or the scrim closes.
Usage
Mount-to-open rather than an open prop: which image the viewer starts on is
whichever thumb was pressed, and remounting is what resets that. The exit fade
comes from useDialogDismiss — the whole surface, chrome and photograph
together, on the same scrim pair every other dialog uses.
import { ImageViewer } from "@/components/base/image-viewer";
{start !== null ? (
<ImageViewer images={images} startIndex={start} onClose={() => setStart(null)} />
) : null}Fixed colours
Every colour here is fixed rather than tokenised, and deliberately so: the ground is a near-black scrim over the entire page, and the discs sit on the photograph rather than on the page — white on dark in both themes, sized for a thumb on touch.
Behaviour
The scrim is the dismiss target — anywhere off the image closes, which is what
a viewer opened by a tap should answer to. The body is scroll-locked while it
is up (with overscroll-contain as belt to iOS Safari's braces), focus
returns to the thumb that opened it, and the index is read through a clamp
because an upload can land while the viewer is open. Stepping between pictures
re-runs only the image's own fade, keyed on the URL — the step is not the
open, and the surface must not replay.