fix(flex): link TX/RX collapse, equal-size meters, phone-only MIC/COMP, inline S-meter dBm
FlexRadio panel refinements from feedback: - TRANSMIT and RECEIVE now share one collapse state — folding either folds both (Card gained an optional controlled open/onToggle mode; the parent owns the shared txrx state, persisted). - Meter sizes: the Tuner Genius PWR/SWR meters used a 2-column grid (wider than the Flex/amp meters). Switched to the same grid-cols-2 sm:grid-cols-3 so every meter across the METERS, AMPLIFIER and TUNER cards is the same width. - MIC and COMP meters are hidden outside phone modes (shown for SSB/AM/FM only). - S-meter dBm moved inline next to the S-value (was a second line under the bar), and the redundant dBm line under PWR removed — keeps only the watts. Saves height.
This commit is contained in:
@@ -262,12 +262,20 @@ function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', extra, displ
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ icon: Icon, title, accent, children, ckey }: { icon: any; title: string; accent?: string; children: React.ReactNode; ckey?: string }) {
|
||||
// Collapsible: a chevron in the header hides the body. State persists per card
|
||||
// (keyed by ckey, falling back to the title) so a folded card stays folded.
|
||||
function Card({ icon: Icon, title, accent, children, ckey, open: openProp, onToggle }: {
|
||||
icon: any; title: string; accent?: string; children: React.ReactNode; ckey?: string;
|
||||
open?: boolean; onToggle?: () => void; // controlled mode — lets sibling cards share one collapse state
|
||||
}) {
|
||||
// Collapsible: a chevron in the header hides the body. Uncontrolled by default,
|
||||
// persisting per card (keyed by ckey, falling back to the title); when `open`/
|
||||
// `onToggle` are supplied the parent owns the state (e.g. linked TX/RX cards).
|
||||
const storeKey = 'opslog.cardOpen.' + (ckey || title);
|
||||
const [open, setOpen] = useState(() => localStorage.getItem(storeKey) !== '0');
|
||||
const toggle = () => setOpen((o) => { const n = !o; localStorage.setItem(storeKey, n ? '1' : '0'); return n; });
|
||||
const [openState, setOpenState] = useState(() => localStorage.getItem(storeKey) !== '0');
|
||||
const controlled = openProp !== undefined;
|
||||
const open = controlled ? openProp : openState;
|
||||
const toggle = controlled
|
||||
? (onToggle ?? (() => {}))
|
||||
: () => setOpenState((o) => { const n = !o; localStorage.setItem(storeKey, n ? '1' : '0'); return n; });
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden">
|
||||
<button type="button" onClick={toggle}
|
||||
@@ -364,6 +372,11 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
|
||||
// TRANSMIT + RECEIVE share ONE collapse state (they sit side by side, so folding
|
||||
// one folds the other and keeps the row tidy). Persisted like the other cards.
|
||||
const [txrxOpen, setTxrxOpen] = useState(() => localStorage.getItem('opslog.cardOpen.txrx') !== '0');
|
||||
const toggleTxrx = () => setTxrxOpen((o) => { const n = !o; localStorage.setItem('opslog.cardOpen.txrx', n ? '1' : '0'); return n; });
|
||||
|
||||
// Tuner Genius XL direct connection — its own card in the Flex panel (like PGXL).
|
||||
const [tg, setTg] = useState<TGStatus>({ connected: false });
|
||||
const [tgEnabled, setTgEnabled] = useState(false);
|
||||
@@ -446,6 +459,9 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
return () => off();
|
||||
}, [st.rit, st.rit_freq]);
|
||||
const isCW = (st.mode || '').toUpperCase().includes('CW');
|
||||
// Phone (voice) modes — MIC / COMP meters only make sense here, so they're
|
||||
// hidden in CW and digital.
|
||||
const isPhone = /\b(SSB|USB|LSB|AM|FM|DFM|NFM)\b/i.test(st.mode || '');
|
||||
const PROC = [{ v: '0', l: 'NOR' }, { v: '1', l: 'DX' }, { v: '2', l: 'DX+' }];
|
||||
const AGC = [{ v: 'off', l: 'OFF' }, { v: 'slow', l: 'SLOW' }, { v: 'med', l: 'MED' }, { v: 'fast', l: 'FAST' }];
|
||||
const CW_BW = [100, 200, 300, 400, 500];
|
||||
@@ -574,22 +590,22 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
};
|
||||
const cur = [
|
||||
sig && (() => { const dbm = peakHold('s', sig.value); const s = sUnit(dbm); return (
|
||||
<MeterBar key="s" label="S-METER" value={s.bar} lo={0} hi={19} accent="#16a34a" display={s.display} extra={`${dbm.toFixed(1)} dBm`}
|
||||
// dBm sits inline next to the S-value (no separate line below) to save height.
|
||||
<MeterBar key="s" label="S-METER" value={s.bar} lo={0} hi={19} accent="#16a34a" display={`${s.display} ${dbm.toFixed(0)} dBm`}
|
||||
title={onReportRST ? t('rst.clickToFill') : undefined}
|
||||
onClick={onReportRST ? () => onReportRST(sMeterRST(s.s, s.over, st.mode)) : undefined}
|
||||
segColor={(fr) => { const sval = fr * 19; return sval < 9 ? '#16a34a' : sval < 12.33 ? '#f59e0b' : '#dc2626'; }} />
|
||||
); })(),
|
||||
fwd && (() => { const w = peakHold('p', isDbm(fwd) ? dbmToW(fwd.value) : fwd.value); return (
|
||||
<MeterBar key="p" label="PWR" unit="W" lo={0} hi={120} accent="#dc2626"
|
||||
value={w} extra={isDbm(fwd) ? `${fwd.value.toFixed(1)} dBm` : undefined} />
|
||||
<MeterBar key="p" label="PWR" unit="W" lo={0} hi={120} accent="#dc2626" value={w} />
|
||||
); })(),
|
||||
swr && <MeterBar key="w" label="SWR" value={peakHold('w', swr.value)} unit="" lo={1} hi={3} accent="#d97706" />,
|
||||
// Mic input level in dBFS — SmartSDR's scale is -40…0 dB.
|
||||
mic && <MeterBar key="mic" label="MIC" value={peakHold('mic', mic.value)} unit={mic.unit || 'dB'} lo={-40} hi={0} accent="#16a34a"
|
||||
// Mic input level in dBFS — SmartSDR's scale is -40…0 dB. Phone modes only.
|
||||
isPhone && mic && <MeterBar key="mic" label="MIC" value={peakHold('mic', mic.value)} unit={mic.unit || 'dB'} lo={-40} hi={0} accent="#16a34a"
|
||||
segColor={(fr) => (fr >= 0.8 ? '#dc2626' : fr >= 0.7 ? '#f59e0b' : '#16a34a')} />,
|
||||
// Speech compression — original working meter, only the top of the
|
||||
// scale changed from the radio-reported 20 to 25 (SmartSDR's -25 max).
|
||||
comp && <MeterBar key="comp" label="COMP" value={peakHold('comp', comp.value)} unit={comp.unit || 'dB'} lo={0} hi={25} accent="#0891b2" />,
|
||||
isPhone && comp && <MeterBar key="comp" label="COMP" value={peakHold('comp', comp.value)} unit={comp.unit || 'dB'} lo={0} hi={25} accent="#0891b2" />,
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">{cur}</div>
|
||||
@@ -606,7 +622,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
{/* TX + RX columns */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
{/* TRANSMIT */}
|
||||
<Card icon={Zap} title={t('flxp.transmit')} accent="#dc2626">
|
||||
<Card icon={Zap} title={t('flxp.transmit')} accent="#dc2626" open={txrxOpen} onToggle={toggleTxrx}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-20 shrink-0 text-xs font-medium text-muted-foreground">{t('flxp.rfPower')}</span>
|
||||
<Slider value={st.rf_power} disabled={off} accent="#dc2626" onChange={(v) => change('rf_power', v, () => FlexSetPower(v))} />
|
||||
@@ -745,7 +761,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
</Card>
|
||||
|
||||
{/* RECEIVE */}
|
||||
<Card icon={AudioLines} title={t('flxp.receiveActive')} accent="#0891b2">
|
||||
<Card icon={AudioLines} title={t('flxp.receiveActive')} accent="#0891b2" open={txrxOpen} onToggle={toggleTxrx}>
|
||||
{/* Antenna selection sits at the very top of the RX column. */}
|
||||
{((st.ant_list?.length ?? 0) > 0 || (st.tx_ant_list?.length ?? 0) > 0) && (
|
||||
<div className="flex items-center gap-2 pb-3 border-b border-border/60">
|
||||
|
||||
@@ -149,8 +149,8 @@ export function TunerCard({ status, t }: { status: TGStatus; t: (k: string, v?:
|
||||
onSelect={() => TunerGeniusActivate(2).catch(() => {})} t={t} />
|
||||
</div>
|
||||
|
||||
{/* PWR + SWR meters. */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* PWR + SWR meters — same grid as the Flex/amp meters so they match size. */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<MeterBar label={t('tgp.power')} value={fwdW} unit="W" lo={0} hi={2000}
|
||||
display={fwdW >= 1 ? `${Math.round(fwdW)} W` : '—'}
|
||||
segColor={(f) => (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} />
|
||||
|
||||
Reference in New Issue
Block a user