@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,142 +1,774 @@
1
- import type { Meta } from '@storybook/react-vite';
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';
2
11
  import { Heading } from './Heading';
3
- import { Button } from '../button/Button';
4
- import { Icon } from '../icon/Icon';
12
+ import { Button } from 'Components/button/Button';
5
13
  import { EditableText } from 'Components/editableText/EditableText';
14
+ import { TooltipWrapper } from 'Components/tooltip/TooltipWrapper';
6
15
 
7
- const meta: Meta<typeof Heading> = {
16
+ // ---------------------------------------------------------------------------
17
+ // Docs page content
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const DESCRIPTION_INTRO = [
21
+ 'The **Heading** component is the page-level heading area for Arbor screens. It renders',
22
+ 'a semantic `h1`–`h4` element (driven by the `level` prop) with consistent typography',
23
+ 'tokens and optional inline action controls — all in one composable unit. It grounds the',
24
+ 'user at a glance: "you are HERE, and here is what you can do."',
25
+ ].join('\n');
26
+
27
+ const USAGE_GUIDANCE = [
28
+ '### When to use',
29
+ '',
30
+ '- **Page-level headings** — the primary H1 heading at the top of any Arbor page',
31
+ '- **Entity names** — the name of the thing the user is looking at (a student record, an attendance register, a report)',
32
+ '- **Section headers** — H2–H4 sub-headings with optional contextual actions',
33
+ '- **Orienting the user** — any moment where "where am I?" is a genuine question',
34
+ '',
35
+ '---',
36
+ '',
37
+ '### When NOT to use',
38
+ '',
39
+ '| Instead of Heading... | Use... | Why |',
40
+ '|---|---|---|',
41
+ '| Section headings inside cards or sidebars | Plain `<h2>` / `<h3>` with custom styling | Heading always fills 100% container width; it is designed for page-level layout, not embedded UI |',
42
+ '| Subtitles or sub-labels under a heading | Nothing — subtitles are **explicitly banned** by the design spec | The design system does not support subtitle slots; adding one breaks the page heading contract |',
43
+ '| Decorative or marketing text | A plain styled element (`<p>`, `<span>`) | Semantic headings affect page outline and screen reader navigation — don\'t pollute the outline |',
44
+ '',
45
+ '---',
46
+ '',
47
+ '### Design guidance (from Confluence page 2393767941)',
48
+ '',
49
+ '- **Single H1 per page** — using more than one H1 breaks accessibility and dilutes the page hierarchy.',
50
+ ' The H1 is almost always the entity name.',
51
+ '- **Actions: max 1 primary + 2 secondary** — the Confluence spec recommends 1–2 total, with a hard ceiling',
52
+ ' of 3. If you have more than 3 actions, consolidate into a `Dropdown`.',
53
+ '- **No subtitles** — the design spec explicitly bans subtitle slots in page headings.',
54
+ '- **Small screens** — heading controls stack below the title. They do NOT collapse into an overflow menu.',
55
+ ' Keep your action count low so stacking looks intentional on mobile.',
56
+ '- **Back button** — the back-chevron pattern (tertiary button in the left container) is a **legacy pattern**',
57
+ ' being phased out. New pages should use deterministic navigation instead.',
58
+ '',
59
+ '---',
60
+ '',
61
+ '### The compound component split',
62
+ '',
63
+ 'Heading is a compound component with one static sub-component: `Heading.InnerContainer`.',
64
+ '',
65
+ 'When you pass **two** `Heading.InnerContainer` children, the heading\'s CSS (`justify-content: space-between`)',
66
+ 'automatically pushes the first container to the left and the second to the right. No extra layout',
67
+ 'wrapper or flex gymnastics needed:',
68
+ '',
69
+ '```tsx',
70
+ '<Heading level={1}>',
71
+ ' <Heading.InnerContainer>Student records</Heading.InnerContainer>',
72
+ ' <Heading.InnerContainer>',
73
+ ' <Button variant="primary" type="button">Save changes</Button>',
74
+ ' </Heading.InnerContainer>',
75
+ '</Heading>',
76
+ '```',
77
+ '',
78
+ '`Heading.InnerContainer` is just a `<span>` with `className="ds-heading__inner-container"`. It groups',
79
+ 'content on one side. It inherits the heading\'s typography tokens, so its text renders at the correct',
80
+ 'heading size automatically.',
81
+ '',
82
+ '#### `className="medium-spacing-gap"` utility',
83
+ '',
84
+ 'When multiple buttons sit in a single `Heading.InnerContainer`, add `className="medium-spacing-gap"`',
85
+ 'to that container. This is a global utility class from `src/global.scss` (line 11) that applies',
86
+ '`gap: var(--spacing-small)` between flex children. It is the canonical way to space multiple buttons',
87
+ 'in a heading — do not add manual margins or padding.',
88
+ '',
89
+ '```tsx',
90
+ '<Heading.InnerContainer className="medium-spacing-gap">',
91
+ ' <Button variant="secondary" type="button">Cancel</Button>',
92
+ ' <Button variant="primary" type="button">Save changes</Button>',
93
+ '</Heading.InnerContainer>',
94
+ '```',
95
+ '',
96
+ '#### Heading.InnerContainer props',
97
+ '',
98
+ '| Prop | Type | Default | Description |',
99
+ '|---|---|---|---|',
100
+ '| `children` | `ReactNode` | — | The content of one "side" of the heading — text, buttons, or any inline element |',
101
+ '| `className` | `string` | — | Additional CSS classes. Use `"medium-spacing-gap"` (global utility, `src/global.scss` line 11) to add `gap: var(--spacing-small)` between multiple button children |',
102
+ '| `...rest` | `HTMLAttributes<HTMLSpanElement>` | — | All native `<span>` attributes pass through |',
103
+ ].join('\n');
104
+
105
+ const DEVELOPER_NOTES = [
106
+ '### Anti-patterns',
107
+ '',
108
+ '1. **Using Heading for section-level headings inside cards or sidebars** — breaks semantic hierarchy',
109
+ ' and layout. Use a plain `<h2>` / `<h3>` with your own styling.',
110
+ '2. **Too many action buttons** — max 3 total per Confluence (1 primary + 2 secondary). If you need',
111
+ ' more, use a `Dropdown` to consolidate.',
112
+ '3. **Subtitle via creative JSX** — do not pass a third child to simulate a subtitle. The design spec',
113
+ ' explicitly bans subtitles in page headings.',
114
+ '4. **Overriding font-size while keeping `level`** — creates a visual/semantic mismatch.',
115
+ '',
116
+ '#### Typography tokens',
117
+ '',
118
+ 'Each `level` maps to a dedicated set of type tokens — for instance `level={1}` uses',
119
+ '`--type-headings-h1-size`, `--type-headings-h1-family`, `--type-headings-h1-weight`, and',
120
+ '`--type-headings-h1-line-height`. Heading typography is always driven by design tokens; never',
121
+ 'override `font-size` with a hardcoded value while keeping a `level` prop, as this creates a',
122
+ 'visual/semantic mismatch (screen reader announces "heading level 1" but the user sees h3-sized text).',
123
+ '',
124
+ '#### Width and spacing',
125
+ '',
126
+ 'Heading always fills **100% of its parent container** — this is baked into `.ds-heading`. Do not',
127
+ 'add top or bottom margin to the `Heading` element itself; control vertical rhythm from the page',
128
+ 'layout wrapper.',
129
+ '',
130
+ '#### HeadingLevel type',
131
+ '',
132
+ '`HeadingLevel` is exported from `Heading.tsx` as `1 | 2 | 3 | 4`, but it is **not re-exported**',
133
+ 'from `src/index.ts` (only `Heading` is). Consumers who need the type must import it directly:',
134
+ '`import type { HeadingLevel } from \'Components/heading/Heading\'`.',
135
+ '',
136
+ '---',
137
+ '',
138
+ '### Accessibility',
139
+ '',
140
+ '- **Single H1 per page** — essential for screen readers; multiple H1s break the page outline',
141
+ '- **Keyboard order** — the DOM order is left container then right container, matching reading order and Tab sequence',
142
+ '- **Icon-only buttons in the heading** require a `TooltipWrapper` with an accessible label. A tooltip',
143
+ ' on hover alone is not sufficient — it must also appear on keyboard focus, and assistive technology',
144
+ ' must be able to read the label. Always pair `iconRightScreenReaderText` with `TooltipWrapper`.',
145
+ '- **`id` + `aria-labelledby`** — give the heading an `id` and reference it from an adjacent',
146
+ ' `<section aria-labelledby="...">` to associate the landmark with its heading. Screen readers',
147
+ ' announce the section name when the user enters the landmark region.',
148
+ '',
149
+ '---',
150
+ '',
151
+ '### TypeScript types',
152
+ '',
153
+ '```ts',
154
+ "import { Heading } from '@arbor-education/design-system.components';",
155
+ '',
156
+ 'function MyHeading(props: Heading.Props) { ... }',
157
+ '```',
158
+ '',
159
+ '| Type | Description |',
160
+ '|---|---|',
161
+ '| `Heading.Props` | Full props interface |',
162
+ '| `Heading.Level` | `1 \\| 2 \\| 3 \\| 4` |',
163
+ '| `Heading.InnerContainerProps` | Props for `<Heading.InnerContainer>` |',
164
+ ].join('\n');
165
+
166
+ const RELATED_COMPONENTS = [
167
+ '## Related components',
168
+ '',
169
+ '[Section](?path=/docs/components-section--docs) · [Button](?path=/docs/components-button--docs) · [Icon](?path=/docs/components-icon--docs) · [EditableText](?path=/docs/components-editabletext--docs) · [TooltipWrapper](?path=/docs/components-tooltip-tooltipwrapper--docs)',
170
+ ].join('\n');
171
+
172
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
173
+
174
+ function HeadingDocsPage() {
175
+ return (
176
+ <>
177
+ <Title />
178
+ <Subtitle />
179
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
180
+ <DocHeading>Interactive example</DocHeading>
181
+ <Markdown>{PROPS_INTRO}</Markdown>
182
+ <DocPrimary />
183
+ <Controls />
184
+ <DocHeading>Usage guidance</DocHeading>
185
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
186
+ <DocHeading>Developer notes</DocHeading>
187
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
188
+ <DocHeading>Examples</DocHeading>
189
+ <Stories title="" />
190
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
191
+ </>
192
+ );
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Meta
197
+ // ---------------------------------------------------------------------------
198
+
199
+ const meta = {
8
200
  title: 'Components/Heading',
9
201
  component: Heading,
10
- };
11
-
12
- export const Default = {
13
- args: {
14
- children: ['Heading Text'],
15
- level: 1,
202
+ tags: ['autodocs'],
203
+ parameters: {
204
+ docs: {
205
+ page: HeadingDocsPage,
206
+ },
16
207
  },
17
208
  argTypes: {
18
209
  level: {
19
210
  control: 'select',
20
211
  options: [1, 2, 3, 4],
21
- description: 'Heading level (h1-h4)',
212
+ description: [
213
+ 'Controls **both** the semantic HTML element (`h1`–`h4`) and the typography — each level maps to its own',
214
+ 'set of type tokens (`--type-headings-h{level}-size`, `--type-headings-h{level}-family`, etc.).',
215
+ 'Never override font-size in CSS while keeping this prop, as that creates a visual/semantic mismatch.',
216
+ ].join(' '),
217
+ table: {
218
+ type: { summary: 'HeadingLevel (1 | 2 | 3 | 4)' },
219
+ defaultValue: { summary: '1' },
220
+ },
221
+ },
222
+ children: {
223
+ control: false,
224
+ description: [
225
+ 'Heading content. Accepts plain text for a simple heading, two `Heading.InnerContainer` elements',
226
+ 'for the left/right split layout, or any `ReactNode`. When two `Heading.InnerContainer` children',
227
+ 'are provided, CSS `justify-content: space-between` automatically positions the first left and the',
228
+ 'second right. Disabled in Controls because complex JSX cannot be serialised — use the named stories',
229
+ 'to explore composition patterns.',
230
+ ].join(' '),
231
+ table: {
232
+ type: { summary: 'ReactNode' },
233
+ },
234
+ },
235
+ className: {
236
+ control: 'text',
237
+ description: [
238
+ 'Additional CSS class names appended to the heading element alongside `ds-heading`.',
239
+ 'Use sparingly — prefer `level` for typography and `Heading.InnerContainer` for layout.',
240
+ ].join(' '),
241
+ table: {
242
+ type: { summary: 'string' },
243
+ },
244
+ },
245
+ id: {
246
+ control: 'text',
247
+ description: [
248
+ 'HTML `id` attribute on the heading element. The canonical accessibility use case is pairing it',
249
+ 'with `aria-labelledby` on an adjacent `<section>` or `<article>`: the screen reader then announces',
250
+ 'the section name when the user enters the landmark region.',
251
+ 'Example: `<Heading id="attendance-heading">Attendance</Heading>`',
252
+ 'with `<section aria-labelledby="attendance-heading">...</section>`.',
253
+ ].join(' '),
254
+ table: {
255
+ type: { summary: 'string' },
256
+ },
22
257
  },
23
258
  },
24
- tags: ['autodocs'],
25
- };
259
+ } satisfies Meta<typeof Heading>;
26
260
 
27
- export const FloatingChildren = {
28
- args: {
29
- children: [<Heading.InnerContainer key={1}>Text floating left</Heading.InnerContainer>, <span key={2}>Text floating right</span>],
30
- level: 1,
31
- },
32
- };
33
-
34
- export const HeadingWithSingleButton = {
35
- args: {
36
- level: 1,
37
- children: [
38
- <Heading.InnerContainer key={1}>Heading</Heading.InnerContainer>,
39
- <Heading.InnerContainer key={2}>
40
- <Button>
41
- <Icon name="info" />
42
- Button Text
43
- </Button>
44
- </Heading.InnerContainer>,
45
- ],
261
+ export default meta;
262
+ type Story = StoryObj<typeof Heading>;
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Helper: attach a per-story description to docs
266
+ // ---------------------------------------------------------------------------
267
+ const withDescription = (story: Story, description: string): Story => ({
268
+ ...story,
269
+ parameters: {
270
+ ...story.parameters,
271
+ docs: { ...story.parameters?.docs, description: { story: description } },
46
272
  },
47
- };
48
-
49
- export const HeadingWithMultipleButtons = {
50
- args: {
51
- level: 1,
52
- children: [
53
- <Heading.InnerContainer key={1}>Heading</Heading.InnerContainer>,
54
- <Heading.InnerContainer key={2} className="medium-spacing-gap">
55
- <Button variant="secondary">
56
- <Icon name="info" />
57
- Button Text
273
+ });
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Named template components — avoids hooks-in-callbacks lint issues.
277
+ // react-hooks plugin is NOT configured — do NOT add eslint-disable comments.
278
+ // ---------------------------------------------------------------------------
279
+
280
+ const AllLevelsTemplate = () => (
281
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xxlarge)' }}>
282
+ <div>
283
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H1 — page-level entity name</p>
284
+ <Heading level={1}>Student records</Heading>
285
+ </div>
286
+ <div>
287
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H2 — primary section heading</p>
288
+ <Heading level={2}>Attendance register</Heading>
289
+ </div>
290
+ <div>
291
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H3 — sub-section heading</p>
292
+ <Heading level={3}>Assessment marksheet</Heading>
293
+ </div>
294
+ <div>
295
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H4 — lowest-level heading</p>
296
+ <Heading level={4}>Behaviour log</Heading>
297
+ </div>
298
+ </div>
299
+ );
300
+
301
+ const TwoContainersTemplate = () => (
302
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
303
+ <Heading level={1}>
304
+ <Heading.InnerContainer>Student records</Heading.InnerContainer>
305
+ <Heading.InnerContainer>Actions go here</Heading.InnerContainer>
306
+ </Heading>
307
+ </div>
308
+ );
309
+
310
+ const WithActionButtonTemplate = () => (
311
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
312
+ <Heading level={1}>
313
+ <Heading.InnerContainer>Attendance register</Heading.InnerContainer>
314
+ <Heading.InnerContainer>
315
+ <Button variant="primary" type="button">Save changes</Button>
316
+ </Heading.InnerContainer>
317
+ </Heading>
318
+ </div>
319
+ );
320
+
321
+ const WithMultipleActionsTemplate = () => (
322
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
323
+ <Heading level={1}>
324
+ <Heading.InnerContainer>Assessment marksheet</Heading.InnerContainer>
325
+ <Heading.InnerContainer className="medium-spacing-gap">
326
+ <Button variant="secondary" type="button">Export to CSV</Button>
327
+ <Button variant="primary" type="button">Save changes</Button>
328
+ </Heading.InnerContainer>
329
+ </Heading>
330
+ </div>
331
+ );
332
+
333
+ const WithIconOnlyActionTemplate = () => (
334
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
335
+ <Heading level={1}>
336
+ <Heading.InnerContainer>Student records</Heading.InnerContainer>
337
+ <Heading.InnerContainer>
338
+ <TooltipWrapper tooltipContent="Download student records">
339
+ <Button
340
+ variant="secondary"
341
+ type="button"
342
+ iconRightName="download"
343
+ iconRightScreenReaderText="Download student records"
344
+ />
345
+ </TooltipWrapper>
346
+ </Heading.InnerContainer>
347
+ </Heading>
348
+ </div>
349
+ );
350
+
351
+ const WithBackControlLegacyTemplate = () => (
352
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
353
+ <Heading level={1}>
354
+ <Heading.InnerContainer className="medium-spacing-gap">
355
+ <Button variant="tertiary" type="button" iconLeftName="chevron-left" iconLeftScreenReaderText="Go back">
356
+ Back
58
357
  </Button>
59
- <Button>
60
- <Icon name="info" />
61
- Button Text
358
+ Attendance register
359
+ </Heading.InnerContainer>
360
+ <Heading.InnerContainer className="medium-spacing-gap">
361
+ <Button variant="secondary" type="button">Archive</Button>
362
+ <Button variant="primary" type="button">Save changes</Button>
363
+ </Heading.InnerContainer>
364
+ </Heading>
365
+ </div>
366
+ );
367
+
368
+ const WithEditableTitleTemplate = () => (
369
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
370
+ <Heading level={1}>
371
+ <EditableText text="Form 7B Registration" onEditSave={() => {}} />
372
+ </Heading>
373
+ </div>
374
+ );
375
+
376
+ const WithEditableTitleAndActionTemplate = () => (
377
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
378
+ <Heading level={1}>
379
+ <Heading.InnerContainer>
380
+ <EditableText text="Form 7B Registration" onEditSave={() => {}} />
381
+ </Heading.InnerContainer>
382
+ <Heading.InnerContainer>
383
+ <Button variant="primary" type="button">Save changes</Button>
384
+ </Heading.InnerContainer>
385
+ </Heading>
386
+ </div>
387
+ );
388
+
389
+ const WithEditableTitleAndMultipleActionsTemplate = () => (
390
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
391
+ <Heading level={1}>
392
+ <Heading.InnerContainer className="medium-spacing-gap">
393
+ <Button variant="tertiary" type="button" iconLeftName="chevron-left" iconLeftScreenReaderText="Go back">
394
+ Back
62
395
  </Button>
63
- </Heading.InnerContainer>,
64
- ],
396
+ <EditableText text="Autumn term report" onEditSave={() => {}} />
397
+ </Heading.InnerContainer>
398
+ <Heading.InnerContainer className="medium-spacing-gap">
399
+ <Button variant="secondary" type="button">Export to CSV</Button>
400
+ <Button variant="primary" type="button">Save changes</Button>
401
+ </Heading.InnerContainer>
402
+ </Heading>
403
+ </div>
404
+ );
405
+
406
+ const AccessibilityLabelledSectionTemplate = () => (
407
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }}>
408
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
409
+ The heading below has an `id`. The section below it references that `id` via `aria-labelledby`.
410
+ Screen readers announce the section name ("Attendance") when the user enters the landmark region.
411
+ </p>
412
+ <Heading id="attendance-heading" level={2}>Attendance</Heading>
413
+ <section aria-labelledby="attendance-heading" style={{ padding: 'var(--spacing-large)', border: 'var(--border-weight) solid var(--color-grey-300)', borderRadius: 'var(--border-radius-small)' }}>
414
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
415
+ This section is labelled "Attendance" by the heading above via `aria-labelledby="attendance-heading"`.
416
+ A screen reader user navigating landmarks will hear: "Attendance, region".
417
+ </p>
418
+ </section>
419
+ </div>
420
+ );
421
+
422
+ // ---------------------------------------------------------------------------
423
+ // Stories
424
+ // ---------------------------------------------------------------------------
425
+
426
+ export const Default: Story = withDescription(
427
+ {
428
+ args: {
429
+ level: 1,
430
+ children: 'Student records',
431
+ },
432
+ render: args => <Heading {...args} />,
65
433
  },
66
- };
67
-
68
- export const HeadingWithButtonsBothSides = {
69
- args: {
70
- level: 1,
71
- children: [
72
- <Heading.InnerContainer key={1} className="medium-spacing-gap">
73
- <Button variant="tertiary">
74
- <Icon name="chevron-left" />
75
- Button Text
76
- </Button>
77
- Heading
78
- </Heading.InnerContainer>,
79
- <Heading.InnerContainer key={2} className="medium-spacing-gap">
80
- <Button variant="secondary">
81
- <Icon name="info" />
82
- Button Text
83
- </Button>
84
- <Button>
85
- <Icon name="info" />
86
- Button Text
87
- </Button>
88
- </Heading.InnerContainer>,
89
- ],
434
+ 'The interactive canvas story — every prop is wired to the **Controls** panel. Use the controls to change `level` (1–4) and see the heading typography scale live. The `children` control is disabled because complex JSX cannot be round-tripped through Storybook\'s serialiser — use the named stories below to explore composition patterns.',
435
+ );
436
+
437
+ export const AllLevels: Story = withDescription(
438
+ {
439
+ render: AllLevelsTemplate,
440
+ parameters: {
441
+ docs: {
442
+ source: {
443
+ language: 'tsx',
444
+ code: `
445
+ import { Heading } from '@arbor-education/design-system.components';
446
+
447
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xxlarge)' }}>
448
+ <div>
449
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H1 — page-level entity name</p>
450
+ <Heading level={1}>Student records</Heading>
451
+ </div>
452
+ <div>
453
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H2 — primary section heading</p>
454
+ <Heading level={2}>Attendance register</Heading>
455
+ </div>
456
+ <div>
457
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H3 — sub-section heading</p>
458
+ <Heading level={3}>Assessment marksheet</Heading>
459
+ </div>
460
+ <div>
461
+ <p className="ds-text" style={{ margin: '0 0 var(--spacing-large)', color: 'var(--color-grey-600)' }}>H4 — lowest-level heading</p>
462
+ <Heading level={4}>Behaviour log</Heading>
463
+ </div>
464
+ </div>
465
+ `.trim(),
466
+ },
467
+ },
468
+ },
90
469
  },
91
- };
92
-
93
- export const HeadingWithEditableText = {
94
- args: {
95
- level: 1,
96
- children: [
97
- <EditableText key={2} text="Editable Heading" onEditSave={() => {}} />,
98
- ],
470
+ 'All four heading levels stacked vertically. Each level maps to a dedicated set of type tokens — `--type-headings-h{level}-size`, `--type-headings-h{level}-family`, `--type-headings-h{level}-weight`, and `--type-headings-h{level}-line-height` — so the entire type scale is driven by design tokens. H1 line-heights are tall, so `var(--spacing-xxlarge)` gives each heading enough breathing room.',
471
+ );
472
+
473
+ export const TwoContainers: Story = withDescription(
474
+ {
475
+ render: TwoContainersTemplate,
476
+ parameters: {
477
+ docs: {
478
+ source: {
479
+ language: 'tsx',
480
+ code: `
481
+ import { Heading } from '@arbor-education/design-system.components';
482
+
483
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
484
+ <Heading level={1}>
485
+ <Heading.InnerContainer>Student records</Heading.InnerContainer>
486
+ <Heading.InnerContainer>Actions go here</Heading.InnerContainer>
487
+ </Heading>
488
+ </div>
489
+ `.trim(),
490
+ },
491
+ },
492
+ },
99
493
  },
100
- };
101
-
102
- export const HeadingWithEditableTextAndButton = {
103
- args: {
104
- level: 1,
105
- children: [
106
- <Heading.InnerContainer key={1}><EditableText text="Editable Heading" onEditSave={() => {}} /></Heading.InnerContainer>,
107
- <Heading.InnerContainer key={2}>
108
- <Button>
109
- <Icon name="info" />
110
- Button Text
111
- </Button>
112
- </Heading.InnerContainer>,
113
- ],
494
+ [
495
+ 'When two `Heading.InnerContainer` children are provided, the heading\'s CSS (`justify-content: space-between`)',
496
+ 'automatically pushes the first container to the left and the second to the right. No extra wrapper or flex',
497
+ 'gymnastics needed. This is the canonical pattern for any heading that combines a title with action controls.',
498
+ '',
499
+ '`Heading.InnerContainer` is a simple `<span className="ds-heading__inner-container">` wrapper — it groups',
500
+ 'content on one "side" of the heading and inherits the heading\'s typography tokens so its text renders at',
501
+ 'the correct size automatically.',
502
+ ].join(' '),
503
+ );
504
+
505
+ export const WithActionButton: Story = withDescription(
506
+ {
507
+ render: WithActionButtonTemplate,
508
+ parameters: {
509
+ docs: {
510
+ source: {
511
+ language: 'tsx',
512
+ code: `
513
+ import { Heading, Button } from '@arbor-education/design-system.components';
514
+
515
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
516
+ <Heading level={1}>
517
+ <Heading.InnerContainer>Attendance register</Heading.InnerContainer>
518
+ <Heading.InnerContainer>
519
+ <Button variant="primary" type="button">Save changes</Button>
520
+ </Heading.InnerContainer>
521
+ </Heading>
522
+ </div>
523
+ `.trim(),
524
+ },
525
+ },
526
+ },
114
527
  },
115
- };
116
-
117
- export const HeadingWithEditableTextAndButtonsBothSides = {
118
- args: {
119
- level: 1,
120
- children: [
121
- <Heading.InnerContainer key={1} className="medium-spacing-gap">
122
- <Button variant="tertiary">
123
- <Icon name="chevron-left" />
124
- Button Text
125
- </Button>
126
- <EditableText text="Editable Heading" onEditSave={() => {}} />
127
- </Heading.InnerContainer>,
128
- <Heading.InnerContainer key={2} className="medium-spacing-gap">
129
- <Button variant="secondary">
130
- <Icon name="info" />
131
- Button Text
132
- </Button>
133
- <Button>
134
- <Icon name="info" />
135
- Button Text
136
- </Button>
137
- </Heading.InnerContainer>,
138
- ],
528
+ 'The most common Heading pattern: title text in the left `Heading.InnerContainer`, one primary action button in the right container. The `justify-content: space-between` CSS keeps the title and button on opposite ends of the heading regardless of how long the title is.',
529
+ );
530
+
531
+ export const WithMultipleActions: Story = withDescription(
532
+ {
533
+ render: WithMultipleActionsTemplate,
534
+ parameters: {
535
+ docs: {
536
+ source: {
537
+ language: 'tsx',
538
+ code: `
539
+ import { Heading, Button } from '@arbor-education/design-system.components';
540
+
541
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
542
+ <Heading level={1}>
543
+ <Heading.InnerContainer>Assessment marksheet</Heading.InnerContainer>
544
+ <Heading.InnerContainer className="medium-spacing-gap">
545
+ <Button variant="secondary" type="button">Export to CSV</Button>
546
+ <Button variant="primary" type="button">Save changes</Button>
547
+ </Heading.InnerContainer>
548
+ </Heading>
549
+ </div>
550
+ `.trim(),
551
+ },
552
+ },
553
+ },
139
554
  },
140
- };
555
+ [
556
+ 'Title left, two action buttons right. When multiple buttons share one `Heading.InnerContainer`, add',
557
+ '`className="medium-spacing-gap"` to the container. This is a global utility class from `src/global.scss`',
558
+ 'that applies `gap: var(--spacing-small)` between flex children — the canonical way to space multiple',
559
+ 'buttons in a heading. **Max 3 actions total per Confluence** (1 primary + 2 secondary recommended).',
560
+ 'If you need more, consolidate into a `Dropdown`.',
561
+ ].join(' '),
562
+ );
141
563
 
142
- export default meta;
564
+ export const WithIconOnlyAction: Story = withDescription(
565
+ {
566
+ render: WithIconOnlyActionTemplate,
567
+ parameters: {
568
+ docs: {
569
+ source: {
570
+ language: 'tsx',
571
+ code: `
572
+ import { Heading, Button, TooltipWrapper } from '@arbor-education/design-system.components';
573
+
574
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
575
+ <Heading level={1}>
576
+ <Heading.InnerContainer>Student records</Heading.InnerContainer>
577
+ <Heading.InnerContainer>
578
+ <TooltipWrapper tooltipContent="Download student records">
579
+ <Button
580
+ variant="secondary"
581
+ type="button"
582
+ iconRightName="download"
583
+ iconRightScreenReaderText="Download student records"
584
+ />
585
+ </TooltipWrapper>
586
+ </Heading.InnerContainer>
587
+ </Heading>
588
+ </div>
589
+ `.trim(),
590
+ },
591
+ },
592
+ },
593
+ },
594
+ [
595
+ 'Title left, an icon-only action button right. **Icon-only buttons in a heading require a `TooltipWrapper`**',
596
+ '— hover tooltips alone are not sufficient. The tooltip must appear on keyboard focus too, and assistive',
597
+ 'technology must be able to read the label. Always pair `TooltipWrapper` with a descriptive',
598
+ '`iconRightScreenReaderText` prop on the button. This story uses both: `tooltipContent="Download student records"`',
599
+ 'for sighted keyboard users and `iconRightScreenReaderText="Download student records"` for screen reader users.',
600
+ ].join(' '),
601
+ );
602
+
603
+ export const WithBackControlLegacy: Story = withDescription(
604
+ {
605
+ render: WithBackControlLegacyTemplate,
606
+ parameters: {
607
+ docs: {
608
+ source: {
609
+ language: 'tsx',
610
+ code: `
611
+ import { Heading, Button } from '@arbor-education/design-system.components';
612
+
613
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
614
+ <Heading level={1}>
615
+ <Heading.InnerContainer className="medium-spacing-gap">
616
+ <Button variant="tertiary" type="button" iconLeftName="chevron-left" iconLeftScreenReaderText="Go back">
617
+ Back
618
+ </Button>
619
+ Attendance register
620
+ </Heading.InnerContainer>
621
+ <Heading.InnerContainer className="medium-spacing-gap">
622
+ <Button variant="secondary" type="button">Archive</Button>
623
+ <Button variant="primary" type="button">Save changes</Button>
624
+ </Heading.InnerContainer>
625
+ </Heading>
626
+ </div>
627
+ `.trim(),
628
+ },
629
+ },
630
+ },
631
+ },
632
+ [
633
+ '**Legacy pattern — do not use on new pages.**',
634
+ '',
635
+ 'A back-chevron tertiary button in the left `Heading.InnerContainer` with actions in the right. This pattern',
636
+ 'is being phased out across Arbor because browser back history is unreliable — it breaks when users open',
637
+ 'pages in new tabs, arrive via deep links, or navigate in unexpected orders. Design guidance recommends',
638
+ 'deterministic navigation instead (e.g. a `<Link>` to a known parent route).',
639
+ '',
640
+ 'The story is preserved here for reference while legacy pages are migrated.',
641
+ ].join(' '),
642
+ );
643
+
644
+ export const WithEditableTitle: Story = withDescription(
645
+ {
646
+ render: WithEditableTitleTemplate,
647
+ parameters: {
648
+ docs: {
649
+ source: {
650
+ language: 'tsx',
651
+ code: `
652
+ import { Heading, EditableText } from '@arbor-education/design-system.components';
653
+
654
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
655
+ <Heading level={1}>
656
+ <EditableText text="Form 7B Registration" onEditSave={() => {}} />
657
+ </Heading>
658
+ </div>
659
+ `.trim(),
660
+ },
661
+ },
662
+ },
663
+ },
664
+ '`EditableText` as the heading children — inline-editable heading. Click the pencil icon (or the title text) to enter editing mode. Press Enter or blur the input to save. Press Escape to cancel and revert. Useful for entity names that users should be able to rename directly on the page, such as a form or report title.',
665
+ );
666
+
667
+ export const WithEditableTitleAndAction: Story = withDescription(
668
+ {
669
+ render: WithEditableTitleAndActionTemplate,
670
+ parameters: {
671
+ docs: {
672
+ source: {
673
+ language: 'tsx',
674
+ code: `
675
+ import { Heading, Button, EditableText } from '@arbor-education/design-system.components';
676
+
677
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
678
+ <Heading level={1}>
679
+ <Heading.InnerContainer>
680
+ <EditableText text="Form 7B Registration" onEditSave={() => {}} />
681
+ </Heading.InnerContainer>
682
+ <Heading.InnerContainer>
683
+ <Button variant="primary" type="button">Save changes</Button>
684
+ </Heading.InnerContainer>
685
+ </Heading>
686
+ </div>
687
+ `.trim(),
688
+ },
689
+ },
690
+ },
691
+ },
692
+ 'An editable title in the left `Heading.InnerContainer` with a primary action button in the right. The `EditableText` component fills its container width, so wrapping it in an `InnerContainer` keeps it left-aligned while the button sits at the far right. Click the pencil icon on the title to activate edit mode.',
693
+ );
694
+
695
+ export const WithEditableTitleAndMultipleActions: Story = withDescription(
696
+ {
697
+ render: WithEditableTitleAndMultipleActionsTemplate,
698
+ parameters: {
699
+ docs: {
700
+ source: {
701
+ language: 'tsx',
702
+ code: `
703
+ import { Heading, Button, EditableText } from '@arbor-education/design-system.components';
704
+
705
+ <div style={{ padding: 'var(--spacing-xlarge)' }}>
706
+ <Heading level={1}>
707
+ <Heading.InnerContainer className="medium-spacing-gap">
708
+ <Button variant="tertiary" type="button" iconLeftName="chevron-left" iconLeftScreenReaderText="Go back">
709
+ Back
710
+ </Button>
711
+ <EditableText text="Autumn term report" onEditSave={() => {}} />
712
+ </Heading.InnerContainer>
713
+ <Heading.InnerContainer className="medium-spacing-gap">
714
+ <Button variant="secondary" type="button">Export to CSV</Button>
715
+ <Button variant="primary" type="button">Save changes</Button>
716
+ </Heading.InnerContainer>
717
+ </Heading>
718
+ </div>
719
+ `.trim(),
720
+ },
721
+ },
722
+ },
723
+ },
724
+ [
725
+ 'The full complex composition: back button + editable title in the left container, multiple actions in the right.',
726
+ '',
727
+ '**Legacy pattern caveat** — this layout includes the back-button pattern, which is being phased out.',
728
+ 'The back button is shown here for completeness while legacy pages are migrated. On new pages, replace it',
729
+ 'with deterministic navigation. The editable title + multiple-actions layout (without the back button) is',
730
+ 'fully supported.',
731
+ ].join(' '),
732
+ );
733
+
734
+ export const AccessibilityLabelledSection: Story = withDescription(
735
+ {
736
+ render: AccessibilityLabelledSectionTemplate,
737
+ parameters: {
738
+ docs: {
739
+ source: {
740
+ language: 'tsx',
741
+ code: `
742
+ import { Heading } from '@arbor-education/design-system.components';
743
+
744
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }}>
745
+ <Heading id="attendance-heading" level={2}>Attendance</Heading>
746
+ <section
747
+ aria-labelledby="attendance-heading"
748
+ style={{
749
+ padding: 'var(--spacing-large)',
750
+ border: 'var(--border-weight) solid var(--color-grey-300)',
751
+ borderRadius: 'var(--border-radius-small)',
752
+ }}
753
+ >
754
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
755
+ Section content — announced as "Attendance, region" by screen readers.
756
+ </p>
757
+ </section>
758
+ </div>
759
+ `.trim(),
760
+ },
761
+ },
762
+ },
763
+ },
764
+ [
765
+ 'Canonical accessibility pattern: a `Heading` with an `id` prop, referenced by an adjacent `<section aria-labelledby="...">`. When a screen reader user navigates to the section landmark, the browser announces the section\'s accessible name — in this case, "Attendance, region".',
766
+ '',
767
+ 'This is especially important for pages with multiple regions (attendance, behaviour, academic) — each section',
768
+ 'should be labelled by its heading so keyboard/screen reader users can orient themselves quickly using landmark',
769
+ 'navigation (`F6` in NVDA, the Rotor in VoiceOver).',
770
+ '',
771
+ 'Pattern: `<Heading id="attendance-heading" level={2}>Attendance</Heading>` paired with',
772
+ '`<section aria-labelledby="attendance-heading">...</section>`.',
773
+ ].join(' '),
774
+ );