banhaten 0.4.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.
- package/CHANGELOG.md +139 -100
- package/CONTRIBUTING.md +46 -44
- package/README.md +192 -190
- package/SECURITY.md +15 -15
- package/banhaten.config.example.json +11 -11
- package/docs/design-spacing.md +13 -0
- package/docs/design-system/README.md +22 -20
- package/docs/design-system/adoption-quality-remediation.md +87 -87
- package/docs/design-system/agent-usage.md +13 -13
- package/docs/design-system/appearance-presets.md +189 -189
- package/docs/design-system/appearances/default.md +94 -94
- package/docs/design-system/appearances/rounded.md +95 -95
- package/docs/design-system/appearances/sharp.md +95 -95
- package/docs/design-system/component-docs-pattern.md +81 -81
- package/docs/design-system/component-implementation-ownership-audit.md +245 -245
- package/docs/design-system/component-quality-audit-2026-08-20.md +225 -225
- package/docs/design-system/component-showcase-consistency-report.md +219 -219
- package/docs/design-system/component-token-consistency-audit.md +173 -173
- package/docs/design-system/components/README.md +90 -76
- package/docs/design-system/components/accordion.md +53 -53
- package/docs/design-system/components/activity-feed.md +94 -94
- package/docs/design-system/components/activity-heatmap.md +57 -57
- package/docs/design-system/components/agent-input.md +85 -0
- package/docs/design-system/components/alert-dialog.md +72 -72
- package/docs/design-system/components/alert.md +97 -97
- package/docs/design-system/components/approval-card.md +134 -0
- package/docs/design-system/components/aspect-ratio.md +45 -45
- package/docs/design-system/components/attribute.md +88 -88
- package/docs/design-system/components/autocomplete.md +75 -75
- package/docs/design-system/components/avatar.md +54 -54
- package/docs/design-system/components/badge.md +54 -54
- package/docs/design-system/components/banner.md +87 -87
- package/docs/design-system/components/breadcrumbs.md +176 -175
- package/docs/design-system/components/button-group.md +98 -98
- package/docs/design-system/components/button.md +83 -80
- package/docs/design-system/components/card.md +88 -88
- package/docs/design-system/components/carousel.md +46 -46
- package/docs/design-system/components/catalog-components.md +47 -47
- package/docs/design-system/components/chart.md +95 -24
- package/docs/design-system/components/checkbox.md +54 -54
- package/docs/design-system/components/collapsible.md +67 -53
- package/docs/design-system/components/command.md +62 -62
- package/docs/design-system/components/context-menu.md +46 -46
- package/docs/design-system/components/date-picker.md +81 -81
- package/docs/design-system/components/divider.md +102 -102
- package/docs/design-system/components/empty-state.md +56 -56
- package/docs/design-system/components/field.md +70 -70
- package/docs/design-system/components/file-upload.md +187 -187
- package/docs/design-system/components/hover-card.md +48 -48
- package/docs/design-system/components/icons.md +48 -48
- package/docs/design-system/components/inline-citations.md +93 -0
- package/docs/design-system/components/input-group.md +58 -58
- package/docs/design-system/components/input-otp.md +56 -56
- package/docs/design-system/components/input.md +114 -60
- package/docs/design-system/components/item.md +59 -59
- package/docs/design-system/components/kbd.md +51 -49
- package/docs/design-system/components/label.md +49 -49
- package/docs/design-system/components/menu.md +71 -61
- package/docs/design-system/components/menubar.md +47 -47
- package/docs/design-system/components/modal.md +105 -105
- package/docs/design-system/components/native-select.md +54 -54
- package/docs/design-system/components/navigation-menu.md +50 -50
- package/docs/design-system/components/onboarding-step-list-item.md +82 -82
- package/docs/design-system/components/page-header.md +93 -86
- package/docs/design-system/components/pagination.md +57 -57
- package/docs/design-system/components/popover.md +60 -60
- package/docs/design-system/components/progress-slider.md +50 -50
- package/docs/design-system/components/progress.md +76 -76
- package/docs/design-system/components/radio-card.md +51 -51
- package/docs/design-system/components/radio-group.md +57 -57
- package/docs/design-system/components/resizable.md +44 -44
- package/docs/design-system/components/rolling-count.md +61 -0
- package/docs/design-system/components/scroll-area.md +51 -51
- package/docs/design-system/components/segmented-control.md +54 -54
- package/docs/design-system/components/select.md +68 -62
- package/docs/design-system/components/sheet.md +67 -67
- package/docs/design-system/components/shimmer-text.md +67 -0
- package/docs/design-system/components/sidebar.md +110 -99
- package/docs/design-system/components/skeleton.md +75 -75
- package/docs/design-system/components/slideout.md +65 -65
- package/docs/design-system/components/slider.md +63 -63
- package/docs/design-system/components/social-button.md +50 -50
- package/docs/design-system/components/spinner.md +62 -62
- package/docs/design-system/components/steps.md +65 -65
- package/docs/design-system/components/streaming-text.md +82 -0
- package/docs/design-system/components/table.md +419 -414
- package/docs/design-system/components/tabs.md +59 -55
- package/docs/design-system/components/tag.md +80 -80
- package/docs/design-system/components/task-list.md +95 -0
- package/docs/design-system/components/textarea.md +50 -50
- package/docs/design-system/components/thinking-reasoning.md +72 -0
- package/docs/design-system/components/thinking-state.md +67 -0
- package/docs/design-system/components/timeline.md +83 -83
- package/docs/design-system/components/toast.md +58 -58
- package/docs/design-system/components/toggle.md +81 -81
- package/docs/design-system/components/toolbar.md +92 -87
- package/docs/design-system/components/tooltip.md +94 -92
- package/docs/design-system/components/typography.md +18 -18
- package/docs/design-system/density-and-elevation.md +65 -65
- package/docs/design-system/design-system-test-missing-items.md +368 -368
- package/docs/design-system/direction-and-localization.md +23 -23
- package/docs/design-system/form-recipes.md +73 -73
- package/docs/design-system/gap-promotion-policy.md +21 -21
- package/docs/design-system/icons.md +74 -74
- package/docs/design-system/installation.md +70 -68
- package/docs/design-system/launch-comparison-shadcn.md +98 -98
- package/docs/design-system/registry-and-cli.md +64 -64
- package/docs/design-system/release-policy.md +30 -30
- package/docs/design-system/roadmap.md +14 -14
- package/docs/design-system/support-matrix.md +23 -23
- package/docs/design-system/tabs.md +54 -53
- package/docs/design-system/template-gap-audit.md +32 -0
- package/docs/design-system/templates.md +59 -0
- package/docs/design-system/token-governance.md +72 -59
- package/package.json +98 -98
- package/registry/component-contract.json +40 -40
- package/registry/components/accordion.tsx +155 -155
- package/registry/components/agent-input-parts.tsx +552 -0
- package/registry/components/agent-input-state.ts +156 -0
- package/registry/components/agent-input.css +307 -0
- package/registry/components/agent-input.tsx +382 -0
- package/registry/components/alert-dialog.tsx +297 -297
- package/registry/components/alert.tsx +293 -293
- package/registry/components/approval-card.css +409 -0
- package/registry/components/approval-card.tsx +401 -0
- package/registry/components/approval-frame.tsx +108 -0
- package/registry/components/approval-questions.tsx +559 -0
- package/registry/components/aspect-ratio.tsx +30 -30
- package/registry/components/attribute.tsx +454 -454
- package/registry/components/autocomplete.tsx +598 -651
- package/registry/components/avatar.tsx +385 -385
- package/registry/components/badge.tsx +629 -629
- package/registry/components/button-group.tsx +565 -565
- package/registry/components/button.tsx +127 -123
- package/registry/components/card.tsx +638 -638
- package/registry/components/carousel.tsx +296 -296
- package/registry/components/chart-block-data.ts +177 -0
- package/registry/components/chart-block.css +171 -0
- package/registry/components/chart-block.tsx +260 -0
- package/registry/components/chart-interactions.ts +123 -0
- package/registry/components/chart-options.cartesian.ts +162 -0
- package/registry/components/chart-options.paint.ts +99 -0
- package/registry/components/chart-options.parts.ts +72 -0
- package/registry/components/chart-options.radial.ts +109 -0
- package/registry/components/chart-options.ts +44 -0
- package/registry/components/chart-options.types.ts +67 -0
- package/registry/components/chart.tsx +203 -203
- package/registry/components/checkbox.tsx +247 -247
- package/registry/components/collapsible.tsx +109 -69
- package/registry/components/command.tsx +208 -208
- package/registry/components/context-menu.tsx +236 -236
- package/registry/components/data-chart.tsx +40 -0
- package/registry/components/date-picker.tsx +1289 -1289
- package/registry/components/echart-theme.ts +246 -0
- package/registry/components/echart.css +49 -0
- package/registry/components/echart.tsx +178 -0
- package/registry/components/expanded/ActivityFeed.tsx +227 -227
- package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
- package/registry/components/expanded/Banner.tsx +179 -179
- package/registry/components/expanded/Breadcrumbs.tsx +131 -131
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
- package/registry/components/expanded/CatalogDivider.tsx +49 -49
- package/registry/components/expanded/EmptyState.tsx +155 -155
- package/registry/components/expanded/FileUpload.tsx +543 -543
- package/registry/components/expanded/OnboardingStepListItem.tsx +60 -60
- package/registry/components/expanded/PageHeader.tsx +179 -166
- package/registry/components/expanded/Slideout.tsx +108 -108
- package/registry/components/expanded/Steps.tsx +402 -402
- package/registry/components/expanded/Table.tsx +1092 -1071
- package/registry/components/expanded/Timeline.tsx +203 -203
- package/registry/components/expanded/activityFeed.css +248 -248
- package/registry/components/expanded/activityHeatmap.css +66 -66
- package/registry/components/expanded/banner.css +260 -260
- package/registry/components/expanded/breadcrumbs.css +140 -140
- package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
- package/registry/components/expanded/divider.css +75 -75
- package/registry/components/expanded/emptyState.css +111 -111
- package/registry/components/expanded/fileUpload.css +457 -457
- package/registry/components/expanded/onboardingStepListItem.css +135 -135
- package/registry/components/expanded/pageHeader.css +248 -290
- package/registry/components/expanded/slideout.css +276 -276
- package/registry/components/expanded/steps.css +356 -356
- package/registry/components/expanded/table.css +606 -606
- package/registry/components/expanded/timeline.css +286 -286
- package/registry/components/field.tsx +283 -283
- package/registry/components/hover-card.tsx +48 -48
- package/registry/components/inline-citations.css +126 -0
- package/registry/components/inline-citations.tsx +292 -0
- package/registry/components/input-content.ts +107 -107
- package/registry/components/input-group.tsx +130 -130
- package/registry/components/input-otp.tsx +584 -584
- package/registry/components/input.tsx +990 -979
- package/registry/components/item.tsx +189 -189
- package/registry/components/kbd.tsx +246 -245
- package/registry/components/label.tsx +78 -78
- package/registry/components/menu.tsx +978 -986
- package/registry/components/menubar.tsx +226 -226
- package/registry/components/modal.tsx +872 -872
- package/registry/components/native-select.tsx +205 -205
- package/registry/components/navigation-menu.tsx +171 -171
- package/registry/components/pagination.css +36 -36
- package/registry/components/pagination.tsx +560 -560
- package/registry/components/popover.tsx +338 -338
- package/registry/components/progress-slider.tsx +216 -216
- package/registry/components/radio-card.tsx +676 -676
- package/registry/components/radio-group.tsx +570 -570
- package/registry/components/resizable.tsx +108 -108
- package/registry/components/rolling-count.css +37 -0
- package/registry/components/rolling-count.tsx +110 -0
- package/registry/components/scroll-area.tsx +91 -91
- package/registry/components/segmented-control.tsx +87 -87
- package/registry/components/select-content.tsx +218 -218
- package/registry/components/select.tsx +937 -932
- package/registry/components/sheet.tsx +305 -305
- package/registry/components/shimmer-text.css +69 -0
- package/registry/components/shimmer-text.tsx +43 -0
- package/registry/components/sidebar.tsx +1433 -1230
- package/registry/components/skeleton.css +54 -54
- package/registry/components/skeleton.tsx +482 -482
- package/registry/components/slider.tsx +380 -380
- package/registry/components/social-button.tsx +387 -387
- package/registry/components/spinner.tsx +78 -78
- package/registry/components/streaming-text.css +45 -0
- package/registry/components/streaming-text.tsx +165 -0
- package/registry/components/table-elements.tsx +128 -128
- package/registry/components/tabs.css +222 -218
- package/registry/components/tabs.tsx +241 -241
- package/registry/components/tag.tsx +423 -423
- package/registry/components/task-list.css +328 -0
- package/registry/components/task-list.tsx +429 -0
- package/registry/components/textarea.tsx +610 -610
- package/registry/components/thinking-reasoning.css +178 -0
- package/registry/components/thinking-reasoning.tsx +253 -0
- package/registry/components/thinking-state.css +8 -0
- package/registry/components/thinking-state.tsx +46 -0
- package/registry/components/toast.tsx +136 -136
- package/registry/components/toggle.tsx +256 -256
- package/registry/components/toolbar.tsx +539 -539
- package/registry/components/tooltip.tsx +433 -431
- package/registry/examples/agent-input-demo.tsx +90 -0
- package/registry/examples/alert-demo.tsx +34 -34
- package/registry/examples/alert-dialog-demo.tsx +42 -42
- package/registry/examples/approval-card-demo.tsx +83 -0
- package/registry/examples/aspect-ratio-demo.tsx +11 -11
- package/registry/examples/attribute-demo.tsx +65 -65
- package/registry/examples/autocomplete-demo.tsx +109 -109
- package/registry/examples/avatar-demo.tsx +132 -132
- package/registry/examples/badge-demo.tsx +69 -69
- package/registry/examples/button-demo.tsx +86 -83
- package/registry/examples/button-group-demo.tsx +63 -63
- package/registry/examples/card-demo.tsx +86 -86
- package/registry/examples/carousel-demo.tsx +43 -43
- package/registry/examples/chart-block-demo.tsx +5 -0
- package/registry/examples/chart-demo.tsx +18 -33
- package/registry/examples/checkbox-demo.tsx +62 -62
- package/registry/examples/collapsible-demo.tsx +16 -16
- package/registry/examples/collection-filter-bar-demo.tsx +135 -135
- package/registry/examples/command-demo.tsx +42 -42
- package/registry/examples/command-palette-demo.tsx +235 -235
- package/registry/examples/context-menu-demo.tsx +29 -29
- package/registry/examples/control-density-contract-demo.tsx +65 -61
- package/registry/examples/date-picker-demo.tsx +127 -127
- package/registry/examples/divider-demo.tsx +17 -17
- package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
- package/registry/examples/expanded/banner-demo.tsx +48 -48
- package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
- package/registry/examples/expanded/empty-state-demo.tsx +39 -39
- package/registry/examples/expanded/file-upload-demo.tsx +60 -60
- package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
- package/registry/examples/expanded/slideout-demo.tsx +15 -15
- package/registry/examples/expanded/steps-demo.tsx +29 -29
- package/registry/examples/expanded/tabs-demo.tsx +13 -13
- package/registry/examples/expanded/timeline-demo.tsx +18 -18
- package/registry/examples/field-demo.tsx +51 -51
- package/registry/examples/hover-card-demo.tsx +23 -23
- package/registry/examples/inline-citations-demo.tsx +31 -0
- package/registry/examples/input-actions-demo.tsx +36 -0
- package/registry/examples/input-demo.tsx +87 -87
- package/registry/examples/input-group-demo.tsx +16 -16
- package/registry/examples/input-otp-demo.tsx +72 -72
- package/registry/examples/item-demo.tsx +46 -46
- package/registry/examples/kbd-demo.tsx +12 -12
- package/registry/examples/label-demo.tsx +20 -20
- package/registry/examples/menu-demo.tsx +122 -122
- package/registry/examples/menubar-demo.tsx +34 -34
- package/registry/examples/modal-demo.tsx +74 -74
- package/registry/examples/native-select-demo.tsx +16 -16
- package/registry/examples/navigation-menu-demo.tsx +29 -29
- package/registry/examples/pagination-demo.tsx +20 -20
- package/registry/examples/popover-demo.tsx +547 -546
- package/registry/examples/progress-demo.tsx +37 -37
- package/registry/examples/progress-slider-demo.tsx +29 -29
- package/registry/examples/radio-card-demo.tsx +51 -51
- package/registry/examples/radio-group-demo.tsx +62 -62
- package/registry/examples/resizable-demo.tsx +22 -22
- package/registry/examples/results-toolbar-demo.tsx +44 -44
- package/registry/examples/rolling-count-demo.tsx +39 -0
- package/registry/examples/scroll-area-demo.tsx +15 -15
- package/registry/examples/segmented-control-demo.tsx +39 -39
- package/registry/examples/select-demo.tsx +87 -87
- package/registry/examples/sheet-demo.tsx +47 -47
- package/registry/examples/shimmer-text-demo.tsx +20 -0
- package/registry/examples/sidebar-demo.tsx +101 -101
- package/registry/examples/slider-demo.tsx +31 -31
- package/registry/examples/social-button-demo.tsx +61 -61
- package/registry/examples/spinner-demo.tsx +59 -59
- package/registry/examples/streaming-text-demo.tsx +28 -0
- package/registry/examples/tabs-demo.tsx +13 -13
- package/registry/examples/tag-demo.tsx +29 -29
- package/registry/examples/task-list-demo.tsx +74 -0
- package/registry/examples/textarea-demo.tsx +79 -79
- package/registry/examples/thinking-reasoning-demo.tsx +74 -0
- package/registry/examples/thinking-state-demo.tsx +13 -0
- package/registry/examples/toast-demo.tsx +35 -35
- package/registry/examples/toggle-demo.tsx +59 -59
- package/registry/examples/tooltip-demo.tsx +115 -115
- package/registry/guidance/USAGE.md +27 -27
- package/registry/guidance/charts.LICENSE.txt +24 -0
- package/registry/hooks/use-controllable-state.ts +33 -0
- package/registry/hooks/use-direction.ts +207 -207
- package/registry/hooks/use-listbox-navigation.ts +73 -0
- package/registry/hooks/use-motion-end.ts +55 -0
- package/registry/hooks/use-reduced-motion.ts +28 -0
- package/registry/index.json +1403 -155
- package/registry/styles/globals.css +946 -1088
- package/registry.json +361 -8
- package/schema/config.schema.json +51 -51
- package/schema/registry.schema.json +194 -190
- package/schema/tokens.schema.json +167 -163
- package/src/cli/command-line.js +194 -194
- package/src/cli/config-editors.js +9 -9
- package/src/cli/doctor.js +399 -399
- package/src/cli/file-transaction.js +302 -295
- package/src/cli/global-css.js +153 -153
- package/src/cli/index.js +1002 -1009
- package/src/cli/install-plan.js +592 -592
- package/src/cli/install-receipt.js +171 -171
- package/src/cli/jsonc.js +203 -203
- package/src/cli/package-install-state.js +254 -254
- package/src/cli/package-manager.js +238 -238
- package/src/cli/package-plan.js +185 -185
- package/src/cli/package-version.js +97 -97
- package/src/cli/registry-query.js +207 -207
- package/src/cli/reporting.js +55 -55
- package/src/cli/schema-validation.js +19 -19
- package/src/cli/source-scanner.js +686 -686
- package/src/cli/vite-config.js +478 -478
- package/tokens/banhaten.tokens.json +306 -192
|
@@ -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
|
+
}
|