aboutsummaryrefslogtreecommitdiffstats
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/app.js76
-rw-r--r--static/deck.css33
-rw-r--r--static/index.html1
3 files changed, 109 insertions, 1 deletions
diff --git a/static/app.js b/static/app.js
index 4c562ff..31083a6 100644
--- a/static/app.js
+++ b/static/app.js
@@ -20,10 +20,13 @@
// the Dashboard. Card order and size are kept in this browser's localStorage.
const tabBar = document.getElementById("tabs");
+document.getElementById("reload").onclick = () => location.reload();
const tabBody = document.getElementById("tab-body");
const refreshers = [];
const LAYOUT_KEY = "deckit.layout";
const TAB_KEY = "deckit.tab";
+// Folded cards: id -> the height to restore when unfolded.
+const FOLD_KEY = "deckit.folded";
const DEFAULT_W = 6;
const DEFAULT_H = 4;
const tabs = {};
@@ -113,6 +116,65 @@ for (const node of loadLayout()) {
if (node.id) savedById[node.id] = node;
}
+function loadFolded() {
+ try {
+ const saved = JSON.parse(localStorage.getItem(FOLD_KEY));
+ return saved && typeof saved === "object" ? saved : {};
+ } catch {
+ return {};
+ }
+}
+const folded = loadFolded();
+
+// Folding shrinks the card to one grid row and hides its body; the row it
+// keeps is only the title bar (the content stops at the header). Its height
+// before folding is kept so unfolding restores it, and it cannot be resized
+// while folded.
+function setFolded(item, fold) {
+ const id = item.getAttribute("gs-id");
+ const content = item.querySelector(".panel");
+ const button = item.querySelector(".panel-fold");
+ if (fold) {
+ const node = item.gridstackNode;
+ if (!(id in folded)) folded[id] = node?.h ?? DEFAULT_H;
+ grid.update(item, { h: 1, noResize: true });
+ closeGaps();
+ } else if (id in folded) {
+ grid.update(item, { h: folded[id], noResize: false });
+ delete folded[id];
+ }
+ content.classList.toggle("folded", fold);
+ button.textContent = fold ? "▸" : "▾";
+ button.setAttribute("aria-label", fold ? "Unfold panel" : "Fold panel");
+ button.setAttribute("aria-expanded", String(!fold));
+ try {
+ localStorage.setItem(FOLD_KEY, JSON.stringify(folded));
+ } catch {}
+}
+
+// The grid floats, so nothing moves into rows a folded card gave back. Pull
+// each card below a folded one, in its columns, up as far as the space is
+// free (never above the folded card's title bar). Unfolding needs no mirror:
+// the grid pushes down whatever the regrown card hits.
+function closeGaps() {
+ const nodes = grid.engine.nodes;
+ const clash = (n, y) => nodes.some((o) => o !== n && o.x < n.x + n.w && o.x + o.w > n.x
+ && o.y < y + n.h && o.y + o.h > y);
+ const folds = nodes.filter((n) => n.el.querySelector(".panel.folded"));
+ const below = nodes
+ .filter((n) => folds.some((f) => n.y > f.y && n.x < f.x + f.w && n.x + n.w > f.x))
+ .sort((a, b) => a.y - b.y);
+ for (const n of below) {
+ const floor = Math.max(...folds.filter((f) => n.y > f.y && n.x < f.x + f.w && n.x + n.w > f.x)
+ .map((f) => f.y + f.h));
+ let y = n.y;
+ while (y - 1 >= floor && !clash(n, y - 1)) y--;
+ if (y !== n.y) {
+ grid.update(n.el, { y });
+ }
+ }
+}
+
function header(title) {
const head = document.createElement("div");
head.className = "panel-head";
@@ -120,11 +182,20 @@ function header(title) {
dot.className = "panel-dot";
const h = document.createElement("h2");
h.textContent = title;
+ const fold = document.createElement("button");
+ fold.className = "panel-fold";
+ fold.textContent = "▾";
+ fold.setAttribute("aria-label", "Fold panel");
+ fold.setAttribute("aria-expanded", "true");
+ fold.onclick = () => {
+ const item = fold.closest(".grid-stack-item");
+ setFolded(item, !item.querySelector(".panel").classList.contains("folded"));
+ };
const drag = document.createElement("button");
drag.className = "panel-drag";
drag.textContent = "⠿";
drag.setAttribute("aria-label", "Move panel");
- head.append(dot, h, drag);
+ head.append(dot, h, fold, drag);
return head;
}
@@ -147,6 +218,7 @@ function makeCard(name, title) {
item.append(content);
gridEl.append(item);
grid.makeWidget(item);
+ if (name in folded) setFolded(item, true);
return body;
}
@@ -196,7 +268,9 @@ async function boot() {
// a broken panel must not take down the rest of the dashboard
}
}
+ closeGaps();
ready = true;
+ saveLayout();
sortTabs();
const saved = localStorage.getItem(TAB_KEY);
showTab(tabs[saved] ? saved : "Dashboard");
diff --git a/static/deck.css b/static/deck.css
index f0a9fdc..8a318ef 100644
--- a/static/deck.css
+++ b/static/deck.css
@@ -37,10 +37,28 @@ body {
.topbar {
flex: none;
+ position: relative;
display: flex;
justify-content: center;
padding: 8px 14px 0;
}
+/* The installed web app has no browser chrome, so no pull-to-refresh. */
+.reload {
+ position: absolute;
+ right: 14px;
+ top: 8px;
+ min-width: 44px;
+ min-height: 40px;
+ font: inherit;
+ font-size: 1.286rem;
+ line-height: 1;
+ color: var(--udt-subtext0);
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 10px;
+ cursor: pointer;
+}
+.reload:active { background: var(--udt-surface0); border-color: var(--udt-surface1); }
.tabs { display: flex; gap: 6px; }
.tabs button {
font: inherit;
@@ -143,6 +161,21 @@ body {
touch-action: none;
}
.panel-drag:hover { color: var(--udt-subtext0); background: var(--udt-surface1); }
+.panel-fold {
+ font: inherit;
+ min-width: 36px;
+ min-height: 32px;
+ color: var(--udt-overlay0);
+ background: transparent;
+ border: 0;
+ border-radius: 6px;
+ cursor: pointer;
+}
+.panel-fold:hover { color: var(--udt-subtext0); background: var(--udt-surface1); }
+/* Folded: the card ends at its title bar instead of filling its grid row. */
+.grid-stack > .grid-stack-item > .grid-stack-item-content.panel.folded { bottom: auto; }
+.panel.folded .panel-body { display: none; }
+.panel.folded .panel-head { border-bottom: 0; }
.panel-body { flex: 1; min-height: 0; overflow: auto; padding: 14px; }
.section-label {
diff --git a/static/index.html b/static/index.html
index b2a3fca..b45904c 100644
--- a/static/index.html
+++ b/static/index.html
@@ -37,6 +37,7 @@
<body>
<header class="topbar">
<nav id="tabs" class="tabs"></nav>
+ <button id="reload" class="reload" aria-label="Reload the dashboard" title="Reload">⟳</button>
</header>
<div id="tab-body"></div>
<script src="vendor/gridstack/gridstack-all.js"></script>