@arbor-education/design-system.components 0.25.3 → 0.25.5

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 (110) hide show
  1. package/.gather/skills/write-stories/README.md +1 -1
  2. package/.gather/skills/write-stories/SKILL.md +1 -1
  3. package/.gather/skills/write-stories/meta.md +1 -1
  4. package/CHANGELOG.md +16 -0
  5. package/component-library.md +26 -1
  6. package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts +3 -0
  7. package/dist/components/breadcrumbs/BreadcrumbCopyButton.d.ts.map +1 -0
  8. package/dist/components/breadcrumbs/BreadcrumbCopyButton.js +13 -0
  9. package/dist/components/breadcrumbs/BreadcrumbCopyButton.js.map +1 -0
  10. package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts +3 -0
  11. package/dist/components/breadcrumbs/BreadcrumbDropdown.d.ts.map +1 -0
  12. package/dist/components/breadcrumbs/BreadcrumbDropdown.js +8 -0
  13. package/dist/components/breadcrumbs/BreadcrumbDropdown.js.map +1 -0
  14. package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts +3 -0
  15. package/dist/components/breadcrumbs/BreadcrumbEllipsis.d.ts.map +1 -0
  16. package/dist/components/breadcrumbs/BreadcrumbEllipsis.js +7 -0
  17. package/dist/components/breadcrumbs/BreadcrumbEllipsis.js.map +1 -0
  18. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts +2 -0
  19. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.d.ts.map +1 -0
  20. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js +5 -0
  21. package/dist/components/breadcrumbs/BreadcrumbItemSeparator.js.map +1 -0
  22. package/dist/components/breadcrumbs/BreadcrumbLink.d.ts +6 -0
  23. package/dist/components/breadcrumbs/BreadcrumbLink.d.ts.map +1 -0
  24. package/dist/components/breadcrumbs/BreadcrumbLink.js +9 -0
  25. package/dist/components/breadcrumbs/BreadcrumbLink.js.map +1 -0
  26. package/dist/components/breadcrumbs/Breadcrumbs.d.ts +21 -0
  27. package/dist/components/breadcrumbs/Breadcrumbs.d.ts.map +1 -0
  28. package/dist/components/breadcrumbs/Breadcrumbs.js +51 -0
  29. package/dist/components/breadcrumbs/Breadcrumbs.js.map +1 -0
  30. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +81 -0
  31. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -0
  32. package/dist/components/breadcrumbs/Breadcrumbs.stories.js +681 -0
  33. package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -0
  34. package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts +2 -0
  35. package/dist/components/breadcrumbs/Breadcrumbs.test.d.ts.map +1 -0
  36. package/dist/components/breadcrumbs/Breadcrumbs.test.js +160 -0
  37. package/dist/components/breadcrumbs/Breadcrumbs.test.js.map +1 -0
  38. package/dist/components/breadcrumbs/types.d.ts +27 -0
  39. package/dist/components/breadcrumbs/types.d.ts.map +1 -0
  40. package/dist/components/breadcrumbs/types.js +2 -0
  41. package/dist/components/breadcrumbs/types.js.map +1 -0
  42. package/dist/components/fileUpload/FileUpload.d.ts +21 -0
  43. package/dist/components/fileUpload/FileUpload.d.ts.map +1 -0
  44. package/dist/components/fileUpload/FileUpload.js +23 -0
  45. package/dist/components/fileUpload/FileUpload.js.map +1 -0
  46. package/dist/components/fileUpload/FileUpload.stories.d.ts +71 -0
  47. package/dist/components/fileUpload/FileUpload.stories.d.ts.map +1 -0
  48. package/dist/components/fileUpload/FileUpload.stories.js +736 -0
  49. package/dist/components/fileUpload/FileUpload.stories.js.map +1 -0
  50. package/dist/components/fileUpload/FileUpload.test.d.ts +2 -0
  51. package/dist/components/fileUpload/FileUpload.test.d.ts.map +1 -0
  52. package/dist/components/fileUpload/FileUpload.test.js +99 -0
  53. package/dist/components/fileUpload/FileUpload.test.js.map +1 -0
  54. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +1 -1
  55. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  56. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
  57. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +14 -15
  58. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
  59. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +11 -0
  60. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
  61. package/dist/components/icon/allowedIcons.d.ts +1 -0
  62. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  63. package/dist/components/icon/allowedIcons.js +2 -1
  64. package/dist/components/icon/allowedIcons.js.map +1 -1
  65. package/dist/components/skeleton/Skeleton.d.ts +10 -0
  66. package/dist/components/skeleton/Skeleton.d.ts.map +1 -0
  67. package/dist/components/skeleton/Skeleton.js +9 -0
  68. package/dist/components/skeleton/Skeleton.js.map +1 -0
  69. package/dist/components/skeleton/Skeleton.stories.d.ts +99 -0
  70. package/dist/components/skeleton/Skeleton.stories.d.ts.map +1 -0
  71. package/dist/components/skeleton/Skeleton.stories.js +539 -0
  72. package/dist/components/skeleton/Skeleton.stories.js.map +1 -0
  73. package/dist/components/skeleton/Skeleton.test.d.ts +2 -0
  74. package/dist/components/skeleton/Skeleton.test.d.ts.map +1 -0
  75. package/dist/components/skeleton/Skeleton.test.js +49 -0
  76. package/dist/components/skeleton/Skeleton.test.js.map +1 -0
  77. package/dist/index.css +225 -0
  78. package/dist/index.css.map +1 -1
  79. package/dist/index.d.ts +3 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +3 -0
  82. package/dist/index.js.map +1 -1
  83. package/package.json +2 -1
  84. package/src/components/breadcrumbs/BreadcrumbCopyButton.tsx +27 -0
  85. package/src/components/breadcrumbs/BreadcrumbDropdown.tsx +27 -0
  86. package/src/components/breadcrumbs/BreadcrumbEllipsis.tsx +16 -0
  87. package/src/components/breadcrumbs/BreadcrumbItemSeparator.tsx +3 -0
  88. package/src/components/breadcrumbs/BreadcrumbLink.tsx +29 -0
  89. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +870 -0
  90. package/src/components/breadcrumbs/Breadcrumbs.test.tsx +185 -0
  91. package/src/components/breadcrumbs/Breadcrumbs.tsx +92 -0
  92. package/src/components/breadcrumbs/breadcrumbs.scss +158 -0
  93. package/src/components/breadcrumbs/types.ts +31 -0
  94. package/src/components/button/button.scss +2 -0
  95. package/src/components/dropdown/dropdown.scss +1 -0
  96. package/src/components/fileUpload/FileUpload.stories.tsx +920 -0
  97. package/src/components/fileUpload/FileUpload.test.tsx +172 -0
  98. package/src/components/fileUpload/FileUpload.tsx +144 -0
  99. package/src/components/fileUpload/fileUpload.scss +74 -0
  100. package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +15 -16
  101. package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +19 -0
  102. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +1 -1
  103. package/src/components/icon/allowedIcons.tsx +2 -0
  104. package/src/components/progress/progress.scss +1 -0
  105. package/src/components/skeleton/Skeleton.stories.tsx +748 -0
  106. package/src/components/skeleton/Skeleton.test.tsx +55 -0
  107. package/src/components/skeleton/Skeleton.tsx +36 -0
  108. package/src/components/skeleton/skeleton.scss +23 -0
  109. package/src/index.scss +3 -0
  110. package/src/index.ts +3 -0
