@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.
Files changed (88) hide show
  1. package/.github/CODEOWNERS +2 -0
  2. package/.github/CONTRIBUTING.md +38 -0
  3. package/.github/FUNDING.yml +4 -0
  4. package/.github/PULL_REQUEST_TEMPLATE/build.md +5 -0
  5. package/.github/PULL_REQUEST_TEMPLATE/standard.md +3 -0
  6. package/.github/RELEASING.md +37 -0
  7. package/.github/copilot-instructions.md +93 -0
  8. package/.github/workflows/ci.yml +82 -0
  9. package/.github/workflows/codeql-analysis.yml +34 -0
  10. package/.github/workflows/release-please.yml +53 -0
  11. package/.husky/pre-commit +1 -0
  12. package/.nvmrc +1 -0
  13. package/.prettierignore +1 -0
  14. package/.storybook/.preview-head.html +1 -0
  15. package/.storybook/main.ts +38 -0
  16. package/.storybook/preview.tsx +30 -0
  17. package/.tool-versions +1 -0
  18. package/.vscode/launch.json +22 -0
  19. package/.vscode/settings.json +30 -0
  20. package/.yarn/releases/yarn-4.12.0.cjs +942 -0
  21. package/.yarnrc.yml +7 -0
  22. package/CHANGELOG.md +490 -0
  23. package/LICENSE +21 -0
  24. package/README.md +116 -0
  25. package/SECURITY.md +9 -0
  26. package/babel.config.js +3 -0
  27. package/dist/favicon.ico +0 -0
  28. package/dist/index.demo.html +40 -0
  29. package/dist/index.js +647 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/index.mjs +1053 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/logo192.png +0 -0
  34. package/dist/logo512.png +0 -0
  35. package/dist/manifest.json +25 -0
  36. package/dist/robots.txt +2 -0
  37. package/dist/vendor-DXgJBoQh.mjs +265 -0
  38. package/dist/vendor-DXgJBoQh.mjs.map +1 -0
  39. package/dist/vendor-nZSsnGb7.js +7 -0
  40. package/dist/vendor-nZSsnGb7.js.map +1 -0
  41. package/docs/MIGRATE_TO_GVTECH_SCOPE.md +74 -0
  42. package/eslint.config.mjs +95 -0
  43. package/netlify.toml +6 -0
  44. package/package.json +130 -0
  45. package/public/favicon.ico +0 -0
  46. package/public/index.demo.html +40 -0
  47. package/public/logo192.png +0 -0
  48. package/public/logo512.png +0 -0
  49. package/public/manifest.json +25 -0
  50. package/public/robots.txt +2 -0
  51. package/scripts/validate.js +56 -0
  52. package/serve.json +4 -0
  53. package/src/Avatar.stories.tsx +67 -0
  54. package/src/Avatar.tsx +174 -0
  55. package/src/Badge.stories.tsx +87 -0
  56. package/src/Badge.tsx +76 -0
  57. package/src/Button.stories.tsx +244 -0
  58. package/src/Button.tsx +384 -0
  59. package/src/Icon.stories.tsx +101 -0
  60. package/src/Icon.tsx +64 -0
  61. package/src/Intro.stories.tsx +20 -0
  62. package/src/Link.stories.tsx +69 -0
  63. package/src/Link.tsx +252 -0
  64. package/src/StoryLinkWrapper.d.ts +1 -0
  65. package/src/StoryLinkWrapper.tsx +33 -0
  66. package/src/__tests__/Avatar.test.tsx +28 -0
  67. package/src/__tests__/Badge.test.tsx +25 -0
  68. package/src/__tests__/Button.test.tsx +38 -0
  69. package/src/__tests__/Icon.test.tsx +26 -0
  70. package/src/__tests__/Link.test.tsx +31 -0
  71. package/src/index.ts +13 -0
  72. package/src/mdx.d.ts +5 -0
  73. package/src/setupTests.ts +1 -0
  74. package/src/shared/animation.d.ts +18 -0
  75. package/src/shared/animation.js +60 -0
  76. package/src/shared/global.d.ts +12 -0
  77. package/src/shared/global.js +120 -0
  78. package/src/shared/icons.d.ts +34 -0
  79. package/src/shared/icons.js +282 -0
  80. package/src/shared/styles.d.ts +86 -0
  81. package/src/shared/styles.js +98 -0
  82. package/src/test-utils/axe.ts +25 -0
  83. package/src/types.ts +316 -0
  84. package/tsconfig.build.json +12 -0
  85. package/tsconfig.json +20 -0
  86. package/tsconfig.node.json +10 -0
  87. package/vite.config.ts +35 -0
  88. 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
+ };