bui-themer: use backstage palette + refine conversion and preview

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-09-14 02:10:10 +02:00
parent c1e3c9b80d
commit 92c412e07c
3 changed files with 131 additions and 119 deletions
@@ -15,6 +15,7 @@
*/
import {
Box,
Button,
Card,
CardBody,
@@ -26,6 +27,8 @@ import {
Select,
Text,
TextField,
TagGroup,
Tag,
} from '@backstage/ui';
interface IsolatedPreviewProps {
@@ -50,26 +53,28 @@ export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) {
}}
>
<Flex direction="column" gap="4">
<Card>
<CardHeader>Theme Preview</CardHeader>
<CardBody>
<Text
variant="body-small"
style={{ color: 'var(--bui-fg-secondary)' }}
>
This preview shows how your theme will look with various Backstage
UI components
</Text>
</CardBody>
</Card>
<Card>
<CardHeader>Button Variants</CardHeader>
<CardBody>
<Flex gap="3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Flex direction="column" gap="2">
<Button variant="primary">Primary</Button>
<Button isDisabled variant="primary">
Disabled
</Button>
</Flex>
<Flex direction="column" gap="2">
<Button variant="secondary">Secondary</Button>
<Button isDisabled variant="secondary">
Disabled
</Button>
</Flex>
<Flex direction="column" gap="2">
<Button variant="tertiary">Tertiary</Button>
<Button isDisabled variant="tertiary">
Disabled
</Button>
</Flex>
</Flex>
</CardBody>
</Card>
@@ -98,42 +103,64 @@ export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) {
</Card>
<Card>
<CardHeader>Surface Variations</CardHeader>
<CardHeader>Tag Variants</CardHeader>
<CardBody>
<Flex gap="3">
<Card
style={{
backgroundColor: 'var(--bui-bg-surface-1)',
color: 'var(--bui-fg-primary)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Surface 1</Text>
</Card>
<Card
style={{
backgroundColor: 'var(--bui-bg-surface-2)',
color: 'var(--bui-fg-primary)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Surface 2</Text>
</Card>
<Card
style={{
backgroundColor: 'var(--bui-bg-solid)',
color: 'var(--bui-fg-solid)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Solid</Text>
</Card>
<TagGroup>
<Tag>Default</Tag>
<Tag
style={{
backgroundColor: 'var(--bui-bg-danger)',
color: 'var(--bui-fg-danger)',
border: `1px solid var(--bui-border-danger)`,
}}
>
Danger
</Tag>
<Tag
style={{
backgroundColor: 'var(--bui-bg-warning)',
color: 'var(--bui-fg-warning)',
border: `1px solid var(--bui-border-warning)`,
}}
>
Warning
</Tag>
<Tag
style={{
backgroundColor: 'var(--bui-bg-success)',
color: 'var(--bui-fg-success)',
border: `1px solid var(--bui-border-success)`,
}}
>
Success
</Tag>
</TagGroup>
</Flex>
</CardBody>
</Card>
<Card>
<CardHeader>Text Variants</CardHeader>
<CardBody>
<Box
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 'var(--bui-space-3)',
}}
>
<Text variant="title-x-small">Title X Small</Text>
<Text variant="body-x-small">Body X Small</Text>
<Text variant="title-small">Title Small</Text>
<Text variant="body-small">Body Small</Text>
<Text variant="title-medium">Title Medium</Text>
<Text variant="body-medium">Body Medium</Text>
<Text variant="title-large">Title Large</Text>
<Text variant="body-large">Body Large</Text>
</Box>
</CardBody>
</Card>
</Flex>
</div>
);
@@ -56,11 +56,13 @@ describe('convertMuiToBuiTheme', () => {
expect(result.css).toContain('--bui-font-regular: Roboto, sans-serif;');
// Spacing is skipped for default 8px
expect(result.css).not.toContain('--bui-space:');
expect(result.css).toContain('--bui-radius-3: 4px;');
expect(result.css).toContain('--bui-bg: #f5f5f5;');
// Border radius tokens are only generated when radius is 0
expect(result.css).not.toContain('--bui-radius-3:');
// Background default maps to surface-2
expect(result.css).toContain('--bui-bg-surface-2: #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;');
// Secondary may not be present without Backstage additions, so don't assert it
expect(result.css).toContain('--bui-bg-solid: #1976d2;');
// Test style object
@@ -94,10 +96,10 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
expect(result.css).toContain("[data-theme-mode='dark'] {");
expect(result.css).toContain('--bui-bg: #121212;');
// Background default maps to surface-2 in dark mode as well
expect(result.css).toContain('--bui-bg-surface-2: #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');
@@ -177,9 +179,16 @@ describe('convertMuiToBuiTheme', () => {
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;');
// Foreground danger/warning/success may be hex or rgb depending on theme
expect(result.css).toMatch(
/--bui-fg-danger:\s*(#[0-9a-f]{6}|rgb\([^)]*\));/i,
);
expect(result.css).toMatch(
/--bui-fg-warning:\s*(#[0-9a-f]{6}|rgb\([^)]*\));/i,
);
expect(result.css).toMatch(
/--bui-fg-success:\s*(#[0-9a-f]{6}|rgb\([^)]*\));/i,
);
});
it('should generate border colors correctly', () => {
@@ -200,7 +209,7 @@ describe('convertMuiToBuiTheme', () => {
const result = convertMuiToBuiTheme(theme);
// Base border colors are no longer generated
// Base border colors are included when border/divider exist; specific statuses always present
expect(result.css).toContain('--bui-border-danger: #f44336;');
expect(result.css).toContain('--bui-border-warning: #ff9800;');
expect(result.css).toContain('--bui-border-success: #4caf50;');
@@ -15,6 +15,7 @@
*/
import { Theme as Mui5Theme } from '@mui/material/styles';
import { BackstagePaletteAdditions } from '@backstage/theme';
import { blend, alpha } from '@mui/system/colorManipulator';
export interface ConvertMuiToBuiThemeResult {
@@ -61,48 +62,25 @@ function generateBuiVariables(theme: Mui5Theme): Record<string, string> {
theme.typography.fontWeightBold || 600,
);
// Font sizes - map MUI typography scale to BUI scale
const fontSizeMap = {
h1: '--bui-font-size-10',
h2: '--bui-font-size-8',
h3: '--bui-font-size-7',
h4: '--bui-font-size-6',
h5: '--bui-font-size-5',
h6: '--bui-font-size-4',
body1: '--bui-font-size-4',
body2: '--bui-font-size-3',
caption: '--bui-font-size-2',
overline: '--bui-font-size-1',
};
Object.entries(fontSizeMap).forEach(([muiKey, buiVar]) => {
const typographyVariant =
theme.typography[muiKey as keyof typeof theme.typography];
const fontSize =
typeof typographyVariant === 'object' && typographyVariant?.fontSize
? typographyVariant.fontSize
: undefined;
if (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') {
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;
styleObject['--bui-radius-3'] = radiusValue;
// Border radius, only translate a 0 radius
if (theme.shape.borderRadius === 0) {
styleObject['--bui-radius-1'] = '0';
styleObject['--bui-radius-2'] = '0';
styleObject['--bui-radius-3'] = '0';
styleObject['--bui-radius-4'] = '0';
styleObject['--bui-radius-5'] = '0';
styleObject['--bui-radius-6'] = '0';
}
// Colors - map MUI palette to BUI color tokens
const palette = theme.palette;
const palette = theme.palette as typeof theme.palette &
Partial<BackstagePaletteAdditions>;
// Base colors
if (palette.common?.black) {
@@ -112,13 +90,27 @@ function generateBuiVariables(theme: Mui5Theme): Record<string, string> {
styleObject['--bui-white'] = palette.common.white;
}
// Background colors
if (palette.background?.default) {
styleObject['--bui-bg'] = palette.background.default;
}
// Generate foreground colors
Object.entries({
primary: palette.text.primary,
secondary: palette.textSubtle,
link: palette.link ?? palette.primary.main,
'link-hover': palette.linkHover ?? palette.primary.dark,
disabled: palette.text.disabled,
solid: palette.primary.contrastText,
'solid-disabled': palette.text.disabled,
tint: palette.textSubtle,
'tint-disabled': palette.textVerySubtle,
danger: palette.error.dark,
warning: palette.warning.dark,
success: palette.success.dark,
}).forEach(([key, value]) => {
styleObject[`--bui-fg-${key}`] = value;
});
// Generate surface colors
Object.entries({
'': palette.background.default,
'surface-1': palette.background.paper,
'surface-2': palette.background.default,
solid: palette.primary.main,
@@ -136,30 +128,6 @@ function generateBuiVariables(theme: Mui5Theme): Record<string, string> {
styleObject[`--bui-bg-${key}`] = value;
});
// Foreground colors
if (palette.text?.primary) {
styleObject['--bui-fg-primary'] = palette.text.primary;
}
if (palette.text?.secondary) {
styleObject['--bui-fg-secondary'] = palette.text.secondary;
}
// Generate foreground colors
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]) => {
styleObject[`--bui-fg-${key}`] = value;
});
// Border colors
Object.entries({
danger: palette.error.main,
@@ -169,9 +137,17 @@ function generateBuiVariables(theme: Mui5Theme): Record<string, string> {
styleObject[`--bui-border${key ? `-${key}` : ''}`] = value;
});
// Base border color if available
if (palette.border || palette.divider) {
styleObject['--bui-border'] = palette.border || palette.divider;
styleObject['--bui-border-danger'] = palette.error.main;
styleObject['--bui-border-warning'] = palette.warning.main;
styleObject['--bui-border-success'] = palette.success.main;
}
// Special colors
if (palette.primary?.main) {
styleObject['--bui-ring'] = palette.primary.main;
if (palette.highlight || palette.primary?.main) {
styleObject['--bui-ring'] = palette.highlight || palette.primary.main;
}
return styleObject;