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