From ddb51a8b4dd329e1d8e8ff6d6708b948b76884c8 Mon Sep 17 00:00:00 2001 From: NixReaper Date: Sat, 11 Apr 2026 14:24:51 -0500 Subject: [PATCH] feat: day/night mode toggle with theme context Add ThemeContext (dark/light, persisted to localStorage, applied as data-theme on ). 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. --- nixserver/src/App.tsx | 9 +++-- nixserver/src/contexts/ThemeContext.tsx | 35 ++++++++++++++++++ nixserver/src/index.css | 42 +++++++++++++++++++++ nixserver/src/pages/Dashboard.tsx | 49 +++++++++++++++++++++---- 4 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 nixserver/src/contexts/ThemeContext.tsx diff --git a/nixserver/src/App.tsx b/nixserver/src/App.tsx index 715b32f..8c3f20c 100644 --- a/nixserver/src/App.tsx +++ b/nixserver/src/App.tsx @@ -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 ( - - - + + + + + ) } diff --git a/nixserver/src/contexts/ThemeContext.tsx b/nixserver/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..38fcbde --- /dev/null +++ b/nixserver/src/contexts/ThemeContext.tsx @@ -0,0 +1,35 @@ +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'dark' | 'light' + +interface ThemeContextValue { + theme: Theme + toggleTheme: () => void +} + +const ThemeContext = createContext(null) + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState(() => + (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 ( + + {children} + + ) +} + +export function useTheme() { + const ctx = useContext(ThemeContext) + if (!ctx) throw new Error('useTheme must be used inside ThemeProvider') + return ctx +} diff --git a/nixserver/src/index.css b/nixserver/src/index.css index c49050f..0bd312b 100644 --- a/nixserver/src/index.css +++ b/nixserver/src/index.css @@ -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; } diff --git a/nixserver/src/pages/Dashboard.tsx b/nixserver/src/pages/Dashboard.tsx index 6881290..2238807 100644 --- a/nixserver/src/pages/Dashboard.tsx +++ b/nixserver/src/pages/Dashboard.tsx @@ -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(null) const [accountStats, setAccountStats] = useState(null) const [cpuHistory, setCpuHistory] = useState<{ t: string; cpu: number; mem: number }[]>([]) @@ -164,17 +166,42 @@ export default function Dashboard() { {/* ── cPanel-style server info bar ─────────────────────────────────── */}
- + - + - + - + - + + + {/* ── Day / Night toggle — far right ── */} +
+ + + +
{/* ── Stat cards ───────────────────────────────────────────────────── */} @@ -210,8 +237,14 @@ export default function Dashboard() {