@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
@@ -1,30 +1,229 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ Controls,
4
+ Heading as DocHeading,
5
+ Markdown,
6
+ Primary as DocPrimary,
7
+ Stories,
8
+ Subtitle,
9
+ Title,
10
+ } from '@storybook/addon-docs/blocks';
11
+ import { Icon } from 'Components/icon/Icon';
2
12
  import { Badge, type BadgeColour } from './Badge';
3
13
 
4
- const colours: BadgeColour[] = [
5
- 'purple',
6
- 'salmon',
7
- 'teal',
8
- 'yellow',
9
- 'green',
10
- 'orange',
11
- 'blue',
12
- ];
13
-
14
- const meta: Meta<typeof Badge> = {
15
- tags: ['autodocs'],
14
+ // ---------------------------------------------------------------------------
15
+ // Docs page content
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const DESCRIPTION_INTRO = [
19
+ 'Badge is a compact numeric count indicator that sits alongside icons, labels, or controls to communicate',
20
+ 'quantities at a glance — unread messages, behaviour event totals, pending form responses.',
21
+ ].join('\n');
22
+
23
+ const USAGE_GUIDANCE = [
24
+ '### When to use',
25
+ '',
26
+ '- **Unread notification counts** — messages, alerts, or action items awaiting review',
27
+ '- **Status totals** — number of students flagged, responses submitted, events logged',
28
+ '- **Count-adjacent icons** — a bell icon with a Badge communicates "3 unread notifications"',
29
+ '- **Tab bar counts** — urgent items in a specific tab need a visual quantity signal',
30
+ '',
31
+ '---',
32
+ '',
33
+ '### When NOT to use',
34
+ '',
35
+ 'Badge is for **numbers only**. It is not a label, filter toggle, or status dot.',
36
+ '',
37
+ '| Component | Use for |',
38
+ '|---|---|',
39
+ '| `Badge` | Numeric counts: `3`, `12`, `99+` |',
40
+ '| [`Tag`](?path=/docs/components-tag--docs) | Category labels: `English`, `Year 9`, `GCSE` |',
41
+ '| [`Pill`](?path=/docs/components-pill--docs) | Filter toggles: `Active`, `Archived`, `Flagged` |',
42
+ '| [`Dot`](?path=/docs/components-dot--docs) | Status indicator (no number): presence, colour-coded state |',
43
+ '',
44
+ '```tsx',
45
+ '// BAD — numbers require different emphasis and spacing than category labels',
46
+ '<Badge colour="purple">English</Badge>',
47
+ '',
48
+ '// GOOD — use Tag for category labels',
49
+ '<Tag color="purple">English</Tag>',
50
+ '```',
51
+ '',
52
+ '---',
53
+ '',
54
+ '### Design guidance',
55
+ '',
56
+ '- **Three sizes** — `sm` (`--badge-small-size`, 1.125rem / 18px), `md` (`--badge-medium-size`,',
57
+ ' 1.25rem / 20px, default), `lg` (`--badge-large-size`, 1.5rem / 24px). All three use',
58
+ ' `--border-radius-round` (a fully circular radius) so single-digit badges appear as perfect circles',
59
+ ' and multi-digit badges expand into pills.',
60
+ '- **Font weight** — `--badge-font-weight` resolves to `--font-weight-semi-bold` (600) across all sizes.',
61
+ '- **Default colour** — no `colour` prop renders with `--color-grey-800` background and',
62
+ ' `--color-mono-white` text (charcoal). All seven extended-palette colours are also available.',
63
+ '- **Colour contrast pairings** — white text on salmon, green, and the default charcoal.',
64
+ ' Dark text (`--color-extended-colours-{colour}-800`) on purple, teal, yellow, orange, and blue.',
65
+ '- **`99+` convention** — when a count exceeds 99, display `"99+"` rather than the real number to',
66
+ ' keep the badge compact and avoid layout overflow.',
67
+ ].join('\n');
68
+
69
+ const DEVELOPER_NOTES = [
70
+ '### Accessibility',
71
+ '',
72
+ '- **Keyboard** — Badge is a purely presentational `<span>`. It has no interactive behaviour and is',
73
+ ' not focusable.',
74
+ '- **Screen readers** — by default, the badge content is read as inline text. If the badge sits',
75
+ ' next to an icon or label that already provides context, the count alone may be confusing',
76
+ ' ("3" without context means nothing).',
77
+ '- **`a11yLabel` pattern** — pass a full human-readable string like `"3 unread messages"`. The',
78
+ ' component sets `aria-label` on the wrapper `<span>` and wraps `children` in an',
79
+ ' `aria-hidden="true"` span to suppress the duplicate numeric announcement. The screen reader',
80
+ ' then announces the full label instead of just the number.',
81
+ '- **Do not rely on colour alone** — the numeric value must be visible. Never convey meaning',
82
+ ' exclusively through badge colour without a label or supporting text.',
83
+ '',
84
+ '---',
85
+ '',
86
+ '### TypeScript types',
87
+ '',
88
+ '```ts',
89
+ "import { Badge } from '@arbor-education/design-system.components';",
90
+ '',
91
+ 'function MyBadge(props: Badge.Props) { ... }',
92
+ '```',
93
+ '',
94
+ '| Type | Description |',
95
+ '|---|---|',
96
+ '| `Badge.Props` | Full props interface |',
97
+ "| `Badge.Size` | `'sm' \\| 'md' \\| 'lg'` |",
98
+ "| `Badge.Colour` | `'purple' \\| 'salmon' \\| 'teal' \\| 'yellow' \\| 'green' \\| 'orange' \\| 'blue'` (same scale as `Dot.Colour`) |",
99
+ ].join('\n');
100
+
101
+ const RELATED_COMPONENTS = [
102
+ '## Related components',
103
+ '',
104
+ '[Tag](?path=/docs/components-tag--docs) · [Pill](?path=/docs/components-pill--docs) · [Dot](?path=/docs/components-dot--docs) · [Icon](?path=/docs/components-icon--docs)',
105
+ ].join('\n');
106
+
107
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
108
+
109
+ function BadgeDocsPage() {
110
+ return (
111
+ <>
112
+ <Title />
113
+ <Subtitle />
114
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
115
+ <DocHeading>Interactive example</DocHeading>
116
+ <Markdown>{PROPS_INTRO}</Markdown>
117
+ <DocPrimary />
118
+ <Controls />
119
+ <DocHeading>Usage guidance</DocHeading>
120
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
121
+ <DocHeading>Developer notes</DocHeading>
122
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
123
+ <DocHeading>Examples</DocHeading>
124
+ <Stories title="" />
125
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
126
+ </>
127
+ );
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Data
132
+ // ---------------------------------------------------------------------------
133
+
134
+ const colours: BadgeColour[] = ['purple', 'salmon', 'teal', 'yellow', 'green', 'orange', 'blue'];
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Meta
138
+ // ---------------------------------------------------------------------------
139
+
140
+ const meta = {
16
141
  title: 'Components/Badge',
17
142
  component: Badge,
143
+ parameters: {
144
+ layout: 'centered',
145
+ docs: {
146
+ page: BadgeDocsPage,
147
+ },
148
+ },
149
+ tags: ['autodocs'],
18
150
  argTypes: {
19
- size: { control: 'select', options: ['sm', 'md', 'lg'] },
20
- colour: { control: 'select', options: [undefined, ...colours] },
151
+ children: {
152
+ control: 'text',
153
+ description: [
154
+ 'The count to display inside the badge. Should always be a number or short numeric string.',
155
+ 'For counts above 99, use `"99+"` to keep the badge compact.',
156
+ 'Badge is for numbers only — do not pass category labels or text strings.',
157
+ ].join(' '),
158
+ table: {
159
+ type: { summary: 'React.ReactNode' },
160
+ },
161
+ },
162
+ size: {
163
+ control: 'select',
164
+ options: ['sm', 'md', 'lg'],
165
+ description: [
166
+ 'Controls the height, min-width, padding, font-size, and border-radius of the badge as a single unit.',
167
+ '`sm` — 1.125rem / 18px via `--badge-small-size`.',
168
+ '`md` — 1.25rem / 20px via `--badge-medium-size` (default).',
169
+ '`lg` — 1.5rem / 24px via `--badge-large-size`.',
170
+ 'All sizes use `--border-radius-round` so single-digit badges appear as circles and multi-digit badges expand into pills.',
171
+ ].join(' '),
172
+ table: {
173
+ type: { summary: "'sm' | 'md' | 'lg'" },
174
+ defaultValue: { summary: "'md'" },
175
+ },
176
+ },
177
+ colour: {
178
+ control: 'select',
179
+ options: [undefined, ...colours],
180
+ description: [
181
+ 'Extended-palette colour applied to the badge background.',
182
+ 'When omitted (default), the badge renders with `--color-grey-800` (charcoal) background and white text.',
183
+ 'White text pairings: salmon, green, and the default charcoal.',
184
+ 'Dark text pairings (`--color-extended-colours-{colour}-800`): purple, teal, yellow, orange, blue.',
185
+ 'Do not rely on colour alone to communicate meaning — the numeric value must always be visible.',
186
+ ].join(' '),
187
+ table: {
188
+ type: { summary: "'purple' | 'salmon' | 'teal' | 'yellow' | 'green' | 'orange' | 'blue' | undefined" },
189
+ defaultValue: { summary: 'undefined (charcoal)' },
190
+ },
191
+ },
192
+ a11yLabel: {
193
+ control: 'text',
194
+ description: [
195
+ 'Accessible label for the badge. Sets `aria-label` on the wrapper `<span>` and wraps',
196
+ '`children` in an `aria-hidden="true"` span to suppress the duplicate numeric announcement.',
197
+ 'Use when the badge sits next to an icon or element that provides no visible label — e.g.',
198
+ '`a11yLabel="5 unread messages"` next to a bell icon.',
199
+ 'When omitted, the numeric content is announced as-is by screen readers.',
200
+ ].join(' '),
201
+ table: {
202
+ type: { summary: 'string' },
203
+ defaultValue: { summary: 'undefined' },
204
+ },
205
+ },
206
+ className: {
207
+ control: false,
208
+ description: 'Additional CSS class names appended to the badge element. Use sparingly — prefer size and colour props.',
209
+ table: {
210
+ type: { summary: 'string' },
211
+ defaultValue: { summary: 'undefined' },
212
+ },
213
+ },
21
214
  },
22
- };
215
+ } satisfies Meta<typeof Badge>;
23
216
 
24
217
  export default meta;
25
-
218
+ // Use StoryObj<typeof Badge> (not typeof meta) so that render-only stories are
219
+ // not forced to provide args for the required `children` prop — the template
220
+ // components supply their own Badge instances directly.
26
221
  type Story = StoryObj<typeof Badge>;
27
222
 
223
+ // ---------------------------------------------------------------------------
224
+ // Helper: attach a per-story description to docs
225
+ // ---------------------------------------------------------------------------
226
+
28
227
  const withDescription = (story: Story, description: string): Story => ({
29
228
  ...story,
30
229
  parameters: {
@@ -38,37 +237,665 @@ const withDescription = (story: Story, description: string): Story => ({
38
237
  },
39
238
  });
40
239
 
41
- export const Count: Story = withDescription({
42
- args: {
43
- children: '3',
240
+ // ---------------------------------------------------------------------------
241
+ // Template components for composition stories
242
+ // (Named components avoid react-hooks lint issues — the react-hooks ESLint
243
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.)
244
+ // ---------------------------------------------------------------------------
245
+
246
+ const WithIconTemplate = () => (
247
+ <div
248
+ style={{
249
+ padding: 'var(--spacing-xlarge)',
250
+ display: 'flex',
251
+ alignItems: 'center',
252
+ gap: 'var(--spacing-large)',
253
+ }}
254
+ >
255
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
256
+ <div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 'var(--spacing-xsmall)' }}>
257
+ <Icon name="mail" size={16} screenReaderText="Notifications" />
258
+ <Badge>3</Badge>
259
+ </div>
260
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
261
+ Notifications
262
+ </p>
263
+ </div>
264
+ </div>
265
+ );
266
+
267
+ const InlineWithTextTemplate = () => (
268
+ <div
269
+ style={{
270
+ padding: 'var(--spacing-xlarge)',
271
+ display: 'flex',
272
+ flexDirection: 'column',
273
+ gap: 'var(--spacing-large)',
274
+ }}
275
+ >
276
+ <div
277
+ style={{
278
+ display: 'flex',
279
+ alignItems: 'center',
280
+ gap: 'var(--spacing-large)',
281
+ }}
282
+ >
283
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
284
+ Behaviour events
285
+ </span>
286
+ <Badge>14</Badge>
287
+ </div>
288
+ <div
289
+ style={{
290
+ display: 'flex',
291
+ alignItems: 'center',
292
+ gap: 'var(--spacing-large)',
293
+ }}
294
+ >
295
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
296
+ Pending exclusions
297
+ </span>
298
+ <Badge colour="salmon">3</Badge>
299
+ </div>
300
+ <div
301
+ style={{
302
+ display: 'flex',
303
+ alignItems: 'center',
304
+ gap: 'var(--spacing-large)',
305
+ }}
306
+ >
307
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
308
+ Attendance alerts
309
+ </span>
310
+ <Badge colour="orange">27</Badge>
311
+ </div>
312
+ </div>
313
+ );
314
+
315
+ const AllSizesTemplate = () => (
316
+ <div
317
+ style={{
318
+ padding: 'var(--spacing-xlarge)',
319
+ display: 'flex',
320
+ gap: 'var(--spacing-xlarge)',
321
+ alignItems: 'flex-end',
322
+ }}
323
+ >
324
+ <div
325
+ style={{
326
+ display: 'flex',
327
+ flexDirection: 'column',
328
+ alignItems: 'center',
329
+ gap: 'var(--spacing-large)',
330
+ }}
331
+ >
332
+ <Badge size="sm">7</Badge>
333
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
334
+ sm — 1.125rem
335
+ </p>
336
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
337
+ --badge-small-size
338
+ </p>
339
+ </div>
340
+ <div
341
+ style={{
342
+ display: 'flex',
343
+ flexDirection: 'column',
344
+ alignItems: 'center',
345
+ gap: 'var(--spacing-large)',
346
+ }}
347
+ >
348
+ <Badge size="md">7</Badge>
349
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
350
+ md — 1.25rem
351
+ </p>
352
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
353
+ --badge-medium-size
354
+ </p>
355
+ </div>
356
+ <div
357
+ style={{
358
+ display: 'flex',
359
+ flexDirection: 'column',
360
+ alignItems: 'center',
361
+ gap: 'var(--spacing-large)',
362
+ }}
363
+ >
364
+ <Badge size="lg">7</Badge>
365
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
366
+ lg — 1.5rem
367
+ </p>
368
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
369
+ --badge-large-size
370
+ </p>
371
+ </div>
372
+ </div>
373
+ );
374
+
375
+ const AllColoursTemplate = () => (
376
+ <div
377
+ style={{
378
+ padding: 'var(--spacing-xlarge)',
379
+ display: 'flex',
380
+ flexDirection: 'column',
381
+ gap: 'var(--spacing-large)',
382
+ }}
383
+ >
384
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
385
+ Default (charcoal) + all 7 extended-palette colours
386
+ </p>
387
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-large)', alignItems: 'center' }}>
388
+ {/* Default charcoal first */}
389
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
390
+ <Badge>9</Badge>
391
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>default</span>
392
+ </div>
393
+ {colours.map(c => (
394
+ <div key={c} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
395
+ <Badge colour={c}>9</Badge>
396
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>{c}</span>
397
+ </div>
398
+ ))}
399
+ </div>
400
+ </div>
401
+ );
402
+
403
+ const AllVariantsTemplate = () => (
404
+ <div
405
+ style={{
406
+ padding: 'var(--spacing-xlarge)',
407
+ display: 'flex',
408
+ flexDirection: 'column',
409
+ gap: 'var(--spacing-xlarge)',
410
+ }}
411
+ >
412
+ {(['sm', 'md', 'lg'] as const).map(size => (
413
+ <div key={size} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
414
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
415
+ Size:
416
+ {' '}
417
+ {size}
418
+ </p>
419
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-large)', alignItems: 'center' }}>
420
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
421
+ <Badge size={size}>9</Badge>
422
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>default</span>
423
+ </div>
424
+ {colours.map(c => (
425
+ <div key={c} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
426
+ <Badge size={size} colour={c}>9</Badge>
427
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>{c}</span>
428
+ </div>
429
+ ))}
430
+ </div>
431
+ </div>
432
+ ))}
433
+ </div>
434
+ );
435
+
436
+ const A11yWithLabelTemplate = () => (
437
+ <div
438
+ style={{
439
+ padding: 'var(--spacing-xlarge)',
440
+ display: 'flex',
441
+ flexDirection: 'column',
442
+ gap: 'var(--spacing-xlarge)',
443
+ }}
444
+ >
445
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
446
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-destructive-600)' }}>
447
+ Without a11yLabel — screen reader announces "5" (no context)
448
+ </p>
449
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
450
+ <Icon name="mail" size={16} screenReaderText="Notifications" />
451
+ <Badge>5</Badge>
452
+ </div>
453
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
454
+ Screen reader: "mail Notifications, 5"
455
+ </p>
456
+ </div>
457
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
458
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-success-600)' }}>
459
+ With a11yLabel — screen reader announces the full meaningful label
460
+ </p>
461
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
462
+ <Icon name="mail" size={16} />
463
+ <Badge a11yLabel="5 unread messages">5</Badge>
464
+ </div>
465
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
466
+ Screen reader: "5 unread messages"
467
+ </p>
468
+ </div>
469
+ </div>
470
+ );
471
+
472
+ // ---------------------------------------------------------------------------
473
+ // Stories
474
+ // ---------------------------------------------------------------------------
475
+
476
+ export const Default: Story = withDescription(
477
+ {
478
+ args: {
479
+ children: '3',
480
+ size: 'md',
481
+ colour: undefined,
482
+ },
483
+ render: args => <Badge {...args} />,
484
+ },
485
+ 'The interactive canvas story — every prop is wired to the **Controls** panel. Use the controls to explore sizes, colours, and the a11yLabel pattern. Tip: try `size="sm"` for navigation contexts or `colour="salmon"` for urgent counts.',
486
+ );
487
+
488
+ export const SingleDigit: Story = withDescription(
489
+ {
490
+ args: { children: '3' },
491
+ parameters: {
492
+ docs: {
493
+ source: {
494
+ language: 'tsx',
495
+ code: `
496
+ import { Badge } from '@arbor-education/design-system.components';
497
+
498
+ function BadgeSingleDigitExample() {
499
+ return <Badge>3</Badge>;
500
+ }
501
+
502
+ export default BadgeSingleDigitExample;
503
+ `.trim(),
504
+ },
505
+ },
506
+ },
507
+ },
508
+ 'A single-digit count — the canonical badge case. Because `min-width` equals `height` via `--badge-medium-size`, single-digit badges render as perfect circles. This circular geometry is intentional: it signals "count" rather than "label" at a glance.',
509
+ );
510
+
511
+ export const DoubleDigit: Story = withDescription(
512
+ {
513
+ args: { children: '12' },
514
+ parameters: {
515
+ docs: {
516
+ source: {
517
+ language: 'tsx',
518
+ code: `
519
+ import { Badge } from '@arbor-education/design-system.components';
520
+
521
+ function BadgeDoubleDigitExample() {
522
+ return <Badge>12</Badge>;
523
+ }
524
+
525
+ export default BadgeDoubleDigitExample;
526
+ `.trim(),
527
+ },
528
+ },
529
+ },
530
+ },
531
+ 'A double-digit count, showing how the badge expands into a pill when content exceeds the minimum width. All three sizes use `--border-radius-round` so the pill shape is consistent regardless of size.',
532
+ );
533
+
534
+ export const Overflow: Story = withDescription(
535
+ {
536
+ args: { children: '99+' },
537
+ parameters: {
538
+ docs: {
539
+ source: {
540
+ language: 'tsx',
541
+ code: `
542
+ import { Badge } from '@arbor-education/design-system.components';
543
+
544
+ function BadgeOverflowExample() {
545
+ return <Badge>99+</Badge>;
546
+ }
547
+
548
+ export default BadgeOverflowExample;
549
+ `.trim(),
550
+ },
551
+ },
552
+ },
553
+ },
554
+ 'The standard truncation convention for counts above 99. Always display `"99+"` rather than the real number to prevent layout overflow and keep the badge visually compact. Never render a three-digit count like `"247"` — it breaks the badge geometry.',
555
+ );
556
+
557
+ export const Zero: Story = withDescription(
558
+ {
559
+ args: { children: '0' },
560
+ parameters: {
561
+ docs: {
562
+ source: {
563
+ language: 'tsx',
564
+ code: `
565
+ import { Badge } from '@arbor-education/design-system.components';
566
+
567
+ function BadgeZeroExample() {
568
+ return <Badge>0</Badge>;
569
+ }
570
+
571
+ export default BadgeZeroExample;
572
+ `.trim(),
573
+ },
574
+ },
575
+ },
44
576
  },
45
- }, 'Shows the default numeric Badge used for counts and status totals.');
577
+ 'A zero count is a valid, intentional state — it signals that all notifications have been read or all items have been resolved. Do not hide the badge when the count reaches zero unless the design explicitly calls for it; the badge disappearing can be confusing.',
578
+ );
579
+
580
+ export const WithIcon: Story = withDescription(
581
+ {
582
+ render: WithIconTemplate,
583
+ parameters: {
584
+ docs: {
585
+ source: {
586
+ language: 'tsx',
587
+ code: `
588
+ import { Badge, Icon } from '@arbor-education/design-system.components';
589
+
590
+ function WithIconTemplate() {
591
+ return (
592
+ <div
593
+ style={{
594
+ padding: 'var(--spacing-xlarge)',
595
+ display: 'flex',
596
+ alignItems: 'center',
597
+ gap: 'var(--spacing-large)',
598
+ }}
599
+ >
600
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
601
+ <div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 'var(--spacing-xsmall)' }}>
602
+ <Icon name="mail" size={16} screenReaderText="Notifications" />
603
+ <Badge>3</Badge>
604
+ </div>
605
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
606
+ Notifications
607
+ </p>
608
+ </div>
609
+ </div>
610
+ );
611
+ }
46
612
 
47
- export const WithA11yLabel: Story = withDescription({
48
- args: {
49
- children: '3',
50
- a11yLabel: '3 selected items',
613
+ export default WithIconTemplate;
614
+ `.trim(),
615
+ },
616
+ },
617
+ },
51
618
  },
52
- }, 'Adds an accessible label when the visual content alone is not descriptive enough.');
619
+ 'The most common real-world usage pattern: a mail icon paired with a count badge and a "Notifications" label. The `Icon` component uses `screenReaderText="Notifications"` for its own label. The badge here has no `a11yLabel` — the adjacent visible label "Notifications" provides the context. For icon-only contexts with no visible label, always add `a11yLabel`.',
620
+ );
53
621
 
