@apify/ui-library 1.99.0 → 1.100.0
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.
- package/dist/src/components/button.d.ts +6 -2
- package/dist/src/components/button.d.ts.map +1 -1
- package/dist/src/components/button.js +35 -8
- package/dist/src/components/button.js.map +1 -1
- package/dist/src/components/button.stories.d.ts +6 -0
- package/dist/src/components/button.stories.d.ts.map +1 -0
- package/dist/src/components/button.stories.js +46 -0
- package/dist/src/components/button.stories.js.map +1 -0
- package/dist/src/components/chip.d.ts +1 -1
- package/dist/src/components/chip.d.ts.map +1 -1
- package/dist/src/components/icon_button.d.ts +55 -0
- package/dist/src/components/icon_button.d.ts.map +1 -0
- package/dist/src/components/icon_button.js +134 -0
- package/dist/src/components/icon_button.js.map +1 -0
- package/dist/src/components/icon_button.stories.d.ts +36 -0
- package/dist/src/components/icon_button.stories.d.ts.map +1 -0
- package/dist/src/components/icon_button.stories.js +59 -0
- package/dist/src/components/icon_button.stories.js.map +1 -0
- package/dist/src/components/index.d.ts +2 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/components/index.js +2 -0
- package/dist/src/components/index.js.map +1 -1
- package/dist/src/components/spinner.d.ts +32 -0
- package/dist/src/components/spinner.d.ts.map +1 -0
- package/dist/src/components/spinner.js +84 -0
- package/dist/src/components/spinner.js.map +1 -0
- package/dist/src/components/spinner.stories.d.ts +8 -0
- package/dist/src/components/spinner.stories.d.ts.map +1 -0
- package/dist/src/components/spinner.stories.js +24 -0
- package/dist/src/components/spinner.stories.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/{button.stories.jsx → button.stories.tsx} +26 -11
- package/src/components/button.tsx +43 -11
- package/src/components/chip.tsx +1 -1
- package/src/components/icon_button.stories.tsx +251 -0
- package/src/components/icon_button.tsx +211 -0
- package/src/components/index.ts +2 -0
- package/src/components/spinner.stories.tsx +37 -0
- 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.
|
|
3
|
+
"version": "1.100.0",
|
|
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": "
|
|
67
|
+
"gitHead": "8773d9462fbe733c88199f30130ed557f676f07d"
|
|
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.
|
|
6
|
-
import { Button } from './button.
|
|
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/
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
300
|
+
const { iconSize } = BUTTON_SIZE_VARIANTS_CSS[size];
|
|
269
301
|
|
|
270
302
|
if (isAnchorButton(props)) {
|
|
271
303
|
const isExternal = isUrlExternal(props.to, windowLocationHost);
|
package/src/components/chip.tsx
CHANGED
|
@@ -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';
|