diff --git a/plugins/bui-themer/package.json b/plugins/bui-themer/package.json index ff97c92ecb..64cad56775 100644 --- a/plugins/bui-themer/package.json +++ b/plugins/bui-themer/package.json @@ -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": { diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts index ffbd4fc392..7c11dd7d25 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.test.ts @@ -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', - ); - }); - }); }); diff --git a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts index dbd60c6ea6..1d6e581b57 100644 --- a/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts +++ b/plugins/bui-themer/src/components/BuiThemerPage/convertMuiToBuiTheme.ts @@ -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 { - 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 { - 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 { - 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'); - })(), - }; -} diff --git a/yarn.lock b/yarn.lock index 41f7dc6d64..9dd2bd9e16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"