@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,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { KVPList } from 'Components/kvpList/KVPList';
3
+ import { Progress } from 'Components/progress/Progress';
4
+ import { KPICard } from './KPICard';
5
+
6
+ const meta = {
7
+ title: 'Components/Card/KPICard',
8
+ component: KPICard,
9
+ } satisfies Meta<typeof KPICard>;
10
+
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ label: 'KPI label text',
16
+ value: 'XX.X',
17
+ isPercentage: true,
18
+ },
19
+ render: args => (
20
+ <KPICard {...args}>
21
+ <KVPList aria-label="Summary values">
22
+ <KVPList.Row orientation="horizontal">
23
+ <KVPList.Term>Bar label text</KVPList.Term>
24
+ <KVPList.Definition prominence="strong">XX.X</KVPList.Definition>
25
+ </KVPList.Row>
26
+ </KVPList>
27
+ <KVPList aria-label="Progress values">
28
+ <KVPList.Row orientation="horizontal">
29
+ <KVPList.Term>Bar label text</KVPList.Term>
30
+ <KVPList.Definition prominence="neutral">X</KVPList.Definition>
31
+ <KVPList.Definition aria-hidden="true" isRow>
32
+ <Progress aria-label="Progress bar first" max={100} value={95} />
33
+ </KVPList.Definition>
34
+ </KVPList.Row>
35
+ <KVPList.Row orientation="horizontal">
36
+ <KVPList.Term>Bar label text</KVPList.Term>
37
+ <KVPList.Definition prominence="neutral">X</KVPList.Definition>
38
+ <KVPList.Definition aria-hidden="true" isRow>
39
+ <Progress aria-label="Progress bar second" max={100} value={95} />
40
+ </KVPList.Definition>
41
+ </KVPList.Row>
42
+ </KVPList>
43
+ </KPICard>
44
+ ),
45
+ };
46
+
47
+ export default meta;
@@ -0,0 +1,60 @@
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 { KPICard } from './KPICard';
5
+ import { KVPList } from 'Components/kvpList/KVPList';
6
+
7
+ describe('KPICard', () => {
8
+ test('renders header content and kvp rows', () => {
9
+ render(
10
+ <KPICard isPercentage label="Attendance" value="95.4">
11
+ <KVPList aria-label="Breakdown">
12
+ <KVPList.Row orientation="horizontal">
13
+ <KVPList.Term>Summary</KVPList.Term>
14
+ <KVPList.Definition prominence="strong">95.4</KVPList.Definition>
15
+ </KVPList.Row>
16
+ </KVPList>
17
+ </KPICard>,
18
+ );
19
+
20
+ expect(screen.getByText('Attendance')).toBeInTheDocument();
21
+ expect(screen.getAllByText('95.4')).toHaveLength(2);
22
+ expect(screen.getByText('%')).toBeInTheDocument();
23
+ });
24
+
25
+ test('does not render a percentage suffix when isPercentage is false', () => {
26
+ render(<KPICard label="Attendance" value="95.4" />);
27
+
28
+ expect(screen.queryByText('%')).not.toBeInTheDocument();
29
+ });
30
+
31
+ test('uses the shared card shell for click interaction', () => {
32
+ const handleClick = vi.fn();
33
+
34
+ render(<KPICard aria-label="Attendance KPI card" label="Attendance" onClick={handleClick} value="95.4" />);
35
+
36
+ fireEvent.click(screen.getByRole('button', { name: 'Attendance KPI card' }));
37
+
38
+ expect(handleClick).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ test('supports keyboard activation through the shared card shell', () => {
42
+ const handleClick = vi.fn();
43
+
44
+ render(<KPICard aria-label="Attendance KPI card" label="Attendance" onClick={handleClick} value="95.4" />);
45
+
46
+ fireEvent.keyDown(screen.getByRole('button', { name: 'Attendance KPI card' }), { key: 'Enter' });
47
+
48
+ expect(handleClick).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ test('does not invoke click handlers when disabled', () => {
52
+ const handleClick = vi.fn();
53
+
54
+ render(<KPICard aria-label="Attendance KPI card" disabled label="Attendance" onClick={handleClick} value="95.4" />);
55
+
56
+ fireEvent.click(screen.getByRole('button', { name: 'Attendance KPI card' }));
57
+
58
+ expect(handleClick).not.toHaveBeenCalled();
59
+ });
60
+ });
@@ -0,0 +1,45 @@
1
+ import { Card, getCardInteractionProps } from 'Components/card/Card';
2
+
3
+ type KPICardBaseProps = {
4
+ children?: React.ReactNode;
5
+ className?: string;
6
+ label: React.ReactNode;
7
+ value: React.ReactNode;
8
+ isPercentage?: boolean;
9
+ disabled?: boolean;
10
+ };
11
+
12
+ export type KPICardProps = KPICardBaseProps & Card.InteractionProps;
13
+
14
+ export const KPICard = (props: KPICardProps): React.JSX.Element => {
15
+ const {
16
+ children,
17
+ className,
18
+ label,
19
+ value,
20
+ isPercentage = false,
21
+ disabled = false,
22
+ } = props;
23
+
24
+ const content = (
25
+ <div className="ds-kpi-card">
26
+ <p className="ds-kpi-card__label">{label}</p>
27
+ <p className="ds-kpi-card__value">
28
+ {value}
29
+ {isPercentage && <span className="ds-kpi-card__suffix">%</span>}
30
+ </p>
31
+ {children}
32
+ </div>
33
+ );
34
+
35
+ return (
36
+ <Card
37
+ {...getCardInteractionProps(props)}
38
+ className={className}
39
+ disabled={disabled}
40
+ spacing="dense"
41
+ >
42
+ {content}
43
+ </Card>
44
+ );
45
+ };
@@ -0,0 +1,35 @@
1
+ .ds-kpi-card {
2
+ display: grid;
3
+ width: 100%;
4
+ grid-template-columns: minmax(0, 1fr) auto;
5
+ gap: var(--spacing-medium) var(--spacing-small);
6
+
7
+ > :not(.ds-kpi-card__label, .ds-kpi-card__value) {
8
+ grid-column: 1 / -1;
9
+ }
10
+ }
11
+
12
+ .ds-kpi-card__label {
13
+ align-self: end;
14
+ margin: 0;
15
+ color: var(--kpi-card-color-text-label);
16
+ font-family: var(--type-body-p-family);
17
+ font-size: var(--type-body-p-size);
18
+ font-weight: var(--type-body-p-weight);
19
+ line-height: var(--type-body-line-height);
20
+ }
21
+
22
+ .ds-kpi-card__value {
23
+ justify-self: end;
24
+ margin: 0;
25
+ color: var(--kpi-card-color-text-value);
26
+ font-family: var(--type-headings-h2-family);
27
+ font-size: var(--type-headings-h2-size);
28
+ font-weight: var(--type-headings-h2-weight);
29
+ line-height: var(--type-headings-h2-line-height);
30
+ }
31
+
32
+ .ds-kpi-card__suffix {
33
+ margin-left: var(--spacing-xsmall);
34
+ font: inherit;
35
+ }
@@ -0,0 +1,51 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Progress } from 'Components/progress/Progress';
3
+ import { KVPList } from './KVPList';
4
+
5
+ const meta = {
6
+ title: 'Components/KVPList',
7
+ component: KVPList,
8
+ } satisfies Meta<typeof KVPList>;
9
+
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const Summary: Story = {
13
+ render: args => (
14
+ <KVPList {...args} aria-label="Attendance metrics">
15
+ <KVPList.Row orientation="horizontal">
16
+ <KVPList.Term>Attendance</KVPList.Term>
17
+ <KVPList.Definition isPercentage prominence="strong">95</KVPList.Definition>
18
+ </KVPList.Row>
19
+ </KVPList>
20
+ ),
21
+ };
22
+
23
+ export const WithProgressAndPercentage: Story = {
24
+ render: args => (
25
+ <KVPList {...args} aria-label="Attendance breakdown">
26
+ <KVPList.Row orientation="horizontal">
27
+ <KVPList.Term>Bar label text</KVPList.Term>
28
+ <KVPList.Definition isPercentage prominence="neutral">95</KVPList.Definition>
29
+ <KVPList.Definition aria-hidden="true" isRow>
30
+ <Progress aria-label="Attendance progress" max={100} value={95} />
31
+ </KVPList.Definition>
32
+ </KVPList.Row>
33
+ </KVPList>
34
+ ),
35
+ };
36
+
37
+ export const WithProgressNoPercentage: Story = {
38
+ render: args => (
39
+ <KVPList {...args} aria-label="Attendance breakdown without percentage">
40
+ <KVPList.Row orientation="horizontal">
41
+ <KVPList.Term>Bar label text</KVPList.Term>
42
+ <KVPList.Definition prominence="neutral">X</KVPList.Definition>
43
+ <KVPList.Definition aria-hidden="true" isRow>
44
+ <Progress aria-label="Attendance progress second" max={100} value={95} />
45
+ </KVPList.Definition>
46
+ </KVPList.Row>
47
+ </KVPList>
48
+ ),
49
+ };
50
+
51
+ export default meta;
@@ -0,0 +1,66 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { KVPList } from './KVPList';
5
+
6
+ describe('KVPList', () => {
7
+ test('renders term and definition semantics', () => {
8
+ render(
9
+ <KVPList aria-label="Attendance metrics">
10
+ <KVPList.Row orientation="horizontal">
11
+ <KVPList.Term>Attendance</KVPList.Term>
12
+ <KVPList.Definition prominence="strong">95</KVPList.Definition>
13
+ </KVPList.Row>
14
+ </KVPList>,
15
+ );
16
+
17
+ expect(screen.getByText('Attendance').tagName).toBe('DT');
18
+ expect(screen.getByText('95').tagName).toBe('DD');
19
+ expect(screen.getByRole('group', { name: 'Attendance metrics' })).toBeInTheDocument();
20
+ });
21
+
22
+ test('renders a typed percentage suffix', () => {
23
+ render(
24
+ <KVPList>
25
+ <KVPList.Row orientation="horizontal">
26
+ <KVPList.Term>Attendance</KVPList.Term>
27
+ <KVPList.Definition isPercentage prominence="neutral">
28
+ 95
29
+ </KVPList.Definition>
30
+ </KVPList.Row>
31
+ </KVPList>,
32
+ );
33
+
34
+ const percentageDefinition = screen.getByText('95').closest('dd');
35
+
36
+ expect(screen.getByText('%')).toBeInTheDocument();
37
+ expect(percentageDefinition).toHaveTextContent('95%');
38
+ });
39
+
40
+ test('supports horizontal rows and row-spanning definitions', () => {
41
+ const { container } = render(
42
+ <KVPList>
43
+ <KVPList.Row orientation="horizontal">
44
+ <KVPList.Term>Progress</KVPList.Term>
45
+ <KVPList.Definition isRow>Full width content</KVPList.Definition>
46
+ </KVPList.Row>
47
+ </KVPList>,
48
+ );
49
+
50
+ expect(container.querySelector('.ds-kvp-list__row--horizontal')).toBeInTheDocument();
51
+ expect(container.querySelector('.ds-kvp-list__description--row')).toBeInTheDocument();
52
+ });
53
+
54
+ test('does not add group semantics when no accessible label is provided', () => {
55
+ render(
56
+ <KVPList>
57
+ <KVPList.Row orientation="horizontal">
58
+ <KVPList.Term>Attendance</KVPList.Term>
59
+ <KVPList.Definition prominence="strong">95</KVPList.Definition>
60
+ </KVPList.Row>
61
+ </KVPList>,
62
+ );
63
+
64
+ expect(screen.queryByRole('group')).not.toBeInTheDocument();
65
+ });
66
+ });
@@ -0,0 +1,109 @@
1
+ import classNames from 'classnames';
2
+
3
+ export type KVPListProps = React.HTMLAttributes<HTMLDivElement> & {
4
+ children?: React.ReactNode;
5
+ };
6
+
7
+ export type KVPListRowProps = React.HTMLAttributes<HTMLDListElement> & {
8
+ children?: React.ReactNode;
9
+ orientation?: 'horizontal' | 'vertical';
10
+ };
11
+
12
+ export type KVPListTermProps = React.HTMLAttributes<HTMLElement> & {
13
+ children?: React.ReactNode;
14
+ };
15
+
16
+ export type KVPListDefinitionProps = React.HTMLAttributes<HTMLElement> & {
17
+ children?: React.ReactNode;
18
+ isRow?: boolean;
19
+ isPercentage?: boolean;
20
+ prominence?: 'neutral' | 'strong';
21
+ };
22
+
23
+ const KVPListRoot = ({
24
+ children,
25
+ className,
26
+ role,
27
+ 'aria-label': ariaLabel,
28
+ 'aria-labelledby': ariaLabelledBy,
29
+ ...rest
30
+ }: KVPListProps): React.JSX.Element => (
31
+ <div
32
+ className={classNames('ds-kvp-list', className)}
33
+ role={role ?? (ariaLabel || ariaLabelledBy ? 'group' : undefined)}
34
+ aria-label={ariaLabel}
35
+ aria-labelledby={ariaLabelledBy}
36
+ {...rest}
37
+ >
38
+ {children}
39
+ </div>
40
+ );
41
+
42
+ const KVPListRow = ({
43
+ children,
44
+ className,
45
+ orientation = 'vertical',
46
+ ...rest
47
+ }: KVPListRowProps): React.JSX.Element => (
48
+ <dl
49
+ className={classNames(
50
+ 'ds-kvp-list__row',
51
+ `ds-kvp-list__row--${orientation}`,
52
+ className,
53
+ )}
54
+ {...rest}
55
+ >
56
+ {children}
57
+ </dl>
58
+ );
59
+
60
+ const KVPListTerm = ({
61
+ children,
62
+ className,
63
+ ...rest
64
+ }: KVPListTermProps): React.JSX.Element => (
65
+ <dt className={classNames('ds-kvp-list__term', className)} {...rest}>
66
+ {children}
67
+ </dt>
68
+ );
69
+
70
+ const KVPListDefinition = ({
71
+ children,
72
+ className,
73
+ isRow = false,
74
+ isPercentage = false,
75
+ prominence = 'neutral',
76
+ ...rest
77
+ }: KVPListDefinitionProps): React.JSX.Element => (
78
+ <dd
79
+ className={classNames(
80
+ 'ds-kvp-list__description',
81
+ `ds-kvp-list__description--${prominence}`,
82
+ {
83
+ 'ds-kvp-list__description--row': isRow,
84
+ },
85
+ className,
86
+ )}
87
+ {...rest}
88
+ >
89
+ {children}
90
+ {isPercentage
91
+ ? (
92
+ <span className="ds-kvp-list__suffix">
93
+ %
94
+ </span>
95
+ )
96
+ : null}
97
+ </dd>
98
+ );
99
+
100
+ KVPListRoot.displayName = 'KVPList';
101
+ KVPListRow.displayName = 'KVPList.Row';
102
+ KVPListTerm.displayName = 'KVPList.Term';
103
+ KVPListDefinition.displayName = 'KVPList.Definition';
104
+
105
+ export const KVPList = Object.assign(KVPListRoot, {
106
+ Row: KVPListRow,
107
+ Term: KVPListTerm,
108
+ Definition: KVPListDefinition,
109
+ });
@@ -0,0 +1,64 @@
1
+ .ds-kvp-list {
2
+ display: flex;
3
+ width: 100%;
4
+ flex-direction: column;
5
+ gap: var(--spacing-medium);
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ .ds-kvp-list__term,
10
+ .ds-kvp-list__description {
11
+ margin: 0;
12
+ flex: 1;
13
+ }
14
+
15
+ .ds-kvp-list__term,
16
+ .ds-kvp-list__description--neutral {
17
+ color: var(--kvp-list-color-text-subtle);
18
+ font-family: var(--type-body-p-family);
19
+ font-size: var(--type-body-p-size);
20
+ font-weight: var(--type-body-p-weight);
21
+ line-height: var(--type-body-line-height);
22
+ }
23
+
24
+ .ds-kvp-list__description--strong {
25
+ color: var(--kvp-list-color-text-strong);
26
+ font-family: var(--type-headings-h2-family);
27
+ font-size: var(--type-headings-h2-size);
28
+ font-weight: var(--type-headings-h2-weight);
29
+ line-height: var(--type-headings-h2-line-height);
30
+ }
31
+
32
+ .ds-kvp-list__description--row {
33
+ flex-basis: 100%;
34
+ width: 100%;
35
+ }
36
+
37
+ .ds-kvp-list__suffix {
38
+ margin-left: var(--spacing-xsmall);
39
+ font: inherit;
40
+ }
41
+
42
+ .ds-kvp-list__description {
43
+ text-align: right;
44
+ }
45
+
46
+ .ds-kvp-list__row {
47
+ display: flex;
48
+ width: 100%;
49
+ margin: 0;
50
+ gap: var(--spacing-small);
51
+
52
+ &--vertical {
53
+ flex-direction: column;
54
+ }
55
+
56
+ &--horizontal {
57
+ flex-wrap: wrap;
58
+ align-items: center;
59
+
60
+ .ds-kvp-list__description:not(.ds-kvp-list__description--row) {
61
+ flex: 0 0 auto;
62
+ }
63
+ }
64
+ }