@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,90 +1,657 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ Controls,
4
+ Heading as DocHeading,
5
+ Markdown,
6
+ Primary as DocPrimary,
7
+ Stories,
8
+ Subtitle,
9
+ Title,
10
+ } from '@storybook/addon-docs/blocks';
11
+ import { useEffect, useState } from 'react';
12
+ import { Button } from 'Components/button/Button';
2
13
  import { Progress } from './Progress';
3
14
 
15
+ // ---------------------------------------------------------------------------
16
+ // Docs page content
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const DESCRIPTION_INTRO = [
20
+ 'Progress is a horizontal progress bar for measurable operations — it fills from left to right as a',
21
+ 'known quantity advances toward a known total. Built on [Radix UI Progress](https://www.radix-ui.com/primitives/docs/components/progress)',
22
+ 'for reliable ARIA semantics.',
23
+ ].join('\n');
24
+
25
+ const USAGE_GUIDANCE = [
26
+ '### When to use',
27
+ '',
28
+ '- **File uploads and imports** — "3 of 5 files uploaded", where the total count is known',
29
+ '- **Multi-step wizards** — completed steps out of total steps',
30
+ '- **Long-running operations** — any process where progress is calculable and meaningful to show',
31
+ '',
32
+ '---',
33
+ '',
34
+ '### When NOT to use',
35
+ '',
36
+ '| Situation | Use instead |',
37
+ '|---|---|',
38
+ '| Unknown-duration operation | Spinner (Icon `name="loader"` with CSS animation) or estimate a `value` |',
39
+ '| Full page / section loading placeholder | Skeleton |',
40
+ '| Operation completed message | [`Banner`](?path=/docs/components-banner--docs) (success state) or [`Toast`](?path=/docs/components-toast--docs) (transient) |',
41
+ '| Decorative loading bar without a value | Do not — violates WCAG 2.1 4.1.2 |',
42
+ ].join('\n');
43
+
44
+ const DEVELOPER_NOTES = [
45
+ '### Critical usage patterns',
46
+ '',
47
+ '**`aria-label` or `aria-labelledby` is REQUIRED.** Radix renders `role="progressbar"` — a progressbar',
48
+ 'with no accessible name is a WCAG 2.1 failure (4.1.2 Name, Role, Value). Always pass one of these.',
49
+ '',
50
+ '**Width is 100% of parent.** Never size `<Progress>` directly. Put a width or `maxWidth` on the',
51
+ 'wrapper element and let Progress fill it.',
52
+ '',
53
+ '**Pass raw values, not percentages.** If you have "3 of 5 items", pass `value={3}` and `max={5}` —',
54
+ 'the component calculates the percentage internally. Do not pre-calculate to `value={60}` and `max={100}`.',
55
+ '',
56
+ '**`value={null}` does NOT produce a visible indeterminate animation.** Radix will set',
57
+ '`data-state="indeterminate"` on the root, but this component has no CSS animation defined for that',
58
+ 'state — the indicator bar just disappears. See the IndeterminateAntiPattern story. Use a Spinner instead.',
59
+ '',
60
+ '**Built on [Radix UI Progress](https://www.radix-ui.com/primitives/docs/components/progress).** Props not listed above are passed through to the Radix primitive — see the Radix docs for the full API.',
61
+ '',
62
+ '---',
63
+ '',
64
+ '### Accessibility',
65
+ '',
66
+ '- `aria-label` (or `aria-labelledby` pointing to a visible element) is required on every Progress',
67
+ '- `getValueLabel` shapes the screen-reader announcement — it becomes `aria-valuetext`. Without it,',
68
+ ' Radix falls back to a rounded percentage string (e.g. `"50%"`). Provide a custom function when you',
69
+ ' need a fuller sentence, e.g. `(v, m) => v + " of " + m + " files uploaded"`.',
70
+ '- Radix auto-manages `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, and `aria-valuetext`.',
71
+ ' Do not set these yourself.',
72
+ '- Colour alone is not sufficient — always pair the bar with a visible label showing the value or percentage.',
73
+ '',
74
+ '---',
75
+ '',
76
+ '### TypeScript types',
77
+ '',
78
+ '```ts',
79
+ "import { Progress } from '@arbor-education/design-system.components';",
80
+ '',
81
+ 'function MyProgress(props: Progress.Props) { ... }',
82
+ '```',
83
+ '',
84
+ '| Type | Description |',
85
+ '|---|---|',
86
+ '| `Progress.Props` | Full props interface |',
87
+ ].join('\n');
88
+
89
+ const RELATED_COMPONENTS = [
90
+ '## Related components',
91
+ '',
92
+ '[Banner](?path=/docs/components-banner--docs) · [Toast](?path=/docs/components-toast--docs) · [Icon](?path=/docs/components-icon--docs) · [Badge](?path=/docs/components-badge--docs)',
93
+ ].join('\n');
94
+
95
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
96
+
97
+ function ProgressDocsPage() {
98
+ return (
99
+ <>
100
+ <Title />
101
+ <Subtitle />
102
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
103
+ <DocHeading>Interactive example</DocHeading>
104
+ <Markdown>{PROPS_INTRO}</Markdown>
105
+ <DocPrimary />
106
+ <Controls />
107
+ <DocHeading>Usage guidance</DocHeading>
108
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
109
+ <DocHeading>Developer notes</DocHeading>
110
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
111
+ <DocHeading>Examples</DocHeading>
112
+ <Stories title="" />
113
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
114
+ </>
115
+ );
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Meta
120
+ // ---------------------------------------------------------------------------
121
+
4
122
  const meta = {
5
123
  title: 'Components/Progress',
6
124
  component: Progress,
7
125
  parameters: {
8
- layout: 'centered',
126
+ layout: 'padded',
127
+ docs: {
128
+ page: ProgressDocsPage,
129
+ },
9
130
  },
10
131
  tags: ['autodocs'],
11
- args: {
12
- 'aria-label': 'Progress bar',
13
- },
14
132
  argTypes: {
15
- value: {
133
+ 'value': {
16
134
  control: { type: 'number', min: 0, max: 100 },
17
- description: 'Current progress value',
135
+ description: [
136
+ 'Current progress value. Defaults to `0`.',
137
+ 'Pass `null` only if you provide a custom animated `indicatorClassName` for an indeterminate state',
138
+ '— see the IndeterminateAntiPattern story for why bare `null` does nothing visible.',
139
+ ].join(' '),
140
+ table: {
141
+ type: { summary: 'number | null' },
142
+ defaultValue: { summary: '0' },
143
+ },
144
+ },
145
+ 'max': {
146
+ control: { type: 'number', min: 1 },
147
+ description: [
148
+ 'Denominator for the progress calculation. Progress calculates the percentage internally',
149
+ '— pass raw values, not percentages. Defaults to `100`.',
150
+ ].join(' '),
151
+ table: {
152
+ type: { summary: 'number' },
153
+ defaultValue: { summary: '100' },
154
+ },
155
+ },
156
+ 'aria-label': {
157
+ control: 'text',
158
+ description: [
159
+ '**Required** (unless `aria-labelledby` is provided).',
160
+ 'A progressbar with no accessible name fails WCAG 2.1 (4.1.2 Name, Role, Value).',
161
+ 'Use `aria-labelledby` instead when a visible label element exists in the DOM.',
162
+ ].join(' '),
163
+ table: {
164
+ type: { summary: 'string' },
165
+ },
166
+ },
167
+ 'getValueLabel': {
168
+ control: false,
169
+ description: [
170
+ 'Function that returns the string announced by screen readers as `aria-valuetext`.',
171
+ 'Without it, Radix falls back to a rounded percentage string (e.g. `"50%"`).',
172
+ 'Provide a custom function for a fuller sentence, e.g. `(v, m) => v + " of " + m + " files uploaded"`.',
173
+ ].join(' '),
174
+ table: {
175
+ type: { summary: '(value: number, max: number) => string' },
176
+ defaultValue: { summary: 'Radix default (rounded percentage, e.g. "50%")' },
177
+ },
178
+ },
179
+ 'indicatorClassName': {
180
+ control: 'text',
181
+ description: [
182
+ 'Escape hatch for styling the moving indicator bar. Rarely needed.',
183
+ 'Pass a CSS class to override colours, add animations, or customise the indeterminate state.',
184
+ ].join(' '),
185
+ table: {
186
+ type: { summary: 'string' },
187
+ defaultValue: { summary: "''" },
188
+ },
18
189
  },
19
- max: {
20
- control: 'number',
21
- description: 'Maximum progress value',
190
+ 'className': {
191
+ control: false,
192
+ description: 'Additional CSS class names on the root progress element. Use sparingly.',
193
+ table: {
194
+ type: { summary: 'string' },
195
+ defaultValue: { summary: "''" },
196
+ },
22
197
  },
23
198
  },
24
- decorators: [
25
- Story => (
26
- <div style={{ width: '400px' }}>
27
- <Story />
28
- </div>
29
- ),
30
- ],
31
199
  } satisfies Meta<typeof Progress>;
32
200
 
33
201
  export default meta;
34
- type Story = StoryObj<typeof meta>;
202
+ // Use StoryObj<typeof Progress> (not typeof meta) so render-only stories are
203
+ // not forced to provide required args — template components handle their own instances.
204
+ type Story = StoryObj<typeof Progress>;
35
205
 
36
- // Default progress
37
- export const Default: Story = {
38
- args: {
39
- value: 50,
40
- max: 100,
206
+ // ---------------------------------------------------------------------------
207
+ // Helper: attach a per-story description to docs
208
+ // ---------------------------------------------------------------------------
209
+
210
+ const withDescription = (story: Story, description: string): Story => ({
211
+ ...story,
212
+ parameters: {
213
+ ...story.parameters,
214
+ docs: {
215
+ ...story.parameters?.docs,
216
+ description: {
217
+ story: description,
218
+ },
219
+ },
41
220
  },
221
+ });
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Template components for composition and stateful stories.
225
+ // Named components avoid react-hooks lint issues — the react-hooks ESLint
226
+ // plugin is NOT configured in this project, so do NOT add eslint-disable.
227
+ // ---------------------------------------------------------------------------
228
+
229
+ const AllStopsTemplate = () => (
230
+ <div
231
+ style={{
232
+ padding: 'var(--spacing-xlarge)',
233
+ display: 'flex',
234
+ flexDirection: 'column',
235
+ gap: 'var(--spacing-large)',
236
+ maxWidth: '60%',
237
+ }}
238
+ >
239
+ {([0, 25, 50, 75, 100] as const).map(stop => (
240
+ <div
241
+ key={stop}
242
+ style={{
243
+ display: 'flex',
244
+ alignItems: 'center',
245
+ gap: 'var(--spacing-xsmall)',
246
+ }}
247
+ >
248
+ <span
249
+ className="ds-text"
250
+ style={{
251
+ color: 'var(--color-grey-600)',
252
+ minWidth: '4rem',
253
+ flexShrink: 0,
254
+ }}
255
+ >
256
+ {stop}
257
+ {' '}
258
+ / 100
259
+ </span>
260
+ <div style={{ flex: 1 }}>
261
+ <Progress
262
+ value={stop}
263
+ max={100}
264
+ aria-label={`${stop}% complete`}
265
+ />
266
+ </div>
267
+ </div>
268
+ ))}
269
+ </div>
270
+ );
271
+
272
+ const InteractiveLoadingTemplate = (args: React.ComponentProps<typeof Progress>) => {
273
+ const [progress, setProgress] = useState(0);
274
+ const [running, setRunning] = useState(true);
275
+
276
+ useEffect(() => {
277
+ if (!running) return;
278
+ if (progress >= 100) {
279
+ setRunning(false);
280
+ return;
281
+ }
282
+ const id = setInterval(() => {
283
+ setProgress((prev) => {
284
+ const next = prev + 2;
285
+ if (next >= 100) {
286
+ clearInterval(id);
287
+ setRunning(false);
288
+ return 100;
289
+ }
290
+ return next;
291
+ });
292
+ }, 60);
293
+ return () => clearInterval(id);
294
+ }, [running, progress]);
295
+
296
+ const handleReset = () => {
297
+ setProgress(0);
298
+ setRunning(true);
299
+ };
300
+
301
+ return (
302
+ <div
303
+ style={{
304
+ padding: 'var(--spacing-xlarge)',
305
+ display: 'flex',
306
+ flexDirection: 'column',
307
+ gap: 'var(--spacing-large)',
308
+ maxWidth: '60%',
309
+ }}
310
+ >
311
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
312
+ File import progress
313
+ </span>
314
+ <Progress
315
+ {...args}
316
+ value={progress}
317
+ max={100}
318
+ aria-label="File import progress"
319
+ getValueLabel={v => `${v}% complete`}
320
+ />
321
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
322
+ <span className="ds-text" style={{ color: 'var(--color-grey-600)' }}>
323
+ {progress}
324
+ % complete
325
+ </span>
326
+ {!running && (
327
+ <Button variant="secondary" size="S" onClick={handleReset}>
328
+ Reset
329
+ </Button>
330
+ )}
331
+ </div>
332
+ </div>
333
+ );
42
334
  };
43
335
 
44
- // Empty progress
45
- export const Empty: Story = {
46
- args: {
47
- value: 0,
48
- max: 100,
336
+ const WithAccessibleLabelTemplate = () => (
337
+ <div
338
+ style={{
339
+ padding: 'var(--spacing-xlarge)',
340
+ display: 'flex',
341
+ flexDirection: 'column',
342
+ gap: 'var(--spacing-xlarge)',
343
+ maxWidth: '60%',
344
+ }}
345
+ >
346
+ {/* Without getValueLabel */}
347
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
348
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
349
+ Without getValueLabel — screen reader announces Radix&apos;s default (a rounded percentage, e.g. &ldquo;65%&rdquo;)
350
+ </p>
351
+ <Progress
352
+ value={65}
353
+ max={100}
354
+ aria-label="Upload progress"
355
+ />
356
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
357
+ Screen reader: "Upload progress, 65%"
358
+ </p>
359
+ </div>
360
+ {/* With getValueLabel */}
361
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
362
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-success-600)' }}>
363
+ With getValueLabel — screen reader announces a full sentence
364
+ </p>
365
+ <Progress
366
+ value={65}
367
+ max={100}
368
+ aria-label="Upload progress"
369
+ getValueLabel={v => `${v}% uploaded`}
370
+ />
371
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontStyle: 'italic' }}>
372
+ Screen reader: "Upload progress, 65% uploaded"
373
+ </p>
374
+ </div>
375
+ </div>
376
+ );
377
+
378
+ const IndeterminateAntiPatternTemplate = () => (
379
+ <div
380
+ style={{
381
+ padding: 'var(--spacing-xlarge)',
382
+ display: 'flex',
383
+ flexDirection: 'column',
384
+ gap: 'var(--spacing-large)',
385
+ maxWidth: '60%',
386
+ }}
387
+ >
388
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-semantic-destructive-600)' }}>
389
+ The blank bar below is the ACTUAL render of value=null — this is what NOT to do
390
+ </p>
391
+ {/* value={null} causes Number(null) = 0, translateX(-100%), indicator invisible */}
392
+ <Progress
393
+ value={null}
394
+ max={100}
395
+ aria-label="Assessment marksheet export"
396
+ />
397
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
398
+ The indicator bar has translated fully out of view. No animation plays. The user sees nothing.
399
+ </p>
400
+ <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)' }}>
401
+ For unknown-duration operations, use a Spinner (Icon with name="loader" and a CSS rotation
402
+ animation) or set an approximate value so the bar is at least partially visible.
403
+ </p>
404
+ </div>
405
+ );
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // Stories
409
+ // ---------------------------------------------------------------------------
410
+
411
+ export const Default: Story = withDescription(
412
+ {
413
+ args: {
414
+ 'value': 50,
415
+ 'max': 100,
416
+ 'aria-label': 'Page loading',
417
+ },
418
+ render: args => <Progress {...args} />,
49
419
  },
50
- };
420
+ [
421
+ 'The interactive canvas — every prop is wired to the Controls panel below.',
422
+ 'Adjust `value` and `max` to see the bar move. Control the width by sizing the parent container',
423
+ '— Progress fills 100% of its parent, so the wrapper\'s `maxWidth` is what you control.',
424
+ ].join(' '),
425
+ );
426
+
427
+ export const AllStops: Story = withDescription(
428
+ {
429
+ render: AllStopsTemplate,
430
+ parameters: {
431
+ controls: { disable: true },
432
+ docs: {
433
+ source: {
434
+ language: 'tsx',
435
+ code: `
436
+ import { Progress } from '@arbor-education/design-system.components';
51
437
 
52
- // Quarter progress
53
- export const Quarter: Story = {
54
- args: {
55
- value: 25,
56
- max: 100,
438
+ function AllStopsExample() {
439
+ return (
440
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
441
+ <Progress value={0} max={100} aria-label="0% complete" />
442
+ <Progress value={25} max={100} aria-label="25% complete" />
443
+ <Progress value={50} max={100} aria-label="50% complete" />
444
+ <Progress value={75} max={100} aria-label="75% complete" />
445
+ <Progress value={100} max={100} aria-label="100% complete" />
446
+ </div>
447
+ );
448
+ }
449
+ export default AllStopsExample;
450
+ `.trim(),
451
+ },
452
+ },
453
+ },
57
454
  },
58
- };
455
+ [
456
+ 'All five canonical stops — 0, 25, 50, 75, and 100 — rendered side by side for visual reference.',
457
+ 'Each bar has an `aria-label` describing its completion percentage.',
458
+ 'The "0 / 100" bar shows that an empty state is a valid initial render, not a missing or broken component.',
459
+ ].join(' '),
460
+ );
59
461
 
