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.
122 lines
5.6 KiB
TypeScript
122 lines
5.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { GetRowColors, SaveRowColors } from '../../wailsjs/go/main/App';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { useI18n } from '@/lib/i18n';
|
|
import { cn } from '@/lib/utils';
|
|
import type { RowColorSettings } from '@/lib/rowColors';
|
|
|
|
// A fixed palette plus a free picker. Muted values on purpose: they are
|
|
// composited at low opacity over a dark grid, where a saturated colour reads as
|
|
// an error state rather than a status.
|
|
const PALETTE = [
|
|
'#16a34a', '#0ea5e9', '#f59e0b', '#a855f7',
|
|
'#dc2626', '#14b8a6', '#eab308', '#ec4899',
|
|
'#64748b', '#84cc16', '#6366f1', '#f97316',
|
|
];
|
|
|
|
// The rule ids the backend orders; the labels live here so a translation never
|
|
// travels through the settings row.
|
|
const LABELS: Record<string, string> = {
|
|
confirmed_lotw: 'appr.confirmedLotw',
|
|
confirmed_paper: 'appr.confirmedPaper',
|
|
sent_waiting: 'appr.sentWaiting',
|
|
to_send: 'appr.toSend',
|
|
};
|
|
|
|
export function AppearancePanel() {
|
|
const { t } = useI18n();
|
|
const [cfg, setCfg] = useState<RowColorSettings | null>(null);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
try { setCfg((await GetRowColors()) as any); } catch { /* defaults on the backend */ }
|
|
})();
|
|
}, []);
|
|
|
|
const save = (next: RowColorSettings) => {
|
|
setCfg(next);
|
|
SaveRowColors(next as any).catch(() => {});
|
|
};
|
|
const patchRule = (id: string, patch: Partial<{ color: string; enabled: boolean }>) => {
|
|
if (!cfg) return;
|
|
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 (
|
|
<div className="space-y-4">
|
|
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
|
<Checkbox checked={cfg.enabled} className="mt-0.5"
|
|
onCheckedChange={(c) => save({ ...cfg, enabled: !!c })} />
|
|
<span>{t('appr.enable')} <span className="text-xs text-muted-foreground">{t('appr.enableHint')}</span></span>
|
|
</label>
|
|
|
|
{cfg.enabled && (
|
|
<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={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>
|
|
<span className="font-medium">{t(LABELS[r.id] ?? r.id)}</span>
|
|
</label>
|
|
{r.enabled && (
|
|
<div className="flex items-center gap-1.5 flex-wrap pl-6">
|
|
{PALETTE.map((c) => (
|
|
<button key={c} type="button" title={c}
|
|
onClick={() => patchRule(r.id, { color: c })}
|
|
className={cn('size-6 rounded-md border-2 transition-transform hover:scale-110',
|
|
r.color.toLowerCase() === c ? 'border-foreground' : 'border-transparent')}
|
|
style={{ backgroundColor: c }} />
|
|
))}
|
|
<input type="color" value={r.color} title={t('appr.custom')}
|
|
onChange={(e) => patchRule(r.id, { color: e.target.value })}
|
|
className="size-6 rounded-md border border-border bg-transparent p-0 cursor-pointer" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|