Add dark mode

This commit is contained in:
Andras Schmelczer 2026-02-01 13:07:24 +00:00
parent 5e210e14bd
commit 7235df0a97
14 changed files with 304 additions and 139 deletions

View file

@ -23,6 +23,8 @@ import type {
HexagonPropertiesResponse,
} from './types';
type Theme = 'light' | 'dark';
const DEBOUNCE_MS = 150;
const URL_DEBOUNCE_MS = 300;
@ -180,9 +182,13 @@ type Page = 'home' | 'dashboard' | 'data-sources';
function Header({
activePage,
onPageChange,
theme,
onToggleTheme,
}: {
activePage: Page;
onPageChange: (page: Page) => void;
theme: Theme;
onToggleTheme: () => void;
}) {
const [copied, setCopied] = useState(false);
@ -240,7 +246,23 @@ function Header({
</button>
</nav>
</div>
{activePage === 'dashboard' && (
<div className="flex items-center gap-2">
<button
onClick={onToggleTheme}
className="flex items-center justify-center w-8 h-8 rounded bg-navy-800 hover:bg-navy-700 transition-colors"
title={`Theme: ${theme}`}
>
{theme === 'light' ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)}
</button>
{activePage === 'dashboard' && (
<button
onClick={handleShare}
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm"
@ -271,7 +293,8 @@ function Header({
</>
)}
</button>
)}
)}
</div>
</header>
);
}
@ -335,6 +358,28 @@ export default function App() {
const [rightPaneTab, setRightPaneTab] = useState<'pois' | 'properties'>(urlState.tab || 'pois');
const [activePage, setActivePage] = useState<Page>('home');
// Theme state — defaults to system preference on first visit
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem('theme');
if (stored === 'light' || stored === 'dark') return stored;
return 'light';
});
// Sync dark class on <html> and persist to localStorage
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
}, []);
// Derive enabled features from filter keys
const enabledFeatures = useMemo(() => new Set(Object.keys(filters)), [filters]);
@ -704,9 +749,9 @@ export default function App() {
return (
<div className="h-screen flex flex-col">
<Header activePage={activePage} onPageChange={setActivePage} />
<Header activePage={activePage} onPageChange={setActivePage} theme={theme} onToggleTheme={toggleTheme} />
{activePage === 'home' ? (
<HomePage onOpenDashboard={() => setActivePage('dashboard')} />
<HomePage onOpenDashboard={() => setActivePage('dashboard')} theme={theme} />
) : activePage === 'data-sources' ? (
<DataSourcesPage />
) : (
@ -742,22 +787,23 @@ export default function App() {
selectedHexagonId={selectedHexagon?.h3 || null}
onHexagonClick={handleHexagonClick}
initialViewState={initialViewState}
theme={theme}
/>
{loading && (
<div className="absolute top-4 right-4 bg-white px-3 py-1 rounded shadow">
<div className="absolute top-4 right-4 bg-white dark:bg-warm-800 dark:text-warm-200 px-3 py-1 rounded shadow">
Loading...
</div>
)}
<DataSources onNavigate={() => setActivePage('data-sources')} />
</div>
<div className="w-72 bg-white shadow-lg z-10 flex flex-col">
<div className="w-72 bg-white dark:bg-warm-900 shadow-lg z-10 flex flex-col">
{/* Tab headers */}
<div className="flex border-b border-warm-200">
<div className="flex border-b border-warm-200 dark:border-warm-700">
<button
className={`flex-1 p-3 ${
rightPaneTab === 'pois'
? 'border-b-2 border-teal-500 font-semibold'
: 'text-warm-600'
? 'border-b-2 border-teal-500 font-semibold dark:text-warm-100'
: 'text-warm-600 dark:text-warm-400'
}`}
onClick={() => setRightPaneTab('pois')}
>
@ -766,8 +812,8 @@ export default function App() {
<button
className={`flex-1 p-3 ${
rightPaneTab === 'properties'
? 'border-b-2 border-teal-500 font-semibold'
: 'text-warm-600'
? 'border-b-2 border-teal-500 font-semibold dark:text-warm-100'
: 'text-warm-600 dark:text-warm-400'
}`}
onClick={() => setRightPaneTab('properties')}
>