From c2808a9c70efd8cc07d2467e497e1cc8507e2bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 1 Jun 2026 17:24:49 +0200 Subject: [PATCH] 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.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. --- .../__tests__/sub-page-shell.test.tsx | 25 ++++++++++++++++++- src/components/insights/sub-page-shell.tsx | 21 ++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/components/insights/__tests__/sub-page-shell.test.tsx b/src/components/insights/__tests__/sub-page-shell.test.tsx index 4873a42e7..e586b2329 100644 --- a/src/components/insights/__tests__/sub-page-shell.test.tsx +++ b/src/components/insights/__tests__/sub-page-shell.test.tsx @@ -47,7 +47,30 @@ describe("", () => { , ); // The shell uses a `

` for the description; no `

` appears when - // the description prop is absent. + // neither a description nor an explainer metric is supplied. expect(html).not.toMatch(/]*class="text-muted-foreground/); }); + + it("renders the explainer body inline beneath the heading", () => { + const html = render( + + + , + ); + // 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( + + + , + ); + // The `?` glyph stays available alongside the inline caption. + expect(html).toMatch(/data-slot="metric-explainer-trigger"/); + }); }); diff --git a/src/components/insights/sub-page-shell.tsx b/src/components/insights/sub-page-shell.tsx index cc2c84566..b26575fe0 100644 --- a/src/components/insights/sub-page-shell.tsx +++ b/src/components/insights/sub-page-shell.tsx @@ -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.{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.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({ ) : null} + {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. +

+ {t(`insights.subPage.explainer.${explainerMetric}Body`)} +

+ ) : null} {description ? (

{description}

) : null}