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;
flex-shrink: 0;
background: #000;
transition: all 0.15s ease;
transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
.candidate-image-item:hover {
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 {
border-color: var(--accent-green);
box-shadow: 0 0 12px rgba(34, 197, 94, 0.6);
transform: translateY(-2px);
box-shadow: 0 0 14px rgba(34, 197, 94, 0.65);
}
.candidate-image-item img {
width: 100%;
@@ -854,6 +855,57 @@
align-items: center;
justify-content: center;
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 {
display: inline-flex;
@@ -1632,6 +1684,12 @@
</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 -->
<div id="toast-container"></div>
@@ -1705,8 +1763,67 @@
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) => {
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');
if (pill) {
const perf = pill.getAttribute('data-performer');
@@ -1717,6 +1834,12 @@
});
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');
if (pill) {
hidePerformerHoverCard();
@@ -5225,7 +5348,7 @@ Return ONLY a valid JSON object with an array named "results":
<!-- Header -->
<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;">
<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 style="font-weight: 700; font-size: 0.95rem; color: #fff; display: flex; align-items: center; gap: 0.4rem;">
<span>${escapeHtml(p.name)}</span>