@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,41 +1,732 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { Dot } from './Dot';
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 { Tag } from 'Components/tag/Tag';
12
+ import { Dot, type DotColour } from './Dot';
3
13
 
4
- const meta: Meta<typeof Dot> = {
5
- tags: ['autodocs'],
14
+ // ---------------------------------------------------------------------------
15
+ // Docs page content
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const DESCRIPTION_INTRO = [
19
+ 'Dot is the smallest coloured indicator in the Arbor design system — a fixed 10 × 10 pixel',
20
+ 'circle that communicates status, category, or grouping through colour. It provides the visual',
21
+ 'primitive; the consuming application owns the semantic meaning.',
22
+ ].join('\n');
23
+
24
+ const USAGE_GUIDANCE = [
25
+ '### When to use',
26
+ '',
27
+ '- **Inside `Tag`\'s `slotStart`** — the canonical dot-prefix pattern for category tags',
28
+ ' (`"Year 7"`, `"SEN"`, `"Active"`)',
29
+ '- **Alongside text in lists or tables** — paired with a student name, status label, or row',
30
+ ' description so that colour is never the sole conveyor of meaning (WCAG 1.4.1)',
31
+ '- **Chart or map legends** — where an adjacent label names the colour grouping',
32
+ '',
33
+ '---',
34
+ '',
35
+ '### When NOT to use',
36
+ '',
37
+ '| Instead of Dot, use | When you need |',
38
+ '|---|---|',
39
+ '| [`Badge`](?path=/docs/components-badge--docs) | A numeric count: `3`, `12`, `99+` |',
40
+ '| [`Icon`](?path=/docs/components-icon--docs) | A recognisable glyph that conveys meaning by shape |',
41
+ '| [`Tag`](?path=/docs/components-tag--docs) | A text label for metadata or category |',
42
+ '| [`Pill`](?path=/docs/components-pill--docs) | An interactive filter toggle |',
43
+ '',
44
+ '```tsx',
45
+ '// BAD — Icon name="dot" is Lucide\'s 16 px dot glyph, not the design-system circle',
46
+ '<Icon name="dot" />',
47
+ '',
48
+ '// GOOD — Dot is always 10 × 10 and perfectly round',
49
+ '<Dot colour="green" />',
50
+ '```',
51
+ '',
52
+ '---',
53
+ '',
54
+ '### Arbor de-facto colour semantics',
55
+ '',
56
+ 'The Arbor platform has established informal conventions for Dot colours. These are **not**',
57
+ 'enforced at the design system level — your application owns the semantic mapping — but',
58
+ 'applying them consistently improves recognition across the product.',
59
+ '',
60
+ '| Colour | Arbor convention |',
61
+ '|---|---|',
62
+ '| `green` | On track, positive attendance, active |',
63
+ '| `salmon` | Pupil Premium, attention needed |',
64
+ '| `orange` | At risk, warning |',
65
+ '| `purple` | SEN |',
66
+ '| `teal` | EAL |',
67
+ '| `blue` | Subject or category grouping |',
68
+ '| `yellow` | Active, neutral attention |',
69
+ '',
70
+ 'Limit the number of distinct colours visible at once — per the Pills & Tags & Badges',
71
+ 'Confluence guidance, fewer colours used consistently carry more meaning than a full rainbow.',
72
+ ].join('\n');
73
+
74
+ const DEVELOPER_NOTES = [
75
+ '### Closed API',
76
+ '',
77
+ 'Dot is deliberately closed: `colour` and `label` are the only props. There is no `size`',
78
+ '(always 10 × 10), no `className` or `style` override, and no `onClick`. If you need a',
79
+ 'different size or a clickable indicator, compose a different component.',
80
+ '',
81
+ '---',
82
+ '',
83
+ '### Accessibility',
84
+ '',
85
+ 'Dot has two modes, controlled by the optional `label` prop:',
86
+ '',
87
+ '| Mode | When | Markup |',
88
+ '|---|---|---|',
89
+ '| **Decorative** (default) | Adjacent text carries the meaning | `aria-hidden="true"` — invisible to assistive technology |',
90
+ '| **Meaningful** | Dot is standalone with no adjacent label | `aria-label={label}` — announced by screen readers |',
91
+ '',
92
+ 'The rule is simple:',
93
+ '',
94
+ '- If visible text next to the dot tells users what the colour means — omit `label`.',
95
+ '- If the dot is the only signal — provide `label`.',
96
+ '',
97
+ '**WCAG 1.4.1 Use of Colour**: colour alone must never be the only means of conveying',
98
+ 'information. Either provide a `label`, or pair the dot with adjacent text that carries the',
99
+ 'semantic weight.',
100
+ '',
101
+ '---',
102
+ '',
103
+ '### TypeScript types',
104
+ '',
105
+ '```ts',
106
+ "import { Dot } from '@arbor-education/design-system.components';",
107
+ '',
108
+ 'function MyDot(props: Dot.Props) { ... }',
109
+ '```',
110
+ '',
111
+ '| Type | Description |',
112
+ '|---|---|',
113
+ '| `Dot.Props` | Full props interface |',
114
+ "| `Dot.Colour` | `'purple' \\| 'salmon' \\| 'teal' \\| 'yellow' \\| 'green' \\| 'orange' \\| 'blue'` |",
115
+ ].join('\n');
116
+
117
+ const RELATED_COMPONENTS = [
118
+ '## Related components',
119
+ '',
120
+ '[Tag](?path=/docs/components-tag--docs) · [Badge](?path=/docs/components-badge--docs) · [Icon](?path=/docs/components-icon--docs) · [Pill](?path=/docs/components-pill--docs)',
121
+ ].join('\n');
122
+
123
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
124
+
125
+ function DotDocsPage() {
126
+ return (
127
+ <>
128
+ <Title />
129
+ <Subtitle />
130
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
131
+ <DocHeading>Interactive example</DocHeading>
132
+ <Markdown>{PROPS_INTRO}</Markdown>
133
+ <DocPrimary />
134
+ <Controls />
135
+ <DocHeading>Usage guidance</DocHeading>
136
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
137
+ <DocHeading>Developer notes</DocHeading>
138
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
139
+ <DocHeading>Examples</DocHeading>
140
+ <Stories title="" />
141
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
142
+ </>
143
+ );
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Data
148
+ // ---------------------------------------------------------------------------
149
+
150
+ const allColours: DotColour[] = ['purple', 'salmon', 'teal', 'yellow', 'green', 'orange', 'blue'];
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Meta
154
+ // ---------------------------------------------------------------------------
155
+
156
+ const meta = {
6
157
  title: 'Components/Dot',
7
158
  component: Dot,
8
- };
159
+ parameters: {
160
+ layout: 'centered',
161
+ docs: {
162
+ page: DotDocsPage,
163
+ },
164
+ },
165
+ tags: ['autodocs'],
166
+ argTypes: {
167
+ colour: {
168
+ control: 'select',
169
+ options: allColours,
170
+ description: [
171
+ 'The background colour of the circle. **Required** — there is no default.',
172
+ 'Establish a consistent semantic mapping in your application (e.g. green = on track,',
173
+ 'salmon = Pupil Premium) and apply it uniformly.',
174
+ ].join(' '),
175
+ table: {
176
+ type: { summary: "'purple' | 'salmon' | 'teal' | 'yellow' | 'green' | 'orange' | 'blue'" },
177
+ },
178
+ },
179
+ label: {
180
+ control: 'text',
181
+ description: [
182
+ 'Accessible label for the dot. When omitted (default), the dot is decorative and',
183
+ 'invisible to assistive technology — correct when adjacent visible text carries the',
184
+ 'meaning. When provided, the screen reader announces the label. Use this prop whenever',
185
+ 'the dot is the sole carrier of information (no adjacent text).',
186
+ ].join(' '),
187
+ table: {
188
+ type: { summary: 'string' },
189
+ defaultValue: { summary: 'undefined (decorative — aria-hidden)' },
190
+ },
191
+ },
192
+ },
193
+ } satisfies Meta<typeof Dot>;
9
194
 
10
195
  export default meta;
11
-
196
+ // Use StoryObj<typeof Dot> (not typeof meta) so that render-only stories are
197
+ // not forced to provide args for the required `colour` prop — template
198
+ // components supply their own Dot instances directly.
12
199
  type Story = StoryObj<typeof Dot>;
13
200
 
14
- export const Purple: Story = { args: { colour: 'purple' } };
15
- export const Salmon: Story = { args: { colour: 'salmon' } };
16
- export const Teal: Story = { args: { colour: 'teal' } };
17
- export const Yellow: Story = { args: { colour: 'yellow' } };
18
- export const Green: Story = { args: { colour: 'green' } };
19
- export const Orange: Story = { args: { colour: 'orange' } };
20
- export const Blue: Story = { args: { colour: 'blue' } };
21
-
22
- export const Labeled: Story = {
23
- args: {
24
- colour: 'purple',
25
- label: 'Priority indicator',
26
- },
27
- };
28
-
29
- export const AllColours: Story = {
30
- render: () => (
31
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
32
- <Dot colour="purple" />
33
- <Dot colour="salmon" />
34
- <Dot colour="teal" />
35
- <Dot colour="yellow" />
36
- <Dot colour="green" />
37
- <Dot colour="orange" />
38
- <Dot colour="blue" />
201
+ // ---------------------------------------------------------------------------
202
+ // Helper: attach a per-story description to docs
203
+ // ---------------------------------------------------------------------------
204
+
205
+ const withDescription = (story: Story, description: string): Story => ({
206
+ ...story,
207
+ parameters: {
208
+ ...story.parameters,
209
+ docs: {
210
+ ...story.parameters?.docs,
211
+ description: {
212
+ story: description,
213
+ },
214
+ },
215
+ },
216
+ });
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Template components for composition stories
220
+ // (Named components avoid react-hooks lint issues — the react-hooks ESLint
221
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.)
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const AllColoursTemplate = () => (
225
+ <div
226
+ style={{
227
+ padding: 'var(--spacing-xlarge)',
228
+ background: 'var(--color-grey-050)',
229
+ borderRadius: 'var(--border-radius-small)',
230
+ display: 'flex',
231
+ flexWrap: 'wrap',
232
+ gap: 'var(--spacing-small)',
233
+ alignItems: 'center',
234
+ }}
235
+ >
236
+ {allColours.map(colour => (
237
+ <div
238
+ key={colour}
239
+ style={{
240
+ display: 'flex',
241
+ flexDirection: 'column',
242
+ alignItems: 'center',
243
+ gap: 'var(--spacing-small)',
244
+ }}
245
+ >
246
+ <Dot colour={colour} label={colour} />
247
+ <span
248
+ className="ds-text"
249
+ style={{ color: 'var(--color-grey-600)' }}
250
+ >
251
+ {colour}
252
+ </span>
253
+ </div>
254
+ ))}
255
+ </div>
256
+ );
257
+
258
+ const DecorativeVsMeaningfulTemplate = () => (
259
+ <div
260
+ style={{
261
+ padding: 'var(--spacing-xlarge)',
262
+ display: 'flex',
263
+ gap: 'var(--spacing-xlarge)',
264
+ flexWrap: 'wrap',
265
+ }}
266
+ >
267
+ {/* LEFT: Decorative dot — adjacent text carries meaning */}
268
+ <div
269
+ style={{
270
+ display: 'flex',
271
+ flexDirection: 'column',
272
+ gap: 'var(--spacing-large)',
273
+ }}
274
+ >
275
+ <p
276
+ className="ds-text"
277
+ style={{ margin: 0, color: 'var(--color-grey-600)' }}
278
+ >
279
+ Decorative (aria-hidden)
280
+ </p>
281
+ <div
282
+ style={{
283
+ display: 'flex',
284
+ alignItems: 'center',
285
+ gap: 'var(--spacing-small)',
286
+ }}
287
+ >
288
+ {/* No label prop — dot is decorative because "Active" text is present */}
289
+ <Dot colour="green" />
290
+ <span className="ds-text">Active</span>
291
+ </div>
292
+ <p
293
+ className="ds-text"
294
+ style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}
295
+ >
296
+ Screen reader: &quot;Active&quot;
297
+ </p>
298
+ <p
299
+ className="ds-text"
300
+ style={{ margin: 0, color: 'var(--color-grey-600)' }}
301
+ >
302
+ The dot renders with no label prop. Adjacent text
303
+ &quot;Active&quot; carries the full meaning — the dot is
304
+ a decorative reinforcement only.
305
+ </p>
306
+ </div>
307
+
308
+ {/* RIGHT: Meaningful dot — standalone, no adjacent text */}
309
+ <div
310
+ style={{
311
+ display: 'flex',
312
+ flexDirection: 'column',
313
+ gap: 'var(--spacing-large)',
314
+ }}
315
+ >
316
+ <p
317
+ className="ds-text"
318
+ style={{ margin: 0, color: 'var(--color-grey-600)' }}
319
+ >
320
+ Meaningful (aria-label)
321
+ </p>
322
+ <div
323
+ style={{
324
+ display: 'flex',
325
+ alignItems: 'center',
326
+ gap: 'var(--spacing-small)',
327
+ }}
328
+ >
329
+ {/* label prop set — dot is meaningful, no adjacent text */}
330
+ <Dot colour="green" label="Active" />
331
+ </div>
332
+ <p
333
+ className="ds-text"
334
+ style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}
335
+ >
336
+ Screen reader: &quot;Active&quot;
337
+ </p>
338
+ <p
339
+ className="ds-text"
340
+ style={{ margin: 0, color: 'var(--color-grey-600)' }}
341
+ >
342
+ The dot renders with
343
+ {' '}
344
+ <code>label=&quot;Active&quot;</code>
345
+ . No adjacent text —
346
+ so the dot itself must carry the semantic meaning via
347
+ aria-label.
348
+ </p>
349
+ </div>
350
+ </div>
351
+ );
352
+
353
+ const InTagContextTemplate = () => (
354
+ <div
355
+ style={{
356
+ padding: 'var(--spacing-xlarge)',
357
+ display: 'flex',
358
+ gap: 'var(--spacing-small)',
359
+ flexWrap: 'wrap',
360
+ alignItems: 'center',
361
+ }}
362
+ >
363
+ {/*
364
+ Dot is decorative in all four Tags below — the Tag's text children
365
+ carry the semantic meaning. flex-shrink: 0 on ds-dot means the circle
366
+ never collapses inside Tag's internal flex layout.
367
+ */}
368
+ <Tag color="blue" slotStart={<Dot colour="blue" />}>Year 7</Tag>
369
+ <Tag color="green" slotStart={<Dot colour="green" />}>Active</Tag>
370
+ <Tag color="purple" slotStart={<Dot colour="purple" />}>SEN</Tag>
371
+ <Tag color="salmon" slotStart={<Dot colour="salmon" />}>At risk</Tag>
372
+ </div>
373
+ );
374
+
375
+ const InStatusListTemplate = () => (
376
+ <div
377
+ style={{
378
+ padding: 'var(--spacing-xlarge)',
379
+ display: 'flex',
380
+ flexDirection: 'column',
381
+ gap: 'var(--spacing-large)',
382
+ }}
383
+ >
384
+ <p
385
+ className="ds-text"
386
+ style={{ margin: 0, color: 'var(--color-grey-600)' }}
387
+ >
388
+ Today's attendance
389
+ </p>
390
+ <div
391
+ style={{
392
+ display: 'flex',
393
+ flexDirection: 'column',
394
+ gap: 'var(--spacing-small)',
395
+ }}
396
+ >
397
+ {/*
398
+ Most students share the same "Present" (green) dot.
399
+ Only flagged entries use different colours. This demonstrates
400
+ the "establish a semantic mapping and apply it consistently"
401
+ guidance — a meaningful pattern, not a decorative rainbow.
402
+
403
+ All dots here are decorative (no label prop) because the
404
+ adjacent student name + status text carries the meaning.
405
+ */}
406
+ {[
407
+ { name: 'Ahmed Al-Rashidi', status: 'Present', colour: 'green' as DotColour },
408
+ { name: 'Maria Santos', status: 'Present', colour: 'green' as DotColour },
409
+ { name: 'James Okonkwo', status: 'Late', colour: 'yellow' as DotColour },
410
+ { name: 'Chloe Patel', status: 'Medical', colour: 'teal' as DotColour },
411
+ { name: 'Oliver Wright', status: 'Absent', colour: 'salmon' as DotColour },
412
+ ].map(({ name, status, colour }) => (
413
+ <div
414
+ key={name}
415
+ style={{
416
+ display: 'flex',
417
+ alignItems: 'center',
418
+ gap: 'var(--spacing-small)',
419
+ }}
420
+ >
421
+ <Dot colour={colour} />
422
+ <span className="ds-text">{name}</span>
423
+ <span
424
+ className="ds-text"
425
+ style={{ color: 'var(--color-grey-600)' }}
426
+ >
427
+
428
+ {' '}
429
+ {status}
430
+ </span>
431
+ </div>
432
+ ))}
39
433
  </div>
40
- ),
41
- };
434
+ </div>
435
+ );
436
+
437
+ // ---------------------------------------------------------------------------
438
+ // Stories
439
+ // ---------------------------------------------------------------------------
440
+
441
+ export const Default: Story = withDescription(
442
+ {
443
+ args: {
444
+ colour: 'purple',
445
+ },
446
+ render: args => <Dot {...args} />,
447
+ },
448
+ 'The interactive canvas story — every prop is wired to the **Controls** panel. Use the controls to explore all 7 colours and toggle the `label` prop to switch between decorative (`aria-hidden`) and meaningful (`aria-label`) modes. Dot is decorative by default: no `label` prop means assistive technology ignores it entirely.',
449
+ );
450
+
451
+ export const AllColours: Story = withDescription(
452
+ {
453
+ parameters: {
454
+ controls: { disable: true },
455
+ docs: {
456
+ source: {
457
+ language: 'tsx',
458
+ code: `
459
+ import { Dot } from '@arbor-education/design-system.components';
460
+
461
+ const allColours = ['purple', 'salmon', 'teal', 'yellow', 'green', 'orange', 'blue'] as const;
462
+
463
+ function AllColoursTemplate() {
464
+ return (
465
+ <div
466
+ style={{
467
+ padding: 'var(--spacing-xlarge)',
468
+ background: 'var(--color-grey-050)',
469
+ borderRadius: 'var(--border-radius-small)',
470
+ display: 'flex',
471
+ flexWrap: 'wrap',
472
+ gap: 'var(--spacing-small)',
473
+ alignItems: 'center',
474
+ }}
475
+ >
476
+ {allColours.map(colour => (
477
+ <div
478
+ key={colour}
479
+ style={{
480
+ display: 'flex',
481
+ flexDirection: 'column',
482
+ alignItems: 'center',
483
+ gap: 'var(--spacing-small)',
484
+ }}
485
+ >
486
+ <Dot colour={colour} label={colour} />
487
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
488
+ {colour}
489
+ </span>
490
+ </div>
491
+ ))}
492
+ </div>
493
+ );
494
+ }
495
+ export default AllColoursTemplate;
496
+ `.trim(),
497
+ },
498
+ },
499
+ },
500
+ render: AllColoursTemplate,
501
+ },
502
+ [
503
+ 'All 7 `DotColour` values displayed together. The subtle grey wash (`--color-grey-050`)',
504
+ 'background ensures 10 × 10 circles are visible against white canvas. Each dot uses',
505
+ '`label={colour}` here so screen readers can navigate the story sensibly — in real',
506
+ 'usage, decorative dots paired with visible text would omit the `label` prop.',
507
+ '',
508
+ '**Note:** `DotColour` shares its 7 values with `BadgeColour` — a single colour string',
509
+ 'works for both components without any casting.',
510
+ ].join(' '),
511
+ );
512
+
513
+ export const WithAccessibleLabel: Story = withDescription(
514
+ {
515
+ args: {
516
+ colour: 'purple',
517
+ label: 'Purple — SEN category',
518
+ },
519
+ render: args => <Dot {...args} />,
520
+ parameters: {
521
+ docs: {
522
+ source: {
523
+ language: 'tsx',
524
+ code: `
525
+ import { Dot } from '@arbor-education/design-system.components';
526
+
527
+ function WithAccessibleLabelExample() {
528
+ return <Dot colour="purple" label="Purple — SEN category" />;
529
+ }
530
+ export default WithAccessibleLabelExample;
531
+ `.trim(),
532
+ },
533
+ },
534
+ },
535
+ },
536
+ [
537
+ 'A dot with a `label` prop — rendered with `aria-label="Purple — SEN category"` and no',
538
+ '`aria-hidden`. A screen reader navigating to this element announces the label directly.',
539
+ 'Use this pattern whenever the dot is the sole carrier of information — for example, a',
540
+ 'standalone status indicator in a column with no adjacent text. When visible text is',
541
+ 'present beside the dot, omit `label` instead (see **DecorativeVsMeaningful**).',
542
+ ].join(' '),
543
+ );
544
+
545
+ export const DecorativeVsMeaningful: Story = withDescription(
546
+ {
547
+ parameters: {
548
+ controls: { disable: true },
549
+ docs: {
550
+ source: {
551
+ language: 'tsx',
552
+ code: `
553
+ import { Dot } from '@arbor-education/design-system.components';
554
+
555
+ function DecorativeVsMeaningfulTemplate() {
556
+ return (
557
+ <div
558
+ style={{
559
+ padding: 'var(--spacing-xlarge)',
560
+ display: 'flex',
561
+ gap: 'var(--spacing-xlarge)',
562
+ flexWrap: 'wrap',
563
+ }}
564
+ >
565
+ {/* Decorative dot — adjacent text carries meaning */}
566
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
567
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
568
+ Decorative (aria-hidden)
569
+ </p>
570
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
571
+ {/* No label prop — dot is decorative because "Active" text is present */}
572
+ <Dot colour="green" />
573
+ <span className="ds-text">Active</span>
574
+ </div>
575
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
576
+ Screen reader: "Active"
577
+ </p>
578
+ </div>
579
+
580
+ {/* Meaningful dot — standalone, no adjacent text */}
581
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
582
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
583
+ Meaningful (aria-label)
584
+ </p>
585
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
586
+ {/* label prop set — dot is meaningful, no adjacent text */}
587
+ <Dot colour="green" label="Active" />
588
+ </div>
589
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
590
+ Screen reader: "Active"
591
+ </p>
592
+ </div>
593
+ </div>
594
+ );
595
+ }
596
+ export default DecorativeVsMeaningfulTemplate;
597
+ `.trim(),
598
+ },
599
+ },
600
+ },
601
+ render: DecorativeVsMeaningfulTemplate,
602
+ },
603
+ [
604
+ 'The core accessibility teaching story. **Left column**: a decorative dot paired with',
605
+ 'visible text ("Active") — no `label` prop, `aria-hidden="true"`, screen reader',
606
+ 'announces only the text. **Right column**: a standalone meaningful dot — `label="Active"`,',
607
+ 'screen reader announces the label.',
608
+ '',
609
+ '**WCAG 1.4.1 Use of Colour**: colour alone must never be the only means of conveying',
610
+ 'information. The rule is: if visible text next to the dot tells users what the colour means,',
611
+ 'omit `label`. If the dot is the only signal, provide `label`.',
612
+ ].join(' '),
613
+ );
614
+
615
+ export const InTagContext: Story = withDescription(
616
+ {
617
+ parameters: {
618
+ controls: { disable: true },
619
+ docs: {
620
+ source: {
621
+ language: 'tsx',
622
+ code: `
623
+ import { Dot, Tag } from '@arbor-education/design-system.components';
624
+
625
+ function InTagContextTemplate() {
626
+ return (
627
+ <div
628
+ style={{
629
+ padding: 'var(--spacing-xlarge)',
630
+ display: 'flex',
631
+ gap: 'var(--spacing-small)',
632
+ flexWrap: 'wrap',
633
+ alignItems: 'center',
634
+ }}
635
+ >
636
+ <Tag color="blue" slotStart={<Dot colour="blue" />}>Year 7</Tag>
637
+ <Tag color="green" slotStart={<Dot colour="green" />}>Active</Tag>
638
+ <Tag color="purple" slotStart={<Dot colour="purple" />}>SEN</Tag>
639
+ <Tag color="salmon" slotStart={<Dot colour="salmon" />}>At risk</Tag>
640
+ </div>
641
+ );
642
+ }
643
+ export default InTagContextTemplate;
644
+ `.trim(),
645
+ },
646
+ },
647
+ },
648
+ render: InTagContextTemplate,
649
+ },
650
+ [
651
+ 'The canonical Arbor usage pattern: `<Tag slotStart={<Dot colour="..." />}>Label</Tag>`.',
652
+ 'All four dots are **decorative** — no `label` prop — because `Tag`\'s text children',
653
+ '("Year 7", "Active", "SEN", "At risk") already carry the full semantic meaning.',
654
+ '',
655
+ '`flex-shrink: 0` is built into `ds-dot`, so the circle never collapses inside',
656
+ '`Tag`\'s internal flex layout, even in very narrow containers.',
657
+ '',
658
+ '**Colour note:** `Tag` and `Dot` use separate colour props (`TagColor` vs `DotColour`)',
659
+ 'but share the same 7 extended-palette values. Always match both to the same colour',
660
+ 'for visual consistency.',
661
+ ].join(' '),
662
+ );
663
+
664
+ export const InStatusList: Story = withDescription(
665
+ {
666
+ parameters: {
667
+ controls: { disable: true },
668
+ docs: {
669
+ source: {
670
+ language: 'tsx',
671
+ code: `
672
+ import { Dot } from '@arbor-education/design-system.components';
673
+
674
+ function InStatusListTemplate() {
675
+ const students = [
676
+ { name: 'Ahmed Al-Rashidi', status: 'Present', colour: 'green' as const },
677
+ { name: 'Maria Santos', status: 'Present', colour: 'green' as const },
678
+ { name: 'James Okonkwo', status: 'Late', colour: 'yellow' as const },
679
+ { name: 'Chloe Patel', status: 'Medical', colour: 'teal' as const },
680
+ { name: 'Oliver Wright', status: 'Absent', colour: 'salmon' as const },
681
+ ];
682
+
683
+ return (
684
+ <div
685
+ style={{
686
+ padding: 'var(--spacing-xlarge)',
687
+ display: 'flex',
688
+ flexDirection: 'column',
689
+ gap: 'var(--spacing-large)',
690
+ }}
691
+ >
692
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
693
+ Today's attendance
694
+ </p>
695
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
696
+ {students.map(({ name, status, colour }) => (
697
+ <div
698
+ key={name}
699
+ style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}
700
+ >
701
+ <Dot colour={colour} />
702
+ <span className="ds-text">{name}</span>
703
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
704
+ — {status}
705
+ </span>
706
+ </div>
707
+ ))}
708
+ </div>
709
+ </div>
710
+ );
711
+ }
712
+ export default InStatusListTemplate;
713
+ `.trim(),
714
+ },
715
+ },
716
+ },
717
+ render: InStatusListTemplate,
718
+ },
719
+ [
720
+ 'A realistic Arbor attendance list: student name, status text, and a Dot for each row.',
721
+ 'Most students share the same `green` dot ("Present") — only flagged entries use',
722
+ 'different colours (`yellow` for Late, `teal` for Medical, `salmon` for Absent).',
723
+ '',
724
+ 'This demonstrates the key design principle: **establish a semantic colour mapping and',
725
+ 'apply it consistently**. A meaningful pattern — where most rows are one colour and',
726
+ 'exceptions stand out — carries far more information than a decorative rainbow.',
727
+ '',
728
+ 'All dots here are **decorative** (no `label` prop) because the adjacent name and',
729
+ 'status text ("Ahmed Al-Rashidi — Present") carries the full meaning for every row.',
730
+ 'WCAG 1.4.1 is satisfied by the visible text, not the colour alone.',
731
+ ].join(' '),
732
+ );