@gsk_poc/untitled-ui-base 0.1.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/components/application/app-navigation/base-components/featured-cards.demo.tsx +86 -0
  4. package/components/application/app-navigation/base-components/featured-cards.story.tsx +49 -0
  5. package/components/application/app-navigation/header-navigation.demo.tsx +59 -0
  6. package/components/application/app-navigation/header-navigation.story.tsx +23 -0
  7. package/components/application/app-navigation/sidebar-navigation.demo.tsx +409 -0
  8. package/components/application/app-navigation/sidebar-navigation.story.tsx +47 -0
  9. package/components/application/carousel/carousel.demo.tsx +107 -0
  10. package/components/application/carousel/carousel.story.tsx +21 -0
  11. package/components/application/charts/activity-gauges.demo.tsx +251 -0
  12. package/components/application/charts/activity-gauges.story.tsx +27 -0
  13. package/components/application/charts/bar-charts.demo.tsx +506 -0
  14. package/components/application/charts/bar-charts.story.tsx +36 -0
  15. package/components/application/charts/line-charts.demo.tsx +604 -0
  16. package/components/application/charts/line-charts.story.tsx +43 -0
  17. package/components/application/charts/pie-charts.demo.tsx +193 -0
  18. package/components/application/charts/pie-charts.story.tsx +30 -0
  19. package/components/application/charts/progress-circles.demo.tsx +110 -0
  20. package/components/application/charts/progress-circles.story.tsx +33 -0
  21. package/components/application/charts/radar-charts.demo.tsx +203 -0
  22. package/components/application/charts/radar-charts.story.tsx +18 -0
  23. package/components/application/date-picker/date-picker.demo.tsx +217 -0
  24. package/components/application/date-picker/date-picker.story.tsx +44 -0
  25. package/components/application/file-upload/file-upload.demo.tsx +292 -0
  26. package/components/application/file-upload/file-upload.story.tsx +70 -0
  27. package/components/application/loading-indicator/loading-indicator.demo.tsx +73 -0
  28. package/components/application/loading-indicator/loading-indicator.story.tsx +22 -0
  29. package/components/application/pagination/pagination.demo.tsx +104 -0
  30. package/components/application/pagination/pagination.story.tsx +54 -0
  31. package/components/application/table/table.demo.tsx +1038 -0
  32. package/components/application/table/table.story.tsx +119 -0
  33. package/components/application/tabs/tabs.demo.tsx +322 -0
  34. package/components/application/tabs/tabs.story.tsx +43 -0
  35. package/components/base/avatar/avatar.demo.tsx +865 -0
  36. package/components/base/avatar/avatar.story.tsx +27 -0
  37. package/components/base/badges/badge-groups.demo.tsx +357 -0
  38. package/components/base/badges/badge-groups.story.tsx +25 -0
  39. package/components/base/badges/badges.demo.tsx +497 -0
  40. package/components/base/badges/badges.story.tsx +83 -0
  41. package/components/base/button-group/button-group.demo.tsx +131 -0
  42. package/components/base/button-group/button-group.story.tsx +16 -0
  43. package/components/base/buttons/app-store-buttons.demo.tsx +129 -0
  44. package/components/base/buttons/app-store-buttons.story.tsx +13 -0
  45. package/components/base/buttons/buttons.demo.tsx +1022 -0
  46. package/components/base/buttons/buttons.story.tsx +42 -0
  47. package/components/base/buttons/social-buttons.demo.tsx +432 -0
  48. package/components/base/buttons/social-buttons.story.tsx +20 -0
  49. package/components/base/checkbox/checkbox.demo.tsx +62 -0
  50. package/components/base/checkbox/checkbox.story.tsx +18 -0
  51. package/components/base/dropdown/dropdown.demo.tsx +137 -0
  52. package/components/base/dropdown/dropdown.story.tsx +22 -0
  53. package/components/base/input/inputs.demo.tsx +758 -0
  54. package/components/base/input/inputs.story.tsx +52 -0
  55. package/components/base/pin-input/pin-input.demo.tsx +126 -0
  56. package/components/base/pin-input/pin-input.story.tsx +22 -0
  57. package/components/base/progress-indicators/progress-indicators.demo.tsx +54 -0
  58. package/components/base/progress-indicators/progress-indicators.story.tsx +57 -0
  59. package/components/base/radio-buttons/radio-buttons.demo.tsx +77 -0
  60. package/components/base/radio-buttons/radio-buttons.story.tsx +19 -0
  61. package/components/base/select/select.demo.tsx +936 -0
  62. package/components/base/select/select.story.tsx +43 -0
  63. package/components/base/slider/slider.demo.tsx +19 -0
  64. package/components/base/slider/slider.story.tsx +26 -0
  65. package/components/base/tags/tags.demo.tsx +341 -0
  66. package/components/base/tags/tags.story.tsx +28 -0
  67. package/components/base/textarea/textarea.demo.tsx +25 -0
  68. package/components/base/textarea/textarea.story.tsx +15 -0
  69. package/components/base/toggle/toggle.demo.tsx +76 -0
  70. package/components/base/toggle/toggle.story.tsx +23 -0
  71. package/components/base/tooltip/tooltip.demo.tsx +125 -0
  72. package/components/base/tooltip/tooltip.story.tsx +21 -0
  73. package/components/foundations/featured-icon/featured-icon.demo.tsx +160 -0
  74. package/components/foundations/featured-icon/featured-icon.story.tsx +25 -0
  75. package/components/shared-assets/credit-card/credit-card.demo.tsx +106 -0
  76. package/components/shared-assets/credit-card/credit-card.story.tsx +41 -0
  77. package/dist/index.d.mts +1417 -0
  78. package/dist/index.d.ts +1417 -0
  79. package/dist/index.js +10435 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/index.mjs +10345 -0
  82. package/dist/index.mjs.map +1 -0
  83. package/package.json +126 -0
  84. package/styles/globals.css +65 -0
  85. package/styles/theme.css +430 -0
  86. package/styles/tokens-mapped.css +233 -0
  87. package/styles/tokens.css +807 -0
  88. package/styles/typography.css +430 -0
  89. package/tokens/design-tokens.dtcg.json +3515 -0
@@ -0,0 +1,217 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { endOfMonth, endOfWeek, getLocalTimeZone, startOfMonth, startOfWeek, today } from "@internationalized/date";
5
+ import type { DateValue } from "react-aria-components";
6
+ import { DatePicker as AriaDatePicker, DateRangePicker as AriaDateRangePicker, Dialog as AriaDialog, useLocale } from "react-aria-components";
7
+ import { Button } from "@/components/base/buttons/button";
8
+ import { Calendar } from "./calendar";
9
+ import { DateInput } from "./date-input";
10
+ import { DatePicker } from "./date-picker";
11
+ import { DateRangePicker } from "./date-range-picker";
12
+ import { RangeCalendar } from "./range-calendar";
13
+ import { RangePresetButton } from "./range-preset";
14
+
15
+ const now = today(getLocalTimeZone());
16
+
17
+ export const CalendarDemo = () => <Calendar aria-label="Calendar" />;
18
+
19
+ export const CalendarCardDemo = () => (
20
+ <AriaDatePicker aria-label="Calendar card" defaultValue={now}>
21
+ <AriaDialog className="rounded-2xl bg-primary shadow-xl ring ring-secondary_alt">
22
+ <div className="flex px-6 py-5">
23
+ <Calendar />
24
+ </div>
25
+ <div className="grid grid-cols-2 gap-3 border-t border-secondary p-4">
26
+ <Button size="md" color="secondary">
27
+ Cancel
28
+ </Button>
29
+ <Button size="md" color="primary">
30
+ Apply
31
+ </Button>
32
+ </div>
33
+ </AriaDialog>
34
+ </AriaDatePicker>
35
+ );
36
+
37
+ export const DatePickerDemo = () => <DatePicker aria-label="Date picker" defaultValue={now} />;
38
+
39
+ export const DatePickerControlledDemo = () => {
40
+ const [value, setValue] = useState<DateValue | null>(now);
41
+
42
+ return <DatePicker aria-label="Date picker" value={value} onChange={setValue} />;
43
+ };
44
+
45
+ export const RangeCalendarDemo = () => <RangeCalendar aria-label="Range calendar" />;
46
+
47
+ export const RangeCalendarCardDemo = () => {
48
+ const { locale } = useLocale();
49
+ const [focusedValue, setFocusedValue] = useState<DateValue | null>(null);
50
+ const [value, setValue] = useState<{ start: DateValue; end: DateValue } | null>({
51
+ start: now.subtract({ days: 7 }),
52
+ end: now,
53
+ });
54
+
55
+ const presets = useMemo(
56
+ () => ({
57
+ today: { label: "Today", value: { start: now, end: now } },
58
+ yesterday: { label: "Yesterday", value: { start: now.subtract({ days: 1 }), end: now.subtract({ days: 1 }) } },
59
+ thisWeek: { label: "This week", value: { start: startOfWeek(now, locale), end: endOfWeek(now, locale) } },
60
+ lastWeek: {
61
+ label: "Last week",
62
+ value: {
63
+ start: startOfWeek(now, locale).subtract({ weeks: 1 }),
64
+ end: endOfWeek(now, locale).subtract({ weeks: 1 }),
65
+ },
66
+ },
67
+ thisMonth: { label: "This month", value: { start: startOfMonth(now), end: endOfMonth(now) } },
68
+ lastMonth: {
69
+ label: "Last month",
70
+ value: {
71
+ start: startOfMonth(now).subtract({ months: 1 }),
72
+ end: endOfMonth(now).subtract({ months: 1 }),
73
+ },
74
+ },
75
+ thisYear: { label: "This year", value: { start: startOfMonth(now.set({ month: 1 })), end: endOfMonth(now.set({ month: 12 })) } },
76
+ lastYear: {
77
+ label: "Last year",
78
+ value: {
79
+ start: startOfMonth(now.set({ month: 1 }).subtract({ years: 1 })),
80
+ end: endOfMonth(now.set({ month: 12 }).subtract({ years: 1 })),
81
+ },
82
+ },
83
+ allTime: {
84
+ label: "All time",
85
+ value: {
86
+ start: now.set({ year: 2000, month: 1, day: 1 }),
87
+ end: now,
88
+ },
89
+ },
90
+ }),
91
+ [locale],
92
+ );
93
+
94
+ return (
95
+ <AriaDateRangePicker aria-label="Range calendar" value={value} onChange={setValue}>
96
+ <AriaDialog className="flex rounded-2xl bg-primary shadow-xl ring ring-secondary_alt focus:outline-hidden">
97
+ <div className="hidden w-38 flex-col gap-0.5 border-r border-solid border-secondary p-3 lg:flex">
98
+ {Object.values(presets).map((preset) => (
99
+ <RangePresetButton
100
+ key={preset.label}
101
+ value={preset.value}
102
+ onClick={() => {
103
+ setFocusedValue(preset.value.start);
104
+ setValue(preset.value);
105
+ }}
106
+ >
107
+ {preset.label}
108
+ </RangePresetButton>
109
+ ))}
110
+ </div>
111
+ <div className="flex flex-col">
112
+ <RangeCalendar
113
+ focusedValue={focusedValue}
114
+ onFocusChange={setFocusedValue}
115
+ presets={{
116
+ lastWeek: presets.lastWeek,
117
+ lastMonth: presets.lastMonth,
118
+ lastYear: presets.lastYear,
119
+ }}
120
+ />
121
+ <div className="flex justify-between gap-3 border-t border-secondary p-4">
122
+ <div className="hidden items-center gap-3 md:flex">
123
+ <DateInput slot="start" className="w-36" />
124
+ <div className="text-md text-quaternary">–</div>
125
+ <DateInput slot="end" className="w-36" />
126
+ </div>
127
+ <div className="grid w-full grid-cols-2 gap-3 md:flex md:w-auto">
128
+ <Button size="md" color="secondary">
129
+ Cancel
130
+ </Button>
131
+ <Button size="md" color="primary">
132
+ Apply
133
+ </Button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </AriaDialog>
138
+ </AriaDateRangePicker>
139
+ );
140
+ };
141
+
142
+ export const DateRangePickerDemo = () => {
143
+ return (
144
+ <DateRangePicker
145
+ aria-label="Date range picker"
146
+ defaultValue={{
147
+ start: now.subtract({ days: 7 }),
148
+ end: now,
149
+ }}
150
+ />
151
+ );
152
+ };
153
+
154
+ export const DateRangePickerControlledDemo = () => {
155
+ const [value, setValue] = useState<{ start: DateValue; end: DateValue } | null>({
156
+ start: now.subtract({ days: 7 }),
157
+ end: now,
158
+ });
159
+
160
+ return <DateRangePicker aria-label="Date range picker" shouldCloseOnSelect={false} value={value} onChange={setValue} />;
161
+ };
162
+
163
+ export const DarkModeDemo = () => {
164
+ const [value, setValue] = useState<DateValue | null>(now);
165
+ const [focusedValue, setFocusedValue] = useState<DateValue | null>(now);
166
+
167
+ return (
168
+ <div className="relative h-180 w-full max-w-180 [--clip-boundary:50%]">
169
+ <div
170
+ style={{
171
+ clipPath: "polygon(0 0, var(--clip-boundary) 0, var(--clip-boundary) 100%, 0 100%)",
172
+ transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)",
173
+ }}
174
+ className="peer/light absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-tertiary outline-1 -outline-offset-1 outline-secondary_alt transition-all duration-200 peer-hover/dark:[--clip-boundary:10%] hover:z-10 hover:[--clip-boundary:90%]"
175
+ >
176
+ <AriaDatePicker aria-label="Calendar card" value={value} onChange={setValue}>
177
+ <AriaDialog className="rounded-2xl bg-primary shadow-xl ring ring-secondary_alt">
178
+ <div className="flex px-6 py-5">
179
+ <Calendar focusedValue={focusedValue} onFocusChange={setFocusedValue} />
180
+ </div>
181
+ <div className="grid grid-cols-2 gap-3 border-t border-secondary p-4">
182
+ <Button size="md" color="secondary">
183
+ Cancel
184
+ </Button>
185
+ <Button size="md" color="primary">
186
+ Apply
187
+ </Button>
188
+ </div>
189
+ </AriaDialog>
190
+ </AriaDatePicker>
191
+ </div>
192
+ <div
193
+ style={{
194
+ clipPath: "polygon(var(--clip-boundary) 0, 100% 0, 100% 100%, var(--clip-boundary) 100%)",
195
+ transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)",
196
+ }}
197
+ className="peer/dark dark-mode absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-tertiary outline-1 -outline-offset-1 outline-secondary_alt transition-all duration-200 peer-hover/light:[--clip-boundary:90%] hover:z-10 hover:[--clip-boundary:10%]"
198
+ >
199
+ <AriaDatePicker aria-label="Calendar card" value={value} onChange={setValue}>
200
+ <AriaDialog className="rounded-2xl bg-primary shadow-xl ring ring-secondary_alt">
201
+ <div className="flex px-6 py-5">
202
+ <Calendar focusedValue={focusedValue} onFocusChange={setFocusedValue} />
203
+ </div>
204
+ <div className="grid grid-cols-2 gap-3 border-t border-secondary p-4">
205
+ <Button size="md" color="secondary">
206
+ Cancel
207
+ </Button>
208
+ <Button size="md" color="primary">
209
+ Apply
210
+ </Button>
211
+ </div>
212
+ </AriaDialog>
213
+ </AriaDatePicker>
214
+ </div>
215
+ </div>
216
+ );
217
+ };
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import type { FC } from "react";
4
+ import * as Demos from "./date-picker.demo";
5
+
6
+ export default {
7
+ title: "Application/Date pickers",
8
+ decorators: [
9
+ (Story: FC) => (
10
+ <div className="flex min-h-screen items-start justify-center bg-tertiary p-8">
11
+ <div className="flex w-75 justify-end">
12
+ <Story />
13
+ </div>
14
+ </div>
15
+ ),
16
+ ],
17
+ };
18
+
19
+ export const CalendarDemo = () => <Demos.CalendarDemo />;
20
+ CalendarDemo.storyName = "Calendar";
21
+
22
+ export const CalendarCardDemo = () => <Demos.CalendarCardDemo />;
23
+ CalendarCardDemo.storyName = "Calendar card";
24
+
25
+ export const DatePickerDemo = () => <Demos.DatePickerDemo />;
26
+ DatePickerDemo.storyName = "Date picker";
27
+
28
+ export const DatePickerControlledDemo = () => <Demos.DatePickerControlledDemo />;
29
+ DatePickerControlledDemo.storyName = "Date picker controlled";
30
+
31
+ export const RangeCalendarDemo = () => <Demos.RangeCalendarDemo />;
32
+ RangeCalendarDemo.storyName = "Range calendar";
33
+
34
+ export const RangeCalendarCardDemo = () => <Demos.RangeCalendarCardDemo />;
35
+ RangeCalendarCardDemo.storyName = "Range calendar card";
36
+
37
+ export const DateRangePickerDemo = () => <Demos.DateRangePickerDemo />;
38
+ DateRangePickerDemo.storyName = "Date range picker";
39
+
40
+ export const DateRangePickerControlledDemo = () => <Demos.DateRangePickerControlledDemo />;
41
+ DateRangePickerControlledDemo.storyName = "Date range picker controlled";
42
+
43
+ export const DarkModeDemo = () => <Demos.DarkModeDemo />;
44
+ DarkModeDemo.storyName = "Dark mode demo";
@@ -0,0 +1,292 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { FileUpload, getReadableFileSize } from "@/components/application/file-upload/file-upload-base";
5
+
6
+ const uploadFile = (file: File, onProgress: (progress: number) => void) => {
7
+ // Add your upload logic here...
8
+
9
+ // This is dummy upload logic
10
+ let progress = 0;
11
+ const interval = setInterval(() => {
12
+ onProgress(++progress);
13
+ if (progress === 100) {
14
+ clearInterval(interval);
15
+ }
16
+ }, 100);
17
+ };
18
+
19
+ type UploadedFile = { id: string; name: string; size: number; progress: number; type?: string; failed?: boolean };
20
+
21
+ export const ImagesOnlyDemo = () => {
22
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
23
+
24
+ const handleDropFiles = (files: FileList) => {
25
+ const newFiles = Array.from(files);
26
+ const newFilesWithIds = newFiles.map((file) => ({
27
+ id: Math.random().toString(),
28
+ name: file.name,
29
+ size: file.size,
30
+ type: file.type,
31
+ progress: 0,
32
+ fileObject: file,
33
+ }));
34
+
35
+ setUploadedFiles([...newFilesWithIds.map(({ fileObject: _, ...file }) => file), ...uploadedFiles]);
36
+
37
+ newFilesWithIds.forEach(({ id, fileObject }) => {
38
+ uploadFile(fileObject, (progress) => {
39
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress } : uploadedFile)));
40
+ });
41
+ });
42
+ };
43
+
44
+ const handleDropUnacceptedFiles = (files: FileList) => {
45
+ console.log("Unaccepted files", files);
46
+ };
47
+
48
+ const handleDeleteFile = (id: string) => {
49
+ setUploadedFiles((prev) => prev.filter((file) => file.id !== id));
50
+ };
51
+
52
+ const handleRetryFile = (id: string) => {
53
+ const file = uploadedFiles.find((file) => file.id === id);
54
+ if (!file) return;
55
+
56
+ uploadFile(new File([], file.name, { type: file.type }), (progress) => {
57
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress, failed: false } : uploadedFile)));
58
+ });
59
+ };
60
+
61
+ return (
62
+ <FileUpload.Root>
63
+ <FileUpload.DropZone
64
+ accept="image/*"
65
+ hint="Please upload PNG or JPEG images only."
66
+ onDropFiles={handleDropFiles}
67
+ onDropUnacceptedFiles={handleDropUnacceptedFiles}
68
+ />
69
+
70
+ <FileUpload.List>
71
+ {uploadedFiles.map((file) => (
72
+ <FileUpload.ListItemProgressBar
73
+ key={file.id}
74
+ {...file}
75
+ size={file.size}
76
+ onDelete={() => handleDeleteFile(file.id)}
77
+ onRetry={() => handleRetryFile(file.id)}
78
+ />
79
+ ))}
80
+ </FileUpload.List>
81
+ </FileUpload.Root>
82
+ );
83
+ };
84
+
85
+ export const MaxSizeLimitDemo = () => {
86
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
87
+
88
+ const handleDropFiles = (files: FileList) => {
89
+ const newFiles = Array.from(files);
90
+ const newFilesWithIds = newFiles.map((file) => ({
91
+ id: Math.random().toString(),
92
+ name: file.name,
93
+ size: file.size,
94
+ type: file.type,
95
+ progress: 0,
96
+ fileObject: file,
97
+ }));
98
+
99
+ setUploadedFiles([...newFilesWithIds.map(({ fileObject: _, ...file }) => file), ...uploadedFiles]);
100
+
101
+ newFilesWithIds.forEach(({ id, fileObject }) => {
102
+ uploadFile(fileObject, (progress) => {
103
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress } : uploadedFile)));
104
+ });
105
+ });
106
+ };
107
+
108
+ const handleMaxSizeExceed = (files: FileList) => {
109
+ console.log("Max size exceeded", files);
110
+ };
111
+
112
+ const handleDeleteFile = (id: string) => {
113
+ setUploadedFiles((prev) => prev.filter((file) => file.id !== id));
114
+ };
115
+
116
+ const handleRetryFile = (id: string) => {
117
+ const file = uploadedFiles.find((file) => file.id === id);
118
+ if (!file) return;
119
+
120
+ uploadFile(new File([], file.name, { type: file.type }), (progress) => {
121
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress, failed: false } : uploadedFile)));
122
+ });
123
+ };
124
+
125
+ const MAX_SIZE = 1024 * 1024 * 1;
126
+
127
+ return (
128
+ <FileUpload.Root>
129
+ <FileUpload.DropZone
130
+ maxSize={MAX_SIZE}
131
+ hint={`Upload files to add to this project (max. ${getReadableFileSize(MAX_SIZE)}).`}
132
+ onDropFiles={handleDropFiles}
133
+ onSizeLimitExceed={handleMaxSizeExceed}
134
+ />
135
+
136
+ <FileUpload.List>
137
+ {uploadedFiles.map((file) => (
138
+ <FileUpload.ListItemProgressBar
139
+ key={file.id}
140
+ {...file}
141
+ size={file.size}
142
+ onDelete={() => handleDeleteFile(file.id)}
143
+ onRetry={() => handleRetryFile(file.id)}
144
+ />
145
+ ))}
146
+ </FileUpload.List>
147
+ </FileUpload.Root>
148
+ );
149
+ };
150
+
151
+ export const DisabledDemo = () => {
152
+ return (
153
+ <FileUpload.Root>
154
+ <FileUpload.DropZone isDisabled />
155
+ </FileUpload.Root>
156
+ );
157
+ };
158
+
159
+ const placeholderFiles = [
160
+ {
161
+ id: "file-01",
162
+ name: "Example dashboard screenshot.jpg",
163
+ type: "jpg",
164
+ size: 720 * 1024,
165
+ progress: 50,
166
+ },
167
+ {
168
+ id: "file-02",
169
+ name: "Tech design requirements_2.pdf",
170
+ type: "pdf",
171
+ size: 720 * 1024,
172
+ progress: 100,
173
+ },
174
+ {
175
+ id: "file-03",
176
+ name: "Tech design requirements.pdf",
177
+ type: "pdf",
178
+ failed: true,
179
+ size: 1024 * 1024 * 1,
180
+ progress: 0,
181
+ },
182
+ ];
183
+
184
+ export const FileUploadProgressBar = (props: { isDisabled?: boolean }) => {
185
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>(placeholderFiles);
186
+
187
+ const handleDropFiles = (files: FileList) => {
188
+ const newFiles = Array.from(files);
189
+ const newFilesWithIds = newFiles.map((file) => ({
190
+ id: Math.random().toString(),
191
+ name: file.name,
192
+ size: file.size,
193
+ type: file.type,
194
+ progress: 0,
195
+ fileObject: file,
196
+ }));
197
+
198
+ setUploadedFiles([...newFilesWithIds.map(({ fileObject: _, ...file }) => file), ...uploadedFiles]);
199
+
200
+ newFilesWithIds.forEach(({ id, fileObject }) => {
201
+ uploadFile(fileObject, (progress) => {
202
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress } : uploadedFile)));
203
+ });
204
+ });
205
+ };
206
+
207
+ const handleDeleteFile = (id: string) => {
208
+ setUploadedFiles((prev) => prev.filter((file) => file.id !== id));
209
+ };
210
+
211
+ const handleRetryFile = (id: string) => {
212
+ const file = uploadedFiles.find((file) => file.id === id);
213
+ if (!file) return;
214
+
215
+ uploadFile(new File([], file.name, { type: file.type }), (progress) => {
216
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress, failed: false } : uploadedFile)));
217
+ });
218
+ };
219
+
220
+ return (
221
+ <FileUpload.Root>
222
+ <FileUpload.DropZone isDisabled={props.isDisabled} onDropFiles={handleDropFiles} />
223
+
224
+ <FileUpload.List>
225
+ {uploadedFiles.map((file) => (
226
+ <FileUpload.ListItemProgressBar
227
+ key={file.id}
228
+ {...file}
229
+ size={file.size}
230
+ onDelete={() => handleDeleteFile(file.id)}
231
+ onRetry={() => handleRetryFile(file.id)}
232
+ />
233
+ ))}
234
+ </FileUpload.List>
235
+ </FileUpload.Root>
236
+ );
237
+ };
238
+
239
+ export const FileUploadProgressFill = (props: { isDisabled?: boolean }) => {
240
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>(placeholderFiles);
241
+
242
+ const handleDropFiles = (files: FileList) => {
243
+ const newFiles = Array.from(files);
244
+ const newFilesWithIds = newFiles.map((file) => ({
245
+ id: Math.random().toString(),
246
+ name: file.name,
247
+ size: file.size,
248
+ type: file.type,
249
+ progress: 0,
250
+ fileObject: file,
251
+ }));
252
+
253
+ setUploadedFiles([...newFilesWithIds.map(({ fileObject: _, ...file }) => file), ...uploadedFiles]);
254
+
255
+ newFilesWithIds.forEach(({ id, fileObject }) => {
256
+ uploadFile(fileObject, (progress) => {
257
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress } : uploadedFile)));
258
+ });
259
+ });
260
+ };
261
+
262
+ const handleDeleteFile = (id: string) => {
263
+ setUploadedFiles((prev) => prev.filter((file) => file.id !== id));
264
+ };
265
+
266
+ const handleRetryFile = (id: string) => {
267
+ const file = uploadedFiles.find((file) => file.id === id);
268
+ if (!file) return;
269
+
270
+ uploadFile(new File([], file.name, { type: file.type }), (progress) => {
271
+ setUploadedFiles((prev) => prev.map((uploadedFile) => (uploadedFile.id === id ? { ...uploadedFile, progress, failed: false } : uploadedFile)));
272
+ });
273
+ };
274
+
275
+ return (
276
+ <FileUpload.Root>
277
+ <FileUpload.DropZone isDisabled={props.isDisabled} onDropFiles={handleDropFiles} />
278
+
279
+ <FileUpload.List>
280
+ {uploadedFiles.map((file) => (
281
+ <FileUpload.ListItemProgressFill
282
+ key={file.id}
283
+ {...file}
284
+ size={file.size}
285
+ onDelete={() => handleDeleteFile(file.id)}
286
+ onRetry={() => handleRetryFile(file.id)}
287
+ />
288
+ ))}
289
+ </FileUpload.List>
290
+ </FileUpload.Root>
291
+ );
292
+ };
@@ -0,0 +1,70 @@
1
+ import type { FC } from "react";
2
+ import { Draggable } from "./draggable";
3
+ import * as FileUploads from "./file-upload.demo";
4
+
5
+ export default {
6
+ title: "Application/File upload",
7
+ decorators: [
8
+ (Story: FC) => (
9
+ <div data-drag-constraint className="flex min-h-screen w-full flex-col items-center gap-12 bg-primary p-8">
10
+ <Story />
11
+ </div>
12
+ ),
13
+ ],
14
+ };
15
+
16
+ export const FileUploadProgressBar = () => (
17
+ <>
18
+ <div className="mb-12 flex">
19
+ <Draggable name="chatgpt-clone.ts" type="application/typescript" size={1024 * 1024 * 0.5} />
20
+ <Draggable name="Side project.mp4" type="video/mp4" size={1024 * 1024 * 2.2} />
21
+ <Draggable name="Invoice #876.pdf" type="application/pdf" size={1024 * 1024 * 1.2} />
22
+ </div>
23
+ <div className="flex w-full max-w-xl flex-col gap-12">
24
+ <FileUploads.FileUploadProgressBar />
25
+ </div>
26
+ </>
27
+ );
28
+ FileUploadProgressBar.storyName = "File upload progress bar";
29
+
30
+ export const FileUploadProgressBarDisabled = () => (
31
+ <>
32
+ <div className="mb-12 flex">
33
+ <Draggable name="chatgpt-clone.ts" type="application/typescript" size={1024 * 1024 * 0.5} />
34
+ <Draggable name="Side project.mp4" type="video/mp4" size={1024 * 1024 * 2.2} />
35
+ <Draggable name="Invoice #876.pdf" type="application/pdf" size={1024 * 1024 * 1.2} />
36
+ </div>
37
+ <div className="flex w-full max-w-xl flex-col gap-12">
38
+ <FileUploads.FileUploadProgressBar isDisabled />
39
+ </div>
40
+ </>
41
+ );
42
+ FileUploadProgressBarDisabled.storyName = "File upload progress bar disabled";
43
+
44
+ export const FileUploadProgressFill = () => (
45
+ <>
46
+ <div className="mb-12 flex">
47
+ <Draggable name="chatgpt-clone.ts" type="application/typescript" size={1024 * 1024 * 0.5} />
48
+ <Draggable name="Side project.mp4" type="video/mp4" size={1024 * 1024 * 2.2} />
49
+ <Draggable name="Invoice #876.pdf" type="application/pdf" size={1024 * 1024 * 1.2} />
50
+ </div>
51
+ <div className="flex w-full max-w-xl flex-col gap-12">
52
+ <FileUploads.FileUploadProgressFill />
53
+ </div>
54
+ </>
55
+ );
56
+ FileUploadProgressFill.storyName = "File upload progress fill";
57
+
58
+ export const FileUploadProgressFillDisabled = () => (
59
+ <>
60
+ <div className="mb-12 flex">
61
+ <Draggable name="chatgpt-clone.ts" type="application/typescript" size={1024 * 1024 * 0.5} />
62
+ <Draggable name="Side project.mp4" type="video/mp4" size={1024 * 1024 * 2.2} />
63
+ <Draggable name="Invoice #876.pdf" type="application/pdf" size={1024 * 1024 * 1.2} />
64
+ </div>
65
+ <div className="flex w-full max-w-xl flex-col gap-12">
66
+ <FileUploads.FileUploadProgressFill isDisabled />
67
+ </div>
68
+ </>
69
+ );
70
+ FileUploadProgressFillDisabled.storyName = "File upload progress fill disabled";