@arbor-education/design-system.components 3.1.0 → 3.2.1

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 (104) hide show
  1. package/.gather/skills/write-stories/README.md +8 -4
  2. package/.gather/skills/write-stories/SKILL.md +59 -5
  3. package/.github/pull_request_template.md +4 -3
  4. package/CHANGELOG.md +20 -0
  5. package/CONTRIBUTING.md +1 -0
  6. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +1 -0
  7. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -1
  8. package/dist/components/breadcrumbs/Breadcrumbs.stories.js +103 -21
  9. package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -1
  10. package/dist/components/card/Card.stories.d.ts.map +1 -1
  11. package/dist/components/card/Card.stories.js +46 -41
  12. package/dist/components/card/Card.stories.js.map +1 -1
  13. package/dist/components/editableText/EditableText.stories.d.ts.map +1 -1
  14. package/dist/components/editableText/EditableText.stories.js +19 -6
  15. package/dist/components/editableText/EditableText.stories.js.map +1 -1
  16. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
  17. package/dist/components/formField/fieldset/Fieldset.stories.js +5 -3
  18. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
  19. package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.d.ts +2 -2
  20. package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.d.ts.map +1 -1
  21. package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.js +0 -1
  22. package/dist/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.js.map +1 -1
  23. package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
  24. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
  25. package/dist/components/icon/Icon.d.ts.map +1 -1
  26. package/dist/components/icon/Icon.js +18 -3
  27. package/dist/components/icon/Icon.js.map +1 -1
  28. package/dist/components/icon/Icon.stories.d.ts +4 -2
  29. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  30. package/dist/components/icon/Icon.stories.js +150 -102
  31. package/dist/components/icon/Icon.stories.js.map +1 -1
  32. package/dist/components/icon/Icon.test.js +146 -6
  33. package/dist/components/icon/Icon.test.js.map +1 -1
  34. package/dist/components/icon/LucideIcon.d.ts +17 -0
  35. package/dist/components/icon/LucideIcon.d.ts.map +1 -0
  36. package/dist/components/icon/LucideIcon.js +73 -0
  37. package/dist/components/icon/LucideIcon.js.map +1 -0
  38. package/dist/components/icon/allowedIcons.d.ts +44 -108
  39. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  40. package/dist/components/icon/allowedIcons.js +52 -112
  41. package/dist/components/icon/allowedIcons.js.map +1 -1
  42. package/dist/components/icon/types.d.ts +5 -4
  43. package/dist/components/icon/types.d.ts.map +1 -1
  44. package/dist/components/row/Row.stories.d.ts.map +1 -1
  45. package/dist/components/row/Row.stories.js +22 -13
  46. package/dist/components/row/Row.stories.js.map +1 -1
  47. package/dist/components/sidebar/Sidebar.stories.d.ts.map +1 -1
  48. package/dist/components/sidebar/Sidebar.stories.js +19 -24
  49. package/dist/components/sidebar/Sidebar.stories.js.map +1 -1
  50. package/dist/components/table/Table.stories.d.ts.map +1 -1
  51. package/dist/components/table/Table.stories.js +16 -10
  52. package/dist/components/table/Table.stories.js.map +1 -1
  53. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
  54. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +8 -5
  55. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
  56. package/dist/components/table/tableControls/TableControls.stories.d.ts +1 -1
  57. package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -1
  58. package/dist/components/table/tableControls/TableControls.stories.js +26 -21
  59. package/dist/components/table/tableControls/TableControls.stories.js.map +1 -1
  60. package/dist/components/tabs/Tabs.d.ts +1 -1
  61. package/dist/components/tabs/Tabs.stories.d.ts +1 -1
  62. package/dist/components/tabs/Tabs.stories.js +1 -1
  63. package/dist/components/tabs/Tabs.stories.js.map +1 -1
  64. package/dist/components/tabs/TabsItem.d.ts +2 -2
  65. package/dist/components/tabs/TabsItem.d.ts.map +1 -1
  66. package/dist/components/tabs/TabsItem.stories.d.ts +5 -5
  67. package/dist/components/tabs/TabsItem.stories.d.ts.map +1 -1
  68. package/dist/components/tabs/TabsItem.stories.js +7 -4
  69. package/dist/components/tabs/TabsItem.stories.js.map +1 -1
  70. package/dist/components/widget/Widget.stories.d.ts +13 -0
  71. package/dist/components/widget/Widget.stories.d.ts.map +1 -1
  72. package/dist/components/widget/Widget.stories.js +45 -6
  73. package/dist/components/widget/Widget.stories.js.map +1 -1
  74. package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts +88 -16
  75. package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts.map +1 -1
  76. package/dist/components/widgetCalendar/WidgetCalendar.stories.js +443 -30
  77. package/dist/components/widgetCalendar/WidgetCalendar.stories.js.map +1 -1
  78. package/dist/index.css +1 -4
  79. package/dist/index.css.map +1 -1
  80. package/package.json +1 -1
  81. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +112 -21
  82. package/src/components/card/Card.stories.tsx +46 -41
  83. package/src/components/datePicker/datePicker.scss +0 -5
  84. package/src/components/editableText/EditableText.stories.tsx +26 -29
  85. package/src/components/formField/fieldset/Fieldset.stories.tsx +5 -3
  86. package/src/components/formField/inputs/selectDropdown/items/item/SelectDropdownItem.tsx +2 -2
  87. package/src/components/icon/Icon.stories.tsx +224 -119
  88. package/src/components/icon/Icon.test.tsx +178 -6
  89. package/src/components/icon/Icon.tsx +25 -3
  90. package/src/components/icon/LucideIcon.tsx +94 -0
  91. package/src/components/icon/allowedIcons.tsx +67 -207
  92. package/src/components/icon/lucideDynamic.d.ts +13 -0
  93. package/src/components/icon/types.ts +5 -4
  94. package/src/components/row/Row.stories.tsx +20 -14
  95. package/src/components/sidebar/Sidebar.stories.tsx +61 -112
  96. package/src/components/table/Table.stories.tsx +25 -10
  97. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +8 -5
  98. package/src/components/table/tableControls/TableControls.stories.tsx +26 -21
  99. package/src/components/tabs/Tabs.stories.tsx +1 -1
  100. package/src/components/tabs/TabsItem.stories.tsx +8 -4
  101. package/src/components/tabs/TabsItem.tsx +2 -2
  102. package/src/components/widget/Widget.stories.tsx +61 -6
  103. package/src/components/widgetCalendar/WidgetCalendar.stories.tsx +547 -74
  104. package/src/tokens.scss +1 -0
