feat: Themes added, 4 themes available (3 dark, 1 light)
This commit is contained in:
@@ -59,6 +59,7 @@ import {
|
||||
import { cn } from '@/lib/utils';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n';
|
||||
import { useTheme, CONCRETE_THEMES, type ThemeChoice } from '@/lib/theme';
|
||||
import { OperatingPanel } from '@/components/OperatingPanel';
|
||||
import { UDPIntegrationsPanel } from '@/components/UDPIntegrationsPanel';
|
||||
|
||||
@@ -333,6 +334,68 @@ function TreeNodeView({
|
||||
|
||||
// ===== Section content panels =====
|
||||
|
||||
// Representative surface/card/accent swatch for each theme (picker preview —
|
||||
// the theme is not applied until selected). Module-scope so their component
|
||||
// identity is STABLE across SettingsModal re-renders; defining them inside the
|
||||
// component would give each render a fresh function, remounting the Radix
|
||||
// Select and slamming the open dropdown shut on any ambient re-render.
|
||||
const THEME_SWATCH: Record<Exclude<ThemeChoice, 'auto'>, { bg: string; card: string; accent: string }> = {
|
||||
'light-warm': { bg: '#e8dfc9', card: '#faf6ea', accent: '#b8410c' },
|
||||
'dark-warm': { bg: '#221d18', card: '#2e2820', accent: '#e07a2e' },
|
||||
'dark-graphite': { bg: '#16181d', card: '#1f232b', accent: '#f97316' },
|
||||
'high-contrast': { bg: '#000000', card: '#0d0d0d', accent: '#ff7a1a' },
|
||||
};
|
||||
|
||||
function ThemeSwatch({ theme }: { theme: Exclude<ThemeChoice, 'auto'> }) {
|
||||
const s = THEME_SWATCH[theme];
|
||||
return (
|
||||
<span className="flex h-5 w-8 overflow-hidden rounded-[3px] border border-black/20 shrink-0" style={{ background: s.bg }}>
|
||||
<span className="m-0.5 flex flex-1 items-center justify-center rounded-[2px]" style={{ background: s.card }}>
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: s.accent }} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Auto swatch = split light/dark, since it follows the OS preference.
|
||||
function ThemeOptionSwatch({ theme }: { theme: ThemeChoice }) {
|
||||
if (theme === 'auto') {
|
||||
return (
|
||||
<span className="flex h-5 w-8 overflow-hidden rounded-[3px] border border-black/20 shrink-0">
|
||||
<span className="flex-1" style={{ background: '#e8dfc9' }} />
|
||||
<span className="flex-1" style={{ background: '#16181d' }} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <ThemeSwatch theme={theme} />;
|
||||
}
|
||||
|
||||
function ThemeSelector() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useI18n();
|
||||
const options: ThemeChoice[] = ['auto', ...CONCRETE_THEMES];
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm w-40">{t('settings.theme')}</Label>
|
||||
<Select value={theme} onValueChange={(v) => setTheme(v as ThemeChoice)}>
|
||||
<SelectTrigger className="h-9 w-64" title={t('settings.themeHint')}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
<span className="flex items-center gap-2.5">
|
||||
<ThemeOptionSwatch theme={opt} />
|
||||
{t(`theme.${opt}`)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ title, hint }: { title: string; hint?: string }) {
|
||||
return (
|
||||
<header className="mb-4">
|
||||
@@ -646,7 +709,7 @@ function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
|
||||
</p>
|
||||
</div>
|
||||
{rxList.length === 0 && (
|
||||
<div className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-md px-3 py-2 max-w-xl">
|
||||
<div className="text-xs text-warning-muted-foreground bg-warning-muted border border-warning-border rounded-md px-3 py-2 max-w-xl">
|
||||
No antennas reported yet — make sure the FlexRadio is connected (CAT interface), then reopen this panel.
|
||||
</div>
|
||||
)}
|
||||
@@ -1474,7 +1537,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
disabled={!current}
|
||||
/>
|
||||
{current?.is_active && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold tracking-wider bg-emerald-100 text-emerald-800 border border-emerald-300">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold tracking-wider bg-success-muted text-success-muted-foreground border border-success-border">
|
||||
{t('prof.active')}
|
||||
</span>
|
||||
)}
|
||||
@@ -1577,7 +1640,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{testing ? t('lk.testing') : t('lk.test')}
|
||||
</Button>
|
||||
</td>
|
||||
<td className={cn('px-2 py-2 text-xs', test?.ok ? 'text-emerald-700' : 'text-destructive')}>
|
||||
<td className={cn('px-2 py-2 text-xs', test?.ok ? 'text-success' : 'text-destructive')}>
|
||||
{test?.msg}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2143,7 +2206,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className={cn(
|
||||
'text-xs rounded-md p-2.5 border',
|
||||
ubTest.ok
|
||||
? 'bg-emerald-50 text-emerald-800 border-emerald-200'
|
||||
? 'bg-success-muted text-success-muted-foreground border-success-border'
|
||||
: 'bg-destructive/10 text-destructive border-destructive/30',
|
||||
)}>
|
||||
{ubTest.msg}
|
||||
@@ -2278,7 +2341,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className={cn(
|
||||
'text-xs rounded-md p-2.5 border',
|
||||
rotatorTest.ok
|
||||
? 'bg-emerald-50 text-emerald-800 border-emerald-200'
|
||||
? 'bg-success-muted text-success-muted-foreground border-success-border'
|
||||
: 'bg-destructive/10 text-destructive border-destructive/30',
|
||||
)}>
|
||||
{rotatorTest.msg}
|
||||
@@ -2336,7 +2399,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).
|
||||
</p>
|
||||
{(!catCfg.enabled || catCfg.backend !== 'icom') && (
|
||||
<p className="text-xs font-medium text-amber-600 -mt-1 flex items-start gap-1.5">
|
||||
<p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
|
||||
<span aria-hidden>⚠</span>
|
||||
<span>
|
||||
Your CAT backend is set to <strong>{catCfg.enabled ? (catCfg.backend || 'none') : 'disabled'}</strong>. Icom CI-V CW needs the CAT backend set to <strong>Icom</strong> and connected — change it under Settings → CAT interface, otherwise sending CW will fail.
|
||||
@@ -2536,16 +2599,16 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<td className="px-3 py-2 font-medium">
|
||||
{s.name}
|
||||
{isMaster && s.enabled && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0 rounded text-[9px] font-bold tracking-wider bg-amber-100 text-amber-800 border border-amber-300">MASTER</span>
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0 rounded text-[9px] font-bold tracking-wider bg-warning-muted text-warning-muted-foreground border border-warning-border">MASTER</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono text-xs">{s.host}:{s.port}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={cn(
|
||||
'inline-flex items-center px-1.5 py-0 rounded text-[9px] font-bold tracking-wider border',
|
||||
state === 'connected' ? 'bg-emerald-100 text-emerald-800 border-emerald-300' :
|
||||
state === 'connecting' || state === 'reconnecting' ? 'bg-amber-100 text-amber-800 border-amber-300' :
|
||||
state === 'error' ? 'bg-rose-100 text-rose-800 border-rose-300' :
|
||||
state === 'connected' ? 'bg-success-muted text-success-muted-foreground border-success-border' :
|
||||
state === 'connecting' || state === 'reconnecting' ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' :
|
||||
state === 'error' ? 'bg-danger-muted text-danger-muted-foreground border-danger-border' :
|
||||
'bg-muted text-muted-foreground border-border',
|
||||
)}>
|
||||
{state.toUpperCase()}
|
||||
@@ -2555,7 +2618,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<td className="px-2 py-2">
|
||||
<div className="flex items-center gap-0.5 justify-end">
|
||||
{state === 'connected' || state === 'connecting' || state === 'reconnecting' ? (
|
||||
<Button variant="ghost" size="icon" className="size-6 text-emerald-600 hover:text-emerald-700"
|
||||
<Button variant="ghost" size="icon" className="size-6 text-success hover:text-success"
|
||||
onClick={async () => { await DisconnectClusterServer(s.id as number).catch(() => {}); await reloadClusterServers(); }}
|
||||
title={t('clu.disconnect')}><Power className="size-3.5" /></Button>
|
||||
) : (
|
||||
@@ -2874,8 +2937,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className={cn(
|
||||
'text-xs px-3 py-2 rounded-md border',
|
||||
backupResult.ok
|
||||
? 'bg-emerald-50 border-emerald-300 text-emerald-800'
|
||||
: 'bg-rose-50 border-rose-300 text-rose-800',
|
||||
? 'bg-success-muted border-success-border text-success-muted-foreground'
|
||||
: 'bg-danger-muted border-danger-border text-danger-muted-foreground',
|
||||
)}>
|
||||
{backupResult.msg}
|
||||
</div>
|
||||
@@ -3083,7 +3146,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<UploadCloud className="size-3.5" /> {qrzTesting ? t('es.testing') : t('es.testConn')}
|
||||
</Button>
|
||||
{qrzTest && (
|
||||
<span className={cn('text-xs', qrzTest.ok ? 'text-emerald-700' : 'text-rose-700')}>
|
||||
<span className={cn('text-xs', qrzTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{qrzTest.msg}
|
||||
</span>
|
||||
)}
|
||||
@@ -3147,7 +3210,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<UploadCloud className="size-3.5" /> {clublogTesting ? t('es.testing') : t('es.testConn')}
|
||||
</Button>
|
||||
{clublogTest && (
|
||||
<span className={cn('text-xs', clublogTest.ok ? 'text-emerald-700' : 'text-rose-700')}>
|
||||
<span className={cn('text-xs', clublogTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{clublogTest.msg}
|
||||
</span>
|
||||
)}
|
||||
@@ -3206,7 +3269,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<UploadCloud className="size-3.5" /> {hrdlogTesting ? t('es.testing') : t('es.testConn')}
|
||||
</Button>
|
||||
{hrdlogTest && (
|
||||
<span className={cn('text-xs', hrdlogTest.ok ? 'text-emerald-700' : 'text-rose-700')}>
|
||||
<span className={cn('text-xs', hrdlogTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{hrdlogTest.msg}
|
||||
</span>
|
||||
)}
|
||||
@@ -3272,7 +3335,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<UploadCloud className="size-3.5" /> {eqslTesting ? t('es.testing') : t('es.testConn')}
|
||||
</Button>
|
||||
{eqslTest && (
|
||||
<span className={cn('text-xs', eqslTest.ok ? 'text-emerald-700' : 'text-rose-700')}>
|
||||
<span className={cn('text-xs', eqslTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{eqslTest.msg}
|
||||
</span>
|
||||
)}
|
||||
@@ -3389,7 +3452,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<UploadCloud className="size-3.5" /> {lotwTesting ? t('es.testing') : t('es.testConn')}
|
||||
</Button>
|
||||
{lotwTest && (
|
||||
<span className={cn('text-xs', lotwTest.ok ? 'text-emerald-700' : 'text-rose-700')}>
|
||||
<span className={cn('text-xs', lotwTest.ok ? 'text-success' : 'text-danger')}>
|
||||
{lotwTest.msg}
|
||||
</span>
|
||||
)}
|
||||
@@ -3423,7 +3486,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
{potaResult && (
|
||||
<div className={cn('text-xs rounded-md px-3 py-2 border',
|
||||
potaResult.ok ? 'border-emerald-300 bg-emerald-50 text-emerald-800' : 'border-destructive/30 bg-destructive/10 text-destructive')}>
|
||||
potaResult.ok ? 'border-success-border bg-success-muted text-success-muted-foreground' : 'border-destructive/30 bg-destructive/10 text-destructive')}>
|
||||
{potaResult.msg}
|
||||
</div>
|
||||
)}
|
||||
@@ -3514,14 +3577,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
}}>
|
||||
{t('db.saveSwitch')}
|
||||
</Button>
|
||||
{restartMsg && <span className="text-[11px] text-emerald-700">{restartMsg}</span>}
|
||||
{restartMsg && <span className="text-[11px] text-success">{restartMsg}</span>}
|
||||
</div>
|
||||
|
||||
{/* Active-backend status: confirms what OpsLog actually opened at launch. */}
|
||||
{backendStatus && (
|
||||
<div className="max-w-2xl mb-4">
|
||||
{backendStatus.fallback ? (
|
||||
<div className="text-xs bg-amber-50 border border-amber-300 text-amber-800 rounded-md px-3 py-2">
|
||||
<div className="text-xs bg-warning-muted border border-warning-border text-warning-muted-foreground rounded-md px-3 py-2">
|
||||
{t('db.fallback')}
|
||||
<div className="font-mono text-[10px] mt-1 break-all">{backendStatus.error}</div>
|
||||
</div>
|
||||
@@ -3549,7 +3612,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className="font-mono text-xs bg-muted/40 border border-border rounded-md px-3 py-2 break-all">
|
||||
{dbSettings.path || '—'}
|
||||
{dbSettings.is_custom
|
||||
? <span className="ml-2 text-[10px] text-emerald-700">{t('db.customLoc')}</span>
|
||||
? <span className="ml-2 text-[10px] text-success">{t('db.customLoc')}</span>
|
||||
: <span className="ml-2 text-[10px] text-muted-foreground">{t('db.default')}</span>}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">{t('db.defaultLabel')} <span className="font-mono">{dbSettings.default_path}</span></div>
|
||||
@@ -3563,7 +3626,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
|
||||
{dbMsg && (
|
||||
<div className="text-xs bg-emerald-50 border border-emerald-300 text-emerald-800 rounded-md px-3 py-2 flex items-center justify-between gap-3 whitespace-pre-line">
|
||||
<div className="text-xs bg-success-muted border border-success-border text-success-muted-foreground rounded-md px-3 py-2 flex items-center justify-between gap-3 whitespace-pre-line">
|
||||
<span>{dbMsg}</span>
|
||||
<Button size="sm" variant="destructive" className="shrink-0" onClick={() => QuitApp()}>{t('db.quitNow')}</Button>
|
||||
</div>
|
||||
@@ -3826,6 +3889,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ThemeSelector />
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={autofocusWB} onCheckedChange={(c) => { const v = !!c; setAutofocusWB(v); writeUiPref('opslog.autofocusWB', v ? '1' : '0'); }} />
|
||||
{t('gen.autofocusWB')}
|
||||
@@ -3858,8 +3923,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Passwords encrypted{' '}
|
||||
{secret.unlocked
|
||||
? <span className="text-emerald-700 font-medium">— unlocked</span>
|
||||
: <span className="text-amber-600 font-medium">— locked (unlock at launch)</span>}.
|
||||
? <span className="text-success font-medium">— unlocked</span>
|
||||
: <span className="text-warning font-medium">— locked (unlock at launch)</span>}.
|
||||
</p>
|
||||
{secret.unlocked && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user