mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(shell): read the home-indicator inset through one property
The rail, main, the bottom bar and the Coach button each read env(safe-area-inset-bottom) on their own. They now read --safe-area-bottom, which is that env() value, so the four always agree and a test can set an inset that no desktop browser reports. The app shell's root also states the content colour itself instead of leaving it to propagate from the body. A WKWebView browser on iPadOS (Edge, Chrome, Firefox) can keep the strip over the home indicator out of the page's layout and fill it with the colour it takes from the root; that strip now meets main in the same colour.
This commit is contained in:
1 parent
0e7a186677
commit
d2be2b181c
8 files changed
+57
-18
No files matched your search
@@ -612,6 +612,16 @@ body,
|
||||
* Scoping it to the body-scrolled branch leaves exactly one gutter per
|
||||
* branch.
|
||||
*/
|
||||
/*
|
||||
* The home-indicator inset, read through one property by everything that
|
||||
* owns the shell's bottom edge (the rail, `<main>`, the bottom bar, the
|
||||
* Coach button). It is `env()` everywhere it matters; a test that needs an
|
||||
* inset no desktop browser reports sets it instead
|
||||
* (`e2e/tablet-shell.spec.ts`).
|
||||
*/
|
||||
:root {
|
||||
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
html[data-scroll="body"] {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
@@ -637,6 +647,17 @@ body,
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
/*
|
||||
* The colour of anything beyond the page. A browser built on WKWebView
|
||||
* (Edge, Chrome, Firefox on iPadOS) can keep the strip over the home
|
||||
* indicator out of the page's layout and fill it with the page's
|
||||
* "under-page" colour, which it takes from the root. The root states the
|
||||
* content colour itself rather than leaving it to propagate from the body,
|
||||
* so that strip meets `<main>` in the same colour.
|
||||
*/
|
||||
html[data-scroll="shell"] {
|
||||
background-color: var(--background);
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
@@ -78,13 +78,11 @@ describe("<LayoutCoachFab>", () => {
|
||||
expect(html).toContain(
|
||||
"md:right-[calc(env(safe-area-inset-right,0px)+2rem)]",
|
||||
);
|
||||
expect(html).toContain(
|
||||
"bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]",
|
||||
);
|
||||
expect(html).toContain("bottom-[calc(var(--safe-area-bottom)+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(
|
||||
"shell-desktop:bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)]",
|
||||
"shell-desktop:bottom-[calc(var(--safe-area-bottom)+2rem)]",
|
||||
);
|
||||
expect(html).toContain('data-keyboard-hide=""');
|
||||
expect(html).not.toContain("md:bottom-6");
|
||||
|
||||
@@ -240,13 +240,13 @@ export function LayoutCoachFab() {
|
||||
// sits symmetrically in the corner. On mobile the right inset is
|
||||
// `right-6` but the bottom is deliberately larger — it clears the
|
||||
// 64 px bottom-nav plus the home-indicator safe-area inset
|
||||
// (`bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]`), so the
|
||||
// (`bottom-[calc(var(--safe-area-bottom)+5rem)]`), so the
|
||||
// gap is asymmetric there by design, not drift.
|
||||
// v1.42 — the side and bottom insets apply at every width: a
|
||||
// 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)]",
|
||||
"shell-desktop:bottom-[calc(env(safe-area-inset-bottom,0px)+2rem)] bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]",
|
||||
"shell-desktop:bottom-[calc(var(--safe-area-bottom)+2rem)] bottom-[calc(var(--safe-area-bottom)+5rem)]",
|
||||
// Dark glyph on the purple/pink gradient — white sat at
|
||||
// ≈2.3:1 against the gradient midpoint; the background token
|
||||
// reads ≈6.5:1.
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("the shell takes the window's edges", () => {
|
||||
);
|
||||
// `fixed` re-rasterises the whole app in its own layer and traps the
|
||||
// bottom bar's z-index in a stacking context.
|
||||
expect(shell).not.toContain("fixed");
|
||||
expect(shell.filter((c) => /(^|:)fixed$/.test(c))).toEqual([]);
|
||||
expect(SHELL_ROOT_CLASS).not.toMatch(
|
||||
/\b(?:min-)?h-(?:dvh|svh|lvh|screen)\b/,
|
||||
);
|
||||
@@ -64,6 +64,28 @@ describe("the shell takes the window's edges", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the home-indicator inset has one source", () => {
|
||||
it("is env() behind one property, which every owner of the bottom edge reads", () => {
|
||||
const css = readSource("src/app/globals.css", "--safe-area-bottom");
|
||||
expect(css).toMatch(
|
||||
/:root\s*\{\s*--safe-area-bottom:\s*env\(safe-area-inset-bottom,\s*0px\);/,
|
||||
);
|
||||
for (const [path, anchor] of [
|
||||
["src/components/layout/sidebar-nav.tsx", "export function SidebarNav"],
|
||||
["src/components/layout/bottom-nav.tsx", 'data-slot="bottom-nav"'],
|
||||
["src/components/insights/layout-coach-fab.tsx", 'data-slot="coach-fab"'],
|
||||
] as const) {
|
||||
expect(readSource(path, anchor)).toContain("--safe-area-bottom");
|
||||
}
|
||||
expect(MAIN_SCROLL_CLASS).not.toContain("safe-area-inset-bottom");
|
||||
// Where a WKWebView browser keeps the page out of the strip over the
|
||||
// home indicator, it fills it with the root's colour: the content's.
|
||||
expect(css).toMatch(
|
||||
/html\[data-scroll="shell"\]\s*\{\s*background-color:\s*var\(--background\);\s*\}/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the bottom edge is cleared in both shells", () => {
|
||||
const fab = readSource(
|
||||
"src/components/insights/layout-coach-fab.tsx",
|
||||
@@ -71,12 +93,12 @@ describe("the bottom edge is cleared in both shells", () => {
|
||||
);
|
||||
const fabSize = rem(/\bsize-(\d+)\b/.exec(fab)![0]);
|
||||
const phoneFabBottom = rem(
|
||||
/(?<![\w:-])bottom-\[calc\(env\(safe-area-inset-bottom,0px\)\+[\d.]+rem\)\]/.exec(
|
||||
/(?<![\w:-])bottom-\[calc\(var\(--safe-area-bottom\)\+[\d.]+rem\)\]/.exec(
|
||||
fab,
|
||||
)![0],
|
||||
);
|
||||
const desktopFabBottom = rem(
|
||||
/shell-desktop:bottom-\[calc\(env\(safe-area-inset-bottom,0px\)\+[\d.]+rem\)\]/.exec(
|
||||
/shell-desktop:bottom-\[calc\(var\(--safe-area-bottom\)\+[\d.]+rem\)\]/.exec(
|
||||
fab,
|
||||
)![0],
|
||||
);
|
||||
@@ -85,14 +107,12 @@ describe("the bottom edge is cleared in both shells", () => {
|
||||
|
||||
it("the desktop shell's main keeps the home indicator off the last line", () => {
|
||||
expect(main).not.toContain("shell-desktop:pb-0");
|
||||
expect(main).toContain(
|
||||
"shell-desktop:pb-[env(safe-area-inset-bottom,0px)]",
|
||||
);
|
||||
expect(main).toContain("shell-desktop:pb-(--safe-area-bottom)");
|
||||
});
|
||||
|
||||
it("the last line scrolls clear of the Coach button in the phone shell", () => {
|
||||
const mainPad = Number(
|
||||
/^pb-\[calc\((\d+(?:\.\d+)?)rem\+env\(safe-area-inset-bottom,0px\)\)\]$/.exec(
|
||||
/^pb-\[calc\((\d+(?:\.\d+)?)rem\+var\(--safe-area-bottom\)\)\]$/.exec(
|
||||
main.find((c) => c.startsWith("pb-[calc("))!,
|
||||
)![1],
|
||||
);
|
||||
|
||||
@@ -600,7 +600,7 @@ describe("<SidebarNav> keeps every destination reachable on a touch tablet", ()
|
||||
|
||||
it("gives the rail the home indicator's inset, so the avatar row stays clear", () => {
|
||||
const aside = /<aside[^>]*class="([^"]*)"/.exec(render())?.[1] ?? "";
|
||||
expect(aside.split(" ")).toContain("pb-[env(safe-area-inset-bottom,0px)]");
|
||||
expect(aside.split(" ")).toContain("pb-(--safe-area-bottom)");
|
||||
});
|
||||
|
||||
it("draws the scroll cue for a finger only, outside the focus order and the accessibility tree", () => {
|
||||
|
||||
@@ -195,7 +195,7 @@ export function BottomNav() {
|
||||
<>
|
||||
{/* v1.4.33 — bar height and safe-area defence so the bar can
|
||||
never overlap the last viewport line of scrollable page content.
|
||||
`min-h-16` floors the bar at 64px; `pb-[env(safe-area-inset-bottom)]`
|
||||
`min-h-16` floors the bar at 64px; `pb-(--safe-area-bottom)`
|
||||
reserves the home-indicator inset on iOS / iPadOS PWA installs;
|
||||
`bg-card` + `border-t` give the bar a clean visual edge so page
|
||||
content that scrolls under the bar is occluded, not bleeding
|
||||
@@ -208,7 +208,7 @@ export function BottomNav() {
|
||||
aria-label={t("nav.mobileNavigation")}
|
||||
data-slot="bottom-nav"
|
||||
data-keyboard-hide=""
|
||||
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"
|
||||
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-(--safe-area-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)}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SHELL_ROOT_CLASS =
|
||||
* browser tab), so the last line never sits under it.
|
||||
*/
|
||||
export const MAIN_SCROLL_CLASS =
|
||||
"relative flex-1 overflow-y-auto pb-[calc(4rem+env(safe-area-inset-bottom,0px))] shell-desktop:pb-[env(safe-area-inset-bottom,0px)]";
|
||||
"relative flex-1 overflow-y-auto pb-[calc(4rem+var(--safe-area-bottom))] shell-desktop:pb-(--safe-area-bottom)";
|
||||
|
||||
/**
|
||||
* The app-chrome header band: one 4rem strip whose bottom border draws the
|
||||
|
||||
@@ -627,7 +627,7 @@ export function SidebarNav() {
|
||||
<aside
|
||||
aria-label={t("nav.sidebar")}
|
||||
className={cn(
|
||||
"bg-sidebar border-sidebar-border shell-desktop:flex shell-desktop:flex-col relative hidden h-full flex-shrink-0 overflow-hidden border-r pb-[env(safe-area-inset-bottom,0px)] transition-[width] duration-200 motion-reduce:transition-none",
|
||||
"bg-sidebar border-sidebar-border shell-desktop:flex shell-desktop:flex-col relative hidden h-full flex-shrink-0 overflow-hidden border-r pb-(--safe-area-bottom) transition-[width] duration-200 motion-reduce:transition-none",
|
||||
collapsed ? "w-16" : "w-64",
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user