uikit-react-public 0.24.4 → 0.24.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,21 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
+ import { css } from '@emotion/css';
2
3
  import Badge from './Badge';
4
+ import { type BadgeVariant } from './Badge';
5
+
6
+ const VARIANTS: BadgeVariant[] = [
7
+ 'neutral',
8
+ 'neutral-subtle',
9
+ 'critical',
10
+ 'critical-subtle',
11
+ 'caution',
12
+ 'caution-subtle',
13
+ 'success',
14
+ 'success-subtle',
15
+ 'warning',
16
+ 'warning-subtle',
17
+ 'disabled',
18
+ ];
3
19
 
4
20
  const meta = {
5
21
  title: 'Components/Badge',
@@ -7,8 +23,25 @@ const meta = {
7
23
  parameters: {
8
24
  layout: 'centered',
9
25
  },
26
+ argTypes: {
27
+ variant: {
28
+ options: VARIANTS,
29
+ control: { type: 'select' },
30
+ },
31
+ size: {
32
+ options: ['medium', 'large'],
33
+ control: { type: 'radio' },
34
+ },
35
+ icon: { control: { type: 'boolean' } },
36
+ statusIndicator: { control: { type: 'boolean' } },
37
+ testId: { control: { type: 'text' } },
38
+ className: { control: false },
39
+ children: { control: { type: 'text' } },
40
+ },
10
41
  args: {
11
42
  children: 'Badge',
43
+ variant: 'neutral',
44
+ size: 'medium',
12
45
  },
13
46
  tags: ['autodocs'],
14
47
  } satisfies Meta<typeof Badge>;
@@ -17,3 +50,83 @@ export default meta;
17
50
  type Story = StoryObj<typeof meta>;
18
51
 
19
52
  export const Default: Story = {};
53
+
54
+ export const Large: Story = {
55
+ args: { size: 'large' },
56
+ };
57
+
58
+ export const WithIcon: Story = {
59
+ args: { variant: 'success', icon: true },
60
+ };
61
+
62
+ export const WithStatusIndicator: Story = {
63
+ args: { statusIndicator: true },
64
+ };
65
+
66
+ const rowStyle = css`
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 8px;
70
+ margin-bottom: 8px;
71
+ `;
72
+
73
+ const labelStyle = css`
74
+ font-family: sans-serif;
75
+ font-size: 12px;
76
+ width: 140px;
77
+ flex-shrink: 0;
78
+ `;
79
+
80
+ export const AllVariants: Story = {
81
+ name: 'All variants',
82
+ parameters: { layout: 'padded' },
83
+ render: () => (
84
+ <div>
85
+ {VARIANTS.map((variant) => (
86
+ <div
87
+ key={variant}
88
+ className={rowStyle}
89
+ >
90
+ <span className={labelStyle}>{variant}</span>
91
+ <Badge variant={variant}>{variant}</Badge>
92
+ <Badge
93
+ variant={variant}
94
+ icon
95
+ >
96
+ {variant}
97
+ </Badge>
98
+ <Badge
99
+ variant={variant}
100
+ statusIndicator
101
+ >
102
+ {variant}
103
+ </Badge>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ ),
108
+ };
109
+
110
+ export const Sizes: Story = {
111
+ parameters: { layout: 'padded' },
112
+ render: () => (
113
+ <div className={rowStyle}>
114
+ <Badge size='medium'>Medium</Badge>
115
+ <Badge size='large'>Large</Badge>
116
+ <Badge
117
+ size='medium'
118
+ icon
119
+ variant='success'
120
+ >
121
+ Medium icon
122
+ </Badge>
123
+ <Badge
124
+ size='large'
125
+ icon
126
+ variant='success'
127
+ >
128
+ Large icon
129
+ </Badge>
130
+ </div>
131
+ ),
132
+ };
@@ -1,47 +1,206 @@
1
1
  import { css, cx } from '@emotion/css';
2
2
  import { useTheme } from '../../theme';
3
3
 
4
- export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ import { Icon } from '../';
5
+ import type { SpecificIconProps } from '../Icon/Icon';
6
+ import type { ThemeType } from '../../theme';
7
+
8
+ export type BadgeVariant =
9
+ | 'neutral'
10
+ | 'neutral-subtle'
11
+ | 'critical'
12
+ | 'critical-subtle'
13
+ | 'caution'
14
+ | 'caution-subtle'
15
+ | 'success'
16
+ | 'success-subtle'
17
+ | 'warning'
18
+ | 'warning-subtle'
19
+ | 'disabled';
20
+
21
+ export const VARIANT_ICON_MAP = {
22
+ neutral: Icon.Info,
23
+ 'neutral-subtle': Icon.Info,
24
+ critical: Icon.XCircle,
25
+ 'critical-subtle': Icon.XCircle,
26
+ caution: Icon.AlertCircle,
27
+ 'caution-subtle': Icon.AlertCircle,
28
+ success: Icon.CheckCircle,
29
+ 'success-subtle': Icon.CheckCircle,
30
+ warning: Icon.AlertTriangle,
31
+ 'warning-subtle': Icon.AlertTriangle,
32
+ disabled: null,
33
+ } satisfies Record<BadgeVariant, React.ComponentType<SpecificIconProps> | null>;
34
+
35
+ export interface BadgeVariantStyle {
36
+ color: string;
37
+ backgroundColor: string;
38
+ border?: string;
39
+ }
40
+
41
+ export const getBadgeVariantColours = (
42
+ theme: ThemeType
43
+ ): Record<BadgeVariant, BadgeVariantStyle> => ({
44
+ neutral: {
45
+ color: theme.colour.text.default,
46
+ backgroundColor: 'transparent',
47
+ border: `1px solid ${theme.colour.border.primary}`,
48
+ },
49
+ 'neutral-subtle': {
50
+ color: theme.colour.text.default,
51
+ backgroundColor: theme.colour.surface.secondary,
52
+ },
53
+ critical: {
54
+ color: theme.colour.text.inverse,
55
+ backgroundColor: theme.colour.fill.critical,
56
+ },
57
+ 'critical-subtle': {
58
+ color: theme.colour.text.critical,
59
+ backgroundColor: theme.primitiveColour.red['02'].$value.hex,
60
+ },
61
+ caution: {
62
+ color: theme.primitiveColour.yellow['14'].$value.hex,
63
+ backgroundColor: theme.primitiveColour.yellow['08'].$value.hex,
64
+ },
65
+ 'caution-subtle': {
66
+ color: theme.primitiveColour.yellow['12'].$value.hex,
67
+ backgroundColor: theme.primitiveColour.yellow['05'].$value.hex,
68
+ },
69
+ success: {
70
+ color: theme.colour.text.inverse,
71
+ backgroundColor: theme.colour.fill.success,
72
+ },
73
+ 'success-subtle': {
74
+ color: theme.primitiveColour.green['11'].$value.hex,
75
+ backgroundColor: theme.primitiveColour.green['02'].$value.hex,
76
+ },
77
+ warning: {
78
+ color: theme.colour.text.inverse,
79
+ backgroundColor: theme.primitiveColour.orange['11'].$value.hex,
80
+ },
81
+ 'warning-subtle': {
82
+ color: theme.primitiveColour.orange['13'].$value.hex,
83
+ backgroundColor: theme.primitiveColour.orange['03'].$value.hex,
84
+ },
85
+ disabled: {
86
+ color: theme.colour.text.disabled,
87
+ backgroundColor: theme.colour.fill.disabled,
88
+ },
89
+ });
90
+
91
+ export type BadgeIndicator =
92
+ | { statusIndicator?: boolean; icon?: never }
93
+ | { icon?: boolean; statusIndicator?: never };
94
+
95
+ export interface BadgeBaseProps extends React.HTMLAttributes<HTMLSpanElement> {
5
96
  testId?: string;
6
97
  className?: string;
98
+ variant?: BadgeVariant;
99
+ size?: 'medium' | 'large';
7
100
  }
8
101
 
102
+ export type BadgeProps = BadgeBaseProps & BadgeIndicator;
103
+
9
104
  const NAME = 'ucl-uikit-badge';
10
105
 
11
106
  const Badge = ({
12
107
  testId = NAME,
13
108
  className,
109
+ variant = 'neutral',
110
+ size = 'medium',
111
+ statusIndicator,
112
+ icon,
14
113
  children,
15
114
  ...props
16
115
  }: BadgeProps) => {
17
116
  const [theme] = useTheme();
18
117
 
118
+ const badgeTypography =
119
+ size === 'large' ? theme.typography.body.sm : theme.typography.body.xs;
120
+
121
+ const iconSize = size === 'large' ? 16 : 14;
122
+
123
+ const variantColours = getBadgeVariantColours(theme);
124
+
125
+ const { color, backgroundColor, border } = variantColours[variant];
126
+
19
127
  const baseStyle = css`
20
128
  display: inline-flex;
21
129
  align-items: center;
22
130
  justify-content: center;
23
131
  box-sizing: border-box;
24
- height: 24px;
25
- padding: ${theme.padding.p8};
26
- color: ${theme.color.text.secondary};
27
- background-color: #e4e4e4; // TODO: Add design token
28
- font-family: ${theme.font.family.primary};
29
- font-size: ${theme.font.size.f14};
30
- font-weight: ${theme.font.weight.regular};
31
- border-radius: ${theme.radius.r4};
132
+ height: ${size === 'large' ? '26px' : '22px'};
133
+ padding: 0 ${theme.padding.p8};
134
+
135
+ color: ${color};
136
+ background-color: ${backgroundColor};
137
+ border: ${border ?? 'none'};
138
+
139
+ font-family: ${badgeTypography.fontFamily};
140
+ font-feature-settings: ${badgeTypography.fontSettings};
141
+ font-size: ${badgeTypography.fontSize}px;
142
+ font-weight: ${badgeTypography.fontWeight};
143
+ line-height: ${badgeTypography.lineHeight}%;
144
+
145
+ border-radius: 100px;
32
146
  white-space: nowrap;
147
+ gap: 4px;
148
+ vertical-align: middle;
149
+ `;
150
+
151
+ const style = cx(NAME, baseStyle, className);
152
+
153
+ const dotBaseStyle = css`
154
+ display: block;
155
+ border-radius: 50%;
156
+ background-color: currentColor;
157
+ flex-shrink: 0;
158
+ `;
159
+
160
+ const dotStyle = cx(
161
+ dotBaseStyle,
162
+ size === 'medium' &&
163
+ css`
164
+ width: 7px;
165
+ height: 7px;
166
+ margin: 0 3.5px;
167
+ `,
168
+ size === 'large' &&
169
+ css`
170
+ width: 8px;
171
+ height: 8px;
172
+ margin: 0 4px;
173
+ `
174
+ );
175
+
176
+ const iconStyle = css`
177
+ display: block;
178
+ flex-shrink: 0;
33
179
  `;
34
180
 
35
- const style = cx(baseStyle, NAME, className);
181
+ const VariantIcon = VARIANT_ICON_MAP[variant];
36
182
 
37
183
  return (
38
- <div
184
+ <span
39
185
  data-testid={testId}
40
186
  className={style}
41
187
  {...props}
42
188
  >
189
+ {statusIndicator && (
190
+ <span
191
+ className={dotStyle}
192
+ aria-hidden='true'
193
+ />
194
+ )}
195
+ {icon && VariantIcon && (
196
+ <VariantIcon
197
+ size={iconSize}
198
+ className={iconStyle}
199
+ aria-hidden='true'
200
+ />
201
+ )}
43
202
  {children}
44
- </div>
203
+ </span>
45
204
  );
46
205
  };
47
206
 
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { ThemeContextProvider } from '../../../theme/useTheme';
4
+ import Badge from '../Badge';
5
+
6
+ const wrap = (ui: React.ReactElement) =>
7
+ render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
8
+
9
+ describe('Badge', () => {
10
+ test('snapshot: default', () => {
11
+ const { container } = wrap(<Badge>Default</Badge>);
12
+ expect(container.firstChild).toMatchSnapshot();
13
+ });
14
+
15
+ test('snapshot: size=large', () => {
16
+ const { container } = wrap(<Badge size='large'>Large</Badge>);
17
+ expect(container.firstChild).toMatchSnapshot();
18
+ });
19
+
20
+ test('snapshot: variant=success with icon', () => {
21
+ const { container } = wrap(
22
+ <Badge
23
+ variant='success'
24
+ icon
25
+ >
26
+ Success
27
+ </Badge>
28
+ );
29
+ expect(container.firstChild).toMatchSnapshot();
30
+ });
31
+
32
+ test('snapshot: variant=critical with icon', () => {
33
+ const { container } = wrap(
34
+ <Badge
35
+ variant='critical'
36
+ icon
37
+ >
38
+ Critical
39
+ </Badge>
40
+ );
41
+ expect(container.firstChild).toMatchSnapshot();
42
+ });
43
+
44
+ test('snapshot: variant=warning with icon', () => {
45
+ const { container } = wrap(
46
+ <Badge
47
+ variant='warning'
48
+ icon
49
+ >
50
+ Warning
51
+ </Badge>
52
+ );
53
+ expect(container.firstChild).toMatchSnapshot();
54
+ });
55
+
56
+ test('snapshot: statusIndicator', () => {
57
+ const { container } = wrap(<Badge statusIndicator>Status</Badge>);
58
+ expect(container.firstChild).toMatchSnapshot();
59
+ });
60
+
61
+ test('snapshot: variant=disabled', () => {
62
+ const { container } = wrap(<Badge variant='disabled'>Disabled</Badge>);
63
+ expect(container.firstChild).toMatchSnapshot();
64
+ });
65
+
66
+ // Functional tests
67
+
68
+ test('renders children', () => {
69
+ const { getByText } = wrap(<Badge>Hello</Badge>);
70
+ expect(getByText('Hello')).toBeTruthy();
71
+ });
72
+
73
+ test('applies custom testId', () => {
74
+ const { getByTestId } = wrap(<Badge testId='my-badge'>Text</Badge>);
75
+ expect(getByTestId('my-badge')).toBeTruthy();
76
+ });
77
+
78
+ test('applies custom className', () => {
79
+ const { getByTestId } = wrap(<Badge className='custom-class'>Text</Badge>);
80
+ expect(getByTestId('ucl-uikit-badge').classList).toContain('custom-class');
81
+ });
82
+
83
+ test('renders as a span element', () => {
84
+ const { getByTestId } = wrap(<Badge>Text</Badge>);
85
+ expect(getByTestId('ucl-uikit-badge').tagName).toBe('SPAN');
86
+ });
87
+
88
+ test('renders status indicator element when statusIndicator=true', () => {
89
+ const { container } = wrap(<Badge statusIndicator>Text</Badge>);
90
+ const indicator = container.querySelector('[aria-hidden="true"]');
91
+ expect(indicator).toBeTruthy();
92
+ });
93
+
94
+ test('does not render icon when variant has no icon mapping', () => {
95
+ const { container } = wrap(
96
+ <Badge
97
+ variant='disabled'
98
+ icon
99
+ >
100
+ Text
101
+ </Badge>
102
+ );
103
+ expect(container.querySelector('svg')).toBeNull();
104
+ });
105
+
106
+ test('renders icon when variant has an icon mapping and icon=true', () => {
107
+ const { container } = wrap(
108
+ <Badge
109
+ variant='success'
110
+ icon
111
+ >
112
+ Text
113
+ </Badge>
114
+ );
115
+ expect(container.querySelector('svg')).toBeTruthy();
116
+ });
117
+
118
+ test('does not render icon when icon prop is not set', () => {
119
+ const { container } = wrap(<Badge variant='success'>Text</Badge>);
120
+ expect(container.querySelector('svg')).toBeNull();
121
+ });
122
+ });
@@ -0,0 +1,133 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Badge > snapshot: default 1`] = `
4
+ <span
5
+ class="ucl-uikit-badge css-b76pju"
6
+ data-testid="ucl-uikit-badge"
7
+ >
8
+ Default
9
+ </span>
10
+ `;
11
+
12
+ exports[`Badge > snapshot: size=large 1`] = `
13
+ <span
14
+ class="ucl-uikit-badge css-6n39yj"
15
+ data-testid="ucl-uikit-badge"
16
+ >
17
+ Large
18
+ </span>
19
+ `;
20
+
21
+ exports[`Badge > snapshot: statusIndicator 1`] = `
22
+ <span
23
+ class="ucl-uikit-badge css-b76pju"
24
+ data-testid="ucl-uikit-badge"
25
+ >
26
+ <span
27
+ aria-hidden="true"
28
+ class="css-170i9e3"
29
+ />
30
+ Status
31
+ </span>
32
+ `;
33
+
34
+ exports[`Badge > snapshot: variant=critical with icon 1`] = `
35
+ <span
36
+ class="ucl-uikit-badge css-1oxz1ke"
37
+ data-testid="ucl-uikit-badge"
38
+ >
39
+ <svg
40
+ aria-hidden="true"
41
+ class="ucl-uikit-icon css-y1s4m2"
42
+ data-testid="ucl-uikit-icon"
43
+ fill="none"
44
+ focusable="false"
45
+ height="14"
46
+ stroke="currentColor"
47
+ stroke-linecap="round"
48
+ stroke-linejoin="round"
49
+ stroke-width="2"
50
+ viewBox="0 0 24 24"
51
+ width="14"
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ >
54
+ <circle
55
+ cx="12"
56
+ cy="12"
57
+ r="10"
58
+ />
59
+ <path
60
+ d="m15 9-6 6m0-6 6 6"
61
+ />
62
+ </svg>
63
+ Critical
64
+ </span>
65
+ `;
66
+
67
+ exports[`Badge > snapshot: variant=disabled 1`] = `
68
+ <span
69
+ class="ucl-uikit-badge css-psg0dk"
70
+ data-testid="ucl-uikit-badge"
71
+ >
72
+ Disabled
73
+ </span>
74
+ `;
75
+
76
+ exports[`Badge > snapshot: variant=success with icon 1`] = `
77
+ <span
78
+ class="ucl-uikit-badge css-16a4ap6"
79
+ data-testid="ucl-uikit-badge"
80
+ >
81
+ <svg
82
+ aria-hidden="true"
83
+ class="ucl-uikit-icon css-y1s4m2"
84
+ data-testid="ucl-uikit-icon"
85
+ fill="none"
86
+ focusable="false"
87
+ height="14"
88
+ stroke="currentColor"
89
+ stroke-linecap="round"
90
+ stroke-linejoin="round"
91
+ stroke-width="2"
92
+ viewBox="0 0 24 24"
93
+ width="14"
94
+ xmlns="http://www.w3.org/2000/svg"
95
+ >
96
+ <path
97
+ d="M22 11.08V12a10 10 0 1 1-5.93-9.14"
98
+ />
99
+ <path
100
+ d="M22 4 12 14.01l-3-3"
101
+ />
102
+ </svg>
103
+ Success
104
+ </span>
105
+ `;
106
+
107
+ exports[`Badge > snapshot: variant=warning with icon 1`] = `
108
+ <span
109
+ class="ucl-uikit-badge css-1o21lbi"
110
+ data-testid="ucl-uikit-badge"
111
+ >
112
+ <svg
113
+ aria-hidden="true"
114
+ class="ucl-uikit-icon css-y1s4m2"
115
+ data-testid="ucl-uikit-icon"
116
+ fill="none"
117
+ focusable="false"
118
+ height="14"
119
+ stroke="currentColor"
120
+ stroke-linecap="round"
121
+ stroke-linejoin="round"
122
+ stroke-width="2"
123
+ viewBox="0 0 24 24"
124
+ width="14"
125
+ xmlns="http://www.w3.org/2000/svg"
126
+ >
127
+ <path
128
+ d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0M12 9v4m0 4h.01"
129
+ />
130
+ </svg>
131
+ Warning
132
+ </span>
133
+ `;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.24.4",
5
+ "version": "0.24.5",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",