@apify/ui-library 1.99.0 → 1.99.1

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.
Files changed (37) hide show
  1. package/dist/src/components/button.d.ts +6 -2
  2. package/dist/src/components/button.d.ts.map +1 -1
  3. package/dist/src/components/button.js +35 -8
  4. package/dist/src/components/button.js.map +1 -1
  5. package/dist/src/components/button.stories.d.ts +6 -0
  6. package/dist/src/components/button.stories.d.ts.map +1 -0
  7. package/dist/src/components/button.stories.js +46 -0
  8. package/dist/src/components/button.stories.js.map +1 -0
  9. package/dist/src/components/icon_button.d.ts +55 -0
  10. package/dist/src/components/icon_button.d.ts.map +1 -0
  11. package/dist/src/components/icon_button.js +134 -0
  12. package/dist/src/components/icon_button.js.map +1 -0
  13. package/dist/src/components/icon_button.stories.d.ts +36 -0
  14. package/dist/src/components/icon_button.stories.d.ts.map +1 -0
  15. package/dist/src/components/icon_button.stories.js +59 -0
  16. package/dist/src/components/icon_button.stories.js.map +1 -0
  17. package/dist/src/components/index.d.ts +2 -0
  18. package/dist/src/components/index.d.ts.map +1 -1
  19. package/dist/src/components/index.js +2 -0
  20. package/dist/src/components/index.js.map +1 -1
  21. package/dist/src/components/spinner.d.ts +32 -0
  22. package/dist/src/components/spinner.d.ts.map +1 -0
  23. package/dist/src/components/spinner.js +84 -0
  24. package/dist/src/components/spinner.js.map +1 -0
  25. package/dist/src/components/spinner.stories.d.ts +8 -0
  26. package/dist/src/components/spinner.stories.d.ts.map +1 -0
  27. package/dist/src/components/spinner.stories.js +24 -0
  28. package/dist/src/components/spinner.stories.js.map +1 -0
  29. package/dist/tsconfig.build.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/components/{button.stories.jsx → button.stories.tsx} +26 -11
  32. package/src/components/button.tsx +43 -11
  33. package/src/components/icon_button.stories.tsx +251 -0
  34. package/src/components/icon_button.tsx +211 -0
  35. package/src/components/index.ts +2 -0
  36. package/src/components/spinner.stories.tsx +37 -0
  37. package/src/components/spinner.tsx +116 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.99.0",
3
+ "version": "1.99.1",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -64,5 +64,5 @@
64
64
  "src",
65
65
  "style"
66
66
  ],
67
- "gitHead": "066d002719f4c6ed241b7394465ed7aa74eea1c8"
67
+ "gitHead": "7c9cea03db98653004a4749f4d233c467aa54793"
68
68
  }
@@ -1,9 +1,12 @@
1
+ import type { Meta } from '@storybook/react-vite';
2
+ // eslint-disable-next-line import/extensions
3
+ import { action } from 'storybook/actions';
1
4
  import styled from 'styled-components';
2
5
 
3
6
  import { CrossIcon } from '@apify/ui-icons';
4
7
 
5
- import { theme } from '../design_system/theme.ts';
6
- import { Button } from './button.tsx';
8
+ import { theme } from '../design_system/theme.js';
9
+ import { Button, type ButtonProps } from './button.js';
7
10
 
8
11
  export default {
9
12
  title: 'UI-Library/Button',
@@ -11,10 +14,10 @@ export default {
11
14
  parameters: {
12
15
  design: {
13
16
  type: 'figma',
14
- url: 'https://www.figma.com/design/yftmrF413idIDbu0OcjlAQ/%F0%9F%8E%AE-Console-library?node-id=0-1',
17
+ url: 'https://www.figma.com/design/duSsGnk84UMYav8mg8QNgR/%F0%9F%93%96-Shared-library?node-id=5235-24898&t=fCZgSCLJK79g0Omo-4',
15
18
  },
16
19
  },
17
- };
20
+ } as Meta<typeof Button>;
18
21
 
