Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/web/src/components/map/SearchInAreaChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { useMap } from "@/integration-api/map/MapContext";
import { BRAND } from "@/integration-api/runtime/theme";
import { PANEL_WIDTH } from "@/lib/layout";
import { PANEL_WIDTH, SEARCH_BAR_INSET, topInset } from "@/lib/layout";

// Floating card: sidebar(400) + gap(24) + card(376) = 800px from left edge
const FLOATING_CARD_RIGHT_EDGE = 800;
Expand Down Expand Up @@ -83,10 +83,11 @@ export function SearchInAreaChip() {
sx={{
position: "absolute",
// Below the category chips row:
// Desktop: chips at top=18, height=40px → top=66; Mobile: chips at top=72 → top=120
// Desktop: chips 6px below the bar's top, 40px tall, then 8px → 54px;
// mobile: chips 60px below it → 108px.
top: {
xs: "calc(120px + var(--omx-safe-top))",
sm: "calc(66px + var(--omx-safe-top))",
xs: topInset(SEARCH_BAR_INSET, 108),
sm: topInset(SEARCH_BAR_INSET, 54),
},
// When the floating card is open, center in the remaining map area to its right.
// Remaining area: FLOATING_CARD_RIGHT_EDGE → 100vw, center = (edge + 100%) / 2
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/map/TopRightControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Box from "@mui/material/Box";
import { AccountAvatarButton } from "@/components/auth/AccountAvatarButton";
import { SEARCH_BAR_TOP } from "@/lib/layout";

/**
* Desktop-only floating account avatar at the top-right of the map. On mobile
Expand All @@ -14,7 +15,7 @@ export function TopRightControls() {
sx={{
display: { xs: "none", sm: "flex" },
position: "absolute",
top: "calc(12px + var(--omx-safe-top))",
top: SEARCH_BAR_TOP,
right: "calc(12px + var(--omx-safe-right))",
alignItems: "center",
gap: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { LocationMinimap } from "@/integration-api/components/LocationMinimap";
import { useStreetLevelProviders } from "@/integration-api/components/useStreetLevelProviders";
import { useMap } from "@/integration-api/map/MapContext";
import { useEnv } from "@/integration-api/runtime/EnvProvider";
import { SEARCH_BAR_CLEARANCE } from "@/lib/layout";
import { SEARCH_BAR_CLEARANCE, topInset } from "@/lib/layout";
import { StreetLevelFlatImage } from "./StreetLevelFlatImage";
import { StreetLevelInfoCard } from "./StreetLevelInfoCard";
import { fetchStreetLevelNode, pickPanoramaUrl, type StreetLevelNode } from "./useStreetLevelNode";
Expand Down Expand Up @@ -352,7 +352,7 @@ export default function StreetLevelViewerInner() {
position: "absolute",
// On a phone the search bar spans the top, so the button goes below
// it; beside it on wider screens. Both clear the status bar.
top: { xs: SEARCH_BAR_CLEARANCE, sm: "calc(8px + var(--omx-safe-top))" },
top: { xs: SEARCH_BAR_CLEARANCE, sm: topInset(8) },
right: "calc(8px + var(--omx-safe-right))",
bgcolor: "rgba(0,0,0,0.5)",
color: "#fff",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { NavigationSettingsDialog } from "@/components/settings/NavigationSettingsDialog";
import { useMapOptional } from "@/integration-api/map/MapContext";
import { NAV_CHROME_INSET, NAV_LANDSCAPE_PANEL_WIDTH } from "@/lib/layout";
import { NAV_CHROME_TOP, NAV_LANDSCAPE_PANEL_WIDTH } from "@/lib/layout";
import { useNavigationMutations } from "@/lib/mobile/useNavigationMutations";
import {
useMobilePanelClearance,
Expand Down Expand Up @@ -217,9 +217,9 @@ export function GroundNavigationChrome({ coverage }: Props) {
flexDirection: "column",
gap: 1,
p: 2,
// Match the 16px (`p: 2`) side inset below the safe-area top so the
// banner's gap to the top equals its gap to the sides.
pt: `calc(var(--omx-safe-top) + ${NAV_CHROME_INSET}px)`,
// The 16px (`p: 2`) side inset at the top of a browser tab; under
// a status bar the banner sits closer, as the search bar does.
pt: NAV_CHROME_TOP,
// On wide screens keep the banner in a left-hand column instead of
// stretching across the whole top edge (maxWidth adds back the 16px
// horizontal padding so the card itself is LANDSCAPE_PANEL_WIDTH).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ vi.mock("./RouteSearchResultsLayer", () => ({
),
}));

import { NAV_CHROME_TOP } from "@/lib/layout";
import { useRouteSearchStore } from "@/lib/navigation/routeSearchStore";
import { RouteSearchControl } from "./RouteSearchControl";

Expand Down Expand Up @@ -258,7 +259,7 @@ it("lines the picker and stop card up with the maneuver banner and bottom bar in
useRouteSearchStore.getState().openPicker();
const { unmount } = render(view());
const picker = screen.getByText("Search along route").closest(".MuiPaper-root") as HTMLElement;
expect(getComputedStyle(picker).top).toBe("calc(var(--omx-safe-top) + 16px)");
expect(getComputedStyle(picker).top).toBe(NAV_CHROME_TOP);
unmount();
useRouteSearchStore.getState().setCategoryKey("preset:amenity/fuel");
render(view());
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/navigation/RouteSearchControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { BrandLogo } from "@/components/search/BrandLogo";
import { AttributionStrip } from "@/components/ui/AttributionStrip";
import { useMapOptional } from "@/integration-api/map/MapContext";
import { BRAND } from "@/integration-api/runtime/theme";
import { NAV_CHROME_INSET } from "@/lib/layout";
import { NAV_CHROME_INSET, NAV_CHROME_TOP } from "@/lib/layout";
import { useRouteSearchStore } from "@/lib/navigation/routeSearchStore";
import { routeStopCopy } from "@/lib/navigation/routeStopCopy";
import { routeSearchQueryFor, useRouteSearch } from "@/lib/navigation/useRouteSearch";
Expand Down Expand Up @@ -287,7 +287,7 @@ export function RouteSearchControl() {
position: "fixed",
left: "50%",
transform: "translateX(-50%)",
top: `calc(var(--omx-safe-top) + ${NAV_CHROME_INSET}px)`,
top: NAV_CHROME_TOP,
width: { xs: `calc(100% - ${2 * NAV_CHROME_INSET}px)`, sm: 480 },
borderRadius: 3,
zIndex: 1350,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { NavigationSettingsDialog } from "@/components/settings/NavigationSettingsDialog";
import { useMapOptional } from "@/integration-api/map/MapContext";
import { NAV_LANDSCAPE_PANEL_WIDTH } from "@/lib/layout";
import { NAV_CHROME_TOP, NAV_LANDSCAPE_PANEL_WIDTH } from "@/lib/layout";
import { useMobileRuntime } from "@/lib/mobile/useMobileRuntime";
import { useNavigationMutations } from "@/lib/mobile/useNavigationMutations";
import { useNavChromeObstructions } from "@/lib/navigation/useNavChromeObstructions";
Expand Down Expand Up @@ -158,9 +158,8 @@ export function TransitNavigationView() {
flexDirection: "column",
gap: 1,
p: 2,
// Match the driving banner's inset below the safe-area top so the
// gap to the top equals the gap to the sides.
pt: "calc(var(--omx-safe-top) + 16px)",
// Same top as the driving banner.
pt: NAV_CHROME_TOP,
// On wide screens keep the chrome in a left-hand column (like the
// driving view) so the map stays clear on the right.
...(isMobile
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/panels/DetailShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import useMediaQuery from "@mui/material/useMediaQuery";
import { PANEL, useNavigationStore, useSidebarStore } from "@openmapx/core";
import { useTranslations } from "next-intl";
import { lazy, type ReactNode, Suspense, useContext, useEffect, useState } from "react";
import { NAV_LANDSCAPE_PANEL_WIDTH, PANEL_WIDTH } from "@/lib/layout";
import { NAV_CHROME_TOP, NAV_LANDSCAPE_PANEL_WIDTH, PANEL_WIDTH } from "@/lib/layout";
import { useMapObstruction } from "@/lib/mapObstructions";
import { useMobilePanelHeightTracker } from "@/lib/mobilePanelHeight";
import { PLACE_DETENTS } from "./sheet/detents";
Expand Down Expand Up @@ -82,7 +82,7 @@ function DesktopDetail({ children }: { children: ReactNode }) {
// While navigating the card sits beside the guidance column instead; the
// nav camera keeps its own framing, so it claims no map obstruction.
const left = navigating ? NAV_LANDSCAPE_PANEL_WIDTH + 2 * CARD_GAP : PANEL_WIDTH + CARD_GAP;
const top = navigating ? "calc(var(--omx-safe-top) + 16px)" : "66px";
const top = navigating ? NAV_CHROME_TOP : "66px";
useMapObstruction(
"detail",
"left",
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/components/search/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ import {
launchExploreTextSearch,
launchTextSearch,
} from "@/lib/launchExplore";
import { SEARCH_BAR_CLEARANCE, SEARCH_BAR_HEIGHT, SEARCH_BAR_INSET } from "@/lib/layout";
import {
SEARCH_BAR_CLEARANCE,
SEARCH_BAR_HEIGHT,
SEARCH_BAR_INSET,
SEARCH_BAR_TOP,
} from "@/lib/layout";
import { useMeasuredMapObstruction } from "@/lib/mapObstructions";
import { servedAttributions } from "@/lib/servedAttributions";
import { useBackDismiss } from "@/lib/useBackDismiss";
Expand Down Expand Up @@ -1029,7 +1034,7 @@ export function SearchBar({ surface = "map" }: SearchBarProps) {
onBlurCapture={handleBlur}
sx={{
position: "absolute",
top: `calc(${SEARCH_BAR_INSET}px + var(--omx-safe-top))`,
top: SEARCH_BAR_TOP,
left: `calc(${SEARCH_BAR_INSET}px + var(--omx-safe-left))`,
right: {
xs: `calc(${SEARCH_BAR_INSET}px + var(--omx-safe-right))`,
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/search/floatingChipSx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SxProps, Theme } from "@mui/material/styles";
import { BRAND } from "@/integration-api/runtime/theme";
import { SEARCH_BAR_HEIGHT, SEARCH_BAR_INSET, topInset } from "@/lib/layout";

/**
* Shared "floating pill chip" styling used by the map-overlay chip rows
Expand Down Expand Up @@ -59,8 +60,9 @@ export function floatingChipSx(active: boolean, variant: "category" | "toggle"):
export const floatingToolbarSx = {
position: "absolute",
top: {
xs: "calc(72px + var(--omx-safe-top))",
sm: "calc(18px + var(--omx-safe-top))",
// Below the search bar on a phone; level with its centre beside it.
xs: topInset(SEARCH_BAR_INSET, SEARCH_BAR_HEIGHT + SEARCH_BAR_INSET),
sm: topInset(SEARCH_BAR_INSET, 6),
},
left: { xs: "var(--omx-safe-left)", sm: "calc(420px + var(--omx-safe-left))" },
right: { xs: "var(--omx-safe-right)", sm: "calc(108px + var(--omx-safe-right))" },
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/weather/WeatherWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { WeatherIcon } from "@/integration-api/components/WeatherIcon";
import { useMap } from "@/integration-api/map/MapContext";
import { useContributorResolver } from "@/integration-api/overlay/useContributorResolver";
import { useMapAttributions } from "@/integration-api/overlay/useMapAttributions";
import { SEARCH_BAR_INSET, topInset } from "@/lib/layout";
import { windDirectionLabel } from "./weatherUtils";

export function WeatherWidget() {
Expand Down Expand Up @@ -80,8 +81,8 @@ export function WeatherWidget() {
sx={{
position: "absolute",
top: {
xs: "calc(118px + var(--omx-safe-top))",
sm: "calc(74px + var(--omx-safe-top))",
xs: topInset(SEARCH_BAR_INSET, 106),
sm: topInset(SEARCH_BAR_INSET, 62),
},
left: "calc(12px + var(--omx-safe-left))",
zIndex: 9,
Expand Down
25 changes: 24 additions & 1 deletion apps/web/src/lib/layout.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { describe, expect, it } from "vitest";
import { isPanelShiftActive, shouldHideLayerSelector } from "./layout";
import {
isPanelShiftActive,
SEARCH_BAR_CLEARANCE,
SEARCH_BAR_TOP,
shouldHideLayerSelector,
topInset,
} from "./layout";

describe("topInset", () => {
it("keeps the page's edge spacing without a status bar and 8px below one", () => {
// The status bar already separates the chrome from the screen edge, so
// the installed app leaves Google Maps' 8px under it, not edge + inset.
expect(topInset(12)).toBe("max(12px, var(--omx-safe-top) + 8px)");
});

it("stacks rows below that top", () => {
expect(topInset(12, 60)).toBe("calc(max(12px, var(--omx-safe-top) + 8px) + 60px)");
});

it("clears the search bar by its height plus the same 12px below it", () => {
expect(SEARCH_BAR_TOP).toBe(topInset(12));
expect(SEARCH_BAR_CLEARANCE).toBe(topInset(12, 60));
});
});

describe("isPanelShiftActive", () => {
it("is active when the sidebar is open, expanded, and not navigating", () => {
Expand Down
32 changes: 29 additions & 3 deletions apps/web/src/lib/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,41 @@ export const SAFE_AREA_PADDING = {
} as const;
export const PANEL_WIDTH = 400;

/**
* Gap between the status bar (or a display cutout) and the chrome floating
* below it, the same 8px Google Maps leaves above its search bar on Android.
* The status bar already sets the chrome apart from the screen edge, so the
* page's own edge spacing on top of it reads as wasted space.
*/
export const STATUS_BAR_GAP = 8;

/**
* Top offset for chrome that floats `edge` px below the top of the page. Under
* a status bar (the installed app, an installed web app) the chrome sits
* `STATUS_BAR_GAP` below the bar instead; in a browser tab the inset is 0 and
* `edge` applies. `below` adds the height of the rows stacked above it.
*/
export function topInset(edge: number, below = 0): string {
const base = `max(${edge}px, var(--omx-safe-top) + ${STATUS_BAR_GAP}px)`;
return below ? `calc(${base} + ${below}px)` : base;
}

/** Offset of the floating search bar from the top and left edges of the map. */
export const SEARCH_BAR_INSET = 12;
/** Height of the floating search bar's input row. */
export const SEARCH_BAR_HEIGHT = 48;
/** Top of the floating search bar. */
export const SEARCH_BAR_TOP = topInset(SEARCH_BAR_INSET);

/**
* Top strip of the desktop sidebar that the floating search bar covers, plus
* the same breathing room below the bar as above it. Panels keep their content
* out of this strip; see `searchBarClearance` in the panels directory.
* `SEARCH_BAR_INSET` of breathing room below the bar. Panels keep their
* content out of this strip; see `searchBarClearance` in the panels directory.
*/
export const SEARCH_BAR_CLEARANCE = `calc(${SEARCH_BAR_INSET * 2 + SEARCH_BAR_HEIGHT}px + var(--omx-safe-top))`;
export const SEARCH_BAR_CLEARANCE = topInset(
SEARCH_BAR_INSET,
SEARCH_BAR_HEIGHT + SEARCH_BAR_INSET,
);

/**
* Width of the left-hand nav-chrome column on wide viewports (desktop and
Expand All @@ -40,6 +64,8 @@ export const NAV_LANDSCAPE_PANEL_WIDTH = 400;
* search-along-route picker and stop card) use it too, so they line up.
*/
export const NAV_CHROME_INSET = 16;
/** Top of the turn-by-turn chrome and the overlays that line up with it. */
export const NAV_CHROME_TOP = topInset(NAV_CHROME_INSET);

/**
* Whether the left sidebar panel is currently occupying horizontal space, so
Expand Down
Loading