aboutsummaryrefslogtreecommitdiffstats
path: root/static/app.js
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-10 10:40:37 +0100
committerDanilo M. <danix@danix.xyz>2026-11-10 10:40:37 +0100
commit84916f24d22cc32b5f51bbc7794b37848469edc8 (patch)
tree13752ed2ac600dd03a32bc470ac5d2d6296a51f0 /static/app.js
parent6933aa3220ff420beeb883d25fc68b9c79aa7df1 (diff)
downloaddeck-it-84916f24d22cc32b5f51bbc7794b37848469edc8.tar.gz
deck-it-84916f24d22cc32b5f51bbc7794b37848469edc8.zip
feat: fold dashboard cards and a reload button
Each Dashboard card gets a fold button in its title bar. Folded, it shrinks to one grid row ending at the title bar and cannot be resized; its height is kept in localStorage (deckit.folded) and restored on unfold. The grid floats, so nothing moved into the rows a folded card gave back: cards below it in its columns are pulled up as far as the space is free, on fold and again once every card has mounted, since a card restored folded from storage never folds at runtime. Unfolding needs no mirror; the grid pushes down whatever the regrown card hits. The installed web app has no browser chrome and so no pull-to-refresh, which left no way to load new page code on the tablet. A reload button sits at the top right; JS and CSS are served no-store, so it always fetches the current code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js76
1 files changed, 75 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");