diff --git a/web/src/components/mobile/MobileBrowse.tsx b/web/src/components/mobile/MobileBrowse.tsx index 43d2eb7..78fb6e8 100644 --- a/web/src/components/mobile/MobileBrowse.tsx +++ b/web/src/components/mobile/MobileBrowse.tsx @@ -31,7 +31,7 @@ interface Props { typed: string; mode: Mode; currentAlbumId: string | null; - /** Pill next to the title: connection lost, or the figure on the reader. Primitives + /** Pill above the group switch: connection lost, or the figure on the reader. Primitives * rather than an element, so `memo` still holds across position frames. */ statusLabel: string | null; offline: boolean; @@ -135,16 +135,12 @@ function MobileBrowseImpl({ return (
-
- -
{SKIN.appName}
+
{statusLabel && (
{statusLabel}
)} -
-
{GROUPS.map((candidate) => ( - setInset(Math.max(0, Math.round(window.innerHeight - viewport.height - viewport.offsetTop))); + // Measured against the height the stage actually has (see lib/fullHeight.ts), not + // `innerHeight`, which can be a status bar short in the home-screen app. + setInset(Math.max(0, Math.round(fullHeight() - viewport.height - viewport.offsetTop))); viewport.addEventListener("resize", measure); viewport.addEventListener("scroll", measure); return () => { diff --git a/web/src/lib/fullHeight.ts b/web/src/lib/fullHeight.ts new file mode 100644 index 0000000..6e56923 --- /dev/null +++ b/web/src/lib/fullHeight.ts @@ -0,0 +1,37 @@ +/** The height the phone layout should fill, in CSS px - and, in the iOS home-screen app, + * a `--full-height` variable on `` that mobile.css sizes the stage with. + * + * In the home-screen app (`navigator.standalone`, iOS only) the page runs edge to edge + * under a translucent status bar, so it should be exactly as tall as the screen. iOS does + * not always agree: the layout viewport - and with it `100dvh`, `innerHeight` and a + * `position: fixed; inset: 0` box - can come back one status bar short, which left the + * dock floating that far above the bottom of the screen with an empty strip below it. + * `screen` does not have that problem, and in standalone mode there is no browser chrome + * for it to overcount. iOS reports `screen` in portrait whatever the orientation, hence + * the swap. + * + * Everywhere else (a Safari tab, Android, the kiosk) this is just `innerHeight` and + * neither the variable nor `data-standalone` is set. */ + +const standalone = + typeof navigator !== "undefined" && + (navigator as Navigator & { standalone?: boolean }).standalone === true; + +export function fullHeight(): number { + if (!standalone) return window.innerHeight; + const landscape = window.innerWidth > window.innerHeight; + const screenHeight = landscape + ? Math.min(screen.width, screen.height) + : Math.max(screen.width, screen.height); + return Math.max(window.innerHeight, screenHeight); +} + +export function installFullHeight(): void { + if (!standalone) return; + const root = document.documentElement; + root.dataset.standalone = ""; + const apply = () => root.style.setProperty("--full-height", `${fullHeight()}px`); + apply(); + window.addEventListener("resize", apply); + window.addEventListener("orientationchange", apply); +} diff --git a/web/src/main.tsx b/web/src/main.tsx index f713f63..187b382 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { App } from "./App"; import { PERF } from "./lib/perfSettings"; +import { installFullHeight } from "./lib/fullHeight"; import { applySkin } from "./lib/skins"; import "./styles/app.css"; import "./styles/mobile.css"; @@ -11,6 +12,7 @@ import "./styles/tippen.css"; // Before the first render, so nothing ever paints in the wrong skin's colours. applySkin(); +installFullHeight(); createRoot(document.getElementById("root")!).render( diff --git a/web/src/styles/mobile.css b/web/src/styles/mobile.css index 9633f56..6b8d5b4 100644 --- a/web/src/styles/mobile.css +++ b/web/src/styles/mobile.css @@ -13,11 +13,12 @@ --m-dock-clearance: calc(96px + env(safe-area-inset-bottom)); } -/* iOS can leave the layout viewport shorter than the screen - after the keyboard has - been up, `100dvh` in a home-screen app comes back one status bar short - and the - page behind the stage showed as a white strip along the bottom. Pinning the stage to - the viewport's edges does not depend on that height at all, and the sea colour - underneath covers whatever might still peek through. */ +/* iOS can leave the layout viewport shorter than the screen in a home-screen app - + `100dvh`, and even `position: fixed; inset: 0`, come back one status bar short - so + the dock floated that far above the bottom with an empty strip below it. There the + stage takes its height from the screen instead (`--full-height` and + `data-standalone`, set by lib/fullHeight.ts only in standalone mode); everywhere else it pins to the viewport. + The sea colour underneath covers whatever might still peek through. */ html, body { background: var(--stage-deep, #001b21); @@ -29,6 +30,11 @@ body { height: auto; } +:root[data-standalone] .stage[data-phone] { + bottom: auto; + height: var(--full-height); +} + .m-glass { background: var(--m-glass-fill); backdrop-filter: blur(14px) saturate(1.7); @@ -62,30 +68,12 @@ body { /* ------------------------------------------------------------------ header -- */ -.m-title-row { - display: flex; - align-items: center; - gap: 10px; - padding: calc(14px + env(safe-area-inset-top)) var(--m-gutter) 4px; -} - -.m-title-mascot { - width: 44px; - height: 44px; - object-fit: contain; - filter: drop-shadow(0 3px 8px oklch(15% 0.05 var(--hue) / 0.5)); -} - -.m-title { - flex: 1; - font-size: 28px; - font-weight: 900; - color: var(--paper); - letter-spacing: -0.01em; - text-shadow: 0 2px 10px oklch(15% 0.05 var(--hue) / 0.45); -} - +/* Connection lost, or the figure on the reader - centred above the group switch, and + only there while it has something to say. No app title above it: that only cost a + row of space on a phone. */ .m-status { + width: fit-content; + margin: 0 auto 8px; font-size: 13px; font-weight: 800; color: var(--paper); @@ -114,12 +102,6 @@ body { padding: calc(10px + env(safe-area-inset-top)) var(--m-gutter) 14px; } -/* Under the title row, overlap the row by the status bar's height: the bar keeps its - place in the layout but already covers the status bar once it sticks. */ -.m-title-row + .m-sticky { - margin-top: calc(-1 * env(safe-area-inset-top)); -} - .m-sticky::before { content: ""; position: absolute;