60
- // Half progress
61
- export const Half: Story = {
62
- args: {
63
- value: 50,
64
- max: 100,
462
+ export const CustomMax: Story = withDescription(
463
+ {
464
+ args: {
465
+ 'value': 30,
466
+ 'max': 50,
467
+ 'aria-label': 'Assessment sections completed',
468
+ 'getValueLabel': (v: number, m: number) => `${v} of ${m}`,
469
+ },
470
+ render: args => (
471
+ <div style={{ maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
472
+ <Progress {...args} />
473
+ </div>
474
+ ),
475
+ parameters: {
476
+ docs: {
477
+ source: {
478
+ language: 'tsx',
479
+ code: `
480
+ import { Progress } from '@arbor-education/design-system.components';
481
+
482
+ // Pass raw values — Progress calculates the percentage internally.
483
+ // value={30} max={50} renders a 60% filled bar.
484
+ function CustomMaxExample() {
485
+ return (
486
+ <Progress
487
+ value={30}
488
+ max={50}
489
+ aria-label="Assessment sections completed"
490
+ getValueLabel={(v, m) => \`\${v} of \${m}\`}
491
+ />
492
+ );
493
+ }
494
+ export default CustomMaxExample;
495
+ `.trim(),
496
+ },
497
+ },
498
+ },
65
499
  },
66
- };
500
+ [
501
+ 'When your total is not 100, set `max` to the real denominator and `value` to the raw count.',
502
+ 'Here `value={30}` and `max={50}` — Progress renders a 60% filled bar without you calculating anything.',
503
+ 'The `getValueLabel` prop shapes the screen-reader announcement — `(v, m) => v + " of " + m` produces',
504
+ '"30 of 50" as `aria-valuetext` instead of just the raw percentage.',
505
+ ].join(' '),
506
+ );
67
507
 
68
- // Three quarters progress
69
- export const ThreeQuarters: Story = {
70
- args: {
71
- value: 75,
72
- max: 100,
508
+ export const InteractiveLoading: Story = withDescription(
509
+ {
510
+ render: InteractiveLoadingTemplate,
511
+ parameters: {
512
+ controls: { disable: true },
513
+ docs: {
514
+ source: {
515
+ language: 'tsx',
516
+ code: `
517
+ import { useEffect, useState } from 'react';
518
+ import { Button, Progress } from '@arbor-education/design-system.components';
519
+
520
+ function FileImportProgress() {
521
+ const [progress, setProgress] = useState(0);
522
+ const [running, setRunning] = useState(true);
523
+
524
+ useEffect(() => {
525
+ if (!running) return;
526
+ if (progress >= 100) { setRunning(false); return; }
527
+ const id = setInterval(() => {
528
+ setProgress((prev) => {
529
+ const next = prev + 2;
530
+ if (next >= 100) { clearInterval(id); setRunning(false); return 100; }
531
+ return next;
532
+ });
533
+ }, 60);
534
+ return () => clearInterval(id);
535
+ }, [running, progress]);
536
+
537
+ return (
538
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', maxWidth: '60%' }}>
539
+ <span>File import progress</span>
540
+ <Progress
541
+ value={progress}
542
+ max={100}
543
+ aria-label="File import progress"
544
+ getValueLabel={v => \`\${v}% complete\`}
545
+ />
546
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-large)' }}>
547
+ <span>{progress}% complete</span>
548
+ {!running && (
549
+ <Button variant="secondary" size="S" onClick={() => { setProgress(0); setRunning(true); }}>
550
+ Reset
551
+ </Button>
552
+ )}
553
+ </div>
554
+ </div>
555
+ );
556
+ }
557
+ export default FileImportProgress;
558
+ `.trim(),
559
+ },
560
+ },
561
+ },
73
562
  },
74
- };
563
+ [
564
+ 'A timed simulation of a file import: 0 → 100% over ~3 seconds using `setInterval` in a',
565
+ 'named template component (the react-hooks ESLint plugin is not configured here, so hooks inside',
566
+ 'a named component are used directly without disable comments).',
567
+ 'The Reset button reruns the animation. `getValueLabel` returns a richer phrase (e.g. "42% complete")',
568
+ 'instead of Radix\'s default rounded-percentage string on each `aria-valuenow` update.',
569
+ 'In a real import, drive `value` from an API polling response — never fake it like this demo.',
570
+ ].join(' '),
571
+ );
572
+
573
+ export const WithAccessibleLabel: Story = withDescription(
574
+ {
575
+ render: WithAccessibleLabelTemplate,
576
+ parameters: {
577
+ controls: { disable: true },
578
+ docs: {
579
+ source: {
580
+ language: 'tsx',
581
+ code: `
582
+ import { Progress } from '@arbor-education/design-system.components';
75
583
 
76
- // Complete progress
77
- export const Complete: Story = {
78
- args: {
79
- value: 100,
80
- max: 100,
584
+ function WithAccessibleLabelExample() {
585
+ return (
586
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xlarge)', maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
587
+ {/* Without getValueLabel — screen reader announces Radix's default (rounded %) */}
588
+ <Progress value={65} max={100} aria-label="Upload progress" />
589
+ {/* With getValueLabel — screen reader announces a full sentence via aria-valuetext */}
590
+ <Progress
591
+ value={65}
592
+ max={100}
593
+ aria-label="Upload progress"
594
+ getValueLabel={v => \`\${v}% uploaded\`}
595
+ />
596
+ </div>
597
+ );
598
+ }
599
+ export default WithAccessibleLabelExample;
600
+ `.trim(),
601
+ },
602
+ },
603
+ },
81
604
  },
82
- };
605
+ [
606
+ 'Side-by-side contrast: the top bar has only `aria-label` — screen readers announce the raw Radix',
607
+ 'default (the percentage as a number). The bottom bar adds `getValueLabel` returning "65% uploaded",',
608
+ 'which populates `aria-valuetext` and gives a screen reader a complete sentence.',
609
+ 'Inspect the DOM to see `aria-valuetext` populated on the lower bar — it will read "65% uploaded"',
610
+ 'rather than "65".',
611
+ ].join(' '),
612
+ );
613
+
614
+ export const IndeterminateAntiPattern: Story = withDescription(
615
+ {
616
+ render: IndeterminateAntiPatternTemplate,
617
+ parameters: {
618
+ controls: { disable: true },
619
+ docs: {
620
+ source: {
621
+ language: 'tsx',
622
+ code: `
623
+ import { Progress } from '@arbor-education/design-system.components';
83
624
 
84
- // Custom max value
85
- export const CustomMax: Story = {
86
- args: {
87
- value: 30,
88
- max: 50,
625
+ function IndeterminateAntiPatternExample() {
626
+ return (
627
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', maxWidth: '60%', padding: 'var(--spacing-xlarge)' }}>
628
+ {/* ⚠️ ANTI-PATTERN — value={null} does NOT produce an indeterminate animation. */}
629
+ {/* Number(null) = 0, so the indicator bar translates fully off-screen. */}
630
+ <Progress value={null} max={100} aria-label="Assessment marksheet export" />
631
+
632
+ {/* ✅ Or set an approximate value so the bar is at least partially visible: */}
633
+ <Progress value={50} max={100} aria-label="Assessment marksheet export" />
634
+ </div>
635
+ );
636
+ }
637
+ export default IndeterminateAntiPatternExample;
638
+ `.trim(),
639
+ },
640
+ },
641
+ },
89
642
  },
90
- };
643
+ [
644
+ '**Do not use `value={null}` with this component.** When `value` is `null`,',
645
+ '`Number(null)` evaluates to `0`, the indicator translates fully off-screen (`translateX(-100%)`),',
646
+ 'and the user sees a blank bar. Radix sets `data-state="indeterminate"` on the root element,',
647
+ 'but this component has NO animation CSS defined for that state.',
648
+ '',
649
+ 'The blank bar in this story IS the actual render — it is not a Storybook display issue.',
650
+ '',
651
+ 'For unknown-duration operations, use a Spinner (Icon with `name="loader"` plus a CSS rotation',
652
+ 'animation) or set an approximate `value` so the bar is at least partially visible.',
653
+ '',
654
+ 'If you genuinely need an animated indeterminate bar, pass a custom `indicatorClassName` with',
655
+ 'a keyframe animation targeting `[data-state="indeterminate"]`.',
656
+ ].join('\n'),
657
+ );