From 2a37c6ec51182ba841ca4e2faea15a03e52a4e16 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 21 May 2026 15:26:39 -0400 Subject: [PATCH] docs: click-to-zoom lightbox for diagrams (scroll-zoom + drag-pan) Graphviz renders SVG as , which swallows clicks and can't be lightboxed. Vendored, dependency-free zoom.js converts each graphviz to a clickable (crisp vector) and adds a fullscreen overlay: click to open, scroll to zoom toward the cursor, drag to pan, Esc / dbl-click / background-click to close. Works for any img.zoomable, so future charts get it free. Degrades gracefully (no JS -> inline image still renders). Wired via html_css_files / html_js_files. --- docs/_source/_static/zoom.css | 43 +++++++++++++ docs/_source/_static/zoom.js | 112 ++++++++++++++++++++++++++++++++++ docs/_source/conf.py | 5 ++ 3 files changed, 160 insertions(+) create mode 100644 docs/_source/_static/zoom.css create mode 100644 docs/_source/_static/zoom.js diff --git a/docs/_source/_static/zoom.css b/docs/_source/_static/zoom.css new file mode 100644 index 0000000..a3f029b --- /dev/null +++ b/docs/_source/_static/zoom.css @@ -0,0 +1,43 @@ +/* Click-to-zoom for diagrams (graphviz SVG + any img.zoomable). + Vendored, dependency-free. See zoom.js. */ + +.graphviz img, +img.zoomable { + cursor: zoom-in; + max-width: 100%; +} + +#arb-zoom-overlay { + position: fixed; + inset: 0; + z-index: 100000; + background: rgba(10, 12, 16, 0.92); + display: none; + overflow: hidden; + cursor: grab; +} +#arb-zoom-overlay.open { display: block; } +#arb-zoom-overlay.grabbing { cursor: grabbing; } + +#arb-zoom-overlay img { + position: absolute; + top: 0; + left: 0; + max-width: none; + transform-origin: 0 0; + user-select: none; + -webkit-user-drag: none; +} + +#arb-zoom-hint { + position: fixed; + top: 10px; + right: 16px; + z-index: 100001; + display: none; + color: #fff; + font: 13px/1.4 system-ui, sans-serif; + opacity: 0.75; + pointer-events: none; +} +#arb-zoom-overlay.open ~ #arb-zoom-hint { display: block; } diff --git a/docs/_source/_static/zoom.js b/docs/_source/_static/zoom.js new file mode 100644 index 0000000..5311d0d --- /dev/null +++ b/docs/_source/_static/zoom.js @@ -0,0 +1,112 @@ +/* Click-to-zoom lightbox for docs diagrams. Dependency-free. + * + * Sphinx renders graphviz SVG as , which swallows clicks and + * can't be lightboxed. We replace each with an (crisp vector, + * clickable), then a click opens a fullscreen overlay with + * scroll-to-zoom + drag-to-pan so dense diagrams can be read in detail. + * Esc / double-click / background-click closes. Degrades gracefully: + * if JS is off, the inline image still renders. */ +(function () { + function ready(fn) { + if (document.readyState !== "loading") fn(); + else document.addEventListener("DOMContentLoaded", fn); + } + + ready(function () { + // 1. graphviz SVG -> clickable + document + .querySelectorAll('.graphviz object[type="image/svg+xml"]') + .forEach(function (obj) { + var img = document.createElement("img"); + img.src = obj.getAttribute("data"); + img.className = "zoomable"; + img.alt = "diagram — click to zoom"; + obj.parentNode.replaceChild(img, obj); + }); + + // 2. overlay scaffold + var ov = document.createElement("div"); + ov.id = "arb-zoom-overlay"; + var big = document.createElement("img"); + ov.appendChild(big); + var hint = document.createElement("div"); + hint.id = "arb-zoom-hint"; + hint.textContent = "scroll = zoom · drag = pan · Esc / dbl-click = close"; + document.body.appendChild(ov); + document.body.appendChild(hint); + + var scale = 1, tx = 0, ty = 0; + var dragging = false, moved = false, sx = 0, sy = 0; + + function apply() { + big.style.transform = + "translate(" + tx + "px," + ty + "px) scale(" + scale + ")"; + } + function open(src) { + ov.classList.add("open"); + big.onload = function () { + var s = + Math.min( + window.innerWidth / big.naturalWidth, + window.innerHeight / big.naturalHeight + ) * 0.95; + scale = s > 0 ? s : 1; + tx = (window.innerWidth - big.naturalWidth * scale) / 2; + ty = (window.innerHeight - big.naturalHeight * scale) / 2; + apply(); + }; + big.src = src; + } + function close() { + ov.classList.remove("open"); + big.src = ""; + } + + document.body.addEventListener("click", function (e) { + var t = e.target; + if (t && t.classList && t.classList.contains("zoomable")) open(t.src); + }); + + ov.addEventListener( + "wheel", + function (e) { + e.preventDefault(); + var f = e.deltaY < 0 ? 1.15 : 1 / 1.15; + // zoom toward the cursor + tx = e.clientX - (e.clientX - tx) * f; + ty = e.clientY - (e.clientY - ty) * f; + scale *= f; + apply(); + }, + { passive: false } + ); + + ov.addEventListener("mousedown", function (e) { + dragging = true; + moved = false; + sx = e.clientX - tx; + sy = e.clientY - ty; + ov.classList.add("grabbing"); + }); + window.addEventListener("mousemove", function (e) { + if (!dragging) return; + moved = true; + tx = e.clientX - sx; + ty = e.clientY - sy; + apply(); + }); + window.addEventListener("mouseup", function () { + dragging = false; + ov.classList.remove("grabbing"); + }); + + // background click (no drag) closes; dbl-click + Esc close + ov.addEventListener("click", function (e) { + if (e.target === ov && !moved) close(); + }); + ov.addEventListener("dblclick", close); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") close(); + }); + }); +})(); diff --git a/docs/_source/conf.py b/docs/_source/conf.py index 5adfb82..19d5f7c 100644 --- a/docs/_source/conf.py +++ b/docs/_source/conf.py @@ -43,6 +43,11 @@ html_theme = "sphinx_book_theme" html_static_path = ["_static"] html_title = "Arborist API Reference" +# Click-to-zoom lightbox for diagrams (graphviz SVG + img.zoomable). +# Vendored, dependency-free — see _static/zoom.{css,js}. +html_css_files = ["zoom.css"] +html_js_files = ["zoom.js"] + # Standard sphinx-book-theme layout: full project TOC on the left, # 'On this page' (current page sections) on the right. show_toc_level=3 # expands the right TOC down to subsections.