|
64 | 64 | /* ── Sidebar ─────────────────────────────────────────────────────── */ |
65 | 65 | #sidebar { width: var(--sidebar-w); flex-shrink: 0; background: var(--bg2); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; } |
66 | 66 | #search-wrap { padding: .4rem .5rem; position: relative; } |
67 | | - .search-icon { position: absolute; left: .5rem; top: 50%; transform: translateY(-50%); color: var(--text3); pointer-events: none; } |
68 | | - #search { width: 100%; padding: .375rem .5rem .375rem 1.75rem; font-size: .6875rem; font-family: var(--font); background: var(--bg3); color: var(--text); border: 1px solid transparent; border-radius: var(--radius); outline: none; transition: border-color .15s; } |
| 67 | + .search-icon { position: absolute; left: 1rem; top: 50%; transform: translateY(-50%); color: var(--text3); pointer-events: none; } |
| 68 | + #search { width: 100%; padding: .375rem .5rem .375rem 2.125rem; font-size: .6875rem; font-family: var(--font); background: var(--bg3); color: var(--text); border: 1px solid transparent; border-radius: var(--radius); outline: none; transition: border-color .15s; } |
69 | 69 | #search:focus { border-color: var(--accent); background: var(--bg); } |
70 | 70 | #search::placeholder { color: var(--text3); } |
71 | 71 | #sidebar-list { flex: 1; overflow-y: auto; padding: .375rem; display: flex; flex-direction: column; gap: .375rem; } |
|
113 | 113 | .dot-new { width: 5px; height: 5px; border-radius: 50%; background: var(--orange); } |
114 | 114 | .dot-heat { width: 5px; height: 5px; border-radius: 50%; background: #f59e0b; } |
115 | 115 | /* img-box: scrollable container for per-image zoom+pan */ |
116 | | - .img-box { background: var(--surface); border-radius: var(--radius); overflow: auto; flex: 1; position: relative; cursor: grab; } |
117 | | - .img-box.dragging { cursor: grabbing; } |
| 116 | + .img-box { background: var(--surface); border-radius: var(--radius); overflow: hidden; flex: 1; position: relative; } |
| 117 | + .img-box.pannable { overflow: auto; cursor: move; } |
| 118 | + .img-box.dragging { cursor: move; } |
118 | 119 | .img-box img { display: block; object-fit: contain; transform-origin: 0 0; } |
119 | 120 | /* At 100% zoom, image fills the box */ |
120 | 121 | .img-box[data-zoom="100"] img { width: 100%; height: 100%; } |
|
250 | 251 | /* Thumbnail: annotated mode → heatmap; normal → new image (no annotation) */ |
251 | 252 | var thumb = state.annotated ? (item.heatmap || item.new || item.original || '') : (item.new || item.original || ''); |
252 | 253 | var hasDiff = item.diff || item.heatmap; |
| 254 | + var badgeText = hasDiff |
| 255 | + ? (item.diff_level != null ? item.diff_level + '%' : 'changed') |
| 256 | + : 'pass'; |
253 | 257 | btn.innerHTML = |
254 | 258 | '<div class="thumb-img-wrap"><img src="' + esc(thumb) + '" alt="" loading="lazy"/></div>' + |
255 | 259 | '<div class="thumb-footer">' + |
256 | 260 | '<div class="thumb-name">' + esc(item.name) + '</div>' + |
257 | | - '<span class="thumb-badge ' + (hasDiff ? 'thumb-badge-fail' : 'thumb-badge-pass') + '">' + (hasDiff ? 'changed' : 'pass') + '</span>' + |
| 261 | + '<span class="thumb-badge ' + (hasDiff ? 'thumb-badge-fail' : 'thumb-badge-pass') + '">' + badgeText + '</span>' + |
258 | 262 | '</div>'; |
259 | 263 | btn.addEventListener('click', function() { selectItem(+this.dataset.idx); }); |
260 | 264 | sidebarList.appendChild(btn); |
|
283 | 287 | var pos = filtered.indexOf(state.current); |
284 | 288 | navCounter.textContent = (pos + 1) + '/' + filtered.length; |
285 | 289 | var hasDiff = item.diff || item.heatmap; |
286 | | - topBadge.textContent = hasDiff ? 'changed' : 'pass'; |
| 290 | + if (hasDiff && item.diff_level != null) { |
| 291 | + topBadge.textContent = item.diff_level + '% diff'; |
| 292 | + } else { |
| 293 | + topBadge.textContent = hasDiff ? 'changed' : 'pass'; |
| 294 | + } |
287 | 295 | topBadge.className = 'diff-badge ' + (hasDiff ? 'diff-badge-fail' : 'diff-badge-pass'); |
288 | 296 |
|
289 | 297 | /* Resolve image sources based on view + annotated toggle */ |
|
326 | 334 | viewBtns.forEach(function(btn) { |
327 | 335 | btn.addEventListener('click', function() { state.view = this.dataset.view; render(); buildSidebar(); }); |
328 | 336 | }); |
329 | | - annotateBtn.addEventListener('click', function() { state.annotated = !state.annotated; render(); buildSidebar(); }); |
| 337 | + function toggleAnnotated() { state.annotated = !state.annotated; render(); buildSidebar(); } |
| 338 | + annotateBtn.addEventListener('click', toggleAnnotated); |
| 339 | + |
| 340 | + /* Click on image toggles annotated mode (ignore drags) */ |
| 341 | + var clickStart = null; |
| 342 | + viewArea.addEventListener('mousedown', function(e) { clickStart = { x: e.clientX, y: e.clientY }; }); |
| 343 | + viewArea.addEventListener('click', function(e) { |
| 344 | + if (!clickStart) return; |
| 345 | + var dx = Math.abs(e.clientX - clickStart.x), dy = Math.abs(e.clientY - clickStart.y); |
| 346 | + clickStart = null; |
| 347 | + if (dx > 3 || dy > 3) return; /* was a drag, not a click */ |
| 348 | + if (e.target.closest('.img-box')) toggleAnnotated(); |
| 349 | + }); |
330 | 350 |
|
331 | 351 | /* ── Per-image zoom ────────────────────────────────────────────────── */ |
332 | 352 | function applyZoom() { |
|
339 | 359 | img.style.width = '100%'; |
340 | 360 | img.style.height = '100%'; |
341 | 361 | img.style.objectFit = 'contain'; |
342 | | - box.style.overflow = 'hidden'; |
| 362 | + box.classList.remove('pannable'); |
343 | 363 | } else { |
344 | | - /* Scale image beyond container — enable scrolling */ |
345 | 364 | img.style.width = (s * 100) + '%'; |
346 | 365 | img.style.height = 'auto'; |
347 | 366 | img.style.objectFit = ''; |
348 | | - box.style.overflow = 'auto'; |
| 367 | + box.classList.add('pannable'); |
349 | 368 | } |
350 | 369 | }); |
351 | 370 | $('zoom-label').textContent = state.zoom + '%'; |
352 | 371 | } |
353 | 372 |
|
354 | | - /* ── Per-image drag-to-pan ─────────────────────────────────────────── */ |
| 373 | + /* ── Synchronized drag-to-pan across all img-boxes ───────────────── */ |
| 374 | + var syncing = false; |
| 375 | + |
| 376 | + function syncScroll(source) { |
| 377 | + if (syncing) return; |
| 378 | + syncing = true; |
| 379 | + var boxes = viewInner.querySelectorAll('.img-box'); |
| 380 | + boxes.forEach(function(box) { |
| 381 | + if (box !== source) { |
| 382 | + box.scrollLeft = source.scrollLeft; |
| 383 | + box.scrollTop = source.scrollTop; |
| 384 | + } |
| 385 | + }); |
| 386 | + syncing = false; |
| 387 | + } |
| 388 | + |
355 | 389 | function initPan() { |
356 | | - viewInner.querySelectorAll('.img-box').forEach(function(box) { |
| 390 | + var boxes = viewInner.querySelectorAll('.img-box'); |
| 391 | + boxes.forEach(function(box) { |
357 | 392 | var drag = false, startX, startY, scrollL, scrollT; |
| 393 | + |
| 394 | + /* Sync on any scroll (drag, wheel, trackpad) */ |
| 395 | + box.addEventListener('scroll', function() { syncScroll(box); }); |
| 396 | + |
| 397 | + /* Drag to pan */ |
358 | 398 | box.addEventListener('mousedown', function(e) { |
359 | 399 | if (state.zoom <= 100) return; |
360 | 400 | drag = true; box.classList.add('dragging'); |
|
371 | 411 | }); |
372 | 412 | } |
373 | 413 |
|
374 | | - function zoomTo(v) { state.zoom = Math.max(50, Math.min(400, v)); applyZoom(); } |
| 414 | + function zoomTo(v) { state.zoom = Math.max(25, Math.min(500, v)); applyZoom(); } |
375 | 415 |
|
376 | 416 | $('zoom-in').addEventListener('click', function() { zoomTo(state.zoom + 50); }); |
377 | 417 | $('zoom-out').addEventListener('click', function() { zoomTo(state.zoom - 50); }); |
378 | 418 | $('zoom-fit').addEventListener('click', function() { zoomTo(100); }); |
379 | 419 |
|
| 420 | + /* Ctrl/Cmd/Alt + scroll wheel to zoom (smooth, small steps) */ |
| 421 | + viewArea.addEventListener('wheel', function(e) { |
| 422 | + if (!(e.ctrlKey || e.metaKey || e.altKey)) return; |
| 423 | + e.preventDefault(); |
| 424 | + /* Use 5% steps for smooth zoom; clamp deltaY to avoid trackpad acceleration */ |
| 425 | + var step = 3; |
| 426 | + var direction = e.deltaY < 0 ? 1 : -1; |
| 427 | + zoomTo(state.zoom + direction * step); |
| 428 | + }, { passive: false }); |
| 429 | + |
380 | 430 | /* ── Nav ───────────────────────────────────────────────────────────── */ |
381 | 431 | $('nav-prev').addEventListener('click', selectPrev); |
382 | 432 | $('nav-next').addEventListener('click', selectNext); |
|
0 commit comments