mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(ui): link the adherence tile straight to the medication wizard and drop dead rail rules
The empty adherence tile linked to the retired /medications/new route, which only redirects. The sidebar's short-touch rules keyed on a touch screen at most 520 px tall, where the phone shell hides the rail, so they could never apply.
This commit is contained in:
1 parent
c1144b3c4c
commit
04084dd6e2
4 files changed
+61
-16
No files matched your search
@@ -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<string, string> = {
|
||||
"/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([]);
|
||||
});
|
||||
});
|
||||
@@ -564,7 +564,7 @@ export function MedicationComplianceChart({
|
||||
action={
|
||||
canWriteDomain("medications") ? (
|
||||
<Button asChild size="sm">
|
||||
<Link href="/medications/new">
|
||||
<Link href="/medications?new=1">
|
||||
{t("charts.medicationsEmptyAction")}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -559,3 +559,16 @@ describe("<SidebarNav> width transition never overflows the nav", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<SidebarNav> 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\)/);
|
||||
});
|
||||
});
|
||||
@@ -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<height<=820px)]:min-h-9 [@media(hover:hover)_and_(pointer:fine)_and_(height<=740px)]:min-h-8";
|
||||
const RAIL_LIST_GAP = "space-y-1 [@media(max-height:940px)]:space-y-0";
|
||||
const RAIL_SHORT_PY = "[@media(max-height:940px)]:py-1.5";
|
||||
|
||||
// Literal class strings: Tailwind only generates what it can read whole.
|
||||
const RAIL_SHORT_TOUCH_COLUMN =
|
||||
"[@media(pointer:coarse)_and_(height<=520px)]:overflow-y-auto [@media(pointer:coarse)_and_(height<=520px)]:[scrollbar-width:none] [@media(pointer:coarse)_and_(height<=520px)]:[&::-webkit-scrollbar]:hidden [@media(pointer:coarse)_and_(height<=520px)]:[&>*]: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,
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user