feat(appearance): give the worked-with-this-call dot its own two colours

The dot is drawn over all five cell backgrounds, so it cannot borrow one of
their colours; it defaults to the theme foreground with a background ring and
now takes two overrides of its own, worked and confirmed, alongside the rest of
the matrix palette.
This commit is contained in:
2026-08-27 15:29:01 +02:00
parent b3547364d4
commit f357dad629
8 changed files with 51 additions and 16 deletions
+7
View File
@@ -174,6 +174,11 @@ type MatrixColors struct {
EntityWorked string `json:"entity_worked"` EntityWorked string `json:"entity_worked"`
NotWorked string `json:"not_worked"` NotWorked string `json:"not_worked"`
CurrentEntry string `json:"current_entry"` CurrentEntry string `json:"current_entry"`
// The dot marking a slot already worked with the callsign in hand. It is a
// mark rather than a background, so it needs its own two colours: it is
// drawn on top of all five of the above and must stay legible over each.
MarkWorked string `json:"mark_worked"`
MarkConfirmed string `json:"mark_confirmed"`
} }
// normMatrixColors keeps only plain hex values. Anything else becomes "" — i.e. // normMatrixColors keeps only plain hex values. Anything else becomes "" — i.e.
@@ -196,6 +201,8 @@ func normMatrixColors(c MatrixColors) MatrixColors {
EntityWorked: clean(c.EntityWorked), EntityWorked: clean(c.EntityWorked),
NotWorked: clean(c.NotWorked), NotWorked: clean(c.NotWorked),
CurrentEntry: clean(c.CurrentEntry), CurrentEntry: clean(c.CurrentEntry),
MarkWorked: clean(c.MarkWorked),
MarkConfirmed: clean(c.MarkConfirmed),
} }
} }
+2 -2
View File
@@ -4,11 +4,11 @@
"date": "", "date": "",
"en": [ "en": [
"Each radio carries its own MY_RIG (Settings → CAT), written on every QSO made with it — ahead of the per-band station in Operating conditions, which says what you planned to use rather than which radio is keying. Left empty, nothing changes.", "Each radio carries its own MY_RIG (Settings → CAT), written on every QSO made with it — ahead of the per-band station in Operating conditions, which says what you planned to use rather than which radio is keying. Left empty, nothing changes.",
"Worked-before matrix: a dot in the corner of a cell now means the callsign you are working has already been worked on that slot, whatever colour the entity status gave the cell." "Worked-before matrix: a dot in the corner of a cell now means the callsign you are working has already been worked on that slot, whatever colour the entity status gave the cell. Its two colours (worked / confirmed with this callsign) are in Appearance with the other matrix colours."
], ],
"fr": [ "fr": [
"Chaque radio porte son propre MY_RIG (Réglages → CAT), inscrit sur chaque QSO fait avec elle — avant la station par bande des Conditions de trafic, qui dit ce qui était prévu et non quelle radio émet. Laissé vide, rien ne change.", "Chaque radio porte son propre MY_RIG (Réglages → CAT), inscrit sur chaque QSO fait avec elle — avant la station par bande des Conditions de trafic, qui dit ce qui était prévu et non quelle radio émet. Laissé vide, rien ne change.",
"Matrice des contacts : un point dans le coin d'une case indique que l'indicatif en cours a déjà été contacté sur ce créneau, quelle que soit la couleur donnée par le statut de l'entité." "Matrice des contacts : un point dans le coin d'une case indique que l'indicatif en cours a déjà été contacté sur ce créneau, quelle que soit la couleur donnée par le statut de l'entité. Ses deux couleurs (contacté / confirmé avec cet indicatif) se règlent dans Apparence avec les autres couleurs de la matrice."
] ]
}, },
{ {
@@ -127,6 +127,12 @@ function MatrixColorsSection() {
<span className="inline-block w-7 h-5 rounded bg-mx-dx-work" /> <span className="inline-block w-7 h-5 rounded bg-mx-dx-work" />
<span className="inline-block w-7 h-5 rounded bg-mx-none" /> <span className="inline-block w-7 h-5 rounded bg-mx-none" />
<span className="inline-block w-7 h-5 rounded bg-mx-none ring-2 ring-mx-cur ring-inset" /> <span className="inline-block w-7 h-5 rounded bg-mx-none ring-2 ring-mx-cur ring-inset" />
<span className="relative inline-block w-7 h-5 rounded bg-mx-dx-conf">
<span className="absolute top-[4px] right-[4px] size-[5px] rounded-full bg-mx-mark-work ring-1 ring-background" />
</span>
<span className="relative inline-block w-7 h-5 rounded bg-mx-dx-conf">
<span className="absolute top-[4px] right-[4px] size-[5px] rounded-full bg-mx-mark-conf ring-1 ring-background" />
</span>
</div> </div>
<button type="button" onClick={reset} <button type="button" onClick={reset}
+17 -11
View File
@@ -81,26 +81,32 @@ const STATUS_CLASSES: Record<string, string> = {
// i18n keys the Appearance panel's colour pickers use, so the two can never // 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 // disagree about which green is which. swatch = the background class (or a
// special ring marker for the current-entry cell). // special ring marker for the current-entry cell).
const LEGEND: { swatch: string; ring?: boolean; mark?: boolean; label: string }[] = [ const LEGEND: { swatch: string; ring?: boolean; mark?: string; label: string }[] = [
{ swatch: 'bg-mx-call-conf', label: 'mx.callConf' }, { swatch: 'bg-mx-call-conf', label: 'mx.callConf' },
{ swatch: 'bg-mx-call-work', label: 'mx.callWork' }, { swatch: 'bg-mx-call-work', label: 'mx.callWork' },
{ swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' }, { swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
{ swatch: 'bg-mx-dx-work', label: 'mx.dxWork' }, { swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
{ swatch: 'bg-mx-none', label: 'mx.none' }, { swatch: 'bg-mx-none', label: 'mx.none' },
{ swatch: 'bg-mx-none', ring: true, label: 'mx.current' }, { swatch: 'bg-mx-none', ring: true, label: 'mx.current' },
{ swatch: 'bg-mx-none', mark: true, label: 'mx.thisCall' }, { swatch: 'bg-mx-none', mark: 'w', label: 'mx.markWork' },
{ swatch: 'bg-mx-none', mark: 'c', label: 'mx.markConf' },
]; ];
// CallMark — "this callsign has already been worked on this slot". // CallMark — "this callsign has already been worked on this slot".
// //
// Drawn exactly the same way on every cell, whatever colour the entity status // Drawn the same way on every cell, whatever colour the entity status gave it:
// gave it. That is the point: the operator learns one shape, and reads it // the operator learns one shape and reads it without first working out what the
// without first working out what the background means. The dot is the theme // background means. Only the fill changes, and only with the callsign's own
// foreground with a ring in the theme background, so it stays legible over all // state (worked / confirmed) — never with the entity's. The ring is the theme
// five cell colours instead of needing a colour per case. // background, which is what keeps the dot legible over all five cell colours.
function CallMark() { function CallMark({ state = 'w' }: { state?: string }) {
return ( return (
<span className="pointer-events-none absolute top-[4px] right-[4px] size-[5px] rounded-full bg-foreground ring-1 ring-background" /> <span
className={cn(
'pointer-events-none absolute top-[4px] right-[4px] size-[5px] rounded-full ring-1 ring-background',
state === 'c' ? 'bg-mx-mark-conf' : 'bg-mx-mark-work',
)}
/>
); );
} }
@@ -354,7 +360,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
isCurrent && 'ring-2 ring-mx-cur ring-inset', isCurrent && 'ring-2 ring-mx-cur ring-inset',
)} )}
> >
{mine ? <CallMark /> : null} {mine ? <CallMark state={mine} /> : null}
</td> </td>
); );
})} })}
@@ -375,7 +381,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCal
l.ring && 'ring-2 ring-mx-cur ring-inset', l.ring && 'ring-2 ring-mx-cur ring-inset',
)} )}
> >
{l.mark ? <CallMark /> : null} {l.mark ? <CallMark state={l.mark} /> : null}
</span> </span>
{t(l.label)} {t(l.label)}
</span> </span>
+2 -2
View File
@@ -127,7 +127,7 @@ const en: Dict = {
'mx.tipCallConf': 'This callsign confirmed', 'mx.tipCallWork': 'This callsign worked (not confirmed)', 'mx.tipCallConf': 'This callsign confirmed', 'mx.tipCallWork': 'This callsign worked (not confirmed)',
'mx.tipDxConf': 'Entity confirmed (other callsign)', 'mx.tipDxWork': 'Entity worked (other callsign)', 'mx.tipDxConf': 'Entity confirmed (other callsign)', 'mx.tipDxWork': 'Entity worked (other callsign)',
'mx.tipNone': 'Never worked', 'mx.tipClick': 'click to list the QSOs', 'mx.tipNone': 'Never worked', 'mx.tipClick': 'click to list the QSOs',
'mx.thisCall': 'this callsign', 'mx.markWork': 'This callsign worked', 'mx.markConf': 'This callsign confirmed',
'mx.tipThisCall': 'already worked with this callsign', 'mx.tipThisCall': 'already worked with this callsign',
'mx.tipThisCallConf': 'already confirmed with this callsign', 'mx.tipThisCallConf': 'already confirmed with this callsign',
// FTx decodes panel (Tools -> FT decodes) // FTx decodes panel (Tools -> FT decodes)
@@ -616,7 +616,7 @@ const fr: Dict = {
'mx.tipCallConf': 'Cet indicatif est confirmé', 'mx.tipCallWork': 'Cet indicatif est contacté (non confirmé)', 'mx.tipCallConf': 'Cet indicatif est confirmé', 'mx.tipCallWork': 'Cet indicatif est contacté (non confirmé)',
'mx.tipDxConf': 'Entité confirmée (autre indicatif)', 'mx.tipDxWork': 'Entité contactée (autre indicatif)', 'mx.tipDxConf': 'Entité confirmée (autre indicatif)', 'mx.tipDxWork': 'Entité contactée (autre indicatif)',
'mx.tipNone': 'Jamais contacté', 'mx.tipClick': 'cliquer pour lister les QSO', 'mx.tipNone': 'Jamais contacté', 'mx.tipClick': 'cliquer pour lister les QSO',
'mx.thisCall': 'cet indicatif', 'mx.markWork': 'Cet indicatif contacté', 'mx.markConf': 'Cet indicatif confirmé',
'mx.tipThisCall': 'déjà contacté avec cet indicatif', 'mx.tipThisCall': 'déjà contacté avec cet indicatif',
'mx.tipThisCallConf': 'déjà confirmé avec cet indicatif', 'mx.tipThisCallConf': 'déjà confirmé avec cet indicatif',
// Panneau des decodes FTx (Outils -> Decodes FT) // Panneau des decodes FTx (Outils -> Decodes FT)
+6 -1
View File
@@ -14,9 +14,11 @@ export type MatrixColors = {
entity_worked: string; entity_worked: string;
not_worked: string; not_worked: string;
current_entry: string; current_entry: string;
mark_worked: string;
mark_confirmed: string;
}; };
// The six settings fields and the CSS custom property each one drives. Also the // The settings fields and the CSS custom property each one drives. Also the
// display order — the same order the legend under the matrix reads in, so the // display order — the same order the legend under the matrix reads in, so the
// settings panel and the grid can never disagree about which green is which. // settings panel and the grid can never disagree about which green is which.
export const MATRIX_VARS: { key: keyof Omit<MatrixColors, 'enabled'>; cssVar: string; label: string }[] = [ export const MATRIX_VARS: { key: keyof Omit<MatrixColors, 'enabled'>; cssVar: string; label: string }[] = [
@@ -26,12 +28,15 @@ export const MATRIX_VARS: { key: keyof Omit<MatrixColors, 'enabled'>; cssVar: st
{ key: 'entity_worked', cssVar: '--mx-dx-work', label: 'mx.dxWork' }, { key: 'entity_worked', cssVar: '--mx-dx-work', label: 'mx.dxWork' },
{ key: 'not_worked', cssVar: '--mx-none', label: 'mx.none' }, { key: 'not_worked', cssVar: '--mx-none', label: 'mx.none' },
{ key: 'current_entry', cssVar: '--mx-cur', label: 'mx.current' }, { key: 'current_entry', cssVar: '--mx-cur', label: 'mx.current' },
{ key: 'mark_worked', cssVar: '--mx-mark-work', label: 'mx.markWork' },
{ key: 'mark_confirmed', cssVar: '--mx-mark-conf', label: 'mx.markConf' },
]; ];
export const emptyMatrixColors = (): MatrixColors => ({ export const emptyMatrixColors = (): MatrixColors => ({
enabled: false, enabled: false,
call_confirmed: '', call_worked: '', entity_confirmed: '', call_confirmed: '', call_worked: '', entity_confirmed: '',
entity_worked: '', not_worked: '', current_entry: '', entity_worked: '', not_worked: '', current_entry: '',
mark_worked: '', mark_confirmed: '',
}); });
// applyMatrixColors stamps (or clears) the overrides on <html>. Safe to call as // applyMatrixColors stamps (or clears) the overrides on <html>. Safe to call as
+7
View File
@@ -91,6 +91,11 @@
be recoloured on its own without dragging every other warning in the app be recoloured on its own without dragging every other warning in the app
with it (Appearance → matrix colours). */ with it (Appearance → matrix colours). */
--mx-cur: var(--warning); --mx-cur: var(--warning);
/* The "worked with this callsign" dot. Declared ONCE, like --mx-cur: it is
drawn over every one of the five cell colours, so it follows the theme's own
foreground/background pair rather than a per-theme colour of its own. */
--mx-mark-work: var(--foreground);
--mx-mark-conf: var(--foreground);
--scrollbar-thumb: #b8a880; --scrollbar-thumb: #b8a880;
--scrollbar-thumb-hover: #968455; --scrollbar-thumb-hover: #968455;
@@ -981,6 +986,8 @@
--color-mx-dx-work: var(--mx-dx-work); --color-mx-dx-work: var(--mx-dx-work);
--color-mx-none: var(--mx-none); --color-mx-none: var(--mx-none);
--color-mx-cur: var(--mx-cur); --color-mx-cur: var(--mx-cur);
--color-mx-mark-work: var(--mx-mark-work);
--color-mx-mark-conf: var(--mx-mark-conf);
--radius: 0.5rem; --radius: 0.5rem;
+4
View File
@@ -3066,6 +3066,8 @@ export namespace main {
entity_worked: string; entity_worked: string;
not_worked: string; not_worked: string;
current_entry: string; current_entry: string;
mark_worked: string;
mark_confirmed: string;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new MatrixColors(source); return new MatrixColors(source);
@@ -3080,6 +3082,8 @@ export namespace main {
this.entity_worked = source["entity_worked"]; this.entity_worked = source["entity_worked"];
this.not_worked = source["not_worked"]; this.not_worked = source["not_worked"];
this.current_entry = source["current_entry"]; this.current_entry = source["current_entry"];
this.mark_worked = source["mark_worked"];
this.mark_confirmed = source["mark_confirmed"];
} }
} }