Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions web/src/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
Box,
} from "@mui/material";
import { ExitToApp, MoreVert } from "@mui/icons-material";
import Brightness4Icon from "@mui/icons-material/Brightness4"; // moon — switch to dark
import Brightness7Icon from "@mui/icons-material/Brightness7"; // sun — switch to light
import {
PAGE_PATH_APPLICATIONS,
PAGE_PATH_DEPLOYMENTS,
Expand All @@ -32,11 +34,14 @@ import logo from "~~/assets/logo.svg";
import NavLink from "./NavLink";
import { IconOpenNewTab, LogoImage } from "./styles";
import useAuth from "~/contexts/auth-context/use-auth";
import { useThemeMode } from "~/theme";

export const APP_HEADER_HEIGHT = 56;

export const Header: FC = memo(function Header() {
const { me } = useAuth();
const { mode, toggleTheme } = useThemeMode();

const [userAnchorEl, setUserAnchorEl] = useState<HTMLButtonElement | null>(
null
);
Expand Down Expand Up @@ -127,16 +132,28 @@ export const Header: FC = memo(function Header() {
>
<MoreVert />
</IconButton>

{/* Dark / light mode toggle — shows moon in light mode, sun in dark mode */}
<IconButton
color="inherit"
aria-label="Toggle dark mode"
onClick={toggleTheme}
size="small"
sx={{ ml: 1 }}
>
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
</IconButton>

<Button
aria-label="User Menu"
aria-controls="user-menu"
aria-haspopup="true"
onClick={(e) => setUserAnchorEl(e.currentTarget)}
style={{
color="inherit"
sx={{
display: "flex",
alignItems: "center",
textTransform: "none",
color: "white",
}}
>
<Avatar
Expand All @@ -147,9 +164,21 @@ export const Header: FC = memo(function Header() {
</Button>
</>
) : (
<NavLink href={PAGE_PATH_LOGIN} active={false}>
<Typography variant="body2">Login</Typography>
</NavLink>
<>
{/* Show the toggle even on the login page so unauthenticated users can still switch */}
<IconButton
color="inherit"
aria-label="Toggle dark mode"
onClick={toggleTheme}
size="small"
sx={{ mr: 1 }}
>
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
</IconButton>
<NavLink href={PAGE_PATH_LOGIN} active={false}>
<Typography variant="body2">Login</Typography>
</NavLink>
</>
)}
</Box>
</Toolbar>
Expand Down
72 changes: 50 additions & 22 deletions web/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ThemeProvider, StyledEngineProvider } from "@mui/material";
import CssBaseline from "@mui/material/CssBaseline";
import { render } from "react-dom";
import { theme } from "./theme";
import { useMemo, useState } from "react";
import { createAppTheme, ThemeContext, ThemeMode } from "./theme";
import { Routes } from "./routes";
import { BrowserRouter } from "react-router-dom";
import { setupDayjs } from "./utils/setup-dayjs";
Expand All @@ -11,6 +12,53 @@ import { AuthProvider } from "./contexts/auth-context";
import { ToastProvider } from "./contexts/toast-context/toast-provider";
import { CommandProvider } from "./contexts/command-context";

// The root component — lives here so we can use React state for the theme toggle.
function App() {
// Pick up whatever the user last chose, or fall back to their OS preference
const savedMode = localStorage.getItem("theme_mode") as ThemeMode | null;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const [mode, setMode] = useState<ThemeMode>(
savedMode ?? (prefersDark ? "dark" : "light")
);

const toggleTheme = () => {
setMode((current) => {
const next = current === "light" ? "dark" : "light";
localStorage.setItem("theme_mode", next); // remember across sessions
return next;
});
};

// Rebuild the MUI theme only when mode actually changes
const appTheme = useMemo(() => createAppTheme(mode), [mode]);

return (
<ThemeContext.Provider value={{ mode, toggleTheme }}>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={appTheme}>
<BrowserRouter
future={{
v7_startTransition: false,
v7_relativeSplatPath: false,
}}
>
<ToastProvider>
<QueryClientWrap>
<AuthProvider>
<CommandProvider>
<CssBaseline />
<Routes />
</CommandProvider>
</AuthProvider>
</QueryClientWrap>
</ToastProvider>
</BrowserRouter>
</ThemeProvider>
</StyledEngineProvider>
</ThemeContext.Provider>
);
}

async function run(): Promise<void> {
if (process.env.ENABLE_MOCK === "true") {
// NOTE: Ignore check exists this module, because this module exclude from production build.
Expand Down Expand Up @@ -47,27 +95,7 @@ Happy PipeCD-ing 🙌

render(
<CookiesProvider>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<BrowserRouter
future={{
v7_startTransition: false,
v7_relativeSplatPath: false,
}}
>
<ToastProvider>
<QueryClientWrap>
<AuthProvider>
<CommandProvider>
<CssBaseline />
<Routes />
</CommandProvider>
</AuthProvider>
</QueryClientWrap>
</ToastProvider>
</BrowserRouter>
</ThemeProvider>
</StyledEngineProvider>
<App />
</CookiesProvider>,
document.getElementById("root")
);
Expand Down
142 changes: 83 additions & 59 deletions web/src/theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createTheme } from "@mui/material/styles";
import { cyan } from "@mui/material/colors";
import { createContext, useContext } from "react";

declare module "@mui/material/styles/createTypography" {
interface FontStyle {
Expand All @@ -11,74 +12,97 @@ declare module "@mui/material/styles/createTypography" {
}
}

export const theme = createTheme({
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
MuiTypography: {
defaultProps: {
variantMapping: {
body1: "div",
body2: "div",
// The two modes the app can be in
export type ThemeMode = "light" | "dark";

// A simple context so any component can read the mode and call toggleTheme
export const ThemeContext = createContext<{
mode: ThemeMode;
toggleTheme: () => void;
}>({
mode: "light",
toggleTheme: () => {},
});

// Convenience hook — just call useThemeMode() anywhere you need the toggle
export const useThemeMode = () => useContext(ThemeContext);

// Build a MUI theme for whichever mode is requested.
// All existing colors and component tweaks are preserved exactly as before.
export const createAppTheme = (mode: ThemeMode) =>
createTheme({
components: {
MuiButtonBase: {
defaultProps: {
disableRipple: true,
},
},
},
MuiCssBaseline: {
styleOverrides: {
html: {
height: "100%",
},
body: {
height: "100%",
MuiTypography: {
defaultProps: {
variantMapping: {
body1: "div",
body2: "div",
},
},
"#root": {
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
MuiCssBaseline: {
styleOverrides: {
html: {
height: "100%",
},
body: {
height: "100%",
},
"#root": {
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
},
},
},
MuiDialog: {
styleOverrides: {
paper: {
borderRadius: 16,
MuiDialog: {
styleOverrides: {
paper: {
borderRadius: 16,
},
},
},
},
MuiDialogActions: {
styleOverrides: {
spacing: {
padding: 16,
MuiDialogActions: {
styleOverrides: {
spacing: {
padding: 16,
},
},
},
},
},
palette: {
primary: { main: "#283778" },
success: {
main: "#539d56",
light: "#83cf84",
dark: "#216e2b",
},
error: {
main: "#d6442c",
light: "#ff7657",
dark: "#9d0001",
},
secondary: cyan,
background: {
default: "#fafafa",
palette: {
mode, // this single line is what makes MUI flip all its colours
primary: { main: "#283778" },
success: {
main: "#539d56",
light: "#83cf84",
dark: "#216e2b",
},
error: {
main: "#d6442c",
light: "#ff7657",
dark: "#9d0001",
},
secondary: cyan,
background: {
default: mode === "light" ? "#fafafa" : "#121212",
paper: mode === "light" ? "#ffffff" : "#1e1e1e",
},
},
},
typography: {
subtitle2: {
fontWeight: 600,
typography: {
subtitle2: {
fontWeight: 600,
},
fontFamilyMono:
'"Roboto Mono",SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace',
},
fontFamilyMono:
'"Roboto Mono",SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace',
},
});
});

// Keep the old named export so nothing else in the project breaks
export const theme = createAppTheme("light");
Loading