Compare commits

..

3 Commits

Author SHA1 Message Date
valknar c9a30a38d7 Fix lightbox close button squishing to oval on narrow mobile screens
flex-shrink: 0 keeps the button at exactly 36×36 px so border-radius: 50%
stays a circle. lb-brand gets min-width: 0 + overflow: hidden so the
brand text yields space instead of pushing the close button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:55:21 +02:00
valknar be24904b78 Add swipe and keyboard navigation to the pure image zoom overlay
Arrow keys and touch swipe now navigate between plates while the
zoom is open, keeping the lightbox in sync underneath. goToZoomSlide
clamps to list bounds and delegates to goToSlide for thumb/index sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:52:05 +02:00
valknar 3b359d2b37 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>
2026-06-07 14:46:24 +02:00
4 changed files with 119 additions and 7 deletions
+12 -2
View File
@@ -136,11 +136,11 @@ mark.hl { background: color-mix(in oklab, var(--roux) 22%, transparent); color:
.lb { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto auto; grid-template-areas: "topbar" "stage" "meta" "thumbs"; }
}
.lb-topbar { grid-area: topbar; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 22px; border-bottom: 1px solid rgba(236,231,221,.08); }
.lb-brand { display: flex; align-items: center; gap: 14px; font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: #c8c0b1; }
.lb-brand { display: flex; align-items: center; gap: 14px; font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: #c8c0b1; min-width: 0; overflow: hidden; }
.lb-brand svg { height: 18px; }
.lb-index { font: 500 11px/1 var(--mono); letter-spacing: .14em; color: #c8c0b1; font-variant-numeric: tabular-nums; }
.lb-index b { color: #ece7dd; }
.lb-close { width: 36px; height: 36px; border: 1px solid rgba(236,231,221,.18); border-radius: 50%; display: grid; place-items: center; color: #ece7dd; }
.lb-close { width: 36px; height: 36px; flex-shrink: 0; border: 1px solid rgba(236,231,221,.18); border-radius: 50%; display: grid; place-items: center; color: #ece7dd; }
.lb-close:hover { background: rgba(236,231,221,.08); }
.lb-stage { grid-area: stage; position: relative; overflow: hidden; display: grid; place-items: center; padding: 24px; min-height: 0; }
.lb-track { position: absolute; inset: 0; display: flex; transition: transform .55s cubic-bezier(.4,.0,.2,1); will-change: transform; }
@@ -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; }
+24 -2
View File
@@ -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"
+17
View File
@@ -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>
+66 -3
View File
@@ -211,6 +211,8 @@
let lbIdx = -1; // index into lbList
let lbBuilt = false;
let lbReferrer = null; // URL to return to on close
let imgZoomOpen = false;
let imgZoomIdx = -1;
function lbOpen(slug, scopedList) {
lbList = scopedList || POSTS;
@@ -348,9 +350,9 @@
});
document.addEventListener('keydown', e => {
if (lb.dataset.open !== 'true') return;
if (e.key === 'Escape') { lbClose(); }
if (e.key === 'ArrowLeft') { e.preventDefault(); goToSlide(lbIdx - 1); }
if (e.key === 'ArrowRight') { e.preventDefault(); goToSlide(lbIdx + 1); }
if (e.key === 'Escape') { if (imgZoomOpen) closeImgZoom(); else lbClose(); }
if (e.key === 'ArrowLeft') { e.preventDefault(); imgZoomOpen ? goToZoomSlide(imgZoomIdx - 1) : goToSlide(lbIdx - 1); }
if (e.key === 'ArrowRight') { e.preventDefault(); imgZoomOpen ? goToZoomSlide(imgZoomIdx + 1) : goToSlide(lbIdx + 1); }
});
// Touch swipe
@@ -469,4 +471,65 @@
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;
imgZoomIdx = lbIdx;
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 = '';
}
function goToZoomSlide(idx) {
if (!lbList.length || !imgZoomImg) return;
imgZoomIdx = Math.max(0, Math.min(idx, lbList.length - 1));
const p = lbList[imgZoomIdx];
if (p) imgZoomImg.src = p.card || p.thumb || '';
goToSlide(imgZoomIdx);
}
// 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);
// Swipe to navigate
let zoomTouchX = null;
imgZoom.addEventListener('touchstart', e => { zoomTouchX = e.touches[0].clientX; }, { passive: true });
imgZoom.addEventListener('touchend', e => {
if (zoomTouchX === null) return;
const dx = e.changedTouches[0].clientX - zoomTouchX;
if (Math.abs(dx) > 50) dx < 0 ? goToZoomSlide(imgZoomIdx + 1) : goToZoomSlide(imgZoomIdx - 1);
zoomTouchX = null;
});
}
// Escape when lightbox is NOT open (standalone zoom)
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && imgZoomOpen && (!lb || lb.dataset.open !== 'true')) closeImgZoom();
});
})();