@arbor-education/design-system.components 0.13.0 → 0.14.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 (232) hide show
  1. package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
  2. package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
  3. package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
  4. package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
  5. package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
  6. package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
  7. package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
  8. package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
  9. package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
  10. package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
  11. package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
  12. package/.gather/gather.yaml +9 -0
  13. package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
  14. package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
  15. package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
  16. package/.gather/skills/analyze-design/meta.md +4 -0
  17. package/{.claude → .gather}/skills/create-page/README.md +5 -0
  18. package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
  19. package/.gather/skills/create-page/meta.md +4 -0
  20. package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
  21. package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
  22. package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
  23. package/.gather/skills/map-legacy/meta.md +4 -0
  24. package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
  25. package/.gather/skills/migrate-page/meta.md +4 -0
  26. package/.gather/skills/write-stories/README.md +157 -0
  27. package/.gather/skills/write-stories/SKILL.md +841 -0
  28. package/.gather/skills/write-stories/meta.md +4 -0
  29. package/.ralph/storybook-upgrade/knowledge.md +308 -0
  30. package/.ralph/storybook-upgrade/prd.json +777 -0
  31. package/.ralph/storybook-upgrade/progress.md +342 -0
  32. package/.storybook/DocsTemplate.tsx +122 -0
  33. package/.storybook/preview.ts +40 -0
  34. package/.stylelintignore +2 -0
  35. package/CHANGELOG.md +14 -0
  36. package/{.claude/component-library.md → component-library.md} +27 -10
  37. package/dist/components/articleCard/ArticleCard.d.ts +30 -0
  38. package/dist/components/articleCard/ArticleCard.d.ts.map +1 -0
  39. package/dist/components/articleCard/ArticleCard.js +24 -0
  40. package/dist/components/articleCard/ArticleCard.js.map +1 -0
  41. package/dist/components/articleCard/ArticleCard.stories.d.ts +18 -0
  42. package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -0
  43. package/dist/components/articleCard/ArticleCard.stories.js +112 -0
  44. package/dist/components/articleCard/ArticleCard.stories.js.map +1 -0
  45. package/dist/components/articleCard/ArticleCard.test.d.ts +2 -0
  46. package/dist/components/articleCard/ArticleCard.test.d.ts.map +1 -0
  47. package/dist/components/articleCard/ArticleCard.test.js +49 -0
  48. package/dist/components/articleCard/ArticleCard.test.js.map +1 -0
  49. package/dist/components/badge/Badge.stories.d.ts +85 -6
  50. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  51. package/dist/components/badge/Badge.stories.js +626 -27
  52. package/dist/components/badge/Badge.stories.js.map +1 -1
  53. package/dist/components/banner/Banner.stories.d.ts +129 -63
  54. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  55. package/dist/components/banner/Banner.stories.js +855 -39
  56. package/dist/components/banner/Banner.stories.js.map +1 -1
  57. package/dist/components/button/Button.stories.d.ts +148 -8
  58. package/dist/components/button/Button.stories.d.ts.map +1 -1
  59. package/dist/components/button/Button.stories.js +1089 -80
  60. package/dist/components/button/Button.stories.js.map +1 -1
  61. package/dist/components/card/Card.d.ts +41 -12
  62. package/dist/components/card/Card.d.ts.map +1 -1
  63. package/dist/components/card/Card.js +46 -17
  64. package/dist/components/card/Card.js.map +1 -1
  65. package/dist/components/card/Card.stories.d.ts +9 -84
  66. package/dist/components/card/Card.stories.d.ts.map +1 -1
  67. package/dist/components/card/Card.stories.js +15 -73
  68. package/dist/components/card/Card.stories.js.map +1 -1
  69. package/dist/components/card/Card.test.js +50 -152
  70. package/dist/components/card/Card.test.js.map +1 -1
  71. package/dist/components/dot/Dot.stories.d.ts +46 -11
  72. package/dist/components/dot/Dot.stories.d.ts.map +1 -1
  73. package/dist/components/dot/Dot.stories.js +504 -15
  74. package/dist/components/dot/Dot.stories.js.map +1 -1
  75. package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
  76. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  77. package/dist/components/dropdown/Dropdown.stories.js +769 -17
  78. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  79. package/dist/components/formField/FormField.stories.d.ts +95 -35
  80. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  81. package/dist/components/formField/FormField.stories.js +1174 -69
  82. package/dist/components/formField/FormField.stories.js.map +1 -1
  83. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
  84. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  85. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
  86. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  87. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
  88. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
  89. package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
  90. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
  91. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
  92. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  93. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
  94. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  95. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
  96. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  97. package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
  98. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
  99. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
  100. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  101. package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
  102. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  103. package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
  104. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
  105. package/dist/components/heading/Heading.stories.d.ts +449 -50
  106. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  107. package/dist/components/heading/Heading.stories.js +536 -60
  108. package/dist/components/heading/Heading.stories.js.map +1 -1
  109. package/dist/components/icoText/IcoText.d.ts +37 -0
  110. package/dist/components/icoText/IcoText.d.ts.map +1 -0
  111. package/dist/components/icoText/IcoText.js +29 -0
  112. package/dist/components/icoText/IcoText.js.map +1 -0
  113. package/dist/components/icoText/IcoText.stories.d.ts +34 -0
  114. package/dist/components/icoText/IcoText.stories.d.ts.map +1 -0
  115. package/dist/components/icoText/IcoText.stories.js +24 -0
  116. package/dist/components/icoText/IcoText.stories.js.map +1 -0
  117. package/dist/components/icoText/IcoText.test.d.ts +2 -0
  118. package/dist/components/icoText/IcoText.test.d.ts.map +1 -0
  119. package/dist/components/icoText/IcoText.test.js +27 -0
  120. package/dist/components/icoText/IcoText.test.js.map +1 -0
  121. package/dist/components/icon/Icon.stories.d.ts +81 -10
  122. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  123. package/dist/components/icon/Icon.stories.js +979 -8
  124. package/dist/components/icon/Icon.stories.js.map +1 -1
  125. package/dist/components/kpiCard/KPICard.d.ts +13 -0
  126. package/dist/components/kpiCard/KPICard.d.ts.map +1 -0
  127. package/dist/components/kpiCard/KPICard.js +8 -0
  128. package/dist/components/kpiCard/KPICard.js.map +1 -0
  129. package/dist/components/kpiCard/KPICard.stories.d.ts +9 -0
  130. package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -0
  131. package/dist/components/kpiCard/KPICard.stories.js +18 -0
  132. package/dist/components/kpiCard/KPICard.stories.js.map +1 -0
  133. package/dist/components/kpiCard/KPICard.test.d.ts +2 -0
  134. package/dist/components/kpiCard/KPICard.test.d.ts.map +1 -0
  135. package/dist/components/kpiCard/KPICard.test.js +37 -0
  136. package/dist/components/kpiCard/KPICard.test.js.map +1 -0
  137. package/dist/components/kvpList/KVPList.d.ts +34 -0
  138. package/dist/components/kvpList/KVPList.d.ts.map +1 -0
  139. package/dist/components/kvpList/KVPList.js +20 -0
  140. package/dist/components/kvpList/KVPList.js.map +1 -0
  141. package/dist/components/kvpList/KVPList.stories.d.ts +27 -0
  142. package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -0
  143. package/dist/components/kvpList/KVPList.stories.js +18 -0
  144. package/dist/components/kvpList/KVPList.stories.js.map +1 -0
  145. package/dist/components/kvpList/KVPList.test.d.ts +2 -0
  146. package/dist/components/kvpList/KVPList.test.d.ts.map +1 -0
  147. package/dist/components/kvpList/KVPList.test.js +29 -0
  148. package/dist/components/kvpList/KVPList.test.js.map +1 -0
  149. package/dist/components/pill/Pill.stories.d.ts +71 -19
  150. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  151. package/dist/components/pill/Pill.stories.js +573 -14
  152. package/dist/components/pill/Pill.stories.js.map +1 -1
  153. package/dist/components/progress/Progress.stories.d.ts +75 -298
  154. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  155. package/dist/components/progress/Progress.stories.js +449 -52
  156. package/dist/components/progress/Progress.stories.js.map +1 -1
  157. package/dist/components/separator/Separator.stories.d.ts +58 -5
  158. package/dist/components/separator/Separator.stories.d.ts.map +1 -1
  159. package/dist/components/separator/Separator.stories.js +443 -4
  160. package/dist/components/separator/Separator.stories.js.map +1 -1
  161. package/dist/components/singleUser/SingleUser.d.ts +1 -1
  162. package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
  163. package/dist/components/tag/Tag.stories.d.ts +116 -5
  164. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  165. package/dist/components/tag/Tag.stories.js +581 -28
  166. package/dist/components/tag/Tag.stories.js.map +1 -1
  167. package/dist/index.css +194 -23
  168. package/dist/index.css.map +1 -1
  169. package/dist/index.d.ts +13 -4
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +12 -3
  172. package/dist/index.js.map +1 -1
  173. package/eslint.config.mts +5 -1
  174. package/package.json +3 -3
  175. package/src/components/articleCard/ArticleCard.stories.tsx +132 -0
  176. package/src/components/articleCard/ArticleCard.test.tsx +121 -0
  177. package/src/components/articleCard/ArticleCard.tsx +100 -0
  178. package/src/components/articleCard/articleCard.scss +39 -0
  179. package/src/components/badge/Badge.stories.tsx +869 -42
  180. package/src/components/banner/Banner.stories.tsx +1081 -63
  181. package/src/components/button/Button.stories.tsx +1394 -99
  182. package/src/components/card/Card.stories.tsx +35 -79
  183. package/src/components/card/Card.test.tsx +72 -190
  184. package/src/components/card/Card.tsx +117 -58
  185. package/src/components/card/card.scss +18 -31
  186. package/src/components/dot/Dot.stories.tsx +723 -32
  187. package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
  188. package/src/components/formField/FormField.stories.tsx +1522 -105
  189. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
  190. package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
  191. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
  192. package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
  193. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
  194. package/src/components/heading/Heading.stories.tsx +752 -120
  195. package/src/components/icoText/IcoText.stories.tsx +47 -0
  196. package/src/components/icoText/IcoText.test.tsx +41 -0
  197. package/src/components/icoText/IcoText.tsx +93 -0
  198. package/src/components/icoText/icoText.scss +34 -0
  199. package/src/components/icon/Icon.stories.tsx +1446 -12
  200. package/src/components/kpiCard/KPICard.stories.tsx +47 -0
  201. package/src/components/kpiCard/KPICard.test.tsx +60 -0
  202. package/src/components/kpiCard/KPICard.tsx +45 -0
  203. package/src/components/kpiCard/kpiCard.scss +35 -0
  204. package/src/components/kvpList/KVPList.stories.tsx +51 -0
  205. package/src/components/kvpList/KVPList.test.tsx +66 -0
  206. package/src/components/kvpList/KVPList.tsx +109 -0
  207. package/src/components/kvpList/kvpList.scss +64 -0
  208. package/src/components/pill/Pill.stories.tsx +867 -21
  209. package/src/components/progress/Progress.stories.tsx +625 -58
  210. package/src/components/separator/Separator.stories.tsx +730 -8
  211. package/src/components/separator/separator.scss +12 -3
  212. package/src/components/tag/Tag.stories.tsx +755 -53
  213. package/src/index.scss +4 -0
  214. package/src/index.ts +13 -4
  215. package/src/tokens.scss +6 -0
  216. package/tokens/json/Arbor.json +30 -0
  217. package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
  218. package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
  219. package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
  220. package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
  221. package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
  222. package/.claude/figma-assessment-7154-58899.md +0 -404
  223. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
  224. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
  225. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
  226. package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
  227. package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
  228. /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
  229. /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
  230. /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
  231. /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
  232. /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
