fix: padlocks unclickable, From-radio level inert, callsign field too narrow
Padlocks. LockBtn was declared INSIDE the App component, so every render gave React a new component type and the button was unmounted and rebuilt — about four times a second while the CAT polls. It flickered under the pointer and swallowed clicks, because the node being clicked no longer existed when the click landed. The padlock now lives at module level and is passed its state. From-radio level. It reached the QSO recorder and nothing else, so an operator listening through OpsLog heard no difference between 10% and 150% — the control was not weak, it was disconnected. It now scales the monitor too, applied on the captured chunk so the network-fed path keeps its own levels, and a running monitor picks up a change immediately: making someone restart the monitor to hear the slider is how a working control gets reported as broken. Callsign field widened to w-56, the room coming from the RST pair which never needs more than five characters. It is the one field always typed into, it carries the REC badges, and a long portable call has to stay readable.
This commit is contained in:
+40
-29
@@ -393,6 +393,33 @@ function WindowControls() {
|
||||
);
|
||||
}
|
||||
|
||||
// LockPad — the padlock toggle shown in a lockable field's label. Its icon
|
||||
// matches the state, so a glance says which fields are immune to CAT updates
|
||||
// and to the live clock.
|
||||
//
|
||||
// It lives HERE, at module level, and not inside App. Defined inside, React saw
|
||||
// a new component type on every render of App — and App re-renders about four
|
||||
// times a second while the CAT polls — so the button was unmounted and rebuilt
|
||||
// each time. It flickered under the pointer and swallowed clicks, because the
|
||||
// node being clicked no longer existed by the time the click landed.
|
||||
function LockPad({ on, title, onToggle }: { on: boolean; title: string; onToggle: () => void }) {
|
||||
const Icon = on ? Lock : Unlock;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={onToggle}
|
||||
title={`${on ? 'Unlock' : 'Lock'} ${title}`}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-3.5 rounded transition-colors',
|
||||
on ? 'text-warning hover:text-warning' : 'text-muted-foreground/40 hover:text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { t, lang } = useI18n();
|
||||
// === Lists from settings (fallback for first paint) ===
|
||||
@@ -455,27 +482,6 @@ export default function App() {
|
||||
return next;
|
||||
});
|
||||
};
|
||||
// Small padlock toggle rendered inside each lockable field's label. Match
|
||||
// the icon to the current state so the user can tell at a glance which
|
||||
// fields are immune to CAT updates / live clock.
|
||||
function LockBtn({ k, title }: { k: LockKey; title: string }) {
|
||||
const on = locks[k];
|
||||
const Icon = on ? Lock : Unlock;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => toggleLock(k)}
|
||||
title={`${on ? 'Unlock' : 'Lock'} ${title}`}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-3.5 rounded transition-colors',
|
||||
on ? 'text-warning hover:text-warning' : 'text-muted-foreground/40 hover:text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
const [band, setBand] = useState('20m');
|
||||
const [mode, setMode] = useState('SSB');
|
||||
const [freqMhz, setFreqMhz] = useState('');
|
||||
@@ -3710,8 +3716,13 @@ export default function App() {
|
||||
// single-row strip or the full Log4OM-style columnar layout below. Keeping
|
||||
// them as shared consts avoids duplicating the (large) per-field JSX +
|
||||
// handlers across the two layouts.
|
||||
// The callsign field is WIDER than the fields around it, deliberately: it is
|
||||
// the one field always typed into, it carries the REC badges on its right, and
|
||||
// a long portable call (VK9/OH1ABC/MM) must stay readable as it is entered.
|
||||
// The room comes from the RST pair, which never needs more than five
|
||||
// characters.
|
||||
const callsignBlock = (
|
||||
<div className="flex flex-col w-44" data-esm="call">
|
||||
<div className="flex flex-col w-56" data-esm="call">
|
||||
<Label className="flex items-center gap-2 h-3.5" style={{ marginBottom: 6 }}>
|
||||
<span className="text-primary font-semibold">{t('field.callsign')}</span>
|
||||
{lookupBusy && (
|
||||
@@ -3858,12 +3869,12 @@ export default function App() {
|
||||
</div>
|
||||
);
|
||||
const rstTxBlock = (
|
||||
<div className="flex flex-col w-20" data-esm="rsttx"><Label className="mb-1 h-3.5">{t('field.rstTx')}</Label>
|
||||
<div className="flex flex-col w-16" data-esm="rsttx"><Label className="mb-1 h-3.5">{t('field.rstTx')}</Label>
|
||||
<Combobox value={rstSent} options={rstOptions(mode, rstLists)} commitOnType onChange={(v) => { setRstSent(v); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
const rstRxBlock = (
|
||||
<div className="flex flex-col w-20" data-esm="rstrx"><Label className="mb-1 h-3.5">{t('field.rstRx')}</Label>
|
||||
<div className="flex flex-col w-16" data-esm="rstrx"><Label className="mb-1 h-3.5">{t('field.rstRx')}</Label>
|
||||
<Combobox value={rstRcvd} options={rstOptions(mode, rstLists)} commitOnType onChange={(v) => { setRstRcvd(v); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
@@ -3897,7 +3908,7 @@ export default function App() {
|
||||
) : null;
|
||||
const startBlock = (
|
||||
<div className="flex flex-col w-28">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-success">{t('field.startUtc')} <LockBtn k="start" title="start time" /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-success">{t('field.startUtc')} <LockPad on={locks.start} title="start time" onToggle={() => toggleLock('start')} /></Label>
|
||||
<Input
|
||||
readOnly={!locks.start}
|
||||
tabIndex={locks.start ? 0 : -1}
|
||||
@@ -3916,7 +3927,7 @@ export default function App() {
|
||||
);
|
||||
const endBlock = (
|
||||
<div className="flex flex-col w-28">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-danger">{t('field.endUtc')} <LockBtn k="end" title="end time" /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1 text-danger">{t('field.endUtc')} <LockPad on={locks.end} title="end time" onToggle={() => toggleLock('end')} /></Label>
|
||||
<Input
|
||||
readOnly={!locks.end}
|
||||
tabIndex={locks.end ? 0 : -1}
|
||||
@@ -4106,7 +4117,7 @@ export default function App() {
|
||||
// used in the full layout to save vertical height.
|
||||
const bandRow = (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.band')} <LockBtn k="band" title="band" /></Label>
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.band')} <LockPad on={locks.band} title="band" onToggle={() => toggleLock('band')} /></Label>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Select value={band} onValueChange={onBandUserChange}>
|
||||
<SelectTrigger tabIndex={-1} className="h-8"><SelectValue /></SelectTrigger>
|
||||
@@ -4117,7 +4128,7 @@ export default function App() {
|
||||
);
|
||||
const modeRow = (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.mode')} <LockBtn k="mode" title="mode" /></Label>
|
||||
<Label className="w-20 shrink-0 flex items-center gap-1">{t('field.mode')} <LockPad on={locks.mode} title="mode" onToggle={() => toggleLock('mode')} /></Label>
|
||||
<div className="flex-1 min-w-0">
|
||||
<Select value={mode} onValueChange={onModeUserChange}>
|
||||
<SelectTrigger tabIndex={-1} className="h-8"><SelectValue /></SelectTrigger>
|
||||
@@ -4173,7 +4184,7 @@ export default function App() {
|
||||
};
|
||||
const freqBlock = (
|
||||
<div className="flex flex-col w-32">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">{t('field.txFreq')} <LockBtn k="freq" title="frequency" /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">{t('field.txFreq')} <LockPad on={locks.freq} title="frequency" onToggle={() => toggleLock('freq')} /></Label>
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
className="font-mono"
|
||||
|
||||
Reference in New Issue
Block a user