bui-themer: more files!

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-09-13 18:34:33 +02:00
parent 516fb0de19
commit 01dbd7b3db
6 changed files with 319 additions and 273 deletions
@@ -0,0 +1,143 @@
/*
* 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 {
Button,
Card,
CardBody,
CardHeader,
Checkbox,
Flex,
Radio,
RadioGroup,
Select,
Text,
TextField,
} 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>Theme Preview</CardHeader>
<CardBody>
<Text
variant="body-small"
style={{ color: 'var(--bui-fg-secondary)' }}
>
This preview shows how your theme will look with various Backstage
UI components
</Text>
</CardBody>
</Card>
<Card>
<CardHeader>Button Variants</CardHeader>
<CardBody>
<Flex gap="3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
</Flex>
</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="radio-option" />
<Text variant="body-small">Option 1</Text>
<Radio value="radio-option" />
<Text variant="body-small">Option 2</Text>
<Radio value="radio-option" />
<Text variant="body-small">Option 3</Text>
</RadioGroup>
</Flex>
</CardBody>
</Card>
<Card>
<CardHeader>Surface Variations</CardHeader>
<CardBody>
<Flex gap="3">
<Card
style={{
backgroundColor: 'var(--bui-bg-surface-1)',
color: 'var(--bui-fg-primary)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Surface 1</Text>
</Card>
<Card
style={{
backgroundColor: 'var(--bui-bg-surface-2)',
color: 'var(--bui-fg-primary)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Surface 2</Text>
</Card>
<Card
style={{
backgroundColor: 'var(--bui-bg-solid)',
color: 'var(--bui-fg-solid)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Solid</Text>
</Card>
</Flex>
</CardBody>
</Card>
</Flex>
</div>
);
}
@@ -14,264 +14,11 @@
* limitations under the License.
*/
import { useMemo, useState, useCallback } from 'react';
import { AppTheme, useApi } from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
import { appThemeApiRef } from '@backstage/core-plugin-api';
import { Theme, useTheme } from '@mui/material/styles';
import {
Box,
Button,
Card,
Container,
Flex,
HeaderPage,
Text,
Tabs,
TabList,
Tab,
TabPanel,
TextField,
Select,
Checkbox,
Radio,
RadioGroup,
CardHeader,
CardBody,
} from '@backstage/ui';
import { convertMuiToBuiTheme } from './convertMuiToBuiTheme';
interface ThemeContentProps {
themeId: string;
themeTitle: string;
variant: 'light' | 'dark';
muiTheme: Theme;
}
interface IsolatedPreviewProps {
mode: 'light' | 'dark';
styleObject: Record<string, string>;
}
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>Theme Preview</CardHeader>
<CardBody>
<Text
variant="body-small"
style={{ color: 'var(--bui-fg-secondary)' }}
>
This preview shows how your theme will look with various Backstage
UI components
</Text>
</CardBody>
</Card>
<Card>
<CardHeader>Button Variants</CardHeader>
<CardBody>
<Flex gap="3">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
</Flex>
</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="radio-option" />
<Text variant="body-small">Option 1</Text>
<Radio value="radio-option" />
<Text variant="body-small">Option 2</Text>
<Radio value="radio-option" />
<Text variant="body-small">Option 3</Text>
</RadioGroup>
</Flex>
</CardBody>
</Card>
<Card>
<CardHeader>Surface Variations</CardHeader>
<CardBody>
<Flex gap="3">
<Card
style={{
backgroundColor: 'var(--bui-bg-surface-1)',
color: 'var(--bui-fg-primary)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Surface 1</Text>
</Card>
<Card
style={{
backgroundColor: 'var(--bui-bg-surface-2)',
color: 'var(--bui-fg-primary)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Surface 2</Text>
</Card>
<Card
style={{
backgroundColor: 'var(--bui-bg-solid)',
color: 'var(--bui-fg-solid)',
padding: '12px',
minWidth: '120px',
}}
>
<Text variant="body-small">Solid</Text>
</Card>
</Flex>
</CardBody>
</Card>
</Flex>
</div>
);
}
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>
);
}
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(props: { setTheme: (theme: Theme) => void }) {
props.setTheme(useTheme());
return null;
}
import { Box, Card, Container, Flex, HeaderPage, Text } from '@backstage/ui';
import { ThemeContent } from './ThemeContent';
import { MuiThemeExtractor } from './MuiThemeExtractor';
export function BuiThemerPage() {
const appThemeApi = useApi(appThemeApiRef);
@@ -0,0 +1,42 @@
/*
* 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 { useState } 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(props: { setTheme: (theme: Theme) => void }) {
props.setTheme(useTheme());
return null;
}
@@ -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>
);
}
@@ -15,3 +15,6 @@
*/
export { BuiThemerPage } from './BuiThemerPage';
export { BuiThemePreview } from './BuiThemePreview';
export { ThemeContent } from './ThemeContent';
export { MuiThemeExtractor } from './MuiThemeExtractor';
-16
View File
@@ -1,16 +0,0 @@
/*
* 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';