@arbor-education/design-system.components 0.25.2 → 0.25.4

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 (90) hide show
  1. package/.gather/skills/write-stories/README.md +1 -1
  2. package/.gather/skills/write-stories/SKILL.md +1 -1
  3. package/.gather/skills/write-stories/meta.md +1 -1
  4. package/CHANGELOG.md +14 -0
  5. package/component-library.md +26 -1
  6. package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts +3 -0
  7. package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts.map +1 -0
  8. package/dist/components/breadcrumbs/BreadcrumbCopyButton.js +13 -0
  9. package/dist/components/breadcrumbs/BreadcrumbCopyButton.js.map +1 -0
  10. package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts +3 -0
  11. package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts.map +1 -0
  12. package/dist/components/breadcrumbs/BreadcrumbDropdown.js +8 -0
  13. package/dist/components/breadcrumbs/BreadcrumbDropdown.js.map +1 -0
  14. package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts +3 -0
  15. package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts.map +1 -0
  16. package/dist/components/breadcrumbs/BreadcrumbEllipsis.js +7 -0
  17. package/dist/components/breadcrumbs/BreadcrumbEllipsis.js.map +1 -0
  18. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts +2 -0
  19. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts.map +1 -0
  20. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js +5 -0
  21. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js.map +1 -0
  22. package/dist/components/breadcrumbs/BreadcrumbLink.d.ts +6 -0
  23. package/dist/components/breadcrumbs/BreadcrumbLink.d.ts.map +1 -0
  24. package/dist/components/breadcrumbs/BreadcrumbLink.js +9 -0
  25. package/dist/components/breadcrumbs/BreadcrumbLink.js.map +1 -0
  26. package/dist/components/breadcrumbs/Breadcrumbs.d.ts +21 -0
  27. package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +1 -0
  28. package/dist/components/breadcrumbs/Breadcrumbs.js +51 -0
  29. package/dist/components/breadcrumbs/Breadcrumbs.js.map +1 -0
  30. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +81 -0
  31. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -0
  32. package/dist/components/breadcrumbs/Breadcrumbs.stories.js +681 -0
  33. package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -0
  34. package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts +2 -0
  35. package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts.map +1 -0
  36. package/dist/components/breadcrumbs/Breadcrumbs.test.js +160 -0
  37. package/dist/components/breadcrumbs/Breadcrumbs.test.js.map +1 -0
  38. package/dist/components/breadcrumbs/types.d.ts +27 -0
  39. package/dist/components/breadcrumbs/types.d.ts.map +1 -0
  40. package/dist/components/breadcrumbs/types.js +2 -0
  41. package/dist/components/breadcrumbs/types.js.map +1 -0
  42. package/dist/components/row/Row.d.ts +1 -0
  43. package/dist/components/row/Row.d.ts.map +1 -1
  44. package/dist/components/row/Row.js +4 -2
  45. package/dist/components/row/Row.js.map +1 -1
  46. package/dist/components/row/Row.stories.d.ts +111 -4
  47. package/dist/components/row/Row.stories.d.ts.map +1 -1
  48. package/dist/components/row/Row.stories.js +362 -64
  49. package/dist/components/row/Row.stories.js.map +1 -1
  50. package/dist/components/row/Row.test.js +19 -0
  51. package/dist/components/row/Row.test.js.map +1 -1
  52. package/dist/components/skeleton/Skeleton.d.ts +10 -0
  53. package/dist/components/skeleton/Skeleton.d.ts.map +1 -0
  54. package/dist/components/skeleton/Skeleton.js +9 -0
  55. package/dist/components/skeleton/Skeleton.js.map +1 -0
  56. package/dist/components/skeleton/Skeleton.stories.d.ts +99 -0
  57. package/dist/components/skeleton/Skeleton.stories.d.ts.map +1 -0
  58. package/dist/components/skeleton/Skeleton.stories.js +539 -0
  59. package/dist/components/skeleton/Skeleton.stories.js.map +1 -0
  60. package/dist/components/skeleton/Skeleton.test.d.ts +2 -0
  61. package/dist/components/skeleton/Skeleton.test.d.ts.map +1 -0
  62. package/dist/components/skeleton/Skeleton.test.js +49 -0
  63. package/dist/components/skeleton/Skeleton.test.js.map +1 -0
  64. package/dist/index.css +159 -0
  65. package/dist/index.css.map +1 -1
  66. package/dist/index.d.ts +2 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/index.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/components/breadcrumbs/BreadcrumbCopyButton.tsx +27 -0
  72. package/src/components/breadcrumbs/BreadcrumbDropdown.tsx +27 -0
  73. package/src/components/breadcrumbs/BreadcrumbEllipsis.tsx +16 -0
  74. package/src/components/breadcrumbs/BreadcrumbItemSeparator.tsx +3 -0
  75. package/src/components/breadcrumbs/BreadcrumbLink.tsx +29 -0
  76. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +870 -0
  77. package/src/components/breadcrumbs/Breadcrumbs.test.tsx +185 -0
  78. package/src/components/breadcrumbs/Breadcrumbs.tsx +92 -0
  79. package/src/components/breadcrumbs/breadcrumbs.scss +158 -0
  80. package/src/components/breadcrumbs/types.ts +31 -0
  81. package/src/components/dropdown/dropdown.scss +1 -0
  82. package/src/components/row/Row.stories.tsx +504 -81
  83. package/src/components/row/Row.test.tsx +25 -0
  84. package/src/components/row/Row.tsx +11 -1
  85. package/src/components/skeleton/Skeleton.stories.tsx +748 -0
  86. package/src/components/skeleton/Skeleton.test.tsx +55 -0
  87. package/src/components/skeleton/Skeleton.tsx +36 -0
  88. package/src/components/skeleton/skeleton.scss +23 -0
  89. package/src/index.scss +2 -0
  90. package/src/index.ts +2 -0
