aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/api/dashboard/dashboard.html105
1 files changed, 103 insertions, 2 deletions
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();
+});
</script>
</body>
</html>