@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.
@@ -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 = isSquare ? DEFAULT_ICONS.idleSquare : DEFAULT_ICONS.idleRectangle;
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 (!isSquare) {
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', isSquare ? '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', isSquare ? '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
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, isSquare);
273
- const statusIconNode = tone === 'success' ? (isSquare
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' ? (isSquare
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' ? (isSquare
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: isSquare ? 'md' : 'xs', className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }));
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', isSquare && (tone === 'error' || tone === 'disabled') ? 'text-fds-base' : 'text-fds-sm', !isSquare && !isFillContainer && 'min-w-0 flex-1', !isSquare && isFillContainer && 'min-w-0', tokenColor, isSquare && 'w-full text-center'), title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), !isSquare && 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 }) }))] }));
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 = isSquare
320
+ const frameSize = isSquareAny
310
321
  ? ''
311
322
  : isFillContainer
312
- ? isFilled ? 'h-full w-full justify-center' : 'h-full w-full'
313
- : effectiveState === 'forbidden'
314
- ? 'h-[69px] w-[193px]'
315
- : isFilled
316
- ? 'h-[60px] w-[198px]'
317
- : 'h-[69px] w-[196px]';
318
- 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' && (isSquare ? 'upload-dash-4-2-square' : isFillContainer ? 'upload-dash-fluid' : 'upload-dash-4-2'), effectiveState === 'forbidden' && (isSquare
319
- ? 'h-[100px] gap-[4px] upload-dash-4-4'
320
- : isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquare && 'h-[100px] p-[11px]!', effectiveState === 'success' && isSquare && 'h-[100px] gap-[4px]', effectiveState === 'error' && isSquare && 'h-[100px] gap-[4px] p-[11px]!', effectiveState === 'disabled' && isSquare && hasFiles && 'h-[100px] gap-[4px] p-[11px]!', effectiveState === 'uploading-spinner' && !isSquare && '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] }));
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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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 = isSquare ? DEFAULT_ICONS.idleSquare : DEFAULT_ICONS.idleRectangle;
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 (!isSquare) {
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', isSquare ? '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', isSquare ? '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
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, isSquare);
269
- const statusIconNode = tone === 'success' ? (isSquare
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' ? (isSquare
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' ? (isSquare
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: isSquare ? 'md' : 'xs', className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }));
276
- return (_jsxs(_Fragment, { children: [statusIconNode, _jsx("span", { className: cn('truncate font-fds-regular leading-fds-body', isSquare && (tone === 'error' || tone === 'disabled') ? 'text-fds-base' : 'text-fds-sm', !isSquare && !isFillContainer && 'min-w-0 flex-1', !isSquare && isFillContainer && 'min-w-0', tokenColor, isSquare && 'w-full text-center'), title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), !isSquare && 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 }) }))] }));
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 = isSquare
316
+ const frameSize = isSquareAny
306
317
  ? ''
307
318
  : isFillContainer
308
- ? isFilled ? 'h-full w-full justify-center' : 'h-full w-full'
309
- : effectiveState === 'forbidden'
310
- ? 'h-[69px] w-[193px]'
311
- : isFilled
312
- ? 'h-[60px] w-[198px]'
313
- : 'h-[69px] w-[196px]';
314
- 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' && (isSquare ? 'upload-dash-4-2-square' : isFillContainer ? 'upload-dash-fluid' : 'upload-dash-4-2'), effectiveState === 'forbidden' && (isSquare
315
- ? 'h-[100px] gap-[4px] upload-dash-4-4'
316
- : isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquare && 'h-[100px] p-[11px]!', effectiveState === 'success' && isSquare && 'h-[100px] gap-[4px]', effectiveState === 'error' && isSquare && 'h-[100px] gap-[4px] p-[11px]!', effectiveState === 'disabled' && isSquare && hasFiles && 'h-[100px] gap-[4px] p-[11px]!', effectiveState === 'uploading-spinner' && !isSquare && '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] }));
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>>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freightos/freightwind",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "private": false,
5
5
  "description": "FreightWind Design System — icons, constants, and utilities for Freightos applications",
6
6
  "main": "./dist/cjs/index.js",