@@ -0,0 +1,132 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { fn } from 'storybook/test';
3
+ import { ArticleCard } from './ArticleCard';
4
+
5
+ const meta = {
6
+ title: 'Components/Card/ArticleCard',
7
+ component: ArticleCard,
8
+ } satisfies Meta<typeof ArticleCard>;
9
+
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const CardWithTitleAndParagraph: Story = {
13
+ args: {
14
+ 'title': 'Title of Card',
15
+ 'paragraph': 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
16
+ 'disabled': false,
17
+ 'onClick': fn(),
18
+ 'onKeyDown': fn(),
19
+ 'aria-label': 'Clickable article card with title and paragraph',
20
+ },
21
+ };
22
+
23
+ export const CardWithTitleParagraphAndIcon: Story = {
24
+ args: {
25
+ 'title': 'Title of Card',
26
+ 'paragraph': 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
27
+ 'icon': 'eye',
28
+ 'disabled': false,
29
+ 'onClick': fn(),
30
+ 'onKeyDown': fn(),
31
+ 'aria-label': 'Clickable article card with title paragraph and icon',
32
+ },
33
+ };
34
+
35
+ export const TheEverythingCard: Story = {
36
+ args: {
37
+ 'title': 'Title of Card',
38
+ 'paragraph': 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
39
+ 'icon': 'eye',
40
+ 'disabled': false,
41
+ 'tagText': 'argle bargle',
42
+ 'tagColor': 'orange',
43
+ 'onClick': fn(),
44
+ 'onKeyDown': fn(),
45
+ 'aria-label': 'Clickable article card with all content',
46
+ },
47
+ };
48
+
49
+ export const CardWithTitleParagraphAndTag: Story = {
50
+ args: {
51
+ 'title': 'Title of Card',
52
+ 'paragraph': 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
53
+ 'disabled': false,
54
+ 'tagText': 'argle bargle',
55
+ 'tagColor': 'orange',
56
+ 'onClick': fn(),
57
+ 'onKeyDown': fn(),
58
+ 'aria-label': 'Clickable article card with title paragraph and tag',
59
+ },
60
+ };
61
+
62
+ export const CardWithTitleAndIcon: Story = {
63
+ args: {
64
+ 'title': 'Title of Card',
65
+ 'icon': 'eye',
66
+ 'disabled': false,
67
+ 'onClick': fn(),
68
+ 'onKeyDown': fn(),
69
+ 'aria-label': 'Clickable article card with title and icon',
70
+ },
71
+ };
72
+
73
+ export const CardWithParagraph: Story = {
74
+ args: {
75
+ 'paragraph': 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
76
+ 'disabled': false,
77
+ 'onClick': fn(),
78
+ 'onKeyDown': fn(),
79
+ 'aria-label': 'Clickable article card with paragraph',
80
+ },
81
+ };
82
+
83
+ export const ClickableDisabledCard: Story = {
84
+ args: {
85
+ 'title': 'Title of Card',
86
+ 'paragraph': 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
87
+ 'icon': 'eye',
88
+ 'tagText': 'argle bargle',
89
+ 'tagColor': 'orange',
90
+ 'disabled': true,
91
+ 'onClick': fn(),
92
+ 'onKeyDown': fn(),
93
+ 'aria-label': 'Disabled clickable article card',
94
+ },
95
+ };
96
+
97
+ export const UnclickableCard: Story = {
98
+ args: {
99
+ title: 'Title of Card',
100
+ paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
101
+ icon: 'eye',
102
+ tagText: 'argle bargle',
103
+ tagColor: 'orange',
104
+ },
105
+ };
106
+
107
+ export const TextOnly: Story = {
108
+ args: {
109
+ title: 'Text-only article card',
110
+ paragraph: 'A compact way to present article-style content in the shared shell.',
111
+ },
112
+ };
113
+
114
+ export const LinkedArticle: Story = {
115
+ args: {
116
+ title: 'Primary article link',
117
+ href: '/articles/primary-article-link',
118
+ paragraph: (
119
+ <>
120
+ Read the full article or visit the
121
+ {' '}
122
+ <a href="/authors/design-system">author page</a>
123
+ .
124
+ </>
125
+ ),
126
+ icon: 'eye',
127
+ tagText: 'Featured',
128
+ tagColor: 'green',
129
+ },
130
+ };
131
+
132
+ export default meta;
@@ -0,0 +1,121 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom/vitest';
3
+ import { describe, expect, test, vi } from 'vitest';
4
+ import { ArticleCard } from './ArticleCard';
5
+
6
+ describe('ArticleCard', () => {
7
+ test('renders the legacy card content composition', () => {
8
+ const { container } = render(
9
+ <ArticleCard
10
+ icon="eye"
11
+ paragraph="Helpful supporting copy"
12
+ tagColor="green"
13
+ tagText="Live"
14
+ title="Article summary"
15
+ />,
16
+ );
17
+
18
+ expect(screen.getByRole('article')).toBeInTheDocument();
19
+ expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Article summary');
20
+ expect(screen.getByText('Helpful supporting copy')).toBeInTheDocument();
21
+ expect(screen.getByText('Live')).toBeInTheDocument();
22
+ expect(container.querySelector('.ds-icon-eye')).toBeInTheDocument();
23
+ });
24
+
25
+ test('renders content without an icon', () => {
26
+ const { container } = render(
27
+ <ArticleCard
28
+ paragraph="Helpful supporting copy"
29
+ title="Article summary"
30
+ />,
31
+ );
32
+
33
+ expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Article summary');
34
+ expect(container.querySelector('.ds-ico-text__icon')).not.toBeInTheDocument();
35
+ });
36
+
37
+ test('renders a tag when provided', () => {
38
+ render(
39
+ <ArticleCard
40
+ paragraph="Helpful supporting copy"
41
+ tagColor="orange"
42
+ tagText="Pinned"
43
+ title="Article summary"
44
+ />,
45
+ );
46
+
47
+ expect(screen.getByText('Pinned')).toBeInTheDocument();
48
+ });
49
+
50
+ test('renders a primary title link without button semantics when href is provided', () => {
51
+ const { container } = render(
52
+ <ArticleCard
53
+ href="/articles/linked-article"
54
+ paragraph={(
55
+ <>
56
+ By
57
+ {' '}
58
+ <a href="/authors/design-system">Design System Team</a>
59
+ </>
60
+ )}
61
+ title="Linked article"
62
+ />,
63
+ );
64
+
65
+ expect(screen.getByRole('link', { name: 'Linked article' })).toHaveAttribute(
66
+ 'href',
67
+ '/articles/linked-article',
68
+ );
69
+ expect(screen.getByRole('link', { name: 'Design System Team' })).toHaveAttribute(
70
+ 'href',
71
+ '/authors/design-system',
72
+ );
73
+ expect(container.querySelector('figure')).not.toHaveAttribute('role', 'button');
74
+ });
75
+
76
+ test('suppresses the primary link when disabled even if href is provided', () => {
77
+ const { container } = render(
78
+ <ArticleCard
79
+ disabled
80
+ href="/articles/linked-article"
81
+ paragraph="Helpful supporting copy"
82
+ title="Linked article"
83
+ />,
84
+ );
85
+
86
+ expect(screen.queryByRole('link', { name: 'Linked article' })).not.toBeInTheDocument();
87
+ expect(container.querySelector('figure')).not.toHaveAttribute('role', 'button');
88
+ });
89
+
90
+ test('uses the shared card shell for interactivity', () => {
91
+ const handleClick = vi.fn();
92
+
93
+ render(
94
+ <ArticleCard
95
+ aria-label="Clickable article card"
96
+ onClick={handleClick}
97
+ paragraph="Helpful supporting copy"
98
+ title="Clickable article"
99
+ />,
100
+ );
101
+
102
+ const card = screen.getByRole('button', { name: 'Clickable article card' });
103
+ fireEvent.click(card);
104
+
105
+ expect(handleClick).toHaveBeenCalledTimes(1);
106
+ });
107
+
108
+ test('renders as a static figure when not clickable', () => {
109
+ const { container } = render(
110
+ <ArticleCard
111
+ paragraph="Helpful supporting copy"
112
+ title="Static article"
113
+ />,
114
+ );
115
+
116
+ const card = container.querySelector('figure');
117
+
118
+ expect(card).toBeInTheDocument();
119
+ expect(card).not.toHaveAttribute('role', 'button');
120
+ });
121
+ });
@@ -0,0 +1,100 @@
1
+ import classNames from 'classnames';
2
+ import type { IcoTextIconProps } from 'Components/icoText/IcoText';
3
+ import type { TagColor } from 'Components/tag/Tag';
4
+ import { Tag } from 'Components/tag/Tag';
5
+ import type { IconName } from 'Components/icon/allowedIcons';
6
+ import { Card, getCardInteractionProps } from 'Components/card/Card';
7
+ import { IcoText } from 'Components/icoText/IcoText';
8
+
9
+ type ArticleCardBaseProps = {
10
+ className?: string;
11
+ paragraph?: React.ReactNode;
12
+ icon?: IconName;
13
+ iconColor?: IcoTextIconProps['color'];
14
+ disabled?: boolean;
15
+ tagText?: string;
16
+ tagColor?: TagColor;
17
+ iconScreenReaderText?: string;
18
+ };
19
+
20
+ type ArticleCardLinkedProps = {
21
+ 'href': string;
22
+ 'title': React.ReactNode;
23
+ 'onClick'?: undefined;
24
+ 'onKeyDown'?: undefined;
25
+ 'aria-label'?: string;
26
+ 'aria-labelledby'?: string;
27
+ };
28
+
29
+ type ArticleCardCardShellProps = {
30
+ href?: undefined;
31
+ title?: React.ReactNode;
32
+ } & Card.InteractionProps;
33
+
34
+ export type ArticleCardProps = ArticleCardBaseProps & (ArticleCardLinkedProps | ArticleCardCardShellProps);
35
+
36
+ export const ArticleCard = (props: ArticleCardProps): React.JSX.Element => {
37
+ const {
38
+ className,
39
+ href,
40
+ title,
41
+ paragraph,
42
+ icon,
43
+ iconColor,
44
+ iconScreenReaderText,
45
+ disabled = false,
46
+ tagText,
47
+ tagColor,
48
+ } = props;
49
+ const hasPrimaryLink = Boolean(href && title && !disabled);
50
+
51
+ const cardClassName = classNames({
52
+ 'ds-card__container--article-link': hasPrimaryLink,
53
+ }, className);
54
+
55
+ const cardInteractionProps = hasPrimaryLink
56
+ ? getCardInteractionProps({
57
+ 'onKeyDown': undefined,
58
+ 'aria-label': props['aria-label'],
59
+ 'aria-labelledby': props['aria-labelledby'],
60
+ })
61
+ : getCardInteractionProps(props);
62
+
63
+ const content = (
64
+ <article className="ds-article-card">
65
+ <IcoText>
66
+ {icon && (
67
+ <IcoText.Icon
68
+ color={iconColor}
69
+ name={icon}
70
+ screenReaderText={iconScreenReaderText}
71
+ />
72
+ )}
73
+ {title && (
74
+ <IcoText.Heading>
75
+ {hasPrimaryLink
76
+ ? (
77
+ <a className="ds-article-card__primary-link" href={href}>
78
+ {title}
79
+ </a>
80
+ )
81
+ : title}
82
+ </IcoText.Heading>
83
+ )}
84
+ {paragraph && <IcoText.Paragraph>{paragraph}</IcoText.Paragraph>}
85
+ {tagText && <Tag color={tagColor}>{tagText}</Tag>}
86
+ </IcoText>
87
+ </article>
88
+ );
89
+
90
+ return (
91
+ <Card
92
+ {...cardInteractionProps}
93
+ className={cardClassName}
94
+ disabled={disabled}
95
+ spacing="default"
96
+ >
97
+ {content}
98
+ </Card>
99
+ );
100
+ };
@@ -0,0 +1,39 @@
1
+ .ds-card__container--article-link {
2
+ position: relative;
3
+
4
+ &:not(.ds-card__container--disabled) {
5
+ cursor: pointer;
6
+ }
7
+
8
+ &:focus-within:not(.ds-card__container--disabled) {
9
+ border: var(--border-weight) solid var(--card-focus-color-border);
10
+ background: var(--card-focus-color-background);
11
+ outline: var(--focus-border) solid var(--card-focus-focus);
12
+ }
13
+ }
14
+
15
+ .ds-article-card {
16
+ width: 100%;
17
+ }
18
+
19
+ .ds-article-card__primary-link {
20
+ color: inherit;
21
+ text-decoration: none;
22
+
23
+ &::after {
24
+ content: '';
25
+ position: absolute;
26
+ inset: 0;
27
+ z-index: 1;
28
+ }
29
+
30
+ &:hover,
31
+ &:focus-visible {
32
+ text-decoration: underline;
33
+ }
34
+ }
35
+
36
+ .ds-article-card a:not(.ds-article-card__primary-link) {
37
+ position: relative;
38
+ z-index: 2;
39
+ }