@designa8/vibedesign-stripe-deepseek-2-176b 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +51 -20
- package/dist/index.js +51 -20
- package/dist/styles.css +0 -5
- package/package.json +1 -1
- package/src/atoms/FileUpload/FileUpload.tsx +104 -37
- package/src/atoms/FileUpload/preview.tsx +62 -112
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @designa8/vibedesign-stripe-deepseek-2-176b
|
|
2
2
|
|
|
3
|
-
Complete design system generated by Atoms Design, version **0.2.
|
|
3
|
+
Complete design system generated by Atoms Design, version **0.2.4**.
|
|
4
4
|
Tokens, atoms, molecules, organisms, and templates — import from the package root;
|
|
5
5
|
never hardcode visual values in consumer code.
|
|
6
6
|
|
package/dist/index.cjs
CHANGED
|
@@ -7274,15 +7274,30 @@ var React35 = __toESM(require("react"));
|
|
|
7274
7274
|
var import_jsx_runtime35 = require("react/jsx-runtime");
|
|
7275
7275
|
var cn29 = (...values) => values.filter(Boolean).join(" ");
|
|
7276
7276
|
var tabular = { fontVariantNumeric: "tabular-nums" };
|
|
7277
|
+
var clampPct = (value) => Math.min(100, Math.max(0, value));
|
|
7278
|
+
var formatSize = (size) => {
|
|
7279
|
+
if (size === void 0 || size === null) return void 0;
|
|
7280
|
+
if (typeof size === "string") return size.trim() || void 0;
|
|
7281
|
+
if (!Number.isFinite(size) || size < 0) return void 0;
|
|
7282
|
+
if (size < 1024) return `${Math.round(size)} B`;
|
|
7283
|
+
const units = ["KB", "MB", "GB"];
|
|
7284
|
+
let value = size / 1024;
|
|
7285
|
+
let unit = 0;
|
|
7286
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7287
|
+
value /= 1024;
|
|
7288
|
+
unit += 1;
|
|
7289
|
+
}
|
|
7290
|
+
return `${value >= 10 ? Math.round(value) : Math.round(value * 10) / 10} ${units[unit]}`;
|
|
7291
|
+
};
|
|
7277
7292
|
var FileUpload = React35.forwardRef(
|
|
7278
7293
|
({
|
|
7279
7294
|
status = "idle",
|
|
7280
7295
|
title = "Upload payouts file",
|
|
7281
7296
|
hint = "CSV, up to 10 MB",
|
|
7282
7297
|
accept = ".csv",
|
|
7283
|
-
fileName
|
|
7284
|
-
fileSize
|
|
7285
|
-
progress
|
|
7298
|
+
fileName,
|
|
7299
|
+
fileSize,
|
|
7300
|
+
progress,
|
|
7286
7301
|
errorLabel = "Failed",
|
|
7287
7302
|
actionLabel = "Upload CSV",
|
|
7288
7303
|
onFileSelect,
|
|
@@ -7292,17 +7307,24 @@ var FileUpload = React35.forwardRef(
|
|
|
7292
7307
|
}, ref) => {
|
|
7293
7308
|
const inputRef = React35.useRef(null);
|
|
7294
7309
|
const [dragActive, setDragActive] = React35.useState(false);
|
|
7295
|
-
const
|
|
7296
|
-
const
|
|
7297
|
-
const
|
|
7310
|
+
const name = typeof fileName === "string" ? fileName.trim() : "";
|
|
7311
|
+
const hasFile = name.length > 0;
|
|
7312
|
+
const sizeLabel = formatSize(fileSize);
|
|
7313
|
+
const resolved = !hasFile && status === "idle" && dragActive ? "dragging" : status;
|
|
7314
|
+
const dragging = !hasFile && resolved === "dragging";
|
|
7315
|
+
const failed = resolved === "error";
|
|
7316
|
+
const errorOnDropzone = failed && !hasFile;
|
|
7317
|
+
const pct = progress === void 0 ? resolved === "complete" ? 100 : 0 : clampPct(progress);
|
|
7318
|
+
const barPct = failed ? progress === void 0 ? 18 : Math.min(pct, 18) : pct;
|
|
7298
7319
|
const openPicker = () => inputRef.current?.click();
|
|
7299
|
-
const fileInput = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
7320
|
+
const fileInput = /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "relative", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
7300
7321
|
Input,
|
|
7301
7322
|
{
|
|
7302
7323
|
ref: inputRef,
|
|
7303
7324
|
type: "file",
|
|
7304
7325
|
accept,
|
|
7305
7326
|
ariaLabel: title,
|
|
7327
|
+
"aria-invalid": failed || void 0,
|
|
7306
7328
|
chrome: "none",
|
|
7307
7329
|
className: "sr-only",
|
|
7308
7330
|
onChange: (event) => {
|
|
@@ -7310,8 +7332,8 @@ var FileUpload = React35.forwardRef(
|
|
|
7310
7332
|
if (file) onFileSelect?.(file);
|
|
7311
7333
|
}
|
|
7312
7334
|
}
|
|
7313
|
-
);
|
|
7314
|
-
if (
|
|
7335
|
+
) });
|
|
7336
|
+
if (!hasFile) {
|
|
7315
7337
|
return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
|
|
7316
7338
|
"div",
|
|
7317
7339
|
{
|
|
@@ -7331,7 +7353,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7331
7353
|
className: cn29(
|
|
7332
7354
|
"flex w-full flex-col items-center gap-md rounded-lg border border-dashed px-xl py-xl text-center",
|
|
7333
7355
|
"transition duration-200 ease-out",
|
|
7334
|
-
dragging ? "border-primary bg-primary-subdued/70" : "border-hairline-strong bg-canvas",
|
|
7356
|
+
dragging ? "border-primary bg-primary-subdued/70" : errorOnDropzone ? "border-semantic-error bg-canvas" : "border-hairline-strong bg-canvas",
|
|
7335
7357
|
className
|
|
7336
7358
|
),
|
|
7337
7359
|
...rest,
|
|
@@ -7351,7 +7373,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7351
7373
|
strokeWidth: 1.25,
|
|
7352
7374
|
className: cn29(
|
|
7353
7375
|
"transition-colors duration-200 ease-out",
|
|
7354
|
-
dragging ? "text-primary" : "text-ink"
|
|
7376
|
+
dragging ? "text-primary" : errorOnDropzone ? "text-semantic-error" : "text-ink"
|
|
7355
7377
|
)
|
|
7356
7378
|
}
|
|
7357
7379
|
),
|
|
@@ -7363,7 +7385,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7363
7385
|
strokeWidth: 2,
|
|
7364
7386
|
className: cn29(
|
|
7365
7387
|
"absolute left-1/2 top-[60%] -translate-x-1/2 -translate-y-1/2 transition-colors duration-200 ease-out",
|
|
7366
|
-
dragging ? "text-primary" : "text-ink"
|
|
7388
|
+
dragging ? "text-primary" : errorOnDropzone ? "text-semantic-error" : "text-ink"
|
|
7367
7389
|
)
|
|
7368
7390
|
}
|
|
7369
7391
|
)
|
|
@@ -7383,12 +7405,12 @@ var FileUpload = React35.forwardRef(
|
|
|
7383
7405
|
),
|
|
7384
7406
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "text-caption text-muted", children: hint })
|
|
7385
7407
|
] }),
|
|
7386
|
-
dragging ? null : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Button, { variant: "secondary", size: "sm", onClick: openPicker, children: actionLabel })
|
|
7408
|
+
dragging ? null : /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Button, { variant: "secondary", size: "sm", onClick: openPicker, children: actionLabel }),
|
|
7409
|
+
errorOnDropzone ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { "aria-live": "polite", className: "text-caption text-semantic-error", children: errorLabel }) : null
|
|
7387
7410
|
]
|
|
7388
7411
|
}
|
|
7389
7412
|
);
|
|
7390
7413
|
}
|
|
7391
|
-
const failed = resolved === "error";
|
|
7392
7414
|
return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
|
|
7393
7415
|
"div",
|
|
7394
7416
|
{
|
|
@@ -7401,18 +7423,27 @@ var FileUpload = React35.forwardRef(
|
|
|
7401
7423
|
),
|
|
7402
7424
|
...rest,
|
|
7403
7425
|
children: [
|
|
7426
|
+
fileInput,
|
|
7404
7427
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-start gap-md", children: [
|
|
7405
|
-
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
7428
|
+
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
7429
|
+
Icon,
|
|
7430
|
+
{
|
|
7431
|
+
name: "file",
|
|
7432
|
+
size: 20,
|
|
7433
|
+
strokeWidth: 1.5,
|
|
7434
|
+
className: "mt-xxs text-ink"
|
|
7435
|
+
}
|
|
7436
|
+
),
|
|
7406
7437
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
7407
|
-
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "truncate text-body-md text-ink", children:
|
|
7408
|
-
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-xxs text-caption text-muted", children:
|
|
7438
|
+
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "truncate text-body-md text-ink", children: name }),
|
|
7439
|
+
sizeLabel ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-xxs text-caption text-muted", children: sizeLabel }) : null
|
|
7409
7440
|
] }),
|
|
7410
7441
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
|
|
7411
7442
|
Button,
|
|
7412
7443
|
{
|
|
7413
7444
|
variant: "bare",
|
|
7414
7445
|
size: "inline",
|
|
7415
|
-
"aria-label": `Remove ${
|
|
7446
|
+
"aria-label": `Remove ${name}`,
|
|
7416
7447
|
onClick: onRemove,
|
|
7417
7448
|
className: "-mr-xs -mt-xs shrink-0 rounded-sm p-xs text-muted hover:text-ink",
|
|
7418
7449
|
children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(Icon, { name: "x", size: 16, strokeWidth: 1.75 })
|
|
@@ -7424,7 +7455,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7424
7455
|
"div",
|
|
7425
7456
|
{
|
|
7426
7457
|
role: "progressbar",
|
|
7427
|
-
"aria-label": `${
|
|
7458
|
+
"aria-label": `${name} upload progress`,
|
|
7428
7459
|
"aria-valuemin": 0,
|
|
7429
7460
|
"aria-valuemax": 100,
|
|
7430
7461
|
"aria-valuenow": failed ? void 0 : pct,
|
|
@@ -7438,7 +7469,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7438
7469
|
"block h-full rounded-full transition-[width,background-color] duration-200 ease-out",
|
|
7439
7470
|
failed ? "bg-semantic-error" : "bg-primary"
|
|
7440
7471
|
),
|
|
7441
|
-
style: { width: `${
|
|
7472
|
+
style: { width: `${barPct}%` }
|
|
7442
7473
|
}
|
|
7443
7474
|
)
|
|
7444
7475
|
}
|
package/dist/index.js
CHANGED
|
@@ -7144,15 +7144,30 @@ import * as React35 from "react";
|
|
|
7144
7144
|
import { jsx as jsx35, jsxs as jsxs32 } from "react/jsx-runtime";
|
|
7145
7145
|
var cn29 = (...values) => values.filter(Boolean).join(" ");
|
|
7146
7146
|
var tabular = { fontVariantNumeric: "tabular-nums" };
|
|
7147
|
+
var clampPct = (value) => Math.min(100, Math.max(0, value));
|
|
7148
|
+
var formatSize = (size) => {
|
|
7149
|
+
if (size === void 0 || size === null) return void 0;
|
|
7150
|
+
if (typeof size === "string") return size.trim() || void 0;
|
|
7151
|
+
if (!Number.isFinite(size) || size < 0) return void 0;
|
|
7152
|
+
if (size < 1024) return `${Math.round(size)} B`;
|
|
7153
|
+
const units = ["KB", "MB", "GB"];
|
|
7154
|
+
let value = size / 1024;
|
|
7155
|
+
let unit = 0;
|
|
7156
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7157
|
+
value /= 1024;
|
|
7158
|
+
unit += 1;
|
|
7159
|
+
}
|
|
7160
|
+
return `${value >= 10 ? Math.round(value) : Math.round(value * 10) / 10} ${units[unit]}`;
|
|
7161
|
+
};
|
|
7147
7162
|
var FileUpload = React35.forwardRef(
|
|
7148
7163
|
({
|
|
7149
7164
|
status = "idle",
|
|
7150
7165
|
title = "Upload payouts file",
|
|
7151
7166
|
hint = "CSV, up to 10 MB",
|
|
7152
7167
|
accept = ".csv",
|
|
7153
|
-
fileName
|
|
7154
|
-
fileSize
|
|
7155
|
-
progress
|
|
7168
|
+
fileName,
|
|
7169
|
+
fileSize,
|
|
7170
|
+
progress,
|
|
7156
7171
|
errorLabel = "Failed",
|
|
7157
7172
|
actionLabel = "Upload CSV",
|
|
7158
7173
|
onFileSelect,
|
|
@@ -7162,17 +7177,24 @@ var FileUpload = React35.forwardRef(
|
|
|
7162
7177
|
}, ref) => {
|
|
7163
7178
|
const inputRef = React35.useRef(null);
|
|
7164
7179
|
const [dragActive, setDragActive] = React35.useState(false);
|
|
7165
|
-
const
|
|
7166
|
-
const
|
|
7167
|
-
const
|
|
7180
|
+
const name = typeof fileName === "string" ? fileName.trim() : "";
|
|
7181
|
+
const hasFile = name.length > 0;
|
|
7182
|
+
const sizeLabel = formatSize(fileSize);
|
|
7183
|
+
const resolved = !hasFile && status === "idle" && dragActive ? "dragging" : status;
|
|
7184
|
+
const dragging = !hasFile && resolved === "dragging";
|
|
7185
|
+
const failed = resolved === "error";
|
|
7186
|
+
const errorOnDropzone = failed && !hasFile;
|
|
7187
|
+
const pct = progress === void 0 ? resolved === "complete" ? 100 : 0 : clampPct(progress);
|
|
7188
|
+
const barPct = failed ? progress === void 0 ? 18 : Math.min(pct, 18) : pct;
|
|
7168
7189
|
const openPicker = () => inputRef.current?.click();
|
|
7169
|
-
const fileInput = /* @__PURE__ */ jsx35(
|
|
7190
|
+
const fileInput = /* @__PURE__ */ jsx35("div", { className: "relative", children: /* @__PURE__ */ jsx35(
|
|
7170
7191
|
Input,
|
|
7171
7192
|
{
|
|
7172
7193
|
ref: inputRef,
|
|
7173
7194
|
type: "file",
|
|
7174
7195
|
accept,
|
|
7175
7196
|
ariaLabel: title,
|
|
7197
|
+
"aria-invalid": failed || void 0,
|
|
7176
7198
|
chrome: "none",
|
|
7177
7199
|
className: "sr-only",
|
|
7178
7200
|
onChange: (event) => {
|
|
@@ -7180,8 +7202,8 @@ var FileUpload = React35.forwardRef(
|
|
|
7180
7202
|
if (file) onFileSelect?.(file);
|
|
7181
7203
|
}
|
|
7182
7204
|
}
|
|
7183
|
-
);
|
|
7184
|
-
if (
|
|
7205
|
+
) });
|
|
7206
|
+
if (!hasFile) {
|
|
7185
7207
|
return /* @__PURE__ */ jsxs32(
|
|
7186
7208
|
"div",
|
|
7187
7209
|
{
|
|
@@ -7201,7 +7223,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7201
7223
|
className: cn29(
|
|
7202
7224
|
"flex w-full flex-col items-center gap-md rounded-lg border border-dashed px-xl py-xl text-center",
|
|
7203
7225
|
"transition duration-200 ease-out",
|
|
7204
|
-
dragging ? "border-primary bg-primary-subdued/70" : "border-hairline-strong bg-canvas",
|
|
7226
|
+
dragging ? "border-primary bg-primary-subdued/70" : errorOnDropzone ? "border-semantic-error bg-canvas" : "border-hairline-strong bg-canvas",
|
|
7205
7227
|
className
|
|
7206
7228
|
),
|
|
7207
7229
|
...rest,
|
|
@@ -7221,7 +7243,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7221
7243
|
strokeWidth: 1.25,
|
|
7222
7244
|
className: cn29(
|
|
7223
7245
|
"transition-colors duration-200 ease-out",
|
|
7224
|
-
dragging ? "text-primary" : "text-ink"
|
|
7246
|
+
dragging ? "text-primary" : errorOnDropzone ? "text-semantic-error" : "text-ink"
|
|
7225
7247
|
)
|
|
7226
7248
|
}
|
|
7227
7249
|
),
|
|
@@ -7233,7 +7255,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7233
7255
|
strokeWidth: 2,
|
|
7234
7256
|
className: cn29(
|
|
7235
7257
|
"absolute left-1/2 top-[60%] -translate-x-1/2 -translate-y-1/2 transition-colors duration-200 ease-out",
|
|
7236
|
-
dragging ? "text-primary" : "text-ink"
|
|
7258
|
+
dragging ? "text-primary" : errorOnDropzone ? "text-semantic-error" : "text-ink"
|
|
7237
7259
|
)
|
|
7238
7260
|
}
|
|
7239
7261
|
)
|
|
@@ -7253,12 +7275,12 @@ var FileUpload = React35.forwardRef(
|
|
|
7253
7275
|
),
|
|
7254
7276
|
/* @__PURE__ */ jsx35("span", { className: "text-caption text-muted", children: hint })
|
|
7255
7277
|
] }),
|
|
7256
|
-
dragging ? null : /* @__PURE__ */ jsx35(Button, { variant: "secondary", size: "sm", onClick: openPicker, children: actionLabel })
|
|
7278
|
+
dragging ? null : /* @__PURE__ */ jsx35(Button, { variant: "secondary", size: "sm", onClick: openPicker, children: actionLabel }),
|
|
7279
|
+
errorOnDropzone ? /* @__PURE__ */ jsx35("p", { "aria-live": "polite", className: "text-caption text-semantic-error", children: errorLabel }) : null
|
|
7257
7280
|
]
|
|
7258
7281
|
}
|
|
7259
7282
|
);
|
|
7260
7283
|
}
|
|
7261
|
-
const failed = resolved === "error";
|
|
7262
7284
|
return /* @__PURE__ */ jsxs32(
|
|
7263
7285
|
"div",
|
|
7264
7286
|
{
|
|
@@ -7271,18 +7293,27 @@ var FileUpload = React35.forwardRef(
|
|
|
7271
7293
|
),
|
|
7272
7294
|
...rest,
|
|
7273
7295
|
children: [
|
|
7296
|
+
fileInput,
|
|
7274
7297
|
/* @__PURE__ */ jsxs32("div", { className: "flex items-start gap-md", children: [
|
|
7275
|
-
/* @__PURE__ */ jsx35(
|
|
7298
|
+
/* @__PURE__ */ jsx35(
|
|
7299
|
+
Icon,
|
|
7300
|
+
{
|
|
7301
|
+
name: "file",
|
|
7302
|
+
size: 20,
|
|
7303
|
+
strokeWidth: 1.5,
|
|
7304
|
+
className: "mt-xxs text-ink"
|
|
7305
|
+
}
|
|
7306
|
+
),
|
|
7276
7307
|
/* @__PURE__ */ jsxs32("div", { className: "min-w-0 flex-1", children: [
|
|
7277
|
-
/* @__PURE__ */ jsx35("p", { className: "truncate text-body-md text-ink", children:
|
|
7278
|
-
/* @__PURE__ */ jsx35("p", { className: "mt-xxs text-caption text-muted", children:
|
|
7308
|
+
/* @__PURE__ */ jsx35("p", { className: "truncate text-body-md text-ink", children: name }),
|
|
7309
|
+
sizeLabel ? /* @__PURE__ */ jsx35("p", { className: "mt-xxs text-caption text-muted", children: sizeLabel }) : null
|
|
7279
7310
|
] }),
|
|
7280
7311
|
/* @__PURE__ */ jsx35(
|
|
7281
7312
|
Button,
|
|
7282
7313
|
{
|
|
7283
7314
|
variant: "bare",
|
|
7284
7315
|
size: "inline",
|
|
7285
|
-
"aria-label": `Remove ${
|
|
7316
|
+
"aria-label": `Remove ${name}`,
|
|
7286
7317
|
onClick: onRemove,
|
|
7287
7318
|
className: "-mr-xs -mt-xs shrink-0 rounded-sm p-xs text-muted hover:text-ink",
|
|
7288
7319
|
children: /* @__PURE__ */ jsx35(Icon, { name: "x", size: 16, strokeWidth: 1.75 })
|
|
@@ -7294,7 +7325,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7294
7325
|
"div",
|
|
7295
7326
|
{
|
|
7296
7327
|
role: "progressbar",
|
|
7297
|
-
"aria-label": `${
|
|
7328
|
+
"aria-label": `${name} upload progress`,
|
|
7298
7329
|
"aria-valuemin": 0,
|
|
7299
7330
|
"aria-valuemax": 100,
|
|
7300
7331
|
"aria-valuenow": failed ? void 0 : pct,
|
|
@@ -7308,7 +7339,7 @@ var FileUpload = React35.forwardRef(
|
|
|
7308
7339
|
"block h-full rounded-full transition-[width,background-color] duration-200 ease-out",
|
|
7309
7340
|
failed ? "bg-semantic-error" : "bg-primary"
|
|
7310
7341
|
),
|
|
7311
|
-
style: { width: `${
|
|
7342
|
+
style: { width: `${barPct}%` }
|
|
7312
7343
|
}
|
|
7313
7344
|
)
|
|
7314
7345
|
}
|
package/dist/styles.css
CHANGED
|
@@ -3701,11 +3701,6 @@
|
|
|
3701
3701
|
display: inline-flex;
|
|
3702
3702
|
}
|
|
3703
3703
|
}
|
|
3704
|
-
.xl\:grid-cols-3 {
|
|
3705
|
-
@media (width >= 1280px) {
|
|
3706
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
3707
|
-
}
|
|
3708
|
-
}
|
|
3709
3704
|
.dark\:opacity-40 {
|
|
3710
3705
|
&:where([data-theme="dark"], [data-theme="dark"] *) {
|
|
3711
3706
|
opacity: 40%;
|
package/package.json
CHANGED
|
@@ -8,8 +8,10 @@ const cn = (...values: Array<string | false | null | undefined>) =>
|
|
|
8
8
|
values.filter(Boolean).join(" ");
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* `idle` and `dragging` share the dashed drop region; `uploading
|
|
12
|
-
*
|
|
11
|
+
* `idle` and `dragging` share the dashed drop region; `uploading` and
|
|
12
|
+
* `complete` share the single-file card with its progress readout. `error`
|
|
13
|
+
* renders the card when a file is tracked, otherwise the drop region carrying
|
|
14
|
+
* the error tint and message so the user can still pick a file.
|
|
13
15
|
*/
|
|
14
16
|
export type FileUploadStatus =
|
|
15
17
|
| "idle"
|
|
@@ -28,13 +30,16 @@ export interface FileUploadProps
|
|
|
28
30
|
hint?: string;
|
|
29
31
|
/** Comma separated accept list handed to the file input, e.g. `.csv`. */
|
|
30
32
|
accept?: string;
|
|
31
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Name of the file being tracked. When omitted (or empty) no file row is
|
|
35
|
+
* rendered in any status — the drop region is shown instead.
|
|
36
|
+
*/
|
|
32
37
|
fileName?: string;
|
|
33
|
-
/**
|
|
34
|
-
fileSize?: string;
|
|
35
|
-
/** Upload completion, 0–100. */
|
|
38
|
+
/** Size of that file, either a readable label or a raw byte count. */
|
|
39
|
+
fileSize?: string | number;
|
|
40
|
+
/** Upload completion, 0–100. Defaults to full when `status` is `complete`. */
|
|
36
41
|
progress?: number;
|
|
37
|
-
/**
|
|
42
|
+
/** Message shown under the control when `status` is `error`. */
|
|
38
43
|
errorLabel?: string;
|
|
39
44
|
/** Label of the picker button inside the drop region. */
|
|
40
45
|
actionLabel?: string;
|
|
@@ -46,6 +51,24 @@ export interface FileUploadProps
|
|
|
46
51
|
|
|
47
52
|
const tabular = { fontVariantNumeric: "tabular-nums" as const };
|
|
48
53
|
|
|
54
|
+
const clampPct = (value: number) => Math.min(100, Math.max(0, value));
|
|
55
|
+
|
|
56
|
+
/** Reads a raw byte count as `1.2 MB`, leaving string labels untouched. */
|
|
57
|
+
const formatSize = (size: string | number | undefined): string | undefined => {
|
|
58
|
+
if (size === undefined || size === null) return undefined;
|
|
59
|
+
if (typeof size === "string") return size.trim() || undefined;
|
|
60
|
+
if (!Number.isFinite(size) || size < 0) return undefined;
|
|
61
|
+
if (size < 1024) return `${Math.round(size)} B`;
|
|
62
|
+
const units = ["KB", "MB", "GB"];
|
|
63
|
+
let value = size / 1024;
|
|
64
|
+
let unit = 0;
|
|
65
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
66
|
+
value /= 1024;
|
|
67
|
+
unit += 1;
|
|
68
|
+
}
|
|
69
|
+
return `${value >= 10 ? Math.round(value) : Math.round(value * 10) / 10} ${units[unit]}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
49
72
|
export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
50
73
|
(
|
|
51
74
|
{
|
|
@@ -53,9 +76,9 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
53
76
|
title = "Upload payouts file",
|
|
54
77
|
hint = "CSV, up to 10 MB",
|
|
55
78
|
accept = ".csv",
|
|
56
|
-
fileName
|
|
57
|
-
fileSize
|
|
58
|
-
progress
|
|
79
|
+
fileName,
|
|
80
|
+
fileSize,
|
|
81
|
+
progress,
|
|
59
82
|
errorLabel = "Failed",
|
|
60
83
|
actionLabel = "Upload CSV",
|
|
61
84
|
onFileSelect,
|
|
@@ -68,30 +91,51 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
68
91
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
69
92
|
const [dragActive, setDragActive] = React.useState(false);
|
|
70
93
|
|
|
94
|
+
const name = typeof fileName === "string" ? fileName.trim() : "";
|
|
95
|
+
const hasFile = name.length > 0;
|
|
96
|
+
const sizeLabel = formatSize(fileSize);
|
|
97
|
+
|
|
71
98
|
// A live drag outranks an idle parent, but any explicit card state wins.
|
|
72
99
|
const resolved: FileUploadStatus =
|
|
73
|
-
status === "idle" && dragActive ? "dragging" : status;
|
|
74
|
-
const dragging = resolved === "dragging";
|
|
75
|
-
const
|
|
100
|
+
!hasFile && status === "idle" && dragActive ? "dragging" : status;
|
|
101
|
+
const dragging = !hasFile && resolved === "dragging";
|
|
102
|
+
const failed = resolved === "error";
|
|
103
|
+
// An error with no file keeps the drop region usable, only tinted.
|
|
104
|
+
const errorOnDropzone = failed && !hasFile;
|
|
105
|
+
|
|
106
|
+
const pct =
|
|
107
|
+
progress === undefined
|
|
108
|
+
? resolved === "complete"
|
|
109
|
+
? 100
|
|
110
|
+
: 0
|
|
111
|
+
: clampPct(progress);
|
|
112
|
+
const barPct = failed
|
|
113
|
+
? progress === undefined
|
|
114
|
+
? 18
|
|
115
|
+
: Math.min(pct, 18)
|
|
116
|
+
: pct;
|
|
76
117
|
|
|
77
118
|
const openPicker = () => inputRef.current?.click();
|
|
78
119
|
|
|
79
120
|
const fileInput = (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
121
|
+
<div className="relative">
|
|
122
|
+
<Input
|
|
123
|
+
ref={inputRef}
|
|
124
|
+
type="file"
|
|
125
|
+
accept={accept}
|
|
126
|
+
ariaLabel={title}
|
|
127
|
+
aria-invalid={failed || undefined}
|
|
128
|
+
chrome="none"
|
|
129
|
+
className="sr-only"
|
|
130
|
+
onChange={(event) => {
|
|
131
|
+
const file = event.target.files?.[0];
|
|
132
|
+
if (file) onFileSelect?.(file);
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
92
136
|
);
|
|
93
137
|
|
|
94
|
-
if (
|
|
138
|
+
if (!hasFile) {
|
|
95
139
|
return (
|
|
96
140
|
<div
|
|
97
141
|
ref={ref}
|
|
@@ -112,7 +156,9 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
112
156
|
"transition duration-200 ease-out",
|
|
113
157
|
dragging
|
|
114
158
|
? "border-primary bg-primary-subdued/70"
|
|
115
|
-
:
|
|
159
|
+
: errorOnDropzone
|
|
160
|
+
? "border-semantic-error bg-canvas"
|
|
161
|
+
: "border-hairline-strong bg-canvas",
|
|
116
162
|
className,
|
|
117
163
|
)}
|
|
118
164
|
{...rest}
|
|
@@ -129,7 +175,11 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
129
175
|
strokeWidth={1.25}
|
|
130
176
|
className={cn(
|
|
131
177
|
"transition-colors duration-200 ease-out",
|
|
132
|
-
dragging
|
|
178
|
+
dragging
|
|
179
|
+
? "text-primary"
|
|
180
|
+
: errorOnDropzone
|
|
181
|
+
? "text-semantic-error"
|
|
182
|
+
: "text-ink",
|
|
133
183
|
)}
|
|
134
184
|
/>
|
|
135
185
|
<Icon
|
|
@@ -138,7 +188,11 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
138
188
|
strokeWidth={2}
|
|
139
189
|
className={cn(
|
|
140
190
|
"absolute left-1/2 top-[60%] -translate-x-1/2 -translate-y-1/2 transition-colors duration-200 ease-out",
|
|
141
|
-
dragging
|
|
191
|
+
dragging
|
|
192
|
+
? "text-primary"
|
|
193
|
+
: errorOnDropzone
|
|
194
|
+
? "text-semantic-error"
|
|
195
|
+
: "text-ink",
|
|
142
196
|
)}
|
|
143
197
|
/>
|
|
144
198
|
</span>
|
|
@@ -160,12 +214,16 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
160
214
|
{actionLabel}
|
|
161
215
|
</Button>
|
|
162
216
|
)}
|
|
217
|
+
|
|
218
|
+
{errorOnDropzone ? (
|
|
219
|
+
<p aria-live="polite" className="text-caption text-semantic-error">
|
|
220
|
+
{errorLabel}
|
|
221
|
+
</p>
|
|
222
|
+
) : null}
|
|
163
223
|
</div>
|
|
164
224
|
);
|
|
165
225
|
}
|
|
166
226
|
|
|
167
|
-
const failed = resolved === "error";
|
|
168
|
-
|
|
169
227
|
return (
|
|
170
228
|
<div
|
|
171
229
|
ref={ref}
|
|
@@ -177,16 +235,25 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
177
235
|
)}
|
|
178
236
|
{...rest}
|
|
179
237
|
>
|
|
238
|
+
{fileInput}
|
|
239
|
+
|
|
180
240
|
<div className="flex items-start gap-md">
|
|
181
|
-
<Icon
|
|
241
|
+
<Icon
|
|
242
|
+
name="file"
|
|
243
|
+
size={20}
|
|
244
|
+
strokeWidth={1.5}
|
|
245
|
+
className="mt-xxs text-ink"
|
|
246
|
+
/>
|
|
182
247
|
<div className="min-w-0 flex-1">
|
|
183
|
-
<p className="truncate text-body-md text-ink">{
|
|
184
|
-
|
|
248
|
+
<p className="truncate text-body-md text-ink">{name}</p>
|
|
249
|
+
{sizeLabel ? (
|
|
250
|
+
<p className="mt-xxs text-caption text-muted">{sizeLabel}</p>
|
|
251
|
+
) : null}
|
|
185
252
|
</div>
|
|
186
253
|
<Button
|
|
187
254
|
variant="bare"
|
|
188
255
|
size="inline"
|
|
189
|
-
aria-label={`Remove ${
|
|
256
|
+
aria-label={`Remove ${name}`}
|
|
190
257
|
onClick={onRemove}
|
|
191
258
|
className="-mr-xs -mt-xs shrink-0 rounded-sm p-xs text-muted hover:text-ink"
|
|
192
259
|
>
|
|
@@ -197,7 +264,7 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
197
264
|
<div className="mt-md flex items-center gap-md">
|
|
198
265
|
<div
|
|
199
266
|
role="progressbar"
|
|
200
|
-
aria-label={`${
|
|
267
|
+
aria-label={`${name} upload progress`}
|
|
201
268
|
aria-valuemin={0}
|
|
202
269
|
aria-valuemax={100}
|
|
203
270
|
aria-valuenow={failed ? undefined : pct}
|
|
@@ -210,7 +277,7 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
210
277
|
"block h-full rounded-full transition-[width,background-color] duration-200 ease-out",
|
|
211
278
|
failed ? "bg-semantic-error" : "bg-primary",
|
|
212
279
|
)}
|
|
213
|
-
style={{ width: `${
|
|
280
|
+
style={{ width: `${barPct}%` }}
|
|
214
281
|
/>
|
|
215
282
|
</div>
|
|
216
283
|
<span
|
|
@@ -1,128 +1,78 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import { FileUpload, type FileUploadStatus } from "./FileUpload";
|
|
4
3
|
import { Button } from "../Button";
|
|
4
|
+
import { FileUpload, type FileUploadStatus } from "./FileUpload";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
label: string;
|
|
8
|
-
children: React.ReactNode;
|
|
9
|
-
}> = ({ label, children }) => (
|
|
10
|
-
<div className="flex flex-col gap-md">
|
|
11
|
-
<span className="text-micro-cap text-muted">{label}</span>
|
|
12
|
-
{children}
|
|
13
|
-
</div>
|
|
14
|
-
);
|
|
6
|
+
const demoFile = { name: "receipt-july.pdf", size: 1245184 } as unknown as File;
|
|
15
7
|
|
|
16
8
|
export default function FileUploadPreview() {
|
|
17
|
-
const [
|
|
18
|
-
const [
|
|
19
|
-
const [fileName, setFileName] = React.useState("payouts.csv");
|
|
20
|
-
const timer = React.useRef<number | null>(null);
|
|
21
|
-
|
|
22
|
-
const stop = React.useCallback(() => {
|
|
23
|
-
if (timer.current !== null) {
|
|
24
|
-
window.clearInterval(timer.current);
|
|
25
|
-
timer.current = null;
|
|
26
|
-
}
|
|
27
|
-
}, []);
|
|
28
|
-
|
|
29
|
-
React.useEffect(() => stop, [stop]);
|
|
30
|
-
|
|
31
|
-
const startUpload = React.useCallback(
|
|
32
|
-
(name: string) => {
|
|
33
|
-
stop();
|
|
34
|
-
setFileName(name);
|
|
35
|
-
setProgress(0);
|
|
36
|
-
setStatus("uploading");
|
|
37
|
-
timer.current = window.setInterval(() => {
|
|
38
|
-
setProgress((value) => {
|
|
39
|
-
const next = value + 6;
|
|
40
|
-
if (next >= 100) {
|
|
41
|
-
stop();
|
|
42
|
-
setStatus("complete");
|
|
43
|
-
return 100;
|
|
44
|
-
}
|
|
45
|
-
return next;
|
|
46
|
-
});
|
|
47
|
-
}, 160);
|
|
48
|
-
},
|
|
49
|
-
[stop],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const failUpload = React.useCallback(() => {
|
|
53
|
-
stop();
|
|
54
|
-
setStatus("uploading");
|
|
55
|
-
window.setTimeout(() => setStatus("error"), 420);
|
|
56
|
-
}, [stop]);
|
|
9
|
+
const [file, setFile] = React.useState<File | null>(null);
|
|
10
|
+
const [errorMode, setErrorMode] = React.useState(true);
|
|
57
11
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
12
|
+
const status: FileUploadStatus = file
|
|
13
|
+
? "complete"
|
|
14
|
+
: errorMode
|
|
15
|
+
? "error"
|
|
16
|
+
: "idle";
|
|
63
17
|
|
|
64
18
|
return (
|
|
65
|
-
<div className="mx-auto w-full max-w-3xl space-y-
|
|
66
|
-
<
|
|
67
|
-
<div className="flex flex-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
19
|
+
<div className="mx-auto w-full max-w-3xl space-y-lg bg-canvas p-lg">
|
|
20
|
+
<div className="space-y-sm">
|
|
21
|
+
<div className="flex flex-wrap items-center gap-sm">
|
|
22
|
+
<Button variant="secondary" size="sm" onClick={() => setErrorMode(false)}>
|
|
23
|
+
Idle
|
|
24
|
+
</Button>
|
|
25
|
+
<Button variant="secondary" size="sm" onClick={() => setErrorMode(true)}>
|
|
26
|
+
Trigger error
|
|
27
|
+
</Button>
|
|
28
|
+
<Button variant="secondary" size="sm" onClick={() => setFile(demoFile)}>
|
|
29
|
+
Attach demo file
|
|
30
|
+
</Button>
|
|
31
|
+
<Button variant="secondary" size="sm" onClick={() => setFile(null)}>
|
|
32
|
+
Clear file
|
|
33
|
+
</Button>
|
|
72
34
|
</div>
|
|
73
35
|
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<FileUpload status="error" progress={18} />
|
|
89
|
-
</StateCell>
|
|
90
|
-
</div>
|
|
91
|
-
</section>
|
|
36
|
+
<FileUpload
|
|
37
|
+
status={status}
|
|
38
|
+
title="Upload receipts, tracking or photos"
|
|
39
|
+
hint="PDF, PNG or JPG, up to 4.5 MB"
|
|
40
|
+
accept=".pdf,.png,.jpg"
|
|
41
|
+
fileName={file?.name}
|
|
42
|
+
fileSize={file?.size}
|
|
43
|
+
errorLabel={
|
|
44
|
+
status === "error" ? "Attach at least one document." : undefined
|
|
45
|
+
}
|
|
46
|
+
onFileSelect={setFile}
|
|
47
|
+
onRemove={() => setFile(null)}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
92
50
|
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
51
|
+
<div className="space-y-sm">
|
|
52
|
+
<p className="text-caption text-muted">Mid-upload with a tracked file</p>
|
|
53
|
+
<FileUpload
|
|
54
|
+
status="uploading"
|
|
55
|
+
title="Upload receipts"
|
|
56
|
+
hint="PDF, PNG or JPG"
|
|
57
|
+
fileName="invoice-0448.png"
|
|
58
|
+
fileSize={684032}
|
|
59
|
+
progress={42}
|
|
60
|
+
onRemove={() => undefined}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
100
63
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
onClick={() => startUpload("payouts-q3.csv")}
|
|
114
|
-
>
|
|
115
|
-
Simulate upload
|
|
116
|
-
</Button>
|
|
117
|
-
<Button variant="ghost" size="sm" onClick={failUpload}>
|
|
118
|
-
Simulate failure
|
|
119
|
-
</Button>
|
|
120
|
-
<Button variant="ghost" size="sm" onClick={reset}>
|
|
121
|
-
Reset
|
|
122
|
-
</Button>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</section>
|
|
64
|
+
<div className="space-y-sm">
|
|
65
|
+
<p className="text-caption text-muted">Error with a tracked file</p>
|
|
66
|
+
<FileUpload
|
|
67
|
+
status="error"
|
|
68
|
+
title="Upload receipts"
|
|
69
|
+
hint="PDF, PNG or JPG"
|
|
70
|
+
fileName="statement.pdf"
|
|
71
|
+
fileSize="3.1 MB"
|
|
72
|
+
errorLabel="Upload failed — try again."
|
|
73
|
+
onRemove={() => undefined}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
126
76
|
</div>
|
|
127
77
|
);
|
|
128
78
|
}
|