diff --git a/web/src/components/markdown.tsx b/web/src/components/markdown.tsx index 8344cb4..f5b9070 100644 --- a/web/src/components/markdown.tsx +++ b/web/src/components/markdown.tsx @@ -18,6 +18,8 @@ function AuthenticatedImage({ const [captureTimestamp, setCaptureTimestamp] = React.useState(null); const [zoomed, setZoomed] = React.useState(false); const [zoomScale, setZoomScale] = React.useState(1); + const [pan, setPan] = React.useState({ x: 0, y: 0 }); + const dragRef = React.useRef<{ x: number; y: number; panX: number; panY: number } | null>(null); const imageSrc = typeof src === "string" ? src : undefined; React.useEffect(() => { @@ -29,6 +31,7 @@ function AuthenticatedImage({ setCaptureTimestamp(null); setZoomed(false); setZoomScale(1); + setPan({ x: 0, y: 0 }); return () => { cancelled = true; }; @@ -82,15 +85,36 @@ function AuthenticatedImage({ open={zoomed} onOpenChange={(next) => { setZoomed(next); - if (!next) setZoomScale(1); + if (!next) { + setZoomScale(1); + setPan({ x: 0, y: 0 }); + } }} > -
-
+
{ + if (zoomScale <= 1 || event.button !== 0) return; + event.currentTarget.setPointerCapture(event.pointerId); + dragRef.current = { x: event.clientX, y: event.clientY, panX: pan.x, panY: pan.y }; + }} + onPointerMove={(event) => { + const drag = dragRef.current; + if (!drag) return; + setPan({ x: drag.panX + event.clientX - drag.x, y: drag.panY + event.clientY - drag.y }); + }} + onPointerUp={(event) => { + dragRef.current = null; + event.currentTarget.releasePointerCapture?.(event.pointerId); + }} + onPointerCancel={() => { dragRef.current = null; }} + style={{ cursor: zoomScale > 1 ? (dragRef.current ? "grabbing" : "grab") : "default" }} + > +
{alt
+ {Math.round(zoomScale * 100)}%