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:
+29
-3
@@ -7,10 +7,26 @@ import Stats from './pages/Stats';
|
||||
import Settings from './pages/Settings';
|
||||
import Gallery from './pages/Gallery';
|
||||
import SolarSystem from './pages/SolarSystem';
|
||||
import { NightModeProvider, useNightMode } from './contexts/NightMode';
|
||||
|
||||
export default function App() {
|
||||
function NightOverlay() {
|
||||
const { on } = useNightMode();
|
||||
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>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
@@ -24,6 +40,16 @@ export default function App() {
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</PageShell>
|
||||
</BrowserRouter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<NightModeProvider>
|
||||
<BrowserRouter>
|
||||
<AppInner />
|
||||
</BrowserRouter>
|
||||
</NightModeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,7 +32,35 @@ function fmtTime(utc?: string): string {
|
||||
}
|
||||
|
||||
export function BottomNav() {
|
||||
const { on, toggle } = useNightMode();
|
||||
return (
|
||||
<>
|
||||
{/* 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
|
||||
@@ -44,6 +73,7 @@ export function BottomNav() {
|
||||
</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' }}>
|
||||
|
||||
@@ -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);
|
||||
@@ -107,6 +107,7 @@ input:focus, select:focus, textarea:focus {
|
||||
/* Sidebar hidden; bottom nav shown */
|
||||
.app-sidebar { display: none !important; }
|
||||
.bottom-nav { display: flex !important; }
|
||||
.night-fab { display: flex !important; }
|
||||
|
||||
/* Main content: zero out PageShell padding; bottom-nav clearance */
|
||||
.app-main { padding: 0 0 68px 0 !important; }
|
||||
|
||||
Reference in New Issue
Block a user