Skip to content

Commit 5494cff

Browse files
marc2332evavirseda
andauthored
refactor(tooling): Split Theme into Theme and ThemePreference (#4239)
* refactor(tooling): Split Theme into `Theme` and `ThemePreference` * fmt * fixes * fix: Avoid setting the preference to system on every load * typo * fmt * fix explorer * chore: Add `iota-` prefixes to theme localstorage key in dashboard and wallet * fix: Proper loading reactivity --------- Co-authored-by: evavirseda <evirseda@boxfish.studio>
1 parent 28aeefc commit 5494cff

File tree

11 files changed

+96
-78
lines changed

11 files changed

+96
-78
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright (c) 2024 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { PropsWithChildren, useState, useEffect, useCallback } from 'react';
5-
import { Theme } from '../../enums';
4+
import { PropsWithChildren, useState, useEffect } from 'react';
5+
import { Theme, ThemePreference } from '../../enums';
66
import { ThemeContext } from '../../contexts';
77

88
interface ThemeProviderProps {
@@ -12,40 +12,72 @@ interface ThemeProviderProps {
1212
export function ThemeProvider({ children, appId }: PropsWithChildren<ThemeProviderProps>) {
1313
const storageKey = `theme_${appId}`;
1414

15-
const getSystemTheme = () =>
16-
window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light;
15+
const getSystemTheme = () => {
16+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light;
17+
};
1718

18-
const getInitialTheme = () => {
19-
if (typeof window === 'undefined') {
20-
return Theme.System;
21-
} else {
22-
const storedTheme = localStorage?.getItem(storageKey);
23-
return storedTheme ? (storedTheme as Theme) : Theme.System;
24-
}
19+
const getThemePreference = () => {
20+
const storedTheme = localStorage?.getItem(storageKey) as ThemePreference | null;
21+
return storedTheme ? storedTheme : ThemePreference.System;
2522
};
2623

27-
const [theme, setTheme] = useState<Theme>(getInitialTheme);
24+
const [systemTheme, setSystemTheme] = useState<Theme>(Theme.Light);
25+
const [themePreference, setThemePreference] = useState<ThemePreference>(ThemePreference.System);
26+
const [isLoadingPreference, setIsLoadingPreference] = useState(true);
2827

29-
const applyTheme = useCallback((currentTheme: Theme) => {
30-
const selectedTheme = currentTheme === Theme.System ? getSystemTheme() : currentTheme;
31-
const documentElement = document.documentElement.classList;
32-
documentElement.toggle(Theme.Dark, selectedTheme === Theme.Dark);
33-
documentElement.toggle(Theme.Light, selectedTheme === Theme.Light);
28+
// Load the theme values on client
29+
useEffect(() => {
30+
if (typeof window === 'undefined') return;
31+
32+
setSystemTheme(getSystemTheme());
33+
setThemePreference(getThemePreference());
34+
35+
// Make the theme preference listener wait
36+
// until the preference is loaded in the next render
37+
setIsLoadingPreference(false);
3438
}, []);
3539

40+
// When the theme preference changes..
3641
useEffect(() => {
37-
if (typeof window === 'undefined') return;
42+
if (typeof window === 'undefined' || isLoadingPreference) return;
43+
44+
// Update localStorage with the new preference
45+
localStorage.setItem(storageKey, themePreference);
3846

39-
localStorage.setItem(storageKey, theme);
40-
applyTheme(theme);
47+
// In case of SystemPreference, listen for system theme changes
48+
if (themePreference === ThemePreference.System) {
49+
const handleSystemThemeChange = () => {
50+
const systemTheme = getSystemTheme();
51+
setSystemTheme(systemTheme);
52+
};
53+
const systemThemeMatcher = window.matchMedia('(prefers-color-scheme: dark)');
54+
systemThemeMatcher.addEventListener('change', handleSystemThemeChange);
55+
return () => systemThemeMatcher.removeEventListener('change', handleSystemThemeChange);
56+
}
57+
}, [themePreference, storageKey, isLoadingPreference]);
4158

42-
if (theme === Theme.System) {
43-
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
44-
const handleSystemThemeChange = () => applyTheme(Theme.System);
45-
systemTheme.addEventListener('change', handleSystemThemeChange);
46-
return () => systemTheme.removeEventListener('change', handleSystemThemeChange);
59+
// Derive the active theme from the preference
60+
const theme = (() => {
61+
switch (themePreference) {
62+
case ThemePreference.Dark:
63+
return Theme.Dark;
64+
case ThemePreference.Light:
65+
return Theme.Light;
66+
case ThemePreference.System:
67+
return systemTheme;
4768
}
48-
}, [theme, applyTheme, storageKey]);
69+
})();
70+
71+
// When the theme (preference or derived) changes update the CSS class
72+
useEffect(() => {
73+
const documentElement = document.documentElement.classList;
74+
documentElement.toggle(Theme.Dark, theme === Theme.Dark);
75+
documentElement.toggle(Theme.Light, theme === Theme.Light);
76+
}, [theme]);
4977

50-
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
78+
return (
79+
<ThemeContext.Provider value={{ theme, setThemePreference, themePreference }}>
80+
{children}
81+
</ThemeContext.Provider>
82+
);
5183
}

apps/core/src/contexts/ThemeContext.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { createContext } from 'react';
5-
import { Theme } from '../enums';
5+
import { Theme, ThemePreference } from '../enums';
66

77
export interface ThemeContextType {
88
theme: Theme;
9-
setTheme: (theme: Theme) => void;
9+
themePreference: ThemePreference;
10+
setThemePreference: (theme: ThemePreference) => void;
1011
}
1112

1213
export const ThemeContext = createContext<ThemeContextType>({
1314
theme: Theme.Light,
14-
setTheme: () => {},
15+
themePreference: ThemePreference.System,
16+
setThemePreference: () => {},
1517
});

apps/core/src/enums/theme.enums.ts

+5
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,10 @@
44
export enum Theme {
55
Light = 'light',
66
Dark = 'dark',
7+
}
8+
9+
export enum ThemePreference {
10+
Light = 'light',
11+
Dark = 'dark',
712
System = 'system',
813
}

apps/explorer/src/components/header/ThemeSwitcher.tsx

+7-32
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,21 @@
33

44
import { Button, ButtonType } from '@iota/apps-ui-kit';
55
import { DarkMode, LightMode } from '@iota/ui-icons';
6-
import { useEffect, useLayoutEffect } from 'react';
7-
import { useTheme, Theme } from '@iota/core';
6+
import { useTheme, Theme, ThemePreference } from '@iota/core';
87

98
export function ThemeSwitcher(): React.JSX.Element {
10-
const { theme, setTheme } = useTheme();
9+
const { theme, themePreference, setThemePreference } = useTheme();
1110

1211
const ThemeIcon = theme === Theme.Dark ? DarkMode : LightMode;
1312

1413
function handleOnClick(): void {
15-
const newTheme = theme === Theme.Light ? Theme.Dark : Theme.Light;
16-
setTheme(newTheme);
17-
saveThemeToLocalStorage(newTheme);
14+
const newTheme =
15+
themePreference === ThemePreference.Light
16+
? ThemePreference.Dark
17+
: ThemePreference.Light;
18+
setThemePreference(newTheme);
1819
}
1920

20-
function saveThemeToLocalStorage(newTheme: Theme): void {
21-
localStorage.setItem('theme', newTheme);
22-
}
23-
24-
function updateDocumentClass(theme: Theme): void {
25-
document.documentElement.classList.toggle('dark', theme === Theme.Dark);
26-
}
27-
28-
useLayoutEffect(() => {
29-
const storedTheme = localStorage.getItem('theme') as Theme | null;
30-
if (storedTheme) {
31-
setTheme(storedTheme);
32-
updateDocumentClass(storedTheme);
33-
} else {
34-
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
35-
const preferredTheme = prefersDarkTheme ? Theme.Dark : Theme.Light;
36-
37-
setTheme(preferredTheme);
38-
updateDocumentClass(preferredTheme);
39-
}
40-
}, []);
41-
42-
useEffect(() => {
43-
updateDocumentClass(theme);
44-
}, [theme]);
45-
4621
return (
4722
<Button
4823
type={ButtonType.Ghost}

apps/wallet-dashboard/app/(protected)/layout.tsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import { Notifications } from '@/components/index';
66
import React, { type PropsWithChildren } from 'react';
77
import { Button } from '@iota/apps-ui-kit';
88
import { Sidebar, TopNav } from './components';
9-
import { Theme, useTheme } from '@iota/core';
9+
import { ThemePreference, useTheme } from '@iota/core';
1010

1111
function DashboardLayout({ children }: PropsWithChildren): JSX.Element {
12-
const { theme, setTheme } = useTheme();
12+
const { theme, themePreference, setThemePreference } = useTheme();
1313

1414
const toggleTheme = () => {
15-
const newTheme = theme === Theme.Light ? Theme.Dark : Theme.Light;
16-
setTheme(newTheme);
15+
const newTheme =
16+
themePreference === ThemePreference.Light
17+
? ThemePreference.Dark
18+
: ThemePreference.Light;
19+
setThemePreference(newTheme);
1720
};
1821

1922
return (

apps/wallet-dashboard/app/page.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ function HomeDashboardPage(): JSX.Element {
3131
<main className="flex h-screen">
3232
<div className="relative hidden sm:flex md:w-1/3">
3333
<video
34+
key={theme}
35+
src={videoSrc}
3436
autoPlay
3537
muted
3638
loop
3739
className="absolute right-0 top-0 h-full w-full min-w-fit object-cover"
3840
disableRemotePlayback
39-
>
40-
<source src={videoSrc} type="video/mp4" />
41-
</video>
41+
></video>
4242
</div>
4343
<div className="flex h-full w-full flex-col items-center justify-between p-md sm:p-2xl">
4444
<IotaLogoWeb width={130} height={32} />

apps/wallet-dashboard/components/staking-overview/StartStaking.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function StartStaking() {
4747
</div>
4848
<div className="relative w-full overflow-hidden">
4949
<video
50+
key={videoSrc}
5051
src={videoSrc}
5152
autoPlay
5253
loop

apps/wallet-dashboard/providers/AppProviders.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function AppProviders({ children }: React.PropsWithChildren) {
3636
},
3737
]}
3838
>
39-
<ThemeProvider appId="dashboard">
39+
<ThemeProvider appId="iota-dashboard">
4040
<PopupProvider>
4141
{children}
4242
<Toaster />

apps/wallet/src/ui/app/components/menu/content/ThemeSettings.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { RadioButton } from '@iota/apps-ui-kit';
5-
import { Theme, useTheme } from '@iota/core';
5+
import { ThemePreference, useTheme } from '@iota/core';
66
import { Overlay } from '_components';
77
import { useNavigate } from 'react-router-dom';
88

99
export function ThemeSettings() {
10-
const { theme, setTheme } = useTheme();
10+
const { themePreference, setThemePreference } = useTheme();
1111

1212
const navigate = useNavigate();
1313

1414
return (
1515
<Overlay showModal title="Theme" closeOverlay={() => navigate('/')} showBackButton>
1616
<div className="flex w-full flex-col">
17-
{Object.entries(Theme).map(([key, value]) => (
17+
{Object.entries(ThemePreference).map(([key, value]) => (
1818
<div className="px-md" key={value}>
1919
<RadioButton
2020
label={key}
21-
isChecked={theme === value}
22-
onChange={() => setTheme(value)}
21+
isChecked={themePreference === value}
22+
onChange={() => setThemePreference(value)}
2323
/>
2424
</div>
2525
))}

apps/wallet/src/ui/app/components/menu/content/WalletSettingsMenuList.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { ampli } from '_src/shared/analytics/ampli';
3131
import { useTheme, getCustomNetwork } from '@iota/core';
3232

3333
function MenuList() {
34-
const { theme } = useTheme();
34+
const { themePreference } = useTheme();
3535
const navigate = useNavigate();
3636
const activeAccount = useActiveAccount();
3737
const networkUrl = useNextMenuUrl(true, '/network');
@@ -84,7 +84,7 @@ function MenuList() {
8484
}
8585

8686
const autoLockSubtitle = handleAutoLockSubtitle();
87-
const themeSubtitle = theme.charAt(0).toUpperCase() + theme.slice(1);
87+
const themeSubtitle = themePreference.charAt(0).toUpperCase() + themePreference.slice(1);
8888
const MENU_ITEMS = [
8989
{
9090
title: 'Network',

apps/wallet/src/ui/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function AppWrapper() {
9696
>
9797
<KioskClientProvider>
9898
<AccountsFormProvider>
99-
<ThemeProvider appId="wallet">
99+
<ThemeProvider appId="iota-wallet">
100100
<UnlockAccountProvider>
101101
<div
102102
className={cn(

0 commit comments

Comments
 (0)