diff --git a/checks/food-kinds.mjs b/checks/food-kinds.mjs index c536572..474d923 100644 --- a/checks/food-kinds.mjs +++ b/checks/food-kinds.mjs @@ -122,4 +122,46 @@ suite("the palette wraps rather than running out"); "…and the palette wraps, so two share rather than one having none"); } +// ---------------------------------------------- the day's split in the overview +// The Meals tile keeps the day's total; this is the breakdown under it. The +// case that matters is the one where it must not appear at all. +{ + let kinds = []; + let el = { hidden: false, textContent: "" }; + const view = load({ + names: ["NO_KIND", "renderDayFoodKinds"], + stubs: { + document: { getElementById: () => el }, + liveFoodKinds: () => kinds, + foodKindNames: () => new Map(kinds.map(k => [k.id, k.name])), + }, + }); + const meal = (grams, foodKindId = "") => ({ type: "eat", grams, foodKindId }); + const show = (evs) => { el = { hidden: false, textContent: "" }; view.renderDayFoodKinds(evs); return el; }; + + suite("the day's food split"); + { + kinds = []; + eq(show([meal(180), meal(120)]).hidden, true, + "meals with no kind show no breakdown — the tile's total already says it"); + eq(show([]).hidden, true, "a day with no meals shows nothing"); + + kinds = [{ id: "d", name: "Dry" }, { id: "f", name: "Fresh" }]; + const both = show([meal(200, "d"), meal(100, "f"), meal(60, "d")]); + eq(both.hidden, false, "once a meal carries a kind, the breakdown appears"); + eq(both.textContent, "Dry 260 g · Fresh 100 g", "…summed per kind, in the chart's order"); + + eq(show([meal(200, "d"), meal(50)]).textContent, "Dry 200 g · No kind 50 g", + "unlabelled food on a day that has kinds is named, not dropped"); + + kinds = []; + eq(show([meal(90, "gone")]).textContent, "Deleted kind 90 g", + "a kind deleted since still labels its food rather than vanishing"); + + kinds = [{ id: "d", name: "Dry" }]; + eq(show([meal(0, "d"), meal(120, "d")]).textContent, "Dry 120 g", + "a meal logged without an amount adds nothing to the split"); + } +} + export default report("food-kinds"); diff --git a/src/app.js b/src/app.js index fc4e45f..35aacd6 100644 --- a/src/app.js +++ b/src/app.js @@ -447,6 +447,42 @@ return new Map(loadExercises().map(x => [x.id, x.name])); } + // The day's food, split the way the chart splits the window. The Meals tile + // keeps the total — that is the headline figure, and the breakdown will not + // fit in a 90px tile — so this sits under the grid instead. + // + // Only shown when a meal that day actually carries a kind: an account not + // using kinds should see the overview it has always seen, and a day whose + // meals all predate kinds is exactly that case. + function renderDayFoodKinds(dayEvents) { + const el = document.getElementById("stat-food-kinds"); + if (!el) return; + const names = foodKindNames(); + const order = liveFoodKinds().map(k => k.id); + const totals = new Map(); + let anyKind = false; + for (const e of dayEvents) { + if (e.type !== "eat" || !(Number.isFinite(e.grams) && e.grams > 0)) continue; + const id = e.foodKindId || NO_KIND; + if (id !== NO_KIND) anyKind = true; + totals.set(id, (totals.get(id) || 0) + e.grams); + } + if (!anyKind) { el.hidden = true; return; } + + // Same order as the chart's layers, with "No kind" last, so the two read + // as the same breakdown rather than two arbitrary lists. + const ids = [...totals.keys()].sort((a, b) => { + if (a === NO_KIND) return 1; + if (b === NO_KIND) return -1; + const ia = order.indexOf(a), ib = order.indexOf(b); + return (ia === -1 ? Infinity : ia) - (ib === -1 ? Infinity : ib); + }); + el.textContent = ids + .map(id => `${id === NO_KIND ? "No kind" : (names.get(id) || "Deleted kind")} ${Math.round(totals.get(id))} g`) + .join(" · "); + el.hidden = false; + } + // The chips both dialogs use. Built from the live kinds plus "No kind", // which always comes last and is always offered — a meal is allowed to have // no kind, permanently, and the picker should never imply otherwise. @@ -898,6 +934,7 @@ const gramsEl = document.getElementById("stat-meals-grams"); gramsEl.textContent = gramsTotal > 0 ? `${Math.round(gramsTotal)} g` : ""; gramsEl.hidden = !(gramsTotal > 0); + renderDayFoodKinds(dayEvents); // Walk time is a duration, not a count, so the tile leads with it and puts // "N walks" underneath — the day's exercise at a glance. document.getElementById("stat-walk").textContent = formatDuration(walkMsInRange(events, from, to)); diff --git a/src/changelog.json b/src/changelog.json index 2892005..a40af2d 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,5 +1,5 @@ [ - { "date": "2026-09-22", "text": "Meals can be labelled with a kind of food. Make up your own in Settings → Food kinds — dry, fresh, raw, whatever you feed — and pick one when you log a meal; tap the ★ beside one to have it chosen for you automatically. You can also invent a kind from inside the log dialog if you realise you need it mid-meal. All of it is optional: “No kind” is always offered, every meal you have already logged keeps working untouched, and with no kinds defined the app looks and behaves exactly as it did. Once you are using them, the Food (grams) chart splits each day's bar by kind with a legend, and draws a separate trend line for each, so you can see fresh creeping up while dry comes down. The sentence underneath names only the kinds that are actually moving and folds the rest into one clause, so it stays short however many kinds you have. Renaming a kind updates the meals logged as it; deleting one keeps them readable under the name it had. A guest can label a meal with a kind you have created but cannot add, rename or delete them" }, + { "date": "2026-09-22", "text": "Meals can be labelled with a kind of food. Make up your own in Settings → Food kinds — dry, fresh, raw, whatever you feed — and pick one when you log a meal; tap the ★ beside one to have it chosen for you automatically. You can also invent a kind from inside the log dialog if you realise you need it mid-meal. All of it is optional: “No kind” is always offered, every meal you have already logged keeps working untouched, and with no kinds defined the app looks and behaves exactly as it did. Once you are using them, today's overview shows the day's food broken down — “Dry 260 g · Fresh 100 g” — under the stat tiles, and the Food (grams) chart splits each day's bar by kind with a legend, and draws a separate trend line for each, so you can see fresh creeping up while dry comes down. The sentence underneath names only the kinds that are actually moving and folds the rest into one clause, so it stays short however many kinds you have. Renaming a kind updates the meals logged as it; deleting one keeps them readable under the name it had. A guest can label a meal with a kind you have created but cannot add, rename or delete them" }, { "date": "2026-09-21", "text": "Fixed the figures under the Food (grams) chart contradicting each other. It read like “down about 329 g a week — roughly 460 g a day then, 320 g a day now”, where subtracting the two amounts gives 140 g, not 329 g. The rate was worked out per week while the line itself only covers the complete days in the window — at most five of them on a 7-day window, since today isn't finished — so it was stretched past the days it was measured from. It now gives the change between the two ends, which is a figure you can check by subtracting them: “down about 140 g — from roughly 460 g a day to 320 g”" }, { "date": "2026-09-21", "text": "You can measure the time between two events. Press and hold one row, press and hold another, and a bar along the bottom shows the gap — “3h 42m · Ate 12:10 → Poo 15:52” — which answers things like how long after a meal he needs to go out. It stays there until you clear it with the ✕, so you can change day in between and pick the second event from another day; when the pair straddles midnight the bar shows the dates too. It works on any row that is a single moment: the history log, the notes log and weigh-ins. Holding a row you already picked unpicks it, and a third pick is ignored until you clear. Tapping a row still opens it for editing as before. One cost: because holding a row now means something, you can no longer select the text of a note to copy it" }, { "date": "2026-09-21", "text": "A day marked “not counted” no longer appears in the Sleep trend or the Walk trend. It was already left out of the average and out of the “yesterday” comparison, but the day you were actually looking at was still drawn as the boldest line on the chart — so the one day you had said not to trust was the one the panel led with. Now it is left off and its legend chip goes with it, leaving the average and yesterday, which is what you would want to see on a day like that" }, diff --git a/src/index.html b/src/index.html index 021a9ea..b0c7de7 100644 --- a/src/index.html +++ b/src/index.html @@ -265,6 +265,12 @@ + + +
Last pee
Last poo
diff --git a/src/style.css b/src/style.css index 7e3d635..01536fa 100644 --- a/src/style.css +++ b/src/style.css @@ -1110,10 +1110,14 @@ button.linklike:hover { text-decoration: underline; filter: none; } } .excluded-note { margin: 8px 0 0; } +/* Sits between the stat tiles and the "last X" rows, so it reads as a + footnote to the Meals tile above it. */ +.food-kind-split { margin: -8px 0 14px; } /* The day's own figures stay readable but visibly step back, so "this one is not in the numbers" is legible from the day as well as from the charts. */ .overview.day-excluded .stats, +.overview.day-excluded .food-kind-split, .overview.day-excluded .last-row { opacity: 0.55; } /* The hatch every chart uses for a day that doesn't count. The pattern itself