diff --git a/src/app.js b/src/app.js index 1beee36..71903f7 100644 --- a/src/app.js +++ b/src/app.js @@ -2464,6 +2464,49 @@ let pedPollTimer = null; let pedNodes = {}; // latest ancestry map, for the progress count + // ---- zoom ---- + // The tree gets very wide when expanded, so it's zoomable: buttons, ctrl/⌘ + + // wheel, and pinch. We scale via the CSS `zoom` property (not transform) so the + // scroll container reflows and every part stays reachable. The level persists. + const pedZoomOut = document.getElementById("ped-zoom-out"); + const pedZoomIn = document.getElementById("ped-zoom-in"); + const pedZoomReset = document.getElementById("ped-zoom-reset"); + const PED_ZOOM_MIN = 0.4, PED_ZOOM_MAX = 1.6; + const pedZoomKey = () => `puppy-tracker:${currentUser.id}:pedigree-zoom:v1`; + let pedZoom = 1; + + function applyPedZoom() { + pedZoom = Math.min(PED_ZOOM_MAX, Math.max(PED_ZOOM_MIN, Math.round(pedZoom * 100) / 100)); + pedTree.style.zoom = pedZoom; + pedZoomReset.textContent = Math.round(pedZoom * 100) + "%"; + try { localStorage.setItem(pedZoomKey(), String(pedZoom)); } catch { /* ignore */ } + } + function pedZoomBy(delta) { pedZoom += delta; applyPedZoom(); } + + pedZoomIn.addEventListener("click", () => pedZoomBy(0.2)); + pedZoomOut.addEventListener("click", () => pedZoomBy(-0.2)); + pedZoomReset.addEventListener("click", () => { pedZoom = 1; applyPedZoom(); }); + // Trackpad/desktop: ctrl or ⌘ + wheel zooms instead of scrolling the page. + pedTree.addEventListener("wheel", (e) => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + pedZoomBy(e.deltaY < 0 ? 0.1 : -0.1); + }, { passive: false }); + // Touch: two-finger pinch. + let pinchDist = 0, pinchZoom = 1; + const touchDist = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY); + pedTree.addEventListener("touchstart", (e) => { + if (e.touches.length === 2) { pinchDist = touchDist(e.touches); pinchZoom = pedZoom; } + }, { passive: true }); + pedTree.addEventListener("touchmove", (e) => { + if (e.touches.length === 2 && pinchDist > 0) { + e.preventDefault(); + pedZoom = pinchZoom * (touchDist(e.touches) / pinchDist); + applyPedZoom(); + } + }, { passive: false }); + pedTree.addEventListener("touchend", (e) => { if (e.touches.length < 2) pinchDist = 0; }); + // The looked-up tree is cached locally per dog id, so reopening the page paints // instantly and still shows the last-known tree offline. The server caches it // too (per dog, permanently); this is just the client-side mirror. @@ -2484,6 +2527,9 @@ const id = loadConfig().pedigreeId; appEl.hidden = true; pedScreen.hidden = false; + const saved = parseFloat(localStorage.getItem(pedZoomKey())); + pedZoom = Number.isFinite(saved) ? saved : 1; + applyPedZoom(); if (!id) { setPedStatus("Set your dog's SKK id in Settings to see its pedigree.", ""); return; } lookupPedigree(id); } diff --git a/src/changelog.json b/src/changelog.json index f918c37..3c1cf1a 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-07-26", "text": "The pedigree is now zoomable — use the +/− buttons, ⌘/Ctrl-scroll, or pinch on a phone — to fit a wide tree on screen or zoom in for detail; your zoom level is remembered" }, { "date": "2026-07-26", "text": "In the pedigree, a dog that fills more than one spot (pedigree collapse, common in a breed's older lines) now carries a ×N badge — tap it to highlight every place that dog appears in the tree" }, { "date": "2026-07-26", "text": "The pedigree now reads top-down like a family tree — your dog on top with its sire and dam branching below — showing three generations at a glance, with each dog expandable to trace the line further back" }, { "date": "2026-07-26", "text": "The pedigree ID set in Settings now syncs reliably to your other devices — it's no longer dropped when two devices' clocks disagree" }, diff --git a/src/index.html b/src/index.html index a5a70ac..9ccd3c7 100644 --- a/src/index.html +++ b/src/index.html @@ -280,6 +280,11 @@

🌳 Pedigree

+
+ + + +

diff --git a/src/style.css b/src/style.css index 91bc6be..1a0b8ad 100644 --- a/src/style.css +++ b/src/style.css @@ -1066,8 +1066,25 @@ section.collapsed > :not(h2) { display: none; } padding: 12px 4px 8px; } .pedigree-header h1 { font-size: 1.4rem; } +.ped-zoom { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} +.ped-zoom .icon-btn { padding: 4px 9px; font-size: 1.1rem; } +#ped-zoom-reset { + padding: 5px 8px; + font-size: 0.8rem; + font-variant-numeric: tabular-nums; + min-width: 48px; +} .pedigree-hint { margin: 4px 0 12px; } +/* The tree scales with the CSS `zoom` property, which reflows so the container + still scrolls to reach the edges at any zoom. */ +.pedigree-tree { touch-action: pan-x pan-y; } .pedigree-status { margin: 10px 0;