Merge branch 'master' into bui-dialog-component
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@techdocs/cli': minor
|
||||
---
|
||||
|
||||
Techdocs CLI serve supports automatic refresh, relying on `mkdocs` `watch` feature.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bumped create-app version.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend-module-email': patch
|
||||
---
|
||||
|
||||
chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Added a new `PasswordField` component. As part of this change, the `password` and `search` types have been removed from `TextField`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-signals': patch
|
||||
---
|
||||
|
||||
Remove `app-root-element:signals/signals-display` which was not doing anything useful
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': patch
|
||||
---
|
||||
|
||||
Fixed an issue with the OpenAPI generated client and server where import/export statements were duplicated.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Update react-aria-components to version 1.13.0
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Bump `better-sqlite3` to the latest version
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Enable tooltips on disabled buttons with automatic wrapper
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Add new `virtualized`, `maxWidth` and `maxHeight` props to `Menu`, `MenuListBox`, `MenuAutocomplete` and `MenuAutocompleteListBox` to allow for virtalization of long lists inside menus.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Added support for data attributes in `<Box />`, `<Container />`, `<Flex />`, and `<Grid />` components, ensuring they are correctly applied to the rendered elements.
|
||||
+20
-1
@@ -218,12 +218,20 @@
|
||||
"create-app-1758639549",
|
||||
"create-app-1758718573",
|
||||
"create-app-1759243273",
|
||||
"create-app-1759849206",
|
||||
"cuddly-mugs-act",
|
||||
"curvy-bobcats-melt",
|
||||
"dependabot-eaf5987",
|
||||
"eager-toes-start",
|
||||
"famous-loops-tickle",
|
||||
"fast-heads-brake",
|
||||
"fast-queens-guess",
|
||||
"few-weeks-create",
|
||||
"five-olives-bet",
|
||||
"flat-peas-run",
|
||||
"forty-crabs-travel",
|
||||
"full-chefs-roll",
|
||||
"fuzzy-trams-kick",
|
||||
"giant-weeks-jump",
|
||||
"heavy-cooks-divide",
|
||||
"hungry-crews-fetch",
|
||||
@@ -232,6 +240,7 @@
|
||||
"legal-eagles-jog",
|
||||
"modern-pugs-appear",
|
||||
"moody-singers-deny",
|
||||
"nasty-moose-rescue",
|
||||
"nice-readers-judge",
|
||||
"public-sites-admire",
|
||||
"public-wombats-say",
|
||||
@@ -240,18 +249,28 @@
|
||||
"ready-pots-arrive",
|
||||
"red-dodos-work",
|
||||
"red-times-bet",
|
||||
"sad-women-rule",
|
||||
"salty-words-wash",
|
||||
"shiny-candles-hide",
|
||||
"short-aliens-invite",
|
||||
"silent-mice-play",
|
||||
"six-cooks-battle",
|
||||
"slimy-signs-agree",
|
||||
"solid-bikes-leave",
|
||||
"tame-hairs-smash",
|
||||
"tender-cups-tap",
|
||||
"thin-hoops-bathe",
|
||||
"thirty-rules-press",
|
||||
"tidy-coats-know",
|
||||
"tired-mice-cheer",
|
||||
"tough-clocks-attack",
|
||||
"twelve-guests-sit",
|
||||
"twelve-oranges-grin",
|
||||
"two-emus-like",
|
||||
"unified-theme-attr-stack",
|
||||
"warm-items-look",
|
||||
"wet-spiders-wait",
|
||||
"wide-flies-jog"
|
||||
"wide-flies-jog",
|
||||
"yarn-plugin-integration"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Implemented AbortController request cancellation for overlapping search requests. This change ensures that when users type quickly, previous search requests are properly canceled before new ones start.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Added a background color default on the body
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
We are restructuring our CSS to have a better layer structure.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch
|
||||
'@backstage/plugin-notifications-backend-module-email': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-gitlab': patch
|
||||
'@backstage/plugin-notifications-backend': patch
|
||||
'@backstage/plugin-notifications': patch
|
||||
---
|
||||
|
||||
Removed unused dependencies
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
'@backstage/theme': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed the built-in `CssBaseline` from `UnifiedThemeProvider`. If your Backstage instance looks broken after this update, you likely forgot to add our new Backstage UI global CSS. To do that, please import `@backstage/ui/css/styles.css` in `packages/app/src/index.tsx`:
|
||||
|
||||
```tsx
|
||||
import '@backstage/ui/css/styles.css';
|
||||
```
|
||||
|
||||
This change also removes the `noCssBaseline` prop, which became redundant.
|
||||
@@ -282,6 +282,7 @@ modularization
|
||||
monorepo
|
||||
Monorepo
|
||||
monorepos
|
||||
monospace
|
||||
morgan
|
||||
msgraph
|
||||
msw
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 7
|
||||
days-before-issue-close: 14
|
||||
exempt-issue-labels: after vacations,no stale
|
||||
stale-issue-label: stale
|
||||
stale-pr-message: >
|
||||
@@ -35,8 +35,8 @@ jobs:
|
||||
recent activity from the author. It will be closed if no further activity occurs.
|
||||
If the PR was closed and you want it re-opened, let us know
|
||||
and we'll re-open the PR so that you can continue the contribution!
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: 7
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 14
|
||||
exempt-pr-labels: after vacations,no stale
|
||||
stale-pr-label: stale
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -41,6 +41,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: yarn install
|
||||
uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
|
||||
with:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -85,6 +85,11 @@ building it incrementally with not conflict with the existing theming system.
|
||||
description="A search field component to help you search for items."
|
||||
href="/components/search-field"
|
||||
/>
|
||||
<ComponentCard
|
||||
title="PasswordField"
|
||||
description="A password field component to help you input passwords."
|
||||
href="/components/password-field"
|
||||
/>
|
||||
<ComponentCard
|
||||
title="Select"
|
||||
description="A select component to help you select items."
|
||||
|
||||
@@ -226,7 +226,7 @@ color of your app.
|
||||
|
||||
### Foreground colors
|
||||
|
||||
Foreground colours are meant to work in pair with a background colours. Typeically this would work
|
||||
Foreground colours are meant to work in pair with a background colours. Typically this would work
|
||||
for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors
|
||||
are prefixed with `fg` to make it easier to identify.
|
||||
|
||||
@@ -446,7 +446,7 @@ monospace font that we use for code blocks and tables.
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<Chip head>--bui-font-mono</Chip>
|
||||
<Chip head>--bui-font-monospace</Chip>
|
||||
</Table.Cell>
|
||||
<Table.Cell>The monospace font for the theme.</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
@@ -70,6 +70,16 @@ export const menuPropDefs: Record<string, PropDef> = {
|
||||
type: 'enum',
|
||||
values: placementValues,
|
||||
},
|
||||
virtualized: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
},
|
||||
maxWidth: {
|
||||
type: 'number',
|
||||
},
|
||||
maxHeight: {
|
||||
type: 'number',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -95,6 +105,16 @@ export const menuListBoxPropDefs: Record<string, PropDef> = {
|
||||
type: 'enum',
|
||||
values: placementValues,
|
||||
},
|
||||
virtualized: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
},
|
||||
maxWidth: {
|
||||
type: 'number',
|
||||
},
|
||||
maxHeight: {
|
||||
type: 'number',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -104,6 +124,16 @@ export const menuAutocompletePropDefs: Record<string, PropDef> = {
|
||||
type: 'enum',
|
||||
values: placementValues,
|
||||
},
|
||||
virtualized: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
},
|
||||
maxWidth: {
|
||||
type: 'number',
|
||||
},
|
||||
maxHeight: {
|
||||
type: 'number',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -113,6 +143,16 @@ export const menuAutocompleteListboxPropDefs: Record<string, PropDef> = {
|
||||
type: 'enum',
|
||||
values: placementValues,
|
||||
},
|
||||
virtualized: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
},
|
||||
maxWidth: {
|
||||
type: 'number',
|
||||
},
|
||||
maxHeight: {
|
||||
type: 'number',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { PasswordFieldSnippet } from '@/snippets/stories-snippets';
|
||||
import {
|
||||
inputPropDefs,
|
||||
passwordFieldUsageSnippet,
|
||||
passwordFieldDefaultSnippet,
|
||||
passwordFieldSizesSnippet,
|
||||
passwordFieldDescriptionSnippet,
|
||||
} from './password-field.props';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
|
||||
<PageTitle
|
||||
title="PasswordField"
|
||||
description="A password field component for your forms."
|
||||
/>
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<PasswordFieldSnippet story="WithLabel" />}
|
||||
code={passwordFieldDefaultSnippet}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={passwordFieldUsageSnippet} />
|
||||
|
||||
## API reference
|
||||
|
||||
<PropsTable data={inputPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
We support two different sizes: `small`, `medium`.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<PasswordFieldSnippet story="Sizes" />}
|
||||
code={passwordFieldSizesSnippet}
|
||||
/>
|
||||
|
||||
### With description
|
||||
|
||||
Here's a simple PasswordField with a description.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<PasswordFieldSnippet story="WithDescription" />}
|
||||
code={passwordFieldDescriptionSnippet}
|
||||
/>
|
||||
|
||||
<Theming component="PasswordField" />
|
||||
|
||||
<ChangelogComponent component="password-field" />
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
|
||||
export const inputPropDefs: Record<string, PropDef> = {
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
default: 'small',
|
||||
responsive: true,
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
},
|
||||
icon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui';
|
||||
|
||||
<PasswordField />`;
|
||||
|
||||
export const passwordFieldDefaultSnippet = `<PasswordField label="Label" placeholder="Enter a secret" />`;
|
||||
|
||||
export const passwordFieldSizesSnippet = `<Flex direction="row" gap="4">
|
||||
<PasswordField size="small" placeholder="Small" icon={<Icon name="sparkling" />} />
|
||||
<PasswordField size="medium" placeholder="Medium" icon={<Icon name="sparkling" />} />
|
||||
</Flex>`;
|
||||
|
||||
export const passwordFieldDescriptionSnippet = `<PasswordField label="Label" description="Description" placeholder="Enter a secret" />`;
|
||||
@@ -29,6 +29,7 @@ import * as HeaderStories from '../../../packages/ui/src/components/Header/Heade
|
||||
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
|
||||
import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories';
|
||||
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
|
||||
import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories';
|
||||
|
||||
// Helper function to create snippet components
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -54,6 +55,8 @@ export const ContainerSnippet = createSnippetComponent(ContainerStories);
|
||||
export const GridSnippet = createSnippetComponent(GridStories);
|
||||
export const IconSnippet = createSnippetComponent(IconStories);
|
||||
export const TextFieldSnippet = createSnippetComponent(TextFieldStories);
|
||||
export const PasswordFieldSnippet =
|
||||
createSnippetComponent(PasswordFieldStories);
|
||||
export const TextSnippet = createSnippetComponent(TextStories);
|
||||
export const FlexSnippet = createSnippetComponent(FlexStories);
|
||||
export const SelectSnippet = createSnippetComponent(SelectStories);
|
||||
|
||||
@@ -25,7 +25,8 @@ export type Component =
|
||||
| 'radio-group'
|
||||
| 'card'
|
||||
| 'skeleton'
|
||||
| 'header';
|
||||
| 'header'
|
||||
| 'passwordfield';
|
||||
|
||||
export type Version = `${number}.${number}.${number}`;
|
||||
|
||||
|
||||
@@ -131,6 +131,11 @@ export const components: Page[] = [
|
||||
slug: 'menu',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'PasswordField',
|
||||
slug: 'password-field',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'RadioGroup',
|
||||
slug: 'radio-group',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,124 @@
|
||||
---
|
||||
id: icons
|
||||
title: Customizing Icons
|
||||
sidebar_label: Icons
|
||||
description: Customizing Icons
|
||||
---
|
||||
|
||||
So far you've seen how to create your own theme and add your own logo, in the following sections you'll be shown how to override the existing icons and how to add more icons
|
||||
|
||||
## Custom Icons
|
||||
|
||||
You can also customize the Project's _default_ icons.
|
||||
|
||||
You can change the following [icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx).
|
||||
|
||||
### Requirements
|
||||
|
||||
- Files in `.svg` format
|
||||
- React components created for the icons
|
||||
|
||||
### Create React Component
|
||||
|
||||
In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `customIcons.tsx` file.
|
||||
|
||||
```tsx title="customIcons.tsx"
|
||||
import { SvgIcon, SvgIconProps } from '@material-ui/core';
|
||||
|
||||
export const ExampleIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
height="1em"
|
||||
display="inline-block"
|
||||
d="M11.6335 10.8398C11.6335 11.6563 12.065 12.9922 13.0863 12.9922C14.1075 12.9922 14.539 11.6563 14.539 10.8398C14.539 10.0234 14.1075 8.6875 13.0863 8.6875C12.065 8.6875 11.6335 10.0234 11.6335 10.8398V10.8398ZM2.38419e-07 8.86719C2.38419e-07 10.1133 0.126667 11.4336 0.692709 12.5781C2.19292 15.5703 6.3175 15.5 9.27042 15.5C12.2708 15.5 16.6408 15.6055 18.2004 12.5781C18.7783 11.4453 19 10.1133 19 8.86719C19 7.23047 18.4498 5.68359 17.3573 4.42969C17.5631 3.8125 17.6621 3.16406 17.6621 2.52344C17.6621 1.68359 17.4681 1.26172 17.0842 0.5C15.291 0.5 14.1431 0.851562 12.7775 1.90625C11.6296 1.63672 10.45 1.51562 9.26646 1.51562C8.19771 1.51562 7.12104 1.62891 6.08396 1.875C4.73813 0.832031 3.59021 0.5 1.81687 0.5C1.42896 1.26172 1.23896 1.68359 1.23896 2.52344C1.23896 3.16406 1.34188 3.80078 1.54375 4.40625C0.455209 5.67188 2.38419e-07 7.23047 2.38419e-07 8.86719V8.86719ZM2.54521 10.8398C2.54521 9.125 3.60208 7.61328 5.45458 7.61328C6.20271 7.61328 6.91917 7.74609 7.67125 7.84766C8.26104 7.9375 8.85083 7.97266 9.45646 7.97266C10.0581 7.97266 10.6479 7.9375 11.2417 7.84766C11.9819 7.74609 12.7063 7.61328 13.4583 7.61328C15.3108 7.61328 16.3677 9.125 16.3677 10.8398C16.3677 14.2695 13.1852 14.7969 10.4144 14.7969H8.50646C5.72375 14.7969 2.54521 14.2734 2.54521 10.8398V10.8398ZM5.81479 8.6875C6.83604 8.6875 7.2675 10.0234 7.2675 10.8398C7.2675 11.6563 6.83604 12.9922 5.81479 12.9922C4.79354 12.9922 4.36208 11.6563 4.36208 10.8398C4.36208 10.0234 4.79354 8.6875 5.81479 8.6875Z"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
```
|
||||
|
||||
### Using the custom icon
|
||||
|
||||
Supply your custom icon in `packages/app/src/App.tsx`
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ExampleIcon } from './assets/icons/CustomIcons'
|
||||
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
components: {
|
||||
{/* ... */}
|
||||
},
|
||||
themes: [
|
||||
{/* ... */}
|
||||
],
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
github: ExampleIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
bindRoutes({ bind }) {
|
||||
{/* ... */}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Adding Icons
|
||||
|
||||
You can add more icons, if the [default icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx) do not fit your needs, so that they can be used in other places like for Links in your entities. For this example we'll be using icons from[Material UI](https://v4.mui.com/components/material-icons/) and specifically the `AlarmIcon`. Here's how to do that:
|
||||
|
||||
1. First you will want to open your `App.tsx` in `/packages/app/src`
|
||||
2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';`
|
||||
3. Next you want to add the icon like this to your `createApp`:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
themes: ...,
|
||||
components: ...,
|
||||
});
|
||||
```
|
||||
|
||||
4. Now we can reference `alert` for our icon in our entity links like this:
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: artist-lookup
|
||||
description: Artist Lookup
|
||||
links:
|
||||
- url: https://example.com/alert
|
||||
title: Alerts
|
||||
icon: alert
|
||||
```
|
||||
|
||||
And this is the result:
|
||||
|
||||

