banhaten 0.5.0 → 0.6.0

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 (290) hide show
  1. package/CHANGELOG.md +177 -139
  2. package/CONTRIBUTING.md +46 -46
  3. package/README.md +192 -192
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -13
  7. package/docs/design-system/README.md +22 -22
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -245
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -225
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +91 -90
  20. package/docs/design-system/components/accordion.md +52 -52
  21. package/docs/design-system/components/activity-feed.md +93 -93
  22. package/docs/design-system/components/activity-heatmap.md +56 -56
  23. package/docs/design-system/components/alert-dialog.md +71 -71
  24. package/docs/design-system/components/alert.md +103 -97
  25. package/docs/design-system/components/app-header.md +97 -0
  26. package/docs/design-system/components/aspect-ratio.md +45 -45
  27. package/docs/design-system/components/attribute.md +101 -89
  28. package/docs/design-system/components/autocomplete.md +75 -75
  29. package/docs/design-system/components/avatar.md +53 -53
  30. package/docs/design-system/components/badge.md +55 -55
  31. package/docs/design-system/components/banner.md +86 -86
  32. package/docs/design-system/components/breadcrumbs.md +181 -177
  33. package/docs/design-system/components/button-group.md +97 -97
  34. package/docs/design-system/components/button.md +80 -80
  35. package/docs/design-system/components/card.md +102 -88
  36. package/docs/design-system/components/carousel.md +46 -46
  37. package/docs/design-system/components/catalog-components.md +46 -46
  38. package/docs/design-system/components/checkbox.md +53 -53
  39. package/docs/design-system/components/collapsible.md +63 -63
  40. package/docs/design-system/components/command.md +61 -61
  41. package/docs/design-system/components/context-menu.md +45 -45
  42. package/docs/design-system/components/date-picker.md +80 -80
  43. package/docs/design-system/components/divider.md +102 -102
  44. package/docs/design-system/components/empty-state.md +56 -56
  45. package/docs/design-system/components/field.md +70 -70
  46. package/docs/design-system/components/file-upload.md +186 -186
  47. package/docs/design-system/components/hover-card.md +47 -47
  48. package/docs/design-system/components/icons.md +48 -48
  49. package/docs/design-system/components/input-group.md +57 -57
  50. package/docs/design-system/components/input-otp.md +56 -56
  51. package/docs/design-system/components/input.md +111 -109
  52. package/docs/design-system/components/item.md +58 -58
  53. package/docs/design-system/components/kbd.md +49 -49
  54. package/docs/design-system/components/label.md +49 -49
  55. package/docs/design-system/components/menu.md +94 -73
  56. package/docs/design-system/components/menubar.md +46 -46
  57. package/docs/design-system/components/modal.md +104 -104
  58. package/docs/design-system/components/native-select.md +53 -53
  59. package/docs/design-system/components/navigation-menu.md +49 -49
  60. package/docs/design-system/components/onboarding-step-list-item.md +81 -81
  61. package/docs/design-system/components/page-header.md +127 -93
  62. package/docs/design-system/components/pagination.md +56 -56
  63. package/docs/design-system/components/popover.md +59 -59
  64. package/docs/design-system/components/progress-slider.md +49 -49
  65. package/docs/design-system/components/progress.md +76 -76
  66. package/docs/design-system/components/radio-card.md +50 -50
  67. package/docs/design-system/components/radio-group.md +56 -56
  68. package/docs/design-system/components/resizable.md +43 -43
  69. package/docs/design-system/components/scroll-area.md +50 -50
  70. package/docs/design-system/components/segmented-control.md +54 -54
  71. package/docs/design-system/components/select.md +68 -64
  72. package/docs/design-system/components/sheet.md +66 -66
  73. package/docs/design-system/components/sidebar.md +167 -111
  74. package/docs/design-system/components/skeleton.md +74 -74
  75. package/docs/design-system/components/slideout.md +64 -64
  76. package/docs/design-system/components/slider.md +62 -62
  77. package/docs/design-system/components/social-button.md +49 -49
  78. package/docs/design-system/components/spinner.md +62 -62
  79. package/docs/design-system/components/steps.md +65 -65
  80. package/docs/design-system/components/table.md +499 -422
  81. package/docs/design-system/components/tabs.md +74 -59
  82. package/docs/design-system/components/tag.md +79 -79
  83. package/docs/design-system/components/textarea.md +49 -49
  84. package/docs/design-system/components/timeline.md +82 -82
  85. package/docs/design-system/components/toast.md +57 -57
  86. package/docs/design-system/components/toggle.md +80 -80
  87. package/docs/design-system/components/toolbar.md +89 -89
  88. package/docs/design-system/components/tooltip.md +92 -92
  89. package/docs/design-system/components/typography.md +18 -18
  90. package/docs/design-system/density-and-elevation.md +65 -65
  91. package/docs/design-system/design-system-test-missing-items.md +368 -368
  92. package/docs/design-system/direction-and-localization.md +23 -23
  93. package/docs/design-system/form-recipes.md +73 -73
  94. package/docs/design-system/gap-promotion-policy.md +21 -21
  95. package/docs/design-system/icons.md +74 -74
  96. package/docs/design-system/installation.md +70 -70
  97. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  98. package/docs/design-system/registry-and-cli.md +64 -64
  99. package/docs/design-system/release-policy.md +30 -30
  100. package/docs/design-system/roadmap.md +14 -14
  101. package/docs/design-system/support-matrix.md +23 -23
  102. package/docs/design-system/tabs.md +54 -54
  103. package/docs/design-system/template-gap-audit.md +32 -32
  104. package/docs/design-system/templates.md +59 -59
  105. package/docs/design-system/token-governance.md +72 -72
  106. package/package.json +98 -98
  107. package/registry/component-contract.json +40 -40
  108. package/registry/components/accordion.tsx +155 -155
  109. package/registry/components/alert-dialog.tsx +297 -297
  110. package/registry/components/alert.tsx +309 -293
  111. package/registry/components/app-header.tsx +151 -0
  112. package/registry/components/aspect-ratio.tsx +30 -30
  113. package/registry/components/attribute.tsx +489 -454
  114. package/registry/components/autocomplete.tsx +598 -598
  115. package/registry/components/avatar.tsx +385 -385
  116. package/registry/components/badge.tsx +630 -629
  117. package/registry/components/button-group.tsx +565 -565
  118. package/registry/components/button.tsx +128 -127
  119. package/registry/components/card.tsx +651 -638
  120. package/registry/components/carousel.tsx +296 -296
  121. package/registry/components/chart.tsx +203 -203
  122. package/registry/components/checkbox.tsx +247 -247
  123. package/registry/components/collapsible.tsx +109 -109
  124. package/registry/components/command.tsx +208 -208
  125. package/registry/components/context-menu.tsx +236 -236
  126. package/registry/components/date-picker.tsx +1289 -1289
  127. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  128. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  129. package/registry/components/expanded/Banner.tsx +179 -179
  130. package/registry/components/expanded/Breadcrumbs.tsx +135 -131
  131. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  132. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  133. package/registry/components/expanded/EmptyState.tsx +155 -155
  134. package/registry/components/expanded/FileUpload.tsx +543 -543
  135. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -60
  136. package/registry/components/expanded/PageHeader.tsx +209 -179
  137. package/registry/components/expanded/Slideout.tsx +108 -108
  138. package/registry/components/expanded/Steps.tsx +402 -402
  139. package/registry/components/expanded/Table.tsx +1109 -1092
  140. package/registry/components/expanded/Timeline.tsx +203 -203
  141. package/registry/components/expanded/activityFeed.css +248 -248
  142. package/registry/components/expanded/activityHeatmap.css +66 -66
  143. package/registry/components/expanded/banner.css +260 -260
  144. package/registry/components/expanded/breadcrumbs.css +157 -140
  145. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  146. package/registry/components/expanded/divider.css +75 -75
  147. package/registry/components/expanded/emptyState.css +111 -111
  148. package/registry/components/expanded/fileUpload.css +457 -457
  149. package/registry/components/expanded/onboardingStepListItem.css +135 -135
  150. package/registry/components/expanded/pageHeader.css +284 -248
  151. package/registry/components/expanded/slideout.css +276 -276
  152. package/registry/components/expanded/steps.css +356 -356
  153. package/registry/components/expanded/table.css +655 -606
  154. package/registry/components/expanded/timeline.css +286 -286
  155. package/registry/components/field.tsx +283 -283
  156. package/registry/components/hover-card.tsx +48 -48
  157. package/registry/components/input-content.ts +107 -107
  158. package/registry/components/input-group.tsx +130 -130
  159. package/registry/components/input-otp.tsx +584 -584
  160. package/registry/components/input.tsx +992 -990
  161. package/registry/components/item.tsx +189 -189
  162. package/registry/components/kbd.tsx +246 -246
  163. package/registry/components/label.tsx +78 -78
  164. package/registry/components/menu.tsx +1091 -978
  165. package/registry/components/menubar.tsx +226 -226
  166. package/registry/components/modal.tsx +872 -872
  167. package/registry/components/native-select.tsx +205 -205
  168. package/registry/components/navigation-menu.tsx +171 -171
  169. package/registry/components/pagination.css +36 -36
  170. package/registry/components/pagination.tsx +560 -560
  171. package/registry/components/popover.tsx +338 -338
  172. package/registry/components/progress-slider.tsx +216 -216
  173. package/registry/components/radio-card.tsx +676 -676
  174. package/registry/components/radio-group.tsx +570 -570
  175. package/registry/components/resizable.tsx +108 -108
  176. package/registry/components/scroll-area.tsx +91 -91
  177. package/registry/components/segmented-control.tsx +87 -87
  178. package/registry/components/select-content.tsx +218 -218
  179. package/registry/components/select.tsx +937 -937
  180. package/registry/components/sheet.tsx +305 -305
  181. package/registry/components/sidebar.tsx +1535 -1433
  182. package/registry/components/skeleton.css +54 -54
  183. package/registry/components/skeleton.tsx +482 -482
  184. package/registry/components/slider.tsx +380 -380
  185. package/registry/components/social-button.tsx +387 -387
  186. package/registry/components/spinner.tsx +78 -78
  187. package/registry/components/table-elements.tsx +287 -128
  188. package/registry/components/tabs.css +246 -222
  189. package/registry/components/tabs.tsx +245 -241
  190. package/registry/components/tag.tsx +423 -423
  191. package/registry/components/textarea.tsx +610 -610
  192. package/registry/components/toast.tsx +136 -136
  193. package/registry/components/toggle.tsx +256 -256
  194. package/registry/components/toolbar.tsx +539 -539
  195. package/registry/components/tooltip.tsx +433 -433
  196. package/registry/examples/alert-demo.tsx +34 -34
  197. package/registry/examples/alert-dialog-demo.tsx +42 -42
  198. package/registry/examples/app-header-demo.tsx +40 -0
  199. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  200. package/registry/examples/attribute-demo.tsx +65 -65
  201. package/registry/examples/autocomplete-demo.tsx +109 -109
  202. package/registry/examples/avatar-demo.tsx +132 -132
  203. package/registry/examples/badge-demo.tsx +69 -69
  204. package/registry/examples/button-demo.tsx +86 -86
  205. package/registry/examples/button-group-demo.tsx +63 -63
  206. package/registry/examples/card-demo.tsx +86 -86
  207. package/registry/examples/carousel-demo.tsx +43 -43
  208. package/registry/examples/chart-demo.tsx +18 -18
  209. package/registry/examples/checkbox-demo.tsx +62 -62
  210. package/registry/examples/collapsible-demo.tsx +16 -16
  211. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  212. package/registry/examples/command-demo.tsx +42 -42
  213. package/registry/examples/command-palette-demo.tsx +235 -235
  214. package/registry/examples/context-menu-demo.tsx +29 -29
  215. package/registry/examples/control-density-contract-demo.tsx +65 -65
  216. package/registry/examples/date-picker-demo.tsx +127 -127
  217. package/registry/examples/divider-demo.tsx +17 -17
  218. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  219. package/registry/examples/expanded/banner-demo.tsx +48 -48
  220. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  221. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  222. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  223. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  224. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  225. package/registry/examples/expanded/steps-demo.tsx +29 -29
  226. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  227. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  228. package/registry/examples/field-demo.tsx +51 -51
  229. package/registry/examples/hover-card-demo.tsx +23 -23
  230. package/registry/examples/input-actions-demo.tsx +36 -36
  231. package/registry/examples/input-demo.tsx +87 -87
  232. package/registry/examples/input-group-demo.tsx +16 -16
  233. package/registry/examples/input-otp-demo.tsx +72 -72
  234. package/registry/examples/item-demo.tsx +46 -46
  235. package/registry/examples/kbd-demo.tsx +12 -12
  236. package/registry/examples/label-demo.tsx +20 -20
  237. package/registry/examples/menu-demo.tsx +122 -122
  238. package/registry/examples/menubar-demo.tsx +34 -34
  239. package/registry/examples/modal-demo.tsx +74 -74
  240. package/registry/examples/native-select-demo.tsx +16 -16
  241. package/registry/examples/navigation-menu-demo.tsx +29 -29
  242. package/registry/examples/pagination-demo.tsx +20 -20
  243. package/registry/examples/popover-demo.tsx +547 -547
  244. package/registry/examples/progress-demo.tsx +37 -37
  245. package/registry/examples/progress-slider-demo.tsx +29 -29
  246. package/registry/examples/radio-card-demo.tsx +51 -51
  247. package/registry/examples/radio-group-demo.tsx +62 -62
  248. package/registry/examples/resizable-demo.tsx +22 -22
  249. package/registry/examples/results-toolbar-demo.tsx +44 -44
  250. package/registry/examples/scroll-area-demo.tsx +15 -15
  251. package/registry/examples/segmented-control-demo.tsx +39 -39
  252. package/registry/examples/select-demo.tsx +87 -87
  253. package/registry/examples/sheet-demo.tsx +47 -47
  254. package/registry/examples/sidebar-demo.tsx +101 -101
  255. package/registry/examples/slider-demo.tsx +31 -31
  256. package/registry/examples/social-button-demo.tsx +61 -61
  257. package/registry/examples/spinner-demo.tsx +59 -59
  258. package/registry/examples/tabs-demo.tsx +13 -13
  259. package/registry/examples/tag-demo.tsx +29 -29
  260. package/registry/examples/textarea-demo.tsx +79 -79
  261. package/registry/examples/toast-demo.tsx +35 -35
  262. package/registry/examples/toggle-demo.tsx +59 -59
  263. package/registry/examples/tooltip-demo.tsx +115 -115
  264. package/registry/guidance/USAGE.md +27 -27
  265. package/registry/hooks/use-direction.ts +207 -207
  266. package/registry/index.json +387 -17
  267. package/registry/styles/globals.css +46 -0
  268. package/registry.json +24 -2
  269. package/schema/config.schema.json +51 -51
  270. package/schema/registry.schema.json +194 -194
  271. package/schema/tokens.schema.json +167 -167
  272. package/src/cli/command-line.js +194 -194
  273. package/src/cli/config-editors.js +9 -9
  274. package/src/cli/doctor.js +399 -399
  275. package/src/cli/file-transaction.js +302 -302
  276. package/src/cli/global-css.js +153 -153
  277. package/src/cli/index.js +1002 -1002
  278. package/src/cli/install-plan.js +592 -592
  279. package/src/cli/install-receipt.js +171 -171
  280. package/src/cli/jsonc.js +203 -203
  281. package/src/cli/package-install-state.js +254 -254
  282. package/src/cli/package-manager.js +238 -238
  283. package/src/cli/package-plan.js +185 -185
  284. package/src/cli/package-version.js +97 -97
  285. package/src/cli/registry-query.js +207 -207
  286. package/src/cli/reporting.js +55 -55
  287. package/src/cli/schema-validation.js +19 -19
  288. package/src/cli/source-scanner.js +686 -686
  289. package/src/cli/vite-config.js +478 -478
  290. package/tokens/banhaten.tokens.json +184 -0
