bui-themer: initial implementation plan execution
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user