103 lines
5.3 KiB
TypeScript
103 lines
5.3 KiB
TypeScript
import { Antenna, X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useI18n } from '@/lib/i18n';
|
|
|
|
export type AGAntenna = { index: number; name: string };
|
|
export type AGStatus = {
|
|
connected: boolean; host?: string; last_error?: string;
|
|
port_a: number; port_b: number; tx_a?: boolean; tx_b?: boolean;
|
|
antennas: AGAntenna[];
|
|
};
|
|
|
|
// Format an antenna name: first letter uppercase, the rest lowercase
|
|
// (e.g. "DX COMMANDER" → "Dx commander").
|
|
function pretty(name: string): string {
|
|
const t = name.trim();
|
|
if (!t) return t;
|
|
return t.charAt(0).toUpperCase() + t.slice(1).toLowerCase();
|
|
}
|
|
|
|
// AntGeniusPanel — antenna-switch widget for a 4O3A Antenna Genius, styled to
|
|
// match the app's light theme with soft gradients + glows. Each antenna row has
|
|
// a port-A button (left) and port-B button (right). Colours: green = selected on
|
|
// port A, blue = selected on port B, red (pulsing) = that port is transmitting.
|
|
// Clicking an already-selected port deselects it (port → None).
|
|
export function AntGeniusPanel({ status, onActivate, onClose }: {
|
|
status: AGStatus;
|
|
onActivate: (port: number, antenna: number) => void; // antenna 0 = deselect
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const list = status.antennas ?? [];
|
|
|
|
const PortBtn = ({ port, index, active, tx }: { port: 1 | 2; index: number; active: boolean; tx: boolean }) => {
|
|
const letter = port === 1 ? 'A' : 'B';
|
|
const cls = tx
|
|
? 'bg-gradient-to-b from-red-500 to-rose-600 text-white border-red-400/50 shadow-[0_0_10px_rgba(244,63,94,0.5)] animate-pulse'
|
|
: active
|
|
? (port === 1
|
|
? 'bg-gradient-to-b from-emerald-400 to-emerald-600 text-white border-emerald-300/60 shadow-[0_0_9px_rgba(16,185,129,0.45)]'
|
|
: 'bg-gradient-to-b from-sky-400 to-sky-600 text-white border-sky-300/60 shadow-[0_0_9px_rgba(14,165,233,0.45)]')
|
|
: 'bg-card text-muted-foreground border-border hover:bg-muted hover:text-foreground';
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => onActivate(port, active ? 0 : index)}
|
|
title={active ? t('agp.portDeselect', { letter }) : t('agp.portSelect', { letter })}
|
|
className={cn('w-8 shrink-0 rounded-lg text-xs font-bold py-1.5 border transition-all active:scale-95', cls)}
|
|
>
|
|
{letter}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col rounded-xl border border-border bg-gradient-to-b from-card to-muted/30 shadow-sm overflow-hidden">
|
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/40 shrink-0">
|
|
<Antenna className={cn('size-4', status.connected ? 'text-success drop-shadow-[0_0_3px_rgba(16,185,129,0.55)]' : 'text-muted-foreground')} />
|
|
<span className="text-xs font-bold uppercase tracking-[0.18em] text-foreground/80">Antenna Genius</span>
|
|
<span className="flex-1" />
|
|
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider">
|
|
<span className={cn('size-1.5 rounded-full', status.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)] animate-pulse' : 'bg-danger')} />
|
|
<span className={status.connected ? 'text-success' : 'text-danger'}>{status.connected ? t('agp.online') : t('agp.offline')}</span>
|
|
</span>
|
|
<button type="button" onClick={onClose} className="ml-1 text-muted-foreground hover:text-foreground transition-colors" title={t('agp.close')}>
|
|
<X className="size-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 min-h-0 overflow-y-auto p-2 space-y-1.5">
|
|
{!status.connected ? (
|
|
<div className="text-center py-6 text-xs space-y-2">
|
|
<div className="text-muted-foreground italic animate-pulse">{t('agp.connecting')}</div>
|
|
{status.last_error && <div className="text-danger font-mono text-[10px] break-words px-2">{status.last_error}</div>}
|
|
</div>
|
|
) : list.length === 0 ? (
|
|
<div className="text-muted-foreground italic text-center py-6 text-xs">{t('agp.noAntennas')}</div>
|
|
) : list.map((a) => {
|
|
const aActive = status.port_a === a.index;
|
|
const bActive = status.port_b === a.index;
|
|
const aTx = aActive && !!status.tx_a;
|
|
const bTx = bActive && !!status.tx_b;
|
|
const nameCls = (aTx || bTx)
|
|
? 'bg-gradient-to-r from-red-500 to-rose-600 text-white border-red-400/40 shadow-[0_0_11px_rgba(244,63,94,0.35)]'
|
|
: aActive
|
|
? 'bg-gradient-to-r from-emerald-500 to-emerald-600 text-white border-emerald-400/40 shadow-[0_0_11px_rgba(16,185,129,0.3)]'
|
|
: bActive
|
|
? 'bg-gradient-to-r from-sky-500 to-sky-600 text-white border-sky-400/40 shadow-[0_0_11px_rgba(14,165,233,0.3)]'
|
|
: 'bg-card/70 text-foreground/80 border-border hover:bg-muted/60';
|
|
return (
|
|
<div key={a.index} className="flex items-center gap-1.5">
|
|
<PortBtn port={1} index={a.index} active={aActive} tx={aTx} />
|
|
<div className={cn('flex-1 min-w-0 truncate text-center text-xs font-semibold tracking-wide rounded-lg px-2 py-1.5 border transition-all', nameCls)}>
|
|
{pretty(a.name)}
|
|
</div>
|
|
<PortBtn port={2} index={a.index} active={bActive} tx={bTx} />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|