From 472a4904cd7022b580aed2710318ecbd1cf35c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 9 Jul 2026 11:49:55 +0200 Subject: [PATCH 1/7] fix(nav): keep Settings and Notifications out of the mobile More hub The mobile bottom-nav More overflow appended the shared utility tail (Settings, Notifications) after the feature destinations. Those two are already reachable from the user/avatar menu, so the hub duplicated them across two menus on the same screen. Return feature destinations only; the utilities stay in the user menu. Sidebar footer is unaffected. Flip the three hub assertions to lock the feature-only contract. --- .../layout/__tests__/nav-model.test.ts | 17 ++++++++-------- src/components/layout/nav-model.ts | 20 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/layout/__tests__/nav-model.test.ts b/src/components/layout/__tests__/nav-model.test.ts index 84cef7aae..e37cbd032 100644 --- a/src/components/layout/__tests__/nav-model.test.ts +++ b/src/components/layout/__tests__/nav-model.test.ts @@ -160,8 +160,8 @@ describe("visibleNavDestinations module gate", () => { }); }); -describe("mobileMoreHubDestinations — the F-1 mobile invariant (N-2)", () => { - it("is exactly the visible feature destinations minus the primary slots, then the utility tail", () => { +describe("mobileMoreHubDestinations — the F-1 mobile invariant (feature-only hub, N-2)", () => { + it("is exactly the visible feature destinations minus the primary slots (no utility tail)", () => { const opts = { modules: { cycle: false } as const, }; @@ -170,9 +170,9 @@ describe("mobileMoreHubDestinations — the F-1 mobile invariant (N-2)", () => { const expectedFeatures = visibleNavDestinations(opts.modules) .map((d) => d.href) .filter((href) => !BOTTOM_NAV_PRIMARY_SLOT_HREFS.includes(href)); - const expectedTail = visibleUtilityDestinations().map((d) => d.href); - expect(hub).toEqual([...expectedFeatures, ...expectedTail]); + // Feature destinations only — the account utilities never ride the hub. + expect(hub).toEqual(expectedFeatures); }); it("never contains a primary slot (Home / Meds / Insights)", () => { @@ -184,7 +184,7 @@ describe("mobileMoreHubDestinations — the F-1 mobile invariant (N-2)", () => { } }); - it("keeps the feature destinations and the utility tail reachable in the hub", () => { + it("keeps the feature destinations reachable but excludes the account utilities", () => { const hub = mobileMoreHubDestinations({ modules: { cycle: false }, }).map((d) => d.href); @@ -192,9 +192,10 @@ describe("mobileMoreHubDestinations — the F-1 mobile invariant (N-2)", () => { expect(hub).toContain("/measurements"); expect(hub).toContain("/mood"); expect(hub).toContain("/checkups"); - // The shared utility tail rides at the end. - expect(hub).toContain("/settings/account"); - expect(hub).toContain("/notifications"); + // Settings + Notifications are account utilities — they live ONLY in the + // user/avatar menu, never duplicated into the mobile More hub. + expect(hub).not.toContain("/settings/account"); + expect(hub).not.toContain("/notifications"); }); it("gates Cycle by the module map, same as the sidebar", () => { diff --git a/src/components/layout/nav-model.ts b/src/components/layout/nav-model.ts index 6f27b106a..7dacefed9 100644 --- a/src/components/layout/nav-model.ts +++ b/src/components/layout/nav-model.ts @@ -300,26 +300,22 @@ export interface MobileMoreHubEntry { /** * The ordered "More" hub for the mobile bottom-nav: every visible feature - * destination that isn't a primary slot, in model order, followed by the - * shared utility tail (Settings, Notifications). The desktop sidebar renders - * the same feature list inline and consumes the same utility tail in its - * footer + avatar menu, so the two bars cannot drift into two hand-curated - * lists. + * destination that isn't a primary slot, in model order. Feature destinations + * only — the account utilities (Settings, Notifications) are NOT appended here. + * They live solely in the user/avatar menu (mobile top-bar dropdown; desktop + * sidebar avatar menu + footer), so surfacing them in the More hub too would + * duplicate a utility across two menus reachable from the same screen. The + * desktop sidebar renders the same feature list inline, so the two bars cannot + * drift into two hand-curated feature lists. */ export function mobileMoreHubDestinations(opts: { modules: ModuleVisibilityMap | undefined; /** Hydration gate — see `isNavDestinationVisible`. Defaults to mounted. */ mounted?: boolean; }): MobileMoreHubEntry[] { - const features = visibleNavDestinations(opts.modules, opts.mounted ?? true) + return visibleNavDestinations(opts.modules, opts.mounted ?? true) .filter((d) => !BOTTOM_NAV_PRIMARY_SLOT_HREFS.includes(d.href)) .map((d) => ({ href: d.href, tKey: d.tKey, icon: d.icon })); - const tail = visibleUtilityDestinations().map((d) => ({ - href: d.href, - tKey: d.tKey, - icon: d.icon, - })); - return [...features, ...tail]; } /** From 79a78162e2f4bcde045bd286b0c1cca00c47ce66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 9 Jul 2026 11:50:34 +0200 Subject: [PATCH 2/7] fix(dashboard): keep the hero verdict action beside the sentence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overdue-dose CTA sat in a flex-wrap row, so a full verdict sentence pushed the take/log button onto its own line — wasted vertical space. Adopt the PageHeader inline-action pattern: items-start justify-between, the sentence gets min-w-0 flex-1 and wraps internally, the button gets shrink-0 and stays beside the text. Tighten the greeting to verdict gap from space-y-3 to space-y-2. --- src/components/dashboard/hero/dashboard-hero.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/dashboard/hero/dashboard-hero.tsx b/src/components/dashboard/hero/dashboard-hero.tsx index 972767541..b8be17dec 100644 --- a/src/components/dashboard/hero/dashboard-hero.tsx +++ b/src/components/dashboard/hero/dashboard-hero.tsx @@ -386,7 +386,7 @@ export function DashboardHero({ drifting to the vertical centre of the taller ring column. */}
-
+
{/* Greeting leads the typographic hierarchy: larger + heavier than the verdict so the personalised line reads first. The verdict keeps its medium weight one step down — still @@ -399,13 +399,17 @@ export function DashboardHero({ > {welcomeText}

-
+ {/* Text left, action right, one row — the PageHeader/TileHeader + inline-action pattern. The sentence takes the available width and + wraps internally (`min-w-0 flex-1`); the button never wraps to its + own line (`shrink-0`), so the action stays beside the text. */} +
{sentenceHref ? ( {sentence} @@ -413,7 +417,7 @@ export function DashboardHero({

{sentence}

@@ -424,7 +428,7 @@ export function DashboardHero({ asChild size="default" variant="outline" - className="min-h-11 sm:min-h-9" + className="min-h-11 shrink-0 sm:min-h-9" data-slot="dashboard-hero-cta" > {t(ctaLabelKey)} @@ -432,7 +436,7 @@ export function DashboardHero({ ) : (