From 39343ff2a441b9033d4e965847ef4ba3004ef1f4 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Mon, 7 Sep 2026 20:14:24 +0000 Subject: [PATCH] Join the tab row to the frozen date row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tab row was painted in the page ground while the day bar above it is a card, and the two sat as separate bars with the day bar's rounded corners cutting between them — so the frozen top of the screen read as two mismatched strips rather than one thing. It takes the same card now: surface colour, radius and shadow. Once the log buttons have scrolled away and the two bars meet, they lose the seam and share one frame — the day bar squares its bottom corners and gives up its shadow, the tab row squares its top, and the pair casts a single shadow below. Scrolled back to the top they are two cards again, which is right: the log buttons genuinely sit between them there. CSS has no way to ask whether a sticky element is currently stuck, so a class is toggled from JS on the simplest available test — whether the two are touching. It rides the rAF-throttled scroll handler that already existed for the timer pill, so it adds no listener, and it is re-checked on a tab switch (a shorter tab can leave the page too short to stay scrolled) and when the day bar changes height. The tab row's sticky offset is now a pixel less than the day bar's height. The safe-area inset is free to be fractional while the measured height is a whole number, and that shortfall would show as a hairline of page ground between the two; the overlap it costs is invisible, since the day bar paints on top in the same colour. --- src/app.js | 34 ++++++++++++++++++++++++++++------ src/changelog.json | 1 + src/style.css | 27 +++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/app.js b/src/app.js index c96a6b1..8f55f0a 100644 --- a/src/app.js +++ b/src/app.js @@ -4766,8 +4766,10 @@ } } // The pill's visibility depends on whether the big card is on screen, and - // the card only exists on one tab. + // the card only exists on one tab. A shorter tab can also leave the page + // too short to stay scrolled, unsticking the bars, so re-check that too. updateBarClockMode(); + updateTabsMerged(); } for (const [i, btn] of tabButtons.entries()) { @@ -4794,13 +4796,28 @@ if (!dayBarEl) return; document.documentElement.style.setProperty("--day-bar-h", `${dayBarEl.offsetHeight}px`); } + const onDayBarResize = () => { measureDayBar(); updateTabsMerged(); }; if (dayBarEl && typeof ResizeObserver === "function") { - new ResizeObserver(measureDayBar).observe(dayBarEl); + new ResizeObserver(onDayBarResize).observe(dayBarEl); } else { - addEventListener("resize", measureDayBar); + addEventListener("resize", onDayBarResize); } measureDayBar(); + // Once the log buttons have scrolled away the two frozen bars meet, and they + // should look like one card rather than two stacked ones — see .merged in the + // stylesheet. CSS cannot ask whether a sticky element is currently stuck, so + // the test is simply whether they are touching. + const tabsEl = document.querySelector(".tabs"); + function updateTabsMerged() { + if (!dayBarEl || !tabsEl) return; + const gap = tabsEl.getBoundingClientRect().top - dayBarEl.getBoundingClientRect().bottom; + const merged = gap <= 1; // they overlap by a pixel by design + dayBarEl.classList.toggle("merged", merged); + tabsEl.classList.toggle("merged", merged); + } + updateTabsMerged(); + showTab(loadTab()); // ---------- wiring ---------- @@ -4836,14 +4853,19 @@ }); }); - // Swap the timer pill in/out of the frozen bar as the big card scrolls - // past. rAF-throttled: scroll events fire far more often than we can paint. + // Swap the timer pill in/out of the frozen bar as the big card scrolls past, + // and join the two frozen bars up once they meet. rAF-throttled: scroll + // events fire far more often than we can paint. { let queued = false; const onScroll = () => { if (queued) return; queued = true; - requestAnimationFrame(() => { queued = false; updateBarClockMode(); }); + requestAnimationFrame(() => { + queued = false; + updateBarClockMode(); + updateTabsMerged(); + }); }; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll, { passive: true }); diff --git a/src/changelog.json b/src/changelog.json index 1644223..7f118b0 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-09-07", "text": "The tab row now matches the frozen row above it that holds the date. It was a different shade, and the two sat as separate bars with the date row's rounded corners cutting between them; they share the same card colour now, and once you have scrolled the log buttons away they join into a single rounded block instead of two stacked ones" }, { "date": "2026-09-07", "text": "Switching tabs no longer jumps the page back to the top. The tab row is frozen to the top of the screen, so you change tabs from wherever you have scrolled to, and being thrown back past the log buttons you had just scrolled off was more disruptive than landing part-way down the new tab" }, { "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" }, diff --git a/src/style.css b/src/style.css index 76e325c..8698ace 100644 --- a/src/style.css +++ b/src/style.css @@ -170,13 +170,36 @@ main { 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)); + /* One pixel under the day bar's height rather than exactly it: the inset is + free to be fractional while the measured height is a whole number, and a + sub-pixel shortfall would show as a hairline of page ground between the + two. The overlap is invisible — the day bar paints on top, same colour. */ + top: calc(env(safe-area-inset-top, 0px) + var(--day-bar-h, 52px) - 1px); z-index: 55; /* under the day bar, over the panels */ display: flex; gap: 4px; padding: 6px 4px; margin: -8px 0 0; - background: var(--bg); + /* Same card as the day bar rather than the page ground, so the two read as + one thing once they are frozen together. */ + background: var(--surface); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +/* At rest the two are separate cards with the log buttons between them. Scroll + the log buttons away and they meet, so the seam goes: the day bar squares its + bottom and gives up its shadow, the tab bar squares its top, and the pair + sits inside one rounded frame casting one shadow. JS adds .merged the moment + they touch — CSS has no way to ask whether a sticky element is stuck. */ +.day-bar.merged { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + box-shadow: none; +} +.tabs.merged { + border-top-left-radius: 0; + border-top-right-radius: 0; } .tabs .tab { flex: 1;