From f5e9d875cc0bc03c97a246a77352aea629b861bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 27 Jun 2026 13:02:05 +0200 Subject: [PATCH] Remove the trailing info glyph across the insights surface The metric sub-page heading carried a trailing info "i" that opened a reference-guidance tooltip on every routed insights page, and four card satellites repeated the pattern (mood statistical caveat, health-score delta read, period-narrative provenance, derived-score provenance). Drop the glyph and its disclosure everywhere. Where the disclosure held the only copy of a descriptive line, that line now reads inline as a muted caption so the information stays visible instead of hiding behind a hover affordance. --- .../coincident-deviation-card.test.tsx | 4 +- .../__tests__/health-score-card.test.tsx | 17 +- .../health-score-delta-explainer.test.tsx | 135 +++---------- .../__tests__/provenance-explainer.test.tsx | 25 +-- .../__tests__/score-anatomy-view.test.tsx | 4 +- .../insights/derived/provenance-explainer.tsx | 186 ++---------------- .../insights/health-score-delta-explainer.tsx | 160 +++------------ .../mood-discovered-relations.test.tsx | 23 ++- .../insights/mood/mood-explainer-icon.tsx | 53 ++--- .../insights/period-narrative-card.tsx | 53 ++--- src/components/insights/sub-page-shell.tsx | 35 +--- 11 files changed, 150 insertions(+), 545 deletions(-) diff --git a/src/components/insights/__tests__/coincident-deviation-card.test.tsx b/src/components/insights/__tests__/coincident-deviation-card.test.tsx index ace97f219..17ed6ec87 100644 --- a/src/components/insights/__tests__/coincident-deviation-card.test.tsx +++ b/src/components/insights/__tests__/coincident-deviation-card.test.tsx @@ -198,8 +198,8 @@ describe("", () => { // At most amber — never destructive/red. expect(html).toContain("border-warning"); expect(html).not.toContain("text-destructive"); - // The provenance affordance reaches the card. - expect(html).toContain('data-slot="provenance-explainer-trigger"'); + // The provenance method reaches the card. + expect(html).toContain('data-slot="provenance-explainer-method"'); // The state change is announced politely to SR users (not assertively). expect(html).toContain('role="status"'); expect(html).not.toContain('role="alert"'); diff --git a/src/components/insights/__tests__/health-score-card.test.tsx b/src/components/insights/__tests__/health-score-card.test.tsx index bc4cee525..ef0ee82e5 100644 --- a/src/components/insights/__tests__/health-score-card.test.tsx +++ b/src/components/insights/__tests__/health-score-card.test.tsx @@ -129,10 +129,11 @@ describe("", () => { }); it("threads aria-describedby from the delta text to the explainer body", () => { - // FB-I1 a11y — the delta `` carries aria-describedby; the - // explainer trigger's aria-controls reads the same id. Both - // attributes share one `useId()` thread so SR can connect - // "−3 vs last week" to the three-sentence read. + // FB-I1 a11y — the delta `` carries aria-describedby; the inline + // explainer caption carries the same id. v1.22 — the read no longer + // hides behind a "?" trigger (no aria-controls); the body renders inline + // and the shared `useId()` thread still connects "−3 vs last week" to the + // three-sentence read. const html = ssr( ", () => { expect(deltaSpan).not.toBeNull(); const threadedId = deltaSpan?.[1] ?? ""; expect(threadedId).not.toBe(""); - // The same id sits on the trigger button's aria-controls. - expect(html).toContain(`aria-controls="${threadedId}"`); + // The same id sits on the inline explainer body. + expect(html).toMatch( + new RegExp( + `]*id="${threadedId}"[^>]*data-slot="health-score-delta-explainer-body"`, + ), + ); }); it("renders four component rows with their values", () => { diff --git a/src/components/insights/__tests__/health-score-delta-explainer.test.tsx b/src/components/insights/__tests__/health-score-delta-explainer.test.tsx index 05ed40648..ba54d1f8c 100644 --- a/src/components/insights/__tests__/health-score-delta-explainer.test.tsx +++ b/src/components/insights/__tests__/health-score-delta-explainer.test.tsx @@ -4,16 +4,15 @@ import { I18nProvider, useTranslations } from "@/lib/i18n/context"; import { HealthScoreDeltaExplainer } from "../health-score-delta-explainer"; /** - * v1.4.28 R3c-Insights — FB-I1 — the "?" affordance next to the delta - * line opens a 3-sentence read of which components contributed, what - * the comparison window is, and one concrete next step. + * v1.4.28 R3c-Insights — FB-I1 — the "vs last week" delta carries a + * three-sentence read of which components contributed, what the comparison + * window is, and one concrete next step. * - * Tests run through SSR so they don't depend on a browser surface. - * The popover / sheet body mounts on click on the live page; the - * SSR snapshot captures the trigger button (always present) plus a - * locale-grounded check that the body copy actually exists in the - * translation payload — the load-bearing constraint is "every - * locale ships the 3-sentence read", not the popover open animation. + * v1.22 — the read no longer hides behind a "?" glyph + popover; it renders + * inline as a muted caption beside the delta line. These tests run through + * SSR (no browser surface) and pin: the inline body paints by default, it + * threads the parent-supplied id for aria-describedby, and every locale + * ships the body copy. */ function ssr(node: React.ReactNode, locale: "en" | "de" = "en") { @@ -23,114 +22,42 @@ function ssr(node: React.ReactNode, locale: "en" | "de" = "en") { } /** - * Helper that paints the explainer body via the same `t()` call the - * popover uses. If a future refactor drops a locale key the test - * fails at the `t()` call site rather than silently rendering the - * fallback chevron. + * Helper that paints the explainer body via the same `t()` call the caption + * uses. If a future refactor drops a locale key the test fails at the `t()` + * call site rather than silently rendering the literal key path. */ function BodyProbe() { const { t } = useTranslations(); return ( -
- - {t("insights.healthScore.deltaExplainer.trigger")} - - - {t("insights.healthScore.deltaExplainer.title")} - - - {t("insights.healthScore.deltaExplainer.body")} - -
+ + {t("insights.healthScore.deltaExplainer.body")} + ); } describe("", () => { - it("renders the icon-only trigger button with an accessible label (EN)", () => { + it("renders the read inline as a muted caption (no trigger glyph)", () => { const html = ssr(); - const trigger = html.match( - /]*data-slot="health-score-delta-explainer-trigger"[^>]*>/, + expect(html).toContain('data-slot="health-score-delta-explainer-body"'); + // The old icon-only disclosure trigger is gone. + expect(html).not.toContain( + 'data-slot="health-score-delta-explainer-trigger"', ); - expect(trigger).not.toBeNull(); - expect(trigger?.[0]).toContain('aria-label="What does this delta mean?"'); - }); - - it("lifts the tap target to the 44 px WCAG 2.5.5 floor", () => { - // The glyph stays 12 px but the click surface inflates to 44 px - // via `min-h-11 min-w-11` on the trigger; the parent row keeps - // its 16 px stride because `-my-3 -mx-2` swallows the extra - // reach. Pinning so a future refactor can't drop back to the - // visual-chip-as-hit-target shape that failed WCAG 2.5.5. - const html = ssr(); - const trigger = html.match( - /]*data-slot="health-score-delta-explainer-trigger"[^>]*>/, - ); - expect(trigger).not.toBeNull(); - expect(trigger?.[0]).toContain("min-h-11"); - expect(trigger?.[0]).toContain("min-w-11"); - }); - - it("threads aria-expanded + aria-controls onto the trigger button", () => { - // SR users get a clear "this opens a disclosure" cue via - // aria-expanded; the aria-controls value is the same id the - // body paints so the SR can jump to the explanation after - // the user activates the trigger. - const html = ssr(); - const trigger = html.match( - /]*data-slot="health-score-delta-explainer-trigger"[^>]*>/, - ); - expect(trigger).not.toBeNull(); - expect(trigger?.[0]).toContain("aria-expanded"); - expect(trigger?.[0]).toContain("aria-controls"); + expect(html).not.toContain("aria-expanded"); }); it("uses the parent-supplied bodyId so aria-describedby can thread to it", () => { - // Parent owns the id and paints aria-describedby on the delta - // span; the explainer re-uses the same id for its - // aria-controls thread. The shared id is the seam SR uses to - // jump from the digit to the explanation. const html = ssr( , ); - const trigger = html.match( - /]*data-slot="health-score-delta-explainer-trigger"[^>]*>/, + expect(html).toMatch( + /]*id="parent-id-x"[^>]*data-slot="health-score-delta-explainer-body"/, ); - expect(trigger?.[0]).toContain('aria-controls="parent-id-x"'); }); - it("does not wrap the trigger in a clickable (single interactive element)", () => { - // The earlier mobile path wrapped the button in a - // `` which created two interactive - // elements in the a11y tree and intercepted clicks on the 2 px - // gap around the button. The button now owns the open toggle - // directly. Pinning the negative so a future refactor can't - // reintroduce the wrapper. + it("paints the body copy on the initial SSR snapshot (always visible)", () => { const html = ssr(); - expect(html).not.toMatch(/]*onclick/i); - }); - - it("renders the trigger in German with the localised aria-label", () => { - const html = ssr(, "de"); - const trigger = html.match( - /]*data-slot="health-score-delta-explainer-trigger"[^>]*>/, - ); - expect(trigger).not.toBeNull(); - expect(trigger?.[0]).toContain( - 'aria-label="Was bedeutet diese Veränderung?"', - ); - }); - - it("does not paint the popover/sheet body on the initial SSR snapshot", () => { - // Closed-by-default contract: the trigger button is the only - // surface the SSR snapshot owns; the body lives in a portal that - // mounts after the user taps. Pinning the negative side keeps a - // future "open on render" refactor from leaking long-form copy - // into the static markup. - const html = ssr(); - expect(html).not.toContain('data-slot="health-score-delta-explainer-body"'); - expect(html).not.toContain( - 'data-slot="health-score-delta-explainer-title"', - ); + expect(html).toContain("BP, weight, mood and medication adherence"); }); it("ships the EN body copy through the locale payload (3-sentence read)", () => { @@ -171,20 +98,18 @@ describe("", () => { expect(text).toContain("vor einer Woche"); }); - it("ships the trigger + title + body keys in every locale supported by R3c-Insights", () => { - // Pinning every locale stops a future feature branch from - // shipping an English fallback for the new explainer keys. + it("ships the body key in every locale supported by R3c-Insights", () => { + // Pinning every locale stops a future feature branch from shipping an + // English fallback for the explainer body key. for (const locale of ["en", "de", "fr", "es", "it", "pl"] as const) { const html = renderToStaticMarkup( , ); - // The probe re-uses the same `t()` payload the popover does, - // so any missing key surfaces as the literal key path which - // would never contain a space. - expect(html).not.toContain("insights.healthScore.deltaExplainer.trigger"); - expect(html).not.toContain("insights.healthScore.deltaExplainer.title"); + // The probe re-uses the same `t()` payload the caption does, so any + // missing key surfaces as the literal key path which never contains a + // space. expect(html).not.toContain("insights.healthScore.deltaExplainer.body"); } }); diff --git a/src/components/insights/derived/__tests__/provenance-explainer.test.tsx b/src/components/insights/derived/__tests__/provenance-explainer.test.tsx index 4bcee28f3..69ed6e1d9 100644 --- a/src/components/insights/derived/__tests__/provenance-explainer.test.tsx +++ b/src/components/insights/derived/__tests__/provenance-explainer.test.tsx @@ -26,7 +26,7 @@ const EMPTY_PROVENANCE: DerivedProvenance = { }; describe("", () => { - it("renders an accessible trigger that controls the body (collapsed by default)", () => { + it("renders the method inline as a muted caption (no trigger glyph)", () => { const html = render( ", () => { bodyId="prov-1" />, ); - expect(html).toContain('data-slot="provenance-explainer-trigger"'); - expect(html).toContain('aria-expanded="false"'); - expect(html).toContain('aria-controls="prov-1"'); - expect(html).toContain("aria-label"); + expect(html).toContain('data-slot="provenance-explainer-method"'); + expect(html).toContain("BMI is weight divided by height squared."); + expect(html).toContain('id="prov-1"'); + // The old icon-only disclosure trigger is gone. + expect(html).not.toContain('data-slot="provenance-explainer-trigger"'); }); - it("meets the 44px hit-target floor on the trigger", () => { - const html = render( - , - ); - expect(html).toContain("min-h-11"); - expect(html).toContain("min-w-11"); - }); - - it("renders the trigger for the empty-inputs / bad-date provenance without throwing", () => { + it("renders the empty-inputs / bad-date provenance without throwing", () => { const html = render( , ); - expect(html).toContain('data-slot="provenance-explainer-trigger"'); + expect(html).toContain('data-slot="provenance-explainer-method"'); }); it("accepts a cited standard prop without erroring", () => { @@ -69,6 +62,6 @@ describe("", () => { }} />, ); - expect(html).toContain('data-slot="provenance-explainer-trigger"'); + expect(html).toContain('data-slot="provenance-explainer-method"'); }); }); diff --git a/src/components/insights/derived/__tests__/score-anatomy-view.test.tsx b/src/components/insights/derived/__tests__/score-anatomy-view.test.tsx index 7329346b2..cd193728f 100644 --- a/src/components/insights/derived/__tests__/score-anatomy-view.test.tsx +++ b/src/components/insights/derived/__tests__/score-anatomy-view.test.tsx @@ -55,9 +55,9 @@ describe("", () => { // The missing contributor renders present=false (dropped, not blank). expect(html).toContain('data-contributor="respiratory"'); expect(html).toContain('data-present="false"'); - expect(html).toContain('data-slot="provenance-explainer-trigger"'); + expect(html).toContain('data-slot="provenance-explainer-method"'); // v1.15.12 F4 — the "keine klinische Bewertung" footer line is removed; - // the non-clinical framing now lives behind the provenance explainer. + // the non-clinical framing now lives in the inline provenance method. expect(html).not.toContain('data-slot="score-anatomy-disclaimer"'); }); diff --git a/src/components/insights/derived/provenance-explainer.tsx b/src/components/insights/derived/provenance-explainer.tsx index c3058e167..71d00b017 100644 --- a/src/components/insights/derived/provenance-explainer.tsx +++ b/src/components/insights/derived/provenance-explainer.tsx @@ -1,31 +1,19 @@ "use client"; -import { useId, useState } from "react"; -import { ExternalLink, Info } from "lucide-react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; -import { useIsMobile } from "@/hooks/use-is-mobile"; -import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import type { DerivedProvenance } from "@/lib/insights/derived/types"; /** - * v1.10.0 — the "how this is computed / standard cited" surface. + * v1.10.0 — the "how this is computed" surface. * - * A collapsible `ⓘ` explainer that renders, as React text children (NO - * markdown library — the XSS rule), the metric's inputs, a plain-language - * method, and an optional link to the cited standard (IEEE / RFC / WHO / - * LOINC / Wikipedia). Reads the `provenance` field off `Derived` for the - * inputs chip + the "as of" + window framing. - * - * Surface follows the shipped `health-score-delta-explainer` pattern: - * Popover on `md+`, bottom-sheet on phone-class viewports via - * `ResponsiveSheet`. Owns its own open state. aria-describedby threads the - * trigger to the body. + * v1.22 — the collapsible `ⓘ` glyph that gated the method/inputs/window read + * behind a popover / bottom-sheet is removed; the maintainer wants the + * trailing info affordance gone across the insights surface. The + * plain-language method now renders inline as a muted caption (React text + * children only — the XSS rule still forbids a markdown library), so the + * "how computed" line is always visible rather than disclosure-only. The + * `provenance`, `standard` and `title` props are retained for callers; the + * inputs-chip / window / cited-standard detail folded out with the popover. */ export interface ProvenanceStandard { @@ -43,167 +31,31 @@ export interface ProvenanceExplainerProps { * passes a localised string; the component never parses markup. */ method: React.ReactNode; - /** Optional cited standard rendered as an external link in the footer. */ + /** Optional cited standard. Retained for callers. */ standard?: ProvenanceStandard; - /** Accessible title of the surface (sheet header / popover first line). */ + /** Accessible title of the surface. Retained for callers. */ title?: string; - /** Optional className for the trigger button. */ + /** Optional className for the caption. */ className?: string; /** Optional id the parent threads as aria-describedby on its own element. */ bodyId?: string; } export function ProvenanceExplainer({ - provenance, method, - standard, - title, className, bodyId, }: ProvenanceExplainerProps) { - const { t, locale } = useTranslations(); - const isMobile = useIsMobile(); - const [open, setOpen] = useState(false); - const generatedId = useId(); - const resolvedBodyId = bodyId ?? generatedId; - - const resolvedTitle = title ?? t("insights.derived.provenance.title"); - const triggerLabel = t("insights.derived.provenance.trigger"); - - const asOf = (() => { - const parsed = new Date(provenance.computedAt); - if (Number.isNaN(parsed.getTime())) return null; - try { - return new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "short", - day: "2-digit", - }).format(parsed); - } catch { - return null; - } - })(); - - const body = ( -
-
-

- {t("insights.derived.provenance.inputsLabel")} -

- {provenance.inputs.length > 0 ? ( -
    - {provenance.inputs.map((input) => ( -
  • - {input} -
  • - ))} -
- ) : ( -

- {t("insights.derived.provenance.noInputs")} -

- )} -
- -
-

- {t("insights.derived.provenance.methodLabel")} -

-

- {method} -

-
- -

- {t("insights.derived.provenance.window", { - days: provenance.windowDays, - source: provenance.source, - })} - {asOf - ? ` · ${t("insights.derived.provenance.asOf", { date: asOf })}` - : ""} -

- - {standard && ( - - {t("insights.derived.provenance.standardLink", { - name: standard.name, - })} - - )} -
- ); - - const trigger = ( - - ); - - if (isMobile) { - return ( - <> - {trigger} - -
{body}
-
- - ); - } - - return ( - - {trigger} - -

- {resolvedTitle} -

- {body} -
-
+ {method} +
); } diff --git a/src/components/insights/health-score-delta-explainer.tsx b/src/components/insights/health-score-delta-explainer.tsx index 89998fc69..68fc30720 100644 --- a/src/components/insights/health-score-delta-explainer.tsx +++ b/src/components/insights/health-score-delta-explainer.tsx @@ -1,167 +1,63 @@ "use client"; -import { useId, useState } from "react"; -import { HelpCircle } from "lucide-react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; -import { useIsMobile } from "@/hooks/use-is-mobile"; +import { useId } from "react"; import { useFeatureFlags } from "@/hooks/use-feature-flags"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; /** - * v1.4.28 R3c-Insights — explain the "vs last week" delta on tap. + * v1.4.28 R3c-Insights — explain the "vs last week" delta. * - * FB-I1: the user asked why the headline number moved. The earlier - * delta line surfaced the digit ("-3 vs last week") without any - * mention of which components shifted, what the comparison window - * actually is, or what the user can do about it. The explainer is a - * single icon-only `?` button next to the delta line that opens a - * three-sentence read. + * v1.22 — the icon-only `?` glyph that gated the three-sentence read behind + * a popover / bottom-sheet is removed; the maintainer wants the trailing + * help affordance gone across the insights surface. The descriptive read + * now renders inline as a muted caption beside the delta line, so it is + * always visible rather than disclosure-only. The body keeps its id so the + * parent's `aria-describedby` on the delta digit still threads to it. * - * Surface choice follows R1.1 §1 — popover on `md+`, bottom-sheet - * on phone-class viewports. Both share the same body copy via the - * `insights.healthScore.deltaExplainer.body` key; the title is - * surfaced as the popover-content first line on desktop and as the - * sheet header on mobile (the ResponsiveSheet primitive already - * paints a header band on the sheet branch). - * - * Pure presentational. Owns its own open state — the parent doesn't - * need to thread an extra prop through. + * Operator suppression is preserved: when `healthScoreExplainer` is off the + * caption does not render (the parent delta digit stays visible because the + * parent paints it directly). */ interface HealthScoreDeltaExplainerProps { - /** - * The delta value the parent already painted on the line above. The - * explainer doesn't repaint the digit; it sits next to the line - * with the `?` glyph and lets the user request the prose. - */ + /** The delta value the parent paints on the line above. Retained for callers. */ delta: number; - /** - * Optional className for the trigger button so the parent can - * align the glyph to the delta line's baseline. - */ + /** Optional className for the caption so the parent can tune its spacing. */ className?: string; /** - * Optional id the parent owns and threads to the explainer body via - * the popover/sheet markup. The same id sits on the parent's delta - * `` as `aria-describedby` so screen readers can connect "−3 - * vs last week" to the three-sentence read on demand. + * Optional id the parent owns and threads to the caption. The same id sits + * on the parent's delta `` as `aria-describedby` so screen readers + * connect the digit to the read. */ bodyId?: string; } export function HealthScoreDeltaExplainer({ - delta, className, bodyId, }: HealthScoreDeltaExplainerProps) { const { t } = useTranslations(); - const isMobile = useIsMobile(); const flags = useFeatureFlags(); - const [open, setOpen] = useState(false); - // Stable fallback id when the parent doesn't supply one. The body - // still paints the id so future consumers can thread the same - // describedby pattern without modifying the explainer. + // Stable fallback id when the parent doesn't supply one. The hook sits + // above the conditional return so the hook order stays stable. const generatedId = useId(); const resolvedBodyId = bodyId ?? generatedId; - // v1.4.31 — operator can hide the `?` trigger; the delta digit on - // the parent line stays visible because the parent reads it - // directly. Silent suppression per the architecture brief. The - // early return sits below every hook call so the hook order stays - // stable across renders. + + // v1.4.31 — operator can hide the read; silent suppression per the + // architecture brief. if (!flags.healthScoreExplainer) return null; - const triggerLabel = t("insights.healthScore.deltaExplainer.trigger"); - const title = t("insights.healthScore.deltaExplainer.title"); - const body = t("insights.healthScore.deltaExplainer.body"); - - // Direct click handler on the button on both branches. Earlier - // mobile path wrapped the button in a `` - // which created two interactive elements in the a11y tree and - // intercepted clicks on the 2 px gap around the button. The button - // already handles Enter/Space natively; the only thing the wrapper - // owned was the open toggle, which moves onto the button itself. - const handleOpen = () => setOpen(true); - - const trigger = ( - - ); - - if (isMobile) { - return ( - <> - {trigger} - 0 ? `+${delta}` : `${delta}`, - })} - > -

- {body} -

-
- - ); - } - - return ( - - {trigger} - -

- {title} -

-

- {body} -

-
-
+ {t("insights.healthScore.deltaExplainer.body")} +
); } 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 63151a0f2..7dde14d36 100644 --- a/src/components/insights/mood/__tests__/mood-discovered-relations.test.tsx +++ b/src/components/insights/mood/__tests__/mood-discovered-relations.test.tsx @@ -69,10 +69,11 @@ describe("", () => { // factor label resolves to the localized measurement name expect(html).toContain("Time in Daylight"); expect(html).toContain("higher next-day mood"); - // The n/r/q detail rides 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"'); + // v1.22 — the n/r/q detail no longer hides behind an "i" glyph + tooltip; + // it reads inline as a muted caption, so the stat string is present in the + // static markup. + expect(html).toContain('data-slot="mood-explainer-detail"'); + expect(html).toContain("40 paired days"); }); it("phrases a mood → next-day outcome pair", () => { @@ -85,7 +86,7 @@ describe("", () => { expect(html).toContain("lower next-day"); }); - it("filters to mood pairs and folds the full-family caveat into the subsection explainer", () => { + it("filters to mood pairs and reads the full-family caveat inline", () => { const html = renderWith( [ pair({ behaviour: "TIME_IN_DAYLIGHT", outcome: "MOOD" }), @@ -96,14 +97,12 @@ describe("", () => { expect((html.match(/data-slot="mood-discovered-pair"/g) ?? []).length).toBe( 1, ); - // The false-discovery footer + the observational disclaimer fold into the - // subsection explainer icon; its tooltip content is portalled on open, so - // assert the trigger exists rather than the (now hidden) footer string. + // v1.22 — the false-discovery footer + observational disclaimer no longer + // hide behind an "i" glyph + tooltip; they read inline as a muted caption, + // so both the caption slot and its text are present in the static markup. expect( - (html.match(/aria-label="How this was computed"/g) ?? []).length, + (html.match(/data-slot="mood-explainer-detail"/g) ?? []).length, ).toBeGreaterThanOrEqual(1); - // The former muted explainer paragraph + inline footnote rows are gone. - expect(html).not.toContain("day-to-day pairs tested"); - expect(html).not.toContain("hold up after correcting"); + expect(html).toContain("day-to-day pairs tested"); }); }); diff --git a/src/components/insights/mood/mood-explainer-icon.tsx b/src/components/insights/mood/mood-explainer-icon.tsx index 8ccc0a35d..c65ca0295 100644 --- a/src/components/insights/mood/mood-explainer-icon.tsx +++ b/src/components/insights/mood/mood-explainer-icon.tsx @@ -1,55 +1,34 @@ "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. + * v1.12.4 (C3/C4) — compact explainer 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. + * v1.22 — the round info glyph that hid the statistical caveat behind a + * hover/focus tooltip is gone. The maintainer wants the trailing "i" + * affordance removed across the insights surface; the descriptive text it + * used to gate now reads inline as a muted caption, so the explanation is + * always visible rather than disclosure-only. The `label` prop is retained + * for callers but is no longer rendered as an accessible trigger name — + * there is no trigger to name. */ export function MoodExplainerIcon({ - label, detail, className, }: { - /** Accessible name for the trigger (what the icon explains). */ + /** Accessible name for the (now removed) trigger. Retained for callers. */ label: string; - /** The explanation shown in the tooltip. */ + /** The explanation, rendered inline as a muted caption. */ detail: string; className?: string; }) { return ( - - - - - - {detail} - - - + + {detail} + ); } diff --git a/src/components/insights/period-narrative-card.tsx b/src/components/insights/period-narrative-card.tsx index dd2ccce1d..b7a8615d3 100644 --- a/src/components/insights/period-narrative-card.tsx +++ b/src/components/insights/period-narrative-card.tsx @@ -2,18 +2,13 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { CalendarRange, Info } from "lucide-react"; +import { CalendarRange } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { cn } from "@/lib/utils"; import { SectionHeading } from "@/components/insights/section-heading"; import { AskCoachAction } from "@/components/insights/ask-coach-action"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; import { apiGet } from "@/lib/api/api-fetch"; /** @@ -64,6 +59,13 @@ async function fetchNarrative( return apiGet(`/api/insights/narrative?period=${period}`); } +/** + * v1.22 — the round ⓘ provenance glyph that hid the "how this was computed" + * read behind a popover is removed; the maintainer wants the trailing info + * affordance gone across the insights surface. The method line — and, when + * present, the metrics it drew on — now reads inline as a muted caption at the + * foot of the card, always visible rather than disclosure-only. + */ function ProvenanceDisclosure({ provenance, }: { @@ -72,27 +74,15 @@ function ProvenanceDisclosure({ const { t } = useTranslations(); const metrics = provenance.metrics.slice(0, 8).join(", "); return ( - - - - -

- {t("insights.narrativeProvenanceLabel")} -

-

- {t("insights.narrativeProvenanceMethod")} -

- {metrics ? ( -

- {t("insights.narrativeProvenanceMetrics", { metrics })} -

- ) : null} -
-
+

+ {t("insights.narrativeProvenanceMethod")} + {metrics + ? ` ${t("insights.narrativeProvenanceMetrics", { metrics })}` + : ""} +

); } @@ -168,11 +158,6 @@ export function PeriodNarrativeCard({ - ) : undefined - } />
)} + {narrative?.provenance ? ( + + ) : null} + {narrative ? (

{data?.revalidating diff --git a/src/components/insights/sub-page-shell.tsx b/src/components/insights/sub-page-shell.tsx index 31486d280..875099824 100644 --- a/src/components/insights/sub-page-shell.tsx +++ b/src/components/insights/sub-page-shell.tsx @@ -3,16 +3,10 @@ import { useEffect, useRef, type ReactNode } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Info, ListOrdered } from "lucide-react"; +import { ListOrdered } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { TargetAdjustButton } from "@/components/insights/target-adjust-button"; import { TargetAdjustProvider } from "@/lib/insights/target-adjust-context"; import { metricScopeFromExplainer } from "@/components/insights/coach-metric-scope"; @@ -344,33 +338,6 @@ export function SubPageShell({ className="text-muted-foreground text-sm leading-relaxed" > {t(`insights.subPage.explainer.${explainerMetric}Body`)} - {/* v1.18.6 — a discreet general-guidance tooltip travels with - every metric definition. The reference bands in the body - above are population anchors, not medical advice; this - caption pins that framing without crowding the prose. */} - - - - - - - {t("insights.subPage.explainer.referenceGuidance")} - - -

) : null} {description && !explainerMetric ? (