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
+29 -3
View File
@@ -7,10 +7,26 @@ import Stats from './pages/Stats';
import Settings from './pages/Settings'; import Settings from './pages/Settings';
import Gallery from './pages/Gallery'; import Gallery from './pages/Gallery';
import SolarSystem from './pages/SolarSystem'; import SolarSystem from './pages/SolarSystem';
import { NightModeProvider, useNightMode } from './contexts/NightMode';
export default function App() { function NightOverlay() {
const { on } = useNightMode();
return ( return (
<BrowserRouter> <div style={{
position: 'fixed', inset: 0, zIndex: 9999,
background: 'rgba(160, 0, 0, 0.55)',
mixBlendMode: 'multiply',
pointerEvents: 'none',
opacity: on ? 1 : 0,
transition: 'opacity 0.4s ease',
}} />
);
}
function AppInner() {
return (
<>
<NightOverlay />
<PageShell> <PageShell>
<Routes> <Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
@@ -24,6 +40,16 @@ export default function App() {
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
</Routes> </Routes>
</PageShell> </PageShell>
</BrowserRouter> </>
);
}
export default function App() {
return (
<NightModeProvider>
<BrowserRouter>
<AppInner />
</BrowserRouter>
</NightModeProvider>
); );
} }
+62 -12
View File
@@ -3,6 +3,7 @@ import { useTonight } from '../../hooks/useTonight';
import { useWeather, useForecast } from '../../hooks/useWeather'; import { useWeather, useForecast } from '../../hooks/useWeather';
import MoonPhaseIcon from '../sky/MoonPhaseIcon'; import MoonPhaseIcon from '../sky/MoonPhaseIcon';
import GoNogo from '../weather/GoNogo'; import GoNogo from '../weather/GoNogo';
import { useNightMode } from '../../contexts/NightMode';
const SEEING_LABELS: Record<number, string> = { const SEEING_LABELS: Record<number, string> = {
1: '0.5″', 2: '0.75″', 3: '1.0″', 4: '1.25″', 1: '0.5″', 2: '0.75″', 3: '1.0″', 4: '1.25″',
@@ -31,19 +32,48 @@ function fmtTime(utc?: string): string {
} }
export function BottomNav() { export function BottomNav() {
const { on, toggle } = useNightMode();
return ( return (
<nav className="bottom-nav"> <>
{navItems.map(item => ( {/* Night mode floating toggle — sits just above bottom nav */}
<NavLink <button
key={item.path} onClick={toggle}
to={item.path} title={on ? 'Exit night mode' : 'Night mode'}
className={({ isActive }) => isActive ? 'active' : ''} style={{
> position: 'fixed',
<span className="bnav-icon">{item.icon}</span> bottom: 66,
{item.label} right: 14,
</NavLink> zIndex: 210,
))} width: 36, height: 36,
</nav> 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: tonight } = useTonight();
const { data: weather } = useWeather(); const { data: weather } = useWeather();
const { data: forecast } = useForecast(); const { data: forecast } = useForecast();
const { on: nightOn, toggle: nightToggle } = useNightMode();
const slot = (forecast as { dataseries?: { seeing?: number; transparency?: number; cloudcover?: number }[] })?.dataseries?.[0]; const slot = (forecast as { dataseries?: { seeing?: number; transparency?: number; cloudcover?: number }[] })?.dataseries?.[0];
@@ -147,6 +178,25 @@ export default function Sidebar() {
</table> </table>
</div> </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 */} {/* Conditions widget */}
<div style={{ borderTop: '1px solid var(--border)', padding: '12px 16px' }}> <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' }}> <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-lo)', letterSpacing: '0.1em', marginBottom: 8, textTransform: 'uppercase' }}>
+18
View File
@@ -0,0 +1,18 @@
import { createContext, useContext, useState, type ReactNode } from 'react';
interface NightModeCtx { on: boolean; toggle: () => void; }
const Ctx = createContext<NightModeCtx>({ on: false, toggle: () => {} });
export function NightModeProvider({ children }: { children: ReactNode }) {
const [on, setOn] = useState(() => localStorage.getItem('astronome_night') === '1');
const toggle = () => setOn(v => {
const next = !v;
localStorage.setItem('astronome_night', next ? '1' : '0');
return next;
});
return <Ctx.Provider value={{ on, toggle }}>{children}</Ctx.Provider>;
}
export const useNightMode = () => useContext(Ctx);
+1
View File
@@ -107,6 +107,7 @@ input:focus, select:focus, textarea:focus {
/* Sidebar hidden; bottom nav shown */ /* Sidebar hidden; bottom nav shown */
.app-sidebar { display: none !important; } .app-sidebar { display: none !important; }
.bottom-nav { display: flex !important; } .bottom-nav { display: flex !important; }
.night-fab { display: flex !important; }
/* Main content: zero out PageShell padding; bottom-nav clearance */ /* Main content: zero out PageShell padding; bottom-nav clearance */
.app-main { padding: 0 0 68px 0 !important; } .app-main { padding: 0 0 68px 0 !important; }