@@ -0,0 +1,94 @@
1
+ import { useEffect, useReducer } from 'react';
2
+ import { dynamicIconImports, iconNames } from 'lucide-react/dynamic.mjs';
3
+ import type { DynamicIconModule, IconName } from 'lucide-react/dynamic';
4
+ import type { CustomIconProps } from './types.js';
5
+
6
+ /** Every icon name published by `lucide-react`, kebab-cased. */
7
+ export type LucideIconName = IconName;
8
+
9
+ /** Every `lucide-react` icon name, for building galleries and validating input. */
10
+ export const lucideIconNames: readonly LucideIconName[] = iconNames;
11
+
12
+ type IconLoader = () => Promise<DynamicIconModule>;
13
+ type LucideIconComponent = DynamicIconModule['default'];
14
+
15
+ const iconLoaders: Readonly<Record<LucideIconName, IconLoader>> = dynamicIconImports;
16
+
17
+ /**
18
+ * Icons resolve through `lucide-react`'s per-icon dynamic imports so that consumers only ship the
19
+ * icons they actually render, rather than all ~1,900 of them. Both caches are module-scoped: the
20
+ * first render of a given icon anywhere in the app pays the import, and every render after that —
21
+ * including new instances in other trees — resolves synchronously from `loadedIcons`.
22
+ */
23
+ const loadedIcons = new Map<LucideIconName, LucideIconComponent>();
24
+ const inFlightLoads = new Map<LucideIconName, Promise<void>>();
25
+
26
+ const loadIcon = (name: LucideIconName): Promise<void> => {
27
+ const inFlight = inFlightLoads.get(name);
28
+ if (inFlight) {
29
+ return inFlight;
30
+ }
31
+ // Annotated rather than inferred: names reaching `Icon` from a backend are not always valid.
32
+ const loader: IconLoader | undefined = iconLoaders[name];
33
+ if (!loader) {
34
+ console.error(`[design-system] "${name}" is not a known icon name.`);
35
+ // Cached so an unknown name is only reported once, however many times it is rendered.
36
+ const noSuchIcon = Promise.resolve();
37
+ inFlightLoads.set(name, noSuchIcon);
38
+ return noSuchIcon;
39
+ }
40
+ const load = loader()
41
+ .then((iconModule) => {
42
+ loadedIcons.set(name, iconModule.default);
43
+ })
44
+ .catch((error: unknown) => {
45
+ // Drop the rejected promise so a later render can retry (e.g. after a transient chunk 404).
46
+ inFlightLoads.delete(name);
47
+ console.error(`[design-system] Could not load the "${name}" icon.`, error);
48
+ });
49
+ inFlightLoads.set(name, load);
50
+ return load;
51
+ };
52
+
53
+ const useLucideIcon = (name: LucideIconName): LucideIconComponent | undefined => {
54
+ const [, rerender] = useReducer((renderCount: number) => renderCount + 1, 0);
55
+
56
+ useEffect(() => {
57
+ if (loadedIcons.has(name)) {
58
+ return;
59
+ }
60
+ let isMounted = true;
61
+ void loadIcon(name).then(() => {
62
+ if (isMounted) {
63
+ rerender();
64
+ }
65
+ });
66
+ return () => {
67
+ isMounted = false;
68
+ };
69
+ }, [name]);
70
+
71
+ return loadedIcons.get(name);
72
+ };
73
+
74
+ export type LucideIconProps = CustomIconProps & {
75
+ name: LucideIconName;
76
+ fill?: string;
77
+ };
78
+
79
+ /**
80
+ * Renders a single `lucide-react` icon, loading it on demand. Until the icon resolves an empty SVG
81
+ * of the same size is rendered in its place, so the surrounding layout does not shift and the
82
+ * element keeps its accessible role throughout.
83
+ */
84
+ export const LucideIcon = (props: LucideIconProps) => {
85
+ const { name, ...iconProps } = props;
86
+ const Component = useLucideIcon(name);
87
+
88
+ if (!Component) {
89
+ const { size, ...placeholderProps } = iconProps;
90
+ return <svg width={size} height={size} viewBox="0 0 24 24" {...placeholderProps} />;
91
+ }
92
+
93
+ return <Component {...iconProps} />;
94
+ };
@@ -1,217 +1,77 @@
1
- import {
2
- Archive,
3
- ArrowDown,
4
- ArrowDownUp,
5
- ArrowLeft,
6
- ArrowRight,
7
- ArrowRightFromLine,
8
- ArrowUp,
9
- ArrowUpRight,
10
- BookOpen,
11
- CalendarFold,
12
- Camera,
13
- ChartColumnIncreasing,
14
- ChartSpline,
15
- Check,
16
- ChevronDown,
17
- ChevronLeft,
18
- ChevronRight,
19
- ChevronsLeft,
20
- ChevronsRight,
21
- ChevronUp,
22
- CircleAlert,
23
- CircleCheck,
24
- CircleCheckBig,
25
- CirclePercent,
26
- CirclePlus,
27
- CircleQuestionMark,
28
- CircleX,
29
- Clipboard,
30
- ClipboardList,
31
- Clock3,
32
- CloudUpload,
33
- Copy,
34
- CornerDownLeft,
35
- Dot,
36
- Download,
37
- Ellipsis,
38
- EllipsisVertical,
39
- Expand,
40
- ExternalLink,
41
- Eye,
42
- EyeOff,
43
- File,
44
- Files,
45
- Flag,
46
- Funnel,
47
- GraduationCap,
48
- Grid3x3,
49
- GripVertical,
50
- History,
51
- House,
52
- Info,
53
- IterationCcw,
54
- LayoutList,
55
- Lightbulb,
56
- Link,
57
- List,
58
- ListFilterPlus,
59
- ListTree,
60
- LoaderCircle,
61
- Lock,
62
- LockOpen,
63
- LogOut, type LucideProps,
64
- Mail,
65
- Menu,
66
- MessageSquareMore,
67
- Mic,
68
- Minimize2,
69
- Minus,
70
- Monitor,
71
- PaintBucket,
72
- Paperclip,
73
- PartyPopper,
74
- Pencil,
75
- Percent,
76
- Phone,
77
- Pin,
78
- Plus,
79
- Projector,
80
- Redo2,
81
- Save,
82
- Search,
83
- Send,
84
- Settings,
85
- Share,
86
- Sheet,
87
- SlidersHorizontal,
88
- Smartphone,
89
- Sparkles,
90
- Star, SwitchCamera, Table, Trash2, TrendingDown, TrendingUp, TriangleAlert, Undo2, Upload, User,
91
- Users,
92
- UsersRound, X, Zap,
93
- } from 'lucide-react';
1
+ import type { ReactNode } from 'react';
2
+ import { LucideIcon, lucideIconNames, type LucideIconName } from './LucideIcon.js';
94
3
  import { AskArbor } from './customIcons/AskArbor.js';
95
4
  import { CheckSolid } from './customIcons/CheckSolid.js';
96
5
  import { Google } from './customIcons/Google.js';
97
6
  import { XSolid } from './customIcons/XSolid.js';
98
7
  import type { CustomIconProps } from './types.js';
99
8
 
100
- export const allowedIcons = {
101
- // lucide icons
102
- '3-dot': EllipsisVertical,
103
- 'archive': Archive,
104
- 'arrow-down': ArrowDown,
105
- 'arrow-left': ArrowLeft,
106
- 'arrow-right-from-line': ArrowRightFromLine,
107
- 'arrow-right': ArrowRight,
108
- 'arrow-up-right': ArrowUpRight,
109
- 'arrow-up': ArrowUp,
110
- 'book-open': BookOpen,
111
- 'camera': Camera,
112
- 'chart-column-increasing': ChartColumnIncreasing,
113
- 'chart-spline': ChartSpline,
114
- 'check': Check,
115
- 'chevron-down': ChevronDown,
116
- 'chevron-left': ChevronLeft,
117
- 'chevron-right': ChevronRight,
118
- 'chevron-up': ChevronUp,
119
- 'chevrons-left': ChevronsLeft,
120
- 'chevrons-right': ChevronsRight,
121
- 'circle-alert': CircleAlert,
122
- 'circle-check-big': CircleCheckBig,
123
- 'circle-check': CircleCheck,
124
- 'circle-help': CircleQuestionMark,
125
- 'circle-percent': CirclePercent,
126
- 'circle-plus': CirclePlus,
127
- 'circle-x': CircleX,
128
- 'clipboard-list': ClipboardList,
129
- 'clipboard': Clipboard,
130
- 'clock-3': Clock3,
131
- 'cloud-upload': CloudUpload,
132
- 'copy': Copy,
133
- 'corner-down-left': CornerDownLeft,
134
- 'date': CalendarFold,
135
- 'dot': Dot,
136
- 'download': Download,
137
- 'ellipsis-vertical': EllipsisVertical,
138
- 'ellipsis': Ellipsis,
139
- 'expand': Expand,
140
- 'external-link': ExternalLink,
141
- 'eye-off': EyeOff,
142
- 'eye': Eye,
143
- 'favourite-outline': Star,
144
- 'file': File,
145
- 'files': Files,
146
- 'flag': Flag,
147
- 'funnel': Funnel,
148
- 'grab': GripVertical,
149
- 'graduation-cap': GraduationCap,
150
- 'grid-3x3': Grid3x3,
151
- 'group': UsersRound,
152
- 'guardians': Users,
153
- 'history': History,
154
- 'house': House,
155
- 'info': Info,
156
- 'iteration-ccw': IterationCcw,
157
- 'layout-list': LayoutList,
158
- 'lightbulb': Lightbulb,
159
- 'link': Link,
160
- 'list-filter-plus': ListFilterPlus,
161
- 'list': List,
162
- 'list-tree': ListTree,
163
- 'loader': LoaderCircle,
164
- 'lock-open': LockOpen,
165
- 'lock': Lock,
166
- 'log-out': LogOut,
167
- 'mail': Mail,
168
- 'menu': Menu,
169
- 'message-square-more': MessageSquareMore,
170
- 'mic': Mic,
171
- 'minus': Minus,
172
- 'monitor': Monitor,
173
- 'paint-bucket': PaintBucket,
174
- 'paperclip': Paperclip,
175
- 'party-popper': PartyPopper,
176
- 'pencil': Pencil,
177
- 'percent': Percent,
178
- 'phone': Phone,
179
- 'pin': Pin,
180
- 'plus': Plus,
181
- 'projector': Projector,
182
- 'redo': Redo2,
183
- 'save': Save,
184
- 'search': Search,
185
- 'send': Send,
186
- 'settings': Settings,
187
- 'share': Share,
188
- 'sheet': Sheet,
189
- 'shrink': Minimize2,
190
- 'sliders-horizontal': SlidersHorizontal,
191
- 'smartphone': Smartphone,
192
- 'sorting': ArrowDownUp,
193
- 'sparkles': Sparkles,
194
- 'staff': GraduationCap,
195
- 'switch-camera': SwitchCamera,
196
- 'table': Table,
197
- 'trash': Trash2,
198
- 'trending-down': TrendingDown,
199
- 'trending-up': TrendingUp,
200
- 'triangle-alert': TriangleAlert,
201
- 'undo': Undo2,
202
- 'upload': Upload,
203
- 'user': User,
204
- 'x': X,
205
- 'zap': Zap,
206
- // custom icons
9
+ /**
10
+ * Arbor names that point at a differently-named Lucide icon. These take precedence over the Lucide
11
+ * name they shadow, so `trash` stays the lidded can (`trash-2`) rather than Lucide's plain `trash`.
12
+ * Seven of these — `grab`, `group`, `loader`, `redo`, `shrink`, `trash` and `undo` — shadow a real
13
+ * Lucide icon with a different glyph, which is deliberate: renaming them would break every existing
14
+ * consumer. Reach for the target name directly if you want Lucide's version.
15
+ *
16
+ * The `satisfies` clause makes every target a compile-time-checked Lucide icon name.
17
+ */
18
+ export const iconAliases = {
19
+ '3-dot': 'ellipsis-vertical',
20
+ 'circle-help': 'circle-question-mark',
21
+ 'date': 'calendar-fold',
22
+ 'favourite-outline': 'star',
23
+ 'grab': 'grip-vertical',
24
+ 'group': 'users-round',
25
+ 'guardians': 'users',
26
+ 'loader': 'loader-circle',
27
+ 'redo': 'redo-2',
28
+ 'shrink': 'minimize-2',
29
+ 'sorting': 'arrow-down-up',
30
+ 'staff': 'graduation-cap',
31
+ 'trash': 'trash-2',
32
+ 'undo': 'undo-2',
33
+ } as const satisfies Readonly<Record<string, LucideIconName>>;
34
+
35
+ /**
36
+ * Icons that are not plain Lucide glyphs: Arbor-specific artwork, and the solid variants built for
37
+ * particular semantic emphasis states.
38
+ */
39
+ export const customIcons = {
207
40
  'ask-arbor': AskArbor,
208
41
  'google': Google,
209
- // solid icons
210
- 'check-solid': (props: CustomIconProps) => <CheckSolid {...props} />,
211
- 'favourite-filled': (props: LucideProps) => (
212
- <Star {...props} fill={props.color} />
42
+ 'check-solid': CheckSolid,
43
+ 'favourite-filled': (props: CustomIconProps) => (
44
+ <LucideIcon name="star" {...props} fill={props.color} />
213
45
  ),
214
- 'x-solid': (props: CustomIconProps) => <XSolid {...props} />,
215
- } as const;
46
+ 'x-solid': XSolid,
47
+ } as const satisfies Readonly<Record<string, (props: CustomIconProps) => ReactNode>>;
48
+
49
+ export type IconName
50
+ = | LucideIconName
51
+ | keyof typeof iconAliases
52
+ | keyof typeof customIcons;
53
+
54
+ // `Object.keys` widens to `string[]`, so the key unions are reasserted here.
55
+ const aliasNames = Object.keys(iconAliases) as ReadonlyArray<keyof typeof iconAliases>;
56
+ const customIconNames = Object.keys(customIcons) as ReadonlyArray<keyof typeof customIcons>;
57
+
58
+ /** Every name `Icon` accepts: the whole Lucide set, plus Arbor's aliases and custom icons. */
59
+ export const iconNames: readonly IconName[] = [
60
+ ...lucideIconNames,
61
+ ...aliasNames,
62
+ ...customIconNames,
63
+ ];
64
+
65
+ /**
66
+ * The `Icon` name allow-list, as a plain object so that membership can be tested with
67
+ * `hasOwnProperty`. Prefer {@link isIconName}; the values carry no meaning and exist only to make
68
+ * this a record. Icon components are no longer held here — they are loaded on demand, so nothing
69
+ * outside a render has a component to hand out.
70
+ */
71
+ export const allowedIcons = Object.freeze(
72
+ Object.fromEntries(iconNames.map(name => [name, true])),
73
+ ) as Readonly<Record<IconName, true>>;
216
74
 
217
- export type IconName = keyof typeof allowedIcons;
75
+ /** Narrows an arbitrary string to an `IconName`, for validating names that come from a backend. */
76
+ export const isIconName = (name: string): name is IconName =>
77
+ Object.prototype.hasOwnProperty.call(allowedIcons, name);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `lucide-react` ships its dynamic-import entry point as `dynamic.mjs` and declares no `exports`
3
+ * map, so the extensionless `lucide-react/dynamic` specifier only resolves in bundlers that list
4
+ * `.mjs` in `resolve.extensions`. `arbor-fe-library`'s webpack config does not, so we import the
5
+ * file by its exact name instead. TypeScript then looks for a sibling `dynamic.d.mts`, which the
6
+ * package does not publish — this declaration points it at the types that do ship.
7
+ *
8
+ * Type-only imports still use `lucide-react/dynamic`, which resolves under both `bundler` and
9
+ * classic `node` module resolution and is erased before any bundler sees it.
10
+ */
11
+ declare module 'lucide-react/dynamic.mjs' {
12
+ export * from 'lucide-react/dynamic';
13
+ }
@@ -2,8 +2,9 @@ export const iconSizes = [12, 16, 24] as const;
2
2
  export type IconSize = typeof iconSizes[number];
3
3
 
4
4
  export type CustomIconProps = {
5
- size: IconSize;
6
- color?: string;
7
- role?: string;
8
- className?: string;
5
+ 'size': IconSize;
6
+ 'color'?: string;
7
+ 'role'?: string;
8
+ 'className'?: string;
9
+ 'aria-hidden'?: boolean;
9
10
  };
@@ -320,13 +320,11 @@ const MixedClickableAndStaticTemplate = () => (
320
320
  <Row
321
321
  label="SEN status"
322
322
  value="SEN Support"
323
- note="Review due May 2026"
324
323
  onClick={() => {}}
325
324
  />
326
325
  <Row
327
326
  label="Attendance (YTD)"
328
327
  value="88.3%"
329
- note="Below 90% threshold"
330
328
  onClick={() => {}}
331
329
  />
332
330
  </Section>
@@ -778,13 +776,11 @@ function PupilDetailSection() {
778
776
  <Row
779
777
  label="SEN status"
780
778
  value="SEN Support"
781
- note="Review due May 2026"
782
779
  onClick={() => navigateToSENRecord()}
783
780
  />
784
781
  <Row
785
782
  label="Attendance (YTD)"
786
783
  value="88.3%"
787
- note="Below 90% threshold"
788
784
  onClick={() => navigateToAttendance()}
789
785
  />
790
786
  </Section>
@@ -832,7 +828,7 @@ export default PersonalDetailsSection;
832
828
  },
833
829
  render: InSectionTemplate,
834
830
  },
835
- '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`.',
831
+ '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` — one row demonstrates a linked value via `href` to show that pattern working inside a realistic panel too.',
836
832
  );
