diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index 6db0f20a..032a7cf8 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -1,15 +1,11 @@
-import { lazy, Suspense } from 'react';
-import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
+import { lazy } from 'react';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClientProvider } from '@tanstack/react-query';
-import { SidebarProvider } from '@/components/ui/sidebar';
-import { AppSidebar } from '@/components/app-sidebar';
-import { ThemeToggle } from '@/components/theme-toggle';
-import { GitHubLink } from '@/components/github-link';
-import { ConnectionIndicator } from '@/components/connection-indicator';
-import { LocalhostDisclaimer } from '@/components/localhost-disclaimer';
import { Toaster } from 'sonner';
import { queryClient } from '@/lib/query-client';
-import { Skeleton } from '@/components/ui/skeleton';
+import { ThemeProvider } from '@/components/theme-provider';
+import { Layout } from '@/components/layout';
+
// Eager load: HomePage (initial route)
import { HomePage } from '@/pages';
@@ -33,58 +29,27 @@ const SettingsPage = lazy(() =>
const HealthPage = lazy(() => import('@/pages/health').then((m) => ({ default: m.HealthPage })));
const SharedPage = lazy(() => import('@/pages/shared').then((m) => ({ default: m.SharedPage })));
-// Loading fallback for lazy-loaded pages
-function PageLoader() {
- return (
-
-
-
-
- );
-}
-
-function Layout() {
- return (
-
-
-
-
-
- }>
-
-
-
-
-
-
- );
-}
-
export default function App() {
return (
-
-
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
-
+
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
);
}
diff --git a/ui/src/components/layout.tsx b/ui/src/components/layout.tsx
new file mode 100644
index 00000000..fba044b2
--- /dev/null
+++ b/ui/src/components/layout.tsx
@@ -0,0 +1,42 @@
+import { Suspense } from 'react';
+import { Outlet } from 'react-router-dom';
+import { SidebarProvider } from '@/components/ui/sidebar';
+import { AppSidebar } from '@/components/app-sidebar';
+import { ThemeToggle } from '@/components/theme-toggle';
+import { GitHubLink } from '@/components/github-link';
+import { ConnectionIndicator } from '@/components/connection-indicator';
+import { LocalhostDisclaimer } from '@/components/localhost-disclaimer';
+import { Skeleton } from '@/components/ui/skeleton';
+
+function PageLoader() {
+ return (
+
+
+
+
+ );
+}
+
+export function Layout() {
+ return (
+
+
+
+
+
+ }>
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/components/theme-provider.tsx b/ui/src/components/theme-provider.tsx
new file mode 100644
index 00000000..d113fe46
--- /dev/null
+++ b/ui/src/components/theme-provider.tsx
@@ -0,0 +1,61 @@
+import { useEffect, useState } from 'react';
+import { ThemeProviderContext } from '@/contexts/theme-context';
+import type { Theme } from '@/contexts/theme-context';
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+export function ThemeProvider({
+ children,
+ defaultTheme = 'system',
+ storageKey = 'vite-ui-theme',
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+ );
+
+ // Track system preference separately
+ // Initialize with window.matchMedia value if available
+ const [systemIsDark, setSystemIsDark] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ }
+ return false;
+ });
+
+ // Listen for system theme changes
+ useEffect(() => {
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
+
+ const listener = (e: MediaQueryListEvent) => {
+ setSystemIsDark(e.matches);
+ };
+
+ media.addEventListener('change', listener);
+ return () => media.removeEventListener('change', listener);
+ }, []);
+
+ // Derive effective theme
+ const isDark = theme === 'dark' || (theme === 'system' && systemIsDark);
+
+ // Update DOM when effective theme changes
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove('light', 'dark');
+ root.classList.add(isDark ? 'dark' : 'light');
+ }, [isDark]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ isDark,
+ };
+
+ return {children};
+}
diff --git a/ui/src/components/theme-toggle.tsx b/ui/src/components/theme-toggle.tsx
index 946aed90..619b3a11 100644
--- a/ui/src/components/theme-toggle.tsx
+++ b/ui/src/components/theme-toggle.tsx
@@ -3,11 +3,17 @@ import { Button } from '@/components/ui/button';
import { useTheme } from '@/hooks/use-theme';
export function ThemeToggle() {
- const { isDark, toggle } = useTheme();
+ const { theme, setTheme } = useTheme();
return (
-