mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Give touch screens 44 px targets through the primitives
A pointer-coarse variant sits beside the pointer-fine one. Under it the default, small and icon buttons, tab triggers, the sidebar rail rows and the phone logo link reach the 44 px touch floor; a mouse keeps the compact desktop density. The rail's height steps now apply to a mouse only. A phone held sideways crosses the md breakpoint at about 390 px of height, where the fixed footer left the list three rows: there the whole rail scrolls as one column, without a native scrollbar.
This commit is contained in:
1 parent
492c2a4131
commit
3a20f23f0f
5 files changed
+37
-8
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<height<=820px)]:min-h-9 [@media(height<=740px)]:min-h-8";
|
||||
"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(
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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 */}
|
||||
<Link href="/" className="flex items-center gap-2 md:hidden">
|
||||
<Link href="/" className="flex min-h-11 items-center gap-2 md:hidden">
|
||||
<Logo className="text-primary" size={20} />
|
||||
<span className="font-bold tracking-tight">HealthLog</span>
|
||||
</Link>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/75 hover:text-foreground dark:text-tab-inactive dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/75 hover:text-foreground dark:text-tab-inactive dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none pointer-coarse:min-h-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
|
||||
Reference in new issue
Block a user