@@ -1111,11 +1144,18 @@ export function MeasurementList({
)}
{Icon && (
- navigateForType(m.type)}
+ aria-label={t("measurements.openMetricInsights", {
+ type: TYPE_LABEL_KEYS[m.type]
+ ? t(TYPE_LABEL_KEYS[m.type])
+ : m.type,
+ })}
+ className={`focus-visible:ring-ring/50 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:outline-none ${TYPE_COLORS[m.type] ?? ""}`}
>
-
+
)}
{/* v1.4.27 MB7 / CF-76 — bump the metadata
diff --git a/src/lib/insights/__tests__/sub-page-metric-reverse.test.ts b/src/lib/insights/__tests__/sub-page-metric-reverse.test.ts
new file mode 100644
index 000000000..ffaa085f0
--- /dev/null
+++ b/src/lib/insights/__tests__/sub-page-metric-reverse.test.ts
@@ -0,0 +1,53 @@
+import { describe, it, expect } from "vitest";
+
+import {
+ TYPE_TO_SUB_PAGE_SLUG,
+ subPageSlugForType,
+} from "@/lib/insights/sub-page-metric";
+
+/**
+ * v1.22 — the measurements list lets a user click a metric type to drill
+ * into its Insights sub-page. The reverse map underneath must resolve an
+ * ambiguous type to its dedicated single-metric page, never a multi-metric
+ * cluster page, and must stay silent for types with no card so the caller
+ * falls back to the filtered list.
+ */
+describe("type → insights sub-page slug", () => {
+ it("prefers the dedicated single-metric page over a multi-metric one", () => {
+ // PULSE lives on both `pulse` (=[PULSE]) and `blood-pressure`
+ // (=[SYS, DIA, PULSE]) — the single-metric page wins.
+ expect(subPageSlugForType("PULSE")).toBe("pulse");
+ // WEIGHT lives on both `weight` (=[WEIGHT]) and the derived `bmi`
+ // (=[WEIGHT]) — the first single-metric page in slug order wins.
+ expect(subPageSlugForType("WEIGHT")).toBe("weight");
+ });
+
+ it("falls back to the multi-metric page for its component types", () => {
+ expect(subPageSlugForType("BLOOD_PRESSURE_SYS")).toBe("blood-pressure");
+ expect(subPageSlugForType("BLOOD_PRESSURE_DIA")).toBe("blood-pressure");
+ // Both HRV flavours only appear on the `hrv` page.
+ expect(subPageSlugForType("HEART_RATE_VARIABILITY")).toBe("hrv");
+ expect(subPageSlugForType("HRV_RMSSD")).toBe("hrv");
+ });
+
+ it("maps a plain single-metric type to its page", () => {
+ expect(subPageSlugForType("ACTIVITY_STEPS")).toBe("steps");
+ expect(subPageSlugForType("MOOD")).toBe("mood");
+ expect(subPageSlugForType("BLOOD_GLUCOSE")).toBe("blood-glucose");
+ });
+
+ it("returns undefined for a type with no sub-page", () => {
+ expect(subPageSlugForType("BODY_FAT")).toBeUndefined();
+ expect(subPageSlugForType("RECOVERY_SCORE")).toBeUndefined();
+ expect(subPageSlugForType("NOT_A_REAL_TYPE")).toBeUndefined();
+ });
+
+ it("never resolves a type to an empty-metric (event-driven) page", () => {
+ // `workouts` and `medications` carry no measurement series, so no type
+ // should ever resolve to them.
+ for (const slug of Object.values(TYPE_TO_SUB_PAGE_SLUG)) {
+ expect(slug).not.toBe("workouts");
+ expect(slug).not.toBe("medications");
+ }
+ });
+});
diff --git a/src/lib/insights/sub-page-metric.ts b/src/lib/insights/sub-page-metric.ts
index 8c6421aa9..73967b7da 100644
--- a/src/lib/insights/sub-page-metric.ts
+++ b/src/lib/insights/sub-page-metric.ts
@@ -321,6 +321,52 @@ export const SUB_PAGE_MANAGER_GROUP_SLUGS: Record<
]),
) as unknown as Record;
+/**
+ * v1.22 — reverse lookup: a measurement type → the routed insights sub-page
+ * slug that focuses it, when one exists.
+ *
+ * Built from {@link SUB_PAGE_METRIC}, preferring the slug whose metric list
+ * is exactly `[type]` so an ambiguous type resolves to its dedicated
+ * single-metric page rather than a multi-metric cluster: `PULSE` → `pulse`
+ * (not `blood-pressure`), `WEIGHT` → `weight` (not the derived `bmi`). When
+ * two single-metric pages list the same type (`weight` + `bmi` both list
+ * `WEIGHT`) the first in `SUB_PAGE_SLUGS` order wins (`weight`). A type that
+ * only ever appears on a multi-metric page (the two blood-pressure
+ * components, the two HRV flavours) falls back to that page. Types with no
+ * sub-page at all (BODY_FAT, RECOVERY_SCORE, the `*_EVENT` series, most
+ * device scores) are absent — the caller falls back to the filtered
+ * measurements list.
+ */
+export const TYPE_TO_SUB_PAGE_SLUG: Readonly> =
+ (() => {
+ const map: Record = {};
+ // Pass 1 — exact single-metric pages win (first occurrence in slug order).
+ for (const slug of SUB_PAGE_SLUGS) {
+ const metrics = SUB_PAGE_METRIC[slug];
+ if (metrics.length !== 1) continue;
+ const type = metrics[0];
+ if (!(type in map)) map[type] = slug;
+ }
+ // Pass 2 — multi-metric pages fill any type a single-metric page misses.
+ for (const slug of SUB_PAGE_SLUGS) {
+ const metrics = SUB_PAGE_METRIC[slug];
+ if (metrics.length === 1) continue;
+ for (const type of metrics) {
+ if (!(type in map)) map[type] = slug;
+ }
+ }
+ return map;
+ })();
+
+/**
+ * The insights sub-page slug for a measurement type, or `undefined` when no
+ * routed card focuses it. See {@link TYPE_TO_SUB_PAGE_SLUG} for the
+ * ambiguity-resolution rule.
+ */
+export function subPageSlugForType(type: string): SubPageSlug | undefined {
+ return TYPE_TO_SUB_PAGE_SLUG[type];
+}
+
/**
* Mother-page route. Kept here as a named constant so call sites
* never have to hard-code the string.