From 4ffdfc25486a091af3cda0ef7e8b73abf1a6527d Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Thu, 13 Aug 2026 01:42:08 +0200 Subject: [PATCH] feat(appearance): left stripe by default, with fill and strength as choices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version filled the whole row at 24%, and in a real log that meant every row was painted: nearly every contact has SOME QSL state, so colour was present everywhere and stopped being information — a striped background with the data behind it. The default is now a 3px stripe down the left edge. Same signal, nothing lost to read it. A filled row is still offered, with a strength slider, for operators who want the block — and the rule cards in Settings preview whichever is chosen, so the decision is made by looking rather than by imagining. Drawn as an inset shadow rather than a border: a border would shift the cells three pixels on coloured rows only, and the columns would stop lining up. Style and strength are normalised rather than rejected — a value out of range is a slider that got away, not a reason to reset the operator's colours. --- appearance.go | 29 +++++++++++++-- appearance_test.go | 26 ++++++++++++++ changelog.json | 8 +++-- frontend/src/components/AppearancePanel.tsx | 37 +++++++++++++++++-- frontend/src/lib/i18n.tsx | 4 +-- frontend/src/lib/rowColors.ts | 40 +++++++++++++++------ frontend/wailsjs/go/models.ts | 4 +++ 7 files changed, 129 insertions(+), 19 deletions(-) diff --git a/appearance.go b/appearance.go index ef62ed6..9262161 100644 --- a/appearance.go +++ b/appearance.go @@ -23,8 +23,18 @@ type RowColorRule struct { // RowColorSettings is the whole appearance block. type RowColorSettings struct { - Enabled bool `json:"enabled"` - Rules []RowColorRule `json:"rules"` + Enabled bool `json:"enabled"` + // Style is how the colour is shown: "bar" paints a stripe down the left edge, + // "tint" washes the row, "both" does each. + // + // A bar is the default because a filled row is a poor signal in a log where + // nearly every contact has SOME QSL state: colour that is always present + // stops being information and becomes a striped background, with the data + // behind it. The stripe says the same thing and costs nothing to read. + Style string `json:"style"` + // Intensity is the tint strength in percent. Only used by "tint"/"both". + Intensity int `json:"intensity"` + Rules []RowColorRule `json:"rules"` } // The rule ids, in priority order. The frontend matches on these and holds the @@ -56,7 +66,20 @@ func normRowColors(s RowColorSettings) RowColorSettings { for _, r := range s.Rules { byID[r.ID] = r } - out := RowColorSettings{Enabled: s.Enabled} + out := RowColorSettings{Enabled: s.Enabled, Style: s.Style, Intensity: s.Intensity} + switch out.Style { + case "bar", "tint", "both": + default: + out.Style = "bar" + } + // Clamped rather than rejected: the value only shapes a colour-mix, and a + // number outside the range is a slider that got away, not a fault worth + // resetting the operator's whole choice for. + if out.Intensity < 5 { + out.Intensity = 12 + } else if out.Intensity > 45 { + out.Intensity = 45 + } for _, id := range rowColorOrder { r := byID[id] r.ID = id diff --git a/appearance_test.go b/appearance_test.go index 5697197..27ba2b8 100644 --- a/appearance_test.go +++ b/appearance_test.go @@ -49,3 +49,29 @@ func TestRowColorsKeepPriorityOrder(t *testing.T) { t.Errorf("confirmed_lotw lost its colour: %q", got.Rules[0].Color) } } + +// Style and strength are normalised, not rejected: they only shape a colour, and +// a value that got away is a slider, not a fault worth resetting the operator's +// whole choice for. +func TestRowColorsNormaliseStyleAndIntensity(t *testing.T) { + for _, tc := range []struct { + inStyle string + inPct int + wantStyle string + wantPct int + }{ + {"bar", 12, "bar", 12}, + {"tint", 30, "tint", 30}, + {"both", 45, "both", 45}, + {"", 0, "bar", 12}, // never configured + {"wallpaper", 12, "bar", 12}, // not a style we draw + {"tint", 900, "tint", 45}, // clamped, not reset + {"tint", -5, "tint", 12}, + } { + got := normRowColors(RowColorSettings{Style: tc.inStyle, Intensity: tc.inPct}) + if got.Style != tc.wantStyle || got.Intensity != tc.wantPct { + t.Errorf("(%q,%d) -> (%q,%d), want (%q,%d)", + tc.inStyle, tc.inPct, got.Style, got.Intensity, tc.wantStyle, tc.wantPct) + } + } +} diff --git a/changelog.json b/changelog.json index b75be7d..84159ff 100644 --- a/changelog.json +++ b/changelog.json @@ -2,8 +2,12 @@ { "version": "0.24.9", "date": "", - "en": [], - "fr": [] + "en": [ + "Appearance: row colouring now defaults to a left stripe, with a filled row and its strength offered as choices." + ], + "fr": [ + "Apparence : la coloration des lignes se fait par défaut sur une barre à gauche, la ligne remplie et son intensité restant proposées." + ] }, { "version": "0.24.8", diff --git a/frontend/src/components/AppearancePanel.tsx b/frontend/src/components/AppearancePanel.tsx index 1a950e8..94d0735 100644 --- a/frontend/src/components/AppearancePanel.tsx +++ b/frontend/src/components/AppearancePanel.tsx @@ -42,6 +42,17 @@ export function AppearancePanel() { save({ ...cfg, rules: cfg.rules.map((r) => (r.id === id ? { ...r, ...patch } : r)) }); }; + // The rule card shows the row exactly as the grid will draw it, so the choice + // is made by looking rather than by imagining. + const preview = (color: string): Record => { + const st = cfg?.style ?? 'bar'; + const pct = cfg?.intensity ?? 12; + const out: Record = {}; + if (st === 'tint' || st === 'both') out.backgroundColor = `color-mix(in srgb, ${color} ${pct}%, transparent)`; + if (st === 'bar' || st === 'both') out.boxShadow = `inset 3px 0 0 ${color}`; + return out; + }; + if (!cfg) return
; return ( @@ -53,13 +64,35 @@ export function AppearancePanel() { {cfg.enabled && ( -
+
+ {/* Style first: it decides whether the colours below are a signal or a + wallpaper, which matters more than which hue they are. */} +
+ {t('appr.style')} +
+ {(['bar', 'tint', 'both'] as const).map((v) => ( + + ))} +
+ {(cfg.style ?? 'bar') !== 'bar' && ( + + )} +
{/* Order matters and is shown: a contact is usually several of these at once, and the first match wins. */}

{t('appr.orderHint')}

{cfg.rules.map((r, i) => (
+ style={r.enabled ? preview(r.color) : undefined}>