Merge pull request #588 from GlancingMind/upstream-dark-colorscheme

Michael Muré created

Dark Colorscheme

Change summary

webui/src/components/BugTitleForm/BugTitleForm.tsx     | 38 +-----
webui/src/components/BugTitleForm/BugTitleInput.tsx    | 40 +++++++
webui/src/components/CloseBugButton/CloseBugButton.tsx | 11 ++
webui/src/components/Header/Header.tsx                 |  7 +
webui/src/components/Themer.tsx                        | 65 ++++++++++++
webui/src/index.tsx                                    |  9 
webui/src/pages/bug/Message.tsx                        | 10 +
webui/src/pages/list/Filter.tsx                        |  4 
webui/src/pages/list/FilterToolbar.tsx                 |  4 
webui/src/pages/list/ListQuery.tsx                     | 30 ++--
webui/src/pages/new/NewBugPage.tsx                     | 27 ----
webui/src/theme.ts                                     | 11 --
webui/src/themes/DefaultDark.ts                        | 25 ++++
webui/src/themes/DefaultLight.ts                       | 24 ++++
webui/src/themes/index.ts                              |  4 
15 files changed, 220 insertions(+), 89 deletions(-)

Detailed changes

webui/src/components/BugTitleForm/BugTitleForm.tsx 🔗

@@ -1,12 +1,6 @@
 import React, { useState } from 'react';
 
-import {
-  Button,
-  fade,
-  makeStyles,
-  TextField,
-  Typography,
-} from '@material-ui/core';
+import { Button, makeStyles, Typography } from '@material-ui/core';
 
 import { TimelineDocument } from '../../pages/bug/TimelineQuery.generated';
 import IfLoggedIn from '../IfLoggedIn/IfLoggedIn';
@@ -14,6 +8,7 @@ import Author from 'src/components/Author';
 import Date from 'src/components/Date';
 import { BugFragment } from 'src/pages/bug/Bug.generated';
 
