Make info panels collapsible with remembered state
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.
This commit is contained in:
+42
@@ -1765,6 +1765,48 @@
|
|||||||
if (ev) openEditDialog(ev);
|
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 ----------
|
// ---------- wiring ----------
|
||||||
document.querySelectorAll("button.action").forEach(btn => {
|
document.querySelectorAll("button.action").forEach(btn => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
|
|||||||
+7
-7
@@ -97,7 +97,7 @@
|
|||||||
<button type="button" id="day-next" class="ghost" aria-label="Next day">→</button>
|
<button type="button" id="day-next" class="ghost" aria-label="Next day">→</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="overview">
|
<section class="overview" data-panel="overview">
|
||||||
<h2 id="overview-title">Today's overview</h2>
|
<h2 id="overview-title">Today's overview</h2>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="timing">
|
<section class="timing" data-panel="timing">
|
||||||
<h2>Bathroom timing <span class="muted-note">(last 7 days)</span></h2>
|
<h2>Bathroom timing <span class="muted-note">(last 7 days)</span></h2>
|
||||||
<div class="lasts">
|
<div class="lasts">
|
||||||
<div class="last-row"><span>Typical time between pees</span><span id="gap-pee">—</span></div>
|
<div class="last-row"><span>Typical time between pees</span><span id="gap-pee">—</span></div>
|
||||||
@@ -142,19 +142,19 @@
|
|||||||
<p class="muted-note timing-hint" id="timing-hint"></p>
|
<p class="muted-note timing-hint" id="timing-hint"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="sleep">
|
<section class="sleep" data-panel="sleep-windows">
|
||||||
<h2>Sleep windows</h2>
|
<h2>Sleep windows</h2>
|
||||||
<ul id="sleep-list" class="wake-list"></ul>
|
<ul id="sleep-list" class="wake-list"></ul>
|
||||||
<p id="sleep-empty" class="empty">No sleep windows yet for this day.</p>
|
<p id="sleep-empty" class="empty">No sleep windows yet for this day.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="wake">
|
<section class="wake" data-panel="wake-windows">
|
||||||
<h2>Wake windows</h2>
|
<h2>Wake windows</h2>
|
||||||
<ul id="wake-list" class="wake-list"></ul>
|
<ul id="wake-list" class="wake-list"></ul>
|
||||||
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
|
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="weekly">
|
<section class="weekly" data-panel="weekly">
|
||||||
<h2>Last 7 days</h2>
|
<h2>Last 7 days</h2>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<div class="chart-title">Sleep (hours)</div>
|
<div class="chart-title">Sleep (hours)</div>
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
<p class="muted-note">Darker = happens more often at that hour.</p>
|
<p class="muted-note">Darker = happens more often at that hour.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="weight">
|
<section class="weight" data-panel="weight">
|
||||||
<h2>Weight</h2>
|
<h2>Weight</h2>
|
||||||
<div class="weight-summary">
|
<div class="weight-summary">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
|
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="history">
|
<section class="history" data-panel="history">
|
||||||
<h2>History</h2>
|
<h2>History</h2>
|
||||||
<ul id="event-list" class="event-list"></ul>
|
<ul id="event-list" class="event-list"></ul>
|
||||||
<p id="empty-state" class="empty">No events logged for this day.</p>
|
<p id="empty-state" class="empty">No events logged for this day.</p>
|
||||||
|
|||||||
@@ -756,3 +756,24 @@ input.switch:checked::after { transform: translateX(18px); }
|
|||||||
.chart-svg .hm-poo { fill: var(--poo); }
|
.chart-svg .hm-poo { fill: var(--poo); }
|
||||||
.chart-svg .hm-eat { fill: var(--eat); }
|
.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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user