@arbor-education/design-system.components 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
  2. package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
  3. package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
  4. package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
  5. package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
  6. package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
  7. package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
  8. package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
  9. package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
  10. package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
  11. package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
  12. package/.gather/gather.yaml +9 -0
  13. package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
  14. package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
  15. package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
  16. package/.gather/skills/analyze-design/meta.md +4 -0
  17. package/{.claude → .gather}/skills/create-page/README.md +5 -0
  18. package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
  19. package/.gather/skills/create-page/meta.md +4 -0
  20. package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
  21. package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
  22. package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
  23. package/.gather/skills/map-legacy/meta.md +4 -0
  24. package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
  25. package/.gather/skills/migrate-page/meta.md +4 -0
  26. package/.gather/skills/write-stories/README.md +157 -0
  27. package/.gather/skills/write-stories/SKILL.md +841 -0
  28. package/.gather/skills/write-stories/meta.md +4 -0
  29. package/.ralph/storybook-upgrade/knowledge.md +308 -0
  30. package/.ralph/storybook-upgrade/prd.json +777 -0
  31. package/.ralph/storybook-upgrade/progress.md +342 -0
  32. package/.storybook/DocsTemplate.tsx +122 -0
  33. package/.storybook/preview.ts +40 -0
  34. package/.stylelintignore +2 -0
  35. package/CHANGELOG.md +14 -0
  36. package/{.claude/component-library.md → component-library.md} +27 -10
  37. package/dist/components/articleCard/ArticleCard.d.ts +30 -0
  38. package/dist/components/articleCard/ArticleCard.d.ts.map +1 -0
  39. package/dist/components/articleCard/ArticleCard.js +24 -0
  40. package/dist/components/articleCard/ArticleCard.js.map +1 -0
  41. package/dist/components/articleCard/ArticleCard.stories.d.ts +18 -0
  42. package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -0
  43. package/dist/components/articleCard/ArticleCard.stories.js +112 -0
  44. package/dist/components/articleCard/ArticleCard.stories.js.map +1 -0
  45. package/dist/components/articleCard/ArticleCard.test.d.ts +2 -0
  46. package/dist/components/articleCard/ArticleCard.test.d.ts.map +1 -0
  47. package/dist/components/articleCard/ArticleCard.test.js +49 -0
  48. package/dist/components/articleCard/ArticleCard.test.js.map +1 -0
  49. package/dist/components/badge/Badge.stories.d.ts +85 -6
  50. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  51. package/dist/components/badge/Badge.stories.js +626 -27
  52. package/dist/components/badge/Badge.stories.js.map +1 -1
  53. package/dist/components/banner/Banner.stories.d.ts +129 -63
  54. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  55. package/dist/components/banner/Banner.stories.js +855 -39
  56. package/dist/components/banner/Banner.stories.js.map +1 -1
  57. package/dist/components/button/Button.stories.d.ts +148 -8
  58. package/dist/components/button/Button.stories.d.ts.map +1 -1
  59. package/dist/components/button/Button.stories.js +1089 -80
  60. package/dist/components/button/Button.stories.js.map +1 -1
  61. package/dist/components/card/Card.d.ts +41 -12
  62. package/dist/components/card/Card.d.ts.map +1 -1
  63. package/dist/components/card/Card.js +46 -17
  64. package/dist/components/card/Card.js.map +1 -1
  65. package/dist/components/card/Card.stories.d.ts +9 -84
  66. package/dist/components/card/Card.stories.d.ts.map +1 -1
  67. package/dist/components/card/Card.stories.js +15 -73
  68. package/dist/components/card/Card.stories.js.map +1 -1
  69. package/dist/components/card/Card.test.js +50 -152
  70. package/dist/components/card/Card.test.js.map +1 -1
  71. package/dist/components/dot/Dot.stories.d.ts +46 -11
  72. package/dist/components/dot/Dot.stories.d.ts.map +1 -1
  73. package/dist/components/dot/Dot.stories.js +504 -15
  74. package/dist/components/dot/Dot.stories.js.map +1 -1
  75. package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
  76. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  77. package/dist/components/dropdown/Dropdown.stories.js +769 -17
  78. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  79. package/dist/components/formField/FormField.stories.d.ts +95 -35
  80. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  81. package/dist/components/formField/FormField.stories.js +1174 -69
  82. package/dist/components/formField/FormField.stories.js.map +1 -1
  83. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
  84. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  85. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
  86. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  87. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
  88. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
  89. package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
  90. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
  91. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
  92. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  93. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
  94. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  95. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
  96. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  97. package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
  98. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
  99. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
  100. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  101. package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
  102. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  103. package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
  104. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
  105. package/dist/components/heading/Heading.stories.d.ts +449 -50
  106. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  107. package/dist/components/heading/Heading.stories.js +536 -60
  108. package/dist/components/heading/Heading.stories.js.map +1 -1
  109. package/dist/components/icoText/IcoText.d.ts +37 -0
  110. package/dist/components/icoText/IcoText.d.ts.map +1 -0
  111. package/dist/components/icoText/IcoText.js +29 -0
  112. package/dist/components/icoText/IcoText.js.map +1 -0
  113. package/dist/components/icoText/IcoText.stories.d.ts +34 -0
  114. package/dist/components/icoText/IcoText.stories.d.ts.map +1 -0
  115. package/dist/components/icoText/IcoText.stories.js +24 -0
  116. package/dist/components/icoText/IcoText.stories.js.map +1 -0
  117. package/dist/components/icoText/IcoText.test.d.ts +2 -0
  118. package/dist/components/icoText/IcoText.test.d.ts.map +1 -0
  119. package/dist/components/icoText/IcoText.test.js +27 -0
  120. package/dist/components/icoText/IcoText.test.js.map +1 -0
  121. package/dist/components/icon/Icon.stories.d.ts +81 -10
  122. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  123. package/dist/components/icon/Icon.stories.js +979 -8
  124. package/dist/components/icon/Icon.stories.js.map +1 -1
  125. package/dist/components/kpiCard/KPICard.d.ts +13 -0
  126. package/dist/components/kpiCard/KPICard.d.ts.map +1 -0
  127. package/dist/components/kpiCard/KPICard.js +8 -0
  128. package/dist/components/kpiCard/KPICard.js.map +1 -0
  129. package/dist/components/kpiCard/KPICard.stories.d.ts +9 -0
  130. package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -0
  131. package/dist/components/kpiCard/KPICard.stories.js +18 -0
  132. package/dist/components/kpiCard/KPICard.stories.js.map +1 -0
  133. package/dist/components/kpiCard/KPICard.test.d.ts +2 -0
  134. package/dist/components/kpiCard/KPICard.test.d.ts.map +1 -0
  135. package/dist/components/kpiCard/KPICard.test.js +37 -0
  136. package/dist/components/kpiCard/KPICard.test.js.map +1 -0
  137. package/dist/components/kvpList/KVPList.d.ts +34 -0
  138. package/dist/components/kvpList/KVPList.d.ts.map +1 -0
  139. package/dist/components/kvpList/KVPList.js +20 -0
  140. package/dist/components/kvpList/KVPList.js.map +1 -0
  141. package/dist/components/kvpList/KVPList.stories.d.ts +27 -0
  142. package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -0
  143. package/dist/components/kvpList/KVPList.stories.js +18 -0
  144. package/dist/components/kvpList/KVPList.stories.js.map +1 -0
  145. package/dist/components/kvpList/KVPList.test.d.ts +2 -0
  146. package/dist/components/kvpList/KVPList.test.d.ts.map +1 -0
  147. package/dist/components/kvpList/KVPList.test.js +29 -0
  148. package/dist/components/kvpList/KVPList.test.js.map +1 -0
  149. package/dist/components/pill/Pill.stories.d.ts +71 -19
  150. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  151. package/dist/components/pill/Pill.stories.js +573 -14
  152. package/dist/components/pill/Pill.stories.js.map +1 -1
  153. package/dist/components/progress/Progress.stories.d.ts +75 -298
  154. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  155. package/dist/components/progress/Progress.stories.js +449 -52
  156. package/dist/components/progress/Progress.stories.js.map +1 -1
  157. package/dist/components/separator/Separator.stories.d.ts +58 -5
  158. package/dist/components/separator/Separator.stories.d.ts.map +1 -1
  159. package/dist/components/separator/Separator.stories.js +443 -4
  160. package/dist/components/separator/Separator.stories.js.map +1 -1
  161. package/dist/components/singleUser/SingleUser.d.ts +1 -1
  162. package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
  163. package/dist/components/tag/Tag.stories.d.ts +116 -5
  164. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  165. package/dist/components/tag/Tag.stories.js +581 -28
  166. package/dist/components/tag/Tag.stories.js.map +1 -1
  167. package/dist/index.css +194 -23
  168. package/dist/index.css.map +1 -1
  169. package/dist/index.d.ts +13 -4
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +12 -3
  172. package/dist/index.js.map +1 -1
  173. package/eslint.config.mts +5 -1
  174. package/package.json +3 -3
  175. package/src/components/articleCard/ArticleCard.stories.tsx +132 -0
  176. package/src/components/articleCard/ArticleCard.test.tsx +121 -0
  177. package/src/components/articleCard/ArticleCard.tsx +100 -0
  178. package/src/components/articleCard/articleCard.scss +39 -0
  179. package/src/components/badge/Badge.stories.tsx +869 -42
  180. package/src/components/banner/Banner.stories.tsx +1081 -63
  181. package/src/components/button/Button.stories.tsx +1394 -99
  182. package/src/components/card/Card.stories.tsx +35 -79
  183. package/src/components/card/Card.test.tsx +72 -190
  184. package/src/components/card/Card.tsx +117 -58
  185. package/src/components/card/card.scss +18 -31
  186. package/src/components/dot/Dot.stories.tsx +723 -32
  187. package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
  188. package/src/components/formField/FormField.stories.tsx +1522 -105
  189. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
  190. package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
  191. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
  192. package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
  193. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
  194. package/src/components/heading/Heading.stories.tsx +752 -120
  195. package/src/components/icoText/IcoText.stories.tsx +47 -0
  196. package/src/components/icoText/IcoText.test.tsx +41 -0
  197. package/src/components/icoText/IcoText.tsx +93 -0
  198. package/src/components/icoText/icoText.scss +34 -0
  199. package/src/components/icon/Icon.stories.tsx +1446 -12
  200. package/src/components/kpiCard/KPICard.stories.tsx +47 -0
  201. package/src/components/kpiCard/KPICard.test.tsx +60 -0
  202. package/src/components/kpiCard/KPICard.tsx +45 -0
  203. package/src/components/kpiCard/kpiCard.scss +35 -0
  204. package/src/components/kvpList/KVPList.stories.tsx +51 -0
  205. package/src/components/kvpList/KVPList.test.tsx +66 -0
  206. package/src/components/kvpList/KVPList.tsx +109 -0
  207. package/src/components/kvpList/kvpList.scss +64 -0
  208. package/src/components/pill/Pill.stories.tsx +867 -21
  209. package/src/components/progress/Progress.stories.tsx +625 -58
  210. package/src/components/separator/Separator.stories.tsx +730 -8
  211. package/src/components/separator/separator.scss +12 -3
  212. package/src/components/tag/Tag.stories.tsx +755 -53
  213. package/src/index.scss +4 -0
  214. package/src/index.ts +13 -4
  215. package/src/tokens.scss +6 -0
  216. package/tokens/json/Arbor.json +30 -0
  217. package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
  218. package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
  219. package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
  220. package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
  221. package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
  222. package/.claude/figma-assessment-7154-58899.md +0 -404
  223. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
  224. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
  225. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
  226. package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
  227. package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
  228. /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
  229. /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
  230. /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
  231. /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
  232. /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
