chore: release v0.26.3

This commit is contained in:
2026-08-21 23:28:55 +02:00
parent 0cd243fe00
commit d48420485f
31 changed files with 1908 additions and 164 deletions
+49 -16
View File
@@ -25,6 +25,16 @@ interface Spot {
band?: string;
comment?: string;
spotter?: string;
// The park reference the POTA poller tagged this spot with. The map never
// reads it — the click handler upstream turns it into the QSO's POTA award
// reference — but it is declared so it cannot be lost by accident.
//
// It travelled only because the spot objects are passed through by reference
// and the type was erased. The first refactor to copy a spot field by field
// would have dropped the park silently, and the only symptom would be an
// activation logged without its reference. Same mistake as the status entry
// below, whose extra markers were arriving and undeclared.
pota_ref?: string;
}
// The FULL status entry, not the two fields the map used to declare. The extra
@@ -83,11 +93,12 @@ interface Props {
// globally from the band-map tab toolbar.
hideDigital?: boolean;
fitToBand?: boolean;
// onToggleFit turns the zoom readout into the fit-to-band switch. Only the
// docked map passes it: the Band Map tab has the same control in its own
// toolbar, above maps that all obey it at once, and a second switch inside
// each card would be four ways to change one setting.
// onToggleFit and onToggleHideDigital add the two switches to this map's own
// header. Only the docked map passes them: the Band Map tab has both in its
// toolbar, above maps that all obey them at once, and a switch inside each
// card as well would be four ways to change one setting.
onToggleFit?: () => void;
onToggleHideDigital?: () => void;
// Mark stations that upload to LoTW (Settings → Appearance).
showLotw?: boolean;
// keyNav enables Ctrl+↑ / Ctrl+↓ to hop to the next spot above / below the rig
@@ -164,6 +175,7 @@ function LegendDot({ cls, colour, label }: { cls?: string; colour?: string; labe
function statusLabel(s: string, t: (k: string) => string): string {
switch (s) {
case 'new': return t('bmp.statusNew');
case 'new-band-mode': return t('bmp.statusNewBandMode');
case 'new-band': return t('bmp.statusNewBand');
case 'new-mode': return t('bmp.statusNewMode');
case 'new-slot': return t('bmp.statusNewSlot');
@@ -191,7 +203,11 @@ function statusStyle(s: string): { pill: string; bar: string; line: string; dot:
// line = SVG leader stroke (visible on hover)
// dot = small marker on the freq scale
switch (s) {
case 'new': return {
// NEW and NEW BAND+MODE share the strongest colour: both mean "there is
// something here you have never had", and a sixth shade in a palette read at
// a glance is a shade nobody can name.
case 'new':
case 'new-band-mode': return {
pill: 'bg-danger-muted text-danger-muted-foreground border-danger-border hover:bg-danger-muted',
bar: 'bg-danger',
line: 'stroke-danger',
@@ -268,7 +284,7 @@ const BOT_PAD = 14; // the top-most freq label isn't clipped at y=0
// last; ties broken by closeness to the rig freq).
const MAX_VISIBLE_SPOTS = 30;
export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false, onToggleFit, keyNav = false, showLotw = false }: Props) {
export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false, onToggleFit, onToggleHideDigital, keyNav = false, showLotw = false }: Props) {
const { t } = useI18n();
// The two display options are applied ONCE here, on the whole map, so the
@@ -366,6 +382,7 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
const k = spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz);
switch (spotStatus[k]?.status ?? '') {
case 'new': return 0;
case 'new-band-mode': return 0;
case 'new-band': return 1;
case 'new-slot': return 2;
case 'new-call': return 2;
@@ -564,16 +581,18 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
title={t('bmp.zoomOut')}>
<Minus className="size-3" />
</button>
{/* The readout IS the switch when the parent offers one — the header is
already tight with four maps side by side, and a separate chip would
cost width to say what this text says anyway. */}
{onToggleFit ? (
<button type="button" onClick={onToggleFit} title={t('bmp.fitTitle')}
className={cn('shrink-0 font-mono text-[10px] normal-case tracking-normal whitespace-nowrap px-1 rounded border transition-colors',
fitToBand ? 'border-primary bg-primary text-primary-foreground' : 'border-border hover:bg-muted')}>
{fitToBand ? t('bmp.fit') : `${pxPerKHz}px/kHz`}
</button>
) : (
{/* A readout, not a switch.
It WAS the switch for a day: clicking the px/kHz value toggled
fit-to-band, and in fit it read "FIT". Nobody found it — a control
whose label is a measurement does not look like a control, and once
switched off there was no longer anything on screen saying the option
existed. The switches below say what they are. */}
{/* In fit mode the readout has nothing left to say — the lit FIT button
says it, and printing the word twice side by side is just noise. The
Band Map tab's cards have no button, so there the readout keeps
reporting both states. */}
{(!fitToBand || !onToggleFit) && (
<span className="shrink-0 font-mono text-[10px] normal-case tracking-normal whitespace-nowrap px-0.5">{fitToBand ? t('bmp.fit') : `${pxPerKHz}px/kHz`}</span>
)}
<button type="button" onClick={() => changeZoom(1)} disabled={fitToBand || zoomIdx === PX_PER_KHZ.length - 1}
@@ -581,6 +600,20 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
title={t('bmp.zoomIn')}>
<Plus className="size-3" />
</button>
{onToggleFit && (
<button type="button" onClick={onToggleFit} title={t('bmp.fitTitle')}
className={cn('shrink-0 text-[10px] font-semibold normal-case tracking-normal px-1 rounded border transition-colors',
fitToBand ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
{t('bmp.fit')}
</button>
)}
{onToggleHideDigital && (
<button type="button" onClick={onToggleHideDigital} title={t('bmp.hideFtTitle')}
className={cn('shrink-0 text-[10px] font-semibold normal-case tracking-normal px-1 rounded border transition-colors',
hideDigital ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
{t('bmp.ftx')}
</button>
)}
<button type="button" onClick={recenterOnRig}
className="size-5 inline-flex items-center justify-center rounded hover:bg-muted"
title={t('bmp.scrollToRig')}>