+import BugTitleInput from './BugTitleInput';
 import { useSetTitleMutation } from './SetTitle.generated';
 
 /**
@@ -45,26 +40,12 @@ const useStyles = makeStyles((theme) => ({
     marginLeft: theme.spacing(2),
   },
   greenButton: {
-    marginLeft: '8px',
-    backgroundColor: '#2ea44fd9',
-    color: '#fff',
-    '&:hover': {
-      backgroundColor: '#2ea44f',
-    },
+    marginLeft: theme.spacing(1),
+    backgroundColor: theme.palette.success.main,
+    color: theme.palette.success.contrastText,
   },
-  titleInput: {
-    borderRadius: theme.shape.borderRadius,
-    borderColor: fade(theme.palette.primary.main, 0.2),
-    borderStyle: 'solid',
-    borderWidth: '1px',
-    backgroundColor: fade(theme.palette.primary.main, 0.05),
-    padding: theme.spacing(0, 0),
-    minWidth: 336,
-    transition: theme.transitions.create([
-      'width',
-      'borderColor',
-      'backgroundColor',
-    ]),
+  saveButton: {
+    marginRight: theme.spacing(1),
   },
 }));
 
@@ -122,11 +103,11 @@ function BugTitleForm({ bug }: Props) {
   function editableBugTitle() {
     return (
       <form className={classes.headerTitle} onSubmit={submitNewTitle}>
-        <TextField
+        <BugTitleInput
           inputRef={(node) => {
             issueTitleInput = node;
           }}
-          className={classes.titleInput}
+          label="Title"
           variant="outlined"
           fullWidth
           margin="dense"
@@ -135,6 +116,7 @@ function BugTitleForm({ bug }: Props) {
         />
         <div className={classes.editButtonContainer}>
           <Button
+            className={classes.saveButton}
             size="small"
             variant="contained"
             type="submit"

webui/src/components/BugTitleForm/BugTitleInput.tsx 🔗

@@ -0,0 +1,40 @@
+import { createStyles, fade, withStyles, TextField } from '@material-ui/core';
+import { Theme } from '@material-ui/core/styles';
+
+const BugTitleInput = withStyles((theme: Theme) =>
+  createStyles({
+    root: {
+      '& .MuiInputLabel-outlined': {
+        color: theme.palette.text.primary,
+      },
+      '& input:valid + fieldset': {
+        color: theme.palette.text.primary,
+        borderColor: theme.palette.divider,
+        borderWidth: 2,
+      },
+      '& input:valid:hover + fieldset': {
+        color: theme.palette.text.primary,
+        borderColor: fade(theme.palette.divider, 0.3),
+        borderWidth: 2,
+      },
+      '& input:valid:focus + fieldset': {
+        color: theme.palette.text.primary,
+        borderColor: theme.palette.divider,
+      },
+      '& input:invalid + fieldset': {
+        borderColor: theme.palette.error.main,
+        borderWidth: 2,
+      },
+      '& input:invalid:hover + fieldset': {
+        borderColor: theme.palette.error.main,
+        borderWidth: 2,
+      },
+      '& input:invalid:focus + fieldset': {
+        borderColor: theme.palette.error.main,
+        borderWidth: 2,
+      },
+    },
+  })
+)(TextField);
+
+export default BugTitleInput;

webui/src/components/CloseBugButton/CloseBugButton.tsx 🔗

@@ -1,12 +1,21 @@
 import React from 'react';
 
 import Button from '@material-ui/core/Button';
+import { makeStyles, Theme } from '@material-ui/core/styles';
+import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
 
 import { BugFragment } from 'src/pages/bug/Bug.generated';
 import { TimelineDocument } from 'src/pages/bug/TimelineQuery.generated';
 
 import { useCloseBugMutation } from './CloseBug.generated';
 
+const useStyles = makeStyles((theme: Theme) => ({
+  closeIssueIcon: {
+    color: theme.palette.secondary.dark,
+    paddingTop: '0.1rem',
+  },
+}));
+
 interface Props {
   bug: BugFragment;
   disabled: boolean;
@@ -14,6 +23,7 @@ interface Props {
 
 function CloseBugButton({ bug, disabled }: Props) {
   const [closeBug, { loading, error }] = useCloseBugMutation();
+  const classes = useStyles();
 
   function closeBugAction() {
     closeBug({
@@ -45,6 +55,7 @@ function CloseBugButton({ bug, disabled }: Props) {
         variant="contained"
         onClick={() => closeBugAction()}
         disabled={bug.status === 'CLOSED' || disabled}
+        startIcon={<ErrorOutlineIcon className={classes.closeIssueIcon} />}
       >
         Close issue
       </Button>

webui/src/components/Header/Header.tsx 🔗

@@ -5,6 +5,7 @@ import AppBar from '@material-ui/core/AppBar';
 import Toolbar from '@material-ui/core/Toolbar';
 import { makeStyles } from '@material-ui/core/styles';
 
+import { LightSwitch } from '../../components/Themer';
 import CurrentIdentity from '../CurrentIdentity/CurrentIdentity';
 
 const useStyles = makeStyles((theme) => ({
@@ -21,6 +22,9 @@ const useStyles = makeStyles((theme) => ({
     display: 'flex',
     alignItems: 'center',
   },
+  lightSwitch: {
+    padding: '0 20px',
+  },
   logo: {
     height: '42px',
     marginRight: theme.spacing(2),
@@ -39,6 +43,9 @@ function Header() {
             git-bug
           </Link>
           <div className={classes.filler}></div>
+          <div className={classes.lightSwitch}>
+            <LightSwitch />
+          </div>
           <CurrentIdentity />
         </Toolbar>
       </AppBar>

webui/src/components/Themer.tsx 🔗

@@ -0,0 +1,65 @@
+import React, { createContext, useContext, useState } from 'react';
+
+import { fade, ThemeProvider } from '@material-ui/core';
+import IconButton from '@material-ui/core/IconButton/IconButton';
+import Tooltip from '@material-ui/core/Tooltip/Tooltip';
+import { Theme } from '@material-ui/core/styles';
+import { NightsStayRounded, WbSunnyRounded } from '@material-ui/icons';
+import { makeStyles } from '@material-ui/styles';
+
+const ThemeContext = createContext({
+  toggleMode: () => {},
+  mode: '',
+});
+
+const useStyles = makeStyles((theme: Theme) => ({
+  iconButton: {
+    color: fade(theme.palette.primary.contrastText, 0.5),
+  },
+}));
+
+const LightSwitch = () => {
+  const { mode, toggleMode } = useContext(ThemeContext);
+  const nextMode = mode === 'light' ? 'dark' : 'light';
+  const description = `Switch to ${nextMode} theme`;
+  const classes = useStyles();
+
+  return (
+    <Tooltip title={description}>
+      <IconButton
+        onClick={toggleMode}
+        aria-label={description}
+        className={classes.iconButton}
+      >
+        {mode === 'light' ? <WbSunnyRounded /> : <NightsStayRounded />}
+      </IconButton>
+    </Tooltip>
+  );
+};
+
+type Props = {
+  children: React.ReactNode;
+  lightTheme: Theme;
+  darkTheme: Theme;
+};
+const Themer = ({ children, lightTheme, darkTheme }: Props) => {
+  const savedMode = localStorage.getItem('themeMode');
+  const preferedMode = savedMode != null ? savedMode : 'light';
+  const [mode, setMode] = useState(preferedMode);
+
+  const toggleMode = () => {
+    const preferedMode = mode === 'light' ? 'dark' : 'light';
+    localStorage.setItem('themeMode', preferedMode);
+    setMode(preferedMode);
+  };
+
+  const preferedTheme = mode === 'dark' ? darkTheme : lightTheme;
+
+  return (
+    <ThemeContext.Provider value={{ toggleMode: toggleMode, mode: mode }}>
+      <ThemeProvider theme={preferedTheme}>{children}</ThemeProvider>
+    </ThemeContext.Provider>
+  );
+};
+
+export { Themer as default, LightSwitch };

webui/src/index.tsx 🔗

@@ -3,18 +3,17 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { BrowserRouter } from 'react-router-dom';
 
-import ThemeProvider from '@material-ui/styles/ThemeProvider';
-
 import App from './App';
 import apolloClient from './apollo';
-import theme from './theme';
+import Themer from './components/Themer';
+import { defaultLightTheme, defaultDarkTheme } from './themes/index';
 
 ReactDOM.render(
   <ApolloProvider client={apolloClient}>
     <BrowserRouter>
-      <ThemeProvider theme={theme}>
+      <Themer lightTheme={defaultLightTheme} darkTheme={defaultDarkTheme}>
         <App />
-      </ThemeProvider>
+      </Themer>
     </BrowserRouter>
   </ApolloProvider>,
   document.getElementById('root')

webui/src/pages/bug/Message.tsx 🔗

@@ -27,11 +27,13 @@ const useStyles = makeStyles((theme) => ({
   },
   header: {
     ...theme.typography.body1,
-    color: '#444',
     padding: '0.5rem 1rem',
-    borderBottom: '1px solid #ddd',
+    borderBottom: `1px solid ${theme.palette.divider}`,
     display: 'flex',
-    backgroundColor: '#e2f1ff',
+    borderTopRightRadius: theme.shape.borderRadius,
+    borderTopLeftRadius: theme.shape.borderRadius,
+    backgroundColor: theme.palette.info.main,
+    color: theme.palette.info.contrastText,
   },
   title: {
     flex: 1,
@@ -47,7 +49,7 @@ const useStyles = makeStyles((theme) => ({
   },
   body: {
     ...theme.typography.body2,
-    padding: '0 1rem',
+    padding: '0.5rem',
   },
 }));
 

webui/src/pages/list/Filter.tsx 🔗

@@ -65,7 +65,7 @@ function stringify(params: Query): string {
 const useStyles = makeStyles((theme) => ({
   element: {
     ...theme.typography.body2,
-    color: '#444',
+    color: theme.palette.text.secondary,
     padding: theme.spacing(0, 1),
     fontWeight: 400,
     textDecoration: 'none',
@@ -75,7 +75,7 @@ const useStyles = makeStyles((theme) => ({
   },
   itemActive: {
     fontWeight: 600,
-    color: '#333',
+    color: theme.palette.text.primary,
   },
   icon: {
     paddingRight: theme.spacing(0.5),

webui/src/pages/list/FilterToolbar.tsx 🔗

@@ -19,8 +19,8 @@ import { useBugCountQuery } from './FilterToolbar.generated';
 
 const useStyles = makeStyles((theme) => ({
   toolbar: {
-    backgroundColor: theme.palette.grey['100'],
-    borderColor: theme.palette.grey['300'],
+    backgroundColor: theme.palette.primary.light,
+    borderColor: theme.palette.divider,
     borderWidth: '1px 0',
     borderStyle: 'solid',
     margin: theme.spacing(0, -1),

webui/src/pages/list/ListQuery.tsx 🔗

@@ -6,7 +6,7 @@ import { Button } from '@material-ui/core';
 import IconButton from '@material-ui/core/IconButton';
 import InputBase from '@material-ui/core/InputBase';
 import Paper from '@material-ui/core/Paper';
-import { fade, makeStyles, Theme } from '@material-ui/core/styles';
+import { makeStyles, Theme } from '@material-ui/core/styles';
 import ErrorOutline from '@material-ui/icons/ErrorOutline';
 import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
 import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
@@ -56,10 +56,11 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
   },
   search: {
     borderRadius: theme.shape.borderRadius,
-    borderColor: fade(theme.palette.primary.main, 0.2),
+    color: theme.palette.text.secondary,
+    borderColor: theme.palette.divider,
     borderStyle: 'solid',
     borderWidth: '1px',
-    backgroundColor: fade(theme.palette.primary.main, 0.05),
+    backgroundColor: theme.palette.primary.light,
     padding: theme.spacing(0, 1),
     width: ({ searching }) => (searching ? '20rem' : '15rem'),
     transition: theme.transitions.create([
@@ -69,13 +70,11 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
     ]),
   },
   searchFocused: {
-    borderColor: fade(theme.palette.primary.main, 0.4),
     backgroundColor: theme.palette.background.paper,
-    width: '20rem!important',
   },
   placeholderRow: {
     padding: theme.spacing(1),
-    borderBottomColor: theme.palette.grey['300'],
+    borderBottomColor: theme.palette.divider,
     borderBottomWidth: '1px',
     borderBottomStyle: 'solid',
     display: 'flex',
@@ -91,7 +90,8 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
     ...theme.typography.h5,
     padding: theme.spacing(8),
     textAlign: 'center',
-    borderBottomColor: theme.palette.grey['300'],
+    color: theme.palette.text.hint,
+    borderBottomColor: theme.palette.divider,
     borderBottomWidth: '1px',
     borderBottomStyle: 'solid',
     '& > p': {
@@ -99,22 +99,22 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
     },
   },
   errorBox: {
-    color: theme.palette.error.main,
+    color: theme.palette.error.dark,
     '& > pre': {
       fontSize: '1rem',
       textAlign: 'left',
-      backgroundColor: theme.palette.grey['900'],
-      color: theme.palette.common.white,
+      borderColor: theme.palette.divider,
+      borderWidth: '1px',
+      borderRadius: theme.shape.borderRadius,
+      borderStyle: 'solid',
+      color: theme.palette.text.primary,
       marginTop: theme.spacing(4),
       padding: theme.spacing(2, 3),
     },
   },
   greenButton: {
-    backgroundColor: '#2ea44fd9',
-    color: '#fff',
-    '&:hover': {
-      backgroundColor: '#2ea44f',
-    },
+    backgroundColor: theme.palette.success.main,
+    color: theme.palette.success.contrastText,
   },
 }));
 

webui/src/pages/new/NewBugPage.tsx 🔗

@@ -2,9 +2,9 @@ import React, { FormEvent, useState } from 'react';
 
 import { Button } from '@material-ui/core';
 import Paper from '@material-ui/core/Paper';
-import TextField from '@material-ui/core/TextField/TextField';
-import { fade, makeStyles, Theme } from '@material-ui/core/styles';
+import { makeStyles, Theme } from '@material-ui/core/styles';
 
+import BugTitleInput from '../../components/BugTitleForm/BugTitleInput';
 import CommentInput from '../../components/CommentInput/CommentInput';
 
 import { useNewBugMutation } from './NewBug.generated';
@@ -21,19 +21,6 @@ const useStyles = makeStyles((theme: Theme) => ({
     padding: theme.spacing(2),
     overflow: 'hidden',
   },
-  titleInput: {
-    borderRadius: theme.shape.borderRadius,
-    borderColor: fade(theme.palette.primary.main, 0.2),
-    borderStyle: 'solid',
-    borderWidth: '1px',
-    backgroundColor: fade(theme.palette.primary.main, 0.05),
-    padding: theme.spacing(0, 0),
-    transition: theme.transitions.create([
-      'width',
-      'borderColor',
-      'backgroundColor',
-    ]),
-  },
   form: {
     display: 'flex',
     flexDirection: 'column',
@@ -43,11 +30,8 @@ const useStyles = makeStyles((theme: Theme) => ({
     justifyContent: 'flex-end',
   },
   greenButton: {
-    backgroundColor: '#2ea44fd9',
-    color: '#fff',
-    '&:hover': {
-      backgroundColor: '#2ea44f',
-    },
+    backgroundColor: theme.palette.success.main,
+    color: theme.palette.success.contrastText,
   },
 }));
 
@@ -85,12 +69,11 @@ function NewBugPage() {
   return (
     <Paper className={classes.main}>
       <form className={classes.form} onSubmit={submitNewIssue}>
-        <TextField
+        <BugTitleInput
           inputRef={(node) => {
             issueTitleInput = node;
           }}
           label="Title"
-          className={classes.titleInput}
           variant="outlined"
           fullWidth
           margin="dense"

webui/src/theme.ts 🔗

@@ -1,11 +0,0 @@
-import { createMuiTheme } from '@material-ui/core/styles';
-
-const theme = createMuiTheme({
-  palette: {
-    primary: {
-      main: '#263238',
-    },
-  },
-});
-
-export default theme;

webui/src/themes/DefaultDark.ts 🔗

@@ -0,0 +1,25 @@
+import { createMuiTheme } from '@material-ui/core/styles';
+
+const defaultDarkTheme = createMuiTheme({
+  palette: {
+    type: 'dark',
+    primary: {
+      main: '#263238',
+      light: '#525252',
+    },
+    error: {
+      main: '#f44336',
+      dark: '#ff4949',
+    },
+    info: {
+      main: '#2a393e',
+      contrastText: '#ffffffb3',
+    },
+    success: {
+      main: '#2ea44fd9',
+      contrastText: '#fff',
+    },
+  },
+});
+
+export default defaultDarkTheme;

webui/src/themes/DefaultLight.ts 🔗

@@ -0,0 +1,24 @@
+import { createMuiTheme } from '@material-ui/core/styles';
+
+const defaultLightTheme = createMuiTheme({
+  palette: {
+    type: 'light',
+    primary: {
+      main: '#263238',
+      light: '#f5f5f5',
+    },
+    info: {
+      main: '#e2f1ff',
+      contrastText: '#555',
+    },
+    success: {
+      main: '#2ea44fd9',
+      contrastText: '#fff',
+    },
+    text: {
+      secondary: '#555',
+    },
+  },
+});
+
+export default defaultLightTheme;

webui/src/themes/index.ts 🔗

@@ -0,0 +1,4 @@
+import defaultDarkTheme from './DefaultDark';
+import defaultLightTheme from './DefaultLight';
+
+export { defaultLightTheme, defaultDarkTheme };