style(elecraft): lay the console out like the other panels

It filled the window: on a wide screen a slider ended a hand's width from
its own label and the panel stopped reading as one instrument. Capped and
centred like the Yaesu, Icom and Flex consoles, grouped into Meters,
Levels and Receive cards, with the level sliders in two columns.

Also opens the 0.26.12 block: the TQSL logging and this console were
written after v0.26.11 was tagged.
This commit is contained in:
2026-08-24 14:42:03 +02:00
parent dd77e7f467
commit 1f94d0ef08
3 changed files with 79 additions and 43 deletions
+63 -35
View File
@@ -60,6 +60,19 @@ function sSegColor(frac: number) {
return '#16a34a';
}
// The card shell the Yaesu and Icom consoles use, so the three read alike.
function Card({ icon: Icon, title, children }: { icon: any; title: string; children: React.ReactNode }) {
return (
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
<Icon className="size-4 text-primary" />
<span className="text-xs font-bold uppercase tracking-wider text-foreground/80">{title}</span>
</div>
<div className="p-3 space-y-3">{children}</div>
</div>
);
}
// One button shape for every on/off control, so the panel reads as one
// instrument instead of a collection of differently-styled switches.
function Toggle({ label, on, off, onClick }: { label: string; on: boolean; off: boolean; onClick: () => void }) {
@@ -114,28 +127,36 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
};
return (
<section className="flex flex-col gap-2 h-full min-h-0 bg-card overflow-auto">
{/* Header */}
<div className="flex items-center gap-2 px-3 py-1.5 bg-muted/40 border-b border-border shrink-0">
<Radio className="size-4 text-primary shrink-0" />
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{st.elecraft ? 'Elecraft' : 'Kenwood'}
</span>
<span className="text-xs font-mono text-foreground/80">{st.model}</span>
<span className={cn('size-2 rounded-full', off ? 'bg-muted-foreground/40' : 'bg-success')} />
<div className="flex-1" />
<span className="font-mono text-sm tabular-nums">{freqHz > 0 ? (freqHz / 1e6).toFixed(6) : '—'}</span>
<button type="button" className="text-[11px] text-muted-foreground hover:text-foreground"
<div className="h-full min-h-0 overflow-auto bg-background">
{/* Same wrapper as the Yaesu, Icom and Flex consoles: capped width and
CENTRED. A console stretched across a 2000 px window puts a slider a
hand's width from its own label, and the panel stops reading as one
instrument. */}
<div className="max-w-5xl mx-auto p-3 space-y-3">
{/* VFO + status */}
<div className="rounded-xl border border-border bg-card shadow-sm px-4 py-3 flex items-center justify-between gap-3 flex-wrap">
<div>
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
<Radio className="size-3.5" />
{st.elecraft ? 'Elecraft' : 'Kenwood'} {st.model}
<span className={cn('size-2 rounded-full', off ? 'bg-muted-foreground/40' : 'bg-success')} />
</div>
<div className="text-2xl font-mono tabular-nums font-bold">
{freqHz > 0 ? (freqHz / 1e6).toFixed(6) : '—'}
</div>
</div>
<button type="button" className="text-[11px] text-muted-foreground hover:text-foreground flex items-center gap-1"
onClick={() => RefreshKenwood().catch(() => {})} title={t('k3.refreshHint')}>
<SlidersHorizontal className="size-3.5" />
</button>
</div>
{off && <div className="px-3 text-xs text-muted-foreground">{t('k3.waiting')}</div>}
{!!err && <div className="px-3 text-[11px] text-danger">{err}</div>}
{off && <div className="text-xs text-muted-foreground px-1">{t('k3.waiting')}</div>}
{!!err && <div className="text-[11px] text-danger px-1">{err}</div>}
{/* Meters */}
<div className="grid grid-cols-3 gap-2 px-3">
<Card icon={Activity} title={t('k3.meters')}>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<MeterBar label="S-METER" value={view.transmitting ? 0 : view.s_meter} lo={0} hi={100}
accent="#16a34a" segColor={sSegColor}
display={view.transmitting ? '—' : sParts(view.s_meter_raw).label}
@@ -153,13 +174,13 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
accent="#f59e0b"
display={view.transmitting && view.swr > 0 ? view.swr.toFixed(1) : '—'} />
</div>
{view.meters_provisional && (
<p className="px-3 text-[10px] text-muted-foreground">{t('k3.provisional')}</p>
<p className="text-[10px] text-muted-foreground">{t('k3.provisional')}</p>
)}
</Card>
{/* MOX + TUNE */}
<div className="flex items-center gap-2 px-3">
<div className="flex items-center gap-2">
<button type="button" disabled={off}
onClick={() => SetKenwoodTX(!view.transmitting).catch((e) => setErr(String(e?.message ?? e)))}
className={cn('flex-1 px-3 py-2.5 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
@@ -176,8 +197,10 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
</button>
</div>
{/* Power + volume */}
<div className="px-3 pb-3 space-y-2">
{/* Levels — two columns, so a slider stays beside the label it belongs to
instead of running the width of the window. */}
<Card icon={SlidersHorizontal} title={t('k3.levels')}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-6 gap-y-2">
<label className="flex items-center gap-2 text-xs">
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.power')}</span>
<input type="range" min={0} max={110} step={1} disabled={off}
@@ -220,11 +243,24 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
className="flex-1 accent-primary" />
<span className="w-12 text-right font-mono tabular-nums">{view.squelch ?? 0}</span>
</label>
{/* CW keyer speed — the radio's own keyer, the one the K3 sends with. */}
<label className="flex items-center gap-2 text-xs">
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.keySpeed')}</span>
<input type="range" min={8} max={50} step={1} disabled={off}
value={view.key_speed || 20}
onChange={(e) => put({ key_speed: Number(e.target.value) }, () => SetKenwoodKeySpeed(Number(e.target.value)))}
className="flex-1 accent-primary" />
<span className="w-12 text-right font-mono tabular-nums">{view.key_speed || 0} wpm</span>
</label>
</div>
</Card>
{/* Receive chain. Toggles, because that is what they are on the radio:
one press each, and the state comes back from the rig rather than
from what the button was asked to do. */}
<div className="flex flex-wrap items-center gap-1.5 pt-1">
{/* Receive chain. Toggles, because that is what they are on the radio:
one press each, and the state comes back from the rig rather than from
what the button was asked to do. */}
<Card icon={AudioLines} title={t('k3.receive')}>
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-1.5">
<Toggle label="PRE" on={view.preamp} off={off} onClick={() => SetKenwoodPreamp(!view.preamp).catch(setErrMsg)} />
<Toggle label="ATT" on={view.att} off={off} onClick={() => SetKenwoodAtt(!view.att).catch(setErrMsg)} />
<Toggle label="NB" on={view.nb} off={off} onClick={() => SetKenwoodNB(!view.nb).catch(setErrMsg)} />
@@ -266,17 +302,9 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
<span className="text-[10px] font-mono text-muted-foreground">{view.filter_hz} Hz</span>
)}
</div>
{/* CW keyer speed — the radio's own keyer, the one the K3 sends with. */}
<label className="flex items-center gap-2 text-xs">
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.keySpeed')}</span>
<input type="range" min={8} max={50} step={1} disabled={off}
value={view.key_speed || 20}
onChange={(e) => put({ key_speed: Number(e.target.value) }, () => SetKenwoodKeySpeed(Number(e.target.value)))}
className="flex-1 accent-primary" />
<span className="w-12 text-right font-mono tabular-nums">{view.key_speed || 0} wpm</span>
</label>
</div>
</section>
</Card>
</div>
</div>
);
}