mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(recovery): drop the per-group sub-headings on the recovery page
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.
This commit is contained in:
1 parent
aa9b713a66
commit
71b29f5b6f
9 files changed
+40
-139
No files matched your search
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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í.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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é.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -206,7 +206,7 @@ describe("<RecoverySection> data-gating", () => {
|
||||
// Routed through the shared <EmptyState> (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("<RecoverySection> data-gating", () => {
|
||||
}),
|
||||
);
|
||||
const html = render(<RecoverySection />);
|
||||
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"');
|
||||
});
|
||||
|
||||
@@ -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 `<h3>` (kept at `text-base`): it
|
||||
nests under the group `SectionHeading` `<h2>`, which itself
|
||||
sits under the page `<h1>` from `SubPageShell`. Demoting it
|
||||
keeps the recovery page's heading outline strictly nested
|
||||
(h1 → h2 → h3) instead of stacking two sibling `<h2>`s. */}
|
||||
<h3 className="text-base font-semibold">{title}</h3>
|
||||
{/* v1.18.6 — block title is an `<h2>` (at `text-base`): the per-group
|
||||
sub-headings were removed, so each block now sits directly under
|
||||
the page `<h1>` from `SubPageShell`. Promoting it from `<h3>` keeps
|
||||
the recovery page's heading outline strictly nested (h1 → h2)
|
||||
with no skipped level. */}
|
||||
<h2 className="text-base font-semibold">{title}</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
{explainer}
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-3" data-slot="recovery-loading">
|
||||
<SectionHeading
|
||||
icon={Battery}
|
||||
title={t("insights.recovery.recharge.title")}
|
||||
/>
|
||||
<ChartSkeleton />
|
||||
</div>
|
||||
);
|
||||
@@ -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 (
|
||||
<div className="space-y-8" data-slot="recovery-section">
|
||||
{/* v1.18.1 — calm Rest Mode cue at the head of the recovery surface.
|
||||
Self-gating: renders nothing unless an episode is active. */}
|
||||
<RestModeBanner annotation={restMode} />
|
||||
{groups.map((group) => {
|
||||
const present = group.blocks.filter(
|
||||
(block) => (summaries[block.type]?.count ?? 0) > 0,
|
||||
);
|
||||
if (present.length === 0) return null;
|
||||
return (
|
||||
<section
|
||||
key={group.key}
|
||||
data-slot={`recovery-group-${group.key}`}
|
||||
className="space-y-4"
|
||||
>
|
||||
<SectionHeading
|
||||
icon={group.headingIcon}
|
||||
title={group.title}
|
||||
action={
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{group.subtitle}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<div className="space-y-8">
|
||||
{present.map((block) => (
|
||||
<RecoveryMetricBlock
|
||||
key={block.type}
|
||||
type={block.type}
|
||||
chartKey={block.chartKey}
|
||||
statusMetric={block.statusMetric}
|
||||
title={block.title}
|
||||
explainer={block.explainer}
|
||||
icon={block.icon}
|
||||
color={block.color}
|
||||
unit={block.unit}
|
||||
summary={summaries[block.type] ?? null}
|
||||
fractionDigits={block.fractionDigits}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{presentBlocks.map((block) => (
|
||||
<RecoveryMetricBlock
|
||||
key={block.type}
|
||||
type={block.type}
|
||||
chartKey={block.chartKey}
|
||||
statusMetric={block.statusMetric}
|
||||
title={block.title}
|
||||
explainer={block.explainer}
|
||||
icon={block.icon}
|
||||
color={block.color}
|
||||
unit={block.unit}
|
||||
summary={summaries[block.type] ?? null}
|
||||
fractionDigits={block.fractionDigits}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user