feat(nav): add the center capture action and More hub, move unit preference into the profile

Reshape the mobile bottom bar to the five-slot shape the native client
uses: Home · Meds · Log · Insights · More. The center "Log" slot is a
capture action rather than a destination — it opens a picker that routes
to the existing measurement, medication-intake and mood quick-entry
sheets (those forms already share a mount contract, so the picker reuses
them verbatim). Expand the former overflow into a real "More" hub that
holds the rest of the top-level destinations: Measurements, Mood,
Workouts, Achievements, Notifications and Settings.

The change is additive — Measurements and Mood leave the always-visible
strip but stay reachable through the More hub and the capture picker, so
nothing loses its route. On desktop the sidebar still lists every
destination.

Move the metric/imperial unit system from its standalone card into a
dropdown in the Profile form beside the timezone picker — it is a
display preference like language and timezone. Persistence is unchanged:
the dropdown PATCHes /api/auth/me/unit-preference on change and
invalidates the same query keys, so chart display transforms re-render
without a reload. Retire the now-unused card.
This commit is contained in:
Marc-André Bombeck committed 2026-06-05 00:08:07 +02:00
1 parent 488cc09ae9
commit 280bf42382
15 files changed
+716 -491

No files matched your search

+46
View File
@@ -172,4 +172,50 @@ test.describe("mobile-viewport smoke", () => {
`Touch-targets below 44 px tall:\n ${failures.join("\n ")}`,
).toEqual([]);
});
test("center capture action opens the picker; the More hub keeps Mood + Measurements reachable", async ({
page,
}) => {
await mockDashboardSnapshot(page, { summaries: WEIGHT_ONLY_SUMMARIES });
await page.route(/\/api\/analytics(\?|$)/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { summaries: {}, bpInTargetPct: null, glucoseByContext: {} },
error: null,
}),
}),
);
await page.route("**/api/mood/analytics", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { entries: [], summary: { count: 0 } },
error: null,
}),
}),
);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.waitForLoadState("networkidle");
// 1) The center capture action opens the capture picker.
await page.getByTestId("bottom-nav-capture").click();
await expect(page.getByTestId("capture-picker-options")).toBeVisible();
for (const kind of ["measurement", "medication", "mood"]) {
await expect(page.getByTestId(`capture-picker-${kind}`)).toBeVisible();
}
// Dismiss the picker before opening the hub.
await page.keyboard.press("Escape");
// 2) The More hub keeps Mood + Measurements reachable (additive
// middle-path — they left the strip but are NOT orphaned).
await page.getByTestId("bottom-nav-more").click();
const hub = page.getByTestId("bottom-nav-more-sheet");
await expect(hub).toBeVisible();
await expect(hub.locator("a[href='/measurements']")).toBeVisible();
await expect(hub.locator("a[href='/mood']")).toBeVisible();
});
});