fix: restore theme on restart + consistent spot pills + red DVK TX

theme: self-heal the persisted theme after mount. The synchronous boot
read only looks at localStorage, which is empty when the WebView cleared
its storage or when syncPortablePrefs ran before the backend wired its
settings store (GetUIPref returned "" with no error) — so a restart could
silently land on the default light theme. ThemeProvider now re-reads the
portable pref from the DB once the backend is up (retrying briefly to ride
out a slow startup) and applies it, without clobbering a manual pick.

ClusterGrid: the Call cell now uses a danger pill for NEW DXCC, consistent
with the NEW BAND / NEW MODE pills, instead of a full-cell red fill. cellChip
inherits the column's font size (no fixed 9px / height) so a pill around a
callsign stays the same size as the plain callsigns beside it.

DvkPanel: the voice-keyer TX indicator (LED + label) is red while
transmitting, not orange.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-18 19:28:55 +02:00
co-authored by Claude Opus 4.8
parent 3ed9f29d9a
commit 5abe4bd0c3
3 changed files with 49 additions and 14 deletions
+33 -1
View File
@@ -1,5 +1,6 @@
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
import { writeUiPref } from './uiPref';
import { GetUIPref } from '../../wailsjs/go/main/App';
// Theme system. Each choice maps to a `data-theme` value on <html> that the
// CSS variables in style.css key off of. 'auto' follows the OS light/dark
@@ -49,13 +50,44 @@ export function useTheme(): Ctx { return useContext(ThemeCtx); }
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<ThemeChoice>(() => readStored());
// Set once the operator changes the theme by hand, so the self-heal below
// never clobbers a fresh choice with a value it read a moment earlier.
const userPicked = useRef(false);
const setTheme = useCallback((t: ThemeChoice) => {
userPicked.current = true;
setThemeState(t);
applyThemeToDom(t);
writeUiPref(LS_KEY, t);
}, []);
// Self-heal the persisted theme. The synchronous boot read (localStorage) can
// miss it when the WebView cleared its storage, OR when syncPortablePrefs ran
// while the backend was still starting (settings store not wired yet → GetUIPref
// returned "" with no error, so nothing was restored) — the "restart lands on
// the light theme sometimes" bug. Re-read the portable pref from the DB once the
// backend is up and apply it, retrying briefly to ride out a slow startup.
useEffect(() => {
let cancelled = false;
let tries = 0;
const load = () => {
tries += 1;
GetUIPref(LS_KEY).then((raw) => {
if (cancelled || userPicked.current) return;
const v = raw as ThemeChoice;
if (v && ALL.includes(v)) {
try { localStorage.setItem(LS_KEY, v); } catch { /* quota */ }
applyThemeToDom(v); // idempotent — safe to call unconditionally
setThemeState(v);
return; // restored
}
if (tries < 8) window.setTimeout(load, 300); // empty (unset or backend not ready yet) → retry
}).catch(() => { if (!cancelled && tries < 8) window.setTimeout(load, 300); });
};
load();
return () => { cancelled = true; };
}, []);
// While in 'auto', re-resolve when the OS light/dark preference flips.
useEffect(() => {
if (theme !== 'auto') return;