feat: Add high-resolution floating image zoom preview popup when hovering over candidate photos
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user