feat(ui): add Badge component
Adds a new `Badge` component to the Backstage UI library. Badge shares the same visual appearance as `Tag` (size tokens, colors, border radius, icon slot) but renders as a plain non-interactive `<span>` with no React Aria plumbing. Key characteristics: - Plain DOM element — accessible text content exposed to screen readers without any role override - Background consumer — participates in the bg context system and steps up neutral background levels (`neutral-2` → `neutral-3` → `neutral-4`) when placed inside colored containers - Supports `icon`, `size` (`small` | `medium`, defaults to `small`), `children`, and `className` props - Fully themeable via `BadgeDefinition` Also includes Storybook stories and full docs-ui documentation (props table, examples, theming section, changelog). 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 new `Badge` component for non-interactive labeling and categorization of content. It shares the visual appearance of `Tag` but renders as a plain DOM element with no interactive states.
|
||||
|
||||
**Affected components:** Badge
|
||||
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '../../../../../packages/ui/src/components/Badge/Badge';
|
||||
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
|
||||
import { RiBugLine } from '@remixicon/react';
|
||||
|
||||
export const Default = () => <Badge>Banana</Badge>;
|
||||
|
||||
export const WithIcon = () => <Badge icon={<RiBugLine />}>Banana</Badge>;
|
||||
|
||||
export const Sizes = () => (
|
||||
<Flex direction="row" gap="2">
|
||||
<Badge size="small">Banana</Badge>
|
||||
<Badge size="medium">Banana</Badge>
|
||||
</Flex>
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { Default, WithIcon, Sizes } from './components';
|
||||
import { badgePropDefs } from './props-definition';
|
||||
import { usage, preview, withIcons, sizes } from './snippets';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { BadgeDefinition } from '../../../utils/definitions';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
|
||||
<PageTitle
|
||||
title="Badge"
|
||||
description="A non-interactive label for annotating, categorizing, or highlighting content."
|
||||
/>
|
||||
|
||||
<Snippet align="center" py={4} preview={<Default />} code={preview} />
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={usage} />
|
||||
|
||||
## API reference
|
||||
|
||||
### Badge
|
||||
|
||||
<PropsTable data={badgePropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### With icons
|
||||
|
||||
<Snippet align="center" py={4} open preview={<WithIcon />} code={withIcons} />
|
||||
|
||||
### Sizes
|
||||
|
||||
<Snippet align="center" py={4} open preview={<Sizes />} code={sizes} />
|
||||
|
||||
<Theming definition={BadgeDefinition} />
|
||||
|
||||
<ChangelogComponent component={['badge']} />
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
childrenPropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
import { Chip } from '@/components/Chip';
|
||||
|
||||
export const badgePropDefs: Record<string, PropDef> = {
|
||||
icon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Icon displayed before the badge text.',
|
||||
},
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
default: 'small',
|
||||
description: (
|
||||
<>
|
||||
Visual size of the badge. Use <Chip>small</Chip> for inline or dense
|
||||
layouts, <Chip>medium</Chip> for standalone badges.
|
||||
</>
|
||||
),
|
||||
},
|
||||
...childrenPropDefs,
|
||||
...classNamePropDefs,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export const usage = `import { Badge } from '@backstage/ui';
|
||||
|
||||
<Badge>Badge</Badge>`;
|
||||
|
||||
export const preview = `<Badge>Banana</Badge>`;
|
||||
|
||||
export const withIcons = `<Badge icon={<RiBugLine />}>Banana</Badge>`;
|
||||
|
||||
export const sizes = `<Flex direction="row" gap="2">
|
||||
<Badge size="small">Banana</Badge>
|
||||
<Badge size="medium">Banana</Badge>
|
||||
</Flex>`;
|
||||
@@ -17,6 +17,10 @@ export const components: Page[] = [
|
||||
title: 'Avatar',
|
||||
slug: 'avatar',
|
||||
},
|
||||
{
|
||||
title: 'Badge',
|
||||
slug: 'badge',
|
||||
},
|
||||
{
|
||||
title: 'Box',
|
||||
slug: 'box',
|
||||
|
||||
@@ -318,6 +318,45 @@ export interface AvatarProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children' | 'className'>,
|
||||
AvatarOwnProps {}
|
||||
|
||||
// @public
|
||||
export const Badge: ForwardRefExoticComponent<
|
||||
BadgeProps & RefAttributes<HTMLSpanElement>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const BadgeDefinition: {
|
||||
readonly styles: {
|
||||
readonly [key: string]: string;
|
||||
};
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Badge';
|
||||
readonly icon: 'bui-BadgeIcon';
|
||||
};
|
||||
readonly bg: 'consumer';
|
||||
readonly propDefs: {
|
||||
readonly icon: {};
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'small';
|
||||
};
|
||||
readonly children: {};
|
||||
readonly className: {};
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type BadgeOwnProps = {
|
||||
icon?: React.ReactNode;
|
||||
size?: 'small' | 'medium';
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface BadgeProps
|
||||
extends BadgeOwnProps,
|
||||
Omit<React.HTMLAttributes<HTMLSpanElement>, keyof BadgeOwnProps> {}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BgContextValue {
|
||||
// (undocumented)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.bui-Badge {
|
||||
color: var(--bui-fg-primary);
|
||||
background-color: var(--bui-bg-neutral-1);
|
||||
border-radius: var(--bui-radius-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
gap: var(--bui-space-1);
|
||||
|
||||
&[data-on-bg='neutral-1'] {
|
||||
background-color: var(--bui-bg-neutral-2);
|
||||
}
|
||||
|
||||
&[data-on-bg='neutral-2'] {
|
||||
background-color: var(--bui-bg-neutral-3);
|
||||
}
|
||||
|
||||
&[data-on-bg='neutral-3'] {
|
||||
background-color: var(--bui-bg-neutral-4);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Badge[data-size='small'] {
|
||||
height: 26px;
|
||||
padding: 0 var(--bui-space-2);
|
||||
font-size: var(--bui-font-size-1);
|
||||
}
|
||||
|
||||
.bui-Badge[data-size='medium'] {
|
||||
height: 32px;
|
||||
padding: 0 var(--bui-space-2);
|
||||
font-size: var(--bui-font-size-2);
|
||||
}
|
||||
|
||||
.bui-BadgeIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import { Badge } from '.';
|
||||
import { Flex } from '../../';
|
||||
import { BUIProvider } from '../../provider';
|
||||
import { RiBugLine } from '@remixicon/react';
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/Badge',
|
||||
component: Badge,
|
||||
decorators: [
|
||||
Story => (
|
||||
<BUIProvider>
|
||||
<Story />
|
||||
</BUIProvider>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
children: 'Banana',
|
||||
},
|
||||
});
|
||||
|
||||
export const Sizes = meta.story({
|
||||
render: () => (
|
||||
<Flex direction="row" gap="2">
|
||||
<Badge size="small">Banana</Badge>
|
||||
<Badge size="medium">Banana</Badge>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithIcon = meta.story({
|
||||
render: () => (
|
||||
<Flex direction="row" gap="2">
|
||||
<Badge size="small" icon={<RiBugLine />}>
|
||||
Banana
|
||||
</Badge>
|
||||
<Badge size="medium" icon={<RiBugLine />}>
|
||||
Banana
|
||||
</Badge>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BadgeProps } from './types';
|
||||
import { forwardRef } from 'react';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { BadgeDefinition } from './definition';
|
||||
|
||||
/**
|
||||
* A non-interactive badge for labeling or categorizing content.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props, ref) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
BadgeDefinition,
|
||||
props,
|
||||
);
|
||||
const { classes, children, icon } = ownProps;
|
||||
|
||||
return (
|
||||
<span ref={ref} className={classes.root} {...dataAttributes} {...restProps}>
|
||||
{icon && <span className={classes.icon}>{icon}</span>}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineComponent } from '../../hooks/useDefinition';
|
||||
import type { BadgeOwnProps } from './types';
|
||||
import styles from './Badge.module.css';
|
||||
|
||||
/**
|
||||
* Component definition for Badge
|
||||
* @public
|
||||
*/
|
||||
export const BadgeDefinition = defineComponent<BadgeOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-Badge',
|
||||
icon: 'bui-BadgeIcon',
|
||||
},
|
||||
bg: 'consumer',
|
||||
propDefs: {
|
||||
icon: {},
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
children: {},
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Badge } from './Badge';
|
||||
export type { BadgeProps, BadgeOwnProps } from './types';
|
||||
export { BadgeDefinition } from './definition';
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Own props for the Badge component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BadgeOwnProps = {
|
||||
/**
|
||||
* The icon to display before the badge text.
|
||||
*/
|
||||
icon?: React.ReactNode;
|
||||
/**
|
||||
* The size of the badge.
|
||||
*/
|
||||
size?: 'small' | 'medium';
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the Badge component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BadgeProps
|
||||
extends BadgeOwnProps,
|
||||
Omit<React.HTMLAttributes<HTMLSpanElement>, keyof BadgeOwnProps> {}
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
} from './components/Accordion/definition';
|
||||
export { AlertDefinition } from './components/Alert/definition';
|
||||
export { AvatarDefinition } from './components/Avatar/definition';
|
||||
export { BadgeDefinition } from './components/Badge/definition';
|
||||
export { BoxDefinition } from './components/Box/definition';
|
||||
export { ButtonDefinition } from './components/Button/definition';
|
||||
export { ButtonIconDefinition } from './components/ButtonIcon/definition';
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from './components/FullPage';
|
||||
export * from './components/Accordion';
|
||||
export * from './components/Alert';
|
||||
export * from './components/Avatar';
|
||||
export * from './components/Badge';
|
||||
export * from './components/Button';
|
||||
export * from './components/Card';
|
||||
export * from './components/Dialog';
|
||||
|
||||
Reference in New Issue
Block a user