diff --git a/links/templates/links/link_list.html b/links/templates/links/link_list.html index 538f93f..a09a2ea 100644 --- a/links/templates/links/link_list.html +++ b/links/templates/links/link_list.html @@ -466,6 +466,33 @@ .fan-lightbox-close:hover, .fan-lightbox-close:focus-visible { background: rgba(255, 255, 255, 0.28); } .fan-lightbox-close svg { width: 1.2rem; height: 1.2rem; } + /* ---------- Fan lightbox: prev / next navigation ---------- */ + .fan-lightbox-nav { + position: absolute; top: 50%; transform: translateY(-50%); + width: 3rem; height: 3rem; border-radius: 50%; border: none; + display: flex; align-items: center; justify-content: center; + background: rgba(255, 255, 255, 0.14); color: #fff; cursor: pointer; + transition: background 0.2s ease, opacity 0.2s ease; + -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); + } + .fan-lightbox-nav:hover, .fan-lightbox-nav:focus-visible { background: rgba(255, 255, 255, 0.28); } + .fan-lightbox-nav svg { width: 1.6rem; height: 1.6rem; } + .fan-lightbox-nav.prev { left: 1.1rem; } + .fan-lightbox-nav.next { right: 1.1rem; } + .fan-lightbox-counter { + position: absolute; bottom: 1.3rem; left: 50%; transform: translateX(-50%); + padding: 0.35rem 1rem; border-radius: 980px; + background: rgba(255, 255, 255, 0.14); color: #fff; + font-size: 0.85rem; font-weight: 500; letter-spacing: 0.01em; + -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); + pointer-events: none; + } + @media (max-width: 640px) { + .fan-lightbox-nav { width: 2.6rem; height: 2.6rem; } + .fan-lightbox-nav.prev { left: 0.6rem; } + .fan-lightbox-nav.next { right: 0.6rem; } + } + /* ---------- Accessibility: visible keyboard focus ---------- */ a:focus-visible, button:focus-visible { outline: 2px solid var(--apple-blue); @@ -729,6 +756,17 @@ {% trans 'Random Image' %} + + + @@ -871,11 +909,41 @@ const fanLightbox = document.getElementById('fan-lightbox'); const fanLightboxImg = document.getElementById('fan-lightbox-img'); const fanLightboxClose = document.getElementById('fan-lightbox-close'); + const fanLightboxPrev = document.getElementById('fan-lightbox-prev'); + const fanLightboxNext = document.getElementById('fan-lightbox-next'); + const fanLightboxCounter = document.getElementById('fan-lightbox-counter'); + + // Snapshot of the five fan-card images, captured once so navigation works + // even if the DOM shuffles later. We store the live elements and + // read their current src each time, so refreshed (re-shuffled) images are + // reflected without rebuilding this list. + const fanImages = Array.from(document.querySelectorAll('.fan-card img')); + let fanIndex = -1; + + function showFanImage(index) { + if (!fanImages.length || !fanLightboxImg) return; + // Wrap around the ends so left/right never dead-end. + fanIndex = ((index % fanImages.length) + fanImages.length) % fanImages.length; + const src = fanImages[fanIndex].currentSrc || fanImages[fanIndex].src; + // Preserve any inline opacity transition state from the source image. + fanLightboxImg.src = src; + fanLightboxImg.alt = fanImages[fanIndex].alt; + if (fanLightboxCounter) { + fanLightboxCounter.textContent = (fanIndex + 1) + ' / ' + fanImages.length; + } + } window.openFanLightbox = function (img) { if (!img || !fanLightbox || !fanLightboxImg) return; - fanLightboxImg.src = img.currentSrc || img.src; - fanLightboxImg.alt = img.alt; + // Track which of the five images was clicked so navigation starts there. + fanIndex = fanImages.indexOf(img); + if (fanIndex < 0) { + // Fallback: match by current src if the element reference was replaced. + const src = img.currentSrc || img.src; + fanIndex = fanImages.findIndex(i => (i.currentSrc || i.src) === src); + if (fanIndex < 0) fanIndex = 0; + } + showFanImage(fanIndex); fanLightbox.hidden = false; document.body.style.overflow = 'hidden'; fanLightboxClose.focus(); @@ -885,17 +953,35 @@ if (!fanLightbox) return; fanLightbox.hidden = true; fanLightboxImg.src = ''; + fanIndex = -1; document.body.style.overflow = ''; } if (fanLightbox) { fanLightboxClose.addEventListener('click', closeFanLightbox); - // Click the dimmed backdrop (not the image itself) to dismiss. + if (fanLightboxPrev) { + fanLightboxPrev.addEventListener('click', function (event) { + event.stopPropagation(); + showFanImage(fanIndex - 1); + fanLightboxPrev.focus(); + }); + } + if (fanLightboxNext) { + fanLightboxNext.addEventListener('click', function (event) { + event.stopPropagation(); + showFanImage(fanIndex + 1); + fanLightboxNext.focus(); + }); + } + // Click the dimmed backdrop (not the image or nav buttons) to dismiss. fanLightbox.addEventListener('click', function (event) { if (event.target === fanLightbox) closeFanLightbox(); }); document.addEventListener('keydown', function (event) { - if (event.key === 'Escape' && !fanLightbox.hidden) closeFanLightbox(); + if (fanLightbox.hidden) return; + if (event.key === 'Escape') closeFanLightbox(); + else if (event.key === 'ArrowLeft') { event.preventDefault(); showFanImage(fanIndex - 1); } + else if (event.key === 'ArrowRight') { event.preventDefault(); showFanImage(fanIndex + 1); } }); }