feat: Add high-resolution floating image zoom preview popup when hovering over candidate photos

This commit is contained in:
david
2026-08-17 11:08:11 -04:00
parent dfb267c7e3
commit 488e187e81
+129 -6
View File
@@ -822,16 +822,17 @@
cursor: pointer; cursor: pointer;
flex-shrink: 0; flex-shrink: 0;
background: #000; background: #000;
transition: all 0.15s ease; transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1);
} }
.candidate-image-item:hover { .candidate-image-item:hover {
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
transform: translateY(-2px); transform: scale(1.08) translateY(-4px);
z-index: 10;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7), 0 0 16px rgba(6, 182, 212, 0.45);
} }
.candidate-image-item.active { .candidate-image-item.active {
border-color: var(--accent-green); border-color: var(--accent-green);
box-shadow: 0 0 12px rgba(34, 197, 94, 0.6); box-shadow: 0 0 14px rgba(34, 197, 94, 0.65);
transform: translateY(-2px);
} }
.candidate-image-item img { .candidate-image-item img {
width: 100%; width: 100%;
@@ -854,6 +855,57 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.6); box-shadow: 0 2px 5px rgba(0,0,0,0.6);
z-index: 5;
}
/* Floating Enlarged Image Zoom Preview */
.image-zoom-preview-modal {
position: fixed;
z-index: 200000;
pointer-events: none;
background: rgba(15, 23, 42, 0.98);
border: 2px solid var(--accent-cyan);
border-radius: var(--radius-md);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.85), 0 0 35px rgba(6, 182, 212, 0.35);
padding: 0.4rem;
width: 280px;
height: 380px;
opacity: 0;
visibility: hidden;
transform: scale(0.92);
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
backdrop-filter: blur(16px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-zoom-preview-modal.visible {
opacity: 1;
visibility: visible;
transform: scale(1);
}
.image-zoom-preview-modal img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--radius-sm);
display: block;
}
.image-zoom-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.85));
color: #fff;
font-size: 0.75rem;
font-weight: 600;
padding: 1.2rem 0.6rem 0.45rem 0.6rem;
text-align: center;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
pointer-events: none;
} }
.btn-preview-play { .btn-preview-play {
display: inline-flex; display: inline-flex;
@@ -1632,6 +1684,12 @@
</div> </div>
</div> </div>
<!-- Floating Enlarged Candidate Image Zoom Preview -->
<div id="image-zoom-preview-modal" class="image-zoom-preview-modal">
<img id="image-zoom-img" src="" alt="Enlarged Preview" />
<div id="image-zoom-caption" class="image-zoom-caption"></div>
</div>
<!-- Toast Notification Container --> <!-- Toast Notification Container -->
<div id="toast-container"></div> <div id="toast-container"></div>
@@ -1705,8 +1763,67 @@
hoverCard.style.top = `${top}px`; hoverCard.style.top = `${top}px`;
} }
// Global event delegation for performer pills & inputs // ==================== Floating Image Zoom Preview Controller ====================
const imageZoomModal = document.getElementById('image-zoom-preview-modal');
const imageZoomImg = document.getElementById('image-zoom-img');
const imageZoomCaption = document.getElementById('image-zoom-caption');
let zoomTimeout = null;
function showImageZoomPreview(imageUrl, caption, targetEl, event) {
if (!imageZoomModal || !imageZoomImg || !imageUrl) return;
clearTimeout(zoomTimeout);
imageZoomImg.src = imageUrl;
if (imageZoomCaption) imageZoomCaption.textContent = caption || '';
positionImageZoom(targetEl, event);
imageZoomModal.classList.add('visible');
}
function hideImageZoomPreview() {
clearTimeout(zoomTimeout);
if (!imageZoomModal) return;
imageZoomModal.classList.remove('visible');
}
function positionImageZoom(targetEl, event) {
const rect = targetEl ? targetEl.getBoundingClientRect() : { left: event.clientX, top: event.clientY, width: 20, height: 20 };
const zoomWidth = 280;
const zoomHeight = 380;
const margin = 14;
// Default: place to the right of thumbnail
let left = rect.right + margin;
let top = rect.top - 40;
// If overflowing right boundary, place to left of thumbnail
if (left + zoomWidth > window.innerWidth - 10) {
left = rect.left - zoomWidth - margin;
}
// If overflowing top or bottom, clamp within screen
if (top < 10) top = 10;
if (top + zoomHeight > window.innerHeight - 10) {
top = window.innerHeight - zoomHeight - 10;
}
if (left < 10) left = 10;
imageZoomModal.style.left = `${left}px`;
imageZoomModal.style.top = `${top}px`;
}
// Global event delegation for candidate images, performer pills & inputs
document.addEventListener('mouseover', (e) => { document.addEventListener('mouseover', (e) => {
const candItem = e.target.closest('.candidate-image-item');
if (candItem) {
const img = candItem.querySelector('img');
const imgUrl = candItem.getAttribute('data-image-url') || (img ? img.src : '');
const perfCard = candItem.closest('.performer-review-card');
const perfName = perfCard ? (perfCard.querySelector('.stash-performer-checkbox')?.dataset?.name || '') : '';
if (imgUrl) {
showImageZoomPreview(imgUrl, perfName, candItem, e);
return;
}
}
const pill = e.target.closest('.performer-pill, .smb-performer-pill, .modal-performer-pill'); const pill = e.target.closest('.performer-pill, .smb-performer-pill, .modal-performer-pill');
if (pill) { if (pill) {
const perf = pill.getAttribute('data-performer'); const perf = pill.getAttribute('data-performer');
@@ -1717,6 +1834,12 @@
}); });
document.addEventListener('mouseout', (e) => { document.addEventListener('mouseout', (e) => {
const candItem = e.target.closest('.candidate-image-item');
if (candItem) {
hideImageZoomPreview();
return;
}
const pill = e.target.closest('.performer-pill, .smb-performer-pill, .modal-performer-pill'); const pill = e.target.closest('.performer-pill, .smb-performer-pill, .modal-performer-pill');
if (pill) { if (pill) {
hidePerformerHoverCard(); hidePerformerHoverCard();
@@ -5225,7 +5348,7 @@ Return ONLY a valid JSON object with an array named "results":
<!-- Header --> <!-- Header -->
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 0.5rem;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 0.5rem;">
<div style="display: flex; align-items: center; gap: 0.6rem;"> <div style="display: flex; align-items: center; gap: 0.6rem;">
<input type="checkbox" class="stash-performer-checkbox" data-id="${p.id}" ${isSelected ? 'checked' : ''} style="cursor: pointer;" /> <input type="checkbox" class="stash-performer-checkbox" data-id="${p.id}" data-name="${escapeHtml(p.name)}" ${isSelected ? 'checked' : ''} style="cursor: pointer;" />
<div> <div>
<div style="font-weight: 700; font-size: 0.95rem; color: #fff; display: flex; align-items: center; gap: 0.4rem;"> <div style="font-weight: 700; font-size: 0.95rem; color: #fff; display: flex; align-items: center; gap: 0.4rem;">
<span>${escapeHtml(p.name)}</span> <span>${escapeHtml(p.name)}</span>