From 6b2458f6748707f262e618c6bb878c4871f1811b Mon Sep 17 00:00:00 2001 From: jay Date: Tue, 30 Jun 2026 16:13:00 -0400 Subject: [PATCH] =?UTF-8?q?art:=20variable=20gallery-inspector=20zoom=20(1?= =?UTF-8?q?=C3=97=E2=80=934=C3=97=20toolbar)=20instead=20of=20a=20binary?= =?UTF-8?q?=20jump=20(Codex)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworked the lightbox desktop zoom from a fixed 2.5× toggle into a proper inspector: enter at 1.5×, a quiet floating toolbar (− / slider / + / % / Fit) drives a continuous 1×–4× scale in 0.1 steps, cursor movement keeps panning (transform-origin). Fit returns to the framed gallery view; Escape steps out then closes; the slider takes native arrow keys. Removed click-to-exit on the artwork (too easy to trigger while inspecting) — exit is the visible Fit control or Escape. Toolbar is a translucent dark pill, hidden on touch (native pinch). Zoom resets when the lightbox closes. Uses the cached full-res asset. Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/art/+page.svelte | 62 ++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/frontend/src/routes/art/+page.svelte b/frontend/src/routes/art/+page.svelte index 4f576b5..7f1cd55 100644 --- a/frontend/src/routes/art/+page.svelte +++ b/frontend/src/routes/art/+page.svelte @@ -27,12 +27,15 @@ // screens; desktop and portrait art stay upright). Aspect is read off the loaded image. let landscape = $state(false); - // Desktop zoom: inside the lightbox, magnify the artwork and pan by moving the cursor - // (transform-origin tracks the pointer). Mobile keeps native pinch — the Zoom button is - // hidden there. ox/oy are the transform-origin in %. + // Desktop zoom: a gallery inspector inside the lightbox. Variable 1×–4× via a floating + // toolbar (− / slider / + / % / Fit); moving the cursor pans (transform-origin tracks the + // pointer). Mobile keeps native pinch — the Zoom button + toolbar are hidden there. let zoomed = $state(false); - let ox = $state(50), oy = $state(50); - function enterZoom() { ox = 50; oy = 50; zoomed = true; } + let zoomLevel = $state(1.5); // scale factor; enter at a gentle 1.5× + let ox = $state(50), oy = $state(50); // transform-origin (%) — the panned-to point + function enterZoom() { zoomLevel = 1.5; ox = 50; oy = 50; zoomed = true; } + function fit() { zoomed = false; } // back to the framed gallery view + function setZoom(v) { zoomLevel = Math.max(1, Math.min(4, Math.round(v * 10) / 10)); } function panZoom(e) { const r = e.currentTarget.getBoundingClientRect(); ox = Math.max(0, Math.min(100, ((e.clientX - r.left) / r.width) * 100)); @@ -41,11 +44,11 @@ function onKey(e) { if (e.key !== 'Escape') return; - if (zoomed) zoomed = false; // Escape steps out of zoom first, then closes + if (zoomed) zoomed = false; // Escape steps out of inspection first, then closes else if (zoom) zoom = false; } - // Leaving the lightbox always drops zoom too, so re-opening starts framed. - $effect(() => { if (!zoom) zoomed = false; }); + // Leaving the lightbox always resets the inspector, so re-opening starts framed. + $effect(() => { if (!zoom) { zoomed = false; zoomLevel = 1.5; } }); // Move focus to the lightbox when it opens, so Escape/Enter work and focus is trapped sanely. $effect(() => { if (zoom && lightboxEl) lightboxEl.focus(); }); @@ -241,12 +244,18 @@ {#if zoomed} - - - Move to look closer · click to zoom out + style="transform:scale({zoomLevel});transform-origin:{ox}% {oy}%" draggable="false" /> + +
+ + + + {Math.round(zoomLevel * 100)}% + +
{:else} @@ -546,14 +555,33 @@ /* magnified inspection view (desktop): cursor pans via transform-origin */ .lb-zoom { position: relative; z-index: 1; width: 96vw; height: 88vh; overflow: hidden; - border: none; background: none; padding: 0; cursor: zoom-out; - display: flex; align-items: center; justify-content: center; + cursor: move; display: flex; align-items: center; justify-content: center; } .lb-zoom img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; - transform: scale(2.5); will-change: transform; user-select: none; -webkit-user-drag: none; + will-change: transform; user-select: none; -webkit-user-drag: none; } - .lb-hint { position: relative; z-index: 1; margin-top: 10px; color: #5b636e; font-size: 0.85rem; } + /* floating gallery-inspector toolbar — a quiet translucent dark pill, white controls */ + .lb-tools { + position: absolute; bottom: clamp(14px, 3vh, 28px); left: 50%; transform: translateX(-50%); + z-index: 3; display: flex; align-items: center; gap: 12px; + background: rgba(20, 18, 16, 0.72); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); + border-radius: 999px; padding: 8px 14px; color: #fff; box-shadow: 0 8px 24px -12px rgba(0, 0, 0, 0.5); + } + .lb-t { + width: 26px; height: 26px; flex: none; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.28); + background: none; color: #fff; cursor: pointer; font-size: 1.05rem; line-height: 1; + display: flex; align-items: center; justify-content: center; + } + .lb-t:hover { background: rgba(255, 255, 255, 0.18); } + .lb-slider { width: clamp(120px, 22vw, 220px); accent-color: #fff; cursor: pointer; } + .lb-pct { font-size: 0.8rem; font-variant-numeric: tabular-nums; min-width: 3.2em; text-align: center; opacity: 0.92; } + .lb-fit { + border: 1px solid rgba(255, 255, 255, 0.32); background: none; color: #fff; cursor: pointer; + border-radius: 999px; padding: 4px 13px; font-size: 0.82rem; font-family: inherit; font-weight: 600; + } + .lb-fit:hover { background: rgba(255, 255, 255, 0.18); } + @media (hover: none) { .lb-tools { display: none; } } /* mobile keeps native pinch */ .lb-zoombtn { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; border: 1px solid #d9cdeb; background: #fff; color: var(--accent);