feat: header rate/propagation colours + CW macro word space
Header: colour the propagation indices semantically (SFI/SSN green when
strong; A/K green quiet → yellow unsettled → red storm) and glow the QSO-rate
numbers the brand accent when active, dim to muted when idle.
WinKeyer/Icom CW: append a trailing word space to each macro send so two
macros fired back-to-back don't run together in the keyer buffer ("CQ"+"TEST"
→ "CQTEST"). The keyer keys the space at the current speed, so it scales with
WPM. Only the macro path is affected — send-on-type stays per-character.
This commit is contained in:
+23
-10
@@ -2008,17 +2008,21 @@ export default function App() {
|
|||||||
async function wkSend(rawText: string) {
|
async function wkSend(rawText: string) {
|
||||||
setWkSent('');
|
setWkSent('');
|
||||||
const resolved = resolveCW(rawText);
|
const resolved = resolveCW(rawText);
|
||||||
|
// Trailing word space so two macros fired back-to-back don't run together in
|
||||||
|
// the keyer buffer ("CQ" + "TEST" → "CQTEST"). The keyer keys a space as a
|
||||||
|
// word gap at the CURRENT speed, so it scales with WPM automatically.
|
||||||
|
const keyed = resolved ? resolved + ' ' : resolved;
|
||||||
const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
|
const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
|
||||||
const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
|
const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
|
||||||
if (cwSourceRef.current === 'icom') {
|
if (cwSourceRef.current === 'icom') {
|
||||||
// The rig's keyer gives no busy echo back, so show the text we sent and,
|
// The rig's keyer gives no busy echo back, so show the text we sent and,
|
||||||
// for <LOGQSO>, wait the estimated send duration before logging.
|
// for <LOGQSO>, wait the estimated send duration before logging.
|
||||||
setWkSent(resolved);
|
setWkSent(resolved);
|
||||||
await IcomSendCW(resolved).catch((e) => setError(String(e?.message ?? e)));
|
await IcomSendCW(keyed).catch((e) => setError(String(e?.message ?? e)));
|
||||||
if (doLog) { await sleep(Math.round(estimateCwMs(resolved, wkWpm)) + 600); void save(); }
|
if (doLog) { await sleep(Math.round(estimateCwMs(resolved, wkWpm)) + 600); void save(); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await WinkeyerSend(resolved).catch((e) => setError(String(e?.message ?? e)));
|
await WinkeyerSend(keyed).catch((e) => setError(String(e?.message ?? e)));
|
||||||
// <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
|
// <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
|
||||||
// finished sending — so the QSO isn't logged (and the form cleared) while CW
|
// finished sending — so the QSO isn't logged (and the form cleared) while CW
|
||||||
// is still going out. We'd like to wait for the busy flag to rise then fall,
|
// is still going out. We'd like to wait for the busy flag to rise then fall,
|
||||||
@@ -3760,16 +3764,17 @@ export default function App() {
|
|||||||
{showQsoRate && (
|
{showQsoRate && (
|
||||||
<div className="flex items-center gap-2.5 font-mono px-2.5 h-8 rounded-md border border-border/60 bg-muted/40 whitespace-nowrap"
|
<div className="flex items-center gap-2.5 font-mono px-2.5 h-8 rounded-md border border-border/60 bg-muted/40 whitespace-nowrap"
|
||||||
title={t('rate.title')}>
|
title={t('rate.title')}>
|
||||||
<Activity className="size-3.5 text-muted-foreground" />
|
<Activity className={cn('size-3.5', (qsoRate.last10 + qsoRate.last60) > 0 ? 'text-primary' : 'text-muted-foreground')} />
|
||||||
{/* Contest-style rate: QSOs/hour projected from each window
|
{/* Contest-style rate: QSOs/hour projected from each window
|
||||||
(10-min count ×6; the 60-min count is already per hour). */}
|
(10-min count ×6; the 60-min count is already per hour). Numbers
|
||||||
|
glow the brand accent when active, dim to muted when idle. */}
|
||||||
<span className="inline-flex items-baseline gap-1">
|
<span className="inline-flex items-baseline gap-1">
|
||||||
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">10′</span>
|
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">10′</span>
|
||||||
<span className="font-bold text-[12px] text-foreground">{qsoRate.last10 * 6}</span>
|
<span className={cn('font-bold text-[12px]', qsoRate.last10 > 0 ? 'text-primary' : 'text-muted-foreground')}>{qsoRate.last10 * 6}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-baseline gap-1">
|
<span className="inline-flex items-baseline gap-1">
|
||||||
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">60′</span>
|
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">60′</span>
|
||||||
<span className="font-bold text-[12px] text-foreground">{qsoRate.last60}</span>
|
<span className={cn('font-bold text-[12px]', qsoRate.last60 > 0 ? 'text-primary' : 'text-muted-foreground')}>{qsoRate.last60}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground text-[9px] uppercase tracking-wider">Q/h</span>
|
<span className="text-muted-foreground text-[9px] uppercase tracking-wider">Q/h</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -3786,6 +3791,14 @@ export default function App() {
|
|||||||
const geo = String(solar.geomag_field || '').toUpperCase();
|
const geo = String(solar.geomag_field || '').toUpperCase();
|
||||||
const geoCls = /STORM|SEVERE/.test(geo) ? 'text-danger'
|
const geoCls = /STORM|SEVERE/.test(geo) ? 'text-danger'
|
||||||
: /ACTIVE|UNSETTLED/.test(geo) ? 'text-warning' : 'text-success';
|
: /ACTIVE|UNSETTLED/.test(geo) ? 'text-warning' : 'text-success';
|
||||||
|
const num = (v: any) => { const n = Number(v); return Number.isFinite(n) ? n : null; };
|
||||||
|
// Semantic colour by band condition: higher flux/sunspots = better HF
|
||||||
|
// (green when strong); A and K measure geomagnetic disturbance, so LOW
|
||||||
|
// is good (green quiet → yellow unsettled → red storm).
|
||||||
|
const sfiCls = (v: any) => { const n = num(v); return n == null ? 'text-foreground' : n >= 120 ? 'text-success' : n >= 90 ? 'text-foreground' : 'text-warning'; };
|
||||||
|
const ssnCls = (v: any) => { const n = num(v); return n != null && n >= 80 ? 'text-success' : 'text-foreground'; };
|
||||||
|
const aCls = (v: any) => { const n = num(v); return n == null ? 'text-foreground' : n <= 7 ? 'text-success' : n <= 15 ? 'text-warning' : 'text-danger'; };
|
||||||
|
const kCls = (v: any) => { const n = num(v); return n == null ? 'text-foreground' : n <= 2 ? 'text-success' : n <= 3 ? 'text-warning' : 'text-danger'; };
|
||||||
const it = (label: string, val: any, cls = 'text-foreground') => (
|
const it = (label: string, val: any, cls = 'text-foreground') => (
|
||||||
<span className="inline-flex items-baseline gap-1">
|
<span className="inline-flex items-baseline gap-1">
|
||||||
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">{label}</span>
|
<span className="text-muted-foreground uppercase tracking-wider text-[9px]">{label}</span>
|
||||||
@@ -3793,10 +3806,10 @@ export default function App() {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
return (<>
|
return (<>
|
||||||
{it('SFI', solar.sfi)}
|
{it('SFI', solar.sfi, sfiCls(solar.sfi))}
|
||||||
{it('SSN', solar.ssn)}
|
{it('SSN', solar.ssn, ssnCls(solar.ssn))}
|
||||||
{it('A', solar.a_index)}
|
{it('A', solar.a_index, aCls(solar.a_index))}
|
||||||
{it('K', solar.k_index)}
|
{it('K', solar.k_index, kCls(solar.k_index))}
|
||||||
{geo ? <span className={cn('font-bold text-[12px]', geoCls)}>{geo}</span> : null}
|
{geo ? <span className={cn('font-bold text-[12px]', geoCls)}>{geo}</span> : null}
|
||||||
</>);
|
</>);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
Reference in New Issue
Block a user