From 468112d27b272214e0dbec08c662c39826eb684a Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 31 Oct 2026 20:25:50 +0100 Subject: dashboard: allow resizing boxes --- src/api/dashboard/dashboard.html | 105 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index e063f8c..8f0f409 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -41,9 +41,10 @@ body.stale main { opacity: .55; } /* Swarm on the left, the full height; peers (as tall as their rows, up to 8) above the figures (the rest) on the right. */ + /* The columns and rows as dragged (`--columns`, `--rows`, set by `sizes`). */ main { flex: 1; min-height: 0; display: grid; gap: 16px; padding: 16px; - grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); - grid-template-rows: auto minmax(0, 1fr) minmax(0, 1fr); + grid-template-columns: var(--columns, minmax(0, 3fr) minmax(0, 2fr)); + grid-template-rows: var(--rows, auto minmax(0, 1fr) minmax(0, 1fr)); grid-template-areas: "map peers" "map figures" "map document"; } @media (max-width: 900px) { body { height: auto; } @@ -56,6 +57,11 @@ color: var(--muted); margin: 0 0 8px; flex: none; } section h2 .what { text-transform: none; letter-spacing: 0; color: var(--fg); } section h2 .what .id { color: var(--muted); font-weight: normal; word-break: break-all; } + /* The space between boxes, where they are resized. */ + main.resize-columns, main.resize-columns * { cursor: col-resize; } + main.resize-rows, main.resize-rows * { cursor: row-resize; } + /* The peers dragged to a height: the table fills it, and scrolls. */ + main.peers-sized #peers-scroll { flex: 1; max-height: none !important; } #map { grid-area: map; } #peers-box { grid-area: peers; } #figures-box { grid-area: figures; } @@ -1326,6 +1332,101 @@ connection.addEventListener("click", () => { // Ages count on between events; nothing else changes without one. setInterval(() => { if (live) render(true); }, 1000); window.addEventListener("resize", () => { lastShape = ""; render(); }); + +// --- Sizes: the space between boxes dragged to resize them, double-clicked to reset; +// kept in this browser --- +const main = document.querySelector("main"); +// The map's share of the width; the peers' height in px; the figures' share of what +// the peers leave. Null: as laid out by default. +let sizes = { columns: null, peers: null, split: null }; +try { sizes = { ...sizes, ...JSON.parse(localStorage.getItem("continuum-sizes") || "{}") }; } +catch (_) {} +function applySizes() { + const style = main.style; + if (sizes.columns === null) style.removeProperty("--columns"); + else style.setProperty("--columns", "minmax(0, " + sizes.columns + "fr) minmax(0, " + + (1 - sizes.columns) + "fr)"); + if (sizes.peers === null && sizes.split === null) style.removeProperty("--rows"); + else { + const split = sizes.split === null ? .5 : sizes.split; + style.setProperty("--rows", (sizes.peers === null ? "auto" : sizes.peers + "px") + + " minmax(0, " + split + "fr) minmax(0, " + (1 - split) + "fr)"); + } + main.classList.toggle("peers-sized", sizes.peers !== null); + try { localStorage.setItem("continuum-sizes", JSON.stringify(sizes)); } catch (_) {} +} +applySizes(); +const box = id => document.getElementById(id).getBoundingClientRect(); +// Which space between boxes `event` is over, if any: "columns", "peers" (below the +// peers) or "split" (between figures and document). Only side by side. +function gapAt(event) { + if (event.target !== main) return null; + const map = box("map"), peers = box("peers-box"), figures = box("figures-box"), + documentBox = box("document-box"); + if (map.right >= peers.left) return null; + if (event.clientX >= map.right && event.clientX <= peers.left) return "columns"; + if (event.clientX < peers.left || event.clientX > peers.right) return null; + if (event.clientY >= peers.bottom && event.clientY <= figures.top) return "peers"; + if (event.clientY >= figures.bottom && event.clientY <= documentBox.top) return "split"; + return null; +} +function resizeTo(gap, event) { + const clamp = (value, low, high) => Math.min(high, Math.max(low, value)); + const half = (box("peers-box").left - box("map").right) / 2; + if (gap === "columns") { + const map = box("map"), peers = box("peers-box"); + sizes.columns = clamp((event.clientX - half - map.left) / (peers.right - map.left - 2 * half), + .2, .8); + } else if (gap === "peers") { + const peers = box("peers-box"), documentBox = box("document-box"); + sizes.peers = Math.round(clamp(event.clientY - half - peers.top, 60, + documentBox.bottom - peers.top - 4 * half - 160)); + } else { + const figures = box("figures-box"), documentBox = box("document-box"); + sizes.split = clamp((event.clientY - half - figures.top) / + (documentBox.bottom - figures.top - 2 * half), .1, .9); + } + applySizes(); +} +let resizing = null; +let resizeFrame = null; +main.addEventListener("pointermove", event => { + if (resizing) { + resizeTo(resizing, event); + // The map's view box follows its size: drawn once a frame. + if (resizeFrame === null) resizeFrame = requestAnimationFrame(() => { + resizeFrame = null; + render(); + }); + return; + } + const gap = gapAt(event); + main.classList.toggle("resize-columns", gap === "columns"); + main.classList.toggle("resize-rows", gap === "peers" || gap === "split"); +}); +main.addEventListener("pointerdown", event => { + resizing = gapAt(event); + if (!resizing) return; + event.preventDefault(); + main.setPointerCapture(event.pointerId); +}); +const resized = () => { + if (!resizing) return; + resizing = null; + main.classList.remove("resize-columns", "resize-rows"); + lastShape = ""; + render(); +}; +main.addEventListener("pointerup", resized); +main.addEventListener("pointercancel", resized); +main.addEventListener("dblclick", event => { + const gap = gapAt(event); + if (!gap) return; + sizes[gap] = null; + applySizes(); + lastShape = ""; + render(); +}); -- cgit v1.3.1