feat(bandmap): show new POTA, new county and worked-callsign

The band map coloured only the entity status and dropped the other three
markers on the floor — not for want of data: they were already in the status
entry it receives, the local type simply never declared them, so a new park on
an entity you have worked was indistinguishable from any other worked spot.

Colours are the DX-cluster list's, taken from it rather than chosen: --info for
a worked callsign, --success for a new county and a new park. The same fact must
not be blue in one panel and green in the next.

They STACK, they do not replace. These markers are orthogonal to the entity
status — worked entity plus new park is an ordinary combination — and the
cluster list already spells them out side by side rather than letting one win.
So the pill keeps the status colour and the markers take its left strip, split
into one segment each. That strip previously repeated the pill's own colour and
carried no information at all, which is what made it the right place.

Note that a new county and a new park share --success in the cluster list, so
they share it here too. Splitting them means changing both views together, which
is the moment to do it — when the per-marker colour setting arrives.
This commit is contained in:
2026-08-09 21:01:54 +02:00
parent ec71dd1661
commit 4dd2c3b997
3 changed files with 55 additions and 6 deletions
+51 -4
View File
@@ -24,7 +24,39 @@ interface Spot {
spotter?: string;
}
type SpotStatusEntry = { status: string; country?: string };
// The FULL status entry, not the two fields the map used to declare. The extra
// markers were already arriving in this object — the local type simply never
// mentioned them, so they could not be drawn.
type SpotStatusEntry = {
status?: string;
country?: string;
worked_call?: boolean;
new_county?: boolean;
new_pota?: boolean;
new_pfx?: boolean;
};
// Extra markers, ORTHOGONAL to the entity status: a spot can be a worked entity
// AND a new park. The cluster grid stacks them as separate badges rather than
// letting one replace another, so the map stacks them too — as segments of the
// pill's left accent bar, which until now only repeated the pill's own colour
// and carried no information of its own.
//
// Colours are the cluster grid's, deliberately: the same fact must not be blue
// in one panel and green in the next. NOTE that a new county and a new park
// share --success there, so they share it here.
const EXTRA_MARKERS: { key: 'new_pota' | 'new_county' | 'worked_call'; cls: string; label: string }[] = [
{ key: 'new_pota', cls: 'bg-success', label: 'bmp.legendNewPota' },
{ key: 'new_county', cls: 'bg-success', label: 'bmp.legendNewCounty' },
{ key: 'worked_call', cls: 'bg-info', label: 'bmp.legendWorkedCall' },
];
// markersFor returns the extra markers active on a spot, in a fixed order so
// the bar reads the same way every time.
function markersFor(e: SpotStatusEntry | undefined): typeof EXTRA_MARKERS {
if (!e) return [];
return EXTRA_MARKERS.filter((m) => !!e[m.key]);
}
interface Props {
band: string;
@@ -567,10 +599,21 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
'hover:translate-x-0.5 hover:shadow',
style.pill,
)}
title={`${p.spot.dx_call}${entry?.country ? ' · ' + entry.country : ''} · ${p.spot.freq_khz.toFixed(1)} kHz · ${statusLabel(st, t)}${p.spot.comment ? ' · ' + p.spot.comment : ''}${p.spot.spotter ? ' · de ' + p.spot.spotter : ''}`}
title={`${p.spot.dx_call}${entry?.country ? ' · ' + entry.country : ''} · ${p.spot.freq_khz.toFixed(1)} kHz · ${statusLabel(st, t)}${markersFor(entry).map((m) => ' · ' + t(m.label)).join('')}${p.spot.comment ? ' · ' + p.spot.comment : ''}${p.spot.spotter ? ' · de ' + p.spot.spotter : ''}`}
>
{/* Status accent strip on the left */}
<span className={cn('w-1 shrink-0', style.bar)} aria-hidden />
{/* Left accent strip. With no extra marker it repeats the status
colour, exactly as before; otherwise it splits into one
segment per marker, so "worked entity + new park" shows both
instead of one hiding the other. */}
{(() => {
const marks = markersFor(entry);
if (marks.length === 0) return <span className={cn('w-1 shrink-0', style.bar)} aria-hidden />;
return (
<span className="w-1 shrink-0 flex flex-col" aria-hidden>
{marks.map((m) => <span key={m.key} className={cn('flex-1', m.cls)} />)}
</span>
);
})()}
<span className="flex items-center gap-1.5 px-2 font-mono text-[11px] font-bold leading-none">
<span>{p.spot.dx_call}</span>
{mode && (
@@ -590,6 +633,10 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
<LegendDot cls="bg-warning" label={t('bmp.legendNewBand')} />
<LegendDot cls="bg-caution" label={t('bmp.legendNewSlot')} />
<LegendDot cls="bg-muted-foreground/30" label={t("bmp.legendWorked")} />
{/* The stacked markers, same colours as the cluster grid. */}
<LegendDot cls="bg-success" label={t('bmp.legendNewPota')} />
<LegendDot cls="bg-success" label={t('bmp.legendNewCounty')} />
<LegendDot cls="bg-info" label={t('bmp.legendWorkedCall')} />
{/* Sub-band shading, so the wash behind the pills is never colour-alone. */}
<span className="mx-0.5 opacity-40">|</span>
<LegendDot colour={SEG_CW} label={t("bmp.legendCW")} />