diff --git a/src/App.jsx b/src/App.jsx index d48f0b4..f4ff02b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,6 +12,7 @@ import { ResourceContextProvider } from "./contexts/ResourceContext"; import { SnackbarProvider, closeSnackbar } from "notistack"; import { IconButton } from "@mui/material"; import Iconify from "./components/Iconify"; +import { ThemeModeContextProvider } from "./contexts/ThemeModeContext"; // ---------------------------------------------------------------------- @@ -19,24 +20,26 @@ export default function App() { return ( - ( - closeSnackbar(snack)} color="inherit"> - - - )} - dense - preventDuplicate - > - - - - - - + + ( + closeSnackbar(snack)} color="inherit"> + + + )} + dense + preventDuplicate + > + + + + + + + ); diff --git a/src/contexts/ThemeModeContext.jsx b/src/contexts/ThemeModeContext.jsx new file mode 100644 index 0000000..8b4bb01 --- /dev/null +++ b/src/contexts/ThemeModeContext.jsx @@ -0,0 +1,29 @@ +import { useMediaQuery } from "@mui/material"; +import { createContext, useState } from "react"; + +const initialState = { + mode: "light", + }; + + export const ThemeModeContext = createContext({ + ...initialState, + }); + + export const ThemeModeContextProvider = ({ children }) => { + const initial = useMediaQuery("(prefers-color-scheme: dark)") + ? "dark" + : "light"; + + const [mode, setMode] = useState(initial); + + const toggleMode = () => { + setMode((prevMode) => (prevMode === "light" ? "dark" : "light")); + }; + + return ( + + {children} + + ); + }; + \ No newline at end of file diff --git a/src/layouts/dashboard/Menu.jsx b/src/layouts/dashboard/Menu.jsx index ffd10fc..e247727 100644 --- a/src/layouts/dashboard/Menu.jsx +++ b/src/layouts/dashboard/Menu.jsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useContext, useRef, useState } from "react"; // keycloak import { useKeycloak } from "@react-keycloak/web"; // @mui @@ -16,6 +16,7 @@ import Link from "@mui/material/Link"; import { Link as RouterLink } from "react-router-dom"; import { useTranslation } from "react-i18next"; import useResource from "src/hooks/useResource"; +import { ThemeModeContext } from "src/contexts/ThemeModeContext"; // ---------------------------------------------------------------------- @@ -23,6 +24,8 @@ export default function Menu() { const anchorRef = useRef(null); const { t } = useTranslation(); + const { mode, toggleMode } = useContext(ThemeModeContext); + const [open, setOpen] = useState(null); const { unread } = useResource(); @@ -199,7 +202,23 @@ export default function Menu() { {t("menu-github")} + + + + Beta + + + + { + toggleMode(); + handleClose(); + }} + > + {t(mode !== "dark" ? "dark-mode" : "light-mode")} + + {shouldRenderAdmin() && ( <> diff --git a/src/locales/en.json b/src/locales/en.json index c74c60c..b8f95a0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -423,6 +423,8 @@ "shared-in-group": "Resource is available through a group you are a member of", "auto-scroll": "Auto scroll", "logs-truncated": "Over 1000 logs: Logs truncated. Please download the full log file to view earlier logs.", - "no-teams": "No teams found, create one or join a friend's team!" + "no-teams": "No teams found, create one or join a friend's team!", + "dark-mode": "Dark mode", + "light-mode": "Light mode" } -} +} \ No newline at end of file diff --git a/src/locales/se.json b/src/locales/se.json index 3ebedc4..e992dfa 100644 --- a/src/locales/se.json +++ b/src/locales/se.json @@ -423,6 +423,8 @@ "shared-in-group": "Resursen är tillgänglig genom en av grupperna du är medlem i", "auto-scroll": "Auto scroll", "logs-truncated": "Över 1000 loggar: Loggar avkortade. Ladda ner hela loggen för att se tidigare loggar.", - "no-teams": "Inga grupper hittades, skapa en eller gå med i en väns grupp!" + "no-teams": "Inga grupper hittades, skapa en eller gå med i en väns grupp!", + "dark-mode": "Mörkt läge", + "light-mode": "Ljust läge" } -} +} \ No newline at end of file diff --git a/src/theme/index.jsx b/src/theme/index.jsx index 3a13cc6..11cd812 100644 --- a/src/theme/index.jsx +++ b/src/theme/index.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { useMemo } from "react"; +import { useContext, useMemo } from "react"; // material import { CssBaseline } from "@mui/material"; import { @@ -8,10 +8,11 @@ import { StyledEngineProvider, } from "@mui/material/styles"; // -import palette from "./palette"; +import { palette, lightPalette } from "./palette"; import typography from "./typography"; import componentsOverride from "./overrides"; import shadows, { customShadows } from "./shadows"; +import { ThemeModeContext } from "src/contexts/ThemeModeContext"; // ---------------------------------------------------------------------- @@ -20,18 +21,18 @@ ThemeProvider.propTypes = { }; export default function ThemeProvider({ children }) { - const themeOptions = useMemo( - () => ({ - palette, - shape: { borderRadius: 8 }, - typography, - shadows, - customShadows, - }), - [] - ); + const { mode } = useContext(ThemeModeContext); + + const getDesignTokens = (mode) => ({ + palette: mode === "light" ? lightPalette : palette, + shape: { borderRadius: 8 }, + typography, + shadows, + customShadows, + }); + + const theme = useMemo(() => createTheme(getDesignTokens(mode)), [mode]); - const theme = createTheme(themeOptions); theme.components = componentsOverride(theme); return ( diff --git a/src/theme/palette.js b/src/theme/palette.js index 74ab3bb..704eb7c 100644 --- a/src/theme/palette.js +++ b/src/theme/palette.js @@ -98,7 +98,7 @@ const CHART_COLORS = { red: ["#FF6C40", "#FF8F6D", "#FFBD98", "#FFF2D4"], }; -const palette = { +export const lightPalette = { common: { black: "#000", white: "#fff" }, primary: { ...PRIMARY }, secondary: { ...SECONDARY }, @@ -124,4 +124,58 @@ const palette = { }, }; -export default palette; +const DARK_GREY = { + 0: "#161C24", + 100: "#212B36", + 200: "#454F5B", + 300: "#637381", + 400: "#919EAB", + 500: "#C4CDD5", + 600: "#DFE3E8", + 700: "#F4F6F8", + 800: "#F9FAFB", + 900: "#FFFFFF", + 500_8: alpha("#919EAB", 0.08), + 500_12: alpha("#919EAB", 0.12), + 500_16: alpha("#919EAB", 0.16), + 500_24: alpha("#919EAB", 0.24), + 500_32: alpha("#919EAB", 0.32), + 500_48: alpha("#919EAB", 0.48), + 500_56: alpha("#919EAB", 0.56), + 500_80: alpha("#919EAB", 0.8), +}; + +export const palette = { + common: { black: "#000", white: "#fff" }, + primary: { ...PRIMARY }, + secondary: { ...SECONDARY }, + info: { ...INFO }, + success: { ...SUCCESS }, + warning: { ...WARNING }, + error: { ...ERROR }, + grey: DARK_GREY, + gradients: GRADIENTS, + chart: CHART_COLORS, + divider: DARK_GREY[500_24], + text: { + primary: DARK_GREY[800], + secondary: DARK_GREY[600], + disabled: DARK_GREY[500], + }, + background: { + paper: DARK_GREY[100], + default: DARK_GREY[0], + neutral: DARK_GREY[200], + }, + action: { + active: DARK_GREY[600], + hover: DARK_GREY[500_8], + selected: DARK_GREY[500_16], + disabled: DARK_GREY[500_80], + disabledBackground: DARK_GREY[500_24], + focus: DARK_GREY[500_24], + hoverOpacity: 0.08, + disabledOpacity: 0.48, + }, + mode: "dark", +}; diff --git a/src/theme/shadows.js b/src/theme/shadows.js index 07af8cf..ea555f4 100644 --- a/src/theme/shadows.js +++ b/src/theme/shadows.js @@ -1,6 +1,6 @@ // material import { alpha } from "@mui/material/styles"; -import palette from "./palette"; +import { lightPalette as palette } from "./palette"; // ----------------------------------------------------------------------