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:
Marc-André Bombeck committed 2026-06-18 21:20:19 +02:00
1 parent aa9b713a66
commit 71b29f5b6f
9 files changed
+40 -139

No files matched your search

@@ -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 &amp; 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>
);
}