feat: add light/dark theme toggle with localStorage persistence

This commit is contained in:
Zac Gaetano 2026-04-14 10:14:32 -04:00
parent 51e7e8f327
commit 575502410f

View file

@ -7,12 +7,25 @@ import { ConfigPanel } from './components/ConfigPanel';
import { SCTE35Trigger } from './components/SCTE35Trigger'; import { SCTE35Trigger } from './components/SCTE35Trigger';
import { PortStatus, RecorderConfig } from './types'; import { PortStatus, RecorderConfig } from './types';
type Theme = 'dark' | 'light';
export default function App() { export default function App() {
const [ports, setPorts] = useState<PortStatus[]>([]); const [ports, setPorts] = useState<PortStatus[]>([]);
const [selectedPort, setSelectedPort] = useState<number | null>(null); const [selectedPort, setSelectedPort] = useState<number | null>(null);
const [configPort, setConfigPort] = useState<number | null>(null); const [configPort, setConfigPort] = useState<number | null>(null);
const [configs, setConfigs] = useState<Record<number, RecorderConfig>>({}); const [configs, setConfigs] = useState<Record<number, RecorderConfig>>({});
const [clock, setClock] = useState(''); const [clock, setClock] = useState('');
const [theme, setTheme] = useState<Theme>(() => {
try { return (localStorage.getItem('de-theme') as Theme) ?? 'dark'; } catch { return 'dark'; }
});
// Apply theme to <html>
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('de-theme', theme); } catch {}
}, [theme]);
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
const wsUrl = `ws://${window.location.host}/ws`; const wsUrl = `ws://${window.location.host}/ws`;
const { isConnected, lastMessage } = useWebSocket(wsUrl); const { isConnected, lastMessage } = useWebSocket(wsUrl);
@ -71,7 +84,7 @@ export default function App() {
setSelectedPort(prev => (prev === portIndex ? null : portIndex)); setSelectedPort(prev => (prev === portIndex ? null : portIndex));
}; };
const selectedPortData = ports.find(p => p.port_index === selectedPort) ?? null; const isDark = theme === 'dark';
return ( return (
<div style={{ minHeight: '100vh', background: 'var(--bg-deepest)', color: 'var(--text-primary)' }}> <div style={{ minHeight: '100vh', background: 'var(--bg-deepest)', color: 'var(--text-primary)' }}>
@ -88,7 +101,7 @@ export default function App() {
position: 'sticky', position: 'sticky',
top: 0, top: 0,
zIndex: 100, zIndex: 100,
boxShadow: '0 2px 20px rgba(0,0,0,0.6)', boxShadow: isDark ? '0 2px 20px rgba(0,0,0,0.6)' : '0 2px 12px rgba(0,0,0,0.08)',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<img <img
@ -96,7 +109,9 @@ export default function App() {
alt="Dragon Encode" alt="Dragon Encode"
style={{ style={{
width: 36, height: 36, objectFit: 'contain', width: 36, height: 36, objectFit: 'contain',
filter: 'drop-shadow(0 0 6px rgba(26,58,255,0.5))', filter: isDark
? 'drop-shadow(0 0 6px rgba(26,58,255,0.5))'
: 'drop-shadow(0 0 4px rgba(26,58,255,0.3))',
}} }}
/> />
<div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1 }}> <div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1 }}>
@ -112,7 +127,8 @@ export default function App() {
</div> </div>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
{/* WS status */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-secondary)', letterSpacing: '0.08em' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-secondary)', letterSpacing: '0.08em' }}>
<div style={{ <div style={{
width: 8, height: 8, borderRadius: '50%', width: 8, height: 8, borderRadius: '50%',
@ -122,10 +138,41 @@ export default function App() {
}} /> }} />
<span>{isConnected ? 'LIVE' : 'DISCONNECTED'}</span> <span>{isConnected ? 'LIVE' : 'DISCONNECTED'}</span>
</div> </div>
<div style={{ width: 1, height: 28, background: 'var(--border)' }} /> <div style={{ width: 1, height: 28, background: 'var(--border)' }} />
{/* Clock */}
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--text-mono)', letterSpacing: '0.05em' }}> <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--text-mono)', letterSpacing: '0.05em' }}>
{clock} {clock}
</span> </span>
<div style={{ width: 1, height: 28, background: 'var(--border)' }} />
{/* Theme toggle */}
<button
onClick={toggleTheme}
title={isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
style={{
background: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)',
border: '1px solid var(--border)',
borderRadius: 3,
color: 'var(--text-secondary)',
cursor: 'pointer',
fontFamily: 'var(--font-ui)',
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.12em',
textTransform: 'uppercase',
padding: '6px 12px',
display: 'flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.15s',
}}
>
<span style={{ fontSize: 14, lineHeight: 1 }}>{isDark ? '☀' : '◑'}</span>
{isDark ? 'Light' : 'Dark'}
</button>
</div> </div>
</header> </header>
@ -185,7 +232,7 @@ export default function App() {
animation: 'panelEnter 0.25s ease both', animation: 'panelEnter 0.25s ease both',
}}> }}>
{/* Preview */} {/* Preview */}
<div style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden' }}> <div style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden', boxShadow: 'var(--shadow-card)' }}>
<div style={{ <div style={{
padding: '12px 16px', background: 'var(--bg-panel)', padding: '12px 16px', background: 'var(--bg-panel)',
borderBottom: '1px solid var(--border)', borderBottom: '1px solid var(--border)',
@ -203,8 +250,8 @@ export default function App() {
</div> </div>
</div> </div>
{/* Global SCTE-35 panel */} {/* Global SCTE-35 */}
<div style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden' }}> <div style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden', boxShadow: 'var(--shadow-card)' }}>
<div style={{ <div style={{
padding: '12px 16px', background: 'var(--bg-panel)', padding: '12px 16px', background: 'var(--bg-panel)',
borderBottom: '1px solid var(--border)', borderBottom: '1px solid var(--border)',
@ -246,7 +293,6 @@ export default function App() {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 14px; gap: 14px;
} }
@media (max-width: 1300px) { .ports-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 900px) { .ports-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 900px) { .ports-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 500px) { .ports-grid { grid-template-columns: 1fr; } } @media (max-width: 500px) { .ports-grid { grid-template-columns: 1fr; } }
`}</style> `}</style>