From b76351e1a870fdef2b24740570b1e40a5be65127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 02:01:15 +0200 Subject: [PATCH] feat(insights): polished summary typography with inline-chart sparklines Bumps the InsightAdvisorCard summary surface to text-base / leading- relaxed and constrains it to max-w-prose so the prose stays readable on a wide viewport. Wraps the existing chart-token renderer in a new mini flag so the summary's inline charts shrink to sparkline form (h-[140px], no range tabs); per-finding inline charts keep the full chart so the user can still interact with the data series. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/insight-advisor-card.test.tsx | 44 +++++++++++++++++++ .../insights/insight-advisor-card.tsx | 38 ++++++++++++---- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/components/insights/__tests__/insight-advisor-card.test.tsx b/src/components/insights/__tests__/insight-advisor-card.test.tsx index 33d722cc4..b7ecdd98e 100644 --- a/src/components/insights/__tests__/insight-advisor-card.test.tsx +++ b/src/components/insights/__tests__/insight-advisor-card.test.tsx @@ -177,3 +177,47 @@ describe(" — inline chart tokens", () => { expect(html).toContain("Weight stable"); }); }); + +describe(" — summary typography polish (B1b)", () => { + it("paints the summary slot with a constrained max-width for readability", () => { + const html = render( + buildInsight({ + summary: "Your BP averaged 132/84 over the last 30 days.", + }), + ); + expect(html).toMatch(/data-slot="insight-summary"/); + // The summary slot carries a Tailwind max-w-* class so prose + // doesn't stretch full-bleed on a wide viewport. + expect(html).toMatch(/max-w-(prose|2xl|3xl)/); + }); + + it("uses the polished prose typography (text-base, leading-relaxed)", () => { + const html = render( + buildInsight({ + summary: "All metrics within target.", + }), + ); + // A larger font + relaxed line-height for the page-anchored + // summary; the per-card content uses text-sm. + expect(html).toMatch( + /data-slot="insight-summary"[^>]*class="[^"]*text-base/, + ); + expect(html).toMatch( + /data-slot="insight-summary"[^>]*class="[^"]*leading-relaxed/, + ); + }); + + it("renders summary inline charts as mini sparklines (data-mini=true)", () => { + const html = render( + buildInsight({ + summary: "BP averaging 132/84 metric:BLOOD_PRESSURE_SYS today", + }), + ); + // The summary's inline-chart wrapper must carry `data-mini="true"` + // so the chart shrinks to sparkline form. Per-finding charts use + // the default detail mode. + expect(html).toMatch( + /data-slot="insight-summary"[\s\S]*?data-mini="true"/, + ); + }); +}); diff --git a/src/components/insights/insight-advisor-card.tsx b/src/components/insights/insight-advisor-card.tsx index 29a3ea29f..36753b40e 100644 --- a/src/components/insights/insight-advisor-card.tsx +++ b/src/components/insights/insight-advisor-card.tsx @@ -229,7 +229,19 @@ const INLINE_CHART_TITLE_KEYS: Record = { COMPLIANCE: "insights.navMedication", }; -function InlineCharts({ tokens }: { tokens: ChartToken[] }) { +function InlineCharts({ + tokens, + mini = false, +}: { + tokens: ChartToken[]; + /** + * v1.4.16 phase B1b — render the embedded charts as compact + * sparklines (no range tabs, h-[140px]). The summary surface uses + * this so the prose stays the headline; the finding-card surface + * keeps the full chart so the user can interact with the data. + */ + mini?: boolean; +}) { const { t } = useTranslations(); if (tokens.length === 0) return null; @@ -255,11 +267,12 @@ function InlineCharts({ tokens }: { tokens: ChartToken[] }) { key={token} data-slot="insight-inline-chart" data-metric={metric} + data-mini={mini ? "true" : "false"} > {kind === "mood" ? ( - + ) : ( - + )} ); @@ -431,12 +444,19 @@ export function InsightAdvisorCard({ {/* Summary — prose first, then any allowlisted charts the model * inlined via `metric:` tokens. Tokens are dropped from the - * visible text so the literal substring never reaches the DOM. */} -
-

- {stripChartTokens(insight.summary)} -

- + * visible text so the literal substring never reaches the DOM. + * + * v1.4.16 phase B1b — typography polish: larger font (text-base), + * relaxed line-height, max-w-prose so the summary doesn't span + * full-bleed on a wide viewport. Wraps the inline-chart strip + * in the same constrained column so the rendered sparklines + * align with the prose. */} +
+

{stripChartTokens(insight.summary)}

+
{/* Findings — top finding gets a hero treatment, rest are a compact list */}