feat(insights): show the metric explainer inline under each category heading

The per-category explainer text only lived behind the "?" popover next to
the heading. Render the same localised definition inline as a muted caption
directly beneath the title, where the short scaffold line already sits, so
the one- to two-sentence definition is discoverable without a tap.

Both surfaces read the identical insights.subPage.explainer.<metric>Body
key, so there is no duplicated copy and all six locales stay covered. The
"?" glyph stays in place as the always-available pointer to the same text.
HealthkitMetricPage already forwards explainerMetric to the shell, so every
routed metric sub-page picks up the inline caption from this one change.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 17:31:02 +02:00
1 parent 0e3a8f9cd3
commit c2808a9c70
2 files changed
+45 -1

No files matched your search

@@ -47,7 +47,30 @@ describe("<SubPageShell>", () => {
</SubPageShell>,
);
// The shell uses a `<p>` for the description; no `<p>` appears when
// the description prop is absent.
// neither a description nor an explainer metric is supplied.
expect(html).not.toMatch(/<p[^>]*class="text-muted-foreground/);
});
it("renders the explainer body inline beneath the heading", () => {
const html = render(
<SubPageShell title="Blood pressure" explainerMetric="bloodPressure">
<span />
</SubPageShell>,
);
// The inline caption reuses the same body string the `?` popover reads.
expect(html).toMatch(/data-slot="metric-explainer-inline"/);
expect(html).toContain(
"Blood pressure is the force your blood exerts on the artery walls",
);
});
it("keeps the explainer trigger glyph when explainerMetric is set", () => {
const html = render(
<SubPageShell title="Blood pressure" explainerMetric="bloodPressure">
<span />
</SubPageShell>,
);
// The `?` glyph stays available alongside the inline caption.
expect(html).toMatch(/data-slot="metric-explainer-trigger"/);
});
});
@@ -5,6 +5,7 @@ import { useEffect, useRef, type ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { MetricExplainer } from "@/components/insights/metric-explainer";
import { useScrollResetOnRoute } from "@/hooks/use-scroll-reset-on-route";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
/**
@@ -41,6 +42,13 @@ export interface SubPageShellProps {
* The value is the metric key feeding
* `insights.subPage.explainer.<metric>{Title,Body}`. Omitted on the
* mother page; every routed metric sub-page passes its key.
*
* v1.8.4 — the same explainer body also renders inline as a muted
* caption directly beneath the heading, so the one- to two-sentence
* definition is visible without opening the popover. The `?` glyph
* stays as the always-available pointer for the same copy. Both the
* inline caption and the popover read the identical
* `insights.subPage.explainer.<metric>Body` string — no duplicated copy.
*/
explainerMetric?: string;
/**
@@ -69,6 +77,7 @@ export function SubPageShell({
focusOnMount = false,
children,
}: SubPageShellProps) {
const { t } = useTranslations();
// a11y: focus the heading on mount so a tab-strip navigation actually
// moves screen-reader focus into the sub-page body. v1.4.27 MB7 /
// CF-35 made the focus call opt-in via `focusOnMount` — the
@@ -118,6 +127,18 @@ export function SubPageShell({
</Badge>
) : null}
</div>
{explainerMetric ? (
// v1.8.4 — surface the explainer definition inline, reusing the
// exact body string the `?` popover reads. Muted caption styling
// matches the description paragraph below so the under-heading
// band reads as one calm block.
<p
data-slot="metric-explainer-inline"
className="text-muted-foreground text-sm leading-relaxed"
>
{t(`insights.subPage.explainer.${explainerMetric}Body`)}
</p>
) : null}
{description ? (
<p className="text-muted-foreground text-sm">{description}</p>
) : null}