@@ -0,0 +1,736 @@
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 { fn } from 'storybook/test';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { Button } from '../button/Button.js';
6
+ import { FileUpload } from './FileUpload.js';
7
+ // ─── Content strings ──────────────────────────────────────────────────────────
8
+ const DESCRIPTION_INTRO = `
9
+ The \`FileUpload\` component provides a drag-and-drop file upload area combined
10
+ with a managed list of uploaded (or uploading) files. It is built on top of
11
+ [react-dropzone](https://react-dropzone.js.org/) and handles the visual
12
+ presentation of the upload state machine — idle dropzone, in-progress upload
13
+ with a progress bar, upload failure, and completed file with download/delete
14
+ actions.
15
+
16
+ Use it wherever school staff need to attach documents to records: term reports,
17
+ policy PDFs, student photographs, spreadsheet imports, and so on.
18
+ `.trim();
19
+ const USAGE_GUIDANCE = `
20
+ ### When to use
21
+
22
+ - Uploading one or more files as part of a form flow (e.g. attaching a
23
+ behaviour letter to a student record).
24
+ - Displaying the progress of an ongoing upload alongside files that have
25
+ already completed.
26
+ - Giving staff a clear way to remove or re-download files they have attached.
27
+
28
+ ---
29
+
30
+ ### When NOT to use
31
+
32
+ | Situation | Prefer instead |
33
+ |-----------|---------------|
34
+ | You need a plain \`<input type="file">\` without drag-and-drop | A \`FormField\` wrapping a native file input |
35
+ | The user can only ever upload a single file and progress feedback is not needed | A simple \`<input type="file">\` or a \`Button\` that opens a file picker |
36
+ | The upload is hidden from the user (background sync) | No UI component needed — handle it silently in a service layer |
37
+ `.trim();
38
+ const DEVELOPER_NOTES = `
39
+ ### Critical usage patterns
40
+
41
+ The component is **not stateful by itself** — it is a controlled component. You
42
+ are responsible for:
43
+
44
+ 1. Calling your upload API inside \`uploadFiles\` and pushing the new
45
+ file (with \`progressPercentage: 0\`) into your own state.
46
+ 2. Updating \`progressPercentage\` as the upload progresses (e.g. via an
47
+ \`XMLHttpRequest\` progress event or polling).
48
+ 3. Setting \`progressPercentage: 100\` (or removing the property) when the
49
+ upload completes — the progress bar is hidden automatically at 100.
50
+ 4. Setting \`hasUploadFailed: true\` if the upload errors — this replaces the
51
+ file-type/size metadata with a red "Upload failed" message and turns the
52
+ progress bar red.
53
+
54
+ The progress bar is rendered **only** when \`progressPercentage\` is a number
55
+ and is strictly less than 100. At exactly 100 the bar disappears.
56
+
57
+ ---
58
+
59
+ ### Accessibility
60
+
61
+ The dropzone area is rendered by \`react-dropzone\` and is fully keyboard
62
+ accessible: it receives focus, responds to \`Enter\` and \`Space\` to open the
63
+ file picker, and announces itself to screen readers. The file list uses
64
+ semantic \`<span>\` elements for layout; make sure the \`fileName\` values
65
+ you pass are human-readable rather than GUIDs.
66
+
67
+ ---
68
+
69
+ ### TypeScript types
70
+
71
+ The \`UploadedFile\` type is **not exported** from the component module. Until
72
+ it is exported you can inline the shape or declare your own local type:
73
+
74
+ \`\`\`tsx
75
+ type UploadedFile = {
76
+ fileName: string;
77
+ icon?: string; // any IconName from the design system Icon component
78
+ fileType: string; // e.g. 'PDF', 'XLSX', 'DOCX'
79
+ fileSize: string; // human-readable, e.g. '1.2 MB'
80
+ onDownload?: () => void;
81
+ onDelete: () => void;
82
+ progressPercentage?: number; // 0–100; hidden at exactly 100
83
+ hasUploadFailed?: boolean;
84
+ };
85
+ \`\`\`
86
+ `.trim();
87
+ const RELATED_COMPONENTS = `
88
+ ## Related components
89
+
90
+ - [Progress](?path=/docs/components-progress--docs) — the progress bar used
91
+ inside each file row to indicate upload completion.
92
+ - [Button](?path=/docs/components-button--docs) — the "click to upload",
93
+ download, and delete buttons inside the component.
94
+ - [Card](?path=/docs/components-card--docs) — the visual wrapper for both the
95
+ dropzone area and the file list.
96
+ - [FormField](?path=/docs/components-formfield--docs) — use when you need a
97
+ labelled file input without drag-and-drop behaviour.
98
+ - [Icon](?path=/docs/components-icon--docs) — all file-type and action icons
99
+ rendered inside the component.
100
+ `.trim();
101
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
102
+ // ─── Docs page ────────────────────────────────────────────────────────────────
103
+ function FileUploadDocsPage() {
104
+ 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 })] }));
105
+ }
106
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
107
+ const meta = {
108
+ title: 'Components/FileUpload',
109
+ component: FileUpload,
110
+ tags: ['autodocs'],
111
+ parameters: {
112
+ layout: 'padded',
113
+ docs: {
114
+ page: FileUploadDocsPage,
115
+ },
116
+ },
117
+ argTypes: {
118
+ uploadedFiles: {
119
+ description: 'Array of file objects to display in the file list. Each object describes '
120
+ + 'a file that has been selected, is uploading, has completed, or has failed. '
121
+ + 'See the **TypeScript types** section for the full shape.',
122
+ control: false,
123
+ table: {
124
+ type: { summary: 'UploadedFile[]' },
125
+ },
126
+ },
127
+ uploadFiles: {
128
+ description: 'Callback fired when the user drops files onto the dropzone or selects them '
129
+ + 'via the file picker. Receives the native `File[]` array. You are responsible '
130
+ + 'for starting the upload and adding the files to `uploadedFiles` state.',
131
+ control: false,
132
+ table: {
133
+ type: { summary: '(files: File[]) => void' },
134
+ },
135
+ },
136
+ dropzoneContent: {
137
+ description: 'Custom content rendered inside the dropzone area. Defaults to a '
138
+ + '"Drag and drop, or click to upload" paragraph. Pass a `ReactNode` to '
139
+ + 'replace this with your own copy or layout.',
140
+ control: false,
141
+ table: {
142
+ type: { summary: 'ReactNode' },
143
+ defaultValue: {
144
+ summary: '<p>Drag and drop, or <Button variant="text-link">click to upload</Button></p>',
145
+ },
146
+ },
147
+ },
148
+ className: {
149
+ description: 'Additional CSS class name(s) applied to the root element.',
150
+ control: 'text',
151
+ table: {
152
+ type: { summary: 'string' },
153
+ },
154
+ },
155
+ },
156
+ args: {
157
+ uploadedFiles: [],
158
+ uploadFiles: fn(),
159
+ },
160
+ };
161
+ export default meta;
162
+ // ─── Helper ───────────────────────────────────────────────────────────────────
163
+ const withDescription = (story, description) => ({
164
+ ...story,
165
+ parameters: {
166
+ ...story.parameters,
167
+ docs: {
168
+ ...story.parameters?.docs,
169
+ description: {
170
+ story: description,
171
+ },
172
+ },
173
+ },
174
+ });
175
+ // ─── Shared helpers ───────────────────────────────────────────────────────────
176
+ const noop = () => { };
177
+ const completedFiles = [
178
+ {
179
+ fileName: 'spring-term-report-2024.pdf',
180
+ icon: 'file',
181
+ fileType: 'PDF',
182
+ fileSize: '1.2 MB',
183
+ onDownload: noop,
184
+ onDelete: noop,
185
+ },
186
+ {
187
+ fileName: 'attendance-summary-jan.xlsx',
188
+ icon: 'file',
189
+ fileType: 'XLSX',
190
+ fileSize: '348 KB',
191
+ onDownload: noop,
192
+ onDelete: noop,
193
+ },
194
+ {
195
+ fileName: 'behaviour-policy-2024.docx',
196
+ icon: 'file',
197
+ fileType: 'DOCX',
198
+ fileSize: '89 KB',
199
+ onDownload: noop,
200
+ onDelete: noop,
201
+ },
202
+ ];
203
+ // ─── Stories ──────────────────────────────────────────────────────────────────
204
+ export const Default = withDescription({
205
+ args: {
206
+ uploadedFiles: [],
207
+ uploadFiles: fn(),
208
+ },
209
+ render: args => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { ...args }) })),
210
+ }, 'The default state shows the dropzone area with no files attached. '
211
+ + 'Drop files onto the area or click it to open the native file picker.');
212
+ // ─────────────────────────────────────────────────────────────────────────────
213
+ export const WithUploadedFiles = withDescription({
214
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: completedFiles, uploadFiles: noop }) })),
215
+ parameters: {
216
+ controls: { disable: true },
217
+ docs: {
218
+ source: {
219
+ language: 'tsx',
220
+ code: `
221
+ import { FileUpload } from '@arbor-education/design-system.components';
222
+
223
+ function UploadWithFilesExample() {
224
+ return (
225
+ <FileUpload
226
+ uploadedFiles={[
227
+ {
228
+ fileName: 'spring-term-report-2024.pdf',
229
+ fileType: 'PDF',
230
+ fileSize: '1.2 MB',
231
+ onDownload: () => { /* download logic */ },
232
+ onDelete: () => { /* remove from state */ },
233
+ },
234
+ {
235
+ fileName: 'attendance-summary-jan.xlsx',
236
+ fileType: 'XLSX',
237
+ fileSize: '348 KB',
238
+ onDownload: () => { /* download logic */ },
239
+ onDelete: () => { /* remove from state */ },
240
+ },
241
+ {
242
+ fileName: 'behaviour-policy-2024.docx',
243
+ fileType: 'DOCX',
244
+ fileSize: '89 KB',
245
+ onDownload: () => { /* download logic */ },
246
+ onDelete: () => { /* remove from state */ },
247
+ },
248
+ ]}
249
+ uploadFiles={(files) => { /* start upload */ }}
250
+ />
251
+ );
252
+ }
253
+ export default UploadWithFilesExample;
254
+ `.trim(),
255
+ },
256
+ },
257
+ },
258
+ }, 'Multiple completed files with both download and delete actions. '
259
+ + 'Each file shows its type and size. The download button only appears when '
260
+ + '`onDownload` is provided.');
261
+ // ─────────────────────────────────────────────────────────────────────────────
262
+ export const SingleFile = withDescription({
263
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [{
264
+ fileName: 'spring-term-report-2024.pdf',
265
+ fileType: 'PDF',
266
+ fileSize: '1.2 MB',
267
+ onDownload: noop,
268
+ onDelete: noop,
269
+ }], uploadFiles: noop }) })),
270
+ parameters: {
271
+ controls: { disable: true },
272
+ docs: {
273
+ source: {
274
+ language: 'tsx',
275
+ code: `
276
+ import { FileUpload } from '@arbor-education/design-system.components';
277
+
278
+ function SingleFileExample() {
279
+ return (
280
+ <FileUpload
281
+ uploadedFiles={[
282
+ {
283
+ fileName: 'spring-term-report-2024.pdf',
284
+ fileType: 'PDF',
285
+ fileSize: '1.2 MB',
286
+ onDownload: () => { /* download logic */ },
287
+ onDelete: () => { /* remove from state */ },
288
+ },
289
+ ]}
290
+ uploadFiles={(files) => { /* start upload */ }}
291
+ />
292
+ );
293
+ }
294
+ export default SingleFileExample;
295
+ `.trim(),
296
+ },
297
+ },
298
+ },
299
+ }, 'A single completed file. When there is only one file in the list the '
300
+ + '`Separator` between rows is not rendered.');
301
+ // ─────────────────────────────────────────────────────────────────────────────
302
+ export const WithoutDownloadButton = withDescription({
303
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
304
+ {
305
+ fileName: 'spring-term-report-2024.pdf',
306
+ fileType: 'PDF',
307
+ fileSize: '1.2 MB',
308
+ onDelete: noop,
309
+ },
310
+ {
311
+ fileName: 'attendance-summary-jan.xlsx',
312
+ fileType: 'XLSX',
313
+ fileSize: '348 KB',
314
+ onDelete: noop,
315
+ },
316
+ ], uploadFiles: noop }) })),
317
+ parameters: {
318
+ controls: { disable: true },
319
+ docs: {
320
+ source: {
321
+ language: 'tsx',
322
+ code: `
323
+ import { FileUpload } from '@arbor-education/design-system.components';
324
+
325
+ function NoDownloadExample() {
326
+ return (
327
+ <FileUpload
328
+ uploadedFiles={[
329
+ {
330
+ fileName: 'spring-term-report-2024.pdf',
331
+ fileType: 'PDF',
332
+ fileSize: '1.2 MB',
333
+ // onDownload omitted — download button will not render
334
+ onDelete: () => { /* remove from state */ },
335
+ },
336
+ {
337
+ fileName: 'attendance-summary-jan.xlsx',
338
+ fileType: 'XLSX',
339
+ fileSize: '348 KB',
340
+ onDelete: () => { /* remove from state */ },
341
+ },
342
+ ]}
343
+ uploadFiles={(files) => { /* start upload */ }}
344
+ />
345
+ );
346
+ }
347
+ export default NoDownloadExample;
348
+ `.trim(),
349
+ },
350
+ },
351
+ },
352
+ }, 'When `onDownload` is omitted from a file object the download button is not '
353
+ + 'rendered for that row. Only the delete (trash) button appears. Use this when '
354
+ + 'the uploaded file should not be retrievable after submission.');
355
+ // ─────────────────────────────────────────────────────────────────────────────
356
+ export const UploadInProgress = withDescription({
357
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
358
+ {
359
+ fileName: 'spring-term-report-2024.pdf',
360
+ fileType: 'PDF',
361
+ fileSize: '1.2 MB',
362
+ onDelete: noop,
363
+ progressPercentage: 25,
364
+ },
365
+ {
366
+ fileName: 'attendance-summary-jan.xlsx',
367
+ fileType: 'XLSX',
368
+ fileSize: '348 KB',
369
+ onDelete: noop,
370
+ progressPercentage: 72,
371
+ },
372
+ {
373
+ fileName: 'behaviour-policy-2024.docx',
374
+ fileType: 'DOCX',
375
+ fileSize: '89 KB',
376
+ onDownload: noop,
377
+ onDelete: noop,
378
+ },
379
+ ], uploadFiles: noop }) })),
380
+ parameters: {
381
+ controls: { disable: true },
382
+ docs: {
383
+ source: {
384
+ language: 'tsx',
385
+ code: `
386
+ import { FileUpload } from '@arbor-education/design-system.components';
387
+
388
+ // progressPercentage < 100 shows the progress bar.
389
+ // progressPercentage === 100 (or undefined) hides it.
390
+ function UploadInProgressExample() {
391
+ return (
392
+ <FileUpload
393
+ uploadedFiles={[
394
+ {
395
+ fileName: 'spring-term-report-2024.pdf',
396
+ fileType: 'PDF',
397
+ fileSize: '1.2 MB',
398
+ onDelete: () => { /* cancel / remove */ },
399
+ progressPercentage: 25,
400
+ },
401
+ {
402
+ fileName: 'attendance-summary-jan.xlsx',
403
+ fileType: 'XLSX',
404
+ fileSize: '348 KB',
405
+ onDelete: () => { /* cancel / remove */ },
406
+ progressPercentage: 72,
407
+ },
408
+ {
409
+ fileName: 'behaviour-policy-2024.docx',
410
+ fileType: 'DOCX',
411
+ fileSize: '89 KB',
412
+ onDownload: () => { /* download logic */ },
413
+ onDelete: () => { /* remove */ },
414
+ // No progressPercentage — this file finished earlier
415
+ },
416
+ ]}
417
+ uploadFiles={(files) => { /* start upload */ }}
418
+ />
419
+ );
420
+ }
421
+ export default UploadInProgressExample;
422
+ `.trim(),
423
+ },
424
+ },
425
+ },
426
+ }, 'Multiple files at different stages of upload. A progress bar appears beneath '
427
+ + 'each row where `progressPercentage` is a number between 0 and 99 (inclusive). '
428
+ + 'The third file has no `progressPercentage` — it completed earlier and shows '
429
+ + 'only the download and delete actions.');
430
+ // ─────────────────────────────────────────────────────────────────────────────
431
+ export const FailedUpload = withDescription({
432
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
433
+ {
434
+ fileName: 'corrupted-report.pdf',
435
+ fileType: 'PDF',
436
+ fileSize: '540 KB',
437
+ onDelete: noop,
438
+ hasUploadFailed: true,
439
+ },
440
+ {
441
+ fileName: 'spring-term-report-2024.pdf',
442
+ fileType: 'PDF',
443
+ fileSize: '1.2 MB',
444
+ onDownload: noop,
445
+ onDelete: noop,
446
+ },
447
+ ], uploadFiles: noop }) })),
448
+ parameters: {
449
+ controls: { disable: true },
450
+ docs: {
451
+ source: {
452
+ language: 'tsx',
453
+ code: `
454
+ import { FileUpload } from '@arbor-education/design-system.components';
455
+
456
+ function FailedUploadExample() {
457
+ return (
458
+ <FileUpload
459
+ uploadedFiles={[
460
+ {
461
+ fileName: 'corrupted-report.pdf',
462
+ fileType: 'PDF',
463
+ fileSize: '540 KB',
464
+ onDelete: () => { /* remove from state */ },
465
+ hasUploadFailed: true,
466
+ },
467
+ {
468
+ fileName: 'spring-term-report-2024.pdf',
469
+ fileType: 'PDF',
470
+ fileSize: '1.2 MB',
471
+ onDownload: () => { /* download logic */ },
472
+ onDelete: () => { /* remove from state */ },
473
+ },
474
+ ]}
475
+ uploadFiles={(files) => { /* retry or start new upload */ }}
476
+ />
477
+ );
478
+ }
479
+ export default FailedUploadExample;
480
+ `.trim(),
481
+ },
482
+ },
483
+ },
484
+ }, "When `hasUploadFailed` is `true` the file's type/size metadata is replaced "
485
+ + 'with a red "Upload failed" message. A completed file alongside the failed one '
486
+ + 'shows the contrast between the two states.');
487
+ // ─────────────────────────────────────────────────────────────────────────────
488
+ export const FailedDuringUpload = withDescription({
489
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [
490
+ {
491
+ fileName: 'oversized-photo.png',
492
+ fileType: 'PNG',
493
+ fileSize: '18 MB',
494
+ onDelete: noop,
495
+ hasUploadFailed: true,
496
+ progressPercentage: 63,
497
+ },
498
+ {
499
+ fileName: 'corrupted-report.pdf',
500
+ fileType: 'PDF',
501
+ fileSize: '540 KB',
502
+ onDelete: noop,
503
+ hasUploadFailed: true,
504
+ },
505
+ ], uploadFiles: noop }) })),
506
+ parameters: {
507
+ controls: { disable: true },
508
+ docs: {
509
+ source: {
510
+ language: 'tsx',
511
+ code: `
512
+ import { FileUpload } from '@arbor-education/design-system.components';
513
+
514
+ // Both hasUploadFailed: true AND progressPercentage < 100 renders
515
+ // a red progress bar in addition to the "Upload failed" message.
516
+ function FailedDuringUploadExample() {
517
+ return (
518
+ <FileUpload
519
+ uploadedFiles={[
520
+ {
521
+ fileName: 'oversized-photo.png',
522
+ fileType: 'PNG',
523
+ fileSize: '18 MB',
524
+ onDelete: () => { /* remove from state */ },
525
+ hasUploadFailed: true,
526
+ progressPercentage: 63,
527
+ },
528
+ {
529
+ fileName: 'corrupted-report.pdf',
530
+ fileType: 'PDF',
531
+ fileSize: '540 KB',
532
+ onDelete: () => { /* remove from state */ },
533
+ hasUploadFailed: true,
534
+ // No progressPercentage — failed before any progress was recorded
535
+ },
536
+ ]}
537
+ uploadFiles={(files) => { /* start upload */ }}
538
+ />
539
+ );
540
+ }
541
+ export default FailedDuringUploadExample;
542
+ `.trim(),
543
+ },
544
+ },
545
+ },
546
+ }, 'When both `hasUploadFailed: true` **and** `progressPercentage < 100` are set '
547
+ + 'together the progress bar is rendered in its error (red) state below the '
548
+ + '"Upload failed" message. The first file shows this combined state; the second '
549
+ + 'failed before any progress was recorded so no bar appears.');
550
+ // ─────────────────────────────────────────────────────────────────────────────
551
+ export const CustomDropzoneContent = withDescription({
552
+ render: () => (_jsx("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)' }, children: _jsx(FileUpload, { uploadedFiles: [], uploadFiles: noop, dropzoneContent: (_jsxs(_Fragment, { children: [_jsx("p", { className: "ds-text", children: "Attach a PDF or spreadsheet (max\u00A010\u00A0MB per file)" }), _jsx(Button, { variant: "secondary", children: "Browse files" })] })) }) })),
553
+ parameters: {
554
+ controls: { disable: true },
555
+ docs: {
556
+ source: {
557
+ language: 'tsx',
558
+ code: `
559
+ import { FileUpload, Button } from '@arbor-education/design-system.components';
560
+
561
+ function CustomDropzoneExample() {
562
+ return (
563
+ <FileUpload
564
+ uploadedFiles={[]}
565
+ uploadFiles={(files) => { /* start upload */ }}
566
+ dropzoneContent={
567
+ <>
568
+ <p>Attach a PDF or spreadsheet (max 10 MB per file)</p>
569
+ <Button variant="secondary">Browse files</Button>
570
+ </>
571
+ }
572
+ />
573
+ );
574
+ }
575
+ export default CustomDropzoneExample;
576
+ `.trim(),
577
+ },
578
+ },
579
+ },
580
+ }, 'Pass a `ReactNode` to `dropzoneContent` to replace the default '
581
+ + '"Drag and drop, or click to upload" copy. This is useful when you need '
582
+ + 'context-specific instructions or a branded call-to-action button.');
583
+ const DEMO_FILENAME = 'spring-term-report-2024.pdf';
584
+ const SimulatedUploadLifecycleTemplate = () => {
585
+ const [files, setFiles] = useState([]);
586
+ const [isRunning, setIsRunning] = useState(false);
587
+ const intervalRef = useRef(null);
588
+ const clearTimer = () => {
589
+ if (intervalRef.current !== null) {
590
+ clearInterval(intervalRef.current);
591
+ intervalRef.current = null;
592
+ }
593
+ };
594
+ const startUpload = () => {
595
+ clearTimer();
596
+ setIsRunning(true);
597
+ setFiles([{ fileName: DEMO_FILENAME, fileType: 'PDF', fileSize: '1.2 MB', progressPercentage: 0 }]);
598
+ };
599
+ const restart = () => {
600
+ clearTimer();
601
+ setIsRunning(false);
602
+ setFiles([]);
603
+ };
604
+ useEffect(() => {
605
+ if (!isRunning) {
606
+ return;
607
+ }
608
+ intervalRef.current = setInterval(() => {
609
+ setFiles((prev) => {
610
+ const current = prev[0];
611
+ if (!current || typeof current.progressPercentage !== 'number') {
612
+ return prev;
613
+ }
614
+ const next = current.progressPercentage + 8;
615
+ if (next >= 100) {
616
+ clearTimer();
617
+ setIsRunning(false);
618
+ return [{ ...current, progressPercentage: 100 }];
619
+ }
620
+ return [{ ...current, progressPercentage: next }];
621
+ });
622
+ }, 250);
623
+ return () => {
624
+ clearTimer();
625
+ };
626
+ }, [isRunning]);
627
+ return (_jsxs("div", { style: { maxWidth: '560px', padding: 'var(--spacing-large)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', gap: 'var(--spacing-medium)' }, children: [_jsx(Button, { variant: "primary", onClick: startUpload, disabled: isRunning, children: "Simulate upload" }), _jsx(Button, { variant: "secondary", onClick: restart, disabled: files.length === 0 && !isRunning, children: "Restart" })] }), _jsx(FileUpload, { uploadedFiles: files.map(f => ({
628
+ ...f,
629
+ onDelete: restart,
630
+ ...(typeof f.progressPercentage === 'number' && f.progressPercentage >= 100
631
+ ? { onDownload: noop }
632
+ : {}),
633
+ })), uploadFiles: startUpload })] }));
634
+ };
635
+ export const SimulatedUploadLifecycle = withDescription({
636
+ render: SimulatedUploadLifecycleTemplate,
637
+ parameters: {
638
+ controls: { disable: true },
639
+ docs: {
640
+ source: {
641
+ language: 'tsx',
642
+ code: `
643
+ import { useState, useEffect, useRef } from 'react';
644
+ import { FileUpload, Button } from '@arbor-education/design-system.components';
645
+
646
+ type ManagedFile = {
647
+ fileName: string;
648
+ fileType: string;
649
+ fileSize: string;
650
+ progressPercentage?: number;
651
+ };
652
+
653
+ function UploadLifecycleDemo() {
654
+ const [files, setFiles] = useState<ManagedFile[]>([]);
655
+ const [isRunning, setIsRunning] = useState(false);
656
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
657
+
658
+ const clearTimer = () => {
659
+ if (intervalRef.current !== null) {
660
+ clearInterval(intervalRef.current);
661
+ intervalRef.current = null;
662
+ }
663
+ };
664
+
665
+ const startUpload = () => {
666
+ clearTimer();
667
+ setIsRunning(true);
668
+ setFiles([{
669
+ fileName: 'spring-term-report-2024.pdf',
670
+ fileType: 'PDF',
671
+ fileSize: '1.2 MB',
672
+ progressPercentage: 0,
673
+ }]);
674
+ };
675
+
676
+ useEffect(() => {
677
+ if (!isRunning) return;
678
+
679
+ intervalRef.current = setInterval(() => {
680
+ setFiles((prev) => {
681
+ const current = prev[0];
682
+ if (!current || typeof current.progressPercentage !== 'number') return prev;
683
+
684
+ const next = current.progressPercentage + 8;
685
+ if (next >= 100) {
686
+ clearTimer();
687
+ setIsRunning(false);
688
+ return [{ ...current, progressPercentage: 100 }];
689
+ }
690
+ return [{ ...current, progressPercentage: next }];
691
+ });
692
+ }, 250);
693
+
694
+ return () => {
695
+ clearTimer();
696
+ };
697
+ }, [isRunning]);
698
+
699
+ return (
700
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
701
+ <div style={{ display: 'flex', gap: 'var(--spacing-medium)' }}>
702
+ <Button variant="primary" onClick={startUpload} disabled={isRunning}>
703
+ Simulate upload
704
+ </Button>
705
+ <Button
706
+ variant="secondary"
707
+ onClick={() => { clearTimer(); setIsRunning(false); setFiles([]); }}
708
+ disabled={files.length === 0 && !isRunning}
709
+ >
710
+ Restart
711
+ </Button>
712
+ </div>
713
+ <FileUpload
714
+ uploadedFiles={files.map((f) => ({
715
+ ...f,
716
+ onDelete: () => { clearTimer(); setIsRunning(false); setFiles([]); },
717
+ ...(typeof f.progressPercentage === 'number' && f.progressPercentage >= 100
718
+ ? { onDownload: () => { /* download */ } }
719
+ : {}),
720
+ }))}
721
+ uploadFiles={startUpload}
722
+ />
723
+ </div>
724
+ );
725
+ }
726
+ export default UploadLifecycleDemo;
727
+ `.trim(),
728
+ },
729
+ },
730
+ },
731
+ }, 'An interactive demo of the full upload lifecycle. Click **"Simulate upload"** '
732
+ + '(or drop a real file) to watch a file enter the list at 0%, tick up through '
733
+ + 'the progress bar, and complete at 100% — at which point the bar disappears '
734
+ + 'and the download button becomes available. Click **"Restart"** to reset and '
735
+ + 'run the demo again.');
736
+ //# sourceMappingURL=FileUpload.stories.js.map