diff --git a/changelog.json b/changelog.json index d22cbf8..5e9a062 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.26.23", "date": "", "en": [ - "LoTW: the downloaded confirmations list gains a Station column, so a list spanning several callsigns says which one each confirmation belongs to. Shown only when the report actually carries more than the one station." + "LoTW: the downloaded confirmations list gains a Station column, so a list spanning several callsigns says which one each confirmation belongs to. Shown only when the report actually carries more than the one station.", + "The band maps follow your IARU region (Settings → General): Region 2’s 40 m runs to 7300 kHz with phone from 7125 and its 80 m to 4000, Region 3’s 80 m to 3900 — the band edges and the CW/digital/phone shading all adjust." ], "fr": [ - "LoTW : la liste des confirmations téléchargées gagne une colonne Station, pour savoir à quel indicatif appartient chaque confirmation quand le téléchargement en couvre plusieurs. Affichée seulement si le rapport en contient effectivement." + "LoTW : la liste des confirmations téléchargées gagne une colonne Station, pour savoir à quel indicatif appartient chaque confirmation quand le téléchargement en couvre plusieurs. Affichée seulement si le rapport en contient effectivement.", + "Les cartes de bande suivent votre région IARU (Réglages → Général) : le 40 m de la Région 2 va jusqu'à 7300 kHz avec la phonie dès 7125 et son 80 m jusqu'à 4000, le 80 m de la Région 3 jusqu'à 3900 — les limites de bande et les zones CW/numérique/phonie s'ajustent." ] }, { diff --git a/frontend/src/components/BandMap.tsx b/frontend/src/components/BandMap.tsx index 3df1b81..a33c596 100644 --- a/frontend/src/components/BandMap.tsx +++ b/frontend/src/components/BandMap.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Minus, Plus, Crosshair, X, PanelLeft, PanelRight } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; +import { bandRange, bandSegments, subscribeIaruRegion, type SegMode } from '@/lib/bandplan'; import { spotStatusKey, inferSpotMode, spotModeCategory } from '@/lib/spot'; import { SPOT_MARKERS, activeMarkers } from '@/lib/spotMarkers'; import { applySpotDisplay, readSpotDisplayOptions } from '@/lib/spotDisplay'; @@ -114,59 +115,26 @@ interface Props { keyNav?: boolean; } -const BAND_RANGES: Record = { - '160m': [1800, 2000], - '80m': [3500, 3800], - '60m': [5350, 5450], - '40m': [7000, 7200], - '30m': [10100, 10150], - '20m': [14000, 14350], - '17m': [18068, 18168], - '15m': [21000, 21450], - '12m': [24890, 24990], - '10m': [28000, 29700], - '6m': [50000, 50500], - '4m': [70000, 70500], - '2m': [144000, 146000], - '70cm': [430000, 440000], +// Band edges and mode segments come from lib/bandplan, which knows the +// operator's IARU region (Settings → General): a Region 2 operator's 40 m runs +// to 7300 with phone from 7125, and drawing Region 1's plan under their spots +// cut the top 100 kHz of the band off the map. +// +// Sub-band shading colours: these are IDENTITIES (which mode the segment is +// for), not states, so they take categorical hues — never the status tokens. +// All three are drawn from the cool end of the categorical order and laid down +// at low opacity, so the band plan stays background context while the warm +// spot pills keep the foreground. Checked with the palette validator in both +// themes (violet failed the dark protan step; magenta clears every check). +const SEG_COLOR: Record = { + cw: 'var(--chart-7)', // magenta + digi: 'var(--chart-1)', // blue + phone: 'var(--chart-2)', // aqua }; - -// Sub-band shading: CW / digital / phone. -// -// These are IDENTITIES (which mode the segment is for), not states, so they take -// categorical hues — never the status tokens. They used to use success / info / -// warning, which collided head-on with the spot pills drawn ON TOP of them: amber -// is "new band" in this very component's legend, so the whole SSB portion read as -// a giant "new band" wash. Status colours are reserved for status. -// -// All three are drawn from the COOL end of the categorical order and laid down at -// low opacity, so the band plan stays background context while the warm spot pills -// keep the foreground to themselves. -// Checked with the palette validator in BOTH themes. Violet was the first pick -// for CW and failed on the dark steps — violet and blue land 1.9 ΔE apart for a -// protan reader there, i.e. the same colour. Magenta clears every check on both -// surfaces (worst adjacent pair 13.0 light / 15.9 dark). -const SEG_CW = 'var(--chart-7)'; // magenta -const SEG_DIGI = 'var(--chart-1)'; // blue -const SEG_PHONE = 'var(--chart-2)'; // aqua // A band-plan wash is CONTEXT, not data: it must stay under the spot pills that // are read on top of it. const SEG_OPACITY = 0.13; -const SEGMENT_COLORS: Record = { - '160m': [[1800, 1838, SEG_CW], [1838, 1840, SEG_DIGI], [1840, 2000, SEG_PHONE]], - '80m': [[3500, 3580, SEG_CW], [3580, 3600, SEG_DIGI], [3600, 3800, SEG_PHONE]], - '60m': [[5350, 5450, SEG_PHONE]], - '40m': [[7000, 7040, SEG_CW], [7040, 7100, SEG_DIGI], [7100, 7200, SEG_PHONE]], - '30m': [[10100, 10130, SEG_CW], [10130, 10150, SEG_DIGI]], - '20m': [[14000, 14070, SEG_CW], [14070, 14100, SEG_DIGI], [14100, 14350, SEG_PHONE]], - '17m': [[18068, 18095, SEG_CW], [18095, 18110, SEG_DIGI], [18110, 18168, SEG_PHONE]], - '15m': [[21000, 21070, SEG_CW], [21070, 21150, SEG_DIGI], [21150, 21450, SEG_PHONE]], - '12m': [[24890, 24915, SEG_CW], [24915, 24940, SEG_DIGI], [24940, 24990, SEG_PHONE]], - '10m': [[28000, 28070, SEG_CW], [28070, 28300, SEG_DIGI], [28300, 29700, SEG_PHONE]], - '6m': [[50000, 50100, SEG_CW], [50100, 50500, SEG_PHONE]], -}; - // Small coloured dot + label used in the band-map legend strip. function LegendDot({ cls, colour, label }: { cls?: string; colour?: string; label: string }) { return ( @@ -309,8 +277,11 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz, for (const k of Object.keys(spotStatusRaw)) out[k] = applySpotDisplay(spotStatusRaw[k], dispOpts) as SpotStatusEntry; return out; }, [spotStatusRaw, dispOpts.muteWorked, dispOpts.slotHighlight]); - const range = BAND_RANGES[band]; - const segments = SEGMENT_COLORS[band] ?? []; + // Re-render when the operator changes their IARU region in Settings. + const [, setRegionTick] = useState(0); + useEffect(() => subscribeIaruRegion(() => setRegionTick((n) => n + 1)), []); + const range = bandRange(band); + const segments = bandSegments(band).map(([a, b, m]) => [a, b, SEG_COLOR[m]] as [number, number, string]); const [zoomIdx, setZoomIdx] = useState(() => readZoom(band)); // The docked map follows the rig, so a band change must bring up THAT band's // remembered zoom. @@ -794,9 +765,9 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz, ))} {/* Sub-band shading, so the wash behind the pills is never colour-alone. */} | - - - + + +
{t('bmp.footerHint')} diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 5df819b..4a4816b 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -81,6 +81,7 @@ import { import { cn } from '@/lib/utils'; import { writeUiPref } from '@/lib/uiPref'; import { setUseMiles } from '@/lib/units'; +import { iaruRegion, setIaruRegion, type IaruRegion } from '@/lib/bandplan'; import { getDateFormat, setDateFormat, type DateFormat } from '@/lib/dateFormat'; import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n'; import { useTheme, CONCRETE_THEMES, type ThemeChoice } from '@/lib/theme'; @@ -1749,6 +1750,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged const [lookupOnBlur, setLookupOnBlur] = useState(() => localStorage.getItem('opslog.lookupOnBlur') === '1'); const [groupDigital, setGroupDigital] = useState(() => localStorage.getItem('opslog.groupDigitalSlots') === '1'); const [milesUnit, setMilesUnit] = useState(() => localStorage.getItem('opslog.distanceMiles') === '1'); + const [region, setRegion] = useState(() => iaruRegion()); const [clusterWorkedSameSlot, setClusterWorkedSameSlot] = useState(() => localStorage.getItem('opslog.clusterWorkedSameSlot') === '1'); // Declared HERE and not in ClusterPanel: that renderer is called as a plain // function by the PANELS map, so it must stay hooks-free. @@ -7239,6 +7241,24 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged { const v = !!c; setGroupDigital(v); writeUiPref('opslog.groupDigitalSlots', v ? '1' : '0'); }} /> {t('gen.groupDigital')} {t('gen.groupDigitalHint')} +
+ {/* The IARU region drives the band edges and mode segments the band + maps draw — Region 2's 40 m runs to 7300, Region 1's stops at + 7200 — and nothing else: spots and logging are region-blind. */} + {t('gen.iaruRegion')} + + {t('gen.iaruHint')} +