@@ -1,25 +1,28 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
3
3
  import { fn } from 'storybook/test';
4
+ import { Section } from '../section/Section.js';
4
5
  import { Row } from './Row.js';
5
6
  import { Badge } from '../badge/Badge.js';
6
7
  import { Tag } from '../tag/Tag.js';
7
8
  import { Icon } from '../icon/Icon.js';
8
9
  // ---------------------------------------------------------------------------
9
- // Content strings for the custom DocsPage
10
+ // Docs page content
10
11
  // ---------------------------------------------------------------------------
11
12
  const DESCRIPTION_INTRO = [
12
- '`Row` is a single-line display component used to present a labelled piece of data — optionally with a supporting note — inside summary panels, detail cards, or any section-style layout.',
13
- 'When an `onClick` handler is provided the row becomes fully interactive: it gains hover and focus styles, renders a pair of animated directional icons, and responds to keyboard activation via **Enter** and **Space**.',
13
+ '`Row` is a single-line display component for presenting a labelled piece of data inside summary panels, detail cards, or section-style layouts.',
14
+ 'Pass an `onClick` handler to make the row fully interactive it enters the tab order, gains hover and focus styles, and renders a pair of animated directional icons.',
15
+ 'Pass `href` to turn the value into a native anchor link, supporting any protocol including `mailto:` and `tel:`.',
14
16
  ].join(' ');
15
17
  const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
16
18
  const USAGE_GUIDANCE = [
17
19
  '### When to use',
18
20
  '',
19
- '- Displaying a labelled piece of read-only data inside a detail panel or summary card (e.g. "Year group · Year 9").',
21
+ '- Displaying a labelled key-value pair in a detail panel or summary card (e.g. "Year group · Year 9").',
20
22
  '- Rendering a list of named data points where a consistent label/value layout improves scannability.',
21
- '- Providing a tappable/clickable drill-down link to a related record or page when `onClick` is supplied.',
22
- '- Showing supplementary context alongside a value (e.g. a last-updated timestamp in the `note` slot).',
23
+ '- Providing a clickable drill-down link to a related record when `onClick` is supplied.',
24
+ '- Wrapping a value in a native link (URL, email address, phone number) via the `href` prop.',
25
+ '- Showing supplementary context alongside a value (e.g. a threshold note or timestamp in the `note` slot).',
23
26
  '',
24
27
  '---',
25
28
  '',
@@ -27,44 +30,75 @@ const USAGE_GUIDANCE = [
27
30
  '',
28
31
  '| Situation | Use instead |',
29
32
  '|---|---|',
30
- '| Navigation links | Native `<a>` or router `<Link>` — a `div` with `onClick` is not a true anchor |',
31
33
  '| Tabular data with sortable columns | [`Table`](?path=/docs/components-table--docs) |',
34
+ '| Form controls with labels | [`FormField`](?path=/docs/components-formfield--docs) |',
32
35
  '| Toggling or selecting items | Checkbox or radio group |',
33
- '| Form controls | [`FormField`](?path=/docs/components-formfield--docs) with an appropriate input |',
36
+ '| Navigation links standing alone | Native `<a>` or router `<Link>` |',
34
37
  ].join('\n');
35
38
  const DEVELOPER_NOTES = [
36
39
  '### Clickable vs static rows',
37
40
  '',
38
- 'The `onClick` prop is the **sole toggle** for all clickable behaviour — there is no separate `isClickable` prop or variant enum.',
41
+ 'The `onClick` prop is the **sole toggle** for all interactive behaviour — there is no separate `isClickable` prop.',
39
42
  '',
40
43
  '| State | `tabIndex` | CSS modifier | Icons rendered |',
41
44
  '|---|---|---|---|',
42
45
  '| Static | `-1` (not in tab order) | — | None |',
43
46
  '| Clickable | `0` (in tab order) | `ds-row--clickable` | `chevron-right` + `arrow-right` |',
44
47
  '',
45
- '**Dual-icon swap:** Both icons are always rendered when `onClick` is provided, but CSS controls their visibility:',
48
+ '**Dual-icon swap:** Both icons are always in the DOM when `onClick` is provided. CSS controls visibility ',
49
+ 'at rest `chevron-right` shows; on hover/focus `arrow-right` shows. No JavaScript state is involved.',
46
50
  '',
47
- '- **At rest:** `chevron-right` is visible; `arrow-right` is hidden via `display: none`.',
48
- '- **On hover / focus:** `chevron-right` is hidden; `arrow-right` becomes visible.',
51
+ '---',
52
+ '',
53
+ '### The `href` prop and value fallback',
49
54
  '',
50
- 'This swap is driven entirely by CSS no JavaScript state is involved.',
55
+ 'When `href` is provided, the value is wrapped in a native `<a>` element:',
56
+ '',
57
+ '```tsx',
58
+ '// value becomes the link text',
59
+ '<Row label="Email" value="admin@school.com" href="mailto:admin@school.com" />',
60
+ '',
61
+ '// href becomes the link text when value is omitted — avoid with special protocols',
62
+ '<Row label="Profile" href="/students/123" />',
63
+ '```',
64
+ '',
65
+ '> **⚠️ Always provide `value` when using `mailto:` or `tel:` hrefs.** Without `value`, the raw protocol',
66
+ '> string (e.g. `"mailto:admin@school.com"`) is used as link text.',
51
67
  '',
52
68
  '---',
53
69
  '',
54
70
  '### Keyboard activation',
55
71
  '',
56
- 'Clickable rows respond to **Enter** and **Space** keydown events by firing the `onClick` handler.',
72
+ 'Clickable rows respond to **Enter** and **Space** keydown events.',
73
+ 'The focus ring uses `box-shadow: 0 0 0 3px var(--color-brand-300)` — applied on `:focus`.',
57
74
  '',
58
75
  '---',
59
76
  '',
60
77
  '### Accessibility gap',
61
78
  '',
62
- '> **Known limitation:** Clickable `Row` instances render as a `<div>` with no `role="button"` attribute.',
63
- '> Screen readers will not announce the element as a button, and `getByRole("button")` will not find it.',
79
+ '> **Known limitation:** The clickable row renders as a `<div>` with no `role="button"` attribute.',
80
+ '> Screen readers will not announce it as interactive. Keyboard users can still activate it, but',
81
+ '> VoiceOver/NVDA users have no indication the row is actionable.',
82
+ '',
83
+ '`href` rows render a real `<a>` tag — full native link semantics and announced correctly by screen readers.',
64
84
  '',
65
- '`Row` does **not** spread arbitrary HTML attributes. The `className` prop is the only escape hatch.',
66
- 'Consumers **cannot** add `aria-label`, `aria-describedby`, `data-testid`, or any HTML attribute without modifying the component source.',
67
- 'These are documented design-debt items for a future accessibility audit.',
85
+ '`Row` does **not** spread arbitrary HTML attributes. `className` and `style` are the only escape hatches.',
86
+ '',
87
+ '---',
88
+ '',
89
+ '### Component tokens',
90
+ '',
91
+ 'Row is themed entirely through `--section-list-row-*` component tokens:',
92
+ '',
93
+ '| Token | Purpose |',
94
+ '|---|---|',
95
+ '| `--section-list-row-default-color-background` | Resting background colour |',
96
+ '| `--section-list-row-hover-hover-bg` | Hover background (clickable rows only) |',
97
+ '| `--section-list-row-spacing-vertical` | Top/bottom padding |',
98
+ '| `--section-list-row-spacing-horizontal` | Left/right padding |',
99
+ '| `--section-list-row-radius` | Border radius |',
100
+ '| `--section-list-row-default-color-icon-arrow` | Chevron/arrow icon stroke at rest |',
101
+ '| `--section-list-row-hover-color-icon-arrow` | Chevron/arrow icon stroke on hover |',
68
102
  '',
69
103
  '---',
70
104
  '',
@@ -77,8 +111,10 @@ const DEVELOPER_NOTES = [
77
111
  '',
78
112
  'type RowProps = {',
79
113
  ' className?: string;',
114
+ ' style?: CSSProperties;',
80
115
  ' label?: ReactNode;',
81
116
  ' value?: ReactNode;',
117
+ ' href?: string;',
82
118
  ' note?: ReactNode;',
83
119
  ' onClick?: MouseEventHandler<HTMLDivElement>;',
84
120
  '};',
@@ -87,7 +123,7 @@ const DEVELOPER_NOTES = [
87
123
  const RELATED_COMPONENTS = [
88
124
  '## Related components',
89
125
  '',
90
- '[Section](?path=/docs/components-section--docs) · [Card](?path=/docs/components-card--docs) · [Table](?path=/docs/components-table--docs)',
126
+ '[Section](?path=/docs/components-section--docs) · [Card](?path=/docs/components-card--docs) · [Table](?path=/docs/components-table--docs) · [FormField](?path=/docs/components-formfield--docs)',
91
127
  ].join('\n');
92
128
  // ---------------------------------------------------------------------------
93
129
  // Custom DocsPage
@@ -103,6 +139,7 @@ const meta = {
103
139
  component: Row,
104
140
  tags: ['autodocs'],
105
141
  parameters: {
142
+ layout: 'padded',
106
143
  docs: {
107
144
  page: RowDocsPage,
108
145
  },
@@ -110,8 +147,8 @@ const meta = {
110
147
  argTypes: {
111
148
  label: {
112
149
  description: [
113
- 'The left-hand label that identifies the data point.',
114
- 'Renders inside a `<span>` with a minimum width of 190 px so labels align consistently across stacked rows.',
150
+ 'Left-column text identifying the data point.',
151
+ 'Rendered in a `<span>` with semi-bold weight and a maximum width of 190 px so labels align consistently across stacked rows.',
115
152
  ].join(' '),
116
153
  control: 'text',
117
154
  table: {
@@ -123,6 +160,8 @@ const meta = {
123
160
  description: [
124
161
  'The right-hand value corresponding to the label.',
125
162
  'Renders in the centre column with `flex-grow: 1`, filling available width.',
163
+ 'When `href` is provided, this becomes the anchor text.',
164
+ 'If `href` is set and `value` is omitted, the href string itself is used as the link text.',
126
165
  ].join(' '),
127
166
  control: 'text',
128
167
  table: {
@@ -130,22 +169,35 @@ const meta = {
130
169
  defaultValue: { summary: 'undefined' },
131
170
  },
132
171
  },
172
+ href: {
173
+ description: [
174
+ 'When provided, wraps `value` in a native `<a>` element pointing to this URL.',
175
+ 'Supports any protocol — `https://`, `mailto:`, `tel:`, etc.',
176
+ 'Always provide `value` when using `mailto:` or `tel:` — without it the raw protocol string becomes the link text.',
177
+ ].join(' '),
178
+ control: 'text',
179
+ table: {
180
+ type: { summary: 'string' },
181
+ defaultValue: { summary: 'undefined' },
182
+ },
183
+ },
133
184
  note: {
134
185
  description: [
135
- 'Optional supplementary text rendered after the value — displayed in italics.',
136
- 'Useful for timestamps, caveats, or secondary context.',
186
+ 'Supplementary italic text rendered right-aligned after the value.',
187
+ 'When present, the CSS `:has()` selector reduces the value column\'s `flex-basis` from 70% to 50% to make room.',
188
+ 'Useful for thresholds, last-updated timestamps, or secondary context.',
137
189
  ].join(' '),
138
190
  control: 'text',
139
191
  table: {
140
- type: { summary: 'ReactNode' },
192
+ type: { summary: 'string' },
141
193
  defaultValue: { summary: 'undefined' },
142
194
  },
143
195
  },
144
196
  onClick: {
145
197
  description: [
146
- 'When provided, the row becomes interactive: it enters the tab order (`tabIndex={0}`),',
147
- 'gains the `ds-row--clickable` modifier, shows hover/focus styles, and renders dual directional icons.',
148
- 'Supports **Enter** and **Space** keyboard activation.',
198
+ 'When provided, activates all clickable behaviour: the row enters the tab order (`tabIndex={0}`),',
199
+ 'gains the `ds-row--clickable` modifier, shows hover/focus styles, and renders the dual directional icon pair.',
200
+ 'Responds to **Enter** and **Space** keyboard events.',
149
201
  ].join(' '),
150
202
  action: 'onClick',
151
203
  control: false,
@@ -155,16 +207,21 @@ const meta = {
155
207
  },
156
208
  },
157
209
  className: {
158
- description: [
159
- 'Additional CSS class names applied to the root `<div>`.',
160
- 'This is the only HTML-attribute escape hatch — arbitrary HTML attributes cannot be passed via props.',
161
- ].join(' '),
210
+ description: 'Additional CSS class names applied to the root `<div>`. The only HTML-attribute escape hatch — arbitrary attributes like `aria-label` or `data-testid` cannot be passed without modifying the component.',
162
211
  control: 'text',
163
212
  table: {
164
213
  type: { summary: 'string' },
165
214
  defaultValue: { summary: 'undefined' },
166
215
  },
167
216
  },
217
+ style: {
218
+ description: 'Inline styles applied to the root `<div>`.',
219
+ control: false,
220
+ table: {
221
+ type: { summary: 'CSSProperties' },
222
+ defaultValue: { summary: 'undefined' },
223
+ },
224
+ },
168
225
  },
169
226
  };
170
227
  export default meta;
@@ -175,10 +232,21 @@ const withDescription = (story, description) => ({
175
232
  ...story,
176
233
  parameters: {
177
234
  ...story.parameters,
178
- docs: { ...story.parameters?.docs, description: { story: description } },
235
+ docs: {
236
+ ...story.parameters?.docs,
237
+ description: {
238
+ story: description,
239
+ },
240
+ },
179
241
  },
180
242
  });
181
243
  // ---------------------------------------------------------------------------
244
+ // Template components
245
+ // ---------------------------------------------------------------------------
246
+ const MixedClickableAndStaticTemplate = () => (_jsxs(Section, { title: "Pupil details", children: [_jsx(Row, { label: "Name", value: "Amara Osei-Bonsu" }), _jsx(Row, { label: "Year group", value: "Year 10" }), _jsx(Row, { label: "Form", value: "10JH" }), _jsx(Row, { label: "SEN status", value: "SEN Support", note: "Review due May 2026", onClick: () => { } }), _jsx(Row, { label: "Attendance (YTD)", value: "88.3%", note: "Below 90% threshold", onClick: () => { } })] }));
247
+ const InSectionTemplate = () => (_jsxs(Section, { title: "Personal details", children: [_jsx(Row, { label: "Pupil", value: "Amara Osei-Bonsu" }), _jsx(Row, { label: "Date of birth", value: "14 March 2010" }), _jsx(Row, { label: "Admission number", value: "001847" }), _jsx(Row, { label: "UPN", value: "A823456789012" }), _jsx(Row, { label: "Email address", value: "a.osei-bonsu@pupil.school.example", href: "mailto:a.osei-bonsu@pupil.school.example" })] }));
248
+ const AccessibilityNotesTemplate = () => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsxs("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-3-14)' }, children: ["Static row \u2014", ' ', _jsx("code", { children: "tabIndex=\"-1\"" }), ", no ARIA role, not in tab order"] }), _jsx(Row, { label: "Year group", value: "Year 9" })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsxs("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-3-14)' }, children: ["Clickable row \u2014", ' ', _jsx("code", { children: "tabIndex=\"0\"" }), ", keyboard-activatable, but", ' ', _jsxs("strong", { children: ["no", _jsx("code", { children: "role=\"button\"" })] }), ' ', "\u2014 screen readers may not announce it as interactive"] }), _jsx(Row, { label: "SEN record", value: "SEN Support", onClick: () => { } })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsxs("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-3-14)' }, children: ["href row \u2014 renders a real", ' ', _jsx("code", { children: "<a>" }), ' ', "tag with full native link semantics \u2713"] }), _jsx(Row, { label: "Email address", value: "admin@school.example", href: "mailto:admin@school.example" })] })] }));
249
+ // ---------------------------------------------------------------------------
182
250
  // Stories
183
251
  // ---------------------------------------------------------------------------
184
252
  export const Default = withDescription({
@@ -189,7 +257,7 @@ export const Default = withDescription({
189
257
  onClick: fn(),
190
258
  },
191
259
  render: args => _jsx(Row, { ...args }),
192
- }, 'The default story is wired to the Controls panel. By default it is clickable — remove `onClick` from the controls to switch to static mode and observe how the icons and hover behaviour disappear.');
260
+ }, 'The interactive canvas — every prop is wired to the Controls panel. By default the row is clickable — clear `onClick` from the controls to switch to static mode and observe how the icons and hover behaviour disappear.');
193
261
  export const LabelOnly = withDescription({
194
262
  parameters: {
195
263
  controls: { disable: true },
@@ -208,8 +276,8 @@ export default EnrolmentStatusRow;
208
276
  },
209
277
  },
210
278
  render: () => _jsx(Row, { label: "Enrolment status" }),
211
- }, 'Minimum meaningful render — a label with no value or note. Both sibling `<span>` elements still render in the DOM (they are unconditionally present), but remain empty.');
212
- export const LabelAndValue = withDescription({
279
+ }, 'Minimum meaningful render — a label with no value, href, or note. The value and note `<span>` elements still render in the DOM but remain empty. The row holds its correct minimum height.');
280
+ export const ValueOnly = withDescription({
213
281
  parameters: {
214
282
  controls: { disable: true },
215
283
  docs: {
@@ -218,16 +286,16 @@ export const LabelAndValue = withDescription({
218
286
  code: `
219
287
  import { Row } from '@arbor-education/design-system.components';
220
288
 
221
- function DateOfBirthRow() {
222
- return <Row label="Date of birth" value="14 March 2009" />;
289
+ function ActiveStatusRow() {
290
+ return <Row value="Active" />;
223
291
  }
224
- export default DateOfBirthRow;
292
+ export default ActiveStatusRow;
225
293
  `.trim(),
226
294
  },
227
295
  },
228
296
  },
229
- render: () => _jsx(Row, { label: "Date of birth", value: "14 March 2009" }),
230
- }, 'The canonical two-column layout. The label column enforces a minimum width of 190 px so values align consistently when multiple rows are stacked.');
297
+ render: () => _jsx(Row, { value: "Active" }),
298
+ }, 'A value with no label — the label column renders empty. Valid but uncommon; most real-world rows pair a label with every value.');
231
299
  export const WithNote = withDescription({
232
300
  parameters: {
233
301
  controls: { disable: true },
@@ -252,7 +320,103 @@ export default AttendanceRow;
252
320
  },
253
321
  },
254
322
  render: () => (_jsx(Row, { label: "Attendance this term", value: "91.4%", note: "Below school target of 95%" })),
255
- }, 'Adding a `note` renders supplementary italic text alongside the value. Use it for thresholds, last-updated timestamps, or secondary context that helps a user interpret the value.');
323
+ }, 'Adding `note` renders supplementary italic text right-aligned after the value. The CSS `:has()` selector automatically shrinks the value column from 70% to 50% flex-basis to make room. Use notes for thresholds, last-updated timestamps, or secondary context that helps interpret the value.');
324
+ export const WithHref = withDescription({
325
+ parameters: {
326
+ controls: { disable: true },
327
+ docs: {
328
+ source: {
329
+ language: 'tsx',
330
+ code: `
331
+ import { Row } from '@arbor-education/design-system.components';
332
+
333
+ function ProfileLinkRow() {
334
+ return (
335
+ <Row
336
+ label="Student profile"
337
+ value="View full profile"
338
+ href="/students/amara-osei-bonsu"
339
+ />
340
+ );
341
+ }
342
+ export default ProfileLinkRow;
343
+ `.trim(),
344
+ },
345
+ },
346
+ },
347
+ render: () => (_jsx(Row, { label: "Student profile", value: "View full profile", href: "/students/amara-osei-bonsu" })),
348
+ }, 'When `href` is provided alongside `value`, the value text becomes the anchor text. The `<a>` element carries full native link semantics — right-click opens the browser context menu, the link can be opened in a new tab, and screen readers announce it correctly as a link.');
349
+ export const WithHrefFallback = withDescription({
350
+ parameters: {
351
+ controls: { disable: true },
352
+ docs: {
353
+ source: {
354
+ language: 'tsx',
355
+ code: `
356
+ import { Row } from '@arbor-education/design-system.components';
357
+
358
+ // ⚠️ href without value — the href string becomes the link text
359
+ function HrefFallbackRow() {
360
+ return <Row label="Profile" href="/students/123" />;
361
+ }
362
+ export default HrefFallbackRow;
363
+ `.trim(),
364
+ },
365
+ },
366
+ },
367
+ render: () => _jsx(Row, { label: "Profile", href: "/students/123" }),
368
+ }, 'When `href` is set but `value` is omitted, the href string itself is used as the link text (`value || href`). For plain paths like `/students/123` this is acceptable. For `mailto:` or `tel:` hrefs, the raw URI string is exposed to the user — always pair those with an explicit `value`. See the **WithHrefMailto** story for the correct email pattern.');
369
+ export const WithHrefMailto = withDescription({
370
+ parameters: {
371
+ controls: { disable: true },
372
+ docs: {
373
+ source: {
374
+ language: 'tsx',
375
+ code: `
376
+ import { Row } from '@arbor-education/design-system.components';
377
+
378
+ function EmailRow() {
379
+ return (
380
+ <Row
381
+ label="Email address"
382
+ value="j.smith@school.example"
383
+ href="mailto:j.smith@school.example"
384
+ />
385
+ );
386
+ }
387
+ export default EmailRow;
388
+ `.trim(),
389
+ },
390
+ },
391
+ },
392
+ render: () => (_jsx(Row, { label: "Email address", value: "j.smith@school.example", href: "mailto:j.smith@school.example" })),
393
+ }, 'The canonical email-address pattern: `href="mailto:..."` opens the user\'s mail client and `value` is the human-readable address shown as link text. Always provide `value` with `mailto:` hrefs — omitting it exposes the raw protocol string (e.g. `"mailto:j.smith@school.example"`) as link text.');
394
+ export const WithHrefAndNote = withDescription({
395
+ parameters: {
396
+ controls: { disable: true },
397
+ docs: {
398
+ source: {
399
+ language: 'tsx',
400
+ code: `
401
+ import { Row } from '@arbor-education/design-system.components';
402
+
403
+ function GuardianEmailRow() {
404
+ return (
405
+ <Row
406
+ label="Guardian email"
407
+ value="s.okafor@guardian.example"
408
+ href="mailto:s.okafor@guardian.example"
409
+ note="Primary contact"
410
+ />
411
+ );
412
+ }
413
+ export default GuardianEmailRow;
414
+ `.trim(),
415
+ },
416
+ },
417
+ },
418
+ render: () => (_jsx(Row, { label: "Guardian email", value: "s.okafor@guardian.example", href: "mailto:s.okafor@guardian.example", note: "Primary contact" })),
419
+ }, '`href` and `note` used together. The anchor sits in the value column while the note stays right-aligned. The CSS `:has()` reflow is active — the value column narrows to 50% flex-basis to accommodate the note.');
256
420
  export const Clickable = withDescription({
257
421
  parameters: {
258
422
  controls: { disable: true },
@@ -277,7 +441,7 @@ export default ClassRow;
277
441
  },
278
442
  },
279
443
  render: () => (_jsx(Row, { label: "Class", value: "9B/En1 \u2014 English", onClick: fn() })),
280
- }, 'Providing an `onClick` handler activates all clickable behaviour: the row gains `ds-row--clickable`, enters the tab order, shows hover/focus styles, and renders the dual-icon pair. **Hover the row** to see the icon swap from `chevron-right` to `arrow-right`. Press **Enter** or **Space** to fire the handler.');
444
+ }, 'Providing `onClick` activates all clickable behaviour: `ds-row--clickable` modifier, pointer cursor, hover background, and the dual icon pair. **Hover the row** to see `chevron-right` swap to `arrow-right` — a pure CSS transition with no JavaScript state. Press **Tab** to focus, then **Enter** or **Space** to fire the handler.');
281
445
  export const ClickableWithNote = withDescription({
282
446
  parameters: {
283
447
  controls: { disable: true },
@@ -303,7 +467,75 @@ export default GuardianRow;
303
467
  },
304
468
  },
305
469
  render: () => (_jsx(Row, { label: "Guardian", value: "Sarah Okafor", note: "Primary contact", onClick: fn() })),
306
- }, 'All four content props used together with a clickable row. The note remains visible while the row is fully interactive useful when secondary context (like "Primary contact") helps the user decide whether to navigate.');
470
+ }, 'Clickable row with all three right-side elements active: value, note, and the chevron/arrow icon pair. The note takes 20% flex-basis; the value narrows to 50%; the icon is flex-shrunk. The secondary context ("Primary contact") persists alongside the navigation affordance.');
471
+ export const MixedClickableAndStatic = withDescription({
472
+ parameters: {
473
+ controls: { disable: true },
474
+ docs: {
475
+ source: {
476
+ language: 'tsx',
477
+ code: `
478
+ import { Row, Section } from '@arbor-education/design-system.components';
479
+
480
+ function PupilDetailSection() {
481
+ return (
482
+ <Section title="Pupil details">
483
+ <Row label="Name" value="Amara Osei-Bonsu" />
484
+ <Row label="Year group" value="Year 10" />
485
+ <Row label="Form" value="10JH" />
486
+ <Row
487
+ label="SEN status"
488
+ value="SEN Support"
489
+ note="Review due May 2026"
490
+ onClick={() => navigateToSENRecord()}
491
+ />
492
+ <Row
493
+ label="Attendance (YTD)"
494
+ value="88.3%"
495
+ note="Below 90% threshold"
496
+ onClick={() => navigateToAttendance()}
497
+ />
498
+ </Section>
499
+ );
500
+ }
501
+ export default PupilDetailSection;
502
+ `.trim(),
503
+ },
504
+ },
505
+ },
506
+ render: MixedClickableAndStaticTemplate,
507
+ }, 'Static and clickable rows freely mixed inside a `Section`. Static rows display data; clickable rows provide navigation affordance. The visual contrast — pointer cursor, hover background, directional icon — makes the interactive rows immediately distinguishable without extra labelling.');
508
+ export const InSection = withDescription({
509
+ parameters: {
510
+ controls: { disable: true },
511
+ docs: {
512
+ source: {
513
+ language: 'tsx',
514
+ code: `
515
+ import { Row, Section } from '@arbor-education/design-system.components';
516
+
517
+ function PersonalDetailsSection() {
518
+ return (
519
+ <Section title="Personal details">
520
+ <Row label="Pupil" value="Amara Osei-Bonsu" />
521
+ <Row label="Date of birth" value="14 March 2010" />
522
+ <Row label="Admission number" value="001847" />
523
+ <Row label="UPN" value="A823456789012" />
524
+ <Row
525
+ label="Email address"
526
+ value="a.osei-bonsu@pupil.school.example"
527
+ href="mailto:a.osei-bonsu@pupil.school.example"
528
+ />
529
+ </Section>
530
+ );
531
+ }
532
+ export default PersonalDetailsSection;
533
+ `.trim(),
534
+ },
535
+ },
536
+ },
537
+ render: InSectionTemplate,
538
+ }, 'The canonical real-world usage: `Row` components as children of a `Section`. The `Section` provides the container heading and background; each `Row` handles the label/value layout. Most product uses of `Row` are inside a `Section`.');
307
539
  export const LongContent = withDescription({
308
540
  parameters: {
309
541
  controls: { disable: true },
@@ -315,7 +547,7 @@ import { Row } from '@arbor-education/design-system.components';
315
547
 
316
548
  function LongContentExample() {
317
549
  return (
318
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
550
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
319
551
  <Row
320
552
  label="Special educational needs and disabilities coordinator"
321
553
  value="Mrs Jacqueline Abernathy-Thornton"
@@ -335,8 +567,8 @@ export default LongContentExample;
335
567
  },
336
568
  },
337
569
  },
338
- render: () => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }, children: [_jsx(Row, { label: "Special educational needs and disabilities coordinator", value: "Mrs Jacqueline Abernathy-Thornton", note: "Updated 3 January 2026" }), _jsx(Row, { label: "Pupil premium eligibility reason", value: "Free school meals \u2014 ever 6", note: "Verified by LA", onClick: fn() })] })),
339
- }, 'Long label and value strings exercise the flex layout. The label column maintains its 190 px minimum width, which may compress the value column significantly in narrow containers.');
570
+ render: () => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx(Row, { label: "Special educational needs and disabilities coordinator", value: "Mrs Jacqueline Abernathy-Thornton", note: "Updated 3 January 2026" }), _jsx(Row, { label: "Pupil premium eligibility reason", value: "Free school meals \u2014 ever 6", note: "Verified by LA", onClick: fn() })] })),
571
+ }, 'Long label and value strings exercise the flex layout. The label column enforces a 190 px maximum width long labels wrap rather than compress the value column. Test your specific container widths if you have long copy in narrow panels.');
340
572
  export const MultipleRows = withDescription({
341
573
  parameters: {
342
574
  controls: { disable: true },
@@ -346,36 +578,24 @@ export const MultipleRows = withDescription({
346
578
  code: `
347
579
  import { Row } from '@arbor-education/design-system.components';
348
580
 
349
- function PupilDetailPanel() {
581
+ function PupilDataList() {
350
582
  return (
351
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
583
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
352
584
  <Row label="Pupil" value="Amara Osei-Bonsu" />
353
585
  <Row label="Year group" value="Year 10" />
354
586
  <Row label="Form" value="10JH" />
355
587
  <Row label="Admission number" value="001847" />
356
588
  <Row label="UPN" value="A823456789012" />
357
- <Row
358
- label="SEN status"
359
- value="SEN Support"
360
- note="Review due May 2026"
361
- onClick={() => navigateToSENRecord()}
362
- />
363
- <Row
364
- label="Attendance (YTD)"
365
- value="88.3%"
366
- note="Below 90% threshold"
367
- onClick={() => navigateToAttendance()}
368
- />
369
589
  </div>
370
590
  );
371
591
  }
372
- export default PupilDetailPanel;
592
+ export default PupilDataList;
373
593
  `.trim(),
374
594
  },
375
595
  },
376
596
  },
377
- render: () => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }, children: [_jsx(Row, { label: "Pupil", value: "Amara Osei-Bonsu" }), _jsx(Row, { label: "Year group", value: "Year 10" }), _jsx(Row, { label: "Form", value: "10JH" }), _jsx(Row, { label: "Admission number", value: "001847" }), _jsx(Row, { label: "UPN", value: "A823456789012" }), _jsx(Row, { label: "SEN status", value: "SEN Support", note: "Review due May 2026", onClick: fn() }), _jsx(Row, { label: "Attendance (YTD)", value: "88.3%", note: "Below 90% threshold", onClick: fn() })] })),
378
- }, 'A realistic pupil detail panel the most common real-world usage. Static and clickable rows can be freely mixed. The `gap: var(--spacing-small)` wrapper token matches the spacing the component uses internally, keeping visual rhythm consistent.');
597
+ render: () => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx(Row, { label: "Pupil", value: "Amara Osei-Bonsu" }), _jsx(Row, { label: "Year group", value: "Year 10" }), _jsx(Row, { label: "Form", value: "10JH" }), _jsx(Row, { label: "Admission number", value: "001847" }), _jsx(Row, { label: "UPN", value: "A823456789012" })] })),
598
+ }, 'Multiple static rows stacked with `gap: var(--spacing-xsmall)`. Each `Row` carries its own background and padding the gap sits between rows. Use `Section` as the outer container in production to get the correct heading and container treatment.');
379
599
  export const ReactNodeInValue = withDescription({
380
600
  parameters: {
381
601
  controls: { disable: true },
@@ -462,4 +682,82 @@ export default AnnotatedLabelRows;
462
682
  },
463
683
  render: () => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }, children: [_jsx(Row, { label: (_jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: ["Pupil premium", ' ', _jsx(Icon, { name: "info", size: 16 })] })), value: "Free school meals \u2014 ever 6" }), _jsx(Row, { label: (_jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: ["EHCP status", ' ', _jsx(Icon, { name: "info", size: 16 })] })), value: "EHC plan in place" })] })),
