add dark theme
This commit is contained in:
+14
@@ -5,6 +5,20 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>RADIUS Admin</title>
|
<title>RADIUS Admin</title>
|
||||||
|
<!-- Critical: paint the correct background before the app CSS loads (avoids flash). -->
|
||||||
|
<style>
|
||||||
|
html { background: oklch(1 0 0); color-scheme: light; }
|
||||||
|
html.dark { background: oklch(0.145 0 0); color-scheme: dark; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
// Apply saved (or system) theme before first paint.
|
||||||
|
(function () {
|
||||||
|
var t = localStorage.getItem('radui.theme')
|
||||||
|
if (t === 'dark' || (!t && matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+22
-2
@@ -1,12 +1,29 @@
|
|||||||
import { NavLink, Navigate, Route, Routes } from 'react-router-dom'
|
import { NavLink, Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { LogOut, Router, Wifi } from 'lucide-react'
|
import { LogOut, Moon, Router, Sun, Wifi } from 'lucide-react'
|
||||||
import { useAuth } from '@/auth/auth'
|
import { useAuth } from '@/auth/auth'
|
||||||
|
import { useTheme } from '@/lib/theme'
|
||||||
import { Login } from '@/pages/Login'
|
import { Login } from '@/pages/Login'
|
||||||
import { Devices } from '@/pages/Devices'
|
import { Devices } from '@/pages/Devices'
|
||||||
import { Vlans } from '@/pages/Vlans'
|
import { Vlans } from '@/pages/Vlans'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
function ThemeToggle() {
|
||||||
|
const { theme, toggle } = useTheme()
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggle}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Nav() {
|
function Nav() {
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
||||||
@@ -28,10 +45,13 @@ function Nav() {
|
|||||||
<NavLink to="/vlans" className={linkClass}>
|
<NavLink to="/vlans" className={linkClass}>
|
||||||
<Wifi className="h-4 w-4" /> VLANs
|
<Wifi className="h-4 w-4" /> VLANs
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<Button variant="ghost" size="sm" className="ml-auto text-muted-foreground" onClick={logout}>
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<ThemeToggle />
|
||||||
|
<Button variant="ghost" size="sm" className="text-muted-foreground" onClick={logout}>
|
||||||
<LogOut className="h-4 w-4" /> Sign out
|
<LogOut className="h-4 w-4" /> Sign out
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark'
|
||||||
|
const STORAGE = 'radui.theme'
|
||||||
|
|
||||||
|
interface ThemeState {
|
||||||
|
theme: Theme
|
||||||
|
toggle: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeState | null>(null)
|
||||||
|
|
||||||
|
function initialTheme(): Theme {
|
||||||
|
const saved = localStorage.getItem(STORAGE)
|
||||||
|
if (saved === 'light' || saved === 'dark') return saved
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(initialTheme)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.classList.toggle('dark', theme === 'dark')
|
||||||
|
localStorage.setItem(STORAGE, theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setTheme((t) => (t === 'dark' ? 'light' : 'dark'))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <ThemeContext value={{ theme, toggle }}>{children}</ThemeContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme(): ThemeState {
|
||||||
|
const ctx = useContext(ThemeContext)
|
||||||
|
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -4,15 +4,18 @@ import { BrowserRouter } from 'react-router-dom'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { AuthProvider } from './auth/auth.tsx'
|
import { AuthProvider } from './auth/auth.tsx'
|
||||||
|
import { ThemeProvider } from './lib/theme.tsx'
|
||||||
import { Toaster } from './components/ui/sonner.tsx'
|
import { Toaster } from './components/ui/sonner.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<ThemeProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<App />
|
<App />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</ThemeProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user