feat: Themes added, 4 themes available (3 dark, 1 light)
This commit is contained in:
+73
-73
@@ -324,7 +324,7 @@ export default function App() {
|
||||
title={`${on ? 'Unlock' : 'Lock'} ${title}`}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-3.5 rounded transition-colors',
|
||||
on ? 'text-amber-600 hover:text-amber-700' : 'text-muted-foreground/40 hover:text-muted-foreground',
|
||||
on ? 'text-warning hover:text-warning' : 'text-muted-foreground/40 hover:text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
@@ -2525,8 +2525,8 @@ export default function App() {
|
||||
{!lookupBusy && lookupResult && (() => {
|
||||
const src = (lookupResult.source || '').toLowerCase();
|
||||
const tone = src.includes('cache') ? 'amber' : src.includes('qrz') ? 'emerald' : src.includes('ham') ? 'sky' : 'slate';
|
||||
const ring = { amber: 'bg-amber-500/12 text-amber-600 ring-amber-500/30', emerald: 'bg-emerald-500/12 text-emerald-600 ring-emerald-500/30', sky: 'bg-sky-500/12 text-sky-600 ring-sky-500/30', slate: 'bg-slate-500/12 text-slate-500 ring-slate-500/30' }[tone];
|
||||
const dot = { amber: 'bg-amber-500', emerald: 'bg-emerald-500', sky: 'bg-sky-500', slate: 'bg-slate-400' }[tone];
|
||||
const ring = { amber: 'bg-warning/12 text-warning ring-warning/30', emerald: 'bg-success/12 text-success ring-success/30', sky: 'bg-info/12 text-info ring-info/30', slate: 'bg-muted-foreground/12 text-muted-foreground ring-muted-foreground/30' }[tone];
|
||||
const dot = { amber: 'bg-warning', emerald: 'bg-success', sky: 'bg-info', slate: 'bg-muted-foreground' }[tone];
|
||||
return (
|
||||
<span className={cn('inline-flex items-center gap-1 rounded-full px-1.5 py-[1px] text-[9px] font-semibold uppercase tracking-wide ring-1 ring-inset', ring)}>
|
||||
<span className={cn('size-1.5 rounded-full', dot)} />{lookupResult.source}
|
||||
@@ -2534,7 +2534,7 @@ export default function App() {
|
||||
);
|
||||
})()}
|
||||
{!lookupBusy && !lookupResult && lookupError && (
|
||||
<span className="inline-flex items-center rounded-full bg-rose-500/12 px-1.5 py-[1px] text-[9px] font-semibold text-rose-600 ring-1 ring-inset ring-rose-500/30">{lookupError}</span>
|
||||
<span className="inline-flex items-center rounded-full bg-danger/12 px-1.5 py-[1px] text-[9px] font-semibold text-danger ring-1 ring-inset ring-danger/30">{lookupError}</span>
|
||||
)}
|
||||
{callsign.trim() && (
|
||||
<button
|
||||
@@ -2552,7 +2552,7 @@ export default function App() {
|
||||
</Label>
|
||||
<div className="relative">
|
||||
{contest.active && contestDupe && (
|
||||
<span className="absolute -top-2 right-1 z-20 rounded bg-red-600 px-1.5 py-0.5 text-[10px] font-black tracking-widest text-white shadow animate-pulse pointer-events-none">
|
||||
<span className="absolute -top-2 right-1 z-20 rounded bg-destructive px-1.5 py-0.5 text-[10px] font-black tracking-widest text-destructive-foreground shadow animate-pulse pointer-events-none">
|
||||
DUPE
|
||||
</span>
|
||||
)}
|
||||
@@ -2562,16 +2562,16 @@ export default function App() {
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={resetRecordingClock}
|
||||
title="Click to restart the recording from 0 — drops everything captured so far (incl. pre-roll) so the file holds only your exchange"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 inline-flex items-center gap-1 text-[10px] font-semibold tabular-nums text-red-600 whitespace-nowrap cursor-pointer rounded px-1 hover:bg-red-50"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 inline-flex items-center gap-1 text-[10px] font-semibold tabular-nums text-destructive whitespace-nowrap cursor-pointer rounded px-1 hover:bg-destructive-muted"
|
||||
>
|
||||
<span className="size-2 rounded-full bg-red-600 animate-pulse" />
|
||||
<span className="size-2 rounded-full bg-destructive animate-pulse" />
|
||||
{String(Math.floor(recSeconds / 60)).padStart(2, '0')}:{String(recSeconds % 60).padStart(2, '0')}
|
||||
</button>
|
||||
)}
|
||||
<Input
|
||||
ref={callsignRef}
|
||||
className={cn('font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card',
|
||||
contest.active && contestDupe && 'ring-2 ring-red-500 !bg-red-50 focus:!bg-red-50 text-red-700')}
|
||||
contest.active && contestDupe && 'ring-2 ring-destructive !bg-destructive-muted focus:!bg-destructive-muted text-destructive-muted-foreground')}
|
||||
value={callsign}
|
||||
onChange={(e) => onCallsignInput(e.target.value)}
|
||||
onBlur={() => {
|
||||
@@ -2608,7 +2608,7 @@ export default function App() {
|
||||
// a past QSO). Sets the DATE part of qsoStartedAt; the time field keeps the time.
|
||||
const dateBlock = locks.start ? (
|
||||
<div className="flex flex-col w-40">
|
||||
<Label className="mb-1 h-3.5 text-emerald-700">Date</Label>
|
||||
<Label className="mb-1 h-3.5 text-success">Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={qsoStartedAt ? qsoStartedAt.toISOString().slice(0, 10) : ''}
|
||||
@@ -2626,7 +2626,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-emerald-700">{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')} <LockBtn k="start" title="start time" /></Label>
|
||||
<Input
|
||||
readOnly={!locks.start}
|
||||
tabIndex={locks.start ? 0 : -1}
|
||||
@@ -2645,7 +2645,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-rose-700">{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')} <LockBtn k="end" title="end time" /></Label>
|
||||
<Input
|
||||
readOnly={!locks.end}
|
||||
tabIndex={locks.end ? 0 : -1}
|
||||
@@ -2682,7 +2682,7 @@ export default function App() {
|
||||
<div className="flex flex-col w-16 shrink-0"><Label className="mb-1 h-3.5">{t('field.snt')}</Label>
|
||||
<Input readOnly tabIndex={-1} value={contestSnt} className="font-mono bg-muted/40" />
|
||||
</div>
|
||||
<div className="flex flex-col w-24 shrink-0"><Label className="mb-1 h-3.5 text-amber-600 font-bold">{t('field.rcv')}</Label>
|
||||
<div className="flex flex-col w-24 shrink-0"><Label className="mb-1 h-3.5 text-warning font-bold">{t('field.rcv')}</Label>
|
||||
<Input value={rcv} placeholder="exch" className="font-mono"
|
||||
onChange={(e) => setRcv(e.target.value.toUpperCase())} />
|
||||
</div>
|
||||
@@ -2764,7 +2764,7 @@ export default function App() {
|
||||
);
|
||||
const rxFreqBlock = (
|
||||
<div className="flex flex-col w-32">
|
||||
<Label className={cn('mb-1 h-3.5', catState.split && 'text-rose-600')}>{t('field.rxFreq')}</Label>
|
||||
<Label className={cn('mb-1 h-3.5', catState.split && 'text-danger')}>{t('field.rxFreq')}</Label>
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
value={freqFocused ? rxFreqMhz : (rxFreqMhz ? fmtFreqDots(rxFreqMhz) : '')}
|
||||
@@ -2772,7 +2772,7 @@ export default function App() {
|
||||
onFocus={() => setFreqFocused(true)}
|
||||
onBlur={() => setFreqFocused(false)}
|
||||
onChange={(e) => { setRxFreqMhz(e.target.value); noteManualEdit(); const rb = bandForMHz(parseFloat(e.target.value)); if (rb) setBandRx(rb); }}
|
||||
className={cn('font-mono', catState.split && 'bg-rose-50/40 border-rose-200 focus:bg-card')}
|
||||
className={cn('font-mono', catState.split && 'bg-danger-muted/40 border-danger-border focus:bg-card')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -2924,7 +2924,7 @@ export default function App() {
|
||||
type="button"
|
||||
onClick={() => setClusterLockBand((v) => !v)}
|
||||
className={cn('inline-flex items-center gap-0.5 text-[10px] px-1 py-0.5 rounded border',
|
||||
clusterLockBand ? 'bg-amber-100 text-amber-800 border-amber-300' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
clusterLockBand ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
title="Lock to the entry strip's current band"
|
||||
>
|
||||
{clusterLockBand ? <Lock className="size-2.5" /> : <Unlock className="size-2.5" />} {band}
|
||||
@@ -2957,7 +2957,7 @@ export default function App() {
|
||||
type="button"
|
||||
onClick={() => setClusterLockMode((v) => !v)}
|
||||
className={cn('inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10px]',
|
||||
clusterLockMode ? 'bg-amber-100 text-amber-800 border-amber-300' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
clusterLockMode ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
title="Only show spots whose mode matches the entry strip"
|
||||
>
|
||||
{clusterLockMode ? <Lock className="size-2.5" /> : <Unlock className="size-2.5" />} Lock mode ({mode})
|
||||
@@ -2968,10 +2968,10 @@ export default function App() {
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Status</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{([
|
||||
{ k: 'new' as SpotStatusKey, label: 'NEW', cls: 'bg-rose-100 text-rose-800 border-rose-300' },
|
||||
{ k: 'new-band' as SpotStatusKey, label: 'NEW BAND', cls: 'bg-amber-100 text-amber-800 border-amber-300' },
|
||||
{ k: 'new-mode' as SpotStatusKey, label: 'NEW MODE', cls: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
|
||||
{ k: 'new-slot' as SpotStatusKey, label: 'NEW SLOT', cls: 'bg-yellow-100 text-yellow-800 border-yellow-300' },
|
||||
{ k: 'new' as SpotStatusKey, label: 'NEW', cls: 'bg-danger-muted text-danger-muted-foreground border-danger-border' },
|
||||
{ k: 'new-band' as SpotStatusKey, label: 'NEW BAND', cls: 'bg-warning-muted text-warning-muted-foreground border-warning-border' },
|
||||
{ k: 'new-mode' as SpotStatusKey, label: 'NEW MODE', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' },
|
||||
{ k: 'new-slot' as SpotStatusKey, label: 'NEW SLOT', cls: 'bg-caution-muted text-caution-muted-foreground border-caution-border' },
|
||||
// (no WORKED chip — use the "Hide worked" checkbox to drop dupes.)
|
||||
]).map((s) => {
|
||||
const on = clusterStatusFilter.has(s.k);
|
||||
@@ -2991,9 +2991,9 @@ export default function App() {
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Mode</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{([
|
||||
{ k: 'SSB' as SpotModeCat, label: 'SSB', cls: 'bg-sky-100 text-sky-800 border-sky-300' },
|
||||
{ k: 'CW' as SpotModeCat, label: 'CW', cls: 'bg-violet-100 text-violet-800 border-violet-300' },
|
||||
{ k: 'DATA' as SpotModeCat, label: 'DATA', cls: 'bg-emerald-100 text-emerald-800 border-emerald-300' },
|
||||
{ k: 'SSB' as SpotModeCat, label: 'SSB', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||
{ k: 'CW' as SpotModeCat, label: 'CW', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||
{ k: 'DATA' as SpotModeCat, label: 'DATA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
|
||||
]).map((s) => {
|
||||
const on = clusterModeFilter.has(s.k);
|
||||
return (
|
||||
@@ -3121,7 +3121,7 @@ export default function App() {
|
||||
<span className="text-sm font-semibold text-primary">{freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}</span>
|
||||
<span className="text-[9px] text-muted-foreground">MHz</span>
|
||||
<Badge variant="accent" className="font-mono ml-2 text-[9px] py-0">{band}</Badge>
|
||||
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 font-mono text-[9px] py-0" variant="outline">{mode}</Badge>
|
||||
<Badge className="bg-success-muted text-success-muted-foreground hover:bg-success-muted font-mono text-[9px] py-0" variant="outline">{mode}</Badge>
|
||||
</div>
|
||||
<span className="ml-2 font-mono text-[10px] text-muted-foreground">{utcNow}<span className="text-[9px]">Z</span></span>
|
||||
<div className="flex-1" />
|
||||
@@ -3154,10 +3154,10 @@ export default function App() {
|
||||
<button className="shrink-0 hover:text-destructive/70" onClick={() => setError('')}><X className="size-3" /></button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-emerald-300 bg-emerald-50 text-emerald-800 px-2.5 py-1 text-xs shadow min-w-0 animate-in fade-in">
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-success-border bg-success-muted text-success-muted-foreground px-2.5 py-1 text-xs shadow min-w-0 animate-in fade-in">
|
||||
<Satellite className="size-3.5 shrink-0" />
|
||||
<span className="truncate" title={toast}>{toast}</span>
|
||||
<button className="shrink-0 text-emerald-600 hover:text-emerald-800" onClick={dismissToast}><X className="size-3" /></button>
|
||||
<button className="shrink-0 text-success hover:text-success" onClick={dismissToast}><X className="size-3" /></button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -3166,14 +3166,14 @@ export default function App() {
|
||||
<span className="text-2xl font-semibold text-primary tracking-wide">{freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}</span>
|
||||
{catState.split && rxFreqMhz && (
|
||||
<span className="text-[10px] text-muted-foreground mt-0.5">
|
||||
<span className="text-rose-600 font-semibold mr-1">RX</span>
|
||||
<span className="text-danger font-semibold mr-1">RX</span>
|
||||
{fmtFreqDots(rxFreqMhz)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground uppercase">MHz</span>
|
||||
{catState.split && (
|
||||
<Badge className="bg-amber-100 text-amber-800 border-amber-300 font-mono text-[10px] py-0" variant="outline">SPLIT</Badge>
|
||||
<Badge className="bg-warning-muted text-warning-muted-foreground border-warning-border font-mono text-[10px] py-0" variant="outline">SPLIT</Badge>
|
||||
)}
|
||||
{/* Band & mode removed here — shown in the QSO entry strip below. */}
|
||||
{catState.enabled && catState.backend === 'omnirig' && (
|
||||
@@ -3197,7 +3197,7 @@ export default function App() {
|
||||
const disabled = !p;
|
||||
const goto = (az: number) => RotatorGoTo(Math.round(az), -1).catch((err) => setError(String(err?.message ?? err)));
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full border border-sky-300 bg-sky-100 overflow-hidden text-[11px] font-mono font-semibold">
|
||||
<div className="inline-flex items-center rounded-full border border-info-border bg-info-muted overflow-hidden text-[11px] font-mono font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
@@ -3209,7 +3209,7 @@ export default function App() {
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 transition-colors',
|
||||
disabled
|
||||
? 'text-muted-foreground/60 cursor-not-allowed'
|
||||
: 'text-sky-800 hover:bg-sky-200 cursor-pointer',
|
||||
: 'text-info-muted-foreground hover:bg-info-muted cursor-pointer',
|
||||
)}
|
||||
>
|
||||
<Compass className="size-3" />
|
||||
@@ -3221,10 +3221,10 @@ export default function App() {
|
||||
onClick={() => p && goto(p.bearingLong)}
|
||||
title={p ? `Rotate long-path · ${Math.round(p.distanceLong).toLocaleString()} km` : ''}
|
||||
className={cn(
|
||||
'px-1.5 py-0.5 border-l border-sky-300 text-[10px] transition-colors',
|
||||
'px-1.5 py-0.5 border-l border-info-border text-[10px] transition-colors',
|
||||
disabled
|
||||
? 'text-muted-foreground/60 cursor-not-allowed'
|
||||
: 'text-sky-800 hover:bg-sky-200 cursor-pointer',
|
||||
: 'text-info-muted-foreground hover:bg-info-muted cursor-pointer',
|
||||
)}
|
||||
>
|
||||
LP {p ? `${Math.round(p.bearingLong)}°` : '—'}
|
||||
@@ -3233,7 +3233,7 @@ export default function App() {
|
||||
type="button"
|
||||
onClick={() => RotatorStop().catch((err) => setError(String(err?.message ?? err)))}
|
||||
title="Stop rotation"
|
||||
className="px-1.5 py-0.5 border-l border-sky-300 text-rose-700 hover:bg-rose-100 hover:text-rose-800 cursor-pointer transition-colors"
|
||||
className="px-1.5 py-0.5 border-l border-info-border text-danger hover:bg-danger-muted hover:text-danger-muted-foreground cursor-pointer transition-colors"
|
||||
>
|
||||
<Square className="size-2.5 fill-current" />
|
||||
</button>
|
||||
@@ -3243,16 +3243,16 @@ export default function App() {
|
||||
|
||||
{/* Ultrabeam pattern (Normal / 180° reverse / Bidirectional), next to the azimuth. */}
|
||||
{ubStatus.enabled && (
|
||||
<div className="inline-flex items-center rounded-full border border-emerald-300 bg-emerald-50 overflow-hidden text-[10px] font-semibold ml-1"
|
||||
<div className="inline-flex items-center rounded-full border border-success-border bg-success-muted overflow-hidden text-[10px] font-semibold ml-1"
|
||||
title={ubStatus.connected ? (ubStatus.moving ? 'Ultrabeam: moving…' : 'Ultrabeam pattern') : 'Ultrabeam: connecting…'}>
|
||||
<button type="button" className="pl-1.5 pr-0.5 flex items-center" onClick={() => { setSettingsSection('antenna'); setShowSettings(true); }} title="Antenna settings">
|
||||
<span className={cn('size-2 rounded-full', ubStatus.connected ? (ubStatus.moving ? 'bg-amber-500' : 'bg-emerald-500') : 'bg-muted-foreground/40')} />
|
||||
<span className={cn('size-2 rounded-full', ubStatus.connected ? (ubStatus.moving ? 'bg-warning' : 'bg-success') : 'bg-muted-foreground/40')} />
|
||||
</button>
|
||||
{([{ d: 0, l: 'N', t: 'Normal' }, { d: 1, l: '180°', t: 'Reverse (180°)' }, { d: 2, l: 'Bi', t: 'Bidirectional' }]).map((o) => (
|
||||
<button key={o.d} type="button" disabled={!ubStatus.connected} title={o.t}
|
||||
onClick={() => { SetUltrabeamDirection(o.d).then(() => setUbStatus((s) => ({ ...s, direction: o.d }))).catch((e: any) => setError(String(e?.message ?? e))); }}
|
||||
className={cn('px-1.5 py-0.5 transition-colors',
|
||||
ubStatus.direction === o.d ? 'bg-emerald-600 text-white' : 'text-emerald-800 hover:bg-emerald-100',
|
||||
ubStatus.direction === o.d ? 'bg-success text-success-foreground' : 'text-success-muted-foreground hover:bg-success-muted',
|
||||
!ubStatus.connected && 'opacity-40 cursor-default')}>
|
||||
{o.l}
|
||||
</button>
|
||||
@@ -3268,13 +3268,13 @@ export default function App() {
|
||||
title={dvkStat.playing ? 'Voice keyer — playing' : dvkEnabled ? 'Voice keyer (DVK) — open · click to close' : 'Voice keyer (DVK) · click to open'}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
dvkStat.playing ? 'border-rose-300 bg-rose-100 text-rose-700'
|
||||
: dvkEnabled ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
dvkStat.playing ? 'border-danger-border bg-danger-muted text-danger-muted-foreground'
|
||||
: dvkEnabled ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Mic className="size-4" />
|
||||
{dvkStat.playing && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-rose-500 animate-pulse" />}
|
||||
{dvkStat.playing && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-danger animate-pulse" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -3282,13 +3282,13 @@ export default function App() {
|
||||
title={wkEnabled && wkStatus.connected ? `CW keyer (WinKeyer) — connected${wkStatus.busy ? ', sending' : ''} · click to close` : wkEnabled ? 'CW keyer (WinKeyer) — enabled · click to close' : 'CW keyer (WinKeyer) · click to open'}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
wkStatus.busy ? 'border-amber-300 bg-amber-100 text-amber-800'
|
||||
: wkEnabled ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
wkStatus.busy ? 'border-warning-border bg-warning-muted text-warning-muted-foreground'
|
||||
: wkEnabled ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Zap className="size-4" />
|
||||
{wkStatus.busy && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-amber-500 animate-pulse" />}
|
||||
{wkStatus.busy && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-warning animate-pulse" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -3300,13 +3300,13 @@ export default function App() {
|
||||
}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
cwOn && cwStatus.active ? 'border-emerald-400 bg-emerald-100 text-emerald-800'
|
||||
: cwEnabled ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
cwOn && cwStatus.active ? 'border-success bg-success-muted text-success-muted-foreground'
|
||||
: cwEnabled ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Ear className="size-4" />
|
||||
{cwOn && cwStatus.active && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-emerald-500 animate-pulse" />}
|
||||
{cwOn && cwStatus.active && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success animate-pulse" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -3314,7 +3314,7 @@ export default function App() {
|
||||
title={showRotor ? 'Rotor compass — shown · click to hide' : 'Rotor compass · click to show'}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
showRotor ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
showRotor ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
@@ -3327,12 +3327,12 @@ export default function App() {
|
||||
title={showAntGenius ? 'Antenna Genius — shown · click to hide' : 'Antenna Genius · click to show'}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
showAntGenius ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
showAntGenius ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Antenna className="size-4" />
|
||||
{showAntGenius && agStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-emerald-500" />}
|
||||
{showAntGenius && agStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />}
|
||||
</button>
|
||||
)}
|
||||
{chatAvailable && (
|
||||
@@ -3341,12 +3341,12 @@ export default function App() {
|
||||
onClick={() => setChatOpen((o) => !o)}
|
||||
title={`Multi-op chat${chatUnread > 0 ? ` — ${chatUnread} new` : ''}`}
|
||||
className={cn('relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
chatShown ? 'border-sky-300 bg-sky-50 text-sky-700 hover:bg-sky-100'
|
||||
chatShown ? 'border-info-border bg-info-muted text-info-muted-foreground hover:bg-info-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted')}
|
||||
>
|
||||
<MessageSquare className="size-4" />
|
||||
{chatUnread > 0 && (
|
||||
<span className="absolute -top-1 -right-1 min-w-3.5 h-3.5 px-0.5 rounded-full bg-rose-500 text-white text-[9px] font-bold leading-[14px] text-center">
|
||||
<span className="absolute -top-1 -right-1 min-w-3.5 h-3.5 px-0.5 rounded-full bg-danger text-danger-foreground text-[9px] font-bold leading-[14px] text-center">
|
||||
{chatUnread > 9 ? '9+' : chatUnread}
|
||||
</span>
|
||||
)}
|
||||
@@ -3369,7 +3369,7 @@ export default function App() {
|
||||
<Antenna className="size-3" />
|
||||
{station.callsign}
|
||||
{station.my_grid && (
|
||||
<span className="bg-white/60 px-1.5 rounded text-[11px] text-primary">{station.my_grid}</span>
|
||||
<span className="bg-card/60 px-1.5 rounded text-[11px] text-primary">{station.my_grid}</span>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
@@ -3468,7 +3468,7 @@ export default function App() {
|
||||
Update available: v{updateInfo.latest} — download
|
||||
</button>
|
||||
) : (
|
||||
<p className="mt-1 text-[11px] text-emerald-600">You're up to date</p>
|
||||
<p className="mt-1 text-[11px] text-success">You're up to date</p>
|
||||
)}
|
||||
<p className="mt-3 text-sm">
|
||||
Developed by <span className="font-semibold text-primary">{APP_AUTHOR}</span>
|
||||
@@ -3746,12 +3746,12 @@ export default function App() {
|
||||
|
||||
{/* ===== CW decoder strip (only when enabled AND mode is CW) ===== */}
|
||||
{cwOn && (
|
||||
<div className="ml-2.5 mt-1.5 -mb-1 w-[45%] flex items-center gap-2 rounded-md border border-emerald-300/70 bg-emerald-50/60 px-2 py-1.5 text-xs">
|
||||
<Ear className={cn('size-4 shrink-0', cwStatus.active ? 'text-emerald-600' : 'text-muted-foreground')} />
|
||||
<div className="ml-2.5 mt-1.5 -mb-1 w-[45%] flex items-center gap-2 rounded-md border border-success-border/70 bg-success-muted/60 px-2 py-1.5 text-xs">
|
||||
<Ear className={cn('size-4 shrink-0', cwStatus.active ? 'text-success' : 'text-muted-foreground')} />
|
||||
{/* Input-level meter — if this stays flat with a strong signal, the RX
|
||||
audio device is wrong/silent rather than a decode problem. */}
|
||||
<div className="shrink-0 w-12 h-1.5 rounded bg-muted overflow-hidden" title={`Audio level ${Math.round(cwStatus.level * 100)}%`}>
|
||||
<div className="h-full bg-emerald-500 transition-[width] duration-100" style={{ width: `${Math.min(100, Math.round(cwStatus.level * 100))}%` }} />
|
||||
<div className="h-full bg-success transition-[width] duration-100" style={{ width: `${Math.min(100, Math.round(cwStatus.level * 100))}%` }} />
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-[10px] text-muted-foreground tabular-nums">
|
||||
{cwStatus.wpm > 0 ? `${cwStatus.wpm} WPM` : '— WPM'} · {cwStatus.pitch > 0 ? `${cwStatus.pitch} Hz` : '— Hz'}
|
||||
@@ -3763,7 +3763,7 @@ export default function App() {
|
||||
onChange={(e) => setCwPitch(e.target.value)}
|
||||
placeholder="auto"
|
||||
title="Lock the decoder to this pitch (Hz). Blank = follow the radio's CW pitch / auto-search."
|
||||
className="shrink-0 w-14 h-5 rounded border border-emerald-300/70 bg-white/60 px-1 text-[10px] font-mono text-center outline-none"
|
||||
className="shrink-0 w-14 h-5 rounded border border-success-border/70 bg-card/60 px-1 text-[10px] font-mono text-center outline-none"
|
||||
/>
|
||||
{/* Left-aligned single line, no scrollbar; auto-scrolled to the newest
|
||||
text (see cwScrollRef effect) so the latest stays in view. */}
|
||||
@@ -3776,7 +3776,7 @@ export default function App() {
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
className="mr-1 shrink-0 rounded px-1 leading-none hover:bg-emerald-200/70"
|
||||
className="mr-1 shrink-0 rounded px-1 leading-none hover:bg-success-muted/70"
|
||||
title="Use as callsign"
|
||||
onClick={() => onCallsignInput(tok, { force: true })}
|
||||
>
|
||||
@@ -3813,9 +3813,9 @@ export default function App() {
|
||||
<TabsTrigger value="awards">{t('tab.awards')}</TabsTrigger>
|
||||
<TabsTrigger value="bandmap">{t('tab.bandmap')}</TabsTrigger>
|
||||
{contestTabEnabled && (
|
||||
<TabsTrigger value="contest" className={cn('gap-1.5', contest.active && 'text-amber-600 data-[state=active]:text-amber-600')}>
|
||||
<TabsTrigger value="contest" className={cn('gap-1.5', contest.active && 'text-warning data-[state=active]:text-warning')}>
|
||||
{t('tab.contest')}
|
||||
{contest.active && <span className="inline-block size-1.5 rounded-full bg-amber-500 animate-pulse" />}
|
||||
{contest.active && <span className="inline-block size-1.5 rounded-full bg-warning animate-pulse" />}
|
||||
<span
|
||||
role="button"
|
||||
aria-label="Close Contest"
|
||||
@@ -3879,20 +3879,20 @@ export default function App() {
|
||||
<div className={cn(
|
||||
'mx-2.5 mt-2 px-3 py-2 rounded-md text-xs border flex flex-col gap-1.5',
|
||||
importResult.errors && importResult.errors.length > 0
|
||||
? 'bg-amber-50 border-amber-300 text-amber-800'
|
||||
: 'bg-emerald-50 border-emerald-300 text-emerald-800',
|
||||
? 'bg-warning-muted border-warning-border text-warning-muted-foreground'
|
||||
: 'bg-success-muted border-success-border text-success-muted-foreground',
|
||||
)}>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<strong>Import complete.</strong>
|
||||
<Badge variant="outline" className="bg-white/60 font-mono text-emerald-700 border-emerald-300">{importResult.imported} imported</Badge>
|
||||
<Badge variant="outline" className="bg-card/60 font-mono text-success border-success-border">{importResult.imported} imported</Badge>
|
||||
{importResult.updated > 0 && (
|
||||
<Badge variant="outline" className="bg-white/60 font-mono text-violet-700 border-violet-300">{importResult.updated} updated</Badge>
|
||||
<Badge variant="outline" className="bg-card/60 font-mono text-info border-info-border">{importResult.updated} updated</Badge>
|
||||
)}
|
||||
{importResult.duplicates > 0 && (
|
||||
<Badge variant="outline" className="bg-white/60 font-mono text-sky-700 border-sky-300">{importResult.duplicates} duplicates</Badge>
|
||||
<Badge variant="outline" className="bg-card/60 font-mono text-info border-info-border">{importResult.duplicates} duplicates</Badge>
|
||||
)}
|
||||
<Badge variant="outline" className="bg-white/60 font-mono text-amber-700 border-amber-300">{importResult.skipped} skipped</Badge>
|
||||
<Badge variant="outline" className="bg-white/60 font-mono">{importResult.total} total</Badge>
|
||||
<Badge variant="outline" className="bg-card/60 font-mono text-warning border-warning-border">{importResult.skipped} skipped</Badge>
|
||||
<Badge variant="outline" className="bg-card/60 font-mono">{importResult.total} total</Badge>
|
||||
{importResult.duplicates > 0 && importResult.duplicate_samples && importResult.duplicate_samples.length > 0 && (
|
||||
<button className="underline text-xs" onClick={() => setImportDupsOpen((v) => !v)}>
|
||||
{importDupsOpen ? 'Hide' : 'Show'} duplicates
|
||||
@@ -3968,7 +3968,7 @@ export default function App() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{qsos.length >= qsoLimit && qsos.length < total && (
|
||||
<span className="text-amber-700">Limit reached — raise Max to see more.</span>
|
||||
<span className="text-warning">Limit reached — raise Max to see more.</span>
|
||||
)}
|
||||
<Label className="text-[11px] text-muted-foreground">Max</Label>
|
||||
<Input
|
||||
@@ -4026,14 +4026,14 @@ export default function App() {
|
||||
key={s.server_id}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold border',
|
||||
s.state === 'connected' ? 'bg-emerald-100 text-emerald-800 border-emerald-300' :
|
||||
s.state === 'connecting' || s.state === 'reconnecting' ? 'bg-amber-100 text-amber-800 border-amber-300' :
|
||||
s.state === 'error' ? 'bg-rose-100 text-rose-800 border-rose-300' :
|
||||
s.state === 'connected' ? 'bg-success-muted text-success-muted-foreground border-success-border' :
|
||||
s.state === 'connecting' || s.state === 'reconnecting' ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' :
|
||||
s.state === 'error' ? 'bg-danger-muted text-danger-muted-foreground border-danger-border' :
|
||||
'bg-muted text-muted-foreground border-border',
|
||||
)}
|
||||
title={`${s.host}:${s.port}${s.error ? ' — ' + s.error : ''}`}
|
||||
>
|
||||
{isMaster && <span className="text-amber-600" title="Master (commands go here)">★</span>}
|
||||
{isMaster && <span className="text-warning" title="Master (commands go here)">★</span>}
|
||||
{s.name}
|
||||
<span className="opacity-60 text-[9px] ml-0.5">{s.state.toUpperCase()}{s.retries ? ` #${s.retries}` : ''}</span>
|
||||
</span>
|
||||
@@ -4254,7 +4254,7 @@ export default function App() {
|
||||
: 'border-border hover:bg-muted cursor-pointer',
|
||||
)}
|
||||
>
|
||||
<span className={cn('size-2 rounded-full', on ? 'bg-emerald-500' : 'bg-muted-foreground/40')} />
|
||||
<span className={cn('size-2 rounded-full', on ? 'bg-success' : 'bg-muted-foreground/40')} />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
@@ -4286,7 +4286,7 @@ export default function App() {
|
||||
title={dbConn.backend === 'mysql' ? `Shared MySQL logbook — ${dbConn.label}` : `Local SQLite logbook — ${dbConn.label}`}
|
||||
className="inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground max-w-[340px]"
|
||||
>
|
||||
<Database className={cn('size-3 shrink-0', dbConn.backend === 'mysql' ? 'text-emerald-600' : 'text-muted-foreground')} />
|
||||
<Database className={cn('size-3 shrink-0', dbConn.backend === 'mysql' ? 'text-success' : 'text-muted-foreground')} />
|
||||
<span className="font-mono truncate">{dbConn.label}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user