@arbor-education/design-system.components 0.12.0 → 0.13.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 (177) hide show
  1. package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
  2. package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
  3. package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
  4. package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
  5. package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
  6. package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
  7. package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
  8. package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
  9. package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
  10. package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
  11. package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
  12. package/.gather/gather.yaml +9 -0
  13. package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
  14. package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
  15. package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
  16. package/.gather/skills/analyze-design/meta.md +4 -0
  17. package/{.claude → .gather}/skills/create-page/README.md +5 -0
  18. package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
  19. package/.gather/skills/create-page/meta.md +4 -0
  20. package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
  21. package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
  22. package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
  23. package/.gather/skills/map-legacy/meta.md +4 -0
  24. package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
  25. package/.gather/skills/migrate-page/meta.md +4 -0
  26. package/.gather/skills/write-stories/README.md +157 -0
  27. package/.gather/skills/write-stories/SKILL.md +841 -0
  28. package/.gather/skills/write-stories/meta.md +4 -0
  29. package/.ralph/storybook-upgrade/knowledge.md +308 -0
  30. package/.ralph/storybook-upgrade/prd.json +777 -0
  31. package/.ralph/storybook-upgrade/progress.md +342 -0
  32. package/.storybook/DocsTemplate.tsx +122 -0
  33. package/.storybook/preview.ts +40 -0
  34. package/.stylelintignore +2 -0
  35. package/CHANGELOG.md +12 -0
  36. package/{.claude/component-library.md → component-library.md} +27 -10
  37. package/dist/components/badge/Badge.stories.d.ts +85 -6
  38. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  39. package/dist/components/badge/Badge.stories.js +626 -27
  40. package/dist/components/badge/Badge.stories.js.map +1 -1
  41. package/dist/components/banner/Banner.stories.d.ts +129 -63
  42. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  43. package/dist/components/banner/Banner.stories.js +855 -39
  44. package/dist/components/banner/Banner.stories.js.map +1 -1
  45. package/dist/components/button/Button.stories.d.ts +148 -8
  46. package/dist/components/button/Button.stories.d.ts.map +1 -1
  47. package/dist/components/button/Button.stories.js +1089 -80
  48. package/dist/components/button/Button.stories.js.map +1 -1
  49. package/dist/components/datePicker/DatePicker.d.ts +1 -0
  50. package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
  51. package/dist/components/datePicker/DatePicker.js +2 -2
  52. package/dist/components/datePicker/DatePicker.js.map +1 -1
  53. package/dist/components/datePicker/DatePicker.stories.d.ts +1 -0
  54. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  55. package/dist/components/dot/Dot.stories.d.ts +46 -11
  56. package/dist/components/dot/Dot.stories.d.ts.map +1 -1
  57. package/dist/components/dot/Dot.stories.js +504 -15
  58. package/dist/components/dot/Dot.stories.js.map +1 -1
  59. package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
  60. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  61. package/dist/components/dropdown/Dropdown.stories.js +769 -17
  62. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  63. package/dist/components/formField/FormField.stories.d.ts +95 -35
  64. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  65. package/dist/components/formField/FormField.stories.js +1174 -69
  66. package/dist/components/formField/FormField.stories.js.map +1 -1
  67. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
  68. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  69. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
  70. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  71. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
  72. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
  73. package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
  74. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
  75. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
  76. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  77. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
  78. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  79. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
  80. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  81. package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
  82. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
  83. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
  84. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  85. package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
  86. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  87. package/dist/components/heading/Heading.stories.d.ts +449 -50
  88. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  89. package/dist/components/heading/Heading.stories.js +536 -60
  90. package/dist/components/heading/Heading.stories.js.map +1 -1
  91. package/dist/components/icon/Icon.stories.d.ts +81 -10
  92. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  93. package/dist/components/icon/Icon.stories.js +979 -8
  94. package/dist/components/icon/Icon.stories.js.map +1 -1
  95. package/dist/components/pill/Pill.stories.d.ts +71 -19
  96. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  97. package/dist/components/pill/Pill.stories.js +573 -14
  98. package/dist/components/pill/Pill.stories.js.map +1 -1
  99. package/dist/components/progress/Progress.stories.d.ts +75 -298
  100. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  101. package/dist/components/progress/Progress.stories.js +449 -52
  102. package/dist/components/progress/Progress.stories.js.map +1 -1
  103. package/dist/components/separator/Separator.stories.d.ts +58 -5
  104. package/dist/components/separator/Separator.stories.d.ts.map +1 -1
  105. package/dist/components/separator/Separator.stories.js +443 -4
  106. package/dist/components/separator/Separator.stories.js.map +1 -1
  107. package/dist/components/table/Table.d.ts +7 -0
  108. package/dist/components/table/Table.d.ts.map +1 -1
  109. package/dist/components/table/Table.js +9 -0
  110. package/dist/components/table/Table.js.map +1 -1
  111. package/dist/components/table/Table.stories.d.ts +1 -0
  112. package/dist/components/table/Table.stories.d.ts.map +1 -1
  113. package/dist/components/table/Table.stories.js +87 -0
  114. package/dist/components/table/Table.stories.js.map +1 -1
  115. package/dist/components/table/Table.test.js +49 -1
  116. package/dist/components/table/Table.test.js.map +1 -1
  117. package/dist/components/table/cellEditors/DateCellEditor.d.ts +3 -0
  118. package/dist/components/table/cellEditors/DateCellEditor.d.ts.map +1 -0
  119. package/dist/components/table/cellEditors/DateCellEditor.js +13 -0
  120. package/dist/components/table/cellEditors/DateCellEditor.js.map +1 -0
  121. package/dist/components/table/cellEditors/DateCellEditor.test.d.ts +2 -0
  122. package/dist/components/table/cellEditors/DateCellEditor.test.d.ts.map +1 -0
  123. package/dist/components/table/cellEditors/DateCellEditor.test.js +81 -0
  124. package/dist/components/table/cellEditors/DateCellEditor.test.js.map +1 -0
  125. package/dist/components/tag/Tag.stories.d.ts +116 -5
  126. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  127. package/dist/components/tag/Tag.stories.js +581 -28
  128. package/dist/components/tag/Tag.stories.js.map +1 -1
  129. package/dist/index.css +8 -1
  130. package/dist/index.css.map +1 -1
  131. package/dist/index.d.ts +0 -5
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +0 -5
  134. package/dist/index.js.map +1 -1
  135. package/eslint.config.mts +5 -1
  136. package/package.json +3 -3
  137. package/src/components/badge/Badge.stories.tsx +869 -42
  138. package/src/components/banner/Banner.stories.tsx +1081 -63
  139. package/src/components/button/Button.stories.tsx +1394 -99
  140. package/src/components/datePicker/DatePicker.tsx +3 -0
  141. package/src/components/dot/Dot.stories.tsx +723 -32
  142. package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
  143. package/src/components/formField/FormField.stories.tsx +1522 -105
  144. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
  145. package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
  146. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
  147. package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
  148. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
  149. package/src/components/heading/Heading.stories.tsx +752 -120
  150. package/src/components/icon/Icon.stories.tsx +1446 -12
  151. package/src/components/pill/Pill.stories.tsx +867 -21
  152. package/src/components/progress/Progress.stories.tsx +625 -58
  153. package/src/components/separator/Separator.stories.tsx +730 -8
  154. package/src/components/separator/separator.scss +12 -3
  155. package/src/components/table/Table.stories.tsx +102 -0
  156. package/src/components/table/Table.test.tsx +82 -3
  157. package/src/components/table/Table.tsx +9 -0
  158. package/src/components/table/cellEditors/DateCellEditor.test.tsx +109 -0
  159. package/src/components/table/cellEditors/DateCellEditor.tsx +27 -0
  160. package/src/components/tag/Tag.stories.tsx +755 -53
  161. package/src/index.ts +0 -5
  162. package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
  163. package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
  164. package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
  165. package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
  166. package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
  167. package/.claude/figma-assessment-7154-58899.md +0 -404
  168. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
  169. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
  170. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
  171. package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
  172. package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
  173. /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
  174. /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
  175. /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
  176. /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
  177. /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
