feat: add light/dark theme toggle with localStorage persistence
This commit is contained in:
parent
51e7e8f327
commit
575502410f
1 changed files with 54 additions and 8 deletions
|
|
@ -7,12 +7,25 @@ import { ConfigPanel } from './components/ConfigPanel';
|
|||
import { SCTE35Trigger } from './components/SCTE35Trigger';
|
||||
import { PortStatus, RecorderConfig } from './types';
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
|
||||
export default function App() {
|
||||
const [ports, setPorts] = useState<PortStatus[]>([]);
|
||||
const [selectedPort, setSelectedPort] = useState<number | null>(null);
|
||||
const [configPort, setConfigPort] = useState<number | null>(null);
|
||||
const [configs, setConfigs] = useState<Record<number, RecorderConfig>>({});
|
||||
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 { isConnected, lastMessage } = useWebSocket(wsUrl);
|
||||
|
|
@ -71,7 +84,7 @@ export default function App() {
|
|||
setSelectedPort(prev => (prev === portIndex ? null : portIndex));
|
||||
};
|
||||
|
||||
const selectedPortData = ports.find(p => p.port_index === selectedPort) ?? null;
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--bg-deepest)', color: 'var(--text-primary)' }}>
|
||||
|
|
@ -88,7 +101,7 @@ export default function App() {
|
|||
position: 'sticky',
|
||||
top: 0,
|
||||
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 }}>
|
||||
<img
|
||||
|
|
@ -96,7 +109,9 @@ export default function App() {
|
|||
alt="Dragon Encode"
|
||||
style={{
|
||||
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 }}>
|
||||
|
|
@ -112,7 +127,8 @@ export default function App() {
|
|||
</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={{
|
||||
width: 8, height: 8, borderRadius: '50%',
|
||||
|
|
@ -122,10 +138,41 @@ export default function App() {
|
|||
}} />
|
||||
<span>{isConnected ? 'LIVE' : 'DISCONNECTED'}</span>
|
||||
</div>
|
||||
|
||||
<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' }}>
|
||||
{clock}
|
||||
</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>
|
||||
</header>
|
||||
|
||||
|
|
@ -185,7 +232,7 @@ export default function App() {
|
|||
animation: 'panelEnter 0.25s ease both',
|
||||
}}>
|
||||
{/* 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={{
|
||||
padding: '12px 16px', background: 'var(--bg-panel)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
|
|
@ -203,8 +250,8 @@ export default function App() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global SCTE-35 panel */}
|
||||
<div style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden' }}>
|
||||
{/* Global SCTE-35 */}
|
||||
<div style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, overflow: 'hidden', boxShadow: 'var(--shadow-card)' }}>
|
||||
<div style={{
|
||||
padding: '12px 16px', background: 'var(--bg-panel)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
|
|
@ -246,7 +293,6 @@ export default function App() {
|
|||
grid-template-columns: repeat(4, 1fr);
|
||||
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: 500px) { .ports-grid { grid-template-columns: 1fr; } }
|
||||
`}</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue