@arbor-education/design-system.components 0.21.1 → 0.23.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 (189) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/component-library.md +77 -14
  3. package/dist/components/articleCard/ArticleCard.d.ts +2 -2
  4. package/dist/components/articleCard/ArticleCard.d.ts.map +1 -1
  5. package/dist/components/articleCard/ArticleCard.js +3 -3
  6. package/dist/components/articleCard/ArticleCard.js.map +1 -1
  7. package/dist/components/articleCard/ArticleCard.stories.d.ts +11 -3
  8. package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -1
  9. package/dist/components/articleCard/ArticleCard.stories.js +16 -11
  10. package/dist/components/articleCard/ArticleCard.stories.js.map +1 -1
  11. package/dist/components/combobox/Combobox.js +1 -1
  12. package/dist/components/combobox/Combobox.js.map +1 -1
  13. package/dist/components/combobox/Combobox.stories.d.ts +4 -0
  14. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
  15. package/dist/components/combobox/Combobox.stories.js +144 -12
  16. package/dist/components/combobox/Combobox.stories.js.map +1 -1
  17. package/dist/components/combobox/Combobox.test.js +22 -0
  18. package/dist/components/combobox/Combobox.test.js.map +1 -1
  19. package/dist/components/combobox/ComboboxButtonTrigger.d.ts +4 -4
  20. package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -1
  21. package/dist/components/combobox/ComboboxButtonTrigger.js +35 -40
  22. package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -1
  23. package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -1
  24. package/dist/components/combobox/ComboboxTrigger.js +11 -4
  25. package/dist/components/combobox/ComboboxTrigger.js.map +1 -1
  26. package/dist/components/combobox/useVisibleTriggerTags.d.ts +21 -0
  27. package/dist/components/combobox/useVisibleTriggerTags.d.ts.map +1 -0
  28. package/dist/components/combobox/useVisibleTriggerTags.js +46 -0
  29. package/dist/components/combobox/useVisibleTriggerTags.js.map +1 -0
  30. package/dist/components/combobox/useVisibleTriggerTags.test.d.ts +2 -0
  31. package/dist/components/combobox/useVisibleTriggerTags.test.d.ts.map +1 -0
  32. package/dist/components/combobox/useVisibleTriggerTags.test.js +81 -0
  33. package/dist/components/combobox/useVisibleTriggerTags.test.js.map +1 -0
  34. package/dist/components/filterBar/FilterBar.d.ts +71 -0
  35. package/dist/components/filterBar/FilterBar.d.ts.map +1 -0
  36. package/dist/components/filterBar/FilterBar.js +89 -0
  37. package/dist/components/filterBar/FilterBar.js.map +1 -0
  38. package/dist/components/filterBar/FilterBar.stories.d.ts +170 -0
  39. package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -0
  40. package/dist/components/filterBar/FilterBar.stories.js +894 -0
  41. package/dist/components/filterBar/FilterBar.stories.js.map +1 -0
  42. package/dist/components/filterBar/FilterBar.test.d.ts +2 -0
  43. package/dist/components/filterBar/FilterBar.test.d.ts.map +1 -0
  44. package/dist/components/filterBar/FilterBar.test.js +164 -0
  45. package/dist/components/filterBar/FilterBar.test.js.map +1 -0
  46. package/dist/components/icon/allowedIcons.d.ts +1 -0
  47. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  48. package/dist/components/icon/allowedIcons.js +2 -1
  49. package/dist/components/icon/allowedIcons.js.map +1 -1
  50. package/dist/components/iconText/IconText.d.ts +43 -0
  51. package/dist/components/iconText/IconText.d.ts.map +1 -0
  52. package/dist/components/iconText/IconText.js +29 -0
  53. package/dist/components/iconText/IconText.js.map +1 -0
  54. package/dist/components/{icoText/IcoText.stories.d.ts → iconText/IconText.stories.d.ts} +8 -9
  55. package/dist/components/iconText/IconText.stories.d.ts.map +1 -0
  56. package/dist/components/{icoText/IcoText.stories.js → iconText/IconText.stories.js} +81 -81
  57. package/dist/components/iconText/IconText.stories.js.map +1 -0
  58. package/dist/components/iconText/IconText.test.d.ts +2 -0
  59. package/dist/components/iconText/IconText.test.d.ts.map +1 -0
  60. package/dist/components/{icoText/IcoText.test.js → iconText/IconText.test.js} +6 -6
  61. package/dist/components/iconText/IconText.test.js.map +1 -0
  62. package/dist/components/modal/Modal.d.ts +1 -0
  63. package/dist/components/modal/Modal.d.ts.map +1 -1
  64. package/dist/components/modal/Modal.js +2 -2
  65. package/dist/components/modal/Modal.js.map +1 -1
  66. package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.d.ts.map +1 -1
  67. package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.js +13 -2
  68. package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.js.map +1 -1
  69. package/dist/components/tag/Tag.d.ts +14 -1
  70. package/dist/components/tag/Tag.d.ts.map +1 -1
  71. package/dist/components/tag/Tag.js +9 -3
  72. package/dist/components/tag/Tag.js.map +1 -1
  73. package/dist/components/tag/Tag.stories.d.ts +1 -1
  74. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  75. package/dist/components/tag/Tag.stories.js +3 -3
  76. package/dist/components/tag/Tag.stories.js.map +1 -1
  77. package/dist/components/tag/Tag.test.js +36 -5
  78. package/dist/components/tag/Tag.test.js.map +1 -1
  79. package/dist/components/tagList/TagList.d.ts +49 -0
  80. package/dist/components/tagList/TagList.d.ts.map +1 -0
  81. package/dist/components/tagList/TagList.js +114 -0
  82. package/dist/components/tagList/TagList.js.map +1 -0
  83. package/dist/components/tagList/TagList.stories.d.ts +130 -0
  84. package/dist/components/tagList/TagList.stories.d.ts.map +1 -0
  85. package/dist/components/tagList/TagList.stories.js +443 -0
  86. package/dist/components/tagList/TagList.stories.js.map +1 -0
  87. package/dist/components/{icoText/IcoText.test.d.ts → tagList/TagList.test.d.ts} +1 -1
  88. package/dist/components/tagList/TagList.test.d.ts.map +1 -0
  89. package/dist/components/tagList/TagList.test.js +246 -0
  90. package/dist/components/tagList/TagList.test.js.map +1 -0
  91. package/dist/components/tagList/useTagListCollapsedLayout.d.ts +19 -0
  92. package/dist/components/tagList/useTagListCollapsedLayout.d.ts.map +1 -0
  93. package/dist/components/tagList/useTagListCollapsedLayout.js +48 -0
  94. package/dist/components/tagList/useTagListCollapsedLayout.js.map +1 -0
  95. package/dist/components/tagList/useVisibleTags.d.ts +18 -0
  96. package/dist/components/tagList/useVisibleTags.d.ts.map +1 -0
  97. package/dist/components/tagList/useVisibleTags.js +41 -0
  98. package/dist/components/tagList/useVisibleTags.js.map +1 -0
  99. package/dist/index.css +272 -13
  100. package/dist/index.css.map +1 -1
  101. package/dist/index.d.ts +4 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +3 -1
  104. package/dist/index.js.map +1 -1
  105. package/dist/utils/hooks/useElementWidth.d.ts.map +1 -0
  106. package/dist/{components/combobox → utils/hooks}/useElementWidth.js +0 -1
  107. package/dist/utils/hooks/useElementWidth.js.map +1 -0
  108. package/dist/utils/hooks/useMeasuredChildWidths.d.ts +8 -0
  109. package/dist/utils/hooks/useMeasuredChildWidths.d.ts.map +1 -0
  110. package/dist/utils/hooks/useMeasuredChildWidths.js +26 -0
  111. package/dist/utils/hooks/useMeasuredChildWidths.js.map +1 -0
  112. package/dist/utils/hooks/useRovingFocus.d.ts +18 -0
  113. package/dist/utils/hooks/useRovingFocus.d.ts.map +1 -0
  114. package/dist/utils/hooks/useRovingFocus.js +130 -0
  115. package/dist/utils/hooks/useRovingFocus.js.map +1 -0
  116. package/dist/utils/hooks/useRovingFocus.test.d.ts +2 -0
  117. package/dist/utils/hooks/useRovingFocus.test.d.ts.map +1 -0
  118. package/dist/utils/hooks/useRovingFocus.test.js +59 -0
  119. package/dist/utils/hooks/useRovingFocus.test.js.map +1 -0
  120. package/dist/utils/spacedWidths.d.ts +3 -0
  121. package/dist/utils/spacedWidths.d.ts.map +1 -0
  122. package/dist/utils/spacedWidths.js +28 -0
  123. package/dist/utils/spacedWidths.js.map +1 -0
  124. package/dist/utils/spacedWidths.test.d.ts +2 -0
  125. package/dist/utils/spacedWidths.test.d.ts.map +1 -0
  126. package/dist/utils/spacedWidths.test.js +17 -0
  127. package/dist/utils/spacedWidths.test.js.map +1 -0
  128. package/package.json +1 -1
  129. package/src/components/articleCard/ArticleCard.stories.tsx +17 -12
  130. package/src/components/articleCard/ArticleCard.tsx +9 -9
  131. package/src/components/combobox/Combobox.stories.tsx +186 -12
  132. package/src/components/combobox/Combobox.test.tsx +53 -0
  133. package/src/components/combobox/Combobox.tsx +3 -3
  134. package/src/components/combobox/ComboboxButtonTrigger.tsx +52 -56
  135. package/src/components/combobox/ComboboxTrigger.tsx +19 -16
  136. package/src/components/combobox/combobox.scss +8 -3
  137. package/src/components/combobox/useVisibleTriggerTags.test.tsx +91 -0
  138. package/src/components/combobox/useVisibleTriggerTags.ts +83 -0
  139. package/src/components/filterBar/FilterBar.stories.tsx +1199 -0
  140. package/src/components/filterBar/FilterBar.test.tsx +248 -0
  141. package/src/components/filterBar/FilterBar.tsx +298 -0
  142. package/src/components/filterBar/filterBar.scss +143 -0
  143. package/src/components/icon/allowedIcons.tsx +3 -1
  144. package/src/components/{icoText/IcoText.stories.tsx → iconText/IconText.stories.tsx} +112 -112
  145. package/src/components/{icoText/IcoText.test.tsx → iconText/IconText.test.tsx} +10 -10
  146. package/src/components/{icoText/IcoText.tsx → iconText/IconText.tsx} +27 -20
  147. package/src/components/modal/Modal.tsx +5 -1
  148. package/src/components/table/cellRenderers/ComboboxCellRenderer.test.tsx +20 -3
  149. package/src/components/tag/Tag.stories.tsx +4 -4
  150. package/src/components/tag/Tag.test.tsx +62 -5
  151. package/src/components/tag/Tag.tsx +61 -3
  152. package/src/components/tag/tag.scss +80 -9
  153. package/src/components/tagList/TagList.stories.tsx +564 -0
  154. package/src/components/tagList/TagList.test.tsx +342 -0
  155. package/src/components/tagList/TagList.tsx +296 -0
  156. package/src/components/tagList/tagList.scss +56 -0
  157. package/src/components/tagList/useTagListCollapsedLayout.ts +83 -0
  158. package/src/components/tagList/useVisibleTags.ts +74 -0
  159. package/src/index.scss +3 -1
  160. package/src/index.ts +13 -1
  161. package/src/tokens.scss +3 -1
  162. package/src/{components/combobox → utils/hooks}/useElementWidth.ts +0 -1
  163. package/src/utils/hooks/useMeasuredChildWidths.ts +39 -0
  164. package/src/utils/hooks/useRovingFocus.test.tsx +105 -0
  165. package/src/utils/hooks/useRovingFocus.ts +163 -0
  166. package/src/utils/spacedWidths.test.ts +20 -0
  167. package/src/utils/spacedWidths.ts +37 -0
  168. package/dist/components/combobox/useElementWidth.d.ts.map +0 -1
  169. package/dist/components/combobox/useElementWidth.js.map +0 -1
  170. package/dist/components/combobox/useVisibleChips.d.ts +0 -21
  171. package/dist/components/combobox/useVisibleChips.d.ts.map +0 -1
  172. package/dist/components/combobox/useVisibleChips.js +0 -59
  173. package/dist/components/combobox/useVisibleChips.js.map +0 -1
  174. package/dist/components/combobox/useVisibleChips.test.d.ts +0 -2
  175. package/dist/components/combobox/useVisibleChips.test.d.ts.map +0 -1
  176. package/dist/components/combobox/useVisibleChips.test.js +0 -81
  177. package/dist/components/combobox/useVisibleChips.test.js.map +0 -1
  178. package/dist/components/icoText/IcoText.d.ts +0 -37
  179. package/dist/components/icoText/IcoText.d.ts.map +0 -1
  180. package/dist/components/icoText/IcoText.js +0 -29
  181. package/dist/components/icoText/IcoText.js.map +0 -1
  182. package/dist/components/icoText/IcoText.stories.d.ts.map +0 -1
  183. package/dist/components/icoText/IcoText.stories.js.map +0 -1
  184. package/dist/components/icoText/IcoText.test.d.ts.map +0 -1
  185. package/dist/components/icoText/IcoText.test.js.map +0 -1
  186. package/src/components/combobox/useVisibleChips.test.tsx +0 -91
  187. package/src/components/combobox/useVisibleChips.ts +0 -100
  188. /package/dist/{components/combobox → utils/hooks}/useElementWidth.d.ts +0 -0
  189. /package/src/components/{icoText/icoText.scss → iconText/iconText.scss} +0 -0
