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;
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user