bui-themer: simplify theme conversion

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-09-13 15:07:43 +02:00
parent 71cb5327c5
commit 6c0a51b3f7
4 changed files with 45 additions and 367 deletions
+1
View File
@@ -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');
})(),
};
}
+1
View File
@@ -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"