Compare commits

..
2 Commits
Author SHA1 Message Date
Alexander Heldt e7b54b82fd Fit the tab labels on narrow phones
Five labels across a 320px viewport is tighter than it looks. The body's own
16px gutters, the bar's padding and four gaps leave about 45px of text per tab,
and "Growth" — the widest label at roughly 3.2em — wants 46px at the default
size. So on an iPhone SE and the smaller Androids it truncated to "Growt…", and
on a 280px foldable cover screen it was 7px short.

The same 370px breakpoint the day bar already uses for the same reason now
trims the type to 0.75rem and the horizontal padding to 2px, which takes the
widest label to about 39px against 43px of room even at 280px. Above the
breakpoint nothing changes; there was never a problem there.

Also fixes the sticky offset going stale. The tab bar sits directly under the
day bar, so its `top` is that bar's measured height, published as --day-bar-h.
That was measured on load and on resize — but the bar also grows the first time
a sleep is logged, when the timer pill appears inside it, and that fires no
resize, leaving the tab bar overlapping the day bar until something else
happened to trigger one. A ResizeObserver on the bar covers that and every
other cause, with the resize listener kept as the fallback.

The label arithmetic is now checked rather than eyeballed, since no browser is
available here: the layout numbers are read back out of the stylesheet so they
cannot drift, text width is estimated from per-character advances, and the pass
mark demands real headroom rather than a bare fit because the estimate is only
good to a few percent. Removing the new rules makes it fail on exactly the two
devices that were broken, which is the only way to know a check like that is
doing anything.
2026-09-07 19:58:17 +00:00
Alexander Heldt 0dfbab82cf Split the page into five tabs
Fourteen panels sat in one column, so reaching the weight curve meant scrolling
past sleep, timing, walks and counts. The page had only grown — walks, walk
patterns, training and the excluded-day marker all landed on the same scroll —
and folding panels away, while it helps, is a per-panel fiddle you then have to
undo to look at anything.

They are grouped by subject now: Today (overview, sleep & wake, history), Sleep,
Walks, Habits (pee/poo/meal timing and counts) and Growth (weight, training,
notes). No tab holds more than three. The day bar and the log buttons stay above
them on every tab, because logging has to be one tap from wherever you are, and
the tab bar sticks under the day bar — two stacked stickies need the second's
offset to be the first's height, so that height is measured and published as
--day-bar-h rather than guessed.

What gets hidden is the wrapper, never the sections inside it. walk-timeline and
walk-trend carry their own hidden, set by renderWalkPatterns once a walk exists,
and hiding them directly would clobber it.

render() still draws every panel on every pass, hidden tabs included. Nothing
measures layout — the charts scale through their viewBox, and the one
getBoundingClientRect belongs to the timer pill — so drawing into a hidden
wrapper is safe, and a tab is never briefly stale when you arrive on it. That
pill's own check gains an explicit "is the card's tab showing": a hidden element
measures as zeroes, which gave the right answer here by coincidence rather than
by rule.

Back returns to Today from any tab in one press, and a second press leaves.
Exactly one history entry is ever live, armed on leaving Today and spent on
returning — including when the return is a tap on the Today tab, which would
otherwise strand the entry and make the next press appear to do nothing. An
entry per switch is what a browser does unaided, and is why tabbed apps get a
reputation for trapping you.

Folding is untouched and composes: tabs group, folding tunes what shows within a
group. Both selectors that reach for panels are descendant selectors, so the
extra nesting cost them nothing.

The reordering was scripted rather than done by hand — fourteen sections moving
between five wrappers is how you silently lose one — and a check now asserts
every panel sits in exactly one tab, that buttons and wrappers correspond, and
that the aria pairs are wired. The first run of that script dropped three
explanatory comments along the way, which is exactly the sort of thing it exists
to catch.
2026-09-07 19:55:40 +00:00
5 changed files with 437 additions and 213 deletions
+27
View File
@@ -43,6 +43,33 @@ source-of-truth and sync between devices.
A status pill in the header shows `syncing…` / `synced 2m ago` / `pending` / A status pill in the header shows `syncing…` / `synced 2m ago` / `pending` /
`sync error` / `offline`. Tap it to force-sync. `sync error` / `offline`. Tap it to force-sync.
## On screen
The panels are grouped into five tabs — **Today**, **Sleep**, **Walks**,
**Habits** (pee/poo/meal timing and counts) and **Growth** (weight, training,
notes) — so each screen holds one subject instead of all fourteen panels in one
column. The day bar and the log buttons sit above the tabs and stay put on all
of them, because logging has to be one tap from wherever you are.
- Each tab is a `.tab-panel` wrapper around the existing sections. The
**wrapper** is what gets hidden, never the sections: `walk-timeline` and
`walk-trend` carry their own `hidden`, set by `renderWalkPatterns` once a walk
exists, and hiding them directly would clobber it.
- `render()` still draws every panel on every pass, including the tabs you
can't see. Nothing measures layout — the charts scale through their `viewBox`
— so drawing into a hidden wrapper is safe, and it means a tab is never
briefly stale when you arrive on it.
- Tapping a panel's heading still folds it away, remembered across reloads, and
composes with tabs: tabs group, folding tunes what shows within a group. The
chosen tab is remembered the same way (device-global, like the theme).
- **Back returns to Today**, from any tab, in one press; a second press leaves
the app. Exactly one history entry is ever live — armed on leaving Today and
spent on returning, whether that return came from the back button or from
tapping the tab. An entry per switch is what a browser does unaided, and is
why tabbed apps get a reputation for trapping you: flick between tabs fifteen
times and it takes fifteen presses to escape. Two presses, always, from
anywhere.
## Layout ## Layout
``` ```
+120 -1
View File
@@ -995,7 +995,12 @@
if (!bigClockState || pill.hidden) return; if (!bigClockState || pill.hidden) return;
const card = document.getElementById("big-clock"); const card = document.getElementById("big-clock");
const bar = document.querySelector(".day-bar"); const bar = document.querySelector(".day-bar");
const cardVisible = // The card lives on the Today tab, so on any other tab it isn't on screen
// at all and the pill is the only timer there is. Asked explicitly rather
// than left to the rect: a hidden element measures as zeroes, which would
// give the right answer here by coincidence rather than by rule.
const cardOnThisTab = card.offsetParent !== null;
const cardVisible = cardOnThisTab &&
card.getBoundingClientRect().bottom > bar.getBoundingClientRect().bottom; card.getBoundingClientRect().bottom > bar.getBoundingClientRect().bottom;
pill.classList.toggle("standby", cardVisible); pill.classList.toggle("standby", cardVisible);
} }
@@ -4680,6 +4685,120 @@
} }
initPanels(); initPanels();
// ---------- tabs ----------
// Fourteen panels in one column meant scrolling past sleep, timing and walks
// to reach the weight curve. They are grouped into five tabs by subject; the
// day bar and the log buttons stay above them, so logging is one tap from
// wherever you are. Folding still works inside a tab — the two compose.
//
// The wrapper is what gets hidden, never the sections inside it: walk-timeline
// and walk-trend carry their own `hidden` (renderWalkPatterns), and hiding
// them directly would clobber that.
const TAB_KEY = "puppy-tracker:tab:v1"; // device-global, like the fold state
const DEFAULT_TAB = "today";
const tabButtons = [...document.querySelectorAll(".tabs .tab")];
const tabPanels = [...document.querySelectorAll("main .tab-panel")];
// Today is the one tab with no charts, so the window picker doesn't belong
// over it.
const TABS_WITHOUT_CHARTS = new Set(["today"]);
let activeTab = DEFAULT_TAB;
function loadTab() {
try {
const t = localStorage.getItem(TAB_KEY);
return tabButtons.some(b => b.dataset.tab === t) ? t : DEFAULT_TAB;
} catch { return DEFAULT_TAB; }
}
// Back returns to Today from any tab in one press, and a second press leaves
// the app — two presses to get out from anywhere, whatever route you took.
//
// The alternative, a history entry per switch, is what a browser would do by
// itself and is the reason tabbed apps get a reputation for trapping you:
// flick between tabs fifteen times and it takes fifteen presses to escape.
// So exactly one entry is ever live. It is armed on leaving Today and
// consumed on returning, whether that return came from the back button or
// from tapping the tab.
let backArmed = false;
function armBack() {
if (backArmed) return;
history.pushState({ puppyTab: true }, "");
backArmed = true;
}
addEventListener("popstate", () => {
// Not ours: some other navigation, so stay out of its way.
if (!backArmed) return;
backArmed = false;
showTab(DEFAULT_TAB, { manageBack: false });
});
function showTab(id, { scroll = true, manageBack = true } = {}) {
if (!tabButtons.some(b => b.dataset.tab === id)) id = DEFAULT_TAB;
activeTab = id;
for (const b of tabButtons) {
const on = b.dataset.tab === id;
b.classList.toggle("active", on);
b.setAttribute("aria-selected", String(on));
// Roving tabindex: the tablist is one stop, arrows move within it.
b.tabIndex = on ? 0 : -1;
}
for (const p of tabPanels) p.hidden = p.dataset.tab !== id;
document.querySelector(".chart-window").hidden = TABS_WITHOUT_CHARTS.has(id);
// Arriving halfway down a different tab is disorienting.
if (scroll) window.scrollTo({ top: 0 });
try { localStorage.setItem(TAB_KEY, id); } catch { /* ignore */ }
if (manageBack) {
if (id !== DEFAULT_TAB) {
armBack();
} else if (backArmed) {
// Tapping Today gets to the same place the back button would, so spend
// the entry rather than leaving a stale one that makes the next press
// appear to do nothing. Clearing the flag first stops the popstate
// this triggers from running the handler above a second time.
backArmed = false;
history.back();
}
}
// The pill's visibility depends on whether the big card is on screen, and
// the card only exists on one tab.
updateBarClockMode();
}
for (const [i, btn] of tabButtons.entries()) {
btn.addEventListener("click", () => showTab(btn.dataset.tab));
btn.addEventListener("keydown", (e) => {
const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!step) return;
e.preventDefault();
const next = tabButtons[(i + step + tabButtons.length) % tabButtons.length];
showTab(next.dataset.tab, { scroll: false });
next.focus();
});
}
// The tab bar sticks directly under the day bar, which means its `top` has to
// be that bar's height — measured, because the bar's contents (and so its
// height) differ between phones and orientations.
//
// A resize listener alone is not enough: the bar also grows the first time a
// sleep is logged, when the timer pill appears inside it, and that fires no
// resize. Watching the element covers both, and every other cause besides.
const dayBarEl = document.querySelector(".day-bar");
function measureDayBar() {
if (!dayBarEl) return;
document.documentElement.style.setProperty("--day-bar-h", `${dayBarEl.offsetHeight}px`);
}
if (dayBarEl && typeof ResizeObserver === "function") {
new ResizeObserver(measureDayBar).observe(dayBarEl);
} else {
addEventListener("resize", measureDayBar);
}
measureDayBar();
showTab(loadTab(), { scroll: false });
// ---------- wiring ---------- // ---------- wiring ----------
document.querySelectorAll("button.action").forEach(btn => { document.querySelectorAll("button.action").forEach(btn => {
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
+1
View File
@@ -1,4 +1,5 @@
[ [
{ "date": "2026-09-07", "text": "The page is split into five tabs — Today, Sleep, Walks, Habits and Growth — instead of one long column of fourteen panels. Today has the overview, sleep & wake and the day's history; Sleep and Walks each have their day-by-day chart, their when-it-happens grid and their trend; Habits has the pee, poo and meal timing and counts; Growth has weight, training and notes. Getting to the weight curve no longer means scrolling past everything else. The day bar and the log buttons sit above the tabs and stay there whichever one you're on, so logging is still one tap from anywhere. Folding a panel by tapping its heading works exactly as before, inside its tab, and the app reopens on the tab you left it on. The back button (or the back gesture) returns you to Today from wherever you are, and pressing it again leaves the app — always two presses to get out, however much you'd been flicking between tabs beforehand" },
{ "date": "2026-09-07", "text": "Guest links can be copied whenever you want. Settings → Guest access now shows each live link's URL next to it with a Copy button, instead of showing it once when you created it and never again. If you lose the message you sent, or want to pass the same link to someone else, you can just take it again rather than making a new one and leaving whoever already had the old one locked out. Links you made before this change can't be shown — only a scrambled form of those was kept — so revoke one and create a fresh one if you need its URL back" }, { "date": "2026-09-07", "text": "Guest links can be copied whenever you want. Settings → Guest access now shows each live link's URL next to it with a Copy button, instead of showing it once when you created it and never again. If you lose the message you sent, or want to pass the same link to someone else, you can just take it again rather than making a new one and leaving whoever already had the old one locked out. Links you made before this change can't be shown — only a scrambled form of those was kept — so revoke one and create a fresh one if you need its URL back" },
{ "date": "2026-09-07", "text": "Fixed buttons that were meant to be hidden but showed anyway. A guest opening an entry the owner logged saw Delete and Save on it — they never worked (the server refuses the change) but they had no business being there. The same fault had been quietly affecting three other things for a while: the 🌳 pedigree button appeared before you had set a pedigree ID, “Send a test notification” appeared when reminders weren't available, and the exercise dialog offered Delete while you were adding a new exercise rather than editing one. One styling rule was overriding every one of them" }, { "date": "2026-09-07", "text": "Fixed buttons that were meant to be hidden but showed anyway. A guest opening an entry the owner logged saw Delete and Save on it — they never worked (the server refuses the change) but they had no business being there. The same fault had been quietly affecting three other things for a while: the 🌳 pedigree button appeared before you had set a pedigree ID, “Send a test notification” appeared when reminders weren't available, and the exercise dialog offered Delete while you were adding a new exercise rather than editing one. One styling rule was overriding every one of them" },
{ "date": "2026-09-07", "text": "A day can now be left out of the stats. Open the day, tap “⊘ Not counted” next to the overview heading, and it stops feeding the charts and averages — useful when someone else had the puppy and the record is thinner than the day really was, so it isn't fair to count it. Nothing is deleted or hidden: the day's own overview, history and sleep & wake list are exactly as they were, just dimmed and labelled, and you can switch it back at any time. In the day-by-day charts the day keeps its place but is drawn as a hatch instead of a bar, so a deliberate gap can't be misread as a day the puppy barely slept. Weigh-ins and notes still count wherever they fall — those are facts you recorded, not behaviour a sparse day distorts — so the weight curve and the Notes log are untouched. The Timing panel throws away gaps that reach across a skipped day rather than measuring them, which would otherwise turn two normal days into one enormous fake gap" }, { "date": "2026-09-07", "text": "A day can now be left out of the stats. Open the day, tap “⊘ Not counted” next to the overview heading, and it stops feeding the charts and averages — useful when someone else had the puppy and the record is thinner than the day really was, so it isn't fair to count it. Nothing is deleted or hidden: the day's own overview, history and sleep & wake list are exactly as they were, just dimmed and labelled, and you can switch it back at any time. In the day-by-day charts the day keeps its place but is drawn as a hatch instead of a bar, so a deliberate gap can't be misread as a day the puppy barely slept. Weigh-ins and notes still count wherever they fall — those are facts you recorded, not behaviour a sparse day distorts — so the weight curve and the Notes log are untouched. The Timing panel throws away gaps that reach across a skipped day rather than measuring them, which would otherwise turn two normal days into one enormous fake gap" },
+235 -212
View File
@@ -106,6 +106,7 @@
<p id="guest-banner" class="guest-banner" hidden></p> <p id="guest-banner" class="guest-banner" hidden></p>
<main> <main>
<section class="day-bar"> <section class="day-bar">
<!-- Compact twin of the big timer below: invisible (but keeping its <!-- Compact twin of the big timer below: invisible (but keeping its
slot) while the big card is on screen, shown once it scrolls slot) while the big card is on screen, shown once it scrolls
@@ -128,12 +129,6 @@
<button type="button" id="day-today" class="ghost">Today</button> <button type="button" id="day-today" class="ghost">Today</button>
</section> </section>
<section id="big-clock" class="big-clock" hidden>
<div class="bc-label" id="bc-label"></div>
<div class="bc-time" id="bc-time">0:00</div>
<div class="bc-since" id="bc-since"></div>
</section>
<section class="quick-actions"> <section class="quick-actions">
<h2>Log event</h2> <h2>Log event</h2>
<!-- Explicit rows rather than one auto-fit grid: a start/end pair has <!-- Explicit rows rather than one auto-fit grid: a start/end pair has
@@ -162,6 +157,17 @@
</div> </div>
</section> </section>
<!-- Five tabs so each screen holds one subject rather than all
fourteen panels in one column. The day bar and the log buttons stay
above them: logging has to be one tap from wherever you are. -->
<nav class="tabs" role="tablist" aria-label="Sections">
<button type="button" class="tab" role="tab" data-tab="today" id="tab-today" aria-controls="tabpanel-today" aria-selected="false" tabindex="-1">Today</button>
<button type="button" class="tab" role="tab" data-tab="sleep" id="tab-sleep" aria-controls="tabpanel-sleep" aria-selected="false" tabindex="-1">Sleep</button>
<button type="button" class="tab" role="tab" data-tab="walks" id="tab-walks" aria-controls="tabpanel-walks" aria-selected="false" tabindex="-1">Walks</button>
<button type="button" class="tab" role="tab" data-tab="habits" id="tab-habits" aria-controls="tabpanel-habits" aria-selected="false" tabindex="-1">Habits</button>
<button type="button" class="tab" role="tab" data-tab="growth" id="tab-growth" aria-controls="tabpanel-growth" aria-selected="false" tabindex="-1">Growth</button>
</nav>
<!-- Page-level, not a panel's: every panel below that covers more than <!-- Page-level, not a panel's: every panel below that covers more than
one day reads this, so it sits on its own row above them all rather one day reads this, so it sits on its own row above them all rather
than inside one of them, where it read as that panel's own control than inside one of them, where it read as that panel's own control
@@ -175,230 +181,247 @@
</div> </div>
</div> </div>
<section class="training" data-panel="training"> <div class="tab-panel" data-tab="today" id="tabpanel-today" role="tabpanel" aria-labelledby="tab-today" hidden>
<h2>Training</h2> <section id="big-clock" class="big-clock" hidden>
<ul id="training-list" class="training-list"></ul> <div class="bc-label" id="bc-label"></div>
<p id="training-empty" class="empty">No exercises yet. Add one to start tracking training.</p> <div class="bc-time" id="bc-time">0:00</div>
<button type="button" id="exercise-add" class="ghost training-add">Add exercise</button> <div class="bc-since" id="bc-since"></div>
<div class="chart training-chart" id="training-chart-wrap" hidden> </section>
<div class="chart-title">Consistency <span data-chart-days-label>(last 7 days)</span></div>
<svg id="chart-training" class="chart-svg" viewBox="0 0 320 60" role="img" aria-label="Training sessions per exercise per day"></svg>
</div>
</section>
<section class="overview" data-panel="overview"> <section class="overview" data-panel="overview">
<!-- The toggle lives here rather than in the day bar: that bar is held <!-- The toggle lives here rather than in the day bar: that bar is held
to one row on small phones and a sixth control would break it, to one row on small phones and a sixth control would break it,
while this panel is the selected day's summary and has the room. --> while this panel is the selected day's summary and has the room. -->
<div class="overview-head"> <div class="overview-head">
<h2 id="overview-title">Today's overview</h2> <h2 id="overview-title">Today's overview</h2>
<button type="button" id="exclude-day" class="ghost exclude-btn" <button type="button" id="exclude-day" class="ghost exclude-btn"
aria-pressed="false" aria-pressed="false"
title="Leave this day out of the charts and averages">⊘ Not counted</button> title="Leave this day out of the charts and averages">⊘ Not counted</button>
</div>
<p id="excluded-note" class="muted-note excluded-note" hidden>
Not counted in the charts and averages. Everything below is unchanged.
</p>
<div class="stats">
<div class="stat">
<div class="stat-label">Sleep</div>
<div class="stat-value" id="stat-sleep">0h 0m</div>
</div> </div>
<div class="stat"> <p id="excluded-note" class="muted-note excluded-note" hidden>
<div class="stat-label">Awake</div> Not counted in the charts and averages. Everything below is unchanged.
<div class="stat-value" id="stat-awake">0h 0m</div> </p>
<div class="stats">
<div class="stat">
<div class="stat-label">Sleep</div>
<div class="stat-value" id="stat-sleep">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Awake</div>
<div class="stat-value" id="stat-awake">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Walks</div>
<div class="stat-value" id="stat-walk">0m</div>
<div class="stat-sub" id="stat-walk-count" hidden></div>
</div>
<div class="stat">
<div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div>
<div class="stat-sub" id="stat-meals-grams" hidden></div>
</div>
<div class="stat">
<div class="stat-label">Pees</div>
<div class="stat-value" id="stat-pees">0</div>
</div>
<div class="stat">
<div class="stat-label">Poos</div>
<div class="stat-value" id="stat-poos">0</div>
</div>
<div class="stat">
<div class="stat-label">Training</div>
<div class="stat-value" id="stat-training">0</div>
</div>
</div> </div>
<div class="stat">
<div class="stat-label">Walks</div> <div class="lasts">
<div class="stat-value" id="stat-walk">0m</div> <div class="last-row"><span>Last pee</span><span id="last-pee"></span></div>
<div class="stat-sub" id="stat-walk-count" hidden></div> <div class="last-row"><span>Last poo</span><span id="last-poo"></span></div>
<div class="last-row"><span>Last meal</span><span id="last-eat"></span></div>
<div class="last-row"><span>Last sleep</span><span id="last-sleep"></span></div>
<div class="last-row"><span>Last walk</span><span id="last-walk"></span></div>
</div> </div>
<div class="stat"> </section>
<div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div> <!-- Sleep and wake windows are the same boundaries read two ways — a wake
<div class="stat-sub" id="stat-meals-grams" hidden></div> 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 &amp; 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="history" data-panel="history">
<h2>History</h2>
<ul id="event-list" class="event-list"></ul>
<p id="empty-state" class="empty">No events logged for this day.</p>
</section>
</div>
<div class="tab-panel" data-tab="sleep" id="tabpanel-sleep" role="tabpanel" aria-labelledby="tab-sleep" hidden>
<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">
<div class="chart-title">Hours per day</div>
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day"></svg>
</div> </div>
<div class="stat"> </section>
<div class="stat-label">Pees</div>
<div class="stat-value" id="stat-pees">0</div> <section class="patterns" data-panel="sleep-timeline">
<h2><span id="sleep-timeline-title">When sleeping</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<svg id="chart-sleep-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Sleep periods per day"></svg>
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. The marker is now. Tap a row to open that day.</p>
</section>
<section class="patterns" data-panel="sleep-trend">
<h2>Sleep trend</h2>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 220" role="img" aria-label="Cumulative sleep hours through the selected day, the day before it, the recent average and (for today) the projected end-of-day total, with the age-based sleep goal band"></svg>
<div class="legend">
<span class="lg trend-today"><span class="sw"></span><span id="legend-trend-today-text">Today</span></span>
<span class="lg trend-projected" id="legend-trend-projected" hidden><span class="sw"></span><span id="legend-trend-projected-text">Projected</span></span>
<span class="lg trend-yesterday" id="legend-trend-yesterday"><span class="sw"></span><span id="legend-trend-yesterday-text">Yesterday</span></span>
<span class="lg trend-avg" id="legend-trend-avg"><span class="sw"></span><span id="legend-trend-avg-text">7-day avg</span></span>
<span class="lg trend-goal" id="legend-trend-goal" hidden><span class="sw"></span><span id="legend-trend-goal-text">Goal</span></span>
</div> </div>
<div class="stat"> <p class="muted-note">Hours slept so far at each point of the day, against yesterday and the average over the picked chart window. The dashed tail continues today's line the way the average day usually plays out. The axis is stretched above 10h to give the hours around the goal more room.</p>
<div class="stat-label">Poos</div> </section>
<div class="stat-value" id="stat-poos">0</div> </div>
<div class="tab-panel" data-tab="walks" id="tabpanel-walks" role="tabpanel" aria-labelledby="tab-walks" hidden>
<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>
<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> </div>
<div class="stat"> </section>
<div class="stat-label">Training</div>
<div class="stat-value" id="stat-training">0</div> <!-- The two walk patterns mirror the sleep ones on the Sleep tab, 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. The marker is now. 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, and the recent average"></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>
</div> </div>
</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>
</div>
<div class="lasts"> <div class="tab-panel" data-tab="habits" id="tabpanel-habits" role="tabpanel" aria-labelledby="tab-habits" hidden>
<div class="last-row"><span>Last pee</span><span id="last-pee"></span></div> <section class="timing" data-panel="timing">
<div class="last-row"><span>Last poo</span><span id="last-poo"></span></div> <h2>Timing <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<div class="last-row"><span>Last meal</span><span id="last-eat"></span></div> <!-- One range chart per type, drawn by drawTimingChart: the band spans
<div class="last-row"><span>Last sleep</span><span id="last-sleep"></span></div> the shortest to the typical gap and the marker is how long it has
<div class="last-row"><span>Last walk</span><span id="last-walk"></span></div> been since the last one, so a marker past the band reads as due. -->
</div> <div class="timing-charts">
</section> <div class="timing-item">
<div class="timing-name">Pees</div>
<section class="timing" data-panel="timing"> <svg id="timing-chart-pee" class="timing-chart" viewBox="0 0 320 50" role="img"></svg>
<h2>Timing <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2> <p class="muted-note timing-note" id="timing-note-pee" hidden></p>
<!-- One range chart per type, drawn by drawTimingChart: the band spans </div>
the shortest to the typical gap and the marker is how long it has <div class="timing-item">
been since the last one, so a marker past the band reads as due. --> <div class="timing-name">Poos</div>
<div class="timing-charts"> <svg id="timing-chart-poo" class="timing-chart" viewBox="0 0 320 50" role="img"></svg>
<div class="timing-item"> <p class="muted-note timing-note" id="timing-note-poo" hidden></p>
<div class="timing-name">Pees</div> </div>
<svg id="timing-chart-pee" class="timing-chart" viewBox="0 0 320 50" role="img"></svg> <div class="timing-item">
<p class="muted-note timing-note" id="timing-note-pee" hidden></p> <div class="timing-name">Meals</div>
<svg id="timing-chart-eat" class="timing-chart" viewBox="0 0 320 50" role="img"></svg>
<p class="muted-note timing-note" id="timing-note-eat" hidden></p>
</div>
</div> </div>
<div class="timing-item"> <!-- The axis is stretched (see drawTimingChart), so say what the middle
<div class="timing-name">Poos</div> of a bar means rather than leave it to be inferred. -->
<svg id="timing-chart-poo" class="timing-chart" viewBox="0 0 320 50" role="img"></svg> <p class="muted-note">The middle of every bar is that type's typical gap: left of it is sooner than usual, right of it is longer, and the faded stretch runs out to the longest gap in the window.</p>
<p class="muted-note timing-note" id="timing-note-poo" hidden></p> </section>
<!-- One panel for the three views of the same events: how many a day,
how much food went with them, and what hours they fall in. -->
<section class="patterns" data-panel="counts">
<h2>Pees, poos &amp; meals <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<div class="chart">
<div class="chart-title">Daily counts</div>
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day"></svg>
<div class="legend legend-toggle" id="counts-metrics" role="group" aria-label="Which counts to show">
<label class="lg pee"><input type="checkbox" data-metric="pees" checked /><span class="sw"></span>Pees</label>
<label class="lg poo"><input type="checkbox" data-metric="poos" checked /><span class="sw"></span>Poos</label>
<label class="lg eat"><input type="checkbox" data-metric="meals" checked /><span class="sw"></span>Meals</label>
</div>
</div> </div>
<div class="timing-item"> <div class="chart" id="grams-chart-wrap" hidden>
<div class="timing-name">Meals</div> <div class="chart-title">Food (grams)</div>
<svg id="timing-chart-eat" class="timing-chart" viewBox="0 0 320 50" role="img"></svg> <svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>
<p class="muted-note timing-note" id="timing-note-eat" hidden></p>
</div> </div>
</div> <div class="chart">
<!-- The axis is stretched (see drawTimingChart), so say what the middle <div class="chart-title">By hour of day</div>
of a bar means rather than leave it to be inferred. --> <svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day"></svg>
<p class="muted-note">The middle of every bar is that type's typical gap: left of it is sooner than usual, right of it is longer, and the faded stretch runs out to the longest gap in the window.</p> <p id="hour-heatmap-info" class="muted-note hour-point-info" aria-live="polite"></p>
</section> <p class="muted-note">Darker = happens more often at that hour; the marker is now.</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 &amp; 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="patterns" data-panel="sleep-daily">
<h2>Sleep <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<div class="chart">
<div class="chart-title">Hours per day</div>
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day"></svg>
</div>
</section>
<section class="patterns" data-panel="sleep-timeline">
<h2><span id="sleep-timeline-title">When sleeping</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<svg id="chart-sleep-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Sleep periods per day"></svg>
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. The marker is now. Tap a row to open that day.</p>
</section>
<section class="patterns" data-panel="sleep-trend">
<h2>Sleep trend</h2>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 220" role="img" aria-label="Cumulative sleep hours through the selected day, the day before it, the recent average and (for today) the projected end-of-day total, with the age-based sleep goal band"></svg>
<div class="legend">
<span class="lg trend-today"><span class="sw"></span><span id="legend-trend-today-text">Today</span></span>
<span class="lg trend-projected" id="legend-trend-projected" hidden><span class="sw"></span><span id="legend-trend-projected-text">Projected</span></span>
<span class="lg trend-yesterday" id="legend-trend-yesterday"><span class="sw"></span><span id="legend-trend-yesterday-text">Yesterday</span></span>
<span class="lg trend-avg" id="legend-trend-avg"><span class="sw"></span><span id="legend-trend-avg-text">7-day avg</span></span>
<span class="lg trend-goal" id="legend-trend-goal" hidden><span class="sw"></span><span id="legend-trend-goal-text">Goal</span></span>
</div>
<p class="muted-note">Hours slept so far at each point of the day, against yesterday and the average over the picked chart window. The dashed tail continues today's line the way the average day usually plays out. The axis is stretched above 10h to give the hours around the goal more room.</p>
</section>
<!-- One panel for the three views of the same events: how many a day,
how much food went with them, and what hours they fall in. -->
<section class="patterns" data-panel="counts">
<h2>Pees, poos &amp; meals <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<div class="chart">
<div class="chart-title">Daily counts</div>
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day"></svg>
<div class="legend legend-toggle" id="counts-metrics" role="group" aria-label="Which counts to show">
<label class="lg pee"><input type="checkbox" data-metric="pees" checked /><span class="sw"></span>Pees</label>
<label class="lg poo"><input type="checkbox" data-metric="poos" checked /><span class="sw"></span>Poos</label>
<label class="lg eat"><input type="checkbox" data-metric="meals" checked /><span class="sw"></span>Meals</label>
</div> </div>
</div> </section>
<div class="chart" id="grams-chart-wrap" hidden> </div>
<div class="chart-title">Food (grams)</div>
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>
</div>
<div class="chart">
<div class="chart-title">By hour of day</div>
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day"></svg>
<p id="hour-heatmap-info" class="muted-note hour-point-info" aria-live="polite"></p>
<p class="muted-note">Darker = happens more often at that hour; the marker is now.</p>
</div>
</section>
<section class="walks" data-panel="walks"> <div class="tab-panel" data-tab="growth" id="tabpanel-growth" role="tabpanel" aria-labelledby="tab-growth" hidden>
<h2>Walks <span class="muted-note" id="walk-total"></span></h2> <section class="weight" data-panel="weight">
<ul id="walk-list" class="wake-list"></ul> <h2>Weight</h2>
<p id="walk-empty" class="empty">No walks yet for this day. Use 🦮 Walk start / 🏁 Walk end to time one.</p> <div class="weight-summary">
<div class="chart walk-chart" id="walk-chart-wrap" hidden> <div class="stat">
<div class="chart-title">Minutes per day <span data-chart-days-label>(last 7 days)</span></div> <div class="stat-label">Latest</div>
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg> <div class="stat-value" id="weight-latest"></div>
</div> </div>
</section> <div class="stat">
<div class="stat-label">Since last</div>
<!-- The two walk patterns mirror the sleep ones below, drawn from walk <div class="stat-value" id="weight-change"></div>
windows instead of sleep windows. Both stay hidden until there is a </div>
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. The marker is now. 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, and the recent average"></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>
</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>
<section class="weight" data-panel="weight">
<h2>Weight</h2>
<div class="weight-summary">
<div class="stat">
<div class="stat-label">Latest</div>
<div class="stat-value" id="weight-latest"></div>
</div> </div>
<div class="stat"> <div class="chart">
<div class="stat-label">Since last</div> <div class="chart-title">Weight (kg)</div>
<div class="stat-value" id="weight-change"></div> <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> </div>
</div> <!-- Folds on its own h3, independently of the Weight panel around it:
<div class="chart"> the row list grows by one every weigh-in, and the summary and the
<div class="chart-title">Weight (kg)</div> curve above it are what you usually want left on screen. -->
<svg id="chart-weight" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Weight in kilograms over time"></svg> <div class="subpanel" data-panel="weight-history">
<p id="weight-point-info" class="muted-note weight-point-info"></p> <h3>History</h3>
</div> <ul id="weight-list" class="wake-list"></ul>
<!-- Folds on its own h3, independently of the Weight panel around it: <p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
the row list grows by one every weigh-in, and the summary and the </div>
curve above it are what you usually want left on screen. --> </section>
<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"> <section class="training" data-panel="training">
<h2>Notes</h2> <h2>Training</h2>
<ul id="notes-list" class="event-list"></ul> <ul id="training-list" class="training-list"></ul>
<p id="notes-empty" class="empty">No notes yet. Use the 📝 Note button to jot down things like vaccinations or vet visits — they'll be listed here across every day.</p> <p id="training-empty" class="empty">No exercises yet. Add one to start tracking training.</p>
</section> <button type="button" id="exercise-add" class="ghost training-add">Add exercise</button>
<div class="chart training-chart" id="training-chart-wrap" hidden>
<div class="chart-title">Consistency <span data-chart-days-label>(last 7 days)</span></div>
<svg id="chart-training" class="chart-svg" viewBox="0 0 320 60" role="img" aria-label="Training sessions per exercise per day"></svg>
</div>
</section>
<section class="history" data-panel="history"> <section class="notes-log" data-panel="notes">
<h2>History</h2> <h2>Notes</h2>
<ul id="event-list" class="event-list"></ul> <ul id="notes-list" class="event-list"></ul>
<p id="empty-state" class="empty">No events logged for this day.</p> <p id="notes-empty" class="empty">No notes yet. Use the 📝 Note button to jot down things like vaccinations or vet visits — they'll be listed here across every day.</p>
</section> </section>
</div>
</main> </main>
<footer class="app-footer"> <footer class="app-footer">
+54
View File
@@ -164,6 +164,60 @@ main {
padding: 16px 0 32px; padding: 16px 0 32px;
} }
/* ---------- tabs ---------- */
/* Sticks directly under the day bar, whose measured height JS publishes as
--day-bar-h (its contents, and so its height, differ between phones). The
fallback keeps the bar usable for the frame before that lands. */
.tabs {
position: sticky;
top: calc(env(safe-area-inset-top, 0px) + var(--day-bar-h, 52px));
z-index: 55; /* under the day bar, over the panels */
display: flex;
gap: 4px;
padding: 6px 4px;
margin: -8px 0 0;
background: var(--bg);
}
.tabs .tab {
flex: 1;
min-width: 0;
padding: 8px 4px;
font-size: 0.85rem;
font-weight: 600;
background: transparent;
color: var(--muted);
border-radius: var(--radius);
/* Five labels across a 360px phone: let them shrink rather than wrap. */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tabs .tab.active {
background: var(--accent-soft);
color: var(--accent);
}
.tabs .tab:hover { filter: none; }
/* Five labels have to fit without truncating on the narrow phones, and the
budget is tight: at a 320px viewport (iPhone SE, smaller Androids) the body's
own 16px gutters, the bar's padding and four gaps leave about 45px of text
per tab, while "Growth" the widest label, roughly 3.3em wants 46px at the
default size. So the same breakpoint the day bar already uses trims the type
and the padding, which takes the widest label to about 40px against 43px of
room even on a 280px foldable cover screen. */
@media (max-width: 370px) {
.tabs { gap: 2px; padding-left: 2px; padding-right: 2px; }
.tabs .tab { font-size: 0.75rem; padding-left: 2px; padding-right: 2px; }
}
/* main's own gap sits between the pinned blocks and the tab area; each tab
then spaces its own panels the same way. */
.tab-panel {
display: flex;
flex-direction: column;
gap: 24px;
}
section { section {
background: var(--surface); background: var(--surface);
border-radius: var(--radius); border-radius: var(--radius);