Mobile map layout and address/coordinate search (#49)#57
Conversation
On narrow layouts (<=900px) the header controls move onto the map: the view toggle and crosshair/patch controls become plain icon buttons in floating right-edge islands, and the control panel opens as a full-screen drawer via a menu button beside the logo.
A floating search box (top-center max 400px on desktop, full width on mobile) geocodes addresses via Nominatim and parses raw coordinates locally. Selecting a result or pressing Enter drops a point through the existing pick flow. Includes recent-search history, an empty state, and a "/" focus shortcut.
|
🚀 Vercel preview deployed Preview URL: https://earth-prints-2uxc2h2ro-anastasiia-s-projects10.vercel.app |
There was a problem hiding this comment.
Code Review
This pull request introduces a mobile-responsive layout for the map editor, including a slide-out controls drawer, floating map controls, and a geocoding-enabled search input with history persistence. The review feedback highlights several critical improvements: resolving an accessibility issue where the desktop sidebar is hidden from screen readers, preventing React hydration mismatches by deferring localStorage access to a client-side effect, avoiding shortcut conflicts with system modifier keys, disabling browser autocomplete on the search input, defensively handling non-array API responses, and adjusting tooltip positioning to prevent visual overlap.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| <aside | ||
| className="flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:max-h-[42vh] max-[900px]:w-full max-[900px]:border-b max-[900px]:border-editor-border" | ||
| className={`flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:fixed max-[900px]:inset-0 max-[900px]:z-[110] max-[900px]:w-full max-[900px]:transition-transform max-[900px]:duration-300 max-[900px]:ease-[cubic-bezier(0.16,1,0.3,1)] ${ | ||
| controlsOpen | ||
| ? "max-[900px]:translate-x-0" | ||
| : "max-[900px]:-translate-x-full" | ||
| }`} | ||
| aria-label="Map controls" | ||
| aria-hidden={!controlsOpen} | ||
| > |
There was a problem hiding this comment.
On desktop layouts (>900px), controlsOpen is initialized to false, which causes aria-hidden={!controlsOpen} to evaluate to true. This hides the entire sidebar from screen readers on desktop. Additionally, when the drawer is closed on mobile, interactive elements inside the sidebar can still be focused via keyboard tabbing.
Using Tailwind's max-[900px]:visible and max-[900px]:invisible utility classes solves both issues: it automatically handles screen reader visibility and prevents keyboard focus when closed on mobile, while keeping the sidebar fully accessible on desktop.
| <aside | |
| className="flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:max-h-[42vh] max-[900px]:w-full max-[900px]:border-b max-[900px]:border-editor-border" | |
| className={`flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:fixed max-[900px]:inset-0 max-[900px]:z-[110] max-[900px]:w-full max-[900px]:transition-transform max-[900px]:duration-300 max-[900px]:ease-[cubic-bezier(0.16,1,0.3,1)] ${ | |
| controlsOpen | |
| ? "max-[900px]:translate-x-0" | |
| : "max-[900px]:-translate-x-full" | |
| }`} | |
| aria-label="Map controls" | |
| aria-hidden={!controlsOpen} | |
| > | |
| <aside | |
| className={"flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:fixed max-[900px]:inset-0 max-[900px]:z-[110] max-[900px]:w-full max-[900px]:transition-[transform,visibility] max-[900px]:duration-300 max-[900px]:ease-[cubic-bezier(0.16,1,0.3,1)] " + ( | |
| controlsOpen | |
| ? "max-[900px]:translate-x-0 max-[900px]:visible" | |
| : "max-[900px]:-translate-x-full max-[900px]:invisible" | |
| )} | |
| aria-label="Map controls" | |
| > |
| const [history, setHistory] = useState<SearchResult[]>(() => | ||
| loadSearchHistory(), | ||
| ); |
There was a problem hiding this comment.
Initializing the history state directly with loadSearchHistory() (which accesses window.localStorage) will cause a hydration mismatch in Next.js/SSR. During server-side rendering, window is undefined so it returns [], but on the client it may return a non-empty array from localStorage, leading to React hydration errors.
To prevent this, initialize the state to an empty array and load the search history from localStorage inside a useEffect hook after the component has mounted on the client.
| const [history, setHistory] = useState<SearchResult[]>(() => | |
| loadSearchHistory(), | |
| ); | |
| const [history, setHistory] = useState<SearchResult[]>([]); | |
| useEffect(() => { | |
| setHistory(loadSearchHistory()); | |
| }, []); |
| const onKeyDown = (event: KeyboardEvent) => { | ||
| if (event.key !== "/") return; | ||
| const target = event.target as HTMLElement | null; |
There was a problem hiding this comment.
The / keyboard shortcut currently intercepts all keydown events where event.key === "/", even if modifier keys like Cmd, Ctrl, or Alt are pressed. This breaks common browser and system shortcuts (such as Cmd + / or Ctrl + / used for toggling comments in developer tools or other web-based editors).
Adding a check for modifier keys ensures standard system shortcuts continue to function correctly.
| const onKeyDown = (event: KeyboardEvent) => { | |
| if (event.key !== "/") return; | |
| const target = event.target as HTMLElement | null; | |
| const onKeyDown = (event: KeyboardEvent) => { | |
| if (event.key !== "/" || event.ctrlKey || event.metaKey || event.altKey) return; |
| <input | ||
| ref={inputRef} | ||
| type="text" | ||
| value={query} | ||
| onChange={(event) => onQueryChange(event.target.value)} | ||
| onFocus={() => setOpen(true)} | ||
| onBlur={() => setOpen(false)} | ||
| onKeyDown={onKeyDown} | ||
| placeholder="Search address or coordinates" | ||
| aria-label="Search address or coordinates" | ||
| className="min-w-0 flex-1 bg-transparent text-sm text-editor-fg-primary placeholder:text-editor-fg-tertiary focus:outline-none" | ||
| /> |
There was a problem hiding this comment.
The search input lacks autocomplete and autocorrect prevention. Browser-native autocomplete dropdowns frequently overlap and obscure custom search dropdowns, and autocorrect can interfere with coordinate inputs.
Disabling autocomplete, autocorrect, autocapitalize, and spellcheck provides a much cleaner and more reliable user experience.
| <input | |
| ref={inputRef} | |
| type="text" | |
| value={query} | |
| onChange={(event) => onQueryChange(event.target.value)} | |
| onFocus={() => setOpen(true)} | |
| onBlur={() => setOpen(false)} | |
| onKeyDown={onKeyDown} | |
| placeholder="Search address or coordinates" | |
| aria-label="Search address or coordinates" | |
| className="min-w-0 flex-1 bg-transparent text-sm text-editor-fg-primary placeholder:text-editor-fg-tertiary focus:outline-none" | |
| /> | |
| <input | |
| ref={inputRef} | |
| type="text" | |
| value={query} | |
| onChange={(event) => onQueryChange(event.target.value)} | |
| onFocus={() => setOpen(true)} | |
| onBlur={() => setOpen(false)} | |
| onKeyDown={onKeyDown} | |
| placeholder="Search address or coordinates" | |
| aria-label="Search address or coordinates" | |
| autoComplete="off" | |
| autoCorrect="off" | |
| autoCapitalize="off" | |
| spellCheck="false" | |
| className="min-w-0 flex-1 bg-transparent text-sm text-editor-fg-primary placeholder:text-editor-fg-tertiary focus:outline-none" | |
| /> |
| const data = (await response.json()) as NominatimPlace[]; | ||
| return data | ||
| .map(toResult) | ||
| .filter((result) => Number.isFinite(result.lon) && Number.isFinite(result.lat)); |
There was a problem hiding this comment.
If Nominatim returns an error object (e.g., {"error":"..."}) or any non-array response due to rate limits or invalid queries, calling data.map will throw a TypeError and crash the search component.
Adding a defensive check to ensure data is an array before mapping prevents runtime crashes.
const data = await response.json();
if (!Array.isArray(data)) {
return [];
}
return data
.map(toResult)
.filter((result) => Number.isFinite(result.lon) && Number.isFinite(result.lat));| <div className="pointer-events-none absolute right-3 top-1/2 z-20 hidden -translate-y-1/2 flex-col items-end gap-2 max-[900px]:flex"> | ||
| <div className={ISLAND_CLASS}> |
There was a problem hiding this comment.
The tooltips for the stacked vertical buttons in the floating islands appear below the buttons (top-full), causing them to overlap and obscure the buttons directly below them.
Since we cannot easily modify the IconButton component's internal tooltip rendering (as it is outside the diff hunks), we can use Tailwind's arbitrary descendant selectors on the container to override the tooltip positioning to the left (right-full) with a smooth horizontal transition.
| <div className="pointer-events-none absolute right-3 top-1/2 z-20 hidden -translate-y-1/2 flex-col items-end gap-2 max-[900px]:flex"> | |
| <div className={ISLAND_CLASS}> | |
| <div className="pointer-events-none absolute right-3 top-1/2 z-20 hidden -translate-y-1/2 flex-col items-end gap-2 max-[900px]:flex [&_[role=tooltip]]:left-auto [&_[role=tooltip]]:right-full [&_[role=tooltip]]:top-1/2 [&_[role=tooltip]]:mt-0 [&_[role=tooltip]]:mr-2 [&_[role=tooltip]]:translate-x-[2px] [&_[role=tooltip]]:-translate-y-1/2 group-hover:[&_[role=tooltip]]:translate-x-0 group-hover:[&_[role=tooltip]]:-translate-y-1/2 group-focus-within:[&_[role=tooltip]]:translate-x-0 group-focus-within:[&_[role=tooltip]]:-translate-y-1/2"> | |
| <div className={ISLAND_CLASS}> |
| onKeyDown={onKeyDown} | ||
| placeholder="Search address or coordinates" | ||
| aria-label="Search address or coordinates" | ||
| className="min-w-0 flex-1 bg-transparent text-sm text-editor-fg-primary placeholder:text-editor-fg-tertiary focus:outline-none" |
There was a problem hiding this comment.
also, let's not do text-sm, a side effect of this on mobile devices is that when clicking, it will zoom-in and the layout get's shifted (the users then needs to find his/her way back to the appropriate zoom level, a thing they shouldnt be face with).

Implements the UI layout proposal from #49.
Mobile layout (<=900px)
MapReadout) opens as a full-screen drawer via a menu button placed to the left of the logo.Search
lat, loninput resolves locally./focus shortcut with keyboard navigation.Includes a merge of the latest
main.