@freightos/freightwind 2.1.4 → 2.1.6
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/dist/cjs/components/multiple-files-upload.js +195 -0
- package/dist/cjs/components/upload.js +33 -21
- package/dist/cjs/index.js +4 -1
- package/dist/esm/components/multiple-files-upload.js +192 -0
- package/dist/esm/components/upload.js +33 -21
- package/dist/esm/index.js +2 -1
- package/dist/types/components/multiple-files-upload.d.ts +33 -0
- package/dist/types/components/upload.d.ts +4 -2
- package/dist/types/index.d.ts +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MultipleFilesUpload = void 0;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_direction_1 = require("@radix-ui/react-direction");
|
|
8
|
+
const utils_1 = require("../lib/utils");
|
|
9
|
+
const icon_utils_1 = require("../lib/icon-utils");
|
|
10
|
+
const use_stable_id_1 = require("../lib/use-stable-id");
|
|
11
|
+
const file_extensions_1 = require("../lib/file-extensions");
|
|
12
|
+
// ─── Defaults ─────────────────────────────────────────────────────────────────
|
|
13
|
+
const DEFAULT_LABELS = {
|
|
14
|
+
idleTitle: 'Upload your document',
|
|
15
|
+
idleSubtitle: 'Click or drag your file here',
|
|
16
|
+
uploading: 'Uploading…',
|
|
17
|
+
forbidden: {
|
|
18
|
+
multiple: { title: 'Oops!', subtitle: 'Too many files' },
|
|
19
|
+
type: { title: 'Oops!', subtitle: 'Unsupported file format' },
|
|
20
|
+
size: { title: 'Oops!', subtitle: 'File is too large' },
|
|
21
|
+
extension: { title: 'Oops!', subtitle: 'Invalid file extension' },
|
|
22
|
+
custom: { title: 'Oops!', subtitle: 'This file is not allowed' },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const identity = (k) => k;
|
|
26
|
+
// ─── Validation helpers ───────────────────────────────────────────────────────
|
|
27
|
+
const EXT_TO_MIME = {
|
|
28
|
+
'.pdf': ['application/pdf'],
|
|
29
|
+
'.png': ['image/png'],
|
|
30
|
+
'.jpg': ['image/jpeg'],
|
|
31
|
+
'.jpeg': ['image/jpeg'],
|
|
32
|
+
'.gif': ['image/gif'],
|
|
33
|
+
'.webp': ['image/webp'],
|
|
34
|
+
'.svg': ['image/svg+xml'],
|
|
35
|
+
'.xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
36
|
+
'.xls': ['application/vnd.ms-excel'],
|
|
37
|
+
'.csv': ['text/csv', 'application/csv', 'text/plain'],
|
|
38
|
+
'.doc': ['application/msword'],
|
|
39
|
+
'.docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|
40
|
+
'.txt': ['text/plain'],
|
|
41
|
+
'.zip': ['application/zip', 'application/x-zip-compressed'],
|
|
42
|
+
};
|
|
43
|
+
function mimeMatchesAccept(mime, accept) {
|
|
44
|
+
return accept.some((rule) => {
|
|
45
|
+
if (rule.endsWith('/*'))
|
|
46
|
+
return mime.startsWith(rule.slice(0, -1));
|
|
47
|
+
if (rule.startsWith('.'))
|
|
48
|
+
return (EXT_TO_MIME[rule.toLowerCase()] ?? []).includes(mime);
|
|
49
|
+
return mime === rule;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function extMatchesAccept(filename, accept) {
|
|
53
|
+
const dot = filename.lastIndexOf('.');
|
|
54
|
+
if (dot < 0)
|
|
55
|
+
return false;
|
|
56
|
+
const ext = filename.slice(dot).toLowerCase();
|
|
57
|
+
return accept.some((rule) => rule.toLowerCase() === ext);
|
|
58
|
+
}
|
|
59
|
+
function fileMatchesAccept(file, accept) {
|
|
60
|
+
return mimeMatchesAccept(file.type, accept) || extMatchesAccept(file.name, accept);
|
|
61
|
+
}
|
|
62
|
+
function validateDragItems(items, { maxFiles, accept, alreadyHeld = 0 }) {
|
|
63
|
+
if (maxFiles != null && items.length + alreadyHeld > maxFiles)
|
|
64
|
+
return { ok: false, reason: 'multiple' };
|
|
65
|
+
if (accept && accept.length > 0) {
|
|
66
|
+
for (let i = 0; i < items.length; i++) {
|
|
67
|
+
const item = items[i];
|
|
68
|
+
if (!item || item.kind !== 'file')
|
|
69
|
+
continue;
|
|
70
|
+
if (!mimeMatchesAccept(item.type, accept))
|
|
71
|
+
return { ok: false, reason: 'type' };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
function validatePickedFile(file, { accept, maxSize }) {
|
|
77
|
+
if (!(0, file_extensions_1.hasValidExtension)(file.name))
|
|
78
|
+
return { ok: false, reason: 'extension' };
|
|
79
|
+
if (accept && accept.length > 0 && !fileMatchesAccept(file, accept))
|
|
80
|
+
return { ok: false, reason: 'type' };
|
|
81
|
+
if (maxSize != null && file.size > maxSize)
|
|
82
|
+
return { ok: false, reason: 'size' };
|
|
83
|
+
return { ok: true };
|
|
84
|
+
}
|
|
85
|
+
// ─── Component ─────────────────────────────────────────────────────────────────
|
|
86
|
+
exports.MultipleFilesUpload = (0, react_1.forwardRef)(function MultipleFilesUpload({ dataTestId, files = [], accept, maxFiles, maxSize, isDisabled = false, isFillContainer = false, labels, t = identity, onUpload, onRemove, onForbidden, ariaLabel, defaultUploading = false, defaultForbidden, className, ...rest }, ref) {
|
|
87
|
+
const inputId = (0, use_stable_id_1.useStableId)('multi-upload');
|
|
88
|
+
const dir = (0, react_direction_1.useDirection)();
|
|
89
|
+
const [isUploading, setIsUploading] = (0, react_1.useState)(defaultUploading);
|
|
90
|
+
const [forbiddenReason, setForbiddenReason] = (0, react_1.useState)(defaultForbidden ?? null);
|
|
91
|
+
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
92
|
+
const mergedForbidden = { ...DEFAULT_LABELS.forbidden, ...(labels?.forbidden ?? {}) };
|
|
93
|
+
const fbCopy = mergedForbidden[forbiddenReason ?? 'multiple'];
|
|
94
|
+
const acceptAttr = accept?.join(',');
|
|
95
|
+
const hasFiles = files.length > 0;
|
|
96
|
+
const hasErrors = files.some((e) => e.status === 'error');
|
|
97
|
+
const handleDragEnter = (e) => {
|
|
98
|
+
if (isDisabled)
|
|
99
|
+
return;
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const result = validateDragItems(e.dataTransfer.items, { maxFiles, accept, alreadyHeld: files.length });
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
setForbiddenReason(result.reason);
|
|
104
|
+
onForbidden?.(result.reason);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const handleDragLeave = (e) => {
|
|
108
|
+
if (e.currentTarget.contains(e.relatedTarget))
|
|
109
|
+
return;
|
|
110
|
+
setForbiddenReason(null);
|
|
111
|
+
};
|
|
112
|
+
const handleDrop = (e) => {
|
|
113
|
+
if (isDisabled)
|
|
114
|
+
return;
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
setForbiddenReason(null);
|
|
117
|
+
const dropped = Array.from(e.dataTransfer.files);
|
|
118
|
+
if (maxFiles != null && dropped.length + files.length > maxFiles) {
|
|
119
|
+
setForbiddenReason('multiple');
|
|
120
|
+
onForbidden?.('multiple');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const f of dropped) {
|
|
124
|
+
const result = validatePickedFile(f, { accept, maxSize });
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
setForbiddenReason(result.reason);
|
|
127
|
+
onForbidden?.(result.reason);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
triggerUpload(dropped);
|
|
132
|
+
};
|
|
133
|
+
const handleNativeChange = (e) => {
|
|
134
|
+
const selected = Array.from(e.target.files ?? []);
|
|
135
|
+
e.target.value = '';
|
|
136
|
+
if (maxFiles != null && selected.length + files.length > maxFiles) {
|
|
137
|
+
setForbiddenReason('multiple');
|
|
138
|
+
onForbidden?.('multiple');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const f of selected) {
|
|
142
|
+
const result = validatePickedFile(f, { accept, maxSize });
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
setForbiddenReason(result.reason);
|
|
145
|
+
onForbidden?.(result.reason);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
setForbiddenReason(null);
|
|
150
|
+
triggerUpload(selected);
|
|
151
|
+
};
|
|
152
|
+
const triggerUpload = (selected) => {
|
|
153
|
+
if (selected.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
onUpload?.(selected, (v) => setIsUploading(v));
|
|
156
|
+
};
|
|
157
|
+
const handleRemoveClick = (e, file) => {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
e.stopPropagation();
|
|
160
|
+
onRemove?.(file);
|
|
161
|
+
};
|
|
162
|
+
const DocumentIcon = icon_utils_1.iconMap['document'];
|
|
163
|
+
const SpinnerIcon = icon_utils_1.iconMap['loading'];
|
|
164
|
+
const CheckIcon = icon_utils_1.iconMap['check-circled'];
|
|
165
|
+
const ErrorIcon = icon_utils_1.iconMap['clear-circled'];
|
|
166
|
+
const BlockIcon = icon_utils_1.iconMap['block'];
|
|
167
|
+
const TrashIcon = icon_utils_1.iconMap['trash'];
|
|
168
|
+
const renderDropzoneContent = () => {
|
|
169
|
+
if (isUploading) {
|
|
170
|
+
return ((0, jsx_runtime_1.jsx)(SpinnerIcon, { size: "md", role: "status", "aria-label": t(mergedLabels.uploading), className: "animate-spin text-fds-blue-30 rtl:direction-[reverse]", "data-test-id": `${dataTestId}-uploading-indicator` }));
|
|
171
|
+
}
|
|
172
|
+
if (forbiddenReason) {
|
|
173
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(BlockIcon, { size: "md", className: "shrink-0 text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-forbidden-icon` }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", "aria-live": "polite", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-fds-sm font-fds-regular leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-title`, children: t(fbCopy?.title ?? '') }), (0, jsx_runtime_1.jsx)("span", { className: "text-fds-xs font-fds-bold leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-subtitle`, children: t(fbCopy?.subtitle ?? '') })] })] }));
|
|
174
|
+
}
|
|
175
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(DocumentIcon, { size: "md", className: "shrink-0 text-fds-gray-80 rtl:scale-x-[-1] dark:text-fds-gray-20", "aria-hidden": true, "data-test-id": `${dataTestId}-idle-icon` }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-fds-sm font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", "data-test-id": `${dataTestId}-idle-title`, children: t(mergedLabels.idleTitle) }), (0, jsx_runtime_1.jsx)("span", { className: "text-fds-xs font-fds-bold leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", "data-test-id": `${dataTestId}-idle-subtitle`, children: t(mergedLabels.idleSubtitle) })] })] }));
|
|
176
|
+
};
|
|
177
|
+
const renderFileRow = (entry, index) => {
|
|
178
|
+
const isSuccess = entry.status === 'success';
|
|
179
|
+
const RowIcon = isSuccess ? CheckIcon : ErrorIcon;
|
|
180
|
+
const iconColor = isSuccess ? 'text-fds-green-30' : 'text-fds-red-30';
|
|
181
|
+
const textColor = isSuccess ? 'text-fds-blue-30' : 'text-fds-red-30';
|
|
182
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex h-[40px] w-full items-center gap-fds-sm", "data-test-id": `${dataTestId}-file-row-${index}`, children: [(0, jsx_runtime_1.jsx)(RowIcon, { size: "sm", className: (0, utils_1.cn)('shrink-0', iconColor), "aria-hidden": true, "data-test-id": `${dataTestId}-file-status-${index}` }), (0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('min-w-0 flex-1 truncate text-fds-base font-fds-regular leading-fds-body', textColor), title: entry.file.name, "data-test-id": `${dataTestId}-filename-${index}`, children: entry.file.name }), !isDisabled && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => handleRemoveClick(e, entry.file), "aria-label": t(`Remove ${entry.file.name}`), "data-test-id": `${dataTestId}-trash-${index}`, className: "inline-flex h-[40px] w-[63px] shrink-0 items-center justify-center cursor-pointer text-fds-red-30 hover:bg-fds-red-05 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fds-red-30 dark:hover:bg-fds-red-30/10", children: (0, jsx_runtime_1.jsx)(TrashIcon, { size: "sm", "aria-hidden": true }) }))] }, `${entry.file.name}-${index}`));
|
|
183
|
+
};
|
|
184
|
+
const dropzoneBorderClass = forbiddenReason
|
|
185
|
+
? 'upload-dash-fluid-red'
|
|
186
|
+
: isUploading
|
|
187
|
+
? 'border border-solid border-fds-gray-30 dark:border-fds-gray-70'
|
|
188
|
+
: hasErrors && hasFiles
|
|
189
|
+
? 'border border-solid border-fds-red-30 dark:border-fds-red-30'
|
|
190
|
+
: hasFiles
|
|
191
|
+
? 'border border-solid border-fds-blue-30 dark:border-fds-blue-30'
|
|
192
|
+
: 'upload-dash-fluid';
|
|
193
|
+
const dropzoneHeight = isFillContainer ? 'h-[147px]' : 'h-[69px]';
|
|
194
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: ref, dir: dir, "data-test-id": dataTestId, "data-state": isUploading ? 'uploading' : hasErrors ? 'errors' : hasFiles ? 'success' : forbiddenReason ? 'forbidden' : 'default', className: (0, utils_1.cn)('relative flex flex-col rounded-fds-md bg-white p-[24px] gap-[10px] dark:bg-fds-gray-95', isFillContainer ? 'w-full' : 'w-[433px]', isDisabled && 'pointer-events-none opacity-60', className), ...rest, children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: inputId, "aria-label": ariaLabel ?? t(mergedLabels.idleTitle), "aria-disabled": isDisabled || undefined, "aria-busy": isUploading || undefined, className: (0, utils_1.cn)('fw-base relative flex w-full cursor-pointer select-none items-center gap-fds-sm rounded-fds-md p-fds-lg transition-colors', 'bg-fds-gray-10 dark:bg-fds-gray-90', dropzoneHeight, dropzoneBorderClass, isDisabled && 'cursor-not-allowed', isUploading && 'cursor-uploading justify-center', forbiddenReason && 'cursor-not-allowed justify-center'), onDragEnter: handleDragEnter, onDragOver: (e) => e.preventDefault(), onDragLeave: handleDragLeave, onDrop: handleDrop, children: [(0, jsx_runtime_1.jsx)("input", { id: inputId, type: "file", accept: acceptAttr, multiple: maxFiles == null || maxFiles > 1, disabled: isDisabled, className: "sr-only", onChange: handleNativeChange, "data-test-id": `${dataTestId}-input` }), renderDropzoneContent()] }), hasFiles && !isUploading && ((0, jsx_runtime_1.jsx)("div", { className: "flex w-full flex-col", "data-test-id": `${dataTestId}-file-list`, children: files.map((entry, i) => renderFileRow(entry, i)) }))] }));
|
|
195
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
'use client';
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.uploadVariants = exports.Upload = void 0;
|
|
4
|
+
exports.SingleFileUpload = exports.uploadVariants = exports.Upload = void 0;
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const react_1 = require("react");
|
|
7
7
|
const class_variance_authority_1 = require("class-variance-authority");
|
|
@@ -15,6 +15,7 @@ const uploadVariants = (0, class_variance_authority_1.cva)('fw-base group relati
|
|
|
15
15
|
variant: {
|
|
16
16
|
rectangle: 'items-center gap-fds-sm p-fds-lg',
|
|
17
17
|
square: 'h-[99px] w-[100px] flex-col items-center justify-center gap-[10px] px-[26px] py-[23px]',
|
|
18
|
+
'square-lg': 'h-[238px] w-[285px] flex-col items-center justify-center gap-fds-lg p-fds-xl',
|
|
18
19
|
},
|
|
19
20
|
state: {
|
|
20
21
|
default: 'cursor-pointer justify-center bg-fds-gray-10 dark:bg-fds-gray-90',
|
|
@@ -33,6 +34,7 @@ const DEFAULT_LABELS = {
|
|
|
33
34
|
idleTitle: 'Upload your document',
|
|
34
35
|
idleSubtitle: 'Click or drag your file here',
|
|
35
36
|
idleCompact: 'Upload',
|
|
37
|
+
idleDragDrop: 'Drag and drop your file',
|
|
36
38
|
uploading: 'Uploading…',
|
|
37
39
|
forbidden: {
|
|
38
40
|
multiple: { title: 'Oops!', subtitle: 'You can only drag one file', compact: 'Oops!' },
|
|
@@ -157,6 +159,8 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
157
159
|
? 'success'
|
|
158
160
|
: 'default';
|
|
159
161
|
const isSquare = variant === 'square';
|
|
162
|
+
const isSquareLg = variant === 'square-lg';
|
|
163
|
+
const isSquareAny = isSquare || isSquareLg;
|
|
160
164
|
const isMultiple = maxFiles == null || maxFiles > 1;
|
|
161
165
|
const acceptAttr = accept?.join(',');
|
|
162
166
|
const triggerUpload = (selected) => {
|
|
@@ -230,11 +234,14 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
230
234
|
onRemove?.();
|
|
231
235
|
};
|
|
232
236
|
const renderIdle = () => {
|
|
233
|
-
const idleIconName =
|
|
237
|
+
const idleIconName = isSquareAny ? DEFAULT_ICONS.idleSquare : DEFAULT_ICONS.idleRectangle;
|
|
234
238
|
const IconCmp = icon_utils_1.iconMap[idleIconName];
|
|
235
239
|
const tone = effectiveState === 'disabled'
|
|
236
240
|
? 'text-fds-gray-50 dark:text-fds-gray-50'
|
|
237
241
|
: 'text-fds-gray-80 dark:text-fds-gray-20';
|
|
242
|
+
if (isSquareLg) {
|
|
243
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(IconCmp, { className: (0, utils_1.cn)('w-[40px] h-[40px]', tone), "aria-hidden": true, "data-test-id": `${dataTestId}-idle-icon` }), (0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('text-fds-base font-fds-regular leading-fds-body text-center', tone), "data-test-id": `${dataTestId}-idle-compact`, children: t(mergedLabels.idleDragDrop) })] }));
|
|
244
|
+
}
|
|
238
245
|
if (!isSquare) {
|
|
239
246
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(IconCmp, { size: "md", className: (0, utils_1.cn)('shrink-0 rtl:scale-x-[-1]', tone), "aria-hidden": true, "data-test-id": `${dataTestId}-idle-icon` }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", children: [(0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('w-[130px] text-fds-sm font-fds-regular leading-fds-body', tone), "data-test-id": `${dataTestId}-idle-title`, children: t(mergedLabels.idleTitle) }), (0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('w-[132px] text-fds-xs font-fds-bold leading-fds-body', tone), "data-test-id": `${dataTestId}-idle-subtitle`, children: t(mergedLabels.idleSubtitle) })] })] }));
|
|
240
247
|
}
|
|
@@ -242,7 +249,7 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
242
249
|
};
|
|
243
250
|
const renderForbidden = () => {
|
|
244
251
|
const IconCmp = icon_utils_1.iconMap[DEFAULT_ICONS.forbidden];
|
|
245
|
-
if (!
|
|
252
|
+
if (!isSquareAny) {
|
|
246
253
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(IconCmp, { size: "md", className: "shrink-0 text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-forbidden-icon` }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", "aria-live": "polite", children: [(0, jsx_runtime_1.jsx)("span", { className: "w-[130px] text-fds-sm font-fds-regular leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-title`, children: t(fbCopy?.title ?? '') }), (0, jsx_runtime_1.jsx)("span", { className: "w-[132px] text-fds-xs font-fds-bold leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-subtitle`, children: t(fbCopy?.subtitle ?? '') })] })] }));
|
|
247
254
|
}
|
|
248
255
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(IconCmp, { size: "lg", className: "text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-forbidden-icon` }), (0, jsx_runtime_1.jsx)("span", { className: "text-fds-base font-fds-regular leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-compact`, children: t(mergedLabels.idleCompact) })] }));
|
|
@@ -250,9 +257,9 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
250
257
|
const renderUploading = () => {
|
|
251
258
|
const SpinnerIcon = icon_utils_1.iconMap[DEFAULT_ICONS.uploading];
|
|
252
259
|
if (uploadingStyle === 'spinner') {
|
|
253
|
-
return ((0, jsx_runtime_1.jsx)(SpinnerIcon, { size: "lg", role: "status", "aria-label": t(mergedLabels.uploading), className: "animate-spin text-fds-blue-30 rtl:direction-[reverse]", "data-test-id": `${dataTestId}-uploading-indicator` }));
|
|
260
|
+
return isSquareLg ? ((0, jsx_runtime_1.jsx)(SpinnerIcon, { className: "w-10 h-10 animate-spin text-fds-blue-30 rtl:direction-[reverse]", role: "status", "aria-label": t(mergedLabels.uploading), "data-test-id": `${dataTestId}-uploading-indicator` })) : ((0, jsx_runtime_1.jsx)(SpinnerIcon, { size: "lg", role: "status", "aria-label": t(mergedLabels.uploading), className: "animate-spin text-fds-blue-30 rtl:direction-[reverse]", "data-test-id": `${dataTestId}-uploading-indicator` }));
|
|
254
261
|
}
|
|
255
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-fds-sm", "data-test-id": `${dataTestId}-uploading-indicator`, children: [(0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20',
|
|
262
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-fds-sm", "data-test-id": `${dataTestId}-uploading-indicator`, children: [(0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20', isSquareAny ? 'w-full text-fds-base' : 'w-[67px] text-fds-sm'), children: t(mergedLabels.uploading) }), (0, jsx_runtime_1.jsx)("div", { className: (0, utils_1.cn)('relative h-0.5 overflow-hidden rounded-[6px] bg-fds-gray-20 dark:bg-fds-gray-70', isSquareAny ? 'w-full' : 'w-[165px]'), role: "progressbar", "aria-valuenow": uploadProgress !== null ? Math.round(uploadProgress * 100) : undefined, "aria-valuemin": uploadProgress !== null ? 0 : undefined, "aria-valuemax": uploadProgress !== null ? 100 : undefined, "aria-label": t(mergedLabels.uploading), children: (0, jsx_runtime_1.jsx)("div", { className: (0, utils_1.cn)('absolute inset-y-0 start-0 rounded-[6px] bg-fds-green-30 upload-progress-bar', uploadProgress === null
|
|
256
263
|
? 'w-1/2 animate-[uploading-progress_1.4s_ease-in-out_infinite]'
|
|
257
264
|
: 'transition-[width] duration-200 ease-in-out'), style: uploadProgress !== null ? { width: `${uploadProgress * 100}%` } : undefined }) })] }));
|
|
258
265
|
};
|
|
@@ -269,15 +276,19 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
269
276
|
: 'text-fds-gray-30 dark:text-fds-gray-50';
|
|
270
277
|
const StatusIcon = icon_utils_1.iconMap[tone === 'error' ? DEFAULT_ICONS.error : DEFAULT_ICONS.success];
|
|
271
278
|
const RemoveIcon = icon_utils_1.iconMap[DEFAULT_ICONS.remove];
|
|
272
|
-
const { display, tooltip } = buildFileLabel(allFiles,
|
|
273
|
-
|
|
279
|
+
const { display, tooltip } = buildFileLabel(allFiles, isSquareAny);
|
|
280
|
+
// square-lg error: special layout with large icon, truncated filenames + tooltip, and error text block
|
|
281
|
+
if (isSquareLg && tone === 'error') {
|
|
282
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(StatusIcon, { className: "w-10 h-10 shrink-0 text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }), (0, jsx_runtime_1.jsx)("span", { className: "w-full truncate text-center text-fds-xs font-fds-regular leading-fds-body text-fds-blue-30 cursor-default", title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col items-center gap-fds-xs text-center", "aria-live": "polite", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-80 dark:text-fds-gray-20", children: t('Error') }), (0, jsx_runtime_1.jsx)("span", { className: "text-fds-xs font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", children: t('We were unable to read your information') }), (0, jsx_runtime_1.jsx)("span", { className: "pointer-events-auto cursor-pointer! text-fds-xs font-fds-regular leading-fds-body text-fds-blue-30 underline", children: t('Try again by selecting another file') })] })] }));
|
|
283
|
+
}
|
|
284
|
+
const statusIconNode = tone === 'success' ? (isSquareAny
|
|
274
285
|
? (0, jsx_runtime_1.jsx)(StatusIcon, { size: "lg", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
275
|
-
: (0, jsx_runtime_1.jsx)(StatusIcon, { className: (0, utils_1.cn)('shrink-0 w-[13.33px] h-[13.33px]', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'error' ? (
|
|
286
|
+
: (0, jsx_runtime_1.jsx)(StatusIcon, { className: (0, utils_1.cn)('shrink-0 w-[13.33px] h-[13.33px]', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'error' ? (isSquareAny
|
|
276
287
|
? (0, jsx_runtime_1.jsx)(StatusIcon, { size: "lg", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
277
|
-
: (0, jsx_runtime_1.jsx)(StatusIcon, { size: "sm", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'disabled' ? (
|
|
288
|
+
: (0, jsx_runtime_1.jsx)(StatusIcon, { size: "sm", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'disabled' ? (isSquareAny
|
|
278
289
|
? (0, jsx_runtime_1.jsx)(StatusIcon, { size: "lg", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
279
|
-
: (0, jsx_runtime_1.jsx)(StatusIcon, { size: "sm", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : ((0, jsx_runtime_1.jsx)(StatusIcon, { size:
|
|
280
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [statusIconNode, (0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('truncate font-fds-regular leading-fds-body',
|
|
290
|
+
: (0, jsx_runtime_1.jsx)(StatusIcon, { size: "sm", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : ((0, jsx_runtime_1.jsx)(StatusIcon, { size: isSquareAny ? 'md' : 'xs', className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }));
|
|
291
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [statusIconNode, (0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('truncate font-fds-regular leading-fds-body', isSquareAny && (tone === 'error' || tone === 'disabled') ? 'text-fds-base' : 'text-fds-sm', !isSquareAny && !isFillContainer && 'min-w-0 flex-1', !isSquareAny && isFillContainer && 'min-w-0', tokenColor, isSquareAny && 'w-full text-center'), title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), !isSquareAny && hasFiles && tone !== 'disabled' && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleRemoveClick, "aria-label": t('Remove all files'), "data-test-id": `${dataTestId}-remove`, className: (0, utils_1.cn)('inline-flex size-6 shrink-0 items-center justify-center rounded-fds-sm focus-visible:outline-none focus-visible:ring-2 cursor-pointer text-fds-red-30 hover:bg-fds-red-05 focus-visible:ring-fds-red-30 dark:hover:bg-fds-red-30/10', !isFillContainer && 'ms-auto'), children: (0, jsx_runtime_1.jsx)(RemoveIcon, { size: "sm", "aria-hidden": true }) }))] }));
|
|
281
292
|
};
|
|
282
293
|
let body = null;
|
|
283
294
|
switch (effectiveState) {
|
|
@@ -306,16 +317,17 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
306
317
|
effectiveState === 'success' ||
|
|
307
318
|
effectiveState === 'error' ||
|
|
308
319
|
(effectiveState === 'disabled' && hasFiles);
|
|
309
|
-
const frameSize =
|
|
320
|
+
const frameSize = isSquareAny
|
|
310
321
|
? ''
|
|
311
322
|
: isFillContainer
|
|
312
|
-
? isFilled ? 'h-full w-full justify-center' : 'h-full w-full'
|
|
313
|
-
:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
? (isFilled ? 'h-full w-full justify-center' : 'h-full w-full')
|
|
324
|
+
: 'h-[69px] w-[198px]';
|
|
325
|
+
return ((0, jsx_runtime_1.jsxs)("label", { ref: ref, htmlFor: inputId, "data-test-id": dataTestId, "data-state": effectiveState, "data-variant": variant, "data-filled": hasFiles ? 'true' : 'false', dir: dir, "aria-label": ariaLabel ?? t(mergedLabels.idleTitle), "aria-disabled": isDisabled || undefined, "aria-busy": (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') || undefined, className: (0, utils_1.cn)(uploadVariants({ variant, state: effectiveState }), effectiveState === 'default' && (isSquareLg ? 'upload-dash-fluid' :
|
|
326
|
+
isSquare ? 'upload-dash-4-2-square' :
|
|
327
|
+
isFillContainer ? 'upload-dash-fluid' : 'upload-dash-4-2'), effectiveState === 'forbidden' && (isSquareLg ? 'upload-dash-fluid-red' :
|
|
328
|
+
isSquare ? 'h-[100px] gap-[4px] upload-dash-4-4' :
|
|
329
|
+
isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquare && 'h-[100px] p-[11px]!', (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquareLg && 'h-[238px]', effectiveState === 'success' && isSquare && 'h-[100px] gap-[4px]', effectiveState === 'error' && isSquare && 'h-[100px] gap-[4px] p-[11px]!',
|
|
330
|
+
// square-lg error: gray border (Figma spec) + allow retry clicks
|
|
331
|
+
effectiveState === 'error' && isSquareLg && 'border-fds-gray-30! dark:border-fds-gray-70! cursor-pointer!', effectiveState === 'disabled' && isSquare && hasFiles && 'h-[100px] gap-[4px] p-[11px]!', effectiveState === 'uploading-spinner' && !isSquareAny && 'py-3.5!', frameSize, className), onDragEnter: handleDragEnter, onDragOver: (e) => e.preventDefault(), onDragLeave: handleDragLeave, onDrop: handleDrop, ...rest, children: [(0, jsx_runtime_1.jsx)("input", { id: inputId, type: "file", accept: acceptAttr, multiple: isMultiple, disabled: isDisabled, className: "sr-only", onChange: handleNativeChange, "data-test-id": `${dataTestId}-input` }), body] }));
|
|
321
332
|
});
|
|
333
|
+
exports.SingleFileUpload = exports.Upload;
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.uploadVariants = exports.Upload = exports.renderInputIcon = exports.iconMap = exports.useStableId = exports.cn = void 0;
|
|
3
|
+
exports.MultipleFilesUpload = exports.SingleFileUpload = exports.uploadVariants = exports.Upload = exports.renderInputIcon = exports.iconMap = exports.useStableId = exports.cn = void 0;
|
|
4
4
|
// Utilities
|
|
5
5
|
var utils_1 = require("./lib/utils");
|
|
6
6
|
Object.defineProperty(exports, "cn", { enumerable: true, get: function () { return utils_1.cn; } });
|
|
@@ -13,3 +13,6 @@ Object.defineProperty(exports, "renderInputIcon", { enumerable: true, get: funct
|
|
|
13
13
|
var upload_1 = require("./components/upload");
|
|
14
14
|
Object.defineProperty(exports, "Upload", { enumerable: true, get: function () { return upload_1.Upload; } });
|
|
15
15
|
Object.defineProperty(exports, "uploadVariants", { enumerable: true, get: function () { return upload_1.uploadVariants; } });
|
|
16
|
+
Object.defineProperty(exports, "SingleFileUpload", { enumerable: true, get: function () { return upload_1.SingleFileUpload; } });
|
|
17
|
+
var multiple_files_upload_1 = require("./components/multiple-files-upload");
|
|
18
|
+
Object.defineProperty(exports, "MultipleFilesUpload", { enumerable: true, get: function () { return multiple_files_upload_1.MultipleFilesUpload; } });
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useState, } from 'react';
|
|
4
|
+
import { useDirection } from '@radix-ui/react-direction';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
import { iconMap } from '../lib/icon-utils';
|
|
7
|
+
import { useStableId } from '../lib/use-stable-id';
|
|
8
|
+
import { hasValidExtension } from '../lib/file-extensions';
|
|
9
|
+
// ─── Defaults ─────────────────────────────────────────────────────────────────
|
|
10
|
+
const DEFAULT_LABELS = {
|
|
11
|
+
idleTitle: 'Upload your document',
|
|
12
|
+
idleSubtitle: 'Click or drag your file here',
|
|
13
|
+
uploading: 'Uploading…',
|
|
14
|
+
forbidden: {
|
|
15
|
+
multiple: { title: 'Oops!', subtitle: 'Too many files' },
|
|
16
|
+
type: { title: 'Oops!', subtitle: 'Unsupported file format' },
|
|
17
|
+
size: { title: 'Oops!', subtitle: 'File is too large' },
|
|
18
|
+
extension: { title: 'Oops!', subtitle: 'Invalid file extension' },
|
|
19
|
+
custom: { title: 'Oops!', subtitle: 'This file is not allowed' },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const identity = (k) => k;
|
|
23
|
+
// ─── Validation helpers ───────────────────────────────────────────────────────
|
|
24
|
+
const EXT_TO_MIME = {
|
|
25
|
+
'.pdf': ['application/pdf'],
|
|
26
|
+
'.png': ['image/png'],
|
|
27
|
+
'.jpg': ['image/jpeg'],
|
|
28
|
+
'.jpeg': ['image/jpeg'],
|
|
29
|
+
'.gif': ['image/gif'],
|
|
30
|
+
'.webp': ['image/webp'],
|
|
31
|
+
'.svg': ['image/svg+xml'],
|
|
32
|
+
'.xlsx': ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
33
|
+
'.xls': ['application/vnd.ms-excel'],
|
|
34
|
+
'.csv': ['text/csv', 'application/csv', 'text/plain'],
|
|
35
|
+
'.doc': ['application/msword'],
|
|
36
|
+
'.docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|
37
|
+
'.txt': ['text/plain'],
|
|
38
|
+
'.zip': ['application/zip', 'application/x-zip-compressed'],
|
|
39
|
+
};
|
|
40
|
+
function mimeMatchesAccept(mime, accept) {
|
|
41
|
+
return accept.some((rule) => {
|
|
42
|
+
if (rule.endsWith('/*'))
|
|
43
|
+
return mime.startsWith(rule.slice(0, -1));
|
|
44
|
+
if (rule.startsWith('.'))
|
|
45
|
+
return (EXT_TO_MIME[rule.toLowerCase()] ?? []).includes(mime);
|
|
46
|
+
return mime === rule;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function extMatchesAccept(filename, accept) {
|
|
50
|
+
const dot = filename.lastIndexOf('.');
|
|
51
|
+
if (dot < 0)
|
|
52
|
+
return false;
|
|
53
|
+
const ext = filename.slice(dot).toLowerCase();
|
|
54
|
+
return accept.some((rule) => rule.toLowerCase() === ext);
|
|
55
|
+
}
|
|
56
|
+
function fileMatchesAccept(file, accept) {
|
|
57
|
+
return mimeMatchesAccept(file.type, accept) || extMatchesAccept(file.name, accept);
|
|
58
|
+
}
|
|
59
|
+
function validateDragItems(items, { maxFiles, accept, alreadyHeld = 0 }) {
|
|
60
|
+
if (maxFiles != null && items.length + alreadyHeld > maxFiles)
|
|
61
|
+
return { ok: false, reason: 'multiple' };
|
|
62
|
+
if (accept && accept.length > 0) {
|
|
63
|
+
for (let i = 0; i < items.length; i++) {
|
|
64
|
+
const item = items[i];
|
|
65
|
+
if (!item || item.kind !== 'file')
|
|
66
|
+
continue;
|
|
67
|
+
if (!mimeMatchesAccept(item.type, accept))
|
|
68
|
+
return { ok: false, reason: 'type' };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
function validatePickedFile(file, { accept, maxSize }) {
|
|
74
|
+
if (!hasValidExtension(file.name))
|
|
75
|
+
return { ok: false, reason: 'extension' };
|
|
76
|
+
if (accept && accept.length > 0 && !fileMatchesAccept(file, accept))
|
|
77
|
+
return { ok: false, reason: 'type' };
|
|
78
|
+
if (maxSize != null && file.size > maxSize)
|
|
79
|
+
return { ok: false, reason: 'size' };
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
// ─── Component ─────────────────────────────────────────────────────────────────
|
|
83
|
+
export const MultipleFilesUpload = forwardRef(function MultipleFilesUpload({ dataTestId, files = [], accept, maxFiles, maxSize, isDisabled = false, isFillContainer = false, labels, t = identity, onUpload, onRemove, onForbidden, ariaLabel, defaultUploading = false, defaultForbidden, className, ...rest }, ref) {
|
|
84
|
+
const inputId = useStableId('multi-upload');
|
|
85
|
+
const dir = useDirection();
|
|
86
|
+
const [isUploading, setIsUploading] = useState(defaultUploading);
|
|
87
|
+
const [forbiddenReason, setForbiddenReason] = useState(defaultForbidden ?? null);
|
|
88
|
+
const mergedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
89
|
+
const mergedForbidden = { ...DEFAULT_LABELS.forbidden, ...(labels?.forbidden ?? {}) };
|
|
90
|
+
const fbCopy = mergedForbidden[forbiddenReason ?? 'multiple'];
|
|
91
|
+
const acceptAttr = accept?.join(',');
|
|
92
|
+
const hasFiles = files.length > 0;
|
|
93
|
+
const hasErrors = files.some((e) => e.status === 'error');
|
|
94
|
+
const handleDragEnter = (e) => {
|
|
95
|
+
if (isDisabled)
|
|
96
|
+
return;
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
const result = validateDragItems(e.dataTransfer.items, { maxFiles, accept, alreadyHeld: files.length });
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
setForbiddenReason(result.reason);
|
|
101
|
+
onForbidden?.(result.reason);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const handleDragLeave = (e) => {
|
|
105
|
+
if (e.currentTarget.contains(e.relatedTarget))
|
|
106
|
+
return;
|
|
107
|
+
setForbiddenReason(null);
|
|
108
|
+
};
|
|
109
|
+
const handleDrop = (e) => {
|
|
110
|
+
if (isDisabled)
|
|
111
|
+
return;
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
setForbiddenReason(null);
|
|
114
|
+
const dropped = Array.from(e.dataTransfer.files);
|
|
115
|
+
if (maxFiles != null && dropped.length + files.length > maxFiles) {
|
|
116
|
+
setForbiddenReason('multiple');
|
|
117
|
+
onForbidden?.('multiple');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const f of dropped) {
|
|
121
|
+
const result = validatePickedFile(f, { accept, maxSize });
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
setForbiddenReason(result.reason);
|
|
124
|
+
onForbidden?.(result.reason);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
triggerUpload(dropped);
|
|
129
|
+
};
|
|
130
|
+
const handleNativeChange = (e) => {
|
|
131
|
+
const selected = Array.from(e.target.files ?? []);
|
|
132
|
+
e.target.value = '';
|
|
133
|
+
if (maxFiles != null && selected.length + files.length > maxFiles) {
|
|
134
|
+
setForbiddenReason('multiple');
|
|
135
|
+
onForbidden?.('multiple');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const f of selected) {
|
|
139
|
+
const result = validatePickedFile(f, { accept, maxSize });
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
setForbiddenReason(result.reason);
|
|
142
|
+
onForbidden?.(result.reason);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
setForbiddenReason(null);
|
|
147
|
+
triggerUpload(selected);
|
|
148
|
+
};
|
|
149
|
+
const triggerUpload = (selected) => {
|
|
150
|
+
if (selected.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
onUpload?.(selected, (v) => setIsUploading(v));
|
|
153
|
+
};
|
|
154
|
+
const handleRemoveClick = (e, file) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
onRemove?.(file);
|
|
158
|
+
};
|
|
159
|
+
const DocumentIcon = iconMap['document'];
|
|
160
|
+
const SpinnerIcon = iconMap['loading'];
|
|
161
|
+
const CheckIcon = iconMap['check-circled'];
|
|
162
|
+
const ErrorIcon = iconMap['clear-circled'];
|
|
163
|
+
const BlockIcon = iconMap['block'];
|
|
164
|
+
const TrashIcon = iconMap['trash'];
|
|
165
|
+
const renderDropzoneContent = () => {
|
|
166
|
+
if (isUploading) {
|
|
167
|
+
return (_jsx(SpinnerIcon, { size: "md", role: "status", "aria-label": t(mergedLabels.uploading), className: "animate-spin text-fds-blue-30 rtl:direction-[reverse]", "data-test-id": `${dataTestId}-uploading-indicator` }));
|
|
168
|
+
}
|
|
169
|
+
if (forbiddenReason) {
|
|
170
|
+
return (_jsxs(_Fragment, { children: [_jsx(BlockIcon, { size: "md", className: "shrink-0 text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-forbidden-icon` }), _jsxs("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", "aria-live": "polite", children: [_jsx("span", { className: "text-fds-sm font-fds-regular leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-title`, children: t(fbCopy?.title ?? '') }), _jsx("span", { className: "text-fds-xs font-fds-bold leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-subtitle`, children: t(fbCopy?.subtitle ?? '') })] })] }));
|
|
171
|
+
}
|
|
172
|
+
return (_jsxs(_Fragment, { children: [_jsx(DocumentIcon, { size: "md", className: "shrink-0 text-fds-gray-80 rtl:scale-x-[-1] dark:text-fds-gray-20", "aria-hidden": true, "data-test-id": `${dataTestId}-idle-icon` }), _jsxs("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", children: [_jsx("span", { className: "text-fds-sm font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", "data-test-id": `${dataTestId}-idle-title`, children: t(mergedLabels.idleTitle) }), _jsx("span", { className: "text-fds-xs font-fds-bold leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", "data-test-id": `${dataTestId}-idle-subtitle`, children: t(mergedLabels.idleSubtitle) })] })] }));
|
|
173
|
+
};
|
|
174
|
+
const renderFileRow = (entry, index) => {
|
|
175
|
+
const isSuccess = entry.status === 'success';
|
|
176
|
+
const RowIcon = isSuccess ? CheckIcon : ErrorIcon;
|
|
177
|
+
const iconColor = isSuccess ? 'text-fds-green-30' : 'text-fds-red-30';
|
|
178
|
+
const textColor = isSuccess ? 'text-fds-blue-30' : 'text-fds-red-30';
|
|
179
|
+
return (_jsxs("div", { className: "flex h-[40px] w-full items-center gap-fds-sm", "data-test-id": `${dataTestId}-file-row-${index}`, children: [_jsx(RowIcon, { size: "sm", className: cn('shrink-0', iconColor), "aria-hidden": true, "data-test-id": `${dataTestId}-file-status-${index}` }), _jsx("span", { className: cn('min-w-0 flex-1 truncate text-fds-base font-fds-regular leading-fds-body', textColor), title: entry.file.name, "data-test-id": `${dataTestId}-filename-${index}`, children: entry.file.name }), !isDisabled && (_jsx("button", { type: "button", onClick: (e) => handleRemoveClick(e, entry.file), "aria-label": t(`Remove ${entry.file.name}`), "data-test-id": `${dataTestId}-trash-${index}`, className: "inline-flex h-[40px] w-[63px] shrink-0 items-center justify-center cursor-pointer text-fds-red-30 hover:bg-fds-red-05 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fds-red-30 dark:hover:bg-fds-red-30/10", children: _jsx(TrashIcon, { size: "sm", "aria-hidden": true }) }))] }, `${entry.file.name}-${index}`));
|
|
180
|
+
};
|
|
181
|
+
const dropzoneBorderClass = forbiddenReason
|
|
182
|
+
? 'upload-dash-fluid-red'
|
|
183
|
+
: isUploading
|
|
184
|
+
? 'border border-solid border-fds-gray-30 dark:border-fds-gray-70'
|
|
185
|
+
: hasErrors && hasFiles
|
|
186
|
+
? 'border border-solid border-fds-red-30 dark:border-fds-red-30'
|
|
187
|
+
: hasFiles
|
|
188
|
+
? 'border border-solid border-fds-blue-30 dark:border-fds-blue-30'
|
|
189
|
+
: 'upload-dash-fluid';
|
|
190
|
+
const dropzoneHeight = isFillContainer ? 'h-[147px]' : 'h-[69px]';
|
|
191
|
+
return (_jsxs("div", { ref: ref, dir: dir, "data-test-id": dataTestId, "data-state": isUploading ? 'uploading' : hasErrors ? 'errors' : hasFiles ? 'success' : forbiddenReason ? 'forbidden' : 'default', className: cn('relative flex flex-col rounded-fds-md bg-white p-[24px] gap-[10px] dark:bg-fds-gray-95', isFillContainer ? 'w-full' : 'w-[433px]', isDisabled && 'pointer-events-none opacity-60', className), ...rest, children: [_jsxs("label", { htmlFor: inputId, "aria-label": ariaLabel ?? t(mergedLabels.idleTitle), "aria-disabled": isDisabled || undefined, "aria-busy": isUploading || undefined, className: cn('fw-base relative flex w-full cursor-pointer select-none items-center gap-fds-sm rounded-fds-md p-fds-lg transition-colors', 'bg-fds-gray-10 dark:bg-fds-gray-90', dropzoneHeight, dropzoneBorderClass, isDisabled && 'cursor-not-allowed', isUploading && 'cursor-uploading justify-center', forbiddenReason && 'cursor-not-allowed justify-center'), onDragEnter: handleDragEnter, onDragOver: (e) => e.preventDefault(), onDragLeave: handleDragLeave, onDrop: handleDrop, children: [_jsx("input", { id: inputId, type: "file", accept: acceptAttr, multiple: maxFiles == null || maxFiles > 1, disabled: isDisabled, className: "sr-only", onChange: handleNativeChange, "data-test-id": `${dataTestId}-input` }), renderDropzoneContent()] }), hasFiles && !isUploading && (_jsx("div", { className: "flex w-full flex-col", "data-test-id": `${dataTestId}-file-list`, children: files.map((entry, i) => renderFileRow(entry, i)) }))] }));
|
|
192
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useState, } from 'react';
|
|
4
4
|
import { cva } from 'class-variance-authority';
|
|
5
5
|
import { useDirection } from '@radix-ui/react-direction';
|
|
@@ -12,6 +12,7 @@ const uploadVariants = cva('fw-base group relative flex select-none rounded-fds-
|
|
|
12
12
|
variant: {
|
|
13
13
|
rectangle: 'items-center gap-fds-sm p-fds-lg',
|
|
14
14
|
square: 'h-[99px] w-[100px] flex-col items-center justify-center gap-[10px] px-[26px] py-[23px]',
|
|
15
|
+
'square-lg': 'h-[238px] w-[285px] flex-col items-center justify-center gap-fds-lg p-fds-xl',
|
|
15
16
|
},
|
|
16
17
|
state: {
|
|
17
18
|
default: 'cursor-pointer justify-center bg-fds-gray-10 dark:bg-fds-gray-90',
|
|
@@ -29,6 +30,7 @@ const DEFAULT_LABELS = {
|
|
|
29
30
|
idleTitle: 'Upload your document',
|
|
30
31
|
idleSubtitle: 'Click or drag your file here',
|
|
31
32
|
idleCompact: 'Upload',
|
|
33
|
+
idleDragDrop: 'Drag and drop your file',
|
|
32
34
|
uploading: 'Uploading…',
|
|
33
35
|
forbidden: {
|
|
34
36
|
multiple: { title: 'Oops!', subtitle: 'You can only drag one file', compact: 'Oops!' },
|
|
@@ -153,6 +155,8 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
153
155
|
? 'success'
|
|
154
156
|
: 'default';
|
|
155
157
|
const isSquare = variant === 'square';
|
|
158
|
+
const isSquareLg = variant === 'square-lg';
|
|
159
|
+
const isSquareAny = isSquare || isSquareLg;
|
|
156
160
|
const isMultiple = maxFiles == null || maxFiles > 1;
|
|
157
161
|
const acceptAttr = accept?.join(',');
|
|
158
162
|
const triggerUpload = (selected) => {
|
|
@@ -226,11 +230,14 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
226
230
|
onRemove?.();
|
|
227
231
|
};
|
|
228
232
|
const renderIdle = () => {
|
|
229
|
-
const idleIconName =
|
|
233
|
+
const idleIconName = isSquareAny ? DEFAULT_ICONS.idleSquare : DEFAULT_ICONS.idleRectangle;
|
|
230
234
|
const IconCmp = iconMap[idleIconName];
|
|
231
235
|
const tone = effectiveState === 'disabled'
|
|
232
236
|
? 'text-fds-gray-50 dark:text-fds-gray-50'
|
|
233
237
|
: 'text-fds-gray-80 dark:text-fds-gray-20';
|
|
238
|
+
if (isSquareLg) {
|
|
239
|
+
return (_jsxs(_Fragment, { children: [_jsx(IconCmp, { className: cn('w-[40px] h-[40px]', tone), "aria-hidden": true, "data-test-id": `${dataTestId}-idle-icon` }), _jsx("span", { className: cn('text-fds-base font-fds-regular leading-fds-body text-center', tone), "data-test-id": `${dataTestId}-idle-compact`, children: t(mergedLabels.idleDragDrop) })] }));
|
|
240
|
+
}
|
|
234
241
|
if (!isSquare) {
|
|
235
242
|
return (_jsxs(_Fragment, { children: [_jsx(IconCmp, { size: "md", className: cn('shrink-0 rtl:scale-x-[-1]', tone), "aria-hidden": true, "data-test-id": `${dataTestId}-idle-icon` }), _jsxs("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", children: [_jsx("span", { className: cn('w-[130px] text-fds-sm font-fds-regular leading-fds-body', tone), "data-test-id": `${dataTestId}-idle-title`, children: t(mergedLabels.idleTitle) }), _jsx("span", { className: cn('w-[132px] text-fds-xs font-fds-bold leading-fds-body', tone), "data-test-id": `${dataTestId}-idle-subtitle`, children: t(mergedLabels.idleSubtitle) })] })] }));
|
|
236
243
|
}
|
|
@@ -238,7 +245,7 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
238
245
|
};
|
|
239
246
|
const renderForbidden = () => {
|
|
240
247
|
const IconCmp = iconMap[DEFAULT_ICONS.forbidden];
|
|
241
|
-
if (!
|
|
248
|
+
if (!isSquareAny) {
|
|
242
249
|
return (_jsxs(_Fragment, { children: [_jsx(IconCmp, { size: "md", className: "shrink-0 text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-forbidden-icon` }), _jsxs("div", { className: "flex min-w-0 flex-col gap-fds-xs text-start", "aria-live": "polite", children: [_jsx("span", { className: "w-[130px] text-fds-sm font-fds-regular leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-title`, children: t(fbCopy?.title ?? '') }), _jsx("span", { className: "w-[132px] text-fds-xs font-fds-bold leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-subtitle`, children: t(fbCopy?.subtitle ?? '') })] })] }));
|
|
243
250
|
}
|
|
244
251
|
return (_jsxs(_Fragment, { children: [_jsx(IconCmp, { size: "lg", className: "text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-forbidden-icon` }), _jsx("span", { className: "text-fds-base font-fds-regular leading-fds-body text-fds-red-30", "data-test-id": `${dataTestId}-forbidden-compact`, children: t(mergedLabels.idleCompact) })] }));
|
|
@@ -246,9 +253,9 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
246
253
|
const renderUploading = () => {
|
|
247
254
|
const SpinnerIcon = iconMap[DEFAULT_ICONS.uploading];
|
|
248
255
|
if (uploadingStyle === 'spinner') {
|
|
249
|
-
return (_jsx(SpinnerIcon, { size: "lg", role: "status", "aria-label": t(mergedLabels.uploading), className: "animate-spin text-fds-blue-30 rtl:direction-[reverse]", "data-test-id": `${dataTestId}-uploading-indicator` }));
|
|
256
|
+
return isSquareLg ? (_jsx(SpinnerIcon, { className: "w-10 h-10 animate-spin text-fds-blue-30 rtl:direction-[reverse]", role: "status", "aria-label": t(mergedLabels.uploading), "data-test-id": `${dataTestId}-uploading-indicator` })) : (_jsx(SpinnerIcon, { size: "lg", role: "status", "aria-label": t(mergedLabels.uploading), className: "animate-spin text-fds-blue-30 rtl:direction-[reverse]", "data-test-id": `${dataTestId}-uploading-indicator` }));
|
|
250
257
|
}
|
|
251
|
-
return (_jsxs("div", { className: "flex w-full flex-col gap-fds-sm", "data-test-id": `${dataTestId}-uploading-indicator`, children: [_jsx("span", { className: cn('font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20',
|
|
258
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-fds-sm", "data-test-id": `${dataTestId}-uploading-indicator`, children: [_jsx("span", { className: cn('font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20', isSquareAny ? 'w-full text-fds-base' : 'w-[67px] text-fds-sm'), children: t(mergedLabels.uploading) }), _jsx("div", { className: cn('relative h-0.5 overflow-hidden rounded-[6px] bg-fds-gray-20 dark:bg-fds-gray-70', isSquareAny ? 'w-full' : 'w-[165px]'), role: "progressbar", "aria-valuenow": uploadProgress !== null ? Math.round(uploadProgress * 100) : undefined, "aria-valuemin": uploadProgress !== null ? 0 : undefined, "aria-valuemax": uploadProgress !== null ? 100 : undefined, "aria-label": t(mergedLabels.uploading), children: _jsx("div", { className: cn('absolute inset-y-0 start-0 rounded-[6px] bg-fds-green-30 upload-progress-bar', uploadProgress === null
|
|
252
259
|
? 'w-1/2 animate-[uploading-progress_1.4s_ease-in-out_infinite]'
|
|
253
260
|
: 'transition-[width] duration-200 ease-in-out'), style: uploadProgress !== null ? { width: `${uploadProgress * 100}%` } : undefined }) })] }));
|
|
254
261
|
};
|
|
@@ -265,15 +272,19 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
265
272
|
: 'text-fds-gray-30 dark:text-fds-gray-50';
|
|
266
273
|
const StatusIcon = iconMap[tone === 'error' ? DEFAULT_ICONS.error : DEFAULT_ICONS.success];
|
|
267
274
|
const RemoveIcon = iconMap[DEFAULT_ICONS.remove];
|
|
268
|
-
const { display, tooltip } = buildFileLabel(allFiles,
|
|
269
|
-
|
|
275
|
+
const { display, tooltip } = buildFileLabel(allFiles, isSquareAny);
|
|
276
|
+
// square-lg error: special layout with large icon, truncated filenames + tooltip, and error text block
|
|
277
|
+
if (isSquareLg && tone === 'error') {
|
|
278
|
+
return (_jsxs(_Fragment, { children: [_jsx(StatusIcon, { className: "w-10 h-10 shrink-0 text-fds-red-30", "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }), _jsx("span", { className: "w-full truncate text-center text-fds-xs font-fds-regular leading-fds-body text-fds-blue-30 cursor-default", title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), _jsxs("div", { className: "flex w-full flex-col items-center gap-fds-xs text-center", "aria-live": "polite", children: [_jsx("span", { className: "text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-80 dark:text-fds-gray-20", children: t('Error') }), _jsx("span", { className: "text-fds-xs font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", children: t('We were unable to read your information') }), _jsx("span", { className: "pointer-events-auto cursor-pointer! text-fds-xs font-fds-regular leading-fds-body text-fds-blue-30 underline", children: t('Try again by selecting another file') })] })] }));
|
|
279
|
+
}
|
|
280
|
+
const statusIconNode = tone === 'success' ? (isSquareAny
|
|
270
281
|
? _jsx(StatusIcon, { size: "lg", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
271
|
-
: _jsx(StatusIcon, { className: cn('shrink-0 w-[13.33px] h-[13.33px]', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'error' ? (
|
|
282
|
+
: _jsx(StatusIcon, { className: cn('shrink-0 w-[13.33px] h-[13.33px]', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'error' ? (isSquareAny
|
|
272
283
|
? _jsx(StatusIcon, { size: "lg", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
273
|
-
: _jsx(StatusIcon, { size: "sm", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'disabled' ? (
|
|
284
|
+
: _jsx(StatusIcon, { size: "sm", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : tone === 'disabled' ? (isSquareAny
|
|
274
285
|
? _jsx(StatusIcon, { size: "lg", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
275
|
-
: _jsx(StatusIcon, { size: "sm", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : (_jsx(StatusIcon, { size:
|
|
276
|
-
return (_jsxs(_Fragment, { children: [statusIconNode, _jsx("span", { className: cn('truncate font-fds-regular leading-fds-body',
|
|
286
|
+
: _jsx(StatusIcon, { size: "sm", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })) : (_jsx(StatusIcon, { size: isSquareAny ? 'md' : 'xs', className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }));
|
|
287
|
+
return (_jsxs(_Fragment, { children: [statusIconNode, _jsx("span", { className: cn('truncate font-fds-regular leading-fds-body', isSquareAny && (tone === 'error' || tone === 'disabled') ? 'text-fds-base' : 'text-fds-sm', !isSquareAny && !isFillContainer && 'min-w-0 flex-1', !isSquareAny && isFillContainer && 'min-w-0', tokenColor, isSquareAny && 'w-full text-center'), title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), !isSquareAny && hasFiles && tone !== 'disabled' && (_jsx("button", { type: "button", onClick: handleRemoveClick, "aria-label": t('Remove all files'), "data-test-id": `${dataTestId}-remove`, className: cn('inline-flex size-6 shrink-0 items-center justify-center rounded-fds-sm focus-visible:outline-none focus-visible:ring-2 cursor-pointer text-fds-red-30 hover:bg-fds-red-05 focus-visible:ring-fds-red-30 dark:hover:bg-fds-red-30/10', !isFillContainer && 'ms-auto'), children: _jsx(RemoveIcon, { size: "sm", "aria-hidden": true }) }))] }));
|
|
277
288
|
};
|
|
278
289
|
let body = null;
|
|
279
290
|
switch (effectiveState) {
|
|
@@ -302,17 +313,18 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
302
313
|
effectiveState === 'success' ||
|
|
303
314
|
effectiveState === 'error' ||
|
|
304
315
|
(effectiveState === 'disabled' && hasFiles);
|
|
305
|
-
const frameSize =
|
|
316
|
+
const frameSize = isSquareAny
|
|
306
317
|
? ''
|
|
307
318
|
: isFillContainer
|
|
308
|
-
? isFilled ? 'h-full w-full justify-center' : 'h-full w-full'
|
|
309
|
-
:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
319
|
+
? (isFilled ? 'h-full w-full justify-center' : 'h-full w-full')
|
|
320
|
+
: 'h-[69px] w-[198px]';
|
|
321
|
+
return (_jsxs("label", { ref: ref, htmlFor: inputId, "data-test-id": dataTestId, "data-state": effectiveState, "data-variant": variant, "data-filled": hasFiles ? 'true' : 'false', dir: dir, "aria-label": ariaLabel ?? t(mergedLabels.idleTitle), "aria-disabled": isDisabled || undefined, "aria-busy": (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') || undefined, className: cn(uploadVariants({ variant, state: effectiveState }), effectiveState === 'default' && (isSquareLg ? 'upload-dash-fluid' :
|
|
322
|
+
isSquare ? 'upload-dash-4-2-square' :
|
|
323
|
+
isFillContainer ? 'upload-dash-fluid' : 'upload-dash-4-2'), effectiveState === 'forbidden' && (isSquareLg ? 'upload-dash-fluid-red' :
|
|
324
|
+
isSquare ? 'h-[100px] gap-[4px] upload-dash-4-4' :
|
|
325
|
+
isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquare && 'h-[100px] p-[11px]!', (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquareLg && 'h-[238px]', effectiveState === 'success' && isSquare && 'h-[100px] gap-[4px]', effectiveState === 'error' && isSquare && 'h-[100px] gap-[4px] p-[11px]!',
|
|
326
|
+
// square-lg error: gray border (Figma spec) + allow retry clicks
|
|
327
|
+
effectiveState === 'error' && isSquareLg && 'border-fds-gray-30! dark:border-fds-gray-70! cursor-pointer!', effectiveState === 'disabled' && isSquare && hasFiles && 'h-[100px] gap-[4px] p-[11px]!', effectiveState === 'uploading-spinner' && !isSquareAny && 'py-3.5!', frameSize, className), onDragEnter: handleDragEnter, onDragOver: (e) => e.preventDefault(), onDragLeave: handleDragLeave, onDrop: handleDrop, ...rest, children: [_jsx("input", { id: inputId, type: "file", accept: acceptAttr, multiple: isMultiple, disabled: isDisabled, className: "sr-only", onChange: handleNativeChange, "data-test-id": `${dataTestId}-input` }), body] }));
|
|
317
328
|
});
|
|
318
329
|
export { uploadVariants };
|
|
330
|
+
export const SingleFileUpload = Upload;
|
package/dist/esm/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export { cn } from './lib/utils';
|
|
|
3
3
|
export { useStableId } from './lib/use-stable-id';
|
|
4
4
|
export { iconMap, renderInputIcon } from './lib/icon-utils';
|
|
5
5
|
// Components
|
|
6
|
-
export { Upload, uploadVariants } from './components/upload';
|
|
6
|
+
export { Upload, uploadVariants, SingleFileUpload } from './components/upload';
|
|
7
|
+
export { MultipleFilesUpload } from './components/multiple-files-upload';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import type { UploadFile, ForbiddenReason, UploadTranslator } from './upload';
|
|
3
|
+
export type MultipleFileEntryStatus = 'success' | 'error';
|
|
4
|
+
export type MultipleFileEntry = {
|
|
5
|
+
file: UploadFile;
|
|
6
|
+
status: MultipleFileEntryStatus;
|
|
7
|
+
};
|
|
8
|
+
export interface MultipleFilesUploadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
9
|
+
dataTestId: string;
|
|
10
|
+
files?: MultipleFileEntry[];
|
|
11
|
+
accept?: string[];
|
|
12
|
+
maxFiles?: number;
|
|
13
|
+
maxSize?: number;
|
|
14
|
+
isDisabled?: boolean;
|
|
15
|
+
isFillContainer?: boolean;
|
|
16
|
+
labels?: Partial<{
|
|
17
|
+
idleTitle: string;
|
|
18
|
+
idleSubtitle: string;
|
|
19
|
+
uploading: string;
|
|
20
|
+
forbidden: Partial<Record<ForbiddenReason, {
|
|
21
|
+
title: string;
|
|
22
|
+
subtitle: string;
|
|
23
|
+
}>>;
|
|
24
|
+
}>;
|
|
25
|
+
t?: UploadTranslator;
|
|
26
|
+
onUpload?: (files: File[], setIsUploading: (v: boolean) => void) => void;
|
|
27
|
+
onRemove?: (file: UploadFile) => void;
|
|
28
|
+
onForbidden?: (reason: ForbiddenReason) => void;
|
|
29
|
+
ariaLabel?: string;
|
|
30
|
+
defaultUploading?: boolean;
|
|
31
|
+
defaultForbidden?: ForbiddenReason;
|
|
32
|
+
}
|
|
33
|
+
export declare const MultipleFilesUpload: import("react").ForwardRefExoticComponent<MultipleFilesUploadProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type HTMLAttributes } from 'react';
|
|
2
2
|
import { type VariantProps } from 'class-variance-authority';
|
|
3
|
-
export type UploadVariant = 'rectangle' | 'square';
|
|
3
|
+
export type UploadVariant = 'rectangle' | 'square' | 'square-lg';
|
|
4
4
|
export type ForbiddenReason = 'multiple' | 'type' | 'size' | 'extension' | 'custom';
|
|
5
5
|
export type UploadFile = File | {
|
|
6
6
|
name: string;
|
|
7
7
|
};
|
|
8
8
|
export type UploadTranslator = (key: string) => string;
|
|
9
9
|
declare const uploadVariants: (props?: ({
|
|
10
|
-
variant?: "square" | "rectangle" | null | undefined;
|
|
10
|
+
variant?: "square" | "rectangle" | "square-lg" | null | undefined;
|
|
11
11
|
state?: "default" | "disabled" | "forbidden" | "uploading" | "uploading-spinner" | "success" | "error" | null | undefined;
|
|
12
12
|
} & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
|
|
13
13
|
export interface UploadProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onChange'>, VariantProps<typeof uploadVariants> {
|
|
@@ -36,6 +36,7 @@ export interface UploadProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onC
|
|
|
36
36
|
idleTitle: string;
|
|
37
37
|
idleSubtitle: string;
|
|
38
38
|
idleCompact: string;
|
|
39
|
+
idleDragDrop: string;
|
|
39
40
|
uploading: string;
|
|
40
41
|
forbidden: Partial<Record<ForbiddenReason, {
|
|
41
42
|
title: string;
|
|
@@ -64,3 +65,4 @@ export interface UploadProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onC
|
|
|
64
65
|
}
|
|
65
66
|
export declare const Upload: import("react").ForwardRefExoticComponent<UploadProps & import("react").RefAttributes<HTMLLabelElement>>;
|
|
66
67
|
export { uploadVariants };
|
|
68
|
+
export declare const SingleFileUpload: import("react").ForwardRefExoticComponent<UploadProps & import("react").RefAttributes<HTMLLabelElement>>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ export { cn } from './lib/utils';
|
|
|
2
2
|
export { useStableId } from './lib/use-stable-id';
|
|
3
3
|
export { iconMap, renderInputIcon } from './lib/icon-utils';
|
|
4
4
|
export type { IconName, InputShellSize } from './lib/icon-utils';
|
|
5
|
-
export { Upload, uploadVariants } from './components/upload';
|
|
5
|
+
export { Upload, uploadVariants, SingleFileUpload } from './components/upload';
|
|
6
6
|
export type { UploadProps, UploadVariant, UploadFile, ForbiddenReason, UploadTranslator } from './components/upload';
|
|
7
|
+
export { MultipleFilesUpload } from './components/multiple-files-upload';
|
|
8
|
+
export type { MultipleFilesUploadProps, MultipleFileEntry, MultipleFileEntryStatus } from './components/multiple-files-upload';
|
package/package.json
CHANGED