mirror of
https://github.com/tiennm99/vngeoguessr.git
synced 2026-10-15 10:15:51 +00:00
refactor(debug): float the coverage inspector over a full-width map
The 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. From lg the inspector now floats over the map's right edge; a phone has neither the width to float into nor the height to split, so there it takes the surface and its close button brings the map back. The map keeps a 16rem floor below lg: the page chrome takes 225px, so a purely flexed surface collapsed to 98px on a landscape phone. Escape closes the inspector unless a select owns the keypress, a ResizeObserver re-measures the map when it reappears, and the tile attribution moves to the corner the inspector does not cover.
This commit is contained in:
1 parent
23ee9d47ff
commit
26dd5f384e
3 files changed
+77
-12
No files matched your search
@@ -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 <div ref={containerRef} className="h-full w-full rounded-lg bg-muted" />;
|
||||
// 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 <div ref={containerRef} className="absolute inset-0 isolate bg-muted" />;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const CoverageMap = dynamic(() => import('./CoverageMap'), {
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="flex h-full w-full items-center justify-center rounded-lg bg-muted text-muted-foreground"
|
||||
className="absolute inset-0 flex items-center justify-center bg-muted text-muted-foreground"
|
||||
>
|
||||
Loading map...
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{/* Same title pattern as the other debug pages, with the tool's own
|
||||
controls beside it. The shared debug layout owns the app chrome. */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border bg-card/60 px-4 py-2">
|
||||
@@ -185,8 +200,25 @@ export default function CoveragePage() {
|
||||
{loading && <span className="text-xs text-muted-foreground">loading…</span>}
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 gap-3 p-3 lg:grid-cols-2">
|
||||
<div className="min-h-[40dvh] lg:min-h-0">
|
||||
{/* 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 <main> scroll instead of shrinking the map
|
||||
below the point of being one. */}
|
||||
<div className="flex min-h-64 flex-1 flex-col lg:relative lg:block lg:min-h-0">
|
||||
{/* Kept mounted while hidden, so coming back costs no reload; the
|
||||
map's ResizeObserver re-measures it when it reappears. */}
|
||||
<div
|
||||
className={`relative min-h-0 flex-1 lg:absolute lg:inset-0 lg:block ${
|
||||
selected ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<CoverageMap
|
||||
boundary={boundary}
|
||||
panos={panos}
|
||||
@@ -197,7 +229,13 @@ export default function CoveragePage() {
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="flex min-h-[40dvh] flex-col overflow-hidden rounded-lg border border-border bg-card lg:min-h-0">
|
||||
<aside
|
||||
aria-label={`Panorama ${selected.id}`}
|
||||
// Bottom sheet on phones, side panel from lg: covering the map
|
||||
// from the edge is what map tools do, and it leaves the clicked
|
||||
// dot exactly where it was.
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden border-t border-border bg-card lg:absolute lg:inset-y-3 lg:right-3 lg:z-(--z-floating) lg:h-auto lg:w-[26rem] lg:rounded-xl lg:border lg:shadow-xl"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-foreground">{selected.id}</p>
|
||||
@@ -226,7 +264,7 @@ export default function CoveragePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 bg-neutral-900">
|
||||
<div className="relative min-h-0 flex-1 bg-neutral-900">
|
||||
{panoError ? (
|
||||
<div role="alert" className="flex h-full items-center justify-center p-4 text-center text-sm text-destructive">
|
||||
{panoError}
|
||||
@@ -243,7 +281,7 @@ export default function CoveragePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 <main>: 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 <main>: document-style pages (hub,
|
||||
// bbox) scroll there, while the coverage map fills it and manages its own
|
||||
// panes. <main> 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 (
|
||||
<div className="flex flex-1 min-h-0 flex-col vn-surface">
|
||||
@@ -35,7 +37,7 @@ export default function DebugLayout({ children }) {
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-y-auto">{children}</main>
|
||||
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user