fix(ftmap): tell Leaflet when its box changes size; fix(decodes): all seven continents

The FT map drew a strip of dead space along the bottom after a zoom.
Leaflet measures its container ONCE, at creation — which for this panel
is the instant the tab is selected, before the flex layout has settled —
and then asks for tiles to fit that stale size for ever. A ResizeObserver
hands it the real size, plus one pass after the first paint for the
layout that settles without ever firing a resize.

And the decode list's continent chips are the seven, fixed. Building
them from the feed meant a row that reshuffled itself every period — a
chip appeared when the first Asian station decoded and moved everything
sideways under the pointer — and it could not say what the filter was
capable of until something had been heard.
This commit is contained in:
2026-09-01 23:19:18 +02:00
parent a6172c4323
commit 4d8cb58550
3 changed files with 39 additions and 16 deletions
+14 -13
View File
@@ -121,6 +121,9 @@ interface Props {
// that starts by hiding most of the band would be lying about what is on it.
type NewCat = 'dxcc' | 'band' | 'mode' | 'slot' | 'pfx' | 'grid' | 'pota' | 'cty' | 'state';
// The seven, in the order an operator reads them.
const CONTINENTS = ['AF', 'AN', 'AS', 'EU', 'NA', 'OC', 'SA'];
const NEW_CATS: { key: NewCat; label: string; colour: string }[] = [
{ key: 'dxcc', label: 'dec.stNew', colour: 'var(--success)' },
{ key: 'band', label: 'dec.stBand', colour: 'var(--warning)' },
@@ -630,27 +633,25 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, o
// dropdowns were pure furniture for most operators; they appear the day a
// second instance puts a second band on the link, which is the only day they
// mean anything.
const { bands, modes, conts, instances } = useMemo(() => {
const b = new Set<string>(), m = new Set<string>(), c = new Set<string>(), i = new Set<string>();
const { bands, modes, instances } = useMemo(() => {
const b = new Set<string>(), m = new Set<string>(), i = new Set<string>();
for (const d of decodes) {
if (d.band) b.add(d.band);
if (d.mode) m.add(d.mode);
if (d.instance) i.add(d.instance);
const ct = statusOf(d)?.continent;
if (ct) c.add(ct);
}
return { bands: [...b].sort(), modes: [...m].sort(), conts: [...c].sort(), instances: [...i].sort() };
return { bands: [...b].sort(), modes: [...m].sort(), instances: [...i].sort() };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [decodes, spotStatus]);
// The chips are the continents on the feed — a selector with nothing to choose
// is furniture — PLUS anything currently selected. Without that second half a
// filter can strand itself: pick AF, the last African station stops decoding,
// and the list empties with no chip left to switch it back off.
const contChips = useMemo(
() => [...new Set([...conts, ...contList])].sort(),
[conts, contList],
);
// All seven, always, in their usual order.
//
// The chips used to be built from the continents ON the feed, which read as a
// list that reshuffled itself every period: a chip appeared when the first
// Asian station decoded and moved everything sideways under the pointer. A
// fixed row can be aimed at — you learn where OC is and it stays there — and
// it also says what the filter can do before anything has been heard.
const contChips = CONTINENTS;
const filtered = useMemo(() => {
const q = search.trim().toUpperCase();
+19 -1
View File
@@ -62,7 +62,25 @@ export function FTMapPanel({ decodes, myGrid }: { decodes: FTMapDecode[]; myGrid
});
mapRef.current = m;
layerRef.current = L.layerGroup().addTo(m);
return () => { m.remove(); mapRef.current = null; layerRef.current = null; };
// Leaflet measures its container ONCE, when the map is created, and then
// draws tiles for that size for ever. This panel is mounted the moment its
// tab is selected — before the flex layout has settled — and the window can
// be resized under it, so the stale measurement showed as a strip of dead
// space along the bottom where tiles were never asked for. The observer
// hands it the real size whenever the box changes.
const ro = new ResizeObserver(() => m.invalidateSize({ animate: false }));
ro.observe(divRef.current);
// Once more after the first paint: the first observation can arrive while
// the panel is still zero-height, and no further resize follows a layout
// that settles by itself.
const settle = window.setTimeout(() => m.invalidateSize({ animate: false }), 100);
return () => {
window.clearTimeout(settle);
ro.disconnect();
m.remove();
mapRef.current = null;
layerRef.current = null;
};
}, []);
// Basemap follows the picker.