mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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.
This commit is contained in:
1 parent
0e9fa7bf80
commit
4935a9b000
9 files changed
+133
-43
No files matched your search
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -64,13 +64,42 @@ describe("<SubPageShell>", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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(
|
||||
<SubPageShell title="Blood pressure" explainerMetric="bloodPressure">
|
||||
<span />
|
||||
</SubPageShell>,
|
||||
);
|
||||
// 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(
|
||||
<SubPageShell
|
||||
title="Weight"
|
||||
diversityNudge={<span data-slot="diversity-probe" />}
|
||||
>
|
||||
<span />
|
||||
</SubPageShell>,
|
||||
);
|
||||
// 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("</header>");
|
||||
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(
|
||||
<SubPageShell title="Pulse">
|
||||
<span />
|
||||
</SubPageShell>,
|
||||
);
|
||||
// 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"');
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<div
|
||||
data-slot="measurement-diversity-nudge"
|
||||
role="note"
|
||||
className="bg-muted/40 text-muted-foreground flex items-start gap-2.5 rounded-lg px-3 py-2.5 text-sm"
|
||||
>
|
||||
<Lightbulb
|
||||
className="text-dracula-yellow mt-0.5 size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="leading-relaxed">{message}</p>
|
||||
</div>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="measurement-diversity-nudge"
|
||||
aria-label={triggerLabel}
|
||||
className="text-dracula-yellow hover:text-dracula-yellow/80 focus-visible:ring-ring/50 -my-3 -mx-2 inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<Lightbulb className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
data-slot="measurement-diversity-nudge-body"
|
||||
align="start"
|
||||
className="max-w-xs leading-relaxed"
|
||||
>
|
||||
{message}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -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.<metric>{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.<metric>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.
|
||||
* 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 `<MeasurementDiversityNudge>`, 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 `<CoachLaunchButton>` 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}
|
||||
</h1>
|
||||
{explainerMetric ? (
|
||||
<MetricExplainer metric={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 variant="outline" className="border text-xs">
|
||||
{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 ? (
|
||||
<CoachLaunchButton variant="icon" className="ml-auto" />
|
||||
) : null}
|
||||
</div>
|
||||
{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.
|
||||
<p
|
||||
data-slot="metric-explainer-inline"
|
||||
className="text-muted-foreground text-sm leading-relaxed"
|
||||
@@ -176,23 +203,26 @@ export function SubPageShell({
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
) : null}
|
||||
</header>
|
||||
{/* 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 ? (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="metric-show-all-values"
|
||||
className="w-full sm:w-auto"
|
||||
className="h-10 w-full sm:w-auto"
|
||||
>
|
||||
<Link href={`/insights/values/${showAllValuesType}`}>
|
||||
<ListOrdered className="mr-1.5 size-4" aria-hidden="true" />
|
||||
|
||||
Reference in new issue
Block a user