From 4935a9b000b877c7f5e9a645e53bf828cd9e337a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 1 Jun 2026 22:49:05 +0200 Subject: [PATCH] feat(insights): surface the diversity hint as a heading lightbulb The measurement-diversity nudge used to render as an inline note block on each category page; read in context it looked like a data finding rather than a tip and confused. Move it onto the heading as a small Lightbulb glyph that only mounts when the spread is genuinely lopsided; the hint text it used to show inline now lives in a hover / focus Tooltip with an accessible name. The sub-page shell carries the glyph in the heading row, drops the removed explainer trigger, and normalises the foot-of-page "show all readings" control to the standard secondary-button height. --- messages/de.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/it.json | 1 + messages/pl.json | 1 + .../__tests__/sub-page-shell.test.tsx | 35 +++++++- .../insights/measurement-diversity-nudge.tsx | 47 +++++++--- src/components/insights/sub-page-shell.tsx | 88 +++++++++++++------ 9 files changed, 133 insertions(+), 43 deletions(-) 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} + + + ); } 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 ? (