@gv-tech/design-system 0.8.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/.github/CODEOWNERS +2 -0
- package/.github/CONTRIBUTING.md +38 -0
- package/.github/FUNDING.yml +4 -0
- package/.github/PULL_REQUEST_TEMPLATE/build.md +5 -0
- package/.github/PULL_REQUEST_TEMPLATE/standard.md +3 -0
- package/.github/RELEASING.md +37 -0
- package/.github/copilot-instructions.md +93 -0
- package/.github/workflows/ci.yml +82 -0
- package/.github/workflows/codeql-analysis.yml +34 -0
- package/.github/workflows/release-please.yml +53 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierignore +1 -0
- package/.storybook/.preview-head.html +1 -0
- package/.storybook/main.ts +38 -0
- package/.storybook/preview.tsx +30 -0
- package/.tool-versions +1 -0
- package/.vscode/launch.json +22 -0
- package/.vscode/settings.json +30 -0
- package/.yarn/releases/yarn-4.12.0.cjs +942 -0
- package/.yarnrc.yml +7 -0
- package/CHANGELOG.md +490 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/SECURITY.md +9 -0
- package/babel.config.js +3 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.demo.html +40 -0
- package/dist/index.js +647 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1053 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logo192.png +0 -0
- package/dist/logo512.png +0 -0
- package/dist/manifest.json +25 -0
- package/dist/robots.txt +2 -0
- package/dist/vendor-DXgJBoQh.mjs +265 -0
- package/dist/vendor-DXgJBoQh.mjs.map +1 -0
- package/dist/vendor-nZSsnGb7.js +7 -0
- package/dist/vendor-nZSsnGb7.js.map +1 -0
- package/docs/MIGRATE_TO_GVTECH_SCOPE.md +74 -0
- package/eslint.config.mjs +95 -0
- package/netlify.toml +6 -0
- package/package.json +130 -0
- package/public/favicon.ico +0 -0
- package/public/index.demo.html +40 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +2 -0
- package/scripts/validate.js +56 -0
- package/serve.json +4 -0
- package/src/Avatar.stories.tsx +67 -0
- package/src/Avatar.tsx +174 -0
- package/src/Badge.stories.tsx +87 -0
- package/src/Badge.tsx +76 -0
- package/src/Button.stories.tsx +244 -0
- package/src/Button.tsx +384 -0
- package/src/Icon.stories.tsx +101 -0
- package/src/Icon.tsx +64 -0
- package/src/Intro.stories.tsx +20 -0
- package/src/Link.stories.tsx +69 -0
- package/src/Link.tsx +252 -0
- package/src/StoryLinkWrapper.d.ts +1 -0
- package/src/StoryLinkWrapper.tsx +33 -0
- package/src/__tests__/Avatar.test.tsx +28 -0
- package/src/__tests__/Badge.test.tsx +25 -0
- package/src/__tests__/Button.test.tsx +38 -0
- package/src/__tests__/Icon.test.tsx +26 -0
- package/src/__tests__/Link.test.tsx +31 -0
- package/src/index.ts +13 -0
- package/src/mdx.d.ts +5 -0
- package/src/setupTests.ts +1 -0
- package/src/shared/animation.d.ts +18 -0
- package/src/shared/animation.js +60 -0
- package/src/shared/global.d.ts +12 -0
- package/src/shared/global.js +120 -0
- package/src/shared/icons.d.ts +34 -0
- package/src/shared/icons.js +282 -0
- package/src/shared/styles.d.ts +86 -0
- package/src/shared/styles.js +98 -0
- package/src/test-utils/axe.ts +25 -0
- package/src/types.ts +316 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +20 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +35 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { userEvent, within } from '@storybook/testing-library';
|
|
5
|
+
import { expect } from '@storybook/jest';
|
|
6
|
+
|
|
7
|
+
import { Button } from './Button';
|
|
8
|
+
import { Icon } from './Icon';
|
|
9
|
+
import { IconName } from './types';
|
|
10
|
+
import { StoryLinkWrapper } from './StoryLinkWrapper';
|
|
11
|
+
|
|
12
|
+
const CustomButton = styled.button`
|
|
13
|
+
border: 1px solid green;
|
|
14
|
+
background: lightgreen;
|
|
15
|
+
color: rebeccapurple;
|
|
16
|
+
padding: 1em;
|
|
17
|
+
font-size: 1.2em;
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
function ButtonWrapper(props: ComponentProps<typeof CustomButton>) {
|
|
21
|
+
return <CustomButton {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
title: 'Design System/Button',
|
|
26
|
+
component: Button,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
import type { StoryFn } from '@storybook/react';
|
|
30
|
+
export const AllButtons: StoryFn = (_args: Record<string, unknown>) => (
|
|
31
|
+
<div>
|
|
32
|
+
<Button appearance="primary">Primary</Button>
|
|
33
|
+
<Button appearance="secondary">Secondary</Button>
|
|
34
|
+
<Button appearance="tertiary">Tertiary</Button>
|
|
35
|
+
<Button appearance="outline">Outline</Button>
|
|
36
|
+
<Button appearance="primaryOutline">Outline primary</Button>
|
|
37
|
+
<Button appearance="secondaryOutline">Outline secondary</Button>
|
|
38
|
+
<Button appearance="primary" disabled>
|
|
39
|
+
Disabled
|
|
40
|
+
</Button>
|
|
41
|
+
<br />
|
|
42
|
+
<Button appearance="primary" loading>
|
|
43
|
+
Primary
|
|
44
|
+
</Button>
|
|
45
|
+
<Button appearance="secondary" loading>
|
|
46
|
+
Secondary
|
|
47
|
+
</Button>
|
|
48
|
+
<Button appearance="tertiary" loading>
|
|
49
|
+
Tertiary
|
|
50
|
+
</Button>
|
|
51
|
+
<Button appearance="outline" loading>
|
|
52
|
+
Outline
|
|
53
|
+
</Button>
|
|
54
|
+
<Button appearance="outline" loading loadingText="Custom...">
|
|
55
|
+
Outline
|
|
56
|
+
</Button>
|
|
57
|
+
<br />
|
|
58
|
+
<Button appearance="primary" size="small">
|
|
59
|
+
Primary
|
|
60
|
+
</Button>
|
|
61
|
+
<Button appearance="secondary" size="small">
|
|
62
|
+
Secondary
|
|
63
|
+
</Button>
|
|
64
|
+
<Button appearance="tertiary" size="small">
|
|
65
|
+
Tertiary
|
|
66
|
+
</Button>
|
|
67
|
+
<Button appearance="outline" size="small">
|
|
68
|
+
Outline
|
|
69
|
+
</Button>
|
|
70
|
+
<Button appearance="primary" disabled size="small">
|
|
71
|
+
Disabled
|
|
72
|
+
</Button>
|
|
73
|
+
<Button appearance="outline" size="small" containsIcon>
|
|
74
|
+
<Icon icon={'link' as IconName} aria-label="Link" />
|
|
75
|
+
</Button>
|
|
76
|
+
<Button appearance="outline" size="small">
|
|
77
|
+
<Icon icon={'link' as IconName} />
|
|
78
|
+
Link
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
AllButtons.storyName = 'all buttons';
|
|
84
|
+
|
|
85
|
+
export const ButtonWrapperStory: StoryFn = (_args: Record<string, unknown>) => (
|
|
86
|
+
<div>
|
|
87
|
+
<ButtonWrapper>Original Button Wrapper</ButtonWrapper>
|
|
88
|
+
<br />
|
|
89
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="primary">
|
|
90
|
+
Primary
|
|
91
|
+
</Button>
|
|
92
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="secondary">
|
|
93
|
+
Secondary
|
|
94
|
+
</Button>
|
|
95
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="tertiary">
|
|
96
|
+
Tertiary
|
|
97
|
+
</Button>
|
|
98
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="outline">
|
|
99
|
+
Outline
|
|
100
|
+
</Button>
|
|
101
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="primaryOutline">
|
|
102
|
+
Outline primary
|
|
103
|
+
</Button>
|
|
104
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="secondaryOutline">
|
|
105
|
+
Outline secondary
|
|
106
|
+
</Button>
|
|
107
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="primary" disabled>
|
|
108
|
+
Disabled
|
|
109
|
+
</Button>
|
|
110
|
+
<br />
|
|
111
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="primary" loading>
|
|
112
|
+
Primary
|
|
113
|
+
</Button>
|
|
114
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="secondary" loading>
|
|
115
|
+
Secondary
|
|
116
|
+
</Button>
|
|
117
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="tertiary" loading>
|
|
118
|
+
Tertiary
|
|
119
|
+
</Button>
|
|
120
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="outline" loading>
|
|
121
|
+
Outline
|
|
122
|
+
</Button>
|
|
123
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="outline" loading loadingText="Custom...">
|
|
124
|
+
Outline
|
|
125
|
+
</Button>
|
|
126
|
+
<br />
|
|
127
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="primary" size="small">
|
|
128
|
+
Primary
|
|
129
|
+
</Button>
|
|
130
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="secondary" size="small">
|
|
131
|
+
Secondary
|
|
132
|
+
</Button>
|
|
133
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="tertiary" size="small">
|
|
134
|
+
Tertiary
|
|
135
|
+
</Button>
|
|
136
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="outline" size="small">
|
|
137
|
+
Outline
|
|
138
|
+
</Button>
|
|
139
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="primary" disabled size="small">
|
|
140
|
+
Disabled
|
|
141
|
+
</Button>
|
|
142
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="outline" size="small" containsIcon>
|
|
143
|
+
<Icon icon={'link' as IconName} aria-label="Link" />
|
|
144
|
+
</Button>
|
|
145
|
+
<Button ButtonWrapper={ButtonWrapper} appearance="outline" size="small">
|
|
146
|
+
<Icon icon={'link' as IconName} />
|
|
147
|
+
Link
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
ButtonWrapperStory.storyName = 'button wrapper';
|
|
153
|
+
|
|
154
|
+
export const AnchorWrapper: StoryFn = (_args: Record<string, unknown>) => (
|
|
155
|
+
<div>
|
|
156
|
+
<StoryLinkWrapper to="http://storybook.js.org">Original Link Wrapper</StoryLinkWrapper>
|
|
157
|
+
<br />
|
|
158
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="primary">
|
|
159
|
+
Primary
|
|
160
|
+
</Button>
|
|
161
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="secondary">
|
|
162
|
+
Secondary
|
|
163
|
+
</Button>
|
|
164
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="tertiary">
|
|
165
|
+
Tertiary
|
|
166
|
+
</Button>
|
|
167
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="outline">
|
|
168
|
+
Outline
|
|
169
|
+
</Button>
|
|
170
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="primaryOutline">
|
|
171
|
+
Outline primary
|
|
172
|
+
</Button>
|
|
173
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="secondaryOutline">
|
|
174
|
+
Outline secondary
|
|
175
|
+
</Button>
|
|
176
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="primary" disabled>
|
|
177
|
+
Disabled
|
|
178
|
+
</Button>
|
|
179
|
+
<br />
|
|
180
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="primary" loading>
|
|
181
|
+
Primary
|
|
182
|
+
</Button>
|
|
183
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="secondary" loading>
|
|
184
|
+
Secondary
|
|
185
|
+
</Button>
|
|
186
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="tertiary" loading>
|
|
187
|
+
Tertiary
|
|
188
|
+
</Button>
|
|
189
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="outline" loading>
|
|
190
|
+
Outline
|
|
191
|
+
</Button>
|
|
192
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="outline" loading loadingText="Custom...">
|
|
193
|
+
Outline
|
|
194
|
+
</Button>
|
|
195
|
+
<br />
|
|
196
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="primary" size="small">
|
|
197
|
+
Primary
|
|
198
|
+
</Button>
|
|
199
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="secondary" size="small">
|
|
200
|
+
Secondary
|
|
201
|
+
</Button>
|
|
202
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="tertiary" size="small">
|
|
203
|
+
Tertiary
|
|
204
|
+
</Button>
|
|
205
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="outline" size="small">
|
|
206
|
+
Outline
|
|
207
|
+
</Button>
|
|
208
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="primary" disabled size="small">
|
|
209
|
+
Disabled
|
|
210
|
+
</Button>
|
|
211
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="outline" size="small" containsIcon>
|
|
212
|
+
<Icon icon={'link' as IconName} aria-label="Link" />
|
|
213
|
+
</Button>
|
|
214
|
+
<Button ButtonWrapper={StoryLinkWrapper} appearance="outline" size="small">
|
|
215
|
+
<Icon icon={'link' as IconName} />
|
|
216
|
+
Link
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
AnchorWrapper.storyName = 'anchor wrapper';
|
|
222
|
+
|
|
223
|
+
/*
|
|
224
|
+
* New story using the play function.
|
|
225
|
+
* See https://storybook.js.org/docs/react/writing-stories/play-function
|
|
226
|
+
* to learn more about the play function.
|
|
227
|
+
*/
|
|
228
|
+
export const WithInteractions: StoryFn = (_args: Record<string, unknown>) => {
|
|
229
|
+
const args = _args as unknown as ComponentProps<typeof Button>;
|
|
230
|
+
return <Button {...args} />;
|
|
231
|
+
};
|
|
232
|
+
WithInteractions.args = {
|
|
233
|
+
appearance: 'primary',
|
|
234
|
+
href: 'http://storybook.js.org',
|
|
235
|
+
ButtonWrapper: StoryLinkWrapper,
|
|
236
|
+
children: 'Button',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
WithInteractions.play = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
240
|
+
// Assigns canvas to the component root element
|
|
241
|
+
const canvas = within(canvasElement);
|
|
242
|
+
await userEvent.click(canvas.getByRole('link'));
|
|
243
|
+
expect(canvas.getByRole('link')).toHaveAttribute('href', 'http://storybook.js.org');
|
|
244
|
+
};
|
package/src/Button.tsx
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import React, { Fragment, JSX } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { darken, rgba } from 'polished';
|
|
4
|
+
|
|
5
|
+
import { color, typography } from './shared/styles';
|
|
6
|
+
import { easing } from './shared/animation';
|
|
7
|
+
import {
|
|
8
|
+
ButtonProps,
|
|
9
|
+
BUTTON_APPEARANCES,
|
|
10
|
+
BUTTON_SIZES,
|
|
11
|
+
SizeableProps,
|
|
12
|
+
AppearanceProps,
|
|
13
|
+
DisableableProps,
|
|
14
|
+
LoadableProps,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Props for styled button components
|
|
19
|
+
*/
|
|
20
|
+
interface StyledButtonProps extends SizeableProps, AppearanceProps, DisableableProps, LoadableProps {
|
|
21
|
+
isUnclickable?: boolean;
|
|
22
|
+
containsIcon?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const APPEARANCES = BUTTON_APPEARANCES;
|
|
26
|
+
const SIZES = BUTTON_SIZES;
|
|
27
|
+
|
|
28
|
+
const Text = styled.span``;
|
|
29
|
+
|
|
30
|
+
const Loading = styled.span``;
|
|
31
|
+
|
|
32
|
+
const StyledButton = styled.button<StyledButtonProps>`
|
|
33
|
+
border: 0;
|
|
34
|
+
border-radius: 3em;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
display: inline-block;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
padding: ${(props) => (props.size === SIZES.SMALL ? '8px 16px' : '13px 20px')};
|
|
39
|
+
position: relative;
|
|
40
|
+
text-align: center;
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
transition: all 150ms ease-out;
|
|
43
|
+
transform: translate3d(0, 0, 0);
|
|
44
|
+
vertical-align: top;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
user-select: none;
|
|
47
|
+
opacity: 1;
|
|
48
|
+
margin: 0;
|
|
49
|
+
background: transparent;
|
|
50
|
+
|
|
51
|
+
font-size: ${(props) => (props.size === SIZES.SMALL ? typography.size.s1 : typography.size.s2)}px;
|
|
52
|
+
font-weight: ${typography.weight.extrabold};
|
|
53
|
+
line-height: 1;
|
|
54
|
+
|
|
55
|
+
${(props) =>
|
|
56
|
+
!props.isLoading &&
|
|
57
|
+
`
|
|
58
|
+
&:hover {
|
|
59
|
+
transform: translate3d(0, -2px, 0);
|
|
60
|
+
box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&:active {
|
|
64
|
+
transform: translate3d(0, 0, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&:focus {
|
|
68
|
+
box-shadow: ${rgba(color.primary, 0.4)} 0 1px 9px 2px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&:focus:hover {
|
|
72
|
+
box-shadow: ${rgba(color.primary, 0.2)} 0 8px 18px 0px;
|
|
73
|
+
}
|
|
74
|
+
`}
|
|
75
|
+
|
|
76
|
+
${Text} {
|
|
77
|
+
transform: scale3d(1, 1, 1) translate3d(0, 0, 0);
|
|
78
|
+
transition: transform 700ms ${easing.rubber};
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
${Loading} {
|
|
83
|
+
transform: translate3d(0, 100%, 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
svg {
|
|
87
|
+
height: ${(props) => (props.size === SIZES.SMALL ? '14' : '16')}px;
|
|
88
|
+
width: ${(props) => (props.size === SIZES.SMALL ? '14' : '16')}px;
|
|
89
|
+
vertical-align: top;
|
|
90
|
+
margin-right: ${(props) => (props.size === SIZES.SMALL ? '4' : '6')}px;
|
|
91
|
+
margin-top: ${(props) => (props.size === SIZES.SMALL ? '-1' : '-2')}px;
|
|
92
|
+
margin-bottom: ${(props) => (props.size === SIZES.SMALL ? '-1' : '-2')}px;
|
|
93
|
+
|
|
94
|
+
/* Necessary for js mouse events to not glitch out when hovering on svgs */
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
${(props) =>
|
|
99
|
+
props.disabled &&
|
|
100
|
+
`
|
|
101
|
+
cursor: not-allowed !important;
|
|
102
|
+
opacity: 0.5;
|
|
103
|
+
&:hover {
|
|
104
|
+
transform: none;
|
|
105
|
+
}
|
|
106
|
+
`}
|
|
107
|
+
|
|
108
|
+
${(props) =>
|
|
109
|
+
props.isUnclickable &&
|
|
110
|
+
`
|
|
111
|
+
cursor: default !important;
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
&:hover {
|
|
114
|
+
transform: none;
|
|
115
|
+
}
|
|
116
|
+
`}
|
|
117
|
+
|
|
118
|
+
${(props) =>
|
|
119
|
+
props.isLoading &&
|
|
120
|
+
`
|
|
121
|
+
cursor: progress !important;
|
|
122
|
+
opacity: 0.7;
|
|
123
|
+
|
|
124
|
+
${Loading} {
|
|
125
|
+
transition: transform 700ms ${easing.rubber};
|
|
126
|
+
transform: translate3d(0, -50%, 0);
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
${Text} {
|
|
131
|
+
transform: scale3d(0, 0, 1) translate3d(0, -100%, 0);
|
|
132
|
+
opacity: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&:hover {
|
|
136
|
+
transform: none;
|
|
137
|
+
}
|
|
138
|
+
`}
|
|
139
|
+
|
|
140
|
+
${(props) =>
|
|
141
|
+
props.containsIcon &&
|
|
142
|
+
`
|
|
143
|
+
svg {
|
|
144
|
+
display: block;
|
|
145
|
+
margin: 0;
|
|
146
|
+
}
|
|
147
|
+
padding: ${props.size === SIZES.SMALL ? '7' : '12'}px;
|
|
148
|
+
`}
|
|
149
|
+
|
|
150
|
+
${(props) =>
|
|
151
|
+
props.appearance === APPEARANCES.PRIMARY &&
|
|
152
|
+
`
|
|
153
|
+
background: ${color.primary};
|
|
154
|
+
color: ${color.lightest};
|
|
155
|
+
|
|
156
|
+
${
|
|
157
|
+
!props.isLoading &&
|
|
158
|
+
`
|
|
159
|
+
&:hover {
|
|
160
|
+
background: ${darken(0.05, color.primary)};
|
|
161
|
+
}
|
|
162
|
+
&:active {
|
|
163
|
+
box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 3em inset;
|
|
164
|
+
}
|
|
165
|
+
&:focus {
|
|
166
|
+
box-shadow: ${rgba(color.primary, 0.4)} 0 1px 9px 2px;
|
|
167
|
+
}
|
|
168
|
+
&:focus:hover {
|
|
169
|
+
box-shadow: ${rgba(color.primary, 0.2)} 0 8px 18px 0px;
|
|
170
|
+
}
|
|
171
|
+
`
|
|
172
|
+
}
|
|
173
|
+
`}
|
|
174
|
+
|
|
175
|
+
${(props) =>
|
|
176
|
+
props.appearance === APPEARANCES.SECONDARY &&
|
|
177
|
+
`
|
|
178
|
+
background: ${color.secondary};
|
|
179
|
+
color: ${color.lightest};
|
|
180
|
+
|
|
181
|
+
${
|
|
182
|
+
!props.isLoading &&
|
|
183
|
+
`
|
|
184
|
+
&:hover {
|
|
185
|
+
background: ${darken(0.05, color.secondary)};
|
|
186
|
+
}
|
|
187
|
+
&:active {
|
|
188
|
+
box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 3em inset;
|
|
189
|
+
}
|
|
190
|
+
&:focus {
|
|
191
|
+
box-shadow: ${rgba(color.secondary, 0.4)} 0 1px 9px 2px;
|
|
192
|
+
}
|
|
193
|
+
&:focus:hover {
|
|
194
|
+
box-shadow: ${rgba(color.secondary, 0.2)} 0 8px 18px 0px;
|
|
195
|
+
}
|
|
196
|
+
`
|
|
197
|
+
}
|
|
198
|
+
`}
|
|
199
|
+
|
|
200
|
+
${(props) =>
|
|
201
|
+
props.appearance === APPEARANCES.TERTIARY &&
|
|
202
|
+
`
|
|
203
|
+
background: ${color.tertiary};
|
|
204
|
+
color: ${color.darkest};
|
|
205
|
+
|
|
206
|
+
${
|
|
207
|
+
!props.isLoading &&
|
|
208
|
+
`
|
|
209
|
+
&:hover {
|
|
210
|
+
background: ${darken(0.05, color.tertiary)};
|
|
211
|
+
}
|
|
212
|
+
&:active {
|
|
213
|
+
box-shadow: rgba(0, 0, 0, 0.1) 0 0 0 3em inset;
|
|
214
|
+
}
|
|
215
|
+
&:focus {
|
|
216
|
+
box-shadow: ${rgba(color.tertiary, 0.4)} 0 1px 9px 2px;
|
|
217
|
+
}
|
|
218
|
+
&:focus:hover {
|
|
219
|
+
box-shadow: ${rgba(color.tertiary, 0.2)} 0 8px 18px 0px;
|
|
220
|
+
}
|
|
221
|
+
`
|
|
222
|
+
}
|
|
223
|
+
`}
|
|
224
|
+
|
|
225
|
+
${(props) =>
|
|
226
|
+
props.appearance === APPEARANCES.OUTLINE &&
|
|
227
|
+
`
|
|
228
|
+
box-shadow: ${color.medium} 0 0 0 1px inset;
|
|
229
|
+
color: ${color.dark};
|
|
230
|
+
background: transparent;
|
|
231
|
+
|
|
232
|
+
${
|
|
233
|
+
!props.isLoading &&
|
|
234
|
+
`
|
|
235
|
+
&:hover {
|
|
236
|
+
box-shadow: ${color.mediumdark} 0 0 0 1px inset;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
&:active {
|
|
240
|
+
background: ${color.medium};
|
|
241
|
+
box-shadow: ${color.medium} 0 0 0 1px inset;
|
|
242
|
+
color: ${color.darkest};
|
|
243
|
+
}
|
|
244
|
+
&:focus {
|
|
245
|
+
box-shadow: ${color.medium} 0 0 0 1px inset, ${rgba(color.secondary, 0.4)} 0 1px 9px 2px;
|
|
246
|
+
}
|
|
247
|
+
&:focus:hover {
|
|
248
|
+
box-shadow: ${color.medium} 0 0 0 1px inset, ${rgba(color.secondary, 0.2)} 0 8px 18px 0px;
|
|
249
|
+
}
|
|
250
|
+
`
|
|
251
|
+
};
|
|
252
|
+
`};
|
|
253
|
+
|
|
254
|
+
${(props) =>
|
|
255
|
+
props.appearance === APPEARANCES.PRIMARY_OUTLINE &&
|
|
256
|
+
`
|
|
257
|
+
box-shadow: ${color.primary} 0 0 0 1px inset;
|
|
258
|
+
color: ${color.primary};
|
|
259
|
+
|
|
260
|
+
&:hover {
|
|
261
|
+
box-shadow: ${color.primary} 0 0 0 1px inset;
|
|
262
|
+
background: transparent;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
&:active {
|
|
266
|
+
background: ${color.primary};
|
|
267
|
+
box-shadow: ${color.primary} 0 0 0 1px inset;
|
|
268
|
+
color: ${color.lightest};
|
|
269
|
+
}
|
|
270
|
+
&:focus {
|
|
271
|
+
box-shadow: ${color.primary} 0 0 0 1px inset, ${rgba(color.primary, 0.4)} 0 1px 9px 2px;
|
|
272
|
+
}
|
|
273
|
+
&:focus:hover {
|
|
274
|
+
box-shadow: ${color.primary} 0 0 0 1px inset, ${rgba(color.primary, 0.2)} 0 8px 18px 0px;
|
|
275
|
+
}
|
|
276
|
+
`};
|
|
277
|
+
|
|
278
|
+
${(props) =>
|
|
279
|
+
props.appearance === APPEARANCES.SECONDARY_OUTLINE &&
|
|
280
|
+
`
|
|
281
|
+
box-shadow: ${color.secondary} 0 0 0 1px inset;
|
|
282
|
+
color: ${color.secondary};
|
|
283
|
+
|
|
284
|
+
&:hover {
|
|
285
|
+
box-shadow: ${color.secondary} 0 0 0 1px inset;
|
|
286
|
+
background: transparent;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
&:active {
|
|
290
|
+
background: ${color.secondary};
|
|
291
|
+
box-shadow: ${color.secondary} 0 0 0 1px inset;
|
|
292
|
+
color: ${color.lightest};
|
|
293
|
+
}
|
|
294
|
+
&:focus {
|
|
295
|
+
box-shadow: ${color.secondary} 0 0 0 1px inset,
|
|
296
|
+
${rgba(color.secondary, 0.4)} 0 1px 9px 2px;
|
|
297
|
+
}
|
|
298
|
+
&:focus:hover {
|
|
299
|
+
box-shadow: ${color.secondary} 0 0 0 1px inset,
|
|
300
|
+
${rgba(color.secondary, 0.2)} 0 8px 18px 0px;
|
|
301
|
+
}
|
|
302
|
+
`};
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
type WrapperProps = Record<string, unknown> & { children?: React.ReactNode };
|
|
306
|
+
|
|
307
|
+
const ButtonLink: React.ComponentType<WrapperProps> = (props) => <StyledButton as="a" {...props} />;
|
|
308
|
+
|
|
309
|
+
const applyStyle = (ButtonWrapper?: ButtonProps['ButtonWrapper']): React.ComponentType<WrapperProps> | undefined => {
|
|
310
|
+
if (!ButtonWrapper) return undefined;
|
|
311
|
+
|
|
312
|
+
const Wrapped = ({
|
|
313
|
+
containsIcon: _containsIcon,
|
|
314
|
+
isLoading: _isLoading,
|
|
315
|
+
isUnclickable: _isUnclickable,
|
|
316
|
+
children: _children,
|
|
317
|
+
...rest
|
|
318
|
+
}: WrapperProps): JSX.Element => <ButtonWrapper {...(rest as Record<string, unknown>)}>{_children}</ButtonWrapper>;
|
|
319
|
+
|
|
320
|
+
return styled(StyledButton).attrs({ as: Wrapped })`` as unknown as React.ComponentType<WrapperProps>;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Button component with multiple appearance and size variants
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```tsx
|
|
328
|
+
* <Button>Default Button</Button>
|
|
329
|
+
* <Button appearance="primary" size="large">Primary Large</Button>
|
|
330
|
+
* <Button loading loadingText="Saving...">Save</Button>
|
|
331
|
+
* <Button disabled>Disabled</Button>
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
export const Button = ({
|
|
335
|
+
isLoading = false,
|
|
336
|
+
loadingText,
|
|
337
|
+
isLink = false,
|
|
338
|
+
appearance = APPEARANCES.TERTIARY,
|
|
339
|
+
isDisabled = false,
|
|
340
|
+
isUnclickable = false,
|
|
341
|
+
containsIcon = false,
|
|
342
|
+
size = SIZES.MEDIUM,
|
|
343
|
+
children,
|
|
344
|
+
ButtonWrapper,
|
|
345
|
+
...props
|
|
346
|
+
}: ButtonProps) => {
|
|
347
|
+
// Dev-time accessibility check: if the button is icon-only, require an accessible name
|
|
348
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
349
|
+
const hasAriaLabel = Object.prototype.hasOwnProperty.call(props, 'aria-label');
|
|
350
|
+
const noVisibleChildren = !children || (typeof children === 'string' && children.trim() === '');
|
|
351
|
+
if (containsIcon && noVisibleChildren && !hasAriaLabel) {
|
|
352
|
+
console.warn('Button: icon-only buttons should include an `aria-label` or visible text for accessibility.');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const buttonInner = (
|
|
356
|
+
<Fragment>
|
|
357
|
+
<Text>{children}</Text>
|
|
358
|
+
{isLoading && <Loading>{loadingText || 'Loading...'}</Loading>}
|
|
359
|
+
</Fragment>
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const StyledButtonWrapper = React.useMemo(() => applyStyle(ButtonWrapper), [ButtonWrapper]);
|
|
363
|
+
|
|
364
|
+
let SelectedButton: React.ComponentType<WrapperProps> = StyledButton as unknown as React.ComponentType<WrapperProps>;
|
|
365
|
+
if (ButtonWrapper && StyledButtonWrapper) {
|
|
366
|
+
SelectedButton = StyledButtonWrapper;
|
|
367
|
+
} else if (isLink) {
|
|
368
|
+
SelectedButton = ButtonLink;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<SelectedButton
|
|
373
|
+
size={size}
|
|
374
|
+
appearance={appearance}
|
|
375
|
+
isLoading={isLoading}
|
|
376
|
+
disabled={isDisabled}
|
|
377
|
+
isUnclickable={isUnclickable}
|
|
378
|
+
containsIcon={containsIcon}
|
|
379
|
+
{...props}
|
|
380
|
+
>
|
|
381
|
+
{buttonInner}
|
|
382
|
+
</SelectedButton>
|
|
383
|
+
);
|
|
384
|
+
};
|