diff --git a/frontend/public/gpu-mode-logo/white-cropped.svg b/frontend/public/gpu-mode-logo/white-cropped.svg
new file mode 100644
index 00000000..e0b66872
--- /dev/null
+++ b/frontend/public/gpu-mode-logo/white-cropped.svg
@@ -0,0 +1,10 @@
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 6d0cbdfa..031e31ab 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,7 +4,7 @@ import "./App.css";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import AppLayout from "./components/app-layout/AppLayout";
import { CssBaseline, ThemeProvider } from "@mui/material";
-import { appTheme } from "./components/common/styles/theme";
+import { createAppTheme } from "./components/common/styles/theme";
import Leaderboard from "./pages/leaderboard/Leaderboard";
import Home from "./pages/home/Home";
import News from "./pages/news/News";
@@ -13,7 +13,8 @@ import Lectures from "./pages/lectures/Lectures";
import ErrorPage from "./pages/Error";
import Login from "./pages/login/login";
import { useAuthStore } from "./lib/store/authStore";
-import { useEffect } from "react";
+import { useThemeStore } from "./lib/store/themeStore";
+import { useEffect, useMemo } from "react";
const errorRoutes = [
{
@@ -32,7 +33,7 @@ const errorRoutes = [
path: "*",
code: 404,
title: "Page Not Found",
- description: "The page you’re looking for doesn’t exist.",
+ description: "The page you're looking for doesn't exist.",
},
];
@@ -43,8 +44,23 @@ function App() {
fetchMe();
}, [fetchMe]);
+ const resolvedMode = useThemeStore((s) => s.resolvedMode);
+ const mode = useThemeStore((s) => s.mode);
+ const setMode = useThemeStore((s) => s.setMode);
+
+ const theme = useMemo(() => createAppTheme(resolvedMode), [resolvedMode]);
+
+ // Listen for OS-level dark mode changes when mode is "system"
+ useEffect(() => {
+ if (mode !== "system") return;
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ const handler = () => setMode("system");
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }, [mode, setMode]);
+
return (
-
+
diff --git a/frontend/src/components/app-layout/Footer.tsx b/frontend/src/components/app-layout/Footer.tsx
index ba85fc6f..a9100169 100644
--- a/frontend/src/components/app-layout/Footer.tsx
+++ b/frontend/src/components/app-layout/Footer.tsx
@@ -11,7 +11,7 @@ export const FooterLinkContainer = styled(Container)(({ theme }) => ({
}));
export const FooterBox = styled(Box)(({ theme }) => ({
- borderTop: "1px solid #ddd",
+ borderTop: `1px solid ${theme.palette.divider}`,
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
textAlign: "center",
diff --git a/frontend/src/components/app-layout/NavBar.tsx b/frontend/src/components/app-layout/NavBar.tsx
index df4c2ebc..3579144e 100644
--- a/frontend/src/components/app-layout/NavBar.tsx
+++ b/frontend/src/components/app-layout/NavBar.tsx
@@ -1,6 +1,10 @@
// components/NavBar.tsx
-import { AppBar, Toolbar, Link, Box } from "@mui/material";
+import { AppBar, Toolbar, Link, Box, IconButton, Tooltip } from "@mui/material";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
+import LightModeIcon from "@mui/icons-material/LightMode";
+import DarkModeIcon from "@mui/icons-material/DarkMode";
+import SettingsBrightnessIcon from "@mui/icons-material/SettingsBrightness";
+import { useTheme } from "@mui/material/styles";
import {
flexRowCenter,
flexRowCenterMediumGap,
@@ -9,6 +13,7 @@ import {
import { appBarStyle, brandStyle } from "./styles";
import { ConstrainedContainer } from "./ConstrainedContainer";
import NavUserProfile from "./NavUserProfile";
+import { useThemeStore } from "../../lib/store/themeStore";
export interface NavLink {
label: string;
@@ -17,6 +22,37 @@ export interface NavLink {
}
export default function NavBar() {
+ const mode = useThemeStore((s) => s.mode);
+ const setMode = useThemeStore((s) => s.setMode);
+ const theme = useTheme();
+ const isDark = theme.palette.mode === "dark";
+
+ const logoSrc = isDark
+ ? "/gpu-mode-logo/white-cropped.svg"
+ : "/gpu-mode-logo/black-cropped.svg";
+
+ const cycleMode = () => {
+ const next =
+ mode === "light" ? "dark" : mode === "dark" ? "system" : "light";
+ setMode(next);
+ };
+
+ const modeIcon =
+ mode === "light" ? (
+
+ ) : mode === "dark" ? (
+
+ ) : (
+
+ );
+
+ const modeLabel =
+ mode === "light"
+ ? "Switch to dark mode"
+ : mode === "dark"
+ ? "Switch to system preference"
+ : "Switch to light mode";
+
const links: NavLink[] = [
{ label: "News", href: "/news" },
{ label: "Lectures", href: "/lectures" },
@@ -34,7 +70,7 @@ export default function NavBar() {
))}
-
+
+
+
+
+ {modeIcon}
+
+
diff --git a/frontend/src/components/app-layout/styles.tsx b/frontend/src/components/app-layout/styles.tsx
index d37dd62e..3d60f257 100644
--- a/frontend/src/components/app-layout/styles.tsx
+++ b/frontend/src/components/app-layout/styles.tsx
@@ -2,10 +2,11 @@ import type { SxProps, Theme } from "@mui/material";
import { flexRowCenter, mediumText } from "../common/styles/shared_style";
export const appBarStyle: SxProps = {
- backgroundColor: "white",
- color: "black",
+ backgroundColor: "background.paper",
+ color: "text.primary",
boxShadow: "none",
- borderBottom: "1px solid #ddd",
+ borderBottom: 1,
+ borderColor: "divider",
width: "100%",
maxWidth: "100vw",
};
diff --git a/frontend/src/components/codeblock/CodeBlock.tsx b/frontend/src/components/codeblock/CodeBlock.tsx
index 3dac3365..3a0b3c8e 100644
--- a/frontend/src/components/codeblock/CodeBlock.tsx
+++ b/frontend/src/components/codeblock/CodeBlock.tsx
@@ -3,6 +3,7 @@ import { Box, IconButton, Tooltip, useTheme } from "@mui/material";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
+import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
interface CodeBlockProps {
code: string;
@@ -29,6 +30,7 @@ const styles = {
export default function CodeBlock({ code }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const theme = useTheme();
+ const syntaxTheme = theme.palette.mode === "dark" ? oneDark : oneLight;
const handleCopy = () => {
navigator.clipboard.writeText(code).then(() => {
@@ -67,11 +69,14 @@ export default function CodeBlock({ code }: CodeBlockProps) {
fontFamily: "monospace !important",
background: "transparent !important",
},
+ "& code": {
+ background: "transparent !important",
+ },
}}
>
({
styleOverrides: {
root: {
- border: `1px solid ${colorPalette.neutral}`,
+ border: `1px solid ${mode === "dark" ? "#333" : colorPalette.neutral}`,
boxShadow: "none",
borderRadius: "12px",
padding: "16px",
},
},
-};
+});
-const MuiButtonGlobalStyle: Components["MuiButton"] = {
+const getMuiButtonGlobalStyle = (
+ mode: PaletteMode,
+): Components["MuiButton"] => ({
defaultProps: {
variant: "contained",
disableElevation: true,
},
styleOverrides: {
root: {
- backgroundColor: "#e6f0ff",
- color: "#333",
+ backgroundColor: mode === "dark" ? "#23272f" : "#e6f0ff",
+ color: mode === "dark" ? "#e0e0e0" : "#333",
fontWeight: 500,
fontSize: "0.9rem",
textTransform: "none",
borderRadius: "13px",
padding: "1px 5px",
boxShadow: "none",
- border: "1px solid #ddd",
+ border: `1px solid ${mode === "dark" ? "#444" : "#ddd"}`,
"&:hover": {
- backgroundColor: "#d0e4ff",
+ backgroundColor: mode === "dark" ? "#2d3340" : "#d0e4ff",
},
"&:active": {
- backgroundColor: "#b3d4ff",
+ backgroundColor: mode === "dark" ? "#3a4050" : "#b3d4ff",
},
},
},
-};
+});
-export const appTheme = createTheme({
- palette: {
- primary: {
- main: colorPalette.primary,
- },
- secondary: {
- main: colorPalette.secondary,
- },
- custom: colorPalette,
- },
- typography: {
- fontFamily:
- 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
- fontSize: 14,
- h1: {
- fontSize: "1.875rem",
- lineHeight: "2.25rem",
- fontWeight: 700,
- marginTop: "1rem",
+export function createAppTheme(mode: PaletteMode) {
+ return createTheme({
+ palette: {
+ mode,
+ primary: {
+ main: colorPalette.primary,
+ },
+ secondary: {
+ main: colorPalette.secondary,
+ },
+ custom: colorPalette,
+ ...(mode === "dark" && {
+ background: {
+ default: "#0f1117",
+ paper: "#181a20",
+ },
+ }),
},
- h2: {
- fontSize: "1.5rem",
- lineHeight: "2rem",
- fontWeight: 700,
- marginTop: "1rem",
+ typography: {
+ fontFamily:
+ 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
+ fontSize: 14,
+ h1: {
+ fontSize: "1.875rem",
+ lineHeight: "2.25rem",
+ fontWeight: 700,
+ marginTop: "1rem",
+ },
+ h2: {
+ fontSize: "1.5rem",
+ lineHeight: "2rem",
+ fontWeight: 700,
+ marginTop: "1rem",
+ },
+ h3: {
+ fontSize: "1.25rem",
+ lineHeight: "1.75rem",
+ fontWeight: 700,
+ marginTop: "1rem",
+ },
},
- h3: {
- fontSize: "1.25rem",
- lineHeight: "1.75rem",
- fontWeight: 700,
- marginTop: "1rem",
+ components: {
+ MuiCard: getMuiCardGlobalStyle(mode),
+ MuiButton: getMuiButtonGlobalStyle(mode),
},
- },
- components: {
- MuiCard: MuiCardGlobalStyle,
- MuiButton: MuiButtonGlobalStyle,
- },
-});
+ });
+}
+
+export const appTheme = createAppTheme("light");
diff --git a/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx b/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx
index a32513d9..fadeb6e6 100644
--- a/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx
+++ b/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
+import { useTheme } from "@mui/material/styles";
type MarkdownRendererProps = {
content: string;
@@ -44,11 +45,18 @@ const MarkdownRenderer: React.FC = ({
}) => {
const mergedImageProps = { ...defaultImageProps, ...imageProps };
const { align, ...styleProps } = mergedImageProps;
+ const theme = useTheme();
return (
(
+
+ ),
figure: ({ node, ...props }) => (
),
diff --git a/frontend/src/lib/store/themeStore.ts b/frontend/src/lib/store/themeStore.ts
new file mode 100644
index 00000000..af66bab9
--- /dev/null
+++ b/frontend/src/lib/store/themeStore.ts
@@ -0,0 +1,42 @@
+import { create } from "zustand";
+
+type ThemeMode = "light" | "dark" | "system";
+
+type ThemeState = {
+ mode: ThemeMode;
+ resolvedMode: "light" | "dark";
+ setMode: (mode: ThemeMode) => void;
+};
+
+function getSystemPreference(): "light" | "dark" {
+ if (typeof window === "undefined") return "light";
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+}
+
+function resolveMode(mode: ThemeMode): "light" | "dark" {
+ return mode === "system" ? getSystemPreference() : mode;
+}
+
+function loadSavedMode(): ThemeMode {
+ try {
+ const saved = localStorage.getItem("theme-mode");
+ if (saved === "light" || saved === "dark" || saved === "system") return saved;
+ } catch {
+ // localStorage unavailable
+ }
+ return "system";
+}
+
+export const useThemeStore = create((set) => {
+ const initial = loadSavedMode();
+ return {
+ mode: initial,
+ resolvedMode: resolveMode(initial),
+ setMode: (mode) => {
+ localStorage.setItem("theme-mode", mode);
+ set({ mode, resolvedMode: resolveMode(mode) });
+ },
+ };
+});
diff --git a/frontend/src/pages/home/components/LeaderboardTile.tsx b/frontend/src/pages/home/components/LeaderboardTile.tsx
index edfd1140..a6a23253 100644
--- a/frontend/src/pages/home/components/LeaderboardTile.tsx
+++ b/frontend/src/pages/home/components/LeaderboardTile.tsx
@@ -25,8 +25,8 @@ const styles = {
},
},
priorityGpuType: {
- backgroundColor: "#f5f5f5",
- color: "#666",
+ backgroundColor: "action.hover",
+ color: "text.secondary",
fontSize: "0.9rem",
},
topUsersList: {
diff --git a/frontend/src/pages/leaderboard/components/RankingLists.tsx b/frontend/src/pages/leaderboard/components/RankingLists.tsx
index ac2fa20c..fbc7d1e1 100644
--- a/frontend/src/pages/leaderboard/components/RankingLists.tsx
+++ b/frontend/src/pages/leaderboard/components/RankingLists.tsx
@@ -8,7 +8,6 @@ import {
type Theme,
Typography,
} from "@mui/material";
-import { grey } from "@mui/material/colors";
import RankingTitleBadge from "./RankingTitleBadge";
import { formatMicroseconds } from "../../../lib/utils/ranking.ts";
@@ -35,7 +34,8 @@ const styles: Record> = {
mt: 5,
},
rankingRow: {
- borderBottom: "1px solid #ddd",
+ borderBottom: 1,
+ borderColor: "divider",
},
header: {
display: "flex",
@@ -64,7 +64,7 @@ const styles: Record> = {
minWidth: "100px",
},
delta: {
- color: grey[600],
+ color: "text.secondary",
minWidth: "90px",
},
};
diff --git a/frontend/src/pages/lectures/Lectures.tsx b/frontend/src/pages/lectures/Lectures.tsx
index a41470f0..c4ccca82 100644
--- a/frontend/src/pages/lectures/Lectures.tsx
+++ b/frontend/src/pages/lectures/Lectures.tsx
@@ -1,4 +1,5 @@
import { Box, Typography, Link, Chip } from "@mui/material";
+import type { Theme } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { fetchEvents, DiscordEvent } from "../../api/api";
import Loading from "../../components/common/loading";
@@ -16,15 +17,17 @@ const styles = {
fontSize: "1.5rem",
fontWeight: 600,
marginBottom: "16px",
- borderBottom: "2px solid #eee",
+ borderBottom: "2px solid",
+ borderColor: "divider",
paddingBottom: "8px",
},
card: {
marginBottom: "16px",
padding: "16px",
- backgroundColor: "#f9f9f9",
+ backgroundColor: "action.hover",
borderRadius: "8px",
- border: "1px solid #eee",
+ border: "1px solid",
+ borderColor: "divider",
},
cardTitle: {
fontSize: "1.1rem",
@@ -32,37 +35,40 @@ const styles = {
marginBottom: "4px",
},
cardMeta: {
- color: "#666",
+ color: "text.secondary",
marginBottom: "8px",
fontSize: "0.9rem",
},
cardDescription: {
marginBottom: "8px",
- color: "#333",
+ color: "text.primary",
},
link: {
- color: "#1976d2",
+ color: "primary.main",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
},
archiveBox: {
- backgroundColor: "#e3f2fd",
+ backgroundColor: (theme: Theme) =>
+ theme.palette.mode === "dark" ? "rgba(33, 150, 243, 0.08)" : "#e3f2fd",
padding: "16px",
borderRadius: "8px",
marginTop: "32px",
- border: "1px solid #90caf9",
+ border: "1px solid",
+ borderColor: (theme: Theme) =>
+ theme.palette.mode === "dark" ? "rgba(144, 202, 249, 0.3)" : "#90caf9",
},
intro: {
marginBottom: "24px",
lineHeight: 1.6,
},
noEvents: {
- color: "#666",
+ color: "text.secondary",
fontStyle: "italic",
padding: "16px",
- backgroundColor: "#f5f5f5",
+ backgroundColor: "action.hover",
borderRadius: "8px",
},
chipContainer: {
@@ -251,14 +257,14 @@ export default function Lectures() {
{/* Upcoming Lectures Section */}
Upcoming Lectures
-
+
Lectures are pulled live from our Discord server. For the most up-to-date schedule,
join the{" "}
GPU MODE Discord
diff --git a/frontend/src/pages/working-groups/WorkingGroups.tsx b/frontend/src/pages/working-groups/WorkingGroups.tsx
index 7cb060bc..fcf20afb 100644
--- a/frontend/src/pages/working-groups/WorkingGroups.tsx
+++ b/frontend/src/pages/working-groups/WorkingGroups.tsx
@@ -1,4 +1,5 @@
import { Box, Typography, Link } from "@mui/material";
+import type { Theme } from "@mui/material/styles";
const styles = {
container: {
@@ -13,15 +14,17 @@ const styles = {
fontSize: "1.5rem",
fontWeight: 600,
marginBottom: "16px",
- borderBottom: "2px solid #eee",
+ borderBottom: "2px solid",
+ borderColor: "divider",
paddingBottom: "8px",
},
projectCard: {
marginBottom: "24px",
padding: "16px",
- backgroundColor: "#f9f9f9",
+ backgroundColor: "action.hover",
borderRadius: "8px",
- border: "1px solid #eee",
+ border: "1px solid",
+ borderColor: "divider",
},
projectTitle: {
fontSize: "1.1rem",
@@ -30,10 +33,10 @@ const styles = {
},
projectDescription: {
marginBottom: "8px",
- color: "#333",
+ color: "text.primary",
},
projectLink: {
- color: "#1976d2",
+ color: "primary.main",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
@@ -44,18 +47,24 @@ const styles = {
lineHeight: 1.6,
},
highlight: {
- backgroundColor: "#fff3cd",
+ backgroundColor: (theme: Theme) =>
+ theme.palette.mode === "dark" ? "rgba(255, 193, 7, 0.08)" : "#fff3cd",
padding: "12px 16px",
borderRadius: "6px",
marginBottom: "24px",
- border: "1px solid #ffc107",
+ border: "1px solid",
+ borderColor: (theme: Theme) =>
+ theme.palette.mode === "dark" ? "rgba(255, 193, 7, 0.3)" : "#ffc107",
},
governance: {
- backgroundColor: "#e3f2fd",
+ backgroundColor: (theme: Theme) =>
+ theme.palette.mode === "dark" ? "rgba(33, 150, 243, 0.08)" : "#e3f2fd",
padding: "16px",
borderRadius: "8px",
marginTop: "32px",
- border: "1px solid #90caf9",
+ border: "1px solid",
+ borderColor: (theme: Theme) =>
+ theme.palette.mode === "dark" ? "rgba(144, 202, 249, 0.3)" : "#90caf9",
},
};
@@ -213,7 +222,7 @@ function ProjectCard({ project }: { project: Project }) {
)}
{project.note && (
{project.note}
@@ -267,7 +276,7 @@ export default function WorkingGroups() {
Kernel DSL Customer Support Channels
-
+
Get help and support for popular kernel development DSLs and
libraries.
diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx
index 221a4624..f3d5fa88 100644
--- a/frontend/src/tests/test-utils.tsx
+++ b/frontend/src/tests/test-utils.tsx
@@ -2,15 +2,16 @@ import { CssBaseline, ThemeProvider } from "@mui/material";
import { render } from "@testing-library/react";
import type { ReactNode } from "react";
import { MemoryRouter } from "react-router-dom";
-import { appTheme } from "../components/common/styles/theme";
+import { createAppTheme } from "../components/common/styles/theme";
export function renderWithRouter(ui: React.ReactElement, options = {}) {
return render(ui, { wrapper: MemoryRouter, ...options });
}
export function renderWithProviders(ui: ReactNode, route = "/") {
+ const theme = createAppTheme("light");
return render(
-
+
{ui}
,