From 71b29f5b6f2fb18ba33040e261978aeb4a29a6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 18 Jun 2026 21:20:19 +0200 Subject: [PATCH] refactor(recovery): drop the per-group sub-headings on the recovery page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read next to the page's "Erholung" heading, the "Recharge" / "Strain & load" / "Daily heart & energy" sub-headings were redundant chrome. Remove them and render the present signal blocks as one flat, evenly-spaced stack — each block already carries its own title + explainer, so nothing loses context. The group structure stays internally to drive block order and the data gate. Promote the block title to an h2 so the page heading outline stays nested (h1 then h2) with no skipped level, and drop the now-orphaned group i18n keys. --- messages/de.json | 12 --- messages/en.json | 12 --- messages/es.json | 12 --- messages/fr.json | 12 --- messages/it.json | 12 --- messages/pl.json | 12 --- .../__tests__/device-score-surfaces.test.tsx | 13 ++- .../recovery/recovery-metric-block.tsx | 12 +-- .../insights/recovery/recovery-section.tsx | 82 ++++++------------- 9 files changed, 40 insertions(+), 139 deletions(-) diff --git a/messages/de.json b/messages/de.json index 8f654ac76..6e9a48271 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3509,18 +3509,6 @@ "description": "Wie gut sich dein Körper erholt hat – die Erholungs-, Belastungs- und Load-Signale, die dein Wearable über Nacht aufzeichnet.", "emptyTitle": "Noch keine Erholungsdaten", "empty": "Noch keine Erholungsdaten. Verbinde ein WHOOP-, Polar- oder Oura-Konto, dann füllen sich diese Werte, sobald dein Gerät synchronisiert.", - "recharge": { - "title": "Recharge", - "subtitle": "Ladung des autonomen Nervensystems" - }, - "strain": { - "title": "Belastung & Load", - "subtitle": "Tägliche kardiovaskuläre Belastung" - }, - "cardio": { - "title": "Herz & Energie pro Tag", - "subtitle": "Herz- und Energiewerte des ganzen Tages" - }, "block": { "ansChargeExplainer": "Wie aufgeladen dein autonomes Nervensystem in den Tag startet – ein Recharge-Wert aus deinen nächtlichen Herz- und Atemsignalen.", "dayStrainExplainer": "Die kardiovaskuläre Tagesbelastung auf der Belastungsskala deines Geräts. Lies sie im Verhältnis zu deiner Erholung, nicht als Ziel für sich.", diff --git a/messages/en.json b/messages/en.json index accbf337f..c33810296 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3509,18 +3509,6 @@ "description": "How well your body has bounced back — the recovery, strain and load signals your wearable records overnight.", "emptyTitle": "No recovery data yet", "empty": "No recovery data yet. Connect a WHOOP, Polar or Oura account and these scores fill in once your device syncs.", - "recharge": { - "title": "Recharge", - "subtitle": "Autonomic-nervous-system charge" - }, - "strain": { - "title": "Strain & load", - "subtitle": "Daily cardiovascular load" - }, - "cardio": { - "title": "Daily heart & energy", - "subtitle": "Whole-day cardiac and energy totals" - }, "block": { "ansChargeExplainer": "How charged your autonomic nervous system is for the day ahead — a recharge reading drawn from your overnight heart and breathing signals.", "dayStrainExplainer": "The day's whole-body cardiovascular load on your device's strain scale. Read it against your recovery, not as a goal on its own.", diff --git a/messages/es.json b/messages/es.json index 9fa3e2b1e..0a1b03839 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3509,18 +3509,6 @@ "description": "Cómo se ha recuperado tu cuerpo: las señales de recuperación, esfuerzo y carga que tu wearable registra durante la noche.", "emptyTitle": "Aún no hay datos de recuperación", "empty": "Aún no hay datos de recuperación. Conecta una cuenta de WHOOP, Polar u Oura y estas puntuaciones se completarán cuando tu dispositivo sincronice.", - "recharge": { - "title": "Recarga", - "subtitle": "Carga del sistema nervioso autónomo" - }, - "strain": { - "title": "Esfuerzo y carga", - "subtitle": "Carga cardiovascular diaria" - }, - "cardio": { - "title": "Corazón y energía del día", - "subtitle": "Totales cardiacos y de energía de todo el día" - }, "block": { "ansChargeExplainer": "Cuán cargado está tu sistema nervioso autónomo para el día — una lectura de recarga obtenida de tus señales nocturnas de corazón y respiración.", "dayStrainExplainer": "La carga cardiovascular del día en la escala de esfuerzo de tu dispositivo. Léela en relación con tu recuperación, no como una meta en sí.", diff --git a/messages/fr.json b/messages/fr.json index 9c1a433c0..38a840ea8 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3509,18 +3509,6 @@ "description": "À quel point votre corps a récupéré : les signaux de récupération, de charge et d'effort que votre wearable enregistre la nuit.", "emptyTitle": "Pas encore de données de récupération", "empty": "Pas encore de données de récupération. Connectez un compte WHOOP, Polar ou Oura et ces scores se rempliront dès que votre appareil se synchronise.", - "recharge": { - "title": "Recharge", - "subtitle": "Charge du système nerveux autonome" - }, - "strain": { - "title": "Effort et charge", - "subtitle": "Charge cardiovasculaire quotidienne" - }, - "cardio": { - "title": "Cœur et énergie du jour", - "subtitle": "Totaux cardiaques et énergétiques de la journée" - }, "block": { "ansChargeExplainer": "À quel point votre système nerveux autonome est rechargé pour la journée — une mesure de recharge issue de vos signaux nocturnes de cœur et de respiration.", "dayStrainExplainer": "La charge cardiovasculaire de la journée sur l'échelle d'effort de votre appareil. Lisez-la au regard de votre récupération, pas comme un objectif en soi.", diff --git a/messages/it.json b/messages/it.json index 77ba27740..489c26670 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3509,18 +3509,6 @@ "description": "Quanto bene il tuo corpo si è ripreso: i segnali di recupero, sforzo e carico che il tuo wearable registra durante la notte.", "emptyTitle": "Ancora nessun dato di recupero", "empty": "Ancora nessun dato di recupero. Collega un account WHOOP, Polar o Oura e questi punteggi si popoleranno appena il dispositivo si sincronizza.", - "recharge": { - "title": "Ricarica", - "subtitle": "Carica del sistema nervoso autonomo" - }, - "strain": { - "title": "Sforzo e carico", - "subtitle": "Carico cardiovascolare giornaliero" - }, - "cardio": { - "title": "Cuore ed energia del giorno", - "subtitle": "Totali cardiaci ed energetici dell'intera giornata" - }, "block": { "ansChargeExplainer": "Quanto è carico il tuo sistema nervoso autonomo per la giornata — una lettura di ricarica ricavata dai segnali notturni di cuore e respiro.", "dayStrainExplainer": "Il carico cardiovascolare della giornata sulla scala di sforzo del tuo dispositivo. Leggilo in rapporto al tuo recupero, non come obiettivo a sé.", diff --git a/messages/pl.json b/messages/pl.json index 6bd2a8336..9b66dda9e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3509,18 +3509,6 @@ "description": "Jak dobrze Twój organizm się zregenerował – sygnały regeneracji, obciążenia i load, które Twoje urządzenie rejestruje w nocy.", "emptyTitle": "Brak danych o regeneracji", "empty": "Brak danych o regeneracji. Połącz konto WHOOP, Polar lub Oura, a te wyniki uzupełnią się po synchronizacji urządzenia.", - "recharge": { - "title": "Recharge", - "subtitle": "Ładowanie autonomicznego układu nerwowego" - }, - "strain": { - "title": "Obciążenie i load", - "subtitle": "Dzienne obciążenie sercowo-naczyniowe" - }, - "cardio": { - "title": "Serce i energia w ciągu dnia", - "subtitle": "Całodobowe sumy sercowe i energetyczne" - }, "block": { "ansChargeExplainer": "Jak naładowany jest Twój autonomiczny układ nerwowy na dany dzień — odczyt regeneracji oparty na nocnych sygnałach serca i oddechu.", "dayStrainExplainer": "Dzienne obciążenie sercowo-naczyniowe na skali wysiłku Twojego urządzenia. Odczytuj je w odniesieniu do regeneracji, nie jako cel sam w sobie.", diff --git a/src/components/insights/__tests__/device-score-surfaces.test.tsx b/src/components/insights/__tests__/device-score-surfaces.test.tsx index 73ae8e968..0018b2cab 100644 --- a/src/components/insights/__tests__/device-score-surfaces.test.tsx +++ b/src/components/insights/__tests__/device-score-surfaces.test.tsx @@ -206,7 +206,7 @@ describe(" data-gating", () => { // Routed through the shared (dashed bordered card), not the // old hand-rolled div — the empty title comes from the unified primitive. expect(html).toContain("border-dashed"); - expect(html).not.toContain('data-slot="recovery-group-strain"'); + expect(html).not.toContain('data-slot="recovery-block-DAY_STRAIN"'); }); it("renders a canonical chart block for the present signal", () => { @@ -218,13 +218,18 @@ describe(" data-gating", () => { }), ); const html = render(); - expect(html).toContain('data-slot="recovery-group-strain"'); + // v1.18.6 — the per-group sub-headings are gone; present blocks render as + // one flat stack. The data gate now reads at the block level. expect(html).toContain('data-slot="recovery-block-DAY_STRAIN"'); // B3 — the redundant "Recovery score" cross-link block is gone; this // surface is reached from the overview already. expect(html).not.toContain('data-slot="recovery-score-link"'); - // The recharge group (ANS charge only) has no data → hidden. - expect(html).not.toContain('data-slot="recovery-group-recharge"'); + // The recharge group's only signal (ANS charge) has no data → its block + // is dropped. + expect(html).not.toContain('data-slot="recovery-block-ANS_CHARGE"'); + // The removed sub-headings must not reappear. + expect(html).not.toContain('data-slot="recovery-group-strain"'); + expect(html).not.toContain("Strain & load"); // Empty note must not show when at least one signal is present. expect(html).not.toContain('data-slot="recovery-empty"'); }); diff --git a/src/components/insights/recovery/recovery-metric-block.tsx b/src/components/insights/recovery/recovery-metric-block.tsx index 09265d492..88527e76e 100644 --- a/src/components/insights/recovery/recovery-metric-block.tsx +++ b/src/components/insights/recovery/recovery-metric-block.tsx @@ -77,12 +77,12 @@ export function RecoveryMetricBlock({ className="text-muted-foreground size-4 shrink-0" aria-hidden="true" /> - {/* v1.18.1 — block title is an `

` (kept at `text-base`): it - nests under the group `SectionHeading` `

`, which itself - sits under the page `

` from `SubPageShell`. Demoting it - keeps the recovery page's heading outline strictly nested - (h1 → h2 → h3) instead of stacking two sibling `

`s. */} -

{title}

+ {/* v1.18.6 — block title is an `

` (at `text-base`): the per-group + sub-headings were removed, so each block now sits directly under + the page `

` from `SubPageShell`. Promoting it from `

` keeps + the recovery page's heading outline strictly nested (h1 → h2) + with no skipped level. */} +

{title}

{explainer} diff --git a/src/components/insights/recovery/recovery-section.tsx b/src/components/insights/recovery/recovery-section.tsx index 9fa4ba138..701442858 100644 --- a/src/components/insights/recovery/recovery-section.tsx +++ b/src/components/insights/recovery/recovery-section.tsx @@ -14,7 +14,6 @@ import { useInsightsAnalytics } from "@/hooks/use-insights-analytics"; import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query"; import { useTranslations } from "@/lib/i18n/context"; import { EmptyState } from "@/components/ui/empty-state"; -import { SectionHeading } from "@/components/insights/section-heading"; import { ChartSkeleton } from "@/components/charts/chart-skeleton"; import { RestModeBanner } from "@/components/insights/rest-mode-banner"; import type { RestModeAnnotation } from "@/lib/analytics/health-score"; @@ -36,9 +35,6 @@ interface BlockSpec { interface BlockGroup { key: string; - headingIcon: typeof Activity; - title: string; - subtitle: string; blocks: BlockSpec[]; } @@ -84,9 +80,6 @@ export function RecoverySection() { const groups: BlockGroup[] = [ { key: "recharge", - headingIcon: Battery, - title: t("insights.recovery.recharge.title"), - subtitle: t("insights.recovery.recharge.subtitle"), blocks: [ { type: "ANS_CHARGE", @@ -103,9 +96,6 @@ export function RecoverySection() { }, { key: "strain", - headingIcon: Gauge, - title: t("insights.recovery.strain.title"), - subtitle: t("insights.recovery.strain.subtitle"), blocks: [ { type: "DAY_STRAIN", @@ -143,9 +133,6 @@ export function RecoverySection() { }, { key: "cardio", - headingIcon: HeartPulse, - title: t("insights.recovery.cardio.title"), - subtitle: t("insights.recovery.cardio.subtitle"), blocks: [ { type: "AVERAGE_HEART_RATE", @@ -190,10 +177,6 @@ export function RecoverySection() { if (isLoading || summaries == null) { return (

-
); @@ -210,51 +193,36 @@ export function RecoverySection() { ); } + // v1.18.6 — the per-group sub-headings ("Recharge" / "Strain & load" / + // "Daily heart & energy") are gone: read next to the page's "Erholung" + // heading they were redundant chrome. The groups still drive ORDER + the + // data gate (a group with no present block contributes nothing); their + // present blocks now render as one flat, evenly-spaced stack, each block + // carrying its own title + explainer so nothing loses context. + const presentBlocks = groups.flatMap((group) => + group.blocks.filter((block) => (summaries[block.type]?.count ?? 0) > 0), + ); + return (
{/* v1.18.1 — calm Rest Mode cue at the head of the recovery surface. Self-gating: renders nothing unless an episode is active. */} - {groups.map((group) => { - const present = group.blocks.filter( - (block) => (summaries[block.type]?.count ?? 0) > 0, - ); - if (present.length === 0) return null; - return ( -
- - {group.subtitle} -

- } - /> -
- {present.map((block) => ( - - ))} -
-
- ); - })} + {presentBlocks.map((block) => ( + + ))}
); }