From 887c79ec865f74ca6e3a416a8266d87ca71413ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 2 Jul 2026 00:00:37 +0200 Subject: [PATCH] refactor(ui): unify page headers and surface read failures Add a PageHeader primitive (one H1 vocabulary, optional description, back-link, and action slots) and route the hand-rolled headers through it, so every module reads with the same title/subtitle rhythm and no icon beside the page title. Mental Wellbeing gains a visible header. Add a QueryErrorCard and wire the list surfaces so a failed read shows a retry card instead of falling through to the empty state, and swap their full-page spinners for skeletons that hold the layout. --- .../__tests__/dashboard-hero-wiring.test.ts | 2 +- src/app/achievements/page.tsx | 31 ++- src/app/coach/conversations/page.tsx | 15 +- src/app/labs/page.tsx | 122 ++++++------ src/app/measurements/page.tsx | 27 +-- src/app/mood/page.tsx | 76 ++++---- src/app/notifications/page.tsx | 35 ++-- src/components/cycle/cycle-view.tsx | 22 +-- src/components/dashboard/dashboard-header.tsx | 182 +++++++++--------- src/components/illness/illness-view.tsx | 104 ++++------ src/components/labs/lab-list.tsx | 12 +- .../measurements/measurement-list.tsx | 14 +- .../mental-health/mental-wellbeing.tsx | 15 +- src/components/mood/mood-list.tsx | 32 ++- src/components/ui/page-header.tsx | 43 +++++ src/components/ui/query-error-card.tsx | 59 ++++++ 16 files changed, 413 insertions(+), 378 deletions(-) create mode 100644 src/components/ui/page-header.tsx create mode 100644 src/components/ui/query-error-card.tsx diff --git a/src/app/__tests__/dashboard-hero-wiring.test.ts b/src/app/__tests__/dashboard-hero-wiring.test.ts index 898830436..c5f562cd0 100644 --- a/src/app/__tests__/dashboard-hero-wiring.test.ts +++ b/src/app/__tests__/dashboard-hero-wiring.test.ts @@ -74,7 +74,7 @@ describe("dashboard hero — header handoff", () => { // prop gates the paragraph, and the `min-h-5` line-box reservation // keeps the header stable through the post-hydration name swap. expect(header).toContain("showGreeting"); - expect(header).toMatch(/\{showGreeting\s*\?/); + expect(header).toMatch(/\{\s*showGreeting\s*\?/); expect(header).toContain("min-h-5"); expect(header).toContain('data-slot="dashboard-header-greeting"'); }); diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx index 40a60c63f..6e4c2fae1 100644 --- a/src/app/achievements/page.tsx +++ b/src/app/achievements/page.tsx @@ -32,6 +32,7 @@ import { import { Progress } from "@/components/ui/progress"; import { Badge } from "@/components/ui/badge"; import { EmptyState } from "@/components/ui/empty-state"; +import { PageHeader } from "@/components/ui/page-header"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { formatDate } from "@/lib/format"; @@ -293,12 +294,10 @@ export default function AchievementsPage() { if (!isAuthenticated) { return (
-

- {t("achievements.title")} -

-

- {t("achievements.loginRequired")} -

+
); } @@ -313,20 +312,14 @@ export default function AchievementsPage() { return (
-
-
-

+ {t("achievements.title")} -

-

- {t("achievements.subtitle")} -

-
-
-
+ + } + description={t("achievements.subtitle")} + />
diff --git a/src/app/coach/conversations/page.tsx b/src/app/coach/conversations/page.tsx index dfa955015..c2677c2d7 100644 --- a/src/app/coach/conversations/page.tsx +++ b/src/app/coach/conversations/page.tsx @@ -6,6 +6,7 @@ import { MessagesSquare, Search, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { PageHeader } from "@/components/ui/page-header"; import { COACH_SCROLLBAR } from "@/components/insights/coach-panel/message-thread"; import { useCoachConversations, @@ -122,13 +123,13 @@ function CoachConversationsBody() { return (
-

-

+ + {t("insights.coach.historyTitle")} + + } + />
); } @@ -146,14 +145,10 @@ export default function NotificationsPage() { if (isError) { return (
-
-

- {t("notifications.title")} -

-

- {t("notifications.subtitle")} -

-
+
-
-

- {t("notifications.title")} -

-

- {t("notifications.subtitle")} -

-
+
diff --git a/src/components/cycle/cycle-view.tsx b/src/components/cycle/cycle-view.tsx index 7489fd3c1..7016b61cd 100644 --- a/src/components/cycle/cycle-view.tsx +++ b/src/components/cycle/cycle-view.tsx @@ -6,6 +6,7 @@ import { Loader2, Plus, RefreshCw, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { PageHeader } from "@/components/ui/page-header"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; @@ -125,19 +126,10 @@ export function CycleView() { return (
-
-
-

- {t("cycle.title")} -

-

- {t("cycle.subtitle")} -

-
-
+ {t("cycle.title")}} + description={t("cycle.subtitle")} + actions={ -
-
+ } + /> {/* Desktop: ring (left) + tabs (right). Single column below lg. */}
diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx index 72d0b7540..7d686cac7 100644 --- a/src/components/dashboard/dashboard-header.tsx +++ b/src/components/dashboard/dashboard-header.tsx @@ -22,11 +22,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { PageHeader } from "@/components/ui/page-header"; import { useTranslations } from "@/lib/i18n/context"; import { useAuth } from "@/hooks/use-auth"; import { useMounted } from "@/hooks/use-mounted"; import { getHourForTimeZone } from "@/components/dashboard/range-display"; -import { cn } from "@/lib/utils"; import type { QuickEntryDialog } from "@/components/dashboard/quick-entry-sheets"; export function DashboardHeader({ @@ -68,106 +68,102 @@ export function DashboardHeader({ : t("dashboard.welcomeBack", { greeting: timeGreeting }); return ( - /* With the greeting the title block is two lines on narrow - viewports: centre the buttons against it on mobile, top-align on - sm+ (the pre-hero posture). Without the greeting the single-line - centre alignment stands. */ -
-
-

- {t("dashboard.title")} -

- {showGreeting ? ( + {welcomeText} -

- ) : null} -
-
- {/* Customize shortcut to the dashboard-customization settings - (the tile/layout editor at /settings/dashboard). Sits to - the left of the add button as a monochrome ghost icon, with - a `min-h-11 min-w-11` mobile floor so it meets the 44 px - touch-target contract the add button also honours, shrinking - back to the 40 px icon footprint on sm+. */} - - - - {/* v1.4.33 maintainer-item-7 — restore proportional sizing - across viewports. The v1.4.27 fix pinned a `size="sm" - min-h-11` combo to hit WCAG 2.5.5's 44 px touch-target - contract on mobile, but `size="sm"` is h-8 (32 px) and - the `min-h-11` override stretched the cap vertically - while keeping the small horizontal padding — the - button read as klobig on Pixel 5. Switch to - `size="default"` (h-10 = 40 px) on mobile with a - responsive `min-h-11 sm:min-h-9` so the button is - 44 px tall under finger pressure and shrinks back to - the desktop-friendly 36 px on `sm:` upwards. The icon - + label keep the same visual contract. */} - - - - {/* Menu items must each carry a self-contained verb-phrase - ("Log measurement", "Log mood") — the trigger above already - says "Add", and the icon is `aria-hidden`, so the visible - text is the only thing distinguishing the rows. v1.4.15 - phase-A3 fix #1 hardened this with a unit guard at - `src/app/__tests__/quick-add-labels.test.ts` — both labels - must differ from each other AND from `common.add`. */} - onQuickEntry("measurement")}> - - onQuickEntry("mood")}> - - {/* v1.4.37 W7b — third quick-add row: medication intake. - Same Sheet-on-mobile / Dialog-on-desktop primitive as - the other two; the menu label is a self-contained - verb-phrase so it doesn't collide with the trigger or - the sibling rows (cf. quick-add-labels.test.ts). */} - onQuickEntry("medicationIntake")}> - - - -
-
+