From c96e2b3445a06b7a7da7fba0d0e8f52fde837943 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 19 Apr 2026 09:48:56 +0200 Subject: [PATCH] feat(ui): add description, tags, and metadata props to Header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Made-with: Cursor --- .changeset/header-improvements.md | 7 ++ packages/ui/package.json | 1 + .../src/components/Header/Header.module.css | 24 +++++++ .../src/components/Header/Header.stories.tsx | 64 +++++++++++++++++++ packages/ui/src/components/Header/Header.tsx | 62 +++++++++++++++++- .../ui/src/components/Header/definition.ts | 7 ++ packages/ui/src/components/Header/types.ts | 30 +++++++++ 7 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 .changeset/header-improvements.md diff --git a/.changeset/header-improvements.md b/.changeset/header-improvements.md new file mode 100644 index 0000000000..961cb3c0c7 --- /dev/null +++ b/.changeset/header-improvements.md @@ -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 diff --git a/packages/ui/package.json b/packages/ui/package.json index 1b17bc8744..242d15036c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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" }, diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index d74d3eeb50..c386d5a00e 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -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; + } } diff --git a/packages/ui/src/components/Header/Header.stories.tsx b/packages/ui/src/components/Header/Header.stories.tsx index c30534a993..0cfe6c03bf 100644 --- a/packages/ui/src/components/Header/Header.stories.tsx +++ b/packages/ui/src/components/Header/Header.stories.tsx @@ -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: , 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' }, + ], }, }); diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index bc8289cdf1..868b084312 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -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 ( + {tags && tags.length > 0 && ( +
+ {tags.map((tag, i) => ( + + {i > 0 && } + {tag.href ? ( + + {tag.label} + + ) : ( + + {tag.label} + + )} + + ))} +
+ )}
{breadcrumbs && @@ -61,6 +89,36 @@ export const Header = (props: HeaderProps) => {
{customActions}
+ {description && ( + ( + + {children} + + ), + a: ({ href, children }) => ( + + {children} + + ), + }} + > + {description} + + )} + {metadata && metadata.length > 0 && ( +
+ {metadata.map(item => ( + + {item.label}: {item.value} + + ))} +
+ )} {tabs && (
diff --git a/packages/ui/src/components/Header/definition.ts b/packages/ui/src/components/Header/definition.ts index 2876770339..29e813974b 100644 --- a/packages/ui/src/components/Header/definition.ts +++ b/packages/ui/src/components/Header/definition.ts @@ -30,6 +30,10 @@ export const HeaderDefinition = defineComponent()({ 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()({ tabs: {}, activeTabId: {}, breadcrumbs: {}, + description: {}, + tags: {}, + metadata: {}, className: {}, }, }); diff --git a/packages/ui/src/components/Header/types.ts b/packages/ui/src/components/Header/types.ts index 1d4acc45ea..b2c687fcf5 100644 --- a/packages/ui/src/components/Header/types.ts +++ b/packages/ui/src/components/Header/types.ts @@ -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; }