bui-themer: initial implementation plan execution

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-09-12 19:46:05 +02:00
parent 05ff15d1c0
commit 6262191013
5 changed files with 1015 additions and 33 deletions
+2
View File
@@ -30,9 +30,11 @@
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
"@mui/material": "^7.3.2",
"react-use": "^17.2.4"
},
"peerDependencies": {
@@ -14,43 +14,276 @@
* limitations under the License.
*/
/*
import { useMemo, useState, useCallback, useEffect } from 'react';
import { useApi } from '@backstage/core-plugin-api';
import { appThemeApiRef } from '@backstage/core-plugin-api';
import { useTheme } from '@mui/material/styles';
import {
Box,
Button,
Card,
Container,
Flex,
Grid,
HeaderPage,
Text,
TextField,
Switch,
} from '@backstage/ui';
import {
convertMuiToBuiTheme,
ConvertMuiToBuiThemeOptions,
} from './convertMuiToBuiTheme';
IMPLEMENTATION PLAN:
interface ThemePreviewProps {
themeId: string;
themeTitle: string;
variant: 'light' | 'dark';
Provider: React.ComponentType<{ children: React.ReactNode }>;
}
- Create a converter utility that maps a MUI v5 `Theme` to BUI CSS variables,
covering palette, typography, spacing, and shape tokens to match
`packages/ui/src/css/core.css`.
- Place it in an adjacent `convertMuiToBuiTheme.ts` file exporting
`convertMuiToBuiTheme(theme: Theme, options?): string`, returning
deterministic CSS text.
- Scope output blocks as:
- `:root { ... }` for light themes
- `[data-theme-mode='dark'] { ... }` for dark themes
Optionally prefix by theme id (e.g. `[data-app-theme='<id>']`) so multiple
theme blocks can coexist without conflicts.
- Do not modify `packages/ui/src/css/core.css`; generate CSS for copy/download
only.
- Define a mapping from MUI fields to `--bui-*` tokens with sensible fallbacks
aligning with core defaults. Handle success/warning/error/background/text
surfaces, borders, and link/hover states.
- Add tests for light/dark and fallback behavior using MUI `createTheme` and
snapshot the generated CSS.
- Build the page to list all installed themes via `useApi(appThemeApiRef)`,
render a minimal child under each theme `Provider`, and read `useTheme()` to
get the runtime theme instance. Provide Copy and Download actions and an
optional live preview applying the generated variables.
- Performance: memoize generated CSS by theme id and regenerate when the
installed theme list changes. Observing `activeThemeId$()` is optional unless
previewing the active theme.
// Memoization cache for generated CSS
const cssCache = new Map<string, string>();
REQUIREMENTS:
interface ThemeContentProps {
themeId: string;
themeTitle: string;
variant: 'light' | 'dark';
}
- No changes can be made outside the plugins/bui-themer/src directory
- Only components from the `@backstage/ui` package at `packages/ui` can be used
function ThemeContent({ themeId, themeTitle, variant }: ThemeContentProps) {
const [generatedCss, setGeneratedCss] = useState<string>('');
const [isPreviewMode, setIsPreviewMode] = useState(false);
const [includeThemeId, setIncludeThemeId] = useState(false);
const muiTheme = useTheme();
*/
const css = useMemo(() => {
// Create cache key based on theme properties and options
const cacheKey = `${themeId}-${includeThemeId}-${JSON.stringify({
palette: muiTheme.palette,
typography: muiTheme.typography,
spacing: muiTheme.spacing,
shape: muiTheme.shape,
})}`;
// Check cache first
if (cssCache.has(cacheKey)) {
return cssCache.get(cacheKey)!;
}
const options: ConvertMuiToBuiThemeOptions = {
themeId,
includeThemeId,
};
const result = convertMuiToBuiTheme(muiTheme, options);
// Cache the result
cssCache.set(cacheKey, result);
// Clean up old cache entries (keep only last 50)
if (cssCache.size > 50) {
const firstKey = cssCache.keys().next().value;
if (firstKey) {
cssCache.delete(firstKey);
}
}
return result;
}, [muiTheme, themeId, includeThemeId]);
useEffect(() => {
setGeneratedCss(css);
}, [css]);
const handleCopy = useCallback(() => {
window.navigator.clipboard.writeText(generatedCss);
}, [generatedCss]);
const handleDownload = useCallback(() => {
const blob = new Blob([generatedCss], { 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);
}, [generatedCss, themeId]);
const handlePreviewToggle = useCallback(() => {
setIsPreviewMode(!isPreviewMode);
if (!isPreviewMode) {
// Apply the generated CSS to a style element
const styleId = `bui-theme-preview-${themeId}`;
let styleElement = document.getElementById(styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
styleElement.textContent = generatedCss;
} else {
// Remove the preview styles
const styleElement = document.getElementById(
`bui-theme-preview-${themeId}`,
);
if (styleElement) {
styleElement.remove();
}
}
}, [isPreviewMode, generatedCss, themeId]);
return (
<Card>
<Box p="4">
<Flex direction="column" gap="3">
<Flex justify="between" align="center">
<Text variant="title-small">{themeTitle}</Text>
<Text variant="body-small" color="secondary">
{variant} theme
</Text>
</Flex>
<Flex gap="2" align="center">
<Switch
isSelected={includeThemeId}
onChange={setIncludeThemeId}
label="Include theme ID scoping"
/>
</Flex>
<Flex gap="2">
<Button onClick={handleCopy} variant="secondary">
Copy CSS
</Button>
<Button onClick={handleDownload} variant="secondary">
Download CSS
</Button>
<Button
onClick={handlePreviewToggle}
variant={isPreviewMode ? 'primary' : 'secondary'}
>
{isPreviewMode ? 'Stop Preview' : 'Preview'}
</Button>
</Flex>
<Box>
<Text variant="body-small" style={{ marginBottom: '8px' }}>
Generated CSS:
</Text>
<TextField
value={generatedCss}
isReadOnly
style={{ fontFamily: 'monospace', fontSize: '12px' }}
/>
</Box>
{isPreviewMode && (
<Box
p="3"
style={{
border: '1px solid var(--bui-border)',
borderRadius: 'var(--bui-radius-2)',
backgroundColor: 'var(--bui-bg-surface-1)',
}}
>
<Text variant="body-small" style={{ marginBottom: '8px' }}>
Live Preview:
</Text>
<Box
p="2"
style={{
backgroundColor: 'var(--bui-bg-solid)',
color: 'var(--bui-fg-solid)',
}}
>
<Text>Solid Button</Text>
</Box>
<Box
p="2"
mt="2"
style={{
backgroundColor: 'var(--bui-bg-tint)',
color: 'var(--bui-fg-tint)',
}}
>
<Text>Tint Button</Text>
</Box>
<Box
p="2"
mt="2"
style={{
backgroundColor: 'var(--bui-bg-surface-2)',
color: 'var(--bui-fg-primary)',
}}
>
<Text>Surface Card</Text>
</Box>
</Box>
)}
</Flex>
</Box>
</Card>
);
}
function ThemePreview({
themeId,
themeTitle,
variant,
Provider,
}: ThemePreviewProps) {
return (
<Provider>
<ThemeContent
themeId={themeId}
themeTitle={themeTitle}
variant={variant}
/>
</Provider>
);
}
export function BuiThemerPage() {
return <div>TODO</div>;
const appThemeApi = useApi(appThemeApiRef);
const installedThemes = appThemeApi.getInstalledThemes();
return (
<Container>
<HeaderPage title="BUI Theme Converter" />
<Box mt="4">
<Text variant="body-medium" color="secondary">
Convert MUI v5 themes to BUI CSS variables. Select a theme to generate
the corresponding BUI CSS variables that can be copied or downloaded.
</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>
) : (
<Grid.Root gap="3">
{installedThemes.map(theme => (
<Grid.Item key={theme.id}>
<ThemePreview
themeId={theme.id}
themeTitle={theme.title}
variant={theme.variant}
Provider={theme.Provider}
/>
</Grid.Item>
))}
</Grid.Root>
)}
</Box>
</Container>
);
}
@@ -0,0 +1,278 @@
/*
* 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).toContain(':root {');
expect(result).toContain('--bui-font-regular: Roboto, sans-serif;');
expect(result).toContain('--bui-space: 8px;');
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;');
});
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).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;');
});
it('should include theme ID scoping when requested', () => {
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
},
});
const result = convertMuiToBuiTheme(theme, {
themeId: 'my-theme',
includeThemeId: true,
});
expect(result).toContain("[data-app-theme='my-theme'] :root {");
});
it('should handle missing theme properties gracefully', () => {
const theme = createTheme({});
const result = convertMuiToBuiTheme(theme);
expect(result).toContain(':root {');
expect(result).toContain('--bui-font-weight-regular: 400;');
expect(result).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;');
});
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: {
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).toContain('--bui-bg-solid: #1976d2;');
expect(result).toContain('--bui-bg-solid-hover: #115293;');
expect(result).toContain('--bui-bg-danger: #ffcdd2;');
expect(result).toContain('--bui-bg-warning: #ffe0b2;');
expect(result).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).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;');
});
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);
expect(result).toContain('--bui-border: rgba(0, 0, 0, 0.1);');
expect(result).toContain('--bui-border-hover: rgba(0, 0, 0, 0.2);');
expect(result).toContain('--bui-border-danger: #f44336;');
expect(result).toContain('--bui-border-warning: #ff9800;');
expect(result).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).toContain('--bui-space: 4px;');
});
it('should handle string-based spacing', () => {
const theme = createTheme({
spacing: '8px',
});
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-space: calc(1 * 8px);');
});
it('should handle string-based border radius', () => {
const theme = createTheme({
shape: {
borderRadius: '8px',
},
});
const result = convertMuiToBuiTheme(theme);
expect(result).toContain('--bui-radius-3: 8px;');
});
});
@@ -0,0 +1,286 @@
/*
* 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';
export interface ConvertMuiToBuiThemeOptions {
/**
* Theme ID to use for scoping CSS variables
*/
themeId?: string;
/**
* Whether to include theme ID scoping in the CSS
*/
includeThemeId?: boolean;
}
/**
* Converts a MUI v5 Theme to BUI CSS variables
* @param theme - The MUI v5 theme to convert
* @param options - Conversion options
* @returns CSS string with BUI variables
*/
export function convertMuiToBuiTheme(
theme: Mui5Theme,
options: ConvertMuiToBuiThemeOptions = {},
): string {
const { themeId, includeThemeId = false } = options;
const isDark = theme.palette.mode === 'dark';
// Generate CSS variables based on theme
const variables = generateBuiVariables(theme);
// Create CSS selector based on theme mode and ID
let selector = isDark ? "[data-theme-mode='dark']" : ':root';
if (includeThemeId && themeId) {
selector = `[data-app-theme='${themeId}'] ${selector}`;
}
return `${selector} {\n${variables}\n}`;
}
/**
* Generates BUI CSS variables from MUI theme
*/
function generateBuiVariables(theme: Mui5Theme): string {
const variables: string[] = [];
// Font families
if (theme.typography.fontFamily) {
variables.push(` --bui-font-regular: ${theme.typography.fontFamily};`);
}
// Font weights
variables.push(
` --bui-font-weight-regular: ${
theme.typography.fontWeightRegular || 400
};`,
);
variables.push(
` --bui-font-weight-bold: ${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) {
variables.push(` ${buiVar}: ${fontSize};`);
}
});
// 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};`);
}
// 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};`);
}
// Colors - map MUI palette to BUI color tokens
const palette = theme.palette;
// Base colors
if (palette.common?.black) {
variables.push(` --bui-black: ${palette.common.black};`);
}
if (palette.common?.white) {
variables.push(` --bui-white: ${palette.common.white};`);
}
// Gray scale - generate from primary color or use defaults
const grayScale = generateGrayScale(palette.mode === 'dark');
Object.entries(grayScale).forEach(([key, value]) => {
variables.push(` --bui-gray-${key}: ${value};`);
});
// 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};`);
}
// Generate surface colors
const surfaceColors = generateSurfaceColors(palette);
Object.entries(surfaceColors).forEach(([key, value]) => {
variables.push(` --bui-bg-${key}: ${value};`);
});
// Foreground colors
if (palette.text?.primary) {
variables.push(` --bui-fg-primary: ${palette.text.primary};`);
}
if (palette.text?.secondary) {
variables.push(` --bui-fg-secondary: ${palette.text.secondary};`);
}
// Generate foreground colors
const foregroundColors = generateForegroundColors(palette);
Object.entries(foregroundColors).forEach(([key, value]) => {
variables.push(` --bui-fg-${key}: ${value};`);
});
// Border colors
const borderColors = generateBorderColors(palette);
Object.entries(borderColors).forEach(([key, value]) => {
variables.push(` --bui-border${key ? `-${key}` : ''}: ${value};`);
});
// Special colors
if (palette.primary?.main) {
variables.push(` --bui-ring: ${palette.primary.main};`);
}
return variables.join('\n');
}
/**
* Generates gray scale colors
*/
function generateGrayScale(isDark: boolean): Record<string, string> {
if (isDark) {
return {
'1': '#191919',
'2': '#242424',
'3': '#373737',
'4': '#464646',
'5': '#575757',
'6': '#7b7b7b',
'7': '#9e9e9e',
'8': '#b4b4b4',
};
}
return {
'1': '#f8f8f8',
'2': '#ececec',
'3': '#d9d9d9',
'4': '#c1c1c1',
'5': '#9e9e9e',
'6': '#8c8c8c',
'7': '#757575',
'8': '#595959',
};
}
/**
* Generates surface background colors
*/
function generateSurfaceColors(
palette: Mui5Theme['palette'],
): Record<string, string> {
const isDark = palette.mode === 'dark';
// Helper function to get tint colors with proper fallbacks
const getTintColor = (opacity: string) => {
if (palette.primary?.main) {
return `${palette.primary.main}${opacity}`;
}
return isDark
? `rgba(156, 201, 255, ${opacity === '40' ? '0.12' : '0.16'})`
: `rgba(31, 84, 147, ${opacity === '40' ? '0.4' : '0.6'})`;
};
// Helper function to get fallback colors based on theme mode
const getFallbackColor = (lightColor: string, darkColor: string) => {
return isDark ? darkColor : lightColor;
};
return {
'surface-2': getFallbackColor('#ececec', '#242424'),
solid: palette.primary?.main || getFallbackColor('#1f5493', '#9cc9ff'),
'solid-hover':
palette.primary?.dark || getFallbackColor('#163a66', '#83b9fd'),
'solid-pressed':
palette.primary?.dark || getFallbackColor('#0f2b4e', '#83b9fd'),
'solid-disabled': getFallbackColor('#ebebeb', '#222222'),
tint: 'transparent',
'tint-hover': getTintColor('40'),
'tint-pressed': getTintColor('60'),
'tint-disabled': getFallbackColor('#ebebeb', 'transparent'),
danger: palette.error?.light || getFallbackColor('#feebe7', '#3b1219'),
warning: palette.warning?.light || getFallbackColor('#fff2b2', '#302008'),
success: palette.success?.light || getFallbackColor('#e6f6eb', '#132d21'),
};
}
/**
* Generates foreground colors
*/
function generateForegroundColors(
palette: Mui5Theme['palette'],
): Record<string, string> {
const isDark = palette.mode === 'dark';
return {
link: palette.primary?.main || (isDark ? '#9cc9ff' : '#1f5493'),
'link-hover': palette.primary?.dark || (isDark ? '#7eb5f7' : '#1f2d5c'),
disabled: palette.text?.disabled || (isDark ? '#9e9e9e' : '#9e9e9e'),
solid: isDark ? '#101821' : '#ffffff',
'solid-disabled': isDark ? '#575757' : '#9c9c9c',
tint: palette.primary?.main || (isDark ? '#9cc9ff' : '#1f5493'),
'tint-disabled': isDark ? '#575757' : '#9e9e9e',
danger: palette.error?.main || '#e22b2b',
warning: palette.warning?.main || '#e36d05',
success: palette.success?.main || '#1db954',
};
}
/**
* Generates border colors
*/
function generateBorderColors(
palette: Mui5Theme['palette'],
): Record<string, string> {
const isDark = palette.mode === 'dark';
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 || '#f87a7a',
warning: palette.warning?.main || '#e36d05',
success: palette.success?.main || '#53db83',
};
}
+184 -1
View File
@@ -2435,6 +2435,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.28.3":
version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4"
checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
languageName: node
linkType: hard
"@babel/template@npm:^7.27.2, @babel/template@npm:^7.3.3":
version: 7.27.2
resolution: "@babel/template@npm:7.27.2"
@@ -4336,9 +4343,11 @@ __metadata:
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/ui": "workspace:^"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.61"
"@mui/material": "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"
@@ -8184,6 +8193,19 @@ __metadata:
languageName: node
linkType: hard
"@emotion/cache@npm:^11.14.0":
version: 11.14.0
resolution: "@emotion/cache@npm:11.14.0"
dependencies:
"@emotion/memoize": "npm:^0.9.0"
"@emotion/sheet": "npm:^1.4.0"
"@emotion/utils": "npm:^1.4.2"
"@emotion/weak-memoize": "npm:^0.4.0"
stylis: "npm:4.2.0"
checksum: 10/52336b28a27b07dde8fcdfd80851cbd1487672bbd4db1e24cca1440c95d8a6a968c57b0453c2b7c88d9b432b717f99554dbecc05b5cdef27933299827e69fd8e
languageName: node
linkType: hard
"@emotion/hash@npm:^0.8.0":
version: 0.8.0
resolution: "@emotion/hash@npm:0.8.0"
@@ -11170,6 +11192,13 @@ __metadata:
languageName: node
linkType: hard
"@mui/core-downloads-tracker@npm:^7.3.2":
version: 7.3.2
resolution: "@mui/core-downloads-tracker@npm:7.3.2"
checksum: 10/01c0976e5fb6a8f0b59ebd58019af5452d7761e97daf0f2ef121bc3323508edf2fea1b13ea1a54b0098d6ee5eef70041c9722fe43291b237b989b6813a24b71e
languageName: node
linkType: hard
"@mui/material@npm:^5.12.2":
version: 5.16.14
resolution: "@mui/material@npm:5.16.14"
@@ -11203,6 +11232,42 @@ __metadata:
languageName: node
linkType: hard
"@mui/material@npm:^7.3.2":
version: 7.3.2
resolution: "@mui/material@npm:7.3.2"
dependencies:
"@babel/runtime": "npm:^7.28.3"
"@mui/core-downloads-tracker": "npm:^7.3.2"
"@mui/system": "npm:^7.3.2"
"@mui/types": "npm:^7.4.6"
"@mui/utils": "npm:^7.3.2"
"@popperjs/core": "npm:^2.11.8"
"@types/react-transition-group": "npm:^4.4.12"
clsx: "npm:^2.1.1"
csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1"
react-is: "npm:^19.1.1"
react-transition-group: "npm:^4.4.5"
peerDependencies:
"@emotion/react": ^11.5.0
"@emotion/styled": ^11.3.0
"@mui/material-pigment-css": ^7.3.2
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@emotion/react":
optional: true
"@emotion/styled":
optional: true
"@mui/material-pigment-css":
optional: true
"@types/react":
optional: true
checksum: 10/e9a5348060d6e6e6cb1ce0fe299a582fa9b37b56dff46b62f72d029d5078acefd56a258e559b29bb3d12e89d0a81fd4f1194ce07cc136bbf03fdb4878fd9b3c0
languageName: node
linkType: hard
"@mui/private-theming@npm:^5.16.14":
version: 5.16.14
resolution: "@mui/private-theming@npm:5.16.14"
@@ -11220,6 +11285,23 @@ __metadata:
languageName: node
linkType: hard
"@mui/private-theming@npm:^7.3.2":
version: 7.3.2
resolution: "@mui/private-theming@npm:7.3.2"
dependencies:
"@babel/runtime": "npm:^7.28.3"
"@mui/utils": "npm:^7.3.2"
prop-types: "npm:^15.8.1"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/5b733fc9e09e5b056af3e448899c665c6d1560ec56f593811fc88b9c5499ad42d8365551b182473880de6141a206da4f1f9e5b2ebcea12461dc6d8954e2a3ed7
languageName: node
linkType: hard
"@mui/styled-engine@npm:^5.16.14":
version: 5.16.14
resolution: "@mui/styled-engine@npm:5.16.14"
@@ -11241,6 +11323,29 @@ __metadata:
languageName: node
linkType: hard
"@mui/styled-engine@npm:^7.3.2":
version: 7.3.2
resolution: "@mui/styled-engine@npm:7.3.2"
dependencies:
"@babel/runtime": "npm:^7.28.3"
"@emotion/cache": "npm:^11.14.0"
"@emotion/serialize": "npm:^1.3.3"
"@emotion/sheet": "npm:^1.4.0"
csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1"
peerDependencies:
"@emotion/react": ^11.4.1
"@emotion/styled": ^11.3.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@emotion/react":
optional: true
"@emotion/styled":
optional: true
checksum: 10/f31a6080b950cc66edd19a5285d30e0557fc16ef07a5e9027d3479f18dda4a569a62976c0fa396a99b077c01000c4fced3c8ab3ea0f6bcd82d6db06b821503b2
languageName: node
linkType: hard
"@mui/styles@npm:^5.14.18":
version: 5.16.14
resolution: "@mui/styles@npm:5.16.14"
@@ -11300,6 +11405,34 @@ __metadata:
languageName: node
linkType: hard
"@mui/system@npm:^7.3.2":
version: 7.3.2
resolution: "@mui/system@npm:7.3.2"
dependencies:
"@babel/runtime": "npm:^7.28.3"
"@mui/private-theming": "npm:^7.3.2"
"@mui/styled-engine": "npm:^7.3.2"
"@mui/types": "npm:^7.4.6"
"@mui/utils": "npm:^7.3.2"
clsx: "npm:^2.1.1"
csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1"
peerDependencies:
"@emotion/react": ^11.5.0
"@emotion/styled": ^11.3.0
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@emotion/react":
optional: true
"@emotion/styled":
optional: true
"@types/react":
optional: true
checksum: 10/1e157027a693877a246c36e5c98dd988172daedb3430f95843a508fa3f8a707c394fe216078a17868d5d25690ea2f4c5af007dffff639436c36d982fb16e920a
languageName: node
linkType: hard
"@mui/types@npm:^7.2.15":
version: 7.2.19
resolution: "@mui/types@npm:7.2.19"
@@ -11312,6 +11445,20 @@ __metadata:
languageName: node
linkType: hard
"@mui/types@npm:^7.4.6":
version: 7.4.6
resolution: "@mui/types@npm:7.4.6"
dependencies:
"@babel/runtime": "npm:^7.28.3"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/6263ef077332b75e01c1b347339f100f8743d8135efdee4a6c167ce70c69ca29d9face54c53395cbc1c8a8f5453fe8ef3f4457842cf0c05318f6ef6cde44456e
languageName: node
linkType: hard
"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.16.14":
version: 5.16.14
resolution: "@mui/utils@npm:5.16.14"
@@ -11332,6 +11479,26 @@ __metadata:
languageName: node
linkType: hard
"@mui/utils@npm:^7.3.2":
version: 7.3.2
resolution: "@mui/utils@npm:7.3.2"
dependencies:
"@babel/runtime": "npm:^7.28.3"
"@mui/types": "npm:^7.4.6"
"@types/prop-types": "npm:^15.7.15"
clsx: "npm:^2.1.1"
prop-types: "npm:^15.8.1"
react-is: "npm:^19.1.1"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/defcebcb8e49cdbdf271e49066d78046d2d25d4c8177176a6c5d3368fe85e6a280c7be71dbf356b434863d96d69236956afe6a4a47bc394de6aeecfa8bc24fda
languageName: node
linkType: hard
"@n1ru4l/push-pull-async-iterable-iterator@npm:^3.1.0":
version: 3.1.0
resolution: "@n1ru4l/push-pull-async-iterable-iterator@npm:3.1.0"
@@ -20765,7 +20932,7 @@ __metadata:
languageName: node
linkType: hard
"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.3":
"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.15, @types/prop-types@npm:^15.7.3":
version: 15.7.15
resolution: "@types/prop-types@npm:15.7.15"
checksum: 10/31aa2f59b28f24da6fb4f1d70807dae2aedfce090ec63eaf9ea01727a9533ef6eaf017de5bff99fbccad7d1c9e644f52c6c2ba30869465dd22b1a7221c29f356
@@ -20879,6 +21046,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-transition-group@npm:^4.4.12":
version: 4.4.12
resolution: "@types/react-transition-group@npm:4.4.12"
peerDependencies:
"@types/react": "*"
checksum: 10/ea14bc84f529a3887f9954b753843820ac8a3c49fcdfec7840657ecc6a8800aad98afdbe4b973eb96c7252286bde38476fcf64b1c09527354a9a9366e516d9a2
languageName: node
linkType: hard
"@types/react-virtualized-auto-sizer@npm:^1.0.1":
version: 1.0.4
resolution: "@types/react-virtualized-auto-sizer@npm:1.0.4"
@@ -42751,6 +42927,13 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^19.1.1":
version: 19.1.1
resolution: "react-is@npm:19.1.1"
checksum: 10/44e0937da1f0da1d5dbd4f01972870768ef207f8a49717f4491f5022454f34c956cb66be560aee5286387169853b0283a81e1f419b51dc62654c8710dc98065a
languageName: node
linkType: hard
"react-json-view@npm:^1.21.3":
version: 1.21.3
resolution: "react-json-view@npm:1.21.3"