diff --git a/src/app/debug/coverage/CoverageMap.js b/src/app/debug/coverage/CoverageMap.js
index a3dd440..7925eed 100644
--- a/src/app/debug/coverage/CoverageMap.js
+++ b/src/app/debug/coverage/CoverageMap.js
@@ -44,6 +44,10 @@ export default function CoverageMap({
if (!containerRef.current || mapRef.current) return;
const map = L.map(containerRef.current, { preferCanvas: true });
+ // Bottom-left: from lg the panorama inspector floats over the bottom-right
+ // corner, and tile attribution has to stay visible. The map option is a
+ // boolean, so the position is set on the control itself.
+ map.attributionControl.setPosition('bottomleft');
const tiles = getTileConfig();
L.tileLayer(tiles.url, tiles.options).addTo(map);
map.setView([16.0, 107.0], 6);
@@ -84,10 +88,25 @@ export default function CoverageMap({
};
map.on('click', pick);
+ // Leaflet caches the container size and only re-reads it on a window
+ // resize, so opening the inspector -- which shrinks the map on phones,
+ // where it stacks rather than floats -- would otherwise leave the map
+ // drawing at its old height.
+ // Zero size means the map is display:none -- the phone layout swaps it out
+ // for the inspector. Measuring then would hand the loader a degenerate
+ // bbox and wipe the point count, so wait until it is on screen again.
+ const resizeObserver = new ResizeObserver(() => {
+ const el = containerRef.current;
+ if (!el || !el.clientWidth || !el.clientHeight) return;
+ map.invalidateSize();
+ });
+ resizeObserver.observe(containerRef.current);
+
mapRef.current = map;
return () => {
clearTimeout(moveTimer);
+ resizeObserver.disconnect();
map.off('moveend', report);
map.off('click', pick);
map.remove();
@@ -173,5 +192,11 @@ export default function CoverageMap({
}).addTo(map);
}, [selectedId, panos]);
- return
;
+ // Sized by insets, so the caller must position its wrapper. A percentage
+ // height would collapse: the wrapper is a flex item, whose flexed main-axis
+ // height is indefinite for percentage resolution.
+ // `isolate` keeps Leaflet's own ladder (panes at 400, controls at 800-1000)
+ // inside this element; without it those values escape to the caller's
+ // stacking context and paint over anything layered on top of the map.
+ return ;
}
diff --git a/src/app/debug/coverage/page.js b/src/app/debug/coverage/page.js
index 7fffba9..d3d50a6 100644
--- a/src/app/debug/coverage/page.js
+++ b/src/app/debug/coverage/page.js
@@ -15,7 +15,7 @@ const CoverageMap = dynamic(() => import('./CoverageMap'), {
Loading map...
@@ -125,6 +125,21 @@ export default function CoveragePage() {
setPanoLoading(false);
}, []);
+ // The inspector covers part of the map, so Escape closes it -- the X in its
+ // header is the only other way out.
+ useEffect(() => {
+ if (!selected) return undefined;
+ const handleKeyDown = (event) => {
+ if (event.key !== 'Escape') return;
+ // An open select owns Escape; closing the inspector behind it would make
+ // one keypress do two things.
+ if (document.querySelector('[data-radix-popper-content-wrapper]')) return;
+ closePano();
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [selected, closePano]);
+
const handleBoundsChange = useCallback(
(nextBbox) => {
load(region, nextBbox);
@@ -133,7 +148,7 @@ export default function CoveragePage() {
);
return (
-
+
{/* Same title pattern as the other debug pages, with the tool's own
controls beside it. The shared debug layout owns the app chrome. */}
+ {/* The map is the tool, so it gets the whole surface. From lg the
+ inspector floats over its right edge: the old two-column grid held
+ half the width empty until a point was picked, and reflowing the map
+ on selection moved the very dot the user had just clicked. A phone has
+ neither the width to float into nor the height to split -- 112px of
+ map is not a map -- so there the inspector takes the surface instead
+ and its close button brings the map back.
+ min-h-64 is the floor that keeps that promise: the page chrome takes
+ 225px, so on a landscape phone a purely flexed surface collapsed to
+ 98px. The floor makes scroll instead of shrinking the map
+ below the point of being one. */}
+
+ {/* Kept mounted while hidden, so coming back costs no reload; the
+ map's ResizeObserver re-measures it when it reappears. */}
+
{selected && (
-
+
)}
diff --git a/src/app/debug/layout.js b/src/app/debug/layout.js
index 3ed351e..9a8c2ca 100644
--- a/src/app/debug/layout.js
+++ b/src/app/debug/layout.js
@@ -7,9 +7,11 @@ import DebugNav from './DebugNav';
// One shell for every debug page, styled like the game screen's app bar, so
// the hub and both tools share identical chrome and only differ in content.
// The column fills the body's sticky-footer layout (viewport minus the global
-// footer strip) with the scroll INSIDE : document-style
-// pages (hub, bbox) scroll there, while the coverage map takes h-full and
-// manages its own panes.
+// footer strip) with the scroll INSIDE : document-style pages (hub,
+// bbox) scroll there, while the coverage map fills it and manages its own
+// panes. is itself a flex column so a full-height page can claim the
+// space with flex-1 -- `h-full` cannot, because main's height comes from
+// flexing inside a min-height container and so is indefinite.
export default function DebugLayout({ children }) {
return (
@@ -35,7 +37,7 @@ export default function DebugLayout({ children }) {
- {children}
+ {children}