mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Keep the phone shell on a phone held sideways
A phone in landscape is 844 to 932 px wide, past md, so it got the desktop rail and top bar: a sliver of content and no thumb-reachable navigation. Two variants now decide the shell: shell-desktop from md up except on a touch screen at most 520 px tall, shell-mobile otherwise. The sidebar, top bar, bottom nav, the main column's bottom clearance, the Coach button, the selection bar and the two full-height Coach pages all key on them instead of a bare md.
This commit is contained in:
1 parent
a0c5ab6591
commit
b53782810f
10 files changed
+42
-13
No files matched your search
@@ -331,7 +331,7 @@ export default function CoachConversationsPage() {
|
||||
// ~50px on notched devices; restored to match exactly. v1.42 — the
|
||||
// shell now takes that inset above the top bar at every width, so
|
||||
// the desktop height subtracts it too, and `/coach/plans` matches.
|
||||
className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))]"
|
||||
className="bg-background shell-desktop:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))] -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6"
|
||||
>
|
||||
<CoachConversationsBody readOnly={!coach.available} />
|
||||
</div>
|
||||
|
||||
@@ -296,7 +296,7 @@ export default function CoachPlansPage() {
|
||||
// mobile-only BottomNav band). The status-bar inset of an installed
|
||||
// app comes off as well, at every width: the shell takes it above
|
||||
// the top bar (`shell-safe-area`).
|
||||
className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))]"
|
||||
className="bg-background shell-desktop:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))] -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6"
|
||||
>
|
||||
<CoachPlansBody readOnly={!coach.available} />
|
||||
</div>
|
||||
|
||||
@@ -58,6 +58,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* `shell-desktop:` / `shell-mobile:` — which app shell the viewport gets.
|
||||
* The desktop rail (sidebar, no bottom nav) from `md` up, EXCEPT a touch
|
||||
* screen at most 520 px tall: a phone held sideways is 844–932 px wide but
|
||||
* only 390–430 px tall, and the rail plus the top bar left it a sliver of
|
||||
* content and no thumb-reachable navigation. It keeps the phone shell (top
|
||||
* bar, bottom nav, capture button) instead. Everything that switches
|
||||
* between the two shells keys on these, never on a bare `md:`, so the bars
|
||||
* cannot disagree about which shell is showing.
|
||||
*/
|
||||
@custom-variant shell-desktop {
|
||||
@media (min-width: 48rem) and (pointer: fine),
|
||||
(min-width: 48rem) and (pointer: none),
|
||||
(min-width: 48rem) and (min-height: 521px) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant shell-mobile {
|
||||
@media (max-width: 47.98rem), (pointer: coarse) and (max-height: 520px) {
|
||||
@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
|
||||
|
||||
@@ -82,7 +82,7 @@ export function SelectionActionBar({
|
||||
// an `sm:` drop sat the bar behind the still-visible nav 640-767px.
|
||||
// z-30 keeps the bar above page content without needing to beat the
|
||||
// (now-cleared) nav's z-50.
|
||||
className="bg-card border-border sticky bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] z-30 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg md:bottom-4"
|
||||
className="bg-card border-border sticky bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] z-30 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg shell-desktop:bottom-4"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
|
||||
@@ -81,8 +81,10 @@ describe("<LayoutCoachFab>", () => {
|
||||
expect(html).toContain(
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]",
|
||||
);
|
||||
// The bottom offset follows the shell, not a width: a phone held
|
||||
// sideways keeps the bottom nav, so the FAB keeps clearing it there.
|
||||
expect(html).toContain(
|
||||
"md:bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)]",
|
||||
"shell-desktop:bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)]",
|
||||
);
|
||||
expect(html).toContain('data-keyboard-hide=""');
|
||||
expect(html).not.toContain("md:bottom-6");
|
||||
|
||||
@@ -246,7 +246,7 @@ export function LayoutCoachFab() {
|
||||
// phone held sideways (844 px, past `md`) has its notch on one
|
||||
// side, and an installed iPad its home indicator along the bottom.
|
||||
"fixed right-[calc(env(safe-area-inset-right,0px)+1.5rem)] z-40 size-14 rounded-full shadow-lg md:right-[calc(env(safe-area-inset-right,0px)+2rem)]",
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] md:bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)]",
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] shell-desktop:bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)]",
|
||||
// Dark glyph on the purple/pink gradient — white sat at
|
||||
// ≈2.3:1 against the gradient midpoint; the background token
|
||||
// reads ≈6.5:1.
|
||||
|
||||
@@ -458,7 +458,7 @@ export function AuthShell({
|
||||
error after the user already filled in a form. */}
|
||||
{demoMode ? <DemoBanner /> : null}
|
||||
<MaintainershipBanner />
|
||||
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||
<div className="shell-desktop:flex-row flex min-h-0 flex-1 flex-col">
|
||||
<SidebarNav />
|
||||
{/* `min-w-0` lets the content column shrink below its children's
|
||||
intrinsic min width (e.g. the measurements table); without it
|
||||
@@ -497,7 +497,7 @@ export function AuthShell({
|
||||
// for all of them at once instead of asking each new
|
||||
// absolutely-positioned child to remember its own wrapper.
|
||||
className={cn(
|
||||
"relative flex-1 overflow-y-auto pb-[calc(4rem+env(safe-area-inset-bottom,0px))] md:pb-0",
|
||||
"shell-desktop:pb-0 relative flex-1 overflow-y-auto pb-[calc(4rem+env(safe-area-inset-bottom,0px))]",
|
||||
fullBleed ? "flex flex-col" : "[scrollbar-gutter:stable]",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -208,7 +208,7 @@ export function BottomNav() {
|
||||
aria-label={t("nav.mobileNavigation")}
|
||||
data-slot="bottom-nav"
|
||||
data-keyboard-hide=""
|
||||
className="bg-card border-border fixed bottom-0 left-0 z-50 min-h-16 w-full border-t pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] backdrop-blur-md md:hidden"
|
||||
className="bg-card border-border shell-desktop:hidden fixed bottom-0 left-0 z-50 min-h-16 w-full border-t pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] backdrop-blur-md"
|
||||
>
|
||||
<div className="mx-auto flex h-16 max-w-lg items-stretch justify-around px-1">
|
||||
{primaryLeft.map(renderPrimary)}
|
||||
@@ -274,7 +274,7 @@ export function BottomNav() {
|
||||
// The sheet auto-sizes to its content; padding-bottom respects
|
||||
// the iOS safe area so the inner buttons stay reachable above
|
||||
// the home-indicator on iPhone-X-class devices.
|
||||
className="mx-auto max-w-md rounded-t-xl pb-[calc(env(safe-area-inset-bottom)+1rem)] md:hidden"
|
||||
className="shell-desktop:hidden mx-auto max-w-md rounded-t-xl pb-[calc(env(safe-area-inset-bottom)+1rem)]"
|
||||
data-testid="bottom-nav-more-sheet"
|
||||
>
|
||||
<SheetHeader>
|
||||
|
||||
@@ -681,7 +681,7 @@ export function SidebarNav() {
|
||||
<aside
|
||||
aria-label={t("nav.sidebar")}
|
||||
className={cn(
|
||||
"bg-sidebar border-sidebar-border relative hidden h-full flex-shrink-0 overflow-hidden border-r transition-[width] duration-200 motion-reduce:transition-none md:flex md:flex-col",
|
||||
"bg-sidebar border-sidebar-border shell-desktop:flex shell-desktop:flex-col relative hidden h-full flex-shrink-0 overflow-hidden border-r transition-[width] duration-200 motion-reduce:transition-none",
|
||||
collapsed ? "w-16" : "w-64",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -97,7 +97,10 @@ 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 min-h-11 items-center gap-2 md:hidden">
|
||||
<Link
|
||||
href="/"
|
||||
className="shell-desktop:hidden flex min-h-11 items-center gap-2"
|
||||
>
|
||||
<Logo className="text-primary" size={20} />
|
||||
<span className="font-bold tracking-tight">HealthLog</span>
|
||||
</Link>
|
||||
@@ -105,7 +108,7 @@ export function TopBar() {
|
||||
{/* Desktop: the page's context, where a page gives one (the Coach's
|
||||
trail); otherwise an empty spacer. User controls are in the
|
||||
sidebar. */}
|
||||
<TopBarContextOutlet className="hidden min-w-0 flex-1 items-center md:flex" />
|
||||
<TopBarContextOutlet className="shell-desktop:flex hidden min-w-0 flex-1 items-center" />
|
||||
|
||||
{/* Page-owned actions (the Coach's conversations toggle). `ml-auto`
|
||||
keeps them at the trailing edge, right before the mobile avatar
|
||||
@@ -117,7 +120,7 @@ export function TopBar() {
|
||||
<TopBarActionsOutlet className="ml-auto flex shrink-0 items-center gap-1 empty:hidden md:-mr-3" />
|
||||
|
||||
{/* Mobile-only auth section (desktop uses sidebar user section) */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<div className="shell-desktop:hidden flex items-center gap-2">
|
||||
{isLoading ? (
|
||||
<Skeleton className="bg-muted h-4 w-20 rounded" />
|
||||
) : user ? (
|
||||
|
||||
Reference in new issue
Block a user