✨(front) add persistent darkmode
add persistent darkmode and change settings modal
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(front) add persistent darkmode
|
||||
- ✨(front) add ui kit #240
|
||||
- 🧱(files) allow to use S3 storage without external access #849
|
||||
- ✨(backend) add FindRagBackend #209
|
||||
|
||||
@@ -2,6 +2,9 @@ import merge from 'lodash/merge';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
|
||||
import { safeLocalStorage } from '@/utils/storages';
|
||||
|
||||
import { tokens } from './cunningham-tokens';
|
||||
|
||||
type Tokens = typeof tokens.themes.default &
|
||||
@@ -26,6 +29,7 @@ interface ThemeStore {
|
||||
setTheme: (theme: Theme) => void;
|
||||
spacingsTokens: Partial<SpacingsTokens>;
|
||||
theme: Theme;
|
||||
baseTheme: Theme; // 'default' or 'dsfr' (not persisted)
|
||||
themeTokens: Partial<Tokens['globals']>;
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
@@ -49,6 +53,15 @@ const getComponentTokens = (
|
||||
const DEFAULT_THEME: Theme = 'default';
|
||||
const defaultTokens = getMergedTokens(DEFAULT_THEME);
|
||||
|
||||
// Helper to get isDarkMode from useChatPreferencesStore
|
||||
const getIsDarkModeFromPreferences = (): boolean => {
|
||||
try {
|
||||
return useChatPreferencesStore.getState().isDarkModePreference ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: ThemeStore = {
|
||||
colorsTokens: defaultTokens.globals.colors,
|
||||
componentTokens: getComponentTokens(defaultTokens),
|
||||
@@ -58,8 +71,9 @@ const initialState: ThemeStore = {
|
||||
setTheme: () => {},
|
||||
spacingsTokens: defaultTokens.globals.spacings,
|
||||
theme: DEFAULT_THEME,
|
||||
baseTheme: DEFAULT_THEME,
|
||||
themeTokens: defaultTokens.globals,
|
||||
isDarkMode: false,
|
||||
isDarkMode: getIsDarkModeFromPreferences(),
|
||||
toggleDarkMode: () => {},
|
||||
};
|
||||
|
||||
@@ -68,30 +82,51 @@ export const useCunninghamTheme = create<ThemeStore>()(
|
||||
(set) => ({
|
||||
...initialState,
|
||||
setTheme: (theme: Theme) => {
|
||||
const newTokens = getMergedTokens(theme);
|
||||
// Extract base theme (default or dsfr)
|
||||
const baseTheme: Theme =
|
||||
theme === 'dark' || theme === 'dsfr-dark'
|
||||
? theme === 'dark'
|
||||
? 'default'
|
||||
: 'dsfr'
|
||||
: theme;
|
||||
|
||||
const isDarkMode =
|
||||
getIsDarkModeFromPreferences() ??
|
||||
(theme === 'dark' || theme === 'dsfr-dark');
|
||||
|
||||
// Apply dark mode based on stored preference or theme
|
||||
const finalTheme: Theme = isDarkMode
|
||||
? baseTheme === 'dsfr'
|
||||
? 'dsfr-dark'
|
||||
: 'dark'
|
||||
: baseTheme;
|
||||
|
||||
const newTokens = getMergedTokens(finalTheme);
|
||||
|
||||
set({
|
||||
colorsTokens: newTokens.globals.colors,
|
||||
componentTokens: getComponentTokens(newTokens),
|
||||
contextualTokens: newTokens.contextuals,
|
||||
currentTokens: tokens.themes[theme] as Partial<Tokens>,
|
||||
currentTokens: tokens.themes[finalTheme] as Partial<Tokens>,
|
||||
fontSizesTokens: newTokens.globals.font.sizes,
|
||||
spacingsTokens: newTokens.globals.spacings,
|
||||
theme,
|
||||
theme: finalTheme,
|
||||
baseTheme,
|
||||
themeTokens: newTokens.globals,
|
||||
isDarkMode: theme === 'dark' || theme === 'dsfr-dark',
|
||||
isDarkMode,
|
||||
});
|
||||
},
|
||||
toggleDarkMode: () => {
|
||||
useChatPreferencesStore.getState().toggleDarkModePreferences();
|
||||
|
||||
set((state) => {
|
||||
const newTheme =
|
||||
state.theme === 'default'
|
||||
? 'dark'
|
||||
: state.theme === 'dark'
|
||||
? 'default'
|
||||
: state.theme === 'dsfr'
|
||||
? 'dsfr-dark'
|
||||
: 'dsfr';
|
||||
const newIsDarkMode = getIsDarkModeFromPreferences();
|
||||
const newTheme: Theme = newIsDarkMode
|
||||
? state.baseTheme === 'dsfr'
|
||||
? 'dsfr-dark'
|
||||
: 'dark'
|
||||
: state.baseTheme;
|
||||
|
||||
const newTokens = getMergedTokens(newTheme);
|
||||
|
||||
return {
|
||||
@@ -102,15 +137,27 @@ export const useCunninghamTheme = create<ThemeStore>()(
|
||||
fontSizesTokens: newTokens.globals.font.sizes,
|
||||
spacingsTokens: newTokens.globals.spacings,
|
||||
theme: newTheme,
|
||||
baseTheme: state.baseTheme,
|
||||
themeTokens: newTokens.globals,
|
||||
isDarkMode: newTheme === 'dark' || newTheme === 'dsfr-dark',
|
||||
isDarkMode: newIsDarkMode,
|
||||
};
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'cunningham-theme',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
||||
storage: safeLocalStorage as any,
|
||||
partialize: (state) => ({ isDarkMode: state.isDarkMode }),
|
||||
onRehydrateStorage: () => (state, error) => {
|
||||
if (error) {
|
||||
console.error('[useCunninghamTheme] Rehydration error:', error);
|
||||
return;
|
||||
}
|
||||
if (state) {
|
||||
state.isDarkMode = getIsDarkModeFromPreferences();
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,8 +4,10 @@ import { persist } from 'zustand/middleware';
|
||||
interface ChatPreferencesState {
|
||||
selectedModelHrid: string | null;
|
||||
forceWebSearch: boolean;
|
||||
isDarkModePreference: boolean;
|
||||
isPanelOpen: boolean;
|
||||
setSelectedModelHrid: (hrid: string | null) => void;
|
||||
toggleDarkModePreferences: () => void;
|
||||
toggleForceWebSearch: () => void;
|
||||
setPanelOpen: (isOpen: boolean) => void;
|
||||
togglePanel: () => void;
|
||||
@@ -16,8 +18,11 @@ export const useChatPreferencesStore = create<ChatPreferencesState>()(
|
||||
(set) => ({
|
||||
selectedModelHrid: null,
|
||||
forceWebSearch: false,
|
||||
isDarkModePreference: false,
|
||||
isPanelOpen: false,
|
||||
setSelectedModelHrid: (hrid) => set({ selectedModelHrid: hrid }),
|
||||
toggleDarkModePreferences: () =>
|
||||
set((state) => ({ isDarkModePreference: !state.isDarkModePreference })),
|
||||
toggleForceWebSearch: () =>
|
||||
set((state) => ({ forceWebSearch: !state.forceWebSearch })),
|
||||
setPanelOpen: (isOpen) => set({ isPanelOpen: isOpen }),
|
||||
|
||||
+32
-42
@@ -2,7 +2,6 @@ import { Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, StyledLink, Text, ToggleSwitch, useToast } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useUserUpdate } from '@/core/api/useUserUpdate';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useAuthQuery } from '@/features/auth/api';
|
||||
@@ -56,7 +55,7 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
|
||||
title={t('Assistant settings')}
|
||||
>
|
||||
<Box aria-label={t('Assistant settings')}>
|
||||
<Box $align="center" $justify="space-between">
|
||||
<Box $justify="space-between">
|
||||
<Box $gap="2xs">
|
||||
<Text
|
||||
$size="xs"
|
||||
@@ -66,49 +65,10 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
|
||||
$padding={{ top: 'sm', bottom: 'sm' }}
|
||||
>
|
||||
{t(
|
||||
'The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.',
|
||||
'The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.',
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Box $gap="2xs" $padding={{ top: 'md' }}>
|
||||
<Text
|
||||
$size="md"
|
||||
$weight="500"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{t('Dark mode')}
|
||||
</Text>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="flex-start"
|
||||
>
|
||||
<Box $css="max-width: 70%;">
|
||||
<Text
|
||||
$css={`
|
||||
display: inline-block;
|
||||
`}
|
||||
$size="xs"
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$weight="400"
|
||||
>
|
||||
{t(
|
||||
'Enable dark mode to reduce eye strain in low-light environments.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<ToggleSwitch
|
||||
checked={isDarkMode}
|
||||
onChange={() => toggleDarkMode()}
|
||||
aria-label={t('Dark mode')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<HorizontalSeparator />
|
||||
|
||||
<Text
|
||||
$size="md"
|
||||
$weight="500"
|
||||
@@ -161,6 +121,36 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
|
||||
aria-label={t('Allow conversation analysis')}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
$display="block"
|
||||
$justify="space-between"
|
||||
$align="flex-end"
|
||||
$gap="2xs"
|
||||
$padding={{ top: 'md' }}
|
||||
$css="min-width: 100%;"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="min-width: 70%;"
|
||||
>
|
||||
<Box $css="min-width: 70%;">
|
||||
<Text
|
||||
$size="md"
|
||||
$weight="500"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{t('Dark mode')}
|
||||
</Text>
|
||||
</Box>
|
||||
<ToggleSwitch
|
||||
checked={isDarkMode}
|
||||
onChange={() => toggleDarkMode()}
|
||||
aria-label={t('Dark mode')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user