pgadmin4/web/pgadmin/static/js/Theme/index.jsx

955 lines
25 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
/* The complete styling file for Material-UI components used
* This will become the main theme file for pgAdmin. All the
* custom themes info will come here.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import CustomPropTypes from '../custom_prop_types';
import getLightTheme from './light';
import getDarkTheme from './dark';
import getHightContrastTheme from './high_contrast';
import { CssBaseline } from '@mui/material';
import pickrOverride from './overrides/pickr.override';
import uplotOverride from './overrides/uplot.override';
import rcdockOverride from './overrides/rcdock.override';
import cmOverride from './overrides/codemirror.override';
import jsonEditorOverride from './overrides/jsoneditor.override';
import pgadminOverride from './overrides/pgadmin.classes.override';
import reactAspenOverride from './overrides/reactaspen.override';
import usePreferences from '../../../preferences/static/js/store';
import szhMenuOverride from './overrides/szhmenu.override';
/* Common settings across all themes */
let basicSettings = createTheme();
basicSettings = createTheme(basicSettings, {
typography: {
fontSize: 14,
htmlFontSize: 14,
fontFamilyIcon: '"Font Awesome 5 Free"',
fontFamilySourceCode: '"Source Code Pro", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontFamily: [
'Roboto',
'"Helvetica Neue"',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(','),
},
shape: {
borderRadius: 4,
},
palette: {
action: {
disabledOpacity: 0.32,
}
},
transitions: {
create: () => 'none',
},
zIndex: {
modal: 3001,
},
components: {
MuiTextField: {
defaultProps: {
variant: 'outlined',
}
},
MuiButton: {
defaultProps: {
disableTouchRipple: true,
variant: 'outlined',
},
styleOverrides: {
root: {
textTransform: 'none',
padding: '2px 10px',
fontSize: 'inherit',
'&.Mui-disabled': {
opacity: 0.60,
},
'&.MuiButton-sizeSmall, &.MuiButton-outlined.MuiButton-sizeSmall, &.MuiButton-contained.MuiButton-sizeSmall': {
height: '28px',
fontSize: '0.875rem',
'& .MuiSvgIcon-root': {
height: '1.2rem',
},
},
},
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
}
},
outlined: {
padding: '3px 9px',
},
startIcon: {
marginRight: basicSettings.spacing(0.5),
},
}
},
MuiIconButton: {
defaultProps: {
size: 'small',
disableTouchRipple: true,
}
},
MuiAccordion: {
defaultProps: {
defaultExpanded: true,
},
styleOverrides: {
root: {
boxShadow: 'none',
}
}
},
MuiTab: {
defaultProps: {
textColor: 'inherit',
},
styleOverrides: {
root: {
lineHeight: '1.75',
textTransform: 'none',
minHeight: 0,
padding: '3px 10px',
[basicSettings.breakpoints.up('xs')]: {
minWidth: 0,
},
[basicSettings.breakpoints.up('sm')]: {
minWidth: 0,
},
[basicSettings.breakpoints.up('md')]: {
minWidth: 0,
},
[basicSettings.breakpoints.up('lg')]: {
minWidth: 0,
},
},
textColorInherit: {
textTransform: 'none',
opacity: 1,
}
}
},
MuiCheckbox: {
defaultProps: {
disableTouchRipple: true,
}
},
MuiDialogTitle: {
defaultProps: {
}
},
MuiCardHeader: {
defaultProps: {
disableTypography: true,
}
},
MuiListItem: {
defaultProps: {
disableGutters: true,
}
},
MuiListItemButton: {
defaultProps: {
disableGutters: true,
disableTouchRipple: true,
}
},
MuiTabs: {
styleOverrides: {
root: {
minHeight: '30px',
}
}
},
PrivateTabIndicator: {
styleOverrides: {
root: {
height: '2px',
transition: basicSettings.transitions.create(['all'], {duration: '150ms'}),
}
}
},
MuiOutlinedInput: {
styleOverrides: {
multiline: {
padding: '0px',
},
input: {
padding: basicSettings.spacing(0.75, 1.5),
borderRadius: 'inherit',
},
inputMultiline: {
padding: basicSettings.spacing(0.75, 1.5),
resize: 'vertical',
height: '100%',
boxSizing: 'border-box',
},
adornedStart: {
paddingLeft: basicSettings.spacing(0.75),
},
inputAdornedStart: {
paddingLeft: '2px',
},
adornedEnd: {
paddingRight: basicSettings.spacing(0.75),
},
marginDense: {
height: '28px',
}
}
},
MuiPickersOutlinedInput: {
styleOverrides: {
multiline: {
padding: '0px',
},
input: {
padding: basicSettings.spacing(0.75, 1.5),
borderRadius: 'inherit',
},
inputMultiline: {
padding: basicSettings.spacing(0.75, 1.5),
resize: 'vertical',
height: '100%',
boxSizing: 'border-box',
},
adornedStart: {
paddingLeft: basicSettings.spacing(0.75),
},
inputAdornedStart: {
paddingLeft: '2px',
},
adornedEnd: {
paddingRight: basicSettings.spacing(0.75),
},
marginDense: {
height: '28px',
},
sectionsContainer: {
padding: '0px',
}
}
},
MuiAccordionSummary: {
styleOverrides: {
root: {
minHeight: 0,
'&.Mui-expanded': {
minHeight: 0,
},
padding: basicSettings.spacing(0, 1),
fontWeight: basicSettings.typography.fontWeightBold
},
content: {
margin: basicSettings.spacing(0.5),
'&.Mui-expanded': {
margin: basicSettings.spacing(0.5),
}
},
expandIconWrapper: {
order: -1,
}
}
},
MuiAccordionDetails: {
styleOverrides: {
root: {
padding: basicSettings.spacing(1),
}
}
},
MuiFormControlLabel: {
styleOverrides: {
root: {
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
gap: '4px'
}
}
},
MuiFormHelperText: {
styleOverrides: {
root: {
fontSize: '1em',
},
contained: {
marginLeft: 0,
marginRight: 0,
}
}
},
MuiTypography: {
styleOverrides: {
root: {
fontSize: '0.875rem',
lineHeight: '1.43em',
letterSpacing: '0.01071em',
},
body1: {
fontSize: '1em',
}
}
},
MuiDialog: {
styleOverrides: {
paper: {
margin: 0,
},
scrollPaper: {
alignItems: 'flex-start',
margin: '5% auto',
}
}
},
MuiTooltip: {
defaultProps: {
arrow: true,
disableInteractive: true
},
styleOverrides: {
popper: {
top: 0,
zIndex: 9999,
},
}
},
MuiMenu: {
styleOverrides: {
list: {
padding: '0',
}
}
},
MuiMenuItem: {
styleOverrides: {
root: {
fontSize: 14,
}
}
},
MuiSelect: {
styleOverrides: {
selectMenu: {
minHeight: 'unset',
},
select:{
'&:focus':{
backgroundColor: 'unset',
}
}
}
}
},
});
/* Get the final theme after merging base theme with selected theme */
function getFinalTheme(baseTheme) {
let mixins = {
panelBorder: {
border: '1px solid '+baseTheme.otherVars.borderColor,
all: {
border: '1px solid '+baseTheme.otherVars.borderColor,
},
top: {
borderTop: '1px solid '+baseTheme.otherVars.borderColor,
},
bottom: {
borderBottom: '1px solid '+baseTheme.otherVars.borderColor,
},
right: {
borderRight: '1px solid '+baseTheme.otherVars.borderColor,
},
left: {
borderLeft: '1px solid '+baseTheme.otherVars.borderColor,
}
},
nodeIcon: {
backgroundPosition: 'center',
padding: baseTheme.spacing(0, 1.5),
},
tabPanel: {
height: '100%',
padding: baseTheme.spacing(1),
overflow: 'auto',
backgroundColor: baseTheme.palette.grey[400],
position: 'relative',
flexGrow: 1,
},
fontSourceCode: {
fontFamily: basicSettings.typography.fontFamilySourceCode,
}
};
baseTheme = createTheme({
mixins: mixins,
}, baseTheme);
return createTheme({
components: {
MuiCssBaseline: {
styleOverrides: {
body: {
fontFamily: baseTheme.typography.fontFamily,
fontSize: '0.875rem',
lineHeight: '1.43em',
letterSpacing: '0.01071em',
height: '100vh',
},
'::-webkit-scrollbar,::-webkit-scrollbar-corner': {
width: '1rem !important',
height: '1rem !important',
background: baseTheme.otherVars.scroll.barBackgroundColor
},
'::-webkit-scrollbar-thumb': {
border: '0.25rem solid transparent',
borderRadius: '0.5rem',
background: baseTheme.otherVars.scroll.thumbBackground + ' !important',
backgroundClip: 'content-box !important',
},
'::-webkit-scrollbar-thumb:hover': {
border: '0.25rem solid transparent',
background: baseTheme.otherVars.scroll.baseColor + ' !important',
backgroundClip: 'content-box !important'
},
'input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus,textarea:-webkit-autofill,textarea:-webkit-autofill:hover,textarea:-webkit-autofill:focus,select:-webkit-autofill,select:-webkit-autofill:hover,select:-webkit-autofill:focus': {
webkitTextFillColor : baseTheme.palette.text.primary,
webkitBoxShadow: '0 0 0px 1000px '+ baseTheme.palette.primary.light +' inset',
transition: 'backgroundColor 5000s ease-in-out 0s',
},
ul: {
margin: 0,
padding: 0,
},
li: {
listStyle: 'none',
margin: 0,
padding: 0,
},
textarea: {
fontFamily: 'inherit',
color: baseTheme.palette.text.primary,
backgroundColor: baseTheme.palette.background.default
},
iframe: {
margin: 0,
padding: 0,
},
svg: {
verticalAlign: 'middle',
},
img: {
verticalAlign: 'middle',
},
...pickrOverride(baseTheme),
...uplotOverride(baseTheme),
...rcdockOverride(baseTheme),
...cmOverride(baseTheme),
...jsonEditorOverride(baseTheme),
...pgadminOverride(baseTheme),
...reactAspenOverride(baseTheme),
...szhMenuOverride(baseTheme)
},
},
MuiOutlinedInput: {
styleOverrides: {
root: {
lineHeight: '1.1876em',
'&.Mui-disabled .MuiOutlinedInput-notchedOutline': {
borderColor: baseTheme.otherVars.inputBorderColor,
},
'.MuiButtonGroup-root &': {
borderRadius: 0,
'& .MuiOutlinedInput-notchedOutline': {
borderRadius: 0,
}
},
},
notchedOutline: {
borderColor: baseTheme.otherVars.inputBorderColor,
}
}
},
MuiPickersTextField: {
styleOverrides: {
root: {
width: '100%',
}
}
},
MuiPickersOutlinedInput: {
styleOverrides: {
root: {
lineHeight: '1.1876em',
fontSize: 'inherit',
backgroundColor: baseTheme.palette.background.default,
textOverflow: 'ellipsis',
'&.Mui-disabled': {
backgroundColor: baseTheme.otherVars.inputDisabledBg,
},
padding: '2px 12px',
},
notchedOutline: {
borderColor: baseTheme.otherVars.inputBorderColor,
},
}
},
MuiPickersSectionList: {
styleOverrides: {
root: {
padding: '0px',
}
}
},
MuiFormControlLabel: {
styleOverrides: {
label: {
'&.Mui-disabled': {
color: baseTheme.palette.text.muted
}
}
}
},
MuiTabs: {
styleOverrides: {
root: {
backgroundColor: baseTheme.otherVars.headerBg,
...mixins.panelBorder.bottom
},
indicator: {
backgroundColor: baseTheme.otherVars.activeColor,
}
}
},
MuiFormLabel: {
styleOverrides: {
root: {
color: baseTheme.palette.text.primary,
fontSize: baseTheme.typography.fontSize,
whiteSpace: 'normal !important'
},
asterisk: {
color: baseTheme.palette.error.main,
}
}
},
MuiInputBase: {
styleOverrides: {
root: {
backgroundColor: baseTheme.palette.background.default,
textOverflow: 'ellipsis',
'&.Mui-disabled': {
backgroundColor: baseTheme.otherVars.inputDisabledBg,
},
},
inputMultiline: {
fontSize: baseTheme.typography.fontSize,
height: 'unset',
backgroundColor: baseTheme.palette.background.default,
'&[readonly], &.Mui-disabled': {
color: baseTheme.palette.text.muted,
backgroundColor: baseTheme.otherVars.inputDisabledBg,
},
},
input: {
fontSize: baseTheme.typography.fontSize,
height: 'unset',
backgroundColor: baseTheme.palette.background.default,
'&[readonly], &.Mui-disabled': {
color: baseTheme.palette.text.muted,
backgroundColor: baseTheme.otherVars.inputDisabledBg,
WebkitTextFillColor: baseTheme.palette.text.muted
},
'&:focus': {
outline: '0 !important',
}
},
sizeSmall: {
height: '28px',
},
inputSizeSmall: {
height: '16px', // + 12px of padding = 28px;
},
}
},
MuiSelect: {
styleOverrides: {
icon: {
color: baseTheme.palette.text.primary,
'&.Mui-disabled': {
color: baseTheme.palette.text.muted,
}
},
}
},
MuiNativeSelect:{
styleOverrides: {
icon: {
color: baseTheme.palette.text.primary,
'&.Mui-disabled': {
color: baseTheme.palette.text.muted,
}
}
}
},
MuiIconButton: {
styleOverrides: {
root: {
color: baseTheme.palette.text.primary,
'&.Mui-disabled': {
color: 'abc',
}
}
}
},
MuiAccordion: {
styleOverrides: {
root: {
...mixins.panelBorder,
'&.Mui-expanded': {
margin: '8px 0px',
},
}
}
},
MuiAccordionSummary: {
styleOverrides: {
root: {
...mixins.panelBorder.bottom,
backgroundColor: baseTheme.otherVars.headerBg,
},
content: {
margin: '4px',
},
expandIconWrapper: {
color: baseTheme.palette.text.primary,
}
}
},
MuiToggleButtonGroup: {
styleOverrides: {
groupedHorizontal : {
'&:not(:first-of-type)': {
borderLeft: 'abc'
}
},
middleButton: {
borderLeftColor: baseTheme.otherVars.borderColor,
},
lastButton: {
borderLeftColor: baseTheme.otherVars.borderColor,
}
}
},
MuiSwitch: {
styleOverrides: {
root: {
width: 54,
height: 28,
padding: '7px 12px',
},
colorPrimary: {
'&.Mui-disabled': {
color: 'abc',
'& + .MuiSwitch-track': {
backgroundColor: 'abc',
}
}
},
switchBase: {
padding: baseTheme.spacing(0.5),
'&.Mui-disabled': {
color: 'abc',
'& + .MuiSwitch-track': {
opacity: baseTheme.palette.action.disabledOpacity,
}
},
'&.Mui-checked': {
transform: 'translateX(24px)',
'& .MuiSwitch-thumb': {
border: 0
}
}
},
thumb: {
border: '1px solid ' + baseTheme.otherVars.inputBorderColor
},
track: {
backgroundColor: baseTheme.otherVars.toggleBtnBg
}
}
},
MuiCheckbox: {
styleOverrides: {
root: {
padding: '0px',
color: baseTheme.custom.checkbox.borderColor,
},
colorPrimary: {
'&.Mui-disabled': {
color: baseTheme.custom.checkbox.disabled
}
}
}
},
MuiRadio: {
styleOverrides: {
root: {
padding: '0px',
color: baseTheme.custom.checkbox.borderColor,
},
colorPrimary: {
'&.Mui-disabled': {
color: baseTheme.custom.checkbox.disabled
}
}
}
},
MuiToggleButton: {
styleOverrides: {
root: {
padding: '3px 16px 3px 4px',
color: 'abc',
textTransform: 'initial',
'&:hover':{
backgroundColor: 'abc',
},
'&.Mui-selected': {
color: baseTheme.palette.primary.contrastTextLight ?? baseTheme.palette.primary.main,
backgroundColor: baseTheme.palette.primary.light,
borderColor: baseTheme.palette.primary.main,
zIndex: 1,
'&:hover':{
backgroundColor: baseTheme.palette.primary.hoverLight,
}
},
backgroundClip: 'padding-box',
},
}
},
MuiFormHelperText: {
styleOverrides: {
root: {
color: baseTheme.palette.text.muted,
},
}
},
MuiDialogContent: {
styleOverrides: {
root: {
padding: 0,
userSelect: 'text',
}
}
},
MuiDialogTitle: {
styleOverrides: {
root: {
fontSize: '0.875rem',
fontWeight: 'bold',
padding: '5px 10px',
cursor: 'move',
display: 'flex',
alignItems: 'center',
...mixins.panelBorder.bottom,
}
}
},
MuiCardHeader: {
styleOverrides: {
root: {
padding: '4px 8px',
backgroundColor: baseTheme.otherVars.cardHeaderBg,
fontWeight: 'bold',
...mixins.panelBorder.bottom,
}
}
},
MuiCardContent: {
styleOverrides: {
root: {
padding: 0,
'&:last-child': {
paddingBottom: 0,
}
}
}
},
MuiListItemButton: {
styleOverrides: {
root: {
color: baseTheme.palette.text.primary,
backgroundColor: baseTheme.palette.background.default,
flexDirection: 'column',
alignItems: 'initial',
padding: '0px 4px',
paddingTop: '0px',
paddingBottom: '0px',
...mixins.panelBorder.top,
...mixins.panelBorder.bottom,
borderTopColor: 'transparent',
cursor: 'pointer',
'&.Mui-selected': {
backgroundColor: baseTheme.palette.primary.light,
borderColor: baseTheme.palette.primary.main,
color: basicSettings.palette.getContrastText(baseTheme.palette.primary.light),
'&:hover': {
backgroundColor: baseTheme.palette.primary.light,
}
},
}
}
},
MuiListItem: {
styleOverrides: {
root: {
color: baseTheme.palette.text.primary,
backgroundColor: baseTheme.palette.background.default,
flexDirection: 'column',
alignItems: 'initial',
padding: '0px 4px',
paddingTop: '0px',
paddingBottom: '0px',
...mixins.panelBorder.top,
...mixins.panelBorder.bottom,
borderTopColor: 'transparent',
cursor: 'pointer',
}
}
},
MuiTooltip: {
styleOverrides: {
tooltip: {
fontSize: '0.7rem',
color: baseTheme.palette.background.default,
backgroundColor: baseTheme.palette.text.primary,
},
arrow: {
color: baseTheme.palette.text.primary,
}
}
},
MuiTab: {
styleOverrides: {
root: {
'&:not(.Mui-disabled).MuiTab-textColorPrimary':{
color: baseTheme.palette.text.primary,
},
'&.Mui-selected': {
color: baseTheme.otherVars.activeColor,
},
},
icon: {
fontSize: '1rem',
marginRight: '2px',
},
}
},
MuiBackdrop: {
styleOverrides: {
root: {
backgroundColor: baseTheme.otherVars.loader.backgroundColor,
}
}
}
}
}, baseTheme);
}
/* Get the actual system theme is user selected system theme in preferences */
function parseSystemTheme(selectedTheme) {
if (selectedTheme === 'system') {
const systemMatchMedia = matchMedia('(prefers-color-scheme: dark)');
return {
'theme': systemMatchMedia.matches ? 'dark' : 'light',
'systemMatchMedia': systemMatchMedia,
};
}
return {
'theme': selectedTheme,
'systemMatchMedia': null,
};
}
/* Theme wrapper used by DOM containers to apply theme */
/* In future, this will be moved to App container */
export default function Theme({children}) {
const prefStore = usePreferences();
const selectedTheme =
prefStore?.getPreferencesForModule('misc')?.theme ||
window.theme ||
'light';
// Initialize theme state
const [theme, setTheme] = useState(parseSystemTheme(selectedTheme).theme);
// Memoize the theme object
const themeObj = useMemo(()=>{
let baseTheme = getLightTheme(basicSettings);
switch(theme) {
case 'dark':
baseTheme = getDarkTheme(baseTheme);
break;
case 'high_contrast':
baseTheme = getHightContrastTheme(baseTheme);
break;
}
return getFinalTheme(baseTheme);
}, [theme]);
// Handle theme updates
useEffect(() => {
let {theme, systemMatchMedia} = parseSystemTheme(selectedTheme);
setTheme(theme);
const updateTheme = (event) => {
const newTheme = event.matches ? 'dark' : 'light';
setTheme(newTheme);
};
systemMatchMedia?.addEventListener('change', updateTheme);
return () => {
systemMatchMedia?.removeEventListener('change', updateTheme);
};
},[selectedTheme]);
return (
<ThemeProvider theme={themeObj}>
<CssBaseline />
<LocalizationProvider dateAdapter={AdapterDateFns}>
{children}
</LocalizationProvider>
</ThemeProvider>
);
}
Theme.propTypes = {
children: CustomPropTypes.children,
};