Compare commits

...

8 Commits

Author SHA1 Message Date
valknar ba9a9dc9c4 Update page title, URL and meta when navigating between lightbox slides
goToSlide() now calls history.replaceState + updates document.title,
description, og:title/description/url and canonical on each slide change.
replaceState is used so swipe/keyboard navigation doesn't pollute history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:03:08 +02:00
valknar 9900b193f6 Fix page meta not updating on client-side post navigation
When a card was clicked, history.pushState changed the URL but left
document.title, description, og:*, and canonical pointing at the
overview page. navigate() also only updated document.title, not meta.

Add setMeta/syncHeadMeta helpers; update title+meta from POSTS data on
card open, and sync all head meta from the fetched document in navigate().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:00:45 +02:00
valknar 9b8f03d4ff Add Pivoine attribution link to footer colophon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 17:23:40 +02:00
valknar 9434c9f192 Add favicon PNGs, Apple touch icon, and PWA webmanifest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:57:54 +02:00
valknar 24733bca05 fix: tailwind hover patch 2026-06-10 11:01:06 +02:00
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
12 changed files with 322 additions and 24 deletions
+14 -2
View File
@@ -1,5 +1,7 @@
@import "tailwindcss";
@custom-variant hover (&:hover);
@theme {
--color-paper: #f1ebe0;
--color-paper-2: #e9e1d2;
@@ -136,11 +138,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 +200,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; }
+125 -14
View File
@@ -2,6 +2,7 @@
"htmlElements": {
"tags": [
"a",
"article",
"aside",
"body",
"br",
@@ -18,6 +19,7 @@
"h4",
"head",
"header",
"hr",
"html",
"img",
"input",
@@ -25,36 +27,67 @@
"link",
"main",
"meta",
"nav",
"p",
"path",
"picture",
"polyline",
"radialgradient",
"script",
"section",
"source",
"span",
"stop",
"sup",
"strong",
"svg",
"title"
],
"classes": [
"[&::-webkit-scrollbar]:hidden",
"[&_a]:border-[var(--rule)]",
"[&_a]:border-b",
"[&_a]:duration-150",
"[&_a]:text-ink-2",
"[&_a]:transition-colors",
"[&_b]:font-semibold",
"[&_b]:mr-1",
"[&_b]:text-ink",
"[&_h2]:first:mt-0",
"[&_h2]:font-medium",
"[&_h2]:font-sans",
"[&_h2]:leading-none",
"[&_h2]:mb-4",
"[&_h2]:mt-10",
"[&_h2]:text-[10px]",
"[&_h2]:text-ink-soft",
"[&_h2]:tracking-[.22em]",
"[&_h2]:uppercase",
"[&_hr]:bg-[var(--rule-2)]",
"[&_hr]:block",
"[&_hr]:border-none",
"[&_hr]:h-px",
"[&_hr]:mb-0",
"[&_hr]:mt-8",
"[&_p]:font-serif",
"[&_p]:leading-[1.72]",
"[&_p]:mb-0",
"[&_p]:mt-3",
"[&_p]:text-[clamp(15px,1.05vw,17px)]",
"[&_p]:text-ink-2",
"[&_strong]:font-medium",
"[&_strong]:font-sans",
"[&_strong]:text-[13px]",
"[&_strong]:text-ink",
"[&_strong]:tracking-[.02em]",
"[-webkit-box-orient:vertical]",
"[-webkit-line-clamp:2]",
"[background:color-mix(in_oklab,var(--paper)_86%,transparent)]",
"[background:color-mix(in_oklab,var(--paper)_95%,transparent)]",
"[display:-webkit-box]",
"[filter:saturate(.92)_contrast(1.02)]",
"[grid-template-columns:1fr_1px_auto]",
"[grid-template-columns:1fr_auto_1fr]",
"[grid-template-columns:2fr_1fr_1fr_1fr]",
"[grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]",
"[grid-template-columns:repeat(auto-fill,minmax(280px,1fr))]",
"[scrollbar-width:none]",
"[transition-duration:.9s,.6s]",
"[transition-timing-function:cubic-bezier(.2,.7,.1,1),ease]",
"absolute",
@@ -63,12 +96,13 @@
"after:inset-0",
"after:pointer-events-none",
"after:shadow-[inset_0_0_0_1px_rgba(22,17,13,.04)]",
"align-top",
"aspect-[2/3]",
"backdrop-blur-[14px]",
"bg-[var(--paper)]",
"bg-[var(--rule)]",
"bg-current",
"bg-ink",
"bg-paper-2",
"bg-roux",
"bg-transparent",
"block",
"border",
@@ -78,9 +112,11 @@
"border-b",
"border-current",
"border-current/30",
"border-ink",
"border-l-0",
"border-r",
"border-t",
"border-transparent",
"border-white/20",
"bottom-0",
"bottom-[10px]",
"card",
@@ -94,6 +130,7 @@
"flex",
"flex-1",
"flex-col",
"flex-wrap",
"font-display",
"font-light",
"font-medium",
@@ -102,20 +139,36 @@
"font-sans",
"font-serif",
"gap-1",
"gap-1.5",
"gap-10",
"gap-2",
"gap-3",
"gap-4",
"gap-5",
"gap-6",
"gap-[18px]",
"gap-[7px]",
"gap-[clamp(12px,2vw,24px)]",
"gap-[var(--gap)]",
"grid",
"group",
"group-hover:scale-[1.02]",
"h-10",
"h-6",
"h-9",
"h-[30px]",
"h-full",
"h-px",
"hover:[&_a]:border-ink",
"hover:[&_a]:text-ink",
"hover:bg-roux-deep",
"hover:bg-white/10",
"hover:border-ink",
"hover:opacity-100",
"hover:text-ink",
"hover:text-white",
"img-zoom",
"inline-flex",
"inset-0",
"inset-x-0",
"italic",
@@ -140,6 +193,7 @@
"lb-track",
"leading-[0.92]",
"leading-[0.94]",
"leading-[0.95]",
"leading-[1.05]",
"leading-[1.1]",
"leading-[1.4]",
@@ -162,37 +216,64 @@
"max-[720px]:hidden",
"max-[720px]:text-[15px]",
"max-[820px]:[grid-template-columns:1fr_1fr]",
"max-[820px]:block",
"max-[820px]:border-[var(--rule)]",
"max-[820px]:border-t",
"max-[820px]:hidden",
"max-h-[96vh]",
"max-w-[14ch]",
"max-w-[40ch]",
"max-w-[44ch]",
"max-w-[55ch]",
"max-w-[62ch]",
"max-w-[96vw]",
"mb-10",
"mb-2",
"mb-4",
"mb-5",
"mb-6",
"mb-8",
"mb-[14px]",
"min-h-[75vh]",
"min-w-0",
"mix-blend-difference",
"ml-1",
"ml-2",
"ml-3",
"mr-3",
"mt-0.5",
"mt-14",
"mt-2",
"mt-[14px]",
"mt-[var(--gap)]",
"mt-auto",
"nav-link",
"object-contain",
"object-cover",
"opacity-30",
"opacity-50",
"opacity-60",
"opacity-[.035]",
"outline-none",
"overflow-hidden",
"overflow-x-auto",
"pb-0",
"pb-14",
"pb-[clamp(28px,3.5vw,48px)]",
"pb-[clamp(32px,4vw,60px)]",
"pb-px",
"place-items-center",
"placeholder:opacity-90",
"placeholder:text-ink-soft",
"pointer-events-none",
"pt-4",
"pt-9",
"pt-[clamp(36px,5vw,64px)]",
"px-[14px]",
"pt-[clamp(40px,6vw,80px)]",
"pt-[var(--gap)]",
"px-4",
"px-6",
"px-[6px]",
"px-[7px]",
"px-[clamp(24px,4vw,60px)]",
"px-[var(--pad)]",
"py-1.5",
"py-2",
@@ -201,18 +282,24 @@
"py-[14px]",
"py-[3px]",
"py-[5px]",
"py-[clamp(24px,3vw,40px)]",
"py-[clamp(32px,4vw,60px)]",
"py-[clamp(36px,5vw,64px)]",
"py-[clamp(48px,8vw,96px)]",
"py-[var(--gap)]",
"relative",
"ribbon",
"right-4",
"right-[10px]",
"rounded-[1px]",
"rounded-[3px]",
"rounded-full",
"saturate-[1.05]",
"searchpop",
"select-none",
"shrink-0",
"space-y-0",
"sticky",
"tabs",
"tabular-nums",
"text-[10px]",
"text-[11px]",
@@ -222,17 +309,26 @@
"text-[30px]",
"text-[8.5px]",
"text-[clamp(13px,1vw,16px)]",
"text-[clamp(14px,1.1vw,17px)]",
"text-[clamp(15px,1.2vw,18px)]",
"text-[clamp(200px,40vw,520px)]",
"text-[clamp(20px,1.6vw,26px)]",
"text-[clamp(22px,2vw,30px)]",
"text-[clamp(40px,6vw,80px)]",
"text-[clamp(42px,6vw,88px)]",
"text-[clamp(48px,6vw,80px)]",
"text-[clamp(48px,7vw,96px)]",
"text-[clamp(60px,8vw,110px)]",
"text-center",
"text-ink",
"text-ink-2",
"text-ink-soft",
"text-ink-soft/40",
"text-ink-soft/50",
"text-paper",
"text-roux",
"text-white/60",
"top-0",
"top-4",
"top-[10px]",
"tracking-[.005em]",
"tracking-[.045em]",
@@ -245,23 +341,34 @@
"tracking-[.22em]",
"tracking-[.32em]",
"tracking-normal",
"transition-[color,border-color,background]",
"transition-[transform,filter]",
"transition-colors",
"transition-opacity",
"transition-transform",
"translate-y-[6%]",
"uppercase",
"w-10",
"w-12",
"w-6",
"w-8",
"w-9",
"w-[30px]",
"w-full",
"whitespace-nowrap",
"z-10",
"z-50",
"z-[100]"
],
"ids": [
"ai-training",
"content",
"copyright",
"count",
"grid",
"hero",
"imgZoom",
"imgZoomClose",
"imgZoomImg",
"lb",
"lbBloom",
"lbIndex",
@@ -271,12 +378,16 @@
"masthead",
"mhBloom",
"mhDate",
"photography",
"publication",
"responsible-person",
"ribbon",
"ribbonClose",
"roux-data",
"searchInput",
"searchpop",
"tabs"
"technical",
"viewFull"
]
}
}
+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>
+1
View File
@@ -37,6 +37,7 @@
<div>
<h4 class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink m-0 mb-[14px]">Colophon</h4>
<p>Set in Italiana &amp; Cormorant Garamond, with Outfit for typographic furniture. © Roux MMXXVI.</p>
<p class="mt-[14px]">A <a href="https://pivoine.art" class="hover:text-ink border-b border-[var(--rule)]" target="_blank" rel="noopener">Pivoine</a> journal.</p>
<p class="mt-[14px] text-ink">Press
<span class="border border-[var(--rule)] px-[6px] py-[3px] rounded-[3px] font-mono text-[10px]">⌘K</span>
from anywhere to search.
+3 -2
View File
@@ -19,8 +19,9 @@
{{- end }}
<link rel="icon" type="image/svg+xml" href="/assets/roux-mark.svg" />
<link rel="apple-touch-icon" href="/assets/roux-mark.svg" />
<link rel="mask-icon" href="/assets/roux-mark.svg" color="#8a3322" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#f1ebe0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+122 -4
View File
@@ -20,6 +20,36 @@
return esc(text).replace(re, '<mark class="hl">$1</mark>');
}
function setMeta(attr, key, val) {
let el = document.querySelector(`meta[${attr}="${key}"]`);
if (val !== null && val !== undefined && val !== '') {
if (!el) { el = document.createElement('meta'); el.setAttribute(attr, key); document.head.appendChild(el); }
el.setAttribute('content', val);
} else if (el) {
el.remove();
}
}
function syncHeadMeta(doc) {
const canon = doc.querySelector('link[rel="canonical"]');
const curCanon = document.querySelector('link[rel="canonical"]');
if (canon && curCanon) curCanon.href = canon.href;
[
['name', 'description'],
['property', 'og:title'],
['property', 'og:description'],
['property', 'og:url'],
['property', 'og:image'],
['property', 'og:image:width'],
['property', 'og:image:height'],
['name', 'twitter:card'],
['name', 'twitter:image'],
].forEach(([attr, key]) => {
const src = doc.querySelector(`meta[${attr}="${key}"]`);
setMeta(attr, key, src ? src.getAttribute('content') : null);
});
}
// ── Inverted search index
const INDEX = (() => {
const map = new Map();
@@ -211,6 +241,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;
@@ -265,9 +297,22 @@
if (lbIndex) {
lbIndex.innerHTML = `<b>${String(lbIdx + 1).padStart(3,'0')}</b> / ${lbList.length}`;
}
lbBuildMeta(lbList[lbIdx]);
const p = lbList[lbIdx];
lbBuildMeta(p);
syncThumbs();
preloadNeighbors(lbIdx);
if (p && smooth) {
const postTitle = p.title + ' — Roux';
const postUrl = new URL(p.url, location.origin).href;
document.title = postTitle;
history.replaceState({ slug: p.slug }, '', p.url);
setMeta('name', 'description', p.description || null);
setMeta('property', 'og:title', postTitle);
setMeta('property', 'og:description', p.description || null);
setMeta('property', 'og:url', postUrl);
const canon = document.querySelector('link[rel="canonical"]');
if (canon) canon.href = postUrl;
}
}
function preloadNeighbors(idx) {
@@ -348,9 +393,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
@@ -375,6 +420,17 @@
const scopedList = issueId ? POSTS.filter(p => p.issue === issueId) : POSTS;
lbReferrer = location.href;
history.pushState({ slug }, '', card.href);
if (clicked) {
const postTitle = clicked.title + ' — Roux';
const postUrl = new URL(clicked.url, location.origin).href;
document.title = postTitle;
setMeta('name', 'description', clicked.description || null);
setMeta('property', 'og:title', postTitle);
setMeta('property', 'og:description', clicked.description || null);
setMeta('property', 'og:url', postUrl);
const canon = document.querySelector('link[rel="canonical"]');
if (canon) canon.href = postUrl;
}
lbOpen(slug, scopedList.length ? scopedList : POSTS);
});
@@ -429,6 +485,7 @@
}
document.title = doc.title;
syncHeadMeta(doc);
history.pushState({}, '', url);
// Re-init page state
@@ -469,4 +526,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();
});
})();
+16
View File
@@ -0,0 +1,16 @@
{
"name": "Roux",
"short_name": "Roux",
"description": "A fashion journal in one hundred plates.",
"icons": [
{ "src": "/favicon-32.png", "type": "image/png", "sizes": "32x32" },
{ "src": "/apple-touch-icon.png", "type": "image/png", "sizes": "180x180" },
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" },
{ "src": "/assets/roux-mark.svg", "type": "image/svg+xml", "sizes": "any" }
],
"start_url": "/",
"display": "standalone",
"background_color": "#f1ebe0",
"theme_color": "#f1ebe0"
}