From b42f6c37b9decb8b8a32bf7d74ccb0021aa8ba53 Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Tue, 15 Jul 2025 17:14:38 +0330 Subject: [PATCH] chore(theme): define palette and typographies and override mui styles with this values, also add rtlProvider --- index.html | 16 +++ package-lock.json | 64 ++++++++- package.json | 5 +- public/locales/en/common.json | 4 +- public/locales/fa/common.json | 4 +- src/App.tsx | 21 ++- src/components/LanguageManager.tsx | 8 +- src/config/i18n.ts | 2 +- src/providers/AppProvider.tsx | 10 +- src/providers/CustomThemeProvider.tsx | 38 ++++++ src/providers/RtlProvider.tsx | 25 ++++ src/theme/color.type.ts | 46 +++++++ src/theme/colorTmp.ts | 188 ++++++++++++++++++++++++++ src/theme/colors.ts | 134 ++++++++++++++++++ src/theme/palette.ts | 28 ++++ src/theme/typography.ts | 65 +++++++++ 16 files changed, 642 insertions(+), 16 deletions(-) create mode 100644 src/providers/CustomThemeProvider.tsx create mode 100644 src/providers/RtlProvider.tsx create mode 100644 src/theme/color.type.ts create mode 100644 src/theme/colorTmp.ts create mode 100644 src/theme/colors.ts create mode 100644 src/theme/palette.ts create mode 100644 src/theme/typography.ts diff --git a/index.html b/index.html index be22d1c..6e5d786 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,22 @@ Harmony club + +
diff --git a/package-lock.json b/package-lock.json index 081d470..1204723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f20b020..666890c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index f63d94d..bc4b6ab 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,3 +1,3 @@ { - "helloWorld": "hello world" -} \ No newline at end of file + "helloWorld": "hello world" +} diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index 7a3cc8e..3f4cd0d 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -1,3 +1,3 @@ { - "helloWorld": "سلام دنیا" -} \ No newline at end of file + "helloWorld": "سلام دنیا" +} diff --git a/src/App.tsx b/src/App.tsx index 4f4cadd..0659ade 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() {

{t('helloWorld')}

The main content and router will go here.

+ + + +
); } export default App; + +import { Button } from '@mui/material'; + +export const ThemeToggleButton = () => { + const { mode, setMode } = useColorScheme(); + + return ( + + ); +}; diff --git a/src/components/LanguageManager.tsx b/src/components/LanguageManager.tsx index 52072b1..70db723 100644 --- a/src/components/LanguageManager.tsx +++ b/src/components/LanguageManager.tsx @@ -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 diff --git a/src/config/i18n.ts b/src/config/i18n.ts index c05d74f..1723690 100644 --- a/src/config/i18n.ts +++ b/src/config/i18n.ts @@ -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'], diff --git a/src/providers/AppProvider.tsx b/src/providers/AppProvider.tsx index 195619e..9bea715 100644 --- a/src/providers/AppProvider.tsx +++ b/src/providers/AppProvider.tsx @@ -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 {children}; + return ( + + + {children} + + + ); }; diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx new file mode 100644 index 0000000..29df78d --- /dev/null +++ b/src/providers/CustomThemeProvider.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/src/providers/RtlProvider.tsx b/src/providers/RtlProvider.tsx new file mode 100644 index 0000000..7bbf52a --- /dev/null +++ b/src/providers/RtlProvider.tsx @@ -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 {children}; +}; diff --git a/src/theme/color.type.ts b/src/theme/color.type.ts new file mode 100644 index 0000000..5e9151d --- /dev/null +++ b/src/theme/color.type.ts @@ -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; + light: Partial; + }; + error: { + dark: PaletteColor; + light: PaletteColor; + }; + warning: { + dark: PaletteColor; + light: PaletteColor; + }; + info: { + dark: PaletteColor; + light: PaletteColor; + }; + success: { + dark: PaletteColor; + light: PaletteColor; + }; + background: { + dark: Partial; + light: Partial; + }; + text: { + dark: Partial; + light: Partial; + }; +} diff --git a/src/theme/colorTmp.ts b/src/theme/colorTmp.ts new file mode 100644 index 0000000..de363b1 --- /dev/null +++ b/src/theme/colorTmp.ts @@ -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', + }, + }, +}; diff --git a/src/theme/colors.ts b/src/theme/colors.ts new file mode 100644 index 0000000..58bf0af --- /dev/null +++ b/src/theme/colors.ts @@ -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', + }, + }, +}; diff --git a/src/theme/palette.ts b/src/theme/palette.ts new file mode 100644 index 0000000..c37351e --- /dev/null +++ b/src/theme/palette.ts @@ -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, +}; diff --git a/src/theme/typography.ts b/src/theme/typography.ts new file mode 100644 index 0000000..4088a33 --- /dev/null +++ b/src/theme/typography.ts @@ -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, + }, +};