feat(dailies): 4:3 photo strip, lightbox, and dot sync
- Aspect ratio 3:2 → 4:3 (less aggressive crop; closer to phone native) - Slides become <button> elements with data-full pointing to original image - Tap/click any photo in the feed opens a full-screen lightbox showing the uncropped original; prev/next browses all feed photos; Esc/arrows/backdrop click close - Dot indicator now syncs with scroll via IntersectionObserver Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
This commit is contained in:
@@ -216,8 +216,13 @@ body::after {
|
|||||||
.journal-photo-slide {
|
.journal-photo-slide {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
aspect-ratio: 3 / 2;
|
aspect-ratio: 4 / 3;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: zoom-in;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal-photo-slide img {
|
.journal-photo-slide img {
|
||||||
@@ -225,8 +230,11 @@ body::after {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.journal-photo-slide:hover img { opacity: 0.9; }
|
||||||
|
|
||||||
.journal-photo-dots {
|
.journal-photo-dots {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ feedMap.on('load', function () {
|
|||||||
{% if images|length > 0 %}
|
{% if images|length > 0 %}
|
||||||
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
<div class="journal-photo-strip" data-slides="{{ images|length }}">
|
||||||
{% for img in images %}
|
{% for img in images %}
|
||||||
<div class="journal-photo-slide">
|
<button class="journal-photo-slide" data-full="{{ img.url }}" data-alt="{{ entry.title }}" aria-label="View photo">
|
||||||
<img src="{{ img.cropResize(900, 600).url }}" alt="{{ entry.title }}" loading="lazy">
|
<img src="{{ img.cropResize(900, 675).url }}" alt="{{ entry.title }}" loading="lazy">
|
||||||
</div>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if images|length > 1 %}
|
{% if images|length > 1 %}
|
||||||
@@ -163,4 +163,74 @@ feedMap.on('load', function () {
|
|||||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="lightbox" id="feed-lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
|
||||||
|
<button class="lightbox-close" id="feed-lb-close" aria-label="Close">✕</button>
|
||||||
|
<button class="lightbox-prev" id="feed-lb-prev" aria-label="Previous">‹</button>
|
||||||
|
<img class="lightbox-img" id="feed-lb-img" src="" alt="">
|
||||||
|
<button class="lightbox-next" id="feed-lb-next" aria-label="Next">›</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ── Journal photo strip: dot sync + lightbox ───────────────── */
|
||||||
|
(function () {
|
||||||
|
/* Dot sync */
|
||||||
|
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
|
||||||
|
var slides = strip.querySelectorAll('.journal-photo-slide');
|
||||||
|
var strip_id = strip.closest('article');
|
||||||
|
if (!strip_id) return;
|
||||||
|
var dots = strip_id.querySelectorAll('.journal-photo-dot');
|
||||||
|
if (!dots.length) return;
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (e) {
|
||||||
|
if (!e.isIntersecting) return;
|
||||||
|
var idx = Array.from(slides).indexOf(e.target);
|
||||||
|
dots.forEach(function (d) { d.classList.remove('is-active'); });
|
||||||
|
if (dots[idx]) dots[idx].classList.add('is-active');
|
||||||
|
});
|
||||||
|
}, { root: strip, threshold: 0.5 });
|
||||||
|
slides.forEach(function (s) { io.observe(s); });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Lightbox */
|
||||||
|
var allSlides = Array.from(document.querySelectorAll('.journal-photo-slide[data-full]'));
|
||||||
|
if (!allSlides.length) return;
|
||||||
|
var lightbox = document.getElementById('feed-lightbox');
|
||||||
|
var lbImg = document.getElementById('feed-lb-img');
|
||||||
|
var current = 0;
|
||||||
|
|
||||||
|
function open(index) {
|
||||||
|
current = index;
|
||||||
|
lbImg.src = allSlides[index].dataset.full;
|
||||||
|
lbImg.alt = allSlides[index].dataset.alt;
|
||||||
|
lightbox.hidden = false;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
document.getElementById('feed-lb-close').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
lightbox.hidden = true;
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
allSlides[current].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() { open((current - 1 + allSlides.length) % allSlides.length); }
|
||||||
|
function next() { open((current + 1) % allSlides.length); }
|
||||||
|
|
||||||
|
allSlides.forEach(function (slide, i) {
|
||||||
|
slide.addEventListener('click', function () { open(i); });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('feed-lb-close').addEventListener('click', close);
|
||||||
|
document.getElementById('feed-lb-prev').addEventListener('click', prev);
|
||||||
|
document.getElementById('feed-lb-next').addEventListener('click', next);
|
||||||
|
lightbox.addEventListener('click', function (e) { if (e.target === lightbox) close(); });
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (lightbox.hidden) return;
|
||||||
|
if (e.key === 'Escape') close();
|
||||||
|
if (e.key === 'ArrowLeft') prev();
|
||||||
|
if (e.key === 'ArrowRight') next();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user