feat: add dark mode with persistent user preference
Add dark mode support using Mantine's built-in color scheme system, persisted via a new Zustand preferences store. Includes a quick toggle in the app header and an enabled switch in User Preferences. Also removes the "AI Health Scores" title from the dashboard to reclaim vertical space. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar, Alert, Button } from '@mantine/core';
|
||||
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar, Alert, Button, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
IconLogout,
|
||||
@@ -9,9 +9,12 @@ import {
|
||||
IconUserCog,
|
||||
IconUsersGroup,
|
||||
IconEyeOff,
|
||||
IconSun,
|
||||
IconMoon,
|
||||
} from '@tabler/icons-react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { AppTour } from '../onboarding/AppTour';
|
||||
import { OnboardingWizard } from '../onboarding/OnboardingWizard';
|
||||
@@ -20,6 +23,7 @@ import logoSrc from '../../assets/logo.svg';
|
||||
export function AppLayout() {
|
||||
const [opened, { toggle, close }] = useDisclosure();
|
||||
const { user, currentOrg, logout, impersonationOriginal, stopImpersonation } = useAuthStore();
|
||||
const { colorScheme, toggleColorScheme } = usePreferencesStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isImpersonating = !!impersonationOriginal;
|
||||
@@ -108,6 +112,16 @@ export function AppLayout() {
|
||||
{currentOrg && (
|
||||
<Text size="sm" c="dimmed">{currentOrg.name}</Text>
|
||||
)}
|
||||
<Tooltip label={colorScheme === 'dark' ? 'Light mode' : 'Dark mode'}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={toggleColorScheme}
|
||||
aria-label="Toggle color scheme"
|
||||
>
|
||||
{colorScheme === 'dark' ? <IconSun size={18} /> : <IconMoon size={18} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Menu shadow="md" width={220}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
|
||||
@@ -10,6 +10,7 @@ import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import { App } from './App';
|
||||
import { theme } from './theme/theme';
|
||||
import { usePreferencesStore } from './stores/preferencesStore';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -21,9 +22,11 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<MantineProvider theme={theme}>
|
||||
function Root() {
|
||||
const colorScheme = usePreferencesStore((s) => s.colorScheme);
|
||||
|
||||
return (
|
||||
<MantineProvider theme={theme} forceColorScheme={colorScheme}>
|
||||
<Notifications position="top-right" />
|
||||
<ModalsProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -33,5 +36,11 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</QueryClientProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Root />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -414,7 +414,6 @@ export function DashboardPage() {
|
||||
<Center h={200}><Loader /></Center>
|
||||
) : (
|
||||
<>
|
||||
<Text size="sm" fw={600} c="dimmed">AI Health Scores</Text>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
<HealthScoreCard
|
||||
score={healthScores?.operating || null}
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
IconUser, IconPalette, IconClock, IconBell, IconEye,
|
||||
} from '@tabler/icons-react';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
||||
|
||||
export function UserPreferencesPage() {
|
||||
const { user, currentOrg } = useAuthStore();
|
||||
const { colorScheme, toggleColorScheme } = usePreferencesStore();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
@@ -66,7 +68,10 @@ export function UserPreferencesPage() {
|
||||
<Text size="sm">Dark Mode</Text>
|
||||
<Text size="xs" c="dimmed">Switch to dark color theme</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
<Switch
|
||||
checked={colorScheme === 'dark'}
|
||||
onChange={toggleColorScheme}
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
@@ -76,7 +81,7 @@ export function UserPreferencesPage() {
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed" ta="center">Display preferences coming in a future release</Text>
|
||||
<Text size="xs" c="dimmed" ta="center">More display preferences coming in a future release</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
|
||||
26
frontend/src/stores/preferencesStore.ts
Normal file
26
frontend/src/stores/preferencesStore.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type ColorScheme = 'light' | 'dark';
|
||||
|
||||
interface PreferencesState {
|
||||
colorScheme: ColorScheme;
|
||||
toggleColorScheme: () => void;
|
||||
setColorScheme: (scheme: ColorScheme) => void;
|
||||
}
|
||||
|
||||
export const usePreferencesStore = create<PreferencesState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
colorScheme: 'light',
|
||||
toggleColorScheme: () =>
|
||||
set((state) => ({
|
||||
colorScheme: state.colorScheme === 'light' ? 'dark' : 'light',
|
||||
})),
|
||||
setColorScheme: (scheme) => set({ colorScheme: scheme }),
|
||||
}),
|
||||
{
|
||||
name: 'ledgeriq-preferences',
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user