diff --git a/assets/css/components/image-zoom.css b/assets/css/components/image-zoom.css new file mode 100644 index 0000000..1d9addc --- /dev/null +++ b/assets/css/components/image-zoom.css @@ -0,0 +1,42 @@ +/* Minimal styles for Hextra image zoom overlay */ +.hextra-zoom-image-overlay { + position: fixed; + inset: 0; + background: var(--hextra-image-zoom-backdrop, rgba(0, 0, 0, 0.9)); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + opacity: 0; + transition: opacity 0.25s ease-out; + cursor: zoom-out; +} + +.hextra-zoom-image-overlay.show { + opacity: 1; +} + +.hextra-zoom-image { + max-width: min(95vw, 1200px); + max-height: 95vh; + border-radius: 8px; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35); + transition: transform 0.3s ease-out; + transform: scale(0.98); +} + +.hextra-zoom-image-overlay.show .hextra-zoom-image { + transform: scale(1); +} + +@media (prefers-reduced-motion: reduce) { + .hextra-zoom-image-overlay, + .hextra-zoom-image { + transition: none !important; + } +} + +/* Show magnifier cursor over zoomable images in content */ +.content img:not([data-no-zoom]) { + cursor: zoom-in; +} diff --git a/assets/js/image-zoom.js b/assets/js/image-zoom.js new file mode 100644 index 0000000..5e1eb81 --- /dev/null +++ b/assets/js/image-zoom.js @@ -0,0 +1,76 @@ +// Minimal, dependency-free image zoom for Hextra +// - Activates on images inside `.content` +// - Close on overlay click or Escape +// - Opt-out with `data-no-zoom` on + +(function () { + function ready(fn) { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", fn, { once: true }); + } else { + fn(); + } + } + + function createOverlay(src, alt) { + const overlay = document.createElement("div"); + overlay.className = "hextra-zoom-image-overlay"; + overlay.setAttribute("role", "dialog"); + overlay.setAttribute("aria-modal", "true"); + + const img = document.createElement("img"); + img.className = "hextra-zoom-image"; + img.src = src; + if (alt) img.alt = alt; + + overlay.appendChild(img); + + function close() { + overlay.classList.remove("show"); + document.documentElement.style.removeProperty("overflow"); + window.removeEventListener("keydown", onKeyDown, true); + overlay.addEventListener( + "transitionend", + () => overlay.remove(), + { once: true } + ); + } + + function onKeyDown(e) { + if (e.key === "Escape") close(); + } + + overlay.addEventListener("click", close, { once: true }); + window.addEventListener("keydown", onKeyDown, true); + + document.body.appendChild(overlay); + // lock scroll + document.documentElement.style.overflow = "hidden"; + + // trigger fade-in + requestAnimationFrame(() => overlay.classList.add("show")); + } + + ready(function () { + const container = document.querySelector(".content"); + if (!container) return; + + container.addEventListener( + "click", + function (e) { + const target = e.target; + if (!(target instanceof HTMLImageElement)) return; + if (target.dataset.noZoom === "" || target.dataset.noZoom === "true") return; + + // avoid following parent links when zooming + e.preventDefault(); + e.stopPropagation(); + + const src = target.currentSrc || target.src; + if (!src) return; + createOverlay(src, target.alt || ""); + }, + true + ); + }); +})(); diff --git a/docs/hugo.yaml b/docs/hugo.yaml index 578d10e..03356e6 100644 --- a/docs/hugo.yaml +++ b/docs/hugo.yaml @@ -238,3 +238,6 @@ params: # inputPosition: top # lang: en # theme: noborder_dark + + imageZoom: + enable: true diff --git a/layouts/_partials/scripts.html b/layouts/_partials/scripts.html index bf49fc9..702bacb 100644 --- a/layouts/_partials/scripts.html +++ b/layouts/_partials/scripts.html @@ -13,3 +13,10 @@ {{- if (.Store.Get "hasAsciinema") -}} {{- partial "scripts/asciinema.html" . -}} {{- end -}} + +{{/* Image zoom */}} +{{- with site.Params.imageZoom }} + {{- if .enable }} + {{- partial "scripts/image-zoom.html" $ -}} + {{- end -}} +{{- end -}} diff --git a/layouts/_partials/scripts/image-zoom.html b/layouts/_partials/scripts/image-zoom.html new file mode 100644 index 0000000..e074556 --- /dev/null +++ b/layouts/_partials/scripts/image-zoom.html @@ -0,0 +1,13 @@ +{{/* Optional minimal image zoom assets */}} +{{- $js := resources.Get "js/image-zoom.js" -}} +{{- $css := resources.Get "css/components/image-zoom.css" -}} + +{{- if hugo.IsProduction -}} + {{- $js = $js | minify | fingerprint -}} + {{- $css = $css | minify | fingerprint -}} +{{- end -}} + + + + +