Skip to content

Commit 7cf294d

Browse files
authored
Merge pull request #92 from karam-ajaj/copilot/add-theme-switching-system
Add dark mode with Light/Dark/Auto theme switching
2 parents 3579870 + 6772663 commit 7cf294d

11 files changed

Lines changed: 306 additions & 113 deletions

File tree

data/react-ui/package-lock.json

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

data/react-ui/src/App.jsx

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { apiGet, getAuthToken } from "./api";
88
import BuildTag from "./components/BuildTag";
99
import MobileHeader from "./components/MobileHeader";
1010
import 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

1314
const 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}

data/react-ui/src/components/BuildTag.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,5 @@ export default function BuildTag({ prefix = "v" }) {
2626
}, []);
2727

2828
if (!tag) return null;
29-
return <span className="text-xs text-gray-400">{prefix}{tag}</span>;
29+
return <span className="text-xs text-gray-400 dark:text-gray-500">{prefix}{tag}</span>;
3030
}

0 commit comments

Comments
 (0)