@@ -55,6 +55,7 @@ import {
55
55
  Link,
56
56
  List,
57
57
  ListFilterPlus,
58
+ ListTree,
58
59
  LoaderCircle,
59
60
  Lock,
60
61
  LockOpen,
@@ -90,10 +91,10 @@ import {
90
91
  UsersRound, X,
91
92
  } from 'lucide-react';
92
93
  import { AskArbor } from './customIcons/AskArbor.js';
93
- import type { CustomIconProps } from './types.js';
94
94
  import { CheckSolid } from './customIcons/CheckSolid.js';
95
95
  import { Google } from './customIcons/Google.js';
96
96
  import { XSolid } from './customIcons/XSolid.js';
97
+ import type { CustomIconProps } from './types.js';
97
98
 
98
99
  export const allowedIcons = {
99
100
  // lucide icons
@@ -156,6 +157,7 @@ export const allowedIcons = {
156
157
  'link': Link,
157
158
  'list-filter-plus': ListFilterPlus,
158
159
  'list': List,
160
+ 'list-tree': ListTree,
159
161
  'loader': LoaderCircle,
160
162
  'lock-open': LockOpen,
161
163
  'lock': Lock,
@@ -1,21 +1,21 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
1
  import {
3
2
  Controls,
4
3
  Heading as DocHeading,
5
- Markdown,
6
4
  Primary as DocPrimary,
5
+ Markdown,
7
6
  Stories,
8
7
  Subtitle,
9
8
  Title,
10
9
  } from '@storybook/addon-docs/blocks';
11
- import { IcoText } from './IcoText.js';
10
+ import type { Meta, StoryObj } from '@storybook/react-vite';
11
+ import { IconText } from './IconText.js';
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // Docs page content
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
17
  const DESCRIPTION_INTRO = [
18
- '`IcoText` is a compound layout component that pairs an icon in a left rail with a heading and/or paragraph.',
18
+ '`IconText` is a compound layout component that pairs an icon in a left rail with a heading and/or paragraph.',
19
19
  'It is the canonical icon-plus-text primitive in this design system — used internally by `ArticleCard`',
20
20
  'and available for direct use in any context that needs an icon anchoring labelled content.',
21
21
  ].join(' ');
@@ -60,26 +60,26 @@ const USAGE_GUIDANCE = [
60
60
  '| Content inside a navigable card | [`ArticleCard`](?path=/docs/components-card-articlecard--docs) |',
61
61
  '| Icon with no text | [`Icon`](?path=/docs/components-icon--docs) directly |',
62
62
  '| Long-form prose | Semantic HTML `<p>` with a separate heading |',
63
- '| Interactive element | Wrap `IcoText` inside a `Card` or `Button` — never add `onClick` to `IcoText` itself |',
63
+ '| Interactive element | Wrap `IconText` inside a `Card` or `Button` — never add `onClick` to `IconText` itself |',
64
64
  ].join('\n');
65
65
 
66
66
  const DEVELOPER_NOTES = [
67
67
  '### Critical usage patterns',
68
68
  '',
69
- '**`IcoText.Icon` is automatically hoisted to the left rail** regardless of position in JSX.',
70
- 'The root inspects `children`, pulls out any `IcoText.Icon` elements into a flex-shrink-0 left column,',
69
+ '**`IconText.Icon` is automatically hoisted to the left rail** regardless of position in JSX.',
70
+ 'The root inspects `children`, pulls out any `IconText.Icon` elements into a flex-shrink-0 left column,',
71
71
  'and places all other children (Heading, Paragraph) in the content column.',
72
72
  '',
73
73
  '```tsx',
74
- '<IcoText>',
75
- ' <IcoText.Icon name="guardians" />',
76
- ' <IcoText.Heading>Year 9 Attendance</IcoText.Heading>',
77
- ' <IcoText.Paragraph>28 pupils · 94.2% this term</IcoText.Paragraph>',
78
- '</IcoText>',
74
+ '<IconText>',
75
+ ' <IconText.Icon name="guardians" />',
76
+ ' <IconText.Heading>Year 9 Attendance</IconText.Heading>',
77
+ ' <IconText.Paragraph>28 pupils · 94.2% this term</IconText.Paragraph>',
78
+ '</IconText>',
79
79
  '```',
80
80
  '',
81
- '**`IcoText.Heading` always renders as `<h4>`.** There is no `level` prop. Verify your document',
82
- 'heading hierarchy is correct before using `IcoText` as a standalone heading.',
81
+ '**`IconText.Heading` always renders as `<h4>`.** There is no `level` prop. Verify your document',
82
+ 'heading hierarchy is correct before using `IconText` as a standalone heading.',
83
83
  '',
84
84
  '**Omit `screenReaderText` for decorative icons.** The icon is rendered with `aria-hidden="true"`',
85
85
  'when `screenReaderText` is absent. The heading or surrounding text should carry the accessible meaning.',
@@ -90,16 +90,16 @@ const DEVELOPER_NOTES = [
90
90
  '',
91
91
  '- Decorative icons (no `screenReaderText`): `aria-hidden="true"` — screen readers skip them',
92
92
  '- Meaningful icons: provide `screenReaderText` so the icon has an accessible label',
93
- '- `IcoText.Heading` renders `<h4>` — verify your document heading hierarchy',
94
- '- `IcoText.Paragraph` renders `<p>` — semantically appropriate for supporting text',
93
+ '- `IconText.Heading` renders `<h4>` — verify your document heading hierarchy',
94
+ '- `IconText.Paragraph` renders `<p>` — semantically appropriate for supporting text',
95
95
  '',
96
96
  '---',
97
97
  '',
98
98
  '### TypeScript types',
99
99
  '',
100
100
  '```ts',
101
- "import { IcoText } from '@arbor-education/design-system.components';",
102
- "import type { IcoTextProps, IcoTextIconProps, IcoTextHeadingProps, IcoTextParagraphProps } from '@arbor-education/design-system.components';",
101
+ "import { IconText } from '@arbor-education/design-system.components';",
102
+ "import type { IconTextProps, IconTextIconProps, IconTextHeadingProps, IconTextParagraphProps } from '@arbor-education/design-system.components';",
103
103
  '```',
104
104
  ].join('\n');
105
105
 
@@ -115,7 +115,7 @@ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tw
115
115
  // Custom DocsPage
116
116
  // ---------------------------------------------------------------------------
117
117
 
118
- function IcoTextDocsPage() {
118
+ function IconTextDocsPage() {
119
119
  return (
120
120
  <>
121
121
  <Title />
@@ -125,11 +125,11 @@ function IcoTextDocsPage() {
125
125
  <Markdown>{PROPS_INTRO}</Markdown>
126
126
  <DocPrimary />
127
127
  <Controls />
128
- <DocHeading>IcoText.Icon props</DocHeading>
128
+ <DocHeading>IconText.Icon props</DocHeading>
129
129
  <Markdown>{ICON_PROPS}</Markdown>
130
- <DocHeading>IcoText.Heading props</DocHeading>
130
+ <DocHeading>IconText.Heading props</DocHeading>
131
131
  <Markdown>{HEADING_PROPS}</Markdown>
132
- <DocHeading>IcoText.Paragraph props</DocHeading>
132
+ <DocHeading>IconText.Paragraph props</DocHeading>
133
133
  <Markdown>{PARAGRAPH_PROPS}</Markdown>
134
134
  <DocHeading>Usage guidance</DocHeading>
135
135
  <Markdown>{USAGE_GUIDANCE}</Markdown>
@@ -147,12 +147,12 @@ function IcoTextDocsPage() {
147
147
  // ---------------------------------------------------------------------------
148
148
 
149
149
  const meta = {
150
- title: 'Components/IcoText',
151
- component: IcoText,
150
+ title: 'Components/IconText',
151
+ component: IconText,
152
152
  tags: ['autodocs'],
153
153
  parameters: {
154
154
  layout: 'padded',
155
- docs: { page: IcoTextDocsPage },
155
+ docs: { page: IconTextDocsPage },
156
156
  },
157
157
  argTypes: {
158
158
  className: {
@@ -162,14 +162,14 @@ const meta = {
162
162
  },
163
163
  children: {
164
164
  control: false,
165
- description: 'Compose using `IcoText.Icon`, `IcoText.Heading`, and `IcoText.Paragraph` sub-components.',
165
+ description: 'Compose using `IconText.Icon`, `IconText.Heading`, and `IconText.Paragraph` sub-components.',
166
166
  table: { type: { summary: 'ReactNode' } },
167
167
  },
168
168
  },
169
- } satisfies Meta<typeof IcoText>;
169
+ } satisfies Meta<typeof IconText>;
170
170
 
171
171
  export default meta;
172
- type Story = StoryObj<typeof IcoText>;
172
+ type Story = StoryObj<typeof meta>;
173
173
 
174
174
  // ---------------------------------------------------------------------------
175
175
  // Helper: attach a per-story description to docs
@@ -194,11 +194,11 @@ const IconSizesTemplate = () => (
194
194
  <p className="ds-text" style={{ margin: '0 0 var(--spacing-small)', color: 'var(--color-grey-600)' }}>
195
195
  {`size={${size}}`}
196
196
  </p>
197
- <IcoText>
198
- <IcoText.Icon name="guardians" size={size} />
199
- <IcoText.Heading>Year 9 Attendance</IcoText.Heading>
200
- <IcoText.Paragraph>28 pupils · 94.2% average this term</IcoText.Paragraph>
201
- </IcoText>
197
+ <IconText>
198
+ <IconText.Icon name="guardians" size={size} />
199
+ <IconText.Heading>Year 9 Attendance</IconText.Heading>
200
+ <IconText.Paragraph>28 pupils · 94.2% average this term</IconText.Paragraph>
201
+ </IconText>
202
202
  </div>
203
203
  ))}
204
204
  </div>
@@ -216,23 +216,23 @@ const IconColorsTemplate = () => (
216
216
  <p className="ds-text" style={{ margin: '0 0 var(--spacing-small)', color: 'var(--color-grey-600)' }}>
217
217
  {label}
218
218
  </p>
219
- <IcoText>
220
- <IcoText.Icon name="triangle-alert" color={color} />
221
- <IcoText.Heading>Attendance alert</IcoText.Heading>
222
- <IcoText.Paragraph>3 pupils below 90% this half-term</IcoText.Paragraph>
223
- </IcoText>
219
+ <IconText>
220
+ <IconText.Icon name="triangle-alert" color={color} />
221
+ <IconText.Heading>Attendance alert</IconText.Heading>
222
+ <IconText.Paragraph>3 pupils below 90% this half-term</IconText.Paragraph>
223
+ </IconText>
224
224
  </div>
225
225
  ))}
226
226
  </div>
227
227
  );
228
228
 
229
229
  const MultipleParagraphsTemplate = () => (
230
- <IcoText>
231
- <IcoText.Icon name="date" />
232
- <IcoText.Heading>Parents&apos; evening — Tuesday 14 May</IcoText.Heading>
233
- <IcoText.Paragraph>4:00 pm – 7:00 pm · Main Hall</IcoText.Paragraph>
234
- <IcoText.Paragraph>Appointments available every 10 minutes. Please arrive 5 minutes early.</IcoText.Paragraph>
235
- </IcoText>
230
+ <IconText>
231
+ <IconText.Icon name="date" />
232
+ <IconText.Heading>Parents&apos; evening — Tuesday 14 May</IconText.Heading>
233
+ <IconText.Paragraph>4:00 pm – 7:00 pm · Main Hall</IconText.Paragraph>
234
+ <IconText.Paragraph>Appointments available every 10 minutes. Please arrive 5 minutes early.</IconText.Paragraph>
235
+ </IconText>
236
236
  );
237
237
 
238
238
  // ---------------------------------------------------------------------------
@@ -242,14 +242,14 @@ const MultipleParagraphsTemplate = () => (
242
242
  export const Default: Story = withDescription(
243
243
  {
244
244
  render: () => (
245
- <IcoText>
246
- <IcoText.Icon name="guardians" />
247
- <IcoText.Heading>Year 9 Attendance</IcoText.Heading>
248
- <IcoText.Paragraph>28 pupils · 94.2% average this term</IcoText.Paragraph>
249
- </IcoText>
245
+ <IconText>
246
+ <IconText.Icon name="guardians" />
247
+ <IconText.Heading>Year 9 Attendance</IconText.Heading>
248
+ <IconText.Paragraph>28 pupils · 94.2% average this term</IconText.Paragraph>
249
+ </IconText>
250
250
  ),
251
251
  },
252
- 'The full canonical composition — `IcoText.Icon` in the left rail, `IcoText.Heading` and `IcoText.Paragraph` in the content column.',
252
+ 'The full canonical composition — `IconText.Icon` in the left rail, `IconText.Heading` and `IconText.Paragraph` in the content column.',
253
253
  );
254
254
 
255
255
  export const HeadingOnly: Story = withDescription(
@@ -260,14 +260,14 @@ export const HeadingOnly: Story = withDescription(
260
260
  source: {
261
261
  language: 'tsx',
262
262
  code: `
263
- import { IcoText } from '@arbor-education/design-system.components';
263
+ import { IconText } from '@arbor-education/design-system.components';
264
264
 
265
265
  function HeadingOnlyExample() {
266
266
  return (
267
- <IcoText>
268
- <IcoText.Icon name="book-open" />
269
- <IcoText.Heading>Assessment policy</IcoText.Heading>
270
- </IcoText>
267
+ <IconText>
268
+ <IconText.Icon name="book-open" />
269
+ <IconText.Heading>Assessment policy</IconText.Heading>
270
+ </IconText>
271
271
  );
272
272
  }
273
273
  export default HeadingOnlyExample;
@@ -276,10 +276,10 @@ export default HeadingOnlyExample;
276
276
  },
277
277
  },
278
278
  render: () => (
279
- <IcoText>
280
- <IcoText.Icon name="book-open" />
281
- <IcoText.Heading>Assessment policy</IcoText.Heading>
282
- </IcoText>
279
+ <IconText>
280
+ <IconText.Icon name="book-open" />
281
+ <IconText.Heading>Assessment policy</IconText.Heading>
282
+ </IconText>
283
283
  ),
284
284
  },
285
285
  'Icon + Heading with no Paragraph — the compact pattern for short navigational labels.',
@@ -293,14 +293,14 @@ export const WithoutIcon: Story = withDescription(
293
293
  source: {
294
294
  language: 'tsx',
295
295
  code: `
296
- import { IcoText } from '@arbor-education/design-system.components';
296
+ import { IconText } from '@arbor-education/design-system.components';
297
297
 
298
298
  function WithoutIconExample() {
299
299
  return (
300
- <IcoText>
301
- <IcoText.Heading>Behaviour report</IcoText.Heading>
302
- <IcoText.Paragraph>No incidents recorded this week. All targets met.</IcoText.Paragraph>
303
- </IcoText>
300
+ <IconText>
301
+ <IconText.Heading>Behaviour report</IconText.Heading>
302
+ <IconText.Paragraph>No incidents recorded this week. All targets met.</IconText.Paragraph>
303
+ </IconText>
304
304
  );
305
305
  }
306
306
  export default WithoutIconExample;
@@ -309,13 +309,13 @@ export default WithoutIconExample;
309
309
  },
310
310
  },
311
311
  render: () => (
312
- <IcoText>
313
- <IcoText.Heading>Behaviour report</IcoText.Heading>
314
- <IcoText.Paragraph>No incidents recorded this week. All targets met.</IcoText.Paragraph>
315
- </IcoText>
312
+ <IconText>
313
+ <IconText.Heading>Behaviour report</IconText.Heading>
314
+ <IconText.Paragraph>No incidents recorded this week. All targets met.</IconText.Paragraph>
315
+ </IconText>
316
316
  ),
317
317
  },
318
- 'Heading + Paragraph with no icon. The left rail disappears cleanly — `IcoText.Icon` is entirely optional.',
318
+ 'Heading + Paragraph with no icon. The left rail disappears cleanly — `IconText.Icon` is entirely optional.',
319
319
  );
320
320
 
321
321
  export const IconWithScreenReaderText: Story = withDescription(
@@ -326,19 +326,19 @@ export const IconWithScreenReaderText: Story = withDescription(
326
326
  source: {
327
327
  language: 'tsx',
328
328
  code: `
329
- import { IcoText } from '@arbor-education/design-system.components';
329
+ import { IconText } from '@arbor-education/design-system.components';
330
330
 
331
331
  function IconWithScreenReaderTextExample() {
332
332
  return (
333
- <IcoText>
334
- <IcoText.Icon
333
+ <IconText>
334
+ <IconText.Icon
335
335
  name="triangle-alert"
336
336
  color="var(--color-semantic-warning-600)"
337
337
  screenReaderText="Warning"
338
338
  />
339
- <IcoText.Heading>Attendance below target</IcoText.Heading>
340
- <IcoText.Paragraph>Alice Johnson — 78.4% this term. Intervention recommended.</IcoText.Paragraph>
341
- </IcoText>
339
+ <IconText.Heading>Attendance below target</IconText.Heading>
340
+ <IconText.Paragraph>Alice Johnson — 78.4% this term. Intervention recommended.</IconText.Paragraph>
341
+ </IconText>
342
342
  );
343
343
  }
344
344
  export default IconWithScreenReaderTextExample;
@@ -347,11 +347,11 @@ export default IconWithScreenReaderTextExample;
347
347
  },
348
348
  },
349
349
  render: () => (
350
- <IcoText>
351
- <IcoText.Icon name="triangle-alert" color="var(--color-semantic-warning-600)" screenReaderText="Warning" />
352
- <IcoText.Heading>Attendance below target</IcoText.Heading>
353
- <IcoText.Paragraph>Alice Johnson — 78.4% this term. Intervention recommended.</IcoText.Paragraph>
354
- </IcoText>
350
+ <IconText>
351
+ <IconText.Icon name="triangle-alert" color="var(--color-semantic-warning-600)" screenReaderText="Warning" />
352
+ <IconText.Heading>Attendance below target</IconText.Heading>
353
+ <IconText.Paragraph>Alice Johnson — 78.4% this term. Intervention recommended.</IconText.Paragraph>
354
+ </IconText>
355
355
  ),
356
356
  },
357
357
  'When the icon carries meaning not conveyed by the heading text, provide `screenReaderText`. Without it the icon is `aria-hidden="true"`. Here the warning triangle conveys severity — screen readers announce "Warning, Attendance below target".',
@@ -365,26 +365,26 @@ export const IconSizes: Story = withDescription(
365
365
  source: {
366
366
  language: 'tsx',
367
367
  code: `
368
- import { IcoText } from '@arbor-education/design-system.components';
368
+ import { IconText } from '@arbor-education/design-system.components';
369
369
 
370
370
  function IconSizesExample() {
371
371
  return (
372
372
  <>
373
- <IcoText>
374
- <IcoText.Icon name="guardians" size={12} />
375
- <IcoText.Heading>size 12</IcoText.Heading>
376
- <IcoText.Paragraph>Use in very compact contexts</IcoText.Paragraph>
377
- </IcoText>
378
- <IcoText>
379
- <IcoText.Icon name="guardians" size={16} />
380
- <IcoText.Heading>size 16</IcoText.Heading>
381
- <IcoText.Paragraph>Use in moderately compact contexts</IcoText.Paragraph>
382
- </IcoText>
383
- <IcoText>
384
- <IcoText.Icon name="guardians" size={24} />
385
- <IcoText.Heading>size 24 (default)</IcoText.Heading>
386
- <IcoText.Paragraph>Standard — aligns best with h4 line-height</IcoText.Paragraph>
387
- </IcoText>
373
+ <IconText>
374
+ <IconText.Icon name="guardians" size={12} />
375
+ <IconText.Heading>size 12</IconText.Heading>
376
+ <IconText.Paragraph>Use in very compact contexts</IconText.Paragraph>
377
+ </IconText>
378
+ <IconText>
379
+ <IconText.Icon name="guardians" size={16} />
380
+ <IconText.Heading>size 16</IconText.Heading>
381
+ <IconText.Paragraph>Use in moderately compact contexts</IconText.Paragraph>
382
+ </IconText>
383
+ <IconText>
384
+ <IconText.Icon name="guardians" size={24} />
385
+ <IconText.Heading>size 24 (default)</IconText.Heading>
386
+ <IconText.Paragraph>Standard — aligns best with h4 line-height</IconText.Paragraph>
387
+ </IconText>
388
388
  </>
389
389
  );
390
390
  }
@@ -395,7 +395,7 @@ export default IconSizesExample;
395
395
  },
396
396
  render: IconSizesTemplate,
397
397
  },
398
- 'All three supported icon sizes — 12, 16, and 24 (default). Use smaller sizes only in compact contexts such as table cells or dense lists. The `24` default aligns best with `IcoText.Heading`\'s h4 line-height.',
398
+ 'All three supported icon sizes — 12, 16, and 24 (default). Use smaller sizes only in compact contexts such as table cells or dense lists. The `24` default aligns best with `IconText.Heading`\'s h4 line-height.',
399
399
  );
400
400
 
401
401
  export const IconColors: Story = withDescription(
@@ -406,15 +406,15 @@ export const IconColors: Story = withDescription(
406
406
  source: {
407
407
  language: 'tsx',
408
408
  code: `
409
- import { IcoText } from '@arbor-education/design-system.components';
409
+ import { IconText } from '@arbor-education/design-system.components';
410
410
 
411
411
  function IconColorsExample() {
412
412
  return (
413
- <IcoText>
414
- <IcoText.Icon name="triangle-alert" color="var(--color-semantic-warning-600)" />
415
- <IcoText.Heading>Attendance alert</IcoText.Heading>
416
- <IcoText.Paragraph>3 pupils below 90% this half-term</IcoText.Paragraph>
417
- </IcoText>
413
+ <IconText>
414
+ <IconText.Icon name="triangle-alert" color="var(--color-semantic-warning-600)" />
415
+ <IconText.Heading>Attendance alert</IconText.Heading>
416
+ <IconText.Paragraph>3 pupils below 90% this half-term</IconText.Paragraph>
417
+ </IconText>
418
418
  );
419
419
  }
420
420
  export default IconColorsExample;
@@ -424,7 +424,7 @@ export default IconColorsExample;
424
424
  },
425
425
  render: IconColorsTemplate,
426
426
  },
427
- 'The `color` prop on `IcoText.Icon` accepts any CSS value — always use design token variables, never hardcoded hex. Semantic tokens (`--color-semantic-*`) are most appropriate for status-indicating icons.',
427
+ 'The `color` prop on `IconText.Icon` accepts any CSS value — always use design token variables, never hardcoded hex. Semantic tokens (`--color-semantic-*`) are most appropriate for status-indicating icons.',
428
428
  );
429
429
 
430
430
  export const MultipleParagraphs: Story = withDescription(
@@ -435,16 +435,16 @@ export const MultipleParagraphs: Story = withDescription(
435
435
  source: {
436
436
  language: 'tsx',
437
437
  code: `
438
- import { IcoText } from '@arbor-education/design-system.components';
438
+ import { IconText } from '@arbor-education/design-system.components';
439
439
 
440
440
  function MultipleParagraphsExample() {
441
441
  return (
442
- <IcoText>
443
- <IcoText.Icon name="calendar" />
444
- <IcoText.Heading>Parents' evening — Tuesday 14 May</IcoText.Heading>
445
- <IcoText.Paragraph>4:00 pm – 7:00 pm · Main Hall</IcoText.Paragraph>
446
- <IcoText.Paragraph>Appointments every 10 minutes. Please arrive 5 minutes early.</IcoText.Paragraph>
447
- </IcoText>
442
+ <IconText>
443
+ <IconText.Icon name="date" />
444
+ <IconText.Heading>Parents' evening — Tuesday 14 May</IconText.Heading>
445
+ <IconText.Paragraph>4:00 pm – 7:00 pm · Main Hall</IconText.Paragraph>
446
+ <IconText.Paragraph>Appointments every 10 minutes. Please arrive 5 minutes early.</IconText.Paragraph>
447
+ </IconText>
448
448
  );
449
449
  }
450
450
  export default MultipleParagraphsExample;
@@ -454,5 +454,5 @@ export default MultipleParagraphsExample;
454
454
  },
455
455
  render: MultipleParagraphsTemplate,
456
456
  },
457
- 'Multiple `IcoText.Paragraph` sub-components stack vertically in the content column. Use for supplementary detail lines below the primary heading.',
457
+ 'Multiple `IconText.Paragraph` sub-components stack vertically in the content column. Use for supplementary detail lines below the primary heading.',
458
458
  );
@@ -1,25 +1,25 @@
1
- import { render, screen, within } from '@testing-library/react';
2
1
  import '@testing-library/jest-dom/vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
3
  import { describe, expect, test } from 'vitest';
4
- import { IcoText } from './IcoText.js';
4
+ import { IconText } from './IconText.js';
5
5
 
6
- describe('IcoText', () => {
6
+ describe('IconText', () => {
7
7
  test('renders icon children before content children and keeps content in the content wrapper', () => {
8
8
  const { container } = render(
9
- <IcoText>
10
- <IcoText.Heading>Article title</IcoText.Heading>
11
- <IcoText.Icon name="eye" screenReaderText="Views" />
12
- <IcoText.Paragraph>Helpful supporting copy</IcoText.Paragraph>
9
+ <IconText>
10
+ <IconText.Heading>Article title</IconText.Heading>
11
+ <IconText.Icon name="eye" screenReaderText="Views" />
12
+ <IconText.Paragraph>Helpful supporting copy</IconText.Paragraph>
13
13
  <span>Metadata</span>
14
- <IcoText.Icon name="eye" screenReaderText="More views" />
15
- </IcoText>,
14
+ <IconText.Icon name="eye" screenReaderText="More views" />
15
+ </IconText>,
16
16
  );
17
17
 
18
18
  const root = container.querySelector<HTMLDivElement>('.ds-ico-text');
19
19
  const content = container.querySelector<HTMLDivElement>('.ds-ico-text__content');
20
20
 
21
21
  if (!content) {
22
- throw new Error('Expected IcoText content wrapper to exist');
22
+ throw new Error('Expected IconText content wrapper to exist');
23
23
  }
24
24
 
25
25
  const iconElements = Array.from(root?.querySelectorAll(':scope > .ds-ico-text__icon') ?? []);
@@ -3,20 +3,20 @@ import type { IconName } from 'Components/icon/allowedIcons';
3
3
  import { Icon } from 'Components/icon/Icon';
4
4
  import { Children, isValidElement } from 'react';
5
5
 
6
- export type IcoTextProps = {
6
+ export type IconTextProps = {
7
7
  children?: React.ReactNode;
8
8
  className?: string;
9
9
  };
10
10
 
11
- export type IcoTextHeadingProps = React.HTMLAttributes<HTMLHeadingElement> & {
11
+ export type IconTextHeadingProps = React.HTMLAttributes<HTMLHeadingElement> & {
12
12
  children: React.ReactNode;
13
13
  };
14
14
 
15
- export type IcoTextParagraphProps = React.HTMLAttributes<HTMLParagraphElement> & {
15
+ export type IconTextParagraphProps = React.HTMLAttributes<HTMLParagraphElement> & {
16
16
  children: React.ReactNode;
17
17
  };
18
18
 
19
- export type IcoTextIconProps = {
19
+ export type IconTextIconProps = {
20
20
  className?: string;
21
21
  color?: Icon.Props['color'];
22
22
  name: IconName;
@@ -24,33 +24,33 @@ export type IcoTextIconProps = {
24
24
  size?: 12 | 16 | 24;
25
25
  };
26
26
 
27
- const IcoTextHeading = ({
27
+ const IconTextHeading = ({
28
28
  children,
29
29
  className,
30
30
  ...rest
31
- }: IcoTextHeadingProps): React.JSX.Element => (
31
+ }: IconTextHeadingProps): React.JSX.Element => (
32
32
  <h4 className={classNames('ds-ico-text__heading', className)} {...rest}>
33
33
  {children}
34
34
  </h4>
35
35
  );
36
36
 
37
- const IcoTextParagraph = ({
37
+ const IconTextParagraph = ({
38
38
  children,
39
39
  className,
40
40
  ...rest
41
- }: IcoTextParagraphProps): React.JSX.Element => (
41
+ }: IconTextParagraphProps): React.JSX.Element => (
42
42
  <p className={classNames('ds-ico-text__paragraph', className)} {...rest}>
43
43
  {children}
44
44
  </p>
45
45
  );
46
46
 
47
- const IcoTextIcon = ({
47
+ const IconTextIcon = ({
48
48
  className,
49
49
  color,
50
50
  name,
51
51
  screenReaderText,
52
52
  size = 24,
53
- }: IcoTextIconProps): React.JSX.Element => (
53
+ }: IconTextIconProps): React.JSX.Element => (
54
54
  <Icon
55
55
  name={name}
56
56
  className={classNames('ds-ico-text__icon', className)}
@@ -60,12 +60,12 @@ const IcoTextIcon = ({
60
60
  />
61
61
  );
62
62
 
63
- const IcoTextRoot = ({ children, className }: IcoTextProps): React.JSX.Element => {
63
+ const IconTextRoot = ({ children, className }: IconTextProps): React.JSX.Element => {
64
64
  const iconChildren: React.ReactNode[] = [];
65
65
  const contentChildren: React.ReactNode[] = [];
66
66
 
67
67
  Children.forEach(children, (child) => {
68
- if (isValidElement(child) && child.type === IcoTextIcon) {
68
+ if (isValidElement(child) && child.type === IconTextIcon) {
69
69
  iconChildren.push(child);
70
70
  return;
71
71
  }
@@ -81,13 +81,20 @@ const IcoTextRoot = ({ children, className }: IcoTextProps): React.JSX.Element =
81
81
  );
82
82
  };
83
83
 
84
- IcoTextHeading.displayName = 'IcoText.Heading';
85
- IcoTextParagraph.displayName = 'IcoText.Paragraph';
86
- IcoTextIcon.displayName = 'IcoText.Icon';
87
- IcoTextRoot.displayName = 'IcoText';
84
+ IconTextHeading.displayName = 'IconText.Heading';
85
+ IconTextParagraph.displayName = 'IconText.Paragraph';
86
+ IconTextIcon.displayName = 'IconText.Icon';
87
+ IconTextRoot.displayName = 'IconText';
88
88
 
89
- export const IcoText = Object.assign(IcoTextRoot, {
90
- Heading: IcoTextHeading,
91
- Paragraph: IcoTextParagraph,
92
- Icon: IcoTextIcon,
89
+ export const IconText = Object.assign(IconTextRoot, {
90
+ Heading: IconTextHeading,
91
+ Paragraph: IconTextParagraph,
92
+ Icon: IconTextIcon,
93
93
  });
94
+
95
+ export namespace IconText {
96
+ export type Props = IconTextProps;
97
+ export type HeadingProps = IconTextHeadingProps;
98
+ export type ParagraphProps = IconTextParagraphProps;
99
+ export type IconProps = IconTextIconProps;
100
+ }
@@ -20,6 +20,8 @@ export type ModalProps = {
20
20
  overlayClassName?: string;
21
21
  open?: boolean;
22
22
  portalTarget?: HTMLElement | null;
23
+ // Optional id for the dialog content so external triggers can point aria-controls at it.
24
+ contentId?: string;
23
25
  children?: React.ReactNode;
24
26
  hideCloseButton?: boolean;
25
27
  closeHandler?: ModalContextValue['closeHandler'];
@@ -32,6 +34,7 @@ export const Modal = (props: ModalProps) => {
32
34
  overlayClassName,
33
35
  open,
34
36
  portalTarget,
37
+ contentId,
35
38
  children,
36
39
  closeHandler,
37
40
  hideCloseButton = false,
@@ -46,7 +49,8 @@ export const Modal = (props: ModalProps) => {
46
49
  <Dialog.Root open={open}>
47
50
  <Dialog.Portal container={portalTarget}>
48
51
  <Dialog.Overlay ref={overlayRef} className={classNames('ds-modal__overlay', overlayClassName)}>
49
- <Dialog.Content className={classNames('ds-modal__container', className)}>
52
+ {/* Preserve an optional target for trigger/content relationships such as FilterBar button-to-dialog wiring. */}
53
+ <Dialog.Content id={contentId} className={classNames('ds-modal__container', className)}>
50
54
  {title && <ModalHeader><ModalTitle>{title}</ModalTitle></ModalHeader>}
51
55
  {children}
52
56
  {!hideCloseButton && <ModalCloseButon className="ds-modal__close-button--top-right" />}