diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 530f0f00d..4c7c80558 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -21,6 +21,7 @@ import ErrorBoundary from './containers/ErrorBoundary'; import TenantAppContainer from './containers/TenantAppContainer'; import AppConfirmModalProvider from './contexts/AppConfirmModalProvider'; import AppEndpointsProvider from './contexts/AppEndpointsProvider'; +import { AppThemeProvider } from './contexts/AppThemeProvider'; import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider'; void initI18n(); @@ -60,17 +61,19 @@ const Content = () => { scopes, }} > - - {!isCloud || isSettle ? ( - - - - - - ) : ( - - )} - + + + {!isCloud || isSettle ? ( + + + + + + ) : ( + + )} + + ); }; diff --git a/packages/console/src/cloud/App.tsx b/packages/console/src/cloud/App.tsx index b09f47a34..be16c4c6a 100644 --- a/packages/console/src/cloud/App.tsx +++ b/packages/console/src/cloud/App.tsx @@ -1,6 +1,5 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import { AppThemeProvider } from '@/contexts/AppThemeProvider'; import Callback from '@cloud/pages/Callback'; import * as styles from './App.module.scss'; @@ -11,16 +10,14 @@ import { CloudRoute } from './types'; const App = () => { return ( - -
- - } /> - } /> - } /> - } /> - -
-
+
+ + } /> + } /> + } /> + } /> + +
); }; diff --git a/packages/console/src/contexts/AppThemeProvider/index.tsx b/packages/console/src/contexts/AppThemeProvider/index.tsx index 6a79ca0d4..c61f83f06 100644 --- a/packages/console/src/contexts/AppThemeProvider/index.tsx +++ b/packages/console/src/contexts/AppThemeProvider/index.tsx @@ -1,5 +1,5 @@ import { Theme } from '@logto/schemas'; -import { conditionalString, trySafe } from '@silverhand/essentials'; +import { conditionalString, noop, trySafe } from '@silverhand/essentials'; import type { ReactNode } from 'react'; import { useEffect, useMemo, useState, createContext } from 'react'; @@ -10,44 +10,55 @@ import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-m import * as styles from './index.module.scss'; type Props = { - fixedTheme?: Theme; - appearanceMode?: AppearanceMode; children: ReactNode; }; -type AppTheme = { +type Context = { theme: Theme; + setAppearanceMode: (mode: AppearanceMode) => void; + setThemeOverride: React.Dispatch>; }; -const defaultTheme: Theme = Theme.Light; - const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)'); const getThemeBySystemConfiguration = (): Theme => darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light; -export const getAppearanceModeFromLocalStorage = (): AppearanceMode => +export const buildDefaultAppearanceMode = (): AppearanceMode => trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ?? DynamicAppearanceMode.System; -export const AppThemeContext = createContext({ +const defaultAppearanceMode = buildDefaultAppearanceMode(); + +const defaultTheme = + defaultAppearanceMode === DynamicAppearanceMode.System + ? getThemeBySystemConfiguration() + : defaultAppearanceMode; + +export const AppThemeContext = createContext({ theme: defaultTheme, + setAppearanceMode: noop, + setThemeOverride: noop, }); -export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props) => { +export const AppThemeProvider = ({ children }: Props) => { const [theme, setTheme] = useState(defaultTheme); + const [themeOverride, setThemeOverride] = useState(); + const [mode, setMode] = useState(defaultAppearanceMode); + + const setAppearanceMode = (mode: AppearanceMode) => { + setMode(mode); + localStorage.setItem(appearanceModeStorageKey, mode); + }; useEffect(() => { - if (fixedTheme) { - setTheme(fixedTheme); + if (themeOverride) { + setTheme(themeOverride); return; } - // Note: if the appearanceMode is not available, attempt to retrieve the last saved value from localStorage. - const appliedAppearanceMode = appearanceMode ?? getAppearanceModeFromLocalStorage(); - - if (appliedAppearanceMode !== DynamicAppearanceMode.System) { - setTheme(appliedAppearanceMode); + if (mode !== DynamicAppearanceMode.System) { + setTheme(mode); return; } @@ -63,7 +74,7 @@ export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props return () => { darkThemeWatchMedia.removeEventListener('change', changeTheme); }; - }, [appearanceMode, fixedTheme]); + }, [mode, themeOverride]); // Set Theme Mode useEffect(() => { @@ -71,9 +82,11 @@ export const AppThemeProvider = ({ fixedTheme, appearanceMode, children }: Props document.body.classList.add(conditionalString(styles[theme])); }, [theme]); - const context = useMemo( + const context = useMemo( () => ({ theme, + setAppearanceMode, + setThemeOverride, }), [theme] ); diff --git a/packages/console/src/hooks/use-user-preferences.ts b/packages/console/src/hooks/use-user-preferences.ts index bf9dbd046..16f11c62c 100644 --- a/packages/console/src/hooks/use-user-preferences.ts +++ b/packages/console/src/hooks/use-user-preferences.ts @@ -1,9 +1,8 @@ import { builtInLanguages as builtInConsoleLanguages } from '@logto/phrases'; -import { useEffect, useMemo } from 'react'; +import { useContext, useEffect, useMemo } from 'react'; import { z } from 'zod'; -import { appearanceModeStorageKey } from '@/consts'; -import { getAppearanceModeFromLocalStorage } from '@/contexts/AppThemeProvider'; +import { AppThemeContext, buildDefaultAppearanceMode } from '@/contexts/AppThemeProvider'; import { appearanceModeGuard } from '@/types/appearance-mode'; import useMeCustomData from './use-me-custom-data'; @@ -22,6 +21,7 @@ export type UserPreferences = z.infer; const useUserPreferences = () => { const { data, error, isLoading, isLoaded, update: updateMeCustomData } = useMeCustomData(); + const { setAppearanceMode } = useContext(AppThemeContext); const userPreferences = useMemo(() => { const parsed = z.object({ [adminConsolePreferencesKey]: userPreferencesGuard }).safeParse(data); @@ -29,7 +29,7 @@ const useUserPreferences = () => { return parsed.success ? parsed.data[adminConsolePreferencesKey] : { - appearanceMode: getAppearanceModeFromLocalStorage(), + appearanceMode: buildDefaultAppearanceMode(), }; }, [data]); @@ -43,8 +43,8 @@ const useUserPreferences = () => { }; useEffect(() => { - localStorage.setItem(appearanceModeStorageKey, userPreferences.appearanceMode); - }, [userPreferences.appearanceMode]); + setAppearanceMode(userPreferences.appearanceMode); + }, [setAppearanceMode, userPreferences.appearanceMode]); return { isLoading, diff --git a/packages/console/src/onboarding/App.tsx b/packages/console/src/onboarding/App.tsx index 68dd49a11..abc71dccf 100644 --- a/packages/console/src/onboarding/App.tsx +++ b/packages/console/src/onboarding/App.tsx @@ -1,4 +1,5 @@ import { Theme } from '@logto/schemas'; +import { useContext, useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { SWRConfig } from 'swr'; @@ -6,7 +7,7 @@ import AppLoading from '@/components/AppLoading'; import Toast from '@/components/Toast'; import { getBasename } from '@/consts'; import AppBoundary from '@/containers/AppBoundary'; -import { AppThemeProvider } from '@/contexts/AppThemeProvider'; +import { AppThemeContext } from '@/contexts/AppThemeProvider'; import useSwrOptions from '@/hooks/use-swr-options'; import NotFound from '@/pages/NotFound'; @@ -24,6 +25,15 @@ const welcomePathname = getOnboardingPage(OnboardingPage.Welcome); const App = () => { const swrOptions = useSwrOptions(); + const { setThemeOverride } = useContext(AppThemeContext); + + useEffect(() => { + setThemeOverride(Theme.Light); + + return () => { + setThemeOverride(undefined); + }; + }, [setThemeOverride]); const { data: { questionnaire }, @@ -38,39 +48,31 @@ const App = () => {
- - - - + + + + } /> + }> } /> - }> - } /> - } /> - : } - /> - - ) : ( - - ) - } - /> - : - } - /> - - } /> - - - + } /> + : } + /> + : + } + /> + : } + /> + + } /> + +
diff --git a/packages/console/src/pages/Main/index.tsx b/packages/console/src/pages/Main/index.tsx index 50af7fd41..33fcc586a 100644 --- a/packages/console/src/pages/Main/index.tsx +++ b/packages/console/src/pages/Main/index.tsx @@ -6,9 +6,7 @@ import { getBasename } from '@/consts'; import AppBoundary from '@/containers/AppBoundary'; import AppContent from '@/containers/AppContent'; import ConsoleContent from '@/containers/ConsoleContent'; -import { AppThemeProvider } from '@/contexts/AppThemeProvider'; import useSwrOptions from '@/hooks/use-swr-options'; -import useUserPreferences from '@/hooks/use-user-preferences'; import Callback from '@/pages/Callback'; import Welcome from '@/pages/Welcome'; @@ -16,26 +14,21 @@ import HandleSocialCallback from '../Profile/containers/HandleSocialCallback'; const Main = () => { const swrOptions = useSwrOptions(); - const { - data: { appearanceMode }, - } = useUserPreferences(); return ( - - - - - } /> - } /> - } /> - }> - } /> - - - - + + + + } /> + } /> + } /> + }> + } /> + + + );