Add full-image zoom overlay to single post pages
A "View full image" button appears in the single-post hero alongside the return link. Clicking the lightbox main image also opens the same overlay from any page. The overlay is pure image on a near-black backdrop; click backdrop, close button, or Escape to dismiss. Escape is layered: closes zoom first, then lightbox if zoom is not open. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -198,6 +198,16 @@ input[type="search"]::-webkit-search-cancel-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── full-image zoom overlay ── */
|
||||
.img-zoom {
|
||||
position: fixed; inset: 0; z-index: 400;
|
||||
background: rgba(8,6,4,.97);
|
||||
display: none; place-items: center;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
.img-zoom[data-open="true"] { display: grid; }
|
||||
.lb-img { cursor: zoom-in; }
|
||||
|
||||
/* ── scrollbar ── */
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 0; }
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
{{ define "main" }}
|
||||
{{- $issue := index (.Params.issues | default (slice "01")) 0 }}
|
||||
{{- $issueURL := printf "/issues/%s/" $issue }}
|
||||
{{- $img := .Resources.GetMatch "*.png" }}
|
||||
{{- $cardSrc := "" }}
|
||||
{{- if $img }}{{- $c := $img.Resize "900x1350 webp" }}{{- $cardSrc = $c.RelPermalink }}{{- end }}
|
||||
|
||||
<section id="hero" class="px-[var(--pad)] pt-[clamp(36px,5vw,64px)] pb-[clamp(28px,3.5vw,48px)] border-b border-[var(--rule)]">
|
||||
<div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">
|
||||
№ {{ $issue }} · {{ index (.Params.categories | default (slice "Plate")) 0 }}
|
||||
</div>
|
||||
<h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4"><em>{{ .Title }}</em></h1>
|
||||
<p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
|
||||
<p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0 mb-5">
|
||||
{{ .Params.description }}
|
||||
<a href="{{ $issueURL }}" class="border-b border-current ml-1">Return to issue →</a>
|
||||
</p>
|
||||
<div class="flex items-center gap-5 flex-wrap">
|
||||
<a href="{{ $issueURL }}"
|
||||
class="font-sans font-medium text-[11px] leading-none tracking-[.16em] uppercase text-ink-soft
|
||||
border-b border-[var(--rule)] pb-px transition-colors duration-200 hover:text-ink hover:border-ink">
|
||||
← Return to issue
|
||||
</a>
|
||||
{{- if $cardSrc }}
|
||||
<button id="viewFull" data-src="{{ $cardSrc }}"
|
||||
class="inline-flex items-center gap-[7px]
|
||||
font-sans font-medium text-[11px] leading-none tracking-[.16em] uppercase text-ink-soft
|
||||
border-b border-[var(--rule)] pb-px transition-colors duration-200 hover:text-ink hover:border-ink">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
|
||||
</svg>
|
||||
View full image
|
||||
</button>
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="grid" data-density="default"
|
||||
|
||||
@@ -59,6 +59,23 @@
|
||||
{{- $posts | jsonify | safeJS }}
|
||||
</script>
|
||||
|
||||
{{/* Pure image zoom — opened by hero button or clicking the lightbox main image */}}
|
||||
<div id="imgZoom" class="img-zoom" role="dialog" aria-modal="true" aria-label="Full size image">
|
||||
<button id="imgZoomClose"
|
||||
class="absolute top-4 right-4 z-10
|
||||
w-10 h-10 grid place-items-center
|
||||
border border-white/20 rounded-full
|
||||
text-white/60 hover:text-white hover:bg-white/10
|
||||
transition-colors duration-200"
|
||||
aria-label="Close full image">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<img id="imgZoomImg" src="" alt="" class="max-w-[96vw] max-h-[96vh] object-contain select-none" />
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+44
-1
@@ -211,6 +211,7 @@
|
||||
let lbIdx = -1; // index into lbList
|
||||
let lbBuilt = false;
|
||||
let lbReferrer = null; // URL to return to on close
|
||||
let imgZoomOpen = false;
|
||||
|
||||
function lbOpen(slug, scopedList) {
|
||||
lbList = scopedList || POSTS;
|
||||
@@ -348,7 +349,7 @@
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (lb.dataset.open !== 'true') return;
|
||||
if (e.key === 'Escape') { lbClose(); }
|
||||
if (e.key === 'Escape') { if (imgZoomOpen) closeImgZoom(); else lbClose(); }
|
||||
if (e.key === 'ArrowLeft') { e.preventDefault(); goToSlide(lbIdx - 1); }
|
||||
if (e.key === 'ArrowRight') { e.preventDefault(); goToSlide(lbIdx + 1); }
|
||||
});
|
||||
@@ -469,4 +470,46 @@
|
||||
lbOpen(window.__ROUX_OPEN_SLUG, scoped.length ? scoped : POSTS);
|
||||
}
|
||||
|
||||
// ── Full-image zoom overlay
|
||||
const imgZoom = document.getElementById('imgZoom');
|
||||
const imgZoomImg = document.getElementById('imgZoomImg');
|
||||
|
||||
function openImgZoom(src) {
|
||||
if (!imgZoom || !imgZoomImg || !src) return;
|
||||
imgZoomImg.src = src;
|
||||
imgZoom.dataset.open = 'true';
|
||||
imgZoomOpen = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeImgZoom() {
|
||||
if (!imgZoom) return;
|
||||
imgZoom.dataset.open = 'false';
|
||||
imgZoomOpen = false;
|
||||
if (!lb || lb.dataset.open !== 'true') document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Hero "View full image" button
|
||||
const viewFullBtn = document.getElementById('viewFull');
|
||||
if (viewFullBtn) {
|
||||
viewFullBtn.addEventListener('click', () => openImgZoom(viewFullBtn.dataset.src));
|
||||
}
|
||||
|
||||
// Click on the lightbox main image → zoom
|
||||
document.addEventListener('click', e => {
|
||||
const img = e.target.closest('.lb-img');
|
||||
if (img) openImgZoom(img.src);
|
||||
});
|
||||
|
||||
// Close on backdrop click or close button
|
||||
if (imgZoom) {
|
||||
imgZoom.addEventListener('click', e => { if (e.target === imgZoom) closeImgZoom(); });
|
||||
document.getElementById('imgZoomClose')?.addEventListener('click', closeImgZoom);
|
||||
}
|
||||
|
||||
// Escape when lightbox is NOT open (standalone zoom)
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && imgZoomOpen && (!lb || lb.dataset.open !== 'true')) closeImgZoom();
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user