@comitor/ui 1.3.3 → 1.4.0
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.
|
@@ -12,13 +12,29 @@ const accentClasses = {
|
|
|
12
12
|
success: "border-l-4 border-l-success-ink",
|
|
13
13
|
destructive: "border-l-4 border-l-destructive-ink"
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
const accentIconClasses = {
|
|
16
|
+
none: "bg-muted text-muted-foreground",
|
|
17
|
+
primary: "bg-muted text-muted-foreground",
|
|
18
|
+
info: "bg-info/15 text-info-ink",
|
|
19
|
+
warning: "bg-warning/20 text-warning-ink",
|
|
20
|
+
success: "bg-success/15 text-success-ink",
|
|
21
|
+
destructive: "bg-destructive/15 text-destructive-ink"
|
|
22
|
+
};
|
|
23
|
+
function SectionHeading({
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
icon: Icon,
|
|
27
|
+
accent
|
|
28
|
+
}) {
|
|
16
29
|
return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start gap-3 text-left", children: [
|
|
17
30
|
Icon && /* @__PURE__ */ jsx(
|
|
18
31
|
"span",
|
|
19
32
|
{
|
|
20
33
|
"aria-hidden": "true",
|
|
21
|
-
className:
|
|
34
|
+
className: cn(
|
|
35
|
+
"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md",
|
|
36
|
+
accentIconClasses[accent]
|
|
37
|
+
),
|
|
22
38
|
children: /* @__PURE__ */ jsx(Icon, { className: "size-4" })
|
|
23
39
|
}
|
|
24
40
|
),
|
|
@@ -53,7 +69,7 @@ function FormSection({
|
|
|
53
69
|
if (!collapsible) {
|
|
54
70
|
return /* @__PURE__ */ jsxs("section", { "data-slot": "form-section", className: shell, children: [
|
|
55
71
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 border-b border-border p-4 md:px-5", children: [
|
|
56
|
-
/* @__PURE__ */ jsx(SectionHeading, { title, description, icon }),
|
|
72
|
+
/* @__PURE__ */ jsx(SectionHeading, { title, description, icon, accent }),
|
|
57
73
|
actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })
|
|
58
74
|
] }),
|
|
59
75
|
/* @__PURE__ */ jsx("div", { className: grid, children })
|
|
@@ -62,7 +78,7 @@ function FormSection({
|
|
|
62
78
|
return /* @__PURE__ */ jsx(Collapsible, { open: isOpen, onOpenChange: setOpen, asChild: true, children: /* @__PURE__ */ jsxs("section", { "data-slot": "form-section", className: shell, children: [
|
|
63
79
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 border-b border-border p-4 md:px-5", children: [
|
|
64
80
|
/* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "flex min-w-0 flex-1 items-start gap-3 rounded-md text-left", children: [
|
|
65
|
-
/* @__PURE__ */ jsx(SectionHeading, { title, description, icon }),
|
|
81
|
+
/* @__PURE__ */ jsx(SectionHeading, { title, description, icon, accent }),
|
|
66
82
|
/* @__PURE__ */ jsx(
|
|
67
83
|
ChevronDown,
|
|
68
84
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\n/*\n * Vạch 4px bên trái MANG THÔNG TIN (phân biệt nhóm trường), nên chịu ngưỡng 3:1 của WCAG\n * 1.4.11 — không phải viền trang trí. Nó đứng trên nền TRANG (`bg-card`) chứ không phải trên\n * nền cùng tone, tức đúng vai trò bậc `-ink` của hợp đồng màu (docs/hop-dong-mau.md §1).\n *\n * Bản trước lấy thẳng bậc NỀN và hỏng ở light — vạch gần như biến mất trên thẻ trắng:\n * gold-300 #E8B824 1,86:1 ✗ · warning #F2C94C 1,59:1 ✗\n * success #2EA56A 3,13:1 · info #1F8A8B 4,14:1 · destructive #C93B3B 5,03:1\n * (dark thì ngược lại — destructive #C93B3B chỉ 3,53:1 trên `--card`.)\n *\n * Bậc ink đạt thoải mái ở CẢ hai theme, đo trên `--card` (light #FFFFFF · dark #16181D):\n * primary-ink 6,51 / 11,90:1 success-ink 6,52 / 10,19:1\n * info-ink 6,68 / 9,54:1 warning-ink 6,51 / 11,60:1\n * destructive-ink 7,15 / 6,42:1\n */\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-primary-ink\",\n info: \"border-l-4 border-l-info-ink\",\n warning: \"border-l-4 border-l-warning-ink\",\n success: \"border-l-4 border-l-success-ink\",\n destructive: \"border-l-4 border-l-destructive-ink\"\n};\n\nfunction SectionHeading({ title, description, icon: Icon }: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\">) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\"\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n {/* KHÔNG `truncate`: ở 320px tiêu đề nhóm dài (\"Thông tin không gian làm việc\") bị cắt\n mất chữ — đo được 26px nội dung biến mất. Tiêu đề xuống dòng thì xấu hơn một chút\n nhưng vẫn đọc được, còn cắt thì mất thông tin. `min-w-0` ở thẻ cha vẫn giữ cho hàng\n header của nhóm không đẩy nút actions ra ngoài. */}\n <h3 className=\"text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AA8DU,cAGJ,YAHI;AA5DV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAwCpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe,EAAE,OAAO,aAAa,MAAM,KAAK,GAA6D;AACpH,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QAEV,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WAKb;AAAA,0BAAC,QAAG,WAAU,wDAAwD,iBAAM;AAAA,MAC3E,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACnE,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACpE;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\n/*\n * Vạch 4px bên trái MANG THÔNG TIN (phân biệt nhóm trường), nên chịu ngưỡng 3:1 của WCAG\n * 1.4.11 — không phải viền trang trí. Nó đứng trên nền TRANG (`bg-card`) chứ không phải trên\n * nền cùng tone, tức đúng vai trò bậc `-ink` của hợp đồng màu (docs/hop-dong-mau.md §1).\n *\n * Bản trước lấy thẳng bậc NỀN và hỏng ở light — vạch gần như biến mất trên thẻ trắng:\n * gold-300 #E8B824 1,86:1 ✗ · warning #F2C94C 1,59:1 ✗\n * success #2EA56A 3,13:1 · info #1F8A8B 4,14:1 · destructive #C93B3B 5,03:1\n * (dark thì ngược lại — destructive #C93B3B chỉ 3,53:1 trên `--card`.)\n *\n * Bậc ink đạt thoải mái ở CẢ hai theme, đo trên `--card` (light #FFFFFF · dark #16181D):\n * primary-ink 6,51 / 11,90:1 success-ink 6,52 / 10,19:1\n * info-ink 6,68 / 9,54:1 warning-ink 6,51 / 11,60:1\n * destructive-ink 7,15 / 6,42:1\n */\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-primary-ink\",\n info: \"border-l-4 border-l-info-ink\",\n warning: \"border-l-4 border-l-warning-ink\",\n success: \"border-l-4 border-l-success-ink\",\n destructive: \"border-l-4 border-l-destructive-ink\"\n};\n\n/*\n * Ô icon đi theo CÙNG accent với vạch trái.\n *\n * Vì sao không để nó trung tính như bản trước: nhóm `accent=\"destructive\"` vẽ vạch đỏ nhưng ô\n * icon vẫn `bg-muted` xám, nên hai tín hiệu trong cùng một header nói hai điều khác nhau — và\n * cái NẶNG KÝ hơn về thị giác (ô 32px có nền, so với vạch 4px) là cái nói sai. Đo trên\n * `/settings/advanced` của comitor-starter: nhóm \"Vùng nguy hiểm\" — nhóm chứa nút xoá sạch dữ\n * liệu — có icon cảnh báo cùng màu với icon của nhóm \"Dữ liệu ứng dụng\" ngay phía trên.\n *\n * ⚠ `primary` CỐ Ý giữ ô trung tính, không tô tint. Ở bảng MẶC ĐỊNH `--primary-ink` trỏ thẳng về\n * `--primary` (styles.css §\"TỪ 0.3.0\"), nên `text-primary-ink` trên `bg-primary/20` là gold\n * #E8B824 trên tint gold #FAF1D3 = **1,64:1** — thủng ngưỡng 3:1 của WCAG 1.4.11, và tệ hơn hẳn\n * ô trung tính đang có (3,83:1). Đây là món nợ brand mà §22 của docs/quyet-dinh-tich-hop.md đã\n * ghi là \"CÒN TỒN, có chủ ý\"; nối accent vào ô icon mà không loại `primary` ra là MỞ RỘNG món nợ\n * đó sang một bề mặt mới. Tô ĐẶC (`bg-primary text-primary-foreground`, 10,19:1 — lối chữa mà\n * §22 dùng cho `AppLauncher`) thì đọc được, nhưng một ô gold đặc 32px kêu to hơn cả nhóm\n * `destructive`, tức lật ngược thứ bậc nghiêm trọng. Nên: giữ trung tính cho tới khi `--primary`\n * có một bậc ink thật ở bảng mặc định.\n *\n * ⚠⚠ SỐ ĐO DƯỚI ĐÂY LÀ CỦA BẢNG MẶC ĐỊNH, và phải luôn ghi rõ bảng nào, vì\n * `pnpm lint:a11y` KHÔNG ĐO BẢNG MẶC ĐỊNH: `scripts/a11y/tailwind.mjs` hợp\n * `[data-contrast=\"high\"]` ĐÈ LÊN `:root`, nên mọi token mà bảng tương phản cao ghi đè đều được\n * đo theo bảng ĐÓ rồi dán nhãn \"light\". Cổng báo `primary` 5,77:1 ✓ trong khi bảng mặc định là\n * 1,64:1 ✗. Với những cặp ấy, comment này là lớp phòng vệ DUY NHẤT — cổng xanh không chứng minh\n * gì. (Bản đầu của chính khối comment này chép số của bảng tương phản cao mà không ghi rõ, tức\n * mắc đúng cái bẫy nó đang cảnh báo.)\n *\n * tone bảng mặc định (light, ink trên tint của chính nó)\n * none 3,83:1 ✓ (bg-muted, giữ nguyên)\n * primary 1,64:1 ✗ → KHÔNG tint, dùng lại ô trung tính\n * warning 5,93:1 ✓\n * success 5,60:1 ✓\n * destructive 3,57:1 ✓\n * info 5,54:1 ✓\n *\n * Bốn tone có tint lấy Y HỆT `statCardIconVariants` (stat-card.tsx) để gói chỉ có MỘT thang tone\n * cho ô icon. Khác duy nhất là hình học — ở đây `size-8 rounded-md`, ở StatCard `size-7\n * rounded-lg` — nên hai chỗ không dùng chung được một `cva`, và đó là lý do bảng này chép giá trị\n * chứ không import. Đổi thang thì phải đổi CẢ HAI.\n *\n * ⚠ Viết THẲNG từng chuỗi, không dựng bằng `bg-${accent}/15`: cổng quét class TĨNH trong `src/**`\n * và xếp lớp dựng lúc chạy vào nhóm \"CẦN KIỂM TAY\" → FAIL. Hành vi cố ý, không phải hạn chế.\n */\nconst accentIconClasses: Record<FormSectionAccent, string> = {\n none: \"bg-muted text-muted-foreground\",\n primary: \"bg-muted text-muted-foreground\",\n info: \"bg-info/15 text-info-ink\",\n warning: \"bg-warning/20 text-warning-ink\",\n success: \"bg-success/15 text-success-ink\",\n destructive: \"bg-destructive/15 text-destructive-ink\"\n};\n\nfunction SectionHeading({\n title,\n description,\n icon: Icon,\n accent\n}: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\"> & { accent: FormSectionAccent }) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md\",\n accentIconClasses[accent]\n )}\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n {/* KHÔNG `truncate`: ở 320px tiêu đề nhóm dài (\"Thông tin không gian làm việc\") bị cắt\n mất chữ — đo được 26px nội dung biến mất. Tiêu đề xuống dòng thì xấu hơn một chút\n nhưng vẫn đọc được, còn cắt thì mất thông tin. `min-w-0` ở thẻ cha vẫn giữ cho hàng\n header của nhóm không đẩy nút actions ra ngoài. */}\n <h3 className=\"text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} accent={accent} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} accent={accent} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AA0HU,cAGJ,YAHI;AAxHV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAwCpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AA6CA,MAAM,oBAAuD;AAAA,EAC3D,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AACF,GAA6F;AAC3F,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,kBAAkB,MAAM;AAAA,QAC1B;AAAA,QAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WAKb;AAAA,0BAAC,QAAG,WAAU,wDAAwD,iBAAM;AAAA,MAC3E,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY,QAAgB;AAAA,QACnF,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY,QAAgB;AAAA,QACpF;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
|
|
@@ -117,7 +117,13 @@ function useFileUpload(options) {
|
|
|
117
117
|
uppy.on("restriction-failed", ((_file, error) => {
|
|
118
118
|
handlers.current.onError?.({ message: error?.message ?? labels.uploadFailed });
|
|
119
119
|
}));
|
|
120
|
-
for (const file of list)
|
|
120
|
+
for (const file of list) {
|
|
121
|
+
try {
|
|
122
|
+
uppy.addFile({ name: file.name, type: file.type, data: file });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!error?.isRestriction) throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
121
127
|
await uppy.upload();
|
|
122
128
|
} finally {
|
|
123
129
|
setBusy(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/uploader/use-file-upload.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { DEFAULT_FILE_UPLOAD_LABELS, type FileUploadLogicLabels } from \"./file-upload-constants.js\";\n\n/**\n * Phần MÁY MÓC của việc tải tệp, tách khỏi mọi quyết định về hình dạng.\n *\n * ── VÌ SAO TÁCH ───────────────────────────────────────────────────────────────────────────────\n * `FileUpload` vẽ một khung kéo-thả viền đứt nét — đúng cho tài liệu nhiều tệp, **sai** cho một ô\n * ảnh đại diện 56px. Nhưng phần bên dưới thì hai ca cần y hệt nhau: hàng đợi Uppy, tiến độ theo\n * byte, bốn cạm bẫy của `@uppy/xhr-upload`, và việc xoá `input.value` sau mỗi lượt.\n *\n * Đường cắt này không phải một trừu tượng mới bịa ra: trong `file-upload.tsx` bản 1.2.0, toàn bộ\n * khối `start` + `useEffect` dọn dẹp + ba `useState` **không chạm một dòng JSX nào**, còn JSX thì\n * chỉ đọc `busy`/`percent`/`dragging`. Đây là việc đặt tên cho một ranh giới đã có sẵn.\n *\n * ⚠ `inputProps` và `dropProps` là **prop-getter**: chỗ gọi phải rải chúng lên đúng phần tử. Thiếu\n * `<input {...inputProps} />` thì `open()` không làm gì, và triệu chứng là một nút bấm im lặng.\n * `dropProps` đã tự gọi `preventDefault` — lỗi kinh điển khi để app tự viết.\n */\n/**\n * Bề mặt Uppy mà component này dùng — khai TAY, không import kiểu thật.\n *\n * Hai lý do, cả hai đều thực dụng: kiểu thật của `Uppy` mang generic `<Meta, Body>` và đổi hình\n * dạng giữa các bản major; và `@uppy/core` là peer TUỲ CHỌN, nên `import type` từ nó sẽ làm\n * `tsc` của một app KHÔNG cài Uppy báo đỏ ở một file họ không hề dùng.\n *\n * Cái giá: nếu Uppy đổi tên một phương thức thì lỗi lộ ra lúc CHẠY, không lúc biên dịch. Bù lại\n * bằng `scripts/smoke-entries.mjs` — nó nạp thật entry này ở mỗi lần publish.\n */\ninterface UppyLike {\n addFile: (file: { name: string; type: string; data: File }) => void;\n upload: () => Promise<unknown>;\n cancelAll: () => void;\n destroy: () => void;\n on: (event: string, handler: (...args: never[]) => void) => void;\n use: (plugin: unknown, options?: unknown) => unknown;\n}\n\n/**\n * Nạp Uppy LƯỜI — chỉ khi người dùng thật sự chọn tệp.\n *\n * `import()` động giữ Uppy ra khỏi bundle của lượt tải trang đầu tiên. Với một màn hồ sơ mà đa số\n * người mở ra chỉ để xem, đó là toàn bộ chi phí của Uppy bị hoãn tới lúc nó thật sự có việc.\n *\n * ⚠ Hai gói này là **peer TUỲ CHỌN**. App không cài mà lại render component này thì `import()` sẽ\n * hỏng — nên lỗi được bắt và trả về qua `onError` với thông điệp nói đúng tên gói phải cài, thay vì\n * một `ChunkLoadError` không gợi gì.\n */\nasync function loadUppy(): Promise<{ Uppy: new (o: unknown) => UppyLike; XHRUpload: unknown } | null> {\n try {\n const [core, xhr] = await Promise.all([import(\"@uppy/core\"), import(\"@uppy/xhr-upload\")]);\n return {\n // `as unknown as` là bắt buộc: kiểu thật của Uppy có generic `<Meta, Body>` mà `UppyLike`\n // cố ý không mang. Xem ghi chú ở `UppyLike` về việc vì sao gói này không dùng kiểu thật.\n Uppy: (core.default ?? (core as unknown as { Uppy: unknown }).Uppy) as unknown as new (o: unknown) => UppyLike,\n XHRUpload: xhr.default ?? xhr\n };\n } catch {\n return null;\n }\n}\n\nexport interface UseFileUploadOptions<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n /** Tên trường trong `FormData`. Mặc định `\"file\"`. */\n fieldName?: string;\n maxFileSize: number;\n allowedFileTypes: string[];\n maxNumberOfFiles?: number;\n onUploaded: (result: FileUploadResult<T>) => void;\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * CHỈ hai chuỗi mà phần máy móc thật sự dùng.\n *\n * Cố ý KHÔNG nhận cả `FileUploadLabels`: ba chuỗi còn lại (`dropHint`/`choose`/`uploading`) là\n * của bố cục, và để chúng lọt vào đây là mời người viết truyền một prop không bao giờ chạy.\n */\n labels?: Partial<FileUploadLogicLabels>;\n disabled?: boolean;\n}\n\n/** Kết quả một tệp tải xong: thân phản hồi JSON của endpoint app. */\nexport interface FileUploadResult<T = unknown> {\n body: T;\n fileName: string;\n}\n\nexport interface UseFileUploadResult {\n /** Mở hộp chọn tệp. Cần `<input {...inputProps} />` có mặt trong cây. */\n open: () => void;\n /** Bắt đầu tải một danh sách tệp có sẵn (dùng cho kéo-thả tự xử lý). */\n start: (files: FileList | File[]) => void;\n /** Huỷ lượt đang chạy. */\n cancel: () => void;\n inputProps: {\n ref: React.RefObject<HTMLInputElement | null>;\n type: \"file\";\n accept: string;\n multiple: boolean;\n disabled: boolean;\n onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;\n };\n dropProps: {\n onDragOver: (event: React.DragEvent) => void;\n onDragLeave: () => void;\n onDrop: (event: React.DragEvent) => void;\n };\n /** Có tệp đang được rê TRÊN khối này. */\n dragging: boolean;\n busy: boolean;\n /** 0–100. */\n percent: number;\n}\n\nexport function useFileUpload<T = unknown>(options: UseFileUploadOptions<T>): UseFileUploadResult {\n const {\n endpoint,\n fieldName = \"file\",\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles = 1,\n onUploaded,\n onError,\n disabled = false\n } = options;\n const labels = { ...DEFAULT_FILE_UPLOAD_LABELS, ...options.labels };\n\n const inputRef = useRef<HTMLInputElement>(null);\n const [busy, setBusy] = useState(false);\n const [percent, setPercent] = useState(0);\n const [dragging, setDragging] = useState(false);\n\n /*\n * `onUploaded`/`onError` đi qua ref: chỗ gọi thường truyền hàm mới ở mỗi lần render, và để chúng\n * trong mảng phụ thuộc là dựng lại Uppy sau mỗi phím gõ vào biểu mẫu bên cạnh.\n */\n const handlers = useRef({ onUploaded, onError });\n handlers.current = { onUploaded, onError };\n\n const uppyRef = useRef<UppyLike | null>(null);\n useEffect(() => {\n return () => {\n uppyRef.current?.destroy();\n uppyRef.current = null;\n };\n }, []);\n\n const start = useCallback(\n async (files: FileList | File[]) => {\n const list = Array.from(files).slice(0, maxNumberOfFiles);\n if (list.length === 0) return;\n\n setBusy(true);\n setPercent(0);\n try {\n const mod = await loadUppy();\n if (!mod) {\n handlers.current.onError?.({ message: labels.missingDependency });\n return;\n }\n\n /*\n * Dựng MỘT Uppy cho MỘT lượt tải rồi huỷ. Giữ lại một instance dài hạn nghe hấp dẫn hơn,\n * nhưng Uppy nhớ cả những tệp đã hỏng ở lượt trước — chọn lại đúng tệp đó sẽ bị chặn vì\n * \"trùng\", và triệu chứng là một nút bấm không làm gì.\n */\n uppyRef.current?.destroy();\n const uppy = new mod.Uppy({\n autoProceed: false,\n restrictions: { maxNumberOfFiles, maxFileSize, allowedFileTypes }\n });\n uppyRef.current = uppy;\n\n /*\n * Chỗ giữ lỗi của lượt gọi gần nhất. `upload-error` của Uppy KHÔNG mang thân phản hồi —\n * thứ nó ném là `new NetworkError(xhr.statusText, xhr)`, mà trên HTTP/2 `statusText` là\n * chuỗi RỖNG. Nên `code` phải được nhặt ở `onAfterResponse`, nơi còn nhìn thấy `xhr`.\n */\n const lastError: { code?: string; status?: number; message?: string } = {};\n\n uppy.use(mod.XHRUpload, {\n endpoint,\n fieldName,\n method: \"POST\",\n // Cookie phiên là cách Comitor xác thực mọi route tự viết — thiếu dòng này là 401.\n withCredentials: true,\n\n /*\n * ⚠ CHỈ THỬ LẠI LỖI 5xx.\n *\n * Mặc định của Uppy là `shouldRetry = () => true` với `retries = 3` (đọc\n * `@uppy/core/lib/utils/fetcher.js`), tức MỌI phản hồi không-2xx bị gửi lại ba lần. Hậu\n * quả đo được bằng chính hợp đồng của Comitor:\n * · một ảnh sai định dạng 2 MB → BỐN lượt POST 2 MB, tất cả đều biết chắc sẽ hỏng;\n * · một **429** của `lib/api/rate-limit.ts` cũng bị thử lại, mỗi lần tiêu thêm một đơn\n * vị hạn mức và bỏ qua `Retry-After` — client tự đào sâu cái hố nó vừa rơi vào.\n * 4xx là \"yêu cầu sai, gửi lại cũng thế\"; chỉ 5xx mới đáng thử lại.\n */\n shouldRetry: (xhr: XMLHttpRequest) => xhr.status >= 500,\n\n /*\n * ⚠ `getResponseError` KHÔNG TỒN TẠI — nó chỉ còn trong một dòng comment cũ ở\n * `@uppy/xhr-upload/lib/index.js`, không có trong `XhrUploadOpts`, và không được đọc ở\n * đâu cả. Truyền nó vào là viết một callback không bao giờ chạy. Đường thật là\n * `onAfterResponse`, thứ XHRUpload có forward xuống fetcher.\n */\n onAfterResponse: (xhr: XMLHttpRequest) => {\n if (xhr.status >= 200 && xhr.status < 300) return;\n lastError.status = xhr.status;\n try {\n const parsed = JSON.parse(xhr.responseText) as { error?: { code?: string; message?: string } };\n lastError.code = parsed.error?.code;\n lastError.message = parsed.error?.message;\n } catch {\n // Phản hồi không phải JSON (proxy trả HTML, 413 của tầng mạng…). Giữ `status` là đủ.\n }\n },\n\n /*\n * ⚠ KHÔNG gửi meta kèm tệp.\n *\n * Mặc định `allowedMetaFields: true` ghi MỌI meta vào cùng `FormData`, và nó ghi TRƯỚC\n * khi append tệp. Route của Comitor đọc `form.get(\"file\")` — hàm đó trả phần tử ĐẦU TIÊN\n * cùng tên. Nên chỉ cần một khoá meta tên `file` là route nhận về một chuỗi,\n * `file instanceof File` thành false, và nó trả 400 \"Thiếu tệp ảnh\" trong khi tệp vẫn\n * nằm nguyên trong request. Mảng rỗng đóng hẳn đường đó.\n */\n allowedMetaFields: []\n });\n\n uppy.on(\"progress\", ((value: number) => setPercent(value)) as never);\n uppy.on(\"upload-success\", ((file: { name?: string } | undefined, response: { body?: T }) => {\n handlers.current.onUploaded({ body: response?.body as T, fileName: file?.name ?? \"\" });\n }) as never);\n uppy.on(\"upload-error\", ((_file: unknown, error: Error) => {\n // `lastError` là nguồn duy nhất có `code` — xem `onAfterResponse` ở trên.\n handlers.current.onError?.({\n code: lastError.code,\n status: lastError.status,\n message: lastError.message ?? error?.message ?? labels.uploadFailed\n });\n }) as never);\n /*\n * `restriction-failed` là đường RIÊNG, không đi qua `upload-error`. Bỏ sót nó thì một tệp\n * quá lớn hay sai định dạng bị Uppy chặn trong im lặng — người dùng bấm rồi không thấy gì.\n */\n uppy.on(\"restriction-failed\", ((_file: unknown, error: Error) => {\n handlers.current.onError?.({ message: error?.message ?? labels.uploadFailed });\n }) as never);\n\n for (const file of list) uppy.addFile({ name: file.name, type: file.type, data: file });\n await uppy.upload();\n } finally {\n setBusy(false);\n setPercent(0);\n /*\n * Xoá giá trị input: chọn LẠI cùng một tệp sau một lần lỗi sẽ KHÔNG bắn `change` nếu giá\n * trị không đổi — và triệu chứng là thử lại im lặng không làm gì.\n */\n if (inputRef.current) inputRef.current.value = \"\";\n }\n },\n [\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles,\n labels.missingDependency,\n labels.uploadFailed\n ]\n );\n\n const cancel = useCallback(() => {\n uppyRef.current?.cancelAll();\n }, []);\n\n return {\n open: () => inputRef.current?.click(),\n start,\n cancel,\n inputProps: {\n ref: inputRef,\n type: \"file\",\n accept: allowedFileTypes.join(\",\"),\n multiple: maxNumberOfFiles > 1,\n disabled: disabled || busy,\n onChange: (event) => {\n if (event.target.files) void start(event.target.files);\n }\n },\n dropProps: {\n onDragOver: (event) => {\n // `preventDefault` là BẮT BUỘC — thiếu nó trình duyệt mở tệp thay vì bắn `onDrop`.\n event.preventDefault();\n if (!disabled && !busy) setDragging(true);\n },\n onDragLeave: () => setDragging(false),\n onDrop: (event) => {\n event.preventDefault();\n setDragging(false);\n if (!disabled && !busy && event.dataTransfer.files.length > 0) void start(event.dataTransfer.files);\n }\n },\n dragging,\n busy,\n percent\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/uploader/use-file-upload.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { DEFAULT_FILE_UPLOAD_LABELS, type FileUploadLogicLabels } from \"./file-upload-constants.js\";\n\n/**\n * Phần MÁY MÓC của việc tải tệp, tách khỏi mọi quyết định về hình dạng.\n *\n * ── VÌ SAO TÁCH ───────────────────────────────────────────────────────────────────────────────\n * `FileUpload` vẽ một khung kéo-thả viền đứt nét — đúng cho tài liệu nhiều tệp, **sai** cho một ô\n * ảnh đại diện 56px. Nhưng phần bên dưới thì hai ca cần y hệt nhau: hàng đợi Uppy, tiến độ theo\n * byte, bốn cạm bẫy của `@uppy/xhr-upload`, và việc xoá `input.value` sau mỗi lượt.\n *\n * Đường cắt này không phải một trừu tượng mới bịa ra: trong `file-upload.tsx` bản 1.2.0, toàn bộ\n * khối `start` + `useEffect` dọn dẹp + ba `useState` **không chạm một dòng JSX nào**, còn JSX thì\n * chỉ đọc `busy`/`percent`/`dragging`. Đây là việc đặt tên cho một ranh giới đã có sẵn.\n *\n * ⚠ `inputProps` và `dropProps` là **prop-getter**: chỗ gọi phải rải chúng lên đúng phần tử. Thiếu\n * `<input {...inputProps} />` thì `open()` không làm gì, và triệu chứng là một nút bấm im lặng.\n * `dropProps` đã tự gọi `preventDefault` — lỗi kinh điển khi để app tự viết.\n */\n/**\n * Bề mặt Uppy mà component này dùng — khai TAY, không import kiểu thật.\n *\n * Hai lý do, cả hai đều thực dụng: kiểu thật của `Uppy` mang generic `<Meta, Body>` và đổi hình\n * dạng giữa các bản major; và `@uppy/core` là peer TUỲ CHỌN, nên `import type` từ nó sẽ làm\n * `tsc` của một app KHÔNG cài Uppy báo đỏ ở một file họ không hề dùng.\n *\n * Cái giá: nếu Uppy đổi tên một phương thức thì lỗi lộ ra lúc CHẠY, không lúc biên dịch. Bù lại\n * bằng `scripts/smoke-entries.mjs` — nó nạp thật entry này ở mỗi lần publish.\n */\ninterface UppyLike {\n addFile: (file: { name: string; type: string; data: File }) => void;\n upload: () => Promise<unknown>;\n cancelAll: () => void;\n destroy: () => void;\n on: (event: string, handler: (...args: never[]) => void) => void;\n use: (plugin: unknown, options?: unknown) => unknown;\n}\n\n/**\n * Nạp Uppy LƯỜI — chỉ khi người dùng thật sự chọn tệp.\n *\n * `import()` động giữ Uppy ra khỏi bundle của lượt tải trang đầu tiên. Với một màn hồ sơ mà đa số\n * người mở ra chỉ để xem, đó là toàn bộ chi phí của Uppy bị hoãn tới lúc nó thật sự có việc.\n *\n * ⚠ Hai gói này là **peer TUỲ CHỌN**. App không cài mà lại render component này thì `import()` sẽ\n * hỏng — nên lỗi được bắt và trả về qua `onError` với thông điệp nói đúng tên gói phải cài, thay vì\n * một `ChunkLoadError` không gợi gì.\n */\nasync function loadUppy(): Promise<{ Uppy: new (o: unknown) => UppyLike; XHRUpload: unknown } | null> {\n try {\n const [core, xhr] = await Promise.all([import(\"@uppy/core\"), import(\"@uppy/xhr-upload\")]);\n return {\n // `as unknown as` là bắt buộc: kiểu thật của Uppy có generic `<Meta, Body>` mà `UppyLike`\n // cố ý không mang. Xem ghi chú ở `UppyLike` về việc vì sao gói này không dùng kiểu thật.\n Uppy: (core.default ?? (core as unknown as { Uppy: unknown }).Uppy) as unknown as new (o: unknown) => UppyLike,\n XHRUpload: xhr.default ?? xhr\n };\n } catch {\n return null;\n }\n}\n\nexport interface UseFileUploadOptions<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n /** Tên trường trong `FormData`. Mặc định `\"file\"`. */\n fieldName?: string;\n maxFileSize: number;\n allowedFileTypes: string[];\n maxNumberOfFiles?: number;\n onUploaded: (result: FileUploadResult<T>) => void;\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * CHỈ hai chuỗi mà phần máy móc thật sự dùng.\n *\n * Cố ý KHÔNG nhận cả `FileUploadLabels`: ba chuỗi còn lại (`dropHint`/`choose`/`uploading`) là\n * của bố cục, và để chúng lọt vào đây là mời người viết truyền một prop không bao giờ chạy.\n */\n labels?: Partial<FileUploadLogicLabels>;\n disabled?: boolean;\n}\n\n/** Kết quả một tệp tải xong: thân phản hồi JSON của endpoint app. */\nexport interface FileUploadResult<T = unknown> {\n body: T;\n fileName: string;\n}\n\nexport interface UseFileUploadResult {\n /** Mở hộp chọn tệp. Cần `<input {...inputProps} />` có mặt trong cây. */\n open: () => void;\n /** Bắt đầu tải một danh sách tệp có sẵn (dùng cho kéo-thả tự xử lý). */\n start: (files: FileList | File[]) => void;\n /** Huỷ lượt đang chạy. */\n cancel: () => void;\n inputProps: {\n ref: React.RefObject<HTMLInputElement | null>;\n type: \"file\";\n accept: string;\n multiple: boolean;\n disabled: boolean;\n onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;\n };\n dropProps: {\n onDragOver: (event: React.DragEvent) => void;\n onDragLeave: () => void;\n onDrop: (event: React.DragEvent) => void;\n };\n /** Có tệp đang được rê TRÊN khối này. */\n dragging: boolean;\n busy: boolean;\n /** 0–100. */\n percent: number;\n}\n\nexport function useFileUpload<T = unknown>(options: UseFileUploadOptions<T>): UseFileUploadResult {\n const {\n endpoint,\n fieldName = \"file\",\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles = 1,\n onUploaded,\n onError,\n disabled = false\n } = options;\n const labels = { ...DEFAULT_FILE_UPLOAD_LABELS, ...options.labels };\n\n const inputRef = useRef<HTMLInputElement>(null);\n const [busy, setBusy] = useState(false);\n const [percent, setPercent] = useState(0);\n const [dragging, setDragging] = useState(false);\n\n /*\n * `onUploaded`/`onError` đi qua ref: chỗ gọi thường truyền hàm mới ở mỗi lần render, và để chúng\n * trong mảng phụ thuộc là dựng lại Uppy sau mỗi phím gõ vào biểu mẫu bên cạnh.\n */\n const handlers = useRef({ onUploaded, onError });\n handlers.current = { onUploaded, onError };\n\n const uppyRef = useRef<UppyLike | null>(null);\n useEffect(() => {\n return () => {\n uppyRef.current?.destroy();\n uppyRef.current = null;\n };\n }, []);\n\n const start = useCallback(\n async (files: FileList | File[]) => {\n const list = Array.from(files).slice(0, maxNumberOfFiles);\n if (list.length === 0) return;\n\n setBusy(true);\n setPercent(0);\n try {\n const mod = await loadUppy();\n if (!mod) {\n handlers.current.onError?.({ message: labels.missingDependency });\n return;\n }\n\n /*\n * Dựng MỘT Uppy cho MỘT lượt tải rồi huỷ. Giữ lại một instance dài hạn nghe hấp dẫn hơn,\n * nhưng Uppy nhớ cả những tệp đã hỏng ở lượt trước — chọn lại đúng tệp đó sẽ bị chặn vì\n * \"trùng\", và triệu chứng là một nút bấm không làm gì.\n */\n uppyRef.current?.destroy();\n const uppy = new mod.Uppy({\n autoProceed: false,\n restrictions: { maxNumberOfFiles, maxFileSize, allowedFileTypes }\n });\n uppyRef.current = uppy;\n\n /*\n * Chỗ giữ lỗi của lượt gọi gần nhất. `upload-error` của Uppy KHÔNG mang thân phản hồi —\n * thứ nó ném là `new NetworkError(xhr.statusText, xhr)`, mà trên HTTP/2 `statusText` là\n * chuỗi RỖNG. Nên `code` phải được nhặt ở `onAfterResponse`, nơi còn nhìn thấy `xhr`.\n */\n const lastError: { code?: string; status?: number; message?: string } = {};\n\n uppy.use(mod.XHRUpload, {\n endpoint,\n fieldName,\n method: \"POST\",\n // Cookie phiên là cách Comitor xác thực mọi route tự viết — thiếu dòng này là 401.\n withCredentials: true,\n\n /*\n * ⚠ CHỈ THỬ LẠI LỖI 5xx.\n *\n * Mặc định của Uppy là `shouldRetry = () => true` với `retries = 3` (đọc\n * `@uppy/core/lib/utils/fetcher.js`), tức MỌI phản hồi không-2xx bị gửi lại ba lần. Hậu\n * quả đo được bằng chính hợp đồng của Comitor:\n * · một ảnh sai định dạng 2 MB → BỐN lượt POST 2 MB, tất cả đều biết chắc sẽ hỏng;\n * · một **429** của `lib/api/rate-limit.ts` cũng bị thử lại, mỗi lần tiêu thêm một đơn\n * vị hạn mức và bỏ qua `Retry-After` — client tự đào sâu cái hố nó vừa rơi vào.\n * 4xx là \"yêu cầu sai, gửi lại cũng thế\"; chỉ 5xx mới đáng thử lại.\n */\n shouldRetry: (xhr: XMLHttpRequest) => xhr.status >= 500,\n\n /*\n * ⚠ `getResponseError` KHÔNG TỒN TẠI — nó chỉ còn trong một dòng comment cũ ở\n * `@uppy/xhr-upload/lib/index.js`, không có trong `XhrUploadOpts`, và không được đọc ở\n * đâu cả. Truyền nó vào là viết một callback không bao giờ chạy. Đường thật là\n * `onAfterResponse`, thứ XHRUpload có forward xuống fetcher.\n */\n onAfterResponse: (xhr: XMLHttpRequest) => {\n if (xhr.status >= 200 && xhr.status < 300) return;\n lastError.status = xhr.status;\n try {\n const parsed = JSON.parse(xhr.responseText) as { error?: { code?: string; message?: string } };\n lastError.code = parsed.error?.code;\n lastError.message = parsed.error?.message;\n } catch {\n // Phản hồi không phải JSON (proxy trả HTML, 413 của tầng mạng…). Giữ `status` là đủ.\n }\n },\n\n /*\n * ⚠ KHÔNG gửi meta kèm tệp.\n *\n * Mặc định `allowedMetaFields: true` ghi MỌI meta vào cùng `FormData`, và nó ghi TRƯỚC\n * khi append tệp. Route của Comitor đọc `form.get(\"file\")` — hàm đó trả phần tử ĐẦU TIÊN\n * cùng tên. Nên chỉ cần một khoá meta tên `file` là route nhận về một chuỗi,\n * `file instanceof File` thành false, và nó trả 400 \"Thiếu tệp ảnh\" trong khi tệp vẫn\n * nằm nguyên trong request. Mảng rỗng đóng hẳn đường đó.\n */\n allowedMetaFields: []\n });\n\n uppy.on(\"progress\", ((value: number) => setPercent(value)) as never);\n uppy.on(\"upload-success\", ((file: { name?: string } | undefined, response: { body?: T }) => {\n handlers.current.onUploaded({ body: response?.body as T, fileName: file?.name ?? \"\" });\n }) as never);\n uppy.on(\"upload-error\", ((_file: unknown, error: Error) => {\n // `lastError` là nguồn duy nhất có `code` — xem `onAfterResponse` ở trên.\n handlers.current.onError?.({\n code: lastError.code,\n status: lastError.status,\n message: lastError.message ?? error?.message ?? labels.uploadFailed\n });\n }) as never);\n /*\n * `restriction-failed` là đường RIÊNG, không đi qua `upload-error`. Bỏ sót nó thì một tệp\n * quá lớn hay sai định dạng bị Uppy chặn trong im lặng — người dùng bấm rồi không thấy gì.\n */\n uppy.on(\"restriction-failed\", ((_file: unknown, error: Error) => {\n handlers.current.onError?.({ message: error?.message ?? labels.uploadFailed });\n }) as never);\n\n /*\n * ⚠ `addFile` BÁO LỖI HẠN CHẾ HAI LẦN, và lần thứ hai là một cú ném.\n *\n * Đọc `@uppy/core@6.0.0` (`Uppy.js:614-619`): nó `#informAndEmit(restrictionErrors)` —\n * tức bắn `restriction-failed` mà ta đã bắt ở trên — RỒI `throw errors[0]`. Vì `start`\n * được gọi bằng `void start(...)` ở `inputProps.onChange` và `dropProps.onDrop`, cú ném\n * ấy thành **unhandled promise rejection**: chọn nhầm định dạng tệp — một thao tác hoàn\n * toàn bình thường — in một lỗi đỏ ra console mỗi lần.\n *\n * Đó không phải chuyện thẩm mỹ. Một kênh cảnh báo lúc nào cũng kêu là kênh sẽ bị bỏ qua,\n * kể cả hôm nó kêu vì một lỗi thật. Nuốt ĐÚNG lỗi hạn chế (`isRestriction`) vì nó đã được\n * báo qua đường sự kiện; mọi lỗi khác vẫn ném tiếp để `catch` bên ngoài xử lý.\n */\n for (const file of list) {\n try {\n uppy.addFile({ name: file.name, type: file.type, data: file });\n } catch (error) {\n if (!(error as { isRestriction?: boolean })?.isRestriction) throw error;\n }\n }\n await uppy.upload();\n } finally {\n setBusy(false);\n setPercent(0);\n /*\n * Xoá giá trị input: chọn LẠI cùng một tệp sau một lần lỗi sẽ KHÔNG bắn `change` nếu giá\n * trị không đổi — và triệu chứng là thử lại im lặng không làm gì.\n */\n if (inputRef.current) inputRef.current.value = \"\";\n }\n },\n [\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles,\n labels.missingDependency,\n labels.uploadFailed\n ]\n );\n\n const cancel = useCallback(() => {\n uppyRef.current?.cancelAll();\n }, []);\n\n return {\n open: () => inputRef.current?.click(),\n start,\n cancel,\n inputProps: {\n ref: inputRef,\n type: \"file\",\n accept: allowedFileTypes.join(\",\"),\n multiple: maxNumberOfFiles > 1,\n disabled: disabled || busy,\n onChange: (event) => {\n if (event.target.files) void start(event.target.files);\n }\n },\n dropProps: {\n onDragOver: (event) => {\n // `preventDefault` là BẮT BUỘC — thiếu nó trình duyệt mở tệp thay vì bắn `onDrop`.\n event.preventDefault();\n if (!disabled && !busy) setDragging(true);\n },\n onDragLeave: () => setDragging(false),\n onDrop: (event) => {\n event.preventDefault();\n setDragging(false);\n if (!disabled && !busy && event.dataTransfer.files.length > 0) void start(event.dataTransfer.files);\n }\n },\n dragging,\n busy,\n percent\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED