diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index 977bf161..8b1931db 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -33,6 +33,7 @@ import { IconButton, MenuItem, DialogContentText, + useTheme, } from "@mui/material"; import EditIcon from "@mui/icons-material/Edit"; import { Trans, useTranslation } from "react-i18next"; @@ -55,7 +56,6 @@ import DialogFooter from "./DialogFooter"; import { Paragraph } from "./styles"; import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; import { ProChip } from "./SubscriptionPopup"; -import theme from "./theme"; import session from "../app/Session"; const Account = () => { @@ -147,6 +147,7 @@ const ChangePassword = () => { }; const ChangePasswordDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [error, setError] = useState(""); const [currentPassword, setCurrentPassword] = useState(""); @@ -430,6 +431,7 @@ const PhoneNumbers = () => { }; const AddPhoneNumberDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [error, setError] = useState(""); const [phoneNumber, setPhoneNumber] = useState(""); @@ -928,6 +930,7 @@ const TokensTable = (props) => { }; const TokenDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [error, setError] = useState(""); const [label, setLabel] = useState(props.token?.label || ""); @@ -1069,6 +1072,7 @@ const DeleteAccount = () => { }; const DeleteAccountDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const { account } = useContext(AccountContext); const [error, setError] = useState(""); diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index 9b939ea5..9c5e8f79 100644 --- a/web/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -1,11 +1,11 @@ import * as React from "react"; import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react"; -import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress } from "@mui/material"; -import { ThemeProvider } from "@mui/material/styles"; +import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery } from "@mui/material"; +import { ThemeProvider, createTheme } from "@mui/material/styles"; import { useLiveQuery } from "dexie-react-hooks"; import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; import { AllSubscriptions, SingleSubscription } from "./Notifications"; -import theme from "./theme"; +import themeOptions, { darkPalette, lightPalette } from "./theme"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; import notifier from "../app/Notifier"; @@ -29,6 +29,19 @@ const App = () => { const [account, setAccount] = useState(null); const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]); + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + + const theme = React.useMemo( + () => + createTheme({ + ...themeOptions, + palette: { + ...(prefersDarkMode ? darkPalette : lightPalette), + }, + }), + [prefersDarkMode] + ); + return ( }> diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index 6de67d7c..0911d69e 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -26,6 +26,7 @@ import { DialogTitle, DialogContent, DialogActions, + useTheme, } from "@mui/material"; import EditIcon from "@mui/icons-material/Edit"; import CloseIcon from "@mui/icons-material/Close"; @@ -34,7 +35,6 @@ import { useLiveQuery } from "dexie-react-hooks"; import { useTranslation } from "react-i18next"; import { Info } from "@mui/icons-material"; import { useOutletContext } from "react-router-dom"; -import theme from "./theme"; import userManager from "../app/UserManager"; import { playSound, shortUrl, shuffle, sounds, validUrl } from "../app/utils"; import session from "../app/Session"; @@ -400,6 +400,7 @@ const UserTable = (props) => { }; const UserDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [baseUrl, setBaseUrl] = useState(""); const [username, setUsername] = useState(""); diff --git a/web/src/components/PublishDialog.jsx b/web/src/components/PublishDialog.jsx index 0929a5e9..6cea1a9c 100644 --- a/web/src/components/PublishDialog.jsx +++ b/web/src/components/PublishDialog.jsx @@ -19,6 +19,7 @@ import { IconButton, MenuItem, Box, + useTheme, } from "@mui/material"; import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; import { Close } from "@mui/icons-material"; @@ -34,7 +35,6 @@ import DialogFooter from "./DialogFooter"; import api from "../app/Api"; import userManager from "../app/UserManager"; import EmojiPicker from "./EmojiPicker"; -import theme from "./theme"; import session from "../app/Session"; import routes from "./routes"; import accountApi from "../app/AccountApi"; @@ -42,6 +42,7 @@ import { UnauthorizedError } from "../app/errors"; import { AccountContext } from "./App"; const PublishDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const { account } = useContext(AccountContext); const [baseUrl, setBaseUrl] = useState(""); @@ -806,6 +807,7 @@ const AttachmentBox = (props) => { }; const ExpandingTextField = (props) => { + const theme = useTheme(); const invisibleFieldRef = useRef(); const [textWidth, setTextWidth] = useState(props.minWidth); const determineTextWidth = () => { diff --git a/web/src/components/ReserveDialogs.jsx b/web/src/components/ReserveDialogs.jsx index e413657a..7eb893cd 100644 --- a/web/src/components/ReserveDialogs.jsx +++ b/web/src/components/ReserveDialogs.jsx @@ -14,10 +14,10 @@ import { MenuItem, ListItemIcon, ListItemText, + useTheme, } from "@mui/material"; import { useTranslation } from "react-i18next"; import { Check, DeleteForever } from "@mui/icons-material"; -import theme from "./theme"; import { validTopic } from "../app/utils"; import DialogFooter from "./DialogFooter"; import session from "../app/Session"; @@ -27,6 +27,7 @@ import ReserveTopicSelect from "./ReserveTopicSelect"; import { TopicReservedError, UnauthorizedError } from "../app/errors"; export const ReserveAddDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [error, setError] = useState(""); const [topic, setTopic] = useState(props.topic || ""); @@ -87,6 +88,7 @@ export const ReserveAddDialog = (props) => { }; export const ReserveEditDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [error, setError] = useState(""); const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL); @@ -124,6 +126,7 @@ export const ReserveEditDialog = (props) => { }; export const ReserveDeleteDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const [error, setError] = useState(""); const [deleteMessages, setDeleteMessages] = useState(false); diff --git a/web/src/components/SubscribeDialog.jsx b/web/src/components/SubscribeDialog.jsx index 09879e33..f7a24f5e 100644 --- a/web/src/components/SubscribeDialog.jsx +++ b/web/src/components/SubscribeDialog.jsx @@ -12,10 +12,10 @@ import { FormGroup, useMediaQuery, Switch, + useTheme, } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useLiveQuery } from "dexie-react-hooks"; -import theme from "./theme"; import api from "../app/Api"; import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils"; import userManager from "../app/UserManager"; @@ -49,6 +49,7 @@ export const subscribeTopic = async (baseUrl, topic, opts) => { }; const SubscribeDialog = (props) => { + const theme = useTheme(); const [baseUrl, setBaseUrl] = useState(""); const [topic, setTopic] = useState(""); const [showLoginPage, setShowLoginPage] = useState(false); diff --git a/web/src/components/SubscriptionPopup.jsx b/web/src/components/SubscriptionPopup.jsx index 24ce9cbc..17b12504 100644 --- a/web/src/components/SubscriptionPopup.jsx +++ b/web/src/components/SubscriptionPopup.jsx @@ -15,6 +15,7 @@ import { MenuItem, IconButton, ListItemIcon, + useTheme, } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -30,7 +31,6 @@ import { RemoveCircle, Send, } from "@mui/icons-material"; -import theme from "./theme"; import subscriptionManager from "../app/SubscriptionManager"; import DialogFooter from "./DialogFooter"; import accountApi, { Role } from "../app/AccountApi"; @@ -281,6 +281,7 @@ export const SubscriptionPopup = (props) => { }; const DisplayNameDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const { subscription } = props; const [error, setError] = useState(""); diff --git a/web/src/components/UpgradeDialog.jsx b/web/src/components/UpgradeDialog.jsx index 6d569fa2..4bf0244d 100644 --- a/web/src/components/UpgradeDialog.jsx +++ b/web/src/components/UpgradeDialog.jsx @@ -21,6 +21,7 @@ import { Box, DialogContentText, DialogActions, + useTheme, } from "@mui/material"; import { Trans, useTranslation } from "react-i18next"; import { Check, Close } from "@mui/icons-material"; @@ -31,7 +32,6 @@ import { AccountContext } from "./App"; import routes from "./routes"; import session from "../app/Session"; import accountApi, { SubscriptionInterval } from "../app/AccountApi"; -import theme from "./theme"; const Feature = (props) => {props.children}; @@ -61,6 +61,7 @@ const Banner = { }; const UpgradeDialog = (props) => { + const theme = useTheme(); const { t } = useTranslation(); const { account } = useContext(AccountContext); // May be undefined! const [error, setError] = useState(""); diff --git a/web/src/components/styles.js b/web/src/components/styles.js index edcfb46e..db0690bc 100644 --- a/web/src/components/styles.js +++ b/web/src/components/styles.js @@ -1,19 +1,18 @@ import { Typography, Container, Backdrop, styled } from "@mui/material"; -import theme from "./theme"; export const Paragraph = styled(Typography)({ paddingTop: 8, paddingBottom: 8, }); -export const VerticallyCenteredContainer = styled(Container)({ +export const VerticallyCenteredContainer = styled(Container)(({ theme }) => ({ display: "flex", flexGrow: 1, flexDirection: "column", justifyContent: "center", alignContent: "center", color: theme.palette.text.primary, -}); +})); export const LightboxBackdrop = styled(Backdrop)({ backgroundColor: "rgba(0, 0, 0, 0.8)", // was: 0.5 diff --git a/web/src/components/theme.js b/web/src/components/theme.js index ca77cdc8..c833e631 100644 --- a/web/src/components/theme.js +++ b/web/src/components/theme.js @@ -1,18 +1,7 @@ -import { red } from "@mui/material/colors"; -import { createTheme } from "@mui/material/styles"; +import { grey, red } from "@mui/material/colors"; -const theme = createTheme({ - palette: { - primary: { - main: "#338574", - }, - secondary: { - main: "#6cead0", - }, - error: { - main: red.A400, - }, - }, +/** @type {import("@mui/material").ThemeOptions} */ +const themeOptions = { components: { MuiListItemIcon: { styleOverrides: { @@ -31,6 +20,32 @@ const theme = createTheme({ }, }, }, -}); +}; -export default theme; +/** @type {import("@mui/material").ThemeOptions['palette']} */ +export const lightPalette = { + mode: "light", + primary: { + main: "#338574", + }, + secondary: { + main: "#6cead0", + }, + error: { + main: red.A400, + }, +}; + +/** @type {import("@mui/material").ThemeOptions['palette']} */ +export const darkPalette = { + ...lightPalette, + mode: "dark", + background: { + paper: grey["800"], + }, + primary: { + main: "#6cead0", + }, +}; + +export default themeOptions;