Web app: add RTL support
Ref: https://mui.com/material-ui/guides/right-to-left https://m2.material.io/design/usability/bidirectionality.html
This commit is contained in:
		
							parent
							
								
									4267c0d9b6
								
							
						
					
					
						commit
						7a1488fcd3
					
				
					 4 changed files with 96 additions and 27 deletions
				
			
		
							
								
								
									
										40
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										40
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -8,6 +8,7 @@ | ||||||
|       "name": "ntfy", |       "name": "ntfy", | ||||||
|       "version": "1.0.0", |       "version": "1.0.0", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|  |         "@emotion/cache": "^11.11.0", | ||||||
|         "@emotion/react": "^11.11.0", |         "@emotion/react": "^11.11.0", | ||||||
|         "@emotion/styled": "^11.11.0", |         "@emotion/styled": "^11.11.0", | ||||||
|         "@mui/icons-material": "^5.4.2", |         "@mui/icons-material": "^5.4.2", | ||||||
|  | @ -25,7 +26,9 @@ | ||||||
|         "react-infinite-scroll-component": "^6.1.0", |         "react-infinite-scroll-component": "^6.1.0", | ||||||
|         "react-router-dom": "^6.2.2", |         "react-router-dom": "^6.2.2", | ||||||
|         "stacktrace-gps": "^3.0.4", |         "stacktrace-gps": "^3.0.4", | ||||||
|         "stacktrace-js": "^2.0.2" |         "stacktrace-js": "^2.0.2", | ||||||
|  |         "stylis": "^4.3.0", | ||||||
|  |         "stylis-plugin-rtl": "^2.1.1" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@vitejs/plugin-react": "^4.0.0", |         "@vitejs/plugin-react": "^4.0.0", | ||||||
|  | @ -1765,6 +1768,11 @@ | ||||||
|         "stylis": "4.2.0" |         "stylis": "4.2.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "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==" | ||||||
|  |     }, | ||||||
|     "node_modules/@emotion/cache": { |     "node_modules/@emotion/cache": { | ||||||
|       "version": "11.11.0", |       "version": "11.11.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", |       "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", | ||||||
|  | @ -1777,6 +1785,11 @@ | ||||||
|         "stylis": "4.2.0" |         "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==" | ||||||
|  |     }, | ||||||
|     "node_modules/@emotion/hash": { |     "node_modules/@emotion/hash": { | ||||||
|       "version": "0.9.1", |       "version": "0.9.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", |       "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", | ||||||
|  | @ -3314,6 +3327,14 @@ | ||||||
|         "node": ">=8" |         "node": ">=8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/cssjanus": { | ||||||
|  |       "version": "2.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.1.0.tgz", | ||||||
|  |       "integrity": "sha512-kAijbny3GmdOi9k+QT6DGIXqFvL96aksNlGr4Rhk9qXDZYWUojU4bRc3IHWxdaLNOqgEZHuXoe5Wl2l7dxLW5g==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=10.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/csstype": { |     "node_modules/csstype": { | ||||||
|       "version": "3.1.2", |       "version": "3.1.2", | ||||||
|       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", |       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", | ||||||
|  | @ -6351,9 +6372,20 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/stylis": { |     "node_modules/stylis": { | ||||||
|       "version": "4.2.0", |       "version": "4.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", |       "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", | ||||||
|       "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" |       "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" | ||||||
|  |     }, | ||||||
|  |     "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==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "cssjanus": "^2.0.1" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "stylis": "4.x" | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/supports-color": { |     "node_modules/supports-color": { | ||||||
|       "version": "5.5.0", |       "version": "5.5.0", | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
|     "lint": "eslint --report-unused-disable-directives --ext .js,.jsx ./src/" |     "lint": "eslint --report-unused-disable-directives --ext .js,.jsx ./src/" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@emotion/cache": "^11.11.0", | ||||||
|     "@emotion/react": "^11.11.0", |     "@emotion/react": "^11.11.0", | ||||||
|     "@emotion/styled": "^11.11.0", |     "@emotion/styled": "^11.11.0", | ||||||
|     "@mui/icons-material": "^5.4.2", |     "@mui/icons-material": "^5.4.2", | ||||||
|  | @ -28,7 +29,9 @@ | ||||||
|     "react-infinite-scroll-component": "^6.1.0", |     "react-infinite-scroll-component": "^6.1.0", | ||||||
|     "react-router-dom": "^6.2.2", |     "react-router-dom": "^6.2.2", | ||||||
|     "stacktrace-gps": "^3.0.4", |     "stacktrace-gps": "^3.0.4", | ||||||
|     "stacktrace-js": "^2.0.2" |     "stacktrace-js": "^2.0.2", | ||||||
|  |     "stylis": "^4.3.0", | ||||||
|  |     "stylis-plugin-rtl": "^2.1.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@vitejs/plugin-react": "^4.0.0", |     "@vitejs/plugin-react": "^4.0.0", | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { createContext, Suspense, useContext, useEffect, useState, useMemo } fro | ||||||
| import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material"; | import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material"; | ||||||
| import { useLiveQuery } from "dexie-react-hooks"; | import { useLiveQuery } from "dexie-react-hooks"; | ||||||
| import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; | import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
| import { AllSubscriptions, SingleSubscription } from "./Notifications"; | import { AllSubscriptions, SingleSubscription } from "./Notifications"; | ||||||
| import { darkTheme, lightTheme } from "./theme"; | import { darkTheme, lightTheme } from "./theme"; | ||||||
| import Navigation from "./Navigation"; | import Navigation from "./Navigation"; | ||||||
|  | @ -21,6 +22,7 @@ import Signup from "./Signup"; | ||||||
| import Account from "./Account"; | import Account from "./Account"; | ||||||
| import "../app/i18n"; // Translations! | import "../app/i18n"; // Translations! | ||||||
| import prefs, { THEME } from "../app/Prefs"; | import prefs, { THEME } from "../app/Prefs"; | ||||||
|  | import RTLCacheProvider from "./RTLCacheProvider"; | ||||||
| 
 | 
 | ||||||
| export const AccountContext = createContext(null); | export const AccountContext = createContext(null); | ||||||
| 
 | 
 | ||||||
|  | @ -39,17 +41,26 @@ const darkModeEnabled = (prefersDarkMode, themePreference) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const App = () => { | const App = () => { | ||||||
|  |   const { i18n } = useTranslation(); | ||||||
|  |   const languageDir = i18n.dir(); | ||||||
|  | 
 | ||||||
|   const [account, setAccount] = useState(null); |   const [account, setAccount] = useState(null); | ||||||
|   const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]); |   const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]); | ||||||
|   const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); |   const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); | ||||||
|   const themePreference = useLiveQuery(() => prefs.theme()); |   const themePreference = useLiveQuery(() => prefs.theme()); | ||||||
|   const theme = React.useMemo( |   const theme = React.useMemo( | ||||||
|     () => createTheme(darkModeEnabled(prefersDarkMode, themePreference) ? darkTheme : lightTheme), |     () => createTheme({ ...(darkModeEnabled(prefersDarkMode, themePreference) ? darkTheme : lightTheme), direction: languageDir }), | ||||||
|     [prefersDarkMode, themePreference] |     [prefersDarkMode, themePreference, languageDir] | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     document.documentElement.setAttribute("lang", i18n.language); | ||||||
|  |     document.dir = languageDir; | ||||||
|  |   }, [i18n.language, languageDir]); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Suspense fallback={<Loader />}> |     <Suspense fallback={<Loader />}> | ||||||
|  |       <RTLCacheProvider> | ||||||
|         <BrowserRouter> |         <BrowserRouter> | ||||||
|           <ThemeProvider theme={theme}> |           <ThemeProvider theme={theme}> | ||||||
|             <AccountContext.Provider value={accountMemo}> |             <AccountContext.Provider value={accountMemo}> | ||||||
|  | @ -70,6 +81,7 @@ const App = () => { | ||||||
|             </AccountContext.Provider> |             </AccountContext.Provider> | ||||||
|           </ThemeProvider> |           </ThemeProvider> | ||||||
|         </BrowserRouter> |         </BrowserRouter> | ||||||
|  |       </RTLCacheProvider> | ||||||
|     </Suspense> |     </Suspense> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								web/src/components/RTLCacheProvider.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/src/components/RTLCacheProvider.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | import React from "react"; | ||||||
|  | 
 | ||||||
|  | import rtlPlugin from "stylis-plugin-rtl"; | ||||||
|  | import { CacheProvider } from "@emotion/react"; | ||||||
|  | import createCache from "@emotion/cache"; | ||||||
|  | import { prefixer } from "stylis"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  | 
 | ||||||
|  | // https://mui.com/material-ui/guides/right-to-left | ||||||
|  | 
 | ||||||
|  | const cacheRtl = createCache({ | ||||||
|  |   key: "muirtl", | ||||||
|  |   stylisPlugins: [prefixer, rtlPlugin], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const RTLCacheProvider = ({ children }) => { | ||||||
|  |   const { i18n } = useTranslation(); | ||||||
|  | 
 | ||||||
|  |   return i18n.dir() === "rtl" ? <CacheProvider value={cacheRtl}>{children}</CacheProvider> : children; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default RTLCacheProvider; | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue