diff --git a/messages/de.json b/messages/de.json index 86fcb9031..f63ed1c59 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2558,7 +2558,8 @@ "weightTitle": "Stimmung vs. Gewicht", "bloodPressureTitle": "Stimmung vs. Blutdruck", "weightAxis": "Gewicht", - "bloodPressureAxis": "Systolisch (mmHg)" + "bloodPressureAxis": "Systolisch (mmHg)", + "sourceLabel": "Wie das berechnet wurde" }, "structuredTagsTitle": "Tag-Auswertung", "inTargetValue": "{pct} %", @@ -2629,7 +2630,9 @@ "moodDown": "Höheres {factor} geht in deinen Daten tendenziell mit schlechterer Stimmung am Folgetag einher.", "outcomeUp": "Bessere Stimmung geht in deinen Daten tendenziell mit höherem {factor} am Folgetag einher.", "outcomeDown": "Bessere Stimmung geht in deinen Daten tendenziell mit niedrigerem {factor} am Folgetag einher.", - "stat": "{n} gepaarte Tage · r = {r} · q = {q}" + "stat": "{n} gepaarte Tage · r = {r} · q = {q}", + "statLabel": "Wie das berechnet wurde", + "explainerLabel": "Wie das berechnet wurde" } }, "derived": { diff --git a/messages/en.json b/messages/en.json index 5d4da4ca3..9b74a7ae0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2553,6 +2553,7 @@ "strengthWeak": "Weak", "strengthNone": "None", "source": "{n} paired days · r = {r}", + "sourceLabel": "How this was computed", "emptyTitle": "Not enough paired data yet", "emptyDescription": "Log mood and this metric on the same days to see a correlation.", "weightTitle": "Mood vs. weight", @@ -2629,7 +2630,9 @@ "moodDown": "Higher {factor} tends to go with lower next-day mood in your data.", "outcomeUp": "Higher mood tends to go with higher next-day {factor} in your data.", "outcomeDown": "Higher mood tends to go with lower next-day {factor} in your data.", - "stat": "{n} paired days · r = {r} · q = {q}" + "stat": "{n} paired days · r = {r} · q = {q}", + "statLabel": "How this was computed", + "explainerLabel": "How this was computed" } }, "derived": { diff --git a/messages/es.json b/messages/es.json index 81c68c037..b71f2d98d 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2558,7 +2558,8 @@ "weightTitle": "Ánimo vs. peso", "bloodPressureTitle": "Ánimo vs. presión arterial", "weightAxis": "Peso", - "bloodPressureAxis": "Sistólica (mmHg)" + "bloodPressureAxis": "Sistólica (mmHg)", + "sourceLabel": "Cómo se calculó esto" }, "structuredTagsTitle": "Desglose de etiquetas", "inTargetValue": "{pct} %", @@ -2629,7 +2630,9 @@ "moodDown": "Un mayor {factor} suele coincidir con peor ánimo al día siguiente en tus datos.", "outcomeUp": "Mejor ánimo suele coincidir con un {factor} más alto al día siguiente en tus datos.", "outcomeDown": "Mejor ánimo suele coincidir con un {factor} más bajo al día siguiente en tus datos.", - "stat": "{n} días emparejados · r = {r} · q = {q}" + "stat": "{n} días emparejados · r = {r} · q = {q}", + "statLabel": "Cómo se calculó esto", + "explainerLabel": "Cómo se calculó esto" } }, "derived": { diff --git a/messages/fr.json b/messages/fr.json index db919b66c..ae2827a11 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2558,7 +2558,8 @@ "weightTitle": "Humeur vs. poids", "bloodPressureTitle": "Humeur vs. tension artérielle", "weightAxis": "Poids", - "bloodPressureAxis": "Systolique (mmHg)" + "bloodPressureAxis": "Systolique (mmHg)", + "sourceLabel": "Comment ceci est calculé" }, "structuredTagsTitle": "Répartition des étiquettes", "inTargetValue": "{pct} %", @@ -2629,7 +2630,9 @@ "moodDown": "Un {factor} plus élevé tend à coïncider avec une moins bonne humeur le lendemain dans vos données.", "outcomeUp": "Une meilleure humeur tend à coïncider avec un {factor} plus élevé le lendemain dans vos données.", "outcomeDown": "Une meilleure humeur tend à coïncider avec un {factor} plus bas le lendemain dans vos données.", - "stat": "{n} jours appariés · r = {r} · q = {q}" + "stat": "{n} jours appariés · r = {r} · q = {q}", + "statLabel": "Comment ceci est calculé", + "explainerLabel": "Comment ceci est calculé" } }, "derived": { diff --git a/messages/it.json b/messages/it.json index e50b0f5f8..6ede77c17 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2558,7 +2558,8 @@ "weightTitle": "Umore vs. peso", "bloodPressureTitle": "Umore vs. pressione sanguigna", "weightAxis": "Peso", - "bloodPressureAxis": "Sistolica (mmHg)" + "bloodPressureAxis": "Sistolica (mmHg)", + "sourceLabel": "Come è stato calcolato" }, "structuredTagsTitle": "Analisi etichette", "inTargetValue": "{pct} %", @@ -2629,7 +2630,9 @@ "moodDown": "Un {factor} più alto tende a coincidere con un umore peggiore il giorno dopo nei tuoi dati.", "outcomeUp": "Un umore migliore tende a coincidere con un {factor} più alto il giorno dopo nei tuoi dati.", "outcomeDown": "Un umore migliore tende a coincidere con un {factor} più basso il giorno dopo nei tuoi dati.", - "stat": "{n} giorni appaiati · r = {r} · q = {q}" + "stat": "{n} giorni appaiati · r = {r} · q = {q}", + "statLabel": "Come è stato calcolato", + "explainerLabel": "Come è stato calcolato" } }, "derived": { diff --git a/messages/pl.json b/messages/pl.json index 647ee2e35..5153f4a78 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2558,7 +2558,8 @@ "weightTitle": "Nastrój vs. waga", "bloodPressureTitle": "Nastrój vs. ciśnienie krwi", "weightAxis": "Waga", - "bloodPressureAxis": "Skurczowe (mmHg)" + "bloodPressureAxis": "Skurczowe (mmHg)", + "sourceLabel": "Jak to obliczono" }, "structuredTagsTitle": "Podział tagów", "inTargetValue": "{pct}%", @@ -2629,7 +2630,9 @@ "moodDown": "Wyższy {factor} zwykle wiąże się z gorszym nastrojem następnego dnia w Twoich danych.", "outcomeUp": "Lepszy nastrój zwykle wiąże się z wyższym {factor} następnego dnia w Twoich danych.", "outcomeDown": "Lepszy nastrój zwykle wiąże się z niższym {factor} następnego dnia w Twoich danych.", - "stat": "{n} sparowanych dni · r = {r} · q = {q}" + "stat": "{n} sparowanych dni · r = {r} · q = {q}", + "statLabel": "Jak to obliczono", + "explainerLabel": "Jak to obliczono" } }, "derived": { diff --git a/src/components/insights/mood/__tests__/mood-discovered-relations.test.tsx b/src/components/insights/mood/__tests__/mood-discovered-relations.test.tsx index 05ea3957e..204510baa 100644 --- a/src/components/insights/mood/__tests__/mood-discovered-relations.test.tsx +++ b/src/components/insights/mood/__tests__/mood-discovered-relations.test.tsx @@ -96,9 +96,11 @@ describe("", () => { // factor label resolves to the localized measurement name expect(html).toContain("Time in Daylight"); expect(html).toContain("higher next-day mood"); - // stat line carries n, r, q - expect(html).toContain("r = 0.50"); - expect(html).toContain("q = 0.020"); + // v1.12.4 (C4) — the n/r/q detail moved off the visible sub-row into a + // focusable explainer icon (its tooltip content is portalled on open, so + // it is absent from the static markup). Assert the icon is wired per pair + // instead of the inline stat string. + expect(html).toContain('aria-label="How this was computed"'); }); it("phrases a mood → next-day outcome pair", () => { @@ -111,7 +113,7 @@ describe("", () => { expect(html).toContain("lower next-day"); }); - it("filters to mood pairs and shows the honest full-family footer", () => { + it("filters to mood pairs and folds the full-family footer into the header explainer", () => { const html = renderWith( [ pair({ behaviour: "TIME_IN_DAYLIGHT", outcome: "MOOD" }), @@ -122,8 +124,16 @@ describe("", () => { expect((html.match(/data-slot="mood-discovered-pair"/g) ?? []).length).toBe( 1, ); - // Footer's denominator is the full behaviour×outcome family the engine - // tested, not the mood-only subset — so it must report 12, not 1 of 1. - expect(html).toContain("from 12 day-to-day pairs"); + // v1.12.4 (C3) — the false-discovery footer (denominator = the full + // behaviour×outcome family the engine tested, i.e. 12, not the mood-only + // subset) + the observational disclaimer were two full-width footnote + // rows; both fold into a single header explainer icon now. Its tooltip + // content is portalled on open, so assert the trigger exists rather than + // the (now hidden) footer string. + expect( + (html.match(/aria-label="How this was computed"/g) ?? []).length, + ).toBeGreaterThanOrEqual(1); + // The two old inline footnote rows are gone from the static markup. + expect(html).not.toContain("day-to-day pairs tested"); }); }); diff --git a/src/components/insights/mood/mood-correlation-cards.tsx b/src/components/insights/mood/mood-correlation-cards.tsx index 1f5983161..f0366ff3a 100644 --- a/src/components/insights/mood/mood-correlation-cards.tsx +++ b/src/components/insights/mood/mood-correlation-cards.tsx @@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; import { useTranslations } from "@/lib/i18n/context"; import type { CorrelationResult } from "@/lib/analytics/correlations"; +import { MoodExplainerIcon } from "./mood-explainer-icon"; /** * v1.8.5 — mood × metric correlation cards (Apple "Life Factors" parity). @@ -96,9 +97,21 @@ function MoodCorrelationCard({ {t(TITLE_KEY[kind])} {hasResult && data.result && ( - - {t(STRENGTH_KEY[data.result.strength])} - +
+ + {t(STRENGTH_KEY[data.result.strength])} + + {/* v1.12.4 (C4) — the "{n} paired days · r" line sat under the + scatter as a half-empty row. Move it into an explainer icon + in the header so the card footprint stays tight. */} + +
)} @@ -117,12 +130,6 @@ function MoodCorrelationCard({ yAxis={{ dataKey: "y", name: t(Y_LABEL_KEY[kind]) }} height={180} /> -

- {t("insights.mood.correlation.source", { - n: data.n, - r: data.result.r.toFixed(2), - })} -

) : ( -

- {t("insights.mood.discovery.title")} -

+
+

+ {t("insights.mood.discovery.title")} +

+ {/* v1.12.4 (C3) — the false-discovery footer + observational + disclaimer were two full-width footnote rows. Fold both into a + single explainer icon so the explanation is one hover/focus away + instead of a low-density block under the list. */} + +

{t("insights.mood.discovery.description")}

@@ -108,28 +121,25 @@ export function MoodDiscoveredRelations() { data-mood-role={moodIsOutcome ? "outcome" : "behaviour"} data-direction={up ? "up" : "down"} > - - {t(sentenceKey, { factor: factorLabel })} - - - {t("insights.mood.discovery.stat", { - n: pair.n, - r: pair.r.toFixed(2), - q: pair.qValue.toFixed(3), - })} + {/* v1.12.4 (C4) — the {n} paired days · r · q line was a + low-density sub-row under each sentence. Move the numeric + detail behind an explainer icon on the same line as the + finding so the list stays one row per pair. */} + + {t(sentenceKey, { factor: factorLabel })} + ); })} -

