From 4a4cb8018990bc0237508fd70571a7e2fb77fa44 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 25 Apr 2026 23:29:25 -0400 Subject: [PATCH] Add light/dark mode with theme toggle --- ui/src/App.jsx | 4 +- ui/src/components/Sidebar.jsx | 4 +- ui/src/components/StatusBar.jsx | 25 ++++++++++- ui/src/index.css | 78 ++++++++++++++++++++++++++++++++- ui/src/main.jsx | 5 ++- ui/src/theme.jsx | 25 +++++++++++ 6 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 ui/src/theme.jsx diff --git a/ui/src/App.jsx b/ui/src/App.jsx index c3e532a..e4d4e3f 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -13,7 +13,7 @@ export default function App() { useEffect(() => { localStorage.setItem('pf_sidebar', sidebarExpanded ? 'expanded' : 'collapsed') }, [sidebarExpanded]) return ( -
+
@@ -25,4 +25,4 @@ export default function App() {
) -} +} \ No newline at end of file diff --git a/ui/src/components/Sidebar.jsx b/ui/src/components/Sidebar.jsx index 8f55fa4..788b3c8 100644 --- a/ui/src/components/Sidebar.jsx +++ b/ui/src/components/Sidebar.jsx @@ -38,7 +38,6 @@ export default function Sidebar({ view, setView, expanded, setExpanded }) { className="bg-white border-r border-gray-200 flex flex-col shrink-0 overflow-hidden transition-all duration-150" style={{ width: expanded ? 200 : 48 }} > - {/* Logo / toggle */}
- {/* Nav */}
) -} +} \ No newline at end of file diff --git a/ui/src/components/StatusBar.jsx b/ui/src/components/StatusBar.jsx index 6050e16..d1a62dc 100644 --- a/ui/src/components/StatusBar.jsx +++ b/ui/src/components/StatusBar.jsx @@ -1,6 +1,10 @@ +import useTheme from '../theme.jsx' + export default function StatusBar() { + const { dark, setDark } = useTheme() + return ( -
+
Source sales_orders | @@ -12,6 +16,23 @@ export default function StatusBar() { | Status open +
+ +
) -} +} \ No newline at end of file diff --git a/ui/src/index.css b/ui/src/index.css index ab0d6a7..86280ca 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -1,4 +1,80 @@ @import "tailwindcss"; -body { margin: 0; } +:root, .light { + --bg-primary: #f3f4f6; + --bg-secondary: #ffffff; + --bg-tertiary: #f9fafb; + --text-primary: #1f2937; + --text-secondary: #374151; + --text-muted: #9ca3af; + --border-color: #e5e7eb; + --border-light: #f3f4f6; + --accent-bg: #eff6ff; + --accent-text: #1d4ed8; +} + +.dark { + --bg-primary: #111827; + --bg-secondary: #1f2937; + --bg-tertiary: #374151; + --text-primary: #f9fafb; + --text-secondary: #e5e7eb; + --text-muted: #6b7280; + --border-color: #374151; + --border-light: #1f2937; + --accent-bg: #1e3a5f; + --accent-text: #60a5fa; +} + +body { margin: 0; background-color: var(--bg-primary); color: var(--text-primary); } #root { height: 100vh; display: flex; } + +.dark .bg-white { background-color: var(--bg-secondary); } +.dark .bg-gray-50 { background-color: var(--bg-tertiary); } +.dark .bg-gray-100 { background-color: var(--bg-tertiary); } +.dark .bg-gray-200 { background-color: var(--bg-tertiary); } +.dark .bg-gray-300 { background-color: var(--bg-tertiary); } +.dark .text-gray-300 { color: var(--text-muted); } +.dark .text-gray-400 { color: var(--text-muted); } +.dark .text-gray-500 { color: var(--text-muted); } +.dark .text-gray-600 { color: var(--text-secondary); } +.dark .text-gray-700 { color: var(--text-secondary); } +.dark .text-gray-800 { color: var(--text-primary); } +.dark .text-gray-900 { color: var(--text-primary); } +.dark .bg-blue-50 { background-color: var(--accent-bg); } +.dark .bg-blue-100 { background-color: var(--accent-bg); } +.dark .text-blue-600 { color: var(--accent-text); } +.dark .text-blue-700 { color: var(--accent-text); } +.dark .border-blue-300 { border-color: var(--accent-text); } +.dark .hover\:bg-blue-50:hover { background-color: var(--accent-bg); } +.dark .bg-green-50 { background-color: #064e3b; } +.dark .text-green-600 { color: #34d399; } +.dark .text-green-700 { color: #34d399; } +.dark .text-green-400 { color: #34d399; } +.dark .bg-yellow-50 { background-color: #451a03; } +.dark .text-yellow-700 { color: #fbbf24; } +.dark .bg-purple-50 { background-color: #1e1b4b; } +.dark .text-purple-700 { color: #a78bfa; } +.dark .bg-red-50 { background-color: #450a0a; } +.dark .text-red-700 { color: #f87171; } +.dark .border-gray-100 { border-color: var(--border-light); } +.dark .border-gray-200 { border-color: var(--border-color); } +.dark .border-gray-300 { border-color: var(--border-color); } +.dark .border-b { border-color: var(--border-color); } +.dark .border-t { border-color: var(--border-color); } +.dark .border-r { border-color: var(--border-color); } +.dark .border-l { border-color: var(--border-color); } +.dark .hover\:bg-gray-50:hover { background-color: var(--bg-tertiary); } +.dark .hover\:bg-gray-100:hover { background-color: var(--bg-tertiary); } +.dark .hover\:bg-gray-200:hover { background-color: var(--bg-tertiary); } +.dark .hover\:text-gray-500:hover { color: var(--text-secondary); } +.dark .hover\:text-gray-600:hover { color: var(--text-secondary); } +.dark .hover\:text-gray-800:hover { color: var(--text-primary); } +.dark .hover\:border-gray-300:hover { border-color: var(--border-color); } +.dark .hover\:border-gray-400:hover { border-color: var(--border-color); } +.dark .focus\:border-gray-300:focus { border-color: var(--border-color); } +.dark ::selection { background-color: var(--accent-bg); color: var(--accent-text); } +.dark input { background-color: var(--bg-secondary); color: var(--text-primary); } +.dark select { background-color: var(--bg-secondary); color: var(--text-primary); } +.dark textarea { background-color: var(--bg-secondary); color: var(--text-primary); } +.dark .bg-transparent { background-color: transparent; } diff --git a/ui/src/main.jsx b/ui/src/main.jsx index b9a1a6d..28ad15f 100644 --- a/ui/src/main.jsx +++ b/ui/src/main.jsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { ThemeProvider } from './theme.jsx' import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/ui/src/theme.jsx b/ui/src/theme.jsx new file mode 100644 index 0000000..afd26ce --- /dev/null +++ b/ui/src/theme.jsx @@ -0,0 +1,25 @@ +import { createContext, useContext, useState, useEffect } from 'react' + +const ThemeContext = createContext() + +export function ThemeProvider({ children }) { + const [dark, setDark] = useState(() => { + const saved = localStorage.getItem('pf_dark') + if (saved !== null) return saved === 'true' + return window.matchMedia('(prefers-color-scheme: dark)').matches + }) + + useEffect(() => { + localStorage.setItem('pf_dark', dark) + document.documentElement.classList.toggle('dark', dark) + }, [dark]) + + return ( + + {children} + + ) +} + +const useTheme = () => useContext(ThemeContext) +export default useTheme \ No newline at end of file