|
||||
|
||||
Another way you can use these icons is from the `AppContext` like this:
|
||||
|
||||
```ts
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
|
||||
const app = useApp();
|
||||
const alertIcon = app.getSystemIcon('alert');
|
||||
```
|
||||
|
||||
You might want to use this method if you have an icon you want to use in several locations.
|
||||
|
||||
:::note Note
|
||||
|
||||
If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon`
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,621 @@
|
||||
---
|
||||
id: index
|
||||
title: Customizing Your App's UI
|
||||
sidebar_label: Introduction
|
||||
description: Learn how to customize the look and feel of your Backstage app, including theming and branding options.
|
||||
---
|
||||
|
||||
Backstage offers built-in support for both light and dark themes, making it easy to get started with a professional look and feel. But many teams want to go further—tailoring the interface to reflect their organization’s unique brand, identity, and experience.
|
||||
|
||||
This section explores the different ways you can customize the appearance of your Backstage instance. You'll learn how the theming system is structured today, how to work with the two coexisting UI systems, and how to define themes that align with your visual language.
|
||||
|
||||
## Theming architecture overview
|
||||
|
||||
Backstage currently supports two parallel UI systems. The original theming and component model is built on Material UI (MUI), a popular React-based framework. More recently, Backstage introduced Backstage UI (BUI), a custom-designed, CSS-first system developed to meet the platform’s evolving needs. Both systems are supported today, with many parts of the ecosystem still using MUI while new components adopt BUI.
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<div style="flex: 1; min-width: 250px; border: 1px solid #ccc; border-radius: 8px; padding: 1rem;">
|
||||
<h3 style="margin-top: 0;">MUI (Legacy)</h3>
|
||||
<ul>
|
||||
<li><strong>Theming:</strong> JS-based with <code>UnifiedThemeProvider</code></li>
|
||||
<li><strong>Coverage:</strong> Most existing plugins</li>
|
||||
<li><strong>Documentation:</strong> <a href="https://mui.com/material-ui/" target="_blank">mui.com</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 250px; border: 1px solid #ccc; border-radius: 8px; padding: 1rem;">
|
||||
<h3 style="margin-top: 0;">Backstage UI (New)</h3>
|
||||
<ul>
|
||||
<li><strong>Theming:</strong> CSS variables and tokens</li>
|
||||
<li><strong>Coverage:</strong> Growing, focused on new work</li>
|
||||
<li><strong>Documentation:</strong> <a href="https://ui.backstage.io" target="_blank">ui.backstage.io</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
:::info
|
||||
We recognize that maintaining two separate theming systems is not ideal. Because of the fundamental architectural differences between MUI and Backstage UI, it can be challenging to automate theme updates or know exactly which theme to modify for a given component. Our recommendation is to inspect the component’s code and check its class names: if you see a class name starting with `bui`, you should use the Backstage UI theming approach to style it.
|
||||
:::
|
||||
|
||||
## Creating custom themes
|
||||
|
||||
During the transition to Backstage UI, you will need to maintain themes in two places: some components and plugins still rely on MUI, while others use Backstage UI. We are working on a plugin that will make help you convert your existing MUI theme into a Backstage UI CSS file you can add to your application. We'll update this page when the plugin is available but for now you can follow progress on this PR [#31140](https://github.com/backstage/backstage/pull/31140).
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-start */
|
||||
import { lightTheme, darkTheme } from './themes'; // MUI themes
|
||||
import './styles.css'; // Backstage UI (BUI) theme
|
||||
/* highlight-add-end */
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
components,
|
||||
/* highlight-add-start */
|
||||
themes: [
|
||||
{
|
||||
id: 'light',
|
||||
title: 'Light theme',
|
||||
variant: 'light',
|
||||
icon: <LightIcon />,
|
||||
Provider: ({ children }) => (
|
||||
<UnifiedThemeProvider theme={lightTheme} children={children} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
title: 'Dark theme',
|
||||
variant: 'dark',
|
||||
icon: <DarkIcon />,
|
||||
Provider: ({ children }) => (
|
||||
<UnifiedThemeProvider theme={darkTheme} children={children} />
|
||||
),
|
||||
},
|
||||
],
|
||||
/* highlight-add-end */
|
||||
});
|
||||
```
|
||||
|
||||
| Name | Description |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Each theme has a unique `id` |
|
||||
| `title` | This will be shown in the settings page to select the right theme. |
|
||||
| `variant` | This can be either `light` or `dark`. This is also referred to as `mode`. On the `body` of your app we are inserting a data attribute to set the theme based on this value: `data-theme-mode="light"`. |
|
||||
| `icon` | This will be shown in the settings page as a visual element to complement the title. |
|
||||
| `Provider` | This is needed to set the legacy theme with MUI only. This will be become redundant later on when we fully replace with BUI but for now you need to have it for MUI to work. BUI is based on CSS and don't rely on any global providers. |
|
||||
|
||||
:::note
|
||||
Your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). Be sure to provide both `light` and `dark` modes so users can choose their preference.
|
||||
:::
|
||||
|
||||
## Create a theme for Backstage UI (New)
|
||||
|
||||
Backstage UI is built entirely using CSS. By default we are providing a default theme that include all our core CSS variables and component styles. To start customising Backstage UI to match your brand you need to create a new CSS file and import it directly in `packages/app/src/App.tsx`. All styles declared in this file will override the default styles. As your file grow you can organise it the way you want or even import multiple files.
|
||||
|
||||
Backstage UI is using light by default under `:root` but you can target it more specifically using the data attribute for mode
|
||||
|
||||
```css title="packages/app/src/styles.css"
|
||||
:root {
|
||||
/* Use :root to set styles for both light and dark themes */
|
||||
.bui-Button {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme-mode='light'] {
|
||||
/* Light theme specific styles */
|
||||
--bui-bg: #f8f8f8;
|
||||
--bui-fg-primary: #000;
|
||||
}
|
||||
|
||||
[data-theme-mode='dark'] {
|
||||
/* Dark theme specific styles */
|
||||
--bui-bg: #333333;
|
||||
--bui-fg-primary: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
### CSS variables
|
||||
|
||||
By adjusting just a few theme variables, you can easily transform the look and feel of your Backstage instance to align with your brand identity. All colors are defined using these variables, ensuring they adapt seamlessly to both light and dark modes.
|
||||
|
||||
We recommend starting with a core set of CSS variables to quickly achieve a branded experience. You’ll also find a complete list of available variables below, giving you full flexibility to fine-tune the design to your needs.
|
||||
|
||||
And if you’d like to go even further, you can target specific component class names for advanced customization.
|
||||
|
||||
| Token Name | Description |
|
||||
| -------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `--bui-bg` | This is used to define the background color of your app. It will only be used once. |
|
||||
| `--bui-bg-surface-1` | We ar using this color to sit on top of `--bui-bg` mostly for `Card`, `Dialog`, ... |
|
||||
| `--bui-bg-surface-2` | This is for content inside elevated components. This colour is less common. |
|
||||
| `--bui-bg-solid` | This is used for main actions like primary buttons. |
|
||||
| `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. |
|
||||
| `--bui-fg-primary` | Your primary text or icon colours. |
|
||||
| `--bui-fg-secondary` | Your secondary text or icon colours. |
|
||||
| `--bui-fg-link` | Used for links. |
|
||||
| `--bui-border` | Main borders around surfaces like `Card`, `Dialog`, ... |
|
||||
| `--bui-font-regular` | The main font of your app. |
|
||||
|
||||
<details>
|
||||
<summary>All available CSS variables</summary>
|
||||
|
||||
#### Base colors
|
||||
|
||||
These colors are used for special purposes like ring, scrollbar, ...
|
||||
|
||||
| Token Name | Description |
|
||||
| -------------- | ----------------------------------------------------------------------- |
|
||||
| `--bui-black` | Pure black color. This one should be the same in light and dark themes. |
|
||||
| `--bui-white` | Pure white color. This one should be the same in light and dark themes. |
|
||||
| `--bui-gray-1` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-2` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-3` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-4` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-5` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-6` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-7` | You can use these mostly for backgrounds colors. |
|
||||
| `--bui-gray-8` | You can use these mostly for backgrounds colors. |
|
||||
|
||||
#### Core background colors
|
||||
|
||||
These colors are used for the background of your application. We are mostly using for now a single elevated background for panels. `--bui-bg` should mostly use as the main background color of your app.
|
||||
|
||||
| Token Name | Description |
|
||||
| ------------------------- | ------------------------------------------------ |
|
||||
| `--bui-bg` | The background color of your Backstage instance. |
|
||||
| `--bui-bg-surface-1` | Use for any panels or elevated surfaces. |
|
||||
| `--bui-bg-surface-2` | Use for any panels or elevated surfaces. |
|
||||
| `--bui-bg-solid` | Used for solid background colors. |
|
||||
| `--bui-bg-solid-hover` | Used for solid background colors when hovered. |
|
||||
| `--bui-bg-solid-pressed` | Used for solid background colors when pressed. |
|
||||
| `--bui-bg-solid-disabled` | Used for solid background colors when disabled. |
|
||||
| `--bui-bg-tint` | Used for tint background colors. |
|
||||
| `--bui-bg-tint-hover` | Used for tint background colors when hovered. |
|
||||
| `--bui-bg-tint-focus` | Used for tint background colors when active. |
|
||||
| `--bui-bg-tint-disabled` | Used for tint background colors when disabled. |
|
||||
| `--bui-bg-danger` | Used to show errors information. |
|
||||
| `--bui-bg-warning` | Used to show warnings information. |
|
||||
| `--bui-bg-success` | Used to show success information. |
|
||||
|
||||
#### Foreground colors
|
||||
|
||||
Foreground colours are meant to work in pair with a background colours. Typically this would work for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors are prefixed with `fg` to make it easier to identify.
|
||||
|
||||
| Token Name | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------- |
|
||||
| `--bui-fg-primary` | It should be used on top of main background surfaces. |
|
||||
| `--bui-fg-secondary` | It should be used on top of main background surfaces. |
|
||||
| `--bui-fg-link` | It should be used on top of main background surfaces. |
|
||||
| `--bui-fg-link-hover` | It should be used on top of main background surfaces. |
|
||||
| `--bui-fg-disabled` | It should be used on top of main background surfaces. |
|
||||
| `--bui-fg-solid` | It should be used on top of solid background colors. |
|
||||
| `--bui-fg-tint` | It should be used on top of tint background colors. |
|
||||
| `--bui-fg-tint-disabled` | It should be used on top of tint background colors when disabled. |
|
||||
| `--bui-fg-danger` | It should be used on top of danger background colors. |
|
||||
| `--bui-fg-warning` | It should be used on top of warning background colors. |
|
||||
| `--bui-fg-success` | It should be used on top of success background colors. |
|
||||
|
||||
#### Border colors
|
||||
|
||||
These border colors are mostly meant to be used as borders on top of any components with low contrast to help as a separator with the different background colors.
|
||||
|
||||
| Token Name | Description |
|
||||
| ----------------------- | --------------------------------------------------- |
|
||||
| `--bui-border` | It should be used on top of `--bui-bg-surface-1`. |
|
||||
| `--bui-border-hover` | Used when the component is interactive and hovered. |
|
||||
| `--bui-border-pressed` | Used when the component is interactive and hovered. |
|
||||
| `--bui-border-disabled` | Used when the component is disabled. |
|
||||
| `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. |
|
||||
| `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. |
|
||||
| `--bui-border-success` | It should be used on top of `--bui-bg-success`. |
|
||||
|
||||
#### Special colors
|
||||
|
||||
These colors are used for special purposes like ring, scrollbar, ...
|
||||
|
||||
| Token Name | Description |
|
||||
| ----------------------- | --------------------------------- |
|
||||
| `--bui-ring` | The color of the ring. |
|
||||
| `--bui-scrollbar` | The color of the scrollbar. |
|
||||
| `--bui-scrollbar-thumb` | The color of the scrollbar thumb. |
|
||||
|
||||
#### Font families
|
||||
|
||||
We have two fonts that we use across Backstage UI. The first one is the sans-serif font that we use for the body of the application. The second one is the monospace font that we use for code blocks and tables.
|
||||
|
||||
| Token Name | Description |
|
||||
| -------------------- | ---------------------------------- |
|
||||
| `--bui-font-regular` | The sans-serif font for the theme. |
|
||||
| `--bui-font-mono` | The monospace font for the theme. |
|
||||
|
||||
#### Font weights
|
||||
|
||||
We have two font weights that we use across Backstage UI. Regular or Bold.
|
||||
|
||||
| Token Name | Description |
|
||||
| --------------------------- | -------------------------------------- |
|
||||
| `--bui-font-weight-regular` | The regular font weight for the theme. |
|
||||
| `--bui-font-weight-bold` | The bold font weight for the theme. |
|
||||
|
||||
#### Spacing
|
||||
|
||||
We built a spacing system based on a single value `--bui-space`. This value is used to calculate the spacing for all the components. By default if you would like to increase or decrease the spacing between your components you can do it simply by updating `--bui-space` and it will apply to all spacing values.
|
||||
|
||||
`--bui-space` is not used directly in any components but serve as an easy way to calculate the other values.
|
||||
|
||||
| Token Name | Description |
|
||||
| ------------- | ----------------------------------------------------------------- |
|
||||
| `--bui-space` | The base unit for the spacing system. Default value is `0.25rem.` |
|
||||
|
||||
#### Radius
|
||||
|
||||
We use a radius system to make sure that the components have a consistent look and feel.
|
||||
|
||||
| Token Name | Description |
|
||||
| ------------------- | --------------------------------------------------------- |
|
||||
| `--bui-radius-1` | The radius of the component. Default value is `0.125rem`. |
|
||||
| `--bui-radius-2` | The radius of the component. Default value is `0.25rem`. |
|
||||
| `--bui-radius-3` | The radius of the component. Default value is `0.5rem`. |
|
||||
| `--bui-radius-4` | The radius of the component. Default value is `0.75rem`. |
|
||||
| `--bui-radius-5` | The radius of the component. Default value is `1rem`. |
|
||||
| `--bui-radius-6` | The radius of the component. Default value is `1.25rem`. |
|
||||
| `--bui-radius-full` | The radius of the component. Default value is `9999px`. |
|
||||
|
||||
</details>
|
||||
|
||||
### Component class names
|
||||
|
||||
All Backstage UI components come with a set of CSS classes that you can use to style them. To make it easier to identify the class name you can use, we use a specific structure for the class names.
|
||||
|
||||

