From 8f809b5f1459f040f2390ee23ed23c4dd56a54b2 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Fri, 10 Jul 2026 16:57:48 +0000 Subject: [PATCH] Make info panels collapsible with remembered state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each main section with a data-panel key can now be folded by clicking its heading (rotating chevron, keyboard-accessible via role/tabindex/aria-expanded and Enter/Space). Collapsed keys are persisted in localStorage and restored on load, so the layout stays how you left it across reloads. State is device-global like the theme (a single non-namespaced key, not per-user data), and only collapsed panels are stored — so any panel added later defaults to open. --- src/app.js | 42 ++++++++++++++++++++++++++++++++++++++++++ src/index.html | 14 +++++++------- src/style.css | 21 +++++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/app.js b/src/app.js index e6acfad..f254e8e 100644 --- a/src/app.js +++ b/src/app.js @@ -1765,6 +1765,48 @@ if (ev) openEditDialog(ev); }); + // ---------- collapsible panels ---------- + // Each main section carrying a data-panel key can be folded by clicking its + // heading; collapsed keys are remembered across reloads. State is device-global + // (like the theme), so a single non-namespaced key is fine — it's not per-user + // data. Only collapsed panels are stored, so newly added panels default open. + const PANELS_KEY = "puppy-tracker:panels:v1"; + function loadPanelState() { + try { + const s = JSON.parse(localStorage.getItem(PANELS_KEY)); + return s && typeof s === "object" ? s : {}; + } catch { return {}; } + } + function savePanelState(state) { + try { localStorage.setItem(PANELS_KEY, JSON.stringify(state)); } catch { /* ignore */ } + } + function initPanels() { + const state = loadPanelState(); + document.querySelectorAll("main section[data-panel]").forEach(section => { + const key = section.dataset.panel; + const h2 = section.querySelector(":scope > h2"); + if (!h2) return; + section.classList.add("collapsible"); + const collapsed = !!state[key]; + section.classList.toggle("collapsed", collapsed); + h2.setAttribute("role", "button"); + h2.setAttribute("tabindex", "0"); + h2.setAttribute("aria-expanded", String(!collapsed)); + const toggle = () => { + const nowCollapsed = section.classList.toggle("collapsed"); + h2.setAttribute("aria-expanded", String(!nowCollapsed)); + const s = loadPanelState(); + if (nowCollapsed) s[key] = true; else delete s[key]; + savePanelState(s); + }; + h2.addEventListener("click", toggle); + h2.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } + }); + }); + } + initPanels(); + // ---------- wiring ---------- document.querySelectorAll("button.action").forEach(btn => { btn.addEventListener("click", () => { diff --git a/src/index.html b/src/index.html index 4ee9b91..f627a9d 100644 --- a/src/index.html +++ b/src/index.html @@ -97,7 +97,7 @@ -
+

Today's overview

@@ -131,7 +131,7 @@
-
+

Bathroom timing (last 7 days)

Typical time between pees
@@ -142,19 +142,19 @@

-
+

Sleep windows

    No sleep windows yet for this day.

    -
    +

    Wake windows

      No wake windows yet for this day.

      -
      +

      Last 7 days

      Sleep (hours)
      @@ -183,7 +183,7 @@

      Darker = happens more often at that hour.

      -
      +

      Weight

      @@ -204,7 +204,7 @@

      No weigh-ins logged yet.

      -
      +

      History

        No events logged for this day.

        diff --git a/src/style.css b/src/style.css index 8958b83..58fe925 100644 --- a/src/style.css +++ b/src/style.css @@ -756,3 +756,24 @@ input.switch:checked::after { transform: translateX(18px); } .chart-svg .hm-poo { fill: var(--poo); } .chart-svg .hm-eat { fill: var(--eat); } +/* ---------- collapsible panels ---------- */ +section.collapsible > h2 { + cursor: pointer; + position: relative; + padding-right: 20px; + -webkit-user-select: none; + user-select: none; +} +section.collapsible > h2::after { + content: "▾"; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + transition: transform 0.15s ease; + color: var(--muted); + font-size: 0.85em; +} +section.collapsed > h2::after { transform: translateY(-50%) rotate(-90deg); } +section.collapsed > h2 { margin-bottom: 0; } +section.collapsed > :not(h2) { display: none; }