feat: day/night mode toggle with theme context
Add ThemeContext (dark/light, persisted to localStorage, applied as data-theme on <html>). Theme toggle is a sun/moon flip switch at the right end of the dashboard info bar. CSS overrides in index.css target every hardcoded Tailwind arbitrary colour class used across all pages via [data-theme="light"] selectors, so the entire panel switches without touching individual page files. Light palette: page #f0f4f8, cards/sidebar #ffffff, hover #f1f5f9, borders #e2e8f0, text #0f172a. Chart tooltip is made theme-aware via inline-style interpolation since Tailwind CSS overrides cannot reach Recharts inline styles.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import Layout from './components/Layout'
|
||||
import Login from './pages/Login'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
@@ -233,9 +234,11 @@ function PageLoader() {
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<AppRoutes />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
type Theme = 'dark' | 'light'
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() =>
|
||||
(localStorage.getItem('nixpanel-theme') as Theme) ?? 'dark'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
localStorage.setItem('nixpanel-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => setTheme(t => (t === 'dark' ? 'light' : 'dark'))
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
|
||||
return ctx
|
||||
}
|
||||
@@ -45,3 +45,45 @@ body {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #3a3d4e;
|
||||
}
|
||||
|
||||
/* ── Light theme ─────────────────────────────────────────────────────────── */
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f0f4f8;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-card: #f1f5f9;
|
||||
--border: #e2e8f0;
|
||||
--text-primary: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
}
|
||||
|
||||
[data-theme="light"] body {
|
||||
background-color: #f0f4f8;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
[data-theme="light"] ::-webkit-scrollbar-track { background: #f1f5f9; }
|
||||
[data-theme="light"] ::-webkit-scrollbar-thumb { background: #cbd5e1; }
|
||||
[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
||||
|
||||
/* ── Background overrides ──────────────────────────────────────────────── */
|
||||
/* Page background (darkest) */
|
||||
[data-theme="light"] .bg-\[\#0f1117\] { background-color: #f0f4f8 !important; }
|
||||
/* Cards / sidebar */
|
||||
[data-theme="light"] .bg-\[\#1a1d27\] { background-color: #ffffff !important; }
|
||||
/* Hover / alternate rows */
|
||||
[data-theme="light"] .bg-\[\#1e2130\] { background-color: #f1f5f9 !important; }
|
||||
|
||||
/* ── Border overrides ──────────────────────────────────────────────────── */
|
||||
[data-theme="light"] .border-\[\#2a2d3e\] { border-color: #e2e8f0 !important; }
|
||||
|
||||
/* ── Text overrides ────────────────────────────────────────────────────── */
|
||||
[data-theme="light"] .text-white { color: #0f172a !important; }
|
||||
[data-theme="light"] .text-\[\#94a3b8\] { color: #475569 !important; }
|
||||
[data-theme="light"] .text-\[\#64748b\] { color: #64748b !important; }
|
||||
[data-theme="light"] .text-\[\#4a5568\] { color: #94a3b8 !important; }
|
||||
|
||||
/* ── Input fields ──────────────────────────────────────────────────────── */
|
||||
[data-theme="light"] input { color: #0f172a !important; }
|
||||
[data-theme="light"] input::placeholder { color: #94a3b8 !important; }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { Users, Server, HardDrive, Activity, AlertTriangle, CheckCircle2 } from 'lucide-react'
|
||||
import { Users, Server, HardDrive, Activity, AlertTriangle, CheckCircle2, Sun, Moon } from 'lucide-react'
|
||||
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { api } from '../api/client'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
|
||||
interface SystemStats {
|
||||
cpu: number
|
||||
@@ -71,6 +72,7 @@ function Divider() {
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user } = useAuth()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const [stats, setStats] = useState<SystemStats | null>(null)
|
||||
const [accountStats, setAccountStats] = useState<AccountStats | null>(null)
|
||||
const [cpuHistory, setCpuHistory] = useState<{ t: string; cpu: number; mem: number }[]>([])
|
||||
@@ -175,6 +177,31 @@ export default function Dashboard() {
|
||||
<InfoCell label="Load Averages" value={loadStr} />
|
||||
<Divider />
|
||||
<InfoCell label="Server Monitoring" value={monitoringNode} />
|
||||
|
||||
{/* ── Day / Night toggle — far right ── */}
|
||||
<div className="ml-auto flex items-center gap-2 flex-shrink-0">
|
||||
<Moon
|
||||
size={13}
|
||||
className={theme === 'dark' ? 'text-indigo-400' : 'text-[#94a3b8]'}
|
||||
/>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Switch to day mode' : 'Switch to night mode'}
|
||||
className={`relative flex items-center w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0 ${
|
||||
theme === 'light' ? 'bg-amber-400' : 'bg-[#2a2d3e]'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
||||
theme === 'light' ? 'translate-x-5' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<Sun
|
||||
size={13}
|
||||
className={theme === 'light' ? 'text-amber-500' : 'text-[#4a5568]'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Stat cards ───────────────────────────────────────────────────── */}
|
||||
@@ -210,8 +237,14 @@ export default function Dashboard() {
|
||||
<XAxis dataKey="t" tick={{ fill: '#64748b', fontSize: 10 }} tickLine={false} axisLine={false} />
|
||||
<YAxis domain={[0, 100]} tick={{ fill: '#64748b', fontSize: 10 }} tickLine={false} axisLine={false} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#1e2130', border: '1px solid #2a2d3e', borderRadius: 8, fontSize: 12 }}
|
||||
labelStyle={{ color: '#94a3b8' }}
|
||||
contentStyle={{
|
||||
background: theme === 'light' ? '#ffffff' : '#1e2130',
|
||||
border: `1px solid ${theme === 'light' ? '#e2e8f0' : '#2a2d3e'}`,
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
color: theme === 'light' ? '#0f172a' : '#e2e8f0',
|
||||
}}
|
||||
labelStyle={{ color: theme === 'light' ? '#64748b' : '#94a3b8' }}
|
||||
/>
|
||||
<Area type="monotone" dataKey="cpu" stroke="#6366f1" fill="url(#cpu)" strokeWidth={2} name="CPU %" />
|
||||
<Area type="monotone" dataKey="mem" stroke="#10b981" fill="url(#mem)" strokeWidth={2} name="Mem %" />
|
||||
|
||||
Reference in New Issue
Block a user