|
||||
|
||||
Every component has a unique prefix `.bui-` followed by the component name. Component props are represented using the `data-` attribute. That way, class names are easily identifiable.
|
||||
|
||||
## Create a theme for MUI (Legacy)
|
||||
|
||||
To customize the appearance of your Backstage app using the legacy MUI theming system, you can define your own theme by extending the built-in light or dark themes. This is done using the createUnifiedTheme utility provided by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. This function allows you to override key aspects of the theme—such as color palette, typography, spacing, and shape—while preserving Backstage’s base configuration and component compatibility.
|
||||
|
||||
The example below shows how to create a new theme based on the default light theme:
|
||||
|
||||
```ts title="packages/app/src/themes.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const lightTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
});
|
||||
|
||||
export const darkTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.dark,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
});
|
||||
```
|
||||
|
||||
You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the
|
||||
[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done.
|
||||
|
||||
<details>
|
||||
<summary>Example of a custom MUI theme</summary>
|
||||
|
||||
For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io).
|
||||
|
||||
```ts title="packages/app/src/themes.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
genPageTheme,
|
||||
palettes,
|
||||
shapes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: {
|
||||
...palettes.light,
|
||||
primary: {
|
||||
main: '#343b58',
|
||||
},
|
||||
secondary: {
|
||||
main: '#565a6e',
|
||||
},
|
||||
error: {
|
||||
main: '#8c4351',
|
||||
},
|
||||
warning: {
|
||||
main: '#8f5e15',
|
||||
},
|
||||
info: {
|
||||
main: '#34548a',
|
||||
},
|
||||
success: {
|
||||
main: '#485e30',
|
||||
},
|
||||
background: {
|
||||
default: '#d5d6db',
|
||||
paper: '#d5d6db',
|
||||
},
|
||||
banner: {
|
||||
info: '#34548a',
|
||||
error: '#8c4351',
|
||||
text: '#343b58',
|
||||
link: '#565a6e',
|
||||
},
|
||||
errorBackground: '#8c4351',
|
||||
warningBackground: '#8f5e15',
|
||||
infoBackground: '#343b58',
|
||||
navigation: {
|
||||
background: '#343b58',
|
||||
indicator: '#8f5e15',
|
||||
color: '#d5d6db',
|
||||
selectedColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultPageTheme: 'home',
|
||||
fontFamily: 'Comic Sans MS',
|
||||
/* below drives the header colors */
|
||||
pageTheme: {
|
||||
home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
documentation: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave2,
|
||||
}),
|
||||
tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }),
|
||||
service: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave,
|
||||
}),
|
||||
website: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave,
|
||||
}),
|
||||
library: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave,
|
||||
}),
|
||||
other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Custom Typography</summary>
|
||||
|
||||
When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
h1: {
|
||||
fontSize: 54,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 40,
|
||||
fontWeight: 700,
|
||||
marginBottom: 8,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 32,
|
||||
fontWeight: 700,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h4: {
|
||||
fontWeight: 700,
|
||||
fontSize: 28,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h5: {
|
||||
fontWeight: 700,
|
||||
fontSize: 24,
|
||||
marginBottom: 4,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
marginBottom: 2,
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
defaultTypography,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
...defaultTypography,
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
h1: {
|
||||
fontSize: 72,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Custom Fonts</summary>
|
||||
|
||||
To add custom fonts, you first need to store the font so that it can be imported. We suggest creating the `assets/fonts` directory in your front-end application `src` folder.
|
||||
|
||||
You can then declare the font style following the `@font-face` syntax from [Material UI Typography](https://mui.com/material-ui/customization/typography/).
|
||||
|
||||
After that you can then utilize the `styleOverrides` of `MuiCssBaseline` under components to add a font to the `@font-face` array.
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2';
|
||||
|
||||
const myCustomFont = {
|
||||
fontFamily: 'My-Custom-Font',
|
||||
fontStyle: 'normal',
|
||||
fontDisplay: 'swap',
|
||||
fontWeight: 300,
|
||||
src: `
|
||||
local('My-Custom-Font'),
|
||||
url(${MyCustomFont}) format('woff2'),
|
||||
`,
|
||||
};
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
fontFamily: 'My-Custom-Font',
|
||||
palette: palettes.light,
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
'@font-face': [myCustomFont],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you want to utilize different or multiple fonts, then you can set the top level `fontFamily` to what you want for your body, and then override `fontFamily` in `typography` to control fonts for various headings.
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2';
|
||||
import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2';
|
||||
|
||||
const myCustomFont = {
|
||||
fontFamily: 'My-Custom-Font',
|
||||
fontStyle: 'normal',
|
||||
fontDisplay: 'swap',
|
||||
fontWeight: 300,
|
||||
src: `
|
||||
local('My-Custom-Font'),
|
||||
url(${MyCustomFont}) format('woff2'),
|
||||
`,
|
||||
};
|
||||
|
||||
const myAwesomeFont = {
|
||||
fontFamily: 'My-Awesome-Font',
|
||||
fontStyle: 'normal',
|
||||
fontDisplay: 'swap',
|
||||
fontWeight: 300,
|
||||
src: `
|
||||
local('My-Awesome-Font'),
|
||||
url(${myAwesomeFont}) format('woff2'),
|
||||
`,
|
||||
};
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
fontFamily: 'My-Custom-Font',
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
'@font-face': [myCustomFont, myAwesomeFont],
|
||||
},
|
||||
},
|
||||
},
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
...defaultTypography,
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'My-Custom-Font',
|
||||
h1: {
|
||||
fontSize: 72,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
fontFamily: 'My-Awesome-Font',
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Overriding Backstage and Material UI components styles</summary>
|
||||
|
||||
When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this:
|
||||
|
||||
```tsx
|
||||
const useStyles = makeStyles<BackstageTheme>(
|
||||
theme => ({
|
||||
header: {
|
||||
padding: theme.spacing(3),
|
||||
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
|
||||
backgroundImage: theme.page.backgroundImage,
|
||||
},
|
||||
}),
|
||||
{ name: 'BackstageHeader' },
|
||||
);
|
||||
```
|
||||
|
||||
Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override.
|
||||
|
||||
Here's how you would do that:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
components: {
|
||||
BackstageHeader: {
|
||||
styleOverrides: {
|
||||
header: ({ theme }) => ({
|
||||
width: 'auto',
|
||||
margin: '20px',
|
||||
boxShadow: 'none',
|
||||
borderBottom: `4px solid ${theme.palette.primary.main}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
id: logo
|
||||
title: Customizing Your Logo
|
||||
sidebar_label: Logo
|
||||
description: Learn how to customize your logo.
|
||||
---
|
||||
|
||||
In addition to a custom theme, you can also customize the logo displayed at the far top left of the site.
|
||||
|
||||
In your frontend app, locate `src/components/Root/` folder. You'll find two components:
|
||||
|
||||
- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened.
|
||||
- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed.
|
||||
|
||||
To replace the images, you can simply replace the relevant code in those components with raw SVG definitions.
|
||||
|
||||
You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code:
|
||||
|
||||
```tsx
|
||||
import MyCustomLogoFull from './logo/my-company-logo.png';
|
||||
|
||||
const LogoFull = () => {
|
||||
return <img src={MyCustomLogoFull} />;
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
id: sidebar
|
||||
title: Customizing Your Sidebar
|
||||
sidebar_label: Sidebar
|
||||
description: Learn how to customize the look and feel of your Sidebar.
|
||||
---
|
||||
|
||||
As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar.
|
||||
|
||||
## Sidebar Sub-menu
|
||||
|
||||
For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives
|
||||
2. Then we want to add the following import for `useApp`:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
```
|
||||
|
||||
3. Then update the `@backstage/core-components` import like this:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
/* highlight-add-start */
|
||||
GroupIcon,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
/* highlight-add-end */
|
||||
} from '@backstage/core-components';
|
||||
```
|
||||
|
||||
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
title="Domains"
|
||||
to="catalog?filters[kind]=domain"
|
||||
icon={useApp().getSystemIcon('kind:domain')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Systems"
|
||||
to="catalog?filters[kind]=system"
|
||||
icon={useApp().getSystemIcon('kind:system')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Components"
|
||||
to="catalog?filters[kind]=component"
|
||||
icon={useApp().getSystemIcon('kind:component')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="APIs"
|
||||
to="catalog?filters[kind]=api"
|
||||
icon={useApp().getSystemIcon('kind:api')}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Resources"
|
||||
to="catalog?filters[kind]=resource"
|
||||
icon={useApp().getSystemIcon('kind:resource')}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Groups"
|
||||
to="catalog?filters[kind]=group"
|
||||
icon={useApp().getSystemIcon('kind:group')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Users"
|
||||
to="catalog?filters[kind]=user"
|
||||
icon={useApp().getSystemIcon('kind:user')}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
```
|
||||
|
||||
When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this:
|
||||
|
||||