837
833
 
838
834
  export const LongContent: Story = withDescription(
@@ -857,7 +853,6 @@ function LongContentExample() {
857
853
  label="Pupil premium eligibility reason"
858
854
  value="Free school meals — ever 6"
859
855
  note="Verified by LA"
860
- onClick={() => navigateToEligibilityDetail()}
861
856
  />
862
857
  </div>
863
858
  );
@@ -878,7 +873,6 @@ export default LongContentExample;
878
873
  label="Pupil premium eligibility reason"
879
874
  value="Free school meals — ever 6"
880
875
  note="Verified by LA"
881
- onClick={fn()}
882
876
  />
883
877
  </div>
884
878
  ),
@@ -1102,16 +1096,28 @@ export const WithTooltip: Story = withDescription(
1102
1096
  code: `
1103
1097
  import { Row } from '@arbor-education/design-system.components';
1104
1098
 
1105
- function ReadOnlyRow() {
1099
+ function ReadOnlyRows() {
1106
1100
  return (
1107
- <Row
1108
- label="UPN"
1109
- value="A823456789012"
1110
- tooltipText="Unique Pupil Number — assigned by the DfE and cannot be changed here"
1111
- />
1101
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
1102
+ <Row
1103
+ label="UPN"
1104
+ value="A823456789012"
1105
+ tooltipText="Unique Pupil Number — assigned by the DfE and cannot be changed here"
1106
+ />
1107
+ <Row
1108
+ label="Admission number"
1109
+ value="001847"
1110
+ tooltipText="Set at admission — contact the office to request a change"
1111
+ />
1112
+ {/* No tooltipText — hovering this row shows nothing */}
1113
+ <Row
1114
+ label="Year group"
1115
+ value="Year 9"
1116
+ />
1117
+ </div>
1112
1118
  );
1113
1119
  }
1114
- export default ReadOnlyRow;
1120
+ export default ReadOnlyRows;
1115
1121
  `.trim(),
1116
1122
  },
1117
1123
  },