Web app: add RTL support
Ref: https://mui.com/material-ui/guides/right-to-left https://m2.material.io/design/usability/bidirectionality.htmlpull/804/head
parent
4267c0d9b6
commit
7a1488fcd3
|
@ -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,37 +41,47 @@ 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 />}>
|
||||||
<BrowserRouter>
|
<RTLCacheProvider>
|
||||||
<ThemeProvider theme={theme}>
|
<BrowserRouter>
|
||||||
<AccountContext.Provider value={accountMemo}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<AccountContext.Provider value={accountMemo}>
|
||||||
<ErrorBoundary>
|
<CssBaseline />
|
||||||
<Routes>
|
<ErrorBoundary>
|
||||||
<Route path={routes.login} element={<Login />} />
|
<Routes>
|
||||||
<Route path={routes.signup} element={<Signup />} />
|
<Route path={routes.login} element={<Login />} />
|
||||||
<Route element={<Layout />}>
|
<Route path={routes.signup} element={<Signup />} />
|
||||||
<Route path={routes.app} element={<AllSubscriptions />} />
|
<Route element={<Layout />}>
|
||||||
<Route path={routes.account} element={<Account />} />
|
<Route path={routes.app} element={<AllSubscriptions />} />
|
||||||
<Route path={routes.settings} element={<Preferences />} />
|
<Route path={routes.account} element={<Account />} />
|
||||||
<Route path={routes.subscription} element={<SingleSubscription />} />
|
<Route path={routes.settings} element={<Preferences />} />
|
||||||
<Route path={routes.subscriptionExternal} element={<SingleSubscription />} />
|
<Route path={routes.subscription} element={<SingleSubscription />} />
|
||||||
</Route>
|
<Route path={routes.subscriptionExternal} element={<SingleSubscription />} />
|
||||||
</Routes>
|
</Route>
|
||||||
</ErrorBoundary>
|
</Routes>
|
||||||
</AccountContext.Provider>
|
</ErrorBoundary>
|
||||||
</ThemeProvider>
|
</AccountContext.Provider>
|
||||||
</BrowserRouter>
|
</ThemeProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</RTLCacheProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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…
Reference in New Issue