feat(ui): add href support to HeaderMetadataUsers, remove bold from description, full-width header
- Add `href` to `HeaderMetadataUser`: avatar becomes a link and name renders as a primary `Link` with `standalone` (no underline at rest) - Add `WithMetadataUsersNoLinks` story alongside `WithMetadataUsers` to cover both link and non-link variants - Remove `strong`/`em` from ReactMarkdown `allowedElements` in Header description — only plain text and inline links are now supported - Replace `<Container>` root with a plain `<div>` so the Header spans its full parent width - Add `HeaderMetadataStatus` component and CSS (new files) - Update docs: fixtures, snippets, props-definition, and page.mdx to reflect all changes Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
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 {
|
||||
@@ -16,10 +17,23 @@ 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',
|
||||
},
|
||||
alice: { name: 'Alice Johnson', src: 'https://i.pravatar.cc/150?u=alice42' },
|
||||
bob: { name: 'Bob Smith', src: 'https://i.pravatar.cc/150?u=bob' },
|
||||
carol: { name: 'Carol Williams', src: 'https://i.pravatar.cc/150?u=carol' },
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
@@ -43,11 +57,14 @@ const breadcrumbs = [
|
||||
const tags = [
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
];
|
||||
|
||||
const metadataUsers = [
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Status',
|
||||
value: <HeaderMetadataStatus label="Passing" color="success" />,
|
||||
},
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[users.giles]} />,
|
||||
@@ -65,7 +82,7 @@ export const WithEverything = () => (
|
||||
<Header
|
||||
title="Page Title"
|
||||
tags={tags}
|
||||
description="A short description of this page. Supports [inline links](https://backstage.io) and **bold text**."
|
||||
description="A short description of this page. Supports [inline links](https://backstage.io)."
|
||||
metadata={metadataUsers}
|
||||
tabs={tabs.slice(0, 2)}
|
||||
customActions={
|
||||
@@ -80,7 +97,24 @@ export const WithEverything = () => (
|
||||
|
||||
export const WithMetadataUsers = () => (
|
||||
<MemoryRouter>
|
||||
<Header title="Page Title" metadata={metadataUsers.slice(0, 2)} />
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -94,7 +128,7 @@ export const WithDescription = () => (
|
||||
<MemoryRouter>
|
||||
<Header
|
||||
title="Page Title"
|
||||
description="A short description of this page. Supports [inline links](https://backstage.io) and **bold text**."
|
||||
description="A short description of this page. Supports [inline links](https://backstage.io)."
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -106,7 +140,34 @@ export const WithMetadata = () => (
|
||||
metadata={[
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
{ label: 'Tier', value: 'gold' },
|
||||
]}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
WithDescription,
|
||||
WithMetadata,
|
||||
WithMetadataUsers,
|
||||
WithMetadataStatus,
|
||||
WithCustomActions,
|
||||
WithMenu,
|
||||
} from './components';
|
||||
import { headerPagePropDefs } from './props-definition';
|
||||
import { headerPagePropDefs, headerMetadataUsersPropDefs } from './props-definition';
|
||||
import {
|
||||
usage,
|
||||
defaultSnippet,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
withDescription,
|
||||
withMetadata,
|
||||
withMetadataUsers,
|
||||
withMetadataStatus,
|
||||
withCustomActions,
|
||||
withMenu,
|
||||
} from './snippets';
|
||||
@@ -53,7 +55,7 @@ Tags are rendered above the title. Each tag with an `href` renders as a link; ta
|
||||
|
||||
### Description
|
||||
|
||||
The description accepts a markdown string. Only inline elements are supported — links, bold, and italic.
|
||||
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} />
|
||||
|
||||
@@ -65,10 +67,18 @@ Key-value pairs displayed below the description.
|
||||
|
||||
### 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.
|
||||
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
|
||||
|
||||
Tabs auto-detect the active tab from the current route when `activeTabId` is omitted. Pass an explicit `activeTabId` to override, or `null` for no active tab.
|
||||
|
||||
@@ -28,7 +28,7 @@ export const headerPagePropDefs: Record<string, PropDef> = {
|
||||
description: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Markdown string rendered below the title. Only inline elements are supported: links, bold, and italic. Block-level markdown such as headings or lists is not rendered.',
|
||||
'Markdown string rendered below the title. Only inline links are supported. Bold, italic, and block-level markdown are not rendered.',
|
||||
},
|
||||
metadata: {
|
||||
type: 'complex',
|
||||
@@ -114,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,30 +2,33 @@ export const usage = `import { Header } from '@backstage/ui';
|
||||
|
||||
<Header title="Page Title" />`;
|
||||
|
||||
export const defaultSnippet = `import { Header, HeaderMetadataUsers } from '@backstage/ui';
|
||||
export const defaultSnippet = `import { Header, HeaderMetadataUsers, HeaderMetadataStatus } from '@backstage/ui';
|
||||
|
||||
<Header
|
||||
title="Page Title"
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
{ label: 'Gold' },
|
||||
]}
|
||||
description="A short description. Supports [inline links](https://backstage.io) and **bold text**."
|
||||
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: '...' }]} />,
|
||||
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[
|
||||
{ name: 'Alice Johnson', src: '...' },
|
||||
{ name: 'Bob Smith', src: '...' },
|
||||
{ name: 'Carol Williams', src: '...' },
|
||||
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
|
||||
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
|
||||
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
@@ -89,7 +92,7 @@ export const withTags = `<Header
|
||||
|
||||
export const withDescription = `<Header
|
||||
title="Page Title"
|
||||
description="A short description. Supports [inline links](https://backstage.io) and **bold text**."
|
||||
description="A short description. Supports [inline links](https://backstage.io)."
|
||||
/>`;
|
||||
|
||||
export const withMetadata = `<Header
|
||||
@@ -97,7 +100,26 @@ export const withMetadata = `<Header
|
||||
metadata={[
|
||||
{ label: 'Owner', value: 'platform-team' },
|
||||
{ label: 'Type', value: 'website' },
|
||||
{ label: 'Tier', value: 'gold' },
|
||||
]}
|
||||
/>`;
|
||||
|
||||
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" />,
|
||||
},
|
||||
]}
|
||||
/>`;
|
||||
|
||||
@@ -106,18 +128,19 @@ export const withMetadataUsers = `import { Header, HeaderMetadataUsers } from '@
|
||||
<Header
|
||||
title="Page Title"
|
||||
metadata={[
|
||||
{ label: 'Type', value: 'website' },
|
||||
{
|
||||
label: 'Owner',
|
||||
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...' }]} />,
|
||||
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
|
||||
},
|
||||
{
|
||||
label: 'Contributors',
|
||||
value: (
|
||||
<HeaderMetadataUsers
|
||||
users={[
|
||||
{ name: 'Alice Johnson', src: '...' },
|
||||
{ name: 'Bob Smith', src: '...' },
|
||||
{ name: 'Carol Williams', src: '...' },
|
||||
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
|
||||
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
|
||||
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -18,6 +18,7 @@ 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';
|
||||
@@ -27,9 +28,6 @@ import { RiMore2Line } from '@remixicon/react';
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/Header',
|
||||
component: Header,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
});
|
||||
|
||||
const tabs: HeaderNavTabItem[] = [
|
||||
@@ -158,7 +156,7 @@ export const WithDescription = meta.story({
|
||||
args: {
|
||||
...Default.input.args,
|
||||
description:
|
||||
'This is a description of the page. It can include [inline links](https://backstage.io) and **bold text**.',
|
||||
'This is a description of the page. It can include [inline links](https://backstage.io).',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -189,10 +187,23 @@ 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',
|
||||
},
|
||||
alice: { name: 'Alice Johnson', src: 'https://i.pravatar.cc/150?u=alicej' },
|
||||
bob: { name: 'Bob Smith', src: 'https://i.pravatar.cc/150?u=bob' },
|
||||
carol: { name: 'Carol Williams', src: 'https://i.pravatar.cc/150?u=carol' },
|
||||
};
|
||||
|
||||
export const WithMetadataUsers = meta.story({
|
||||
@@ -218,12 +229,72 @@ export const WithMetadataUsers = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
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) and **bold text**."
|
||||
description="This is a description of the page. It can include [inline links](https://backstage.io)."
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
@@ -257,7 +328,7 @@ export const WithEverything = meta.story({
|
||||
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) and **bold text**."
|
||||
description="This is a description of the page. It can include [inline links](https://backstage.io)."
|
||||
tags={[
|
||||
{ label: 'TypeScript' },
|
||||
{ label: 'Platform', href: '/platform' },
|
||||
|
||||
@@ -20,7 +20,6 @@ import { RiArrowRightSLine } from '@remixicon/react';
|
||||
import { HeaderNav } from './HeaderNav';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { HeaderDefinition } from './definition';
|
||||
import { Container } from '../Container';
|
||||
import { Link } from '../Link';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
@@ -45,14 +44,19 @@ export const Header = (props: HeaderProps) => {
|
||||
} = ownProps;
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<div 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-medium" standalone>
|
||||
<Link
|
||||
href={tag.href}
|
||||
variant="body-medium"
|
||||
color="secondary"
|
||||
standalone
|
||||
>
|
||||
{tag.label}
|
||||
</Link>
|
||||
) : (
|
||||
@@ -92,7 +96,7 @@ export const Header = (props: HeaderProps) => {
|
||||
{description && (
|
||||
<ReactMarkdown
|
||||
className={classes.description}
|
||||
allowedElements={['p', 'a', 'strong', 'em']}
|
||||
allowedElements={['p', 'a']}
|
||||
unwrapDisallowed
|
||||
components={{
|
||||
p: ({ children }) => (
|
||||
@@ -127,7 +131,7 @@ export const Header = (props: HeaderProps) => {
|
||||
<HeaderNav tabs={tabs} activeTabId={activeTabId} />
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,47 @@
|
||||
/*
|
||||
* 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 { HeaderMetadataStatusItem } 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,
|
||||
}: HeaderMetadataStatusItem) => {
|
||||
return (
|
||||
<div className={styles.single}>
|
||||
<span className={`${styles.dot} ${styles[`dot-${color}`]}`} />
|
||||
<Text variant="body-medium">
|
||||
{href ? (
|
||||
<Link href={href} standalone>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -30,4 +30,9 @@
|
||||
align-items: center;
|
||||
gap: var(--bui-space-1);
|
||||
}
|
||||
|
||||
.avatarLink {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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';
|
||||
|
||||
@@ -25,6 +26,7 @@ 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 overlapping avatars with the name revealed on hover via tooltip.
|
||||
* When a user has an `href`, the avatar and name become links.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -37,15 +39,34 @@ export const HeaderMetadataUsers = ({
|
||||
|
||||
if (users.length === 1) {
|
||||
const user = users[0];
|
||||
const avatar = (
|
||||
<Avatar
|
||||
src={user.src ?? ''}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className={styles.single}>
|
||||
<Avatar
|
||||
src={user.src ?? ''}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
/>
|
||||
<Text variant="body-medium">{user.name}</Text>
|
||||
{user.href ? (
|
||||
<Link
|
||||
href={user.href}
|
||||
aria-label={user.name}
|
||||
className={styles.avatarLink}
|
||||
>
|
||||
{avatar}
|
||||
</Link>
|
||||
) : (
|
||||
avatar
|
||||
)}
|
||||
{user.href ? (
|
||||
<Link href={user.href} variant="body-medium" standalone>
|
||||
{user.name}
|
||||
</Link>
|
||||
) : (
|
||||
<Text variant="body-medium">{user.name}</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -54,14 +75,29 @@ export const HeaderMetadataUsers = ({
|
||||
<div className={styles.stack}>
|
||||
{users.map(user => (
|
||||
<TooltipTrigger key={user.name}>
|
||||
<Pressable>
|
||||
<Avatar
|
||||
src={user.src ?? ''}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="informative"
|
||||
/>
|
||||
</Pressable>
|
||||
{user.href ? (
|
||||
<Link
|
||||
href={user.href}
|
||||
aria-label={user.name}
|
||||
className={styles.avatarLink}
|
||||
>
|
||||
<Avatar
|
||||
src={user.src ?? ''}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="decoration"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Pressable>
|
||||
<Avatar
|
||||
src={user.src ?? ''}
|
||||
name={user.name}
|
||||
size="small"
|
||||
purpose="informative"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Tooltip>{user.name}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
))}
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
HeaderNavGroupDefinition,
|
||||
} from './HeaderNavDefinition';
|
||||
export { HeaderMetadataUsers } from './HeaderMetadataUsers';
|
||||
export { HeaderMetadataStatus } from './HeaderMetadataStatus';
|
||||
export type {
|
||||
HeaderNavTab,
|
||||
HeaderNavTabGroup,
|
||||
@@ -31,6 +32,7 @@ export type {
|
||||
HeaderTag,
|
||||
HeaderMetadataItem,
|
||||
HeaderMetadataUser,
|
||||
HeaderMetadataStatusItem,
|
||||
HeaderPageOwnProps,
|
||||
HeaderPageProps,
|
||||
HeaderPageBreadcrumb,
|
||||
|
||||
@@ -80,6 +80,18 @@ export interface HeaderMetadataItem {
|
||||
export interface HeaderMetadataUser {
|
||||
name: string;
|
||||
src?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a status item in the HeaderMetadataStatus component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HeaderMetadataStatusItem {
|
||||
label: string;
|
||||
color: 'danger' | 'warning' | 'success' | 'info';
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +109,8 @@ export interface HeaderOwnProps {
|
||||
*/
|
||||
breadcrumbs?: HeaderBreadcrumb[];
|
||||
/**
|
||||
* Markdown string rendered below the title. Only inline elements are
|
||||
* supported (links, bold, italic). Block-level markdown is not rendered.
|
||||
* Markdown string rendered below the title. Only inline links are supported.
|
||||
* Bold, italic, and block-level markdown are not rendered.
|
||||
*/
|
||||
description?: string;
|
||||
tags?: HeaderTag[];
|
||||
|
||||
Reference in New Issue
Block a user