theme: add unified theme abstraction + creators and base theme options
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
committed by
Philipp Hugenroth
parent
744b365476
commit
f25222d7c1
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Theme as Mui4Theme,
|
||||
createTheme as createV4Theme,
|
||||
ThemeOptions as ThemeOptionsV4,
|
||||
} from '@material-ui/core/styles';
|
||||
import { PaletteOptions as PaletteOptionsV4 } from '@material-ui/core/styles/createPalette';
|
||||
import { PaletteOptions as PaletteOptionsV5 } from '@mui/material/styles/createPalette';
|
||||
import {
|
||||
adaptV4Theme,
|
||||
Theme as Mui5Theme,
|
||||
createTheme as createV5Theme,
|
||||
ThemeOptions as ThemeOptionsV5,
|
||||
} from '@mui/material/styles';
|
||||
import { transformV5ComponentThemesToV4 } from './overrides';
|
||||
import { PageTheme } from '../types';
|
||||
import { defaultComponentThemes } from '../v5';
|
||||
import { createBaseThemeOptions } from './createBaseThemeOptions';
|
||||
import { UnifiedTheme } from './types';
|
||||
|
||||
export class UnifiedThemeHolder implements UnifiedTheme {
|
||||
#themes = new Map<string, unknown>();
|
||||
|
||||
constructor(v4?: Mui4Theme, v5?: Mui5Theme) {
|
||||
this.#themes = new Map();
|
||||
if (v4) {
|
||||
this.#themes.set('v4', v4);
|
||||
}
|
||||
if (v5) {
|
||||
this.#themes.set('v5', v5);
|
||||
}
|
||||
}
|
||||
|
||||
getTheme(version: string): unknown | undefined {
|
||||
return this.#themes.get(version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a new {@link UnifiedTheme}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface UnifiedThemeOptions {
|
||||
palette: PaletteOptionsV4 & PaletteOptionsV5;
|
||||
defaultPageTheme?: string;
|
||||
pageTheme?: Record<string, PageTheme>;
|
||||
fontFamily?: string;
|
||||
htmlFontSize?: number;
|
||||
components?: ThemeOptionsV5['components'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link UnifiedTheme} using the provided options.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createUnifiedTheme(
|
||||
options: UnifiedThemeOptions,
|
||||
): UnifiedThemeHolder {
|
||||
const themeOptions = createBaseThemeOptions(options);
|
||||
const components = { ...defaultComponentThemes, ...options.components };
|
||||
const v5Theme = createV5Theme({ ...themeOptions, components });
|
||||
const v4Overrides = transformV5ComponentThemesToV4(v5Theme, components);
|
||||
const v4Theme = { ...createV4Theme(themeOptions), ...v4Overrides };
|
||||
|
||||
return new UnifiedThemeHolder(v4Theme, v5Theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link UnifiedTheme} using MUI v4 theme options.
|
||||
* Note that this uses `adaptV4Theme` from MUI v5, which is deprecated.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createUnifiedThemeFromV4(options: ThemeOptionsV4) {
|
||||
const v4Theme = createV4Theme(options);
|
||||
const v5Theme = adaptV4Theme(options as any);
|
||||
|
||||
return new UnifiedThemeHolder(v4Theme, v5Theme);
|
||||
}
|
||||
+5
-5
@@ -24,15 +24,15 @@ import {
|
||||
ThemeProvider as Mui5Provider,
|
||||
} from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { MultiThemeHolder } from './types';
|
||||
import { UnifiedTheme } from './types';
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
theme: MultiThemeHolder;
|
||||
theme: UnifiedTheme;
|
||||
noCssBaseline?: boolean;
|
||||
}
|
||||
|
||||
export function MultiThemeProvider(props: ThemeProviderProps) {
|
||||
export function UnifiedThemeProvider(props: ThemeProviderProps) {
|
||||
const { children, theme, noCssBaseline } = props;
|
||||
|
||||
let result = noCssBaseline ? (
|
||||
@@ -44,12 +44,12 @@ export function MultiThemeProvider(props: ThemeProviderProps) {
|
||||
</>
|
||||
);
|
||||
|
||||
const v4Theme = theme.getThemeForVersion('v4');
|
||||
const v4Theme = theme.getTheme('v4');
|
||||
if (v4Theme) {
|
||||
result = <Mui4Provider theme={v4Theme as Mui4Theme}>{result}</Mui4Provider>;
|
||||
}
|
||||
|
||||
const v5Theme = theme.getThemeForVersion('v5');
|
||||
const v5Theme = theme.getTheme('v5');
|
||||
if (v5Theme) {
|
||||
result = <Mui5Provider theme={v5Theme as Mui5Theme}>{result}</Mui5Provider>;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PageTheme, PageThemeSelector } from '../types';
|
||||
import { pageTheme as defaultPageThemes } from '../pageTheme';
|
||||
|
||||
const DEFAULT_HTML_FONT_SIZE = 16;
|
||||
const DEFAULT_FONT_FAMILY =
|
||||
'"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif';
|
||||
const DEFAULT_PAGE_THEME = 'home';
|
||||
|
||||
export interface BaseThemeOptionsInput<PaletteOptions> {
|
||||
palette: PaletteOptions;
|
||||
defaultPageTheme?: string;
|
||||
pageTheme?: Record<string, PageTheme>;
|
||||
fontFamily?: string;
|
||||
htmlFontSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for creating theme options.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createBaseThemeOptions<PaletteOptions>(
|
||||
options: BaseThemeOptionsInput<PaletteOptions>,
|
||||
) {
|
||||
const {
|
||||
palette,
|
||||
htmlFontSize = DEFAULT_HTML_FONT_SIZE,
|
||||
fontFamily = DEFAULT_FONT_FAMILY,
|
||||
defaultPageTheme = DEFAULT_PAGE_THEME,
|
||||
pageTheme = defaultPageThemes,
|
||||
} = options;
|
||||
|
||||
if (!pageTheme[defaultPageTheme]) {
|
||||
throw new Error(`${defaultPageTheme} is not defined in pageTheme.`);
|
||||
}
|
||||
|
||||
return {
|
||||
palette,
|
||||
typography: {
|
||||
htmlFontSize,
|
||||
fontFamily,
|
||||
h6: {
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
marginBottom: 2,
|
||||
},
|
||||
h5: {
|
||||
fontWeight: 700,
|
||||
fontSize: 24,
|
||||
marginBottom: 4,
|
||||
},
|
||||
h4: {
|
||||
fontWeight: 700,
|
||||
fontSize: 28,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 32,
|
||||
fontWeight: 700,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 40,
|
||||
fontWeight: 700,
|
||||
marginBottom: 8,
|
||||
},
|
||||
h1: {
|
||||
fontSize: 54,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
},
|
||||
page: pageTheme[defaultPageTheme],
|
||||
getPageTheme: ({ themeId }: PageThemeSelector) =>
|
||||
pageTheme[themeId] ?? pageTheme[defaultPageTheme],
|
||||
};
|
||||
}
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { MultiThemeProvider } from './MultiThemeProvider';
|
||||
export type { MultiThemeHolder } from './types';
|
||||
export { UnifiedThemeProvider } from './UnifiedThemeProvider';
|
||||
export type { UnifiedTheme } from './types';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Overrides } from '@material-ui/core/styles/overrides';
|
||||
import { ComponentsProps } from '@material-ui/core/styles/props';
|
||||
import { ComponentsOverrides, Theme } from '@mui/material/styles';
|
||||
import { ComponentsOverrides, Theme, ThemeOptions } from '@mui/material/styles';
|
||||
|
||||
type V5Override = ComponentsOverrides[keyof ComponentsOverrides];
|
||||
type V4Override = Overrides[keyof Overrides];
|
||||
@@ -46,7 +46,7 @@ function adaptV5Override(theme: Theme, overrides: V5Override): V4Override {
|
||||
// Transform v5 theme overrides into a v4 theme, by converting the callback-based overrides
|
||||
export function transformV5ComponentThemesToV4(
|
||||
theme: Theme,
|
||||
components: Theme['components'] = {},
|
||||
components: ThemeOptions['components'] = {},
|
||||
): { overrides: Overrides; props: ComponentsProps } {
|
||||
const overrides: Record<string, V4Override> = {};
|
||||
const props: Record<string, ComponentsProps[keyof ComponentsProps]> = {};
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface MultiThemeHolder {
|
||||
getThemeForVersion(version: string): unknown | undefined;
|
||||
/**
|
||||
* A container of one theme for multiple different MUI versions.
|
||||
*
|
||||
* Currently known keys are 'v4' and 'v5'.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface UnifiedTheme {
|
||||
getTheme(version: string): unknown | undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { darken, lighten, ThemeOptions } from '@mui/material/styles';
|
||||
|
||||
/**
|
||||
* A helper for creating theme overrides.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const defaultComponentThemes: ThemeOptions['components'] = {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: theme => ({
|
||||
'@global': {
|
||||
html: {
|
||||
height: '100%',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
},
|
||||
body: {
|
||||
height: '100%',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
'overscroll-behavior-y': 'none',
|
||||
},
|
||||
a: {
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
MuiGrid: {
|
||||
defaultProps: {
|
||||
spacing: 2,
|
||||
},
|
||||
},
|
||||
MuiSwitch: {
|
||||
defaultProps: {
|
||||
color: 'primary',
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
// Alternating row backgrounds
|
||||
root: ({ theme }) => ({
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}),
|
||||
// Use pointer for hoverable rows
|
||||
hover: {
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
// Alternating head backgrounds
|
||||
head: ({ theme }) => ({
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Tables are more dense than default mui tables
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '1',
|
||||
margin: 0,
|
||||
padding: theme.spacing(3, 2, 3, 2.5),
|
||||
borderBottom: 0,
|
||||
}),
|
||||
sizeSmall: ({ theme }) => ({
|
||||
padding: theme.spacing(1.5, 2, 1.5, 2.5),
|
||||
}),
|
||||
head: {
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'hidden',
|
||||
color: 'rgb(179, 179, 179)',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
styleOverrides: {
|
||||
// Tabs are smaller than default mui tab rows
|
||||
root: {
|
||||
minHeight: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
// Tabs are smaller and have a hover background
|
||||
root: ({ theme }) => ({
|
||||
color: theme.palette.link,
|
||||
minHeight: 24,
|
||||
textTransform: 'initial',
|
||||
letterSpacing: '0.07em',
|
||||
'&:hover': {
|
||||
color: darken(theme.palette.link, 0.3),
|
||||
background: lighten(theme.palette.link, 0.95),
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
minWidth: 120,
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
fontWeight: 500,
|
||||
},
|
||||
}),
|
||||
textColorPrimary: ({ theme }) => ({
|
||||
color: theme.palette.link,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiTableSortLabel: {
|
||||
styleOverrides: {
|
||||
// No color change on hover, just rely on the arrow showing up instead.
|
||||
root: {
|
||||
color: 'inherit',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'&:focus': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
// Bold font for highlighting selected column
|
||||
active: {
|
||||
fontWeight: 'bold',
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemText: {
|
||||
styleOverrides: {
|
||||
dense: {
|
||||
// Default dense list items to adding ellipsis for really long str...
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
text: {
|
||||
// Text buttons have less padding by default, but we want to keep the original padding
|
||||
padding: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
backgroundColor: '#D9D9D9',
|
||||
// By default there's no margin, but it's usually wanted, so we add some trailing margin
|
||||
marginRight: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.grey[900],
|
||||
}),
|
||||
outlined: ({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
}),
|
||||
label: ({ theme }) => ({
|
||||
lineHeight: `${theme.spacing(2.5)}px`,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
fontSize: `${theme.spacing(1.75)}px`,
|
||||
}),
|
||||
labelSmall: ({ theme }) => ({
|
||||
fontSize: `${theme.spacing(1.5)}px`,
|
||||
}),
|
||||
deleteIcon: ({ theme }) => ({
|
||||
color: theme.palette.grey[500],
|
||||
width: `${theme.spacing(3)}px`,
|
||||
height: `${theme.spacing(3)}px`,
|
||||
margin: `0 ${theme.spacing(0.75)}px 0 -${theme.spacing(0.75)}px`,
|
||||
}),
|
||||
deleteIconSmall: ({ theme }) => ({
|
||||
width: `${theme.spacing(2)}px`,
|
||||
height: `${theme.spacing(2)}px`,
|
||||
margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// When cards have a forced size, such as when they are arranged in a
|
||||
// CSS grid, the content needs to flex such that the actions (buttons
|
||||
// etc) end up at the bottom of the card instead of just below the body
|
||||
// contents.
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardHeader: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// Reduce padding between header and content
|
||||
paddingBottom: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// When cards have a forced size, such as when they are arranged in a
|
||||
// CSS grid, the content needs to flex such that the actions (buttons
|
||||
// etc) end up at the bottom of the card instead of just below the body
|
||||
// contents.
|
||||
flexGrow: 1,
|
||||
'&:last-child': {
|
||||
paddingBottom: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardActions: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// We default to putting the card actions at the end
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export { defaultComponentThemes } from './defaultComponentThemes';
|
||||
|
||||
@@ -14,7 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePaletteAdditions, BackstageThemeAdditions } from '../types';
|
||||
import { PaletteOptions } from '@mui/material/styles';
|
||||
import {
|
||||
BackstagePaletteAdditions,
|
||||
BackstageThemeAdditions,
|
||||
PageTheme,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* A simpler configuration for creating a new theme that just tweaks some parts
|
||||
* of the backstage one.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SimpleV5ThemeOptions = {
|
||||
palette: PaletteOptions;
|
||||
defaultPageTheme?: string;
|
||||
pageTheme?: Record<string, PageTheme>;
|
||||
fontFamily?: string;
|
||||
htmlFontSize?: number;
|
||||
};
|
||||
|
||||
declare module '@mui/material/styles/createPalette' {
|
||||
interface Palette extends BackstagePaletteAdditions {}
|
||||
|
||||
Reference in New Issue
Block a user