chore(theme): define palette and typographies and override mui styles with this values, also add rtlProvider

This commit is contained in:
Sajad Mirjalili
2025-07-15 17:14:38 +03:30
parent 76d728c2b5
commit b42f6c37b9
16 changed files with 642 additions and 16 deletions

View File

@@ -5,6 +5,22 @@
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Harmony club</title>
<!-- this script add for preventing initial theme flashing -->
<script>
(function () {
try {
const THEME_STORAGE_KEY = 'mui-mode';
const DARK_THEME_CLASS_NAME = 'dark';
const savedMode = localStorage.getItem(THEME_STORAGE_KEY);
const prefersDark = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches;
if (savedMode === 'dark' || (!savedMode && prefersDark)) {
document.documentElement.classList.add(DARK_THEME_CLASS_NAME);
}
} catch (e) {}
})();
</script>
</head>
<body>
<div id="root"></div>

64
package-lock.json generated
View File

@@ -11,12 +11,15 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.2.0",
"@mui/stylis-plugin-rtl": "^7.2.0",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0"
"react-i18next": "^15.6.0",
"stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
@@ -358,6 +361,12 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
@@ -371,6 +380,12 @@
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
@@ -1298,6 +1313,26 @@
}
}
},
"node_modules/@mui/stylis-plugin-rtl": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/stylis-plugin-rtl/-/stylis-plugin-rtl-7.2.0.tgz",
"integrity": "sha512-x+9UMuAuB82dbzxxFeCoZEZaLzdBVOSlkSEhFu8iJgsEmwJXJ9l09NsiONw8sJlp7J5etpGEiafDz0XSM/C/Fw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"cssjanus": "^2.3.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"stylis": "4.x"
}
},
"node_modules/@mui/system": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz",
@@ -2414,6 +2449,15 @@
"node": ">= 8"
}
},
"node_modules/cssjanus": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.3.0.tgz",
"integrity": "sha512-ZZXXn51SnxRxAZ6fdY7mBDPmA4OZd83q/J9Gdqz3YmE9TUq+9tZl+tdOnCi7PpNygI6PEkehj9rgifv5+W8a5A==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3911,11 +3955,23 @@
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/stylis-plugin-rtl": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/stylis-plugin-rtl/-/stylis-plugin-rtl-2.1.1.tgz",
"integrity": "sha512-q6xIkri6fBufIO/sV55md2CbgS5c6gg9EhSVATtHHCdOnbN/jcI0u3lYhNVeuI65c4lQPo67g8xmq5jrREvzlg==",
"license": "MIT",
"dependencies": {
"cssjanus": "^2.0.1"
},
"peerDependencies": {
"stylis": "4.x"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -14,12 +14,15 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.2.0",
"@mui/stylis-plugin-rtl": "^7.2.0",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0"
"react-i18next": "^15.6.0",
"stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",

View File

@@ -1,3 +1,3 @@
{
"helloWorld": "hello world"
}
"helloWorld": "hello world"
}

View File

@@ -1,3 +1,3 @@
{
"helloWorld": "سلام دنیا"
}
"helloWorld": "سلام دنیا"
}

View File

