feat: dashboard - Lightbox image click + arrow-key navigation in Revise modal

Clicking a card image now opens the Revise modal as a lightbox. Adds a
navigation bar with prev/next buttons and a position counter. Arrow keys
(← →), Space/Shift+Space navigate between filtered items; 1-9 favorites
the current image. Grid keyboard shortcuts are suppressed while the modal
is open to prevent conflicts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Hoku
2026-05-07 13:13:19 -07:00
co-authored by Claude Opus 4.7
parent bcadb7e4e3
commit 8790f9771a
3 changed files with 114 additions and 2 deletions
+102 -2
View File
@@ -1314,8 +1314,8 @@ function createCard(d) {
const isVideo = d.media_type === 'video';
const mediaElement = isVideo
? `<video ondblclick="toggleFavorite(this)" data-src="${d.file}" muted loop playsinline preload="metadata" draggable="false"></video>`
: `<img ondblclick="toggleFavorite(this)" data-src="${d.file}" alt="Image ${d.id}" draggable="false">`;
const reviseBtn = isVideo ? '' : `<button class="revise-btn" onclick="openM(${d.id})">REVISE</button>`;
: `<img ondblclick="toggleFavorite(this)" onclick="event.stopPropagation(); openM(${d.id})" data-src="${d.file}" alt="Image ${d.id}" draggable="false" style="cursor:pointer;" title="Click to open lightbox">`;
const reviseBtn = isVideo ? '' : `<button class="revise-btn" onclick="event.stopPropagation(); openM(${d.id})">REVISE</button>`;
const upscaleBtn = isVideo ? '' : `<button class="upscale-btn" onclick="openUpscaleModal(${d.id})" title="Upscale this image">\u2B06</button>`;
const videoBadge = isVideo ? '<div class="video-badge">\u25B6 VIDEO</div>' : '';
@@ -1421,10 +1421,110 @@ function openM(id) {
});
document.getElementById('modal').style.display = 'flex';
// Mark modal as open so grid keyboard shortcuts are suppressed
window._modalOpen = true;
// Attach modal keyboard handler (capture phase so it fires before grid handler)
if (window._modalKeyHandler) {
document.removeEventListener('keydown', window._modalKeyHandler, true);
}
window._modalKeyHandler = function(e) {
if (!window._modalOpen) return;
// Don't intercept typing in form inputs
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const currentId = window.currentModalId;
if (currentId == null || !processedData || processedData.length === 0) return;
// Navigate to next/prev item in the current filtered/sorted set (processedData)
const currentIndex = processedData.findIndex(x => x.id === currentId);
const goNext = () => {
if (currentIndex < processedData.length - 1) {
openM(processedData[currentIndex + 1].id);
}
};
const goPrev = () => {
if (currentIndex > 0) {
openM(processedData[currentIndex - 1].id);
}
};
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
goNext();
break;
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
goPrev();
break;
case ' ':
e.preventDefault();
e.stopPropagation();
if (e.shiftKey) { goPrev(); } else { goNext(); }
break;
case 'Escape':
e.preventDefault();
e.stopPropagation();
closeM();
break;
default:
// 1-9 keys (no shift/ctrl/alt): quick favorite current modal item
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey &&
e.code >= 'Digit1' && e.code <= 'Digit9') {
e.preventDefault();
e.stopPropagation();
const item = activeData ? activeData.find(x => x.id === currentId) : null;
if (item) {
item.favorited = !item.favorited;
if (typeof markItemChanged === 'function') markItemChanged(item);
if (typeof scheduleJSONUpdate === 'function') scheduleJSONUpdate();
// Visual feedback on the modal image
const mImg = document.getElementById('m-img');
if (mImg) {
const color = item.favorited ? '#00cc44' : '#cc4444';
mImg.style.transition = 'box-shadow 0.3s';
mImg.style.boxShadow = `0 0 30px ${color}`;
setTimeout(() => { mImg.style.boxShadow = ''; }, 400);
}
// Update the navigation counter if displayed
const counterEl = document.getElementById('modal-nav-counter');
if (counterEl) {
const favIcon = item.favorited ? '★' : '☆';
counterEl.title = item.favorited ? 'Favorited' : 'Not favorited';
}
}
}
break;
}
};
document.addEventListener('keydown', window._modalKeyHandler, true);
// Update navigation counter
_updateModalCounter();
}
// Update the modal nav counter display
function _updateModalCounter() {
const counterEl = document.getElementById('modal-nav-counter');
if (!counterEl || !processedData) return;
const idx = processedData.findIndex(x => x.id === window.currentModalId);
if (idx === -1) { counterEl.textContent = ''; return; }
counterEl.textContent = `${idx + 1} / ${processedData.length}`;
}
function closeM() {
document.getElementById('modal').style.display = 'none';
window._modalOpen = false;
if (window._modalKeyHandler) {
document.removeEventListener('keydown', window._modalKeyHandler, true);
window._modalKeyHandler = null;
}
}
// THROTTLED JSON Updates
+3
View File
@@ -852,6 +852,9 @@ if (viewport) {
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Suppress grid shortcuts when the lightbox/revise modal is open
// (modal installs its own capture-phase handler that handles navigation)
if (window._modalOpen) return;
switch (e.key) {
case '+':
+9
View File
@@ -668,6 +668,15 @@
<div id="modal">
<button class="modal-close-x" onclick="closeM()" title="Close (Esc)">&times;</button>
<!-- Lightbox navigation bar -->
<div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(0,0,0,0.6); border-bottom: 1px solid #333; flex-shrink: 0;">
<button onclick="(function(){ var idx = processedData.findIndex(function(x){return x.id===window.currentModalId;}); if(idx>0) openM(processedData[idx-1].id); })()" title="Previous image (← or Shift+Space)"
style="background: #222; border: 1px solid #444; color: #ccc; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">◀</button>
<span id="modal-nav-counter" style="font-size: 11px; color: #777; min-width: 60px; text-align: center;"></span>
<button onclick="(function(){ var idx = processedData.findIndex(function(x){return x.id===window.currentModalId;}); if(idx < processedData.length-1) openM(processedData[idx+1].id); })()" title="Next image (→ or Space)"
style="background: #222; border: 1px solid #444; color: #ccc; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">▶</button>
<span style="font-size: 9px; color: #444; margin-left: 8px;">← → Space navigate &nbsp;|&nbsp; 1-9 favorite</span>
</div>
<div class="modal-main">
<div class="preview"><img id="m-img" src=""></div>
<div class="controls">