@freightos/freightwind 2.1.6 → 2.1.7
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.
|
@@ -10,12 +10,14 @@ const utils_1 = require("../lib/utils");
|
|
|
10
10
|
const icon_utils_1 = require("../lib/icon-utils");
|
|
11
11
|
const use_stable_id_1 = require("../lib/use-stable-id");
|
|
12
12
|
const file_extensions_1 = require("../lib/file-extensions");
|
|
13
|
+
const SQUARE_SIZE = 'h-[100px] w-[100px]';
|
|
14
|
+
const SQUARE_LG_SIZE = 'h-[238px] w-[285px]';
|
|
13
15
|
const uploadVariants = (0, class_variance_authority_1.cva)('fw-base group relative flex select-none rounded-fds-md outline-none transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-fds-blue-30 has-[:focus-visible]:ring-offset-2', {
|
|
14
16
|
variants: {
|
|
15
17
|
variant: {
|
|
16
|
-
rectangle: 'items-center gap-fds-
|
|
17
|
-
square:
|
|
18
|
-
'square-lg':
|
|
18
|
+
rectangle: 'items-center gap-fds-md p-fds-lg',
|
|
19
|
+
square: `${SQUARE_SIZE} flex-col items-center justify-center gap-[10px] p-fds-xl`,
|
|
20
|
+
'square-lg': `${SQUARE_LG_SIZE} flex-col items-center justify-center gap-fds-lg p-fds-xl`,
|
|
19
21
|
},
|
|
20
22
|
state: {
|
|
21
23
|
default: 'cursor-pointer justify-center bg-fds-gray-10 dark:bg-fds-gray-90',
|
|
@@ -36,6 +38,8 @@ const DEFAULT_LABELS = {
|
|
|
36
38
|
idleCompact: 'Upload',
|
|
37
39
|
idleDragDrop: 'Drag and drop your file',
|
|
38
40
|
uploading: 'Uploading…',
|
|
41
|
+
uploadingTitle: 'Hang on...',
|
|
42
|
+
uploadingSubtitle: 'We’re uploading all your details',
|
|
39
43
|
forbidden: {
|
|
40
44
|
multiple: { title: 'Oops!', subtitle: 'You can only drag one file', compact: 'Oops!' },
|
|
41
45
|
type: { title: 'Oops!', subtitle: 'Unsupported file format', compact: 'Oops!' },
|
|
@@ -123,6 +127,14 @@ function validateDragItems(items, { maxFiles, accept, alreadyHeld = 0 }) {
|
|
|
123
127
|
}
|
|
124
128
|
return { ok: true };
|
|
125
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Run every per-file gate in the order the UI cares about:
|
|
132
|
+
* 1. global extension whitelist (catches `document.mohtadi`),
|
|
133
|
+
* 2. instance-specific `accept` list,
|
|
134
|
+
* 3. `maxSize`.
|
|
135
|
+
*
|
|
136
|
+
* Returned reason maps directly onto a {@link ForbiddenReason} branch.
|
|
137
|
+
*/
|
|
126
138
|
function validatePickedFile(file, { accept, maxSize }) {
|
|
127
139
|
if (!(0, file_extensions_1.hasValidExtension)(file.name))
|
|
128
140
|
return { ok: false, reason: 'extension' };
|
|
@@ -147,7 +159,7 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
147
159
|
const fbCopy = mergedForbidden[forbiddenReason ?? 'multiple'];
|
|
148
160
|
const allFiles = filesProp ?? (value ? [value] : []);
|
|
149
161
|
const hasFiles = allFiles.length > 0;
|
|
150
|
-
const
|
|
162
|
+
const baseEffectiveState = isDisabled
|
|
151
163
|
? 'disabled'
|
|
152
164
|
: forbiddenReason
|
|
153
165
|
? 'forbidden'
|
|
@@ -160,9 +172,22 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
160
172
|
: 'default';
|
|
161
173
|
const isSquare = variant === 'square';
|
|
162
174
|
const isSquareLg = variant === 'square-lg';
|
|
163
|
-
const
|
|
175
|
+
const isRectangle = !isSquare && !isSquareLg;
|
|
164
176
|
const isMultiple = maxFiles == null || maxFiles > 1;
|
|
165
177
|
const acceptAttr = accept?.join(',');
|
|
178
|
+
// Multi-file list mode: rectangle with 2+ files and explicit maxFiles>1 renders the drop zone
|
|
179
|
+
// in its idle state on top, and a per-file list of rows below (each row = check + filename + trash).
|
|
180
|
+
const isMultiFileList = isRectangle &&
|
|
181
|
+
hasFiles &&
|
|
182
|
+
maxFiles != null &&
|
|
183
|
+
maxFiles > 1 &&
|
|
184
|
+
allFiles.length >= 2 &&
|
|
185
|
+
!isError &&
|
|
186
|
+
!isDisabled &&
|
|
187
|
+
!isUploading &&
|
|
188
|
+
!forbiddenReason;
|
|
189
|
+
// In list mode, the drop zone renders idle so users can drop more files; file status lives in rows below.
|
|
190
|
+
const effectiveState = isMultiFileList ? 'default' : baseEffectiveState;
|
|
166
191
|
const triggerUpload = (selected) => {
|
|
167
192
|
if (selected.length === 0)
|
|
168
193
|
return;
|
|
@@ -234,13 +259,13 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
234
259
|
onRemove?.();
|
|
235
260
|
};
|
|
236
261
|
const renderIdle = () => {
|
|
237
|
-
const idleIconName =
|
|
262
|
+
const idleIconName = (isSquare || isSquareLg) ? DEFAULT_ICONS.idleSquare : DEFAULT_ICONS.idleRectangle;
|
|
238
263
|
const IconCmp = icon_utils_1.iconMap[idleIconName];
|
|
239
264
|
const tone = effectiveState === 'disabled'
|
|
240
265
|
? 'text-fds-gray-50 dark:text-fds-gray-50'
|
|
241
266
|
: 'text-fds-gray-80 dark:text-fds-gray-20';
|
|
242
267
|
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-
|
|
268
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(IconCmp, { className: (0, utils_1.cn)('!w-7 !h-7', 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
269
|
}
|
|
245
270
|
if (!isSquare) {
|
|
246
271
|
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) })] })] }));
|
|
@@ -249,17 +274,27 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
249
274
|
};
|
|
250
275
|
const renderForbidden = () => {
|
|
251
276
|
const IconCmp = icon_utils_1.iconMap[DEFAULT_ICONS.forbidden];
|
|
252
|
-
if (!
|
|
277
|
+
if (!isSquare && !isSquareLg) {
|
|
253
278
|
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 ?? '') })] })] }));
|
|
254
279
|
}
|
|
255
280
|
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) })] }));
|
|
256
281
|
};
|
|
257
282
|
const renderUploading = () => {
|
|
258
283
|
const SpinnerIcon = icon_utils_1.iconMap[DEFAULT_ICONS.uploading];
|
|
284
|
+
// Square-lg: 40px spinner + title + subtitle, centered vertically with 16px gap (Figma spec).
|
|
285
|
+
if (isSquareLg) {
|
|
286
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center justify-center gap-fds-lg text-center", "data-test-id": `${dataTestId}-uploading-indicator`, children: [(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) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex w-[190px] flex-col items-center", 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(mergedLabels.uploadingTitle) }), (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(mergedLabels.uploadingSubtitle) })] })] }));
|
|
287
|
+
}
|
|
288
|
+
// Parts F + H: spinner-only — text is NEVER rendered here (Rule 1 / Part H square rule).
|
|
259
289
|
if (uploadingStyle === 'spinner') {
|
|
260
|
-
return
|
|
290
|
+
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` }));
|
|
261
291
|
}
|
|
262
|
-
|
|
292
|
+
// Parts E + G: progress bar.
|
|
293
|
+
// Fill uses absolute+start-0 so it anchors to the logical-start edge — left in LTR,
|
|
294
|
+
// right in RTL — giving correct fill direction in both reading modes.
|
|
295
|
+
// The existing [dir="rtl"] .upload-progress-bar keyframe swap in globals.css covers
|
|
296
|
+
// the indeterminate animation direction automatically.
|
|
297
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, utils_1.cn)('flex flex-col', isSquare ? 'w-[78px] gap-[13px]' : 'w-full 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-[78px] 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-[78px]' : '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
|
|
263
298
|
? 'w-1/2 animate-[uploading-progress_1.4s_ease-in-out_infinite]'
|
|
264
299
|
: 'transition-[width] duration-200 ease-in-out'), style: uploadProgress !== null ? { width: `${uploadProgress * 100}%` } : undefined }) })] }));
|
|
265
300
|
};
|
|
@@ -268,7 +303,7 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
268
303
|
? 'text-fds-blue-30'
|
|
269
304
|
: tone === 'error'
|
|
270
305
|
? 'text-fds-red-30'
|
|
271
|
-
: 'text-fds-gray-
|
|
306
|
+
: 'text-fds-gray-50 dark:text-fds-gray-50';
|
|
272
307
|
const accent = tone === 'success'
|
|
273
308
|
? 'text-fds-green-30'
|
|
274
309
|
: tone === 'error'
|
|
@@ -276,19 +311,35 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
276
311
|
: 'text-fds-gray-30 dark:text-fds-gray-50';
|
|
277
312
|
const StatusIcon = icon_utils_1.iconMap[tone === 'error' ? DEFAULT_ICONS.error : DEFAULT_ICONS.success];
|
|
278
313
|
const RemoveIcon = icon_utils_1.iconMap[DEFAULT_ICONS.remove];
|
|
279
|
-
const { display, tooltip } = buildFileLabel(allFiles,
|
|
280
|
-
// square-lg
|
|
314
|
+
const { display, tooltip } = buildFileLabel(allFiles, isSquare || isSquareLg);
|
|
315
|
+
// square-lg success (Figma Frame 6): 237×123 column, 8px gap between four children —
|
|
316
|
+
// 40px green check-circled icon (Semantic Green 30 #47A96E)
|
|
317
|
+
// file names: 184×18, 12px/18px Regular, Primary Blue 30 (#2075BD), centered
|
|
318
|
+
// "Hang on..." title: H6 16px/24px Bold (Gray 80)
|
|
319
|
+
// "We're uploading all your details" subtitle: 12px/18px Regular (Gray 80)
|
|
320
|
+
if (isSquareLg && tone === 'success') {
|
|
321
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-[237px] flex-col items-center gap-fds-sm text-center", children: [(0, jsx_runtime_1.jsx)(StatusIcon, { className: "w-10 h-10 shrink-0 text-fds-green-30", "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }), (0, jsx_runtime_1.jsx)("span", { className: "w-[184px] truncate 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.jsx)("span", { className: "w-[190px] text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-80 dark:text-fds-gray-20", children: t(mergedLabels.uploadingTitle) }), (0, jsx_runtime_1.jsx)("span", { className: "w-[190px] text-fds-xs font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", children: t(mergedLabels.uploadingSubtitle) })] }));
|
|
322
|
+
}
|
|
323
|
+
// square-lg disabled (filled): mirrors success layout but muted —
|
|
324
|
+
// 40px gray check-circled icon, gray filenames, gray "Hang on..." title + subtitle.
|
|
325
|
+
// Solid gray border + flat gray background come from the cva 'disabled' state.
|
|
326
|
+
if (isSquareLg && tone === 'disabled') {
|
|
327
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-[237px] flex-col items-center gap-fds-sm text-center", children: [(0, jsx_runtime_1.jsx)(StatusIcon, { className: "w-10 h-10 shrink-0 text-fds-gray-50 dark:text-fds-gray-50", "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }), (0, jsx_runtime_1.jsx)("span", { className: "w-[184px] truncate text-fds-xs font-fds-regular leading-fds-body text-fds-gray-50 cursor-default dark:text-fds-gray-50", title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), (0, jsx_runtime_1.jsx)("span", { className: "w-[190px] text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-50 dark:text-fds-gray-50", children: t(mergedLabels.uploadingTitle) }), (0, jsx_runtime_1.jsx)("span", { className: "w-[190px] text-fds-xs font-fds-regular leading-fds-body text-fds-gray-50 dark:text-fds-gray-50", children: t(mergedLabels.uploadingSubtitle) })] }));
|
|
328
|
+
}
|
|
329
|
+
// square-lg error: large icon, truncated filenames with native tooltip, error text block
|
|
281
330
|
if (isSquareLg && tone === 'error') {
|
|
282
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(StatusIcon, {
|
|
331
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(StatusIcon, { size: "lg", className: "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
332
|
}
|
|
284
|
-
const
|
|
333
|
+
const isSquareAny = isSquare || isSquareLg;
|
|
334
|
+
const statusIconNode = isSquareAny
|
|
285
335
|
? (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` })
|
|
286
|
-
: (0, jsx_runtime_1.jsx)(StatusIcon, { className: (0, utils_1.cn)('shrink-0
|
|
287
|
-
|
|
288
|
-
: (0, jsx_runtime_1.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
336
|
+
: (0, jsx_runtime_1.jsx)(StatusIcon, { size: "xs", className: (0, utils_1.cn)('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` });
|
|
337
|
+
if (isSquareAny) {
|
|
338
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [statusIconNode, (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-fds-xs", children: [(0, jsx_runtime_1.jsx)("span", { className: (0, utils_1.cn)('min-w-0 truncate font-fds-regular leading-fds-body text-fds-sm', tokenColor), title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), tone !== 'disabled' && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleRemoveClick, "aria-label": t('Remove all files'), "data-test-id": `${dataTestId}-remove`, className: "inline-flex size-4 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", children: (0, jsx_runtime_1.jsx)(RemoveIcon, { size: "sm", "aria-hidden": true }) }))] })] }));
|
|
339
|
+
}
|
|
340
|
+
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',
|
|
341
|
+
// Square error/disabled: 14px/21px LH per Parts L/N. Square success/others: 12px/18px (text-fds-sm).
|
|
342
|
+
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)("span", { "aria-hidden": true, "data-test-id": `${dataTestId}-remove-disabled`, className: (0, utils_1.cn)('inline-flex size-4 shrink-0 items-center justify-center text-fds-gray-50 dark:text-fds-gray-50', !isFillContainer && 'ms-auto'), children: (0, jsx_runtime_1.jsx)(RemoveIcon, { size: "sm", "aria-hidden": true }) })), !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-4 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 }) }))] }));
|
|
292
343
|
};
|
|
293
344
|
let body = null;
|
|
294
345
|
switch (effectiveState) {
|
|
@@ -317,17 +368,36 @@ exports.Upload = (0, react_1.forwardRef)(function Upload({ dataTestId, variant =
|
|
|
317
368
|
effectiveState === 'success' ||
|
|
318
369
|
effectiveState === 'error' ||
|
|
319
370
|
(effectiveState === 'disabled' && hasFiles);
|
|
320
|
-
|
|
371
|
+
// All rectangle states share the same unified 198×69px frame (Figma spec, 2026-05-06).
|
|
372
|
+
const frameSize = (isSquare || isSquareLg)
|
|
321
373
|
? ''
|
|
322
374
|
: isFillContainer
|
|
323
375
|
? (isFilled ? 'h-full w-full justify-center' : 'h-full w-full')
|
|
324
376
|
: 'h-[69px] w-[198px]';
|
|
325
|
-
|
|
377
|
+
const labelNode = ((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 }),
|
|
378
|
+
// Default state: fluid gradient border for fill-container (scales with element),
|
|
379
|
+
// fixed SVG border otherwise (pixel-exact per Figma spec).
|
|
380
|
+
// Square: pixel-perfect 100×99 SVG — identical stroke on all four sides, no corner overlap.
|
|
381
|
+
// Rectangle: fixed SVG at spec dimensions, fluid gradient when stretching to fill container.
|
|
382
|
+
effectiveState === 'default' && (isSquareLg ? 'upload-dash-4-2-square-lg' :
|
|
326
383
|
isSquare ? 'upload-dash-4-2-square' :
|
|
327
384
|
isFillContainer ? 'upload-dash-fluid' : 'upload-dash-4-2'), effectiveState === 'forbidden' && (isSquareLg ? 'upload-dash-fluid-red' :
|
|
328
|
-
isSquare ? '
|
|
329
|
-
isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') &&
|
|
385
|
+
isSquare ? 'upload-dash-4-4' :
|
|
386
|
+
isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquareLg && 'border-0 upload-dash-4-2-square-lg',
|
|
387
|
+
// square-lg success: dashed gray border per Figma (overrides solid blue from cva)
|
|
388
|
+
effectiveState === 'success' && isSquareLg && 'border-0 upload-dash-4-2-square-lg',
|
|
330
389
|
// square-lg error: gray border (Figma spec) + allow retry clicks
|
|
331
|
-
effectiveState === 'error' && isSquareLg && 'border-
|
|
390
|
+
effectiveState === 'error' && isSquareLg && 'border-0 upload-dash-4-2-square-lg cursor-pointer!', 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] }));
|
|
391
|
+
if (!isMultiFileList) {
|
|
392
|
+
return labelNode;
|
|
393
|
+
}
|
|
394
|
+
const RowCheckIcon = icon_utils_1.iconMap[DEFAULT_ICONS.success];
|
|
395
|
+
const RowTrashIcon = icon_utils_1.iconMap[DEFAULT_ICONS.remove];
|
|
396
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, utils_1.cn)('flex flex-col gap-fds-sm', isFillContainer ? 'w-full' : 'w-[198px]'), "data-test-id": `${dataTestId}-list`, dir: dir, children: [labelNode, (0, jsx_runtime_1.jsx)("ul", { className: "flex flex-col", "data-test-id": `${dataTestId}-files`, children: allFiles.map((f, idx) => ((0, jsx_runtime_1.jsxs)("li", { className: "flex items-center gap-fds-sm border-b border-fds-gray-20 py-fds-sm last:border-b-0 dark:border-fds-gray-70", "data-test-id": `${dataTestId}-file-${idx}`, children: [(0, jsx_runtime_1.jsx)(RowCheckIcon, { size: "sm", className: "shrink-0 text-fds-green-30", "aria-hidden": true }), (0, jsx_runtime_1.jsx)("span", { className: "min-w-0 flex-1 truncate text-fds-sm font-fds-regular leading-fds-body text-fds-blue-30", title: f.name, "data-test-id": `${dataTestId}-file-${idx}-name`, children: f.name }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: (e) => {
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
e.stopPropagation();
|
|
399
|
+
onRemove?.(idx);
|
|
400
|
+
}, "aria-label": t('Remove file'), "data-test-id": `${dataTestId}-file-${idx}-remove`, className: "inline-flex size-4 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", children: (0, jsx_runtime_1.jsx)(RowTrashIcon, { size: "sm", "aria-hidden": true }) })] }, `${f.name}-${idx}`))) })] }));
|
|
332
401
|
});
|
|
402
|
+
/** Alias for `Upload` enforcing single-file semantics. Use instead of `Upload` for all new code. */
|
|
333
403
|
exports.SingleFileUpload = exports.Upload;
|
|
@@ -7,12 +7,14 @@ import { cn } from '../lib/utils';
|
|
|
7
7
|
import { iconMap } from '../lib/icon-utils';
|
|
8
8
|
import { useStableId } from '../lib/use-stable-id';
|
|
9
9
|
import { hasValidExtension } from '../lib/file-extensions';
|
|
10
|
+
const SQUARE_SIZE = 'h-[100px] w-[100px]';
|
|
11
|
+
const SQUARE_LG_SIZE = 'h-[238px] w-[285px]';
|
|
10
12
|
const uploadVariants = cva('fw-base group relative flex select-none rounded-fds-md outline-none transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-fds-blue-30 has-[:focus-visible]:ring-offset-2', {
|
|
11
13
|
variants: {
|
|
12
14
|
variant: {
|
|
13
|
-
rectangle: 'items-center gap-fds-
|
|
14
|
-
square:
|
|
15
|
-
'square-lg':
|
|
15
|
+
rectangle: 'items-center gap-fds-md p-fds-lg',
|
|
16
|
+
square: `${SQUARE_SIZE} flex-col items-center justify-center gap-[10px] p-fds-xl`,
|
|
17
|
+
'square-lg': `${SQUARE_LG_SIZE} flex-col items-center justify-center gap-fds-lg p-fds-xl`,
|
|
16
18
|
},
|
|
17
19
|
state: {
|
|
18
20
|
default: 'cursor-pointer justify-center bg-fds-gray-10 dark:bg-fds-gray-90',
|
|
@@ -32,6 +34,8 @@ const DEFAULT_LABELS = {
|
|
|
32
34
|
idleCompact: 'Upload',
|
|
33
35
|
idleDragDrop: 'Drag and drop your file',
|
|
34
36
|
uploading: 'Uploading…',
|
|
37
|
+
uploadingTitle: 'Hang on...',
|
|
38
|
+
uploadingSubtitle: 'We’re uploading all your details',
|
|
35
39
|
forbidden: {
|
|
36
40
|
multiple: { title: 'Oops!', subtitle: 'You can only drag one file', compact: 'Oops!' },
|
|
37
41
|
type: { title: 'Oops!', subtitle: 'Unsupported file format', compact: 'Oops!' },
|
|
@@ -119,6 +123,14 @@ function validateDragItems(items, { maxFiles, accept, alreadyHeld = 0 }) {
|
|
|
119
123
|
}
|
|
120
124
|
return { ok: true };
|
|
121
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Run every per-file gate in the order the UI cares about:
|
|
128
|
+
* 1. global extension whitelist (catches `document.mohtadi`),
|
|
129
|
+
* 2. instance-specific `accept` list,
|
|
130
|
+
* 3. `maxSize`.
|
|
131
|
+
*
|
|
132
|
+
* Returned reason maps directly onto a {@link ForbiddenReason} branch.
|
|
133
|
+
*/
|
|
122
134
|
function validatePickedFile(file, { accept, maxSize }) {
|
|
123
135
|
if (!hasValidExtension(file.name))
|
|
124
136
|
return { ok: false, reason: 'extension' };
|
|
@@ -143,7 +155,7 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
143
155
|
const fbCopy = mergedForbidden[forbiddenReason ?? 'multiple'];
|
|
144
156
|
const allFiles = filesProp ?? (value ? [value] : []);
|
|
145
157
|
const hasFiles = allFiles.length > 0;
|
|
146
|
-
const
|
|
158
|
+
const baseEffectiveState = isDisabled
|
|
147
159
|
? 'disabled'
|
|
148
160
|
: forbiddenReason
|
|
149
161
|
? 'forbidden'
|
|
@@ -156,9 +168,22 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
156
168
|
: 'default';
|
|
157
169
|
const isSquare = variant === 'square';
|
|
158
170
|
const isSquareLg = variant === 'square-lg';
|
|
159
|
-
const
|
|
171
|
+
const isRectangle = !isSquare && !isSquareLg;
|
|
160
172
|
const isMultiple = maxFiles == null || maxFiles > 1;
|
|
161
173
|
const acceptAttr = accept?.join(',');
|
|
174
|
+
// Multi-file list mode: rectangle with 2+ files and explicit maxFiles>1 renders the drop zone
|
|
175
|
+
// in its idle state on top, and a per-file list of rows below (each row = check + filename + trash).
|
|
176
|
+
const isMultiFileList = isRectangle &&
|
|
177
|
+
hasFiles &&
|
|
178
|
+
maxFiles != null &&
|
|
179
|
+
maxFiles > 1 &&
|
|
180
|
+
allFiles.length >= 2 &&
|
|
181
|
+
!isError &&
|
|
182
|
+
!isDisabled &&
|
|
183
|
+
!isUploading &&
|
|
184
|
+
!forbiddenReason;
|
|
185
|
+
// In list mode, the drop zone renders idle so users can drop more files; file status lives in rows below.
|
|
186
|
+
const effectiveState = isMultiFileList ? 'default' : baseEffectiveState;
|
|
162
187
|
const triggerUpload = (selected) => {
|
|
163
188
|
if (selected.length === 0)
|
|
164
189
|
return;
|
|
@@ -230,13 +255,13 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
230
255
|
onRemove?.();
|
|
231
256
|
};
|
|
232
257
|
const renderIdle = () => {
|
|
233
|
-
const idleIconName =
|
|
258
|
+
const idleIconName = (isSquare || isSquareLg) ? DEFAULT_ICONS.idleSquare : DEFAULT_ICONS.idleRectangle;
|
|
234
259
|
const IconCmp = iconMap[idleIconName];
|
|
235
260
|
const tone = effectiveState === 'disabled'
|
|
236
261
|
? 'text-fds-gray-50 dark:text-fds-gray-50'
|
|
237
262
|
: 'text-fds-gray-80 dark:text-fds-gray-20';
|
|
238
263
|
if (isSquareLg) {
|
|
239
|
-
return (_jsxs(_Fragment, { children: [_jsx(IconCmp, { className: cn('w-
|
|
264
|
+
return (_jsxs(_Fragment, { children: [_jsx(IconCmp, { className: cn('!w-7 !h-7', 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
265
|
}
|
|
241
266
|
if (!isSquare) {
|
|
242
267
|
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) })] })] }));
|
|
@@ -245,17 +270,27 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
245
270
|
};
|
|
246
271
|
const renderForbidden = () => {
|
|
247
272
|
const IconCmp = iconMap[DEFAULT_ICONS.forbidden];
|
|
248
|
-
if (!
|
|
273
|
+
if (!isSquare && !isSquareLg) {
|
|
249
274
|
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 ?? '') })] })] }));
|
|
250
275
|
}
|
|
251
276
|
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) })] }));
|
|
252
277
|
};
|
|
253
278
|
const renderUploading = () => {
|
|
254
279
|
const SpinnerIcon = iconMap[DEFAULT_ICONS.uploading];
|
|
280
|
+
// Square-lg: 40px spinner + title + subtitle, centered vertically with 16px gap (Figma spec).
|
|
281
|
+
if (isSquareLg) {
|
|
282
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center gap-fds-lg text-center", "data-test-id": `${dataTestId}-uploading-indicator`, children: [_jsx(SpinnerIcon, { className: "w-10 h-10 animate-spin text-fds-blue-30 rtl:direction-[reverse]", role: "status", "aria-label": t(mergedLabels.uploading) }), _jsxs("div", { className: "flex w-[190px] flex-col items-center", children: [_jsx("span", { className: "text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-80 dark:text-fds-gray-20", children: t(mergedLabels.uploadingTitle) }), _jsx("span", { className: "text-fds-xs font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", children: t(mergedLabels.uploadingSubtitle) })] })] }));
|
|
283
|
+
}
|
|
284
|
+
// Parts F + H: spinner-only — text is NEVER rendered here (Rule 1 / Part H square rule).
|
|
255
285
|
if (uploadingStyle === 'spinner') {
|
|
256
|
-
return
|
|
286
|
+
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` }));
|
|
257
287
|
}
|
|
258
|
-
|
|
288
|
+
// Parts E + G: progress bar.
|
|
289
|
+
// Fill uses absolute+start-0 so it anchors to the logical-start edge — left in LTR,
|
|
290
|
+
// right in RTL — giving correct fill direction in both reading modes.
|
|
291
|
+
// The existing [dir="rtl"] .upload-progress-bar keyframe swap in globals.css covers
|
|
292
|
+
// the indeterminate animation direction automatically.
|
|
293
|
+
return (_jsxs("div", { className: cn('flex flex-col', isSquare ? 'w-[78px] gap-[13px]' : 'w-full 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-[78px] 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-[78px]' : '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
|
|
259
294
|
? 'w-1/2 animate-[uploading-progress_1.4s_ease-in-out_infinite]'
|
|
260
295
|
: 'transition-[width] duration-200 ease-in-out'), style: uploadProgress !== null ? { width: `${uploadProgress * 100}%` } : undefined }) })] }));
|
|
261
296
|
};
|
|
@@ -264,7 +299,7 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
264
299
|
? 'text-fds-blue-30'
|
|
265
300
|
: tone === 'error'
|
|
266
301
|
? 'text-fds-red-30'
|
|
267
|
-
: 'text-fds-gray-
|
|
302
|
+
: 'text-fds-gray-50 dark:text-fds-gray-50';
|
|
268
303
|
const accent = tone === 'success'
|
|
269
304
|
? 'text-fds-green-30'
|
|
270
305
|
: tone === 'error'
|
|
@@ -272,19 +307,35 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
272
307
|
: 'text-fds-gray-30 dark:text-fds-gray-50';
|
|
273
308
|
const StatusIcon = iconMap[tone === 'error' ? DEFAULT_ICONS.error : DEFAULT_ICONS.success];
|
|
274
309
|
const RemoveIcon = iconMap[DEFAULT_ICONS.remove];
|
|
275
|
-
const { display, tooltip } = buildFileLabel(allFiles,
|
|
276
|
-
// square-lg
|
|
310
|
+
const { display, tooltip } = buildFileLabel(allFiles, isSquare || isSquareLg);
|
|
311
|
+
// square-lg success (Figma Frame 6): 237×123 column, 8px gap between four children —
|
|
312
|
+
// 40px green check-circled icon (Semantic Green 30 #47A96E)
|
|
313
|
+
// file names: 184×18, 12px/18px Regular, Primary Blue 30 (#2075BD), centered
|
|
314
|
+
// "Hang on..." title: H6 16px/24px Bold (Gray 80)
|
|
315
|
+
// "We're uploading all your details" subtitle: 12px/18px Regular (Gray 80)
|
|
316
|
+
if (isSquareLg && tone === 'success') {
|
|
317
|
+
return (_jsxs("div", { className: "flex w-[237px] flex-col items-center gap-fds-sm text-center", children: [_jsx(StatusIcon, { className: "w-10 h-10 shrink-0 text-fds-green-30", "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }), _jsx("span", { className: "w-[184px] truncate text-fds-xs font-fds-regular leading-fds-body text-fds-blue-30 cursor-default", title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), _jsx("span", { className: "w-[190px] text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-80 dark:text-fds-gray-20", children: t(mergedLabels.uploadingTitle) }), _jsx("span", { className: "w-[190px] text-fds-xs font-fds-regular leading-fds-body text-fds-gray-80 dark:text-fds-gray-20", children: t(mergedLabels.uploadingSubtitle) })] }));
|
|
318
|
+
}
|
|
319
|
+
// square-lg disabled (filled): mirrors success layout but muted —
|
|
320
|
+
// 40px gray check-circled icon, gray filenames, gray "Hang on..." title + subtitle.
|
|
321
|
+
// Solid gray border + flat gray background come from the cva 'disabled' state.
|
|
322
|
+
if (isSquareLg && tone === 'disabled') {
|
|
323
|
+
return (_jsxs("div", { className: "flex w-[237px] flex-col items-center gap-fds-sm text-center", children: [_jsx(StatusIcon, { className: "w-10 h-10 shrink-0 text-fds-gray-50 dark:text-fds-gray-50", "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` }), _jsx("span", { className: "w-[184px] truncate text-fds-xs font-fds-regular leading-fds-body text-fds-gray-50 cursor-default dark:text-fds-gray-50", title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), _jsx("span", { className: "w-[190px] text-fds-h6 font-fds-bold leading-fds-title text-fds-gray-50 dark:text-fds-gray-50", children: t(mergedLabels.uploadingTitle) }), _jsx("span", { className: "w-[190px] text-fds-xs font-fds-regular leading-fds-body text-fds-gray-50 dark:text-fds-gray-50", children: t(mergedLabels.uploadingSubtitle) })] }));
|
|
324
|
+
}
|
|
325
|
+
// square-lg error: large icon, truncated filenames with native tooltip, error text block
|
|
277
326
|
if (isSquareLg && tone === 'error') {
|
|
278
|
-
return (_jsxs(_Fragment, { children: [_jsx(StatusIcon, {
|
|
327
|
+
return (_jsxs(_Fragment, { children: [_jsx(StatusIcon, { size: "lg", className: "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
328
|
}
|
|
280
|
-
const
|
|
329
|
+
const isSquareAny = isSquare || isSquareLg;
|
|
330
|
+
const statusIconNode = isSquareAny
|
|
281
331
|
? _jsx(StatusIcon, { size: "lg", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` })
|
|
282
|
-
: _jsx(StatusIcon, { className: cn('shrink-0
|
|
283
|
-
|
|
284
|
-
:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
332
|
+
: _jsx(StatusIcon, { size: "xs", className: cn('shrink-0', accent), "aria-hidden": true, "data-test-id": `${dataTestId}-status-icon` });
|
|
333
|
+
if (isSquareAny) {
|
|
334
|
+
return (_jsxs(_Fragment, { children: [statusIconNode, _jsxs("div", { className: "flex items-center gap-fds-xs", children: [_jsx("span", { className: cn('min-w-0 truncate font-fds-regular leading-fds-body text-fds-sm', tokenColor), title: tooltip, "data-test-id": `${dataTestId}-filename`, children: display }), tone !== 'disabled' && (_jsx("button", { type: "button", onClick: handleRemoveClick, "aria-label": t('Remove all files'), "data-test-id": `${dataTestId}-remove`, className: "inline-flex size-4 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", children: _jsx(RemoveIcon, { size: "sm", "aria-hidden": true }) }))] })] }));
|
|
335
|
+
}
|
|
336
|
+
return (_jsxs(_Fragment, { children: [statusIconNode, _jsx("span", { className: cn('truncate font-fds-regular leading-fds-body',
|
|
337
|
+
// Square error/disabled: 14px/21px LH per Parts L/N. Square success/others: 12px/18px (text-fds-sm).
|
|
338
|
+
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("span", { "aria-hidden": true, "data-test-id": `${dataTestId}-remove-disabled`, className: cn('inline-flex size-4 shrink-0 items-center justify-center text-fds-gray-50 dark:text-fds-gray-50', !isFillContainer && 'ms-auto'), children: _jsx(RemoveIcon, { size: "sm", "aria-hidden": true }) })), !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-4 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 }) }))] }));
|
|
288
339
|
};
|
|
289
340
|
let body = null;
|
|
290
341
|
switch (effectiveState) {
|
|
@@ -313,18 +364,37 @@ export const Upload = forwardRef(function Upload({ dataTestId, variant = 'rectan
|
|
|
313
364
|
effectiveState === 'success' ||
|
|
314
365
|
effectiveState === 'error' ||
|
|
315
366
|
(effectiveState === 'disabled' && hasFiles);
|
|
316
|
-
|
|
367
|
+
// All rectangle states share the same unified 198×69px frame (Figma spec, 2026-05-06).
|
|
368
|
+
const frameSize = (isSquare || isSquareLg)
|
|
317
369
|
? ''
|
|
318
370
|
: isFillContainer
|
|
319
371
|
? (isFilled ? 'h-full w-full justify-center' : 'h-full w-full')
|
|
320
372
|
: 'h-[69px] w-[198px]';
|
|
321
|
-
|
|
373
|
+
const labelNode = (_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 }),
|
|
374
|
+
// Default state: fluid gradient border for fill-container (scales with element),
|
|
375
|
+
// fixed SVG border otherwise (pixel-exact per Figma spec).
|
|
376
|
+
// Square: pixel-perfect 100×99 SVG — identical stroke on all four sides, no corner overlap.
|
|
377
|
+
// Rectangle: fixed SVG at spec dimensions, fluid gradient when stretching to fill container.
|
|
378
|
+
effectiveState === 'default' && (isSquareLg ? 'upload-dash-4-2-square-lg' :
|
|
322
379
|
isSquare ? 'upload-dash-4-2-square' :
|
|
323
380
|
isFillContainer ? 'upload-dash-fluid' : 'upload-dash-4-2'), effectiveState === 'forbidden' && (isSquareLg ? 'upload-dash-fluid-red' :
|
|
324
|
-
isSquare ? '
|
|
325
|
-
isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') &&
|
|
381
|
+
isSquare ? 'upload-dash-4-4' :
|
|
382
|
+
isFillContainer ? 'upload-dash-fluid-red' : 'upload-dash-4-2-red'), (effectiveState === 'uploading' || effectiveState === 'uploading-spinner') && isSquareLg && 'border-0 upload-dash-4-2-square-lg',
|
|
383
|
+
// square-lg success: dashed gray border per Figma (overrides solid blue from cva)
|
|
384
|
+
effectiveState === 'success' && isSquareLg && 'border-0 upload-dash-4-2-square-lg',
|
|
326
385
|
// square-lg error: gray border (Figma spec) + allow retry clicks
|
|
327
|
-
effectiveState === 'error' && isSquareLg && 'border-
|
|
386
|
+
effectiveState === 'error' && isSquareLg && 'border-0 upload-dash-4-2-square-lg cursor-pointer!', 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] }));
|
|
387
|
+
if (!isMultiFileList) {
|
|
388
|
+
return labelNode;
|
|
389
|
+
}
|
|
390
|
+
const RowCheckIcon = iconMap[DEFAULT_ICONS.success];
|
|
391
|
+
const RowTrashIcon = iconMap[DEFAULT_ICONS.remove];
|
|
392
|
+
return (_jsxs("div", { className: cn('flex flex-col gap-fds-sm', isFillContainer ? 'w-full' : 'w-[198px]'), "data-test-id": `${dataTestId}-list`, dir: dir, children: [labelNode, _jsx("ul", { className: "flex flex-col", "data-test-id": `${dataTestId}-files`, children: allFiles.map((f, idx) => (_jsxs("li", { className: "flex items-center gap-fds-sm border-b border-fds-gray-20 py-fds-sm last:border-b-0 dark:border-fds-gray-70", "data-test-id": `${dataTestId}-file-${idx}`, children: [_jsx(RowCheckIcon, { size: "sm", className: "shrink-0 text-fds-green-30", "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate text-fds-sm font-fds-regular leading-fds-body text-fds-blue-30", title: f.name, "data-test-id": `${dataTestId}-file-${idx}-name`, children: f.name }), _jsx("button", { type: "button", onClick: (e) => {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
e.stopPropagation();
|
|
395
|
+
onRemove?.(idx);
|
|
396
|
+
}, "aria-label": t('Remove file'), "data-test-id": `${dataTestId}-file-${idx}-remove`, className: "inline-flex size-4 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", children: _jsx(RowTrashIcon, { size: "sm", "aria-hidden": true }) })] }, `${f.name}-${idx}`))) })] }));
|
|
328
397
|
});
|
|
329
398
|
export { uploadVariants };
|
|
399
|
+
/** Alias for `Upload` enforcing single-file semantics. Use instead of `Upload` for all new code. */
|
|
330
400
|
export const SingleFileUpload = Upload;
|
|
@@ -38,6 +38,8 @@ export interface UploadProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onC
|
|
|
38
38
|
idleCompact: string;
|
|
39
39
|
idleDragDrop: string;
|
|
40
40
|
uploading: string;
|
|
41
|
+
uploadingTitle: string;
|
|
42
|
+
uploadingSubtitle: string;
|
|
41
43
|
forbidden: Partial<Record<ForbiddenReason, {
|
|
42
44
|
title: string;
|
|
43
45
|
subtitle: string;
|
|
@@ -48,8 +50,8 @@ export interface UploadProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onC
|
|
|
48
50
|
t?: UploadTranslator;
|
|
49
51
|
/** Fires when one or more files are selected or dropped. The component flips to `uploading` after this fires; call `setIsUploading(false)` when your async upload completes. Pass an optional `progress` value (0–1) to drive the determinate progress bar; omit it for the indeterminate animation. */
|
|
50
52
|
onUpload?: (files: File[], setIsUploading: (v: boolean, progress?: number) => void) => void;
|
|
51
|
-
/** Fires when
|
|
52
|
-
onRemove?: () => void;
|
|
53
|
+
/** Fires when a trash button is clicked. Called with no argument from the single-slot trash (rectangle/square) — parent should clear all files. Called with the row index from the multi-file list view — parent should remove that one file. */
|
|
54
|
+
onRemove?: (index?: number) => void;
|
|
53
55
|
/** Fires when an invalid drag/drop is detected. */
|
|
54
56
|
onForbidden?: (reason: ForbiddenReason) => void;
|
|
55
57
|
/** A11y label override for the surface. */
|
|
@@ -65,4 +67,5 @@ export interface UploadProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onC
|
|
|
65
67
|
}
|
|
66
68
|
export declare const Upload: import("react").ForwardRefExoticComponent<UploadProps & import("react").RefAttributes<HTMLLabelElement>>;
|
|
67
69
|
export { uploadVariants };
|
|
70
|
+
/** Alias for `Upload` enforcing single-file semantics. Use instead of `Upload` for all new code. */
|
|
68
71
|
export declare const SingleFileUpload: import("react").ForwardRefExoticComponent<UploadProps & import("react").RefAttributes<HTMLLabelElement>>;
|
package/package.json
CHANGED