From 5e9227766df3d35ba2cabadbc6cabb4aaafbae9e Mon Sep 17 00:00:00 2001 From: david Date: Mon, 19 Oct 2026 16:12:55 +0200 Subject: [PATCH] feat: click image to zoom --- src/components/layout.astro | 6 ++++- src/lib/enhance-image-zoom.ts | 41 +++++++++++++++++++++++++++++++++++ src/styles/global.css | 33 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/lib/enhance-image-zoom.ts diff --git a/src/components/layout.astro b/src/components/layout.astro index cc8a32f..b7bfd21 100644 --- a/src/components/layout.astro +++ b/src/components/layout.astro @@ -51,10 +51,14 @@ import Analytics from "@/components/analytics.astro"; > diff --git a/src/lib/enhance-image-zoom.ts b/src/lib/enhance-image-zoom.ts new file mode 100644 index 0000000..5533913 --- /dev/null +++ b/src/lib/enhance-image-zoom.ts @@ -0,0 +1,41 @@ +let dialog: HTMLDialogElement | undefined; + +function getDialog() { + if (dialog?.isConnected) return dialog; + + dialog = document.createElement("dialog"); + dialog.className = "image-zoom-dialog"; + dialog.setAttribute("aria-label", "Zoomed image"); + dialog.append(document.createElement("img")); + dialog.addEventListener("click", () => dialog?.close()); + document.body.append(dialog); + return dialog; +} + +function openZoom(source: HTMLImageElement) { + const target = getDialog(); + const img = target.querySelector("img")!; + img.src = source.currentSrc || source.src; + img.alt = source.alt; + target.showModal(); +} + +function enhanceImage(img: HTMLImageElement) { + if (img.closest("a")) return; + img.classList.add("image-zoom"); + img.tabIndex = 0; + img.setAttribute("role", "button"); + img.setAttribute("aria-haspopup", "dialog"); + img.addEventListener("click", () => openZoom(img)); + img.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + openZoom(img); + }); +} + +export function enhanceImageZoom() { + for (const img of document.querySelectorAll(".prose img")) { + enhanceImage(img); + } +} diff --git a/src/styles/global.css b/src/styles/global.css index 83854f6..95d478e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -185,3 +185,36 @@ font-weight: 500; color: var(--foreground); } + +.prose img.image-zoom { + cursor: zoom-in; +} + +.image-zoom-dialog { + max-width: 100vw; + max-height: 100vh; + width: 100vw; + height: 100vh; + background: transparent; + cursor: zoom-out; +} + +.image-zoom-dialog[open] { + display: flex; + align-items: center; + justify-content: center; +} + +.image-zoom-dialog::backdrop { + background: color-mix(in oklab, var(--background) 85%, transparent); + backdrop-filter: blur(4px); +} + +.image-zoom-dialog img { + max-width: 100%; + max-height: 100%; +} + +html:has(.image-zoom-dialog[open]) { + overflow: hidden; +}