@@ -1,543 +1,543 @@
1
- import { useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent, type ReactNode } from "react";
2
-
3
- import { Button } from "@/components/ui/button";
4
-
5
- import "./fileUpload.css";
6
-
7
- export type FileUploadSize = "lg" | "sm";
8
- export type FileUploadVariant = "default" | "button";
9
- export type FileUploadDropzoneState = "default" | "hover";
10
- export type FileUploadItemStatus = "uploaded" | "uploading" | "uploading-progress" | "error";
11
-
12
- export type FileUploadFile = {
13
- errorLabel?: string;
14
- extensionLabel?: string;
15
- id: string;
16
- name: string;
17
- progress?: number;
18
- progressLabel?: string;
19
- sizeLabel: string;
20
- status: FileUploadItemStatus;
21
- uploadedLabel?: string;
22
- uploadLabel?: string;
23
- versionLabel?: string;
24
- };
25
-
26
- export type FileUploadProps = {
27
- accept?: string;
28
- browseLabel?: string;
29
- cancelLabel?: string;
30
- dir?: "ltr" | "rtl" | "auto";
31
- disabled?: boolean;
32
- dropzoneState?: FileUploadDropzoneState;
33
- files?: FileUploadFile[];
34
- helperText?: string;
35
- moreLabel?: string;
36
- multiple?: boolean;
37
- onBrowse?: () => void;
38
- onCancel?: (fileId: string) => void;
39
- onDrop?: (files: FileList) => void;
40
- onMore?: (fileId: string) => void;
41
- onRemove?: (fileId: string) => void;
42
- onRetry?: (fileId: string) => void;
43
- prompt?: string;
44
- removeLabel?: string;
45
- retryLabel?: string;
46
- size?: FileUploadSize;
47
- title?: string;
48
- variant?: FileUploadVariant;
49
- };
50
-
51
- export function FileUpload({
52
- accept,
53
- browseLabel,
54
- cancelLabel = "Cancel upload",
55
- dir,
56
- disabled = false,
57
- dropzoneState = "default",
58
- files = [],
59
- helperText,
60
- moreLabel = "More file actions",
61
- multiple = false,
62
- onBrowse,
63
- onCancel,
64
- onDrop,
65
- onMore,
66
- onRemove,
67
- onRetry,
68
- prompt,
69
- removeLabel = "Remove file",
70
- retryLabel = "Try again",
71
- size = "lg",
72
- title,
73
- variant = "default",
74
- }: FileUploadProps) {
75
- const inputRef = useRef<HTMLInputElement | null>(null);
76
- const [isDragging, setIsDragging] = useState(false);
77
- const isHover = dropzoneState === "hover" || isDragging;
78
-
79
- const resolvedPrompt =
80
- prompt ?? (variant === "button" ? "Drag and drop your files" : "Drop your files here, or");
81
- const resolvedBrowseLabel = browseLabel ?? (variant === "button" ? "Or choose files" : "click to browse");
82
- const resolvedHelperText = helperText ?? "Unlimited files, 5GB total limit";
83
-
84
- function browse() {
85
- if (disabled) return;
86
- onBrowse?.();
87
- inputRef.current?.click();
88
- }
89
-
90
- function handleDropzoneClick(event: MouseEvent<HTMLDivElement>) {
91
- if (event.target instanceof HTMLElement && event.target.closest("button")) return;
92
- browse();
93
- }
94
-
95
- function handleInputChange(event: ChangeEvent<HTMLInputElement>) {
96
- const selectedFiles = event.currentTarget.files;
97
- if (selectedFiles?.length) {
98
- onDrop?.(selectedFiles);
99
- }
100
- event.currentTarget.value = "";
101
- }
102
-
103
- function handleDragOver(event: DragEvent<HTMLDivElement>) {
104
- if (disabled) return;
105
- event.preventDefault();
106
- setIsDragging(true);
107
- }
108
-
109
- function handleDrop(event: DragEvent<HTMLDivElement>) {
110
- if (disabled) return;
111
- event.preventDefault();
112
- setIsDragging(false);
113
- if (event.dataTransfer.files.length) {
114
- onDrop?.(event.dataTransfer.files);
115
- }
116
- }
117
-
118
- return (
119
- <div className={cx("ds-file-upload", `ds-file-upload--${size}`, `ds-file-upload--${variant}`)} dir={dir}>
120
- <div
121
- aria-disabled={disabled || undefined}
122
- className={cx("ds-file-upload__dropzone", isHover && "is-hover")}
123
- onClick={handleDropzoneClick}
124
- onDragLeave={() => setIsDragging(false)}
125
- onDragOver={handleDragOver}
126
- onDrop={handleDrop}
127
- >
128
- <input
129
- accept={accept}
130
- className="ds-file-upload__input"
131
- disabled={disabled}
132
- multiple={multiple}
133
- onChange={handleInputChange}
134
- ref={inputRef}
135
- type="file"
136
- />
137
- <span className="ds-file-upload__icon" aria-hidden="true">
138
- <UploadCloudIcon />
139
- </span>
140
-
141
- {variant === "button" ? (
142
- <>
143
- <div className="ds-file-upload__copy">
144
- <p className="ds-file-upload__title">{title ?? resolvedPrompt}</p>
145
- {resolvedHelperText && <span className="ds-file-upload__helper">{resolvedHelperText}</span>}
146
- </div>
147
- <Button className="ds-file-upload__button" disabled={disabled} onClick={browse} type="button">
148
- {resolvedBrowseLabel}
149
- </Button>
150
- </>
151
- ) : (
152
- <div className="ds-file-upload__copy">
153
- {(resolvedPrompt || resolvedBrowseLabel) && (
154
- <p className="ds-file-upload__title">
155
- {resolvedPrompt}{" "}
156
- {resolvedBrowseLabel && (
157
- <Button className="ds-file-upload__browse" disabled={disabled} onClick={browse} size="xs" type="button" variant="link">
158
- {resolvedBrowseLabel}
159
- </Button>
160
- )}
161
- </p>
162
- )}
163
- {resolvedHelperText && <span className="ds-file-upload__helper">{resolvedHelperText}</span>}
164
- </div>
165
- )}
166
- </div>
167
-
168
- {files.length > 0 && (
169
- <ul className="ds-file-upload__list" aria-label="Queued files">
170
- {files.map((file) => (
171
- <FileUploadRow
172
- cancelLabel={cancelLabel}
173
- file={file}
174
- key={file.id}
175
- moreLabel={moreLabel}
176
- onCancel={onCancel}
177
- onMore={onMore}
178
- onRemove={onRemove}
179
- onRetry={onRetry}
180
- removeLabel={removeLabel}
181
- retryLabel={retryLabel}
182
- size={size}
183
- />
184
- ))}
185
- </ul>
186
- )}
187
- </div>
188
- );
189
- }
190
-
191
- function FileUploadRow({
192
- cancelLabel,
193
- file,
194
- moreLabel,
195
- onCancel,
196
- onMore,
197
- onRemove,
198
- onRetry,
199
- removeLabel,
200
- retryLabel,
201
- size,
202
- }: {
203
- cancelLabel: string;
204
- file: FileUploadFile;
205
- moreLabel: string;
206
- onCancel?: (fileId: string) => void;
207
- onMore?: (fileId: string) => void;
208
- onRemove?: (fileId: string) => void;
209
- onRetry?: (fileId: string) => void;
210
- removeLabel: string;
211
- retryLabel: string;
212
- size: FileUploadSize;
213
- }) {
214
- const isSmall = size === "sm";
215
- const isProgress = file.status === "uploading-progress";
216
- const progressValue = clampProgress(file.progress ?? 40);
217
- const progressLabel = file.progressLabel ?? `${progressValue}%`;
218
- const uploadedLabel = file.uploadedLabel ?? "Uploaded";
219
- const uploadLabel = file.uploadLabel ?? "Uploading...";
220
- const errorLabel = file.errorLabel ?? "Upload failed";
221
-
222
- return (
223
- <li className={cx("ds-file-upload-row", `ds-file-upload-row--${size}`, `ds-file-upload-row--${file.status}`)}>
224
- <div className="ds-file-upload-row__content">
225
- <FileTypeIcon label={file.extensionLabel ?? getExtensionLabel(file.name)} size={size} />
226
-
227
- <div className="ds-file-upload-row__copy">
228
- {isSmall || isProgress ? (
229
- <div className="ds-file-upload-row__headline">
230
- <strong title={file.name}>{file.name}</strong>
231
- <span aria-hidden="true" className="ds-file-upload-row__dot">
232
- {"\u00b7"}
233
- </span>
234
- <span>{file.sizeLabel}</span>
235
- </div>
236
- ) : (
237
- <>
238
- <strong title={file.name}>{file.name}</strong>
239
- <div className="ds-file-upload-row__details">
240
- <span>{getMetaLabel(file)}</span>
241
- {(file.status === "uploaded" || file.status === "uploading" || file.status === "error") && (
242
- <span className="ds-file-upload-row__state">
243
- <StatusIcon status={file.status} />
244
- {file.status === "uploaded" && <span>{uploadedLabel}</span>}
245
- {file.status === "uploading" && <span>{uploadLabel}</span>}
246
- {file.status === "error" && <span>{errorLabel}</span>}
247
- </span>
248
- )}
249
- </div>
250
- </>
251
- )}
252
-
253
- {isProgress && (
254
- <div
255
- aria-label={`Upload progress ${progressLabel}`}
256
- aria-valuemax={100}
257
- aria-valuemin={0}
258
- aria-valuenow={progressValue}
259
- className="ds-file-upload-row__progress"
260
- role="progressbar"
261
- >
262
- <span className="ds-file-upload-row__progress-track">
263
- <span className="ds-file-upload-row__progress-fill" style={{ width: `${progressValue}%` }} />
264
- </span>
265
- <span className="ds-file-upload-row__progress-label">{progressLabel}</span>
266
- </div>
267
- )}
268
-
269
- {!isSmall && file.status === "error" && (
270
- <Button className="ds-file-upload-row__retry" onClick={() => onRetry?.(file.id)} size="xs" type="button" variant="link">
271
- {retryLabel}
272
- </Button>
273
- )}
274
- </div>
275
- </div>
276
-
277
- {isSmall && file.status !== "uploaded" && file.status !== "uploading-progress" && (
278
- <StatusIcon status={file.status} standalone />
279
- )}
280
-
281
- <div className="ds-file-upload-row__actions">
282
- {file.status === "uploaded" && (
283
- <IconAction ariaLabel={`${moreLabel}: ${file.name}`} icon={<MoreFillIcon />} onClick={() => onMore?.(file.id)} />
284
- )}
285
- {file.status === "uploading" && (
286
- <IconAction ariaLabel={`${cancelLabel}: ${file.name}`} icon={<CloseFillIcon />} onClick={() => onCancel?.(file.id)} />
287
- )}
288
- {file.status !== "uploading" && (
289
- <IconAction ariaLabel={`${removeLabel}: ${file.name}`} icon={<DeleteBinLineIcon />} onClick={() => onRemove?.(file.id)} />
290
- )}
291
- </div>
292
- </li>
293
- );
294
- }
295
-
296
- function FileTypeIcon({ label, size }: { label: string; size: FileUploadSize }) {
297
- const iconLabel = normalizeExtensionLabel(label);
298
- const icon = resolveFileIconVariant(iconLabel);
299
- const badgeX = icon.badgeX ?? getFileIconBadgeX(icon.badgeWidth);
300
- const badgeTextX = badgeX + icon.badgeWidth / 2;
301
- const badgeStyle = {
302
- "--ds-file-upload-file-badge-bg": icon.badgeFill,
303
- } as CSSProperties;
304
-
305
- return (
306
- <span className={cx("ds-file-upload-file-icon", `ds-file-upload-file-icon--${size}`)} style={badgeStyle} aria-hidden="true">
307
- <svg className="ds-file-upload-file-icon__svg" viewBox="0 0 40 40" fill="none" focusable="false">
308
- <path
309
- className="ds-file-upload-file-icon__page"
310
- d="M13 3.5H24.1719C24.5696 3.50008 24.9512 3.65822 25.2324 3.93945L34.0605 12.7676C34.3418 13.0488 34.4999 13.4304 34.5 13.8281V33C34.5 34.933 32.933 36.5 31 36.5H13C11.067 36.5 9.5 34.933 9.5 33V7C9.5 5.067 11.067 3.5 13 3.5Z"
311
- />
312
- <path
313
- className="ds-file-upload-file-icon__fold"
314
- d="M24.5 3.59117V9.21432C24.5 11.4235 26.2909 13.2143 28.5 13.2143H34.0414"
315
- />
316
- <rect className="ds-file-upload-file-icon__badge" x={badgeX} y="20" width={icon.badgeWidth} height="12" rx="3" />
317
- <text className="ds-file-upload-file-icon__label" x={badgeTextX} y="26.55" textAnchor="middle" dominantBaseline="middle">
318
- {iconLabel}
319
- </text>
320
- </svg>
321
- </span>
322
- );
323
- }
324
-
325
- type FileIconVariant = {
326
- badgeFill: string;
327
- badgeWidth: number;
328
- badgeX?: number;
329
- };
330
-
331
- const FILE_ICON_VARIANTS = {
332
- AI: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 20 },
333
- AVI: { badgeFill: "var(--bh-bg-accent-lime-default)", badgeWidth: 21 },
334
- BMP: { badgeFill: "var(--bh-bg-accent-lime-default)", badgeWidth: 24 },
335
- CRD: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 23 },
336
- CSV: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 23 },
337
- DLL: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 21 },
338
- DOC: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 24 },
339
- DOCX: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 29 },
340
- EPS: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 21 },
341
- EXE: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 21 },
342
- FIG: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 20 },
343
- FLV: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 20 },
344
- GIF: { badgeFill: "var(--bh-bg-accent-rose-default)", badgeWidth: 19 },
345
- HTML: { badgeFill: "var(--bh-bg-accent-lime-default)", badgeWidth: 30, badgeX: 3 },
346
- IMG: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 22 },
347
- ISO: { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth: 19 },
348
- JAVA: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 27 },
349
- JPG: { badgeFill: "var(--bh-bg-accent-fuchsia-default)", badgeWidth: 22 },
350
- JSON: { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth: 28 },
351
- MID: { badgeFill: "var(--bh-bg-accent-fuchsia-default)", badgeWidth: 23 },
352
- MOV: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 25 },
353
- MP3: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 24 },
354
- MP4: { badgeFill: "var(--bh-bg-accent-rose-default)", badgeWidth: 24 },
355
- MPEG: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 30, badgeX: 3 },
356
- PDF: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 22 },
357
- PNG: { badgeFill: "var(--bh-bg-accent-aqua-default)", badgeWidth: 23 },
358
- PPT: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 22 },
359
- PPTX: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 28 },
360
- PSD: { badgeFill: "var(--bh-bg-accent-blue-default)", badgeWidth: 22 },
361
- PUB: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 22 },
362
- RAR: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 22 },
363
- RAW: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 25 },
364
- RSS: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 22 },
365
- SVG: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 23 },
366
- TIFF: { badgeFill: "var(--bh-bg-accent-fuchsia-default)", badgeWidth: 23 },
367
- TXT: { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth: 22 },
368
- WAV: { badgeFill: "var(--bh-bg-accent-amber-default)", badgeWidth: 25 },
369
- WMA: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 28 },
370
- XML: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 23 },
371
- XSL: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 21 },
372
- XSLX: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 27 },
373
- ZIP: { badgeFill: "var(--bh-bg-accent-aqua-default)", badgeWidth: 21 },
374
- } satisfies Record<string, FileIconVariant>;
375
-
376
- const FILE_ICON_ALIASES: Record<string, keyof typeof FILE_ICON_VARIANTS> = {
377
- CDR: "CRD",
378
- JPEG: "JPG",
379
- XLS: "XSL",
380
- XLSX: "XSLX",
381
- };
382
-
383
- function resolveFileIconVariant(label: string): FileIconVariant {
384
- const directVariant = FILE_ICON_VARIANTS[label as keyof typeof FILE_ICON_VARIANTS];
385
- if (directVariant) return directVariant;
386
-
387
- const aliasedVariant = FILE_ICON_ALIASES[label];
388
- if (aliasedVariant) return FILE_ICON_VARIANTS[aliasedVariant];
389
-
390
- const badgeWidth = label.length <= 2 ? 20 : label.length >= 4 ? 30 : 23;
391
- return { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth, badgeX: getFileIconBadgeX(badgeWidth) };
392
- }
393
-
394
- function getFileIconBadgeX(badgeWidth: number) {
395
- return badgeWidth >= 30 ? 3 : 5;
396
- }
397
-
398
- function StatusIcon({
399
- standalone = false,
400
- status,
401
- }: {
402
- standalone?: boolean;
403
- status: Exclude<FileUploadItemStatus, "uploading-progress">;
404
- }) {
405
- return (
406
- <span
407
- aria-hidden={standalone ? undefined : "true"}
408
- aria-label={standalone ? getStatusLabel(status) : undefined}
409
- className={cx(
410
- "ds-file-upload-row__status",
411
- standalone && "ds-file-upload-row__status--standalone",
412
- `ds-file-upload-row__status--${status}`
413
- )}
414
- role={standalone ? "img" : undefined}
415
- >
416
- {status === "uploaded" && <CheckboxCircleFillIcon />}
417
- {status === "uploading" && <Loader2LineIcon />}
418
- {status === "error" && <ErrorWarningFillIcon />}
419
- </span>
420
- );
421
- }
422
-
423
- function UploadCloudIcon() {
424
- return (
425
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 24 24">
426
- <path
427
- d="M1 14.5C1 12.1716 2.22429 10.1291 4.06426 8.9812C4.56469 5.044 7.92686 2 12 2C16.0731 2 19.4353 5.044 19.9357 8.9812C21.7757 10.1291 23 12.1716 23 14.5C23 17.9216 20.3562 20.7257 17 20.9811L7 21C3.64378 20.7257 1 17.9216 1 14.5ZM16.8483 18.9868C19.1817 18.8093 21 16.8561 21 14.5C21 12.927 20.1884 11.4962 18.8771 10.6781L18.0714 10.1754L17.9517 9.23338C17.5735 6.25803 15.0288 4 12 4C8.97116 4 6.42647 6.25803 6.0483 9.23338L5.92856 10.1754L5.12288 10.6781C3.81156 11.4962 3 12.927 3 14.5C3 16.8561 4.81833 18.8093 7.1517 18.9868L7.325 19H16.675L16.8483 18.9868ZM13 13V17H11V13H8L12 8L16 13H13Z"
428
- fill="currentColor"
429
- />
430
- </svg>
431
- );
432
- }
433
-
434
- function CheckboxCircleFillIcon() {
435
- return (
436
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 16 16">
437
- <path
438
- d="M8.00016 14.6667C11.682 14.6667 14.6668 11.6819 14.6668 8.00001C14.6668 4.31811 11.682 1.33334 8.00016 1.33334C4.31826 1.33334 1.3335 4.31811 1.3335 8.00001C1.3335 11.6819 4.31826 14.6667 8.00016 14.6667ZM11.6382 6.30475L7.3335 10.6095L4.52876 7.80474L5.47157 6.86194L7.3335 8.72388L10.6954 5.36194L11.6382 6.30475Z"
439
- fill="currentColor"
440
- />
441
- </svg>
442
- );
443
- }
444
-
445
- function Loader2LineIcon() {
446
- return (
447
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 16 16">
448
- <path
449
- d="M8.00016 1.33333C8.36836 1.33333 8.66683 1.63181 8.66683 1.99999V3.99999C8.66683 4.36818 8.36836 4.66666 8.00016 4.66666C7.63196 4.66666 7.3335 4.36818 7.3335 3.99999V1.99999C7.3335 1.63181 7.63196 1.33333 8.00016 1.33333ZM8.00016 11.3333C8.36836 11.3333 8.66683 11.6318 8.66683 12V14C8.66683 14.3682 8.36836 14.6667 8.00016 14.6667C7.63196 14.6667 7.3335 14.3682 7.3335 14V12C7.3335 11.6318 7.63196 11.3333 8.00016 11.3333ZM14.6668 7.99999C14.6668 8.36819 14.3684 8.66666 14.0002 8.66666H12.0002C11.632 8.66666 11.3335 8.36819 11.3335 7.99999C11.3335 7.63179 11.632 7.33333 12.0002 7.33333H14.0002C14.3684 7.33333 14.6668 7.63179 14.6668 7.99999ZM4.66683 7.99999C4.66683 8.36819 4.36835 8.66666 4.00016 8.66666H2.00016C1.63198 8.66666 1.3335 8.36819 1.3335 7.99999C1.3335 7.63179 1.63198 7.33333 2.00016 7.33333H4.00016C4.36835 7.33333 4.66683 7.63179 4.66683 7.99999ZM12.7142 12.7141C12.4538 12.9744 12.0318 12.9744 11.7714 12.7141L10.3572 11.2998C10.0968 11.0395 10.0968 10.6174 10.3572 10.357C10.6176 10.0967 11.0396 10.0967 11.3 10.357L12.7142 11.7713C12.9746 12.0316 12.9746 12.4537 12.7142 12.7141ZM5.64314 5.64297C5.38279 5.90332 4.96068 5.90332 4.70033 5.64297L3.28612 4.22876C3.02577 3.96841 3.02577 3.5463 3.28612 3.28595C3.54647 3.0256 3.96858 3.0256 4.22893 3.28595L5.64314 4.70016C5.90349 4.96051 5.90349 5.38262 5.64314 5.64297ZM3.28612 12.7141C3.02577 12.4537 3.02577 12.0316 3.28612 11.7713L4.70033 10.357C4.96068 10.0967 5.38279 10.0967 5.64314 10.357C5.90349 10.6174 5.90349 11.0395 5.64314 11.2998L4.22893 12.7141C3.96858 12.9744 3.54647 12.9744 3.28612 12.7141ZM10.3572 5.64297C10.0968 5.38262 10.0968 4.96051 10.3572 4.70016L11.7714 3.28595C12.0318 3.0256 12.4538 3.0256 12.7142 3.28595C12.9746 3.5463 12.9746 3.96841 12.7142 4.22876L11.3 5.64297C11.0396 5.90332 10.6176 5.90332 10.3572 5.64297Z"
450
- fill="currentColor"
451
- />
452
- </svg>
453
- );
454
- }
455
-
456
- function ErrorWarningFillIcon() {
457
- return (
458
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 16 16">
459
- <path
460
- d="M8.00016 14.6667C4.31826 14.6667 1.3335 11.6819 1.3335 8.00001C1.3335 4.31811 4.31826 1.33334 8.00016 1.33334C11.682 1.33334 14.6668 4.31811 14.6668 8.00001C14.6668 11.6819 11.682 14.6667 8.00016 14.6667ZM7.3335 10V11.3333H8.66683V10H7.3335ZM7.3335 4.66668V8.66668H8.66683V4.66668H7.3335Z"
461
- fill="currentColor"
462
- />
463
- </svg>
464
- );
465
- }
466
-
467
- function MoreFillIcon() {
468
- return (
469
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 20 20">
470
- <path
471
- d="M4.16667 8.33334C3.25 8.33334 2.5 9.08334 2.5 10C2.5 10.9167 3.25 11.6667 4.16667 11.6667C5.08333 11.6667 5.83333 10.9167 5.83333 10C5.83333 9.08334 5.08333 8.33334 4.16667 8.33334ZM15.8333 8.33334C14.9167 8.33334 14.1667 9.08334 14.1667 10C14.1667 10.9167 14.9167 11.6667 15.8333 11.6667C16.75 11.6667 17.5 10.9167 17.5 10C17.5 9.08334 16.75 8.33334 15.8333 8.33334ZM10 8.33334C9.08333 8.33334 8.33333 9.08334 8.33333 10C8.33333 10.9167 9.08333 11.6667 10 11.6667C10.9167 11.6667 11.6667 10.9167 11.6667 10C11.6667 9.08334 10.9167 8.33334 10 8.33334Z"
472
- fill="currentColor"
473
- />
474
- </svg>
475
- );
476
- }
477
-
478
- function CloseFillIcon() {
479
- return (
480
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 20 20">
481
- <path
482
- d="M9.99959 8.82208L14.1244 4.69727L15.3029 5.87577L11.1781 10.0006L15.3029 14.1253L14.1244 15.3038L9.99959 11.1791L5.87481 15.3038L4.69629 14.1253L8.82109 10.0006L4.69629 5.87577L5.87481 4.69727L9.99959 8.82208Z"
483
- fill="currentColor"
484
- />
485
- </svg>
486
- );
487
- }
488
-
489
- function DeleteBinLineIcon() {
490
- return (
491
- <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 20 20">
492
- <path
493
- d="M14.1665 4.99999H18.3332V6.66666H16.6665V17.5C16.6665 17.9602 16.2934 18.3333 15.8332 18.3333H4.1665C3.70627 18.3333 3.33317 17.9602 3.33317 17.5V6.66666H1.6665V4.99999H5.83317V2.49999C5.83317 2.03976 6.20627 1.66666 6.6665 1.66666H13.3332C13.7934 1.66666 14.1665 2.03976 14.1665 2.49999V4.99999ZM14.9998 6.66666H4.99984V16.6667H14.9998V6.66666ZM7.49984 9.16666H9.1665V14.1667H7.49984V9.16666ZM10.8332 9.16666H12.4998V14.1667H10.8332V9.16666ZM7.49984 3.33332V4.99999H12.4998V3.33332H7.49984Z"
494
- fill="currentColor"
495
- />
496
- </svg>
497
- );
498
- }
499
-
500
- function IconAction({
501
- ariaLabel,
502
- icon,
503
- onClick,
504
- }: {
505
- ariaLabel: string;
506
- icon: ReactNode;
507
- onClick: () => void;
508
- }) {
509
- return (
510
- <Button aria-label={ariaLabel} className="ds-file-upload-row__action" onClick={onClick} size="icon-sm" type="button" variant="ghost">
511
- {icon}
512
- </Button>
513
- );
514
- }
515
-
516
- function getExtensionLabel(name: string) {
517
- const extension = name.split(".").pop();
518
- if (!extension || extension === name) return "FILE";
519
- return normalizeExtensionLabel(extension);
520
- }
521
-
522
- function normalizeExtensionLabel(label: string) {
523
- const normalized = label.trim().replace(/^\./, "").toUpperCase().replace(/[^A-Z0-9]/g, "");
524
- return normalized ? normalized.slice(0, 4) : "FILE";
525
- }
526
-
527
- function getMetaLabel(file: FileUploadFile) {
528
- return [file.sizeLabel, file.versionLabel].filter(Boolean).join(" \u00b7 ");
529
- }
530
-
531
- function getStatusLabel(status: Exclude<FileUploadItemStatus, "uploading-progress">) {
532
- if (status === "uploaded") return "Uploaded";
533
- if (status === "uploading") return "Uploading";
534
- return "Upload failed";
535
- }
536
-
537
- function clampProgress(value: number) {
538
- return Math.min(100, Math.max(0, Math.round(value)));
539
- }
540
-
541
- function cx(...classes: Array<string | false | null | undefined>) {
542
- return classes.filter(Boolean).join(" ");
543
- }
1
+ import { useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent, type ReactNode } from "react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ import "./fileUpload.css";
6
+
7
+ export type FileUploadSize = "lg" | "sm";
8
+ export type FileUploadVariant = "default" | "button";
9
+ export type FileUploadDropzoneState = "default" | "hover";
10
+ export type FileUploadItemStatus = "uploaded" | "uploading" | "uploading-progress" | "error";
11
+
12
+ export type FileUploadFile = {
13
+ errorLabel?: string;
14
+ extensionLabel?: string;
15
+ id: string;
16
+ name: string;
17
+ progress?: number;
18
+ progressLabel?: string;
19
+ sizeLabel: string;
20
+ status: FileUploadItemStatus;
21
+ uploadedLabel?: string;
22
+ uploadLabel?: string;
23
+ versionLabel?: string;
24
+ };
25
+
26
+ export type FileUploadProps = {
27
+ accept?: string;
28
+ browseLabel?: string;
29
+ cancelLabel?: string;
30
+ dir?: "ltr" | "rtl" | "auto";
31
+ disabled?: boolean;
32
+ dropzoneState?: FileUploadDropzoneState;
33
+ files?: FileUploadFile[];
34
+ helperText?: string;
35
+ moreLabel?: string;
36
+ multiple?: boolean;
37
+ onBrowse?: () => void;
38
+ onCancel?: (fileId: string) => void;
39
+ onDrop?: (files: FileList) => void;
40
+ onMore?: (fileId: string) => void;
41
+ onRemove?: (fileId: string) => void;
42
+ onRetry?: (fileId: string) => void;
43
+ prompt?: string;
44
+ removeLabel?: string;
45
+ retryLabel?: string;
46
+ size?: FileUploadSize;
47
+ title?: string;
48
+ variant?: FileUploadVariant;
49
+ };
50
+
51
+ export function FileUpload({
52
+ accept,
53
+ browseLabel,
54
+ cancelLabel = "Cancel upload",
55
+ dir,
56
+ disabled = false,
57
+ dropzoneState = "default",
58
+ files = [],
59
+ helperText,
60
+ moreLabel = "More file actions",
61
+ multiple = false,
62
+ onBrowse,
63
+ onCancel,
64
+ onDrop,
65
+ onMore,
66
+ onRemove,
67
+ onRetry,
68
+ prompt,
69
+ removeLabel = "Remove file",
70
+ retryLabel = "Try again",
71
+ size = "lg",
72
+ title,
73
+ variant = "default",
74
+ }: FileUploadProps) {
75
+ const inputRef = useRef<HTMLInputElement | null>(null);
76
+ const [isDragging, setIsDragging] = useState(false);
77
+ const isHover = dropzoneState === "hover" || isDragging;
78
+
79
+ const resolvedPrompt =
80
+ prompt ?? (variant === "button" ? "Drag and drop your files" : "Drop your files here, or");
81
+ const resolvedBrowseLabel = browseLabel ?? (variant === "button" ? "Or choose files" : "click to browse");
82
+ const resolvedHelperText = helperText ?? "Unlimited files, 5GB total limit";
83
+
84
+ function browse() {
85
+ if (disabled) return;
86
+ onBrowse?.();
87
+ inputRef.current?.click();
88
+ }
89
+
90
+ function handleDropzoneClick(event: MouseEvent<HTMLDivElement>) {
91
+ if (event.target instanceof HTMLElement && event.target.closest("button")) return;
92
+ browse();
93
+ }
94
+
95
+ function handleInputChange(event: ChangeEvent<HTMLInputElement>) {
96
+ const selectedFiles = event.currentTarget.files;
97
+ if (selectedFiles?.length) {
98
+ onDrop?.(selectedFiles);
99
+ }
100
+ event.currentTarget.value = "";
101
+ }
102
+
103
+ function handleDragOver(event: DragEvent<HTMLDivElement>) {
104
+ if (disabled) return;
105
+ event.preventDefault();
106
+ setIsDragging(true);
107
+ }
108
+
109
+ function handleDrop(event: DragEvent<HTMLDivElement>) {
110
+ if (disabled) return;
111
+ event.preventDefault();
112
+ setIsDragging(false);
113
+ if (event.dataTransfer.files.length) {
114
+ onDrop?.(event.dataTransfer.files);
115
+ }
116
+ }
117
+
118
+ return (
119
+ <div className={cx("ds-file-upload", `ds-file-upload--${size}`, `ds-file-upload--${variant}`)} dir={dir}>
120
+ <div
121
+ aria-disabled={disabled || undefined}
122
+ className={cx("ds-file-upload__dropzone", isHover && "is-hover")}
123
+ onClick={handleDropzoneClick}
124
+ onDragLeave={() => setIsDragging(false)}
125
+ onDragOver={handleDragOver}
126
+ onDrop={handleDrop}
127
+ >
128
+ <input
129
+ accept={accept}
130
+ className="ds-file-upload__input"
131
+ disabled={disabled}
132
+ multiple={multiple}
133
+ onChange={handleInputChange}
134
+ ref={inputRef}
135
+ type="file"
136
+ />
137
+ <span className="ds-file-upload__icon" aria-hidden="true">
138
+ <UploadCloudIcon />
139
+ </span>
140
+
141
+ {variant === "button" ? (
142
+ <>
143
+ <div className="ds-file-upload__copy">
144
+ <p className="ds-file-upload__title">{title ?? resolvedPrompt}</p>
145
+ {resolvedHelperText && <span className="ds-file-upload__helper">{resolvedHelperText}</span>}
146
+ </div>
147
+ <Button className="ds-file-upload__button" disabled={disabled} onClick={browse} type="button">
148
+ {resolvedBrowseLabel}
149
+ </Button>
150
+ </>
151
+ ) : (
152
+ <div className="ds-file-upload__copy">
153
+ {(resolvedPrompt || resolvedBrowseLabel) && (
154
+ <p className="ds-file-upload__title">
155
+ {resolvedPrompt}{" "}
156
+ {resolvedBrowseLabel && (
157
+ <Button className="ds-file-upload__browse" disabled={disabled} onClick={browse} size="xs" type="button" variant="link">
158
+ {resolvedBrowseLabel}
159
+ </Button>
160
+ )}
161
+ </p>
162
+ )}
163
+ {resolvedHelperText && <span className="ds-file-upload__helper">{resolvedHelperText}</span>}
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {files.length > 0 && (
169
+ <ul className="ds-file-upload__list" aria-label="Queued files">
170
+ {files.map((file) => (
171
+ <FileUploadRow
172
+ cancelLabel={cancelLabel}
173
+ file={file}
174
+ key={file.id}
175
+ moreLabel={moreLabel}
176
+ onCancel={onCancel}
177
+ onMore={onMore}
178
+ onRemove={onRemove}
179
+ onRetry={onRetry}
180
+ removeLabel={removeLabel}
181
+ retryLabel={retryLabel}
182
+ size={size}
183
+ />
184
+ ))}
185
+ </ul>
186
+ )}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ function FileUploadRow({
192
+ cancelLabel,
193
+ file,
194
+ moreLabel,
195
+ onCancel,
196
+ onMore,
197
+ onRemove,
198
+ onRetry,
199
+ removeLabel,
200
+ retryLabel,
201
+ size,
202
+ }: {
203
+ cancelLabel: string;
204
+ file: FileUploadFile;
205
+ moreLabel: string;
206
+ onCancel?: (fileId: string) => void;
207
+ onMore?: (fileId: string) => void;
208
+ onRemove?: (fileId: string) => void;
209
+ onRetry?: (fileId: string) => void;
210
+ removeLabel: string;
211
+ retryLabel: string;
212
+ size: FileUploadSize;
213
+ }) {
214
+ const isSmall = size === "sm";
215
+ const isProgress = file.status === "uploading-progress";
216
+ const progressValue = clampProgress(file.progress ?? 40);
217
+ const progressLabel = file.progressLabel ?? `${progressValue}%`;
218
+ const uploadedLabel = file.uploadedLabel ?? "Uploaded";
219
+ const uploadLabel = file.uploadLabel ?? "Uploading...";
220
+ const errorLabel = file.errorLabel ?? "Upload failed";
221
+
222
+ return (
223
+ <li className={cx("ds-file-upload-row", `ds-file-upload-row--${size}`, `ds-file-upload-row--${file.status}`)}>
224
+ <div className="ds-file-upload-row__content">
225
+ <FileTypeIcon label={file.extensionLabel ?? getExtensionLabel(file.name)} size={size} />
226
+
227
+ <div className="ds-file-upload-row__copy">
228
+ {isSmall || isProgress ? (
229
+ <div className="ds-file-upload-row__headline">
230
+ <strong title={file.name}>{file.name}</strong>
231
+ <span aria-hidden="true" className="ds-file-upload-row__dot">
232
+ {"\u00b7"}
233
+ </span>
234
+ <span>{file.sizeLabel}</span>
235
+ </div>
236
+ ) : (
237
+ <>
238
+ <strong title={file.name}>{file.name}</strong>
239
+ <div className="ds-file-upload-row__details">
240
+ <span>{getMetaLabel(file)}</span>
241
+ {(file.status === "uploaded" || file.status === "uploading" || file.status === "error") && (
242
+ <span className="ds-file-upload-row__state">
243
+ <StatusIcon status={file.status} />
244
+ {file.status === "uploaded" && <span>{uploadedLabel}</span>}
245
+ {file.status === "uploading" && <span>{uploadLabel}</span>}
246
+ {file.status === "error" && <span>{errorLabel}</span>}
247
+ </span>
248
+ )}
249
+ </div>
250
+ </>
251
+ )}
252
+
253
+ {isProgress && (
254
+ <div
255
+ aria-label={`Upload progress ${progressLabel}`}
256
+ aria-valuemax={100}
257
+ aria-valuemin={0}
258
+ aria-valuenow={progressValue}
259
+ className="ds-file-upload-row__progress"
260
+ role="progressbar"
261
+ >
262
+ <span className="ds-file-upload-row__progress-track">
263
+ <span className="ds-file-upload-row__progress-fill" style={{ width: `${progressValue}%` }} />
264
+ </span>
265
+ <span className="ds-file-upload-row__progress-label">{progressLabel}</span>
266
+ </div>
267
+ )}
268
+
269
+ {!isSmall && file.status === "error" && (
270
+ <Button className="ds-file-upload-row__retry" onClick={() => onRetry?.(file.id)} size="xs" type="button" variant="link">
271
+ {retryLabel}
272
+ </Button>
273
+ )}
274
+ </div>
275
+ </div>
276
+
277
+ {isSmall && file.status !== "uploaded" && file.status !== "uploading-progress" && (
278
+ <StatusIcon status={file.status} standalone />
279
+ )}
280
+
281
+ <div className="ds-file-upload-row__actions">
282
+ {file.status === "uploaded" && (
283
+ <IconAction ariaLabel={`${moreLabel}: ${file.name}`} icon={<MoreFillIcon />} onClick={() => onMore?.(file.id)} />
284
+ )}
285
+ {file.status === "uploading" && (
286
+ <IconAction ariaLabel={`${cancelLabel}: ${file.name}`} icon={<CloseFillIcon />} onClick={() => onCancel?.(file.id)} />
287
+ )}
288
+ {file.status !== "uploading" && (
289
+ <IconAction ariaLabel={`${removeLabel}: ${file.name}`} icon={<DeleteBinLineIcon />} onClick={() => onRemove?.(file.id)} />
290
+ )}
291
+ </div>
292
+ </li>
293
+ );
294
+ }
295
+
296
+ function FileTypeIcon({ label, size }: { label: string; size: FileUploadSize }) {
297
+ const iconLabel = normalizeExtensionLabel(label);
298
+ const icon = resolveFileIconVariant(iconLabel);
299
+ const badgeX = icon.badgeX ?? getFileIconBadgeX(icon.badgeWidth);
300
+ const badgeTextX = badgeX + icon.badgeWidth / 2;
301
+ const badgeStyle = {
302
+ "--ds-file-upload-file-badge-bg": icon.badgeFill,
303
+ } as CSSProperties;
304
+
305
+ return (
306
+ <span className={cx("ds-file-upload-file-icon", `ds-file-upload-file-icon--${size}`)} style={badgeStyle} aria-hidden="true">
307
+ <svg className="ds-file-upload-file-icon__svg" viewBox="0 0 40 40" fill="none" focusable="false">
308
+ <path
309
+ className="ds-file-upload-file-icon__page"
310
+ d="M13 3.5H24.1719C24.5696 3.50008 24.9512 3.65822 25.2324 3.93945L34.0605 12.7676C34.3418 13.0488 34.4999 13.4304 34.5 13.8281V33C34.5 34.933 32.933 36.5 31 36.5H13C11.067 36.5 9.5 34.933 9.5 33V7C9.5 5.067 11.067 3.5 13 3.5Z"
311
+ />
312
+ <path
313
+ className="ds-file-upload-file-icon__fold"
314
+ d="M24.5 3.59117V9.21432C24.5 11.4235 26.2909 13.2143 28.5 13.2143H34.0414"
315
+ />
316
+ <rect className="ds-file-upload-file-icon__badge" x={badgeX} y="20" width={icon.badgeWidth} height="12" rx="3" />
317
+ <text className="ds-file-upload-file-icon__label" x={badgeTextX} y="26.55" textAnchor="middle" dominantBaseline="middle">
318
+ {iconLabel}
319
+ </text>
320
+ </svg>
321
+ </span>
322
+ );
323
+ }
324
+
325
+ type FileIconVariant = {
326
+ badgeFill: string;
327
+ badgeWidth: number;
328
+ badgeX?: number;
329
+ };
330
+
331
+ const FILE_ICON_VARIANTS = {
332
+ AI: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 20 },
333
+ AVI: { badgeFill: "var(--bh-bg-accent-lime-default)", badgeWidth: 21 },
334
+ BMP: { badgeFill: "var(--bh-bg-accent-lime-default)", badgeWidth: 24 },
335
+ CRD: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 23 },
336
+ CSV: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 23 },
337
+ DLL: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 21 },
338
+ DOC: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 24 },
339
+ DOCX: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 29 },
340
+ EPS: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 21 },
341
+ EXE: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 21 },
342
+ FIG: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 20 },
343
+ FLV: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 20 },
344
+ GIF: { badgeFill: "var(--bh-bg-accent-rose-default)", badgeWidth: 19 },
345
+ HTML: { badgeFill: "var(--bh-bg-accent-lime-default)", badgeWidth: 30, badgeX: 3 },
346
+ IMG: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 22 },
347
+ ISO: { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth: 19 },
348
+ JAVA: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 27 },
349
+ JPG: { badgeFill: "var(--bh-bg-accent-fuchsia-default)", badgeWidth: 22 },
350
+ JSON: { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth: 28 },
351
+ MID: { badgeFill: "var(--bh-bg-accent-fuchsia-default)", badgeWidth: 23 },
352
+ MOV: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 25 },
353
+ MP3: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 24 },
354
+ MP4: { badgeFill: "var(--bh-bg-accent-rose-default)", badgeWidth: 24 },
355
+ MPEG: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 30, badgeX: 3 },
356
+ PDF: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 22 },
357
+ PNG: { badgeFill: "var(--bh-bg-accent-aqua-default)", badgeWidth: 23 },
358
+ PPT: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 22 },
359
+ PPTX: { badgeFill: "var(--bh-bg-accent-red-default)", badgeWidth: 28 },
360
+ PSD: { badgeFill: "var(--bh-bg-accent-blue-default)", badgeWidth: 22 },
361
+ PUB: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 22 },
362
+ RAR: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 22 },
363
+ RAW: { badgeFill: "var(--bh-bg-accent-sky-base)", badgeWidth: 25 },
364
+ RSS: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 22 },
365
+ SVG: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 23 },
366
+ TIFF: { badgeFill: "var(--bh-bg-accent-fuchsia-default)", badgeWidth: 23 },
367
+ TXT: { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth: 22 },
368
+ WAV: { badgeFill: "var(--bh-bg-accent-amber-default)", badgeWidth: 25 },
369
+ WMA: { badgeFill: "var(--bh-bg-accent-golden-default)", badgeWidth: 28 },
370
+ XML: { badgeFill: "var(--bh-bg-accent-purple-default)", badgeWidth: 23 },
371
+ XSL: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 21 },
372
+ XSLX: { badgeFill: "var(--bh-bg-accent-green-default)", badgeWidth: 27 },
373
+ ZIP: { badgeFill: "var(--bh-bg-accent-aqua-default)", badgeWidth: 21 },
374
+ } satisfies Record<string, FileIconVariant>;
375
+
376
+ const FILE_ICON_ALIASES: Record<string, keyof typeof FILE_ICON_VARIANTS> = {
377
+ CDR: "CRD",
378
+ JPEG: "JPG",
379
+ XLS: "XSL",
380
+ XLSX: "XSLX",
381
+ };
382
+
383
+ function resolveFileIconVariant(label: string): FileIconVariant {
384
+ const directVariant = FILE_ICON_VARIANTS[label as keyof typeof FILE_ICON_VARIANTS];
385
+ if (directVariant) return directVariant;
386
+
387
+ const aliasedVariant = FILE_ICON_ALIASES[label];
388
+ if (aliasedVariant) return FILE_ICON_VARIANTS[aliasedVariant];
389
+
390
+ const badgeWidth = label.length <= 2 ? 20 : label.length >= 4 ? 30 : 23;
391
+ return { badgeFill: "var(--bh-bg-neutral-default)", badgeWidth, badgeX: getFileIconBadgeX(badgeWidth) };
392
+ }
393
+
394
+ function getFileIconBadgeX(badgeWidth: number) {
395
+ return badgeWidth >= 30 ? 3 : 5;
396
+ }
397
+
398
+ function StatusIcon({
399
+ standalone = false,
400
+ status,
401
+ }: {
402
+ standalone?: boolean;
403
+ status: Exclude<FileUploadItemStatus, "uploading-progress">;
404
+ }) {
405
+ return (
406
+ <span
407
+ aria-hidden={standalone ? undefined : "true"}
408
+ aria-label={standalone ? getStatusLabel(status) : undefined}
409
+ className={cx(
410
+ "ds-file-upload-row__status",
411
+ standalone && "ds-file-upload-row__status--standalone",
412
+ `ds-file-upload-row__status--${status}`
413
+ )}
414
+ role={standalone ? "img" : undefined}
415
+ >
416
+ {status === "uploaded" && <CheckboxCircleFillIcon />}
417
+ {status === "uploading" && <Loader2LineIcon />}
418
+ {status === "error" && <ErrorWarningFillIcon />}
419
+ </span>
420
+ );
421
+ }
422
+
423
+ function UploadCloudIcon() {
424
+ return (
425
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 24 24">
426
+ <path
427
+ d="M1 14.5C1 12.1716 2.22429 10.1291 4.06426 8.9812C4.56469 5.044 7.92686 2 12 2C16.0731 2 19.4353 5.044 19.9357 8.9812C21.7757 10.1291 23 12.1716 23 14.5C23 17.9216 20.3562 20.7257 17 20.9811L7 21C3.64378 20.7257 1 17.9216 1 14.5ZM16.8483 18.9868C19.1817 18.8093 21 16.8561 21 14.5C21 12.927 20.1884 11.4962 18.8771 10.6781L18.0714 10.1754L17.9517 9.23338C17.5735 6.25803 15.0288 4 12 4C8.97116 4 6.42647 6.25803 6.0483 9.23338L5.92856 10.1754L5.12288 10.6781C3.81156 11.4962 3 12.927 3 14.5C3 16.8561 4.81833 18.8093 7.1517 18.9868L7.325 19H16.675L16.8483 18.9868ZM13 13V17H11V13H8L12 8L16 13H13Z"
428
+ fill="currentColor"
429
+ />
430
+ </svg>
431
+ );
432
+ }
433
+
434
+ function CheckboxCircleFillIcon() {
435
+ return (
436
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 16 16">
437
+ <path
438
+ d="M8.00016 14.6667C11.682 14.6667 14.6668 11.6819 14.6668 8.00001C14.6668 4.31811 11.682 1.33334 8.00016 1.33334C4.31826 1.33334 1.3335 4.31811 1.3335 8.00001C1.3335 11.6819 4.31826 14.6667 8.00016 14.6667ZM11.6382 6.30475L7.3335 10.6095L4.52876 7.80474L5.47157 6.86194L7.3335 8.72388L10.6954 5.36194L11.6382 6.30475Z"
439
+ fill="currentColor"
440
+ />
441
+ </svg>
442
+ );
443
+ }
444
+
445
+ function Loader2LineIcon() {
446
+ return (
447
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 16 16">
448
+ <path
449
+ d="M8.00016 1.33333C8.36836 1.33333 8.66683 1.63181 8.66683 1.99999V3.99999C8.66683 4.36818 8.36836 4.66666 8.00016 4.66666C7.63196 4.66666 7.3335 4.36818 7.3335 3.99999V1.99999C7.3335 1.63181 7.63196 1.33333 8.00016 1.33333ZM8.00016 11.3333C8.36836 11.3333 8.66683 11.6318 8.66683 12V14C8.66683 14.3682 8.36836 14.6667 8.00016 14.6667C7.63196 14.6667 7.3335 14.3682 7.3335 14V12C7.3335 11.6318 7.63196 11.3333 8.00016 11.3333ZM14.6668 7.99999C14.6668 8.36819 14.3684 8.66666 14.0002 8.66666H12.0002C11.632 8.66666 11.3335 8.36819 11.3335 7.99999C11.3335 7.63179 11.632 7.33333 12.0002 7.33333H14.0002C14.3684 7.33333 14.6668 7.63179 14.6668 7.99999ZM4.66683 7.99999C4.66683 8.36819 4.36835 8.66666 4.00016 8.66666H2.00016C1.63198 8.66666 1.3335 8.36819 1.3335 7.99999C1.3335 7.63179 1.63198 7.33333 2.00016 7.33333H4.00016C4.36835 7.33333 4.66683 7.63179 4.66683 7.99999ZM12.7142 12.7141C12.4538 12.9744 12.0318 12.9744 11.7714 12.7141L10.3572 11.2998C10.0968 11.0395 10.0968 10.6174 10.3572 10.357C10.6176 10.0967 11.0396 10.0967 11.3 10.357L12.7142 11.7713C12.9746 12.0316 12.9746 12.4537 12.7142 12.7141ZM5.64314 5.64297C5.38279 5.90332 4.96068 5.90332 4.70033 5.64297L3.28612 4.22876C3.02577 3.96841 3.02577 3.5463 3.28612 3.28595C3.54647 3.0256 3.96858 3.0256 4.22893 3.28595L5.64314 4.70016C5.90349 4.96051 5.90349 5.38262 5.64314 5.64297ZM3.28612 12.7141C3.02577 12.4537 3.02577 12.0316 3.28612 11.7713L4.70033 10.357C4.96068 10.0967 5.38279 10.0967 5.64314 10.357C5.90349 10.6174 5.90349 11.0395 5.64314 11.2998L4.22893 12.7141C3.96858 12.9744 3.54647 12.9744 3.28612 12.7141ZM10.3572 5.64297C10.0968 5.38262 10.0968 4.96051 10.3572 4.70016L11.7714 3.28595C12.0318 3.0256 12.4538 3.0256 12.7142 3.28595C12.9746 3.5463 12.9746 3.96841 12.7142 4.22876L11.3 5.64297C11.0396 5.90332 10.6176 5.90332 10.3572 5.64297Z"
450
+ fill="currentColor"
451
+ />
452
+ </svg>
453
+ );
454
+ }
455
+
456
+ function ErrorWarningFillIcon() {
457
+ return (
458
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 16 16">
459
+ <path
460
+ d="M8.00016 14.6667C4.31826 14.6667 1.3335 11.6819 1.3335 8.00001C1.3335 4.31811 4.31826 1.33334 8.00016 1.33334C11.682 1.33334 14.6668 4.31811 14.6668 8.00001C14.6668 11.6819 11.682 14.6667 8.00016 14.6667ZM7.3335 10V11.3333H8.66683V10H7.3335ZM7.3335 4.66668V8.66668H8.66683V4.66668H7.3335Z"
461
+ fill="currentColor"
462
+ />
463
+ </svg>
464
+ );
465
+ }
466
+
467
+ function MoreFillIcon() {
468
+ return (
469
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 20 20">
470
+ <path
471
+ d="M4.16667 8.33334C3.25 8.33334 2.5 9.08334 2.5 10C2.5 10.9167 3.25 11.6667 4.16667 11.6667C5.08333 11.6667 5.83333 10.9167 5.83333 10C5.83333 9.08334 5.08333 8.33334 4.16667 8.33334ZM15.8333 8.33334C14.9167 8.33334 14.1667 9.08334 14.1667 10C14.1667 10.9167 14.9167 11.6667 15.8333 11.6667C16.75 11.6667 17.5 10.9167 17.5 10C17.5 9.08334 16.75 8.33334 15.8333 8.33334ZM10 8.33334C9.08333 8.33334 8.33333 9.08334 8.33333 10C8.33333 10.9167 9.08333 11.6667 10 11.6667C10.9167 11.6667 11.6667 10.9167 11.6667 10C11.6667 9.08334 10.9167 8.33334 10 8.33334Z"
472
+ fill="currentColor"
473
+ />
474
+ </svg>
475
+ );
476
+ }
477
+
478
+ function CloseFillIcon() {
479
+ return (
480
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 20 20">
481
+ <path
482
+ d="M9.99959 8.82208L14.1244 4.69727L15.3029 5.87577L11.1781 10.0006L15.3029 14.1253L14.1244 15.3038L9.99959 11.1791L5.87481 15.3038L4.69629 14.1253L8.82109 10.0006L4.69629 5.87577L5.87481 4.69727L9.99959 8.82208Z"
483
+ fill="currentColor"
484
+ />
485
+ </svg>
486
+ );
487
+ }
488
+
489
+ function DeleteBinLineIcon() {
490
+ return (
491
+ <svg aria-hidden="true" fill="none" focusable="false" viewBox="0 0 20 20">
492
+ <path
493
+ d="M14.1665 4.99999H18.3332V6.66666H16.6665V17.5C16.6665 17.9602 16.2934 18.3333 15.8332 18.3333H4.1665C3.70627 18.3333 3.33317 17.9602 3.33317 17.5V6.66666H1.6665V4.99999H5.83317V2.49999C5.83317 2.03976 6.20627 1.66666 6.6665 1.66666H13.3332C13.7934 1.66666 14.1665 2.03976 14.1665 2.49999V4.99999ZM14.9998 6.66666H4.99984V16.6667H14.9998V6.66666ZM7.49984 9.16666H9.1665V14.1667H7.49984V9.16666ZM10.8332 9.16666H12.4998V14.1667H10.8332V9.16666ZM7.49984 3.33332V4.99999H12.4998V3.33332H7.49984Z"
494
+ fill="currentColor"
495
+ />
496
+ </svg>
497
+ );
498
+ }
499
+
500
+ function IconAction({
501
+ ariaLabel,
502
+ icon,
503
+ onClick,
504
+ }: {
505
+ ariaLabel: string;
506
+ icon: ReactNode;
507
+ onClick: () => void;
508
+ }) {
509
+ return (
510
+ <Button aria-label={ariaLabel} className="ds-file-upload-row__action" onClick={onClick} size="icon-sm" type="button" variant="ghost">
511
+ {icon}
512
+ </Button>
513
+ );
514
+ }
515
+
516
+ function getExtensionLabel(name: string) {
517
+ const extension = name.split(".").pop();
518
+ if (!extension || extension === name) return "FILE";
519
+ return normalizeExtensionLabel(extension);
520
+ }
521
+
522
+ function normalizeExtensionLabel(label: string) {
523
+ const normalized = label.trim().replace(/^\./, "").toUpperCase().replace(/[^A-Z0-9]/g, "");
524
+ return normalized ? normalized.slice(0, 4) : "FILE";
525
+ }
526
+
527
+ function getMetaLabel(file: FileUploadFile) {
528
+ return [file.sizeLabel, file.versionLabel].filter(Boolean).join(" \u00b7 ");
529
+ }
530
+
531
+ function getStatusLabel(status: Exclude<FileUploadItemStatus, "uploading-progress">) {
532
+ if (status === "uploaded") return "Uploaded";
533
+ if (status === "uploading") return "Uploading";
534
+ return "Upload failed";
535
+ }
536
+
537
+ function clampProgress(value: number) {
538
+ return Math.min(100, Math.max(0, Math.round(value)));
539
+ }
540
+
541
+ function cx(...classes: Array<string | false | null | undefined>) {
542
+ return classes.filter(Boolean).join(" ");
543
+ }