Improve styles

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-08-18 11:20:03 +02:00
parent f76130684a
commit ac5369d6c1
11 changed files with 381 additions and 11 deletions
+4
View File
@@ -209,6 +209,10 @@
.bui-Input {
border-radius: var(--bui-radius-3);
}
.bui-Tag {
border-radius: var(--bui-radius-full);
}
}
[data-theme-mode='light'][data-theme-name='spotify'] {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { TagGroupSnippet } from '@/snippets/stories-snippets';
import {
tagGroupPropDefs,
tagPropDefs,
usage,
preview,
withLink,
disabled,
withIcons,
sizes,
removingTags,
} from './tag-group.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="TagGroup"
description="A tag group is a list of labels, categories, keywords, filters, or other items, that can be used to group and filter content."
/>
<Snippet
align="center"
py={4}
preview={<TagGroupSnippet story="Default" />}
code={preview}
/>
## Usage
<CodeBlock code={usage} />
## API reference
### TagGroup
A tag group is a list of tags.
<PropsTable data={tagGroupPropDefs} />
### Tag
A tag is a single item in a tag group.
<PropsTable data={tagPropDefs} />
## Examples
### With Links
A tag can be a link by passing a `href` prop.
<Snippet
align="center"
py={4}
open
preview={<TagGroupSnippet story="WithLink" />}
code={withLink}
/>
### With Icons
A tag can have an icon by passing a `icon` prop.
<Snippet
align="center"
py={4}
open
preview={<TagGroupSnippet story="WithIcon" />}
code={withIcons}
/>
### Sizes
A tag can have a size by passing a `size` prop. It could be `small` or `medium`.
<Snippet
align="center"
py={4}
open
preview={<TagGroupSnippet story="Sizes" />}
code={sizes}
/>
### Removing tags
A tag can be removed by passing a `onRemove` prop.
<Snippet
align="center"
py={4}
open
preview={<TagGroupSnippet story="RemovingTags" />}
code={removingTags}
/>
### Disabled
A switch can be disabled using the `isDisabled` prop.
<Snippet
align="center"
py={4}
open
preview={<TagGroupSnippet story="Disabled" />}
code={disabled}
/>
<Theming component="Switch" />
<ChangelogComponent component="switch" />
@@ -0,0 +1,161 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const tagGroupPropDefs: Record<string, PropDef> = {
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'A list of row keys to disable.',
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
description: 'The type of selection that is allowed in the collection.',
},
selectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The currently selected keys in the collection (controlled).',
},
defaultSelectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The initial selected keys in the collection (uncontrolled).',
},
onRemove: {
type: 'enum',
values: ['(keys: Set<Key>) => void'],
description: 'Handler that is called when a tag is removed.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler that is called when the selection changes.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tagPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
description: 'The id of the tag.',
},
textValue: {
type: 'string',
description: 'The text value of the tag.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the tag is disabled.',
},
href: {
type: 'string',
description: 'The href of the tag.',
},
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'The icon of the tag.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const preview = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag>Banana</Tag>
<Tag>Apple</Tag>
<Tag>Orange</Tag>
<Tag>Pear</Tag>
<Tag>Grape</Tag>
<Tag>Pineapple</Tag>
<Tag>Strawberry</Tag>
</TagGroup>
`;
export const usage = `import { TagGroup, Tag } from '@backstage/ui';
// Basic usage
<TagGroup aria-label="Tag Group">
<Tag>Tag 1</Tag>
<Tag>Tag 2</Tag>
<Tag>Tag 3</Tag>
</TagGroup>
// With the items prop
const list = [
{ id: '1', name: 'Tag 1' },
{ id: '2', name: 'Tag 2' },
{ id: '3', name: 'Tag 3' },
];
<TagGroup aria-label="Tag Group" items={list}>
{item => <Tag>{item.name}</Tag>}
</TagGroup>`;
export const withLink = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag href="/items/banana">Banana</Tag>
<Tag href="/items/apple">Apple</Tag>
<Tag href="/items/orange">Orange</Tag>
...
</TagGroup>`;
export const disabled = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag>Banana</Tag>
<Tag isDisabled>Apple</Tag>
<Tag isDisabled>Orange</Tag>
<Tag>Pear</Tag>
...
</TagGroup>`;
export const withIcons = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag icon={<RiBugLine />}>Banana</Tag>
<Tag icon={<RiAccountCircleLine />}>Apple</Tag>
<Tag icon={<RiEyeLine />}>Orange</Tag>
...
</TagGroup>`;
export const sizes = `import { TagGroup, Tag, Flex } from '@backstage/ui';
<Flex direction="column">
<TagGroup aria-label="Tag Group">
<Tag size="small">Banana</Tag>
<Tag size="small">Apple</Tag>
<Tag size="small">Orange</Tag>
...
</TagGroup>
<TagGroup aria-label="Tag Group">
<Tag size="medium">Banana</Tag>
<Tag size="medium">Apple</Tag>
<Tag size="medium">Orange</Tag>
...
</TagGroup>
</Flex>`;
export const removingTags = `import { TagGroup, Tag } from '@backstage/ui';
import type { Selection } from 'react-aria-components';
import { useListData } from 'react-stately';
const [selected, setSelected] = useState<Selection>(new Set(['travel']));
const list = useListData({
initialItems: initialList,
});
<TagGroup
items={list.items}
onRemove={keys => list.remove(...keys)}
selectedKeys={selected}
onSelectionChange={setSelected}
{...args}
>
{item => <Tag>{item.name}</Tag>}
</TagGroup>`;
@@ -27,6 +27,7 @@ import * as CardStories from '../../../packages/ui/src/components/Card/Card.stor
import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories';
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
// Helper function to create snippet components
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -69,3 +70,4 @@ export const CardSnippet = createSnippetComponent(CardStories);
export const HeaderSnippet = createSnippetComponent(HeaderStories);
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
export const TableSnippet = createSnippetComponent(TableStories);
export const TagGroupSnippet = createSnippetComponent(TagGroupStories);
+6 -1
View File
@@ -154,13 +154,18 @@ export const components: Page[] = [
{
title: 'Table',
slug: 'table',
status: 'inProgress',
status: 'alpha',
},
{
title: 'Tabs',
slug: 'tabs',
status: 'alpha',
},
{
title: 'TagGroup',
slug: 'tag-group',
status: 'alpha',
},
{
title: 'Text',
slug: 'text',
+6 -1
View File
@@ -10269,7 +10269,7 @@
.bui-Tag {
color: var(--bui-fg-primary);
background-color: var(--bui-gray-2);
border-radius: var(--bui-radius-full);
border-radius: var(--bui-radius-2);
font-weight: var(--bui-font-weight-regular);
justify-content: center;
align-items: center;
@@ -10307,6 +10307,11 @@
box-shadow: inset 0 0 0 1px var(--bui-gray-8);
}
.bui-Tag[data-disabled] {
color: var(--bui-fg-disabled);
cursor: not-allowed;
}
.bui-TagRemoveButton {
cursor: pointer;
color: var(--bui-fg-primary);
@@ -18,13 +18,15 @@ import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { TagGroup, Tag } from '.';
import type { Selection } from 'react-aria-components';
import { Icon, IconNames } from '@backstage/ui';
import { Flex, Icon, IconNames } from '../../';
import { useListData } from 'react-stately';
import { MemoryRouter } from 'react-router-dom';
export interface ListItem {
id: string;
name: string;
icon: IconNames;
isDisabled?: boolean;
}
const meta = {
@@ -39,6 +41,13 @@ const meta = {
control: { type: 'text' },
},
},
decorators: [
Story => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
} satisfies Meta<typeof TagGroup<ListItem>>;
export default meta;
@@ -46,8 +55,8 @@ type Story = StoryObj<typeof meta>;
const initialList: ListItem[] = [
{ id: 'banana', name: 'Banana', icon: 'bug' },
{ id: 'apple', name: 'Apple', icon: 'account-circle' },
{ id: 'orange', name: 'Orange', icon: 'eye' },
{ id: 'apple', name: 'Apple', icon: 'account-circle', isDisabled: true },
{ id: 'orange', name: 'Orange', icon: 'eye', isDisabled: true },
{ id: 'pear', name: 'Pear', icon: 'heart' },
{ id: 'grape', name: 'Grape', icon: 'bug' },
{ id: 'pineapple', name: 'Pineapple', icon: 'eye' },
@@ -67,6 +76,30 @@ export const Default: Story = {
),
};
export const Sizes: Story = {
args: {
...Default.args,
},
render: args => (
<Flex direction="column">
<TagGroup {...args}>
{initialList.map(item => (
<Tag key={item.id} size="small" icon={<Icon name={item.icon} />}>
{item.name}
</Tag>
))}
</TagGroup>
<TagGroup {...args}>
{initialList.map(item => (
<Tag key={item.id} size="medium" icon={<Icon name={item.icon} />}>
{item.name}
</Tag>
))}
</TagGroup>
</Flex>
),
};
export const SelectionModeSingle: Story = {
args: {
selectionMode: 'single',
@@ -129,7 +162,31 @@ export const WithIcon: Story = {
),
};
export const WithRemoveButton: Story = {
export const WithLink: Story = {
render: args => (
<TagGroup {...args}>
{initialList.map(item => (
<Tag key={item.id} href={`/items/${item.id}`}>
{item.name}
</Tag>
))}
</TagGroup>
),
};
export const Disabled: Story = {
render: args => (
<TagGroup {...args}>
{initialList.map(item => (
<Tag key={item.id} isDisabled={item.isDisabled}>
{item.name}
</Tag>
))}
</TagGroup>
),
};
export const RemovingTags: Story = {
args: {
...Default.args,
},
@@ -23,7 +23,7 @@
.bui-Tag {
color: var(--bui-fg-primary);
background-color: var(--bui-gray-2);
border-radius: var(--bui-radius-full);
border-radius: var(--bui-radius-2);
display: flex;
align-items: center;
justify-content: center;
@@ -61,6 +61,11 @@
box-shadow: inset 0 0 0 1px var(--bui-gray-8);
}
.bui-Tag[data-disabled] {
color: var(--bui-fg-disabled);
cursor: not-allowed;
}
.bui-TagRemoveButton {
background-color: transparent;
border: none;
@@ -20,11 +20,14 @@ import {
TagList as ReactAriaTagList,
Tag as ReactAriaTag,
Button as ReactAriaButton,
RouterProvider,
} from 'react-aria-components';
import type { ReactNode } from 'react';
import { RiCloseCircleLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { isExternalLink } from '../../utils/isExternalLink';
import { useNavigate, useHref } from 'react-router-dom';
/**
* A component that renders a list of tags.
@@ -57,15 +60,19 @@ export const TagGroup = <T extends object>({
* @public
*/
export const Tag = (props: TagProps) => {
const { children, className, icon, size = 'small', ...rest } = props;
const { children, className, icon, size = 'small', href, ...rest } = props;
const textValue = typeof children === 'string' ? children : undefined;
const { classNames } = useStyles('TagGroup');
const navigate = useNavigate();
const isLink = href !== undefined;
const isExternal = isExternalLink(href);
return (
const content = (
<ReactAriaTag
textValue={textValue}
className={clsx(classNames.tag, className)}
data-size={size}
href={href}
{...rest}
>
{({ allowsRemoving }) => (
@@ -84,4 +91,14 @@ export const Tag = (props: TagProps) => {
)}
</ReactAriaTag>
);
if (isLink && !isExternal) {
return (
<RouterProvider navigate={navigate} useHref={useHref}>
{content}
</RouterProvider>
);
}
return content;
};