@arbor-education/design-system.components 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
  2. package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
  3. package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
  4. package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
  5. package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
  6. package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
  7. package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
  8. package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
  9. package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
  10. package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
  11. package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
  12. package/.gather/gather.yaml +9 -0
  13. package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
  14. package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
  15. package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
  16. package/.gather/skills/analyze-design/meta.md +4 -0
  17. package/{.claude → .gather}/skills/create-page/README.md +5 -0
  18. package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
  19. package/.gather/skills/create-page/meta.md +4 -0
  20. package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
  21. package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
  22. package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
  23. package/.gather/skills/map-legacy/meta.md +4 -0
  24. package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
  25. package/.gather/skills/migrate-page/meta.md +4 -0
  26. package/.gather/skills/write-stories/README.md +157 -0
  27. package/.gather/skills/write-stories/SKILL.md +841 -0
  28. package/.gather/skills/write-stories/meta.md +4 -0
  29. package/.ralph/storybook-upgrade/knowledge.md +308 -0
  30. package/.ralph/storybook-upgrade/prd.json +777 -0
  31. package/.ralph/storybook-upgrade/progress.md +342 -0
  32. package/.storybook/DocsTemplate.tsx +122 -0
  33. package/.storybook/preview.ts +40 -0
  34. package/.stylelintignore +2 -0
  35. package/CHANGELOG.md +12 -0
  36. package/{.claude/component-library.md → component-library.md} +27 -10
  37. package/dist/components/badge/Badge.stories.d.ts +85 -6
  38. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  39. package/dist/components/badge/Badge.stories.js +626 -27
  40. package/dist/components/badge/Badge.stories.js.map +1 -1
  41. package/dist/components/banner/Banner.stories.d.ts +129 -63
  42. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  43. package/dist/components/banner/Banner.stories.js +855 -39
  44. package/dist/components/banner/Banner.stories.js.map +1 -1
  45. package/dist/components/button/Button.stories.d.ts +148 -8
  46. package/dist/components/button/Button.stories.d.ts.map +1 -1
  47. package/dist/components/button/Button.stories.js +1089 -80
  48. package/dist/components/button/Button.stories.js.map +1 -1
  49. package/dist/components/datePicker/DatePicker.d.ts +1 -0
  50. package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
  51. package/dist/components/datePicker/DatePicker.js +2 -2
  52. package/dist/components/datePicker/DatePicker.js.map +1 -1
  53. package/dist/components/datePicker/DatePicker.stories.d.ts +1 -0
  54. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  55. package/dist/components/dot/Dot.stories.d.ts +46 -11
  56. package/dist/components/dot/Dot.stories.d.ts.map +1 -1
  57. package/dist/components/dot/Dot.stories.js +504 -15
  58. package/dist/components/dot/Dot.stories.js.map +1 -1
  59. package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
  60. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  61. package/dist/components/dropdown/Dropdown.stories.js +769 -17
  62. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  63. package/dist/components/formField/FormField.stories.d.ts +95 -35
  64. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  65. package/dist/components/formField/FormField.stories.js +1174 -69
  66. package/dist/components/formField/FormField.stories.js.map +1 -1
  67. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
  68. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  69. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
  70. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  71. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
  72. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
  73. package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
  74. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
  75. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
  76. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  77. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
  78. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  79. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
  80. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  81. package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
  82. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
  83. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
  84. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  85. package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
  86. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  87. package/dist/components/heading/Heading.stories.d.ts +449 -50
  88. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  89. package/dist/components/heading/Heading.stories.js +536 -60
  90. package/dist/components/heading/Heading.stories.js.map +1 -1
  91. package/dist/components/icon/Icon.stories.d.ts +81 -10
  92. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  93. package/dist/components/icon/Icon.stories.js +979 -8
  94. package/dist/components/icon/Icon.stories.js.map +1 -1
  95. package/dist/components/pill/Pill.stories.d.ts +71 -19
  96. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  97. package/dist/components/pill/Pill.stories.js +573 -14
  98. package/dist/components/pill/Pill.stories.js.map +1 -1
  99. package/dist/components/progress/Progress.stories.d.ts +75 -298
  100. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  101. package/dist/components/progress/Progress.stories.js +449 -52
  102. package/dist/components/progress/Progress.stories.js.map +1 -1
  103. package/dist/components/separator/Separator.stories.d.ts +58 -5
  104. package/dist/components/separator/Separator.stories.d.ts.map +1 -1
  105. package/dist/components/separator/Separator.stories.js +443 -4
  106. package/dist/components/separator/Separator.stories.js.map +1 -1
  107. package/dist/components/table/Table.d.ts +7 -0
  108. package/dist/components/table/Table.d.ts.map +1 -1
  109. package/dist/components/table/Table.js +9 -0
  110. package/dist/components/table/Table.js.map +1 -1
  111. package/dist/components/table/Table.stories.d.ts +1 -0
  112. package/dist/components/table/Table.stories.d.ts.map +1 -1
  113. package/dist/components/table/Table.stories.js +87 -0
  114. package/dist/components/table/Table.stories.js.map +1 -1
  115. package/dist/components/table/Table.test.js +49 -1
  116. package/dist/components/table/Table.test.js.map +1 -1
  117. package/dist/components/table/cellEditors/DateCellEditor.d.ts +3 -0
  118. package/dist/components/table/cellEditors/DateCellEditor.d.ts.map +1 -0
  119. package/dist/components/table/cellEditors/DateCellEditor.js +13 -0
  120. package/dist/components/table/cellEditors/DateCellEditor.js.map +1 -0
  121. package/dist/components/table/cellEditors/DateCellEditor.test.d.ts +2 -0
  122. package/dist/components/table/cellEditors/DateCellEditor.test.d.ts.map +1 -0
  123. package/dist/components/table/cellEditors/DateCellEditor.test.js +81 -0
  124. package/dist/components/table/cellEditors/DateCellEditor.test.js.map +1 -0
  125. package/dist/components/tag/Tag.stories.d.ts +116 -5
  126. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  127. package/dist/components/tag/Tag.stories.js +581 -28
  128. package/dist/components/tag/Tag.stories.js.map +1 -1
  129. package/dist/index.css +8 -1
  130. package/dist/index.css.map +1 -1
  131. package/dist/index.d.ts +0 -5
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +0 -5
  134. package/dist/index.js.map +1 -1
  135. package/eslint.config.mts +5 -1
  136. package/package.json +3 -3
  137. package/src/components/badge/Badge.stories.tsx +869 -42
  138. package/src/components/banner/Banner.stories.tsx +1081 -63
  139. package/src/components/button/Button.stories.tsx +1394 -99
  140. package/src/components/datePicker/DatePicker.tsx +3 -0
  141. package/src/components/dot/Dot.stories.tsx +723 -32
  142. package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
  143. package/src/components/formField/FormField.stories.tsx +1522 -105
  144. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
  145. package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
  146. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
  147. package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
  148. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
  149. package/src/components/heading/Heading.stories.tsx +752 -120
  150. package/src/components/icon/Icon.stories.tsx +1446 -12
  151. package/src/components/pill/Pill.stories.tsx +867 -21
  152. package/src/components/progress/Progress.stories.tsx +625 -58
  153. package/src/components/separator/Separator.stories.tsx +730 -8
  154. package/src/components/separator/separator.scss +12 -3
  155. package/src/components/table/Table.stories.tsx +102 -0
  156. package/src/components/table/Table.test.tsx +82 -3
  157. package/src/components/table/Table.tsx +9 -0
  158. package/src/components/table/cellEditors/DateCellEditor.test.tsx +109 -0
  159. package/src/components/table/cellEditors/DateCellEditor.tsx +27 -0
  160. package/src/components/tag/Tag.stories.tsx +755 -53
  161. package/src/index.ts +0 -5
  162. package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
  163. package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
  164. package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
  165. package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
  166. package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
  167. package/.claude/figma-assessment-7154-58899.md +0 -404
  168. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
  169. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
  170. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
  171. package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
  172. package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
  173. /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
  174. /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
  175. /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
  176. /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
  177. /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
@@ -1,9 +1,134 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
3
+ import { useState, useRef } from 'react';
1
4
  import { Button } from './Button';
5
+ // ---------------------------------------------------------------------------
6
+ // Component description — built as a joined array to avoid no-useless-escape
7
+ // on backtick code spans inside template literals.
8
+ //
9
+ // The global DocsTemplate renders the description BEFORE the Primary story
10
+ // and renders parameters.docs.relatedComponents as a markdown block at the
11
+ // very bottom of the page (after Stories) — so "Related components" does NOT
12
+ // belong in this description.
13
+ // ---------------------------------------------------------------------------
14
+ const DESCRIPTION_INTRO = [
15
+ 'The **Button** is the primary interactive control in the Arbor design system. It wraps a native',
16
+ '`<button>` element with `forwardRef` support, consistent design tokens, icon slots, and a full set',
17
+ 'of variant and state modifiers.',
18
+ ].join('\n');
19
+ const USAGE_GUIDANCE = [
20
+ '### When to use',
21
+ '',
22
+ '| Variant | Use when |',
23
+ '|---|---|',
24
+ '| `primary` | The single most important action in a page section — Save, Confirm, Continue |',
25
+ '| `secondary` | A supporting or alternative action — Cancel, Back, Edit |',
26
+ '| `tertiary` | A quiet, low-priority action — Clear filters, Reset, optional utility actions |',
27
+ '| `primary-destructive` | An irreversible, high-impact action — Delete permanently, Expel student |',
28
+ '| `secondary-destructive` | A softer destructive action that may have a confirmation step — Archive record |',
29
+ '| `text-link` | An inline contextual action embedded in a sentence or label |',
30
+ '| `dropdown` | A button styled as a dropdown trigger — uses `justify-content: space-between` so a trailing chevron icon aligns to the right edge. Used internally by `SelectDropdown`, `ColourPickerDropdown`, and `UserDropdown`, and available to consumers for custom dropdown triggers. |',
31
+ '',
32
+ '---',
33
+ '',
34
+ '### When NOT to use',
35
+ '',
36
+ '| Instead of Button... | Use... | Why |',
37
+ '|---|---|---|',
38
+ '| URL navigation | `<a>` tag | Semantic: links navigate, buttons act |',
39
+ '| Tab switching | [`Tabs`](?path=/docs/components-tabs--docs) | Correct `role="tab"` semantics |',
40
+ '| Filter selection | [`Pill`](?path=/docs/components-pill--docs) | Purpose-built toggle for filter UI |',
41
+ '| Boolean on/off | [`Toggle`](?path=/docs/components-toggle--docs) or [`CheckboxInput`](?path=/docs/components-formfield-inputs-checkbox--docs) | Communicates checked state via ARIA |',
42
+ '',
43
+ '---',
44
+ '',
45
+ '### Design guidance',
46
+ '',
47
+ '- **One primary per section** — using multiple primary buttons in the same form or toolbar dilutes',
48
+ ' the visual hierarchy. Pick the single most important action.',
49
+ '- **Destructive tier** — use `primary-destructive` for permanent, unrecoverable operations (delete a',
50
+ ' student record, permanently remove data). Use `secondary-destructive` for softer operations that',
51
+ ' may have a confirmation step or are partially reversible (archive, suspend).',
52
+ '- **Tertiary** is for quiet utility actions. Do not use it as a "Cancel" button next to a Primary —',
53
+ ' use `secondary` for Cancel so the hierarchy reads clearly.',
54
+ '- **text-link** is for inline contextual actions. Do not use it as a Cancel in a confirm/cancel row.',
55
+ ].join('\n');
56
+ const DEVELOPER_NOTES = [
57
+ '### Critical gotchas',
58
+ '',
59
+ '#### 1. The `type` attribute trap',
60
+ 'The `type` prop is **deliberately omitted** from the component\'s prop spread',
61
+ '(`Omit<ButtonHTMLAttributes<HTMLButtonElement>, \'type\' | \'onClick\'>` on `Button.tsx` line 24).',
62
+ 'This means the underlying `<button>` has no `type` attribute, and browsers default to',
63
+ '`type="submit"`. If this button lives inside a `<form>`, clicking it **will submit the form**',
64
+ 'unless you explicitly pass `type="button"`.',
65
+ '',
66
+ '```tsx',
67
+ '// BAD — will submit any ancestor form',
68
+ '<Button variant="secondary" onClick={handleCancel}>Cancel</Button>',
69
+ '',
70
+ '// GOOD',
71
+ '<Button variant="secondary" type="button" onClick={handleCancel}>Cancel</Button>',
72
+ '```',
73
+ '',
74
+ '#### 2. Icon-only accessibility',
75
+ 'When `children` is omitted, the component enters icon-only mode (`ds-button--icon-only`). The icon',
76
+ 'name becomes the default screen reader text, but icon names like `"3-dot"` or `"x"` are not',
77
+ 'meaningful in context. Always provide `iconLeftScreenReaderText` or `iconRightScreenReaderText`.',
78
+ '',
79
+ '```tsx',
80
+ '// BAD — screen reader says "3-dot"',
81
+ '<Button variant="secondary" iconRightName="3-dot" />',
82
+ '',
83
+ '// GOOD',
84
+ '<Button variant="secondary" iconRightName="3-dot" iconRightScreenReaderText="More actions" />',
85
+ '```',
86
+ '',
87
+ '---',
88
+ '',
89
+ '### Accessibility',
90
+ '',
91
+ '- Native `<button>` — focusable with Tab, activated with Space or Enter',
92
+ '- Focus ring: 3px outline using `--focus-border` and `--color-brand-500` via `--focus-color-focus`',
93
+ '- `disabled` removes the button from tab order AND blocks pointer events (`opacity: 0.5; cursor: not-allowed; pointer-events: none`)',
94
+ '- Icon-only mode: provide `iconLeftScreenReaderText` / `iconRightScreenReaderText` for meaningful labels',
95
+ '- The `error` prop is **purely visual** (adds a red border) — it carries no ARIA semantics.',
96
+ ' The consumer is still responsible for communicating the error state to assistive tech — e.g.',
97
+ ' setting `aria-invalid` on the associated input or wiring `aria-describedby` to an error message.',
98
+ '',
99
+ '---',
100
+ '',
101
+ '### TypeScript types',
102
+ '',
103
+ '```ts',
104
+ "import { Button } from '@arbor-education/design-system.components';",
105
+ '',
106
+ 'function MyButton(props: Button.Props) { ... }',
107
+ '```',
108
+ '',
109
+ '| Type | Description |',
110
+ '|---|---|',
111
+ '| `Button.Props` | Full props interface |',
112
+ "| `Button.Variant` | `'primary' \\| 'secondary' \\| 'tertiary' \\| 'primary-destructive' \\| 'secondary-destructive' \\| 'text-link' \\| 'dropdown'` |",
113
+ "| `Button.Size` | `'M' \\| 'S'` |",
114
+ ].join('\n');
115
+ const RELATED_COMPONENTS = [
116
+ '## Related components',
117
+ '',
118
+ '[Icon](?path=/docs/components-icon--docs) · [Dropdown](?path=/docs/components-dropdown--docs) · [Pill](?path=/docs/components-pill--docs) · [Tabs](?path=/docs/components-tabs--docs) · [Toggle](?path=/docs/components-toggle--docs)',
119
+ ].join('\n');
120
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
121
+ function ButtonDocsPage() {
122
+ 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 })] }));
123
+ }
2
124
  const meta = {
3
125
  title: 'Components/Button',
4
126
  component: Button,
5
127
  parameters: {
6
128
  layout: 'centered',
129
+ docs: {
130
+ page: ButtonDocsPage,
131
+ },
7
132
  },
8
133
  tags: ['autodocs'],
9
134
  argTypes: {
@@ -18,134 +143,1018 @@ const meta = {
18
143
  'text-link',
19
144
  'dropdown',
20
145
  ],
21
- description: 'Button variant',
146
+ description: [
147
+ 'Visual style and semantic hierarchy of the button.',
148
+ '`primary` — main CTA (brand green background, white text).',
149
+ '`secondary` — supporting action (outlined, brand-coloured text).',
150
+ '`tertiary` — quiet utility action (text-only style).',
151
+ '`primary-destructive` — irreversible destructive (red background, white text).',
152
+ '`secondary-destructive` — softer destructive (red text on white, inverts on hover).',
153
+ '`text-link` — inline contextual action (underlined text, no box).',
154
+ '`dropdown` — styled as a dropdown trigger (chevron-friendly layout); used internally by SelectDropdown and friends, also available to consumers.',
155
+ ].join(' '),
156
+ table: {
157
+ type: { summary: "'primary' | 'secondary' | 'tertiary' | 'primary-destructive' | 'secondary-destructive' | 'text-link' | 'dropdown'" },
158
+ defaultValue: { summary: "'primary'" },
159
+ },
22
160
  },
23
161
  size: {
24
162
  control: 'select',
25
163
  options: ['M', 'S'],
26
- description: 'Button size',
164
+ description: [
165
+ 'Height of the button.',
166
+ '`M` resolves to `--size-medium` (2.25rem / 36px).',
167
+ '`S` resolves to `--size-small` (2rem / 32px).',
168
+ 'Note: the tertiary variant uses `--button-small-tertiary-*` tokens for both sizes',
169
+ 'because `--button-medium-tertiary-*` tokens do not exist — this is a known token gap.',
170
+ ].join(' '),
171
+ table: {
172
+ type: { summary: "'M' | 'S'" },
173
+ defaultValue: { summary: "'M'" },
174
+ },
175
+ },
176
+ children: {
177
+ control: 'text',
178
+ description: [
179
+ 'Button label content. Accepts any `React.ReactNode`.',
180
+ 'When omitted (and at least one icon prop is provided), the button enters',
181
+ 'icon-only mode (`ds-button--icon-only`) — square aspect ratio, no padding compensation needed.',
182
+ 'Always pair icon-only mode with descriptive `iconLeftScreenReaderText` / `iconRightScreenReaderText`.',
183
+ ].join(' '),
184
+ table: {
185
+ type: { summary: 'React.ReactNode' },
186
+ },
27
187
  },
28
188
  disabled: {
29
189
  control: 'boolean',
30
- description: 'Disable the button',
190
+ description: [
191
+ 'Native HTML disabled attribute.',
192
+ 'Applies `opacity: 0.5`, `cursor: not-allowed`, and `pointer-events: none`.',
193
+ 'Removes the button from tab order. Do NOT simulate disabled with manual opacity — use this prop.',
194
+ ].join(' '),
195
+ table: {
196
+ type: { summary: 'boolean' },
197
+ defaultValue: { summary: 'false' },
198
+ },
31
199
  },
32
- children: {
200
+ error: {
201
+ control: 'boolean',
202
+ description: [
203
+ 'Adds a red border (`--color-semantic-destructive-500`) via `ds-button--error`.',
204
+ 'Used by the `dropdown` variant to indicate form validation failure on a SelectDropdown field.',
205
+ 'This is purely visual — it adds no ARIA semantics.',
206
+ 'Consumers are responsible for communicating the error state to assistive tech (e.g. `aria-invalid` on the associated input, or `aria-describedby` pointing to an error message).',
207
+ ].join(' '),
208
+ table: {
209
+ type: { summary: 'boolean' },
210
+ defaultValue: { summary: 'false' },
211
+ },
212
+ },
213
+ hasHorizontalPadding: {
214
+ control: 'boolean',
215
+ description: [
216
+ 'When `false`, removes horizontal padding via `ds-button--no-horizontal-padding`.',
217
+ 'Use for flush-mounted buttons — e.g. a `text-link` variant sitting inline in a form label',
218
+ 'where you want the text to align with surrounding content without a left indent.',
219
+ 'Do not use as a general layout escape hatch.',
220
+ ].join(' '),
221
+ table: {
222
+ type: { summary: 'boolean' },
223
+ defaultValue: { summary: 'true' },
224
+ },
225
+ },
226
+ iconLeftName: {
227
+ control: 'select',
228
+ options: [
229
+ undefined,
230
+ 'plus',
231
+ 'pencil',
232
+ 'save',
233
+ 'trash',
234
+ 'download',
235
+ 'upload',
236
+ 'mail',
237
+ 'search',
238
+ 'info',
239
+ 'check',
240
+ 'x',
241
+ 'arrow-left',
242
+ 'arrow-right',
243
+ 'settings',
244
+ 'loader',
245
+ '3-dot',
246
+ 'user',
247
+ 'send',
248
+ ],
249
+ description: [
250
+ 'Name of an icon to render **before** the button label at 16px.',
251
+ 'Renders an `<Icon>` component with `size={16}`.',
252
+ 'Screen reader text defaults to the icon name — always override with `iconLeftScreenReaderText`',
253
+ 'when the icon name is not meaningful in context (e.g. `"3-dot"`, `"x"`).',
254
+ ].join(' '),
255
+ table: {
256
+ type: { summary: 'IconName' },
257
+ defaultValue: { summary: 'undefined' },
258
+ },
259
+ },
260
+ iconLeftScreenReaderText: {
261
+ control: 'text',
262
+ description: [
263
+ 'Screen reader label for the left icon.',
264
+ 'Defaults to the icon name when omitted.',
265
+ 'In icon-only mode this becomes the accessible label for the entire button — make it descriptive.',
266
+ ].join(' '),
267
+ table: {
268
+ type: { summary: 'string' },
269
+ },
270
+ },
271
+ iconRightName: {
272
+ control: 'select',
273
+ options: [
274
+ undefined,
275
+ 'plus',
276
+ 'pencil',
277
+ 'save',
278
+ 'trash',
279
+ 'download',
280
+ 'upload',
281
+ 'mail',
282
+ 'search',
283
+ 'info',
284
+ 'check',
285
+ 'x',
286
+ 'arrow-right',
287
+ 'chevron-down',
288
+ 'chevron-up',
289
+ 'settings',
290
+ 'loader',
291
+ '3-dot',
292
+ 'external-link',
293
+ 'send',
294
+ ],
295
+ description: [
296
+ 'Name of an icon to render **after** the button label at 16px.',
297
+ 'Renders an `<Icon>` component with `size={16}`.',
298
+ 'Screen reader text defaults to the icon name — always override with `iconRightScreenReaderText`.',
299
+ ].join(' '),
300
+ table: {
301
+ type: { summary: 'IconName' },
302
+ defaultValue: { summary: 'undefined' },
303
+ },
304
+ },
305
+ iconRightScreenReaderText: {
33
306
  control: 'text',
34
- description: 'Button text',
307
+ description: [
308
+ 'Screen reader label for the right icon.',
309
+ 'Defaults to the icon name when omitted.',
310
+ 'In icon-only mode this is the accessible label — always set it explicitly.',
311
+ ].join(' '),
312
+ table: {
313
+ type: { summary: 'string' },
314
+ },
315
+ },
316
+ borderless: {
317
+ control: 'boolean',
318
+ description: [
319
+ 'Removes the border entirely via `ds-button--borderless`.',
320
+ 'For toolbar and icon-button contexts where a visible border would be visually noisy.',
321
+ 'Pairs well with `variant="tertiary"` and an icon.',
322
+ ].join(' '),
323
+ table: {
324
+ type: { summary: 'boolean' },
325
+ defaultValue: { summary: 'false' },
326
+ },
35
327
  },
36
328
  onClick: {
37
- action: 'clicked',
329
+ description: [
330
+ 'Click handler. Receives the `React.MouseEvent` as the first argument.',
331
+ 'Additional spread arguments are accepted (non-standard) to support legacy callers.',
332
+ 'Wire to the Controls action logger with `fn()` in meta.',
333
+ ].join(' '),
334
+ table: {
335
+ type: { summary: '(event: React.MouseEvent<HTMLButtonElement>, ...otherArgs: unknown[]) => void' },
336
+ },
337
+ },
338
+ className: {
339
+ control: 'text',
340
+ description: 'Additional CSS class names appended to the button element. Use sparingly — prefer variant and size props.',
341
+ table: {
342
+ type: { summary: 'string' },
343
+ defaultValue: { summary: "''" },
344
+ },
38
345
  },
39
346
  },
40
347
  };
41
348
  export default meta;
42
- // Primary button
43
- export const Primary = {
349
+ // ---------------------------------------------------------------------------
350
+ // Helper: attach a per-story description to docs
351
+ // ---------------------------------------------------------------------------
352
+ const withDescription = (story, description) => ({
353
+ ...story,
354
+ parameters: {
355
+ ...story.parameters,
356
+ docs: { ...story.parameters?.docs, description: { story: description } },
357
+ },
358
+ });
359
+ // ---------------------------------------------------------------------------
360
+ // Stateful template components
361
+ // Named components avoid hooks-in-callbacks lint issues (react-hooks plugin not
362
+ // configured — do NOT add eslint-disable comments for it).
363
+ // ---------------------------------------------------------------------------
364
+ const ClickTrackingTemplate = () => {
365
+ const [count, setCount] = useState(0);
366
+ return (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'center' }, children: [_jsxs("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: ["Button clicked", ' ', _jsx("strong", { children: count }), ' ', count === 1 ? 'time' : 'times'] }), _jsx(Button, { variant: "primary", type: "button", onClick: () => setCount(c => c + 1), children: "Save attendance" })] }));
367
+ };
368
+ const WithForwardRefTemplate = () => {
369
+ const buttonRef = useRef(null);
370
+ return (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'center' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Click the trigger to programmatically focus the primary button." }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)' }, children: [_jsx(Button, { variant: "secondary", type: "button", onClick: () => buttonRef.current?.focus(), children: "Focus primary button" }), _jsx(Button, { variant: "primary", type: "button", ref: buttonRef, children: "Save attendance" })] })] }));
371
+ };
372
+ const FormTypeAttributeTemplate = () => {
373
+ const [submitted, setSubmitted] = useState(false);
374
+ const [cancelled, setCancelled] = useState(false);
375
+ return (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', minWidth: '340px' }, children: [_jsxs("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: ["Both buttons live inside a form. The Cancel button uses", ' ', _jsx("code", { children: "type=\"button\"" }), ' ', "so it does not submit. The Submit button uses", ' ', _jsx("code", { children: "type=\"submit\"" }), "."] }), submitted && (_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-success-600)' }, children: "Form submitted! (type=\"submit\" fired)" })), cancelled && !submitted && (_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Cancelled \u2014 form was NOT submitted." })), _jsxs("form", { onSubmit: (e) => {
376
+ e.preventDefault();
377
+ setSubmitted(true);
378
+ setCancelled(false);
379
+ }, style: { display: 'flex', gap: 'var(--spacing-large)' }, children: [_jsx(Button, { variant: "secondary", type: "button", onClick: () => {
380
+ setSubmitted(false);
381
+ setCancelled(true);
382
+ }, children: "Cancel" }), _jsx(Button, { variant: "primary", type: "submit", children: "Save attendance" })] })] }));
383
+ };
384
+ const LoadingCompositionTemplate = () => {
385
+ const [loading, setLoading] = useState(false);
386
+ const [done, setDone] = useState(false);
387
+ const handleClick = () => {
388
+ setLoading(true);
389
+ setDone(false);
390
+ setTimeout(() => {
391
+ setLoading(false);
392
+ setDone(true);
393
+ }, 2000);
394
+ };
395
+ return (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'center' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Button has no built-in loading state \u2014 compose it with disabled + a loader icon." }), done && (_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-success-600)' }, children: "Exported successfully!" })), _jsx(Button, { variant: "primary", type: "button", disabled: loading, iconLeftName: loading ? 'loader' : undefined, iconLeftScreenReaderText: loading ? 'Loading, please wait' : undefined, onClick: handleClick, children: loading ? 'Exporting...' : 'Export to CSV' })] }));
396
+ };
397
+ // ---------------------------------------------------------------------------
398
+ // Stories
399
+ // ---------------------------------------------------------------------------
400
+ export const Default = withDescription({
44
401
  args: {
45
402
  variant: 'primary',
46
- children: 'Button Text',
47
403
  size: 'M',
404
+ children: 'Save attendance',
405
+ disabled: false,
406
+ error: false,
407
+ hasHorizontalPadding: true,
408
+ borderless: false,
48
409
  },
49
- };
50
- export const PrimaryIconOnly = {
410
+ render: args => _jsx(Button, { ...args }),
411
+ }, 'The interactive canvas story — every prop is wired to the **Controls** panel. Use the controls to explore variants, sizes, icons, and states. Tip: try setting `iconLeftName` to `"save"` or `"plus"` to see the icon slot in action.');
412
+ export const Primary = withDescription({
51
413
  args: {
52
- ...Primary.args,
53
- children: null,
54
- iconRightName: 'info',
414
+ variant: 'primary',
415
+ size: 'M',
416
+ children: 'Save attendance',
55
417
  },
56
- };
57
- // Secondary button
58
- export const Secondary = {
59
- args: {
60
- ...Primary.args,
61
- variant: 'secondary',
418
+ parameters: {
419
+ docs: {
420
+ source: {
421
+ language: 'tsx',
422
+ code: `
423
+ import { Button } from '@arbor-education/design-system.components';
424
+
425
+ function ButtonPrimaryExample() {
426
+ return <Button variant="primary" size="M">Save attendance</Button>;
427
+ }
428
+
429
+ export default ButtonPrimaryExample;
430
+ `.trim(),
431
+ },
432
+ },
62
433
  },
63
- };
64
- export const SecondaryIconOnly = {
434
+ }, 'The `primary` variant is the main call-to-action. Background resolves to `--button-medium-primary-default-color-background` → `--color-brand-600` (Arbor green). Use at most once per page section. All other actions should be `secondary` or lower in hierarchy.');
435
+ export const Secondary = withDescription({
65
436
  args: {
66
- ...Primary.args,
67
437
  variant: 'secondary',
68
- children: null,
69
- iconRightName: 'info',
438
+ size: 'M',
439
+ children: 'Cancel',
70
440
  },
71
- };
72
- // Tertiary button
73
- export const Tertiary = {
74
- args: {
75
- ...Primary.args,
76
- variant: 'tertiary',
441
+ parameters: {
442
+ docs: {
443
+ source: {
444
+ language: 'tsx',
445
+ code: `
446
+ import { Button } from '@arbor-education/design-system.components';
447
+
448
+ function ButtonSecondaryExample() {
449
+ return <Button variant="secondary" size="M">Cancel</Button>;
450
+ }
451
+
452
+ export default ButtonSecondaryExample;
453
+ `.trim(),
454
+ },
455
+ },
77
456
  },
78
- };
79
- export const TertiaryIconOnly = {
457
+ }, 'The `secondary` variant is for supporting actions — Cancel, Back, Edit. Outlined with brand-coloured text. Use next to a `primary` button to form a confirm/cancel pair. Never use `text-link` as a Cancel button in a row with Primary.');
458
+ export const Tertiary = withDescription({
80
459
  args: {
81
- ...Primary.args,
82
- children: null,
83
- iconRightName: 'info',
84
460
  variant: 'tertiary',
461
+ size: 'M',
462
+ children: 'Clear filters',
85
463
  },
86
- };
87
- // Dropdown button
88
- export const Dropdown = {
89
- args: {
90
- ...Primary.args,
91
- variant: 'dropdown',
464
+ parameters: {
465
+ docs: {
466
+ source: {
467
+ language: 'tsx',
468
+ code: `
469
+ import { Button } from '@arbor-education/design-system.components';
470
+
471
+ function ButtonTertiaryExample() {
472
+ return <Button variant="tertiary" size="M">Clear filters</Button>;
473
+ }
474
+
475
+ export default ButtonTertiaryExample;
476
+ `.trim(),
477
+ },
478
+ },
92
479
  },
93
- };
94
- // Primary destructive button
95
- export const PrimaryDestructive = {
480
+ }, [
481
+ 'The `tertiary` variant is quiet and de-emphasised — for low-priority utility actions like "Clear filters", "Reset", or optional configuration.',
482
+ '',
483
+ '**Known token note:** the tertiary variant uses `--button-small-tertiary-*` tokens for **both** `M` and `S` sizes.',
484
+ '`--button-medium-tertiary-*` tokens do not exist in `tokens.scss`. The visual result is correct and intentional,',
485
+ 'but the token name reflects "small" regardless of the `size` prop. This is a known gap in the token taxonomy.',
486
+ ].join(' '));
487
+ export const PrimaryDestructive = withDescription({
96
488
  args: {
97
- ...Primary.args,
98
489
  variant: 'primary-destructive',
490
+ size: 'M',
491
+ children: 'Delete permanently',
99
492
  },
100
- };
101
- export const PrimaryDestructiveIconOnly = {
102
- args: {
103
- ...Primary.args,
104
- children: null,
105
- iconRightName: 'info',
106
- variant: 'primary-destructive',
493
+ parameters: {
494
+ docs: {
495
+ source: {
496
+ language: 'tsx',
497
+ code: `
498
+ import { Button } from '@arbor-education/design-system.components';
499
+
500
+ function ButtonPrimaryDestructiveExample() {
501
+ return <Button variant="primary-destructive" size="M">Delete permanently</Button>;
502
+ }
503
+
504
+ export default ButtonPrimaryDestructiveExample;
505
+ `.trim(),
506
+ },
507
+ },
107
508
  },
108
- };
109
- // Secondary destructive button
110
- export const SecondaryDestructive = {
509
+ }, 'The `primary-destructive` variant is for irreversible, high-impact actions — permanently deleting a student record, removing an enrolment, expelling a student. Background resolves to `--button-medium-primary-destructive-default-color-background` → `--color-semantic-destructive-500`. Always consider whether a confirmation Modal is warranted before this action executes.');
510
+ export const SecondaryDestructive = withDescription({
111
511
  args: {
112
- ...Primary.args,
113
512
  variant: 'secondary-destructive',
513
+ size: 'M',
514
+ children: 'Archive record',
114
515
  },
115
- };
116
- export const SecondaryDestructiveIconOnly = {
117
- args: {
118
- ...Primary.args,
119
- children: null,
120
- iconRightName: 'info',
121
- variant: 'secondary-destructive',
516
+ parameters: {
517
+ docs: {
518
+ source: {
519
+ language: 'tsx',
520
+ code: `
521
+ import { Button } from '@arbor-education/design-system.components';
522
+
523
+ function ButtonSecondaryDestructiveExample() {
524
+ return <Button variant="secondary-destructive" size="M">Archive record</Button>;
525
+ }
526
+
527
+ export default ButtonSecondaryDestructiveExample;
528
+ `.trim(),
529
+ },
530
+ },
122
531
  },
123
- };
124
- // Text link button
125
- export const TextLink = {
532
+ }, 'The `secondary-destructive` variant is for softer destructive actions — archiving, suspending, removing from a group. Default state: red text on white background with red border. On hover: white text on red background (token-driven inversion, intentional). Use when the action is destructive but may have a confirmation step or is partially reversible.');
533
+ export const TextLink = withDescription({
126
534
  args: {
127
- ...Primary.args,
128
535
  variant: 'text-link',
536
+ size: 'M',
537
+ children: 'View full report',
129
538
  },
130
- };
131
- // Small buttons
132
- export const SmallPrimary = {
539
+ parameters: {
540
+ docs: {
541
+ source: {
542
+ language: 'tsx',
543
+ code: `
544
+ import { Button } from '@arbor-education/design-system.components';
545
+
546
+ function ButtonTextLinkExample() {
547
+ return <Button variant="text-link" size="M">View full report</Button>;
548
+ }
549
+
550
+ export default ButtonTextLinkExample;
551
+ `.trim(),
552
+ },
553
+ },
554
+ },
555
+ }, 'The `text-link` variant renders as underlined text with no box or padding — for inline contextual actions embedded in sentences, labels, or table cells. Do not use `text-link` in a confirm/cancel button row alongside Primary — that breaks affordance hierarchy. For inline placement, combine with `hasHorizontalPadding={false}` to align the text flush with surrounding content.');
556
+ export const Dropdown = withDescription({
557
+ args: {
558
+ variant: 'dropdown',
559
+ size: 'M',
560
+ children: 'Select year group',
561
+ },
562
+ parameters: {
563
+ docs: {
564
+ source: {
565
+ language: 'tsx',
566
+ code: `
567
+ import { Button } from '@arbor-education/design-system.components';
568
+
569
+ function ButtonDropdownExample() {
570
+ return (
571
+ <Button variant="dropdown" size="M" iconRightName="chevron-down" iconRightScreenReaderText="Open dropdown">
572
+ Select year group
573
+ </Button>
574
+ );
575
+ }
576
+
577
+ export default ButtonDropdownExample;
578
+ `.trim(),
579
+ },
580
+ },
581
+ },
582
+ }, [
583
+ 'The `dropdown` variant has `justify-content: space-between` and specific padding designed for use as a',
584
+ 'dropdown trigger — a trailing chevron icon aligns to the right edge automatically. Used internally by',
585
+ '`SelectDropdown`, `ColourPickerDropdown`, and `UserDropdown`, and available to consumers for custom',
586
+ 'dropdown triggers. Pair it with a chevron icon (e.g. `iconRightName="chevron-down"`) for the canonical',
587
+ 'look.',
588
+ ].join(' '));
589
+ export const AllVariants = withDescription({
590
+ render: () => (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Action hierarchy" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap' }, children: [_jsx(Button, { variant: "primary", type: "button", children: "Save attendance" }), _jsx(Button, { variant: "secondary", type: "button", children: "Cancel" }), _jsx(Button, { variant: "tertiary", type: "button", children: "Clear filters" })] })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-destructive-600)' }, children: "Destructive actions" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap' }, children: [_jsx(Button, { variant: "primary-destructive", type: "button", children: "Delete permanently" }), _jsx(Button, { variant: "secondary-destructive", type: "button", children: "Archive record" })] })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Specialised" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap' }, children: [_jsx(Button, { variant: "text-link", type: "button", children: "View full report" }), _jsx(Button, { variant: "dropdown", type: "button", children: "Select year group" })] })] })] })),
591
+ parameters: {
592
+ docs: {
593
+ source: {
594
+ language: 'tsx',
595
+ code: `
596
+ import { Button } from '@arbor-education/design-system.components';
597
+
598
+ function ButtonAllVariantsExample() {
599
+ return (
600
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }}>
601
+ {/* Action hierarchy */}
602
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap' }}>
603
+ <Button variant="primary" type="button">Save attendance</Button>
604
+ <Button variant="secondary" type="button">Cancel</Button>
605
+ <Button variant="tertiary" type="button">Clear filters</Button>
606
+ </div>
607
+ {/* Destructive actions */}
608
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap' }}>
609
+ <Button variant="primary-destructive" type="button">Delete permanently</Button>
610
+ <Button variant="secondary-destructive" type="button">Archive record</Button>
611
+ </div>
612
+ {/* Specialised */}
613
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', flexWrap: 'wrap' }}>
614
+ <Button variant="text-link" type="button">View full report</Button>
615
+ <Button variant="dropdown" type="button">Select year group</Button>
616
+ </div>
617
+ </div>
618
+ );
619
+ }
620
+
621
+ export default ButtonAllVariantsExample;
622
+ `.trim(),
623
+ },
624
+ },
625
+ },
626
+ }, 'All 7 variants at a glance, grouped by hierarchy tier. Action hierarchy (primary / secondary / tertiary), Destructive actions (primary-destructive / secondary-destructive), Specialised (text-link / dropdown). Use this story as a reference for the full visual range of the component.');
627
+ export const AllSizes = withDescription({
628
+ render: () => (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Medium (M) \u2014 2.25rem / 36px via --size-medium" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)', alignItems: 'center' }, children: [_jsx(Button, { variant: "primary", size: "M", type: "button", children: "Save attendance" }), _jsx(Button, { variant: "secondary", size: "M", type: "button", children: "Cancel" }), _jsx(Button, { variant: "tertiary", size: "M", type: "button", children: "Clear filters" })] })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)' }, children: "Small (S) \u2014 2rem / 32px via --size-small" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)', alignItems: 'center' }, children: [_jsx(Button, { variant: "primary", size: "S", type: "button", children: "Save attendance" }), _jsx(Button, { variant: "secondary", size: "S", type: "button", children: "Cancel" }), _jsx(Button, { variant: "tertiary", size: "S", type: "button", children: "Clear filters" })] })] })] })),
629
+ parameters: {
630
+ docs: {
631
+ source: {
632
+ language: 'tsx',
633
+ code: `
634
+ import { Button } from '@arbor-education/design-system.components';
635
+
636
+ function ButtonAllSizesExample() {
637
+ return (
638
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }}>
639
+ {/* Medium (M) — 2.25rem / 36px */}
640
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', alignItems: 'center' }}>
641
+ <Button variant="primary" size="M" type="button">Save attendance</Button>
642
+ <Button variant="secondary" size="M" type="button">Cancel</Button>
643
+ <Button variant="tertiary" size="M" type="button">Clear filters</Button>
644
+ </div>
645
+
646
+ {/* Small (S) — 2rem / 32px */}
647
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)', alignItems: 'center' }}>
648
+ <Button variant="primary" size="S" type="button">Save attendance</Button>
649
+ <Button variant="secondary" size="S" type="button">Cancel</Button>
650
+ <Button variant="tertiary" size="S" type="button">Clear filters</Button>
651
+ </div>
652
+ </div>
653
+ );
654
+ }
655
+
656
+ export default ButtonAllSizesExample;
657
+ `.trim(),
658
+ },
659
+ },
660
+ },
661
+ }, 'Both sizes side by side. `M` resolves to `--size-medium` (2.25rem, 36px). `S` resolves to `--size-small` (2rem, 32px). Heights are entirely token-driven — never hardcode a height on a button wrapper.');
662
+ export const SmallPrimary = withDescription({
133
663
  args: {
134
- ...Primary.args,
664
+ variant: 'primary',
135
665
  size: 'S',
666
+ children: 'Save attendance',
136
667
  },
137
- };
138
- export const SmallSecondary = {
668
+ parameters: {
669
+ docs: {
670
+ source: {
671
+ language: 'tsx',
672
+ code: `
673
+ import { Button } from '@arbor-education/design-system.components';
674
+
675
+ function ButtonSmallPrimaryExample() {
676
+ return <Button variant="primary" size="S">Save attendance</Button>;
677
+ }
678
+
679
+ export default ButtonSmallPrimaryExample;
680
+ `.trim(),
681
+ },
682
+ },
683
+ },
684
+ }, 'Small primary button — 2rem / 32px height via `--size-small`. Use in dense toolbars or compact table headers where the medium size would feel too tall.');
685
+ export const SmallSecondary = withDescription({
139
686
  args: {
140
- ...Secondary.args,
687
+ variant: 'secondary',
141
688
  size: 'S',
689
+ children: 'Cancel',
142
690
  },
143
- };
144
- // Disabled button
145
- export const Disabled = {
691
+ parameters: {
692
+ docs: {
693
+ source: {
694
+ language: 'tsx',
695
+ code: `
696
+ import { Button } from '@arbor-education/design-system.components';
697
+
698
+ function ButtonSmallSecondaryExample() {
699
+ return <Button variant="secondary" size="S">Cancel</Button>;
700
+ }
701
+
702
+ export default ButtonSmallSecondaryExample;
703
+ `.trim(),
704
+ },
705
+ },
706
+ },
707
+ }, 'Small secondary button. Matches the small primary in height for pairing in compact confirm/cancel rows.');
708
+ export const WithIconLeft = withDescription({
709
+ args: {
710
+ variant: 'primary',
711
+ size: 'M',
712
+ children: 'Export to CSV',
713
+ iconLeftName: 'download',
714
+ iconLeftScreenReaderText: 'Download',
715
+ },
716
+ parameters: {
717
+ docs: {
718
+ source: {
719
+ language: 'tsx',
720
+ code: `
721
+ import { Button } from '@arbor-education/design-system.components';
722
+
723
+ function ButtonWithIconLeftExample() {
724
+ return (
725
+ <Button variant="primary" size="M" iconLeftName="download" iconLeftScreenReaderText="Download">
726
+ Export to CSV
727
+ </Button>
728
+ );
729
+ }
730
+
731
+ export default ButtonWithIconLeftExample;
732
+ `.trim(),
733
+ },
734
+ },
735
+ },
736
+ }, 'Icon on the left of the label via `iconLeftName`. The icon renders at 16px. `iconLeftScreenReaderText` overrides the accessible label for the icon — here it is set to `"Download"` which is more natural than the icon name `"download"` (though similar in this case). Use left icons for actions where the icon reinforces the verb: download, add, send.');
737
+ export const WithIconRight = withDescription({
738
+ args: {
739
+ variant: 'secondary',
740
+ size: 'M',
741
+ children: 'Send to parents',
742
+ iconRightName: 'send',
743
+ iconRightScreenReaderText: 'Send',
744
+ },
745
+ parameters: {
746
+ docs: {
747
+ source: {
748
+ language: 'tsx',
749
+ code: `
750
+ import { Button } from '@arbor-education/design-system.components';
751
+
752
+ function ButtonWithIconRightExample() {
753
+ return (
754
+ <Button variant="secondary" size="M" iconRightName="send" iconRightScreenReaderText="Send">
755
+ Send to parents
756
+ </Button>
757
+ );
758
+ }
759
+
760
+ export default ButtonWithIconRightExample;
761
+ `.trim(),
762
+ },
763
+ },
764
+ },
765
+ }, 'Icon on the right of the label via `iconRightName`. Right icons typically indicate direction or outcome: `arrow-right` for "next", `external-link` for "opens in new tab", `chevron-down` for "opens a menu". Pair right icons with the `dropdown` variant for SelectDropdown triggers.');
766
+ export const WithBothIcons = withDescription({
767
+ args: {
768
+ variant: 'primary',
769
+ size: 'M',
770
+ children: 'Generate report',
771
+ iconLeftName: 'sparkles',
772
+ iconLeftScreenReaderText: 'AI generated',
773
+ iconRightName: 'arrow-right',
774
+ iconRightScreenReaderText: 'Continue',
775
+ },
776
+ parameters: {
777
+ docs: {
778
+ source: {
779
+ language: 'tsx',
780
+ code: `
781
+ import { Button } from '@arbor-education/design-system.components';
782
+
783
+ function ButtonWithBothIconsExample() {
784
+ return (
785
+ <Button
786
+ variant="primary"
787
+ size="M"
788
+ iconLeftName="sparkles"
789
+ iconLeftScreenReaderText="AI generated"
790
+ iconRightName="arrow-right"
791
+ iconRightScreenReaderText="Continue"
792
+ >
793
+ Generate report
794
+ </Button>
795
+ );
796
+ }
797
+
798
+ export default ButtonWithBothIconsExample;
799
+ `.trim(),
800
+ },
801
+ },
802
+ },
803
+ }, 'Both icon slots used simultaneously. Left icon reinforces the action type (`sparkles` for AI-generated), right icon indicates direction or outcome (`arrow-right` for proceed). The dual-icon pattern is rare — only use it when both icons add distinct meaning. The label should be short enough that the button does not become unwieldy.');
804
+ export const IconOnly = withDescription({
805
+ args: {
806
+ variant: 'secondary',
807
+ size: 'M',
808
+ iconRightName: '3-dot',
809
+ iconRightScreenReaderText: 'More actions',
810
+ },
811
+ parameters: {
812
+ docs: {
813
+ source: {
814
+ language: 'tsx',
815
+ code: `
816
+ import { Button } from '@arbor-education/design-system.components';
817
+
818
+ function ButtonIconOnlyExample() {
819
+ return <Button variant="secondary" size="M" iconRightName="3-dot" iconRightScreenReaderText="More actions" />;
820
+ }
821
+
822
+ export default ButtonIconOnlyExample;
823
+ `.trim(),
824
+ },
825
+ },
826
+ },
827
+ }, 'Icon-only mode is triggered automatically when `children` is omitted and at least one icon prop is provided. The component adds `ds-button--icon-only` for square sizing. **Always** provide `iconRightScreenReaderText` (or `iconLeftScreenReaderText`) — the icon name `"3-dot"` is meaningless to a screen reader. This story uses `"More actions"` which clearly communicates the button\'s purpose.');
828
+ export const IconOnlyAccessibility = withDescription({
829
+ render: () => (_jsxs("div", { style: { padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-destructive-600)' }, children: "Bad \u2014 screen reader announces \"3-dot\" or \"x\" (the raw icon name)" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)' }, children: [_jsx(Button, { variant: "secondary", type: "button", iconRightName: "3-dot" }), _jsx(Button, { variant: "secondary", type: "button", iconRightName: "x" }), _jsx(Button, { variant: "secondary", type: "button", iconRightName: "pencil" })] })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-success-600)' }, children: "Good \u2014 screen reader announces a human-readable action name" }), _jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-large)' }, children: [_jsx(Button, { variant: "secondary", type: "button", iconRightName: "3-dot", iconRightScreenReaderText: "More actions" }), _jsx(Button, { variant: "secondary", type: "button", iconRightName: "x", iconRightScreenReaderText: "Dismiss" }), _jsx(Button, { variant: "secondary", type: "button", iconRightName: "pencil", iconRightScreenReaderText: "Edit student details" })] })] })] })),
830
+ parameters: {
831
+ docs: {
832
+ source: {
833
+ language: 'tsx',
834
+ code: `
835
+ import { Button } from '@arbor-education/design-system.components';
836
+
837
+ function ButtonIconOnlyAccessibilityExample() {
838
+ return (
839
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
840
+ {/* Bad — screen reader announces raw icon names */}
841
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)' }}>
842
+ <Button variant="secondary" type="button" iconRightName="3-dot" />
843
+ <Button variant="secondary" type="button" iconRightName="x" />
844
+ <Button variant="secondary" type="button" iconRightName="pencil" />
845
+ </div>
846
+
847
+ {/* Good — human-readable action names */}
848
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)' }}>
849
+ <Button variant="secondary" type="button" iconRightName="3-dot" iconRightScreenReaderText="More actions" />
850
+ <Button variant="secondary" type="button" iconRightName="x" iconRightScreenReaderText="Dismiss" />
851
+ <Button variant="secondary" type="button" iconRightName="pencil" iconRightScreenReaderText="Edit student details" />
852
+ </div>
853
+ </div>
854
+ );
855
+ }
856
+
857
+ export default ButtonIconOnlyAccessibilityExample;
858
+ `.trim(),
859
+ },
860
+ },
861
+ },
862
+ }, 'Icon-only accessibility contrast. The top row has no `iconRightScreenReaderText` override — screen readers announce the raw icon names (`"3-dot"`, `"x"`, `"pencil"`). The bottom row overrides with human-readable action names. Always provide `iconLeftScreenReaderText` or `iconRightScreenReaderText` when using icon-only buttons — the icon name is almost never descriptive enough in context.');
863
+ export const Disabled = withDescription({
146
864
  args: {
147
- ...Primary.args,
865
+ variant: 'primary',
866
+ size: 'M',
867
+ children: 'Save attendance',
148
868
  disabled: true,
149
869
  },
150
- };
870
+ parameters: {
871
+ docs: {
872
+ source: {
873
+ language: 'tsx',
874
+ code: `
875
+ import { Button } from '@arbor-education/design-system.components';
876
+
877
+ function ButtonDisabledExample() {
878
+ return <Button variant="primary" size="M" disabled>Save attendance</Button>;
879
+ }
880
+
881
+ export default ButtonDisabledExample;
882
+ `.trim(),
883
+ },
884
+ },
885
+ },
886
+ }, 'Native HTML `disabled` attribute. Applies `opacity: 0.5`, `cursor: not-allowed`, and `pointer-events: none`. Removes the button from tab order. Do NOT simulate disabled with manual opacity or CSS — use this prop so the browser and assistive technologies also receive the disabled signal.');
887
+ export const ErrorState = withDescription({
888
+ args: {
889
+ variant: 'dropdown',
890
+ size: 'M',
891
+ children: 'Select year group',
892
+ error: true,
893
+ },
894
+ parameters: {
895
+ docs: {
896
+ source: {
897
+ language: 'tsx',
898
+ code: `
899
+ import { Button } from '@arbor-education/design-system.components';
900
+
901
+ function ButtonErrorStateExample() {
902
+ return <Button variant="dropdown" size="M" error>Select year group</Button>;
903
+ }
904
+
905
+ export default ButtonErrorStateExample;
906
+ `.trim(),
907
+ },
908
+ },
909
+ },
910
+ }, 'The `error` prop adds a red border (`--color-semantic-destructive-500`) via `ds-button--error`. This is used by `SelectDropdown` and other form-field trigger buttons to indicate validation failure on the field. It is **purely visual** — no `aria-invalid` is set on the button itself. Consumers are responsible for communicating the error state to assistive tech (e.g. `aria-invalid` on the associated input).');
911
+ export const Borderless = withDescription({
912
+ args: {
913
+ variant: 'tertiary',
914
+ size: 'M',
915
+ children: 'Settings',
916
+ iconLeftName: 'settings',
917
+ iconLeftScreenReaderText: 'Settings',
918
+ borderless: true,
919
+ },
920
+ parameters: {
921
+ docs: {
922
+ source: {
923
+ language: 'tsx',
924
+ code: `
925
+ import { Button } from '@arbor-education/design-system.components';
926
+
927
+ function ButtonBorderlessExample() {
928
+ return (
929
+ <Button variant="tertiary" size="M" iconLeftName="settings" iconLeftScreenReaderText="Settings" borderless>
930
+ Settings
931
+ </Button>
932
+ );
933
+ }
934
+
935
+ export default ButtonBorderlessExample;
936
+ `.trim(),
937
+ },
938
+ },
939
+ },
940
+ }, 'The `borderless` prop removes the border entirely via `ds-button--borderless`. Useful in toolbar contexts where a visible border would add visual noise — for example, icon-adjacent utility buttons in a rich text editor toolbar or a data grid header. Pairs naturally with `variant="tertiary"` and an icon.');
941
+ export const NoHorizontalPadding = withDescription({
942
+ args: {
943
+ variant: 'text-link',
944
+ size: 'M',
945
+ children: 'View full attendance report',
946
+ hasHorizontalPadding: false,
947
+ },
948
+ parameters: {
949
+ docs: {
950
+ source: {
951
+ language: 'tsx',
952
+ code: `
953
+ import { Button } from '@arbor-education/design-system.components';
954
+
955
+ function ButtonNoHorizontalPaddingExample() {
956
+ return (
957
+ <Button variant="text-link" size="M" hasHorizontalPadding={false}>
958
+ View full attendance report
959
+ </Button>
960
+ );
961
+ }
962
+
963
+ export default ButtonNoHorizontalPaddingExample;
964
+ `.trim(),
965
+ },
966
+ },
967
+ },
968
+ }, '`hasHorizontalPadding={false}` removes horizontal padding via `ds-button--no-horizontal-padding`. The canonical use case is a `text-link` variant sitting inline in a form label or table cell, where the default padding would indent the text away from the surrounding content. Do not use this as a general layout escape hatch — it is specifically for flush-mounted inline actions.');
969
+ export const FormTypeAttribute = withDescription({
970
+ render: FormTypeAttributeTemplate,
971
+ parameters: {
972
+ docs: {
973
+ source: {
974
+ language: 'tsx',
975
+ code: `
976
+ import { useState } from 'react';
977
+ import { Button } from '@arbor-education/design-system.components';
978
+
979
+ function FormTypeAttributeExample() {
980
+ const [submitted, setSubmitted] = useState(false);
981
+ const [cancelled, setCancelled] = useState(false);
982
+ return (
983
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', minWidth: '340px' }}>
984
+ {submitted && (
985
+ <p style={{ color: 'var(--color-semantic-success-600)' }}>
986
+ Form submitted! (type="submit" fired)
987
+ </p>
988
+ )}
989
+ {cancelled && !submitted && (
990
+ <p style={{ color: 'var(--color-grey-600)' }}>
991
+ Cancelled — form was NOT submitted.
992
+ </p>
993
+ )}
994
+ <form
995
+ onSubmit={(e) => {
996
+ e.preventDefault();
997
+ setSubmitted(true);
998
+ setCancelled(false);
999
+ }}
1000
+ style={{ display: 'flex', gap: 'var(--spacing-large)' }}
1001
+ >
1002
+ <Button
1003
+ variant="secondary"
1004
+ type="button"
1005
+ onClick={() => {
1006
+ setSubmitted(false);
1007
+ setCancelled(true);
1008
+ }}
1009
+ >
1010
+ Cancel
1011
+ </Button>
1012
+ <Button variant="primary" type="submit">
1013
+ Save attendance
1014
+ </Button>
1015
+ </form>
1016
+ </div>
1017
+ );
1018
+ }
1019
+
1020
+ export default FormTypeAttributeExample;
1021
+ `.trim(),
1022
+ },
1023
+ },
1024
+ },
1025
+ }, [
1026
+ '**Critical gotcha demo.** The `type` prop is explicitly omitted from `ButtonProps` via',
1027
+ '`Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "onClick">`. This means `<Button>` never',
1028
+ 'sets a `type` attribute, and browsers default to `type="submit"`. Any `<Button>` inside a `<form>`',
1029
+ 'will submit that form on click unless you pass `type="button"` explicitly.',
1030
+ '',
1031
+ 'This story shows a form with both patterns side by side. The "Cancel" button uses `type="button"`',
1032
+ 'so clicking it does not submit. The "Save attendance" button uses `type="submit"` to intentionally',
1033
+ 'submit. Click both and watch the status message.',
1034
+ ].join(' '));
1035
+ export const LoadingComposition = withDescription({
1036
+ render: LoadingCompositionTemplate,
1037
+ parameters: {
1038
+ docs: {
1039
+ source: {
1040
+ language: 'tsx',
1041
+ code: `
1042
+ import { useState } from 'react';
1043
+ import { Button } from '@arbor-education/design-system.components';
1044
+
1045
+ function LoadingCompositionExample() {
1046
+ const [loading, setLoading] = useState(false);
1047
+ const [done, setDone] = useState(false);
1048
+
1049
+ const handleClick = () => {
1050
+ setLoading(true);
1051
+ setDone(false);
1052
+ setTimeout(() => {
1053
+ setLoading(false);
1054
+ setDone(true);
1055
+ }, 2000);
1056
+ };
1057
+
1058
+ return (
1059
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'center' }}>
1060
+ {done && (
1061
+ <p style={{ color: 'var(--color-semantic-success-600)' }}>Exported successfully!</p>
1062
+ )}
1063
+ <Button
1064
+ variant="primary"
1065
+ type="button"
1066
+ disabled={loading}
1067
+ iconLeftName={loading ? 'loader' : undefined}
1068
+ iconLeftScreenReaderText={loading ? 'Loading, please wait' : undefined}
1069
+ onClick={handleClick}
1070
+ >
1071
+ {loading ? 'Exporting...' : 'Export to CSV'}
1072
+ </Button>
1073
+ </div>
1074
+ );
1075
+ }
1076
+
1077
+ export default LoadingCompositionExample;
1078
+ `.trim(),
1079
+ },
1080
+ },
1081
+ },
1082
+ }, [
1083
+ 'Button has **no built-in loading state** — compose one with `disabled`, `iconLeftName`, and a label change.',
1084
+ 'This pattern uses `disabled={loading}` to block interaction during the async operation, `iconLeftName="loader"`',
1085
+ 'to show a spinner icon, and `iconLeftScreenReaderText="Loading, please wait"` so screen reader users are',
1086
+ 'informed. After 2 seconds the mock export resolves and the button returns to its default state.',
1087
+ '',
1088
+ 'The `loader` icon (`LoaderCircle` from Lucide) does not spin by default — animate it with a CSS',
1089
+ '`animation: spin 1s linear infinite` keyframe on the `svg` within the button if your design requires it.',
1090
+ ].join(' '));
1091
+ export const WithForwardRef = withDescription({
1092
+ render: WithForwardRefTemplate,
1093
+ parameters: {
1094
+ docs: {
1095
+ source: {
1096
+ language: 'tsx',
1097
+ code: `
1098
+ import { useRef } from 'react';
1099
+ import { Button } from '@arbor-education/design-system.components';
1100
+
1101
+ function WithForwardRefExample() {
1102
+ const buttonRef = useRef<HTMLButtonElement>(null);
1103
+ return (
1104
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'center' }}>
1105
+ <p style={{ color: 'var(--color-grey-600)' }}>
1106
+ Click the trigger to programmatically focus the primary button.
1107
+ </p>
1108
+ <div style={{ display: 'flex', gap: 'var(--spacing-large)' }}>
1109
+ <Button
1110
+ variant="secondary"
1111
+ type="button"
1112
+ onClick={() => buttonRef.current?.focus()}
1113
+ >
1114
+ Focus primary button
1115
+ </Button>
1116
+ <Button variant="primary" type="button" ref={buttonRef}>
1117
+ Save attendance
1118
+ </Button>
1119
+ </div>
1120
+ </div>
1121
+ );
1122
+ }
1123
+
1124
+ export default WithForwardRefExample;
1125
+ `.trim(),
1126
+ },
1127
+ },
1128
+ },
1129
+ }, '`Button` uses `forwardRef` — it exposes the underlying `<button>` DOM node via `ref`. This story demonstrates programmatic focus: clicking "Focus primary button" calls `buttonRef.current?.focus()` on the second button. Common real-world uses: restoring focus after a modal closes, advancing focus in a multi-step form, or triggering a button from a keyboard shortcut handler.');
1130
+ export const ClickTracking = withDescription({
1131
+ render: ClickTrackingTemplate,
1132
+ parameters: {
1133
+ docs: {
1134
+ source: {
1135
+ language: 'tsx',
1136
+ code: `
1137
+ import { useState } from 'react';
1138
+ import { Button } from '@arbor-education/design-system.components';
1139
+
1140
+ function ClickTrackingExample() {
1141
+ const [count, setCount] = useState(0);
1142
+ return (
1143
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', alignItems: 'center' }}>
1144
+ <p style={{ color: 'var(--color-grey-600)' }}>
1145
+ Button clicked <strong>{count}</strong> {count === 1 ? 'time' : 'times'}
1146
+ </p>
1147
+ <Button variant="primary" type="button" onClick={() => setCount(c => c + 1)}>
1148
+ Save attendance
1149
+ </Button>
1150
+ </div>
1151
+ );
1152
+ }
1153
+
1154
+ export default ClickTrackingExample;
1155
+ `.trim(),
1156
+ },
1157
+ },
1158
+ },
1159
+ }, 'Demonstrates that `onClick` fires correctly with each button activation (click, Space, Enter). The counter increments on every activation. Note `type="button"` — this story\'s button is not inside a form but it is good practice to always be explicit about `type` to avoid accidental form submissions.');
151
1160
  //# sourceMappingURL=Button.stories.js.map