bui-themer: simplify theme conversion
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/system": "^7.3.2",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -54,7 +54,8 @@ describe('convertMuiToBuiTheme', () => {
|
||||
|
||||
expect(result).toContain(':root {');
|
||||
expect(result).toContain('--bui-font-regular: Roboto, sans-serif;');
|
||||
expect(result).toContain('--bui-space: 8px;');
|
||||
// Spacing is skipped for default 8px
|
||||
expect(result).not.toContain('--bui-space:');
|
||||
expect(result).toContain('--bui-radius-3: 4px;');
|
||||
expect(result).toContain('--bui-bg: #f5f5f5;');
|
||||
expect(result).toContain('--bui-bg-surface-1: #ffffff;');
|
||||
@@ -122,38 +123,6 @@ describe('convertMuiToBuiTheme', () => {
|
||||
expect(result).toContain('--bui-white: #fff;');
|
||||
});
|
||||
|
||||
it('should generate proper gray scale for light theme', () => {
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: '#1976d2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = convertMuiToBuiTheme(theme);
|
||||
|
||||
expect(result).toContain('--bui-gray-1: #f8f8f8;');
|
||||
expect(result).toContain('--bui-gray-8: #595959;');
|
||||
});
|
||||
|
||||
it('should generate proper gray scale for dark theme', () => {
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#90caf9',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = convertMuiToBuiTheme(theme);
|
||||
|
||||
expect(result).toContain('--bui-gray-1: #191919;');
|
||||
expect(result).toContain('--bui-gray-8: #b4b4b4;');
|
||||
});
|
||||
|
||||
it('should generate surface colors correctly', () => {
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
@@ -180,7 +149,7 @@ describe('convertMuiToBuiTheme', () => {
|
||||
const result = convertMuiToBuiTheme(theme);
|
||||
|
||||
expect(result).toContain('--bui-bg-solid: #1976d2;');
|
||||
expect(result).toContain('--bui-bg-solid-hover: #115293;');
|
||||
expect(result).toContain('--bui-bg-solid-hover: rgb(21, 100, 179);');
|
||||
expect(result).toContain('--bui-bg-danger: #ffcdd2;');
|
||||
expect(result).toContain('--bui-bg-warning: #ffe0b2;');
|
||||
expect(result).toContain('--bui-bg-success: #c8e6c9;');
|
||||
@@ -237,8 +206,7 @@ describe('convertMuiToBuiTheme', () => {
|
||||
|
||||
const result = convertMuiToBuiTheme(theme);
|
||||
|
||||
expect(result).toContain('--bui-border: rgba(0, 0, 0, 0.1);');
|
||||
expect(result).toContain('--bui-border-hover: rgba(0, 0, 0, 0.2);');
|
||||
// Base border colors are no longer generated
|
||||
expect(result).toContain('--bui-border-danger: #f44336;');
|
||||
expect(result).toContain('--bui-border-warning: #ff9800;');
|
||||
expect(result).toContain('--bui-border-success: #4caf50;');
|
||||
@@ -251,7 +219,7 @@ describe('convertMuiToBuiTheme', () => {
|
||||
|
||||
const result = convertMuiToBuiTheme(theme);
|
||||
|
||||
expect(result).toContain('--bui-space: 4px;');
|
||||
expect(result).toContain('--bui-space: calc(4px * 0.5);');
|
||||
});
|
||||
|
||||
it('should handle string-based spacing', () => {
|
||||
@@ -261,7 +229,7 @@ describe('convertMuiToBuiTheme', () => {
|
||||
|
||||
const result = convertMuiToBuiTheme(theme);
|
||||
|
||||
expect(result).toContain('--bui-space: calc(1 * 8px);');
|
||||
expect(result).toContain('--bui-space: calc(calc(1 * 8px) * 0.5);');
|
||||
});
|
||||
|
||||
it('should handle string-based border radius', () => {
|
||||
@@ -275,64 +243,4 @@ describe('convertMuiToBuiTheme', () => {
|
||||
|
||||
expect(result).toContain('--bui-radius-3: 8px;');
|
||||
});
|
||||
|
||||
describe('early validation', () => {
|
||||
it('should throw error when theme is undefined', () => {
|
||||
expect(() => convertMuiToBuiTheme(undefined as any)).toThrow(
|
||||
'Theme is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when theme palette is missing', () => {
|
||||
const theme = { typography: {}, spacing: () => 8, shape: {} } as any;
|
||||
expect(() => convertMuiToBuiTheme(theme)).toThrow(
|
||||
'Theme palette is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when theme palette mode is missing', () => {
|
||||
const theme = {
|
||||
palette: {},
|
||||
typography: {},
|
||||
spacing: () => 8,
|
||||
shape: {},
|
||||
} as any;
|
||||
expect(() => convertMuiToBuiTheme(theme)).toThrow(
|
||||
'Theme palette mode is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when theme typography is missing', () => {
|
||||
const theme = {
|
||||
palette: { mode: 'light' },
|
||||
spacing: () => 8,
|
||||
shape: {},
|
||||
} as any;
|
||||
expect(() => convertMuiToBuiTheme(theme)).toThrow(
|
||||
'Theme typography is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when theme spacing is missing', () => {
|
||||
const theme = {
|
||||
palette: { mode: 'light' },
|
||||
typography: {},
|
||||
shape: {},
|
||||
} as any;
|
||||
expect(() => convertMuiToBuiTheme(theme)).toThrow(
|
||||
'Theme spacing is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when theme shape is missing', () => {
|
||||
const theme = {
|
||||
palette: { mode: 'light' },
|
||||
typography: {},
|
||||
spacing: () => 8,
|
||||
} as any;
|
||||
expect(() => convertMuiToBuiTheme(theme)).toThrow(
|
||||
'Theme shape is required',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Theme as Mui5Theme } from '@mui/material/styles';
|
||||
import { themes } from '@backstage/theme';
|
||||
import { blend, alpha } from '@mui/system/colorManipulator';
|
||||
|
||||
export interface ConvertMuiToBuiThemeOptions {
|
||||
/**
|
||||
@@ -38,31 +38,6 @@ export function convertMuiToBuiTheme(
|
||||
theme: Mui5Theme,
|
||||
options: ConvertMuiToBuiThemeOptions = {},
|
||||
): string {
|
||||
// Early validation of required theme properties
|
||||
if (!theme) {
|
||||
throw new Error('Theme is required');
|
||||
}
|
||||
|
||||
if (!theme.palette) {
|
||||
throw new Error('Theme palette is required');
|
||||
}
|
||||
|
||||
if (!theme.palette.mode) {
|
||||
throw new Error('Theme palette mode is required');
|
||||
}
|
||||
|
||||
if (!theme.typography) {
|
||||
throw new Error('Theme typography is required');
|
||||
}
|
||||
|
||||
if (!theme.spacing) {
|
||||
throw new Error('Theme spacing is required');
|
||||
}
|
||||
|
||||
if (!theme.shape) {
|
||||
throw new Error('Theme shape is required');
|
||||
}
|
||||
|
||||
const { themeId, includeThemeId = false } = options;
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
|
||||
@@ -125,13 +100,10 @@ function generateBuiVariables(theme: Mui5Theme): string {
|
||||
}
|
||||
});
|
||||
|
||||
// Spacing - map MUI spacing to BUI spacing scale
|
||||
if (theme.spacing) {
|
||||
const spacingUnit =
|
||||
typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing;
|
||||
const spacingValue =
|
||||
typeof spacingUnit === 'number' ? `${spacingUnit}px` : spacingUnit;
|
||||
variables.push(` --bui-space: ${spacingValue};`);
|
||||
const spacing = theme.spacing(1);
|
||||
// Skip spacing if the theme is using the default
|
||||
if (spacing !== '8px') {
|
||||
variables.push(` --bui-space: calc(${spacing} * 0.5);`);
|
||||
}
|
||||
|
||||
// Border radius
|
||||
@@ -161,8 +133,21 @@ function generateBuiVariables(theme: Mui5Theme): string {
|
||||
}
|
||||
|
||||
// Generate surface colors
|
||||
const surfaceColors = generateSurfaceColors(palette);
|
||||
Object.entries(surfaceColors).forEach(([key, value]) => {
|
||||
Object.entries({
|
||||
'surface-1': palette.background.default,
|
||||
'surface-2': palette.background.paper,
|
||||
solid: palette.primary.main,
|
||||
'solid-hover': blend(palette.primary.main, palette.primary.dark, 0.5),
|
||||
'solid-pressed': palette.primary.dark,
|
||||
'solid-disabled': palette.action.disabledBackground,
|
||||
tint: 'transparent',
|
||||
'tint-hover': alpha(palette.primary.main, 0.4),
|
||||
'tint-pressed': alpha(palette.primary.main, 0.6),
|
||||
'tint-disabled': palette.action.disabledBackground,
|
||||
danger: palette.error.light,
|
||||
warning: palette.warning.light,
|
||||
success: palette.success.light,
|
||||
}).forEach(([key, value]) => {
|
||||
variables.push(` --bui-bg-${key}: ${value};`);
|
||||
});
|
||||
|
||||
@@ -175,14 +160,27 @@ function generateBuiVariables(theme: Mui5Theme): string {
|
||||
}
|
||||
|
||||
// Generate foreground colors
|
||||
const foregroundColors = generateForegroundColors(palette);
|
||||
Object.entries(foregroundColors).forEach(([key, value]) => {
|
||||
Object.entries({
|
||||
link: palette.primary.main,
|
||||
'link-hover': palette.primary.dark,
|
||||
disabled: palette.text.disabled,
|
||||
solid: palette.text.primary,
|
||||
'solid-disabled': palette.action.disabled,
|
||||
tint: palette.primary.main,
|
||||
'tint-disabled': palette.action.disabled,
|
||||
danger: palette.error.main,
|
||||
warning: palette.warning.main,
|
||||
success: palette.success.main,
|
||||
}).forEach(([key, value]) => {
|
||||
variables.push(` --bui-fg-${key}: ${value};`);
|
||||
});
|
||||
|
||||
// Border colors
|
||||
const borderColors = generateBorderColors(palette);
|
||||
Object.entries(borderColors).forEach(([key, value]) => {
|
||||
Object.entries({
|
||||
danger: palette.error.main,
|
||||
warning: palette.warning.main,
|
||||
success: palette.success.main,
|
||||
}).forEach(([key, value]) => {
|
||||
variables.push(` --bui-border${key ? `-${key}` : ''}: ${value};`);
|
||||
});
|
||||
|
||||
@@ -193,233 +191,3 @@ function generateBuiVariables(theme: Mui5Theme): string {
|
||||
|
||||
return variables.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates surface background colors
|
||||
*/
|
||||
function generateSurfaceColors(
|
||||
palette: Mui5Theme['palette'],
|
||||
): Record<string, string> {
|
||||
const isDark = palette.mode === 'dark';
|
||||
|
||||
// Get the default Backstage theme for fallback values
|
||||
const defaultTheme = isDark ? themes.dark : themes.light;
|
||||
const defaultMuiTheme = defaultTheme.getTheme('v5');
|
||||
|
||||
if (!defaultMuiTheme) {
|
||||
throw new Error(
|
||||
`Failed to get MUI v5 theme from Backstage ${
|
||||
isDark ? 'dark' : 'light'
|
||||
} theme`,
|
||||
);
|
||||
}
|
||||
|
||||
const defaultPalette = defaultMuiTheme.palette;
|
||||
if (!defaultPalette) {
|
||||
throw new Error(
|
||||
`Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to get tint colors
|
||||
const getTintColor = (opacity: string) => {
|
||||
const primaryColor = palette.primary?.main || defaultPalette.primary?.main;
|
||||
if (!primaryColor) {
|
||||
throw new Error('Primary color not found in current or default theme');
|
||||
}
|
||||
return `${primaryColor}${opacity}`;
|
||||
};
|
||||
|
||||
// Helper function to get colors based on theme mode
|
||||
const getThemeColor = (lightColor: string, darkColor: string) => {
|
||||
return isDark ? darkColor : lightColor;
|
||||
};
|
||||
|
||||
return {
|
||||
'surface-2': getThemeColor('#ececec', '#242424'),
|
||||
solid:
|
||||
palette.primary?.main ||
|
||||
defaultPalette.primary?.main ||
|
||||
(() => {
|
||||
throw new Error('Primary color not found in current or default theme');
|
||||
})(),
|
||||
'solid-hover':
|
||||
palette.primary?.dark ||
|
||||
defaultPalette.primary?.dark ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Primary dark color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
'solid-pressed':
|
||||
palette.primary?.dark ||
|
||||
defaultPalette.primary?.dark ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Primary dark color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
'solid-disabled': getThemeColor('#ebebeb', '#222222'),
|
||||
tint: 'transparent',
|
||||
'tint-hover': getTintColor('40'),
|
||||
'tint-pressed': getTintColor('60'),
|
||||
'tint-disabled': getThemeColor('#ebebeb', 'transparent'),
|
||||
danger:
|
||||
palette.error?.light ||
|
||||
defaultPalette.error?.light ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Error light color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
warning:
|
||||
palette.warning?.light ||
|
||||
defaultPalette.warning?.light ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Warning light color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
success:
|
||||
palette.success?.light ||
|
||||
defaultPalette.success?.light ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Success light color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates foreground colors
|
||||
*/
|
||||
function generateForegroundColors(
|
||||
palette: Mui5Theme['palette'],
|
||||
): Record<string, string> {
|
||||
const isDark = palette.mode === 'dark';
|
||||
|
||||
// Get the default Backstage theme for fallback values
|
||||
const defaultTheme = isDark ? themes.dark : themes.light;
|
||||
const defaultMuiTheme = defaultTheme.getTheme('v5');
|
||||
|
||||
if (!defaultMuiTheme) {
|
||||
throw new Error(
|
||||
`Failed to get MUI v5 theme from Backstage ${
|
||||
isDark ? 'dark' : 'light'
|
||||
} theme`,
|
||||
);
|
||||
}
|
||||
|
||||
const defaultPalette = defaultMuiTheme.palette;
|
||||
if (!defaultPalette) {
|
||||
throw new Error(
|
||||
`Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
link:
|
||||
palette.primary?.main ||
|
||||
defaultPalette.primary?.main ||
|
||||
(() => {
|
||||
throw new Error('Primary color not found in current or default theme');
|
||||
})(),
|
||||
'link-hover':
|
||||
palette.primary?.dark ||
|
||||
defaultPalette.primary?.dark ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Primary dark color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
disabled:
|
||||
palette.text?.disabled ||
|
||||
defaultPalette.text?.disabled ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Text disabled color not found in current or default theme',
|
||||
);
|
||||
})(),
|
||||
solid: isDark ? '#101821' : '#ffffff',
|
||||
'solid-disabled': isDark ? '#575757' : '#9c9c9c',
|
||||
tint:
|
||||
palette.primary?.main ||
|
||||
defaultPalette.primary?.main ||
|
||||
(() => {
|
||||
throw new Error('Primary color not found in current or default theme');
|
||||
})(),
|
||||
'tint-disabled': isDark ? '#575757' : '#9e9e9e',
|
||||
danger:
|
||||
palette.error?.main ||
|
||||
defaultPalette.error?.main ||
|
||||
(() => {
|
||||
throw new Error('Error color not found in current or default theme');
|
||||
})(),
|
||||
warning:
|
||||
palette.warning?.main ||
|
||||
defaultPalette.warning?.main ||
|
||||
(() => {
|
||||
throw new Error('Warning color not found in current or default theme');
|
||||
})(),
|
||||
success:
|
||||
palette.success?.main ||
|
||||
defaultPalette.success?.main ||
|
||||
(() => {
|
||||
throw new Error('Success color not found in current or default theme');
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates border colors
|
||||
*/
|
||||
function generateBorderColors(
|
||||
palette: Mui5Theme['palette'],
|
||||
): Record<string, string> {
|
||||
const isDark = palette.mode === 'dark';
|
||||
|
||||
// Get the default Backstage theme for fallback values
|
||||
const defaultTheme = isDark ? themes.dark : themes.light;
|
||||
const defaultMuiTheme = defaultTheme.getTheme('v5');
|
||||
|
||||
if (!defaultMuiTheme) {
|
||||
throw new Error(
|
||||
`Failed to get MUI v5 theme from Backstage ${
|
||||
isDark ? 'dark' : 'light'
|
||||
} theme`,
|
||||
);
|
||||
}
|
||||
|
||||
const defaultPalette = defaultMuiTheme.palette;
|
||||
if (!defaultPalette) {
|
||||
throw new Error(
|
||||
`Failed to get palette from Backstage ${isDark ? 'dark' : 'light'} theme`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
'': isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)',
|
||||
hover: isDark ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.2)',
|
||||
pressed: isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.4)',
|
||||
disabled: isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
|
||||
danger:
|
||||
palette.error?.main ||
|
||||
defaultPalette.error?.main ||
|
||||
(() => {
|
||||
throw new Error('Error color not found in current or default theme');
|
||||
})(),
|
||||
warning:
|
||||
palette.warning?.main ||
|
||||
defaultPalette.warning?.main ||
|
||||
(() => {
|
||||
throw new Error('Warning color not found in current or default theme');
|
||||
})(),
|
||||
success:
|
||||
palette.success?.main ||
|
||||
defaultPalette.success?.main ||
|
||||
(() => {
|
||||
throw new Error('Success color not found in current or default theme');
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4348,6 +4348,7 @@ __metadata:
|
||||
"@material-ui/icons": "npm:^4.9.1"
|
||||
"@material-ui/lab": "npm:^4.0.0-alpha.61"
|
||||
"@mui/material": "npm:^7.3.2"
|
||||
"@mui/system": "npm:^7.3.2"
|
||||
"@testing-library/jest-dom": "npm:^6.0.0"
|
||||
"@testing-library/react": "npm:^14.0.0"
|
||||
"@testing-library/user-event": "npm:^14.0.0"
|
||||
|
||||
Reference in New Issue
Block a user