- {t("insights.mood.discovery.footer", { - tested: data.pairsTested, - })} -

-

- {t("insights.mood.discovery.disclaimer")} -

); } diff --git a/src/components/insights/mood/mood-explainer-icon.tsx b/src/components/insights/mood/mood-explainer-icon.tsx new file mode 100644 index 000000000..8ccc0a35d --- /dev/null +++ b/src/components/insights/mood/mood-explainer-icon.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Info } from "lucide-react"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +/** + * v1.12.4 (C3/C4) — compact explainer affordance for the mood surface. + * + * Replaces the low-density inline "how this was computed" text rows + * (paired-day counts, the false-discovery footer, the observational + * disclaimer) with a single ring/info icon. The detail moves into a + * hover/focus tooltip so the surface stops spending a full-width row on a + * footnote. The trigger is a real focusable `button` carrying an + * `aria-label`, so the explanation is reachable by keyboard and screen + * readers — not hover-only. Self-contained `TooltipProvider` so callers + * don't need an ambient one. + */ +export function MoodExplainerIcon({ + label, + detail, + className, +}: { + /** Accessible name for the trigger (what the icon explains). */ + label: string; + /** The explanation shown in the tooltip. */ + detail: string; + className?: string; +}) { + return ( + + + + + + {detail} + + + + ); +} diff --git a/src/components/insights/mood/mood-narrative-feed.tsx b/src/components/insights/mood/mood-narrative-feed.tsx index ce2f2444c..2e53c899d 100644 --- a/src/components/insights/mood/mood-narrative-feed.tsx +++ b/src/components/insights/mood/mood-narrative-feed.tsx @@ -64,8 +64,12 @@ export function MoodNarrativeFeed({ items }: { items: MoodNarrativeItem[] }) { if (items.length === 0) return null; return ( + // v1.12.4 (C2) — the takeaways are short one-liners, so a single + // full-width column wasted horizontal space and stacked into a long + // ladder. Lay them two-up on anything wider than a phone; a lone + // takeaway still spans the row rather than orphaning half of it.
    @@ -82,7 +86,7 @@ export function MoodNarrativeFeed({ items }: { items: MoodNarrativeItem[] }) { return (