feat(ui): add description, tags, and metadata props to Header
Add three new props to the Header component: - `tags`: renders a row of text/link items above the title, separated by 3×3px circle dividers - `description`: renders a markdown string below the title with inline link support via react-markdown - `metadata`: renders key-value pairs below the description with 20px gaps Also deprecates the `breadcrumbs` prop for future removal. Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Added `description`, `tags`, and `metadata` props to the `Header` component. The `description` prop accepts a markdown string with support for inline links. The `tags` prop renders a row of text or link items above the title. The `metadata` prop renders key-value pairs below the description. The `breadcrumbs` prop has been deprecated and will be removed in a future release.
|
||||
|
||||
**Affected components:** Header
|
||||
@@ -52,6 +52,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"react-aria": "~3.48.0",
|
||||
"react-aria-components": "~1.17.0",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-stately": "~3.46.0",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
|
||||
@@ -47,4 +47,28 @@
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-HeaderTags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bui-HeaderTagDivider {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bui-fg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bui-HeaderMetaRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,58 @@ export const WithLongBreadcrumbs = meta.story({
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDescription = meta.story({
|
||||
decorators: [withRouter],
|
||||
args: {
|
||||
...Default.input.args,
|
||||
description:
|
||||
'This is a description of the page. It can include [inline links](https://backstage.io) and **bold text**.',
|
||||
},
|
||||
});
|
||||
|
||||
export const WithTags = meta.story({
|
||||
decorators: [withRouter],
|
||||
args: {
|
||||
...Default.input.args,
|
||||
tags: [
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const WithMetadata = meta.story({
|
||||
decorators: [withRouter],
|
||||
args: {
|
||||
...Default.input.args,
|
||||
metadata: [
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
{ label: 'Tier', value: 'gold' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDescriptionTagsAndMetadata = meta.story({
|
||||
decorators: [withRouter],
|
||||
args: {
|
||||
...Default.input.args,
|
||||
description:
|
||||
'This is a description of the page. It can include [inline links](https://backstage.io) and **bold text**.',
|
||||
tags: [
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
],
|
||||
metadata: [
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
{ label: 'Tier', value: 'gold' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const WithEverything = meta.story({
|
||||
decorators: [withRouter],
|
||||
args: {
|
||||
@@ -159,6 +211,18 @@ export const WithEverything = meta.story({
|
||||
tabs,
|
||||
customActions: <Button>Custom action</Button>,
|
||||
breadcrumbs: [{ label: 'Home', href: '/' }],
|
||||
description:
|
||||
'This is a description of the page. It can include [inline links](https://backstage.io) and **bold text**.',
|
||||
tags: [
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
],
|
||||
metadata: [
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
{ label: 'Tier', value: 'gold' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { HeaderDefinition } from './definition';
|
||||
import { Container } from '../Container';
|
||||
import { Link } from '../Link';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
/**
|
||||
* A secondary header with title, breadcrumbs, tabs, and actions.
|
||||
@@ -31,11 +32,38 @@ import { Fragment } from 'react/jsx-runtime';
|
||||
*/
|
||||
export const Header = (props: HeaderProps) => {
|
||||
const { ownProps } = useDefinition(HeaderDefinition, props);
|
||||
const { classes, title, tabs, activeTabId, customActions, breadcrumbs } =
|
||||
ownProps;
|
||||
const {
|
||||
classes,
|
||||
title,
|
||||
tabs,
|
||||
activeTabId,
|
||||
customActions,
|
||||
breadcrumbs,
|
||||
description,
|
||||
tags,
|
||||
metadata,
|
||||
} = ownProps;
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
{tags && tags.length > 0 && (
|
||||
<div className={classes.tags}>
|
||||
{tags.map((tag, i) => (
|
||||
<Fragment key={tag.label}>
|
||||
{i > 0 && <span className={classes.tagDivider} aria-hidden />}
|
||||
{tag.href ? (
|
||||
<Link href={tag.href} variant="body-small" standalone>
|
||||
{tag.label}
|
||||
</Link>
|
||||
) : (
|
||||
<Text variant="body-small" color="secondary">
|
||||
{tag.label}
|
||||
</Text>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.content}>
|
||||
<div className={classes.breadcrumbs}>
|
||||
{breadcrumbs &&
|
||||
@@ -61,6 +89,36 @@ export const Header = (props: HeaderProps) => {
|
||||
</div>
|
||||
<div className={classes.controls}>{customActions}</div>
|
||||
</div>
|
||||
{description && (
|
||||
<ReactMarkdown
|
||||
className={classes.description}
|
||||
allowedElements={['p', 'a', 'strong', 'em']}
|
||||
unwrapDisallowed
|
||||
components={{
|
||||
p: ({ children }) => (
|
||||
<Text variant="body-medium" color="secondary">
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<Link href={href ?? ''} standalone>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
{metadata && metadata.length > 0 && (
|
||||
<div className={classes.metaRow}>
|
||||
{metadata.map(item => (
|
||||
<Text key={item.label} variant="body-small" color="secondary">
|
||||
<strong>{item.label}:</strong> {item.value}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tabs && (
|
||||
<div className={classes.tabsWrapper}>
|
||||
<HeaderNav tabs={tabs} activeTabId={activeTabId} />
|
||||
|
||||
@@ -30,6 +30,10 @@ export const HeaderDefinition = defineComponent<HeaderOwnProps>()({
|
||||
breadcrumbs: 'bui-HeaderBreadcrumbs',
|
||||
tabsWrapper: 'bui-HeaderTabsWrapper',
|
||||
controls: 'bui-HeaderControls',
|
||||
tags: 'bui-HeaderTags',
|
||||
tagDivider: 'bui-HeaderTagDivider',
|
||||
description: 'bui-HeaderDescription',
|
||||
metaRow: 'bui-HeaderMetaRow',
|
||||
},
|
||||
propDefs: {
|
||||
title: {},
|
||||
@@ -37,6 +41,9 @@ export const HeaderDefinition = defineComponent<HeaderOwnProps>()({
|
||||
tabs: {},
|
||||
activeTabId: {},
|
||||
breadcrumbs: {},
|
||||
description: {},
|
||||
tags: {},
|
||||
metadata: {},
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,6 +52,26 @@ export interface HeaderNavTabGroup {
|
||||
*/
|
||||
export type HeaderNavTabItem = HeaderNavTab | HeaderNavTabGroup;
|
||||
|
||||
/**
|
||||
* Represents a tag item in the header.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HeaderTag {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a metadata key-value pair in the header.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HeaderMetadataItem {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Own props for the Header component.
|
||||
*
|
||||
@@ -62,7 +82,17 @@ export interface HeaderOwnProps {
|
||||
customActions?: React.ReactNode;
|
||||
tabs?: HeaderNavTabItem[];
|
||||
activeTabId?: string | null;
|
||||
/**
|
||||
* @deprecated The breadcrumbs prop will be removed in a future release.
|
||||
*/
|
||||
breadcrumbs?: HeaderBreadcrumb[];
|
||||
/**
|
||||
* Markdown string rendered below the title. Only inline elements are
|
||||
* supported (links, bold, italic). Block-level markdown is not rendered.
|
||||
*/
|
||||
description?: string;
|
||||
tags?: HeaderTag[];
|
||||
metadata?: HeaderMetadataItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user