@@ -1,27 +1,998 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
3
+ import { Button } from '../button/Button';
1
4
  import { Icon } from './Icon';
2
5
  import { allowedIcons } from './allowedIcons';
3
6
  import { iconSizes } from './types';
7
+ // ---------------------------------------------------------------------------
8
+ // Docs page content
9
+ // ---------------------------------------------------------------------------
10
+ const DESCRIPTION_INTRO = [
11
+ 'Icon renders a single SVG icon from the Arbor design system icon set — built on the',
12
+ '[Lucide](https://lucide.dev) library with a small set of custom Arbor-specific additions.',
13
+ 'Every icon is named with a kebab-case key and accessed via the `name` prop.',
14
+ ].join('\n');
15
+ const USAGE_GUIDANCE = [
16
+ '### When to use',
17
+ '',
18
+ '- **Support an action** — a leading `plus` icon before "Add student" makes the button scannable at a glance',
19
+ '- **Indicate status** — `circle-check`, `triangle-alert`, `circle-x` paired with status text communicate severity',
20
+ '- **Reinforce key information** — `graduation-cap` next to a student name, `date` next to a date field',
21
+ '',
22
+ '---',
23
+ '',
24
+ '### When NOT to use',
25
+ '',
26
+ '- **As decoration or filler** — every icon must earn its place. Icons that add no meaning create visual noise',
27
+ ' and slow down scanning.',
28
+ '- **As the sole communicator of critical meaning** — colour-blind users and screen reader users depend on',
29
+ ' text alongside icons. An icon alone is never sufficient for critical information.',
30
+ '',
31
+ '---',
32
+ '',
33
+ '### Design guidance',
34
+ '',
35
+ '#### Three sizes only',
36
+ '',
37
+ 'Arbor uses exactly three icon sizes (Confluence page 2431778909):',
38
+ '',
39
+ '| Size | Token | Use when |',
40
+ '|---|---|---|',
41
+ '| `12` | `--icon-size-xsmall` | Dense UI only — table cells, compact lists, tight badges |',
42
+ '| `16` | `--icon-size-small` | **Default** — almost every icon in the system |',
43
+ '| `24` | `--icon-size-medium` | High-significance only — empty states, hero callouts |',
44
+ '',
45
+ 'The token names (`--icon-size-xsmall`, `--icon-size-small`, `--icon-size-medium`) are consumed by',
46
+ 'other SCSS files (e.g. Checkbox, NumberInput) for sizing adjacent containers. The `Icon` component',
47
+ 'itself takes the numeric value directly (`size={16}`) — but if you need to size a container around',
48
+ 'an icon, reference the token.',
49
+ '',
50
+ '**Sizes do not change across breakpoints** — layout changes; icon sizes do not.',
51
+ '',
52
+ '#### Lucide outline set',
53
+ '',
54
+ 'Arbor uses the Lucide outline (stroke) icon set. Do not mix filled styles unless it is a',
55
+ 'deliberate system-wide decision. The exception is the `check-solid`, `x-solid`, and `favourite-filled`',
56
+ 'variants — these are purpose-built for specific semantic emphasis states.',
57
+ '',
58
+ '#### Icon placement',
59
+ '',
60
+ '- **Icon before label** — the default pattern for action buttons and form labels',
61
+ '- **Trailing icons** — use for action-indicator icons: `chevron-down` (opens menu), `external-link`',
62
+ ' (opens new tab), `arrow-right` (advances a step). These communicate what happens AFTER the action.',
63
+ '- **Icon-only** — only when the meaning is completely unambiguous (e.g. a close button on a modal',
64
+ ' after the user has already opened it) OR when space is severely constrained. Always add an',
65
+ ' accessible label AND a tooltip in icon-only contexts.',
66
+ '',
67
+ '#### `currentColor` is a powerful default',
68
+ '',
69
+ 'The `color` prop defaults to `"currentColor"`, so the icon inherits the text colour of its parent',
70
+ 'element. This is especially useful for icon + label pairings — style the wrapper once and the icon',
71
+ 'tracks the label through state changes (hover, active, disabled) for free.',
72
+ '',
73
+ '```tsx',
74
+ '// Icon inherits the wrapper colour — the icon and label stay in sync.',
75
+ '<span style={{ color: "var(--color-brand-700)" }}>',
76
+ ' <Icon name="pencil" />',
77
+ ' Edit year group',
78
+ '</span>',
79
+ '```',
80
+ '',
81
+ 'Pass `color` explicitly when:',
82
+ '',
83
+ '- The icon carries a **specific semantic meaning** regardless of context — status icons where',
84
+ ' the colour IS the message (info / success / warning / error)',
85
+ '- The icon must stay a fixed brand or accent colour independent of surrounding text',
86
+ '',
87
+ '```tsx',
88
+ '// BAD — hardcoded hex breaks theming and dark mode',
89
+ '<Icon name="circle-check" color="#078427" />',
90
+ '',
91
+ '// GOOD — use a design token so the colour travels with the theme',
92
+ '<Icon name="circle-check" color="var(--color-semantic-success-600)" />',
93
+ '```',
94
+ '',
95
+ '---',
96
+ '',
97
+ '### Named icon aliases worth knowing',
98
+ '',
99
+ '| `name` value | Renders | Notes |',
100
+ '|---|---|---|',
101
+ '| `grab` | GripVertical | Drag handle — always pair with drag-and-drop behaviour |',
102
+ '| `date` | CalendarFold | Preferred alias for calendar/date field icons |',
103
+ '| `staff` | GraduationCap | Same icon as `graduation-cap` — use `staff` for staff contexts |',
104
+ '| `guardians` | Users | Use for parent/guardian relationships |',
105
+ '| `group` | UsersRound | Use for student groups and cohorts |',
106
+ '| `3-dot` | EllipsisVertical | Same icon as `ellipsis-vertical` — canonical Arbor alias for "more actions" menus |',
107
+ '| `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 |',
108
+ '| `sorting` | ArrowDownUp | Table column sort toggle |',
109
+ '| `shrink` | Minimize2 | Collapse / exit fullscreen |',
110
+ '',
111
+ '---',
112
+ '',
113
+ '### Custom and solid variants',
114
+ '',
115
+ '| `name` | Description |',
116
+ '|---|---|',
117
+ '| `check-solid` | Filled check mark — for confirmation emphasis (e.g. "saved" state) |',
118
+ '| `x-solid` | Filled X — for rejection/remove emphasis |',
119
+ '| `favourite-filled` | Star with fill — for active/selected favourite state |',
120
+ '| `favourite-outline` | Star outline — for default/unselected favourite state |',
121
+ '| `ask-arbor` | Custom Arbor AI brand icon |',
122
+ '| `google` | Custom Google brand icon |',
123
+ '',
124
+ 'Use outline as the default and filled only for selected/active/confirmation states.',
125
+ 'Never mix filled and outline icons in the same UI context.',
126
+ ].join('\n');
127
+ const DEVELOPER_NOTES = [
128
+ '### Critical patterns',
129
+ '',
130
+ '#### `aria-hidden` + `sr-only` pattern',
131
+ '',
132
+ 'The SVG always renders with `aria-hidden="true"` (and `role="img"`). This means screen readers',
133
+ 'skip the SVG entirely. See: [Why `aria-hidden` beats `aria-label` on SVGs](https://gomakethings.com/revisting-aria-label-versus-a-visually-hidden-class/)',
134
+ '',
135
+ 'When you pass `screenReaderText`, the component renders a `<span class="sr-only">` immediately',
136
+ 'after the SVG. Screen readers announce the span; they never see the SVG.',
137
+ '',
138
+ '```tsx',
139
+ '// The SVG is hidden from AT. The span is visually hidden but readable by AT.',
140
+ '<Icon name="triangle-alert" screenReaderText="Warning:" />',
141
+ '// Renders: <svg aria-hidden /> <span class="sr-only">Warning:</span>',
142
+ '```',
143
+ '',
144
+ '#### Decorative vs meaningful — when to use `screenReaderText`',
145
+ '',
146
+ '| Situation | Pattern | Why |',
147
+ '|---|---|---|',
148
+ '| Icon next to visible text (`<button>Edit <Icon name="pencil" /></button>`) | Omit `screenReaderText` | Screen reader reads "Edit". Adding SR text causes "Edit pencil" duplication |',
149
+ '| Icon-only button (`<button><Icon name="pencil" /></button>`) | Use `aria-label` on the button, OR `screenReaderText` on Icon | The button needs ONE accessible name |',
150
+ '| Standalone status icon in a summary list | Use `screenReaderText` | No adjacent text; icon must carry its own meaning |',
151
+ '',
152
+ '**The rule:** one accessible name per element. Choose where to put it, but never duplicate it.',
153
+ '',
154
+ '#### Icon-only buttons require BOTH accessible label AND tooltip',
155
+ '',
156
+ 'Per Confluence guidance: icon-only buttons must have an accessible label (via `aria-label` on the',
157
+ 'button or `screenReaderText` on the Icon) **and** a tooltip on hover/focus. The label serves',
158
+ 'keyboard/AT users; the tooltip serves sighted mouse users who may not recognise the icon.',
159
+ '',
160
+ '```tsx',
161
+ '// Pair with TooltipWrapper in production',
162
+ '<TooltipWrapper content="Edit student record">',
163
+ ' <button aria-label="Edit student record">',
164
+ ' <Icon name="pencil" size={16} />',
165
+ ' </button>',
166
+ '</TooltipWrapper>',
167
+ '```',
168
+ '',
169
+ '---',
170
+ '',
171
+ '### Accessibility',
172
+ '',
173
+ '- **SVG always `aria-hidden`** — the SVG element is always hidden from assistive tech regardless of',
174
+ ' whether `screenReaderText` is provided. Never attempt to put an accessible label directly on the SVG.',
175
+ '- **Decorative icons** — omit `screenReaderText`. The icon adds no information beyond the visible text.',
176
+ '- **Meaningful icons** — provide `screenReaderText` OR put `aria-label` on the interactive parent.',
177
+ '- **Status icons** — must be paired with visible text where possible (WCAG 1.4.1: use of colour).',
178
+ ' Do not rely on icon colour alone to communicate success, warning, or error.',
179
+ '- **Icon-only interactions** — must have accessible label + tooltip on hover/focus.',
180
+ '- **Avoid `title` attributes** — Lucide SVGs sometimes include `<title>` elements; Arbor\'s `allowedIcons`',
181
+ ' wrapper suppresses these via `aria-hidden`. Do not rely on SVG `title` for accessible names.',
182
+ '- **"Opens in new tab"** — communicate this in text (`<span class="sr-only">opens in new tab</span>`),',
183
+ ' not only via the `external-link` icon.',
184
+ '',
185
+ '---',
186
+ '',
187
+ '### TypeScript types',
188
+ '',
189
+ '```ts',
190
+ "import { Icon } from '@arbor-education/design-system.components';",
191
+ '',
192
+ 'function MyIcon(props: Icon.Props) { ... }',
193
+ '```',
194
+ '',
195
+ '| Type | Description |',
196
+ '|---|---|',
197
+ '| `Icon.Props` | Full props interface |',
198
+ '| `Icon.Name` | Union of all icon name strings — see argTypes for full list |',
199
+ '| `Icon.Size` | `12 \\| 16 \\| 24` |',
200
+ '| `Icon.CustomProps` | Props for custom SVG icons |',
201
+ ].join('\n');
202
+ const RELATED_COMPONENTS = [
203
+ '## Related components',
204
+ '',
205
+ '[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)',
206
+ ].join('\n');
207
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
208
+ function IconDocsPage() {
209
+ return (_jsxs(_Fragment, { children: [_jsx(Title, {}), _jsx(Subtitle, {}), _jsx(Markdown, { children: DESCRIPTION_INTRO }), _jsx(DocHeading, { children: "Interactive example" }), _jsx(Markdown, { children: PROPS_INTRO }), _jsx(DocPrimary, {}), _jsx(Controls, {}), _jsx(DocHeading, { children: "Usage guidance" }), _jsx(Markdown, { children: USAGE_GUIDANCE }), _jsx(DocHeading, { children: "Developer notes" }), _jsx(Markdown, { children: DEVELOPER_NOTES }), _jsx(DocHeading, { children: "Examples" }), _jsx(Stories, { title: "" }), _jsx(DocHeading, { children: "Icon catalogue" }), _jsx(IconGalleryTemplate, {}), _jsx(Markdown, { children: RELATED_COMPONENTS })] }));
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Meta
213
+ // ---------------------------------------------------------------------------
4
214
  const meta = {
5
215
  title: 'Components/Icon',
6
216
  component: Icon,
7
- };
8
- export const Default = {
9
- args: {
10
- name: '3-dot',
11
- size: 16,
217
+ parameters: {
218
+ layout: 'centered',
219
+ docs: {
220
+ page: IconDocsPage,
221
+ },
12
222
  },
223
+ tags: ['autodocs'],
13
224
  argTypes: {
14
225
  name: {
15
226
  control: 'select',
16
- description: 'Icon name',
17
227
  options: Object.keys(allowedIcons),
228
+ description: [
229
+ 'Kebab-case key identifying the icon to render. Required.',
230
+ 'Maps to an entry in `allowedIcons` — TypeScript errors on any unknown value.',
231
+ 'There are currently 106 valid options covering navigation, actions, status,',
232
+ 'content, data, people, and system contexts.',
233
+ 'Commonly used: `pencil` (edit), `trash` (delete), `plus` (add), `x` (close/remove),',
234
+ '`check` (confirm), `3-dot` (more actions), `chevron-down` (expand), `triangle-alert` (warning).',
235
+ ].join(' '),
236
+ table: {
237
+ type: { summary: 'IconName' },
238
+ },
18
239
  },
19
240
  size: {
20
241
  control: 'select',
21
- description: 'Icon size',
22
- options: iconSizes,
242
+ options: [...iconSizes],
243
+ description: [
244
+ 'Pixel size of the icon SVG. Only three values are valid: `12`, `16`, `24`.',
245
+ '`12` — dense UI only (table cells, compact lists).',
246
+ '`16` — default for almost every icon in the system.',
247
+ '`24` — high-significance only (empty states, hero callouts).',
248
+ 'Do not use arbitrary pixel sizes — the three-sizes-only rule is a deliberate design constraint',
249
+ 'that keeps the visual rhythm of the system consistent across all surfaces.',
250
+ ].join(' '),
251
+ table: {
252
+ type: { summary: '12 | 16 | 24' },
253
+ defaultValue: { summary: '16' },
254
+ },
255
+ },
256
+ color: {
257
+ control: 'text',
258
+ description: [
259
+ 'SVG fill/stroke colour. Defaults to `"currentColor"` — the icon inherits the text colour of its parent.',
260
+ 'The default shines for icon + label pairings: style the wrapper once and the icon tracks the label',
261
+ 'through hover, active, and disabled states for free.',
262
+ 'Pass `color` explicitly when the icon carries a specific semantic meaning (status icons where the',
263
+ 'colour IS the message) or must stay a fixed brand/accent colour regardless of context.',
264
+ 'Always use design tokens (CSS custom properties) — never hardcode hex values.',
265
+ ].join(' '),
266
+ table: {
267
+ type: { summary: 'string' },
268
+ defaultValue: { summary: "'currentColor'" },
269
+ },
270
+ },
271
+ screenReaderText: {
272
+ control: 'text',
273
+ description: [
274
+ 'Accessible label for meaningful (non-decorative) icons.',
275
+ 'When provided, renders a `<span class="sr-only">` after the SVG. Screen readers announce the span;',
276
+ 'the SVG is always `aria-hidden="true"` and is never announced.',
277
+ 'Omit for decorative icons that sit next to visible text — adding SR text to decorative icons',
278
+ 'causes duplication (e.g. "Edit pencil" instead of just "Edit").',
279
+ 'For icon-only buttons, put `aria-label` on the button element instead — or use this prop',
280
+ 'and let the button\'s accessible name derive from its content.',
281
+ ].join(' '),
282
+ table: {
283
+ type: { summary: 'string' },
284
+ defaultValue: { summary: 'undefined' },
285
+ },
286
+ },
287
+ className: {
288
+ control: 'text',
289
+ description: [
290
+ 'Additional CSS class names appended to the SVG element.',
291
+ 'The component always prefixes with `ds-icon ds-icon-{name}` — the `className` prop',
292
+ 'appends after those base classes.',
293
+ 'Use sparingly: prefer `color` and `size` props for styling. `className` is mainly',
294
+ 'useful for one-off layout adjustments (e.g. vertical alignment with adjacent text).',
295
+ ].join(' '),
296
+ table: {
297
+ type: { summary: 'string' },
298
+ defaultValue: { summary: 'undefined' },
299
+ },
23
300
  },
24
301
  },
25
302
  };
26
303
  export default meta;
304
+ // ---------------------------------------------------------------------------
305
+ // Helper: attach a per-story description to docs
306
+ // ---------------------------------------------------------------------------
307
+ const withDescription = (story, description) => ({
308
+ ...story,
309
+ parameters: {
310
+ ...story.parameters,
311
+ docs: {
312
+ ...story.parameters?.docs,
313
+ description: {
314
+ story: description,
315
+ },
316
+ },
317
+ },
318
+ });
319
+ // ---------------------------------------------------------------------------
320
+ // Template components for composition stories
321
+ // (Named components avoid hooks-in-callbacks lint issues — the react-hooks ESLint
322
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.)
323
+ // ---------------------------------------------------------------------------
324
+ const AllSizesTemplate = () => (_jsxs("div", { style: {
325
+ padding: 'var(--spacing-xlarge)',
326
+ display: 'flex',
327
+ gap: 'var(--spacing-xlarge)',
328
+ alignItems: 'flex-end',
329
+ }, children: [_jsxs("div", { style: {
330
+ display: 'flex',
331
+ flexDirection: 'column',
332
+ alignItems: 'center',
333
+ gap: 'var(--spacing-large)',
334
+ }, children: [_jsx(Icon, { name: "dot", size: 12 }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "12" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "dense UI only" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "(tables, compact lists)" })] }), _jsxs("div", { style: {
335
+ display: 'flex',
336
+ flexDirection: 'column',
337
+ alignItems: 'center',
338
+ gap: 'var(--spacing-large)',
339
+ }, children: [_jsx(Icon, { name: "graduation-cap", size: 16 }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "16" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "default (almost everything)" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "\u00A0" })] }), _jsxs("div", { style: {
340
+ display: 'flex',
341
+ flexDirection: 'column',
342
+ alignItems: 'center',
343
+ gap: 'var(--spacing-large)',
344
+ }, children: [_jsx(Icon, { name: "files", size: 24 }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "24" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "high-significance only" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "(empty states, hero callouts)" })] })] }));
345
+ const CurrentColorInheritanceTemplate = () => (_jsxs("div", { style: {
346
+ padding: 'var(--spacing-xlarge)',
347
+ display: 'flex',
348
+ flexDirection: 'column',
349
+ gap: 'var(--spacing-large)',
350
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "The same icon with no `color` prop \u2014 inheriting parent `color` in each case" }), _jsxs("div", { style: {
351
+ display: 'flex',
352
+ alignItems: 'center',
353
+ gap: 'var(--spacing-small)',
354
+ color: 'var(--color-grey-900)',
355
+ }, children: [_jsx(Icon, { name: "info", size: 16 }), _jsx("span", { className: "ds-text", children: "Body text colour (--color-grey-900)" })] }), _jsxs("div", { style: {
356
+ display: 'flex',
357
+ alignItems: 'center',
358
+ gap: 'var(--spacing-small)',
359
+ color: 'var(--color-brand-700)',
360
+ }, children: [_jsx(Icon, { name: "info", size: 16 }), _jsx("span", { className: "ds-text", children: "Brand heading colour (--color-brand-700)" })] }), _jsxs("div", { style: {
361
+ display: 'flex',
362
+ alignItems: 'center',
363
+ gap: 'var(--spacing-small)',
364
+ color: 'var(--color-semantic-destructive-600)',
365
+ }, children: [_jsx(Icon, { name: "info", size: 16 }), _jsx("span", { className: "ds-text", children: "Error text colour (--color-semantic-destructive-600)" })] }), _jsxs("div", { style: {
366
+ display: 'flex',
367
+ alignItems: 'center',
368
+ gap: 'var(--spacing-small)',
369
+ color: 'var(--color-grey-400)',
370
+ }, children: [_jsx(Icon, { name: "info", size: 16 }), _jsx("span", { className: "ds-text", children: "Disabled/muted colour (--color-grey-400)" })] })] }));
371
+ const SemanticColoursTemplate = () => (_jsxs("div", { style: {
372
+ padding: 'var(--spacing-xlarge)',
373
+ display: 'flex',
374
+ flexDirection: 'column',
375
+ gap: 'var(--spacing-large)',
376
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Status icons \u2014 the only case where passing `color` explicitly is correct" }), _jsxs("div", { style: {
377
+ display: 'flex',
378
+ alignItems: 'center',
379
+ gap: 'var(--spacing-small)',
380
+ color: 'var(--color-semantic-info-600)',
381
+ }, children: [_jsx(Icon, { name: "circle-alert", size: 16, color: "var(--color-semantic-info-600)" }), _jsx("span", { className: "ds-text", children: "Heads up" })] }), _jsxs("div", { style: {
382
+ display: 'flex',
383
+ alignItems: 'center',
384
+ gap: 'var(--spacing-small)',
385
+ color: 'var(--color-semantic-success-600)',
386
+ }, children: [_jsx(Icon, { name: "circle-check", size: 16, color: "var(--color-semantic-success-600)" }), _jsx("span", { className: "ds-text", children: "Saved successfully" })] }), _jsxs("div", { style: {
387
+ display: 'flex',
388
+ alignItems: 'center',
389
+ gap: 'var(--spacing-small)',
390
+ color: 'var(--color-semantic-warning-600)',
391
+ }, children: [_jsx(Icon, { name: "triangle-alert", size: 16, color: "var(--color-semantic-warning-600)" }), _jsx("span", { className: "ds-text", children: "Check before submitting" })] }), _jsxs("div", { style: {
392
+ display: 'flex',
393
+ alignItems: 'center',
394
+ gap: 'var(--spacing-small)',
395
+ color: 'var(--color-semantic-destructive-600)',
396
+ }, children: [_jsx(Icon, { name: "circle-x", size: 16, color: "var(--color-semantic-destructive-600)" }), _jsx("span", { className: "ds-text", children: "Save failed" })] })] }));
397
+ const AccessibleLabelTemplate = () => (_jsxs("div", { style: {
398
+ padding: 'var(--spacing-xlarge)',
399
+ display: 'flex',
400
+ flexDirection: 'column',
401
+ gap: 'var(--spacing-xlarge)',
402
+ }, children: [_jsxs("div", { style: {
403
+ display: 'flex',
404
+ flexDirection: 'column',
405
+ gap: 'var(--spacing-large)',
406
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-semantic-destructive-600)' }, children: "Without screenReaderText \u2014 screen reader announces NOTHING (correct for decorative use)" }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "triangle-alert", size: 16 }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "Decorative \u2014 sits next to visible text, no SR text needed" })] }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }, children: "Screen reader: (silence \u2014 SVG has aria-hidden)" })] }), _jsxs("div", { style: {
407
+ display: 'flex',
408
+ flexDirection: 'column',
409
+ gap: 'var(--spacing-large)',
410
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-semantic-success-600)' }, children: "With screenReaderText=\"Warning:\" \u2014 screen reader announces \"Warning:\" from the sr-only span" }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "triangle-alert", size: 16, screenReaderText: "Warning:" }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "Meaningful \u2014 standalone icon, SR text carries the label" })] }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }, children: "Screen reader: \"Warning:\"" })] })] }));
411
+ const DecorativeVsMeaningfulTemplate = () => (_jsxs("div", { style: {
412
+ padding: 'var(--spacing-xlarge)',
413
+ display: 'flex',
414
+ flexDirection: 'column',
415
+ gap: 'var(--spacing-xlarge)',
416
+ }, children: [_jsxs("div", { style: {
417
+ display: 'flex',
418
+ flexDirection: 'column',
419
+ gap: 'var(--spacing-large)',
420
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Decorative \u2014 icon accompanies visible button text. No screenReaderText needed. Screen reader announces \"Edit\" from the button text." }), _jsx(Button, { variant: "secondary", type: "button", iconRightName: "pencil", children: "Edit" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }, children: "Screen reader: \"Edit, button\"" })] }), _jsxs("div", { style: {
421
+ display: 'flex',
422
+ flexDirection: 'column',
423
+ gap: 'var(--spacing-large)',
424
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Meaningful \u2014 icon-only button. Accessible name via iconRightScreenReaderText. No screenReaderText needed on Icon itself." }), _jsx(Button, { variant: "tertiary", type: "button", iconRightName: "pencil", iconRightScreenReaderText: "Edit student record" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }, children: "Screen reader: \"Edit student record, button\"" })] })] }));
425
+ const CustomSolidVariantsTemplate = () => (_jsxs("div", { style: {
426
+ padding: 'var(--spacing-xlarge)',
427
+ display: 'flex',
428
+ flexDirection: 'column',
429
+ gap: 'var(--spacing-xlarge)',
430
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Outline (default) vs filled (emphasis) \u2014 never mix in the same UI context" }), _jsxs("div", { style: {
431
+ display: 'flex',
432
+ flexDirection: 'column',
433
+ gap: 'var(--spacing-large)',
434
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Confirmation emphasis" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "check", size: 24 }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "check (outline)" })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "check-solid", size: 24, color: "var(--color-semantic-success-600)" }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "check-solid (filled)" })] })] })] }), _jsxs("div", { style: {
435
+ display: 'flex',
436
+ flexDirection: 'column',
437
+ gap: 'var(--spacing-large)',
438
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Rejection/remove emphasis" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "x", size: 24 }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "x (outline)" })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "x-solid", size: 24, color: "var(--color-semantic-destructive-600)" }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "x-solid (filled)" })] })] })] }), _jsxs("div", { style: {
439
+ display: 'flex',
440
+ flexDirection: 'column',
441
+ gap: 'var(--spacing-large)',
442
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Favourite/bookmark active state" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "favourite-outline", size: 24 }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "favourite-outline" })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "favourite-filled", size: 24, color: "var(--color-brand-600)" }), _jsx("span", { className: "ds-text", style: { color: 'var(--color-grey-600)' }, children: "favourite-filled" })] })] })] })] }));
443
+ const ICON_CATEGORIES = [
444
+ {
445
+ heading: 'Navigation',
446
+ icons: [
447
+ 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right',
448
+ 'arrow-right-from-line', 'arrow-up-right',
449
+ 'chevron-up', 'chevron-down', 'chevron-left', 'chevron-right',
450
+ 'chevrons-left', 'chevrons-right',
451
+ 'corner-down-left', 'menu',
452
+ 'ellipsis', 'ellipsis-vertical', '3-dot', 'grab',
453
+ ],
454
+ },
455
+ {
456
+ heading: 'Actions',
457
+ icons: [
458
+ 'pencil', 'trash', 'save', 'copy', 'download', 'upload',
459
+ 'share', 'send', 'redo', 'undo', 'plus', 'minus',
460
+ 'circle-plus', 'iteration-ccw', 'switch-camera',
461
+ ],
462
+ },
463
+ {
464
+ heading: 'Status / severity',
465
+ icons: [
466
+ 'info', 'circle-alert', 'triangle-alert',
467
+ 'circle-check', 'circle-check-big', 'circle-x',
468
+ 'circle-help', 'check', 'check-solid', 'x', 'x-solid',
469
+ ],
470
+ },
471
+ {
472
+ heading: 'Content / media',
473
+ icons: [
474
+ 'file', 'files', 'clipboard', 'clipboard-list',
475
+ 'book-open', 'camera', 'mic', 'paperclip',
476
+ 'sheet', 'table',
477
+ 'sparkles', 'party-popper', 'lightbulb',
478
+ 'favourite-outline', 'favourite-filled',
479
+ ],
480
+ },
481
+ {
482
+ heading: 'Charts / data',
483
+ icons: [
484
+ 'chart-column-increasing', 'chart-spline',
485
+ 'trending-up', 'trending-down',
486
+ 'percent', 'circle-percent',
487
+ 'sorting', 'funnel', 'sliders-horizontal', 'list-filter-plus',
488
+ ],
489
+ },
490
+ {
491
+ heading: 'People / org',
492
+ icons: [
493
+ 'user', 'guardians', 'group', 'staff', 'graduation-cap',
494
+ ],
495
+ },
496
+ {
497
+ heading: 'System / state',
498
+ icons: [
499
+ 'eye', 'eye-off', 'lock', 'lock-open', 'loader',
500
+ 'pin', 'flag', 'house', 'history', 'clock-3', 'date',
501
+ 'settings', 'expand', 'shrink', 'link', 'external-link',
502
+ 'log-out', 'search', 'mail', 'phone', 'smartphone',
503
+ 'monitor', 'projector', 'archive',
504
+ ],
505
+ },
506
+ {
507
+ heading: 'Other / misc',
508
+ icons: [
509
+ 'dot', 'layout-list', 'list', 'grid-3x3',
510
+ 'paint-bucket', 'message-square-more',
511
+ ],
512
+ },
513
+ {
514
+ heading: 'Custom / brand',
515
+ icons: ['ask-arbor', 'google'],
516
+ },
517
+ ];
518
+ const IconGalleryTemplate = () => (_jsx("div", { style: {
519
+ padding: 'var(--spacing-xlarge)',
520
+ width: '100%',
521
+ maxWidth: '900px',
522
+ }, children: ICON_CATEGORIES.map(category => (_jsxs("div", { style: { marginBottom: 'var(--spacing-xxlarge)' }, children: [_jsx("p", { className: "ds-text", style: {
523
+ margin: '0 0 var(--spacing-large) 0',
524
+ color: 'var(--color-grey-600)',
525
+ fontWeight: 'var(--font-weight-semi-bold)',
526
+ }, children: category.heading }), _jsx("div", { style: {
527
+ display: 'grid',
528
+ gridTemplateColumns: 'repeat(auto-fill, minmax(7rem, 1fr))',
529
+ gap: 'var(--spacing-small)',
530
+ }, children: category.icons.map(name => (_jsxs("div", { style: {
531
+ display: 'flex',
532
+ flexDirection: 'column',
533
+ alignItems: 'center',
534
+ gap: 'var(--spacing-xsmall)',
535
+ padding: 'var(--spacing-medium)',
536
+ borderRadius: 'var(--border-radius-small)',
537
+ border: '1px solid var(--color-grey-200)',
538
+ background: 'var(--color-grey-100)',
539
+ }, children: [_jsx(Icon, { name: name, size: 16 }), _jsx("span", { className: "ds-text", style: {
540
+ color: 'var(--color-grey-600)',
541
+ wordBreak: 'break-word',
542
+ textAlign: 'center',
543
+ fontSize: 'var(--font-size-1-11)',
544
+ }, children: name })] }, name))) })] }, category.heading))) }));
545
+ const InButtonContextTemplate = () => (_jsxs("div", { style: {
546
+ padding: 'var(--spacing-xlarge)',
547
+ display: 'flex',
548
+ flexDirection: 'column',
549
+ gap: 'var(--spacing-large)',
550
+ }, children: [_jsxs("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: ["Button manages Icon internally \u2014 use iconLeftName/iconRightName, not", ' ', '<Icon>', ' ', "as a child"] }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap', alignItems: 'center' }, children: [_jsx(Button, { variant: "primary", type: "button", iconLeftName: "plus", children: "Add student" }), _jsx(Button, { variant: "secondary", type: "button", iconRightName: "chevron-down", children: "Select year group" }), _jsx(Button, { variant: "tertiary", type: "button", iconRightName: "3-dot", iconRightScreenReaderText: "More actions" })] }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }, children: "The icon-only tertiary button above should be wrapped in a TooltipWrapper in production to provide a hover/focus label for sighted mouse users." })] }));
551
+ const InStatusIndicatorContextTemplate = () => (_jsxs("div", { style: {
552
+ padding: 'var(--spacing-xlarge)',
553
+ display: 'flex',
554
+ flexDirection: 'column',
555
+ gap: 'var(--spacing-large)',
556
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Bulk action validation summary \u2014 icons communicate outcome severity at a glance" }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "circle-check", size: 16, color: "var(--color-semantic-success-600)", screenReaderText: "Success:" }), _jsx("span", { className: "ds-text", children: "3 marksheets saved" })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "triangle-alert", size: 16, color: "var(--color-semantic-warning-600)", screenReaderText: "Warning:" }), _jsx("span", { className: "ds-text", children: "2 conflicts to review" })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }, children: [_jsx(Icon, { name: "circle-x", size: 16, color: "var(--color-semantic-destructive-600)", screenReaderText: "Error:" }), _jsx("span", { className: "ds-text", children: "1 submission failed" })] })] })] }));
557
+ const InEmptyStateContextTemplate = () => (_jsxs("div", { style: {
558
+ padding: 'var(--spacing-xlarge)',
559
+ display: 'flex',
560
+ flexDirection: 'column',
561
+ alignItems: 'center',
562
+ gap: 'var(--spacing-large)',
563
+ textAlign: 'center',
564
+ }, children: [_jsx(Icon, { name: "files", size: 24, color: "var(--color-grey-400)" }), _jsx("p", { className: "ds-text", style: { margin: 0, fontWeight: 'var(--font-weight-semi-bold)' }, children: "No reports yet" }), _jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Upload your first report to get started." })] }));
565
+ // ---------------------------------------------------------------------------
566
+ // Stories
567
+ // ---------------------------------------------------------------------------
568
+ export const Default = withDescription({
569
+ args: {
570
+ name: '3-dot',
571
+ size: 16,
572
+ },
573
+ render: args => _jsx(Icon, { ...args }),
574
+ }, '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.');
575
+ export const AllSizes = withDescription({
576
+ render: AllSizesTemplate,
577
+ parameters: {
578
+ docs: {
579
+ source: {
580
+ language: 'tsx',
581
+ code: `
582
+ import { Icon } from '@arbor-education/design-system.components';
583
+
584
+ function AllSizesExample() {
585
+ return (
586
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'flex-end' }}>
587
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
588
+ <Icon name="dot" size={12} />
589
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>12 — dense UI only</p>
590
+ </div>
591
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
592
+ <Icon name="graduation-cap" size={16} />
593
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>16 — default</p>
594
+ </div>
595
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-large)' }}>
596
+ <Icon name="files" size={24} />
597
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>24 — high-significance only</p>
598
+ </div>
599
+ </div>
600
+ );
601
+ }
602
+ export default AllSizesExample;
603
+ `.trim(),
604
+ },
605
+ },
606
+ },
607
+ }, [
608
+ 'All three valid sizes side by side with their intended use cases.',
609
+ '',
610
+ '**12** (`--icon-size-xsmall`) — dense UI only. Table cells, compact lists, tight inline indicators.',
611
+ 'Too small for standalone use or touch targets.',
612
+ '',
613
+ '**16** (`--icon-size-small`) — the default for almost everything in the system.',
614
+ 'Balances visual weight against surrounding text at standard body size.',
615
+ '',
616
+ '**24** (`--icon-size-medium`) — reserved for high-significance moments: empty states, hero callouts,',
617
+ 'onboarding prompts. Using 24px icons in dense list or form contexts creates visual imbalance.',
618
+ '',
619
+ 'Never use arbitrary sizes outside these three — the size constraint is a deliberate design system rule.',
620
+ ].join(' '));
621
+ export const CurrentColorInheritance = withDescription({
622
+ render: CurrentColorInheritanceTemplate,
623
+ parameters: {
624
+ docs: {
625
+ source: {
626
+ language: 'tsx',
627
+ code: `
628
+ import { Icon } from '@arbor-education/design-system.components';
629
+
630
+ function CurrentColorInheritanceExample() {
631
+ return (
632
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
633
+ {/* Icon inherits the wrapper colour — no color prop needed */}
634
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-grey-900)' }}>
635
+ <Icon name="info" size={16} />
636
+ <span className="ds-text">Body text colour (--color-grey-900)</span>
637
+ </div>
638
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-brand-700)' }}>
639
+ <Icon name="info" size={16} />
640
+ <span className="ds-text">Brand heading colour (--color-brand-700)</span>
641
+ </div>
642
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-semantic-destructive-600)' }}>
643
+ <Icon name="info" size={16} />
644
+ <span className="ds-text">Error text colour (--color-semantic-destructive-600)</span>
645
+ </div>
646
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)', color: 'var(--color-grey-400)' }}>
647
+ <Icon name="info" size={16} />
648
+ <span className="ds-text">Disabled/muted colour (--color-grey-400)</span>
649
+ </div>
650
+ </div>
651
+ );
652
+ }
653
+ export default CurrentColorInheritanceExample;
654
+ `.trim(),
655
+ },
656
+ },
657
+ },
658
+ }, [
659
+ 'The `color` prop defaults to `"currentColor"` — the SVG inherits the `color` CSS property of its',
660
+ 'parent element. This story shows the **same `<Icon name="info" />` with no `color` prop** inside',
661
+ 'four different parent containers.',
662
+ '',
663
+ 'The icon colour tracks the parent text colour automatically. This is especially handy for icon + label',
664
+ 'pairings: style the wrapper once and the icon follows the label through hover, active, and disabled',
665
+ 'states. It also lets you control a whole cluster of icons by setting one CSS token on their shared',
666
+ 'parent, instead of threading `color` through each `<Icon>`.',
667
+ ].join(' '));
668
+ export const SemanticColours = withDescription({
669
+ render: SemanticColoursTemplate,
670
+ parameters: {
671
+ docs: {
672
+ source: {
673
+ language: 'tsx',
674
+ code: `
675
+ import { Icon } from '@arbor-education/design-system.components';
676
+
677
+ function SemanticColoursExample() {
678
+ return (
679
+ <div style={{ padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
680
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
681
+ <Icon name="circle-alert" size={16} color="var(--color-semantic-info-600)" />
682
+ <span className="ds-text">Heads up</span>
683
+ </div>
684
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
685
+ <Icon name="circle-check" size={16} color="var(--color-semantic-success-600)" />
686
+ <span className="ds-text">Saved successfully</span>
687
+ </div>
688
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
689
+ <Icon name="triangle-alert" size={16} color="var(--color-semantic-warning-600)" />
690
+ <span className="ds-text">Check before submitting</span>
691
+ </div>
692
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
693
+ <Icon name="circle-x" size={16} color="var(--color-semantic-destructive-600)" />
694
+ <span className="ds-text">Save failed</span>
695
+ </div>
696
+ </div>
697
+ );
698
+ }
699
+ export default SemanticColoursExample;
700
+ `.trim(),
701
+ },
702
+ },
703
+ },
704
+ }, [
705
+ 'A canonical use case for passing `color` explicitly: status icons that must communicate severity',
706
+ 'independently of the surrounding text colour. (You can also pass `color` anywhere else it makes',
707
+ 'sense — for brand accents, fixed-colour service logos, or intentionally muted empty-state glyphs —',
708
+ 'just always prefer a design token over a hardcoded hex.)',
709
+ '',
710
+ 'Each row pairs an icon with a text label using the same semantic colour token. This satisfies',
711
+ 'WCAG 1.4.1 (use of colour) — the meaning is communicated by both the icon shape AND the colour',
712
+ 'AND the text label, not by colour alone.',
713
+ '',
714
+ 'Tokens used: `--color-semantic-info-600`, `--color-semantic-success-600`,',
715
+ '`--color-semantic-warning-600`, `--color-semantic-destructive-600`.',
716
+ ].join(' '));
717
+ export const AccessibleLabel = withDescription({
718
+ render: AccessibleLabelTemplate,
719
+ parameters: {
720
+ docs: {
721
+ source: {
722
+ language: 'tsx',
723
+ code: `
724
+ import { Icon } from '@arbor-education/design-system.components';
725
+
726
+ function AccessibleLabelExample() {
727
+ return (
728
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', padding: 'var(--spacing-xlarge)' }}>
729
+ {/* Decorative — no screenReaderText, sits next to visible text */}
730
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
731
+ <Icon name="triangle-alert" size={16} />
732
+ <span className="ds-text">Decorative — sits next to visible text, no SR text needed</span>
733
+ </div>
734
+ {/* Meaningful — standalone icon, screenReaderText carries the label */}
735
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
736
+ <Icon name="triangle-alert" size={16} screenReaderText="Warning:" />
737
+ <span className="ds-text">Meaningful — standalone icon, SR text carries the label</span>
738
+ </div>
739
+ </div>
740
+ );
741
+ }
742
+ export default AccessibleLabelExample;
743
+ `.trim(),
744
+ },
745
+ },
746
+ },
747
+ }, [
748
+ 'The `aria-hidden` + `sr-only` pattern in practice.',
749
+ '',
750
+ 'The SVG **always** has `aria-hidden="true"` — screen readers skip it entirely.',
751
+ 'When `screenReaderText` is provided, a `<span class="sr-only">` is rendered immediately after',
752
+ 'the SVG. Screen readers announce the span; they never see the SVG.',
753
+ '',
754
+ 'See: [Why aria-hidden beats aria-label on SVGs](https://gomakethings.com/revisting-aria-label-versus-a-visually-hidden-class/)',
755
+ '',
756
+ '**Top row** — no `screenReaderText`. The icon is purely decorative in the context of accompanying',
757
+ 'visible text. Screen reader announces nothing from the icon (which is correct).',
758
+ '',
759
+ '**Bottom row** — `screenReaderText="Warning:"` provided. The `<span class="sr-only">` carries',
760
+ 'the accessible label. Screen reader announces "Warning:".',
761
+ ].join(' '));
762
+ export const DecorativeVsMeaningful = withDescription({
763
+ render: DecorativeVsMeaningfulTemplate,
764
+ parameters: {
765
+ docs: {
766
+ source: {
767
+ language: 'tsx',
768
+ code: `
769
+ import { Button } from '@arbor-education/design-system.components';
770
+
771
+ function DecorativeVsMeaningfulExample() {
772
+ return (
773
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', padding: 'var(--spacing-xlarge)' }}>
774
+ {/* Decorative — icon accompanies visible button text. Screen reader: "Edit, button" */}
775
+ <Button variant="secondary" type="button" iconRightName="pencil">
776
+ Edit
777
+ </Button>
778
+ {/* Meaningful — icon-only button. iconRightScreenReaderText provides accessible name. */}
779
+ <Button
780
+ variant="tertiary"
781
+ type="button"
782
+ iconRightName="pencil"
783
+ iconRightScreenReaderText="Edit student record"
784
+ />
785
+ </div>
786
+ );
787
+ }
788
+ export default DecorativeVsMeaningfulExample;
789
+ `.trim(),
790
+ },
791
+ },
792
+ },
793
+ }, [
794
+ 'Two composed button examples showing the two main accessibility patterns.',
795
+ '',
796
+ '**Decorative (top):** the pencil icon sits next to visible "Edit" text in the button.',
797
+ 'No `screenReaderText` on the Icon. The button\'s accessible name is "Edit" — from the visible text.',
798
+ 'Adding SR text here would cause "Edit pencil" duplication.',
799
+ '',
800
+ '**Meaningful (bottom):** an icon-only button with no visible text.',
801
+ '`aria-label="Edit student record"` on the `<button>` element provides the accessible name.',
802
+ 'No `screenReaderText` needed on the Icon — the button already has one accessible name.',
803
+ '',
804
+ 'The rule is: **one accessible name per interactive element**. Choose where to put it, but never',
805
+ 'duplicate it. For icon-only buttons in production, also add a TooltipWrapper for sighted mouse users.',
806
+ ].join(' '));
807
+ export const CustomSolidVariants = withDescription({
808
+ render: CustomSolidVariantsTemplate,
809
+ parameters: {
810
+ docs: {
811
+ source: {
812
+ language: 'tsx',
813
+ code: `
814
+ import { Icon } from '@arbor-education/design-system.components';
815
+
816
+ function CustomSolidVariantsExample() {
817
+ return (
818
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', padding: 'var(--spacing-xlarge)' }}>
819
+ {/* Confirmation emphasis — outline vs filled */}
820
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
821
+ <Icon name="check" size={24} />
822
+ <Icon name="check-solid" size={24} color="var(--color-semantic-success-600)" />
823
+ </div>
824
+ {/* Rejection/remove emphasis — outline vs filled */}
825
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
826
+ <Icon name="x" size={24} />
827
+ <Icon name="x-solid" size={24} color="var(--color-semantic-destructive-600)" />
828
+ </div>
829
+ {/* Favourite active state — outline vs filled */}
830
+ <div style={{ display: 'flex', gap: 'var(--spacing-xlarge)', alignItems: 'center' }}>
831
+ <Icon name="favourite-outline" size={24} />
832
+ <Icon name="favourite-filled" size={24} color="var(--color-brand-600)" />
833
+ </div>
834
+ </div>
835
+ );
836
+ }
837
+ export default CustomSolidVariantsExample;
838
+ `.trim(),
839
+ },
840
+ },
841
+ },
842
+ }, [
843
+ 'Arbor\'s three filled/solid icon variants alongside their outline equivalents.',
844
+ '',
845
+ '`check-solid` — a custom filled check mark. Use for confirmed/completed states where the standard',
846
+ 'outline `check` feels too subtle. Pair with `--color-semantic-success-600`.',
847
+ '',
848
+ '`x-solid` — a custom filled X. Use for rejected/removed/dismissed states.',
849
+ 'Pair with `--color-semantic-destructive-600`.',
850
+ '',
851
+ '`favourite-filled` — the `Star` icon rendered with a fill matching its colour.',
852
+ 'Use for the active "favourited" state. Toggle with `favourite-outline` for the default state.',
853
+ '',
854
+ '**Design rule:** never mix filled and outline icons in the same UI context. If you use',
855
+ '`check-solid` for one state, use `check` (outline) for the other — don\'t swap to a different icon.',
856
+ ].join(' '));
857
+ export const InButtonContext = withDescription({
858
+ render: InButtonContextTemplate,
859
+ parameters: {
860
+ docs: {
861
+ source: {
862
+ language: 'tsx',
863
+ code: `
864
+ import { Button } from '@arbor-education/design-system.components';
865
+
866
+ function InButtonContextExample() {
867
+ return (
868
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap', alignItems: 'center' }}>
869
+ <Button variant="primary" type="button" iconLeftName="plus">
870
+ Add student
871
+ </Button>
872
+ <Button variant="secondary" type="button" iconRightName="chevron-down">
873
+ Select year group
874
+ </Button>
875
+ {/* Icon-only — wrap in TooltipWrapper in production for hover/focus affordance */}
876
+ <Button
877
+ variant="tertiary"
878
+ type="button"
879
+ iconRightName="3-dot"
880
+ iconRightScreenReaderText="More actions"
881
+ />
882
+ </div>
883
+ );
884
+ }
885
+ export default InButtonContextExample;
886
+ `.trim(),
887
+ },
888
+ },
889
+ },
890
+ }, [
891
+ 'How Icon appears in real Button usage.',
892
+ '',
893
+ '**Do not** render `<Icon>` as a child of `<Button>` directly. The `Button` component manages',
894
+ 'its own icon rendering internally via `iconLeftName` and `iconRightName` props.',
895
+ '',
896
+ '`iconLeftName="plus"` — icon before label (the default icon+label pattern for action buttons).',
897
+ '',
898
+ '`iconRightName="chevron-down"` — trailing action-indicator icon (communicates "opens a menu").',
899
+ '',
900
+ 'Icon-only tertiary with `iconRightScreenReaderText="More actions"` — in production, this should',
901
+ 'be wrapped in a `TooltipWrapper` to provide a hover/focus label for sighted users per Confluence',
902
+ 'guidance on icon-only interactive elements.',
903
+ ].join(' '));
904
+ export const InStatusIndicatorContext = withDescription({
905
+ render: InStatusIndicatorContextTemplate,
906
+ parameters: {
907
+ docs: {
908
+ source: {
909
+ language: 'tsx',
910
+ code: `
911
+ import { Icon } from '@arbor-education/design-system.components';
912
+
913
+ function InStatusIndicatorContextExample() {
914
+ return (
915
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
916
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
917
+ <Icon name="circle-check" size={16} color="var(--color-semantic-success-600)" screenReaderText="Success:" />
918
+ <span className="ds-text">3 marksheets saved</span>
919
+ </div>
920
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
921
+ <Icon name="triangle-alert" size={16} color="var(--color-semantic-warning-600)" screenReaderText="Warning:" />
922
+ <span className="ds-text">2 conflicts to review</span>
923
+ </div>
924
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-small)' }}>
925
+ <Icon name="circle-x" size={16} color="var(--color-semantic-destructive-600)" screenReaderText="Error:" />
926
+ <span className="ds-text">1 submission failed</span>
927
+ </div>
928
+ </div>
929
+ );
930
+ }
931
+ export default InStatusIndicatorContextExample;
932
+ `.trim(),
933
+ },
934
+ },
935
+ },
936
+ }, [
937
+ 'A realistic validation summary panel — the kind that appears after a bulk marksheet save action.',
938
+ '',
939
+ 'Each row uses a status icon with explicit `color` AND `screenReaderText`. The `screenReaderText`',
940
+ 'is important here: the icons are standalone status indicators, not decorative accompaniments to',
941
+ 'already-labelled text.',
942
+ '',
943
+ 'Screen reader output: "Success: 3 marksheets saved", "Warning: 2 conflicts to review",',
944
+ '"Error: 1 submission failed". The SR prefix communicates severity before the message.',
945
+ '',
946
+ 'Note `--spacing-small` gap between icon and text — tight enough to read as a unit,',
947
+ 'loose enough not to crowd.',
948
+ ].join(' '));
949
+ export const InEmptyStateContext = withDescription({
950
+ render: InEmptyStateContextTemplate,
951
+ parameters: {
952
+ docs: {
953
+ source: {
954
+ language: 'tsx',
955
+ code: `
956
+ import { Icon } from '@arbor-education/design-system.components';
957
+
958
+ function InEmptyStateContextExample() {
959
+ return (
960
+ <div
961
+ style={{
962
+ padding: 'var(--spacing-xlarge)',
963
+ display: 'flex',
964
+ flexDirection: 'column',
965
+ alignItems: 'center',
966
+ gap: 'var(--spacing-large)',
967
+ textAlign: 'center',
968
+ }}
969
+ >
970
+ <Icon name="files" size={24} color="var(--color-grey-400)" />
971
+ <p className="ds-text" style={{ margin: 0, fontWeight: 'var(--font-weight-semi-bold)' }}>
972
+ No reports yet
973
+ </p>
974
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
975
+ Upload your first report to get started.
976
+ </p>
977
+ </div>
978
+ );
979
+ }
980
+ export default InEmptyStateContextExample;
981
+ `.trim(),
982
+ },
983
+ },
984
+ },
985
+ }, [
986
+ 'The canonical use case for `size={24}` — an empty state layout.',
987
+ '',
988
+ 'The `files` icon at 24px is large enough to anchor the empty state visually without',
989
+ 'being overwhelming. `color="var(--color-grey-400)"` renders it muted — the icon is a',
990
+ 'supporting visual element, not the primary message.',
991
+ '',
992
+ 'This pattern appears throughout Arbor wherever a list, table, or content area has no data.',
993
+ 'Follow this layout: centred icon (size 24, muted grey) → bold heading → supporting subtext.',
994
+ '',
995
+ 'Do not use `size={24}` in list items, table cells, or button icons — reserve it for',
996
+ 'full-area statements like this one.',
997
+ ].join(' '));
27
998
  //# sourceMappingURL=Icon.stories.js.map