bui-themer: refactor the conversion to style object first

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-09-13 17:36:04 +02:00
parent 728b007505
commit 9768c9bbca
3 changed files with 91 additions and 81 deletions
@@ -49,10 +49,10 @@ interface ThemeContentProps {
interface IsolatedPreviewProps {
mode: 'light' | 'dark';
css: string;
styleObject: Record<string, string>;
}
function BuiThemePreview({ mode, css }: IsolatedPreviewProps) {
function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) {
return (
<div
// Setting the theme mode ensures that the correct defaults are picked up from the BUI theme
@@ -60,16 +60,7 @@ function BuiThemePreview({ mode, css }: IsolatedPreviewProps) {
style={{
// Apply the generated CSS variables directly to this container
// This creates a scoped context where the variables take precedence
...Object.fromEntries(
css
.split('\n')
.filter(line => line.trim().startsWith('--bui-'))
.map(line => {
const [key, value] = line.trim().split(':');
return [key?.trim(), value?.replace(';', '').trim()];
})
.filter(([key, value]) => key && value),
),
...styleObject,
width: '100%',
backgroundColor: 'var(--bui-bg-surface-2)',
padding: 'var(--bui-space-3)',
@@ -178,7 +169,7 @@ function ThemeContent({
const [generatedCss, setGeneratedCss] = useState<string>('');
const [activeTab, setActiveTab] = useState<string>('css');
const css = useMemo(() => {
const { css, styleObject } = useMemo(() => {
return convertMuiToBuiTheme(muiTheme);
}, [muiTheme]);
@@ -258,7 +249,7 @@ function ThemeContent({
<TabPanel id="preview">
<Box>
<BuiThemePreview mode={variant} css={generatedCss} />
<BuiThemePreview mode={variant} styleObject={styleObject} />
</Box>
</TabPanel>
</Tabs>
@@ -52,16 +52,24 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain(':root {');
expect(result).toContain('--bui-font-regular: Roboto, sans-serif;');
expect(result.css).toContain(':root {');
expect(result.css).toContain('--bui-font-regular: Roboto, sans-serif;');
// 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;');
expect(result).toContain('--bui-fg-primary: #000000;');
expect(result).toContain('--bui-fg-secondary: #666666;');
expect(result).toContain('--bui-bg-solid: #1976d2;');
expect(result.css).not.toContain('--bui-space:');
expect(result.css).toContain('--bui-radius-3: 4px;');
expect(result.css).toContain('--bui-bg: #f5f5f5;');
expect(result.css).toContain('--bui-bg-surface-1: #ffffff;');
expect(result.css).toContain('--bui-fg-primary: #000000;');
expect(result.css).toContain('--bui-fg-secondary: #666666;');
expect(result.css).toContain('--bui-bg-solid: #1976d2;');
// Test style object
expect(result.styleObject).toHaveProperty(
'--bui-font-regular',
'Roboto, sans-serif',
);
expect(result.styleObject).toHaveProperty('--bui-bg-solid', '#1976d2');
expect(result.styleObject).toHaveProperty('--bui-fg-primary', '#000000');
});
it('should generate CSS for dark theme', () => {
@@ -85,11 +93,15 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain("[data-theme-mode='dark'] {");
expect(result).toContain('--bui-bg: #121212;');
expect(result).toContain('--bui-bg-surface-1: #1e1e1e;');
expect(result).toContain('--bui-fg-primary: #ffffff;');
expect(result).toContain('--bui-fg-secondary: #b3b3b3;');
expect(result.css).toContain("[data-theme-mode='dark'] {");
expect(result.css).toContain('--bui-bg: #121212;');
expect(result.css).toContain('--bui-bg-surface-1: #1e1e1e;');
expect(result.css).toContain('--bui-fg-primary: #ffffff;');
expect(result.css).toContain('--bui-fg-secondary: #b3b3b3;');
// Test style object
expect(result.styleObject).toHaveProperty('--bui-bg-surface-1', '#1e1e1e');
expect(result.styleObject).toHaveProperty('--bui-fg-primary', '#ffffff');
});
it('should handle missing theme properties gracefully', () => {
@@ -97,12 +109,12 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain(':root {');
expect(result).toContain('--bui-font-weight-regular: 400;');
expect(result).toContain('--bui-font-weight-bold: 700;');
expect(result.css).toContain(':root {');
expect(result.css).toContain('--bui-font-weight-regular: 400;');
expect(result.css).toContain('--bui-font-weight-bold: 700;');
// Should have default values even when theme properties are missing
expect(result).toContain('--bui-black: #000;');
expect(result).toContain('--bui-white: #fff;');
expect(result.css).toContain('--bui-black: #000;');
expect(result.css).toContain('--bui-white: #fff;');
});
it('should generate surface colors correctly', () => {
@@ -130,11 +142,11 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-bg-solid: #1976d2;');
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;');
expect(result.css).toContain('--bui-bg-solid: #1976d2;');
expect(result.css).toContain('--bui-bg-solid-hover: rgb(21, 100, 179);');
expect(result.css).toContain('--bui-bg-danger: #ffcdd2;');
expect(result.css).toContain('--bui-bg-warning: #ffe0b2;');
expect(result.css).toContain('--bui-bg-success: #c8e6c9;');
});
it('should generate foreground colors correctly', () => {
@@ -162,12 +174,12 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-fg-link: #1976d2;');
expect(result).toContain('--bui-fg-link-hover: #115293;');
expect(result).toContain('--bui-fg-disabled: #cccccc;');
expect(result).toContain('--bui-fg-danger: #f44336;');
expect(result).toContain('--bui-fg-warning: #ff9800;');
expect(result).toContain('--bui-fg-success: #4caf50;');
expect(result.css).toContain('--bui-fg-link: #1976d2;');
expect(result.css).toContain('--bui-fg-link-hover: #115293;');
expect(result.css).toContain('--bui-fg-disabled: #cccccc;');
expect(result.css).toContain('--bui-fg-danger: #f44336;');
expect(result.css).toContain('--bui-fg-warning: #ff9800;');
expect(result.css).toContain('--bui-fg-success: #4caf50;');
});
it('should generate border colors correctly', () => {
@@ -189,9 +201,9 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
// 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;');
expect(result.css).toContain('--bui-border-danger: #f44336;');
expect(result.css).toContain('--bui-border-warning: #ff9800;');
expect(result.css).toContain('--bui-border-success: #4caf50;');
});
it('should handle function-based spacing', () => {
@@ -201,7 +213,7 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-space: calc(4px * 0.5);');
expect(result.css).toContain('--bui-space: calc(4px * 0.5);');
});
it('should handle string-based spacing', () => {
@@ -211,7 +223,7 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-space: calc(calc(1 * 8px) * 0.5);');
expect(result.css).toContain('--bui-space: calc(calc(1 * 8px) * 0.5);');
});
it('should handle string-based border radius', () => {
@@ -223,6 +235,6 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-radius-3: 8px;');
expect(result.css).toContain('--bui-radius-3: 8px;');
});
});
@@ -17,39 +17,48 @@
import { Theme as Mui5Theme } from '@mui/material/styles';
import { blend, alpha } from '@mui/system/colorManipulator';
export interface ConvertMuiToBuiThemeResult {
css: string;
styleObject: Record<string, string>;
}
/**
* Converts a MUI v5 Theme to BUI CSS variables
* @param theme - The MUI v5 theme to convert
* @returns CSS string with BUI variables
* @returns Object containing CSS string and style object with BUI variables
*/
export function convertMuiToBuiTheme(theme: Mui5Theme): string {
const variables = generateBuiVariables(theme);
export function convertMuiToBuiTheme(
theme: Mui5Theme,
): ConvertMuiToBuiThemeResult {
const styleObject = generateBuiVariables(theme);
const selector =
theme.palette.mode === 'dark' ? "[data-theme-mode='dark']" : ':root';
return `${selector} {\n${variables}\n}`;
const css = `${selector} {\n${Object.entries(styleObject)
.map(([key, value]) => ` ${key}: ${value};`)
.join('\n')}\n}`;
return { css, styleObject };
}
/**
* Generates BUI CSS variables from MUI theme
*/
function generateBuiVariables(theme: Mui5Theme): string {
const variables: string[] = [];
function generateBuiVariables(theme: Mui5Theme): Record<string, string> {
const styleObject: Record<string, string> = {};
// Font families
if (theme.typography.fontFamily) {
variables.push(` --bui-font-regular: ${theme.typography.fontFamily};`);
styleObject['--bui-font-regular'] = theme.typography.fontFamily;
}
// Font weights
variables.push(
` --bui-font-weight-regular: ${
theme.typography.fontWeightRegular || 400
};`,
styleObject['--bui-font-weight-regular'] = String(
theme.typography.fontWeightRegular || 400,
);
variables.push(
` --bui-font-weight-bold: ${theme.typography.fontWeightBold || 600};`,
styleObject['--bui-font-weight-bold'] = String(
theme.typography.fontWeightBold || 600,
);
// Font sizes - map MUI typography scale to BUI scale
@@ -74,21 +83,22 @@ function generateBuiVariables(theme: Mui5Theme): string {
? typographyVariant.fontSize
: undefined;
if (fontSize) {
variables.push(` ${buiVar}: ${fontSize};`);
styleObject[buiVar] =
typeof fontSize === 'string' ? fontSize : `${fontSize}px`;
}
});
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);`);
styleObject['--bui-space'] = `calc(${spacing} * 0.5)`;
}
// Border radius
if (theme.shape?.borderRadius) {
const radius = theme.shape.borderRadius;
const radiusValue = typeof radius === 'number' ? `${radius}px` : radius;
variables.push(` --bui-radius-3: ${radiusValue};`);
styleObject['--bui-radius-3'] = radiusValue;
}
// Colors - map MUI palette to BUI color tokens
@@ -96,24 +106,21 @@ function generateBuiVariables(theme: Mui5Theme): string {
// Base colors
if (palette.common?.black) {
variables.push(` --bui-black: ${palette.common.black};`);
styleObject['--bui-black'] = palette.common.black;
}
if (palette.common?.white) {
variables.push(` --bui-white: ${palette.common.white};`);
styleObject['--bui-white'] = palette.common.white;
}
// Background colors
if (palette.background?.default) {
variables.push(` --bui-bg: ${palette.background.default};`);
}
if (palette.background?.paper) {
variables.push(` --bui-bg-surface-1: ${palette.background.paper};`);
styleObject['--bui-bg'] = palette.background.default;
}
// Generate surface colors
Object.entries({
'surface-1': palette.background.default,
'surface-2': palette.background.paper,
'surface-1': palette.background.paper,
'surface-2': palette.background.default,
solid: palette.primary.main,
'solid-hover': blend(palette.primary.main, palette.primary.dark, 0.5),
'solid-pressed': palette.primary.dark,
@@ -126,15 +133,15 @@ function generateBuiVariables(theme: Mui5Theme): string {
warning: palette.warning.light,
success: palette.success.light,
}).forEach(([key, value]) => {
variables.push(` --bui-bg-${key}: ${value};`);
styleObject[`--bui-bg-${key}`] = value;
});
// Foreground colors
if (palette.text?.primary) {
variables.push(` --bui-fg-primary: ${palette.text.primary};`);
styleObject['--bui-fg-primary'] = palette.text.primary;
}
if (palette.text?.secondary) {
variables.push(` --bui-fg-secondary: ${palette.text.secondary};`);
styleObject['--bui-fg-secondary'] = palette.text.secondary;
}
// Generate foreground colors
@@ -150,7 +157,7 @@ function generateBuiVariables(theme: Mui5Theme): string {
warning: palette.warning.main,
success: palette.success.main,
}).forEach(([key, value]) => {
variables.push(` --bui-fg-${key}: ${value};`);
styleObject[`--bui-fg-${key}`] = value;
});
// Border colors
@@ -159,13 +166,13 @@ function generateBuiVariables(theme: Mui5Theme): string {
warning: palette.warning.main,
success: palette.success.main,
}).forEach(([key, value]) => {
variables.push(` --bui-border${key ? `-${key}` : ''}: ${value};`);
styleObject[`--bui-border${key ? `-${key}` : ''}`] = value;
});
// Special colors
if (palette.primary?.main) {
variables.push(` --bui-ring: ${palette.primary.main};`);
styleObject['--bui-ring'] = palette.primary.main;
}
return variables.join('\n');
return styleObject;
}