From 045da7013329bf318586b69be047d813997a36f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 21:01:05 +0200 Subject: [PATCH] fix(insights): relocate range controls below the chart, right-align timestamps, tighten sleep cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the time-range pills + period-over-period delta out of the slot between the stat strip and the chart and render them as a clean row below the chart on every metric sub-page (the HealthKit scaffold plus weight, BMI, pulse, blood pressure, sleep). The control owns its own persisted range pref and delta read, and the chart owns its window via its in-card range tabs, so the relocation is a pure DOM reorder that leaves the chart range untouched. Right-align the last-updated timestamp on the period narrative ("Zeitraum im Überblick") and the daily-briefing footer so the "updated" line matches the status cards, which already route through the right-aligned footer. Tighten the sleep average and hypnogram cards — drop the default card gap and the headline card's extra padding that left tall empty bands — and give the hypnogram clock-aligned X-axis ticks (whole hours on a 1–3 h step picked from the night's span) so the time axis reads 23:00 / 01:00 / 03:00 / 05:00 instead of raw bedtime/wake stamps. Add a compact density to the short-horizon projection card for the metric sub-pages: it sheds the min-height floor, shortens the chart, and surfaces how many more days of tracking are needed before a projection can appear, drawn from the coverage envelope. The Insights overview keeps the roomier default geometry for grid symmetry. --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/pl.json | 3 +- src/app/insights/blood-pressure/page.tsx | 3 +- src/app/insights/bmi/page.tsx | 3 +- src/app/insights/pulse/page.tsx | 3 +- src/app/insights/sleep/page.tsx | 5 +- src/app/insights/weight/page.tsx | 4 +- src/components/insights/daily-briefing.tsx | 2 +- .../derived/trajectory-forecast-card.tsx | 75 ++++++++++++++++--- .../insights/healthkit-metric-page.tsx | 14 ++-- .../insights/period-narrative-card.tsx | 2 +- src/components/insights/sleep-hypnogram.tsx | 48 ++++++++++-- src/components/insights/sleep-overview.tsx | 10 ++- 17 files changed, 148 insertions(+), 39 deletions(-) diff --git a/messages/de.json b/messages/de.json index a7421034d..62f56f668 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2751,7 +2751,8 @@ "headline": "Wenn dieses Muster anhält — die nächsten {days} Tage", "range": "Projiziert auf rund {low}–{high} {unit} in {days} Tagen.", "caveat": "Eine Fortschreibung deines eigenen jüngsten Trends, keine Vorhersage. Das schattierte Band wird breiter und zeigt, wie schnell die Unsicherheit wächst.", - "insufficient": "Noch kein klar genuger Trend für eine Projektion — erfasse weiter, dann erscheint eine Projektion, sobald das jüngste Muster deutlich genug ist." + "insufficient": "Noch kein klar genuger Trend für eine Projektion — erfasse weiter, dann erscheint eine Projektion, sobald das jüngste Muster deutlich genug ist.", + "daysNeeded": "Noch etwa {count} Tage Erfassung nötig, bis eine Projektion erscheinen kann." } }, "cardioFitness": { diff --git a/messages/en.json b/messages/en.json index 4d483d001..28e8ae249 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2751,7 +2751,8 @@ "headline": "If this pattern continues — the next {days} days", "range": "Projected around {low}–{high} {unit} in {days} days.", "caveat": "A projection of your own recent trend, not a prediction. The shaded band widens to show how quickly the uncertainty grows.", - "insufficient": "Not enough of a clear trend to project yet — keep tracking and a projection will appear once the recent pattern is strong enough." + "insufficient": "Not enough of a clear trend to project yet — keep tracking and a projection will appear once the recent pattern is strong enough.", + "daysNeeded": "About {count} more days of tracking needed before a projection can appear." } }, "cardioFitness": { diff --git a/messages/es.json b/messages/es.json index a2e3a9331..fa606ac4b 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2751,7 +2751,8 @@ "headline": "Si este patrón continúa: los próximos {days} días", "range": "Proyectado en torno a {low}–{high} {unit} en {days} días.", "caveat": "Una proyección de tu propia tendencia reciente, no una predicción. La banda sombreada se ensancha para mostrar la rapidez con que crece la incertidumbre.", - "insufficient": "Aún no hay una tendencia lo bastante clara para proyectar: sigue registrando y aparecerá una proyección cuando el patrón reciente sea lo bastante marcado." + "insufficient": "Aún no hay una tendencia lo bastante clara para proyectar: sigue registrando y aparecerá una proyección cuando el patrón reciente sea lo bastante marcado.", + "daysNeeded": "Faltan unos {count} días más de registro para que pueda aparecer una proyección." } }, "cardioFitness": { diff --git a/messages/fr.json b/messages/fr.json index 3e43c1dbd..fb6ca414a 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2751,7 +2751,8 @@ "headline": "Si ce schéma se poursuit — les {days} prochains jours", "range": "Projeté autour de {low}–{high} {unit} dans {days} jours.", "caveat": "Une projection de votre propre tendance récente, pas une prédiction. La bande ombrée s’élargit pour montrer la vitesse à laquelle l’incertitude augmente.", - "insufficient": "Pas encore de tendance assez nette pour projeter — continuez à enregistrer et une projection apparaîtra dès que le schéma récent sera suffisamment marqué." + "insufficient": "Pas encore de tendance assez nette pour projeter — continuez à enregistrer et une projection apparaîtra dès que le schéma récent sera suffisamment marqué.", + "daysNeeded": "Encore environ {count} jours d’enregistrement nécessaires avant qu’une projection puisse apparaître." } }, "cardioFitness": { diff --git a/messages/it.json b/messages/it.json index a1fa85e22..d03d132d7 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2751,7 +2751,8 @@ "headline": "Se questo schema continua — i prossimi {days} giorni", "range": "Proiettato intorno a {low}–{high} {unit} tra {days} giorni.", "caveat": "Una proiezione della tua tendenza recente, non una previsione. La banda ombreggiata si allarga per mostrare quanto rapidamente cresce l’incertezza.", - "insufficient": "Non c’è ancora una tendenza abbastanza chiara da proiettare — continua a registrare e comparirà una proiezione quando lo schema recente sarà abbastanza marcato." + "insufficient": "Non c’è ancora una tendenza abbastanza chiara da proiettare — continua a registrare e comparirà una proiezione quando lo schema recente sarà abbastanza marcato.", + "daysNeeded": "Servono ancora circa {count} giorni di registrazione prima che possa comparire una proiezione." } }, "cardioFitness": { diff --git a/messages/pl.json b/messages/pl.json index eef8da327..8229ce793 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2751,7 +2751,8 @@ "headline": "Jeśli ten wzorzec się utrzyma — następne {days} dni", "range": "Rzutowane na około {low}–{high} {unit} za {days} dni.", "caveat": "Rzut Twojego własnego ostatniego trendu, nie prognoza. Zacieniony pas poszerza się, pokazując, jak szybko rośnie niepewność.", - "insufficient": "Jeszcze brak wystarczająco wyraźnego trendu do rzutowania — rejestruj dalej, a rzut pojawi się, gdy ostatni wzorzec będzie dość wyraźny." + "insufficient": "Jeszcze brak wystarczająco wyraźnego trendu do rzutowania — rejestruj dalej, a rzut pojawi się, gdy ostatni wzorzec będzie dość wyraźny.", + "daysNeeded": "Potrzeba jeszcze około {count} dni rejestrowania, zanim pojawi się rzut." } }, "cardioFitness": { diff --git a/src/app/insights/blood-pressure/page.tsx b/src/app/insights/blood-pressure/page.tsx index 87fd0d52c..994bfbc96 100644 --- a/src/app/insights/blood-pressure/page.tsx +++ b/src/app/insights/blood-pressure/page.tsx @@ -118,7 +118,6 @@ export default function InsightsBlutdruckPage() { > {/* The period-over-period delta keys on the systolic series — the canonical BP figure the targets + status surfaces lead with. */} - + {/* v1.12.0 — range pills + period-over-period delta below the chart. */} + diff --git a/src/app/insights/bmi/page.tsx b/src/app/insights/bmi/page.tsx index a405931e1..5b74d78dc 100644 --- a/src/app/insights/bmi/page.tsx +++ b/src/app/insights/bmi/page.tsx @@ -121,7 +121,6 @@ export default function InsightsBmiPage() { page's controls. BMI is derived from WEIGHT, so the period-over-period delta keys on the underlying weight series the chart reuses. */} - + {/* v1.12.0 — range pills + period-over-period delta below the chart. */} + diff --git a/src/app/insights/pulse/page.tsx b/src/app/insights/pulse/page.tsx index becdb4e0a..fb7ab30c7 100644 --- a/src/app/insights/pulse/page.tsx +++ b/src/app/insights/pulse/page.tsx @@ -132,7 +132,6 @@ export default function InsightsPulsPage() { coachLaunch showAllValuesType="PULSE" > - + {/* v1.12.0 — range pills + period-over-period delta below the chart. */} + diff --git a/src/app/insights/sleep/page.tsx b/src/app/insights/sleep/page.tsx index f2a4642c2..ca88e2191 100644 --- a/src/app/insights/sleep/page.tsx +++ b/src/app/insights/sleep/page.tsx @@ -80,10 +80,11 @@ export default function InsightsSchlafPage() { min / max / median / mean strip would duplicate it and read in an awkward unit. */ > - - + {/* v1.12.0 — range pills + period-over-period delta below the chart. */} + + {/* diff --git a/src/app/insights/weight/page.tsx b/src/app/insights/weight/page.tsx index b9e4824b5..a48e6e8a7 100644 --- a/src/app/insights/weight/page.tsx +++ b/src/app/insights/weight/page.tsx @@ -90,7 +90,6 @@ export default function InsightsGewichtPage() { coachLaunch showAllValuesType="WEIGHT" > - + {/* v1.12.0 — range pills + period-over-period delta below the chart. */} + @@ -109,6 +110,7 @@ export default function InsightsGewichtPage() { unit="kg" color="#bd93f9" enabled={!isEmpty} + compact /> {t("insights.heroGenerated", { time: formatRelativeTime(updatedAt, t), diff --git a/src/components/insights/derived/trajectory-forecast-card.tsx b/src/components/insights/derived/trajectory-forecast-card.tsx index bff5eb41b..af1e6cd6c 100644 --- a/src/components/insights/derived/trajectory-forecast-card.tsx +++ b/src/components/insights/derived/trajectory-forecast-card.tsx @@ -47,6 +47,16 @@ import type { DerivedProvenance } from "@/lib/insights/derived/types"; const TRAJECTORY_METRIC = "TRAJECTORY"; +/** + * Mirror of `TRAJECTORY_MIN_HISTORY_DAYS` (`src/lib/insights/derived/trajectory.ts`). + * Re-declared as a client-side literal rather than value-imported because the + * engine module pulls the rollup/baseline server graph in at its top level — + * importing the constant would drag that graph into the client bundle (the same + * reason `TrajectoryValue` is a type-only import). Keep this in lockstep with + * the engine; the unit test pins both at 14. + */ +const TRAJECTORY_MIN_HISTORY_DAYS = 14; + /** A single value-scale used to render stored values in the display unit. */ function scaled(value: number, scale: number): number { return value * scale; @@ -63,6 +73,14 @@ interface TrajectoryForecastCardProps { color?: string; /** Gate the underlying derived read (e.g. on the auth/empty flag). */ enabled?: boolean; + /** + * v1.12.0 — density by render context. Sub-pages pass `compact` so the + * card sheds the `min-h-48` floor + drops to a shorter chart and a tighter + * no-trend state (a too-tall "1 of 1" empty band was the maintainer flag). + * The Insights OVERVIEW grid omits it so the tile KEEPS its current size + * for grid symmetry — the default is the unchanged, roomier geometry. + */ + compact?: boolean; className?: string; } @@ -96,16 +114,25 @@ function TrajectoryProvenance({ /** Card shell — uppercase label + provenance affordance + body children. */ function CardShell({ provenance, + compact = false, children, }: { provenance?: DerivedProvenance; + compact?: boolean; children: React.ReactNode; }) { const { t } = useTranslations(); return (
@@ -119,18 +146,21 @@ function CardShell({ } /** CLS-safe skeleton matching the resolved card geometry. */ -function CardSkeleton() { +function CardSkeleton({ compact = false }: { compact?: boolean }) { return (