diff --git a/messages/de.json b/messages/de.json
index 6c7742275..41ebbfe8f 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1969,6 +1969,7 @@
"valuesPageTitle": "{metric} — alle Werte",
"valuesPageDescription": "Jeder einzelne Wert hinter dieser Kennzahl. Einträge lassen sich direkt bearbeiten oder entfernen.",
"diversity": {
+ "trigger": "Messhinweis",
"weekday": "Die meisten deiner {metric}-Werte fallen auf einen Wochentag. Messungen an anderen Tagen ergeben ein vollständigeres Bild.",
"timeOfDay": "Die meisten deiner {metric}-Werte fallen auf eine ähnliche Tageszeit. Unterschiedliche Zeiten ergeben ein vollständigeres Bild."
},
diff --git a/messages/en.json b/messages/en.json
index 3d4b3a817..b9cd8cf3f 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1969,6 +1969,7 @@
"valuesPageTitle": "{metric} — all readings",
"valuesPageDescription": "Every reading behind this metric. Edit or remove an entry directly.",
"diversity": {
+ "trigger": "Measurement tip",
"weekday": "Most of your {metric} readings land on one weekday. Logging on other days gives a fuller picture.",
"timeOfDay": "Most of your {metric} readings land at a similar time of day. Varying the time gives a fuller picture."
},
diff --git a/messages/es.json b/messages/es.json
index 51b9f4b91..f3f928fda 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1969,6 +1969,7 @@
"valuesPageTitle": "{metric} — todas las mediciones",
"valuesPageDescription": "Cada medición detrás de esta métrica. Edita o elimina una entrada directamente.",
"diversity": {
+ "trigger": "Consejo de medición",
"weekday": "La mayoría de tus mediciones de {metric} caen en un mismo día de la semana. Registrar en otros días ofrece una imagen más completa.",
"timeOfDay": "La mayoría de tus mediciones de {metric} caen a una hora similar del día. Variar la hora ofrece una imagen más completa."
},
diff --git a/messages/fr.json b/messages/fr.json
index 3cd6503e5..43b0ed6c6 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1969,6 +1969,7 @@
"valuesPageTitle": "{metric} — toutes les mesures",
"valuesPageDescription": "Chaque mesure derrière cette métrique. Modifiez ou supprimez une entrée directement.",
"diversity": {
+ "trigger": "Conseil de mesure",
"weekday": "La plupart de vos mesures de {metric} tombent le même jour de la semaine. Enregistrer d'autres jours donne une image plus complète.",
"timeOfDay": "La plupart de vos mesures de {metric} tombent à une heure similaire de la journée. Varier l'heure donne une image plus complète."
},
diff --git a/messages/it.json b/messages/it.json
index 62a9078b4..be1d03f75 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1969,6 +1969,7 @@
"valuesPageTitle": "{metric} — tutte le misurazioni",
"valuesPageDescription": "Ogni misurazione dietro questa metrica. Modifica o rimuovi una voce direttamente.",
"diversity": {
+ "trigger": "Suggerimento di misurazione",
"weekday": "La maggior parte delle tue misurazioni di {metric} cade in un solo giorno della settimana. Registrare in altri giorni offre un quadro più completo.",
"timeOfDay": "La maggior parte delle tue misurazioni di {metric} cade in un orario simile della giornata. Variare l'orario offre un quadro più completo."
},
diff --git a/messages/pl.json b/messages/pl.json
index abaaa6539..6e68a6b00 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1969,6 +1969,7 @@
"valuesPageTitle": "{metric} — wszystkie pomiary",
"valuesPageDescription": "Każdy pomiar stojący za tą metryką. Edytuj lub usuń wpis bezpośrednio.",
"diversity": {
+ "trigger": "Wskazówka pomiarowa",
"weekday": "Większość Twoich pomiarów {metric} przypada na jeden dzień tygodnia. Pomiary w inne dni dają pełniejszy obraz.",
"timeOfDay": "Większość Twoich pomiarów {metric} przypada o podobnej porze dnia. Różne pory dają pełniejszy obraz."
},
diff --git a/src/components/insights/__tests__/sub-page-shell.test.tsx b/src/components/insights/__tests__/sub-page-shell.test.tsx
index e586b2329..46394c46d 100644
--- a/src/components/insights/__tests__/sub-page-shell.test.tsx
+++ b/src/components/insights/__tests__/sub-page-shell.test.tsx
@@ -64,13 +64,42 @@ describe("", () => {
);
});
- it("keeps the explainer trigger glyph when explainerMetric is set", () => {
+ it("no longer paints the round `?` explainer trigger next to the heading", () => {
const html = render(
,
);
- // The `?` glyph stays available alongside the inline caption.
- expect(html).toMatch(/data-slot="metric-explainer-trigger"/);
+ // v1.8.6 — the `?` popover affordance was dropped; only the inline
+ // definition caption remains.
+ expect(html).not.toMatch(/data-slot="metric-explainer-trigger"/);
+ });
+
+ it("renders the diversity nudge node inside the heading row", () => {
+ const html = render(
+ }
+ >
+
+ ,
+ );
+ // v1.8.6 — the nudge moved up to the heading row (a `Lightbulb`
+ // glyph), no longer an inline block beneath the stat strip.
+ const headerEnd = html.indexOf("");
+ const probeIdx = html.indexOf('data-slot="diversity-probe"');
+ expect(probeIdx).toBeGreaterThan(-1);
+ expect(probeIdx).toBeLessThan(headerEnd);
+ });
+
+ it("mounts no coach launch surface when coachLaunch is omitted", () => {
+ const html = render(
+
+
+ ,
+ );
+ // The coach icon only renders under the launch provider + flags; the
+ // shell renders nothing for it on the default path.
+ expect(html).not.toContain('data-slot="coach-launch-icon"');
});
});
diff --git a/src/components/insights/measurement-diversity-nudge.tsx b/src/components/insights/measurement-diversity-nudge.tsx
index 23b81d76b..74eb1bf63 100644
--- a/src/components/insights/measurement-diversity-nudge.tsx
+++ b/src/components/insights/measurement-diversity-nudge.tsx
@@ -3,6 +3,12 @@
import { useQuery } from "@tanstack/react-query";
import { Lightbulb } from "lucide-react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -18,6 +24,14 @@ import { detectMeasurementDiversity } from "@/lib/insights/measurement-diversity
* when the spread is genuinely lopsided — surfaces a gentle hint to
* measure on other days / times.
*
+ * v1.8.6 — the hint moved off the page body and onto the heading. The
+ * inline `role="note"` block confused on the category pages (it read
+ * like a data finding rather than a tip), so the nudge now renders as a
+ * small `Lightbulb` glyph beside the page title. The hint text the block
+ * used to show inline lives in a hover / focus Tooltip; the glyph mounts
+ * only when the spread is actually lopsided, so a healthy metric shows
+ * no extra affordance at all.
+ *
* v1.8.3 anti-freeze posture: the read is a bounded `limit`-capped query
* with a 5-minute `staleTime`, fires only after the page has mounted
* (no SSR blocking), and renders nothing while in flight or when the
@@ -75,18 +89,29 @@ export function MeasurementDiversityNudge({
const message = t(`insights.subPage.diversity.${signal.kind}`, {
metric: metricLabel,
});
+ const triggerLabel = t("insights.subPage.diversity.trigger");
return (
-
-
-
{message}
-
+
+
+
+
+
+
+ {message}
+
+
+
);
}
diff --git a/src/components/insights/sub-page-shell.tsx b/src/components/insights/sub-page-shell.tsx
index 1bb8454d6..1142b5a51 100644
--- a/src/components/insights/sub-page-shell.tsx
+++ b/src/components/insights/sub-page-shell.tsx
@@ -6,7 +6,7 @@ import { ListOrdered } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { MetricExplainer } from "@/components/insights/metric-explainer";
+import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { useScrollResetOnRoute } from "@/hooks/use-scroll-reset-on-route";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
@@ -40,18 +40,16 @@ export interface SubPageShellProps {
*/
description?: string;
/**
- * v1.8.0 — when set, a `?` glyph next to the heading opens a static
- * "What is X?" explainer (popover on desktop, bottom-sheet on phones).
- * 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.0 — when set, the metric's static "What is X?" definition
+ * renders as a muted caption directly beneath the heading. The value
+ * is the metric key feeding `insights.subPage.explainer.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.
+ * v1.8.6 — the round `?` popover affordance next to the heading is
+ * gone (it read as restless next to the title). The inline definition
+ * caption stays — it is the single always-visible source of the
+ * one- to two-sentence definition, no tap required.
*/
explainerMetric?: string;
/**
@@ -77,11 +75,27 @@ export interface SubPageShellProps {
*/
statStrip?: ReactNode;
/**
- * v1.8.5 W4b — measurement-diversity nudge mounted just under the stat
- * strip. A gentle hint when readings cluster on one weekday / time.
- * Self-gating: renders nothing when the spread is healthy.
+ * v1.8.5 W4b — measurement-diversity nudge.
+ *
+ * v1.8.6 — relocated from an inline block under the stat strip to a
+ * `Lightbulb` glyph beside the heading (a hover / focus Tooltip carries
+ * the hint copy). The node is ``, which is
+ * self-gating: it renders the glyph only when readings cluster on one
+ * weekday / time, and nothing when the spread is healthy.
*/
diversityNudge?: ReactNode;
+ /**
+ * v1.8.6 — opt-in Coach launch in the page header.
+ *
+ * When true, the shell mounts an icon-only `` at
+ * the top-right of the header, vertically aligned with the heading, so
+ * "Coach fragen" reads as a header action rather than a foot-of-page
+ * button. Every routed category page passes it; the mother page and
+ * empty-state branches omit it. The button self-gates on the Coach
+ * feature flag + per-user opt-out, so a disabled-Coach tenant shows
+ * nothing.
+ */
+ coachLaunch?: boolean;
/**
* v1.8.5 W4b — a "show all readings" entry rendered at the foot of the
* page, after the chart + cards. Sub-pages pass the metric's
@@ -100,6 +114,7 @@ export function SubPageShell({
focusOnMount = false,
statStrip,
diversityNudge,
+ coachLaunch = false,
showAllValuesType,
children,
}: SubPageShellProps) {
@@ -144,18 +159,26 @@ export function SubPageShell({
>
{title}
- {explainerMetric ? (
-
- ) : null}
+ {/* v1.8.6 — the diversity nudge now rides the heading as a
+ `Lightbulb` glyph (the node self-gates to nothing when the
+ spread is healthy), replacing the old inline block. */}
+ {diversityNudge}
{badge ? (
{badge}
) : null}
+ {/* v1.8.6 — Coach launch sits top-right at heading height as an
+ icon. `ml-auto` pushes it to the row's trailing edge so it
+ aligns with the title regardless of badge / glyph presence.
+ The button self-gates on the Coach flag + per-user opt-out. */}
+ {coachLaunch ? (
+
+ ) : null}
{explainerMetric ? (
- // v1.8.4 — surface the explainer definition inline, reusing the
- // exact body string the `?` popover reads.
+ // v1.8.4 — surface the metric's static definition inline,
+ // directly beneath the heading.
//
// v1.8.5 W4a — render the explainer body as the *single* caption
// under the heading. Pre-v1.8.5 both this paragraph and the
@@ -165,6 +188,10 @@ export function SubPageShell({
// explainer body is the definition, so it wins; the description
// only renders when no explainer is set (the mother page and any
// future explainer-less sub-page).
+ //
+ // v1.8.6 — this caption is now the only surface for the
+ // definition; the round `?` popover that used to read the same
+ // string was removed.
{description}
) : null}
- {/* v1.8.5 W4b — numbers-first stat strip + diversity nudge sit
- between the header and the chart so the page leads with data,
- mirroring the Apple Health / Withings detail layout. Both are
- self-gating (render nothing without data), so the spacing
- rhythm holds on the empty-state and brand-new-metric paths. */}
+ {/* v1.8.5 W4b — numbers-first stat strip sits between the header
+ and the chart so the page leads with data, mirroring the Apple
+ Health / Withings detail layout. Self-gating (renders nothing
+ without data), so the spacing rhythm holds on the empty-state
+ and brand-new-metric paths.
+ v1.8.6 — the diversity nudge moved up to the heading row. */}
{statStrip}
- {diversityNudge}
{children}
{/* v1.8.5 W4b — "show all readings" entry at the foot, linking to
- the dedicated per-metric values subpage. */}
+ the dedicated per-metric values subpage.
+ v1.8.6 — normalised to the `h-10` secondary-button height so it
+ reads as a consistent control now that the foot-of-page Coach
+ button (which set the old visual baseline) has moved to the
+ header. */}
{showAllValuesType ? (