@@ -1,30 +1,1464 @@
1
- import type { Meta } from '@storybook/react-vite';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ Controls,
4
+ Heading as DocHeading,
5
+ Markdown,
6
+ Primary as DocPrimary,
7
+ Stories,
8
+ Subtitle,
9
+ Title,
10
+ } from '@storybook/addon-docs/blocks';
11
+ import { Button } from 'Components/button/Button';
2
12
  import { Icon } from './Icon';
3
13
  import { allowedIcons } from './allowedIcons';
4
14
  import { iconSizes } from './types';
5
15
 
6
- const meta: Meta<typeof Icon> = {
16
+ // ---------------------------------------------------------------------------
17
+ // Docs page content
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const DESCRIPTION_INTRO = [
21
+ 'Icon renders a single SVG icon from the Arbor design system icon set — built on the',
22
+ '[Lucide](https://lucide.dev) library with a small set of custom Arbor-specific additions.',
23
+ 'Every icon is named with a kebab-case key and accessed via the `name` prop.',
24
+ ].join('\n');
25
+
26
+ const USAGE_GUIDANCE = [
27
+ '### When to use',
28
+ '',
29
+ '- **Support an action** — a leading `plus` icon before "Add student" makes the button scannable at a glance',
30
+ '- **Indicate status** — `circle-check`, `triangle-alert`, `circle-x` paired with status text communicate severity',
31
+ '- **Reinforce key information** — `graduation-cap` next to a student name, `date` next to a date field',
32
+ '',
33
+ '---',
34
+ '',
35
+ '### When NOT to use',
36
+ '',
37
+ '- **As decoration or filler** — every icon must earn its place. Icons that add no meaning create visual noise',
38
+ ' and slow down scanning.',
39
+ '- **As the sole communicator of critical meaning** — colour-blind users and screen reader users depend on',
40
+ ' text alongside icons. An icon alone is never sufficient for critical information.',
41
+ '',
42
+ '---',
43
+ '',
44
+ '### Design guidance',
45
+ '',
46
+ '#### Three sizes only',
47
+ '',
48
+ 'Arbor uses exactly three icon sizes (Confluence page 2431778909):',
49
+ '',
50
+ '| Size | Token | Use when |',
51
+ '|---|---|---|',
52
+ '| `12` | `--icon-size-xsmall` | Dense UI only — table cells, compact lists, tight badges |',
53
+ '| `16` | `--icon-size-small` | **Default** — almost every icon in the system |',
54
+ '| `24` | `--icon-size-medium` | High-significance only — empty states, hero callouts |',
55
+ '',
56
+ 'The token names (`--icon-size-xsmall`, `--icon-size-small`, `--icon-size-medium`) are consumed by',
57
+ 'other SCSS files (e.g. Checkbox, NumberInput) for sizing adjacent containers. The `Icon` component',
58
+ 'itself takes the numeric value directly (`size={16}`) — but if you need to size a container around',
59
+ 'an icon, reference the token.',
60
+ '',
61
+ '**Sizes do not change across breakpoints** — layout changes; icon sizes do not.',
62
+ '',
63
+ '#### Lucide outline set',
64
+ '',
65
+ 'Arbor uses the Lucide outline (stroke) icon set. Do not mix filled styles unless it is a',
66
+ 'deliberate system-wide decision. The exception is the `check-solid`, `x-solid`, and `favourite-filled`',
67
+ 'variants — these are purpose-built for specific semantic emphasis states.',
68
+ '',
69
+ '#### Icon placement',
70
+ '',
71
+ '- **Icon before label** — the default pattern for action buttons and form labels',
72
+ '- **Trailing icons** — use for action-indicator icons: `chevron-down` (opens menu), `external-link`',
73
+ ' (opens new tab), `arrow-right` (advances a step). These communicate what happens AFTER the action.',
74
+ '- **Icon-only** — only when the meaning is completely unambiguous (e.g. a close button on a modal',
75
+ ' after the user has already opened it) OR when space is severely constrained. Always add an',
76
+ ' accessible label AND a tooltip in icon-only contexts.',
77
+ '',
78
+ '#### `currentColor` is a powerful default',
79
+ '',
80
+ 'The `color` prop defaults to `"currentColor"`, so the icon inherits the text colour of its parent',
81
+ 'element. This is especially useful for icon + label pairings — style the wrapper once and the icon',
82
+ 'tracks the label through state changes (hover, active, disabled) for free.',
83
+ '',
84
+ '```tsx',
85
+ '// Icon inherits the wrapper colour — the icon and label stay in sync.',
86
+ '<span style={{ color: "var(--color-brand-700)" }}>',
87
+ ' <Icon name="pencil" />',
88
+ ' Edit year group',
89
+ '</span>',
90
+ '```',
91
+ '',
92
+ 'Pass `color` explicitly when:',
93
+ '',
94
+ '- The icon carries a **specific semantic meaning** regardless of context — status icons where',
95
+ ' the colour IS the message (info / success / warning / error)',
96
+ '- The icon must stay a fixed brand or accent colour independent of surrounding text',
97
+ '',
98
+ '```tsx',
99
+ '// BAD — hardcoded hex breaks theming and dark mode',
100
+ '<Icon name="circle-check" color="#078427" />',
101
+ '',
102
+ '// GOOD — use a design token so the colour travels with the theme',
103
+ '<Icon name="circle-check" color="var(--color-semantic-success-600)" />',
104
+ '```',
105
+ '',
106
+ '---',
107
+ '',
108
+ '### Named icon aliases worth knowing',
109
+ '',
110
+ '| `name` value | Renders | Notes |',
111
+ '|---|---|---|',
112
+ '| `grab` | GripVertical | Drag handle — always pair with drag-and-drop behaviour |',
113
+ '| `date` | CalendarFold | Preferred alias for calendar/date field icons |',
114
+ '| `staff` | GraduationCap | Same icon as `graduation-cap` — use `staff` for staff contexts |',
115
+ '| `guardians` | Users | Use for parent/guardian relationships |',
116
+ '| `group` | UsersRound | Use for student groups and cohorts |',
117
+ '| `3-dot` | EllipsisVertical | Same icon as `ellipsis-vertical` — canonical Arbor alias for "more actions" menus |',
118
+ '| `loader` | LoaderCircle | Loading spinner glyph. No rotation is applied by the Icon component itself — add a `spin` CSS animation on the parent or via `className` if your design calls for rotation |',
119
+ '| `sorting` | ArrowDownUp | Table column sort toggle |',
120
+ '| `shrink` | Minimize2 | Collapse / exit fullscreen |',
121
+ '',
122
+ '---',
123
+ '',
124
+ '### Custom and solid variants',
125
+ '',
126
+ '| `name` | Description |',
127
+ '|---|---|',
128
+ '| `check-solid` | Filled check mark — for confirmation emphasis (e.g. "saved" state) |',
129
+ '| `x-solid` | Filled X — for rejection/remove emphasis |',
130
+ '| `favourite-filled` | Star with fill — for active/selected favourite state |',
131
+ '| `favourite-outline` | Star outline — for default/unselected favourite state |',
132
+ '| `ask-arbor` | Custom Arbor AI brand icon |',
133
+ '| `google` | Custom Google brand icon |',
134
+ '',
135
+ 'Use outline as the default and filled only for selected/active/confirmation states.',
136
+ 'Never mix filled and outline icons in the same UI context.',
137
+ ].join('\n');
138
+
139
+ const DEVELOPER_NOTES = [
140
+ '### Critical patterns',
141
+ '',
142
+ '#### `aria-hidden` + `sr-only` pattern',
143
+ '',
144
+ 'The SVG always renders with `aria-hidden="true"` (and `role="img"`). This means screen readers',
145
+ 'skip the SVG entirely. See: [Why `aria-hidden` beats `aria-label` on SVGs](https://gomakethings.com/revisting-aria-label-versus-a-visually-hidden-class/)',
146
+ '',
147
+ 'When you pass `screenReaderText`, the component renders a `<span class="sr-only">` immediately',
148
+ 'after the SVG. Screen readers announce the span; they never see the SVG.',
149
+ '',
150
+ '```tsx',
151
+ '// The SVG is hidden from AT. The span is visually hidden but readable by AT.',
152
+ '<Icon name="triangle-alert" screenReaderText="Warning:" />',
153
+ '// Renders: <svg aria-hidden /> <span class="sr-only">Warning:</span>',
154
+ '```',
155
+ '',
156
+ '#### Decorative vs meaningful — when to use `screenReaderText`',
157
+ '',
158
+ '| Situation | Pattern | Why |',
159
+ '|---|---|---|',
160
+ '| Icon next to visible text (`<button>Edit <Icon name="pencil" /></button>`) | Omit `screenReaderText` | Screen reader reads "Edit". Adding SR text causes "Edit pencil" duplication |',
161
+ '| Icon-only button (`<button><Icon name="pencil" /></button>`) | Use `aria-label` on the button, OR `screenReaderText` on Icon | The button needs ONE accessible name |',
162
+ '| Standalone status icon in a summary list | Use `screenReaderText` | No adjacent text; icon must carry its own meaning |',
163
+ '',
164
+ '**The rule:** one accessible name per element. Choose where to put it, but never duplicate it.',
165
+ '',
166
+ '#### Icon-only buttons require BOTH accessible label AND tooltip',
167
+ '',
168
+ 'Per Confluence guidance: icon-only buttons must have an accessible label (via `aria-label` on the',
169
+ 'button or `screenReaderText` on the Icon) **and** a tooltip on hover/focus. The label serves',
170
+ 'keyboard/AT users; the tooltip serves sighted mouse users who may not recognise the icon.',
171
+ '',
172
+ '```tsx',
173
+ '// Pair with TooltipWrapper in production',
174
+ '<TooltipWrapper content="Edit student record">',
175
+ ' <button aria-label="Edit student record">',
176
+ ' <Icon name="pencil" size={16} />',
177
+ ' </button>',
178
+ '</TooltipWrapper>',
179
+ '```',
180
+ '',
181
+ '---',
182
+ '',
183
+ '### Accessibility',
184
+ '',
185
+ '- **SVG always `aria-hidden`** — the SVG element is always hidden from assistive tech regardless of',
186
+ ' whether `screenReaderText` is provided. Never attempt to put an accessible label directly on the SVG.',
187
+ '- **Decorative icons** — omit `screenReaderText`. The icon adds no information beyond the visible text.',
188
+ '- **Meaningful icons** — provide `screenReaderText` OR put `aria-label` on the interactive parent.',
189
+ '- **Status icons** — must be paired with visible text where possible (WCAG 1.4.1: use of colour).',
190
+ ' Do not rely on icon colour alone to communicate success, warning, or error.',
191
+ '- **Icon-only interactions** — must have accessible label + tooltip on hover/focus.',
192
+ '- **Avoid `title` attributes** — Lucide SVGs sometimes include `<title>` elements; Arbor\'s `allowedIcons`',
193
+ ' wrapper suppresses these via `aria-hidden`. Do not rely on SVG `title` for accessible names.',
194
+ '- **"Opens in new tab"** — communicate this in text (`<span class="sr-only">opens in new tab</span>`),',
195
+ ' not only via the `external-link` icon.',
196
+ '',
197
+ '---',
198
+ '',
199
+ '### TypeScript types',
200
+ '',
201
+ '```ts',
202
+ "import { Icon } from '@arbor-education/design-system.components';",
203
+ '',
204
+ 'function MyIcon(props: Icon.Props) { ... }',
205
+ '```',
206
+ '',
207
+ '| Type | Description |',
208
+ '|---|---|',
209
+ '| `Icon.Props` | Full props interface |',
210
+ '| `Icon.Name` | Union of all icon name strings — see argTypes for full list |',
211
+ '| `Icon.Size` | `12 \\| 16 \\| 24` |',
212
+ '| `Icon.CustomProps` | Props for custom SVG icons |',
213
+ ].join('\n');
214
+
215
+ const RELATED_COMPONENTS = [
216
+ '## Related components',
217
+ '',
218
+ '[Button](?path=/docs/components-button--docs) · [Banner](?path=/docs/components-banner--docs) · [Badge](?path=/docs/components-badge--docs) · [Dot](?path=/docs/components-dot--docs) · [Tag](?path=/docs/components-tag--docs)',
219
+ ].join('\n');
220
+
221
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
222
+
223
+ function IconDocsPage() {
224
+ return (
225
+ <>
226
+ <Title />
227
+ <Subtitle />
228
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
229
+ <DocHeading>Interactive example</DocHeading>
230
+ <Markdown>{PROPS_INTRO}</Markdown>
231
+ <DocPrimary />
232
+ <Controls />
233
+ <DocHeading>Usage guidance</DocHeading>
234
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
235
+ <DocHeading>Developer notes</DocHeading>
236
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
237
+ <DocHeading>Examples</DocHeading>
238
+ <Stories title="" />
239
+ <DocHeading>Icon catalogue</DocHeading>
240
+ <IconGalleryTemplate />
241
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
242
+ </>
243
+ );
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Meta
248
+ // ---------------------------------------------------------------------------
249
+
250
+ const meta = {
7
251
  title: 'Components/Icon',
8
252
  component: Icon,
9
- };
10
-
11
- export const Default = {
12
- args: {
13
- name: '3-dot',
14
- size: 16,
253
+ parameters: {
254
+ layout: 'centered',
255
+ docs: {
256
+ page: IconDocsPage,
257
+ },
15
258
  },
259
+ tags: ['autodocs'],
16
260
  argTypes: {
17
261
  name: {
18
262
  control: 'select',
19
- description: 'Icon name',
20
263
  options: Object.keys(allowedIcons),
264
+ description: [
265
+ 'Kebab-case key identifying the icon to render. Required.',
266
+ 'Maps to an entry in `allowedIcons` — TypeScript errors on any unknown value.',
267
+ 'There are currently 106 valid options covering navigation, actions, status,',
268
+ 'content, data, people, and system contexts.',
269
+ 'Commonly used: `pencil` (edit), `trash` (delete), `plus` (add), `x` (close/remove),',
270
+ '`check` (confirm), `3-dot` (more actions), `chevron-down` (expand), `triangle-alert` (warning).',
271
+ ].join(' '),
272
+ table: {
273
+ type: { summary: 'IconName' },
274
+ },
21
275
  },
22
276
  size: {
23
277
  control: 'select',
24
- description: 'Icon size',
25
- options: iconSizes,
278
+ options: [...iconSizes],
279
+ description: [
280
+ 'Pixel size of the icon SVG. Only three values are valid: `12`, `16`, `24`.',
281
+ '`12` — dense UI only (table cells, compact lists).',
282
+ '`16` — default for almost every icon in the system.',
283
+ '`24` — high-significance only (empty states, hero callouts).',
284
+ 'Do not use arbitrary pixel sizes — the three-sizes-only rule is a deliberate design constraint',
285
+ 'that keeps the visual rhythm of the system consistent across all surfaces.',
286
+ ].join(' '),
287
+ table: {
288
+ type: { summary: '12 | 16 | 24' },
289
+ defaultValue: { summary: '16' },
290
+ },
291
+ },
292
+ color: {
293
+ control: 'text',
294
+ description: [
295
+ 'SVG fill/stroke colour. Defaults to `"currentColor"` — the icon inherits the text colour of its parent.',
296
+ 'The default shines for icon + label pairings: style the wrapper once and the icon tracks the label',
297
+ 'through hover, active, and disabled states for free.',
298
+ 'Pass `color` explicitly when the icon carries a specific semantic meaning (status icons where the',
299
+ 'colour IS the message) or must stay a fixed brand/accent colour regardless of context.',
300
+ 'Always use design tokens (CSS custom properties) — never hardcode hex values.',
301
+ ].join(' '),
302
+ table: {
303
+ type: { summary: 'string' },
304
+ defaultValue: { summary: "'currentColor'" },
305
+ },
306
+ },
307
+ screenReaderText: {
308
+ control: 'text',
309
+ description: [
310
+ 'Accessible label for meaningful (non-decorative) icons.',
311
+ 'When provided, renders a `<span class="sr-only">` after the SVG. Screen readers announce the span;',
312
+ 'the SVG is always `aria-hidden="true"` and is never announced.',
313
+ 'Omit for decorative icons that sit next to visible text — adding SR text to decorative icons',
314
+ 'causes duplication (e.g. "Edit pencil" instead of just "Edit").',
315
+ 'For icon-only buttons, put `aria-label` on the button element instead — or use this prop',
316
+ 'and let the button\'s accessible name derive from its content.',
317
+ ].join(' '),
318
+ table: {
319
+ type: { summary: 'string' },
320
+ defaultValue: { summary: 'undefined' },
321
+ },
322
+ },
323
+ className: {
324
+ control: 'text',
325
+ description: [
326
+ 'Additional CSS class names appended to the SVG element.',
327
+ 'The component always prefixes with `ds-icon ds-icon-{name}` — the `className` prop',
328
+ 'appends after those base classes.',
329
+ 'Use sparingly: prefer `color` and `size` props for styling. `className` is mainly',
330
+ 'useful for one-off layout adjustments (e.g. vertical alignment with adjacent text).',
331
+ ].join(' '),
332
+ table: {
333
+ type: { summary: 'string' },
334
+ defaultValue: { summary: 'undefined' },
335
+ },
26
336
  },
27
337
  },
28
- };
338
+ } satisfies Meta<typeof Icon>;
29
339
 
30
340
  export default meta;
341
+ // Use StoryObj<typeof Icon> (not typeof meta) so that render-only stories are not
342
+ // forced to provide args for the required `name` prop.
343
+ type Story = StoryObj<typeof Icon>;
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Helper: attach a per-story description to docs
347
+ // ---------------------------------------------------------------------------
348
+
349
+ const withDescription = (story: Story, description: string): Story => ({
350
+ ...story,
351
+ parameters: {
352
+ ...story.parameters,
353
+ docs: {
354
+ ...story.parameters?.docs,
355
+ description: {
356
+ story: description,
357
+ },
358
+ },
359
+ },
360
+ });
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // Template components for composition stories
364
+ // (Named components avoid hooks-in-callbacks lint issues — the react-hooks ESLint
365
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.)
366
+ // ---------------------------------------------------------------------------
367
+
368
+ const AllSizesTemplate = () => (
369
+ <div
370
+ style={{
371
+ padding: 'var(--spacing-xlarge)',
372
+ display: 'flex',
373
+ gap: 'var(--spacing-xlarge)',
374
+ alignItems: 'flex-end',
375
+ }}
376
+ >
377
+ <div
378
+ style={{
379
+ display: 'flex',
380
+ flexDirection: 'column',
381
+ alignItems: 'center',
382
+ gap: 'var(--spacing-large)',
383
+ }}
384
+ >
385
+ <Icon name="dot" size={12} />
386
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
387
+ 12
388
+ </p>
389
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
390
+ dense UI only
391
+ </p>
392
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
393
+ (tables, compact lists)
394
+ </p>
395
+ </div>
396
+ <div
397
+ style={{
398
+ display: 'flex',
399
+ flexDirection: 'column',
400
+ alignItems: 'center',
401
+ gap: 'var(--spacing-large)',
402
+ }}
403
+ >
404
+ <Icon name="graduation-cap" size={16} />
405
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
406
+ 16
407
+ </p>
408
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
409
+ default (almost everything)
410
+ </p>
411
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
412
+ &nbsp;
413
+ </p>
414
+ </div>
415
+ <div
416
+ style={{
417
+ display: 'flex',
418
+ flexDirection: 'column',
419
+ alignItems: 'center',
420
+ gap: 'var(--spacing-large)',
421
+ }}
422
+ >
423
+ <Icon name="files" size={24} />
424
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
425
+ 24
426
+ </p>
427
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
428
+ high-significance only
429
+ </p>
430
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
431
+ (empty states, hero callouts)
432
+ </p>
433
+ </div>
434
+ </div>
435
+ );
436
+
437
+ const CurrentColorInheritanceTemplate = () => (
438
+ <div
439
+ style={{
440
+ padding: 'var(--spacing-xlarge)',
441
+ display: 'flex',
442
+ flexDirection: 'column',
443
+ gap: 'var(--spacing-large)',
444
+ }}
445
+ >
446
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
447
+ The same icon with no `color` prop — inheriting parent `color` in each case
448
+ </p>
449
+ <div
450
+ style={{
451
+ display: 'flex',
452
+ alignItems: 'center',
453
+ gap: 'var(--spacing-small)',
454
+ color: 'var(--color-grey-900)',
455
+ }}
456
+ >
457
+ <Icon name="info" size={16} />
458
+ <span className="ds-text">Body text colour (--color-grey-900)</span>
459
+ </div>
460
+ <div
461
+ style={{
462
+ display: 'flex',
463
+ alignItems: 'center',
464
+ gap: 'var(--spacing-small)',
465
+ color: 'var(--color-brand-700)',
466
+ }}
467
+ >
468
+ <Icon name="info" size={16} />
469
+ <span className="ds-text">Brand heading colour (--color-brand-700)</span>
470
+ </div>
471
+ <div
472
+ style={{
473
+ display: 'flex',
474
+ alignItems: 'center',
475
+ gap: 'var(--spacing-small)',
476
+ color: 'var(--color-semantic-destructive-600)',
477
+ }}
478
+ >
479
+ <Icon name="info" size={16} />
480
+ <span className="ds-text">Error text colour (--color-semantic-destructive-600)</span>
481
+ </div>
482
+ <div
483
+ style={{
484
+ display: 'flex',
485
+ alignItems: 'center',
486
+ gap: 'var(--spacing-small)',
487
+ color: 'var(--color-grey-400)',
488
+ }}
489
+ >
490
+ <Icon name="info" size={16} />
491
+ <span className="ds-text">Disabled/muted colour (--color-grey-400)</span>
492
+ </div>
493
+ </div>
494
+ );
495
+
496
+ const SemanticColoursTemplate = () => (
497
+ <div
498
+ style={{
499
+ padding: 'var(--spacing-xlarge)',
500
+ display: 'flex',
501
+ flexDirection: 'column',
502
+ gap: 'var(--spacing-large)',
503
+ }}
504
+ >
505
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
506
+ Status icons — the only case where passing `color` explicitly is correct
507
+ </p>
508
+ <div
509
+ style={{
510
+ display: 'flex',
511
+ alignItems: 'center',
512
+ gap: 'var(--spacing-small)',
513
+ color: 'var(--color-semantic-info-600)',
514
+ }}
515
+ >
516
+ <Icon name="circle-alert" size={16} color="var(--color-semantic-info-600)" />
517
+ <span className="ds-text">Heads up</span>
518
+ </div>
519
+ <div
520
+ style={{
521
+ display: 'flex',
522
+ alignItems: 'center',
523
+ gap: 'var(--spacing-small)',
524
+ color: 'var(--color-semantic-success-600)',
525
+ }}
526
+ >
527
+ <Icon name="circle-check" size={16} color="var(--color-semantic-success-600)" />
528
+ <span className="ds-text">Saved successfully</span>
529
+ </div>
530
+ <div
531
+ style={{
532
+ display: 'flex',
533
+ alignItems: 'center',
534
+ gap: 'var(--spacing-small)',
535
+ color: 'var(--color-semantic-warning-600)',
536
+ }}
537
+ >
538
+ <Icon name="triangle-alert" size={16} color="var(--color-semantic-warning-600)" />
539
+ <span className="ds-text">Check before submitting</span>
540
+ </div>
541
+ <div
542
+ style={{
543
+ display: 'flex',
544
+ alignItems: 'center',
545
+ gap: 'var(--spacing-small)',
546
+ color: 'var(--color-semantic-destructive-600)',
547
+ }}
548
+ >
549
+ <Icon name="circle-x" size={16} color="var(--color-semantic-destructive-600)" />
550
+ <span className="ds-text">Save failed</span>
551
+ </div>
552
+ </div>
553
+ );
554
+
555
+ const AccessibleLabelTemplate = () => (
556
+ <div
557
+ style={{
558
+ padding: 'var(--spacing-xlarge)',
559
+ display: 'flex',
560
+ flexDirection: 'column',
561
+ gap: 'var(--spacing-xlarge)',
562
+ }}
563
+ >
564
+ <div
565
+ style={{
566
+ display: 'flex',
567
+ flexDirection: 'column',
568
+ gap: 'var(--spacing-large)',
569
+ }}
570
+ >
571
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-destructive-600)' }}>
572
+ Without screenReaderText — screen reader announces NOTHING (correct for decorative use)
573
+ </p>
574
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
575
+ <Icon name="triangle-alert" size={16} />
576
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
577
+ Decorative — sits next to visible text, no SR text needed
578
+ </span>
579
+ </div>
580
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
581
+ Screen reader: (silence — SVG has aria-hidden)
582
+ </p>
583
+ </div>
584
+ <div
585
+ style={{
586
+ display: 'flex',
587
+ flexDirection: 'column',
588
+ gap: 'var(--spacing-large)',
589
+ }}
590
+ >
591
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-success-600)' }}>
592
+ With screenReaderText="Warning:" — screen reader announces "Warning:" from the sr-only span
593
+ </p>
594
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
595
+ <Icon name="triangle-alert" size={16} screenReaderText="Warning:" />
596
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
597
+ Meaningful — standalone icon, SR text carries the label
598
+ </span>
599
+ </div>
600
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
601
+ Screen reader: "Warning:"
602
+ </p>
603
+ </div>
604
+ </div>
605
+ );
606
+
607
+ const DecorativeVsMeaningfulTemplate = () => (
608
+ <div
609
+ style={{
610
+ padding: 'var(--spacing-xlarge)',
611
+ display: 'flex',
612
+ flexDirection: 'column',
613
+ gap: 'var(--spacing-xlarge)',
614
+ }}
615
+ >
616
+ <div
617
+ style={{
618
+ display: 'flex',
619
+ flexDirection: 'column',
620
+ gap: 'var(--spacing-large)',
621
+ }}
622
+ >
623
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
624
+ Decorative — icon accompanies visible button text. No screenReaderText needed.
625
+ Screen reader announces "Edit" from the button text.
626
+ </p>
627
+ <Button variant="secondary" type="button" iconRightName="pencil">
628
+ Edit
629
+ </Button>
630
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
631
+ Screen reader: "Edit, button"
632
+ </p>
633
+ </div>
634
+ <div
635
+ style={{
636
+ display: 'flex',
637
+ flexDirection: 'column',
638
+ gap: 'var(--spacing-large)',
639
+ }}
640
+ >
641
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
642
+ Meaningful — icon-only button. Accessible name via iconRightScreenReaderText.
643
+ No screenReaderText needed on Icon itself.
644
+ </p>
645
+ <Button
646
+ variant="tertiary"
647
+ type="button"
648
+ iconRightName="pencil"
649
+ iconRightScreenReaderText="Edit student record"
650
+ />
651
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
652
+ Screen reader: "Edit student record, button"
653
+ </p>
654
+ </div>
655
+ </div>
656
+ );
657
+
658
+ const CustomSolidVariantsTemplate = () => (
659
+ <div
660
+ style={{
661
+ padding: 'var(--spacing-xlarge)',
662
+ display: 'flex',
663
+ flexDirection: 'column',
664
+ gap: 'var(--spacing-xlarge)',
665
+ }}
666
+ >
667
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
668
+ Outline (default) vs filled (emphasis) — never mix in the same UI context
669
+ </p>
670
+ <div
671
+ style={{
672
+ display: 'flex',
673
+ flexDirection: 'column',
674
+ gap: 'var(--spacing-large)',
675
+ }}
676
+ >
677
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
678
+ Confirmation emphasis
679
+ </p>
680
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
681
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
682
+ <Icon name="check" size={24} />
683
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>check (outline)</span>
684
+ </div>
685
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
686
+ <Icon name="check-solid" size={24} color="var(--color-semantic-success-600)" />
687
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>check-solid (filled)</span>
688
+ </div>
689
+ </div>
690
+ </div>
691
+ <div
692
+ style={{
693
+ display: 'flex',
694
+ flexDirection: 'column',
695
+ gap: 'var(--spacing-large)',
696
+ }}
697
+ >
698
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
699
+ Rejection/remove emphasis
700
+ </p>
701
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
702
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
703
+ <Icon name="x" size={24} />
704
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>x (outline)</span>
705
+ </div>
706
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
707
+ <Icon name="x-solid" size={24} color="var(--color-semantic-destructive-600)" />
708
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>x-solid (filled)</span>
709
+ </div>
710
+ </div>
711
+ </div>
712
+ <div
713
+ style={{
714
+ display: 'flex',
715
+ flexDirection: 'column',
716
+ gap: 'var(--spacing-large)',
717
+ }}
718
+ >
719
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
720
+ Favourite/bookmark active state
721
+ </p>
722
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
723
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
724
+ <Icon name="favourite-outline" size={24} />
725
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>favourite-outline</span>
726
+ </div>
727
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }}>
728
+ <Icon name="favourite-filled" size={24} color="var(--color-brand-600)" />
729
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>favourite-filled</span>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </div>
734
+ );
735
+
736
+ // ---------------------------------------------------------------------------
737
+ // IconGallery — all icons grouped by semantic category
738
+ // ---------------------------------------------------------------------------
739
+
740
+ type IconCategory = {
741
+ heading: string;
742
+ icons: Array<keyof typeof allowedIcons>;
743
+ };
744
+
745
+ const ICON_CATEGORIES: IconCategory[] = [
746
+ {
747
+ heading: 'Navigation',
748
+ icons: [
749
+ 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
750
+ 'arrow-right-from-line', 'arrow-up-right',
751
+ 'chevron-up', 'chevron-down', 'chevron-left', 'chevron-right',
752
+ 'chevrons-left', 'chevrons-right',
753
+ 'corner-down-left', 'menu',
754
+ 'ellipsis', 'ellipsis-vertical', '3-dot', 'grab',
755
+ ],
756
+ },
757
+ {
758
+ heading: 'Actions',
759
+ icons: [
760
+ 'pencil', 'trash', 'save', 'copy', 'download', 'upload',
761
+ 'share', 'send', 'redo', 'undo', 'plus', 'minus',
762
+ 'circle-plus', 'iteration-ccw', 'switch-camera',
763
+ ],
764
+ },
765
+ {
766
+ heading: 'Status / severity',
767
+ icons: [
768
+ 'info', 'circle-alert', 'triangle-alert',
769
+ 'circle-check', 'circle-check-big', 'circle-x',
770
+ 'circle-help', 'check', 'check-solid', 'x', 'x-solid',
771
+ ],
772
+ },
773
+ {
774
+ heading: 'Content / media',
775
+ icons: [
776
+ 'file', 'files', 'clipboard', 'clipboard-list',
777
+ 'book-open', 'camera', 'mic', 'paperclip',
778
+ 'sheet', 'table',
779
+ 'sparkles', 'party-popper', 'lightbulb',
780
+ 'favourite-outline', 'favourite-filled',
781
+ ],
782
+ },
783
+ {
784
+ heading: 'Charts / data',
785
+ icons: [
786
+ 'chart-column-increasing', 'chart-spline',
787
+ 'trending-up', 'trending-down',
788
+ 'percent', 'circle-percent',
789
+ 'sorting', 'funnel', 'sliders-horizontal', 'list-filter-plus',
790
+ ],
791
+ },
792
+ {
793
+ heading: 'People / org',
794
+ icons: [
795
+ 'user', 'guardians', 'group', 'staff', 'graduation-cap',
796
+ ],
797
+ },
798
+ {
799
+ heading: 'System / state',
800
+ icons: [
801
+ 'eye', 'eye-off', 'lock', 'lock-open', 'loader',
802
+ 'pin', 'flag', 'house', 'history', 'clock-3', 'date',
803
+ 'settings', 'expand', 'shrink', 'link', 'external-link',
804
+ 'log-out', 'search', 'mail', 'phone', 'smartphone',
805
+ 'monitor', 'projector', 'archive',
806
+ ],
807
+ },
808
+ {
809
+ heading: 'Other / misc',
810
+ icons: [
811
+ 'dot', 'layout-list', 'list', 'grid-3x3',
812
+ 'paint-bucket', 'message-square-more',
813
+ ],
814
+ },
815
+ {
816
+ heading: 'Custom / brand',
817
+ icons: ['ask-arbor', 'google'],
818
+ },
819
+ ];
820
+
821
+ const IconGalleryTemplate = () => (
822
+ <div
823
+ style={{
824
+ padding: 'var(--spacing-xlarge)',
825
+ width: '100%',
826
+ maxWidth: '900px',
827
+ }}
828
+ >
829
+ {ICON_CATEGORIES.map(category => (
830
+ <div
831
+ key={category.heading}
832
+ style={{ marginBottom: 'var(--spacing-xxlarge)' }}
833
+ >
834
+ <p
835
+ className="ds-text"
836
+ style={{
837
+ margin: '0 0 var(--spacing-large) 0',
838
+ color: 'var(--color-grey-600)',
839
+ fontWeight: 'var(--font-weight-semi-bold)',
840
+ }}
841
+ >
842
+ {category.heading}
843
+ </p>
844
+ <div
845
+ style={{
846
+ display: 'grid',
847
+ gridTemplateColumns: 'repeat(auto-fill, minmax(7rem, 1fr))',
848
+ gap: 'var(--spacing-small)',
849
+ }}
850
+ >
851
+ {category.icons.map(name => (
852
+ <div
853
+ key={name}
854
+ style={{
855
+ display: 'flex',
856
+ flexDirection: 'column',
857
+ alignItems: 'center',
858
+ gap: 'var(--spacing-xsmall)',
859
+ padding: 'var(--spacing-medium)',
860
+ borderRadius: 'var(--border-radius-small)',
861
+ border: '1px solid var(--color-grey-200)',
862
+ background: 'var(--color-grey-100)',
863
+ }}
864
+ >
865
+ <Icon name={name} size={16} />
866
+ <span
867
+ className="ds-text"
868
+ style={{
869
+ color: 'var(--color-grey-600)',
870
+ wordBreak: 'break-word',
871
+ textAlign: 'center',
872
+ fontSize: 'var(--font-size-1-11)',
873
+ }}
874
+ >
875
+ {name}
876
+ </span>
877
+ </div>
878
+ ))}
879
+ </div>
880
+ </div>
881
+ ))}
882
+ </div>
883
+ );
884
+
885
+ const InButtonContextTemplate = () => (
886
+ <div
887
+ style={{
888
+ padding: 'var(--spacing-xlarge)',
889
+ display: 'flex',
890
+ flexDirection: 'column',
891
+ gap: 'var(--spacing-large)',
892
+ }}
893
+ >
894
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
895
+ Button manages Icon internally — use iconLeftName/iconRightName, not
896
+ {' '}
897
+ {'<Icon>'}
898
+ {' '}
899
+ as a child
900
+ </p>
901
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap', alignItems: 'center' }}>
902
+ <Button variant="primary" type="button" iconLeftName="plus">
903
+ Add student
904
+ </Button>
905
+ <Button
906
+ variant="secondary"
907
+ type="button"
908
+ iconRightName="chevron-down"
909
+ >
910
+ Select year group
911
+ </Button>
912
+ {/* Icon-only tertiary — in production, wrap in TooltipWrapper for hover/focus affordance */}
913
+ <Button
914
+ variant="tertiary"
915
+ type="button"
916
+ iconRightName="3-dot"
917
+ iconRightScreenReaderText="More actions"
918
+ />
919
+ </div>
920
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
921
+ The icon-only tertiary button above should be wrapped in a TooltipWrapper in production
922
+ to provide a hover/focus label for sighted mouse users.
923
+ </p>
924
+ </div>
925
+ );
926
+
927
+ const InStatusIndicatorContextTemplate = () => (
928
+ <div
929
+ style={{
930
+ padding: 'var(--spacing-xlarge)',
931
+ display: 'flex',
932
+ flexDirection: 'column',
933
+ gap: 'var(--spacing-large)',
934
+ }}
935
+ >
936
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
937
+ Bulk action validation summary — icons communicate outcome severity at a glance
938
+ </p>
939
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
940
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
941
+ <Icon
942
+ name="circle-check"
943
+ size={16}
944
+ color="var(--color-semantic-success-600)"
945
+ screenReaderText="Success:"
946
+ />
947
+ <span className="ds-text">3 marksheets saved</span>
948
+ </div>
949
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
950
+ <Icon
951
+ name="triangle-alert"
952
+ size={16}
953
+ color="var(--color-semantic-warning-600)"
954
+ screenReaderText="Warning:"
955
+ />
956
+ <span className="ds-text">2 conflicts to review</span>
957
+ </div>
958
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
959
+ <Icon
960
+ name="circle-x"
961
+ size={16}
962
+ color="var(--color-semantic-destructive-600)"
963
+ screenReaderText="Error:"
964
+ />
965
+ <span className="ds-text">1 submission failed</span>
966
+ </div>
967
+ </div>
968
+ </div>
969
+ );
970
+
971
+ const InEmptyStateContextTemplate = () => (
972
+ <div
973
+ style={{
974
+ padding: 'var(--spacing-xlarge)',
975
+ display: 'flex',
976
+ flexDirection: 'column',
977
+ alignItems: 'center',
978
+ gap: 'var(--spacing-large)',
979
+ textAlign: 'center',
980
+ }}
981
+ >
982
+ <Icon name="files" size={24} color="var(--color-grey-400)" />
983
+ <p className="ds-text" style={{ margin: 0, fontWeight: 'var(--font-weight-semi-bold)' }}>
984
+ No reports yet
985
+ </p>
986
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
987
+ Upload your first report to get started.
988
+ </p>
989
+ </div>
990
+ );
991
+
992
+ // ---------------------------------------------------------------------------
993
+ // Stories
994
+ // ---------------------------------------------------------------------------
995
+
996
+ export const Default: Story = withDescription(
997
+ {
998
+ args: {
999
+ name: '3-dot',
1000
+ size: 16,
1001
+ },
1002
+ render: args => <Icon {...args} />,
1003
+ },
1004
+ 'The interactive canvas story — every prop is wired to the **Controls** panel. Use the controls to explore all 106+ icon names, all three sizes, and the screenReaderText pattern. Tip: try `name="loader"` with `size={24}` to see the large format, or `screenReaderText="Warning"` with `name="triangle-alert"` to explore the sr-only pattern.',
1005
+ );
1006
+
1007
+ export const AllSizes: Story = withDescription(
1008
+ {
1009
+ render: AllSizesTemplate,
1010
+ parameters: {
1011
+ docs: {
1012
+ source: {
1013
+ language: 'tsx',
1014
+ code: `
1015
+ import { Icon } from '@arbor-education/design-system.components';
1016
+
1017
+ function AllSizesExample() {
1018
+ return (
1019
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'flex-end' }}>
1020
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
1021
+ <Icon name="dot" size={12} />
1022
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>12 — dense UI only</p>
1023
+ </div>
1024
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
1025
+ <Icon name="graduation-cap" size={16} />
1026
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>16 — default</p>
1027
+ </div>
1028
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
1029
+ <Icon name="files" size={24} />
1030
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>24 — high-significance only</p>
1031
+ </div>
1032
+ </div>
1033
+ );
1034
+ }
1035
+ export default AllSizesExample;
1036
+ `.trim(),
1037
+ },
1038
+ },
1039
+ },
1040
+ },
1041
+ [
1042
+ 'All three valid sizes side by side with their intended use cases.',
1043
+ '',
1044
+ '**12** (`--icon-size-xsmall`) — dense UI only. Table cells, compact lists, tight inline indicators.',
1045
+ 'Too small for standalone use or touch targets.',
1046
+ '',
1047
+ '**16** (`--icon-size-small`) — the default for almost everything in the system.',
1048
+ 'Balances visual weight against surrounding text at standard body size.',
1049
+ '',
1050
+ '**24** (`--icon-size-medium`) — reserved for high-significance moments: empty states, hero callouts,',
1051
+ 'onboarding prompts. Using 24px icons in dense list or form contexts creates visual imbalance.',
1052
+ '',
1053
+ 'Never use arbitrary sizes outside these three — the size constraint is a deliberate design system rule.',
1054
+ ].join(' '),
1055
+ );
1056
+
1057
+ export const CurrentColorInheritance: Story = withDescription(
1058
+ {
1059
+ render: CurrentColorInheritanceTemplate,
1060
+ parameters: {
1061
+ docs: {
1062
+ source: {
1063
+ language: 'tsx',
1064
+ code: `
1065
+ import { Icon } from '@arbor-education/design-system.components';
1066
+
1067
+ function CurrentColorInheritanceExample() {
1068
+ return (
1069
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
1070
+ {/* Icon inherits the wrapper colour — no color prop needed */}
1071
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-grey-900)' }}>
1072
+ <Icon name="info" size={16} />
1073
+ <span className="ds-text">Body text colour (--color-grey-900)</span>
1074
+ </div>
1075
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-brand-700)' }}>
1076
+ <Icon name="info" size={16} />
1077
+ <span className="ds-text">Brand heading colour (--color-brand-700)</span>
1078
+ </div>
1079
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-semantic-destructive-600)' }}>
1080
+ <Icon name="info" size={16} />
1081
+ <span className="ds-text">Error text colour (--color-semantic-destructive-600)</span>
1082
+ </div>
1083
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-grey-400)' }}>
1084
+ <Icon name="info" size={16} />
1085
+ <span className="ds-text">Disabled/muted colour (--color-grey-400)</span>
1086
+ </div>
1087
+ </div>
1088
+ );
1089
+ }
1090
+ export default CurrentColorInheritanceExample;
1091
+ `.trim(),
1092
+ },
1093
+ },
1094
+ },
1095
+ },
1096
+ [
1097
+ 'The `color` prop defaults to `"currentColor"` — the SVG inherits the `color` CSS property of its',
1098
+ 'parent element. This story shows the **same `<Icon name="info" />` with no `color` prop** inside',
1099
+ 'four different parent containers.',
1100
+ '',
1101
+ 'The icon colour tracks the parent text colour automatically. This is especially handy for icon + label',
1102
+ 'pairings: style the wrapper once and the icon follows the label through hover, active, and disabled',
1103
+ 'states. It also lets you control a whole cluster of icons by setting one CSS token on their shared',
1104
+ 'parent, instead of threading `color` through each `<Icon>`.',
1105
+ ].join(' '),
1106
+ );
1107
+
1108
+ export const SemanticColours: Story = withDescription(
1109
+ {
1110
+ render: SemanticColoursTemplate,
1111
+ parameters: {
1112
+ docs: {
1113
+ source: {
1114
+ language: 'tsx',
1115
+ code: `
1116
+ import { Icon } from '@arbor-education/design-system.components';
1117
+
1118
+ function SemanticColoursExample() {
1119
+ return (
1120
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
1121
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1122
+ <Icon name="circle-alert" size={16} color="var(--color-semantic-info-600)" />
1123
+ <span className="ds-text">Heads up</span>
1124
+ </div>
1125
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1126
+ <Icon name="circle-check" size={16} color="var(--color-semantic-success-600)" />
1127
+ <span className="ds-text">Saved successfully</span>
1128
+ </div>
1129
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1130
+ <Icon name="triangle-alert" size={16} color="var(--color-semantic-warning-600)" />
1131
+ <span className="ds-text">Check before submitting</span>
1132
+ </div>
1133
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1134
+ <Icon name="circle-x" size={16} color="var(--color-semantic-destructive-600)" />
1135
+ <span className="ds-text">Save failed</span>
1136
+ </div>
1137
+ </div>
1138
+ );
1139
+ }
1140
+ export default SemanticColoursExample;
1141
+ `.trim(),
1142
+ },
1143
+ },
1144
+ },
1145
+ },
1146
+ [
1147
+ 'A canonical use case for passing `color` explicitly: status icons that must communicate severity',
1148
+ 'independently of the surrounding text colour. (You can also pass `color` anywhere else it makes',
1149
+ 'sense — for brand accents, fixed-colour service logos, or intentionally muted empty-state glyphs —',
1150
+ 'just always prefer a design token over a hardcoded hex.)',
1151
+ '',
1152
+ 'Each row pairs an icon with a text label using the same semantic colour token. This satisfies',
1153
+ 'WCAG 1.4.1 (use of colour) — the meaning is communicated by both the icon shape AND the colour',
1154
+ 'AND the text label, not by colour alone.',
1155
+ '',
1156
+ 'Tokens used: `--color-semantic-info-600`, `--color-semantic-success-600`,',
1157
+ '`--color-semantic-warning-600`, `--color-semantic-destructive-600`.',
1158
+ ].join(' '),
1159
+ );
1160
+
1161
+ export const AccessibleLabel: Story = withDescription(
1162
+ {
1163
+ render: AccessibleLabelTemplate,
1164
+ parameters: {
1165
+ docs: {
1166
+ source: {
1167
+ language: 'tsx',
1168
+ code: `
1169
+ import { Icon } from '@arbor-education/design-system.components';
1170
+
1171
+ function AccessibleLabelExample() {
1172
+ return (
1173
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', padding: 'var(--spacing-xlarge)' }}>
1174
+ {/* Decorative — no screenReaderText, sits next to visible text */}
1175
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1176
+ <Icon name="triangle-alert" size={16} />
1177
+ <span className="ds-text">Decorative — sits next to visible text, no SR text needed</span>
1178
+ </div>
1179
+ {/* Meaningful — standalone icon, screenReaderText carries the label */}
1180
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1181
+ <Icon name="triangle-alert" size={16} screenReaderText="Warning:" />
1182
+ <span className="ds-text">Meaningful — standalone icon, SR text carries the label</span>
1183
+ </div>
1184
+ </div>
1185
+ );
1186
+ }
1187
+ export default AccessibleLabelExample;
1188
+ `.trim(),
1189
+ },
1190
+ },
1191
+ },
1192
+ },
1193
+ [
1194
+ 'The `aria-hidden` + `sr-only` pattern in practice.',
1195
+ '',
1196
+ 'The SVG **always** has `aria-hidden="true"` — screen readers skip it entirely.',
1197
+ 'When `screenReaderText` is provided, a `<span class="sr-only">` is rendered immediately after',
1198
+ 'the SVG. Screen readers announce the span; they never see the SVG.',
1199
+ '',
1200
+ 'See: [Why aria-hidden beats aria-label on SVGs](https://gomakethings.com/revisting-aria-label-versus-a-visually-hidden-class/)',
1201
+ '',
1202
+ '**Top row** — no `screenReaderText`. The icon is purely decorative in the context of accompanying',
1203
+ 'visible text. Screen reader announces nothing from the icon (which is correct).',
1204
+ '',
1205
+ '**Bottom row** — `screenReaderText="Warning:"` provided. The `<span class="sr-only">` carries',
1206
+ 'the accessible label. Screen reader announces "Warning:".',
1207
+ ].join(' '),
1208
+ );
1209
+
1210
+ export const DecorativeVsMeaningful: Story = withDescription(
1211
+ {
1212
+ render: DecorativeVsMeaningfulTemplate,
1213
+ parameters: {
1214
+ docs: {
1215
+ source: {
1216
+ language: 'tsx',
1217
+ code: `
1218
+ import { Button } from '@arbor-education/design-system.components';
1219
+
1220
+ function DecorativeVsMeaningfulExample() {
1221
+ return (
1222
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', padding: 'var(--spacing-xlarge)' }}>
1223
+ {/* Decorative — icon accompanies visible button text. Screen reader: "Edit, button" */}
1224
+ <Button variant="secondary" type="button" iconRightName="pencil">
1225
+ Edit
1226
+ </Button>
1227
+ {/* Meaningful — icon-only button. iconRightScreenReaderText provides accessible name. */}
1228
+ <Button
1229
+ variant="tertiary"
1230
+ type="button"
1231
+ iconRightName="pencil"
1232
+ iconRightScreenReaderText="Edit student record"
1233
+ />
1234
+ </div>
1235
+ );
1236
+ }
1237
+ export default DecorativeVsMeaningfulExample;
1238
+ `.trim(),
1239
+ },
1240
+ },
1241
+ },
1242
+ },
1243
+ [
1244
+ 'Two composed button examples showing the two main accessibility patterns.',
1245
+ '',
1246
+ '**Decorative (top):** the pencil icon sits next to visible "Edit" text in the button.',
1247
+ 'No `screenReaderText` on the Icon. The button\'s accessible name is "Edit" — from the visible text.',
1248
+ 'Adding SR text here would cause "Edit pencil" duplication.',
1249
+ '',
1250
+ '**Meaningful (bottom):** an icon-only button with no visible text.',
1251
+ '`aria-label="Edit student record"` on the `<button>` element provides the accessible name.',
1252
+ 'No `screenReaderText` needed on the Icon — the button already has one accessible name.',
1253
+ '',
1254
+ 'The rule is: **one accessible name per interactive element**. Choose where to put it, but never',
1255
+ 'duplicate it. For icon-only buttons in production, also add a TooltipWrapper for sighted mouse users.',
1256
+ ].join(' '),
1257
+ );
1258
+
1259
+ export const CustomSolidVariants: Story = withDescription(
1260
+ {
1261
+ render: CustomSolidVariantsTemplate,
1262
+ parameters: {
1263
+ docs: {
1264
+ source: {
1265
+ language: 'tsx',
1266
+ code: `
1267
+ import { Icon } from '@arbor-education/design-system.components';
1268
+
1269
+ function CustomSolidVariantsExample() {
1270
+ return (
1271
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', padding: 'var(--spacing-xlarge)' }}>
1272
+ {/* Confirmation emphasis — outline vs filled */}
1273
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
1274
+ <Icon name="check" size={24} />
1275
+ <Icon name="check-solid" size={24} color="var(--color-semantic-success-600)" />
1276
+ </div>
1277
+ {/* Rejection/remove emphasis — outline vs filled */}
1278
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
1279
+ <Icon name="x" size={24} />
1280
+ <Icon name="x-solid" size={24} color="var(--color-semantic-destructive-600)" />
1281
+ </div>
1282
+ {/* Favourite active state — outline vs filled */}
1283
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
1284
+ <Icon name="favourite-outline" size={24} />
1285
+ <Icon name="favourite-filled" size={24} color="var(--color-brand-600)" />
1286
+ </div>
1287
+ </div>
1288
+ );
1289
+ }
1290
+ export default CustomSolidVariantsExample;
1291
+ `.trim(),
1292
+ },
1293
+ },
1294
+ },
1295
+ },
1296
+ [
1297
+ 'Arbor\'s three filled/solid icon variants alongside their outline equivalents.',
1298
+ '',
1299
+ '`check-solid` — a custom filled check mark. Use for confirmed/completed states where the standard',
1300
+ 'outline `check` feels too subtle. Pair with `--color-semantic-success-600`.',
1301
+ '',
1302
+ '`x-solid` — a custom filled X. Use for rejected/removed/dismissed states.',
1303
+ 'Pair with `--color-semantic-destructive-600`.',
1304
+ '',
1305
+ '`favourite-filled` — the `Star` icon rendered with a fill matching its colour.',
1306
+ 'Use for the active "favourited" state. Toggle with `favourite-outline` for the default state.',
1307
+ '',
1308
+ '**Design rule:** never mix filled and outline icons in the same UI context. If you use',
1309
+ '`check-solid` for one state, use `check` (outline) for the other — don\'t swap to a different icon.',
1310
+ ].join(' '),
1311
+ );
1312
+
1313
+ export const InButtonContext: Story = withDescription(
1314
+ {
1315
+ render: InButtonContextTemplate,
1316
+ parameters: {
1317
+ docs: {
1318
+ source: {
1319
+ language: 'tsx',
1320
+ code: `
1321
+ import { Button } from '@arbor-education/design-system.components';
1322
+
1323
+ function InButtonContextExample() {
1324
+ return (
1325
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap', alignItems: 'center' }}>
1326
+ <Button variant="primary" type="button" iconLeftName="plus">
1327
+ Add student
1328
+ </Button>
1329
+ <Button variant="secondary" type="button" iconRightName="chevron-down">
1330
+ Select year group
1331
+ </Button>
1332
+ {/* Icon-only — wrap in TooltipWrapper in production for hover/focus affordance */}
1333
+ <Button
1334
+ variant="tertiary"
1335
+ type="button"
1336
+ iconRightName="3-dot"
1337
+ iconRightScreenReaderText="More actions"
1338
+ />
1339
+ </div>
1340
+ );
1341
+ }
1342
+ export default InButtonContextExample;
1343
+ `.trim(),
1344
+ },
1345
+ },
1346
+ },
1347
+ },
1348
+ [
1349
+ 'How Icon appears in real Button usage.',
1350
+ '',
1351
+ '**Do not** render `<Icon>` as a child of `<Button>` directly. The `Button` component manages',
1352
+ 'its own icon rendering internally via `iconLeftName` and `iconRightName` props.',
1353
+ '',
1354
+ '`iconLeftName="plus"` — icon before label (the default icon+label pattern for action buttons).',
1355
+ '',
1356
+ '`iconRightName="chevron-down"` — trailing action-indicator icon (communicates "opens a menu").',
1357
+ '',
1358
+ 'Icon-only tertiary with `iconRightScreenReaderText="More actions"` — in production, this should',
1359
+ 'be wrapped in a `TooltipWrapper` to provide a hover/focus label for sighted users per Confluence',
1360
+ 'guidance on icon-only interactive elements.',
1361
+ ].join(' '),
1362
+ );
1363
+
1364
+ export const InStatusIndicatorContext: Story = withDescription(
1365
+ {
1366
+ render: InStatusIndicatorContextTemplate,
1367
+ parameters: {
1368
+ docs: {
1369
+ source: {
1370
+ language: 'tsx',
1371
+ code: `
1372
+ import { Icon } from '@arbor-education/design-system.components';
1373
+
1374
+ function InStatusIndicatorContextExample() {
1375
+ return (
1376
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
1377
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1378
+ <Icon name="circle-check" size={16} color="var(--color-semantic-success-600)" screenReaderText="Success:" />
1379
+ <span className="ds-text">3 marksheets saved</span>
1380
+ </div>
1381
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1382
+ <Icon name="triangle-alert" size={16} color="var(--color-semantic-warning-600)" screenReaderText="Warning:" />
1383
+ <span className="ds-text">2 conflicts to review</span>
1384
+ </div>
1385
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
1386
+ <Icon name="circle-x" size={16} color="var(--color-semantic-destructive-600)" screenReaderText="Error:" />
1387
+ <span className="ds-text">1 submission failed</span>
1388
+ </div>
1389
+ </div>
1390
+ );
1391
+ }
1392
+ export default InStatusIndicatorContextExample;
1393
+ `.trim(),
1394
+ },
1395
+ },
1396
+ },
1397
+ },
1398
+ [
1399
+ 'A realistic validation summary panel — the kind that appears after a bulk marksheet save action.',
1400
+ '',
1401
+ 'Each row uses a status icon with explicit `color` AND `screenReaderText`. The `screenReaderText`',
1402
+ 'is important here: the icons are standalone status indicators, not decorative accompaniments to',
1403
+ 'already-labelled text.',
1404
+ '',
1405
+ 'Screen reader output: "Success: 3 marksheets saved", "Warning: 2 conflicts to review",',
1406
+ '"Error: 1 submission failed". The SR prefix communicates severity before the message.',
1407
+ '',
1408
+ 'Note `--spacing-small` gap between icon and text — tight enough to read as a unit,',
1409
+ 'loose enough not to crowd.',
1410
+ ].join(' '),
1411
+ );
1412
+
1413
+ export const InEmptyStateContext: Story = withDescription(
1414
+ {
1415
+ render: InEmptyStateContextTemplate,
1416
+ parameters: {
1417
+ docs: {
1418
+ source: {
1419
+ language: 'tsx',
1420
+ code: `
1421
+ import { Icon } from '@arbor-education/design-system.components';
1422
+
1423
+ function InEmptyStateContextExample() {
1424
+ return (
1425
+ <div
1426
+ style={{
1427
+ padding: 'var(--spacing-xlarge)',
1428
+ display: 'flex',
1429
+ flexDirection: 'column',
1430
+ alignItems: 'center',
1431
+ gap: 'var(--spacing-large)',
1432
+ textAlign: 'center',
1433
+ }}
1434
+ >
1435
+ <Icon name="files" size={24} color="var(--color-grey-400)" />
1436
+ <p className="ds-text" style={{ margin: 0, fontWeight: 'var(--font-weight-semi-bold)' }}>
1437
+ No reports yet
1438
+ </p>
1439
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
1440
+ Upload your first report to get started.
1441
+ </p>
1442
+ </div>
1443
+ );
1444
+ }
1445
+ export default InEmptyStateContextExample;
1446
+ `.trim(),
1447
+ },
1448
+ },
1449
+ },
1450
+ },
1451
+ [
1452
+ 'The canonical use case for `size={24}` — an empty state layout.',
1453
+ '',
1454
+ 'The `files` icon at 24px is large enough to anchor the empty state visually without',
1455
+ 'being overwhelming. `color="var(--color-grey-400)"` renders it muted — the icon is a',
1456
+ 'supporting visual element, not the primary message.',
1457
+ '',
1458
+ 'This pattern appears throughout Arbor wherever a list, table, or content area has no data.',
1459
+ 'Follow this layout: centred icon (size 24, muted grey) → bold heading → supporting subtext.',
1460
+ '',
1461
+ 'Do not use `size={24}` in list items, table cells, or button icons — reserve it for',
1462
+ 'full-area statements like this one.',
1463
+ ].join(' '),
1464
+ );