feat(appearance): left stripe by default, with fill and strength as choices

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.
This commit is contained in:
2026-08-13 01:42:08 +02:00
parent 83e7727aab
commit 4ffdfc2548
7 changed files with 129 additions and 19 deletions
+35 -2
View File
@@ -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<string, string> => {
const st = cfg?.style ?? 'bar';
const pct = cfg?.intensity ?? 12;
const out: Record<string, string> = {};
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 <div className="p-1 text-sm text-muted-foreground"></div>;
return (
@@ -53,13 +64,35 @@ export function AppearancePanel() {
</label>
{cfg.enabled && (
<div className="space-y-2">
<div className="space-y-3">
{/* Style first: it decides whether the colours below are a signal or a
wallpaper, which matters more than which hue they are. */}
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm">{t('appr.style')}</span>
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs">
{(['bar', 'tint', 'both'] as const).map((v) => (
<button key={v} type="button" onClick={() => save({ ...cfg, style: v })}
className={cn('px-3 py-1.5 font-medium', (cfg.style ?? 'bar') === v ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
{t('appr.style' + v[0].toUpperCase() + v.slice(1))}
</button>
))}
</div>
{(cfg.style ?? 'bar') !== 'bar' && (
<label className="flex items-center gap-2 text-sm">
{t('appr.intensity')}
<input type="range" min={5} max={45} step={1} value={cfg.intensity ?? 12}
onChange={(e) => save({ ...cfg, intensity: parseInt(e.target.value, 10) })}
className="w-32 accent-[var(--primary)]" />
<span className="font-mono text-xs text-muted-foreground w-8">{cfg.intensity ?? 12}%</span>
</label>
)}
</div>
{/* Order matters and is shown: a contact is usually several of these at
once, and the first match wins. */}
<p className="text-xs text-muted-foreground">{t('appr.orderHint')}</p>
{cfg.rules.map((r, i) => (
<div key={r.id} className="rounded-lg border border-border/60 p-2.5 space-y-2"
style={{ backgroundColor: r.enabled ? `color-mix(in srgb, ${r.color} 24%, transparent)` : undefined }}>
style={r.enabled ? preview(r.color) : undefined}>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={r.enabled} onCheckedChange={(c) => patchRule(r.id, { enabled: !!c })} />
<span className="font-mono text-xs text-muted-foreground">{i + 1}.</span>