diff --git a/src/app/page-client.tsx b/src/app/page-client.tsx index 158f85062..955a0ba96 100644 --- a/src/app/page-client.tsx +++ b/src/app/page-client.tsx @@ -25,6 +25,7 @@ import { } from "@/lib/dashboard-layout"; import type { DashboardAnalyticsData as AnalyticsData } from "@/types/analytics"; import dynamic from "next/dynamic"; +import Link from "next/link"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { ChartErrorBoundary } from "@/components/charts/chart-error-state"; @@ -131,6 +132,30 @@ import { resolveChartRowPlaceholderCount, } from "@/components/dashboard/dashboard-gates"; +/** + * Dashboard metric tiles are no longer dead-ends: each tile links to its + * Insights detail page (bands, correlations, history). Keyed by the stable + * tile `id` used in the `trendCards` builder; glucose contexts share one + * blood-glucose destination via the `glucose-` prefix. Tiles with no + * dedicated sub-page (body fat %) stay un-linked rather than misroute. + */ +const TILE_INSIGHT_HREF: Record = { + weight: "/insights/weight", + "bp-sys": "/insights/blood-pressure", + "bp-dia": "/insights/blood-pressure", + bpInTarget: "/insights/blood-pressure", + pulse: "/insights/pulse", + mood: "/insights/mood", + sleep: "/insights/sleep", + steps: "/insights/steps", + vo2Max: "/insights/cardio-fitness", +}; + +function tileInsightHref(id: string): string | null { + if (id.startsWith("glucose-")) return "/insights/blood-glucose"; + return TILE_INSIGHT_HREF[id] ?? null; +} + export default function DashboardPageClient() { const { isAuthenticated, user } = useAuth(); const mounted = useMounted(); @@ -1611,27 +1636,45 @@ export default function DashboardPageClient() { data-tour-id="dashboard-tile-strip" data-tile-count={trendCards.length} > - {trendCards.map((entry) => ( -
- {/* - * Per-tile `` boundary with a layout-stable - * placeholder that mirrors the trend-card chrome. Tile - * bodies are synchronous today so the fallback rarely - * paints, but a future RSC hoist of any tile slot - * would otherwise leave the grid track empty and - * trigger CLS as the cell paints in. - * - * v1.4.40 W-RSC — boundary added; v1.4.41 W-FRONTEND-FACTORY — fallback hoisted to layout-stable placeholder. - */} - {/* v1.16.0 — the fallback is the same structured - silhouette the tile-strip skeleton paints, so a - suspending tile slot previews the final shape - instead of an empty card. */} + {trendCards.map((entry) => { + /* + * Per-tile `` boundary with a layout-stable + * placeholder that mirrors the trend-card chrome. Tile + * bodies are synchronous today so the fallback rarely + * paints, but a future RSC hoist of any tile slot + * would otherwise leave the grid track empty and + * trigger CLS as the cell paints in. + * + * v1.16.0 — the fallback is the same structured + * silhouette the tile-strip skeleton paints, so a + * suspending tile slot previews the final shape + * instead of an empty card. + */ + const cell = ( }> {entry.node} -
- ))} + ); + // A tile is no longer a dead-end: it links to its Insights + // detail page. The link fills the grid cell and paints a + // ring on hover / focus so the whole tile reads as tappable. + const href = tileInsightHref(entry.id); + return href ? ( + + {cell} + + ) : ( +
+ {cell} +
+ ); + })} )} {charts.map((entry) => ( diff --git a/src/components/dashboard/recent-workouts-tile.tsx b/src/components/dashboard/recent-workouts-tile.tsx index a4557b5c0..ffc508ce9 100644 --- a/src/components/dashboard/recent-workouts-tile.tsx +++ b/src/components/dashboard/recent-workouts-tile.tsx @@ -15,6 +15,7 @@ import { import { useTranslations } from "@/lib/i18n/context"; import { useWorkouts, type WorkoutListEntry } from "@/hooks/use-workouts"; import { cn } from "@/lib/utils"; +import { TileHeader } from "@/components/insights/tile-header"; /** * v1.4.32 — ``. @@ -107,23 +108,23 @@ export function RecentWorkoutsTile() { "bg-card border-border space-y-3 rounded-xl border p-4 md:p-6", )} > -
-
-
- - {t("dashboard.recentWorkouts.viewAll")} - -
+ {/* Canonical tile header (foreground icon + title) — the same + `TileHeader` contract the Insights tiles use, so the dashboard + stops speaking a separate header language. `size="sm"` matches the + compact dashboard tile. */} + + {t("dashboard.recentWorkouts.viewAll")} + + } + /> {isLoading ? ( // v1.4.43 W11-L6 — reserve roughly the loaded tile height diff --git a/src/components/insights/__tests__/daily-briefing.test.tsx b/src/components/insights/__tests__/daily-briefing.test.tsx index 8db6630e4..25aa60033 100644 --- a/src/components/insights/__tests__/daily-briefing.test.tsx +++ b/src/components/insights/__tests__/daily-briefing.test.tsx @@ -95,6 +95,36 @@ describe("", () => { expect(html).toContain("Weight down 30 d"); }); + it("links the key-finding rows for metrics whose Insights sub-page exists", () => { + // Regression guard for the stale METRIC_HREF map: hrv / resting_hr / + // active_energy / flights / distance / body_temp / recovery all have + // dedicated Insights sub-pages now, so their briefing rows must be + // tappable `` links, not inert rows. + const html = render( + , + ); + expect(html).toContain('href="/insights/hrv"'); + expect(html).toContain('href="/insights/resting-pulse"'); + expect(html).toContain('href="/insights/active-energy"'); + expect(html).toContain('href="/insights/flights-climbed"'); + expect(html).toContain('href="/insights/walking-distance"'); + expect(html).toContain('href="/insights/body-temperature"'); + expect(html).toContain('href="/insights/recovery"'); + }); + it("renders the delta string when supplied", () => { const html = render(); expect(html).toMatch(/data-slot="daily-briefing-delta"/); diff --git a/src/components/insights/daily-briefing.tsx b/src/components/insights/daily-briefing.tsx index ce3da3cff..65eebed90 100644 --- a/src/components/insights/daily-briefing.tsx +++ b/src/components/insights/daily-briefing.tsx @@ -53,28 +53,28 @@ export const METRIC_HREF: Record< pulse: "/insights/pulse", mood: "/insights/mood", compliance: "/insights/medications", - // Apple Health / GLP-1 additive metrics — no dedicated sub-page yet. - // They render as static rows; v1.4.28 can wire each as they ship. - hrv: null, + // Apple Health / GLP-1 additive metrics — each now routes to its + // dedicated sub-page that shipped since the original map was written. + hrv: "/insights/hrv", sleep: "/insights/sleep", - resting_hr: null, + resting_hr: "/insights/resting-pulse", // v1.12.4 — steps has no dedicated metric sub-page, so it routes to the // generic readings view keyed to its `MeasurementType` (the same target // the category pages use to drill into a single type). Every other // briefing finding with a real destination is already tappable; steps // was the lone static row. steps: "/insights/steps", - active_energy: null, - flights: null, - distance: null, + active_energy: "/insights/active-energy", + flights: "/insights/flights-climbed", + distance: "/insights/walking-distance", vo2_max: "/insights/pulse", - body_temp: null, + body_temp: "/insights/body-temperature", glp1_plateau: "/insights/medications", // ── v1.10.0 derived-wellness additive ── - // Readiness has a score-anatomy detail page; recovery is a read-only - // ring with no dedicated sub-page yet (renders as a static row). + // Readiness has a score-anatomy detail page; recovery has its own + // read-only sub-page. readiness: "/insights/scores/readiness", - recovery: null, + recovery: "/insights/recovery", }; /** diff --git a/src/components/insights/hero-strip.tsx b/src/components/insights/hero-strip.tsx index d97dbf8b9..24570cf17 100644 --- a/src/components/insights/hero-strip.tsx +++ b/src/components/insights/hero-strip.tsx @@ -284,8 +284,11 @@ export function HeroStrip({
+ {/* Header glyph stays in the foreground tier — UI-STANDARDS §1 + reserves primary/accent for actions, not decorative header + icons. */}