diff --git a/src/__tests__/retired-route-links-guard.test.ts b/src/__tests__/retired-route-links-guard.test.ts new file mode 100644 index 000000000..b5d11d6d5 --- /dev/null +++ b/src/__tests__/retired-route-links-guard.test.ts @@ -0,0 +1,43 @@ +/** + * Links point at a route that renders, not at a retired one that only + * redirects: every hop through a redirect is a round trip on a phone, and + * the redirect stubs exist for old bookmarks, not for the app's own links. + */ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const ROOT = join(__dirname, "..", ".."); + +/** Retired route → where it redirects. */ +const RETIRED: Record = { + "/medications/new": "/medications?new=1", +}; + +function walk(dir: string): string[] { + return readdirSync(dir).flatMap((name) => { + if (name === "__tests__" || name === "generated") return []; + const abs = join(dir, name); + if (statSync(abs).isDirectory()) return walk(abs); + return /\.tsx?$/.test(name) ? [abs] : []; + }); +} + +describe("no link targets a retired route", () => { + const files = walk(join(ROOT, "src")); + + it("scans the source tree", () => { + expect(files.length).toBeGreaterThan(100); + }); + + it.each(Object.entries(RETIRED))("%s is linked as %s", (route) => { + const pattern = new RegExp( + `(?:href|push|replace)[=(]\\s*\\{?\\s*["'\`]${route.replace(/\//g, "\\/")}["'\`?#]`, + ); + const offenders = files + .filter((file) => pattern.test(readFileSync(file, "utf8"))) + .map((file) => relative(ROOT, file)); + expect(offenders).toEqual([]); + }); +}); diff --git a/src/components/charts/medication-compliance-chart.tsx b/src/components/charts/medication-compliance-chart.tsx index 0a5e1659b..13697a4c7 100644 --- a/src/components/charts/medication-compliance-chart.tsx +++ b/src/components/charts/medication-compliance-chart.tsx @@ -564,7 +564,7 @@ export function MedicationComplianceChart({ action={ canWriteDomain("medications") ? ( diff --git a/src/components/layout/__tests__/sidebar-nav.test.tsx b/src/components/layout/__tests__/sidebar-nav.test.tsx index cbc5566a8..45500ad35 100644 --- a/src/components/layout/__tests__/sidebar-nav.test.tsx +++ b/src/components/layout/__tests__/sidebar-nav.test.tsx @@ -559,3 +559,16 @@ describe(" width transition never overflows the nav", () => { ); }); }); + +describe(" carries no rules for a viewport it never shows on", () => { + it("has no short-touch variants, which the phone shell owns", () => { + // A touch screen at most 520 px tall gets the phone shell (the + // `shell-mobile` variant), so the rail is hidden there and a class keyed + // on that query can never apply. + const source = readFileSync( + join(__dirname, "..", "sidebar-nav.tsx"), + "utf8", + ); + expect(source).not.toMatch(/pointer:coarse\)_and_\(height<=520px\)/); + }); +}); diff --git a/src/components/layout/sidebar-nav.tsx b/src/components/layout/sidebar-nav.tsx index 88031141a..68ea65357 100644 --- a/src/components/layout/sidebar-nav.tsx +++ b/src/components/layout/sidebar-nav.tsx @@ -334,20 +334,15 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) { // Disjoint height ranges, not nested max-heights: two arbitrary media // variants on one property cascade in stylesheet order, not by how narrow // the query is, so a nested pair let the 36 px step win on a 720 px window. -// The height steps apply to a mouse only. Under a finger (a touch tablet, -// a phone held sideways past the md breakpoint) the rows keep the 44 px -// touch floor and the list scrolls behind its fades instead. +// The height steps apply to a mouse only. Under a finger (a touch tablet) +// the rows keep the 44 px touch floor and the list scrolls behind its fades +// instead. A phone held sideways never sees the rail: it keeps the phone +// shell (`shell-mobile`). const RAIL_ROW_HEIGHT = "min-h-10 pointer-coarse:min-h-11 [@media(hover:hover)_and_(pointer:fine)_and_(740px*]:shrink-0"; -const RAIL_SHORT_TOUCH_NAV = - "[@media(pointer:coarse)_and_(height<=520px)]:flex-none [@media(pointer:coarse)_and_(height<=520px)]:overflow-visible"; - /** Rail row, collapsed (icon only) or expanded (icon + label). */ function railRowClass(collapsed: boolean): string { return cn( @@ -698,11 +693,6 @@ export function SidebarNav() { data-slot="sidebar-column" className={cn( "flex h-full w-full flex-col", - // A phone held sideways crosses the md breakpoint at ~390 px of - // height: the fixed footer and user rows left the list three - // rows. There the whole column scrolls as one (no native bar), - // so every entry stays reachable at the 44 px touch floor. - RAIL_SHORT_TOUCH_COLUMN, !collapsed && SIDEBAR_EXPANDED_CONTENT_WIDTH, )} > @@ -743,7 +733,6 @@ export function SidebarNav() { // the focused row clear of them. className={cn( "flex-1 scroll-py-6 [scrollbar-width:none] overflow-y-auto [&::-webkit-scrollbar]:hidden", - RAIL_SHORT_TOUCH_NAV, collapsed ? "p-1.5" : "p-3", RAIL_SHORT_PY, )}