Compare commits
2 Commits
01d64b682b
...
a2c9aa9716
| Author | SHA1 | Date | |
|---|---|---|---|
| a2c9aa9716 | |||
| 12a5bd0548 |
+60
@@ -2464,6 +2464,62 @@
|
|||||||
let pedPollTimer = null;
|
let pedPollTimer = null;
|
||||||
let pedNodes = {}; // latest ancestry map, for the progress count
|
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(); });
|
||||||
|
|
||||||
|
// ---- collapse / expand all ----
|
||||||
|
const pedFoldAll = document.getElementById("ped-foldall");
|
||||||
|
function pedSetAll(collapsed) {
|
||||||
|
pedTree.querySelectorAll("li").forEach((li) => {
|
||||||
|
if (!li.querySelector(":scope > ul")) return; // no ancestors to fold
|
||||||
|
li.classList.toggle("collapsed", collapsed);
|
||||||
|
const t = li.querySelector(":scope > .ped-card > .ped-toggle");
|
||||||
|
if (t) t.textContent = collapsed ? "+" : "−";
|
||||||
|
});
|
||||||
|
pedFoldAll.textContent = collapsed ? "Expand all" : "Collapse all";
|
||||||
|
}
|
||||||
|
pedFoldAll.addEventListener("click", () => pedSetAll(pedFoldAll.textContent[0] === "C"));
|
||||||
|
// 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
|
// 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
|
// instantly and still shows the last-known tree offline. The server caches it
|
||||||
// too (per dog, permanently); this is just the client-side mirror.
|
// too (per dog, permanently); this is just the client-side mirror.
|
||||||
@@ -2484,6 +2540,9 @@
|
|||||||
const id = loadConfig().pedigreeId;
|
const id = loadConfig().pedigreeId;
|
||||||
appEl.hidden = true;
|
appEl.hidden = true;
|
||||||
pedScreen.hidden = false;
|
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; }
|
if (!id) { setPedStatus("Set your dog's SKK id in Settings to see its pedigree.", ""); return; }
|
||||||
lookupPedigree(id);
|
lookupPedigree(id);
|
||||||
}
|
}
|
||||||
@@ -2683,6 +2742,7 @@
|
|||||||
ul.className = "ped-tree-h";
|
ul.className = "ped-tree-h";
|
||||||
ul.append(root);
|
ul.append(root);
|
||||||
pedTree.append(ul);
|
pedTree.append(ul);
|
||||||
|
if (pedFoldAll) pedFoldAll.textContent = "Collapse all"; // fresh tree starts partly open
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight (or unhighlight) every card that is the same dog as `key`.
|
// Highlight (or unhighlight) every card that is the same dog as `key`.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[
|
[
|
||||||
|
{ "date": "2026-07-26", "text": "Added a Collapse all / Expand all toggle to the pedigree, to fold the whole tree down to your dog or open every branch at once" },
|
||||||
|
{ "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": "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 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" },
|
{ "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" },
|
||||||
|
|||||||
@@ -280,6 +280,14 @@
|
|||||||
<header class="pedigree-header">
|
<header class="pedigree-header">
|
||||||
<button type="button" id="pedigree-back" class="ghost icon-btn" aria-label="Back to tracker" title="Back">←</button>
|
<button type="button" id="pedigree-back" class="ghost icon-btn" aria-label="Back to tracker" title="Back">←</button>
|
||||||
<h1>🌳 Pedigree</h1>
|
<h1>🌳 Pedigree</h1>
|
||||||
|
<div class="ped-controls">
|
||||||
|
<button type="button" id="ped-foldall" class="ghost">Collapse all</button>
|
||||||
|
<div class="ped-zoom" role="group" aria-label="Zoom">
|
||||||
|
<button type="button" id="ped-zoom-out" class="ghost icon-btn" aria-label="Zoom out" title="Zoom out">−</button>
|
||||||
|
<button type="button" id="ped-zoom-reset" class="ghost" aria-label="Reset zoom" title="Reset zoom">100%</button>
|
||||||
|
<button type="button" id="ped-zoom-in" class="ghost icon-btn" aria-label="Zoom in" title="Zoom in">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="pedigree-main">
|
<main class="pedigree-main">
|
||||||
<p class="muted-note pedigree-hint">
|
<p class="muted-note pedigree-hint">
|
||||||
|
|||||||
@@ -1065,9 +1065,34 @@ section.collapsed > :not(h2) { display: none; }
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px 4px 8px;
|
padding: 12px 4px 8px;
|
||||||
}
|
}
|
||||||
|
.pedigree-header { flex-wrap: wrap; }
|
||||||
.pedigree-header h1 { font-size: 1.4rem; }
|
.pedigree-header h1 { font-size: 1.4rem; }
|
||||||
|
.ped-controls {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#ped-foldall { padding: 5px 10px; font-size: 0.8rem; white-space: nowrap; }
|
||||||
|
.ped-zoom {
|
||||||
|
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; }
|
.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 {
|
.pedigree-status {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user