Skip to content

Commit 5eaea25

Browse files
committed
feat: add badge component
1 parent 4d93309 commit 5eaea25

4 files changed

Lines changed: 205 additions & 0 deletions

File tree

‎webui/src/Components/Badge.css‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* A small pill: uppercase label, tight padding, sized to sit inline with body text.
2+
Each colour declares both fills; `.badge-tonal` picks the tinted pair instead of the solid one. */
3+
.badge-element {
4+
display: inline-flex;
5+
align-items: center;
6+
gap: 0.25rem;
7+
padding: 0.1rem 0.4rem;
8+
border-radius: 0.5rem;
9+
background-color: var(--badge-solid-bg);
10+
color: var(--badge-solid-fg);
11+
font-size: 0.7rem;
12+
font-weight: bold;
13+
line-height: 1.3;
14+
text-transform: uppercase;
15+
white-space: nowrap;
16+
vertical-align: middle;
17+
18+
&.badge-tonal {
19+
background-color: var(--badge-tonal-bg);
20+
color: var(--badge-tonal-fg);
21+
}
22+
23+
&.badge-primary {
24+
--badge-solid-bg: var(--color-primary);
25+
--badge-solid-fg: var(--color-on-dark);
26+
--badge-tonal-bg: var(--color-primary-bg-subtle);
27+
--badge-tonal-fg: var(--color-primary-text-emphasis);
28+
}
29+
30+
&.badge-secondary {
31+
--badge-solid-bg: var(--color-secondary);
32+
--badge-solid-fg: var(--color-text);
33+
--badge-tonal-bg: var(--color-secondary-bg-subtle);
34+
--badge-tonal-fg: var(--color-secondary-text-emphasis);
35+
}
36+
37+
&.badge-success {
38+
--badge-solid-bg: var(--color-success);
39+
--badge-solid-fg: var(--color-on-dark);
40+
--badge-tonal-bg: var(--color-success-bg-subtle);
41+
--badge-tonal-fg: var(--color-success-text-emphasis);
42+
}
43+
44+
&.badge-danger {
45+
--badge-solid-bg: var(--color-danger);
46+
--badge-solid-fg: var(--color-on-dark);
47+
--badge-tonal-bg: var(--color-danger-bg-subtle);
48+
--badge-tonal-fg: var(--color-danger-text-emphasis);
49+
}
50+
51+
/* Amber is too light to carry white text, so the solid variant keeps the body colour */
52+
&.badge-warning {
53+
--badge-solid-bg: var(--color-warning);
54+
--badge-solid-fg: var(--color-text);
55+
--badge-tonal-bg: var(--color-warning-bg-subtle);
56+
--badge-tonal-fg: var(--color-warning-text-emphasis);
57+
}
58+
59+
&.badge-info {
60+
--badge-solid-bg: var(--color-info);
61+
--badge-solid-fg: var(--color-on-dark);
62+
--badge-tonal-bg: var(--color-info-bg-subtle);
63+
--badge-tonal-fg: var(--color-info-text-emphasis);
64+
}
65+
66+
&.badge-dark {
67+
--badge-solid-bg: var(--color-dark);
68+
--badge-solid-fg: var(--color-on-dark);
69+
--badge-tonal-bg: var(--color-dark-bg-subtle);
70+
--badge-tonal-fg: var(--color-dark-text-emphasis);
71+
}
72+
73+
&.badge-light {
74+
--badge-solid-bg: var(--color-light);
75+
--badge-solid-fg: var(--color-text);
76+
--badge-tonal-bg: var(--color-light-bg-subtle);
77+
--badge-tonal-fg: var(--color-light-text-emphasis);
78+
}
79+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { Badge } from './Badge'
3+
4+
const meta = {
5+
component: Badge,
6+
decorators: [
7+
(Story) => (
8+
<div style={{ padding: 24, background: 'var(--color-surface)' }}>
9+
<Story />
10+
</div>
11+
),
12+
],
13+
} satisfies Meta<typeof Badge>
14+
15+
export default meta
16+
type Story = StoryObj<typeof meta>
17+
18+
export const Solid: Story = {
19+
args: { color: 'warning', children: 'Deprecated' },
20+
}
21+
22+
export const Tonal: Story = {
23+
args: { color: 'warning', variant: 'tonal', children: 'Deprecated' },
24+
}
25+
26+
const COLORS = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'] as const
27+
28+
export const AllColors: Story = {
29+
args: { color: 'primary', children: 'Badge' },
30+
render: () => (
31+
<div className="flex flex-col gap-2">
32+
{COLORS.map((color) => (
33+
<div key={color} className="flex items-center gap-2">
34+
<Badge color={color}>{color}</Badge>
35+
<Badge color={color} variant="tonal">
36+
{color}
37+
</Badge>
38+
</div>
39+
))}
40+
</div>
41+
),
42+
}
43+
44+
export const InlineWithText: Story = {
45+
args: { color: 'warning', children: 'Deprecated' },
46+
render: (args) => (
47+
<p>
48+
Generic Module <Badge {...args} /> is still installed.
49+
</p>
50+
),
51+
}

‎webui/src/Components/Badge.tsx‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import classNames from 'classnames'
2+
import './Badge.css'
3+
import { forwardRef, type HTMLAttributes } from 'react'
4+
5+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
6+
/**
7+
* A string of all className you want applied to the component.
8+
*/
9+
className?: string
10+
/**
11+
* Sets the colour context of the component to one of the themed colours.
12+
*/
13+
color: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light'
14+
/**
15+
* Set the badge variant. `solid` is a filled pill, `tonal` a tinted pill with matching text.
16+
*/
17+
variant?: 'solid' | 'tonal'
18+
}
19+
20+
/**
21+
* A small pill labelling the thing it sits beside - a status, a flag, a count.
22+
* Keep the label to a word or two; anything longer belongs in an `Alert` or a tooltip.
23+
*/
24+
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
25+
({ children, className, color, variant = 'solid', ...rest }, ref) => {
26+
return (
27+
<span
28+
className={classNames('badge-element', `badge-${color}`, { 'badge-tonal': variant === 'tonal' }, className)}
29+
{...rest}
30+
ref={ref}
31+
>
32+
{children}
33+
</span>
34+
)
35+
}
36+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { render, screen } from '@testing-library/react'
2+
import { describe, expect, it } from 'vitest'
3+
import { Badge } from '../Badge.js'
4+
5+
describe('Badge', () => {
6+
it('renders its label', () => {
7+
render(<Badge color="warning">Deprecated</Badge>)
8+
9+
expect(screen.getByText('Deprecated')).toBeInTheDocument()
10+
})
11+
12+
it('carries the colour class, and the solid fill by default', () => {
13+
render(<Badge color="danger">Broken</Badge>)
14+
15+
const badge = screen.getByText('Broken')
16+
expect(badge).toHaveClass('badge-element', 'badge-danger')
17+
expect(badge).not.toHaveClass('badge-tonal')
18+
})
19+
20+
it('marks the tonal variant so it picks the tinted fill', () => {
21+
render(
22+
<Badge color="danger" variant="tonal">
23+
Broken
24+
</Badge>
25+
)
26+
27+
expect(screen.getByText('Broken')).toHaveClass('badge-tonal')
28+
})
29+
30+
it('keeps any className passed by the caller', () => {
31+
render(
32+
<Badge color="info" className="me-1">
33+
Beta
34+
</Badge>
35+
)
36+
37+
expect(screen.getByText('Beta')).toHaveClass('me-1', 'badge-info')
38+
})
39+
})

0 commit comments

Comments
 (0)