19
22
  const Wrapper = styled.div`
20
23
  background-color: ${theme.color.neutral.background};
@@ -27,7 +30,7 @@ const Wrapper = styled.div`
27
30
 
28
31
  const TwoColumns = styled.div`
29
32
  display: grid;
30
- grid-template-columns: auto auto;
33
+ grid-template-columns: repeat(5, auto);
31
34
  gap: 40px;
32
35
  `;
33
36
 
@@ -37,9 +40,12 @@ const ButtonGrid = styled.div`
37
40
  margin-bottom: 16px;
38
41
  `;
39
42
 
40
- const ButtonSection = ({
41
- ...props
42
- }) => {
43
+ const ButtonSection = ({ ...rest }: Partial<ButtonProps>) => {
44
+ const props: ButtonProps = {
45
+ ...rest,
46
+ onClick: action('onClick'),
47
+ };
48
+
43
49
  return (
44
50
  <Wrapper>
45
51
  <ButtonGrid>
@@ -109,18 +115,27 @@ export const Default = () => {
109
115
  <Wrapper>
110
116
  <h4>Primary</h4>
111
117
  <TwoColumns>
112
- <ButtonSection color='default' />
118
+ <ButtonSection color='default' size='extraLarge' />
119
+ <ButtonSection color='default' size='large' />
120
+ <ButtonSection color='default' size='medium' />
113
121
  <ButtonSection color='default' size='small' />
122
+ <ButtonSection color='default' size='extraSmall' />
114
123
  </TwoColumns>
115
124
  <h4>Success</h4>
116
125
  <TwoColumns>
117
- <ButtonSection color='success' />
126
+ <ButtonSection color='success' size='extraLarge' />
127
+ <ButtonSection color='success' size='large' />
128
+ <ButtonSection color='success' size='medium' />
118
129
  <ButtonSection color='success' size='small' />
130
+ <ButtonSection color='success' size='extraSmall' />
119
131
  </TwoColumns>
120
132
  <h4>Danger</h4>
121
133
  <TwoColumns>
122
- <ButtonSection color='danger' />
134
+ <ButtonSection color='danger' size='extraLarge' />
135
+ <ButtonSection color='danger' size='large' />
136
+ <ButtonSection color='danger' size='medium' />
123
137
  <ButtonSection color='danger' size='small' />
138
+ <ButtonSection color='danger' size='extraSmall' />
124
139
  </TwoColumns>
125
140
  </Wrapper>
126
141
  );
@@ -3,7 +3,7 @@ import type React from 'react';
3
3
  import { forwardRef } from 'react';
4
4
  import styled, { css } from 'styled-components';
5
5
 
6
- import { ExternalLinkIcon } from '@apify/ui-icons';
6
+ import { ExternalLinkIcon, type IconSize } from '@apify/ui-icons';
7
7
 
8
8
  import { theme } from '../design_system/theme.js';
9
9
  import { type WithRequired, type WithTransientProps } from '../type_utils.js';
@@ -11,7 +11,7 @@ import { useSharedUiDependencies } from '../ui_dependency_provider.js';
11
11
  import { Box, type MarginSpacingProps, type RegularBoxProps } from './box.js';
12
12
  import { isUrlExternal, Link, type LinkProps } from './link.js';
13
13
 
14
- type ButtonSize = 'medium' | 'small';
14
+ export type ButtonSize = 'extraLarge' | 'large' | 'medium' | 'small' | 'extraSmall';
15
15
  type ButtonColor = 'default' | 'success' | 'danger';
16
16
  type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
17
17
 
@@ -67,8 +67,11 @@ type ButtonColorCombinations = {
67
67
 
68
68
  type ButtonSizeCombinations = {
69
69
  [Size in ButtonSize]: {
70
+ typography: string,
71
+ size: number,
70
72
  horizontalPadding: number,
71
- height: number,
73
+ borderRadius: string,
74
+ iconSize: IconSize,
72
75
  };
73
76
  };
74
77
 
@@ -150,13 +153,40 @@ const BUTTON_COLOR_VARIANTS_CSS: ButtonColorCombinations = {
150
153
  };
151
154
 
152
155
  export const BUTTON_SIZE_VARIANTS_CSS: ButtonSizeCombinations = {
156
+ extraLarge: {
157
+ typography: theme.typography.shared.mobile.bodyMMedium,
158
+ size: 40,
159
+ horizontalPadding: 12,
160
+ borderRadius: theme.radius.radius8,
161
+ iconSize: '20',
162
+ },
163
+ large: {
164
+ typography: theme.typography.shared.mobile.bodyMMedium,
165
+ size: 36,
166
+ horizontalPadding: 12,
167
+ borderRadius: theme.radius.radius6,
168
+ iconSize: '20',
169
+ },
153
170
  medium: {
154
- height: 32,
171
+ typography: theme.typography.shared.mobile.bodyMMedium,
172
+ size: 32,
155
173
  horizontalPadding: 12,
174
+ borderRadius: theme.radius.radius6,
175
+ iconSize: '16',
156
176
  },
157
177
  small: {
158
- height: 28,
178
+ typography: theme.typography.shared.mobile.bodyMMedium,
179
+ size: 28,
159
180
  horizontalPadding: 8,
181
+ borderRadius: theme.radius.radius6,
182
+ iconSize: '16',
183
+ },
184
+ extraSmall: {
185
+ typography: theme.typography.shared.mobile.bodySMedium,
186
+ size: 24,
187
+ horizontalPadding: 6,
188
+ borderRadius: theme.radius.radius6,
189
+ iconSize: '12',
160
190
  },
161
191
  };
162
192
 
@@ -201,27 +231,30 @@ export const getButtonColorStyles = (variant: ButtonVariant = 'primary', color:
201
231
 
202
232
  export const getButtonSizeStyles = (size: ButtonSize = 'medium') => {
203
233
  const {
204
- height,
234
+ typography,
235
+ size: height,
205
236
  horizontalPadding,
237
+ borderRadius,
206
238
  } = BUTTON_SIZE_VARIANTS_CSS[size];
207
239
 
208
240
  return css`
241
+ ${typography};
209
242
  height: ${height}px;
210
243
  /* We just want to ensure padding on the sides. Height is set the the hard way for simplicity */
211
244
  padding: 0 ${horizontalPadding}px;
245
+ border-radius: ${borderRadius};
246
+
212
247
  `;
213
248
  };
214
249
 
215
250
  const StyledButton = styled(Box)<WithTransientProps<Required<TransientButtonProps>>>`
216
- ${theme.typography.shared.mobile.bodyMMedium};
217
-
218
251
  /* Basic positioning */
219
252
  display: inline-flex;
220
253
  align-items: center;
221
254
  /* NOT sure about this. It needs to be set when we want 100% width button */
222
255
  /* Maybe we can add block property */
223
256
  justify-content: center;
224
- gap: ${theme.space.space8};
257
+ gap: ${theme.space.space4};
225
258
 
226
259
  /* Basic styles */
227
260
  white-space: nowrap;
@@ -231,7 +264,6 @@ const StyledButton = styled(Box)<WithTransientProps<Required<TransientButtonProp
231
264
  /* Border is always defined, but it can be set to transparent */
232
265
  border-style: solid;
233
266
  border-width: 1px;
234
- border-radius: ${theme.radius.radius6};
235
267
  border-color: transparent;
236
268
 
237
269
  /* Colors */
@@ -265,7 +297,7 @@ export const Button = forwardRef<HTMLElement, ButtonProps | AnchorButtonProps>((
265
297
  if (onClick) onClick(e);
266
298
  };
267
299
 
268
- const iconSize = size === 'medium' ? '16' : '12';
300
+ const { iconSize } = BUTTON_SIZE_VARIANTS_CSS[size];
269
301
 
270
302
  if (isAnchorButton(props)) {
271
303
  const isExternal = isUrlExternal(props.to, windowLocationHost);
@@ -0,0 +1,251 @@
1
+ import type { Meta } from '@storybook/react-vite';
2
+ // eslint-disable-next-line import/extensions
3
+ import { action } from 'storybook/actions';
4
+ import styled from 'styled-components';
5
+
6
+ import {
7
+ BellIcon,
8
+ DeleteIcon, PeopleIcon,
9
+ } from '@apify/ui-icons';
10
+
11
+ import {
12
+ ICON_BUTTON_VARIANTS,
13
+ IconButton,
14
+ type IconButtonProps,
15
+ } from './icon_button.js';
16
+
17
+ export default {
18
+ title: 'UI-Library/IconButton',
19
+ component: IconButton,
20
+ args: {
21
+ onClick: action('onClick'),
22
+ },
23
+ parameters: {
24
+ design: {
25
+ type: 'figma',
26
+ url: 'https://www.figma.com/design/duSsGnk84UMYav8mg8QNgR/%F0%9F%93%96-Shared-library?node-id=5236-40552&t=fCZgSCLJK79g0Omo-4',
27
+ },
28
+ },
29
+ } as Meta<typeof IconButton>;
30
+
31
+ const Table = styled.table`
32
+ td {
33
+ padding: 8px;
34
+ }
35
+ margin-bottom: 32px;
36
+ `;
37
+
38
+ export const Default = (props: IconButtonProps<'button'>) => {
39
+ return (<>
40
+ <Table>
41
+ <tbody>
42
+ {Object.values(ICON_BUTTON_VARIANTS).map((variant) => (
43
+ <tr key={variant}>
44
+ <td>
45
+ <IconButton
46
+ {...props}
47
+ variant={variant}
48
+ Icon={DeleteIcon}
49
+ size='extraLarge'
50
+ title='Delete example'
51
+ />
52
+ </td>
53
+ <td>
54
+ <IconButton
55
+ {...props}
56
+ variant={variant}
57
+ Icon={DeleteIcon}
58
+ size='large'
59
+ title='Delete example'
60
+ />
61
+ </td>
62
+ <td>
63
+ <IconButton
64
+ {...props}
65
+ variant={variant}
66
+ Icon={DeleteIcon}
67
+ size='medium'
68
+ title='Delete example'
69
+ />
70
+ </td>
71
+ <td>
72
+ <IconButton
73
+ {...props}
74
+ variant={variant}
75
+ Icon={DeleteIcon}
76
+ size='small'
77
+ title='Delete example'
78
+ />
79
+ </td>
80
+ <td>
81
+ <IconButton
82
+ {...props}
83
+ variant={variant}
84
+ Icon={DeleteIcon}
85
+ size='extraSmall'
86
+ title='Delete example'
87
+ />
88
+ </td>
89
+ <td>
90
+ <IconButton
91
+ {...props}
92
+ variant={variant}
93
+ Icon={DeleteIcon}
94
+ disabled
95
+ size='extraLarge'
96
+ title='Disabled delete example'
97
+ />
98
+ </td>
99
+ <td>
100
+ <IconButton
101
+ {...props}
102
+ variant={variant}
103
+ Icon={DeleteIcon}
104
+ disabled
105
+ size='large'
106
+ title='Disabled delete example'
107
+ />
108
+ </td>
109
+ <td>
110
+ <IconButton
111
+ {...props}
112
+ variant={variant}
113
+ Icon={DeleteIcon}
114
+ disabled
115
+ size='medium'
116
+ title='Disabled delete example'
117
+ />
118
+ </td>
119
+ <td>
120
+ <IconButton
121
+ {...props}
122
+ variant={variant}
123
+ Icon={DeleteIcon}
124
+ disabled
125
+ size='small'
126
+ title='Disabled delete example'
127
+ />
128
+ </td>
129
+ <td>
130
+ <IconButton
131
+ {...props}
132
+ variant={variant}
133
+ Icon={DeleteIcon}
134
+ disabled
135
+ size='extraSmall'
136
+ title='Disabled delete example'
137
+ />
138
+ </td>
139
+ <td>
140
+ <IconButton
141
+ {...props}
142
+ variant={variant}
143
+ Icon={DeleteIcon}
144
+ isLoading
145
+ size='extraLarge'
146
+ title='Loading delete example'
147
+ />
148
+ </td>
149
+ <td>
150
+ <IconButton
151
+ {...props}
152
+ variant={variant}
153
+ Icon={DeleteIcon}
154
+ isLoading
155
+ size='extraLarge'
156
+ title='Loading delete example'
157
+ />
158
+ </td>
159
+ <td>
160
+ <IconButton
161
+ {...props}
162
+ variant={variant}
163
+ Icon={DeleteIcon}
164
+ isLoading
165
+ size='large'
166
+ title='Loading delete example'
167
+ />
168
+ </td>
169
+ <td>
170
+ <IconButton
171
+ {...props}
172
+ variant={variant}
173
+ Icon={DeleteIcon}
174
+ isLoading
175
+ size='medium'
176
+ title='Loading delete example'
177
+ />
178
+ </td>
179
+ <td>
180
+ <IconButton
181
+ {...props}
182
+ variant={variant}
183
+ Icon={DeleteIcon}
184
+ isLoading
185
+ size='small'
186
+ title='Loading delete example'
187
+ />
188
+ </td>
189
+ <td>
190
+ <IconButton
191
+ {...props}
192
+ variant={variant}
193
+ Icon={DeleteIcon}
194
+ isLoading
195
+ size='extraSmall'
196
+ title='Loading delete example'
197
+ />
198
+ </td>
199
+ </tr>
200
+ ))}
201
+ </tbody>
202
+ </Table>
203
+ </>);
204
+ };
205
+
206
+ const StyledPlayground = styled.div`
207
+ display: flex;
208
+ flex-direction: row;
209
+ gap: 8px;
210
+ `;
211
+
212
+ export const Playground = (props: IconButtonProps<'button'>) => {
213
+ return (<StyledPlayground>
214
+ <IconButton
215
+ {...props}
216
+ Icon={PeopleIcon}
217
+ />
218
+ <IconButton
219
+ {...props}
220
+ Icon={DeleteIcon}
221
+ />
222
+ <IconButton
223
+ {...props}
224
+ Icon={BellIcon}
225
+ />
226
+ </StyledPlayground>);
227
+ };
228
+
229
+ Playground.args = {
230
+ variant: ICON_BUTTON_VARIANTS.DEFAULT,
231
+ size: 'medium',
232
+ disabled: false,
233
+ isLoading: false,
234
+ };
235
+
236
+ Playground.argTypes = {
237
+ variant: {
238
+ options: ICON_BUTTON_VARIANTS,
239
+ control: 'select',
240
+ },
241
+ size: {
242
+ options: ['medium', 'small'],
243
+ control: 'select',
244
+ },
245
+ disabled: {
246
+ control: 'boolean',
247
+ },
248
+ isLoading: {
249
+ control: 'boolean',
250
+ },
251
+ };
@@ -0,0 +1,211 @@
1
+ import { forwardRef } from 'react';
2
+ import styled, { css } from 'styled-components';
3
+
4
+ import type { IconComponent, IconSize } from '@apify/ui-icons';
5
+
6
+ import type { ValueOf } from '@apify-packages/types';
7
+
8
+ import { theme } from '../design_system/theme.js';
9
+ import type { RegularBoxProps } from './box.js';
10
+ import { Button, BUTTON_SIZE_VARIANTS_CSS, type ButtonSize, type RegularButtonProps, type TransientButtonProps } from './button.js';
11
+ import { Tooltip } from './floating/tooltip.js';
12
+ import { Link, type RegularLinkProps } from './link.js';
13
+ import { InlineSpinner, spinnerClassNames } from './spinner.js';
14
+
15
+ export const ICON_BUTTON_VARIANTS = {
16
+ DEFAULT: 'DEFAULT',
17
+ BORDERED: 'BORDERED',
18
+ DANGER: 'DANGER',
19
+ DANGER_BORDERED: 'DANGER_BORDERED',
20
+ } as const;
21
+ export type ICON_BUTTON_VARIANTS = ValueOf<typeof ICON_BUTTON_VARIANTS>;
22
+
23
+ const iconButtonVariantStyle = {
24
+ [ICON_BUTTON_VARIANTS.DEFAULT]: {
25
+ backgroundColor: 'transparent',
26
+ backgroundHoverColor: theme.color.neutral.hover,
27
+ backgroundDisabledColor: 'transparent',
28
+ borderColor: 'transparent',
29
+ borderHoverColor: 'transparent',
30
+ borderDisabledColor: 'transparent',
31
+ iconColor: theme.color.neutral.text,
32
+ },
33
+ [ICON_BUTTON_VARIANTS.BORDERED]: {
34
+ backgroundColor: theme.color.neutral.backgroundMuted,
35
+ backgroundHoverColor: theme.color.neutral.hover,
36
+ backgroundDisabledColor: theme.color.neutral.disabled,
37
+ borderColor: theme.color.neutral.border,
38
+ borderHoverColor: theme.color.neutral.border,
39
+ borderDisabledColor: theme.color.neutral.border,
40
+ iconColor: theme.color.neutral.text,
41
+ },
42
+ [ICON_BUTTON_VARIANTS.DANGER_BORDERED]: {
43
+ backgroundColor: theme.color.neutral.backgroundMuted,
44
+ backgroundHoverColor: theme.color.danger.backgroundHover,
45
+ backgroundDisabledColor: theme.color.neutral.backgroundSubtle,
46
+ borderColor: theme.color.neutral.border,
47
+ borderHoverColor: theme.color.danger.borderSubtle,
48
+ borderDisabledColor: theme.color.neutral.border,
49
+ iconColor: theme.color.danger.text,
50
+ },
51
+ [ICON_BUTTON_VARIANTS.DANGER]: {
52
+ backgroundColor: 'transparent',
53
+ backgroundHoverColor: theme.color.danger.backgroundHover,
54
+ backgroundDisabledColor: theme.color.neutral.backgroundSubtle,
55
+ borderColor: 'transparent',
56
+ borderHoverColor: 'transparent',
57
+ borderDisabledColor: 'transparent',
58
+ iconColor: theme.color.danger.text,
59
+ },
60
+ } satisfies Record<ICON_BUTTON_VARIANTS, unknown>;
61
+
62
+ type IconButtonSizeConfig = {
63
+ iconSize: IconSize;
64
+ spinnerSize: string;
65
+ }
66
+
67
+ const iconButtonSizeConfig: Record<ButtonSize, IconButtonSizeConfig> = {
68
+ extraLarge: {
69
+ iconSize: '24',
70
+ spinnerSize: '2.4rem',
71
+ },
72
+ large: {
73
+ iconSize: '20',
74
+ spinnerSize: '2rem',
75
+ },
76
+ medium: {
77
+ iconSize: '16',
78
+ spinnerSize: '1.6rem',
79
+ },
80
+ small: {
81
+ iconSize: '16',
82
+ spinnerSize: '1.6rem',
83
+ },
84
+ extraSmall: {
85
+ iconSize: '12',
86
+ spinnerSize: '1.2rem',
87
+ },
88
+ };
89
+
90
+ const getIconButtonColorStyles = (variant: ICON_BUTTON_VARIANTS) => {
91
+ const {
92
+ backgroundColor,
93
+ backgroundHoverColor,
94
+ backgroundDisabledColor,
95
+ borderDisabledColor,
96
+ borderColor,
97
+ borderHoverColor,
98
+ iconColor,
99
+ } = iconButtonVariantStyle[variant];
100
+
101
+ return css`
102
+ color: ${iconColor};
103
+ background-color: ${backgroundColor};
104
+ border-color: ${borderColor};
105
+
106
+ &:hover {
107
+ background-color: ${backgroundHoverColor};
108
+ border-color: ${borderHoverColor || borderColor};
109
+ color: ${iconColor};
110
+ }
111
+
112
+ &:focus {
113
+ border-color: ${theme.color.primary.fieldBorderActive};
114
+ }
115
+
116
+ &:active {
117
+ background-color: ${theme.color.neutral.actionSecondaryActive};
118
+ border-color: ${theme.color.neutral.actionSecondaryActive};
119
+ }
120
+
121
+ &:disabled {
122
+ color: ${theme.color.neutral.iconDisabled};
123
+ background-color: ${backgroundDisabledColor};
124
+ border-color: ${borderDisabledColor};
125
+
126
+ cursor: default;
127
+ }
128
+ `;
129
+ };
130
+
131
+ const StyledButton = styled(Button) <{ $variant: ICON_BUTTON_VARIANTS, $size: ButtonSize }>`
132
+ ${({ $variant }) => getIconButtonColorStyles($variant)}
133
+
134
+ /* Override default button styles */
135
+ box-sizing: border-box;
136
+ width: ${({ $size }) => (BUTTON_SIZE_VARIANTS_CSS[$size].size)}px;
137
+ padding: 0;
138
+
139
+ .${spinnerClassNames.SPINNER} .path {
140
+ stroke: ${theme.color.neutral.icon};
141
+ }
142
+ `;
143
+
144
+ type IconButtonNodeType = Extract<React.ElementType, 'a' | 'button'>;
145
+ type IconButtonNodePropsMap = {
146
+ a: {
147
+ element: HTMLAnchorElement;
148
+ props: RegularLinkProps;
149
+ };
150
+ button: {
151
+ element: HTMLButtonElement;
152
+ props: RegularButtonProps;
153
+ };
154
+ }
155
+
156
+ export type IconButtonProps<T extends IconButtonNodeType> = RegularBoxProps & Pick<TransientButtonProps, 'size'> & {
157
+ as?: T
158
+ variant?: ICON_BUTTON_VARIANTS
159
+ Icon: IconComponent
160
+ disabled?: boolean
161
+ isLoading?: boolean
162
+ title?: string
163
+ tooltipProps?: unknown
164
+ } & Omit<IconButtonNodePropsMap[T]['props'], 'size' | 'variant'>;
165
+
166
+ /**
167
+ * Simplified button component that displays an icon.
168
+ * Has a tooltip and a loading state.
169
+ */
170
+ export const IconButton = forwardRef(<T extends IconButtonNodeType>(
171
+ {
172
+ variant = ICON_BUTTON_VARIANTS.DEFAULT,
173
+ size = 'medium',
174
+ as = 'button' as T,
175
+ title,
176
+ disabled = false,
177
+ isLoading = false,
178
+ Icon,
179
+ tooltipProps,
180
+ ...passThroughProps
181
+ }: IconButtonProps<T>, ref: React.Ref<IconButtonNodePropsMap[T]['element']>) => {
182
+ const otherProps = { ...passThroughProps, size };
183
+
184
+ const component: React.ElementType = as === 'a' ? Link : as;
185
+ const { iconSize, spinnerSize } = iconButtonSizeConfig[size];
186
+
187
+ const button = (
188
+ <StyledButton
189
+ forwardedAs={component}
190
+ ref={ref}
191
+ disabled={disabled || isLoading}
192
+ type='button'
193
+ aria-label={title}
194
+ LeftIcon={isLoading ? () => <InlineSpinner size={spinnerSize} /> : () => <Icon size={iconSize} />}
195
+ // We apply our own styles to the button, so we need to override the default variant
196
+ variant="tertiary"
197
+ $size={size}
198
+ $variant={variant}
199
+ {...otherProps}
200
+ />
201
+ );
202
+
203
+ return title ? (
204
+ // @ts-expect-error tooltip is not migrated to TS yet
205
+ <Tooltip content={title} {...tooltipProps}>
206
+ {button}
207
+ </Tooltip>
208
+ ) : button;
209
+ });
210
+
211
+ IconButton.displayName = 'IconButton';
@@ -21,3 +21,5 @@ export * from './badge.js';
21
21
  export * from './tag.js';
22
22
  export * from './tabs/index.js';
23
23
  export * from './shortcut.js';
24
+ export * from './icon_button.js';
25
+ export * from './spinner.js';