Add night mode red overlay for dark-adapted vision

- NightModeProvider context (localStorage persisted) in contexts/NightMode.tsx
- Full-screen fixed red overlay (rgba 160,0,0 @ 55%, mix-blend-mode: multiply) fades in over the entire UI; multiply blend keeps dark backgrounds black while turning all white/bright content deep red
- Desktop: toggle button at the bottom of the sidebar, glows red when active
- Mobile: floating red circle button fixed just above the bottom nav bar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 07:38:37 +02:00
parent 561de4f13b
commit 4fbc578413
4 changed files with 110 additions and 15 deletions
+62 -12
View File
@@ -3,6 +3,7 @@ import { useTonight } from '../../hooks/useTonight';
import { useWeather, useForecast } from '../../hooks/useWeather';
import MoonPhaseIcon from '../sky/MoonPhaseIcon';
import GoNogo from '../weather/GoNogo';
import { useNightMode } from '../../contexts/NightMode';
const SEEING_LABELS: Record<number, string> = {
1: '0.5″', 2: '0.75″', 3: '1.0″', 4: '1.25″',
@@ -31,19 +32,48 @@ function fmtTime(utc?: string): string {
}
export function BottomNav() {
const { on, toggle } = useNightMode();
return (
<nav className="bottom-nav">
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => isActive ? 'active' : ''}
>
<span className="bnav-icon">{item.icon}</span>
{item.label}
</NavLink>
))}
</nav>
<>
{/* Night mode floating toggle — sits just above bottom nav */}
<button
onClick={toggle}
title={on ? 'Exit night mode' : 'Night mode'}
style={{
position: 'fixed',
bottom: 66,
right: 14,
zIndex: 210,
width: 36, height: 36,
borderRadius: '50%',
background: on ? 'rgba(160,0,0,0.85)' : 'var(--bg-panel)',
border: `1px solid ${on ? '#800000' : 'var(--border)'}`,
color: on ? '#ff6666' : 'var(--text-lo)',
fontSize: 16,
display: 'none', // shown by .bottom-nav display:flex breakpoint via CSS class
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: on ? '0 0 12px rgba(160,0,0,0.5)' : 'none',
transition: 'all 0.3s',
}}
className="night-fab"
>
🔴
</button>
<nav className="bottom-nav">
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => isActive ? 'active' : ''}
>
<span className="bnav-icon">{item.icon}</span>
{item.label}
</NavLink>
))}
</nav>
</>
);
}
@@ -51,6 +81,7 @@ export default function Sidebar() {
const { data: tonight } = useTonight();
const { data: weather } = useWeather();
const { data: forecast } = useForecast();
const { on: nightOn, toggle: nightToggle } = useNightMode();
const slot = (forecast as { dataseries?: { seeing?: number; transparency?: number; cloudcover?: number }[] })?.dataseries?.[0];
@@ -147,6 +178,25 @@ export default function Sidebar() {
</table>
</div>
{/* Night mode toggle */}
<div style={{ borderTop: '1px solid var(--border)', padding: '10px 16px' }}>
<button
onClick={nightToggle}
style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 10,
background: nightOn ? 'rgba(160,0,0,0.2)' : 'transparent',
border: `1px solid ${nightOn ? '#600000' : 'var(--border)'}`,
borderRadius: 4, padding: '7px 10px', cursor: 'pointer',
color: nightOn ? '#ff4444' : 'var(--text-lo)',
fontFamily: 'var(--font-mono)', fontSize: 11,
letterSpacing: '0.06em', transition: 'all 0.2s',
}}
>
<span style={{ fontSize: 13 }}>🔴</span>
{nightOn ? 'Exit Night Mode' : 'Night Mode'}
</button>
</div>
{/* Conditions widget */}
<div style={{ borderTop: '1px solid var(--border)', padding: '12px 16px' }}>
<div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-lo)', letterSpacing: '0.1em', marginBottom: 8, textTransform: 'uppercase' }}>