@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,84 +1,900 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
4
+ import { fn } from 'storybook/test';
1
5
  import { Banner, BANNER_LEVEL } from './Banner';
6
+ // Banner is Object.assign(BannerRoot, {...}) — Storybook reads the internal
7
+ // function name 'BannerRoot' for code generation. Setting displayName here
8
+ // ensures the global source transform uses the correct public export name.
9
+ Banner.displayName = 'Banner';
10
+ // ---------------------------------------------------------------------------
11
+ // Docs page content
12
+ // ---------------------------------------------------------------------------
13
+ const DESCRIPTION_INTRO = [
14
+ 'Banner is a full-width horizontal strip placed at the top of a page or surface to deliver page-level state,',
15
+ 'system guidance, or high-risk alerts. Unlike inline notifications, a Banner addresses the entire surface',
16
+ '— think of it as the town crier of your UI.',
17
+ ].join('\n');
18
+ const USAGE_GUIDANCE = [
19
+ '### When to use',
20
+ '',
21
+ '- **Page-level state** — information that applies to everything the user sees on the current surface',
22
+ '- **System problems** — connectivity issues, failed background syncs, service degradation',
23
+ '- **High-risk situations** — data that is locked, read-only, or from a prior academic year',
24
+ '- **Essential page guidance** — critical "how this page works" context a user must not miss',
25
+ '',
26
+ '---',
27
+ '',
28
+ '### When NOT to use',
29
+ '',
30
+ '| Situation | Use instead |',
31
+ '|---|---|',
32
+ '| Inline field errors | Form validation (field-level error messages) |',
33
+ '| Success messages / snackbars | [`Toast`](?path=/docs/components-toast--docs) |',
34
+ '| More than one alert on the same surface | Consolidate, prioritise, or use a different pattern |',
35
+ '| Marketing / promotional copy | A dedicated promotional component |',
36
+ '',
37
+ '---',
38
+ '',
39
+ '### Design guidance',
40
+ '',
41
+ '**Four semantic variants:**',
42
+ '',
43
+ '- `INFO` (blue) — helpful context the user should know but does not need to act on immediately',
44
+ '- `NEUTRAL` (transparent background) — non-urgent notes that do not carry urgency or risk',
45
+ '- `WARNING` (amber) — something is risky or incomplete, but the user can still proceed',
46
+ '- `DESTRUCTIVE` (red) — a serious error or locked state **(legacy only — see critical notes below)**',
47
+ '',
48
+ '**Tone of voice:** Confident, straightforward, positive. Short sentences, plain language, active voice.',
49
+ 'State impact before action. CTAs must use active verbs ("Fix settings", "Review exclusions") — never "Click here".',
50
+ ].join('\n');
51
+ const DEVELOPER_NOTES = [
52
+ '### Critical notes',
53
+ '',
54
+ '#### `DESTRUCTIVE` = "Error" in design docs',
55
+ '',
56
+ 'The source enum uses developer naming (`BANNER_LEVEL.DESTRUCTIVE`) but the Confluence design spec',
57
+ 'calls this variant **"Error"**. They are the same thing. If you are reading the design spec and see',
58
+ '"Error banner", reach for `BANNER_LEVEL.DESTRUCTIVE` in code.',
59
+ '',
60
+ '#### Error banners are legacy-only',
61
+ '',
62
+ 'Per current design guidance, `BANNER_LEVEL.DESTRUCTIVE` should only appear in legacy surfaces.',
63
+ 'For new features, prefer `WARNING` (risky-but-reversible state) or a page-level empty state.',
64
+ 'Error notifications should be backed by reliable, confirmed system/data state — not manual admin notes.',
65
+ '',
66
+ '#### One banner per surface — hard rule',
67
+ '',
68
+ 'Never render more than one Banner on the same page or surface at a time. The `AllVariants` story',
69
+ 'in this file is a **visual reference only**. In a real application, pick the one Banner that',
70
+ 'matters most and surface only that.',
71
+ '',
72
+ '#### Persistent — no built-in dismiss',
73
+ '',
74
+ 'Banner has no close button and no auto-dismiss timer. It stays visible until the underlying condition',
75
+ 'resolves. If your product requirement calls for a dismissible banner, wrap `Banner` in your own',
76
+ '`useState` + conditional render (see the `DismissiblePattern` story for the recommended approach).',
77
+ '',
78
+ '#### No automatic `role="alert"`',
79
+ '',
80
+ 'Banner does **not** set `role="alert"` or `role="status"` automatically. Static banners (present on',
81
+ 'page load) do not need these roles. If your banner appears dynamically in response to a user action',
82
+ '(e.g. after a failed save), you are responsible for adding `role="alert"` via the `className` prop',
83
+ 'or by wrapping the Banner and managing ARIA outside the component.',
84
+ '',
85
+ '---',
86
+ '',
87
+ '### Accessibility',
88
+ '',
89
+ '- **Static banners** (present on load) need no special ARIA. Screen readers encounter them in normal',
90
+ ' document flow as heading + paragraph content.',
91
+ '- **Dynamic banners** (appear after user action): add `role="alert"` externally. Do not overuse alert',
92
+ ' roles — they interrupt screen reader flow and should only fire when the banner content is truly',
93
+ ' time-sensitive.',
94
+ '- **Colour alone** must not be the only signal of severity. The banner text must describe the impact.',
95
+ '- **Icons** receive `aria-hidden="true"` by default (handled by the `Icon` component) when they',
96
+ ' duplicate the meaning already expressed in text.',
97
+ '- **Focus order:** page title → banner content → interactive controls. Do not break this by',
98
+ ' positioning the banner after the main content area.',
99
+ '- **CTA button keyboard access:** the optional text-link button is keyboard-focusable and activated',
100
+ ' by Enter/Space. Its label must make sense out of context — "Fix settings" beats "Click here".',
101
+ '',
102
+ '---',
103
+ '',
104
+ '### Component tokens',
105
+ '',
106
+ 'Banner exposes a structured set of CSS custom properties for theming. The naming pattern is:',
107
+ '',
108
+ '`--banner-{level}-color-{role}` where `{level}` is `info | neutral | warning | destructive`',
109
+ 'and `{role}` is `background | border | text | icon`.',
110
+ '',
111
+ 'Example: `--banner-info-color-background` controls the Info variant background.',
112
+ '',
113
+ 'These tokens wrap the semantic colour system (`--color-semantic-info-*`, etc.) — always use the',
114
+ 'banner-specific tokens rather than the underlying semantic ones so that theme overrides propagate',
115
+ 'correctly. Note that the Neutral variant intentionally has no background token — the transparent',
116
+ 'background is by design.',
117
+ '',
118
+ 'Layout tokens: `--banner-spacing-vertical`, `--banner-spacing-horizontal`, `--banner-spacing-gap`,',
119
+ '`--banner-radius`.',
120
+ '',
121
+ '---',
122
+ '',
123
+ '### TypeScript types',
124
+ '',
125
+ '```ts',
126
+ "import { Banner, BANNER_LEVEL } from '@arbor-education/design-system.components';",
127
+ '',
128
+ 'function MyBanner(props: Banner.Props) { ... }',
129
+ '```',
130
+ '',
131
+ '| Type | Description |',
132
+ '|---|---|',
133
+ '| `Banner.Props` | Full props interface |',
134
+ '| `Banner.Level` | Enum — use as `Banner.Level.INFO`, `.NEUTRAL`, `.WARNING`, `.DESTRUCTIVE`. Replaces the old flat `BANNER_LEVEL` import. |',
135
+ ].join('\n');
136
+ const RELATED_COMPONENTS = [
137
+ '## Related components',
138
+ '',
139
+ '[Toast](?path=/docs/components-toast--docs) · [Modal](?path=/docs/components-modals-modal--docs) · [Button](?path=/docs/components-button--docs) · [Icon](?path=/docs/components-icon--docs)',
140
+ ].join('\n');
141
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
142
+ function BannerDocsPage() {
143
+ 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(Markdown, { children: RELATED_COMPONENTS })] }));
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Curated icon options for the Controls panel.
147
+ // Only icons that make semantic sense in a banner context are listed.
148
+ // All names verified against src/components/icon/allowedIcons.tsx.
149
+ // ---------------------------------------------------------------------------
150
+ const BANNER_ICON_OPTIONS = [
151
+ undefined,
152
+ 'info',
153
+ 'triangle-alert',
154
+ 'circle-alert',
155
+ 'circle-check',
156
+ 'lock',
157
+ 'lock-open',
158
+ 'sparkles',
159
+ 'lightbulb',
160
+ 'mail',
161
+ 'user',
162
+ 'date',
163
+ 'settings',
164
+ 'flag',
165
+ 'clock-3',
166
+ ];
167
+ // ---------------------------------------------------------------------------
168
+ // Meta
169
+ // ---------------------------------------------------------------------------
2
170
  const meta = {
3
- tags: ['autodocs'],
4
171
  title: 'Components/Banner',
5
172
  component: Banner,
173
+ tags: ['autodocs'],
174
+ parameters: {
175
+ layout: 'padded',
176
+ docs: {
177
+ page: BannerDocsPage,
178
+ },
179
+ },
6
180
  argTypes: {
7
181
  level: {
8
- control: { type: 'select' },
182
+ control: 'select',
9
183
  options: Object.values(BANNER_LEVEL),
184
+ description: [
185
+ 'Semantic variant controlling the colour scheme and default icon.',
186
+ '`INFO` (blue) — helpful context.',
187
+ '`NEUTRAL` (transparent) — non-urgent notes.',
188
+ '`WARNING` (amber) — risky but recoverable.',
189
+ '`DESTRUCTIVE` (red) — serious error or locked state.',
190
+ 'Note: the design spec calls `DESTRUCTIVE` the "Error" variant — they are the same thing.',
191
+ ].join(' '),
192
+ table: {
193
+ type: { summary: 'BANNER_LEVEL' },
194
+ defaultValue: { summary: 'BANNER_LEVEL.NEUTRAL' },
195
+ },
196
+ },
197
+ title: {
198
+ control: 'text',
199
+ description: [
200
+ 'Optional heading rendered as an `<h3>` inside the banner.',
201
+ 'Use a short noun phrase (e.g. "Session expiring soon") rather than a full sentence.',
202
+ 'Keep it under 8 words — longer headings push the CTA button onto an awkward line.',
203
+ ].join(' '),
204
+ table: {
205
+ type: { summary: 'string' },
206
+ defaultValue: { summary: 'undefined' },
207
+ },
208
+ },
209
+ text: {
210
+ control: 'text',
211
+ description: [
212
+ 'Body copy for the banner. 1–2 sentences is ideal.',
213
+ 'State the impact before the action: "The census period is closed. Export data before Friday."',
214
+ 'If body copy fills more than 3 lines, consider a Modal instead.',
215
+ ].join(' '),
216
+ table: {
217
+ type: { summary: 'string' },
218
+ defaultValue: { summary: 'undefined' },
219
+ },
220
+ },
221
+ icon: {
222
+ control: 'select',
223
+ options: BANNER_ICON_OPTIONS,
224
+ description: [
225
+ 'Override the default icon for this level.',
226
+ 'Default icons: `info` for INFO and NEUTRAL, `triangle-alert` for WARNING, `circle-alert` for DESTRUCTIVE.',
227
+ 'Use a more specific metaphor when it helps (e.g. `lock` for a locked record, `circle-check` for a completed state).',
228
+ 'Icon colour is controlled by the level token, not the icon choice.',
229
+ 'All options in this control are verified against the allowedIcons set.',
230
+ ].join(' '),
231
+ table: {
232
+ type: { summary: 'IconName' },
233
+ defaultValue: { summary: 'Auto per level' },
234
+ },
235
+ },
236
+ hideIcon: {
237
+ control: 'boolean',
238
+ description: [
239
+ 'When `true`, completely suppresses the icon — no space is reserved and the central container',
240
+ 'expands to fill the full width.',
241
+ 'Use sparingly: icons reinforce the semantic level at a glance for users who scan.',
242
+ ].join(' '),
243
+ table: {
244
+ type: { summary: 'boolean' },
245
+ defaultValue: { summary: 'false' },
246
+ },
247
+ },
248
+ buttonText: {
249
+ control: 'text',
250
+ description: [
251
+ 'Label for the optional text-link CTA button rendered at the trailing edge of the banner.',
252
+ 'Must use an active verb: "Fix settings", "Review exclusions", "Switch to current year".',
253
+ 'Keep it short — the button has `text-wrap: nowrap` and the banner does not wrap to a second row.',
254
+ 'Do not use "Click here" — it conveys no information to screen reader users navigating by links.',
255
+ ].join(' '),
256
+ table: {
257
+ type: { summary: 'string' },
258
+ defaultValue: { summary: 'undefined' },
259
+ },
260
+ },
261
+ buttonOnClick: {
262
+ control: false,
263
+ description: [
264
+ 'Click handler for the CTA button.',
265
+ 'Required when `buttonText` is provided — otherwise the button renders with no action.',
266
+ 'Receives the native `React.MouseEvent<HTMLButtonElement>` so you can call `e.preventDefault()` if needed.',
267
+ ].join(' '),
268
+ table: {
269
+ type: { summary: '(e: React.MouseEvent<HTMLButtonElement>) => void' },
270
+ defaultValue: { summary: 'undefined' },
271
+ },
272
+ },
273
+ className: {
274
+ control: false,
275
+ description: [
276
+ 'Additional CSS class names appended to the root `<div>` via CVA.',
277
+ 'Merges cleanly with the `ds-banner--{level}` modifier — no specificity conflicts.',
278
+ 'Primary use: add `role="alert"` via a utility class, or apply a custom top-margin for layout.',
279
+ 'Do not use this to override banner colours — use the `--banner-*` CSS tokens instead.',
280
+ ].join(' '),
281
+ table: {
282
+ type: { summary: 'string' },
283
+ defaultValue: { summary: 'undefined' },
284
+ },
10
285
  },
11
286
  },
12
287
  args: {
13
- buttonOnClick: () => { console.log('click!'); },
288
+ buttonOnClick: fn(),
14
289
  },
15
290
  };
16
- export const Neutral = {
17
- args: {
18
- text: 'This is a neutral banner message with some useful information for the user.',
291
+ export default meta;
292
+ // ---------------------------------------------------------------------------
293
+ // Helper: attach a per-story description to docs
294
+ // ---------------------------------------------------------------------------
295
+ const withDescription = (story, description) => ({
296
+ ...story,
297
+ parameters: {
298
+ ...story.parameters,
299
+ docs: {
300
+ ...story.parameters?.docs,
301
+ description: {
302
+ story: description,
303
+ },
304
+ },
19
305
  },
306
+ });
307
+ // ---------------------------------------------------------------------------
308
+ // Template components for stateful or compositional stories.
309
+ // Named components avoid react-hooks lint issues — the react-hooks ESLint
310
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.
311
+ // ---------------------------------------------------------------------------
312
+ const DismissibleTemplate = () => {
313
+ const [visible, setVisible] = useState(true);
314
+ return (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Banner has no built-in dismiss. Consumers implement it with useState + conditional render:" }), visible && (_jsx(Banner, { level: BANNER_LEVEL.WARNING, text: "Some marksheet entries have not been saved. Please review before leaving the page.", buttonText: "Dismiss", buttonOnClick: () => setVisible(false) })), !visible && (_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }, children: "Banner dismissed. In a real app, reload the page or resolve the underlying condition to show it again." }))] }));
20
315
  };
21
- export const Info = {
316
+ const AllVariantsTemplate = () => (_jsxs("div", { style: {
317
+ padding: 'var(--spacing-xlarge)',
318
+ display: 'flex',
319
+ flexDirection: 'column',
320
+ gap: 'var(--spacing-xxlarge)',
321
+ }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-semantic-warning-600)', fontStyle: 'italic' }, children: "Reference layout only \u2014 never ship multiple banners on one surface." }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "INFO" }), _jsx(Banner, { level: BANNER_LEVEL.INFO, text: "Your session will expire in 10 minutes. Save your work to avoid losing changes." })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "NEUTRAL (shown against a grey-100 backdrop \u2014 the banner itself is transparent)" }), _jsx("div", { style: { background: 'var(--color-grey-100)', borderRadius: 'var(--border-radius-small)', padding: 'var(--spacing-large)' }, children: _jsx(Banner, { level: BANNER_LEVEL.NEUTRAL, text: "You are viewing last year's data. Switch to the current academic year to make changes." }) })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "WARNING" }), _jsx(Banner, { level: BANNER_LEVEL.WARNING, text: "The marksheet has unsaved entries. Review before leaving to avoid data loss." })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-grey-600)' }, children: "DESTRUCTIVE (called \"Error\" in design spec \u2014 legacy use only)" }), _jsx(Banner, { level: BANNER_LEVEL.DESTRUCTIVE, text: "This student record is locked because the reporting period has been closed." })] })] }));
322
+ const ContentGuidelinesTemplate = () => (_jsxs("div", { style: {
323
+ padding: 'var(--spacing-xlarge)',
324
+ display: 'flex',
325
+ flexDirection: 'column',
326
+ gap: 'var(--spacing-xxlarge)',
327
+ }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-semantic-success-600)' }, children: "Ideal \u2014 short title, 1\u20132 line body, active verb CTA" }), _jsx(Banner, { level: BANNER_LEVEL.WARNING, title: "Reporting period closing soon", text: "The census submission deadline is Friday 17 April at 5pm.", buttonText: "Review exclusions", buttonOnClick: () => undefined })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { className: "ds-text", style: { margin: 0, color: 'var(--color-semantic-destructive-600)' }, children: "Too long \u2014 paragraph-length body belongs in a Modal, not a Banner" }), _jsx(Banner, { level: BANNER_LEVEL.WARNING, title: "Action required before the reporting period closes", text: "The census submission window closes at 5pm on Friday 17 April. You must review all exclusion records, verify that attendance data has been finalised for every year group, and confirm that your designated senior leader has approved the submission. Failure to complete these steps before the deadline will require an extension request to your local authority, which may take up to 10 working days to process.", buttonText: "Learn more", buttonOnClick: () => undefined })] })] }));
328
+ // ---------------------------------------------------------------------------
329
+ // Stories
330
+ // ---------------------------------------------------------------------------
331
+ export const Default = withDescription({
332
+ args: {
333
+ level: BANNER_LEVEL.NEUTRAL,
334
+ text: "You are viewing last year's data. Switch to the current academic year to make changes.",
335
+ },
336
+ render: args => _jsx(Banner, { ...args }),
337
+ }, [
338
+ 'The interactive canvas story — every prop is wired to the **Controls** panel below.',
339
+ 'Use the controls to explore all four levels, toggle `hideIcon`, add a `title`, provide `buttonText`,',
340
+ 'and override the default `icon`. Start by changing `level` to see the colour scheme shift.',
341
+ ].join(' '));
342
+ export const Info = withDescription({
22
343
  args: {
23
344
  level: BANNER_LEVEL.INFO,
24
345
  text: 'Your session will expire in 10 minutes. Save your work to avoid losing changes.',
25
346
  },
26
- };
27
- export const Warning = {
347
+ parameters: {
348
+ docs: {
349
+ source: {
350
+ language: 'tsx',
351
+ code: `
352
+ import { Banner } from '@arbor-education/design-system.components';
353
+
354
+ function BannerInfoExample() {
355
+ return (
356
+ <Banner
357
+ level={Banner.Level.INFO}
358
+ text="Your session will expire in 10 minutes. Save your work to avoid losing changes."
359
+ />
360
+ );
361
+ }
362
+
363
+ export default BannerInfoExample;
364
+ `.trim(),
365
+ },
366
+ },
367
+ },
368
+ }, [
369
+ '**Info (blue)** — helpful context the user should know but does not need to act on right now.',
370
+ 'Use for non-urgent system messages like session warnings, scheduled maintenance, or general guidance.',
371
+ 'The default icon is `info`. The blue colour scheme communicates "pay attention" without alarm.',
372
+ ].join(' '));
373
+ export const Neutral = withDescription({
374
+ args: {
375
+ level: BANNER_LEVEL.NEUTRAL,
376
+ text: "You are viewing last year's data. Switch to the current academic year to make changes.",
377
+ },
378
+ parameters: {
379
+ docs: {
380
+ source: {
381
+ language: 'tsx',
382
+ code: `
383
+ import { Banner } from '@arbor-education/design-system.components';
384
+
385
+ function BannerNeutralExample() {
386
+ return (
387
+ <Banner
388
+ level={Banner.Level.NEUTRAL}
389
+ text="You are viewing last year's data. Switch to the current academic year to make changes."
390
+ />
391
+ );
392
+ }
393
+
394
+ export default BannerNeutralExample;
395
+ `.trim(),
396
+ },
397
+ },
398
+ },
399
+ }, [
400
+ '**Neutral (transparent)** — contextual notes that carry no urgency or risk.',
401
+ 'The banner background is transparent, inheriting the page surface behind it.',
402
+ 'Use for orientation messages: "you are in read-only view", "this is archived data", "filters are active".',
403
+ 'The `AllVariants` story wraps this in a grey-100 backdrop so the transparent background is visible against white.',
404
+ ].join(' '));
405
+ export const Warning = withDescription({
28
406
  args: {
29
407
  level: BANNER_LEVEL.WARNING,
30
- text: 'This action may affect student records. Please review before proceeding.',
408
+ text: 'The marksheet has unsaved entries. Review before leaving to avoid data loss.',
31
409
  },
32
- };
33
- export const Destructive = {
410
+ parameters: {
411
+ docs: {
412
+ source: {
413
+ language: 'tsx',
414
+ code: `
415
+ import { Banner } from '@arbor-education/design-system.components';
416
+
417
+ function BannerWarningExample() {
418
+ return (
419
+ <Banner
420
+ level={Banner.Level.WARNING}
421
+ text="The marksheet has unsaved entries. Review before leaving to avoid data loss."
422
+ />
423
+ );
424
+ }
425
+
426
+ export default BannerWarningExample;
427
+ `.trim(),
428
+ },
429
+ },
430
+ },
431
+ }, [
432
+ '**Warning (amber)** — something is risky or incomplete, but the user can still proceed.',
433
+ 'Reserve Warning for situations where continuing without action could lead to a recoverable bad outcome:',
434
+ 'unsaved data, missing required setup, outdated records, approaching deadlines.',
435
+ 'The amber colour is attention-grabbing without implying a hard stop.',
436
+ ].join(' '));
437
+ export const Destructive = withDescription({
34
438
  args: {
35
439
  level: BANNER_LEVEL.DESTRUCTIVE,
36
- text: 'Something went wrong. Please contact your system administrator.',
440
+ text: 'This student record is locked because the reporting period has been closed.',
37
441
  },
38
- };
39
- export const WithTitle = {
442
+ parameters: {
443
+ docs: {
444
+ source: {
445
+ language: 'tsx',
446
+ code: `
447
+ import { Banner } from '@arbor-education/design-system.components';
448
+
449
+ function BannerDestructiveExample() {
450
+ return (
451
+ <Banner
452
+ level={Banner.Level.DESTRUCTIVE}
453
+ text="This student record is locked because the reporting period has been closed."
454
+ />
455
+ );
456
+ }
457
+
458
+ export default BannerDestructiveExample;
459
+ `.trim(),
460
+ },
461
+ },
462
+ },
463
+ }, [
464
+ '**Destructive / "Error" (red) — legacy use only.**',
465
+ '',
466
+ 'The source enum is `BANNER_LEVEL.DESTRUCTIVE` but the Confluence design spec calls this variant "Error".',
467
+ 'They are the same thing — if you see "Error banner" in design docs, use `BANNER_LEVEL.DESTRUCTIVE` in code.',
468
+ '',
469
+ 'Per current design guidance, this variant is for **legacy surfaces only**. On new features,',
470
+ 'reach for `WARNING` (risky-but-reversible) or a page-level empty state instead.',
471
+ 'Error banners should be backed by reliable, confirmed system/data state — not manual admin notes.',
472
+ ].join(' '));
473
+ export const WithTitle = withDescription({
40
474
  args: {
41
475
  level: BANNER_LEVEL.INFO,
42
- title: 'Important Update',
43
- text: 'A new version of the system is available. Refresh the page to apply updates.',
476
+ title: 'Session expiring soon',
477
+ text: 'Your session will expire in 10 minutes. Save your work to avoid losing changes.',
44
478
  },
45
- };
46
- export const WithButton = {
479
+ parameters: {
480
+ docs: {
481
+ source: {
482
+ language: 'tsx',
483
+ code: `
484
+ import { Banner } from '@arbor-education/design-system.components';
485
+
486
+ function BannerWithTitleExample() {
487
+ return (
488
+ <Banner
489
+ level={Banner.Level.INFO}
490
+ title="Session expiring soon"
491
+ text="Your session will expire in 10 minutes. Save your work to avoid losing changes."
492
+ />
493
+ );
494
+ }
495
+
496
+ export default BannerWithTitleExample;
497
+ `.trim(),
498
+ },
499
+ },
500
+ },
501
+ }, [
502
+ 'The canonical structure when both a heading and body copy are needed.',
503
+ 'The `title` renders as an `<h3 class="ds-banner__title">` — keep it under 8 words.',
504
+ 'Use a short noun phrase, not a full sentence. The body copy fills in the detail.',
505
+ 'When title and text are both present, the title acts as a visual anchor for users scanning the page.',
506
+ ].join(' '));
507
+ export const WithButton = withDescription({
47
508
  args: {
48
509
  level: BANNER_LEVEL.WARNING,
49
- text: 'You have unsaved changes that will be lost.',
50
- buttonText: 'Undo changes',
51
- buttonOnClick: () => { console.log('Banner button clicked'); },
510
+ text: "The census submission deadline is Friday 17 April at 5pm. Don't leave it to the last minute.",
511
+ buttonText: 'Review exclusions',
512
+ buttonOnClick: fn(),
52
513
  },
53
- };
54
- export const WithTitleAndButton = {
514
+ parameters: {
515
+ docs: {
516
+ source: {
517
+ language: 'tsx',
518
+ code: `
519
+ import { Banner } from '@arbor-education/design-system.components';
520
+
521
+ function BannerWithButtonExample() {
522
+ return (
523
+ <Banner
524
+ level={Banner.Level.WARNING}
525
+ text="The census submission deadline is Friday 17 April at 5pm. Don't leave it to the last minute."
526
+ buttonText="Review exclusions"
527
+ buttonOnClick={() => {}}
528
+ />
529
+ );
530
+ }
531
+
532
+ export default BannerWithButtonExample;
533
+ `.trim(),
534
+ },
535
+ },
536
+ },
537
+ }, [
538
+ 'A banner with an optional text-link CTA at the trailing edge.',
539
+ 'The button uses `color: inherit`, so its text colour matches the level — amber on Warning, blue on Info, etc.',
540
+ 'Always use an active verb: "Fix settings", "Review exclusions", "Switch to current year".',
541
+ 'Keep button labels short — the button has `text-wrap: nowrap` and the banner does not wrap to a second row.',
542
+ ].join(' '));
543
+ export const WithTitleAndButton = withDescription({
55
544
  args: {
56
545
  level: BANNER_LEVEL.INFO,
57
- title: 'Scheduled Maintenance',
58
- text: 'The system will be unavailable on Saturday 1st March between 2am–4am.',
546
+ title: 'Scheduled maintenance',
547
+ text: 'The system will be unavailable on Saturday 19 April between 2am–4am.',
59
548
  buttonText: 'Learn more',
60
- buttonOnClick: () => { console.log('Banner button clicked'); },
549
+ buttonOnClick: fn(),
61
550
  },
62
- };
63
- export const TitleOnly = {
551
+ parameters: {
552
+ docs: {
553
+ source: {
554
+ language: 'tsx',
555
+ code: `
556
+ import { Banner } from '@arbor-education/design-system.components';
557
+
558
+ function BannerWithTitleAndButtonExample() {
559
+ return (
560
+ <Banner
561
+ level={Banner.Level.INFO}
562
+ title="Scheduled maintenance"
563
+ text="The system will be unavailable on Saturday 19 April between 2am–4am."
564
+ buttonText="Learn more"
565
+ buttonOnClick={() => {}}
566
+ />
567
+ );
568
+ }
569
+
570
+ export default BannerWithTitleAndButtonExample;
571
+ `.trim(),
572
+ },
573
+ },
574
+ },
575
+ }, [
576
+ 'The full Banner structure: icon + title + body + CTA.',
577
+ 'This is the maximum content a banner should carry. If you find yourself needing more,',
578
+ 'consider a Modal — the banner flex layout is `row nowrap` and will not wrap to a second line.',
579
+ 'The CTA button inherits the level text colour and sits flush to the trailing edge.',
580
+ ].join(' '));
581
+ export const TitleOnly = withDescription({
64
582
  args: {
65
583
  level: BANNER_LEVEL.NEUTRAL,
66
- title: 'No results found for the selected filters.',
584
+ title: 'No students match the selected filters.',
67
585
  },
68
- };
69
- export const CustomIcon = {
586
+ parameters: {
587
+ docs: {
588
+ source: {
589
+ language: 'tsx',
590
+ code: `
591
+ import { Banner } from '@arbor-education/design-system.components';
592
+
593
+ function BannerTitleOnlyExample() {
594
+ return (
595
+ <Banner
596
+ level={Banner.Level.NEUTRAL}
597
+ title="No students match the selected filters."
598
+ />
599
+ );
600
+ }
601
+
602
+ export default BannerTitleOnlyExample;
603
+ `.trim(),
604
+ },
605
+ },
606
+ },
607
+ }, [
608
+ 'A banner with only a `title` — no body copy.',
609
+ 'Valid for situations where the heading is self-explanatory and body text would be redundant.',
610
+ 'If the message requires any clarification or a call-to-action, add `text` and/or `buttonText`.',
611
+ ].join(' '));
612
+ export const TextOnly = withDescription({
70
613
  args: {
71
614
  level: BANNER_LEVEL.INFO,
72
- icon: 'circle-check',
73
- text: 'Your changes have been saved successfully.',
615
+ text: 'The census submission deadline is Friday 17 April at 5pm.',
74
616
  },
75
- };
76
- export const HideIcon = {
617
+ parameters: {
618
+ docs: {
619
+ source: {
620
+ language: 'tsx',
621
+ code: `
622
+ import { Banner } from '@arbor-education/design-system.components';
623
+
624
+ function BannerTextOnlyExample() {
625
+ return (
626
+ <Banner
627
+ level={Banner.Level.INFO}
628
+ text="The census submission deadline is Friday 17 April at 5pm."
629
+ />
630
+ );
631
+ }
632
+
633
+ export default BannerTextOnlyExample;
634
+ `.trim(),
635
+ },
636
+ },
637
+ },
638
+ }, [
639
+ 'A banner with body copy only — no title heading.',
640
+ 'Appropriate for short, self-contained messages that do not need a heading to orient the user.',
641
+ 'When the message is 1 clear sentence, `title` is optional overhead.',
642
+ ].join(' '));
643
+ export const CustomIcon = withDescription({
644
+ args: {
645
+ level: BANNER_LEVEL.INFO,
646
+ icon: 'lock',
647
+ title: 'Record locked',
648
+ text: 'This student record is read-only. The reporting period for this academic year has been closed.',
649
+ },
650
+ parameters: {
651
+ docs: {
652
+ source: {
653
+ language: 'tsx',
654
+ code: `
655
+ import { Banner } from '@arbor-education/design-system.components';
656
+
657
+ function BannerCustomIconExample() {
658
+ return (
659
+ <Banner
660
+ level={Banner.Level.INFO}
661
+ icon="lock"
662
+ title="Record locked"
663
+ text="This student record is read-only. The reporting period for this academic year has been closed."
664
+ />
665
+ );
666
+ }
667
+
668
+ export default BannerCustomIconExample;
669
+ `.trim(),
670
+ },
671
+ },
672
+ },
673
+ }, [
674
+ 'Override the default icon with a more specific metaphor.',
675
+ 'Here `lock` (instead of the default `info`) communicates the locked-record context at a glance.',
676
+ 'The icon colour still comes from the level token — swapping the icon shape does not change the colour.',
677
+ 'Good overrides: `lock` / `lock-open` for access states, `circle-check` for confirmed states,',
678
+ '`sparkles` for AI-assisted features, `date` for deadline-related messages.',
679
+ ].join(' '));
680
+ export const HideIcon = withDescription({
77
681
  args: {
78
682
  level: BANNER_LEVEL.WARNING,
79
683
  hideIcon: true,
80
- text: 'This banner has its icon hidden.',
684
+ text: 'You are viewing data for a student who has left this school. Some fields may be read-only.',
81
685
  },
82
- };
83
- export default meta;
686
+ parameters: {
687
+ docs: {
688
+ source: {
689
+ language: 'tsx',
690
+ code: `
691
+ import { Banner } from '@arbor-education/design-system.components';
692
+
693
+ function BannerHideIconExample() {
694
+ return (
695
+ <Banner
696
+ level={Banner.Level.WARNING}
697
+ hideIcon
698
+ text="You are viewing data for a student who has left this school. Some fields may be read-only."
699
+ />
700
+ );
701
+ }
702
+
703
+ export default BannerHideIconExample;
704
+ `.trim(),
705
+ },
706
+ },
707
+ },
708
+ }, [
709
+ 'Set `hideIcon={true}` to completely suppress the icon.',
710
+ 'No space is reserved — the central container expands to fill the full width.',
711
+ 'Use sparingly: the icon provides a quick visual severity signal for users who scan.',
712
+ 'A valid case is when the banner sits in a context where the surrounding chrome already conveys severity,',
713
+ 'and the icon would be visually redundant.',
714
+ ].join(' '));
715
+ export const AllVariants = withDescription({
716
+ render: AllVariantsTemplate,
717
+ parameters: {
718
+ docs: {
719
+ source: {
720
+ language: 'tsx',
721
+ code: `
722
+ import { Banner } from '@arbor-education/design-system.components';
723
+
724
+ function BannerAllVariantsExample() {
725
+ return (
726
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xxlarge)' }}>
727
+ {/* INFO */}
728
+ <Banner
729
+ level={Banner.Level.INFO}
730
+ text="Your session will expire in 10 minutes. Save your work to avoid losing changes."
731
+ />
732
+
733
+ {/* NEUTRAL — wrap in a grey-100 container so transparent background is visible */}
734
+ <div style={{ background: 'var(--color-grey-100)', borderRadius: 'var(--border-radius-small)', padding: 'var(--spacing-large)' }}>
735
+ <Banner
736
+ level={Banner.Level.NEUTRAL}
737
+ text="You are viewing last year's data. Switch to the current academic year to make changes."
738
+ />
739
+ </div>
740
+
741
+ {/* WARNING */}
742
+ <Banner
743
+ level={Banner.Level.WARNING}
744
+ text="The marksheet has unsaved entries. Review before leaving to avoid data loss."
745
+ />
746
+
747
+ {/* DESTRUCTIVE — legacy use only */}
748
+ <Banner
749
+ level={Banner.Level.DESTRUCTIVE}
750
+ text="This student record is locked because the reporting period has been closed."
751
+ />
752
+ </div>
753
+ );
754
+ }
755
+
756
+ export default BannerAllVariantsExample;
757
+ `.trim(),
758
+ },
759
+ },
760
+ },
761
+ }, [
762
+ 'All four variants stacked for visual comparison.',
763
+ '',
764
+ '**Important:** This layout is a Storybook-only reference. Never ship multiple banners on the same surface.',
765
+ 'Pick the one that matters most and surface only that. The Neutral variant is shown against a',
766
+ 'grey-100 backdrop to make its transparent background visible.',
767
+ ].join(' '));
768
+ export const ContentGuidelines = withDescription({
769
+ render: ContentGuidelinesTemplate,
770
+ parameters: {
771
+ docs: {
772
+ source: {
773
+ language: 'tsx',
774
+ code: `
775
+ import { Banner } from '@arbor-education/design-system.components';
776
+
777
+ function BannerContentGuidelinesExample() {
778
+ return (
779
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xxlarge)' }}>
780
+ {/* Ideal — short title, 1–2 line body, active verb CTA */}
781
+ <Banner
782
+ level={Banner.Level.WARNING}
783
+ title="Reporting period closing soon"
784
+ text="The census submission deadline is Friday 17 April at 5pm."
785
+ buttonText="Review exclusions"
786
+ buttonOnClick={() => {}}
787
+ />
788
+
789
+ {/* Too long — paragraph-length body belongs in a Modal, not a Banner */}
790
+ <Banner
791
+ level={Banner.Level.WARNING}
792
+ title="Action required before the reporting period closes"
793
+ text="The census submission window closes at 5pm on Friday 17 April. You must review all exclusion records, verify that attendance data has been finalised for every year group, and confirm that your designated senior leader has approved the submission."
794
+ buttonText="Learn more"
795
+ buttonOnClick={() => {}}
796
+ />
797
+ </div>
798
+ );
799
+ }
800
+
801
+ export default BannerContentGuidelinesExample;
802
+ `.trim(),
803
+ },
804
+ },
805
+ },
806
+ }, [
807
+ 'Content length guidelines: what good looks like vs what should be a Modal instead.',
808
+ '',
809
+ 'The **ideal** banner (top) has a short title, a 1–2 sentence body, and an active-verb CTA.',
810
+ 'The **too long** banner (bottom) carries a multi-sentence paragraph that would be better served by a Modal',
811
+ 'or a dedicated help page. A Banner is a strip, not a notice board.',
812
+ ].join(' '));
813
+ export const DismissiblePattern = withDescription({
814
+ render: DismissibleTemplate,
815
+ parameters: {
816
+ docs: {
817
+ source: {
818
+ language: 'tsx',
819
+ code: `
820
+ import { useState } from 'react';
821
+ import { Banner } from '@arbor-education/design-system.components';
822
+
823
+ function DismissibleBanner() {
824
+ const [visible, setVisible] = useState(true);
825
+
826
+ if (!visible) return null;
827
+
828
+ return (
829
+ <Banner
830
+ level={Banner.Level.WARNING}
831
+ text="Some marksheet entries have not been saved. Please review before leaving the page."
832
+ buttonText="Dismiss"
833
+ buttonOnClick={() => setVisible(false)}
834
+ />
835
+ );
836
+ }
837
+
838
+ export default DismissibleBanner;
839
+ `.trim(),
840
+ },
841
+ },
842
+ },
843
+ }, [
844
+ 'The recommended consumer pattern for a dismissible banner.',
845
+ '',
846
+ 'Banner has **no built-in dismiss** — it is intentionally persistent so it stays visible until',
847
+ 'the underlying condition is resolved. When your requirement calls for user-dismissal, wrap Banner',
848
+ 'in `useState` + conditional render in your own component. The CTA button triggers `setVisible(false)`.',
849
+ '',
850
+ 'If the dismissed state should persist across page reloads, store the flag in `localStorage`',
851
+ 'or your application state layer — the Banner component itself has no opinion about persistence.',
852
+ ].join(' '));
853
+ export const LegacyErrorWarning = withDescription({
854
+ args: {
855
+ level: BANNER_LEVEL.DESTRUCTIVE,
856
+ icon: 'circle-alert',
857
+ title: 'Submission failed',
858
+ text: 'The exclusion could not be submitted. Contact your system administrator if this problem persists.',
859
+ buttonText: 'Fix settings',
860
+ buttonOnClick: fn(),
861
+ },
862
+ parameters: {
863
+ docs: {
864
+ source: {
865
+ language: 'tsx',
866
+ code: `
867
+ import { Banner } from '@arbor-education/design-system.components';
868
+
869
+ function BannerLegacyErrorWarningExample() {
870
+ return (
871
+ /* Legacy use only — prefer Banner.Level.WARNING on new surfaces */
872
+ <Banner
873
+ level={Banner.Level.DESTRUCTIVE}
874
+ icon="circle-alert"
875
+ title="Submission failed"
876
+ text="The exclusion could not be submitted. Contact your system administrator if this problem persists."
877
+ buttonText="Fix settings"
878
+ buttonOnClick={() => {}}
879
+ />
880
+ );
881
+ }
882
+
883
+ export default BannerLegacyErrorWarningExample;
884
+ `.trim(),
885
+ },
886
+ },
887
+ },
888
+ }, [
889
+ '**Legacy use only.** This story demonstrates the `DESTRUCTIVE` ("Error") variant.',
890
+ '',
891
+ 'Per design guidance: Error banners should only appear on **legacy surfaces**.',
892
+ 'For new alerts on modern pages, prefer:',
893
+ '',
894
+ '- `WARNING` — for risky-but-reversible situations ("some data has not been saved")',
895
+ '- Page-level empty states — when there is nothing to show due to an error condition',
896
+ '- [`Toast`](?path=/docs/components-toast--docs) — for ephemeral success/error feedback after a user action',
897
+ '',
898
+ 'Error banners must be backed by reliable, confirmed system state — not advisory text added manually.',
899
+ ].join(' '));
84
900
  //# sourceMappingURL=Banner.stories.js.map