diff --git a/src/app/globals.css b/src/app/globals.css index e2f619321..63361cc9f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -46,6 +46,18 @@ } } +/* + * `pointer-coarse:` — a finger is the primary pointer. The control + * primitives (Button, tab triggers, the sidebar rail rows) grow their hit + * area to the 44 px touch floor under it, so a phone or a touch tablet gets + * reachable targets while a mouse keeps the compact desktop density. + */ +@custom-variant pointer-coarse { + @media (pointer: coarse) { + @slot; + } +} + @theme inline { /* Extra-small breakpoint (30rem / 480px) for the narrowest phones — the default scale starts at sm (640px), which leaves a gap between an iPhone SE diff --git a/src/components/layout/sidebar-nav.tsx b/src/components/layout/sidebar-nav.tsx index 2e2bd4af9..755adc995 100644 --- a/src/components/layout/sidebar-nav.tsx +++ b/src/components/layout/sidebar-nav.tsx @@ -334,11 +334,20 @@ 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. const RAIL_ROW_HEIGHT = - "min-h-10 [@media(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( @@ -689,6 +698,11 @@ 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, )} > @@ -729,6 +743,7 @@ 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, )} diff --git a/src/components/layout/top-bar.tsx b/src/components/layout/top-bar.tsx index 1f767fe9f..a751fc9c1 100644 --- a/src/components/layout/top-bar.tsx +++ b/src/components/layout/top-bar.tsx @@ -97,7 +97,7 @@ export function TopBar() { // inside its own 4rem, a 59 px inset left the bar a 5 px content box. > {/* Mobile logo */} - + HealthLog diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index bcf43539b..07a1b5fb3 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -46,13 +46,13 @@ const buttonVariants = cva( link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-10 px-4 py-2 has-[>svg]:px-3", + default: "h-10 px-4 py-2 has-[>svg]:px-3 pointer-coarse:min-h-11", xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 pointer-coarse:min-h-11 pointer-coarse:min-w-11", lg: "h-11 rounded-md px-6 has-[>svg]:px-4", - icon: "size-10", + icon: "size-10 pointer-coarse:min-h-11 pointer-coarse:min-w-11", "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", + "icon-sm": "size-8 pointer-coarse:min-h-11 pointer-coarse:min-w-11", "icon-lg": "size-11", }, }, diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 8cdc30378..4aadf57fb 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -47,8 +47,10 @@ function Tabs({ // Padding sits at `p-1` (4 px) so the 3 px focus-visible ring // (`focus-visible:ring-[3px]`) on triggers stays inside the // `overflow-y-hidden` clamp without clipping the bottom of the ring. +// Under a coarse pointer the strip sizes to its triggers, which take the +// 44 px touch floor; a mouse keeps the compact 36 px strip. const tabsListVariants = cva( - "rounded-lg p-1 group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit max-w-full items-center justify-center overflow-x-auto overflow-y-hidden touch-pan-x scrollbar-thin group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col group-data-[orientation=vertical]/tabs:overflow-x-visible group-data-[orientation=vertical]/tabs:overflow-y-visible", + "rounded-lg p-1 group-data-[orientation=horizontal]/tabs:h-9 pointer-coarse:group-data-[orientation=horizontal]/tabs:h-auto data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit max-w-full items-center justify-center overflow-x-auto overflow-y-hidden touch-pan-x scrollbar-thin group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col group-data-[orientation=vertical]/tabs:overflow-x-visible group-data-[orientation=vertical]/tabs:overflow-y-visible", { variants: { variant: { @@ -86,7 +88,7 @@ function TabsTrigger({