mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Streamline theme management and improve AppearanceSettings component
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MantineProvider, ColorSchemeScript } from '@mantine/core';
|
import {
|
||||||
|
MantineProvider,
|
||||||
|
ColorSchemeScript,
|
||||||
|
localStorageColorSchemeManager,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Notifications } from '@mantine/notifications';
|
import { Notifications } from '@mantine/notifications';
|
||||||
import { ModalsProvider } from '@mantine/modals';
|
import { ModalsProvider } from '@mantine/modals';
|
||||||
import Layout from './components/layout/Layout';
|
import Layout from './components/layout/Layout';
|
||||||
@@ -39,11 +43,18 @@ const AuthenticatedContent: React.FC<AuthenticatedContentProps> = () => {
|
|||||||
|
|
||||||
type AppProps = object;
|
type AppProps = object;
|
||||||
|
|
||||||
|
const colorSchemeManager = localStorageColorSchemeManager({
|
||||||
|
key: 'mantine-color-scheme',
|
||||||
|
});
|
||||||
|
|
||||||
const App: React.FC<AppProps> = () => {
|
const App: React.FC<AppProps> = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColorSchemeScript defaultColorScheme="light" />
|
<ColorSchemeScript defaultColorScheme="light" />
|
||||||
<MantineProvider defaultColorScheme="light">
|
<MantineProvider
|
||||||
|
defaultColorScheme="light"
|
||||||
|
colorSchemeManager={colorSchemeManager}
|
||||||
|
>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('AppearanceSettings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders dark mode toggle with correct state', () => {
|
it('renders dark mode toggle with correct state', () => {
|
||||||
render(<AppearanceSettings onThemeChange={mockOnThemeChange} />);
|
render(<AppearanceSettings />);
|
||||||
|
|
||||||
expect(screen.getByText('Dark Mode')).toBeInTheDocument();
|
expect(screen.getByText('Dark Mode')).toBeInTheDocument();
|
||||||
const toggle = screen.getByRole('switch');
|
const toggle = screen.getByRole('switch');
|
||||||
@@ -46,20 +46,19 @@ describe('AppearanceSettings', () => {
|
|||||||
updateColorScheme: mockUpdateColorScheme,
|
updateColorScheme: mockUpdateColorScheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<AppearanceSettings onThemeChange={mockOnThemeChange} />);
|
render(<AppearanceSettings />);
|
||||||
|
|
||||||
const toggle = screen.getByRole('switch');
|
const toggle = screen.getByRole('switch');
|
||||||
expect(toggle).toBeChecked();
|
expect(toggle).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toggles theme from light to dark', () => {
|
it('toggles theme from light to dark', () => {
|
||||||
render(<AppearanceSettings onThemeChange={mockOnThemeChange} />);
|
render(<AppearanceSettings />);
|
||||||
|
|
||||||
const toggle = screen.getByRole('switch');
|
const toggle = screen.getByRole('switch');
|
||||||
fireEvent.click(toggle);
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Dark);
|
expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Dark);
|
||||||
expect(mockOnThemeChange).toHaveBeenCalledWith(Theme.Dark);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toggles theme from dark to light', async () => {
|
it('toggles theme from dark to light', async () => {
|
||||||
@@ -69,12 +68,11 @@ describe('AppearanceSettings', () => {
|
|||||||
updateColorScheme: mockUpdateColorScheme,
|
updateColorScheme: mockUpdateColorScheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<AppearanceSettings onThemeChange={mockOnThemeChange} />);
|
render(<AppearanceSettings />);
|
||||||
|
|
||||||
const toggle = screen.getByRole('switch');
|
const toggle = screen.getByRole('switch');
|
||||||
fireEvent.click(toggle);
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Light);
|
expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Light);
|
||||||
expect(mockOnThemeChange).toHaveBeenCalledWith(Theme.Light);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, Switch, Group, Box } from '@mantine/core';
|
import { Text, Switch, Group, Box } from '@mantine/core';
|
||||||
import { useTheme } from '../../../contexts/ThemeContext';
|
|
||||||
import { Theme } from '@/types/models';
|
import { Theme } from '@/types/models';
|
||||||
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
|
|
||||||
interface AppearanceSettingsProps {
|
const AppearanceSettings: React.FC = () => {
|
||||||
onThemeChange: (newTheme: Theme) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppearanceSettings: React.FC<AppearanceSettingsProps> = ({
|
|
||||||
onThemeChange,
|
|
||||||
}) => {
|
|
||||||
const { colorScheme, updateColorScheme } = useTheme();
|
const { colorScheme, updateColorScheme } = useTheme();
|
||||||
|
|
||||||
const handleThemeChange = (): void => {
|
const handleThemeChange = (): void => {
|
||||||
const newTheme = colorScheme === 'dark' ? Theme.Light : Theme.Dark;
|
const newTheme = colorScheme === 'dark' ? Theme.Light : Theme.Dark;
|
||||||
updateColorScheme(newTheme);
|
updateColorScheme(newTheme);
|
||||||
onThemeChange(newTheme);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import WorkspaceSettings from './WorkspaceSettings';
|
|||||||
import { Theme } from '@/types/models';
|
import { Theme } from '@/types/models';
|
||||||
|
|
||||||
const mockUpdateSettings = vi.fn();
|
const mockUpdateSettings = vi.fn();
|
||||||
|
const mockUpdateColorScheme = vi.fn();
|
||||||
vi.mock('../../../hooks/useWorkspace', () => ({
|
vi.mock('../../../hooks/useWorkspace', () => ({
|
||||||
useWorkspace: vi.fn(),
|
useWorkspace: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -48,11 +49,9 @@ vi.mock('./GeneralSettings', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./AppearanceSettings', () => ({
|
vi.mock('./AppearanceSettings', () => ({
|
||||||
default: ({ onThemeChange }: { onThemeChange: (theme: string) => void }) => (
|
default: () => (
|
||||||
<div data-testid="appearance-settings">
|
<div data-testid="appearance-settings">
|
||||||
<button onClick={() => onThemeChange('dark')} data-testid="theme-toggle">
|
Appearance Settings
|
||||||
Toggle Theme
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@@ -105,7 +104,7 @@ describe('WorkspaceSettings', () => {
|
|||||||
updateSettings: mockUpdateSettings,
|
updateSettings: mockUpdateSettings,
|
||||||
loading: false,
|
loading: false,
|
||||||
colorScheme: 'light',
|
colorScheme: 'light',
|
||||||
updateColorScheme: vi.fn(),
|
updateColorScheme: mockUpdateColorScheme,
|
||||||
switchWorkspace: vi.fn(),
|
switchWorkspace: vi.fn(),
|
||||||
deleteCurrentWorkspace: vi.fn(),
|
deleteCurrentWorkspace: vi.fn(),
|
||||||
});
|
});
|
||||||
@@ -152,13 +151,10 @@ describe('WorkspaceSettings', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles theme changes', () => {
|
it('renders appearance settings', () => {
|
||||||
render(<WorkspaceSettings />);
|
render(<WorkspaceSettings />);
|
||||||
|
|
||||||
const themeToggle = screen.getByTestId('theme-toggle');
|
expect(screen.getByTestId('appearance-settings')).toBeInTheDocument();
|
||||||
fireEvent.click(themeToggle);
|
|
||||||
|
|
||||||
expect(screen.getByText('Unsaved Changes')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes modal when cancel is clicked', () => {
|
it('closes modal when cancel is clicked', () => {
|
||||||
@@ -192,4 +188,16 @@ describe('WorkspaceSettings', () => {
|
|||||||
|
|
||||||
expect(mockUpdateSettings).not.toHaveBeenCalled();
|
expect(mockUpdateSettings).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reverts theme when canceling', () => {
|
||||||
|
render(<WorkspaceSettings />);
|
||||||
|
|
||||||
|
// Click cancel
|
||||||
|
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
|
||||||
|
// Theme should be reverted to saved state (Light)
|
||||||
|
expect(mockUpdateColorScheme).toHaveBeenCalledWith(Theme.Light);
|
||||||
|
expect(mockSetSettingsModalVisible).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useModalContext } from '../../../contexts/ModalContext';
|
|||||||
import DangerZoneSettings from './DangerZoneSettings';
|
import DangerZoneSettings from './DangerZoneSettings';
|
||||||
import AccordionControl from '../AccordionControl';
|
import AccordionControl from '../AccordionControl';
|
||||||
import {
|
import {
|
||||||
type Theme,
|
Theme,
|
||||||
type Workspace,
|
type Workspace,
|
||||||
type SettingsAction,
|
type SettingsAction,
|
||||||
SettingsActionType,
|
SettingsActionType,
|
||||||
@@ -72,7 +72,7 @@ function settingsReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WorkspaceSettings: React.FC = () => {
|
const WorkspaceSettings: React.FC = () => {
|
||||||
const { currentWorkspace, updateSettings } = useWorkspace();
|
const { currentWorkspace, updateSettings, updateColorScheme, colorScheme } = useWorkspace();
|
||||||
const { settingsModalVisible, setSettingsModalVisible } = useModalContext();
|
const { settingsModalVisible, setSettingsModalVisible } = useModalContext();
|
||||||
const [state, dispatch] = useReducer(settingsReducer, initialState);
|
const [state, dispatch] = useReducer(settingsReducer, initialState);
|
||||||
const isInitialMount = useRef<boolean>(true);
|
const isInitialMount = useRef<boolean>(true);
|
||||||
@@ -118,7 +118,13 @@ const WorkspaceSettings: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateSettings(state.localSettings);
|
// Save with current Mantine theme
|
||||||
|
const settingsToSave = {
|
||||||
|
...state.localSettings,
|
||||||
|
theme: colorScheme as Theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateSettings(settingsToSave);
|
||||||
dispatch({ type: SettingsActionType.MARK_SAVED });
|
dispatch({ type: SettingsActionType.MARK_SAVED });
|
||||||
notifications.show({
|
notifications.show({
|
||||||
message: 'Settings saved successfully',
|
message: 'Settings saved successfully',
|
||||||
@@ -137,8 +143,12 @@ const WorkspaceSettings: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
|
// Revert theme to saved state
|
||||||
|
if (state.initialSettings.theme) {
|
||||||
|
updateColorScheme(state.initialSettings.theme);
|
||||||
|
}
|
||||||
setSettingsModalVisible(false);
|
setSettingsModalVisible(false);
|
||||||
}, [setSettingsModalVisible]);
|
}, [setSettingsModalVisible, state.initialSettings.theme, updateColorScheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -180,11 +190,7 @@ const WorkspaceSettings: React.FC = () => {
|
|||||||
<Accordion.Item value="appearance">
|
<Accordion.Item value="appearance">
|
||||||
<AccordionControl>Appearance</AccordionControl>
|
<AccordionControl>Appearance</AccordionControl>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<AppearanceSettings
|
<AppearanceSettings />
|
||||||
onThemeChange={(newTheme: string) =>
|
|
||||||
handleInputChange('theme', newTheme as Theme)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, {
|
|||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useMemo,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useMantineColorScheme, type MantineColorScheme } from '@mantine/core';
|
import { useMantineColorScheme, type MantineColorScheme } from '@mantine/core';
|
||||||
@@ -32,11 +33,16 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Ensure colorScheme is never undefined by falling back to light theme
|
// Ensure colorScheme is never undefined by falling back to light theme
|
||||||
const value: ThemeContextType = {
|
const normalizedColorScheme =
|
||||||
colorScheme:
|
colorScheme === 'light' || colorScheme === 'dark' ? colorScheme : 'light';
|
||||||
colorScheme === 'light' || colorScheme === 'dark' ? colorScheme : 'light',
|
|
||||||
updateColorScheme,
|
const value: ThemeContextType = useMemo(
|
||||||
};
|
() => ({
|
||||||
|
colorScheme: normalizedColorScheme,
|
||||||
|
updateColorScheme,
|
||||||
|
}),
|
||||||
|
[normalizedColorScheme, updateColorScheme]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ export const WorkspaceDataProvider: React.FC<WorkspaceDataProviderProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateColorScheme]
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadFirstAvailableWorkspace = useCallback(async (): Promise<void> => {
|
const loadFirstAvailableWorkspace = useCallback(async (): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user