Merge pull request #2707 from MartinFournier/feature/theme-browser

Add Theme Browser accessible from GameOptions
This commit is contained in:
hydroflame
2022-01-26 00:46:17 -05:00
committed by GitHub
56 changed files with 1034 additions and 658 deletions
+13 -9
View File
@@ -22,12 +22,11 @@ import TextField from "@mui/material/TextField";
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import SaveIcon from "@mui/icons-material/Save";
import PaletteIcon from '@mui/icons-material/Palette';
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
import { dialogBoxCreate } from "./DialogBox";
import { ConfirmationModal } from "./ConfirmationModal";
import { ThemeEditorModal } from "./ThemeEditorModal";
import { StyleEditorModal } from "./StyleEditorModal";
import { SnackbarEvents } from "./Snackbar";
@@ -37,6 +36,9 @@ import { formatTime } from "../../utils/helpers/formatTime";
import { OptionSwitch } from "./OptionSwitch";
import { DeleteGameButton } from "./DeleteGameButton";
import { SoftResetButton } from "./SoftResetButton";
import { IRouter } from "../Router";
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -50,6 +52,7 @@ const useStyles = makeStyles((theme: Theme) =>
interface IProps {
player: IPlayer;
router: IRouter;
save: () => void;
export: () => void;
forceKill: () => void;
@@ -74,8 +77,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
const [importSaveOpen, setImportSaveOpen] = useState(false);
const [importData, setImportData] = useState<ImportData | null>(null);
@@ -636,9 +637,14 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button>
</Tooltip>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<Button onClick={() => setThemeEditorOpen(true)}>Theme editor</Button>
<Button onClick={() => setStyleEditorOpen(true)}>Style editor</Button>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
<Tooltip title="Head to the theme browser to see a collection of prebuilt themes.">
<Button startIcon={<PaletteIcon />} onClick={() => props.router.toThemeBrowser()}>
Theme Browser
</Button>
</Tooltip>
<ThemeEditorButton router={props.router} />
<StyleEditorButton />
</Box>
<Box>
<Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank">
@@ -663,8 +669,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
</Box>
</Grid>
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} />
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
</div>
);
}
+5 -1
View File
@@ -14,6 +14,10 @@ const useStyles = makeStyles(() => ({
snackbar: {
// Log popup z-index increments, so let's add a padding to be well above them.
zIndex: `${logBoxBaseZIndex + 1000} !important` as any,
"& .MuiAlert-icon": {
alignSelf: 'center',
},
}
}));
@@ -27,7 +31,7 @@ export function SnackbarProvider(props: IProps): React.ReactElement {
);
}
export const SnackbarEvents = new EventEmitter<[string, "success" | "warning" | "error" | "info", number]>();
export const SnackbarEvents = new EventEmitter<[string | React.ReactNode, "success" | "warning" | "error" | "info", number]>();
export function Snackbar(): React.ReactElement {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
-165
View File
@@ -1,165 +0,0 @@
import React, { useEffect, useState } from "react";
import { Modal } from "./Modal";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import TextField from "@mui/material/TextField";
import ReplyIcon from "@mui/icons-material/Reply";
import SaveIcon from "@mui/icons-material/Save";
import { ThemeEvents } from "./Theme";
import { Settings } from "../../Settings/Settings";
import { defaultStyles } from "../../Settings/Styles";
import { Tooltip } from "@mui/material";
import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions";
interface IProps {
open: boolean;
onClose: () => void;
}
interface FontFamilyProps {
value: React.CSSProperties["fontFamily"];
onChange: (newValue: React.CSSProperties["fontFamily"], error?: string) => void;
refreshId: number;
}
function FontFamilyField({ value, onChange, refreshId }: FontFamilyProps): React.ReactElement {
const [errorText, setErrorText] = useState<string | undefined>();
const [fontFamily, setFontFamily] = useState<React.CSSProperties["fontFamily"]>(value);
function update(newValue: React.CSSProperties["fontFamily"]): void {
setFontFamily(newValue);
if (!newValue) {
setErrorText("Must have a value");
} else {
setErrorText("");
}
}
function onTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
update(event.target.value);
}
useEffect(() => onChange(fontFamily, errorText), [fontFamily]);
useEffect(() => update(value), [refreshId]);
return (
<TextField
sx={{ my: 1 }}
label={"Font-Family"}
error={!!errorText}
value={fontFamily}
helperText={errorText}
onChange={onTextChange}
fullWidth
/>
);
}
interface LineHeightProps {
value: React.CSSProperties["lineHeight"];
onChange: (newValue: React.CSSProperties["lineHeight"], error?: string) => void;
refreshId: number;
}
function LineHeightField({ value, onChange, refreshId }: LineHeightProps): React.ReactElement {
const [errorText, setErrorText] = useState<string | undefined>();
const [lineHeight, setLineHeight] = useState<React.CSSProperties["lineHeight"]>(value);
function update(newValue: React.CSSProperties["lineHeight"]): void {
setLineHeight(newValue);
if (!newValue) {
setErrorText("Must have a value");
} else if (isNaN(Number(newValue))) {
setErrorText("Must be a number");
} else {
setErrorText("");
}
}
function onTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
update(event.target.value);
}
useEffect(() => onChange(lineHeight, errorText), [lineHeight]);
useEffect(() => update(value), [refreshId]);
return (
<TextField
sx={{ my: 1 }}
label={"Line Height"}
error={!!errorText}
value={lineHeight}
helperText={errorText}
onChange={onTextChange}
/>
);
}
export function StyleEditorModal(props: IProps): React.ReactElement {
const [refreshId, setRefreshId] = useState<number>(0);
const [error, setError] = useState<string | undefined>();
const [customStyle, setCustomStyle] = useState<IStyleSettings>({
...Settings.styles,
});
function persistToSettings(styles: IStyleSettings): void {
Object.assign(Settings.styles, styles);
ThemeEvents.emit();
}
function saveStyles(): void {
persistToSettings(customStyle);
}
function setDefaults(): void {
const styles = { ...defaultStyles };
setCustomStyle(styles);
persistToSettings(styles);
setRefreshId(refreshId + 1);
}
function update(styles: IStyleSettings, errorMessage?: string): void {
setError(errorMessage);
if (!errorMessage) {
setCustomStyle(styles);
}
}
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography variant="h6">Styles Editor</Typography>
<Typography>
WARNING: Changing styles <strong>may mess up</strong> the interface. Drastic changes are{" "}
<strong>NOT recommended</strong>.
</Typography>
<Paper sx={{ p: 2, my: 2 }}>
<FontFamilyField
value={customStyle.fontFamily}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, fontFamily: value as any }, error)}
/>
<br />
<LineHeightField
value={customStyle.lineHeight}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, lineHeight: value as any }, error)}
/>
<br />
<ButtonGroup sx={{ my: 1 }}>
<Button onClick={setDefaults} startIcon={<ReplyIcon />} color="secondary" variant="outlined">
Revert to Defaults
</Button>
<Tooltip title={"Save styles to settings"}>
<Button onClick={saveStyles} endIcon={<SaveIcon />} color={error ? "error" : "primary"} disabled={!!error}>
Save Modifications
</Button>
</Tooltip>
</ButtonGroup>
</Paper>
</Modal>
);
}
-377
View File
@@ -1,377 +0,0 @@
import React from "react";
import { createTheme, ThemeProvider, Theme, StyledEngineProvider } from "@mui/material/styles";
import { EventEmitter } from "../../utils/EventEmitter";
import { Settings } from "../../Settings/Settings";
export const ThemeEvents = new EventEmitter<[]>();
declare module "@mui/material/styles" {
interface Theme {
colors: {
hp: React.CSSProperties["color"];
money: React.CSSProperties["color"];
hack: React.CSSProperties["color"];
combat: React.CSSProperties["color"];
cha: React.CSSProperties["color"];
int: React.CSSProperties["color"];
rep: React.CSSProperties["color"];
backgroundprimary: React.CSSProperties["color"];
backgroundsecondary: React.CSSProperties["color"];
button: React.CSSProperties["color"];
successlight: React.CSSProperties["color"];
success: React.CSSProperties["color"];
successdark: React.CSSProperties["color"];
white: React.CSSProperties["color"];
black: React.CSSProperties["color"];
};
}
interface ThemeOptions {
colors: {
hp: React.CSSProperties["color"];
money: React.CSSProperties["color"];
hack: React.CSSProperties["color"];
combat: React.CSSProperties["color"];
cha: React.CSSProperties["color"];
int: React.CSSProperties["color"];
rep: React.CSSProperties["color"];
backgroundprimary: React.CSSProperties["color"];
backgroundsecondary: React.CSSProperties["color"];
button: React.CSSProperties["color"];
successlight: React.CSSProperties["color"];
success: React.CSSProperties["color"];
successdark: React.CSSProperties["color"];
white: React.CSSProperties["color"];
black: React.CSSProperties["color"];
};
}
}
let theme: Theme;
export function refreshTheme(): void {
theme = createTheme({
colors: {
hp: Settings.theme.hp,
money: Settings.theme.money,
hack: Settings.theme.hack,
combat: Settings.theme.combat,
cha: Settings.theme.cha,
int: Settings.theme.int,
rep: Settings.theme.rep,
backgroundprimary: Settings.theme.backgroundprimary,
backgroundsecondary: Settings.theme.backgroundsecondary,
button: Settings.theme.button,
successlight: Settings.theme.successlight,
success: Settings.theme.success,
successdark: Settings.theme.successdark,
white: Settings.theme.white,
black: Settings.theme.black,
},
palette: {
primary: {
light: Settings.theme.primarylight,
main: Settings.theme.primary,
dark: Settings.theme.primarydark,
},
secondary: {
light: Settings.theme.secondarylight,
main: Settings.theme.secondary,
dark: Settings.theme.secondarydark,
},
error: {
light: Settings.theme.errorlight,
main: Settings.theme.error,
dark: Settings.theme.errordark,
},
info: {
light: Settings.theme.infolight,
main: Settings.theme.info,
dark: Settings.theme.infodark,
},
warning: {
light: Settings.theme.warninglight,
main: Settings.theme.warning,
dark: Settings.theme.warningdark,
},
success: {
light: Settings.theme.successlight,
main: Settings.theme.success,
dark: Settings.theme.successdark,
},
background: {
default: Settings.theme.backgroundprimary,
paper: Settings.theme.well,
},
action: {
disabled: Settings.theme.disabled,
},
},
typography: {
fontFamily: Settings.styles.fontFamily,
button: {
textTransform: "none",
},
},
components: {
MuiInputBase: {
styleOverrides: {
root: {
backgroundColor: Settings.theme.well,
color: Settings.theme.primary,
},
input: {
"&::placeholder": {
userSelect: "none",
color: Settings.theme.primarydark,
},
},
},
},
MuiInput: {
styleOverrides: {
root: {
backgroundColor: Settings.theme.well,
borderBottomColor: "#fff",
},
underline: {
"&:hover": {
borderBottomColor: Settings.theme.primarydark,
},
"&:before": {
borderBottomColor: Settings.theme.primary,
},
"&:after": {
borderBottomColor: Settings.theme.primarylight,
},
},
},
},
MuiInputLabel: {
styleOverrides: {
root: {
color: Settings.theme.primarydark, // why is this switched?
userSelect: "none",
"&:before": {
color: Settings.theme.primarylight,
},
},
},
},
MuiButtonGroup: {
styleOverrides: {
root: {
'& .MuiButton-root:not(:last-of-type)': {
marginRight: '1px',
}
}
}
},
MuiButton: {
styleOverrides: {
root: {
backgroundColor: Settings.theme.button,
border: "1px solid " + Settings.theme.well,
// color: Settings.theme.primary,
"&:hover": {
backgroundColor: Settings.theme.backgroundsecondary,
},
borderRadius: 0,
},
},
},
MuiSelect: {
styleOverrides: {
icon: {
color: Settings.theme.primary,
},
},
defaultProps: {
variant: "standard",
},
},
MuiTextField: {
defaultProps: {
variant: "standard",
},
},
MuiTypography: {
defaultProps: {
color: "primary",
},
styleOverrides: {
root: {
lineHeight: Settings.styles.lineHeight,
}
}
},
MuiMenu: {
styleOverrides: {
list: {
backgroundColor: Settings.theme.well,
},
},
},
MuiMenuItem: {
styleOverrides: {
root: {
color: Settings.theme.primary,
},
},
},
MuiAccordionSummary: {
styleOverrides: {
root: {
backgroundColor: "#111",
},
},
},
MuiAccordionDetails: {
styleOverrides: {
root: {
backgroundColor: Settings.theme.backgroundsecondary,
},
},
},
MuiIconButton: {
styleOverrides: {
root: {
color: Settings.theme.primary,
},
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
fontSize: "1em",
color: Settings.theme.primary,
backgroundColor: Settings.theme.well,
borderRadius: 0,
border: "2px solid white",
maxWidth: "100vh",
},
},
defaultProps: {
disableInteractive: true,
},
},
MuiSlider: {
styleOverrides: {
valueLabel: {
color: Settings.theme.primary,
backgroundColor: Settings.theme.well,
},
},
},
MuiDrawer: {
styleOverrides: {
paper: {
"&::-webkit-scrollbar": {
// webkit
display: "none",
},
scrollbarWidth: "none", // firefox
backgroundColor: Settings.theme.backgroundsecondary,
},
paperAnchorDockedLeft: {
borderRight: "1px solid " + Settings.theme.welllight,
},
},
},
MuiDivider: {
styleOverrides: {
root: {
backgroundColor: Settings.theme.welllight,
},
},
},
MuiFormControlLabel: {
styleOverrides: {
root: {
color: Settings.theme.primary,
},
},
},
MuiSwitch: {
styleOverrides: {
switchBase: {
color: Settings.theme.primarydark,
},
track: {
backgroundColor: Settings.theme.welllight,
},
},
},
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 0,
backgroundColor: Settings.theme.backgroundsecondary,
border: "1px solid " + Settings.theme.welllight,
},
},
},
MuiTablePagination: {
styleOverrides: {
select: {
color: Settings.theme.primary,
},
selectLabel: {
color: Settings.theme.primary,
},
displayedRows: {
color: Settings.theme.primary,
},
},
},
MuiTab: {
styleOverrides: {
textColorPrimary: {
color: Settings.theme.secondary,
"&.Mui-selected": {
color: Settings.theme.primary,
},
},
},
},
MuiAlert: {
styleOverrides: {
root: {
backgroundColor: Settings.theme.black,
borderRadius: 0,
border: "1px solid " + Settings.theme.well,
},
standardSuccess: {
color: Settings.theme.successlight,
},
standardError: {
color: Settings.theme.errorlight,
},
standardWarning: {
color: Settings.theme.warninglight,
},
standardInfo: {
color: Settings.theme.infolight,
},
},
},
},
});
document.body.style.backgroundColor = theme.colors.backgroundprimary?.toString() ?? "black";
}
refreshTheme();
interface IProps {
children: JSX.Element[] | JSX.Element;
}
export const TTheme = ({ children }: IProps): React.ReactElement => (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</StyledEngineProvider>
);
-381
View File
@@ -1,381 +0,0 @@
import React, { useState } from "react";
import { Modal } from "./Modal";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import Paper from "@mui/material/Paper";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import ReplyIcon from "@mui/icons-material/Reply";
import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
import { Color, ColorPicker } from "material-ui-color";
import { ThemeEvents } from "./Theme";
import { Settings, defaultSettings } from "../../Settings/Settings";
import { getPredefinedThemes } from "../../Settings/Themes";
import { UserInterfaceTheme } from "../../ScriptEditor/NetscriptDefinitions";
interface IProps {
open: boolean;
onClose: () => void;
}
interface IColorEditorProps {
name: string;
color: string | undefined;
onColorChange: (name: string, value: string) => void;
defaultColor: string;
}
function ColorEditor({ name, onColorChange, color, defaultColor }: IColorEditorProps): React.ReactElement {
if (color === undefined) {
console.error(`color ${name} was undefined, reverting to default`);
color = defaultColor;
}
return (
<>
<TextField
sx={{ mx: 1 }}
label={name}
value={color}
InputProps={{
startAdornment: (
<>
<ColorPicker
hideTextfield
deferred
value={color}
onChange={(newColor: Color) => onColorChange(name, "#" + newColor.hex)}
disableAlpha
/>
</>
),
endAdornment: (
<>
<IconButton onClick={() => onColorChange(name, defaultColor)}>
<ReplyIcon color="primary" />
</IconButton>
</>
),
}}
/>
</>
);
}
export function ThemeEditorModal(props: IProps): React.ReactElement {
const [customTheme, setCustomTheme] = useState<{ [key: string]: string | undefined }>({
...Settings.theme,
});
const predefinedThemes = getPredefinedThemes();
const themes = predefinedThemes && Object.entries(predefinedThemes)
.map(([key, templateTheme]) => {
const name = templateTheme.name || key;
let inner = <Typography>{name}</Typography>;
let toolTipTitle;
if (templateTheme.credit) {
toolTipTitle = <Typography>{templateTheme.description || name} <em>by {templateTheme.credit}</em></Typography>;
} else if (templateTheme.description) {
toolTipTitle = <Typography>{templateTheme.description}</Typography>;
}
if (toolTipTitle) {
inner = <Tooltip title={toolTipTitle}>{inner}</Tooltip>
}
return (
<Button onClick={() => setTemplateTheme(templateTheme.colors)}
startIcon={<PaletteSharpIcon />} key={key} sx={{ mr: 1, mb: 1 }}>
{inner}
</Button>
);
}) || <></>;
function setTheme(theme: UserInterfaceTheme): void {
setCustomTheme(theme);
Object.assign(Settings.theme, theme);
ThemeEvents.emit();
}
function onThemeChange(event: React.ChangeEvent<HTMLInputElement>): void {
try {
const importedTheme = JSON.parse(event.target.value);
if (typeof importedTheme !== "object") return;
setCustomTheme(importedTheme);
for (const key of Object.keys(importedTheme)) {
Settings.theme[key] = importedTheme[key];
}
ThemeEvents.emit();
} catch (err) {
// ignore
}
}
function onColorChange(name: string, value: string): void {
setCustomTheme((old: any) => {
old[name] = value;
return old;
});
Settings.theme[name] = value;
ThemeEvents.emit();
}
function setTemplateTheme(theme: UserInterfaceTheme): void {
setTheme(theme);
}
return (
<Modal open={props.open} onClose={props.onClose}>
<Paper sx={{ px: 1, py: 1, my: 1 }}>
<Tooltip open={true} placement={"top"} title={<Typography>Example tooltip</Typography>}>
<Button color="primary" size="small">primary button</Button>
</Tooltip>
<Button color="secondary" size="small">secondary button</Button>
<Button color="warning" size="small">warning button</Button>
<Button color="info" size="small">info button</Button>
<Button color="error" size="small">error button</Button>
<Button disabled size="small">disabled button</Button>
<br />
<Typography color="primary" variant="caption">text with primary color</Typography>&nbsp;
<Typography color="secondary" variant="caption">text with secondary color</Typography>&nbsp;
<Typography color="error" variant="caption">text with error color</Typography>
<br />
<TextField value={"Text field"} size="small" />
</Paper>
<Paper sx={{ py: 1, my: 1 }}>
<ColorEditor
name="primarylight"
onColorChange={onColorChange}
color={customTheme["primarylight"]}
defaultColor={defaultSettings.theme["primarylight"]}
/>
<ColorEditor
name="primary"
onColorChange={onColorChange}
color={customTheme["primary"]}
defaultColor={defaultSettings.theme["primary"]}
/>
<ColorEditor
name="primarydark"
onColorChange={onColorChange}
color={customTheme["primarydark"]}
defaultColor={defaultSettings.theme["primarydark"]}
/>
<br />
<ColorEditor
name="successlight"
onColorChange={onColorChange}
color={customTheme["successlight"]}
defaultColor={defaultSettings.theme["successlight"]}
/>
<ColorEditor
name="success"
onColorChange={onColorChange}
color={customTheme["success"]}
defaultColor={defaultSettings.theme["success"]}
/>
<ColorEditor
name="successdark"
onColorChange={onColorChange}
color={customTheme["successdark"]}
defaultColor={defaultSettings.theme["successdark"]}
/>
<br />
<ColorEditor
name="errorlight"
onColorChange={onColorChange}
color={customTheme["errorlight"]}
defaultColor={defaultSettings.theme["errorlight"]}
/>
<ColorEditor
name="error"
onColorChange={onColorChange}
color={customTheme["error"]}
defaultColor={defaultSettings.theme["error"]}
/>
<ColorEditor
name="errordark"
onColorChange={onColorChange}
color={customTheme["errordark"]}
defaultColor={defaultSettings.theme["errordark"]}
/>
<br />
<ColorEditor
name="secondarylight"
onColorChange={onColorChange}
color={customTheme["secondarylight"]}
defaultColor={defaultSettings.theme["secondarylight"]}
/>
<ColorEditor
name="secondary"
onColorChange={onColorChange}
color={customTheme["secondary"]}
defaultColor={defaultSettings.theme["secondary"]}
/>
<ColorEditor
name="secondarydark"
onColorChange={onColorChange}
color={customTheme["secondarydark"]}
defaultColor={defaultSettings.theme["secondarydark"]}
/>
<br />
<ColorEditor
name="warninglight"
onColorChange={onColorChange}
color={customTheme["warninglight"]}
defaultColor={defaultSettings.theme["warninglight"]}
/>
<ColorEditor
name="warning"
onColorChange={onColorChange}
color={customTheme["warning"]}
defaultColor={defaultSettings.theme["warning"]}
/>
<ColorEditor
name="warningdark"
onColorChange={onColorChange}
color={customTheme["warningdark"]}
defaultColor={defaultSettings.theme["warningdark"]}
/>
<br />
<ColorEditor
name="infolight"
onColorChange={onColorChange}
color={customTheme["infolight"]}
defaultColor={defaultSettings.theme["infolight"]}
/>
<ColorEditor
name="info"
onColorChange={onColorChange}
color={customTheme["info"]}
defaultColor={defaultSettings.theme["info"]}
/>
<ColorEditor
name="infodark"
onColorChange={onColorChange}
color={customTheme["infodark"]}
defaultColor={defaultSettings.theme["infodark"]}
/>
<br />
<ColorEditor
name="welllight"
onColorChange={onColorChange}
color={customTheme["welllight"]}
defaultColor={defaultSettings.theme["welllight"]}
/>
<ColorEditor
name="well"
onColorChange={onColorChange}
color={customTheme["well"]}
defaultColor={defaultSettings.theme["well"]}
/>
<ColorEditor
name="white"
onColorChange={onColorChange}
color={customTheme["white"]}
defaultColor={defaultSettings.theme["white"]}
/>
<ColorEditor
name="black"
onColorChange={onColorChange}
color={customTheme["black"]}
defaultColor={defaultSettings.theme["black"]}
/>
<ColorEditor
name="backgroundprimary"
onColorChange={onColorChange}
color={customTheme["backgroundprimary"]}
defaultColor={defaultSettings.theme["backgroundprimary"]}
/>
<ColorEditor
name="backgroundsecondary"
onColorChange={onColorChange}
color={customTheme["backgroundsecondary"]}
defaultColor={defaultSettings.theme["backgroundsecondary"]}
/>
<ColorEditor
name="button"
onColorChange={onColorChange}
color={customTheme["button"]}
defaultColor={defaultSettings.theme["button"]}
/>
<br />
<ColorEditor
name="hp"
onColorChange={onColorChange}
color={customTheme["hp"]}
defaultColor={defaultSettings.theme["hp"]}
/>
<ColorEditor
name="money"
onColorChange={onColorChange}
color={customTheme["money"]}
defaultColor={defaultSettings.theme["money"]}
/>
<ColorEditor
name="hack"
onColorChange={onColorChange}
color={customTheme["hack"]}
defaultColor={defaultSettings.theme["hack"]}
/>
<ColorEditor
name="combat"
onColorChange={onColorChange}
color={customTheme["combat"]}
defaultColor={defaultSettings.theme["combat"]}
/>
<ColorEditor
name="cha"
onColorChange={onColorChange}
color={customTheme["cha"]}
defaultColor={defaultSettings.theme["cha"]}
/>
<ColorEditor
name="int"
onColorChange={onColorChange}
color={customTheme["int"]}
defaultColor={defaultSettings.theme["int"]}
/>
<ColorEditor
name="rep"
onColorChange={onColorChange}
color={customTheme["rep"]}
defaultColor={defaultSettings.theme["rep"]}
/>
<ColorEditor
name="disabled"
onColorChange={onColorChange}
color={customTheme["disabled"]}
defaultColor={defaultSettings.theme["disabled"]}
/>
</Paper>
<Paper sx={{ px: 1, py: 1, my: 1 }}>
<TextField
sx={{ mb: 1 }}
multiline
fullWidth
maxRows={3}
label={"import / export theme"}
value={JSON.stringify(customTheme)}
onChange={onThemeChange}
/>
<>
<Typography sx={{ my: 1 }}>Backup your theme or share it with others by copying the string above.</Typography>
<Typography sx={{ my: 1 }}>Replace the current theme with a pre-built template using the buttons below.</Typography>
{themes}
</>
</Paper>
</Modal>
);
}