bui-themer: more files!
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user