From e4b056b5b9fb52e2a2d080fa69ebf6567957b949 Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Wed, 9 Sep 2026 04:13:03 +0000 Subject: [PATCH] Drop the big clock; keep both timers in the bar, with seconds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The asleep/awake counter rendered twice: a big card at the top of Today, and a pill in the frozen bar that stayed invisible until the card scrolled out of sight. That arrangement made the timer hardest to see exactly when you wanted it — it lived on one tab in five, and hid itself whenever it was on screen. So the card goes. Both timers sit in the frozen bar, on every tab, always visible. That takes the standby mechanism with it, along with the rect comparison that decided when to engage it, renderBigClockCard, and one of the two jobs the scroll handler was doing. Having a single place to render them is also what pays for the seconds. They were cut to minutes last change to buy width; the month grid has since given back about 93px by taking Today off the bar, which more than covers the 32px the seconds cost. One row from 360px up now, wrapping only on a 320px SE and the 280px foldable. The pills stay at 0.9rem rather than going back to 1rem. It is tempting now they are the only timer, but there are 6px of slack at 375px and the larger type adds about 10, which would wrap an iPhone SE 2 and an iPhone 8. --- README.md | 9 ++- src/app.js | 137 ++++++++++----------------------------------- src/changelog.json | 1 + src/index.html | 17 ++---- src/style.css | 38 +------------ 5 files changed, 44 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 0c79d37..d654cbe 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,12 @@ of them, because logging has to be one tap from wherever you are. - The day bar carries up to two timers, each of which logs the boundary that ends what it is counting when tapped: the asleep/awake one, and — only while - a walk is running — the walk. They count in minutes; the big card on Today - keeps the seconds, and shows whichever of the two is the more immediate — the - walk, when there is one, since you are necessarily awake on a walk. + a walk is running — the walk. They are frozen at the top on every tab, and + they are the only place a timer appears. There used to be a big card at the + top of Today as well, with the pills standing by until it scrolled out of + sight; a timer you have to scroll to, on one tab in five, is not doing the + job a timer is for. Having one place to render them is also what lets them + carry seconds — they are the display now, not a summary of one. - **The day picker is a month grid of the app's own**, not the browser's. The native one is a sheet covering the screen, and the reason to change day is to see what the figures did on it — so this is a small panel under the bar, with diff --git a/src/app.js b/src/app.js index 903c65d..1b3414d 100644 --- a/src/app.js +++ b/src/app.js @@ -629,18 +629,6 @@ return { walking: latest.type === "walk-start", since: latest.at }; } - // Minutes rather than seconds, for the frozen bar only. Two timers and the - // day controls only fit on one row if the timers are short, and a counter - // ticking every second at pill size is motion rather than information — the - // big card keeps its seconds, which is where you look if you want them. - function formatPillDuration(ms) { - if (ms < 0) ms = 0; - const totalMin = Math.floor(ms / 60000); - const h = Math.floor(totalMin / 60); - const m = totalMin % 60; - return h > 0 ? `${h}h${String(m).padStart(2, "0")}` : `${m}m`; - } - function formatCounter(ms) { if (ms < 0) ms = 0; const totalSec = Math.floor(ms / 1000); @@ -995,8 +983,8 @@ // Tracks the latest sleep transition so the 1-second tick can update the // counter without re-deriving from the event log. - let bigClockState = null; // "asleep" | "awake" | null - let bigClockSince = 0; + let sleepState = null; // "asleep" | "awake" | null + let sleepSince = 0; let walkSince = 0; // start of the walk in progress, 0 when none is // The walk currently in progress, if any. pairWindows already marks an @@ -1007,88 +995,34 @@ return open ? open.start : 0; } - // The counter renders twice: the big card at the top of the page and the - // compact pill in the frozen day bar. The pill only *shows* once the big - // card is scrolled out of sight (see updateBarClockMode); while the card is - // visible the pill is invisible but keeps its slot so the bar never shifts. - function updateBarClockMode() { - const pill = document.getElementById("bar-clock"); - const walkPill = document.getElementById("bar-walk"); - const card = document.getElementById("big-clock"); - const bar = document.querySelector(".day-bar"); - // The card lives on the Today tab, so on any other tab it isn't on screen - // at all and the pills are the only timers there are. 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; - // Both stand by together. standby keeps the slot rather than removing it, - // so the bar holds its height — including a wrapped second row — and the - // tab bar stuck to that height doesn't shift about as you scroll. - pill.classList.toggle("standby", cardVisible); - walkPill.classList.toggle("standby", cardVisible); - } - - function renderBigClock(events) { - const card = document.getElementById("big-clock"); - const label = document.getElementById("bc-label"); - const time = document.getElementById("bc-time"); - const since = document.getElementById("bc-since"); + // Both timers live in the frozen bar and nowhere else. There used to be a big + // card at the top of Today as well, with the pills standing by until it + // scrolled out of sight — but a timer you have to scroll to, on one tab out + // of five, is not doing the job a timer is for. The bar shows them always, + // and having only one place to render them is what lets them carry seconds + // again: they are the display now, not a summary of one. + function renderTimers(events) { const pill = document.getElementById("bar-clock"); const icon = document.getElementById("bar-clock-icon"); const ptime = document.getElementById("bar-clock-time"); const { state, since: ts } = currentSleepState(events); - bigClockState = state; - bigClockSince = ts; + sleepState = state; + sleepSince = ts; - // The walk pill is independent of the sleep one: it comes and goes with the - // walk, and the sleep pill is unaffected by either. + // The two are independent: the walk pill comes and goes with the walk, and + // the sleep pill is unaffected by it. walkSince = currentWalkStart(events); renderWalkPill(); - if (!state) { - card.hidden = true; - pill.hidden = true; - // A walk can be running before any sleep has ever been logged, and the - // card is the only place its timer shows at the top of the day. - renderBigClockCard(); - updateBarClockMode(); - return; - } - card.hidden = false; + if (!state) { pill.hidden = true; return; } pill.hidden = false; pill.classList.toggle("asleep", state === "asleep"); pill.classList.toggle("awake", state === "awake"); - ptime.textContent = formatPillDuration(Date.now() - ts); + ptime.textContent = formatCounter(Date.now() - ts); icon.textContent = state === "asleep" ? "😴" : "☀️"; const flip = state === "asleep" ? "Sleep end" : "Sleep start"; pill.title = `${state === "asleep" ? "Asleep" : "Awake"} since ${formatTime(ts)} — tap to log ${flip.toLowerCase()}`; pill.setAttribute("aria-label", `${state === "asleep" ? "Asleep" : "Awake"} since ${formatTime(ts)}. Log ${flip.toLowerCase()}.`); - renderBigClockCard(); - updateBarClockMode(); - } - - // The card shows one thing at a time, and a walk in progress is the more - // immediate of the two — you are necessarily awake on a walk, so "awake for - // 3h" is the less useful reading. The bar keeps both. - function renderBigClockCard() { - const card = document.getElementById("big-clock"); - const label = document.getElementById("bc-label"); - const time = document.getElementById("bc-time"); - const since = document.getElementById("bc-since"); - const walking = walkSince > 0; - if (!walking && !bigClockState) { card.hidden = true; return; } - card.hidden = false; - card.classList.toggle("walking", walking); - card.classList.toggle("asleep", !walking && bigClockState === "asleep"); - card.classList.toggle("awake", !walking && bigClockState === "awake"); - const ts = walking ? walkSince : bigClockSince; - label.textContent = walking - ? "Walking for" - : (bigClockState === "asleep" ? "Asleep for" : "Awake for"); - time.textContent = formatCounter(Date.now() - ts); - since.textContent = `since ${formatTime(ts)}`; } function renderWalkPill() { @@ -1096,24 +1030,19 @@ const wtime = document.getElementById("bar-walk-time"); if (!walkSince) { pill.hidden = true; return; } pill.hidden = false; - wtime.textContent = formatPillDuration(Date.now() - walkSince); + wtime.textContent = formatCounter(Date.now() - walkSince); pill.title = `Walking since ${formatTime(walkSince)} — tap to log walk end`; pill.setAttribute("aria-label", `Walking since ${formatTime(walkSince)}. Log walk end.`); } - function tickBigClock() { - if (bigClockState) { + function tickTimers() { + if (sleepState) { const ptime = document.getElementById("bar-clock-time"); - if (ptime) ptime.textContent = formatPillDuration(Date.now() - bigClockSince); + if (ptime) ptime.textContent = formatCounter(Date.now() - sleepSince); } if (walkSince) { const wtime = document.getElementById("bar-walk-time"); - if (wtime) wtime.textContent = formatPillDuration(Date.now() - walkSince); - } - // The card follows whichever of the two it is currently showing. - if (bigClockState || walkSince) { - const time = document.getElementById("bc-time"); - if (time) time.textContent = formatCounter(Date.now() - (walkSince || bigClockSince)); + if (wtime) wtime.textContent = formatCounter(Date.now() - walkSince); } } @@ -2852,7 +2781,7 @@ renderChartWindow(); // Day-scoped panels get the full list: you navigated to this day, so you // should see what is actually on it, marked or not. - renderBigClock(events); + renderTimers(events); renderActionHints(events); renderStats(events); renderLasts(events); @@ -4844,10 +4773,8 @@ history.back(); } } - // The pill's visibility depends on whether the big card is on screen, and - // 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(); + // A shorter tab can leave the page too short to stay scrolled, which + // unsticks the two frozen bars and parts them again. updateTabsMerged(); } @@ -4912,8 +4839,7 @@ // The timer pill doubles as a one-tap sleep toggle: tapping it logs the // boundary that ends the state it shows, exactly like the matching quick - // action. Only reachable once the big card has scrolled away — in standby - // the pill is visibility:hidden, so clicks can never land on it. + // action. document.getElementById("bar-clock").addEventListener("click", () => { const { state } = currentSleepState(live()); if (!state) return; // no sleep history yet; the pill is hidden anyway @@ -4939,19 +4865,14 @@ }); }); - // 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. + // 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(); - updateTabsMerged(); - }); + requestAnimationFrame(() => { queued = false; updateTabsMerged(); }); }; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll, { passive: true }); @@ -5266,7 +5187,7 @@ setInterval(() => { const evs = live(); renderHeader(); - renderBigClock(evs); + renderTimers(evs); renderActionHints(evs); renderStats(evs); renderLasts(evs); @@ -5282,7 +5203,7 @@ if (navigator.onLine && !syncing) setStatus(); }, 60_000); - setInterval(tickBigClock, 1000); + setInterval(tickTimers, 1000); setInterval(sync, SYNC_POLL_MS); setInterval(syncConfig, SYNC_POLL_MS); diff --git a/src/changelog.json b/src/changelog.json index 2667b8a..6389e9a 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-09-09", "text": "The big asleep/awake card at the top of Today is gone, and both timers now live permanently in the frozen bar at the top — visible on every tab, wherever you have scrolled to. The card only existed on one tab and the timers hid themselves whenever it was on screen, which meant the thing you most often want at a glance was the thing you had to go and find. With only one place left to show them they have their seconds back too" }, { "date": "2026-09-08", "text": "The date now opens a small month grid of the app's own instead of the browser's date picker. The browser's one is a sheet that covers the screen, which is backwards when the reason to change day is to see what the numbers did on it — this one sits under the bar with the overview still visible and updating as you move. ← and → still step a day at a time; the grid is for jumping further, and the Today button now lives inside it. That is what made room for the walk timer and the sleep timer to sit on one row: the top bar no longer splits onto two rows on a phone, except on the very smallest. The two timers count in minutes now rather than seconds — the big card on Today still ticks in seconds, which is where you look if you want them" }, { "date": "2026-09-08", "text": "A walk in progress now has a timer in the frozen bar at the top, next to the asleep/awake one, counting from when the walk started — so you can see how long you have been out from any tab without going to look. Tapping it ends the walk, the same way tapping the sleep timer logs the sleep boundary. On a narrow phone two timers no longer fit beside the date controls, so the bar splits onto two rows while a walk is on and goes back to one when it ends. The big timer card on Today shows the walk too while there is one — you are awake on a walk either way, so the walk is the more useful of the two" }, { "date": "2026-09-07", "text": "Going back to the Today tab no longer jumps the viewport either — by tapping it or with the back button. The other tabs stopped jumping in the last change, but returning to Today is a history step, and the browser was restoring the scroll position from when you last left it" }, diff --git a/src/index.html b/src/index.html index 6b1d606..7ac439f 100644 --- a/src/index.html +++ b/src/index.html @@ -113,11 +113,12 @@ and it has to wrap between the timers and the day controls rather than splitting the controls across two lines. --> - +