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 { 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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue