feat: desktop lightbox — click to open, arrows navigate, J/S/X tag, Esc close

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:29:43 +02:00
parent 1c5526c56c
commit a8804547e7
@@ -6,7 +6,9 @@
@keydown.x.window="tagFocused('skip')" @keydown.x.window="tagFocused('skip')"
@keydown.space.prevent.window="tagFocused('skip')" @keydown.space.prevent.window="tagFocused('skip')"
@keydown.left.prevent.window="navigate(-1)" @keydown.left.prevent.window="navigate(-1)"
@keydown.right.prevent.window="navigate(1)"> @keydown.right.prevent.window="navigate(1)"
@keydown.escape.window="closeLightbox()"
@keydown.enter.window="focused && openLightbox(focused)">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-bold">Triage</h1> <h1 class="text-xl font-bold">Triage</h1>
@@ -42,7 +44,7 @@
data-asset-id="{{ photo.id }}" data-asset-id="{{ photo.id }}"
data-tag="{{ photo.tag }}" data-tag="{{ photo.tag }}"
tabindex="0" tabindex="0"
@click="select($el)" @click="openLightbox($el)"
@focus="select($el)"> @focus="select($el)">
<img src="/proxy/thumb/{{ photo.id }}" <img src="/proxy/thumb/{{ photo.id }}"
class="w-full aspect-square object-cover" loading="lazy" alt=""> class="w-full aspect-square object-cover" loading="lazy" alt="">
@@ -63,6 +65,25 @@
{% endfor %} {% endfor %}
</div> </div>
{# ── Lightbox overlay (desktop) ── #}
<div id="lb" class="fixed inset-0 z-50 bg-black/95 flex items-center justify-center" style="display:none">
<button class="absolute top-4 right-4 btn btn-circle btn-sm btn-ghost text-white opacity-60 hover:opacity-100 text-lg"
@click="closeLightbox()">&#10005;</button>
<button class="absolute left-3 top-1/2 -translate-y-1/2 btn btn-circle btn-ghost text-white text-4xl opacity-60 hover:opacity-100"
@click="navigate(-1)">&#8249;</button>
<button class="absolute right-3 top-1/2 -translate-y-1/2 btn btn-circle btn-ghost text-white text-4xl opacity-60 hover:opacity-100"
@click="navigate(1)">&#8250;</button>
<div class="flex flex-col items-center gap-3 px-16 max-w-full">
<img id="lb-img" src="" class="max-h-[82vh] max-w-[88vw] object-contain rounded-lg shadow-2xl" alt="">
<div class="flex items-center gap-4 text-white/60 text-sm">
<span id="lb-date"></span>
<span id="lb-filename" class="opacity-40"></span>
<span id="lb-tag-badge" class="badge badge-sm"></span>
<span class="opacity-30 text-xs">J journal · S story · X skip · ← → navigate · Esc close</span>
</div>
</div>
</div>
{# ── Mobile card UI (hidden on desktop) ── #} {# ── Mobile card UI (hidden on desktop) ── #}
<div id="mobile-view" style="display:none"> <div id="mobile-view" style="display:none">
{# Progress bar #} {# Progress bar #}
@@ -141,6 +162,8 @@ function triageApp(albumId) {
return { return {
focused: null, focused: null,
lightboxOpen: false,
init() { init() {
const first = document.querySelector('.photo-card'); const first = document.querySelector('.photo-card');
if (first) this.select(first); if (first) this.select(first);
@@ -153,6 +176,44 @@ function triageApp(albumId) {
el.classList.add('ring-4', 'ring-white', 'ring-offset-2', 'z-10'); el.classList.add('ring-4', 'ring-white', 'ring-offset-2', 'z-10');
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
if (this.lightboxOpen) this.updateLightbox();
},
openLightbox(el) {
this.select(el);
this.lightboxOpen = true;
document.getElementById('lb').style.display = '';
this.updateLightbox();
},
closeLightbox() {
if (!this.lightboxOpen) return;
this.lightboxOpen = false;
document.getElementById('lb').style.display = 'none';
},
updateLightbox() {
const el = this.focused;
if (!el) return;
const assetId = el.dataset.assetId;
const tag = el.dataset.tag;
document.getElementById('lb-img').src = `/proxy/thumb/${assetId}`;
const timeEl = el.querySelector('div');
document.getElementById('lb-date').textContent = timeEl ? timeEl.textContent.trim() : '';
document.getElementById('lb-filename').textContent = el.dataset.filename || '';
const badgeEl = document.getElementById('lb-tag-badge');
const MAP = {
journal: ['badge-success', 'Journal'],
story: ['badge-info', 'Story'],
skip: ['badge-ghost opacity-60', 'Skip'],
};
if (MAP[tag]) {
badgeEl.className = `badge badge-sm ${MAP[tag][0]}`;
badgeEl.textContent = MAP[tag][1];
} else {
badgeEl.className = 'badge badge-sm badge-outline opacity-30';
badgeEl.textContent = 'Untagged';
}
}, },
navigate(dir) { navigate(dir) {
@@ -187,6 +248,7 @@ function triageApp(albumId) {
} }
updateBadge(el, tag); updateBadge(el, tag);
this.updateCount(); this.updateCount();
if (this.lightboxOpen) this.updateLightbox();
}, },
updateCount() { updateCount() {