From 4fbc5784132bee530e3ba02f0dc9d435f357bcd3 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Fri, 17 Apr 2026 07:38:37 +0200 Subject: [PATCH] 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 --- frontend/src/App.tsx | 32 +++++++++- frontend/src/components/layout/Sidebar.tsx | 74 ++++++++++++++++++---- frontend/src/contexts/NightMode.tsx | 18 ++++++ frontend/src/styles/global.css | 1 + 4 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 frontend/src/contexts/NightMode.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0de7e4d..2ea48d6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( - +
+ ); +} + +function AppInner() { + return ( + <> + } /> @@ -24,6 +40,16 @@ export default function App() { } /> - + + ); +} + +export default function App() { + return ( + + + + + ); } diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 363d4c6..e5178a8 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -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 = { 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 ( - + <> + {/* Night mode floating toggle — sits just above bottom 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() {
+ {/* Night mode toggle */} +
+ +
+ {/* Conditions widget */}
diff --git a/frontend/src/contexts/NightMode.tsx b/frontend/src/contexts/NightMode.tsx new file mode 100644 index 0000000..0f8f00c --- /dev/null +++ b/frontend/src/contexts/NightMode.tsx @@ -0,0 +1,18 @@ +import { createContext, useContext, useState, type ReactNode } from 'react'; + +interface NightModeCtx { on: boolean; toggle: () => void; } +const Ctx = createContext({ 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 {children}; +} + +export const useNightMode = () => useContext(Ctx); diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 0ab1f35..6ac96c0 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -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; }