diff --git a/src/app/insights/scores/[metric]/page.tsx b/src/app/insights/scores/[metric]/page.tsx
index 3e3c82932..fb417c319 100644
--- a/src/app/insights/scores/[metric]/page.tsx
+++ b/src/app/insights/scores/[metric]/page.tsx
@@ -1,11 +1,9 @@
"use client";
-import Link from "next/link";
import { useParams } from "next/navigation";
-import { ArrowLeft } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
-import { Button } from "@/components/ui/button";
+import { BackLink } from "@/components/ui/back-link";
import { SubPageShell } from "@/components/insights/sub-page-shell";
import {
CompositeScoreAnatomy,
@@ -88,18 +86,11 @@ export default function CompositeScorePage() {
description={t(header.description)}
explainerMetric={EXPLAINER_METRIC[metric]}
backLink={
-
+
}
coachLaunch
>
diff --git a/src/app/insights/values/[type]/page.tsx b/src/app/insights/values/[type]/page.tsx
index ca7c75b6d..e54f9868c 100644
--- a/src/app/insights/values/[type]/page.tsx
+++ b/src/app/insights/values/[type]/page.tsx
@@ -1,9 +1,7 @@
"use client";
import { use } from "react";
-import Link from "next/link";
import { notFound, useSearchParams } from "next/navigation";
-import { ArrowLeft } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import {
@@ -12,7 +10,7 @@ import {
} from "@/lib/insights/values-back-link";
import { measurementTypeEnum } from "@/lib/validations/measurement";
import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/components/measurements/measurement-list-meta";
-import { Button } from "@/components/ui/button";
+import { BackLink } from "@/components/ui/back-link";
import { MeasurementList } from "@/components/measurements/measurement-list";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -63,22 +61,17 @@ export default function InsightsMetricValuesPage({
title={t("insights.subPage.valuesPageTitle", { metric: metricLabel })}
description={t("insights.subPage.valuesPageDescription")}
backLink={
-
+ : t("insights.subPage.valuesBack")
+ }
+ dataSlot="metric-values-back"
+ />
}
>
diff --git a/src/app/insights/workouts/[id]/page.tsx b/src/app/insights/workouts/[id]/page.tsx
index 74a81adf6..627a1e2f5 100644
--- a/src/app/insights/workouts/[id]/page.tsx
+++ b/src/app/insights/workouts/[id]/page.tsx
@@ -1,12 +1,10 @@
"use client";
import { use } from "react";
-import Link from "next/link";
-import { ArrowLeft } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { useWorkoutDetail } from "@/hooks/use-workouts";
-import { Button } from "@/components/ui/button";
+import { BackLink } from "@/components/ui/back-link";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { SubPageShell } from "@/components/insights/sub-page-shell";
import { Skeleton } from "@/components/ui/skeleton";
@@ -41,18 +39,11 @@ export default function InsightsWorkoutDetailPage({
title={t("insights.workouts.title")}
description={t("insights.workouts.description")}
backLink={
-
+
}
>
{isLoading ? (
diff --git a/src/app/medications/[id]/page.tsx b/src/app/medications/[id]/page.tsx
index b8f898195..ffe82a7be 100644
--- a/src/app/medications/[id]/page.tsx
+++ b/src/app/medications/[id]/page.tsx
@@ -13,16 +13,15 @@
*/
import { use, useEffect } from "react";
-import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
-import { ArrowLeft, Loader2 } from "lucide-react";
+import { Loader2 } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
-import { Button } from "@/components/ui/button";
+import { BackLink } from "@/components/ui/back-link";
import { Card } from "@/components/ui/card";
import {
MedicationDetailTabs,
@@ -87,12 +86,7 @@ export default function MedicationDetailPage({
if (isError || !medication) {
return (
-
+ ", () => {
+ it("renders an anchor to the href carrying the label", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+ expect(html).toContain('href="/insights/workouts"');
+ expect(html).toContain("Back to workouts");
+ // The decorative chevron is hidden from screen readers; the label
+ // carries the meaning.
+ expect(html).toContain('aria-hidden="true"');
+ });
+
+ it("keeps the shared left-bleed alignment on the control", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+ // The `-ml-2 w-fit` left-bleed is the standard back-nav placement; it
+ // must survive on every adopting page.
+ expect(html).toContain("-ml-2");
+ expect(html).toContain("w-fit");
+ });
+
+ it("threads the optional data-slot for e2e/visual targeting", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+ expect(html).toContain('data-slot="composite-score-back"');
+ });
+});
diff --git a/src/components/ui/back-link.tsx b/src/components/ui/back-link.tsx
new file mode 100644
index 000000000..41c71bd25
--- /dev/null
+++ b/src/components/ui/back-link.tsx
@@ -0,0 +1,47 @@
+import Link from "next/link";
+import { ArrowLeft } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+/**
+ * v1.16.16 — shared back-navigation control.
+ *
+ * The detail / sub-page surfaces (`/insights/workouts/[id]`,
+ * `/insights/values/[type]`, `/insights/scores/[metric]`,
+ * `/medications/[id]`) each hand-rolled the identical
+ * `Button(ghost, sm, -ml-2 w-fit) > Link > ArrowLeft + label` block. This
+ * folds that into one component so the back-nav reads the same — same hit
+ * area, same icon, same left-bleed alignment — on every page, and a future
+ * change to the affordance lives in one place.
+ *
+ * The label travels in from the caller (each page already owns its own
+ * translation key, e.g. `insights.workouts.detail.backToList`), so this
+ * component adds no new i18n surface.
+ */
+export interface BackLinkProps {
+ /** Internal href the back-nav points at. */
+ href: string;
+ /** Already-translated label text. */
+ label: string;
+ /** Optional per-surface `data-slot` for e2e/visual targeting. */
+ dataSlot?: string;
+ className?: string;
+}
+
+export function BackLink({ href, label, dataSlot, className }: BackLinkProps) {
+ return (
+
+ );
+}