Compare commits
2
Commits
8f9e8783fa
...
02be8ce3e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02be8ce3e7 | ||
|
|
50d0d8a726 |
+48
-34
@@ -999,8 +999,14 @@
|
||||
}
|
||||
empty.hidden = true;
|
||||
for (const w of windows) {
|
||||
// A window may bring its own class and label, which is how the merged
|
||||
// sleep/wake list states every row's kind. A single-kind list passes one
|
||||
// class and one label for the lot and only tags the open row, where
|
||||
// "Walking" is news rather than a repetition of the heading.
|
||||
const cls = w.cls || extraClass;
|
||||
const tag = w.tag || (w.ongoing ? ongoingLabel : "");
|
||||
const li = document.createElement("li");
|
||||
li.className = `ww ${extraClass}` + (w.ongoing ? " ongoing" : "");
|
||||
li.className = `ww ${cls}` + (w.ongoing ? " ongoing" : "");
|
||||
const range = document.createElement("span");
|
||||
range.className = "ww-range";
|
||||
range.textContent = w.ongoing
|
||||
@@ -1011,29 +1017,34 @@
|
||||
dur.textContent = formatDuration(w.end - w.start);
|
||||
li.appendChild(range);
|
||||
li.appendChild(dur);
|
||||
if (w.ongoing) {
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "ww-tag";
|
||||
tag.textContent = ongoingLabel;
|
||||
li.appendChild(tag);
|
||||
if (tag) {
|
||||
const tagEl = document.createElement("span");
|
||||
tagEl.className = "ww-tag";
|
||||
tagEl.textContent = tag;
|
||||
li.appendChild(tagEl);
|
||||
}
|
||||
list.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSleepWindows(events) {
|
||||
renderWindowList(
|
||||
"sleep-list", "sleep-empty",
|
||||
sleepWindowsForDay(events, selectedDay()),
|
||||
"Asleep", "sleep-ww",
|
||||
);
|
||||
// Sleep and wake windows are the same boundaries read two ways — a wake
|
||||
// window is precisely the gap between two sleeps — so they alternate by
|
||||
// construction and sorting the two sets by start time interleaves them back
|
||||
// into the day's actual rhythm. Read in order, "45 minutes asleep after two
|
||||
// and a half hours up" is a single glance instead of a comparison between
|
||||
// two lists.
|
||||
function sleepWakeWindowsForDay(events, day) {
|
||||
return [
|
||||
...sleepWindowsForDay(events, day).map(w => ({ ...w, cls: "sleep-ww", tag: "Asleep" })),
|
||||
...wakeWindowsForDay(events, day).map(w => ({ ...w, cls: "wake-ww", tag: "Awake" })),
|
||||
].sort((a, b) => a.start - b.start);
|
||||
}
|
||||
|
||||
function renderWakeWindows(events) {
|
||||
function renderSleepWake(events) {
|
||||
renderWindowList(
|
||||
"wake-list", "wake-empty",
|
||||
wakeWindowsForDay(events, selectedDay()),
|
||||
"Awake", "wake-ww",
|
||||
"sleep-wake-list", "sleep-wake-empty",
|
||||
sleepWakeWindowsForDay(events, selectedDay()),
|
||||
"", "",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2595,8 +2606,7 @@
|
||||
renderStats(events);
|
||||
renderLasts(events);
|
||||
renderTiming(events);
|
||||
renderSleepWindows(events);
|
||||
renderWakeWindows(events);
|
||||
renderSleepWake(events);
|
||||
renderWalks(events);
|
||||
renderWeekly(events);
|
||||
renderSleepTimeline(events);
|
||||
@@ -4193,10 +4203,15 @@
|
||||
});
|
||||
|
||||
// ---------- collapsible panels ----------
|
||||
// Each main section carrying a data-panel key can be folded by clicking its
|
||||
// Anything in main 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.
|
||||
//
|
||||
// A whole section heads with an h2, a foldable block inside one with an h3.
|
||||
// The two nest without special handling: each has its own key, and a folded
|
||||
// section hides the block along with everything else it holds, which leaves
|
||||
// the block's own state to reappear as it was when the section reopens.
|
||||
const PANELS_KEY = "puppy-tracker:panels:v1";
|
||||
function loadPanelState() {
|
||||
try {
|
||||
@@ -4209,25 +4224,25 @@
|
||||
}
|
||||
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");
|
||||
document.querySelectorAll("main [data-panel]").forEach(panel => {
|
||||
const key = panel.dataset.panel;
|
||||
const head = panel.querySelector(":scope > h2, :scope > h3");
|
||||
if (!head) return;
|
||||
panel.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));
|
||||
panel.classList.toggle("collapsed", collapsed);
|
||||
head.setAttribute("role", "button");
|
||||
head.setAttribute("tabindex", "0");
|
||||
head.setAttribute("aria-expanded", String(!collapsed));
|
||||
const toggle = () => {
|
||||
const nowCollapsed = section.classList.toggle("collapsed");
|
||||
h2.setAttribute("aria-expanded", String(!nowCollapsed));
|
||||
const nowCollapsed = panel.classList.toggle("collapsed");
|
||||
head.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) => {
|
||||
head.addEventListener("click", toggle);
|
||||
head.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); }
|
||||
});
|
||||
});
|
||||
@@ -4469,8 +4484,7 @@
|
||||
renderStats(evs);
|
||||
renderLasts(evs);
|
||||
renderTiming(evs);
|
||||
renderSleepWindows(evs);
|
||||
renderWakeWindows(evs);
|
||||
renderSleepWake(evs);
|
||||
renderWalks(evs);
|
||||
renderWeekly(evs);
|
||||
renderSleepTimeline(evs);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[
|
||||
{ "date": "2026-08-31", "text": "The Walks panel and its two charts have moved down the page, below “Pees, poos & meals”" },
|
||||
{ "date": "2026-08-31", "text": "Sleep windows and Wake windows are one “Sleep & wake” panel now, with the two interleaved into a single list in time order — asleep 22:10–06:05, awake until 08:30, asleep 45 min, and so on down the day. A wake window is just the gap between two sleeps, so the two lists were always halves of the same sequence, and reading them in order makes “only a 45-minute nap after two and a half hours up” obvious in a way two separate lists never did. Each row says which it is and carries a stripe in its colour; whichever one is still running keeps the highlight" },
|
||||
{ "date": "2026-08-31", "text": "The list of weigh-ins under the Weight chart folds away on its own now — tap “History” inside the panel to collapse just the rows and keep the latest figure and the curve on screen. It remembers the choice like the panels do, and folding the whole Weight panel still takes everything with it" },
|
||||
{ "date": "2026-08-31", "text": "Walks now get the same two pattern views sleep has. “When … walks” is a day-per-row grid shaded where a walk was on, so you can see at a glance whether the routine is actually regular or drifts around. “Walk trend” draws the minutes walked so far at each point of the day against yesterday and the average over the picked window, with the age-based goal as a line and a ✓ once the day clears it — the line climbs only while a walk is on, so every step is one walk. Both appear under the Walks panel as soon as you have logged a walk, and stay out of the way until then" },
|
||||
{ "date": "2026-08-31", "text": "The 7d / 14d / 30d buttons now set the window for the Timing panel too. Until now the typical, shortest and longest gaps between pees, poos and meals were always measured over the last 7 days whatever you picked; switch to 30d and they are measured over 30, which settles down the typical gap once there is a month of history to draw on" },
|
||||
{ "date": "2026-08-31", "text": "The charts are grouped by subject instead of all sharing one “Last 7 days” panel. Sleep hours per day is now its own Sleep panel, sitting just above the “When … sleeps” timeline with the rest of the sleep views. Daily counts and Food have joined the by-hour chart in one “Pees, poos & meals” panel, so how many a day, how much food went with them and what hours they fall in read together. Minutes walked per day has moved into the Walks panel. The 7d / 14d / 30d picker now sits in the Sleep panel and still sets the window for every one of these charts" },
|
||||
|
||||
+55
-50
@@ -218,56 +218,17 @@
|
||||
<p class="muted-note timing-hint" id="timing-hint"></p>
|
||||
</section>
|
||||
|
||||
<section class="sleep" data-panel="sleep-windows">
|
||||
<h2>Sleep windows</h2>
|
||||
<ul id="sleep-list" class="wake-list"></ul>
|
||||
<p id="sleep-empty" class="empty">No sleep windows yet for this day.</p>
|
||||
<!-- Sleep and wake windows are the same boundaries read two ways — a wake
|
||||
window is exactly the gap between two sleeps — so they interleave into
|
||||
one alternating list rather than sitting in two panels that each show
|
||||
half the day. Every row carries its state; the open one keeps the
|
||||
highlight. -->
|
||||
<section class="sleepwake" data-panel="sleep-wake">
|
||||
<h2>Sleep & wake</h2>
|
||||
<ul id="sleep-wake-list" class="wake-list"></ul>
|
||||
<p id="sleep-wake-empty" class="empty">No sleep or wake windows yet for this day.</p>
|
||||
</section>
|
||||
|
||||
<section class="wake" data-panel="wake-windows">
|
||||
<h2>Wake windows</h2>
|
||||
<ul id="wake-list" class="wake-list"></ul>
|
||||
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
|
||||
</section>
|
||||
|
||||
<section class="walks" data-panel="walks">
|
||||
<h2>Walks <span class="muted-note" id="walk-total"></span></h2>
|
||||
<ul id="walk-list" class="wake-list"></ul>
|
||||
<p id="walk-empty" class="empty">No walks yet for this day. Use 🦮 Walk start / 🏁 Walk end to time one.</p>
|
||||
<!-- Age-based guidance ("the five-minute rule"), only when a birthday
|
||||
is set and the puppy is still growing. -->
|
||||
<p id="walk-goal" class="muted-note" hidden></p>
|
||||
<div class="chart walk-chart" id="walk-chart-wrap" hidden>
|
||||
<div class="chart-title">Minutes per day <span data-chart-days-label>(last 7 days)</span></div>
|
||||
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- The two walk patterns mirror the sleep ones below, drawn from walk
|
||||
windows instead of sleep windows. Both stay hidden until there is a
|
||||
walk to draw, so they cost nothing to anyone not tracking walks. -->
|
||||
<section class="patterns" data-panel="walk-timeline" hidden>
|
||||
<h2><span id="walk-timeline-title">When walking</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<svg id="chart-walk-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Walks per day"></svg>
|
||||
<p class="muted-note">Each row is a day, midnight to midnight; shaded = out on a walk. Tap a row to open that day.</p>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="walk-trend" hidden>
|
||||
<h2>Walk trend</h2>
|
||||
<svg id="chart-walk-trend" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Cumulative minutes walked through the selected day, the day before it, the recent average, and the age-based daily goal"></svg>
|
||||
<div class="legend">
|
||||
<span class="lg wtrend-today"><span class="sw"></span><span id="legend-wtrend-today-text">Today</span></span>
|
||||
<span class="lg wtrend-yesterday" id="legend-wtrend-yesterday"><span class="sw"></span><span id="legend-wtrend-yesterday-text">Yesterday</span></span>
|
||||
<span class="lg wtrend-avg" id="legend-wtrend-avg"><span class="sw"></span><span id="legend-wtrend-avg-text">7-day avg</span></span>
|
||||
<span class="lg wtrend-goal" id="legend-wtrend-goal" hidden><span class="sw"></span><span id="legend-wtrend-goal-text">Goal</span></span>
|
||||
</div>
|
||||
<p class="muted-note">Minutes walked so far at each point of the day, against yesterday and the average over the picked window. The line climbs only while a walk is on, so every step is one walk.</p>
|
||||
</section>
|
||||
|
||||
<!-- The day-window picker lives in this panel but governs every
|
||||
day-window chart on the page — the training grid above, both sleep
|
||||
patterns below, the counts panel and the walk chart — so changing it
|
||||
here changes all of them (see renderChartWindow). -->
|
||||
<section class="patterns" data-panel="sleep-daily">
|
||||
<h2>Sleep <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<div class="chart-days-picker" role="group" aria-label="How many days the charts cover">
|
||||
@@ -325,6 +286,44 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="walks" data-panel="walks">
|
||||
<h2>Walks <span class="muted-note" id="walk-total"></span></h2>
|
||||
<ul id="walk-list" class="wake-list"></ul>
|
||||
<p id="walk-empty" class="empty">No walks yet for this day. Use 🦮 Walk start / 🏁 Walk end to time one.</p>
|
||||
<!-- Age-based guidance ("the five-minute rule"), only when a birthday
|
||||
is set and the puppy is still growing. -->
|
||||
<p id="walk-goal" class="muted-note" hidden></p>
|
||||
<div class="chart walk-chart" id="walk-chart-wrap" hidden>
|
||||
<div class="chart-title">Minutes per day <span data-chart-days-label>(last 7 days)</span></div>
|
||||
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- The two walk patterns mirror the sleep ones below, drawn from walk
|
||||
windows instead of sleep windows. Both stay hidden until there is a
|
||||
walk to draw, so they cost nothing to anyone not tracking walks. -->
|
||||
<section class="patterns" data-panel="walk-timeline" hidden>
|
||||
<h2><span id="walk-timeline-title">When walking</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
|
||||
<svg id="chart-walk-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Walks per day"></svg>
|
||||
<p class="muted-note">Each row is a day, midnight to midnight; shaded = out on a walk. Tap a row to open that day.</p>
|
||||
</section>
|
||||
|
||||
<section class="patterns" data-panel="walk-trend" hidden>
|
||||
<h2>Walk trend</h2>
|
||||
<svg id="chart-walk-trend" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Cumulative minutes walked through the selected day, the day before it, the recent average, and the age-based daily goal"></svg>
|
||||
<div class="legend">
|
||||
<span class="lg wtrend-today"><span class="sw"></span><span id="legend-wtrend-today-text">Today</span></span>
|
||||
<span class="lg wtrend-yesterday" id="legend-wtrend-yesterday"><span class="sw"></span><span id="legend-wtrend-yesterday-text">Yesterday</span></span>
|
||||
<span class="lg wtrend-avg" id="legend-wtrend-avg"><span class="sw"></span><span id="legend-wtrend-avg-text">7-day avg</span></span>
|
||||
<span class="lg wtrend-goal" id="legend-wtrend-goal" hidden><span class="sw"></span><span id="legend-wtrend-goal-text">Goal</span></span>
|
||||
</div>
|
||||
<p class="muted-note">Minutes walked so far at each point of the day, against yesterday and the average over the picked window. The line climbs only while a walk is on, so every step is one walk.</p>
|
||||
</section>
|
||||
|
||||
<!-- The day-window picker lives in this panel but governs every
|
||||
day-window chart on the page — the training grid above, both sleep
|
||||
patterns below, the counts panel and the walk chart — so changing it
|
||||
here changes all of them (see renderChartWindow). -->
|
||||
<section class="weight" data-panel="weight">
|
||||
<h2>Weight</h2>
|
||||
<div class="weight-summary">
|
||||
@@ -342,8 +341,14 @@
|
||||
<svg id="chart-weight" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Weight in kilograms over time"></svg>
|
||||
<p id="weight-point-info" class="muted-note weight-point-info"></p>
|
||||
</div>
|
||||
<ul id="weight-list" class="wake-list"></ul>
|
||||
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
|
||||
<!-- Folds on its own h3, independently of the Weight panel around it:
|
||||
the row list grows by one every weigh-in, and the summary and the
|
||||
curve above it are what you usually want left on screen. -->
|
||||
<div class="subpanel" data-panel="weight-history">
|
||||
<h3>History</h3>
|
||||
<ul id="weight-list" class="wake-list"></ul>
|
||||
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="notes-log" data-panel="notes">
|
||||
|
||||
+43
-7
@@ -462,18 +462,36 @@ textarea { resize: vertical; }
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
.ww.ongoing .ww-tag {
|
||||
/* Every row can carry a state, not only the open one: the merged sleep/wake
|
||||
list labels each row asleep or awake, where the label is the whole point. */
|
||||
.ww .ww-tag {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--muted);
|
||||
}
|
||||
/* A stripe in the row's own colour, so an alternating list reads as alternating
|
||||
before you get to the words. */
|
||||
.ww.sleep-ww { border-left: 3px solid color-mix(in srgb, var(--sleep) 55%, transparent); }
|
||||
.ww.wake-ww { border-left: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); }
|
||||
.ww.walk-ww { border-left: 3px solid color-mix(in srgb, var(--walk) 55%, transparent); }
|
||||
.ww.sleep-ww .ww-tag { color: var(--sleep); }
|
||||
.ww.wake-ww .ww-tag { color: var(--accent); }
|
||||
.ww.walk-ww .ww-tag { color: var(--walk); }
|
||||
.ww.ongoing .ww-tag { color: var(--accent); }
|
||||
.ww.sleep-ww.ongoing {
|
||||
border-color: var(--sleep);
|
||||
background: color-mix(in srgb, var(--sleep) 14%, var(--surface));
|
||||
}
|
||||
.ww.sleep-ww.ongoing .ww-tag { color: var(--sleep); }
|
||||
/* Spelled out like the other two: the faded stripe above would otherwise win
|
||||
over the generic .ww.ongoing on specificity ties and leave the open awake
|
||||
row without its full-strength edge. */
|
||||
.ww.wake-ww.ongoing {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 14%, var(--surface));
|
||||
}
|
||||
.ww.walk-ww.ongoing {
|
||||
border-color: var(--walk);
|
||||
background: color-mix(in srgb, var(--walk) 14%, var(--surface));
|
||||
@@ -1207,14 +1225,19 @@ button.ex-edit { padding: 6px 12px; flex-shrink: 0; }
|
||||
.walk-chart { margin-top: 16px; }
|
||||
|
||||
/* ---------- collapsible panels ---------- */
|
||||
section.collapsible > h2 {
|
||||
/* A panel folds by its own heading: h2 for a whole section, h3 for a block
|
||||
inside one. Scoped to main on purpose — the pedigree tree uses .collapsed
|
||||
for its own branches, and a bare selector here would fold those too. */
|
||||
main .collapsible > h2,
|
||||
main .collapsible > h3 {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
section.collapsible > h2::after {
|
||||
main .collapsible > h2::after,
|
||||
main .collapsible > h3::after {
|
||||
content: "▾";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -1224,9 +1247,22 @@ section.collapsible > h2::after {
|
||||
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; }
|
||||
main .collapsed > h2::after,
|
||||
main .collapsed > h3::after { transform: translateY(-50%) rotate(-90deg); }
|
||||
main .collapsed > h2,
|
||||
main .collapsed > h3 { margin-bottom: 0; }
|
||||
main .collapsed > :not(h2):not(h3) { display: none; }
|
||||
|
||||
/* Sub-heading for a foldable block inside a panel: the section h2's look, a
|
||||
size down, so it reads as subordinate to it rather than as a second panel. */
|
||||
.subpanel > h3 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
/* ---------- pedigree lookup ---------- */
|
||||
.pedigree-screen {
|
||||
|
||||
Reference in New Issue
Block a user