|
||||
|
||||
You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar)
|
||||
@@ -103,6 +103,4 @@ accessibility.
|
||||
[7]: https://v4.mui.com/components/cards/
|
||||
[8]: https://v4.mui.com/customization/palette/#default-values
|
||||
[9]: https://v4.mui.com/customization/typography/
|
||||
[10]: https://backstage.io/docs/getting-started/app-custom-theme
|
||||
[11]: https://backstage.io/docs/getting-started/app-custom-theme#overriding-backstage-and-material-ui-components-styles
|
||||
[12]: https://v4.mui.com/customization/default-theme/#explore
|
||||
[10]: https://backstage.io/docs/conf/user-interface
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ your own plugin for Backstage.
|
||||
**[Discord](https://discord.gg/backstage-687207715902193673)** - all design questions should be
|
||||
directed to the _#design_ channel.
|
||||
|
||||
**[Customize Backstage's look and feel](https://backstage.io/docs/getting-started/app-custom-theme)** -
|
||||
**[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)** -
|
||||
How to customize the look and feel of your Backstage instance by extending the
|
||||
theme.
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ indexBuilder.addCollator({
|
||||
The default highlighting styling for matched terms in search results is your
|
||||
browsers default styles for the `<mark>` HTML tag. If you want to customize
|
||||
how highlighted terms look you can follow Backstage's guide on how to
|
||||
[Customize the look-and-feel of your App](https://backstage.io/docs/getting-started/app-custom-theme)
|
||||
[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)
|
||||
to create an override with your preferred styling.
|
||||
|
||||
For example, using the new MUI V4+V5 unified theming method, the following will result
|
||||
|
||||
@@ -34,6 +34,14 @@ const examplePage = createExtension({
|
||||
});
|
||||
```
|
||||
|
||||
### `title`
|
||||
|
||||
| id | type |
|
||||
| :----------: | :------: |
|
||||
| `core.title` | `string` |
|
||||
|
||||
The `title` data reference can be used for defining the extension input/output of string titles.
|
||||
|
||||
### `routePath`
|
||||
|
||||
| id | type |
|
||||
|
||||
@@ -1,613 +0,0 @@
|
||||
---
|
||||
id: app-custom-theme
|
||||
title: Customize the look-and-feel of your App
|
||||
description: Documentation on Customizing look and feel of the App
|
||||
---
|
||||
|
||||
Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, which also includes utilities for customizing the default theme, or creating completely new themes.
|
||||
|
||||
## Creating a Custom Theme
|
||||
|
||||
The easiest way to create a new theme is to use the `createUnifiedTheme` function exported by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override some basic parameters of the default theme such as the color palette and font.
|
||||
|
||||
For example, you can create a new theme based on the default light theme like this:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
});
|
||||
```
|
||||
|
||||
:::note Note
|
||||
|
||||
we recommend creating a `theme` folder in `packages/app/src` to place your theme file to keep things nicely organized.
|
||||
|
||||
:::
|
||||
|
||||
You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the
|
||||
[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done.
|
||||
|
||||
## Using your Custom Theme
|
||||
|
||||
To add a custom theme to your Backstage app, you pass it as configuration to `createApp`.
|
||||
|
||||
For example, adding the theme that we created in the previous section can be done like this:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import LightIcon from '@material-ui/icons/WbSunny';
|
||||
import { UnifiedThemeProvider} from '@backstage/theme';
|
||||
import { myTheme } from './themes/myTheme';
|
||||
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
themes: [{
|
||||
id: 'my-theme',
|
||||
title: 'My Custom Theme',
|
||||
variant: 'light',
|
||||
icon: <LightIcon />,
|
||||
Provider: ({ children }) => (
|
||||
<UnifiedThemeProvider theme={myTheme} children={children} />
|
||||
),
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme).
|
||||
|
||||
## Example of a custom theme
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
genPageTheme,
|
||||
palettes,
|
||||
shapes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: {
|
||||
...palettes.light,
|
||||
primary: {
|
||||
main: '#343b58',
|
||||
},
|
||||
secondary: {
|
||||
main: '#565a6e',
|
||||
},
|
||||
error: {
|
||||
main: '#8c4351',
|
||||
},
|
||||
warning: {
|
||||
main: '#8f5e15',
|
||||
},
|
||||
info: {
|
||||
main: '#34548a',
|
||||
},
|
||||
success: {
|
||||
main: '#485e30',
|
||||
},
|
||||
background: {
|
||||
default: '#d5d6db',
|
||||
paper: '#d5d6db',
|
||||
},
|
||||
banner: {
|
||||
info: '#34548a',
|
||||
error: '#8c4351',
|
||||
text: '#343b58',
|
||||
link: '#565a6e',
|
||||
},
|
||||
errorBackground: '#8c4351',
|
||||
warningBackground: '#8f5e15',
|
||||
infoBackground: '#343b58',
|
||||
navigation: {
|
||||
background: '#343b58',
|
||||
indicator: '#8f5e15',
|
||||
color: '#d5d6db',
|
||||
selectedColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultPageTheme: 'home',
|
||||
fontFamily: 'Comic Sans MS',
|
||||
/* below drives the header colors */
|
||||
pageTheme: {
|
||||
home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
documentation: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave2,
|
||||
}),
|
||||
tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }),
|
||||
service: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave,
|
||||
}),
|
||||
website: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave,
|
||||
}),
|
||||
library: genPageTheme({
|
||||
colors: ['#8c4351', '#343b58'],
|
||||
shape: shapes.wave,
|
||||
}),
|
||||
other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io).
|
||||
|
||||
## Custom Typography
|
||||
|
||||
When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
h1: {
|
||||
fontSize: 54,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 40,
|
||||
fontWeight: 700,
|
||||
marginBottom: 8,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 32,
|
||||
fontWeight: 700,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h4: {
|
||||
fontWeight: 700,
|
||||
fontSize: 28,
|
||||
marginBottom: 6,
|
||||
},
|
||||
h5: {
|
||||
fontWeight: 700,
|
||||
fontSize: 24,
|
||||
marginBottom: 4,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
marginBottom: 2,
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
defaultTypography,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
...defaultTypography,
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
h1: {
|
||||
fontSize: 72,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Fonts
|
||||
|
||||
To add custom fonts, you first need to store the font so that it can be imported. We suggest creating the `assets/fonts` directory in your front-end application `src` folder.
|
||||
|
||||
You can then declare the font style following the `@font-face` syntax from [Material UI Typography](https://mui.com/material-ui/customization/typography/).
|
||||
|
||||
After that you can then utilize the `styleOverrides` of `MuiCssBaseline` under components to add a font to the `@font-face` array.
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2';
|
||||
|
||||
const myCustomFont = {
|
||||
fontFamily: 'My-Custom-Font',
|
||||
fontStyle: 'normal',
|
||||
fontDisplay: 'swap',
|
||||
fontWeight: 300,
|
||||
src: `
|
||||
local('My-Custom-Font'),
|
||||
url(${MyCustomFont}) format('woff2'),
|
||||
`,
|
||||
};
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
fontFamily: 'My-Custom-Font',
|
||||
palette: palettes.light,
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
'@font-face': [myCustomFont],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you want to utilize different or multiple fonts, then you can set the top level `fontFamily` to what you want for your body, and then override `fontFamily` in `typography` to control fonts for various headings.
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2';
|
||||
import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2';
|
||||
|
||||
const myCustomFont = {
|
||||
fontFamily: 'My-Custom-Font',
|
||||
fontStyle: 'normal',
|
||||
fontDisplay: 'swap',
|
||||
fontWeight: 300,
|
||||
src: `
|
||||
local('My-Custom-Font'),
|
||||
url(${MyCustomFont}) format('woff2'),
|
||||
`,
|
||||
};
|
||||
|
||||
const myAwesomeFont = {
|
||||
fontFamily: 'My-Awesome-Font',
|
||||
fontStyle: 'normal',
|
||||
fontDisplay: 'swap',
|
||||
fontWeight: 300,
|
||||
src: `
|
||||
local('My-Awesome-Font'),
|
||||
url(${myAwesomeFont}) format('woff2'),
|
||||
`,
|
||||
};
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
fontFamily: 'My-Custom-Font',
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
'@font-face': [myCustomFont, myAwesomeFont],
|
||||
},
|
||||
},
|
||||
},
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
typography: {
|
||||
...defaultTypography,
|
||||
htmlFontSize: 16,
|
||||
fontFamily: 'My-Custom-Font',
|
||||
h1: {
|
||||
fontSize: 72,
|
||||
fontWeight: 700,
|
||||
marginBottom: 10,
|
||||
fontFamily: 'My-Awesome-Font',
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Overriding Backstage and Material UI components styles
|
||||
|
||||
When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this:
|
||||
|
||||
```tsx
|
||||
const useStyles = makeStyles<BackstageTheme>(
|
||||
theme => ({
|
||||
header: {
|
||||
padding: theme.spacing(3),
|
||||
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
|
||||
backgroundImage: theme.page.backgroundImage,
|
||||
},
|
||||
}),
|
||||
{ name: 'BackstageHeader' },
|
||||
);
|
||||
```
|
||||
|
||||
Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override.
|
||||
|
||||
Here's how you would do that:
|
||||
|
||||
```ts title="packages/app/src/theme/myTheme.ts"
|
||||
import {
|
||||
createBaseThemeOptions,
|
||||
createUnifiedTheme,
|
||||
palettes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
export const myTheme = createUnifiedTheme({
|
||||
...createBaseThemeOptions({
|
||||
palette: palettes.light,
|
||||
}),
|
||||
fontFamily: 'Comic Sans MS',
|
||||
defaultPageTheme: 'home',
|
||||
components: {
|
||||
BackstageHeader: {
|
||||
styleOverrides: {
|
||||
header: ({ theme }) => ({
|
||||
width: 'auto',
|
||||
margin: '20px',
|
||||
boxShadow: 'none',
|
||||
borderBottom: `4px solid ${theme.palette.primary.main}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Logo
|
||||
|
||||
In addition to a custom theme, you can also customize the logo displayed at the far top left of the site.
|
||||
|
||||
In your frontend app, locate `src/components/Root/` folder. You'll find two components:
|
||||
|
||||
- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened.
|
||||
- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed.
|
||||
|
||||
To replace the images, you can simply replace the relevant code in those components with raw SVG definitions.
|
||||
|
||||
You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code:
|
||||
|
||||
```tsx
|
||||
import MyCustomLogoFull from './logo/my-company-logo.png';
|
||||
|
||||
const LogoFull = () => {
|
||||
return <img src={MyCustomLogoFull} />;
|
||||
};
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
So far you've seen how to create your own theme and add your own logo, in the following sections you'll be shown how to override the existing icons and how to add more icons
|
||||
|
||||
### Custom Icons
|
||||
|
||||
You can also customize the Project's _default_ icons.
|
||||
|
||||
You can change the following [icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx).
|
||||
|
||||
#### Requirements
|
||||
|
||||
- Files in `.svg` format
|
||||
- React components created for the icons
|
||||
|
||||
#### Create React Component
|
||||
|
||||
In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `CustomIcons.tsx` file.
|
||||
|
||||
```tsx title="customIcons.tsx"
|
||||
import { SvgIcon, SvgIconProps } from '@material-ui/core';
|
||||
|
||||
export const ExampleIcon = (props: SvgIconProps) => (
|
||||
<SvgIcon {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
height="1em"
|
||||
display="inline-block"
|
||||
d="M11.6335 10.8398C11.6335 11.6563 12.065 12.9922 13.0863 12.9922C14.1075 12.9922 14.539 11.6563 14.539 10.8398C14.539 10.0234 14.1075 8.6875 13.0863 8.6875C12.065 8.6875 11.6335 10.0234 11.6335 10.8398V10.8398ZM2.38419e-07 8.86719C2.38419e-07 10.1133 0.126667 11.4336 0.692709 12.5781C2.19292 15.5703 6.3175 15.5 9.27042 15.5C12.2708 15.5 16.6408 15.6055 18.2004 12.5781C18.7783 11.4453 19 10.1133 19 8.86719C19 7.23047 18.4498 5.68359 17.3573 4.42969C17.5631 3.8125 17.6621 3.16406 17.6621 2.52344C17.6621 1.68359 17.4681 1.26172 17.0842 0.5C15.291 0.5 14.1431 0.851562 12.7775 1.90625C11.6296 1.63672 10.45 1.51562 9.26646 1.51562C8.19771 1.51562 7.12104 1.62891 6.08396 1.875C4.73813 0.832031 3.59021 0.5 1.81687 0.5C1.42896 1.26172 1.23896 1.68359 1.23896 2.52344C1.23896 3.16406 1.34188 3.80078 1.54375 4.40625C0.455209 5.67188 2.38419e-07 7.23047 2.38419e-07 8.86719V8.86719ZM2.54521 10.8398C2.54521 9.125 3.60208 7.61328 5.45458 7.61328C6.20271 7.61328 6.91917 7.74609 7.67125 7.84766C8.26104 7.9375 8.85083 7.97266 9.45646 7.97266C10.0581 7.97266 10.6479 7.9375 11.2417 7.84766C11.9819 7.74609 12.7063 7.61328 13.4583 7.61328C15.3108 7.61328 16.3677 9.125 16.3677 10.8398C16.3677 14.2695 13.1852 14.7969 10.4144 14.7969H8.50646C5.72375 14.7969 2.54521 14.2734 2.54521 10.8398V10.8398ZM5.81479 8.6875C6.83604 8.6875 7.2675 10.0234 7.2675 10.8398C7.2675 11.6563 6.83604 12.9922 5.81479 12.9922C4.79354 12.9922 4.36208 11.6563 4.36208 10.8398C4.36208 10.0234 4.79354 8.6875 5.81479 8.6875Z"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
```
|
||||
|
||||
#### Using the custom icon
|
||||
|
||||
Supply your custom icon in `packages/app/src/App.tsx`
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { ExampleIcon } from './assets/icons/CustomIcons'
|
||||
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
components: {
|
||||
{/* ... */}
|
||||
},
|
||||
themes: [
|
||||
{/* ... */}
|
||||
],
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
github: ExampleIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
bindRoutes({ bind }) {
|
||||
{/* ... */}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Adding Icons
|
||||
|
||||
You can add more icons, if the [default icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx) do not fit your needs, so that they can be used in other places like for Links in your entities. For this example we'll be using icons from[Material UI](https://v4.mui.com/components/material-icons/) and specifically the `AlarmIcon`. Here's how to do that:
|
||||
|
||||
1. First you will want to open your `App.tsx` in `/packages/app/src`
|
||||
2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';`
|
||||
3. Next you want to add the icon like this to your `createApp`:
|
||||
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
/* highlight-add-start */
|
||||
icons: {
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
/* highlight-add-end */
|
||||
themes: ...,
|
||||
components: ...,
|
||||
});
|
||||
```
|
||||
|
||||
4. Now we can reference `alert` for our icon in our entity links like this:
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: artist-lookup
|
||||
description: Artist Lookup
|
||||
links:
|
||||
- url: https://example.com/alert
|
||||
title: Alerts
|
||||
icon: alert
|
||||
```
|
||||
|
||||
And this is the result:
|
||||
|
||||

|
||||
|
||||
Another way you can use these icons is from the `AppContext` like this:
|
||||
|
||||
```ts
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
|
||||
const app = useApp();
|
||||
const alertIcon = app.getSystemIcon('alert');
|
||||
```
|
||||
|
||||
You might want to use this method if you have an icon you want to use in several locations.
|
||||
|
||||
:::note Note
|
||||
|
||||
If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon`
|
||||
|
||||
:::
|
||||
|
||||
## Custom Sidebar
|
||||
|
||||
As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar.
|
||||
|
||||
### Sidebar Sub-menu
|
||||
|
||||
For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives
|
||||
2. Then we want to add the following import for `useApp`:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
```
|
||||
|
||||
3. Then update the `@backstage/core-components` import like this:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
/* highlight-add-start */
|
||||
GroupIcon,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
/* highlight-add-end */
|
||||
} from '@backstage/core-components';
|
||||
```
|
||||
|
||||
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
|
||||
|
||||
```tsx title="packages/app/src/components/Root/Root.tsx"
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
title="Domains"
|
||||
to="catalog?filters[kind]=domain"
|
||||
icon={useApp().getSystemIcon('kind:domain')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Systems"
|
||||
to="catalog?filters[kind]=system"
|
||||
icon={useApp().getSystemIcon('kind:system')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Components"
|
||||
to="catalog?filters[kind]=component"
|
||||
icon={useApp().getSystemIcon('kind:component')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="APIs"
|
||||
to="catalog?filters[kind]=api"
|
||||
icon={useApp().getSystemIcon('kind:api')}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Resources"
|
||||
to="catalog?filters[kind]=resource"
|
||||
icon={useApp().getSystemIcon('kind:resource')}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Groups"
|
||||
to="catalog?filters[kind]=group"
|
||||
icon={useApp().getSystemIcon('kind:group')}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Users"
|
||||
to="catalog?filters[kind]=user"
|
||||
icon={useApp().getSystemIcon('kind:user')}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
```
|
||||
|
||||
When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this:
|
||||
|
||||

|
||||
|
||||
You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar)
|
||||
|
||||
## Custom Homepage
|
||||
|
||||
In addition to a custom theme, a custom logo, you can also customize the
|
||||
homepage of your app. Read the full guide on the [next page](homepage.md).
|
||||
|
||||
## Migrating to Material UI v5
|
||||
|
||||
We now support Material UI v5 in Backstage. Check out our [migration guide](../tutorials/migrate-to-mui5.md) to get started.
|
||||
@@ -157,7 +157,7 @@ Choose the correct next steps for your user role, if you're likely to be deployi
|
||||
- Configuring Backstage
|
||||
|
||||
- [Adding plugins](./configure-app-with-plugins.md)
|
||||
- [Customizing the theme](./app-custom-theme.md)
|
||||
- [Customizing Your App's UI](../conf/user-interface/index.md)
|
||||
- [Populating the homepage](./homepage.md)
|
||||
|
||||
### Developer
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
id: doc-landing-page
|
||||
title: Documentation
|
||||
description: Documentation landing page.
|
||||
---
|
||||
|
||||
## Understand, try, and administer Backstage
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Understand Backstage</th>
|
||||
<th>Try Backstage</th>
|
||||
<th>Administration</th>
|
||||
<th>Additional Features</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign='top'><i>Learn about Backstage.</i><br><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/overview/what-is-backstage'>What is Backstage?</a></li>
|
||||
<li><a href='https://backstage.io/docs/overview/technical-overview'>Technical Overview</a></li>
|
||||
<li><a href='https://backstage.io/docs/overview/architecture-overview'>Architecture overview</a></li>
|
||||
<li><a href='https://backstage.io/docs/plugins/'>Introduction to Plugins</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/software-catalog/'>Software Catalog Overview</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/software-catalog/system-model'>System Model</a></li>
|
||||
<li><a href='https://backstage.io/docs/auth/'>Authentication in Backstage</a></li>
|
||||
<li><a href='https://backstage.io/docs/backend-system/'>Backend System Overview</a></li>
|
||||
<li><a href='https://backstage.io/docs/frontend-system/architecture/index'>New Frontend System Overview</a></li>
|
||||
<li><a href='https://backstage.io/docs/overview/threat-model'>Security & Threat Model</a></li>
|
||||
<li><a href='https://backstage.io/docs/overview/versioning-policy'>Release & Versioning Policy</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td valign='top'><i>Build a customizable Backstage app.</i><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/getting-started/'>Create your Backstage App</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/logging-in'>Logging into Backstage</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/register-a-component'>Registering a Component</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/create-a-component'>Creating a Component</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/software-templates/configuration'>Configuring a Software Template</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/software-templates/adding-templates'>Creating a Software Template</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/kubernetes/'>Check health of services with Kubernetes</a></li>
|
||||
<li><a href='https://backstage.io/docs/tooling/cli/overview/'>Backstage CLI</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/techdocs/'>TechDocs - a docs-like-code solution</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td valign='top'><i>Configure, Deploy, & Upgrade.</i><br><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/getting-started/config/authentication'>Setup Authentication</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/config/database'>Configure a Database</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/configure-app-with-plugins'>Add plugins to expand functionality</a></li>
|
||||
<li><a href='https://backstage.io/docs/notifications/'>Install Notification System</a></li>
|
||||
<li><a href='https://backstage.io/docs/permissions/overview'>Setup permissions</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/app-custom-theme'>Customize the look-and-feel</a></li>
|
||||
<li><a href='https://backstage.io/docs/getting-started/homepage'>Customize Backstage homepage</a></li>
|
||||
<li><a href='https://backstage.io/docs/integrations/'>Integrate with external providers to read or publish data</a></li>
|
||||
<li><a href='https://backstage.io/docs/deployment/'>Deploy Backstage to Production</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td valign='top'><i>Additional Backstage Features</i><br><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/features/search/'>Search</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/software-catalog/'>Software Catalog</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/software-templates/'>Software Templates (aka Scaffolder)</a></li>
|
||||
<li><a href='https://backstage.io/docs/features/techdocs/'>TechDocs</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Plugin Development, references, and how to contribute
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Plugin Development</th>
|
||||
<th>References</th>
|
||||
<th>Contribute to Backstage</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign='top'><i>Creating and Configuring Plugins</i><br><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/plugins/'>Introduction to Plugins</a></li>
|
||||
<li><a href='https://backstage.io/docs/plugins/create-a-plugin'>Create a Backstage Plugin</a></li>
|
||||
<li><a href='https://backstage.io/docs/plugins/plugin-development'>Plugin Development</a></li>
|
||||
<li><a href='https://backstage.io/docs/plugins/integrating-plugin-into-software-catalog'>Integrating a Plugin into the Software Catalog</a></li>
|
||||
<li><a href='https://backstage.io/docs/plugins/integrating-search-into-plugins'>Integrating Search into a Plugin</a></li>
|
||||
<li><a href='https://backstage.io/docs/conf/defining'>Defining Configuration for your Plugin</a></li>
|
||||
<li><a href='https://backstage.io/docs/permissions/plugin-authors/01-setup'>Tutorial: using Permissions in your Plugin</a></li>
|
||||
<li><a href='https://backstage.io/docs/plugins/analytics'>Plugin Analytics</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td valign='top'><i>Glossary, FAQ, and other reference material.</i><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/references/glossary'>Glossary</a></li>
|
||||
<li><a href='https://backstage.io/docs/dls/design'>Designing for Backstage</a></li>
|
||||
<li><a href='https://backstage.io/docs/architecture-decisions/'>Architecture Decision Records (ADRs)</a></li>
|
||||
<li><a href='https://backstage.io/docs/api/deprecations'>Deprecations</a></li>
|
||||
<li><a href='https://backstage.io/docs/api/utility-apis'>Utility APIs</a></li>
|
||||
<li><a href='https://backstage.io/docs/reference/'>Package Index</a></li>
|
||||
<li><a href='https://backstage.io/docs/faq/'>FAQ</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td valign='top'><i>Contributions are welcome and greatly appreciated!</i><br>
|
||||
<ul>
|
||||
<li><a href='https://backstage.io/docs/contribute/'>Contributor's Guide</a></li>
|
||||
<li><a href='https://backstage.io/docs/contribute/getting-involved'>Getting Involved</a></li>
|
||||
<li><a href=https://backstage.io/docs/contribute/project-structure'>Backstage Project Structure</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,219 @@
|
||||
# Release v1.44.0-next.3
|
||||
|
||||
Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.44.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.44.0-next.3)
|
||||
|
||||
## @backstage/backend-app-api@1.2.8-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dd69cf6: Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process.
|
||||
|
||||
## @backstage/cli@0.34.4-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f2cf564: Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30.
|
||||
|
||||
- 024645e: Remove unused @octokit modules from cli package
|
||||
|
||||
- @octokit/graphql
|
||||
- @octokit/graphql-schema
|
||||
- @octokit/oauth-app
|
||||
|
||||
- d14ef24: Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies.
|
||||
|
||||
## @backstage/core-components@0.18.2-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 431130c: Added `renderEdge` prop to `<DependencyGraph />` component in `@backstage/core-components` to allow custom rendering of graph edges.
|
||||
|
||||
## @backstage/create-app@0.7.5-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Bumped create-app version.
|
||||
|
||||
## @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ed53eb: Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too.
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
|
||||
## @backstage/ui@0.7.2-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3c921c5: Making href mandatory in tabs that are part of a Header component
|
||||
- 5c21e45: Add react router for internal routing for ButtonLinks
|
||||
- 9781815: Remove auto selection of tabs for tabs that all have href defined
|
||||
- f6dff5b: Using react router for internal links in the Menu component
|
||||
|
||||
## @backstage/plugin-app-visualizer@0.1.24-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4406144: Ensure that the text rendering has react keys for all elements
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-aws@0.4.16-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-azure@0.3.10-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.4-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.4-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-gerrit@0.3.7-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-github@0.11.1-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-github-org@0.3.15-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-catalog-backend-module-github@0.11.1-next.2
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0443119: Fixed an issue in `GitlabDiscoveryEntityProvider` where entity fetching could fail for projects with special characters or that had been renamed or moved.
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.14-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-msgraph@0.8.1-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-backend-module-puppetdb@0.2.15-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-catalog-graph@0.5.2-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 431130c: Added `renderEdge` prop to `<DependencyGraph />` component in `@backstage/core-components` to allow custom rendering of graph edges.
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
## @backstage/plugin-home@0.8.13-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e7d59d3: fix(home): correct `clearAll` logic to properly handle `deletable` flag
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
## @backstage/plugin-kubernetes@0.12.12-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
## @backstage/plugin-kubernetes-cluster@0.0.30-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 99fcf98: Removed unused dependencies
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
|
||||
## @backstage/plugin-notifications@0.5.10-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f5e0963: Removed unused dependencies
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
## @backstage/plugin-notifications-backend@0.5.11-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f5e0963: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-notifications-backend-module-email@0.3.14-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b8cf31a: chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7
|
||||
- f5e0963: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-scaffolder@1.34.2-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d9aed74: Forward `ui:disabled` in `OwnedEntityPicker` to allow disabling it
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
|
||||
## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f5e0963: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-scaffolder-backend-module-gitlab@0.9.6-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f5e0963: Removed unused dependencies
|
||||
|
||||
## @backstage/plugin-signals@0.0.24-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b41d9d: Remove `app-root-element:signals/signals-display` which was not doing anything useful
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
- @backstage/frontend-plugin-api@0.12.1-next.2
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Datadog Entity Sync
|
||||
author: datadog
|
||||
authorUrl: https://www.datadoghq.com/
|
||||
category: Development
|
||||
description: Sync entities from the Backstage catalog to the Datadog Software Catalog.
|
||||
documentation: https://github.com/DataDog/datadog-backstage-plugins/tree/main/plugins/datadog-entity-sync-backend#installation
|
||||
iconUrl: https://imgix.datadoghq.com/img/about/presskit/logo-v/dd_vertical_purple.png?auto=format&fit=max&w=847&dpr=2
|
||||
npmPackageName: '@datadog/backstage-plugin-datadog-entity-sync-backend'
|
||||
addedDate: '2025-09-19'
|
||||
@@ -269,6 +269,10 @@ const config: Config = {
|
||||
from: '/docs/plugins/url-reader/',
|
||||
to: '/docs/backend-system/core-services/url-reader',
|
||||
},
|
||||
{
|
||||
from: '/docs/getting-started/app-custom-theme',
|
||||
to: '/docs/conf/user-interface',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
|
||||
+10
-1
@@ -37,7 +37,6 @@ export default {
|
||||
'getting-started/config/database',
|
||||
'getting-started/config/authentication',
|
||||
'getting-started/configure-app-with-plugins',
|
||||
'getting-started/app-custom-theme',
|
||||
'getting-started/homepage',
|
||||
],
|
||||
},
|
||||
@@ -560,6 +559,16 @@ export default {
|
||||
'tooling/package-metadata',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'User Interface',
|
||||
items: [
|
||||
'conf/user-interface/index',
|
||||
'conf/user-interface/logo',
|
||||
'conf/user-interface/icons',
|
||||
'conf/user-interface/sidebar',
|
||||
],
|
||||
},
|
||||
],
|
||||
Tutorials: [
|
||||
{ 'Non-technical': ['overview/adopting'] },
|
||||
|
||||
@@ -171,6 +171,11 @@ html[data-theme='light'] {
|
||||
border-left: 3px solid rgb(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* For the docusaurus-pushfeedback plugin */
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ nav:
|
||||
- Database: 'getting-started/config/database.md'
|
||||
- Authentication: 'getting-started/config/authentication.md'
|
||||
- Configuring App with plugins: 'getting-started/configure-app-with-plugins.md'
|
||||
- Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md'
|
||||
- Customize the look-and-feel of your App: 'conf/user-interface/index.md'
|
||||
- Customizing your Homepage: 'getting-started/homepage.md'
|
||||
- Deployment:
|
||||
- Deploying Backstage: 'deployment/index.md'
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "root",
|
||||
"version": "1.44.0-next.2",
|
||||
"version": "1.44.0-next.3",
|
||||
"backstage": {
|
||||
"cli": {
|
||||
"new": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @backstage/backend-app-api
|
||||
|
||||
## 1.2.8-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dd69cf6: Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process.
|
||||
|
||||
## 1.2.8-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-app-api",
|
||||
"version": "1.2.8-next.0",
|
||||
"version": "1.2.8-next.1",
|
||||
"description": "Core API used by Backstage backend apps",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @backstage/cli
|
||||
|
||||
## 0.34.4-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f2cf564: Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30.
|
||||
- 024645e: Remove unused @octokit modules from cli package
|
||||
|
||||
- @octokit/graphql
|
||||
- @octokit/graphql-schema
|
||||
- @octokit/oauth-app
|
||||
|
||||
- d14ef24: Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies.
|
||||
|
||||
## 0.34.4-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -16,29 +16,12 @@
|
||||
|
||||
const { default: JestRuntime } = require('jest-runtime');
|
||||
|
||||
const scriptTransformCache = new Map();
|
||||
|
||||
module.exports = class CachingJestRuntime extends JestRuntime {
|
||||
constructor(config, ...restArgs) {
|
||||
super(config, ...restArgs);
|
||||
this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts');
|
||||
}
|
||||
|
||||
// This may or may not be a good idea. Theoretically I don't know why this would impact
|
||||
// test correctness and flakiness, but it seems like it may introduce flakiness and strange failures.
|
||||
// It does seem to speed up test execution by a fair amount though.
|
||||
createScriptFromCode(scriptSource, filename) {
|
||||
let script = scriptTransformCache.get(scriptSource);
|
||||
if (!script) {
|
||||
script = super.createScriptFromCode(scriptSource, filename);
|
||||
// Tried to store the script object in a WeakRef here. It starts out at
|
||||
// about 90% hit rate, but eventually drops all the way to 20%, and overall
|
||||
// it seemed to increase memory usage by 20% or so.
|
||||
scriptTransformCache.set(scriptSource, script);
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
// Unfortunately we need to use this unstable API to make sure that .js files
|
||||
// are only loaded as modules where ESM is supported, i.e. Node.js packages.
|
||||
unstable_shouldLoadAsEsm(path, ...restArgs) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"version": "0.34.4-next.2",
|
||||
"version": "0.34.4-next.3",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"backstage": {
|
||||
"role": "cli"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @backstage/core-components
|
||||
|
||||
## 0.18.2-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 431130c: Added `renderEdge` prop to `<DependencyGraph />` component in `@backstage/core-components` to allow custom rendering of graph edges.
|
||||
|
||||
## 0.18.2-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/core-components",
|
||||
"version": "0.18.2-next.2",
|
||||
"version": "0.18.2-next.3",
|
||||
"description": "Core components used by Backstage plugins and apps",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @backstage/create-app
|
||||
|
||||
## 0.7.5-next.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Bumped create-app version.
|
||||
|
||||
## 0.7.5-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/create-app",
|
||||
"version": "0.7.5-next.2",
|
||||
"version": "0.7.5-next.3",
|
||||
"description": "A CLI that helps you create your own Backstage app",
|
||||
"backstage": {
|
||||
"role": "cli"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@backstage/plugin-signals-backend": "^{{version '@backstage/plugin-signals-backend'}}",
|
||||
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
|
||||
"app": "link:../app",
|
||||
"better-sqlite3": "^9.0.0",
|
||||
"better-sqlite3": "^12.0.0",
|
||||
"node-gyp": "^10.0.0",
|
||||
"pg": "^8.11.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @backstage/frontend-plugin-api
|
||||
|
||||
## 0.12.1-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8ed53eb: Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too.
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.18.2-next.3
|
||||
|
||||
## 0.12.1-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/frontend-plugin-api",
|
||||
"version": "0.12.1-next.1",
|
||||
"version": "0.12.1-next.2",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
},
|
||||
|
||||
@@ -377,6 +377,7 @@ export interface ConfigurableExtensionDataRef<
|
||||
|
||||
// @public (undocumented)
|
||||
export const coreExtensionData: {
|
||||
title: ConfigurableExtensionDataRef<string, 'core.title', {}>;
|
||||
reactElement: ConfigurableExtensionDataRef<
|
||||
JSX_3.Element,
|
||||
'core.reactElement',
|
||||
|
||||
@@ -20,6 +20,7 @@ import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
|
||||
/** @public */
|
||||
export const coreExtensionData = {
|
||||
title: createExtensionDataRef<string>().with({ id: 'core.title' }),
|
||||
reactElement: createExtensionDataRef<JSX.Element>().with({
|
||||
id: 'core.reactElement',
|
||||
}),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
getPathToCurrentOpenApiSpec,
|
||||
toGeneratorAdditionalProperties,
|
||||
} from '../../../../../lib/openapi/helpers';
|
||||
import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports';
|
||||
|
||||
async function generate(
|
||||
outputDirectory: string,
|
||||
@@ -101,6 +102,14 @@ async function generate(
|
||||
});
|
||||
}
|
||||
|
||||
// Deduplicate imports in generated files
|
||||
const generatedFiles = await fs.readdir(resolvedOutputDirectory);
|
||||
for (const file of generatedFiles) {
|
||||
if (file.endsWith('.ts')) {
|
||||
deduplicateImports(resolve(resolvedOutputDirectory, file));
|
||||
}
|
||||
}
|
||||
|
||||
fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore'));
|
||||
|
||||
fs.rmSync(resolve(resolvedOutputDirectory, '.openapi-generator'), {
|
||||
|
||||
@@ -14,24 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { resolvePackagePath } from '@backstage/backend-plugin-api';
|
||||
import chalk from 'chalk';
|
||||
import { resolve, dirname, join } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import YAML from 'js-yaml';
|
||||
import { dirname, join, resolve } from 'path';
|
||||
import { exec } from '../../../../../lib/exec';
|
||||
import {
|
||||
OLD_SCHEMA_PATH,
|
||||
OPENAPI_IGNORE_FILES,
|
||||
OUTPUT_PATH,
|
||||
TS_SCHEMA_PATH,
|
||||
} from '../../../../../lib/openapi/constants';
|
||||
import { paths as cliPaths } from '../../../../../lib/paths';
|
||||
import fs from 'fs-extra';
|
||||
import { exec } from '../../../../../lib/exec';
|
||||
import { resolvePackagePath } from '@backstage/backend-plugin-api';
|
||||
import { deduplicateImports } from '../../../../../lib/openapi/dedupe-imports';
|
||||
import {
|
||||
getPathToCurrentOpenApiSpec,
|
||||
getRelativePathToFile,
|
||||
toGeneratorAdditionalProperties,
|
||||
} from '../../../../../lib/openapi/helpers';
|
||||
import { paths as cliPaths } from '../../../../../lib/paths';
|
||||
|
||||
async function generateSpecFile() {
|
||||
const openapiPath = await getPathToCurrentOpenApiSpec();
|
||||
@@ -133,6 +134,14 @@ async function generate(
|
||||
},
|
||||
);
|
||||
|
||||
// Deduplicate imports in generated files
|
||||
const generatedFiles = await fs.readdir(resolvedOutputDirectory);
|
||||
for (const file of generatedFiles) {
|
||||
if (file.endsWith('.ts')) {
|
||||
deduplicateImports(resolve(resolvedOutputDirectory, file));
|
||||
}
|
||||
}
|
||||
|
||||
await exec(
|
||||
`yarn backstage-cli package lint --fix ${resolvedOutputDirectory}`,
|
||||
[],
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 fs from 'fs';
|
||||
|
||||
/**
|
||||
* Removes duplicate import and export statements from TypeScript/JavaScript files.
|
||||
* Preserves different import syntaxes, multi-line imports, and code structure.
|
||||
*
|
||||
* @param filePath - Absolute path to the file to deduplicate
|
||||
*/
|
||||
export function deduplicateImports(filePath: string): void {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Handle import/export statements
|
||||
if (trimmed.startsWith('import ') || trimmed.startsWith('export ')) {
|
||||
let fullStatement = line;
|
||||
let endIndex = i;
|
||||
|
||||
// Check if multi-line (has { but no })
|
||||
if (trimmed.includes('{') && !trimmed.includes('}')) {
|
||||
// Collect full multi-line statement
|
||||
while (endIndex < lines.length && !lines[endIndex].includes('}')) {
|
||||
endIndex++;
|
||||
}
|
||||
if (endIndex < lines.length) {
|
||||
fullStatement = lines.slice(i, endIndex + 1).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize for comparison (remove comments, extra spaces)
|
||||
const normalized = fullStatement
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
// Add all lines of the statement
|
||||
for (let j = i; j <= endIndex; j++) {
|
||||
result.push(lines[j]);
|
||||
}
|
||||
}
|
||||
|
||||
i = endIndex + 1;
|
||||
} else {
|
||||
result.push(line);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, result.join('\n'));
|
||||
}
|
||||
@@ -14,14 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { paths } from '../paths';
|
||||
import { YAML_SCHEMA_PATH } from './constants';
|
||||
import { resolve } from 'path';
|
||||
import Parser from '@apidevtools/swagger-parser';
|
||||
import fs, { pathExists } from 'fs-extra';
|
||||
import YAML from 'js-yaml';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import Parser from '@apidevtools/swagger-parser';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { paths } from '../paths';
|
||||
import { YAML_SCHEMA_PATH } from './constants';
|
||||
|
||||
export const getPathToFile = async (directory: string, filename: string) => {
|
||||
return resolve(directory, filename);
|
||||
|
||||
@@ -7,3 +7,7 @@ backend:
|
||||
|
||||
techdocs:
|
||||
builder: 'external'
|
||||
sanitizer:
|
||||
# Allow live reload locally. Added in this config to avoid updating the main techdocs plugin.
|
||||
allowedCustomElementTagNameRegExp: '^live-reload$'
|
||||
allowedCustomElementAttributeNameRegExp: '^live-reload-(epoch|request-id)$'
|
||||
|
||||
@@ -36,6 +36,7 @@ import * as plugins from './plugins';
|
||||
import { configLoader } from './config';
|
||||
import { Root } from './components/Root';
|
||||
import { techDocsPage, TechDocsThemeToggle } from './components/TechDocsPage';
|
||||
import { TechDocsLiveReload } from './LiveReloadAddon';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -54,6 +55,14 @@ const ThemeToggleAddon = techdocsPlugin.provide(
|
||||
}),
|
||||
);
|
||||
|
||||
const LiveReloadAddon = techdocsPlugin.provide(
|
||||
createTechDocsAddonExtension({
|
||||
name: 'LiveReloadAddon',
|
||||
component: TechDocsLiveReload,
|
||||
location: TechDocsAddonLocations.Content,
|
||||
}),
|
||||
);
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Navigate key="/" to="/docs/default/component/local/" />
|
||||
@@ -71,6 +80,7 @@ const routes = (
|
||||
>
|
||||
{techDocsPage}
|
||||
<TechDocsAddons>
|
||||
<LiveReloadAddon />
|
||||
<ThemeToggleAddon />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { TechDocsLiveReload } from './LiveReloadAddon';
|
||||
|
||||
jest.mock('@backstage/plugin-techdocs-react', () => ({
|
||||
useShadowRootElements: jest.fn(() => [
|
||||
{
|
||||
querySelector: jest.fn((selector: string) => {
|
||||
if (selector === 'live-reload') {
|
||||
return {
|
||||
getAttribute: (name: string) => {
|
||||
if (name === 'live-reload-epoch') return '10';
|
||||
if (name === 'live-reload-request-id') return '1';
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
describe('TechDocsLiveReload', () => {
|
||||
const originalXHR = global.XMLHttpRequest;
|
||||
let originalLocation: Location;
|
||||
let openSpy: jest.Mock;
|
||||
let sendSpy: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
originalLocation = window.location;
|
||||
openSpy = jest.fn();
|
||||
sendSpy = jest.fn(function (this: any) {
|
||||
// simulate long-poll response that does NOT trigger reload (epoch unchanged)
|
||||
setTimeout(() => {
|
||||
(this as any).status = 200;
|
||||
(this as any).responseText = '10';
|
||||
(this as any).onloadend?.call(this);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
class MockXHR {
|
||||
onloadend: ((this: any) => void) | null = null;
|
||||
status = 0;
|
||||
responseText = '';
|
||||
open = openSpy;
|
||||
send = sendSpy as any;
|
||||
abort = jest.fn();
|
||||
}
|
||||
|
||||
global.XMLHttpRequest = MockXHR as any;
|
||||
|
||||
// Replace window.location with a mutable object for tests
|
||||
delete (window as any).location;
|
||||
(window as any).location = { ...originalLocation, reload: jest.fn() };
|
||||
jest.spyOn(window, 'addEventListener').mockImplementation(() => {});
|
||||
jest.spyOn(window, 'removeEventListener').mockImplementation(() => {});
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.XMLHttpRequest = originalXHR;
|
||||
jest.restoreAllMocks();
|
||||
// restore original window.location
|
||||
delete (window as any).location;
|
||||
(window as any).location = originalLocation;
|
||||
});
|
||||
|
||||
it('polls livereload endpoint and does not reload when epoch unchanged', async () => {
|
||||
const reloadSpy = window.location.reload as unknown as jest.Mock;
|
||||
render(<TechDocsLiveReload enabled />);
|
||||
expect(openSpy).toHaveBeenCalledWith('GET', '/.livereload/10/1');
|
||||
// give microtask queue a tick
|
||||
await new Promise(res => setTimeout(res, 0));
|
||||
expect(reloadSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reloads when server epoch increases', async () => {
|
||||
const reloadSpy = window.location.reload as unknown as jest.Mock;
|
||||
|
||||
sendSpy.mockImplementation(function (this: any) {
|
||||
setTimeout(() => {
|
||||
(this as any).status = 200;
|
||||
(this as any).responseText = '11';
|
||||
(this as any).onloadend?.call(this);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
render(<TechDocsLiveReload enabled />);
|
||||
await new Promise(res => setTimeout(res, 0));
|
||||
expect(reloadSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 { useShadowRootElements } from '@backstage/plugin-techdocs-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface TechDocsLiveReloadProps {
|
||||
/** Whether to enable livereload (default: true in development) */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* LiveReload addon for Techdocs CLI.
|
||||
*
|
||||
* Support mkdocs built-in livereload, in a TechDocs CLI preview environment.
|
||||
* See https://github.com/backstage/backstage/issues/30514 for more details.
|
||||
*/
|
||||
export const TechDocsLiveReload = ({
|
||||
enabled = true,
|
||||
}: TechDocsLiveReloadProps) => {
|
||||
const body = useShadowRootElements<HTMLBodyElement>(['body']);
|
||||
const reqRef = useRef<XMLHttpRequest | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const LIVE_RELOAD_ELEMENT = 'live-reload';
|
||||
const LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch';
|
||||
const LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id';
|
||||
const CLI_LIVERELOAD_PATH = '/.livereload';
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !body[0]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const liveReloadElement = body[0].querySelector(LIVE_RELOAD_ELEMENT);
|
||||
|
||||
if (!liveReloadElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const epoch = parseInt(
|
||||
liveReloadElement.getAttribute(LIVE_RELOAD_ATTR_EPOCH) || '0',
|
||||
10,
|
||||
);
|
||||
const requestId = parseInt(
|
||||
liveReloadElement.getAttribute(LIVE_RELOAD_ATTR_REQUEST_ID) || '0',
|
||||
10,
|
||||
);
|
||||
|
||||
if (!epoch || !requestId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const livereloadUrl = CLI_LIVERELOAD_PATH;
|
||||
|
||||
const poll = () => {
|
||||
reqRef.current = new XMLHttpRequest();
|
||||
reqRef.current.onloadend = function handleLoadEnd(this: XMLHttpRequest) {
|
||||
if (parseFloat(this.responseText) > epoch) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
timeoutRef.current = setTimeout(poll, this.status === 200 ? 0 : 3000);
|
||||
}
|
||||
};
|
||||
reqRef.current.open('GET', `${livereloadUrl}/${epoch}/${requestId}`);
|
||||
reqRef.current.send();
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (reqRef.current) {
|
||||
reqRef.current.abort();
|
||||
reqRef.current = null;
|
||||
}
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Stop when tab is inactive
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
poll();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
stop();
|
||||
};
|
||||
|
||||
// Start polling if page is visible
|
||||
if (document.visibilityState === 'visible') {
|
||||
poll();
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [body, enabled]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -18,6 +18,10 @@ import serveHandler from 'serve-handler';
|
||||
import http from 'http';
|
||||
import httpProxy from 'http-proxy';
|
||||
import { createLogger } from './utility';
|
||||
import {
|
||||
proxyHtmlWithLivereloadInjection,
|
||||
proxyMkdocsLivereload,
|
||||
} from './livereload';
|
||||
|
||||
export default class HTTPServer {
|
||||
private readonly proxyEndpoint: string;
|
||||
@@ -59,8 +63,8 @@ export default class HTTPServer {
|
||||
const proxyHandler = this.createProxy();
|
||||
const server = http.createServer(
|
||||
(request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
// This endpoind is used by the frontend to issue a cookie for the user.
|
||||
// But the MkDocs server doesn't expose it as a the Backestage backend does.
|
||||
// This endpoint is used by the frontend to issue a cookie for the user.
|
||||
// But the MkDocs server doesn't expose it as a the Backstage backend does.
|
||||
// So we need to fake it here to prevent 404 errors.
|
||||
if (request.url === '/api/techdocs/.backstage/auth/v1/cookie') {
|
||||
const oneHourInMilliseconds = 60 * 60 * 1000;
|
||||
@@ -72,6 +76,19 @@ export default class HTTPServer {
|
||||
}
|
||||
|
||||
if (request.url?.startsWith(this.proxyEndpoint)) {
|
||||
// Handle HTML files with livereload parameter injection
|
||||
if (request.url?.endsWith('.html')) {
|
||||
proxyHtmlWithLivereloadInjection({
|
||||
request,
|
||||
response,
|
||||
mkdocsTargetAddress: this.mkdocsTargetAddress,
|
||||
proxyEndpoint: this.proxyEndpoint,
|
||||
onError: (error: Error) => reject(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle non-HTML files with regular proxy
|
||||
const [proxy, forwardPath] = proxyHandler(request);
|
||||
|
||||
proxy.on('error', (error: Error) => {
|
||||
@@ -93,6 +110,17 @@ export default class HTTPServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// This endpoint is used by the frontend to pass livereload requests to the mkdocs server.
|
||||
if (request.url?.startsWith('/.livereload')) {
|
||||
proxyMkdocsLivereload({
|
||||
request,
|
||||
response,
|
||||
mkdocsTargetAddress: this.mkdocsTargetAddress,
|
||||
onError: (error: Error) => reject(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
serveHandler(request, response, {
|
||||
public: this.backstageBundleDir,
|
||||
trailingSlash: true,
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 http from 'http';
|
||||
import {
|
||||
injectLivereloadParameters,
|
||||
proxyHtmlWithLivereloadInjection,
|
||||
proxyMkdocsLivereload,
|
||||
} from './livereload';
|
||||
|
||||
// Note: This mock returns a singleton proxy object so tests can access the
|
||||
// registered event handlers (e.g. `proxyRes`) from code under test.
|
||||
jest.mock('http-proxy', () => {
|
||||
const handlers: Record<string, Function> = {};
|
||||
const fakeProxy = {
|
||||
on: jest.fn((event: string, cb: Function) => {
|
||||
handlers[event] = cb;
|
||||
}),
|
||||
web: jest.fn((_req: unknown, _res: unknown) => {
|
||||
// no-op; tests will manually trigger handlers['proxyRes'] when needed
|
||||
}),
|
||||
__handlers: handlers,
|
||||
};
|
||||
const create = jest.fn(() => fakeProxy);
|
||||
return {
|
||||
__esModule: true,
|
||||
default: { createProxyServer: create },
|
||||
createProxyServer: create,
|
||||
};
|
||||
});
|
||||
|
||||
describe('livereload helpers', () => {
|
||||
describe('injectLivereloadParameters', () => {
|
||||
it('injects live-reload element when mkdocs script is present', () => {
|
||||
const html =
|
||||
'<html><body><script>livereload(123, 456);</script></body></html>';
|
||||
const result = injectLivereloadParameters(html);
|
||||
expect(result).toContain('<live-reload');
|
||||
expect(result).toContain('live-reload-epoch="123"');
|
||||
expect(result).toContain('live-reload-request-id="456"');
|
||||
expect(result).toContain('</body>');
|
||||
});
|
||||
|
||||
it('returns original html when mkdocs script is absent', () => {
|
||||
const html = '<html><body><h1>No livereload</h1></body></html>';
|
||||
const result = injectLivereloadParameters(html);
|
||||
expect(result).toBe(html);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxyHtmlWithLivereloadInjection', () => {
|
||||
it('injects parameters into HTML responses and sets CORS headers', () => {
|
||||
const { createProxyServer } = jest.requireMock('http-proxy') as any;
|
||||
const proxy = createProxyServer();
|
||||
|
||||
const req = {
|
||||
url: '/api/techdocs/some/path/index.html',
|
||||
} as unknown as http.IncomingMessage;
|
||||
const headers: Record<string, string> = {};
|
||||
const res = {
|
||||
setHeader: (k: string, v: any) => {
|
||||
headers[k] = String(v);
|
||||
},
|
||||
end: jest.fn(),
|
||||
} as unknown as http.ServerResponse;
|
||||
|
||||
proxyHtmlWithLivereloadInjection({
|
||||
request: req,
|
||||
response: res,
|
||||
mkdocsTargetAddress: 'http://localhost:8000',
|
||||
proxyEndpoint: '/api/techdocs/',
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
// Simulate mkdocs proxy response with HTML
|
||||
const proxyRes: any = {
|
||||
headers: { 'content-type': 'text/html; charset=utf-8' },
|
||||
on: (event: string, cb: Function) => {
|
||||
if (event === 'data') {
|
||||
cb('<html><body><script>livereload(1, 2);</script></body></html>');
|
||||
}
|
||||
if (event === 'end') {
|
||||
cb();
|
||||
}
|
||||
},
|
||||
pipe: jest.fn(),
|
||||
};
|
||||
|
||||
(proxy as any).__handlers.proxyRes(proxyRes, {} as any, res);
|
||||
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
const injectedHtml = (res.end as jest.Mock).mock.calls[0][0] as string;
|
||||
expect(injectedHtml).toContain('<live-reload');
|
||||
expect(headers['Access-Control-Allow-Origin']).toBe('*');
|
||||
expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS');
|
||||
// Ensure proxyEndpoint was stripped from request url
|
||||
expect(req.url).toBe('some/path/index.html');
|
||||
});
|
||||
|
||||
it('passes through non-HTML responses without injection', () => {
|
||||
const { createProxyServer } = jest.requireMock('http-proxy') as any;
|
||||
const proxy = createProxyServer();
|
||||
|
||||
const req = {
|
||||
url: '/api/techdocs/some/path/asset.css',
|
||||
} as unknown as http.IncomingMessage;
|
||||
const headers: Record<string, string> = {};
|
||||
const res = {
|
||||
setHeader: (k: string, v: any) => {
|
||||
headers[k] = String(v);
|
||||
},
|
||||
end: jest.fn(),
|
||||
} as unknown as http.ServerResponse;
|
||||
|
||||
proxyHtmlWithLivereloadInjection({
|
||||
request: req,
|
||||
response: res,
|
||||
mkdocsTargetAddress: 'http://localhost:8000',
|
||||
proxyEndpoint: '/api/techdocs/',
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
const proxyRes: any = {
|
||||
headers: { 'content-type': 'text/css' },
|
||||
on: jest.fn(),
|
||||
pipe: jest.fn(),
|
||||
};
|
||||
|
||||
(proxy as any).__handlers.proxyRes(proxyRes, {} as any, res);
|
||||
|
||||
expect(res.end).not.toHaveBeenCalled();
|
||||
expect(proxyRes.pipe).toHaveBeenCalledWith(res);
|
||||
expect(headers['Access-Control-Allow-Origin']).toBe('*');
|
||||
expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxyMkdocsLivereload', () => {
|
||||
it('rewrites path and sets CORS headers', () => {
|
||||
const { createProxyServer } = jest.requireMock('http-proxy') as any;
|
||||
const proxy = createProxyServer();
|
||||
|
||||
const req = {
|
||||
url: '/.livereload/1/2',
|
||||
} as unknown as http.IncomingMessage;
|
||||
const headers: Record<string, string> = {};
|
||||
const res = {
|
||||
setHeader: (k: string, v: any) => {
|
||||
headers[k] = String(v);
|
||||
},
|
||||
} as unknown as http.ServerResponse;
|
||||
|
||||
proxyMkdocsLivereload({
|
||||
request: req,
|
||||
response: res,
|
||||
mkdocsTargetAddress: 'http://localhost:8000',
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
expect(req.url).toBe('/livereload/1/2');
|
||||
expect(headers['Access-Control-Allow-Origin']).toBe('*');
|
||||
expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS');
|
||||
expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type');
|
||||
expect((proxy as any).web).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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 http from 'http';
|
||||
import httpProxy from 'http-proxy';
|
||||
|
||||
/**
|
||||
* Livereload support for techdocs-cli.
|
||||
*
|
||||
* Context:
|
||||
* - MkDocs implements autoreload using a long-poll endpoint `/livereload` and a script that injects
|
||||
* a call like: `livereload(epoch, request_id)`, where `epoch` is derived from Python's
|
||||
* `time.monotonic()`.
|
||||
* - Node.js monotonic clocks (`process.hrtime`/`performance.now`) are not compatible with Python's
|
||||
* value across processes and platforms. We therefore CANNOT reliably re-create the same epoch on
|
||||
* the frontend or in this CLI, and must read the values produced by MkDocs itself.
|
||||
* - The MkDocs script tag is removed by DOM sanitization (DomPurify) in TechDocs, so we can't rely
|
||||
* on the script being present in the embedded app. To bridge this, we extract the parameters on
|
||||
* the server side while proxying HTML and inject them as a safe custom element that survives
|
||||
* sanitization: `<live-reload live-reload-epoch="…" live-reload-request-id="…"/>`.
|
||||
* - The frontend addon reads that element and polls `/.livereload` (served by techdocs-cli), which
|
||||
* this module maps to MkDocs `/livereload` with permissive CORS headers.
|
||||
* - Quality-of-life: if extraction fails or the endpoint is unavailable, normal docs still work.
|
||||
*
|
||||
* See issue for background and rationale: https://github.com/backstage/backstage/issues/30514
|
||||
*/
|
||||
|
||||
const LIVE_RELOAD_ELEMENT = 'live-reload';
|
||||
const LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch';
|
||||
const LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id';
|
||||
const CLI_LIVERELOAD_PATH = '/.livereload';
|
||||
const MKDOCS_LIVERELOAD_PATH = '/livereload';
|
||||
const CONTENT_TYPE_HTML = 'text/html';
|
||||
const HEADER_CONTENT_LENGTH = 'content-length';
|
||||
|
||||
const BODY_START_RE = /<body\b[^>]*>/;
|
||||
// Matches mkdocs injected call livereload(epoch, requestId)
|
||||
const MKDOCS_LIVERELOAD_CALL_RE = /livereload\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*;?/;
|
||||
|
||||
/**
|
||||
* Extract livereload parameters from mkdocs HTML and inject them as a custom element.
|
||||
* The injected element will later be read by the frontend addon even after DOM sanitization.
|
||||
*
|
||||
* Note:
|
||||
* - we don't add to <head> because of DomPurify sanitization.
|
||||
* - we add close to the body opening to avoid reading too far into the body.
|
||||
* - we should use streamed injection to improve performance.
|
||||
*/
|
||||
export function injectLivereloadParameters(html: string): string {
|
||||
const livereloadMatch = html.match(MKDOCS_LIVERELOAD_CALL_RE);
|
||||
|
||||
// If we couldn't find livereload parameters, return original HTML untouched.
|
||||
if (!livereloadMatch) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const [, epoch, requestId] = livereloadMatch;
|
||||
// Insert a minimal custom element that the frontend addon can discover post-sanitization.
|
||||
// Note: embedded app needs a custom config to allow the element to survive sanitization.
|
||||
const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}="${epoch}" ${LIVE_RELOAD_ATTR_REQUEST_ID}="${requestId}"></${LIVE_RELOAD_ELEMENT}>`;
|
||||
|
||||
// Naively find where to insert the livereload tag.
|
||||
const bodyStart = html.match(BODY_START_RE);
|
||||
const bodyStartIndex = bodyStart?.index ?? 0;
|
||||
const bodyStartLength = bodyStart?.[0]?.length ?? 0;
|
||||
if (bodyStartIndex === 0 || bodyStartLength === 0) {
|
||||
return html;
|
||||
}
|
||||
const bodyEndIndex = bodyStartIndex + bodyStartLength;
|
||||
|
||||
return html.slice(0, bodyEndIndex) + liveReloadTag + html.slice(bodyEndIndex);
|
||||
}
|
||||
|
||||
function setCorsHeaders(response: http.ServerResponse) {
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies a mkdocs HTML response, injecting livereload parameters into the HTML body.
|
||||
*/
|
||||
export function proxyHtmlWithLivereloadInjection(options: {
|
||||
request: http.IncomingMessage;
|
||||
response: http.ServerResponse;
|
||||
mkdocsTargetAddress: string;
|
||||
proxyEndpoint: string;
|
||||
onError: (error: Error) => void;
|
||||
}): void {
|
||||
const { request, response, mkdocsTargetAddress, proxyEndpoint, onError } =
|
||||
options;
|
||||
|
||||
const htmlProxy = httpProxy.createProxyServer({
|
||||
target: mkdocsTargetAddress,
|
||||
selfHandleResponse: true,
|
||||
});
|
||||
|
||||
htmlProxy.on('error', onError);
|
||||
|
||||
// Intercept HTML responses to inject `<live-reload …>`
|
||||
htmlProxy.on('proxyRes', (proxyRes, _req, res) => {
|
||||
const contentType = proxyRes.headers['content-type'];
|
||||
const contentEncoding = proxyRes.headers['content-encoding'];
|
||||
const isHtml =
|
||||
contentType &&
|
||||
typeof contentType === 'string' &&
|
||||
contentType.startsWith(CONTENT_TYPE_HTML);
|
||||
if (isHtml && !contentEncoding) {
|
||||
const chunks: Buffer[] = [];
|
||||
proxyRes.on('data', (chunk: Buffer | string) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
proxyRes.on('end', () => {
|
||||
const body = Buffer.concat(chunks).toString('utf8');
|
||||
const modifiedHtml = injectLivereloadParameters(body);
|
||||
res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200;
|
||||
Object.keys(proxyRes.headers).forEach(key => {
|
||||
if (key.toLowerCase() !== HEADER_CONTENT_LENGTH) {
|
||||
res.setHeader(key, proxyRes.headers[key]!);
|
||||
}
|
||||
});
|
||||
setCorsHeaders(res);
|
||||
res.end(modifiedHtml);
|
||||
});
|
||||
} else {
|
||||
res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200;
|
||||
Object.keys(proxyRes.headers).forEach(key => {
|
||||
res.setHeader(key, proxyRes.headers[key]!);
|
||||
});
|
||||
setCorsHeaders(res);
|
||||
proxyRes.pipe(res);
|
||||
}
|
||||
});
|
||||
|
||||
const forwardPath =
|
||||
request.url?.replace(new RegExp(`^${proxyEndpoint}`, 'i'), '') || '';
|
||||
request.url = forwardPath;
|
||||
htmlProxy.web(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxies mkdocs livereload long-polling requests, mapping the CLI path to mkdocs path.
|
||||
*/
|
||||
export function proxyMkdocsLivereload(options: {
|
||||
request: http.IncomingMessage;
|
||||
response: http.ServerResponse;
|
||||
mkdocsTargetAddress: string;
|
||||
onError: (error: Error) => void;
|
||||
}): void {
|
||||
const { request, response, mkdocsTargetAddress, onError } = options;
|
||||
|
||||
const proxy = httpProxy.createProxyServer({ target: mkdocsTargetAddress });
|
||||
proxy.on('error', onError);
|
||||
|
||||
setCorsHeaders(response);
|
||||
response.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
request.url = request.url?.replace(
|
||||
CLI_LIVERELOAD_PATH,
|
||||
MKDOCS_LIVERELOAD_PATH,
|
||||
);
|
||||
proxy.web(request, response);
|
||||
}
|
||||
@@ -459,8 +459,6 @@ export interface UnifiedThemeProviderProps {
|
||||
// (undocumented)
|
||||
children: ReactNode;
|
||||
// (undocumented)
|
||||
noCssBaseline?: boolean;
|
||||
// (undocumented)
|
||||
theme: UnifiedTheme;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import {
|
||||
ThemeProvider,
|
||||
StylesProvider,
|
||||
@@ -38,7 +37,6 @@ import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material
|
||||
export interface UnifiedThemeProviderProps {
|
||||
children: ReactNode;
|
||||
theme: UnifiedTheme;
|
||||
noCssBaseline?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +66,7 @@ import { useApplyThemeAttributes } from './useApplyThemeAttributes';
|
||||
export function UnifiedThemeProvider(
|
||||
props: UnifiedThemeProviderProps,
|
||||
): JSX.Element {
|
||||
const { children, theme, noCssBaseline = false } = props;
|
||||
const { children, theme } = props;
|
||||
|
||||
const v4Theme = theme.getTheme('v4') as Mui4Theme;
|
||||
const v5Theme = theme.getTheme('v5') as Mui5Theme;
|
||||
@@ -78,17 +76,7 @@ export function UnifiedThemeProvider(
|
||||
'backstage',
|
||||
);
|
||||
|
||||
let cssBaseline: JSX.Element | undefined = undefined;
|
||||
if (!noCssBaseline) {
|
||||
cssBaseline = <CssBaseline />;
|
||||
}
|
||||
|
||||
let result = (
|
||||
<>
|
||||
{cssBaseline}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
let result = children as JSX.Element;
|
||||
|
||||
if (v4Theme) {
|
||||
result = (
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/ui
|
||||
|
||||
## 0.7.2-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3c921c5: Making href mandatory in tabs that are part of a Header component
|
||||
- 5c21e45: Add react router for internal routing for ButtonLinks
|
||||
- 9781815: Remove auto selection of tabs for tabs that all have href defined
|
||||
- f6dff5b: Using react router for internal links in the Menu component
|
||||
|
||||
## 0.7.2-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+10646
-10584
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/ui",
|
||||
"version": "0.7.2-next.1",
|
||||
"version": "0.7.2-next.2",
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
},
|
||||
@@ -42,7 +42,7 @@
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
"react-aria-components": "^1.10.1"
|
||||
"react-aria-components": "^1.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
@@ -521,6 +521,7 @@ export const componentDefinitions: {
|
||||
readonly item: 'bui-MenuItem';
|
||||
readonly itemListBox: 'bui-MenuItemListBox';
|
||||
readonly itemListBoxCheck: 'bui-MenuItemListBoxCheck';
|
||||
readonly itemWrapper: 'bui-MenuItemWrapper';
|
||||
readonly itemContent: 'bui-MenuItemContent';
|
||||
readonly itemArrow: 'bui-MenuItemArrow';
|
||||
readonly separator: 'bui-MenuSeparator';
|
||||
@@ -530,6 +531,12 @@ export const componentDefinitions: {
|
||||
readonly emptyState: 'bui-MenuEmptyState';
|
||||
};
|
||||
};
|
||||
readonly PasswordField: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-PasswordField';
|
||||
readonly inputVisibility: 'bui-InputVisibility';
|
||||
};
|
||||
};
|
||||
readonly Popover: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Popover';
|
||||
@@ -648,6 +655,7 @@ export const componentDefinitions: {
|
||||
readonly inputWrapper: 'bui-InputWrapper';
|
||||
readonly input: 'bui-Input';
|
||||
readonly inputIcon: 'bui-InputIcon';
|
||||
readonly inputAction: 'bui-InputAction';
|
||||
};
|
||||
readonly dataAttributes: {
|
||||
readonly invalid: readonly [true, false];
|
||||
@@ -1342,9 +1350,15 @@ export interface MenuAutocompleteListBoxProps<T>
|
||||
extends ListBoxProps<T>,
|
||||
Omit<ListBoxProps<T>, 'children'> {
|
||||
// (undocumented)
|
||||
maxHeight?: string;
|
||||
// (undocumented)
|
||||
maxWidth?: string;
|
||||
// (undocumented)
|
||||
placeholder?: string;
|
||||
// (undocumented)
|
||||
placement?: PopoverProps['placement'];
|
||||
// (undocumented)
|
||||
virtualized?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -1352,9 +1366,15 @@ export interface MenuAutocompleteProps<T>
|
||||
extends MenuProps_2<T>,
|
||||
Omit<MenuProps_2<T>, 'children'> {
|
||||
// (undocumented)
|
||||
maxHeight?: string;
|
||||
// (undocumented)
|
||||
maxWidth?: string;
|
||||
// (undocumented)
|
||||
placeholder?: string;
|
||||
// (undocumented)
|
||||
placement?: PopoverProps['placement'];
|
||||
// (undocumented)
|
||||
virtualized?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -1391,7 +1411,13 @@ export interface MenuListBoxProps<T>
|
||||
extends ListBoxProps<T>,
|
||||
Omit<ListBoxProps<T>, 'children'> {
|
||||
// (undocumented)
|
||||
maxHeight?: string;
|
||||
// (undocumented)
|
||||
maxWidth?: string;
|
||||
// (undocumented)
|
||||
placement?: PopoverProps['placement'];
|
||||
// (undocumented)
|
||||
virtualized?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -1399,7 +1425,13 @@ export interface MenuProps<T>
|
||||
extends MenuProps_2<T>,
|
||||
Omit<MenuProps_2<T>, 'children'> {
|
||||
// (undocumented)
|
||||
maxHeight?: string;
|
||||
// (undocumented)
|
||||
maxWidth?: string;
|
||||
// (undocumented)
|
||||
placement?: PopoverProps['placement'];
|
||||
// (undocumented)
|
||||
virtualized?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -1832,6 +1864,7 @@ export interface TextFieldProps
|
||||
icon?: ReactNode;
|
||||
placeholder?: string;
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
type?: 'text' | 'email' | 'tel' | 'url';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -14,52 +14,54 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-AvatarRoot {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
border-radius: 100%;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
color: var(--bui-fg-primary);
|
||||
background-color: var(--bui-bg-surface-2);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
@layer components {
|
||||
.bui-AvatarRoot {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
border-radius: 100%;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
color: var(--bui-fg-primary);
|
||||
background-color: var(--bui-bg-surface-2);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='small'] {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.bui-AvatarRoot[data-size='small'] {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='medium'] {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
.bui-AvatarRoot[data-size='medium'] {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.bui-AvatarRoot[data-size='large'] {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
.bui-AvatarRoot[data-size='large'] {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.bui-AvatarImage {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.bui-AvatarImage {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bui-AvatarFallback {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
border-radius: var(--bui-radius-full);
|
||||
.bui-AvatarFallback {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,12 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
|
||||
};
|
||||
|
||||
const { classNames } = useStyles('Box');
|
||||
const { className, style } = extractProps(props, propDefs);
|
||||
const { className, style, dataProps } = extractProps(props, propDefs);
|
||||
|
||||
return createElement(props.as || 'div', {
|
||||
ref,
|
||||
className: clsx(classNames.root, className),
|
||||
...dataProps,
|
||||
style,
|
||||
children,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
.bui-Box {
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.bui-Box {
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Button } from './Button';
|
||||
import { Flex } from '../Flex';
|
||||
import { Text } from '../Text';
|
||||
import { Icon } from '../Icon';
|
||||
import { Tooltip, TooltipTrigger } from '../Tooltip';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/Button',
|
||||
@@ -217,12 +216,3 @@ export const Playground: Story = {
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const DisabledWithTooltips: Story = {
|
||||
render: () => (
|
||||
<TooltipTrigger>
|
||||
<Button isDisabled>Save</Button>
|
||||
<Tooltip>Why this is disabled</Tooltip>
|
||||
</TooltipTrigger>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Button as RAButton, Focusable } from 'react-aria-components';
|
||||
import { Button as RAButton } from 'react-aria-components';
|
||||
import type { ButtonProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
|
||||
@@ -30,7 +30,6 @@ export const Button = forwardRef(
|
||||
iconEnd,
|
||||
children,
|
||||
className,
|
||||
isDisabled,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -39,29 +38,18 @@ export const Button = forwardRef(
|
||||
variant,
|
||||
});
|
||||
|
||||
const btn = (
|
||||
return (
|
||||
<RAButton
|
||||
className={clsx(classNames.root, className)}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
{...rest}
|
||||
isDisabled={!!isDisabled}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</RAButton>
|
||||
);
|
||||
|
||||
return isDisabled ? (
|
||||
<Focusable>
|
||||
<span role="button" tabIndex={0} style={{ display: 'inline-flex' }}>
|
||||
{btn}
|
||||
</span>
|
||||
</Focusable>
|
||||
) : (
|
||||
btn
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -14,118 +14,120 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-Button {
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: var(--bui-radius-2);
|
||||
gap: var(--bui-space-1_5);
|
||||
flex-shrink: 0;
|
||||
@layer components {
|
||||
.bui-Button {
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: var(--bui-radius-2);
|
||||
gap: var(--bui-space-1_5);
|
||||
flex-shrink: 0;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='primary'] {
|
||||
background-color: var(--bui-bg-solid);
|
||||
color: var(--bui-fg-solid);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bui-bg-solid-hover);
|
||||
transition: background-color 150ms ease;
|
||||
&[data-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bui-bg-solid-pressed);
|
||||
.bui-Button[data-variant='primary'] {
|
||||
background-color: var(--bui-bg-solid);
|
||||
color: var(--bui-fg-solid);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bui-bg-solid-hover);
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bui-bg-solid-pressed);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
background-color: var(--bui-bg-solid-disabled);
|
||||
color: var(--bui-fg-solid-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
background-color: var(--bui-bg-solid-disabled);
|
||||
color: var(--bui-fg-solid-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='secondary'] {
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
|
||||
transition: box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-pressed);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-disabled);
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='tertiary'] {
|
||||
background-color: transparent;
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
&:hover {
|
||||
.bui-Button[data-variant='secondary'] {
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
transition: background-color 200ms ease;
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
|
||||
transition: box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-pressed);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-disabled);
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bui-bg-surface-2);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
.bui-Button[data-variant='tertiary'] {
|
||||
background-color: transparent;
|
||||
color: var(--bui-fg-disabled);
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
transition: background-color 200ms ease;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bui-bg-surface-2);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
transition: none;
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
background-color: transparent;
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='medium'] {
|
||||
font-size: var(--bui-font-size-4);
|
||||
padding: 0 var(--bui-space-3);
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] {
|
||||
font-size: var(--bui-font-size-3);
|
||||
padding: 0 var(--bui-space-2);
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.bui-Button[data-size='medium'] svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='medium'] {
|
||||
font-size: var(--bui-font-size-4);
|
||||
padding: 0 var(--bui-space-3);
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] {
|
||||
font-size: var(--bui-font-size-3);
|
||||
padding: 0 var(--bui-space-2);
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.bui-Button[data-size='medium'] svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -14,17 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-ButtonIcon {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@layer components {
|
||||
.bui-ButtonIcon {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-size='small'] {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
.bui-ButtonIcon[data-size='small'] {
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.bui-ButtonIcon[data-size='medium'] {
|
||||
padding: 0;
|
||||
width: 2.5rem;
|
||||
.bui-ButtonIcon[data-size='medium'] {
|
||||
padding: 0;
|
||||
width: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,30 +14,32 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-Card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--bui-space-3);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
border-radius: var(--bui-radius-3);
|
||||
padding-block: var(--bui-space-3);
|
||||
color: var(--bui-fg-primary);
|
||||
border: 1px solid var(--bui-border);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@layer components {
|
||||
.bui-Card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--bui-space-3);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
border-radius: var(--bui-radius-3);
|
||||
padding-block: var(--bui-space-3);
|
||||
color: var(--bui-fg-primary);
|
||||
border: 1px solid var(--bui-border);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bui-CardBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.bui-CardBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bui-CardHeader {
|
||||
padding-inline: var(--bui-space-3);
|
||||
}
|
||||
.bui-CardHeader {
|
||||
padding-inline: var(--bui-space-3);
|
||||
}
|
||||
|
||||
.bui-CardFooter {
|
||||
padding-inline: var(--bui-space-3);
|
||||
.bui-CardFooter {
|
||||
padding-inline: var(--bui-space-3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,70 @@
|
||||
.bui-CheckboxRoot {
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
padding: 0;
|
||||
flex-shrink: 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.
|
||||
*/
|
||||
|
||||
.bui-CheckboxRoot:focus-visible {
|
||||
transition: none;
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@layer components {
|
||||
.bui-CheckboxRoot {
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bui-CheckboxRoot[data-checked] {
|
||||
background-color: var(--bui-bg-solid);
|
||||
box-shadow: none;
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
.bui-CheckboxRoot:focus-visible {
|
||||
transition: none;
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.bui-CheckboxLabel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
user-select: none;
|
||||
.bui-CheckboxRoot[data-checked] {
|
||||
background-color: var(--bui-bg-solid);
|
||||
box-shadow: none;
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .bui-CheckboxRoot:not([data-checked]) {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
|
||||
.bui-CheckboxLabel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
& .bui-CheckboxRoot:not([data-checked]) {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-CheckboxIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bui-fg-solid);
|
||||
.bui-CheckboxIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-CollapsiblePanel {
|
||||
display: flex;
|
||||
height: var(--collapsible-panel-height);
|
||||
overflow: hidden;
|
||||
transition: all 150ms ease-out;
|
||||
@layer components {
|
||||
.bui-CollapsiblePanel {
|
||||
display: flex;
|
||||
height: var(--collapsible-panel-height);
|
||||
overflow: hidden;
|
||||
transition: all 150ms ease-out;
|
||||
|
||||
&[data-starting-style],
|
||||
&[data-ending-style] {
|
||||
height: 0;
|
||||
&[data-starting-style],
|
||||
&[data-ending-style] {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,12 @@ export const Container = forwardRef<HTMLDivElement, ContainerProps>(
|
||||
...displayPropDefs,
|
||||
...containerSpacingProps,
|
||||
};
|
||||
const { className, style } = extractProps(props, propDefs);
|
||||
const { className, style, dataProps } = extractProps(props, propDefs);
|
||||
|
||||
return createElement('div', {
|
||||
ref,
|
||||
className: clsx(classNames.root, className),
|
||||
...dataProps,
|
||||
style,
|
||||
children,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
.bui-Container {
|
||||
max-width: 120rem;
|
||||
padding-inline: var(--bui-space-4);
|
||||
margin-inline: auto;
|
||||
transition: padding 0.2s ease-in-out;
|
||||
}
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@layer components {
|
||||
.bui-Container {
|
||||
padding-inline: var(--bui-space-5);
|
||||
max-width: 120rem;
|
||||
padding-inline: var(--bui-space-4);
|
||||
margin-inline: auto;
|
||||
transition: padding 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.bui-Container {
|
||||
padding-inline: var(--bui-space-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
.bui-FieldError {
|
||||
display: inline-block;
|
||||
color: var(--bui-fg-danger);
|
||||
font-size: var(--bui-font-size-2);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
margin-top: var(--bui-space-2);
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.bui-FieldError {
|
||||
display: inline-block;
|
||||
color: var(--bui-fg-danger);
|
||||
font-size: var(--bui-font-size-2);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
margin-top: var(--bui-space-2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
.bui-FieldLabelWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--bui-space-3);
|
||||
gap: var(--bui-space-1);
|
||||
}
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.bui-FieldLabel {
|
||||
color: var(--bui-fg-primary);
|
||||
margin-right: auto;
|
||||
cursor: pointer;
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
font-size: var(--bui-font-size-2);
|
||||
}
|
||||
@layer components {
|
||||
.bui-FieldLabelWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--bui-space-3);
|
||||
gap: var(--bui-space-1);
|
||||
}
|
||||
|
||||
.bui-FieldSecondaryLabel {
|
||||
color: var(--bui-fg-secondary);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
margin-left: var(--bui-space-1);
|
||||
}
|
||||
.bui-FieldLabel {
|
||||
color: var(--bui-fg-primary);
|
||||
margin-right: auto;
|
||||
cursor: pointer;
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
font-size: var(--bui-font-size-2);
|
||||
}
|
||||
|
||||
.bui-FieldDescription {
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
font-size: var(--bui-font-size-2);
|
||||
color: var(--bui-fg-secondary);
|
||||
margin: 0;
|
||||
.bui-FieldSecondaryLabel {
|
||||
color: var(--bui-fg-secondary);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
margin-left: var(--bui-space-1);
|
||||
}
|
||||
|
||||
.bui-FieldDescription {
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
font-size: var(--bui-font-size-2);
|
||||
color: var(--bui-fg-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
|
||||
};
|
||||
|
||||
const { classNames } = useStyles('Flex');
|
||||
const { className, style } = extractProps(props, propDefs);
|
||||
const { className, style, dataProps } = extractProps(props, propDefs);
|
||||
|
||||
return createElement('div', {
|
||||
ref,
|
||||
className: clsx(classNames.root, className),
|
||||
...dataProps,
|
||||
style,
|
||||
children: props.children,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,9 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-Flex {
|
||||
display: flex;
|
||||
@layer components {
|
||||
.bui-Flex {
|
||||
display: flex;
|
||||
|
||||
/* This helps when using `truncate` on text inside a flex container */
|
||||
min-width: 0;
|
||||
/* This helps when using `truncate` on text inside a flex container */
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
|
||||
|
||||
const { classNames } = useStyles('Grid');
|
||||
|
||||
const { className, style } = extractProps(props, propDefs);
|
||||
const { className, style, dataProps } = extractProps(props, propDefs);
|
||||
|
||||
return createElement('div', {
|
||||
ref,
|
||||
className: clsx(classNames.root, className),
|
||||
...dataProps,
|
||||
style,
|
||||
children: props.children,
|
||||
});
|
||||
@@ -48,11 +49,12 @@ const GridItem = forwardRef<HTMLDivElement, GridItemProps>((props, ref) => {
|
||||
};
|
||||
|
||||
const { classNames } = useStyles('Grid');
|
||||
const { className, style } = extractProps(props, propDefs);
|
||||
const { className, style, dataProps } = extractProps(props, propDefs);
|
||||
|
||||
return createElement('div', {
|
||||
ref,
|
||||
className: clsx(classNames.item, className),
|
||||
...dataProps,
|
||||
style,
|
||||
children: props.children,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
.bui-Grid {
|
||||
display: grid;
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@layer components {
|
||||
.bui-Grid {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,81 +14,83 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-HeaderToolbar {
|
||||
margin-bottom: var(--bui-space-6);
|
||||
@layer components {
|
||||
.bui-HeaderToolbar {
|
||||
margin-bottom: var(--bui-space-6);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 16px;
|
||||
background-color: var(--bui-bg);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&[data-has-tabs='true'] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarWrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
padding-inline: var(--bui-space-5);
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
color: var(--bui-fg-primary);
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarName {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--bui-bg);
|
||||
z-index: 0;
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
& svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-has-tabs='true'] {
|
||||
margin-bottom: 0;
|
||||
.bui-HeaderToolbarControls {
|
||||
position: absolute;
|
||||
right: var(--bui-space-5);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-HeaderTabsWrapper {
|
||||
margin-bottom: var(--bui-space-4);
|
||||
padding-inline: var(--bui-space-3);
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarWrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
padding-inline: var(--bui-space-5);
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
color: var(--bui-fg-primary);
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarName {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
& svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-HeaderToolbarControls {
|
||||
position: absolute;
|
||||
right: var(--bui-space-5);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-HeaderTabsWrapper {
|
||||
margin-bottom: var(--bui-space-4);
|
||||
padding-inline: var(--bui-space-3);
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user