banhaten 0.3.0 → 0.5.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 (348) hide show
  1. package/CHANGELOG.md +139 -73
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  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 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  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 +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +98 -98
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +89 -89
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +177 -176
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -79
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +127 -76
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -61
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -69
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -60
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1277
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -255
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -126
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -603
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -555
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -677
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -358
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -236
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -11
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -57
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +458 -224
@@ -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
+ }