diff --git a/src/assets/appearance/dark.svg b/src/assets/appearance/dark.svg
new file mode 100644
index 0000000..ada70d0
--- /dev/null
+++ b/src/assets/appearance/dark.svg
@@ -0,0 +1,40 @@
+
\ No newline at end of file
diff --git a/src/assets/appearance/light.svg b/src/assets/appearance/light.svg
new file mode 100644
index 0000000..e916047
--- /dev/null
+++ b/src/assets/appearance/light.svg
@@ -0,0 +1,39 @@
+
\ No newline at end of file
diff --git a/src/assets/appearance/system.svg b/src/assets/appearance/system.svg
new file mode 100644
index 0000000..549ee6c
--- /dev/null
+++ b/src/assets/appearance/system.svg
@@ -0,0 +1,104 @@
+
\ No newline at end of file
diff --git a/src/components/preferences/components/Section.tsx b/src/components/preferences/components/PreferencesSection.tsx
similarity index 88%
rename from src/components/preferences/components/Section.tsx
rename to src/components/preferences/components/PreferencesSection.tsx
index 0eed257..852ed8f 100644
--- a/src/components/preferences/components/Section.tsx
+++ b/src/components/preferences/components/PreferencesSection.tsx
@@ -9,7 +9,7 @@ interface SectionProps {
onClose: () => void;
}
-const Section = ({ className = '', children, title, onBackButtonClicked, onClose }: SectionProps) => {
+const PreferencesSection = ({ className = '', children, title, onBackButtonClicked, onClose }: SectionProps) => {
return (
@@ -25,7 +25,7 @@ const Section = ({ className = '', children, title, onBackButtonClicked, onClose
@@ -35,4 +35,4 @@ const Section = ({ className = '', children, title, onBackButtonClicked, onClose
);
};
-export default Section;
+export default PreferencesSection;
diff --git a/src/components/preferences/sections/general/components/PreferenceSectionLayout.tsx b/src/components/preferences/sections/general/components/PreferenceSectionLayout.tsx
new file mode 100644
index 0000000..986904d
--- /dev/null
+++ b/src/components/preferences/sections/general/components/PreferenceSectionLayout.tsx
@@ -0,0 +1,29 @@
+import { CaretLeftIcon } from '@phosphor-icons/react';
+import { type ReactNode } from 'react';
+
+interface SectionProps {
+ className?: string;
+ children: ReactNode;
+ title: string;
+ onBackButtonClicked?: () => void;
+}
+
+const PreferenceSectionLayout = ({ className = '', children, title, onBackButtonClicked }: Readonly
) => {
+ return (
+
+
+ {onBackButtonClicked && (
+
+ )}
+
{title}
+
+ {children}
+
+ );
+};
+
+export default PreferenceSectionLayout;
diff --git a/src/components/preferences/sections/general/components/appearance/index.tsx b/src/components/preferences/sections/general/components/appearance/index.tsx
new file mode 100644
index 0000000..a7f35a1
--- /dev/null
+++ b/src/components/preferences/sections/general/components/appearance/index.tsx
@@ -0,0 +1,39 @@
+import { useThemeContext } from '@/context/theme/useThemeContext';
+import { useTranslationContext } from '@/i18n';
+import appearance_dark from '@/assets/appearance/dark.svg';
+import appearance_light from '@/assets/appearance/light.svg';
+import appearance_system from '@/assets/appearance/system.svg';
+import PreferenceSectionLayout from '../PreferenceSectionLayout';
+import ThemeButton from './theme-button';
+import type { Theme } from '@/context/theme/types';
+
+const themes = [
+ { theme: 'system', img: appearance_system },
+ { theme: 'light', img: appearance_light },
+ { theme: 'dark', img: appearance_dark },
+];
+
+const Appearance = () => {
+ const { translate } = useTranslationContext();
+ const { currentTheme, toggleTheme } = useThemeContext();
+
+ return (
+
+
+
+ {themes.map((themeInfo) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Appearance;
diff --git a/src/components/preferences/sections/general/components/appearance/theme-button/index.tsx b/src/components/preferences/sections/general/components/appearance/theme-button/index.tsx
new file mode 100644
index 0000000..b733cc8
--- /dev/null
+++ b/src/components/preferences/sections/general/components/appearance/theme-button/index.tsx
@@ -0,0 +1,38 @@
+import type { Theme } from '@/context/theme/types';
+import { useTranslationContext } from '@/i18n';
+
+interface ThemeButtonProps {
+ theme: Theme;
+ toggleTheme: (theme: Theme) => void;
+ isSelected: boolean;
+ img: string;
+}
+
+const ThemeButton = ({ theme, toggleTheme, isSelected, img }: ThemeButtonProps) => {
+ const { translate } = useTranslationContext();
+
+ return (
+
+ );
+};
+
+export default ThemeButton;
diff --git a/src/components/preferences/sections/general/index.tsx b/src/components/preferences/sections/general/index.tsx
index 765eb25..6bcf7bf 100644
--- a/src/components/preferences/sections/general/index.tsx
+++ b/src/components/preferences/sections/general/index.tsx
@@ -1,13 +1,18 @@
import { useTranslationContext } from '@/i18n';
-import Section from '../../components/Section';
+import PreferencesSection from '../../components/PreferencesSection';
+import Appearance from './components/appearance';
const GeneralSection = ({ onClose }: { onClose: () => void }) => {
const { translate } = useTranslationContext();
return (
-
- {/* TODO: Add appearance, language and support components */}
- {translate('modals.preferences.sections.general.title')}
-
+
+ {/* TODO: Add language and support components */}
+
+
);
};
diff --git a/src/context/theme/ThemeProvider.tsx b/src/context/theme/ThemeProvider.tsx
new file mode 100644
index 0000000..de841ed
--- /dev/null
+++ b/src/context/theme/ThemeProvider.tsx
@@ -0,0 +1,66 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { LocalStorageService } from '@/services/local-storage';
+import { ThemeContext } from './useThemeContext';
+import type { Theme } from './types';
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+}
+
+export const ThemeProvider: React.FC = ({ children }) => {
+ const rootRef = useRef(document.getElementById('root'));
+ const stored = LocalStorageService.instance.get('theme') as Theme | null;
+ const defaultTheme = stored ?? 'system';
+ const prefersDark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches;
+ const [currentTheme, setCurrentTheme] = useState(defaultTheme);
+ const [checkoutTheme, setCheckoutTheme] = useState<'light' | 'dark'>(prefersDark ? 'dark' : 'light');
+
+ const toggleTheme = (theme: Theme) => setCurrentTheme(theme);
+
+ const persistDarkTheme = (value: boolean) => {
+ LocalStorageService.instance.set('theme:isDark', value ? 'true' : 'false');
+ };
+
+ useEffect(() => {
+ const root = rootRef.current;
+ if (!root || !currentTheme) return;
+
+ const updateTheme = () => {
+ const prefersDark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches;
+
+ LocalStorageService.instance.set('theme', currentTheme);
+
+ if (currentTheme === 'dark' || (currentTheme === 'system' && prefersDark)) {
+ root.style.backgroundImage = 'none';
+ document.documentElement.classList.add('dark');
+ setCheckoutTheme('dark');
+ persistDarkTheme(true);
+ return;
+ }
+
+ // fallback to light theme
+ root.style.backgroundImage = 'none';
+ document.documentElement.classList.remove('dark');
+ setCheckoutTheme('light');
+ persistDarkTheme(false);
+ };
+
+ updateTheme();
+
+ const mediaQuery = globalThis.matchMedia('(prefers-color-scheme: dark)');
+ mediaQuery.addEventListener('change', updateTheme);
+
+ return () => mediaQuery.removeEventListener('change', updateTheme);
+ }, [currentTheme]);
+
+ const themeContextValue = useMemo(
+ () => ({
+ currentTheme,
+ checkoutTheme,
+ toggleTheme,
+ }),
+ [currentTheme, checkoutTheme],
+ );
+
+ return {children};
+};
diff --git a/src/context/theme/types/index.ts b/src/context/theme/types/index.ts
new file mode 100644
index 0000000..a3d61e3
--- /dev/null
+++ b/src/context/theme/types/index.ts
@@ -0,0 +1 @@
+export type Theme = 'system' | 'light' | 'dark';
diff --git a/src/context/theme/useThemeContext.ts b/src/context/theme/useThemeContext.ts
new file mode 100644
index 0000000..23bf211
--- /dev/null
+++ b/src/context/theme/useThemeContext.ts
@@ -0,0 +1,16 @@
+import { createContext, useContext } from 'react';
+import type { Theme } from './types';
+
+interface ThemeContextProps {
+ currentTheme: Theme | undefined;
+ checkoutTheme: 'light' | 'dark' | undefined;
+ toggleTheme: (theme: Theme) => void;
+}
+
+export const ThemeContext = createContext({
+ currentTheme: undefined,
+ checkoutTheme: undefined,
+ toggleTheme: () => {},
+});
+
+export const useThemeContext = (): ThemeContextProps => useContext(ThemeContext);
diff --git a/src/features/mail/MailView.tsx b/src/features/mail/MailView.tsx
index 8c6590b..6227bdb 100644
--- a/src/features/mail/MailView.tsx
+++ b/src/features/mail/MailView.tsx
@@ -25,7 +25,7 @@ const MailView = ({ folder }: MailViewProps) => {
{/* Tray */}
{/* Mail Preview */}
-
+
diff --git a/src/features/welcome/index.tsx b/src/features/welcome/index.tsx
index 72d10ea..35f0dcf 100644
--- a/src/features/welcome/index.tsx
+++ b/src/features/welcome/index.tsx
@@ -24,7 +24,7 @@ const WelcomePage = () => {
-
{translate('meet')}
+
{translate('title')}