Add light/dark mode with theme toggle
This commit is contained in:
parent
bd5ea1c60e
commit
4a4cb80189
@ -13,7 +13,7 @@ export default function App() {
|
||||
useEffect(() => { localStorage.setItem('pf_sidebar', sidebarExpanded ? 'expanded' : 'collapsed') }, [sidebarExpanded])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full bg-gray-100 text-sm text-gray-800 overflow-hidden">
|
||||
<div className="flex h-screen w-full text-sm overflow-hidden">
|
||||
<Sidebar view={view} setView={setView} expanded={sidebarExpanded} setExpanded={setSidebarExpanded} />
|
||||
<div className="flex flex-col flex-1 overflow-hidden min-w-0">
|
||||
<StatusBar />
|
||||
@ -25,4 +25,4 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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 */}
|
||||
<div className="h-12 flex items-center px-3 border-b border-gray-100 gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => setExpanded(e => !e)}
|
||||
@ -59,7 +58,6 @@ export default function Sidebar({ view, setView, expanded, setExpanded }) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex flex-col gap-0.5 p-2 flex-1">
|
||||
{NAV.map(item => {
|
||||
const active = view === item.id
|
||||
@ -87,4 +85,4 @@ export default function Sidebar({ view, setView, expanded, setExpanded }) {
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
import useTheme from '../theme.jsx'
|
||||
|
||||
export default function StatusBar() {
|
||||
const { dark, setDark } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="bg-white border-b border-gray-100 px-4 h-8 flex items-center gap-4 shrink-0 text-xs">
|
||||
<div className="bg-white border-b border-gray-200 px-4 h-8 flex items-center gap-4 shrink-0 text-xs">
|
||||
<span className="text-gray-400">Source</span>
|
||||
<span className="font-medium text-gray-700">sales_orders</span>
|
||||
<span className="text-gray-200">|</span>
|
||||
@ -12,6 +16,23 @@ export default function StatusBar() {
|
||||
<span className="text-gray-200">|</span>
|
||||
<span className="text-gray-400">Status</span>
|
||||
<span className="text-green-600 font-medium">open</span>
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
onClick={() => setDark(d => !d)}
|
||||
className="w-6 h-6 flex items-center justify-center rounded hover:bg-gray-100"
|
||||
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{dark ? (
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M6 .278a.768.768 0 0 1 1.065.02A.75.75 0 0 1 5.792 15.5a.75.75 0 0 1-1.498-.075.768.768 0 0 1-.02-1.05A8 8 0 1 0 6.278 14.72a.768.768 0 0 1-1.055-.02A.75.75 0 0 1 2.5 13.75a.75.75 0 0 1 1.498.075A8 8 0 1 0 6 .278z"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
25
ui/src/theme.jsx
Normal file
25
ui/src/theme.jsx
Normal file
@ -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 (
|
||||
<ThemeContext.Provider value={{ dark, setDark }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useTheme = () => useContext(ThemeContext)
|
||||
export default useTheme
|
||||
Loading…
Reference in New Issue
Block a user