@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 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**.
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 = "payouts.csv",
7284
- fileSize = "42 KB",
7285
- progress = 0,
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 resolved = status === "idle" && dragActive ? "dragging" : status;
7296
- const dragging = resolved === "dragging";
7297
- const pct = Math.min(100, Math.max(0, progress));
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 (resolved === "idle" || dragging) {
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)(Icon, { name: "file", size: 20, strokeWidth: 1.5, className: "mt-xxs text-ink" }),
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: fileName }),
7408
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "mt-xxs text-caption text-muted", children: fileSize })
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 ${fileName}`,
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": `${fileName} upload progress`,
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: `${failed ? Math.min(pct, 18) : pct}%` }
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 = "payouts.csv",
7154
- fileSize = "42 KB",
7155
- progress = 0,
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 resolved = status === "idle" && dragActive ? "dragging" : status;
7166
- const dragging = resolved === "dragging";
7167
- const pct = Math.min(100, Math.max(0, progress));
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 (resolved === "idle" || dragging) {
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(Icon, { name: "file", size: 20, strokeWidth: 1.5, className: "mt-xxs text-ink" }),
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: fileName }),
7278
- /* @__PURE__ */ jsx35("p", { className: "mt-xxs text-caption text-muted", children: fileSize })
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 ${fileName}`,
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": `${fileName} upload progress`,
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: `${failed ? Math.min(pct, 18) : pct}%` }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designa8/vibedesign-stripe-deepseek-2-176b",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Complete design system \"My Project stripe-deepseek\" generated by Atoms Design.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -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`, `complete`,
12
- * and `error` share the single-file card with its progress readout.
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
- /** Name of the file being tracked by the card states. */
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
- /** Human readable size of that file. */
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
- /** Readout used instead of the percentage when `status` is `error`. */
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 = "payouts.csv",
57
- fileSize = "42 KB",
58
- progress = 0,
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 pct = Math.min(100, Math.max(0, progress));
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
- <Input
81
- ref={inputRef}
82
- type="file"
83
- accept={accept}
84
- ariaLabel={title}
85
- chrome="none"
86
- className="sr-only"
87
- onChange={(event) => {
88
- const file = event.target.files?.[0];
89
- if (file) onFileSelect?.(file);
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 (resolved === "idle" || dragging) {
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
- : "border-hairline-strong bg-canvas",
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 ? "text-primary" : "text-ink",
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 ? "text-primary" : "text-ink",
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 name="file" size={20} strokeWidth={1.5} className="mt-xxs text-ink" />
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">{fileName}</p>
184
- <p className="mt-xxs text-caption text-muted">{fileSize}</p>
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 ${fileName}`}
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={`${fileName} upload progress`}
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: `${failed ? Math.min(pct, 18) : pct}%` }}
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 StateCell: React.FC<{
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 [status, setStatus] = React.useState<FileUploadStatus>("idle");
18
- const [progress, setProgress] = React.useState(0);
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 reset = React.useCallback(() => {
59
- stop();
60
- setProgress(0);
61
- setStatus("idle");
62
- }, [stop]);
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-xxl bg-canvas p-lg">
66
- <section className="flex flex-col gap-lg">
67
- <div className="flex flex-col gap-xxs">
68
- <h2 className="text-body-md text-ink">File upload control</h2>
69
- <p className="text-caption text-muted">
70
- Drop region, live drag, and the tracked-file card.
71
- </p>
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
- <div className="grid gap-xl sm:grid-cols-2 xl:grid-cols-3">
75
- <StateCell label="Default (dropzone)">
76
- <FileUpload status="idle" />
77
- </StateCell>
78
- <StateCell label="Drag over">
79
- <FileUpload status="dragging" />
80
- </StateCell>
81
- <StateCell label="Uploading">
82
- <FileUpload status="uploading" progress={62} />
83
- </StateCell>
84
- <StateCell label="Complete">
85
- <FileUpload status="complete" progress={100} />
86
- </StateCell>
87
- <StateCell label="Error">
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
- <section className="flex flex-col gap-lg">
94
- <div className="flex flex-col gap-xxs">
95
- <h2 className="text-body-md text-ink">Interactive</h2>
96
- <p className="text-caption text-muted">
97
- Pick or drop a file to run the upload, or force the failure state.
98
- </p>
99
- </div>
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
- <div className="flex flex-col gap-md">
102
- <FileUpload
103
- status={status}
104
- progress={progress}
105
- fileName={fileName}
106
- onFileSelect={(file) => startUpload(file.name)}
107
- onRemove={reset}
108
- />
109
- <div className="flex flex-wrap items-center gap-sm">
110
- <Button
111
- variant="secondary"
112
- size="sm"
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
  }