diff --git a/checks/layout.mjs b/checks/layout.mjs index 66ec6a6..133648d 100644 --- a/checks/layout.mjs +++ b/checks/layout.mjs @@ -149,4 +149,48 @@ suite("both day-bar timers fit on a row of their own"); } } +// Anything wider than the screen makes the whole document wider than the +// viewport, and a phone responds by letting you zoom out — which is how this +// class of bug is usually noticed, long after it was introduced. +suite("nothing can push the page wider than the screen"); +{ + // Free text the user types has no width limit of its own. A long unbroken + // token — a URL in a note, a chemical name off a food bag — sets a flex + // item's content-based minimum, or simply spills out of its box, and either + // way it widens the document. Every element that renders user input needs a + // break rule; this is the list, and it is easier to extend than to remember. + const USER_TEXT = [ + "\\.event \\.note", // a note on a history row + "\\.event \\.note-text", // the Notes log + "\\.ex-name", // exercise names + "\\.ex-note", // exercise instructions + "\\.guest-item-label", // the label on a guest link + ]; + for (const sel of USER_TEXT) { + const block = rule(new RegExp(`\\n${sel}[^{]*\\{([^}]*)\\}`)); + ok(/overflow-wrap:\s*(anywhere|break-word)/.test(block), + `${sel.replace(/\\/g, "")} can break a long unbroken word`); + } + + // The month grid is the one panel positioned against something narrower than + // the page. Centred on the date button it hung off the right of a phone; it + // is anchored to the day bar instead, which spans the content width. + const dayCal = rule(/\n\.day-cal \{([^}]*)\}/); + ok(/right:/.test(dayCal) && !/left:\s*50%/.test(dayCal), + "the month grid is edge-anchored, not centred on the date button"); + ok(!/max-width:[^;]*vw/.test(dayCal), + "…and bounded by its container rather than by the viewport"); + const main = html.slice(html.indexOf('
'), + html.indexOf("
", html.indexOf('
'))); + ok(/id="day-cal"/.test(main), "…and sits inside the day bar, which is what it is measured against"); + + // A fixed width wider than the narrowest content box cannot fit by + // definition. 320px phone, less the body's two 16px gutters. + const NARROWEST = 320 - 2 * BODY_GUTTER; + const tooWide = [...css.matchAll(/(?:^|[;{\s])(width|min-width):\s*(\d{3,})px/g)] + .filter(m => Number(m[2]) > NARROWEST) + .map(m => `${m[1]}: ${m[2]}px`); + eq(tooWide, [], `no fixed width exceeds a ${NARROWEST}px content box`); +} + export default report("layout"); diff --git a/src/changelog.json b/src/changelog.json index 0edd551..fd77b1e 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-09-20", "text": "Fixed the page being wider than the screen on a phone, which is why it had started letting you zoom out. The month grid behind the date was the main culprit: it was centred on the date button, which sits near the right edge, so part of the panel hung off the side of the screen. It is anchored to the edge of the bar now and stays on screen at any width. Also fixed a long unbroken word — a link, or something copied off a food bag — in a history note, an exercise name or its instructions pushing its row wider than the screen instead of wrapping" }, { "date": "2026-09-20", "text": "Fixed three things that went wrong around a day marked “not counted”. The Timing panel measured “how long since the last pee” from before the marked day rather than from the actual last one, so the marker sat far out to the right. The Sleep and Walk trends drew the marked day's own curve as a flat zero when you were looking at that day — marking a day means don't let it drag the average, not pretend nothing happened on it. And a nap that started on a marked day and ended the next morning vanished from that next day's figures, even though the next day wasn't marked and the puppy really did sleep those hours" }, { "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" }, diff --git a/src/index.html b/src/index.html index 7ac439f..177e246 100644 --- a/src/index.html +++ b/src/index.html @@ -144,19 +144,26 @@ - + + +
diff --git a/src/style.css b/src/style.css index 033f333..911a6a9 100644 --- a/src/style.css +++ b/src/style.css @@ -298,14 +298,18 @@ body::before { browser's own picker does: changing day is worth doing *because* of the figures below, so they have to stay in view while you move. Deliberately kept short — six rows of small cells — for the same reason. */ +/* Positioned against the day bar, not the date button (see index.html). The + bar is exactly the content width, so pinning the panel to its inner right + edge and capping it at the bar's own width keeps it on screen at every size. + Centring it on the button instead let it hang off the right of a phone, + which widens the document and lets the page zoom out. */ .day-cal { position: absolute; top: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); + right: 10px; /* the bar's own horizontal padding */ z-index: 70; /* over the day bar itself, which is 60 */ width: 268px; - max-width: calc(100vw - 24px); + max-width: calc(100% - 20px); padding: 10px; background: var(--surface); border: 1px solid var(--border); @@ -726,7 +730,11 @@ textarea { resize: vertical; } .event .time { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 60px; } .event .label { font-weight: 600; min-width: 110px; } -.event .note { color: var(--muted); font-size: 0.9rem; flex: 1; } +/* min-width:0 and a break rule, or a long unbroken word — a URL, a chemical + name off a food bag — sets this flex item's content-based minimum and pushes + the whole row wider than the screen. The Notes log's own text below already + guards against it; the History row was missed. */ +.event .note { color: var(--muted); font-size: 0.9rem; flex: 1; min-width: 0; overflow-wrap: anywhere; } /* Notes log rows: a date instead of a time-of-day, then the note text. */ .event .note-date { font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; } @@ -1505,7 +1513,7 @@ input.switch:checked::after { transform: translateX(18px); } gap: 2px; } -.ex-name { font-weight: 600; } +.ex-name { font-weight: 600; overflow-wrap: anywhere; } .ex-meta { color: var(--muted); font-size: 0.8rem; } button.ex-log { @@ -1526,11 +1534,13 @@ button.ex-log { .ex-note { flex: 1; + min-width: 0; margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.4; white-space: pre-wrap; + overflow-wrap: anywhere; } button.ex-edit { padding: 6px 12px; flex-shrink: 0; }