54
- export const Sizes: Story = withDescription({
55
- render: () => (
56
- <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
57
- <Badge size="sm">1</Badge>
58
- <Badge size="md">2</Badge>
59
- <Badge size="lg">3</Badge>
622
+ export const InlineWithText: Story = withDescription(
623
+ {
624
+ render: InlineWithTextTemplate,
625
+ parameters: {
626
+ docs: {
627
+ source: {
628
+ language: 'tsx',
629
+ code: `
630
+ import { Badge } from '@arbor-education/design-system.components';
631
+
632
+ function InlineWithTextTemplate() {
633
+ return (
634
+ <div
635
+ style={{
636
+ padding: 'var(--spacing-xlarge)',
637
+ display: 'flex',
638
+ flexDirection: 'column',
639
+ gap: 'var(--spacing-large)',
640
+ }}
641
+ >
642
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
643
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
644
+ Behaviour events
645
+ </span>
646
+ <Badge>14</Badge>
647
+ </div>
648
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
649
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
650
+ Pending exclusions
651
+ </span>
652
+ <Badge colour="salmon">3</Badge>
653
+ </div>
654
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
655
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
656
+ Attendance alerts
657
+ </span>
658
+ <Badge colour="orange">27</Badge>
659
+ </div>
60
660
  </div>
61
- ),
62
- }, 'Compares the small, medium, and large Badge size variants.');
661
+ );
662
+ }
63
663
 
64
- export const Colours: Story = withDescription({
65
- render: () => (
66
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
67
- {colours.map(c => (
68
- <Badge key={c} colour={c} size="md">
69
- {c.slice(0, 1).toUpperCase()}
70
- </Badge>
664
+ export default InlineWithTextTemplate;
665
+ `.trim(),
666
+ },
667
+ },
668
+ },
669
+ },
670
+ 'Badge next to a text label in a statistics row. The badge uses `flex-shrink: 0` via `ds-badge` so it never squishes inside a flex container. All three rows use design token colours — salmon for urgent (pending exclusions), orange for elevated (attendance alerts), and the default charcoal for neutral counts.',
671
+ );
672
+
673
+ export const AllSizes: Story = withDescription(
674
+ {
675
+ render: AllSizesTemplate,
676
+ parameters: {
677
+ docs: {
678
+ source: {
679
+ language: 'tsx',
680
+ code: `
681
+ import { Badge } from '@arbor-education/design-system.components';
682
+
683
+ function AllSizesTemplate() {
684
+ return (
685
+ <div
686
+ style={{
687
+ padding: 'var(--spacing-xlarge)',
688
+ display: 'flex',
689
+ gap: 'var(--spacing-xlarge)',
690
+ alignItems: 'flex-end',
691
+ }}
692
+ >
693
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
694
+ <Badge size="sm">7</Badge>
695
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
696
+ sm — 1.125rem
697
+ </p>
698
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
699
+ --badge-small-size
700
+ </p>
701
+ </div>
702
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
703
+ <Badge size="md">7</Badge>
704
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
705
+ md — 1.25rem
706
+ </p>
707
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
708
+ --badge-medium-size
709
+ </p>
710
+ </div>
711
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
712
+ <Badge size="lg">7</Badge>
713
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
714
+ lg — 1.5rem
715
+ </p>
716
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)' }}>
717
+ --badge-large-size
718
+ </p>
719
+ </div>
720
+ </div>
721
+ );
722
+ }
723
+
724
+ export default AllSizesTemplate;
725
+ `.trim(),
726
+ },
727
+ },
728
+ },
729
+ },
730
+ 'All three sizes side by side with their token names and resolved rem values. `sm` is right for navigation items and tab bars. `md` (the default) suits most standalone badge contexts. `lg` is appropriate when badge counts need to be more prominent — for example in a dashboard summary card. Never mix sizes arbitrarily — pick the size that matches the surrounding typographic scale.',
731
+ );
732
+
733
+ export const AllColours: Story = withDescription(
734
+ {
735
+ render: AllColoursTemplate,
736
+ parameters: {
737
+ docs: {
738
+ source: {
739
+ language: 'tsx',
740
+ code: `
741
+ import { Badge } from '@arbor-education/design-system.components';
742
+
743
+ const colours = ['purple', 'salmon', 'teal', 'yellow', 'green', 'orange', 'blue'] as const;
744
+
745
+ function AllColoursTemplate() {
746
+ return (
747
+ <div
748
+ style={{
749
+ padding: 'var(--spacing-xlarge)',
750
+ display: 'flex',
751
+ flexDirection: 'column',
752
+ gap: 'var(--spacing-large)',
753
+ }}
754
+ >
755
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
756
+ Default (charcoal) + all 7 extended-palette colours
757
+ </p>
758
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-large)', alignItems: 'center' }}>
759
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
760
+ <Badge>9</Badge>
761
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>default</span>
762
+ </div>
763
+ {colours.map(c => (
764
+ <div key={c} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
765
+ <Badge colour={c}>9</Badge>
766
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>{c}</span>
767
+ </div>
768
+ ))}
769
+ </div>
770
+ </div>
771
+ );
772
+ }
773
+
774
+ export default AllColoursTemplate;
775
+ `.trim(),
776
+ },
777
+ },
778
+ },
779
+ },
780
+ 'The default charcoal (no `colour` prop) first, followed by all seven extended-palette colours. White text renders on salmon and green; dark text (`--color-extended-colours-{colour}-800`) renders on purple, teal, yellow, orange, and blue for WCAG contrast compliance. Remember: colour alone must not be the only signal — the numeric value is always the primary communicator.',
781
+ );
782
+
783
+ export const AllVariants: Story = withDescription(
784
+ {
785
+ render: AllVariantsTemplate,
786
+ parameters: {
787
+ docs: {
788
+ source: {
789
+ language: 'tsx',
790
+ code: `
791
+ import { Badge } from '@arbor-education/design-system.components';
792
+
793
+ const colours = ['purple', 'salmon', 'teal', 'yellow', 'green', 'orange', 'blue'] as const;
794
+
795
+ function AllVariantsTemplate() {
796
+ return (
797
+ <div
798
+ style={{
799
+ padding: 'var(--spacing-xlarge)',
800
+ display: 'flex',
801
+ flexDirection: 'column',
802
+ gap: 'var(--spacing-xlarge)',
803
+ }}
804
+ >
805
+ {(['sm', 'md', 'lg'] as const).map(size => (
806
+ <div key={size} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
807
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
808
+ Size: {size}
809
+ </p>
810
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-large)', alignItems: 'center' }}>
811
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
812
+ <Badge size={size}>9</Badge>
813
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>default</span>
814
+ </div>
815
+ {colours.map(c => (
816
+ <div key={c} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
817
+ <Badge size={size} colour={c}>9</Badge>
818
+ <span className="ds-text" style={{ fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-600)' }}>{c}</span>
819
+ </div>
820
+ ))}
821
+ </div>
822
+ </div>
71
823
  ))}
72
824
  </div>
73
- ),
74
- }, 'Displays each available Badge colour variant side by side.');
825
+ );
826
+ }
827
+ export default AllVariantsTemplate;
828
+ `.trim(),
829
+ },
830
+ },
831
+ },
832
+ },
833
+ 'Full 3 × 8 grid: all three sizes crossed with all eight colour states (default charcoal + 7 palette colours). Use this story as a complete visual reference for the Badge component. Each cell uses the same count (`"9"`) to make size and colour comparisons accurate.',
834
+ );
835
+
836
+ export const A11yWithLabel: Story = withDescription(
837
+ {
838
+ render: A11yWithLabelTemplate,
839
+ parameters: {
840
+ docs: {
841
+ source: {
842
+ language: 'tsx',
843
+ code: `
844
+ import { Badge, Icon } from '@arbor-education/design-system.components';
845
+
846
+ function A11yWithLabelTemplate() {
847
+ return (
848
+ <div
849
+ style={{
850
+ padding: 'var(--spacing-xlarge)',
851
+ display: 'flex',
852
+ flexDirection: 'column',
853
+ gap: 'var(--spacing-xlarge)',
854
+ }}
855
+ >
856
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
857
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-destructive-600)' }}>
858
+ Without a11yLabel — screen reader announces "5" (no context)
859
+ </p>
860
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
861
+ <Icon name="mail" size={16} screenReaderText="Notifications" />
862
+ <Badge>5</Badge>
863
+ </div>
864
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
865
+ Screen reader: "mail Notifications, 5"
866
+ </p>
867
+ </div>
868
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
869
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-success-600)' }}>
870
+ With a11yLabel — screen reader announces the full meaningful label
871
+ </p>
872
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
873
+ <Icon name="mail" size={16} />
874
+ <Badge a11yLabel="5 unread messages">5</Badge>
875
+ </div>
876
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
877
+ Screen reader: "5 unread messages"
878
+ </p>
879
+ </div>
880
+ </div>
881
+ );
882
+ }
883
+ export default A11yWithLabelTemplate;
884
+ `.trim(),
885
+ },
886
+ },
887
+ },
888
+ },
889
+ [
890
+ 'Accessibility contrast: top row shows a badge **without** `a11yLabel` — a screen reader',
891
+ 'moving through the icon and badge would announce the icon text and then the bare number `"5"`,',
892
+ 'providing no context about what those 5 items are.',
893
+ '',
894
+ 'The bottom row adds `a11yLabel="5 unread messages"`. The component sets `aria-label` on the',
895
+ 'wrapper `<span>` and wraps `children` in an `aria-hidden="true"` span. The screen reader now',
896
+ 'announces `"5 unread messages"` as a single coherent label — no duplicate number, full context.',
897
+ '',
898
+ 'Use `a11yLabel` whenever the badge sits next to an icon-only element or in a context where',
899
+ 'the number alone would be ambiguous to a screen reader user.',
900
+ ].join(' '),
901
+ );