From b438f38dcd97015c27864e6e0a1388265149c2a4 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 4 Nov 2026 00:49:41 +0100 Subject: [PATCH] fix(web): keep full-screen surfaces clear of the system bars The installed app draws the page under the status and navigation bars (viewport-fit=cover). Several full-screen surfaces ignored the safe-area insets, so their controls sat under the clock and battery icons: - Every MUI dialog and drawer now gets the insets from the theme: a full-screen dialog pads its content, a floating one is sized within the safe area, and a drawer pads the edges it touches. This covers the hamburger menu, the command palette and the place import dialog, which had no inset, and replaces the shared paper style's own top and bottom padding. - The street-level viewer's info card and close button start below the search bar instead of under it and the status bar, and its minimap clears the navigation bar. - The place photo gallery pads all four edges. - The bottom sheet's full height never reaches under the status bar. - The impersonation banner and the admin top bar grow by the top inset. --- apps/web/src/app/providers.tsx | 38 +++++++++++++++++++ apps/web/src/components/admin/AdminLayout.tsx | 2 +- apps/web/src/components/admin/AdminTopBar.tsx | 1 + .../components/admin/ImpersonationBanner.tsx | 6 ++- .../StreetLevelInfoCard.tsx | 13 +++++-- .../StreetLevelViewerInner.tsx | 11 ++++-- .../panels/place/PlacePhotoGallery.tsx | 3 ++ .../src/components/panels/sheet/detents.ts | 4 +- .../runtime/useFullScreenOnMobile.ts | 12 ++---- apps/web/src/lib/layout.ts | 13 +++++++ 10 files changed, 84 insertions(+), 19 deletions(-) diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index f2f2fa4bd..31c3f7bb5 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -103,6 +103,44 @@ const theme = createTheme({ }, }, }, + // The installed app draws the page under the status and navigation bars + // (`viewport-fit=cover`), so every dialog and drawer keeps its content out + // of them. In a plain browser tab the insets are 0. + MuiDialog: { + styleOverrides: { + // A floating dialog is centred and sized within the safe area. + container: ({ ownerState }) => + ownerState.fullScreen + ? {} + : { + boxSizing: "border-box", + paddingTop: "var(--omx-safe-top)", + paddingBottom: "var(--omx-safe-bottom)", + paddingLeft: "var(--omx-safe-left)", + paddingRight: "var(--omx-safe-right)", + }, + // A full-screen dialog still covers the bars, with its content inset. + paperFullScreen: { + boxSizing: "border-box", + paddingTop: "var(--omx-safe-top)", + paddingBottom: "var(--omx-safe-bottom)", + paddingLeft: "var(--omx-safe-left)", + paddingRight: "var(--omx-safe-right)", + }, + }, + }, + MuiDrawer: { + styleOverrides: { + // The screen edge the drawer is attached to and the two beside it; + // its open side faces the page, not a system bar. + paper: ({ ownerState }) => ({ + ...(ownerState.anchor !== "bottom" && { paddingTop: "var(--omx-safe-top)" }), + ...(ownerState.anchor !== "top" && { paddingBottom: "var(--omx-safe-bottom)" }), + ...(ownerState.anchor !== "right" && { paddingLeft: "var(--omx-safe-left)" }), + ...(ownerState.anchor !== "left" && { paddingRight: "var(--omx-safe-right)" }), + }), + }, + }, }, }); diff --git a/apps/web/src/components/admin/AdminLayout.tsx b/apps/web/src/components/admin/AdminLayout.tsx index 385634bfb..9a8a8653d 100644 --- a/apps/web/src/components/admin/AdminLayout.tsx +++ b/apps/web/src/components/admin/AdminLayout.tsx @@ -57,7 +57,7 @@ export function AdminLayout({ children, user, selfHosted = false }: AdminLayoutP diff --git a/apps/web/src/components/admin/AdminTopBar.tsx b/apps/web/src/components/admin/AdminTopBar.tsx index dbbc8f6af..a80270846 100644 --- a/apps/web/src/components/admin/AdminTopBar.tsx +++ b/apps/web/src/components/admin/AdminTopBar.tsx @@ -69,6 +69,7 @@ export function AdminTopBar({ user, sidebarOpen, onToggleSidebar }: AdminTopBarP borderBottom: "1px solid", borderColor: "divider", color: "text.primary", + pt: "var(--omx-safe-top)", width: { md: sidebarOpen ? `calc(100% - ${SIDEBAR_WIDTH}px)` : "100%" }, ml: { md: sidebarOpen ? `${SIDEBAR_WIDTH}px` : 0 }, transition: diff --git a/apps/web/src/components/admin/ImpersonationBanner.tsx b/apps/web/src/components/admin/ImpersonationBanner.tsx index f983ca002..50c321e7b 100644 --- a/apps/web/src/components/admin/ImpersonationBanner.tsx +++ b/apps/web/src/components/admin/ImpersonationBanner.tsx @@ -37,7 +37,9 @@ export function ImpersonationBanner() { left: 0, right: 0, zIndex: 9999, - height: 48, + boxSizing: "border-box", + height: "calc(48px + var(--omx-safe-top))", + pt: "calc(6px + var(--omx-safe-top))", "& .MuiAlert-message": { display: "flex", alignItems: "center", @@ -61,7 +63,7 @@ export function ImpersonationBanner() { Impersonating {session.user.name} ({session.user.email}) {/* Spacer to push page content below the fixed banner */} -
+
); } diff --git a/apps/web/src/components/map/street-level-imagery/StreetLevelInfoCard.tsx b/apps/web/src/components/map/street-level-imagery/StreetLevelInfoCard.tsx index 5a5e387ac..a2a5a42b0 100644 --- a/apps/web/src/components/map/street-level-imagery/StreetLevelInfoCard.tsx +++ b/apps/web/src/components/map/street-level-imagery/StreetLevelInfoCard.tsx @@ -24,6 +24,7 @@ import { useEffect, useRef, useState } from "react"; import { AttributionStrip } from "@/components/ui/AttributionStrip"; import { AttributionText } from "@/components/ui/AttributionText"; import { useMap } from "@/integration-api/map/MapContext"; +import { SEARCH_BAR_CLEARANCE } from "@/lib/layout"; /** Capture month and year in the active locale, or null when unknown. */ export function formatCaptureDate(capturedAt: string | undefined, locale: string): string | null { @@ -98,14 +99,20 @@ export function StreetLevelInfoCard({ {/* Top bar */} diff --git a/apps/web/src/components/panels/sheet/detents.ts b/apps/web/src/components/panels/sheet/detents.ts index 7a9fac922..60633b752 100644 --- a/apps/web/src/components/panels/sheet/detents.ts +++ b/apps/web/src/components/panels/sheet/detents.ts @@ -31,7 +31,9 @@ export interface SnapSlot { detent: "top" | Detent; } -const MAX_HEIGHT = "94dvh"; +// Never under the status bar, whose height `dvh` does not know about when the +// page is drawn beneath it. +const MAX_HEIGHT = "min(94dvh, calc(100dvh - var(--omx-safe-top) - 8px))"; export const PLACE_DETENTS: DetentConfig = { peek: "180px", diff --git a/apps/web/src/integration-api/runtime/useFullScreenOnMobile.ts b/apps/web/src/integration-api/runtime/useFullScreenOnMobile.ts index 0af60a809..ea9184c9c 100644 --- a/apps/web/src/integration-api/runtime/useFullScreenOnMobile.ts +++ b/apps/web/src/integration-api/runtime/useFullScreenOnMobile.ts @@ -15,9 +15,10 @@ import type { SystemStyleObject } from "@mui/system/styleFunctionSx"; * slotProps={{ paper: { sx: mobileFullScreenDialogPaperSx } }} * /> * - * The shared paper sx squares off the corners on mobile, keeps the content - * clear of the system bars, and vertically centers DialogContent when its children don't fill the viewport (using - * `safe center` so long, scrollable content still starts at the top). + * The shared paper sx squares off the corners on mobile and vertically centers + * DialogContent when its children don't fill the viewport (using `safe center` + * so long, scrollable content still starts at the top). The theme keeps every + * full-screen dialog's content clear of the system bars. */ export function useFullScreenOnMobile(): boolean { const theme = useTheme(); @@ -26,11 +27,6 @@ export function useFullScreenOnMobile(): boolean { export const mobileFullScreenDialogPaperSx: SystemStyleObject = { borderRadius: { xs: 0, sm: "12px" }, - // A full-screen dialog covers the status bar and home indicator wherever the - // page draws edge to edge (an installed PWA, the installed app), so its - // content keeps clear of the system bars there. - paddingTop: { xs: "var(--omx-safe-top)", sm: 0 }, - paddingBottom: { xs: "var(--omx-safe-bottom)", sm: 0 }, "& .MuiDialogContent-root": { display: { xs: "flex", sm: "block" }, flexDirection: "column", diff --git a/apps/web/src/lib/layout.ts b/apps/web/src/lib/layout.ts index 31d89e66e..d15651b1f 100644 --- a/apps/web/src/lib/layout.ts +++ b/apps/web/src/lib/layout.ts @@ -1,4 +1,17 @@ /** Shared layout constants for the sidebar panels. */ + +/** + * Padding that keeps a full-screen surface's content out of the system bars + * and display cutouts. The installed app and an installed web app draw the + * page under them (`viewport-fit=cover`); in a plain browser tab the insets + * are 0, so this changes nothing there. + */ +export const SAFE_AREA_PADDING = { + pt: "var(--omx-safe-top)", + pb: "var(--omx-safe-bottom)", + pl: "var(--omx-safe-left)", + pr: "var(--omx-safe-right)", +} as const; export const PANEL_WIDTH = 400; /** Offset of the floating search bar from the top and left edges of the map. */