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
+64
View File
@@ -138,6 +138,70 @@ func normRowColors(s RowColorSettings) RowColorSettings {
return out
}
// MatrixColors recolours the band/mode matrix — the PH/CW/DIG grid in the Stats
// panel, whose five fills and current-entry ring are the fastest read in the
// whole app and the one an operator is most likely to want in their own colours.
//
// Every colour is OPTIONAL and an empty one keeps whatever the active theme
// paints. That is why this stores OVERRIDES rather than a palette: each of the
// twelve themes ships a matrix ramp tuned to its own background, and an operator
// who only wants a different green must not thereby freeze the other four to the
// theme they happened to be using the day they picked it.
type MatrixColors struct {
// Enabled off leaves the theme's own ramp untouched, so switching it off is a
// genuine revert and not "some other set of colours".
Enabled bool `json:"enabled"`
CallConfirmed string `json:"call_confirmed"`
CallWorked string `json:"call_worked"`
EntityConfirmed string `json:"entity_confirmed"`
EntityWorked string `json:"entity_worked"`
NotWorked string `json:"not_worked"`
CurrentEntry string `json:"current_entry"`
}
// normMatrixColors keeps only plain hex values. Anything else becomes "" — i.e.
// "use the theme's" — because these are written straight into a CSS custom
// property, and the same rule as hexColor's own comment applies: what cannot be
// trusted into a stylesheet is refused rather than passed through.
func normMatrixColors(c MatrixColors) MatrixColors {
clean := func(s string) string {
s = strings.TrimSpace(s)
if hexColor.MatchString(s) {
return strings.ToLower(s)
}
return ""
}
return MatrixColors{
Enabled: c.Enabled,
CallConfirmed: clean(c.CallConfirmed),
CallWorked: clean(c.CallWorked),
EntityConfirmed: clean(c.EntityConfirmed),
EntityWorked: clean(c.EntityWorked),
NotWorked: clean(c.NotWorked),
CurrentEntry: clean(c.CurrentEntry),
}
}
// GetMatrixColors returns the operator's matrix palette overrides. All-empty
// (the default) means "whatever the theme says".
func (a *App) GetMatrixColors() MatrixColors {
var c MatrixColors
if raw := a.settingOr(keyMatrixColors, ""); raw != "" {
_ = json.Unmarshal([]byte(raw), &c)
}
return normMatrixColors(c)
}
// SaveMatrixColors persists them.
func (a *App) SaveMatrixColors(c MatrixColors) error {
b, err := json.Marshal(normMatrixColors(c))
if err != nil {
return err
}
a.setSetting(keyMatrixColors, string(b))
return nil
}
// GetRowColors returns the row-colouring configuration, defaults included so the
// panel never has to invent one.
func (a *App) GetRowColors() RowColorSettings {