464
684
  }, 'The `label` slot accepts any `ReactNode` — inline icons can annotate labels that require clarification. An `Icon` placed inline with the label text signals to the user that more information is available, without cluttering the value column.');
685
+ export const AccessibilityNotes = withDescription({
686
+ parameters: {
687
+ controls: { disable: true },
688
+ docs: {
689
+ source: {
690
+ language: 'tsx',
691
+ code: `
692
+ import { Row } from '@arbor-education/design-system.components';
693
+
694
+ // Static: tabIndex="-1", no ARIA role, not keyboard-focusable
695
+ <Row label="Year group" value="Year 9" />
696
+
697
+ // Clickable: tabIndex="0", Enter/Space activate onClick
698
+ // ⚠️ No role="button" — screen readers may not announce as interactive
699
+ <Row label="SEN record" value="SEN Support" onClick={handleClick} />
700
+
701
+ // href: real <a> tag — full native link semantics, announced as a link ✓
702
+ <Row label="Email address" value="admin@school.example" href="mailto:admin@school.example" />
703
+ `.trim(),
704
+ },
705
+ },
706
+ },
707
+ render: AccessibilityNotesTemplate,
708
+ }, [
709
+ '**Known accessibility gap:** Clickable rows (`onClick` provided) render as a `<div>` with no `role="button"`.',
710
+ 'Screen readers will not announce them as interactive.',
711
+ 'Keyboard users can still activate via Tab + Enter/Space, but VoiceOver/NVDA users have no indication the row is actionable.',
712
+ '',
713
+ '`href` rows are the exception — they render a real `<a>` tag with full native link semantics.',
714
+ '',
715
+ 'If your use case requires screen-reader announcements on clickable rows, pass `className` with a workaround or request a component update.',
716
+ ].join(' '));
717
+ export const CustomClassAndStyle = withDescription({
718
+ parameters: {
719
+ controls: { disable: true },
720
+ docs: {
721
+ source: {
722
+ language: 'tsx',
723
+ code: `
724
+ import { Row } from '@arbor-education/design-system.components';
725
+
726
+ function HighlightedRow() {
727
+ return (
728
+ <Row
729
+ label="Safeguarding flag"
730
+ value="CP Plan — Stage 3"
731
+ className="my-highlighted-row"
732
+ style={{ borderLeft: '4px solid var(--color-semantic-destructive-600)' }}
733
+ />
734
+ );
735
+ }
736
+ export default HighlightedRow;
737
+ `.trim(),
738
+ },
739
+ },
740
+ },
741
+ render: () => (_jsx(Row, { label: "Safeguarding flag", value: "CP Plan \u2014 Stage 3", className: "my-highlighted-row", style: { borderLeft: '4px solid var(--color-semantic-destructive-600)' } })),
742
+ }, '`className` and `style` are the only HTML-attribute escape hatches — `Row` does not spread arbitrary HTML attributes. Use `className` to apply component-token overrides or modifier classes; use `style` for one-off inline tweaks.');
743
+ export const EmptyRow = withDescription({
744
+ parameters: {
745
+ controls: { disable: true },
746
+ docs: {
747
+ source: {
748
+ language: 'tsx',
749
+ code: `
750
+ import { Row } from '@arbor-education/design-system.components';
751
+
752
+ // All props omitted — renders three empty spans in a flex container
753
+ function EmptyRowExample() {
754
+ return <Row />;
755
+ }
756
+ export default EmptyRowExample;
757
+ `.trim(),
758
+ },
759
+ },
760
+ },
761
+ render: () => _jsx(Row, {}),
762
+ }, 'All props omitted. The component renders without crashing — three empty `<span>` elements in a flex container at the default row height. A `Row` with no props is visible as an empty strip, not invisible, so conditionally-rendered row content produces a layout placeholder rather than disappearing silently.');
465
763
  //# sourceMappingURL=Row.stories.js.map