From 6d9eea71fa927f7ad47938606c78b703d7069f74 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 31 Oct 2026 23:26:16 +0100 Subject: dashboard: document changes breathe --- src/api/dashboard/dashboard.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'src/api/dashboard/dashboard.html') diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 08436d0..53f8ccf 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -1107,6 +1107,20 @@ function editableLeaf(key, shown, value, path) { return leaf; } +// An entry that changed in the document breathes once, as the map's labels do: its +// row glows and fades. +function glow(entry) { + if (calm.matches || !entry.animate) return; + const row = entry.tagName === "DETAILS" ? entry.querySelector(":scope > summary") + : entry.classList.contains("leaf") ? entry : entry.firstElementChild || entry; + const colour = getComputedStyle(document.documentElement).getPropertyValue("--self").trim(); + row.animate([ + { backgroundColor: "transparent" }, + { backgroundColor: "color-mix(in srgb, " + colour + " 30%, transparent)", offset: .3 }, + { backgroundColor: "transparent" }, + ], { duration: 900, easing: "ease-in-out" }); +} + // One entry: a short scalar as it is; anything else a branch read when opened. A // branch's entries are kept across reads, so what's open inside stays open. function documentEntry(key, summary, path) { @@ -1169,6 +1183,8 @@ function documentEntry(key, summary, path) { })); } let read = false; + // Read once at least: what's new or changed from then on breathes (`glow`). + let filled = false; const fill = async () => { // Being edited as a whole: read again once that's done. if (details.classList.contains("editing")) { documentDeferred.add(id); return; } @@ -1195,11 +1211,13 @@ function documentEntry(key, summary, path) { if (known && known.element.sameKind && known.element.sameKind(newer)) { known.element.updated(newer); known.summary = newer; + glow(known.element); continue; } const element = documentEntry(k, newer, [...path, k]); if (known) known.element.replaceWith(element); children.set(k, { element, summary: newer }); + if (filled) glow(element); } // In order; moving keeps their state. details.querySelectorAll(":scope > .note").forEach(note => note.remove()); @@ -1210,7 +1228,9 @@ function documentEntry(key, summary, path) { const shown = el("pre", {}, typeof value === "string" ? value : JSON.stringify(value, null, 2)); const before = details.querySelector(":scope > pre"); if (before) before.replaceWith(shown); else details.append(shown); + if (before && before.textContent !== shown.textContent) glow(shown); } + filled = true; } catch (error) { details.querySelectorAll(":scope > .note").forEach(note => note.remove()); details.append(el("div", { class: "note error" }, "Not read: " + error.message)); -- cgit v1.3.1