@@ -8,7 +8,8 @@ import { apiGet, getAuthToken } from "./api";
88import BuildTag from "./components/BuildTag" ;
99import MobileHeader from "./components/MobileHeader" ;
1010import LoginModal from "./components/LoginModal" ;
11- // Theme toggle removed per request
11+ import ThemeToggle from "./components/ThemeToggle" ;
12+ import { getInitialTheme , setThemeCookie , resolveTheme , applyTheme , getSystemTheme } from "./theme/themeUtils" ;
1213
1314const tabs = [ "Network Map" , "Hosts Table" , "Scripts" , "Logs" ] ;
1415
@@ -70,14 +71,14 @@ function Sidebar({ activeTab, setActiveTab, visible, setVisible, onShowDuplicate
7071 { /* Overlay (mobile only) */ }
7172 { visible && (
7273 < div
73- className = "fixed inset-0 bg-black/40 z-30 lg:hidden"
74+ className = "fixed inset-0 bg-black/40 dark:bg-black/60 z-30 lg:hidden"
7475 onClick = { ( ) => setVisible ( false ) }
7576 > </ div >
7677 ) }
7778
7879 { /* Sidebar container (mobile: slide-over, desktop: collapsible rail) */ }
7980 < div
80- className = { `z-40 top-0 left-0 bg-gray-900 text-white flex flex-col transition-all duration-300
81+ className = { `z-40 top-0 left-0 bg-gray-900 dark:bg-gray-800 text-white flex flex-col transition-all duration-300
8182 fixed h-full w-64 transform ${ visible ? "translate-x-0" : "-translate-x-full" } lg:static lg:h-auto lg:transform-none
8283 ${ visible ? "lg:w-64" : "lg:w-16" } ` }
8384 ref = { sidebarRef }
@@ -113,7 +114,7 @@ function Sidebar({ activeTab, setActiveTab, visible, setVisible, onShowDuplicate
113114 } }
114115 title = { tab }
115116 className = { `w-full flex items-center ${ visible ? "justify-start" : "justify-center" } p-2 rounded transition-colors duration-200 ${
116- activeTab === tab ? "bg-gray-700" : "hover:bg-gray-800"
117+ activeTab === tab ? "bg-gray-700 dark:bg-gray-600 " : "hover:bg-gray-800 dark:hover:bg-gray-700 "
117118 } `}
118119 >
119120 < TabIcon tab = { tab } />
@@ -130,7 +131,7 @@ function Sidebar({ activeTab, setActiveTab, visible, setVisible, onShowDuplicate
130131 </ div >
131132
132133 { /* Stats (hidden on desktop when collapsed) */ }
133- < div className = { `mt-auto text-sm pt-6 border-t border-gray-700 px-4 ${ visible ? "lg:block" : "lg:hidden" } ` } >
134+ < div className = { `mt-auto text-sm pt-6 border-t border-gray-700 dark:border-gray-600 px-4 ${ visible ? "lg:block" : "lg:hidden" } ` } >
134135 < h2 className = "font-semibold mb-1" > Network Stats:</ h2 >
135136 < p > Total Hosts: { stats . total } </ p >
136137 < p >
@@ -172,10 +173,48 @@ export default function App() {
172173 const githubUrl = "https://github.com/karam-ajaj/atlas" ;
173174
174175 const [ authState , setAuthState ] = useState ( { checked : false , enabled : false , authenticated : false } ) ;
176+
177+ // Theme state: user's preference (light/dark/auto)
178+ const [ themePreference , setThemePreference ] = useState ( ( ) => getInitialTheme ( ) ) ;
175179
176180 const openLogin = ( ) => setLoginVisible ( true ) ;
177181 const closeLogin = ( ) => setLoginVisible ( false ) ;
178182
183+ // Initialize and apply theme on mount
184+ useEffect ( ( ) => {
185+ const effectiveTheme = resolveTheme ( themePreference ) ;
186+ applyTheme ( effectiveTheme ) ;
187+ } , [ ] ) ;
188+
189+ // Update theme when preference changes
190+ useEffect ( ( ) => {
191+ setThemeCookie ( themePreference ) ;
192+ const effectiveTheme = resolveTheme ( themePreference ) ;
193+ applyTheme ( effectiveTheme ) ;
194+ } , [ themePreference ] ) ;
195+
196+ // Listen for OS theme changes when in auto mode
197+ useEffect ( ( ) => {
198+ if ( themePreference !== 'auto' ) return ;
199+
200+ const mediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
201+ const handleChange = ( ) => {
202+ const effectiveTheme = resolveTheme ( themePreference ) ;
203+ applyTheme ( effectiveTheme ) ;
204+ } ;
205+
206+ // Modern browsers
207+ if ( mediaQuery . addEventListener ) {
208+ mediaQuery . addEventListener ( 'change' , handleChange ) ;
209+ return ( ) => mediaQuery . removeEventListener ( 'change' , handleChange ) ;
210+ }
211+ // Fallback for older browsers
212+ else if ( mediaQuery . addListener ) {
213+ mediaQuery . addListener ( handleChange ) ;
214+ return ( ) => mediaQuery . removeListener ( handleChange ) ;
215+ }
216+ } , [ themePreference ] ) ;
217+
179218 // Auth gate: if server auth is enabled, require login before rendering the app.
180219 useEffect ( ( ) => {
181220 let aborted = false ;
@@ -260,12 +299,12 @@ export default function App() {
260299 // Prevent any data/UI flash before auth check completes.
261300 // If auth is enabled, we will immediately show the login gate after the check.
262301 if ( ! authState . checked ) {
263- return < div className = "h-screen bg-gray-100" /> ;
302+ return < div className = "h-screen bg-gray-100 dark:bg-gray-900 " /> ;
264303 }
265304
266305 if ( mustLogin ) {
267306 return (
268- < div className = "h-screen bg-gray-100 relative" >
307+ < div className = "h-screen bg-gray-100 dark:bg-gray-900 relative" >
269308 < LoginModal
270309 open
271310 force
@@ -282,12 +321,14 @@ export default function App() {
282321 }
283322
284323 return (
285- < div className = "flex flex-col h-screen bg-gray-100 relative" >
324+ < div className = "flex flex-col h-screen bg-gray-100 dark:bg-gray-900 relative" >
286325 { /* Mobile Header - only visible on mobile; pass menu opener */ }
287326 < MobileHeader
288327 onOpenMenu = { ( ) => setSidebarVisible ( true ) }
289328 onOpenLogin = { openLogin }
290329 githubUrl = { githubUrl }
330+ themePreference = { themePreference }
331+ setThemePreference = { setThemePreference }
291332 />
292333
293334 < div className = "flex flex-1 overflow-hidden" >
@@ -308,13 +349,14 @@ export default function App() {
308349 { /* Left placeholder (kept intentionally empty) */ }
309350 < div />
310351
311- { /* Right: desktop-only login button (placeholder for real auth) */ }
312- < div className = "flex items-center" >
352+ { /* Right: theme toggle + GitHub + login button (desktop) */ }
353+ < div className = "flex items-center gap-2" >
354+ < ThemeToggle themePreference = { themePreference } setThemePreference = { setThemePreference } />
313355 < a
314356 href = { githubUrl }
315357 target = "_blank"
316358 rel = "noreferrer"
317- className = "hidden lg:inline-flex bg-transparent text-gray-700 hover:text-gray-900 p-2 rounded-md"
359+ className = "hidden lg:inline-flex bg-transparent text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white p-2 rounded-md"
318360 title = "View on GitHub"
319361 aria-label = "View on GitHub"
320362 >
@@ -323,7 +365,7 @@ export default function App() {
323365 </ svg >
324366 </ a >
325367 < button
326- className = "hidden lg:inline-flex bg-transparent text-gray-700 hover:text-gray-900 p-2 rounded-md"
368+ className = "hidden lg:inline-flex bg-transparent text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white p-2 rounded-md"
327369 title = "Login"
328370 aria-label = "Login"
329371 onClick = { openLogin }
0 commit comments