@arbor-education/design-system.components 0.0.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 (141) hide show
  1. package/.checkov.yml +10 -0
  2. package/.github/CODEOWNERS +7 -0
  3. package/.github/workflows/chromatic.yml +52 -0
  4. package/.github/workflows/pr-checks.yml +18 -0
  5. package/.github/workflows/pr-housekeeping.yaml +16 -0
  6. package/.husky/pre-commit +2 -0
  7. package/.nvmrc +1 -0
  8. package/.pre-commit-config.yaml +28 -0
  9. package/.storybook/main.ts +21 -0
  10. package/.storybook/preview.ts +23 -0
  11. package/.storybook/vitest.setup.ts +7 -0
  12. package/.tflint.hcl +13 -0
  13. package/CODEOWNERS +5 -0
  14. package/README.md +21 -0
  15. package/bin/createComponent.sh +96 -0
  16. package/dist/components/button/Button.d.ts +10 -0
  17. package/dist/components/button/Button.d.ts.map +1 -0
  18. package/dist/components/button/Button.js +8 -0
  19. package/dist/components/button/Button.js.map +1 -0
  20. package/dist/components/card/Card.d.ts +18 -0
  21. package/dist/components/card/Card.d.ts.map +1 -0
  22. package/dist/components/card/Card.js +31 -0
  23. package/dist/components/card/Card.js.map +1 -0
  24. package/dist/components/card/Card.stories.d.ts +86 -0
  25. package/dist/components/card/Card.stories.d.ts.map +1 -0
  26. package/dist/components/card/Card.stories.js +89 -0
  27. package/dist/components/card/Card.stories.js.map +1 -0
  28. package/dist/components/heading/Heading.d.ts +393 -0
  29. package/dist/components/heading/Heading.d.ts.map +1 -0
  30. package/dist/components/heading/Heading.js +12 -0
  31. package/dist/components/heading/Heading.js.map +1 -0
  32. package/dist/components/heading/Heading.stories.d.ts +35 -0
  33. package/dist/components/heading/Heading.stories.d.ts.map +1 -0
  34. package/dist/components/heading/Heading.stories.js +50 -0
  35. package/dist/components/heading/Heading.stories.js.map +1 -0
  36. package/dist/components/heading/HeadingInnerContainer.d.ts +5 -0
  37. package/dist/components/heading/HeadingInnerContainer.d.ts.map +1 -0
  38. package/dist/components/heading/HeadingInnerContainer.js +7 -0
  39. package/dist/components/heading/HeadingInnerContainer.js.map +1 -0
  40. package/dist/components/icon/Icon.d.ts +13 -0
  41. package/dist/components/icon/Icon.d.ts.map +1 -0
  42. package/dist/components/icon/Icon.js +10 -0
  43. package/dist/components/icon/Icon.js.map +1 -0
  44. package/dist/components/icon/Icon.stories.d.ts +11 -0
  45. package/dist/components/icon/Icon.stories.d.ts.map +1 -0
  46. package/dist/components/icon/Icon.stories.js +13 -0
  47. package/dist/components/icon/Icon.stories.js.map +1 -0
  48. package/dist/components/icon/allowedIcons.d.ts +110 -0
  49. package/dist/components/icon/allowedIcons.d.ts.map +1 -0
  50. package/dist/components/icon/allowedIcons.js +117 -0
  51. package/dist/components/icon/allowedIcons.js.map +1 -0
  52. package/dist/components/icon/customIcons/AskArbor.d.ts +3 -0
  53. package/dist/components/icon/customIcons/AskArbor.d.ts.map +1 -0
  54. package/dist/components/icon/customIcons/AskArbor.js +6 -0
  55. package/dist/components/icon/customIcons/AskArbor.js.map +1 -0
  56. package/dist/components/icon/customIcons/CheckSolid.d.ts +3 -0
  57. package/dist/components/icon/customIcons/CheckSolid.d.ts.map +1 -0
  58. package/dist/components/icon/customIcons/CheckSolid.js +9 -0
  59. package/dist/components/icon/customIcons/CheckSolid.js.map +1 -0
  60. package/dist/components/icon/customIcons/Google.d.ts +3 -0
  61. package/dist/components/icon/customIcons/Google.d.ts.map +1 -0
  62. package/dist/components/icon/customIcons/Google.js +7 -0
  63. package/dist/components/icon/customIcons/Google.js.map +1 -0
  64. package/dist/components/icon/customIcons/XSolid.d.ts +3 -0
  65. package/dist/components/icon/customIcons/XSolid.d.ts.map +1 -0
  66. package/dist/components/icon/customIcons/XSolid.js +8 -0
  67. package/dist/components/icon/customIcons/XSolid.js.map +1 -0
  68. package/dist/components/icon/types.d.ts +8 -0
  69. package/dist/components/icon/types.d.ts.map +1 -0
  70. package/dist/components/icon/types.js +2 -0
  71. package/dist/components/icon/types.js.map +1 -0
  72. package/dist/components/pill/Pill.d.ts +8 -0
  73. package/dist/components/pill/Pill.d.ts.map +1 -0
  74. package/dist/components/pill/Pill.js +6 -0
  75. package/dist/components/pill/Pill.js.map +1 -0
  76. package/dist/components/pill/Pill.stories.d.ts +8 -0
  77. package/dist/components/pill/Pill.stories.d.ts.map +1 -0
  78. package/dist/components/pill/Pill.stories.js +10 -0
  79. package/dist/components/pill/Pill.stories.js.map +1 -0
  80. package/dist/index.css +1557 -0
  81. package/dist/index.css.map +1 -0
  82. package/dist/index.d.ts +7 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +7 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/utils/generateUuid.d.ts +2 -0
  87. package/dist/utils/generateUuid.d.ts.map +1 -0
  88. package/dist/utils/generateUuid.js +10 -0
  89. package/dist/utils/generateUuid.js.map +1 -0
  90. package/dist/utils/hooks/useMemoGenerateUuid.d.ts +8 -0
  91. package/dist/utils/hooks/useMemoGenerateUuid.d.ts.map +1 -0
  92. package/dist/utils/hooks/useMemoGenerateUuid.js +8 -0
  93. package/dist/utils/hooks/useMemoGenerateUuid.js.map +1 -0
  94. package/dist/utils/keyboardConstants.d.ts +13 -0
  95. package/dist/utils/keyboardConstants.d.ts.map +1 -0
  96. package/dist/utils/keyboardConstants.js +13 -0
  97. package/dist/utils/keyboardConstants.js.map +1 -0
  98. package/dist/utils/waitForElement.d.ts +2 -0
  99. package/dist/utils/waitForElement.d.ts.map +1 -0
  100. package/dist/utils/waitForElement.js +18 -0
  101. package/dist/utils/waitForElement.js.map +1 -0
  102. package/eslint.config.mts +30 -0
  103. package/package.json +76 -0
  104. package/src/components/button/Button.story.tsx +116 -0
  105. package/src/components/button/Button.test.tsx +49 -0
  106. package/src/components/button/Button.tsx +37 -0
  107. package/src/components/button/button.scss +181 -0
  108. package/src/components/card/Card.stories.tsx +99 -0
  109. package/src/components/card/Card.test.tsx +231 -0
  110. package/src/components/card/Card.tsx +96 -0
  111. package/src/components/card/card.scss +68 -0
  112. package/src/components/heading/Heading.stories.tsx +85 -0
  113. package/src/components/heading/Heading.test.tsx +29 -0
  114. package/src/components/heading/Heading.tsx +17 -0
  115. package/src/components/heading/HeadingInnerContainer.tsx +18 -0
  116. package/src/components/heading/heading.scss +48 -0
  117. package/src/components/icon/Icon.stories.tsx +16 -0
  118. package/src/components/icon/Icon.test.tsx +17 -0
  119. package/src/components/icon/Icon.tsx +27 -0
  120. package/src/components/icon/allowedIcons.tsx +208 -0
  121. package/src/components/icon/customIcons/AskArbor.tsx +28 -0
  122. package/src/components/icon/customIcons/CheckSolid.tsx +43 -0
  123. package/src/components/icon/customIcons/Google.tsx +33 -0
  124. package/src/components/icon/customIcons/XSolid.tsx +32 -0
  125. package/src/components/icon/types.ts +8 -0
  126. package/src/components/pill/Pill.stories.tsx +14 -0
  127. package/src/components/pill/Pill.test.tsx +21 -0
  128. package/src/components/pill/Pill.tsx +24 -0
  129. package/src/components/pill/pill.scss +51 -0
  130. package/src/global.scss +19 -0
  131. package/src/index.scss +7 -0
  132. package/src/index.ts +6 -0
  133. package/src/tokens.scss +1249 -0
  134. package/src/utils/generateUuid.ts +9 -0
  135. package/src/utils/hooks/useMemoGenerateUuid.ts +13 -0
  136. package/src/utils/keyboardConstants.ts +12 -0
  137. package/src/utils/waitForElement.ts +22 -0
  138. package/stylelint.config.mjs +10 -0
  139. package/tsconfig.json +49 -0
  140. package/vitest.config.ts +26 -0
  141. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,99 @@
