Merge pull request #31140 from backstage/rugvip/bui-themer

plugins: add bui themer plugin to convert MUI themes to BUI
This commit is contained in:
Patrik Oldsberg
2025-09-26 18:34:42 +02:00
committed by GitHub
32 changed files with 1599 additions and 12 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-mui-to-bui': minor
---
This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/theme': patch
---
The `UnifiedThemeProvider` now coordinates theme attributes on the document `body` in case multiple theme providers are rendered.
+1
View File
@@ -56,6 +56,7 @@
"@backstage/plugin-home": "workspace:^",
"@backstage/plugin-kubernetes": "workspace:^",
"@backstage/plugin-kubernetes-cluster": "workspace:^",
"@backstage/plugin-mui-to-bui": "workspace:^",
"@backstage/plugin-notifications": "workspace:^",
"@backstage/plugin-org": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
+2
View File
@@ -73,6 +73,7 @@ import {
} from '@backstage/plugin-notifications';
import { CustomizableHomePage } from './components/home/CustomizableHomePage';
import { HomePage } from './components/home/HomePage';
import { BuiThemerPage } from '@backstage/plugin-mui-to-bui';
const app = createApp({
apis,
@@ -208,6 +209,7 @@ const routes = (
{customDevToolsPage}
</Route>
<Route path="/notifications" element={<NotificationsPage />} />
<Route path="/mui-to-bui" element={<BuiThemerPage />} />
</FlatRoutes>
);
@@ -96,6 +96,7 @@ describe('createPublicSignInApp', () => {
<body
data-theme-mode="light"
data-theme-name="backstage"
data-unified-theme-stack="[{"mode":"light","name":"backstage"}]"
>
<div>
<form
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ReactNode, useEffect } from 'react';
import { ReactNode } from 'react';
import CssBaseline from '@material-ui/core/CssBaseline';
import {
ThemeProvider,
@@ -58,6 +58,8 @@ const generateV4ClassName = createGenerateClassName({
productionPrefix: 'jss4-',
});
import { useApplyThemeAttributes } from './useApplyThemeAttributes';
/**
* Provides themes for all Material UI versions supported by the provided unified theme.
*
@@ -70,18 +72,11 @@ export function UnifiedThemeProvider(
const v4Theme = theme.getTheme('v4') as Mui4Theme;
const v5Theme = theme.getTheme('v5') as Mui5Theme;
const themeMode = v4Theme ? v4Theme.palette.type : v5Theme?.palette.mode;
const themeName = 'backstage';
useEffect(() => {
document.body.setAttribute('data-theme-mode', themeMode);
document.body.setAttribute('data-theme-name', themeName);
return () => {
document.body.removeAttribute('data-theme-mode');
document.body.removeAttribute('data-theme-name');
};
}, [themeMode, themeName]);
useApplyThemeAttributes(
v4Theme ? v4Theme.palette.type : v5Theme?.palette.mode,
'backstage',
);
let cssBaseline: JSX.Element | undefined = undefined;
if (!noCssBaseline) {
@@ -0,0 +1,60 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import { renderHook } from '@testing-library/react';
import { useApplyThemeAttributes } from './useApplyThemeAttributes';
describe('useApplyThemeAttributes', () => {
beforeEach(() => {
document.body.removeAttribute('data-unified-theme-stack');
document.body.removeAttribute('data-theme-mode');
document.body.removeAttribute('data-theme-name');
});
it('pushes attributes on mount and pops on unmount', () => {
const { unmount } = renderHook(() =>
useApplyThemeAttributes('light', 'one'),
);
expect(document.body.getAttribute('data-theme-mode')).toBe('light');
expect(document.body.getAttribute('data-theme-name')).toBe('one');
expect(
JSON.parse(document.body.getAttribute('data-unified-theme-stack') || '[]')
.length,
).toBe(1);
unmount();
expect(document.body.getAttribute('data-theme-mode')).toBeNull();
expect(document.body.getAttribute('data-theme-name')).toBeNull();
expect(document.body.getAttribute('data-unified-theme-stack')).toBeNull();
});
it('stacks multiple mounts and applies top-most', () => {
const r1 = renderHook(() => useApplyThemeAttributes('light', 'one'));
const r2 = renderHook(() => useApplyThemeAttributes('dark', 'two'));
expect(document.body.getAttribute('data-theme-mode')).toBe('dark');
expect(document.body.getAttribute('data-theme-name')).toBe('two');
r2.unmount();
expect(document.body.getAttribute('data-theme-mode')).toBe('light');
expect(document.body.getAttribute('data-theme-name')).toBe('one');
r1.unmount();
expect(document.body.getAttribute('data-theme-mode')).toBeNull();
expect(document.body.getAttribute('data-theme-name')).toBeNull();
});
});
@@ -0,0 +1,72 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
type ThemeFrame = { mode?: string; name?: string };
const STACK_ATTR = 'data-unified-theme-stack';
const MODE_ATTR = 'data-theme-mode';
const NAME_ATTR = 'data-theme-name';
function mutateAttrStack(mutator: (stack: ThemeFrame[]) => void) {
const stack = (() => {
const raw = document.body.getAttribute(STACK_ATTR);
if (!raw) {
return [] as ThemeFrame[];
}
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as ThemeFrame[]) : [];
} catch {
return [] as ThemeFrame[];
}
})();
mutator(stack);
if (stack.length === 0) {
document.body.removeAttribute(STACK_ATTR);
} else {
document.body.setAttribute(STACK_ATTR, JSON.stringify(stack));
}
const top = stack[stack.length - 1];
if (top?.mode) {
document.body.setAttribute(MODE_ATTR, String(top.mode));
} else {
document.body.removeAttribute(MODE_ATTR);
}
if (top?.name) {
document.body.setAttribute(NAME_ATTR, String(top.name));
} else {
document.body.removeAttribute(NAME_ATTR);
}
}
export function useApplyThemeAttributes(themeMode: string, themeName: string) {
useEffect(() => {
mutateAttrStack(stack => {
stack.push({ mode: themeMode, name: themeName });
});
return () => {
mutateAttrStack(stack => {
stack.pop();
});
};
}, [themeMode, themeName]);
}
@@ -99,6 +99,7 @@ describe('appModulePublicSignIn', () => {
<body
data-theme-mode="light"
data-theme-name="backstage"
data-unified-theme-stack="[{"mode":"light","name":"backstage"}]"
>
<div>
<form
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+63
View File
@@ -0,0 +1,63 @@
# mui-to-bui
## Description
The Backstage UI Themer helps you convert an existing MUI v5 theme into Backstage UI (BUI) CSS custom properties. It detects installed app themes, generates a complete set of BUI CSS variables for each theme (light and dark), and lets you preview how common BUI components look with your colors and typography. You can copy the generated CSS to the clipboard or download it as a `.css` file, and it works with both the old and new Backstage frontend systems without requiring any backend setup.
## Installation
### 1) Add the dependency to your app
Run this from your Backstage repo root:
```bash
yarn add --cwd packages/app @backstage/plugin-mui-to-bui
```
### 2) Wire it up depending on your frontend system
#### Old frontend system (legacy `App.tsx` with `<FlatRoutes>`)
Add a route for the page in your app:
```tsx
// packages/app/src/App.tsx
import React from 'react';
import { Route } from 'react-router-dom';
import { FlatRoutes } from '@backstage/core-app-api';
import { BuiThemerPage } from '@backstage/plugin-mui-to-bui';
export const App = () => (
<FlatRoutes>
{/* ...your other routes */}
<Route path="/mui-to-bui" element={<BuiThemerPage />} />
</FlatRoutes>
);
```
#### New frontend system
If package discovery is enabled in your app, this plugin is picked up automatically after installation — no code changes required. Just navigate to `/mui-to-bui`.
If you prefer explicit registration (or don't use discovery), register the plugin as a feature. The page route (`/mui-to-bui`) is provided by the plugin.
```tsx
// packages/app/src/App.tsx (or your app entry where you call createApp)
import React from 'react';
import { createApp } from '@backstage/frontend-defaults';
import buiThemerPlugin from '@backstage/plugin-mui-to-bui';
const app = createApp({
features: [
// ...other features
buiThemerPlugin,
],
});
export default app.createRoot();
```
## Accessing the Themer page
- Navigate to `/mui-to-bui` in your Backstage app (for example `http://localhost:3000/mui-to-bui`).
- Optional: Add a sidebar/link in your app that points to `/mui-to-bui` if you want a permanent navigation entry.
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-mui-to-bui
title: '@backstage/plugin-mui-to-bui'
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: maintainers
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { buiThemerPlugin, BuiThemerPage } from '../src/plugin';
createDevApp()
.registerPlugin(buiThemerPlugin)
.addPage({
element: <BuiThemerPage />,
title: 'Root Page',
path: '/mui-to-bui',
})
.render();
+2
View File
@@ -0,0 +1,2 @@
# Knip report
+69
View File
@@ -0,0 +1,69 @@
{
"name": "@backstage/plugin-mui-to-bui",
"version": "0.1.0",
"backstage": {
"role": "frontend-plugin",
"pluginId": "mui-to-bui",
"pluginPackages": [
"@backstage/plugin-mui-to-bui"
]
},
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/bui-themer"
},
"license": "Apache-2.0",
"sideEffects": false,
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/core-compat-api": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/ui": "workspace:^",
"@mui/material": "^5.12.2",
"@mui/system": "^5.16.14"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@types/react": "^18.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-router-dom": "^6.3.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
}
+66
View File
@@ -0,0 +1,66 @@
## API Report File for "@backstage/plugin-mui-to-bui"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { JSX as JSX_3 } from 'react/jsx-runtime';
import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { RouteRef as RouteRef_2 } from '@backstage/core-plugin-api';
// @public (undocumented)
export const BuiThemerPage: () => JSX_3.Element;
// @public (undocumented)
export const buiThemerPlugin: BackstagePlugin<
{
root: RouteRef_2<undefined>;
},
{}
>;
// @public (undocumented)
const _default: OverridableFrontendPlugin<
{
root: RouteRef<undefined>;
},
{},
{
'page:mui-to-bui': ExtensionDefinition<{
kind: 'page';
name: undefined;
config: {
path: string | undefined;
};
configInput: {
path?: string | undefined;
};
output:
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>;
inputs: {};
params: {
defaultPath?: [Error: `Use the 'path' param instead`];
path: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef;
};
}>;
}
>;
export default _default;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,55 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { renderInTestApp } from '@backstage/test-utils';
import { BuiThemePreview } from './BuiThemePreview';
describe('BuiThemePreview', () => {
it('renders headings and sample components', async () => {
const { container } = await renderInTestApp(
<BuiThemePreview
mode="light"
styleObject={{
'--bui-bg-surface-2': '#ffffff',
'--bui-border': '#cccccc',
'--bui-radius-2': '4px',
'--bui-space-3': '12px',
'--bui-fg-secondary': '#777777',
'--bui-bg-solid': '#000000',
'--bui-fg-solid': '#ffffff',
'--bui-fg-primary': '#111111',
'--bui-bg-surface-1': '#f5f5f5',
}}
/>,
);
expect(container.firstChild).toHaveAttribute('data-theme-mode', 'light');
expect(await screen.findByText('Button Variants')).toBeInTheDocument();
expect(screen.getByText('Button Variants')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Primary' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Secondary' }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Tertiary' }),
).toBeInTheDocument();
expect(screen.getByText('Form Inputs')).toBeInTheDocument();
expect(screen.getByText('Tag Variants')).toBeInTheDocument();
expect(screen.getByText('Text Variants')).toBeInTheDocument();
});
});
@@ -0,0 +1,167 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Box,
Button,
Card,
CardBody,
CardHeader,
Checkbox,
Flex,
Radio,
RadioGroup,
Select,
Text,
TextField,
TagGroup,
Tag,
} from '@backstage/ui';
interface IsolatedPreviewProps {
mode: 'light' | 'dark';
styleObject: Record<string, string>;
}
export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) {
return (
<div
// Setting the theme mode ensures that the correct defaults are picked up from the BUI theme
data-theme-mode={mode}
style={{
// Apply the generated CSS variables directly to this container
// This creates a scoped context where the variables take precedence
...styleObject,
width: '100%',
backgroundColor: 'var(--bui-bg-surface-2)',
padding: 'var(--bui-space-3)',
borderRadius: 'var(--bui-radius-2)',
border: '1px solid var(--bui-border)',
}}
>
<Flex direction="column" gap="4">
<Card>
<CardHeader>Button Variants</CardHeader>
<CardBody>
<Flex gap="3">
<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>
<Card>
<CardHeader>Form Inputs</CardHeader>
<CardBody>
<Flex direction="column" gap="3">
<TextField label="Text Input" placeholder="Enter some text" />
<Select
label="Select Input"
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
]}
/>
<Checkbox label="Checkbox Option" />
<RadioGroup label="Radio Group" orientation="horizontal">
<Radio value="option-1">Option 1</Radio>
<Radio value="option-2">Option 2</Radio>
<Radio value="option-3">Option 3</Radio>
</RadioGroup>
</Flex>
</CardBody>
</Card>
<Card>
<CardHeader>Tag Variants</CardHeader>
<CardBody>
<Flex gap="3">
<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>
);
}
@@ -0,0 +1,67 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BuiThemerPage } from './BuiThemerPage';
import { appThemeApiRef } from '@backstage/core-plugin-api';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { ThemeProvider, createTheme } from '@mui/material/styles';
function makeAppTheme(id: string, title: string, variant: 'light' | 'dark') {
return {
id,
title,
variant,
Provider: ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={createTheme()}>{children}</ThemeProvider>
),
};
}
describe('BuiThemerPage', () => {
it('renders empty state when no themes installed', async () => {
const apis = [[appThemeApiRef, { getInstalledThemes: () => [] }]] as const;
await renderInTestApp(
<TestApiProvider apis={apis}>
<BuiThemerPage />
</TestApiProvider>,
);
expect(
screen.getByText(/No themes found. Please install some themes/i),
).toBeInTheDocument();
});
it('renders ThemeContent for each installed theme', async () => {
const themes = [
makeAppTheme('light', 'Light', 'light'),
makeAppTheme('dark', 'Dark', 'dark'),
];
const apis = [
[appThemeApiRef, { getInstalledThemes: () => themes }],
] as const;
await renderInTestApp(
<TestApiProvider apis={apis}>
<BuiThemerPage />
</TestApiProvider>,
);
expect(screen.getByText('Light')).toBeInTheDocument();
expect(screen.getByText('Dark')).toBeInTheDocument();
});
});
@@ -0,0 +1,68 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useApi } from '@backstage/core-plugin-api';
import { appThemeApiRef } from '@backstage/core-plugin-api';
import { Box, Card, Container, Flex, HeaderPage, Text } from '@backstage/ui';
import { ThemeContent } from './ThemeContent';
import { MuiThemeExtractor } from './MuiThemeExtractor';
export const BuiThemerPage = () => {
const appThemeApi = useApi(appThemeApiRef);
const installedThemes = appThemeApi.getInstalledThemes();
return (
<Container>
<HeaderPage title="BUI Theme Converter" />
<Box m="4">
<Text variant="body-medium" color="secondary">
Convert your MUI v5 theme into BUI CSS variables. Pick a theme to view
and export the generated BUI CSS variable definitions for use in your
project.
</Text>
</Box>
<Box mt="4">
{installedThemes.length === 0 ? (
<Card>
<Box p="4">
<Text>
No themes found. Please install some themes in your Backstage
app.
</Text>
</Box>
</Card>
) : (
<Flex direction="column" gap="4">
{installedThemes.map(theme => (
<MuiThemeExtractor key={theme.id} appTheme={theme}>
{muiTheme => (
<ThemeContent
key={theme.id}
themeId={theme.id}
themeTitle={theme.title}
variant={theme.variant}
muiTheme={muiTheme}
/>
)}
</MuiThemeExtractor>
))}
</Flex>
)}
</Box>
</Container>
);
};
@@ -0,0 +1,41 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MuiThemeExtractor } from './MuiThemeExtractor';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const mockAppTheme = {
id: 'mock',
title: 'Mock',
variant: 'light' as const,
Provider: ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={createTheme()}>{children}</ThemeProvider>
),
};
describe('MuiThemeExtractor', () => {
it('invokes render prop with extracted theme', async () => {
render(
<MuiThemeExtractor appTheme={mockAppTheme}>
{theme => <div>theme-palette-mode-{theme.palette.mode}</div>}
</MuiThemeExtractor>,
);
expect(screen.getByText('theme-palette-mode-light')).toBeInTheDocument();
});
});
@@ -0,0 +1,49 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState, JSX } from 'react';
import { AppTheme } from '@backstage/core-plugin-api';
import { Theme, useTheme } from '@mui/material/styles';
export function MuiThemeExtractor(props: {
appTheme: AppTheme;
children: (theme: Theme) => JSX.Element;
}): JSX.Element {
const { appTheme, children } = props;
const [theme, setTheme] = useState<Theme | null>(null);
const { Provider } = appTheme;
if (theme) {
return children(theme);
}
return (
<Provider>
<MuiThemeExtractorInner setTheme={setTheme} />
</Provider>
);
}
function MuiThemeExtractorInner({
setTheme,
}: {
setTheme: (theme: Theme) => void;
}) {
const theme = useTheme();
useEffect(() => {
setTheme(theme);
}, [theme, setTheme]);
return null;
}
@@ -0,0 +1,83 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ThemeContent } from './ThemeContent';
import { createTheme } from '@mui/material/styles';
import { renderInTestApp } from '@backstage/test-utils';
jest.mock('./convertMuiToBuiTheme', () => ({
convertMuiToBuiTheme: () => ({
css: ':root { --bui-color: #000; }',
styleObject: { '--bui-color': '#000' },
}),
}));
describe('ThemeContent', () => {
const muiTheme = createTheme();
it('renders title, variant and tabs', async () => {
await renderInTestApp(
<ThemeContent
themeId="light"
themeTitle="Light Theme"
variant="light"
muiTheme={muiTheme}
/>,
);
expect(screen.getByText('Light Theme')).toBeInTheDocument();
expect(screen.getByText('light theme')).toBeInTheDocument();
expect(screen.getByText('Generated CSS')).toBeInTheDocument();
expect(screen.getByText('Live Preview')).toBeInTheDocument();
expect(
screen.getByText(':root { --bui-color: #000; }'),
).toBeInTheDocument();
});
it('handles Copy CSS click', async () => {
const writeText = jest.fn();
Object.assign(window.navigator, { clipboard: { writeText } });
await renderInTestApp(
<ThemeContent
themeId="light"
themeTitle="Light Theme"
variant="light"
muiTheme={muiTheme}
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Copy CSS' }));
expect(writeText).toHaveBeenCalledWith(':root { --bui-color: #000; }');
});
it('switches to Live Preview tab', async () => {
await renderInTestApp(
<ThemeContent
themeId="light"
themeTitle="Light Theme"
variant="light"
muiTheme={muiTheme}
/>,
);
fireEvent.click(screen.getByText('Live Preview'));
// The preview renders content from BuiThemePreview; basic smoke check:
expect(await screen.findByText('Button Variants')).toBeInTheDocument();
});
});
@@ -0,0 +1,127 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo, useState, useCallback } from 'react';
import { Theme } from '@mui/material/styles';
import {
Box,
Button,
Card,
Flex,
Text,
Tabs,
TabList,
Tab,
TabPanel,
} from '@backstage/ui';
import { convertMuiToBuiTheme } from './convertMuiToBuiTheme';
import { BuiThemePreview } from './BuiThemePreview';
interface ThemeContentProps {
themeId: string;
themeTitle: string;
variant: 'light' | 'dark';
muiTheme: Theme;
}
export function ThemeContent({
themeId,
themeTitle,
variant,
muiTheme,
}: ThemeContentProps) {
const [activeTab, setActiveTab] = useState<string>('css');
const { css, styleObject } = useMemo(() => {
return convertMuiToBuiTheme(muiTheme);
}, [muiTheme]);
const handleCopy = useCallback(() => {
window.navigator.clipboard.writeText(css);
}, [css]);
const handleDownload = useCallback(() => {
const blob = new Blob([css], { type: 'text/css' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bui-theme-${themeId}.css`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, [css, themeId]);
return (
<Card>
<Box p="6">
<Flex direction="column" gap="4">
<Flex justify="between" align="center">
<Text variant="title-medium">{themeTitle}</Text>
<Text variant="body-small" color="secondary">
{variant} theme
</Text>
</Flex>
<Flex gap="3">
<Button onClick={handleCopy} variant="secondary">
Copy CSS
</Button>
<Button onClick={handleDownload} variant="secondary">
Download CSS
</Button>
</Flex>
<Tabs
selectedKey={activeTab}
onSelectionChange={key => setActiveTab(key as string)}
>
<TabList>
<Tab id="css">Generated CSS</Tab>
<Tab id="preview">Live Preview</Tab>
</TabList>
<TabPanel id="css">
<Box
p="3"
style={{
backgroundColor: 'var(--bui-bg-surface-2)',
border: '1px solid var(--bui-border)',
borderRadius: 'var(--bui-radius-2)',
fontFamily: 'monospace',
fontSize: '14px',
lineHeight: '1.5',
height: '600px',
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
}}
>
{css}
</Box>
</TabPanel>
<TabPanel id="preview">
<Box>
<BuiThemePreview mode={variant} styleObject={styleObject} />
</Box>
</TabPanel>
</Tabs>
</Flex>
</Box>
</Card>
);
}
@@ -0,0 +1,227 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createTheme } from '@mui/material/styles';
import { convertMuiToBuiTheme } from './convertMuiToBuiTheme';
describe('convertMuiToBuiTheme', () => {
it('should generate CSS for light theme', () => {
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
dark: '#115293',
},
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
text: {
primary: '#000000',
secondary: '#666666',
},
},
typography: {
fontFamily: 'Roboto, sans-serif',
h1: {
fontSize: '2.5rem',
},
body1: {
fontSize: '1rem',
},
},
spacing: 8,
shape: {
borderRadius: 4,
},
});
const result = convertMuiToBuiTheme(theme);
expect(result.css).toContain(':root {');
expect(result.css).toContain('--bui-font-regular: Roboto, sans-serif;');
// Spacing is skipped for default 8px
expect(result.css).not.toContain('--bui-space:');
// 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;');
// Secondary may not be present without Backstage additions, so don't assert it
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', () => {
const theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#90caf9',
dark: '#42a5f5',
},
background: {
default: '#121212',
paper: '#1e1e1e',
},
text: {
primary: '#ffffff',
secondary: '#b3b3b3',
},
},
});
const result = convertMuiToBuiTheme(theme);
expect(result.css).toContain("[data-theme-mode='dark'] {");
// 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;');
// 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', () => {
const theme = createTheme({});
const result = convertMuiToBuiTheme(theme);
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.css).toContain('--bui-black: #000;');
expect(result.css).toContain('--bui-white: #fff;');
});
it('should generate surface colors correctly', () => {
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
dark: '#115293',
},
error: {
main: '#f44336',
light: '#ffcdd2',
},
warning: {
main: '#ff9800',
light: '#ffe0b2',
},
success: {
main: '#4caf50',
light: '#c8e6c9',
},
},
});
const result = convertMuiToBuiTheme(theme);
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', () => {
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
dark: '#115293',
},
error: {
main: '#f44336',
},
warning: {
main: '#ff9800',
},
success: {
main: '#4caf50',
},
text: {
disabled: '#cccccc',
},
},
});
const result = convertMuiToBuiTheme(theme);
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;');
// 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', () => {
const theme = createTheme({
palette: {
mode: 'light',
error: {
main: '#f44336',
},
warning: {
main: '#ff9800',
},
success: {
main: '#4caf50',
},
},
});
const result = convertMuiToBuiTheme(theme);
// 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;');
});
it('should handle function-based spacing', () => {
const theme = createTheme({
spacing: (factor: number) => `${factor * 4}px`,
});
const result = convertMuiToBuiTheme(theme);
expect(result.css).toContain('--bui-space: calc(4px * 0.5);');
});
});
@@ -0,0 +1,150 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Theme as Mui5Theme } from '@mui/material/styles';
import { BackstagePaletteAdditions } from '@backstage/theme';
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 Object containing CSS string and style object with BUI variables
*/
export function convertMuiToBuiTheme(
theme: Mui5Theme,
): ConvertMuiToBuiThemeResult {
const styleObject = generateBuiVariables(theme);
const selector =
theme.palette.mode === 'dark' ? "[data-theme-mode='dark']" : ':root';
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): Record<string, string> {
const styleObject: Record<string, string> = {};
// Font families
if (theme.typography.fontFamily) {
styleObject['--bui-font-regular'] = theme.typography.fontFamily;
}
// Font weights
if (theme.typography.fontWeightRegular) {
styleObject['--bui-font-weight-regular'] = String(
theme.typography.fontWeightRegular,
);
}
if (theme.typography.fontWeightBold) {
styleObject['--bui-font-weight-bold'] = String(
theme.typography.fontWeightBold,
);
}
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, 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 as typeof theme.palette &
Partial<BackstagePaletteAdditions>;
// Base colors
styleObject['--bui-black'] = palette.common.black;
styleObject['--bui-white'] = palette.common.white;
// 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,
'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]) => {
styleObject[`--bui-bg-${key}`] = value;
});
// Border colors
Object.entries({
danger: palette.error.main,
warning: palette.warning.main,
success: palette.success.main,
}).forEach(([key, value]) => {
styleObject[`--bui-border${key ? `-${key}` : ''}`] = value;
});
// Base border color if available
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
styleObject['--bui-ring'] = palette.highlight || palette.primary.main;
return styleObject;
}
@@ -0,0 +1,17 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { BuiThemerPage } from './BuiThemerPage';
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default, buiThemerPlugin, BuiThemerPage } from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { buiThemerPlugin } from './plugin';
describe('mui-to-bui', () => {
it('should export plugin', () => {
expect(buiThemerPlugin).toBeDefined();
});
});
+67
View File
@@ -0,0 +1,67 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
convertLegacyRouteRef,
convertLegacyRouteRefs,
} from '@backstage/core-compat-api';
import {
createPlugin,
createRoutableExtension,
} from '@backstage/core-plugin-api';
import {
createFrontendPlugin,
PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { rootRouteRef } from './routes';
// Old system
/** @public */
export const buiThemerPlugin = createPlugin({
id: 'mui-to-bui',
routes: {
root: rootRouteRef,
},
});
/** @public */
export const BuiThemerPage = buiThemerPlugin.provide(
createRoutableExtension({
name: 'BuiThemerPage',
component: () =>
import('./components/BuiThemerPage').then(m => m.BuiThemerPage),
mountPoint: rootRouteRef,
}),
);
// New system
/** @public */
export default createFrontendPlugin({
pluginId: 'mui-to-bui',
extensions: [
PageBlueprint.make({
params: {
path: '/mui-to-bui',
loader: () =>
import('./components/BuiThemerPage').then(m => <m.BuiThemerPage />),
routeRef: convertLegacyRouteRef(rootRouteRef),
},
}),
],
routes: convertLegacyRouteRefs({
root: rootRouteRef,
}),
});
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'mui-to-bui',
});
+32
View File
@@ -5708,6 +5708,37 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-mui-to-bui@workspace:^, @backstage/plugin-mui-to-bui@workspace:plugins/bui-themer":
version: 0.0.0-use.local
resolution: "@backstage/plugin-mui-to-bui@workspace:plugins/bui-themer"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-compat-api": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/ui": "workspace:^"
"@mui/material": "npm:^5.12.2"
"@mui/system": "npm:^5.16.14"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
"@types/react": "npm:^18.0.0"
react: "npm:^18.0.2"
react-dom: "npm:^18.0.2"
react-router-dom: "npm:^6.3.0"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
react-router-dom: ^6.3.0
peerDependenciesMeta:
"@types/react":
optional: true
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-backend-module-email@workspace:plugins/notifications-backend-module-email"
@@ -29673,6 +29704,7 @@ __metadata:
"@backstage/plugin-home": "workspace:^"
"@backstage/plugin-kubernetes": "workspace:^"
"@backstage/plugin-kubernetes-cluster": "workspace:^"
"@backstage/plugin-mui-to-bui": "workspace:^"
"@backstage/plugin-notifications": "workspace:^"
"@backstage/plugin-org": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"