feat(appearance): the band/mode matrix colours can be chosen

The PH/CW/DIG grid in the Stats panel is the fastest read in the app and its
palette was fixed per theme. Settings -> Appearance now offers the six: the
four status fills, the never-worked fill, and the ring on the cell being
entered.

Stored as OVERRIDES, not as a palette. Each of the twelve themes ships an
--mx-* ramp tuned to its own background, so an operator who only wants a
different green must not thereby freeze the other four to the theme they
happened to be using that day. An empty value means "whatever the theme
says"; the chosen ones are stamped inline on <html>, where they win over
every theme; switching the feature off hands the colours straight back.

The pickers are seeded from what the matrix is painting at that moment
rather than from a fixed palette, so the choice starts from the colours in
front of the operator. A new --mx-cur token carries the current-entry ring:
it follows --warning by default, so it stays theme-correct on all twelve,
but can be recoloured without dragging every other warning in the app along.

The legend under the matrix and its cell tooltips were hardcoded English.
They now go through t() with the same keys as the pickers, so the grid and
the settings cannot disagree about which green is which.
This commit is contained in:
2026-08-17 16:23:39 +02:00
parent 7be6f64596
commit 5e80c27f61
11 changed files with 348 additions and 32 deletions
+23 -19
View File
@@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { sunTimes } from '@/lib/sun';
import { isQSLConfirmed } from '@/lib/qsl';
import { useI18n } from '@/lib/i18n';
import { BandSlotQSOs } from '../../wailsjs/go/main/App';
import type { WorkedBeforeView } from '@/types';
@@ -76,28 +77,31 @@ const STATUS_CLASSES: Record<string, string> = {
dxcc_w: 'bg-mx-dx-work',
};
// Legend entries, in the same colour order as the cells. swatch = the
// background class (or a special ring marker for the current-entry cell).
// Legend entries, in the same colour order as the cells — and the same order and
// i18n keys the Appearance panel's colour pickers use, so the two can never
// disagree about which green is which. swatch = the background class (or a
// special ring marker for the current-entry cell).
const LEGEND: { swatch: string; ring?: boolean; label: string }[] = [
{ swatch: 'bg-mx-call-conf', label: 'Call confirmed' },
{ swatch: 'bg-mx-call-work', label: 'Call worked' },
{ swatch: 'bg-mx-dx-conf', label: 'Entity confirmed' },
{ swatch: 'bg-mx-dx-work', label: 'Entity worked' },
{ swatch: 'bg-mx-none', label: 'Not worked' },
{ swatch: 'bg-mx-none', ring: true, label: 'Current entry' },
{ swatch: 'bg-mx-call-conf', label: 'mx.callConf' },
{ swatch: 'bg-mx-call-work', label: 'mx.callWork' },
{ swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
{ swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
{ swatch: 'bg-mx-none', label: 'mx.none' },
{ swatch: 'bg-mx-none', ring: true, label: 'mx.current' },
];
function cellTitle(band: string, cls: string, status: string, current: boolean): string {
function cellTitle(t: (k: string) => string, band: string, cls: string, status: string, current: boolean): string {
const desc =
status === 'call_c' ? 'This callsign confirmed' :
status === 'call_w' ? 'This callsign worked (not confirmed)' :
status === 'dxcc_c' ? 'Entity confirmed (other callsign)' :
status === 'dxcc_w' ? 'Entity worked (other callsign)' :
'Never worked';
return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`;
status === 'call_c' ? t('mx.tipCallConf') :
status === 'call_w' ? t('mx.tipCallWork') :
status === 'dxcc_c' ? t('mx.tipDxConf') :
status === 'dxcc_w' ? t('mx.tipDxWork') :
t('mx.tipNone');
return `${band} ${cls}: ${desc}${current ? ' — ' + t('mx.current') : ''}`;
}
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true, lat, lon, forCall, onEditQso }: Props) {
const { t } = useI18n();
// Cell drill-down: which band+class the operator clicked, or null.
const [slot, setSlot] = useState<{ band: string; cls: string } | null>(null);
// Columns from the operator's configured bands (so the matrix shows only the
@@ -308,7 +312,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
return (
<td
key={b.tag}
title={cellTitle(b.tag, cls, st, isCurrent) + (st ? ' — click to list the QSOs' : '')}
title={cellTitle(t, b.tag, cls, st, isCurrent) + (st ? ' — ' + t('mx.tipClick') : '')}
onClick={st ? () => setSlot({ band: b.tag, cls }) : undefined}
className={cn(
'w-[28px] h-[24px] rounded transition-colors p-0',
@@ -316,7 +320,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
// Only a filled cell has anything to show — an empty one
// stays inert rather than opening a "no QSOs" dialog.
st && 'cursor-pointer hover:brightness-110',
isCurrent && 'ring-2 ring-warning ring-inset',
isCurrent && 'ring-2 ring-mx-cur ring-inset',
)}
/>
);
@@ -335,10 +339,10 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
className={cn(
'inline-block size-3 rounded shrink-0',
l.swatch,
l.ring && 'ring-2 ring-warning ring-inset',
l.ring && 'ring-2 ring-mx-cur ring-inset',
)}
/>
{l.label}
{t(l.label)}
</span>
))}
</div>