@@ -1,4 +1,4 @@
import { CssBaseline } from '@mui/material';
import { Box, CssBaseline, TextField, useColorScheme } from '@mui/material';
import './App.css';
import { useTranslation } from 'react-i18next';
import { LanguageManager } from './components/LanguageManager';
@@ -13,9 +13,28 @@ function App() {
<div style={{ padding: '16px' }}>
<h1>{t('helloWorld')}</h1>
<p>The main content and router will go here.</p>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<ThemeToggleButton />
<TextField label={t('helloWorld')} />
</Box>
</div>
</>
);
}
export default App;
import { Button } from '@mui/material';
export const ThemeToggleButton = () => {
const { mode, setMode } = useColorScheme();
return (
<Button
variant="contained"
onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}
>
Switch to {mode === 'light' ? 'Dark' : 'Light'} Mode
</Button>
);
};

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
/**
@@ -8,10 +8,10 @@ import { useTranslation } from 'react-i18next';
export const LanguageManager = () => {
const { i18n } = useTranslation();
useEffect(() => {
useLayoutEffect(() => {
const handleLanguageChange = (lng: string) => {
document.documentElement.dir = i18n.dir(lng);
document.documentElement.lang = lng;
document.documentElement.setAttribute('dir', i18n.dir(lng));
document.documentElement.setAttribute('lang', lng);
};
// Set initial values on component mount

View File

@@ -9,7 +9,7 @@ i18n
.use(initReactI18next) // Passes i18n down to react-i18next
.init({
supportedLngs: ['en', 'fa'], // Supported languages
fallbackLng: 'fa',
fallbackLng: 'en',
detection: {
order: ['localStorage', 'cookie', 'navigator'],
caches: ['localStorage', 'cookie'],

View File

@@ -1,9 +1,17 @@
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/config/i18n';
import { CustomThemeProvider } from './CustomThemeProvider';
import { RtlProvider } from './RtlProvider';
export const AppProviders: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
return (
<I18nextProvider i18n={i18n}>
<RtlProvider>
<CustomThemeProvider>{children}</CustomThemeProvider>
</RtlProvider>
</I18nextProvider>
);
};

View File

@@ -0,0 +1,38 @@
import React, { useMemo } from 'react';
import { createTheme, ThemeProvider } from '@mui/material';
import { darkPalette, lightPalette } from '@/theme/palette';
import { useTranslation } from 'react-i18next';
import { typography } from '@/theme/typography';
export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { i18n } = useTranslation();
const theme = useMemo(() => {
const direction = i18n.dir(i18n.language);
return createTheme({
direction: direction,
colorSchemes: {
light: {
palette: lightPalette,
},
dark: {
palette: darkPalette,
},
},
cssVariables: {
colorSchemeSelector: 'class',
},
spacing: 8,
typography: typography,
});
}, [i18n]);
return (
<ThemeProvider theme={theme} defaultMode="system">
{children}
</ThemeProvider>
);
};

View File

@@ -0,0 +1,25 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import rtlPlugin from 'stylis-plugin-rtl';
// This provider configures Emotion's cache to support RTL.
export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { i18n } = useTranslation();
const [cache, setCache] = useState(createCache({ key: 'css' }));
useEffect(() => {
const newDir = i18n.dir(i18n.language);
const newCache = createCache({
key: 'css',
stylisPlugins: newDir === 'rtl' ? [rtlPlugin] : [],
});
setCache(newCache);
}, [i18n, i18n.language]);
return <CacheProvider value={cache}>{children}</CacheProvider>;
};

46
src/theme/color.type.ts Normal file
View File

@@ -0,0 +1,46 @@
import type {
PaletteColor,
TypeAction,
TypeBackground,
TypeText,
} from '@mui/material';
import type { PaletteColorOptions } from 'node_modules/@mui/material';
export interface Palette {
primary: {
dark: PaletteColorOptions;
light: PaletteColorOptions;
};
secondary: {
dark: PaletteColor;
light: PaletteColor;
};
action: {
dark: Partial<TypeAction>;
light: Partial<TypeAction>;
};
error: {
dark: PaletteColor;
light: PaletteColor;
};
warning: {
dark: PaletteColor;
light: PaletteColor;
};
info: {
dark: PaletteColor;
light: PaletteColor;
};
success: {
dark: PaletteColor;
light: PaletteColor;
};
background: {
dark: Partial<TypeBackground>;
light: Partial<TypeBackground>;
};
text: {
dark: Partial<TypeText>;
light: Partial<TypeText>;
};
}

188
src/theme/colorTmp.ts Normal file
View File

@@ -0,0 +1,188 @@
export const PALETTE = {
primary: {
light: {
main: '#212121',
dark: '#000000',
light: '#616161',
contrastText: '#FFFFFF',
hover: '#21212104',
selected: '#21212108',
focus: '#21212112',
focusVisible: '#21212130',
outlinedBorder: '#21212150',
},
// TODO
dark: {
main: '#64b5f6',
light: '#90caf9',
dark: '#42a5f5',
contrastText: 'rgba(0, 0, 0, 0.87)',
},
},
secondary: {
light: {
main: '#FF5722',
dark: '#E64A19',
light: '#FF8A65',
contrastText: '#FFFFFF',
hover: '#FF572204',
selected: '#FF572208',
focus: '#FF572212',
focusVisible: '#FF572230',
outlinedBorder: '#FF572250',
},
// TODO
dark: {
main: '#ce93d8',
light: '#f3e5f5',
dark: '#ab47bc',
contrastText: 'rgba(0, 0, 0, 0.87)',
},
},
action: {
light: {
action: '#00000056',
hover: '#00000004',
selected: '#00000008',
focus: '#00000012',
disabled: '#00000038',
disabledBackground: '#00000012',
},
// TODO
dark: {
action: '#FFFFFF56',
hover: '#FFFFFF04',
selected: '#FFFFFF08',
focus: '#FFFFFF12',
disabled: '#FFFFFF38',
disabledBackground: '#FFFFFF12',
},
},
error: {
light: {
main: '#E53935',
dark: '#C62828',
light: '#EF5350',
contrast: '#FFFFFF',
hover: '#D32F2F04',
selected: '#D32F2F08',
focusVisible: '#D32F2F30',
outlinedBorder: '#D32F2F50',
},
// TODO
dark: {
main: '#E53935',
dark: '#C62828',
light: '#EF5350',
contrast: '#FFFFFF',
hover: '#D32F2F04',
selected: '#D32F2F08',
focusVisible: '#D32F2F30',
outlinedBorder: '#D32F2F50',
},
},
warning: {
light: {
main: '#EF6C00',
dark: '#E65100',
light: '#FF9800',
contrast: '#FFFFFF',
hover: '#EF6C0004',
selected: '#EF6C0008',
focusVisible: '#EF6C0030',
outlinedBorder: '#EF6C0050',
},
// TODO
dark: {
main: '#EF6C00',
dark: '#E65100',
light: '#FF9800',
contrast: '#FFFFFF',
hover: '#EF6C0004',
selected: '#EF6C0008',
focusVisible: '#EF6C0030',
outlinedBorder: '#EF6C0050',
},
},
info: {
light: {
main: '#29B6F6',
dark: '#0288D1',
light: '#4FC3F7',
contrast: '#FFFFFF',
hover: '#0288D104',
selected: '#0288D108',
focusVisible: '#0288D130',
outlinedBorder: '#0288D150',
},
// TODO
dark: {
main: '#29B6F6',
dark: '#0288D1',
light: '#4FC3F7',
contrast: '#FFFFFF',
hover: '#0288D104',
selected: '#0288D108',
focusVisible: '#0288D130',
outlinedBorder: '#0288D150',
},
},
success: {
light: {
main: '#43A047',
dark: '#1B5E20',
light: '#81C784',
contrast: '#FFFFFF',
hover: '#2E7D3204',
selected: '#2E7D3208',
focusVisible: '#2E7D3230',
outlinedBorder: '#2E7D3250',
},
// TODO
dark: {
main: '#43A047',
dark: '#1B5E20',
light: '#81C784',
contrast: '#FFFFFF',
hover: '#2E7D3204',
selected: '#2E7D3208',
focusVisible: '#2E7D3230',
outlinedBorder: '#2E7D3250',
},
},
// Background colors
background: {
light: {
default: '#FAFAFA',
},
dark: {
default: '#121212',
},
},
// Text colors
text: {
light: {
primary: '#000000',
secondary: '#424242',
tertiary: '#757575',
disabled: '#00000038',
hover: '#00000004',
selected: '#00000008',
divider: '#E0E0E0',
focus: '#00000012',
focusVisible: '#00000030',
},
// TODO
dark: {
primary: '#FFFFFF',
secondary: '#424242',
tertiary: '#757575',
disabled: '#FFFFFF38',
hover: '#FFFFFF04',
selected: '#FFFFFF08',
divider: '#E0E0E0',
focus: '#FFFFFF12',
focusVisible: '#FFFFFF30',
},
},
};

134
src/theme/colors.ts Normal file
View File

@@ -0,0 +1,134 @@
import type { Palette } from './color.type';
export const PALETTE: Palette = {
primary: {
light: {
main: '#212121',
dark: '#000000',
light: '#616161',
contrastText: '#FFFFFF',
},
// TODO
dark: {
main: '#64b5f6',
light: '#90caf9',
dark: '#42a5f5',
contrastText: 'rgba(0, 0, 0, 0.87)',
},
},
secondary: {
light: {
main: '#FF5722',
dark: '#E64A19',
light: '#FF8A65',
contrastText: '#FFFFFF',
},
// TODO
dark: {
main: '#ce93d8',
light: '#f3e5f5',
dark: '#ab47bc',
contrastText: 'rgba(0, 0, 0, 0.87)',
},
},
action: {
light: {
hover: '#00000004',
selected: '#00000008',
focus: '#00000012',
disabled: '#00000038',
disabledBackground: '#00000012',
},
// TODO
dark: {
hover: '#FFFFFF04',
selected: '#FFFFFF08',
focus: '#FFFFFF12',
disabled: '#FFFFFF38',
disabledBackground: '#FFFFFF12',
},
},
error: {
light: {
main: '#E53935',
dark: '#C62828',
light: '#EF5350',
contrastText: '#FFFFFF',
},
// TODO
dark: {
main: '#E53935',
dark: '#C62828',
light: '#EF5350',
contrastText: '#FFFFFF',
},
},
warning: {
light: {
main: '#EF6C00',
dark: '#E65100',
light: '#FF9800',
contrastText: '#FFFFFF',
},
// TODO
dark: {
main: '#EF6C00',
dark: '#E65100',
light: '#FF9800',
contrastText: '#FFFFFF',
},
},
info: {
light: {
main: '#29B6F6',
dark: '#0288D1',
light: '#4FC3F7',
contrastText: '#FFFFFF',
},
// TODO
dark: {
main: '#29B6F6',
dark: '#0288D1',
light: '#4FC3F7',
contrastText: '#FFFFFF',
},
},
success: {
light: {
main: '#43A047',
dark: '#1B5E20',
light: '#81C784',
contrastText: '#FFFFFF',
},
// TODO
dark: {
main: '#43A047',
dark: '#1B5E20',
light: '#81C784',
contrastText: '#FFFFFF',
},
},
// Background colors
background: {
light: {
default: '#FAFAFA',
},
dark: {
default: '#121212',
},
},
// Text colors
text: {
light: {
primary: '#000000',
secondary: '#424242',
disabled: '#00000038',
},
// TODO
dark: {
primary: '#FFFFFF',
secondary: '#424242',
disabled: '#FFFFFF38',
},
},
};

28
src/theme/palette.ts Normal file
View File

@@ -0,0 +1,28 @@
import { type PaletteOptions } from '@mui/material/styles';
import { PALETTE } from './colors';
export const lightPalette: PaletteOptions = {
mode: 'light',
primary: PALETTE.primary.light,
secondary: PALETTE.secondary.light,
action: PALETTE.action.light,
error: PALETTE.error.light,
success: PALETTE.success.light,
warning: PALETTE.warning.light,
info: PALETTE.info.light,
background: PALETTE.background.light,
text: PALETTE.text.light,
};
export const darkPalette: PaletteOptions = {
mode: 'dark',
primary: PALETTE.primary.dark,
secondary: PALETTE.secondary.dark,
action: PALETTE.action.dark,
error: PALETTE.error.dark,
success: PALETTE.success.dark,
warning: PALETTE.warning.dark,
info: PALETTE.info.dark,
background: PALETTE.background.dark,
text: PALETTE.text.dark,
};

65
src/theme/typography.ts Normal file
View File

@@ -0,0 +1,65 @@
// No need for a function, just a static object
export const typography = {
fontFamily: ['Roboto', 'sans-serif'].join(','),
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 700,
h1: {
fontWeight: 700,
fontSize: '6rem', // 96px
lineHeight: 1.17,
},
h2: {
fontWeight: 700,
fontSize: '3.75rem', // 60px
lineHeight: 1.2,
},
h3: {
fontWeight: 700,
fontSize: '3rem', // 48px
lineHeight: 1.17,
},
h4: {
fontWeight: 700,
fontSize: '2.125rem', // 34px
lineHeight: 1.24,
},
h5: {
fontWeight: 700,
fontSize: '1.5rem', // 24px
lineHeight: 1.35,
},
h6: {
fontWeight: 700,
fontSize: '1.25rem', // 20px
lineHeight: 1.6,
},
subtitle1: {
fontSize: '1rem', // 16px
fontWeight: 500,
},
subtitle2: {
fontSize: '.785rem', // 16px
fontWeight: 500,
},
body1: {
fontSize: '1rem', // 16px
fontWeight: 400,
lineHeight: 1.5,
},
body2: {
fontSize: '0.875rem', // 14px
fontWeight: 400,
lineHeight: 1.5,
},
caption: {
fontSize: '0.75rem', // 12px
fontWeight: 400,
lineHeight: 1.5,
},
overline: {
fontSize: '0.75rem', // 12px
fontWeight: 400,
},
};