Merge pull request #33997 from backstage/charlesdedreuille/act-355-header-improvements
feat(ui): add description, tags, and metadata props to Header
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
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Header } from '../../../../../packages/ui/src/components/Header/Header';
|
||||
import { HeaderMetadataUsers } from '../../../../../packages/ui/src/components/Header/HeaderMetadataUsers';
|
||||
import { HeaderMetadataStatus } from '../../../../../packages/ui/src/components/Header/HeaderMetadataStatus';
|
||||
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
|
||||
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
|
||||
import {
|
||||
@@ -11,6 +13,29 @@ import {
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { RiMore2Line } from '@remixicon/react';
|
||||
|
||||
const users = {
|
||||
giles: {
|
||||
name: 'Giles Peyton-Nicoll',
|
||||
src: 'https://i.pravatar.cc/150?u=giles',
|
||||
href: '/users/giles',
|
||||
},
|
||||
alice: {
|
||||
name: 'Alice Johnson',
|
||||
src: 'https://i.pravatar.cc/150?u=alice42',
|
||||
href: '/users/alice',
|
||||
},
|
||||
bob: {
|
||||
name: 'Bob Smith',
|
||||
src: 'https://i.pravatar.cc/150?u=bob',
|
||||
href: '/users/bob',
|
||||
},
|
||||
carol: {
|
||||
name: 'Carol Williams',
|
||||
src: 'https://i.pravatar.cc/150?u=carol',
|
||||
href: '/users/carol',
|
||||
},
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview', href: '/overview' },
|
||||
{ id: 'checks', label: 'Checks', href: '/checks' },
|
||||
@@ -29,12 +54,37 @@ const breadcrumbs = [
|
||||
},
|
||||
];
|
||||
|
||||
const tags = [
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
];
|
||||
|
||||
const metadataUsers = [
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Status',
|
||||
value: <HeaderMetadataStatus label="Passing" color="success" />,
|
||||
},
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[users.giles]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers users={[users.alice, users.bob, users.carol]} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const WithEverything = () => (
|
||||
<MemoryRouter initialEntries={['/overview']}>
|
||||
<Header
|
||||
title="Page Title"
|
||||
tags={tags}
|
||||
description="A short description of this page. Supports [inline links](https://backstage.io)."
|
||||
metadata={metadataUsers}
|
||||
tabs={tabs.slice(0, 2)}
|
||||
breadcrumbs={breadcrumbs.slice(0, 2)}
|
||||
customActions={
|
||||
<>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
@@ -45,6 +95,84 @@ export const WithEverything = () => (
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithMetadataUsers = () => (
|
||||
<MemoryRouter>
|
||||
<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[users.giles]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[users.alice, users.bob, users.carol]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithTags = () => (
|
||||
<MemoryRouter>
|
||||
<Header title="Page Title" tags={tags} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithDescription = () => (
|
||||
<MemoryRouter>
|
||||
<Header
|
||||
title="Page Title"
|
||||
description="A short description of this page. Supports [inline links](https://backstage.io)."
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithMetadata = () => (
|
||||
<MemoryRouter>
|
||||
<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithMetadataStatus = () => (
|
||||
<MemoryRouter>
|
||||
<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{
|
||||
label: 'Status',
|
||||
value: <HeaderMetadataStatus label="Passing" color="success" />,
|
||||
},
|
||||
{
|
||||
label: 'Build',
|
||||
value: (
|
||||
<HeaderMetadataStatus
|
||||
label="Failed"
|
||||
color="danger"
|
||||
href="/builds/123"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Coverage',
|
||||
value: <HeaderMetadataStatus label="Warning" color="warning" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithLongBreadcrumbs = () => (
|
||||
<MemoryRouter>
|
||||
<Header title="Page Title" breadcrumbs={breadcrumbs.slice(0, 2)} />
|
||||
|
||||
@@ -3,17 +3,28 @@ import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import {
|
||||
WithEverything,
|
||||
WithLongBreadcrumbs,
|
||||
WithTabs,
|
||||
WithTags,
|
||||
WithDescription,
|
||||
WithMetadata,
|
||||
WithMetadataUsers,
|
||||
WithMetadataStatus,
|
||||
WithCustomActions,
|
||||
WithMenu,
|
||||
} from './components';
|
||||
import { headerPagePropDefs } from './props-definition';
|
||||
import {
|
||||
headerPagePropDefs,
|
||||
headerMetadataUsersPropDefs,
|
||||
} from './props-definition';
|
||||
import {
|
||||
usage,
|
||||
defaultSnippet,
|
||||
withTabs,
|
||||
withBreadcrumbs,
|
||||
withTags,
|
||||
withDescription,
|
||||
withMetadata,
|
||||
withMetadataUsers,
|
||||
withMetadataStatus,
|
||||
withCustomActions,
|
||||
withMenu,
|
||||
} from './snippets';
|
||||
@@ -24,7 +35,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
|
||||
<PageTitle
|
||||
title="Header"
|
||||
description="A secondary header with title, breadcrumbs, tabs, and actions."
|
||||
description="A secondary header with title, tags, description, metadata, tabs, and actions."
|
||||
/>
|
||||
|
||||
<Snippet py={4} preview={<WithEverything />} code={defaultSnippet} />
|
||||
@@ -39,11 +50,37 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
|
||||
## Examples
|
||||
|
||||
### Breadcrumbs
|
||||
### Tags
|
||||
|
||||
Labels are truncated at 240px.
|
||||
Tags are rendered above the title. Each tag with an `href` renders as a link; tags without `href` render as plain text. Tags are separated by a small circle divider.
|
||||
|
||||
<Snippet open preview={<WithLongBreadcrumbs />} code={withBreadcrumbs} />
|
||||
<Snippet open preview={<WithTags />} code={withTags} />
|
||||
|
||||
### Description
|
||||
|
||||
The description accepts a markdown string with support for inline links. Bold, italic, and block-level markdown are not rendered.
|
||||
|
||||
<Snippet open preview={<WithDescription />} code={withDescription} />
|
||||
|
||||
### Metadata
|
||||
|
||||
Key-value pairs displayed below the description.
|
||||
|
||||
<Snippet open preview={<WithMetadata />} code={withMetadata} />
|
||||
|
||||
### Metadata with users
|
||||
|
||||
Use `HeaderMetadataUsers` as the metadata value to display users as avatars. A single user shows the avatar with their name beside it. Multiple users show a row of avatars — hover to reveal each name via tooltip. When a user has an `href`, the avatar and name become links.
|
||||
|
||||
<Snippet open preview={<WithMetadataUsers />} code={withMetadataUsers} />
|
||||
|
||||
<PropsTable data={headerMetadataUsersPropDefs} />
|
||||
|
||||
### Metadata with status
|
||||
|
||||
Use `HeaderMetadataStatus` as the metadata value to display a status indicator. The dot colour is driven by the `color` prop which maps to BUI status tokens. Pass an `href` to make the label a link.
|
||||
|
||||
<Snippet open preview={<WithMetadataStatus />} code={withMetadataStatus} />
|
||||
|
||||
### Tabs
|
||||
|
||||
|
||||
@@ -5,6 +5,51 @@ export const headerPagePropDefs: Record<string, PropDef> = {
|
||||
type: 'string',
|
||||
description: 'Page heading displayed in the header.',
|
||||
},
|
||||
tags: {
|
||||
type: 'complex',
|
||||
description:
|
||||
'Items displayed above the title. Each tag renders as a link when href is provided, or as plain text otherwise. Tags are separated by a small circle divider.',
|
||||
complexType: {
|
||||
name: 'HeaderTag[]',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Display text for the tag.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'URL to navigate to when the tag is clicked.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Markdown string rendered below the title. Only inline links are supported. Bold, italic, and block-level markdown are not rendered.',
|
||||
},
|
||||
metadata: {
|
||||
type: 'complex',
|
||||
description: 'Key-value pairs displayed below the description.',
|
||||
complexType: {
|
||||
name: 'HeaderMetadataItem[]',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The key label, displayed in secondary color.',
|
||||
},
|
||||
value: {
|
||||
type: 'string | ReactNode',
|
||||
required: true,
|
||||
description:
|
||||
'The value to display alongside the label. Pass a string for plain text or a ReactNode for custom content such as HeaderMetadataUsers.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
customActions: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
@@ -49,6 +94,7 @@ export const headerPagePropDefs: Record<string, PropDef> = {
|
||||
},
|
||||
breadcrumbs: {
|
||||
type: 'complex',
|
||||
deprecated: true,
|
||||
description: 'Breadcrumb trail displayed above the title.',
|
||||
complexType: {
|
||||
name: 'HeaderBreadcrumb[]',
|
||||
@@ -68,3 +114,33 @@ export const headerPagePropDefs: Record<string, PropDef> = {
|
||||
},
|
||||
...classNamePropDefs,
|
||||
};
|
||||
|
||||
export const headerMetadataUsersPropDefs: Record<string, PropDef> = {
|
||||
users: {
|
||||
type: 'complex',
|
||||
description:
|
||||
'List of users to display. A single user shows the avatar with their name beside it. Multiple users show a row of avatars with names revealed on hover via tooltip.',
|
||||
complexType: {
|
||||
name: 'HeaderMetadataUser[]',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description:
|
||||
'Display name shown beside the avatar (single) or in the tooltip (multiple).',
|
||||
},
|
||||
src: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'URL for the avatar image.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'When provided, the avatar becomes a link and the name is rendered as a Link component.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,15 +2,41 @@ export const usage = `import { Header } from '@backstage/ui';
|
||||
|
||||
<Header title="Page Title" />`;
|
||||
|
||||
export const defaultSnippet = `<Header
|
||||
export const defaultSnippet = `import { Header, HeaderMetadataUsers, HeaderMetadataStatus } from '@backstage/ui';
|
||||
|
||||
<Header
|
||||
title="Page Title"
|
||||
breadcrumbs={[
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Dashboard', href: '/dashboard' },
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
]}
|
||||
description="A short description. Supports [inline links](https://backstage.io)."
|
||||
metadata={[
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Status',
|
||||
value: <HeaderMetadataStatus label="Passing" color="success" />,
|
||||
},
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[
|
||||
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
|
||||
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
|
||||
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
tabs={[
|
||||
{ id: 'overview', label: 'Overview', href: '/overview' },
|
||||
{ id: 'settings', label: 'Settings', href: '/settings' },
|
||||
{ id: 'checks', label: 'Checks', href: '/checks' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
@@ -54,3 +80,70 @@ export const withMenu = `<Header
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>`;
|
||||
|
||||
export const withTags = `<Header
|
||||
title="Page Title"
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const withDescription = `<Header
|
||||
title="Page Title"
|
||||
description="A short description. Supports [inline links](https://backstage.io)."
|
||||
/>`;
|
||||
|
||||
export const withMetadata = `<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const withMetadataStatus = `import { Header, HeaderMetadataStatus } from '@backstage/ui';
|
||||
|
||||
<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{
|
||||
label: 'Status',
|
||||
value: <HeaderMetadataStatus label="Passing" color="success" />,
|
||||
},
|
||||
{
|
||||
label: 'Build',
|
||||
value: <HeaderMetadataStatus label="Failed" color="danger" href="/builds/123" />,
|
||||
},
|
||||
{
|
||||
label: 'Coverage',
|
||||
value: <HeaderMetadataStatus label="Warning" color="warning" />,
|
||||
},
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const withMetadataUsers = `import { Header, HeaderMetadataUsers } from '@backstage/ui';
|
||||
|
||||
<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[
|
||||
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
|
||||
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
|
||||
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>`;
|
||||
|
||||
@@ -4,12 +4,18 @@ import styles from './styles.module.css';
|
||||
export const Chip = ({
|
||||
children,
|
||||
head = false,
|
||||
deprecated = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
head?: boolean;
|
||||
deprecated?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<span className={`${styles.chip} ${head ? styles.head : ''}`}>
|
||||
<span
|
||||
className={`${styles.chip} ${head ? styles.head : ''} ${
|
||||
deprecated ? styles.deprecated : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.deprecated {
|
||||
background-color: #fff4e5;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
[data-theme-mode='dark'] .chip {
|
||||
background-color: #2c2c2c;
|
||||
color: #fff;
|
||||
@@ -22,3 +27,8 @@
|
||||
[data-theme-mode='dark'] .chip.head {
|
||||
background-color: #33405b;
|
||||
}
|
||||
|
||||
[data-theme-mode='dark'] .chip.deprecated {
|
||||
background-color: #3d2a10;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,12 @@ export const PropsTable = <T extends Record<string, PropData>>({
|
||||
|
||||
switch (column) {
|
||||
case 'prop':
|
||||
return <Chip head>{propName}</Chip>;
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}>
|
||||
<Chip head>{propName}</Chip>
|
||||
{propData.deprecated && <Chip deprecated>deprecated</Chip>}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'type':
|
||||
return (
|
||||
|
||||
@@ -44,6 +44,7 @@ export type PropDef = {
|
||||
required?: boolean;
|
||||
responsive?: boolean;
|
||||
description?: ReactNode;
|
||||
deprecated?: boolean;
|
||||
};
|
||||
|
||||
export { breakpoints };
|
||||
|
||||
@@ -47,10 +47,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@braintree/sanitize-url": "^7.1.2",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@remixicon/react": ">=4.6.0 <4.9.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
"marked": "^15.0.12",
|
||||
"react-aria": "~3.48.0",
|
||||
"react-aria-components": "~1.17.0",
|
||||
"react-stately": "~3.46.0",
|
||||
|
||||
@@ -1585,6 +1585,11 @@ export const HeaderDefinition: {
|
||||
readonly breadcrumbs: 'bui-HeaderBreadcrumbs';
|
||||
readonly tabsWrapper: 'bui-HeaderTabsWrapper';
|
||||
readonly controls: 'bui-HeaderControls';
|
||||
readonly tags: 'bui-HeaderTags';
|
||||
readonly tag: 'bui-HeaderTag';
|
||||
readonly description: 'bui-HeaderDescription';
|
||||
readonly metaRow: 'bui-HeaderMetaRow';
|
||||
readonly metaItem: 'bui-HeaderMetaItem';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly title: {};
|
||||
@@ -1592,10 +1597,51 @@ export const HeaderDefinition: {
|
||||
readonly tabs: {};
|
||||
readonly activeTabId: {};
|
||||
readonly breadcrumbs: {};
|
||||
readonly description: {};
|
||||
readonly tags: {};
|
||||
readonly metadata: {};
|
||||
readonly className: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface HeaderMetadataItem {
|
||||
// (undocumented)
|
||||
label: string;
|
||||
// (undocumented)
|
||||
value: React.ReactNode;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const HeaderMetadataStatus: (
|
||||
input: HeaderMetadataStatusProps,
|
||||
) => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export interface HeaderMetadataStatusProps {
|
||||
// (undocumented)
|
||||
color: 'danger' | 'warning' | 'success' | 'info';
|
||||
// (undocumented)
|
||||
href?: string;
|
||||
// (undocumented)
|
||||
label: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface HeaderMetadataUser {
|
||||
// (undocumented)
|
||||
href?: string;
|
||||
// (undocumented)
|
||||
name: string;
|
||||
// (undocumented)
|
||||
src?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const HeaderMetadataUsers: (input: {
|
||||
users: HeaderMetadataUser[];
|
||||
}) => JSX_2.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const HeaderNavDefinition: {
|
||||
readonly styles: {
|
||||
@@ -1676,15 +1722,20 @@ export type HeaderNavTabItem = HeaderNavTab | HeaderNavTabGroup;
|
||||
export interface HeaderOwnProps {
|
||||
// (undocumented)
|
||||
activeTabId?: string | null;
|
||||
// (undocumented)
|
||||
// @deprecated (undocumented)
|
||||
breadcrumbs?: HeaderBreadcrumb[];
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
// (undocumented)
|
||||
customActions?: React.ReactNode;
|
||||
description?: string;
|
||||
// (undocumented)
|
||||
metadata?: HeaderMetadataItem[];
|
||||
// (undocumented)
|
||||
tabs?: HeaderNavTabItem[];
|
||||
// (undocumented)
|
||||
tags?: HeaderTag[];
|
||||
// (undocumented)
|
||||
title?: string;
|
||||
}
|
||||
|
||||
@@ -1705,6 +1756,11 @@ export const HeaderPageDefinition: {
|
||||
readonly breadcrumbs: 'bui-HeaderBreadcrumbs';
|
||||
readonly tabsWrapper: 'bui-HeaderTabsWrapper';
|
||||
readonly controls: 'bui-HeaderControls';
|
||||
readonly tags: 'bui-HeaderTags';
|
||||
readonly tag: 'bui-HeaderTag';
|
||||
readonly description: 'bui-HeaderDescription';
|
||||
readonly metaRow: 'bui-HeaderMetaRow';
|
||||
readonly metaItem: 'bui-HeaderMetaItem';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly title: {};
|
||||
@@ -1712,6 +1768,9 @@ export const HeaderPageDefinition: {
|
||||
readonly tabs: {};
|
||||
readonly activeTabId: {};
|
||||
readonly breadcrumbs: {};
|
||||
readonly description: {};
|
||||
readonly tags: {};
|
||||
readonly metadata: {};
|
||||
readonly className: {};
|
||||
};
|
||||
};
|
||||
@@ -1736,6 +1795,14 @@ export interface HeaderTab {
|
||||
matchStrategy?: TabMatchStrategy;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface HeaderTag {
|
||||
// (undocumented)
|
||||
href?: string;
|
||||
// (undocumented)
|
||||
label: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type JustifyContent =
|
||||
| 'stretch'
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
.bui-Header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--bui-space-1);
|
||||
gap: var(--bui-space-3);
|
||||
margin-top: var(--bui-space-6);
|
||||
}
|
||||
|
||||
@@ -47,4 +47,50 @@
|
||||
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;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bui-HeaderTag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-HeaderTag + .bui-HeaderTag::before {
|
||||
content: '';
|
||||
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: var(--bui-space-5);
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bui-HeaderMetaItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import type { StoryFn } from '@storybook/react-vite';
|
||||
import { Header } from './Header';
|
||||
import { HeaderMetadataUsers } from './HeaderMetadataUsers';
|
||||
import { HeaderMetadataStatus } from './HeaderMetadataStatus';
|
||||
import type { HeaderNavTabItem } from './types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { BUIProvider } from '../../provider';
|
||||
@@ -152,16 +154,208 @@ export const WithLongBreadcrumbs = meta.story({
|
||||
},
|
||||
});
|
||||
|
||||
export const WithEverything = meta.story({
|
||||
export const WithDescription = meta.story({
|
||||
decorators: [withRouter],
|
||||
args: {
|
||||
...Default.input.args,
|
||||
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).',
|
||||
},
|
||||
});
|
||||
|
||||
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' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const users = {
|
||||
giles: {
|
||||
name: 'Giles Peyton-Nicoll',
|
||||
src: 'https://i.pravatar.cc/150?u=giles',
|
||||
href: '/users/giles',
|
||||
},
|
||||
alice: {
|
||||
name: 'Alice Johnson',
|
||||
src: 'https://i.pravatar.cc/150?u=alicej',
|
||||
href: '/users/alice',
|
||||
},
|
||||
bob: {
|
||||
name: 'Bob Smith',
|
||||
src: 'https://i.pravatar.cc/150?u=bob',
|
||||
href: '/users/bob',
|
||||
},
|
||||
carol: {
|
||||
name: 'Carol Williams',
|
||||
src: 'https://i.pravatar.cc/150?u=carol',
|
||||
href: '/users/carol',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithMetadataUsers = meta.story({
|
||||
decorators: [withRouter],
|
||||
render: () => (
|
||||
<Header
|
||||
{...Default.input.args}
|
||||
metadata={[
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[users.giles]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[users.alice, users.bob, users.carol]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithMetadataUsersNoLinks = meta.story({
|
||||
decorators: [withRouter],
|
||||
render: () => (
|
||||
<Header
|
||||
{...Default.input.args}
|
||||
metadata={[
|
||||
{
|
||||
label: 'Owner',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[{ name: users.giles.name, src: users.giles.src }]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[
|
||||
{ name: users.alice.name, src: users.alice.src },
|
||||
{ name: users.bob.name, src: users.bob.src },
|
||||
{ name: users.carol.name, src: users.carol.src },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithMetadataStatus = meta.story({
|
||||
decorators: [withRouter],
|
||||
render: () => (
|
||||
<Header
|
||||
{...Default.input.args}
|
||||
metadata={[
|
||||
{
|
||||
label: 'Status',
|
||||
value: <HeaderMetadataStatus label="Passing" color="success" />,
|
||||
},
|
||||
{
|
||||
label: 'Build',
|
||||
value: (
|
||||
<HeaderMetadataStatus
|
||||
label="Failed"
|
||||
color="danger"
|
||||
href="/builds/123"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Coverage',
|
||||
value: <HeaderMetadataStatus label="Warning" color="warning" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithDescriptionTagsAndMetadata = meta.story({
|
||||
decorators: [withRouter],
|
||||
render: () => (
|
||||
<Header
|
||||
{...Default.input.args}
|
||||
description="This is a description of the page. It can include [inline links](https://backstage.io)."
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
]}
|
||||
metadata={[
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[users.giles]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[users.alice, users.bob, users.carol]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ label: 'Type', value: 'website' },
|
||||
{ label: 'Tier', value: 'gold' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithEverything = meta.story({
|
||||
decorators: [withRouter],
|
||||
render: () => (
|
||||
<Header
|
||||
{...Default.input.args}
|
||||
tabs={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)."
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
]}
|
||||
metadata={[
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[users.giles]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[users.alice, users.bob, users.carol]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const groupedTabs: HeaderNavTabItem[] = [
|
||||
{ id: 'overview', label: 'Overview', href: '/overview' },
|
||||
{
|
||||
|
||||
@@ -20,9 +20,31 @@ import { RiArrowRightSLine } from '@remixicon/react';
|
||||
import { HeaderNav } from './HeaderNav';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { HeaderDefinition } from './definition';
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { Container } from '../Container';
|
||||
import { Lexer } from 'marked';
|
||||
import { Link } from '../Link';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import { Fragment, useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Parses inline Markdown links in a string and returns an array of React nodes.
|
||||
* URLs are sanitized via `@braintree/sanitize-url`; unsafe URLs are rendered as
|
||||
* plain text. Uses `marked` instead of `react-markdown` to avoid ESM issues.
|
||||
*/
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
return Lexer.lexInline(text).map((token, i) => {
|
||||
if (token.type === 'link') {
|
||||
const href = sanitizeUrl(token.href);
|
||||
if (href === 'about:blank') return token.text;
|
||||
return (
|
||||
<Link key={i} href={href} standalone>
|
||||
{token.text}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return token.raw;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A secondary header with title, breadcrumbs, tabs, and actions.
|
||||
@@ -31,11 +53,50 @@ 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;
|
||||
|
||||
const descriptionNodes = useMemo(
|
||||
() => (description ? renderInlineMarkdown(description) : null),
|
||||
[description],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
{tags && tags.length > 0 && (
|
||||
<ul className={classes.tags}>
|
||||
{tags.map((tag, i) => (
|
||||
<li
|
||||
key={`${i}:${tag.label}:${tag.href ?? ''}`}
|
||||
className={classes.tag}
|
||||
>
|
||||
{tag.href ? (
|
||||
<Link
|
||||
href={tag.href}
|
||||
variant="body-medium"
|
||||
color="secondary"
|
||||
standalone
|
||||
>
|
||||
{tag.label}
|
||||
</Link>
|
||||
) : (
|
||||
<Text variant="body-medium" color="secondary">
|
||||
{tag.label}
|
||||
</Text>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className={classes.content}>
|
||||
<div className={classes.breadcrumbs}>
|
||||
{breadcrumbs &&
|
||||
@@ -61,6 +122,35 @@ export const Header = (props: HeaderProps) => {
|
||||
</div>
|
||||
<div className={classes.controls}>{customActions}</div>
|
||||
</div>
|
||||
{description && (
|
||||
<Text
|
||||
variant="body-medium"
|
||||
color="secondary"
|
||||
className={classes.description}
|
||||
>
|
||||
{descriptionNodes}
|
||||
</Text>
|
||||
)}
|
||||
{metadata && metadata.length > 0 && (
|
||||
<dl className={classes.metaRow}>
|
||||
{metadata.map((item, i) => (
|
||||
<div key={`${i}:${item.label}`} className={classes.metaItem}>
|
||||
<dt>
|
||||
<Text variant="body-medium" color="secondary">
|
||||
{item.label}
|
||||
</Text>
|
||||
</dt>
|
||||
<dd>
|
||||
{typeof item.value === 'string' ? (
|
||||
<Text variant="body-medium">{item.value}</Text>
|
||||
) : (
|
||||
item.value
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
{tabs && (
|
||||
<div className={classes.tabsWrapper}>
|
||||
<HeaderNav tabs={tabs} activeTabId={activeTabId} />
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2026 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 tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.single {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot-danger {
|
||||
background-color: var(--bui-fg-danger);
|
||||
}
|
||||
|
||||
.dot-warning {
|
||||
background-color: var(--bui-fg-warning);
|
||||
}
|
||||
|
||||
.dot-success {
|
||||
background-color: var(--bui-fg-success);
|
||||
}
|
||||
|
||||
.dot-info {
|
||||
background-color: var(--bui-fg-info);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2026 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 type { HeaderMetadataStatusProps } from './types';
|
||||
import { Text } from '../Text';
|
||||
import { Link } from '../Link';
|
||||
import styles from './HeaderMetadataStatus.module.css';
|
||||
|
||||
/**
|
||||
* Displays a single status indicator as a coloured dot with a label inside a
|
||||
* Header metadata value. Optionally renders the label as a link when href is provided.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HeaderMetadataStatus = ({
|
||||
label,
|
||||
color,
|
||||
href,
|
||||
}: HeaderMetadataStatusProps) => {
|
||||
return (
|
||||
<div className={styles.single}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${styles.dot} ${styles[`dot-${color}`]}`}
|
||||
/>
|
||||
<Text variant="body-medium">
|
||||
{href ? (
|
||||
<Link href={href} standalone>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2026 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 tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.single {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--bui-space-1);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.avatarLink {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2026 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 type { HeaderMetadataUser } from './types';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { Tooltip, TooltipTrigger } from '../Tooltip';
|
||||
import { Text } from '../Text';
|
||||
import { Link } from '../Link';
|
||||
import { Pressable } from 'react-aria';
|
||||
import styles from './HeaderMetadataUsers.module.css';
|
||||
|
||||
/**
|
||||
* Displays a list of users as avatars inside a Header metadata value.
|
||||
* A single user shows the avatar with their name beside it.
|
||||
* Multiple users show avatars in a row with the name revealed on hover via tooltip.
|
||||
* When a user has an `href`, the avatar and name become links.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HeaderMetadataUsers = ({
|
||||
users,
|
||||
}: {
|
||||
users: HeaderMetadataUser[];
|
||||
}) => {
|
||||
if (users.length === 0) return null;
|
||||
|
||||
if (users.length === 1) {
|
||||
const user = users[0];
|
||||
if (user.href) {
|
||||
return (
|
||||
<Link
|
||||
href={user.href}
|
||||
variant="body-medium"
|
||||
standalone
|
||||
className={styles.single}
|
||||
>
|
||||
<Avatar
|
||||
src={user.src ?? 'data:,'}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
/>
|
||||
{user.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.single}>
|
||||
<Avatar
|
||||
src={user.src ?? 'data:,'}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
/>
|
||||
<Text variant="body-medium">{user.name}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={styles.stack}>
|
||||
{users.map((user, i) => (
|
||||
<li key={user.href ?? `${i}:${user.name}`}>
|
||||
<TooltipTrigger>
|
||||
{user.href ? (
|
||||
<Link
|
||||
href={user.href}
|
||||
aria-label={user.name}
|
||||
className={styles.avatarLink}
|
||||
>
|
||||
<Avatar
|
||||
src={user.src ?? 'data:,'}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Pressable>
|
||||
<Avatar
|
||||
src={user.src ?? 'data:,'}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="informative"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Tooltip>{user.name}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
@@ -30,6 +30,11 @@ export const HeaderDefinition = defineComponent<HeaderOwnProps>()({
|
||||
breadcrumbs: 'bui-HeaderBreadcrumbs',
|
||||
tabsWrapper: 'bui-HeaderTabsWrapper',
|
||||
controls: 'bui-HeaderControls',
|
||||
tags: 'bui-HeaderTags',
|
||||
tag: 'bui-HeaderTag',
|
||||
description: 'bui-HeaderDescription',
|
||||
metaRow: 'bui-HeaderMetaRow',
|
||||
metaItem: 'bui-HeaderMetaItem',
|
||||
},
|
||||
propDefs: {
|
||||
title: {},
|
||||
@@ -37,6 +42,9 @@ export const HeaderDefinition = defineComponent<HeaderOwnProps>()({
|
||||
tabs: {},
|
||||
activeTabId: {},
|
||||
breadcrumbs: {},
|
||||
description: {},
|
||||
tags: {},
|
||||
metadata: {},
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ export {
|
||||
HeaderNavItemDefinition,
|
||||
HeaderNavGroupDefinition,
|
||||
} from './HeaderNavDefinition';
|
||||
export { HeaderMetadataUsers } from './HeaderMetadataUsers';
|
||||
export { HeaderMetadataStatus } from './HeaderMetadataStatus';
|
||||
export type {
|
||||
HeaderNavTab,
|
||||
HeaderNavTabGroup,
|
||||
@@ -27,6 +29,10 @@ export type {
|
||||
HeaderOwnProps,
|
||||
HeaderProps,
|
||||
HeaderBreadcrumb,
|
||||
HeaderTag,
|
||||
HeaderMetadataItem,
|
||||
HeaderMetadataUser,
|
||||
HeaderMetadataStatusProps,
|
||||
HeaderPageOwnProps,
|
||||
HeaderPageProps,
|
||||
HeaderPageBreadcrumb,
|
||||
|
||||
@@ -52,6 +52,48 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user in the HeaderMetadataUsers component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HeaderMetadataUser {
|
||||
name: string;
|
||||
src?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a status item in the HeaderMetadataStatus component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HeaderMetadataStatusProps {
|
||||
label: string;
|
||||
color: 'danger' | 'warning' | 'success' | 'info';
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Own props for the Header component.
|
||||
*
|
||||
@@ -62,7 +104,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 links are supported.
|
||||
* Bold, italic, and block-level markdown are not rendered.
|
||||
*/
|
||||
description?: string;
|
||||
tags?: HeaderTag[];
|
||||
metadata?: HeaderMetadataItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@ export const catalogReactTranslationRef: TranslationRef<
|
||||
readonly 'inspectEntityDialog.overviewPage.labels': 'Labels';
|
||||
readonly 'inspectEntityDialog.overviewPage.status.title': 'Status';
|
||||
readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity';
|
||||
readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations';
|
||||
readonly 'inspectEntityDialog.overviewPage.tags': 'Tags';
|
||||
readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations';
|
||||
readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations';
|
||||
readonly 'inspectEntityDialog.overviewPage.copyAriaLabel': 'Copy {{label}}';
|
||||
readonly 'inspectEntityDialog.overviewPage.copiedStatus': 'Copied';
|
||||
@@ -122,8 +122,8 @@ export const catalogReactTranslationRef: TranslationRef<
|
||||
readonly 'entityTableColumnTitle.description': 'Description';
|
||||
readonly 'entityTableColumnTitle.system': 'System';
|
||||
readonly 'entityTableColumnTitle.namespace': 'Namespace';
|
||||
readonly 'entityTableColumnTitle.domain': 'Domain';
|
||||
readonly 'entityTableColumnTitle.tags': 'Tags';
|
||||
readonly 'entityTableColumnTitle.domain': 'Domain';
|
||||
readonly 'entityTableColumnTitle.owner': 'Owner';
|
||||
readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle';
|
||||
readonly 'entityTableColumnTitle.targets': 'Targets';
|
||||
|
||||
@@ -204,8 +204,8 @@ export const catalogReactTranslationRef: TranslationRef<
|
||||
readonly 'inspectEntityDialog.overviewPage.labels': 'Labels';
|
||||
readonly 'inspectEntityDialog.overviewPage.status.title': 'Status';
|
||||
readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity';
|
||||
readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations';
|
||||
readonly 'inspectEntityDialog.overviewPage.tags': 'Tags';
|
||||
readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations';
|
||||
readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations';
|
||||
readonly 'inspectEntityDialog.overviewPage.copyAriaLabel': 'Copy {{label}}';
|
||||
readonly 'inspectEntityDialog.overviewPage.copiedStatus': 'Copied';
|
||||
@@ -244,8 +244,8 @@ export const catalogReactTranslationRef: TranslationRef<
|
||||
readonly 'entityTableColumnTitle.description': 'Description';
|
||||
readonly 'entityTableColumnTitle.system': 'System';
|
||||
readonly 'entityTableColumnTitle.namespace': 'Namespace';
|
||||
readonly 'entityTableColumnTitle.domain': 'Domain';
|
||||
readonly 'entityTableColumnTitle.tags': 'Tags';
|
||||
readonly 'entityTableColumnTitle.domain': 'Domain';
|
||||
readonly 'entityTableColumnTitle.owner': 'Owner';
|
||||
readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle';
|
||||
readonly 'entityTableColumnTitle.targets': 'Targets';
|
||||
|
||||
@@ -66,15 +66,15 @@ export function createGithubBranchProtectionAction(options: {
|
||||
dismissStaleReviews?: boolean | undefined;
|
||||
bypassPullRequestAllowances?:
|
||||
| {
|
||||
users?: string[] | undefined;
|
||||
apps?: string[] | undefined;
|
||||
teams?: string[] | undefined;
|
||||
users?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
restrictions?:
|
||||
| {
|
||||
teams: string[];
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
@@ -241,9 +241,9 @@ export function createGithubRepoCreateAction(options: {
|
||||
branch?: string | undefined;
|
||||
bypassPullRequestAllowances?:
|
||||
| {
|
||||
users?: string[] | undefined;
|
||||
apps?: string[] | undefined;
|
||||
teams?: string[] | undefined;
|
||||
users?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
collaborators?:
|
||||
@@ -290,8 +290,8 @@ export function createGithubRepoCreateAction(options: {
|
||||
requireLastPushApproval?: boolean | undefined;
|
||||
restrictions?:
|
||||
| {
|
||||
teams: string[];
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
@@ -328,16 +328,16 @@ export function createGithubRepoPushAction(options: {
|
||||
requiredStatusCheckContexts?: string[] | undefined;
|
||||
bypassPullRequestAllowances?:
|
||||
| {
|
||||
users?: string[] | undefined;
|
||||
apps?: string[] | undefined;
|
||||
teams?: string[] | undefined;
|
||||
users?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
requiredApprovingReviewCount?: number | undefined;
|
||||
restrictions?:
|
||||
| {
|
||||
teams: string[];
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
@@ -398,16 +398,16 @@ export function createPublishGithubAction(options: {
|
||||
access?: string | undefined;
|
||||
bypassPullRequestAllowances?:
|
||||
| {
|
||||
users?: string[] | undefined;
|
||||
apps?: string[] | undefined;
|
||||
teams?: string[] | undefined;
|
||||
users?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
requiredApprovingReviewCount?: number | undefined;
|
||||
restrictions?:
|
||||
| {
|
||||
teams: string[];
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -7945,6 +7945,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@braintree/sanitize-url": "npm:^7.1.2"
|
||||
"@internationalized/date": "npm:^3.12.0"
|
||||
"@remixicon/react": "npm:>=4.6.0 <4.9.0"
|
||||
"@storybook/react-vite": "npm:^10.3.3"
|
||||
@@ -7958,6 +7959,7 @@ __metadata:
|
||||
eslint-plugin-storybook: "npm:^10.3.3"
|
||||
glob: "npm:^13.0.0"
|
||||
globals: "npm:^17.0.0"
|
||||
marked: "npm:^15.0.12"
|
||||
react: "npm:^18.0.2"
|
||||
react-aria: "npm:~3.48.0"
|
||||
react-aria-components: "npm:~1.17.0"
|
||||
@@ -8020,6 +8022,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@braintree/sanitize-url@npm:^7.1.2":
|
||||
version: 7.1.2
|
||||
resolution: "@braintree/sanitize-url@npm:7.1.2"
|
||||
checksum: 10/d9626ff8f8eb5e192cd055e6e743449c21102c76bb59e405b7028fe56230fa080bfcc80dfb1e21850a6876e75adda9f7b3c888cf0685942bb74da4d2866d6ec3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bundled-es-modules/cookie@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@bundled-es-modules/cookie@npm:2.0.1"
|
||||
@@ -11859,8 +11868,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@mswjs/interceptors@npm:^0.39.1":
|
||||
version: 0.39.6
|
||||
resolution: "@mswjs/interceptors@npm:0.39.6"
|
||||
version: 0.39.8
|
||||
resolution: "@mswjs/interceptors@npm:0.39.8"
|
||||
dependencies:
|
||||
"@open-draft/deferred-promise": "npm:^2.2.0"
|
||||
"@open-draft/logger": "npm:^0.3.0"
|
||||
@@ -11868,7 +11877,7 @@ __metadata:
|
||||
is-node-process: "npm:^1.2.0"
|
||||
outvariant: "npm:^1.4.3"
|
||||
strict-event-emitter: "npm:^0.5.1"
|
||||
checksum: 10/c87d3edf08353bde825c87b151b24d538070540ab419206cef1774c932e888af0f920183182fb7c94c3eee42068da5a0a5855853fded8514f33c870921ef37ec
|
||||
checksum: 10/d92546cf9bf670ddb927c53f5fa19f0554b7475a264ead4e1ae2339874f4312fe4ada5d42588f27eea3577bee29fa8f46889d398f0e7ecb3f7a4c1d3e0b71bdc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user