Three things reported from the radio, all of them mine.
THE LEVELS. Two half-width sliders side by side left each of them a couple
of centimetres long, which is not enough to set 15% with. One level per
row now, full width, with the value typed in or stepped with ± — and in
the radio's own units, so the volume is dB and the squelch a dBm
threshold, matching what ExpertSDR3's own window shows.
THE FILTERS. The button said 250 and the radio was set to 300-550: 250 Hz
wide, but sitting where no CW note is. The edges are computed from the
width now, and a narrow filter is CENTRED ON THE CW NOTE — a 250 Hz filter
from 100 to 350 would put the note outside its own passband. A button also
lights on the WIDTH the radio reports rather than on an exact pair of
edges, so moving one edge on the radio no longer darkens every button.
MUTE. Read from one shape only, while this radio reports the other
('mute:0,false'), so the button showed the opposite of the truth. Both are
accepted now.
None of this should have reached main before somebody had a radio in front
of it.
73 lines
3.1 KiB
TypeScript
73 lines
3.1 KiB
TypeScript
import { useState } from 'react';
|
||
import { cn } from '@/lib/utils';
|
||
import { WheelRange } from '@/components/WheelRange';
|
||
|
||
// LevelRow — a named level with a slider, a value you can type into, and ±.
|
||
//
|
||
// Shared, like ShiftRow, and for the same complaint: the consoles each drew
|
||
// their levels their own way. This is the wide shape — one row per level, the
|
||
// slider taking the width it needs — rather than two half-width sliders side by
|
||
// side, which is what "c'est laid et elles sont toutes petites" was about.
|
||
//
|
||
// Four ways to move it, so nobody has to learn ours: drag, wheel over the
|
||
// track, ± for one step, or click the number and type. Typing matters for the
|
||
// levels TCI reports in real units — a squelch at -95 dBm is a value an
|
||
// operator knows, not a position to hunt for with a mouse.
|
||
export function LevelRow({
|
||
label, value, min = 0, max = 100, step = 1, unit = '', accent, disabled, onSet,
|
||
}: {
|
||
label: string;
|
||
value: number;
|
||
min?: number;
|
||
max?: number;
|
||
step?: number;
|
||
unit?: string;
|
||
accent?: string;
|
||
disabled?: boolean;
|
||
onSet: (v: number) => void;
|
||
}) {
|
||
const [editing, setEditing] = useState<string | null>(null);
|
||
const clamp = (v: number) => Math.max(min, Math.min(max, v));
|
||
const commit = (raw: string) => {
|
||
setEditing(null);
|
||
const v = parseInt(raw.replace(/[^0-9+-]/g, ''), 10);
|
||
if (!Number.isNaN(v)) onSet(clamp(v));
|
||
};
|
||
return (
|
||
<div className="flex items-center gap-3">
|
||
<span className="w-24 shrink-0 text-[11px] font-bold uppercase tracking-wider text-muted-foreground">
|
||
{label}
|
||
</span>
|
||
<WheelRange
|
||
min={min} max={max} step={step} value={value} disabled={disabled} accent={accent}
|
||
onChange={onSet}
|
||
className="h-2.5 flex-1 [&::-webkit-slider-thumb]:size-4"
|
||
/>
|
||
<div className={cn('flex items-center gap-0.5 shrink-0', disabled && 'opacity-40')}>
|
||
<button type="button" disabled={disabled} onClick={() => onSet(clamp(value - step))}
|
||
className="px-1.5 text-sm font-bold text-muted-foreground hover:text-foreground disabled:opacity-40">−</button>
|
||
{editing !== null ? (
|
||
<input
|
||
autoFocus
|
||
value={editing}
|
||
onChange={(e) => setEditing(e.target.value)}
|
||
onBlur={(e) => commit(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') commit((e.target as HTMLInputElement).value);
|
||
else if (e.key === 'Escape') setEditing(null);
|
||
}}
|
||
className="w-14 rounded border border-border bg-background px-1 text-right text-xs font-mono tabular-nums outline-none"
|
||
/>
|
||
) : (
|
||
<button type="button" disabled={disabled} onClick={() => setEditing(String(value))}
|
||
className="w-14 text-right text-xs font-mono tabular-nums hover:text-primary disabled:cursor-default">
|
||
{value}{unit}
|
||
</button>
|
||
)}
|
||
<button type="button" disabled={disabled} onClick={() => onSet(clamp(value + step))}
|
||
className="px-1.5 text-sm font-bold text-muted-foreground hover:text-foreground disabled:opacity-40">+</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|