1
+ import type { Meta } from '@storybook/react-vite';
2
+ import { Card } from './Card';
3
+ import { fn } from 'storybook/test';
4
+
5
+ const meta: Meta<typeof Card> = {
6
+ title: 'Components/Card',
7
+ component: Card,
8
+ };
9
+
10
+ export const CardWithTitleAndParagraph = {
11
+ args: {
12
+ title: 'Title of Card',
13
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
14
+ disabled: false,
15
+ onClick: fn(),
16
+ onKeyDown: fn(),
17
+ },
18
+ };
19
+
20
+ export const CardWithTitleParagraphAndIcon = {
21
+ args: {
22
+ title: 'Title of Card',
23
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
24
+ icon: 'eye',
25
+ disabled: false,
26
+ onClick: fn(),
27
+ onKeyDown: fn(),
28
+ },
29
+ };
30
+
31
+ export const TheEverythingCard = {
32
+ args: {
33
+ title: 'Title of Card',
34
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
35
+ icon: 'eye',
36
+ disabled: false,
37
+ pillText: 'argle bargle',
38
+ pillColor: 'orange',
39
+ onClick: fn(),
40
+ onKeyDown: fn(),
41
+ },
42
+ };
43
+
44
+ export const CardWithTitleParagraphAndPill = {
45
+ args: {
46
+ title: 'Title of Card',
47
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
48
+ disabled: false,
49
+ pillText: 'argle bargle',
50
+ pillColor: 'orange',
51
+ onClick: fn(),
52
+ onKeyDown: fn(),
53
+ },
54
+ };
55
+
56
+ export const CardWithTitleAndIcon = {
57
+ args: {
58
+ title: 'Title of Card',
59
+ icon: 'eye',
60
+ disabled: false,
61
+ onClick: fn(),
62
+ onKeyDown: fn(),
63
+ },
64
+ };
65
+
66
+ export const CardWithParagraph = {
67
+ args: {
68
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
69
+ disabled: false,
70
+ onClick: fn(),
71
+ onKeyDown: fn(),
72
+ },
73
+ };
74
+
75
+ export const ClickableDisabledCard = {
76
+ args: {
77
+ title: 'Title of Card',
78
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
79
+ icon: 'eye',
80
+ disabled: true,
81
+ pillText: 'argle bargle',
82
+ pillColor: 'orange',
83
+ onClick: fn(),
84
+ onKeyDown: fn(),
85
+ },
86
+ };
87
+
88
+ export const UnclickableCard = {
89
+ args: {
90
+ title: 'Title of Card',
91
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
92
+ icon: 'eye',
93
+ disabled: true,
94
+ pillText: 'argle bargle',
95
+ pillColor: 'orange',
96
+ },
97
+ };
98
+
99
+ export default meta;
@@ -0,0 +1,231 @@
1
+ import React from 'react';
2
+ import { expect, test, describe, vi } from 'vitest';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import { Card } from './Card';
5
+ import '@testing-library/jest-dom/vitest';
6
+
7
+ describe('Card Component', () => {
8
+ test('renders empty card without any props', () => {
9
+ render(<Card />);
10
+ const card = screen.getByRole('article');
11
+ expect(card).toBeInTheDocument();
12
+ expect(card).toHaveAttribute('aria-label', 'Card');
13
+ });
14
+
15
+ test('renders card with title', () => {
16
+ render(<Card title="Test Title" />);
17
+ expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Test Title');
18
+ expect(screen.getByText('Test Title')).toHaveClass('ds-card__title');
19
+ });
20
+
21
+ test('renders card with paragraph', () => {
22
+ render(<Card paragraph="Test paragraph content" />);
23
+ expect(screen.getByText('Test paragraph content')).toBeInTheDocument();
24
+ expect(screen.getByText('Test paragraph content')).toHaveClass('ds-card__paragraph');
25
+ });
26
+
27
+ test('renders card with both title and paragraph', () => {
28
+ render(<Card title="Test Title" paragraph="Test paragraph" />);
29
+ expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Test Title');
30
+ expect(screen.getByText('Test paragraph')).toBeInTheDocument();
31
+ });
32
+
33
+ test('renders card with icon', () => {
34
+ const { container } = render(<Card icon="eye" />);
35
+ expect(container.querySelector('.ds-icon-eye')).toBeInTheDocument();
36
+ });
37
+
38
+ test('renders card with pill', () => {
39
+ render(<Card pillText="Test Pill" pillColor="orange" />);
40
+ expect(screen.getByText('Test Pill')).toBeInTheDocument();
41
+ });
42
+
43
+ test('renders complete card with all props', () => {
44
+ const mockClick = vi.fn();
45
+ const mockKeyDown = vi.fn();
46
+
47
+ const { container } = render(
48
+ <Card
49
+ title="Complete Card"
50
+ paragraph="This is a complete card"
51
+ icon="eye"
52
+ iconColor="#blue"
53
+ pillText="Complete"
54
+ pillColor="green"
55
+ onClick={mockClick}
56
+ onKeyDown={mockKeyDown}
57
+ />,
58
+ );
59
+
60
+ expect(screen.getByText('Complete Card')).toBeInTheDocument();
61
+ expect(screen.getByText('This is a complete card')).toBeInTheDocument();
62
+ expect(container.querySelector('.ds-icon-eye')).toBeInTheDocument();
63
+ expect(screen.getByText('Complete')).toBeInTheDocument();
64
+ });
65
+
66
+ describe('Click interactions', () => {
67
+ test('calls onClick handler when card is clicked', () => {
68
+ const mockClick = vi.fn();
69
+ render(<Card title="Clickable Card" onClick={mockClick} />);
70
+
71
+ const card = screen.getByRole('article');
72
+ fireEvent.click(card);
73
+
74
+ expect(mockClick).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ test('does not call onClick when card is disabled', () => {
78
+ const mockClick = vi.fn();
79
+ render(<Card title="Disabled Card" onClick={mockClick} disabled />);
80
+
81
+ const card = screen.getByRole('article');
82
+ fireEvent.click(card);
83
+
84
+ expect(mockClick).not.toHaveBeenCalled();
85
+ });
86
+
87
+ test('shows click arrow icon when card is clickable', () => {
88
+ const mockClick = vi.fn();
89
+ const { container } = render(<Card title="Clickable Card" onClick={mockClick} />);
90
+
91
+ expect(container.querySelector('.ds-icon-chevron-right')).toBeInTheDocument();
92
+ });
93
+
94
+ test('does not show click arrow icon when card is not clickable', () => {
95
+ const { container } = render(<Card title="Non-clickable Card" />);
96
+
97
+ expect(container.querySelector('.ds-icon-chevron-right')).not.toBeInTheDocument();
98
+ });
99
+
100
+ test('does not show click arrow icon when card is disabled', () => {
101
+ const mockClick = vi.fn();
102
+ const { container } = render(<Card title="Disabled Card" onClick={mockClick} disabled />);
103
+
104
+ expect(container.querySelector('.ds-icon-chevron-right')).not.toBeInTheDocument();
105
+ });
106
+
107
+ test('uses arrow type when specified', () => {
108
+ const mockClick = vi.fn();
109
+ const { container } = render(<Card title="Clickable Card" onClick={mockClick} arrowType="arrow" />);
110
+ expect(container.querySelector('.ds-icon-arrow-right')).toBeInTheDocument();
111
+ });
112
+ });
113
+
114
+ describe('Keyboard interactions', () => {
115
+ test('calls onKeyDown handler when key is pressed on clickable card', () => {
116
+ const mockKeyDown = vi.fn();
117
+ render(<Card title="Keyboard Card" onClick={vi.fn()} onKeyDown={mockKeyDown} />);
118
+
119
+ const card = screen.getByRole('article');
120
+ fireEvent.keyDown(card, { key: 'Enter' });
121
+
122
+ expect(mockKeyDown).toHaveBeenCalledTimes(1);
123
+ });
124
+
125
+ test('does not call onKeyDown when card is disabled', () => {
126
+ const mockKeyDown = vi.fn();
127
+ render(<Card title="Disabled Card" onClick={vi.fn()} onKeyDown={mockKeyDown} disabled />);
128
+
129
+ const card = screen.getByRole('article');
130
+ fireEvent.keyDown(card, { key: 'Enter' });
131
+
132
+ expect(mockKeyDown).not.toHaveBeenCalled();
133
+ });
134
+
135
+ test('does not call onKeyDown when card is not clickable', () => {
136
+ const mockKeyDown = vi.fn();
137
+ render(<Card title="Non-clickable Card" onKeyDown={mockKeyDown} />);
138
+
139
+ const card = screen.getByRole('article');
140
+ fireEvent.keyDown(card, { key: 'Enter' });
141
+
142
+ expect(mockKeyDown).not.toHaveBeenCalled();
143
+ });
144
+ });
145
+
146
+ describe('CSS classes and accessibility', () => {
147
+ test('applies clickable class when onClick is provided and not disabled', () => {
148
+ const mockClick = vi.fn();
149
+ render(<Card onClick={mockClick} />);
150
+
151
+ const card = screen.getByRole('article');
152
+ expect(card).toHaveClass('ds-card__container--clickable');
153
+ });
154
+
155
+ test('applies disabled class when disabled', () => {
156
+ render(<Card disabled />);
157
+
158
+ const card = screen.getByRole('article');
159
+ expect(card).toHaveClass('ds-card__container--disabled');
160
+ });
161
+
162
+ test('does not apply clickable class when disabled', () => {
163
+ const mockClick = vi.fn();
164
+ render(<Card onClick={mockClick} disabled />);
165
+
166
+ const card = screen.getByRole('article');
167
+ expect(card).not.toHaveClass('ds-card__container--clickable');
168
+ expect(card).toHaveClass('ds-card__container--disabled');
169
+ });
170
+
171
+ test('sets correct tabIndex for clickable card', () => {
172
+ const mockClick = vi.fn();
173
+ render(<Card onClick={mockClick} />);
174
+
175
+ const card = screen.getByRole('article');
176
+ expect(card).toHaveAttribute('tabIndex', '0');
177
+ });
178
+
179
+ test('sets correct tabIndex for non-clickable card', () => {
180
+ render(<Card />);
181
+
182
+ const card = screen.getByRole('article');
183
+ expect(card).toHaveAttribute('tabIndex', '-1');
184
+ });
185
+
186
+ test('sets correct tabIndex for disabled card', () => {
187
+ const mockClick = vi.fn();
188
+ render(<Card onClick={mockClick} disabled />);
189
+
190
+ const card = screen.getByRole('article');
191
+ expect(card).toHaveAttribute('tabIndex', '-1');
192
+ });
193
+
194
+ test('has correct aria-label', () => {
195
+ render(<Card />);
196
+
197
+ const card = screen.getByRole('article');
198
+ expect(card).toHaveAttribute('aria-label', 'Card');
199
+ });
200
+ });
201
+
202
+ describe('Event handler edge cases', () => {
203
+ test('handles onClick without onKeyDown', () => {
204
+ const mockClick = vi.fn();
205
+ render(<Card title="Click only" onClick={mockClick} />);
206
+
207
+ const card = screen.getByRole('article');
208
+ fireEvent.click(card);
209
+
210
+ expect(mockClick).toHaveBeenCalledTimes(1);
211
+ });
212
+
213
+ test('handles onKeyDown without onClick', () => {
214
+ const mockKeyDown = vi.fn();
215
+ render(<Card title="KeyDown only" onKeyDown={mockKeyDown} />);
216
+
217
+ const card = screen.getByRole('article');
218
+ fireEvent.keyDown(card, { key: 'Enter' });
219
+
220
+ // Should not be called because card is not clickable (no onClick)
221
+ expect(mockKeyDown).not.toHaveBeenCalled();
222
+ });
223
+
224
+ test('does not throw error when clicking card without handlers', () => {
225
+ render(<Card title="No handlers" />);
226
+
227
+ const card = screen.getByRole('article');
228
+ expect(() => fireEvent.click(card)).not.toThrow();
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,96 @@
1
+ import classNames from 'classnames';
2
+
3
+ import { Icon } from '../icon/Icon';
4
+ import { allowedIcons } from '../icon/allowedIcons';
5
+ import { Pill, type PillColor } from '../pill/Pill';
6
+
7
+ type CardProps = {
8
+ title?: string;
9
+ paragraph?: string;
10
+ icon?: keyof typeof allowedIcons;
11
+ iconColor?: string;
12
+ disabled?: boolean;
13
+ pillText?: string;
14
+ pillColor?: PillColor;
15
+ onClick?: (e: React.MouseEvent<HTMLElement>) => void;
16
+ onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
17
+ arrowType?: 'chevron' | 'arrow';
18
+ iconScreenReaderText?: string;
19
+ };
20
+
21
+ export const Card = ({
22
+ title,
23
+ paragraph,
24
+ icon,
25
+ iconColor,
26
+ iconScreenReaderText,
27
+ onClick,
28
+ onKeyDown,
29
+ disabled,
30
+ pillText,
31
+ pillColor,
32
+ arrowType = 'chevron',
33
+ }: CardProps) => {
34
+ const handleClick = (e: React.MouseEvent<HTMLElement>) => {
35
+ if (onClick) {
36
+ onClick(e);
37
+ }
38
+ };
39
+
40
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
41
+ if (onKeyDown) {
42
+ onKeyDown(e);
43
+ }
44
+ };
45
+
46
+ const isCardClickable = onClick && !disabled;
47
+
48
+ return (
49
+ <article
50
+ className={classNames('ds-card__container', {
51
+ 'ds-card__container--clickable': isCardClickable,
52
+ 'ds-card__container--disabled': disabled,
53
+ })}
54
+ onClick={(e) => {
55
+ if (isCardClickable) {
56
+ handleClick(e);
57
+ }
58
+ }}
59
+ onKeyDown={(e) => {
60
+ if (isCardClickable) {
61
+ handleKeyDown(e);
62
+ }
63
+ }}
64
+ aria-label="Card"
65
+ tabIndex={isCardClickable ? 0 : -1}
66
+ >
67
+ <div className="ds-card__content">
68
+ {icon && (
69
+ <Icon
70
+ name={icon}
71
+ className="ds-card__icon-left"
72
+ screenReaderText={iconScreenReaderText}
73
+ color={iconColor}
74
+ size={24}
75
+ />
76
+ )}
77
+ <div className="ds-card__text">
78
+ {title && (
79
+ <span className="ds-card__title-container">
80
+ {title && <h4 className="ds-card__title">{title}</h4>}
81
+ </span>
82
+ )}
83
+ {paragraph && <p className="ds-card__paragraph">{paragraph}</p>}
84
+ {pillText && <Pill text={pillText} color={pillColor} />}
85
+ </div>
86
+ {isCardClickable && (
87
+ <Icon
88
+ name={`${arrowType}-right`}
89
+ className="ds-card__icon-click"
90
+ size={24}
91
+ />
92
+ )}
93
+ </div>
94
+ </article>
95
+ );
96
+ };
@@ -0,0 +1,68 @@
1
+ .ds-card__container {
2
+ display: flex;
3
+ width: 348px;
4
+ padding: var(--card-spacing-y, 24px) var(--card-spacing-x, 24px);
5
+ flex-direction: column;
6
+ align-items: flex-start;
7
+ gap: var(--card-spacing-gap-y, 16px);
8
+ border-radius: var(--card-radius, 8px);
9
+ border: 1px solid var(--card-default-color-border, #efefef);
10
+ background: var(--card-default-color-background, #fff);
11
+ color: var(--card-default-color-text);
12
+ line-height: 1.5;
13
+
14
+ &:hover:not(.ds-card__container--disabled) {
15
+ border: 1px solid var(--card-hover-color-border, #efefef);
16
+ background: var(--card-hover-color-background, #fff);
17
+ box-shadow: 0 4px 12px 0 rgb(32 32 32 / 8%);
18
+ }
19
+
20
+ &:focus:not(.ds-card__container--disabled) {
21
+ border: 1px solid var(--card-focus-color-border, #efefef);
22
+ background: var(--card-focus-color-background, #fff);
23
+ box-shadow: 0 0 0 3px
24
+ var(--button-medium-primary-focus-color-focus, #3cad51);
25
+ }
26
+
27
+ &--clickable {
28
+ cursor: pointer;
29
+ }
30
+ }
31
+
32
+ .ds-card__content {
33
+ display: flex;
34
+ flex-direction: row;
35
+ align-items: flex-start;
36
+ gap: var(--spacing-large, 16px);
37
+ width: 100%;
38
+
39
+ .ds-card__icon-left {
40
+ flex-shrink: 0;
41
+ }
42
+
43
+ .ds-card__text {
44
+ display: flex;
45
+ flex-direction: column;
46
+ justify-content: space-between;
47
+ gap: var(--card-spacing-gap-x);
48
+ flex-grow: 1;
49
+
50
+ .ds-card__title {
51
+ margin: 0;
52
+ font-family: var(--type-headings-h4-family);
53
+ font-size: var(--type-headings-h4-size);
54
+ font-weight: var(--type-headings-h4-weight);
55
+ }
56
+
57
+ .ds-card__paragraph {
58
+ margin: 0;
59
+ font-family: var(--type-body-p-family);
60
+ font-size: var(--type-body-p-size);
61
+ font-weight: var(--type-body-p-weight);
62
+ }
63
+ }
64
+
65
+ .ds-card__icon-click {
66
+ flex-shrink: 0;
67
+ }
68
+ }
@@ -0,0 +1,85 @@
1
+ import type { Meta } from '@storybook/react-vite';
2
+ import { Heading } from './Heading';
3
+ import { Button } from '../button/Button';
4
+ import { Icon } from '../icon/Icon';
5
+ import { HeadingInnerContainer } from './HeadingInnerContainer';
6
+
7
+ const meta: Meta<typeof Heading> = {
8
+ title: 'Components/Heading',
9
+ component: Heading,
10
+ };
11
+
12
+ export const Default = {
13
+ args: {
14
+ children: ['Heading Text'],
15
+ level: 1,
16
+ },
17
+ };
18
+
19
+ export const FloatingChildren = {
20
+ args: {
21
+ children: [<HeadingInnerContainer key={1}>Text floating left</HeadingInnerContainer>, <span key={2}>Text floating right</span>],
22
+ level: 1,
23
+ },
24
+ };
25
+
26
+ export const HeadingWithSingleButton = {
27
+ args: {
28
+ level: 1,
29
+ children: [
30
+ <HeadingInnerContainer key={1}>Heading</HeadingInnerContainer>,
31
+ <HeadingInnerContainer key={2}>
32
+ <Button>
33
+ <Icon name="info" />
34
+ Button Text
35
+ </Button>
36
+ </HeadingInnerContainer>,
37
+ ],
38
+ },
39
+ };
40
+
41
+ export const HeadingWithMultipleButtons = {
42
+ args: {
43
+ level: 1,
44
+ children: [
45
+ <HeadingInnerContainer key={1}>Heading</HeadingInnerContainer>,
46
+ <HeadingInnerContainer key={2} className="medium-spacing-gap">
47
+ <Button type="secondary">
48
+ <Icon name="info" />
49
+ Button Text
50
+ </Button>
51
+ <Button>
52
+ <Icon name="info" />
53
+ Button Text
54
+ </Button>
55
+ </HeadingInnerContainer>,
56
+ ],
57
+ },
58
+ };
59
+
60
+ export const HeadingWithButtonsBothSides = {
61
+ args: {
62
+ level: 1,
63
+ children: [
64
+ <HeadingInnerContainer key={1} className="medium-spacing-gap">
65
+ <Button type="tertiary">
66
+ <Icon name="chevron-left" />
67
+ Button Text
68
+ </Button>
69
+ Heading
70
+ </HeadingInnerContainer>,
71
+ <HeadingInnerContainer key={2} className="medium-spacing-gap">
72
+ <Button type="secondary">
73
+ <Icon name="info" />
74
+ Button Text
75
+ </Button>
76
+ <Button>
77
+ <Icon name="info" />
78
+ Button Text
79
+ </Button>
80
+ </HeadingInnerContainer>,
81
+ ],
82
+ },
83
+ };
84
+
85
+ export default meta;
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { Heading } from './Heading';
4
+ import '@testing-library/jest-dom/vitest';
5
+
6
+ describe('Heading', () => {
7
+ test('Heading renders', () => {
8
+ render(
9
+ <Heading>
10
+ Hello, World!
11
+ </Heading>,
12
+ );
13
+ expect(screen.getByText('Hello, World!')).toBeInTheDocument();
14
+ });
15
+
16
+ test('Heading renders an h1 tag by default', () => {
17
+ render(
18
+ <Heading>foobar</Heading>,
19
+ );
20
+
21
+ expect(screen.getByText('foobar').tagName).toBe('H1');
22
+ });
23
+
24
+ test('Heading respects level prop', () => {
25
+ render(<Heading level={2}>foobar</Heading>);
26
+
27
+ expect(screen.getByText('foobar').tagName).toBe('H2');
28
+ });
29
+ });
@@ -0,0 +1,17 @@
1
+ import { createElement, type HTMLProps } from 'react';
2
+
3
+ type HeadingProps = {
4
+ level?: 1 | 2 | 3 | 4;
5
+ } & HTMLProps<HTMLHeadingElement>;
6
+
7
+ export const Heading = (props: HeadingProps) => {
8
+ const { level = 1, children } = props;
9
+ const Component = {
10
+ 1: 'h1',
11
+ 2: 'h2',
12
+ 3: 'h3',
13
+ 4: 'h4',
14
+ }[level];
15
+
16
+ return createElement(Component, { className: 'ds-heading', ...props }, children);
17
+ };
@@ -0,0 +1,18 @@
1
+ import classNames from 'classnames';
2
+ import type { HTMLAttributes } from 'react';
3
+
4
+ type HeadingSubContainerProps = HTMLAttributes<HTMLSpanElement>;
5
+
6
+ export const HeadingInnerContainer = (props: HeadingSubContainerProps) => {
7
+ const { className, ...rest } = props;
8
+
9
+ return (
10
+ <span
11
+ className={classNames(
12
+ className,
13
+ 'ds-heading__inner-container',
14
+ )}
15
+ {...rest}
16
+ />
17
+ );
18
+ };
@@ -0,0 +1,48 @@
1
+ .ds-heading {
2
+ padding: var(--page-heading-spacing-gap);
3
+ color: var(--page-heading-color-text);
4
+ display: flex;
5
+ width: 100%;
6
+ flex-direction: row;
7
+ justify-content: space-between;
8
+ box-sizing: border-box;
9
+ align-items: center;
10
+
11
+ &__inner-container {
12
+ display: flex;
13
+ flex-direction: row;
14
+ align-items: center;
15
+ }
16
+ }
17
+
18
+ h1 {
19
+ &.ds-heading {
20
+ font-family: var(--type-headings-h1-family);
21
+ font-size: var(--type-headings-h1-size);
22
+ font-weight: var(--type-headings-h1-weight);
23
+ }
24
+ }
25
+
26
+ h2 {
27
+ &.ds-heading {
28
+ font-family: var(--type-headings-h2-family);
29
+ font-size: var(--type-headings-h2-size);
30
+ font-weight: var(--type-headings-h2-weight);
31
+ }
32
+ }
33
+
34
+ h3 {
35
+ &.ds-heading {
36
+ font-family: var(--type-headings-h3-family);
37
+ font-size: var(--type-headings-h3-size);
38
+ font-weight: var(--type-headings-h3-weight);
39
+ }
40
+ }
41
+
42
+ h4 {
43
+ &.ds-heading {
44
+ font-family: var(--type-headings-h4-family);
45
+ font-size: var(--type-headings-h4-size);
46
+ font-weight: var(--type-headings-h4-weight);
47
+ }
48
+ }