@comitor/ui 3.12.1 → 3.13.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.
- package/dist/components/composite/data-table.d.ts +2 -5
- package/dist/components/composite/data-table.js +9 -33
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/form-section.js +5 -5
- package/dist/components/composite/form-section.js.map +1 -1
- package/dist/components/composite/gantt.js +47 -28
- package/dist/components/composite/gantt.js.map +1 -1
- package/dist/components/composite/inline-edit.js +33 -38
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/search-filter-bar.js +2 -1
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +17 -15
- package/dist/components/composite/status-config.js +6 -2
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/ui/breadcrumb.js +1 -4
- package/dist/components/ui/breadcrumb.js.map +1 -1
- package/dist/components/ui/button.js +7 -7
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/circular-progress.d.ts +9 -4
- package/dist/components/ui/circular-progress.js +58 -24
- package/dist/components/ui/circular-progress.js.map +1 -1
- package/dist/components/ui/progress.d.ts +6 -0
- package/dist/components/ui/progress.js +2 -2
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/table.d.ts +7 -13
- package/dist/components/ui/table.js +1 -1
- package/dist/components/ui/table.js.map +1 -1
- package/dist/components/ui/tabs-variants.js +2 -2
- package/dist/components/ui/tabs-variants.js.map +1 -1
- package/dist/components/ui/timeline.js +19 -14
- package/dist/components/ui/timeline.js.map +1 -1
- package/dist/i18n/namespaces/file-upload.js +5 -1
- package/dist/i18n/namespaces/file-upload.js.map +1 -1
- package/dist/i18n/namespaces/image-upload-field.js +10 -0
- package/dist/i18n/namespaces/image-upload-field.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/uploader/file-upload-constants.d.ts +26 -2
- package/dist/uploader/file-upload-constants.js.map +1 -1
- package/dist/uploader/file-upload.d.ts +18 -1
- package/dist/uploader/file-upload.js +50 -5
- package/dist/uploader/file-upload.js.map +1 -1
- package/dist/uploader/image-upload-field.d.ts +31 -2
- package/dist/uploader/image-upload-field.js +139 -8
- package/dist/uploader/image-upload-field.js.map +1 -1
- package/dist/uploader/index.d.ts +2 -2
- package/dist/uploader/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
4
|
import { Button } from "../components/ui/button.js";
|
|
5
|
+
import { Cropper, CropperArea, CropperImage } from "../components/ui/cropper.js";
|
|
4
6
|
import { Progress } from "../components/ui/progress.js";
|
|
7
|
+
import {
|
|
8
|
+
ResponsiveDialog,
|
|
9
|
+
ResponsiveDialogClose,
|
|
10
|
+
ResponsiveDialogContent,
|
|
11
|
+
ResponsiveDialogDescription,
|
|
12
|
+
ResponsiveDialogFooter,
|
|
13
|
+
ResponsiveDialogHeader,
|
|
14
|
+
ResponsiveDialogTitle
|
|
15
|
+
} from "../components/ui/responsive-dialog.js";
|
|
16
|
+
import { Slider } from "../components/ui/slider.js";
|
|
5
17
|
import { useComitorLabels } from "../i18n/locale-provider.js";
|
|
18
|
+
import { imageUploadFieldEn } from "../i18n/namespaces/image-upload-field.js";
|
|
6
19
|
import { cn } from "../lib/cn.js";
|
|
7
20
|
import { useFileUpload } from "./use-file-upload.js";
|
|
21
|
+
const OUTPUT_EXTENSION = {
|
|
22
|
+
"image/jpeg": "jpg",
|
|
23
|
+
"image/png": "png",
|
|
24
|
+
"image/webp": "webp"
|
|
25
|
+
};
|
|
8
26
|
function ImageUploadField({
|
|
9
27
|
endpoint,
|
|
10
28
|
fieldName,
|
|
@@ -18,9 +36,24 @@ function ImageUploadField({
|
|
|
18
36
|
onUploaded,
|
|
19
37
|
onError,
|
|
20
38
|
labels,
|
|
39
|
+
crop,
|
|
40
|
+
showUploadButton = true,
|
|
41
|
+
showRemoveButton = true,
|
|
21
42
|
className
|
|
22
43
|
}) {
|
|
23
44
|
const text = useComitorLabels("imageUploadField", labels);
|
|
45
|
+
const cropOptions = crop === true ? {} : crop || null;
|
|
46
|
+
const aspectRatio = cropOptions?.aspectRatio ?? 1;
|
|
47
|
+
const [source, setSource] = useState(null);
|
|
48
|
+
const [zoom, setZoom] = useState(1);
|
|
49
|
+
const area = useRef(null);
|
|
50
|
+
const picture = useRef(null);
|
|
51
|
+
useEffect(
|
|
52
|
+
() => () => {
|
|
53
|
+
if (source) URL.revokeObjectURL(source.url);
|
|
54
|
+
},
|
|
55
|
+
[source]
|
|
56
|
+
);
|
|
24
57
|
const upload = useFileUpload({
|
|
25
58
|
endpoint,
|
|
26
59
|
...fieldName ? { fieldName } : {},
|
|
@@ -33,29 +66,127 @@ function ImageUploadField({
|
|
|
33
66
|
disabled: !editable || busy
|
|
34
67
|
});
|
|
35
68
|
const blocked = !editable || busy || upload.busy;
|
|
69
|
+
function pick(file) {
|
|
70
|
+
if (!file?.type.startsWith("image/")) return;
|
|
71
|
+
area.current = null;
|
|
72
|
+
setZoom(1);
|
|
73
|
+
setSource({ url: URL.createObjectURL(file), name: file.name });
|
|
74
|
+
}
|
|
75
|
+
async function saveCrop() {
|
|
76
|
+
const image = picture.current;
|
|
77
|
+
if (!image || !area.current || !source) return;
|
|
78
|
+
const outputSize = cropOptions?.outputSize ?? 512;
|
|
79
|
+
const type = cropOptions?.outputType ?? "image/jpeg";
|
|
80
|
+
const width = aspectRatio >= 1 ? outputSize : Math.round(outputSize * aspectRatio);
|
|
81
|
+
const height = Math.round(width / aspectRatio);
|
|
82
|
+
const { x, y, width: w, height: h } = area.current;
|
|
83
|
+
const canvas = document.createElement("canvas");
|
|
84
|
+
canvas.width = width;
|
|
85
|
+
canvas.height = height;
|
|
86
|
+
canvas.getContext("2d")?.drawImage(image, x, y, w, h, 0, 0, width, height);
|
|
87
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, type, cropOptions?.quality ?? 0.9));
|
|
88
|
+
if (!blob) return;
|
|
89
|
+
const extension = OUTPUT_EXTENSION[blob.type] ?? "png";
|
|
90
|
+
const stem = source.name.replace(/\.[^.]+$/, "") || "image";
|
|
91
|
+
setSource(null);
|
|
92
|
+
upload.start([new File([blob], `${stem}.${extension}`, { type: blob.type })]);
|
|
93
|
+
}
|
|
94
|
+
const inputProps = cropOptions ? {
|
|
95
|
+
...upload.inputProps,
|
|
96
|
+
onChange: (event) => {
|
|
97
|
+
pick(event.target.files?.[0]);
|
|
98
|
+
event.target.value = "";
|
|
99
|
+
}
|
|
100
|
+
} : upload.inputProps;
|
|
101
|
+
const dropProps = cropOptions ? {
|
|
102
|
+
...upload.dropProps,
|
|
103
|
+
onDrop: (event) => {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
upload.dropProps.onDragLeave();
|
|
106
|
+
if (!blocked) pick(event.dataTransfer.files[0]);
|
|
107
|
+
}
|
|
108
|
+
} : upload.dropProps;
|
|
36
109
|
if (!editable) return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-4", className), children: preview });
|
|
37
110
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2", className), children: [
|
|
38
|
-
/* @__PURE__ */ jsx("input", { ...
|
|
111
|
+
/* @__PURE__ */ jsx("input", { ...inputProps, className: "sr-only" }),
|
|
39
112
|
/* @__PURE__ */ jsxs(
|
|
40
113
|
"div",
|
|
41
114
|
{
|
|
42
|
-
...
|
|
115
|
+
...dropProps,
|
|
43
116
|
className: cn(
|
|
44
117
|
"relative flex flex-wrap items-center gap-4 rounded-lg border-2 border-dashed p-2 transition-colors",
|
|
45
118
|
upload.dragging ? "border-primary-ink" : "border-transparent"
|
|
46
119
|
),
|
|
47
120
|
children: [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
121
|
+
/* @__PURE__ */ jsx(
|
|
122
|
+
"button",
|
|
123
|
+
{
|
|
124
|
+
type: "button",
|
|
125
|
+
...showUploadButton ? { tabIndex: -1, "aria-hidden": true } : { "aria-label": upload.busy ? text.uploading : text.choose },
|
|
126
|
+
disabled: blocked,
|
|
127
|
+
onClick: upload.open,
|
|
128
|
+
className: "shrink-0 cursor-pointer rounded-md transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-default disabled:hover:opacity-100",
|
|
129
|
+
children: preview
|
|
130
|
+
}
|
|
131
|
+
),
|
|
132
|
+
showUploadButton || showRemoveButton && hasImage ? /* @__PURE__ */ jsxs("div", { className: cn("flex flex-wrap items-center gap-2", blocked && "opacity-60"), children: [
|
|
133
|
+
showUploadButton ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", loading: upload.busy, onClick: upload.open, children: upload.busy ? text.uploading : text.choose }) : null,
|
|
134
|
+
showRemoveButton && hasImage ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: blocked, onClick: onRemove, children: text.remove }) : null
|
|
135
|
+
] }) : null,
|
|
53
136
|
upload.dragging ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-card/90", children: /* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: text.dropPrompt }) }) : null
|
|
54
137
|
]
|
|
55
138
|
}
|
|
56
139
|
),
|
|
57
140
|
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: text.hint }),
|
|
58
|
-
upload.busy ? /* @__PURE__ */ jsx(Progress, { value: upload.percent, "aria-label": text.uploading }) : null
|
|
141
|
+
upload.busy ? /* @__PURE__ */ jsx(Progress, { value: upload.percent, "aria-label": text.uploading }) : null,
|
|
142
|
+
cropOptions ? /* @__PURE__ */ jsx(
|
|
143
|
+
ResponsiveDialog,
|
|
144
|
+
{
|
|
145
|
+
open: source !== null,
|
|
146
|
+
onOpenChange: (open) => {
|
|
147
|
+
if (!open) setSource(null);
|
|
148
|
+
},
|
|
149
|
+
children: /* @__PURE__ */ jsxs(ResponsiveDialogContent, { children: [
|
|
150
|
+
/* @__PURE__ */ jsxs(ResponsiveDialogHeader, { children: [
|
|
151
|
+
/* @__PURE__ */ jsx(ResponsiveDialogTitle, { children: text.cropTitle ?? imageUploadFieldEn.cropTitle }),
|
|
152
|
+
/* @__PURE__ */ jsx(ResponsiveDialogDescription, { children: text.cropDescription ?? imageUploadFieldEn.cropDescription })
|
|
153
|
+
] }),
|
|
154
|
+
source ? /* @__PURE__ */ jsxs(
|
|
155
|
+
Cropper,
|
|
156
|
+
{
|
|
157
|
+
aspectRatio,
|
|
158
|
+
shape: cropOptions.shape ?? "circle",
|
|
159
|
+
zoom,
|
|
160
|
+
onZoomChange: setZoom,
|
|
161
|
+
className: "min-h-72 rounded-xl",
|
|
162
|
+
"data-vaul-no-drag": "",
|
|
163
|
+
onCropAreaChange: (_, pixels) => {
|
|
164
|
+
area.current = pixels;
|
|
165
|
+
},
|
|
166
|
+
children: [
|
|
167
|
+
/* @__PURE__ */ jsx(CropperImage, { ref: picture, src: source.url, alt: "" }),
|
|
168
|
+
/* @__PURE__ */ jsx(CropperArea, {})
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
) : null,
|
|
172
|
+
/* @__PURE__ */ jsx(
|
|
173
|
+
Slider,
|
|
174
|
+
{
|
|
175
|
+
min: 1,
|
|
176
|
+
max: 3,
|
|
177
|
+
step: 0.01,
|
|
178
|
+
value: [zoom],
|
|
179
|
+
onValueChange: ([next]) => setZoom(next ?? 1),
|
|
180
|
+
"aria-label": text.cropZoom ?? imageUploadFieldEn.cropZoom
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
/* @__PURE__ */ jsxs(ResponsiveDialogFooter, { children: [
|
|
184
|
+
/* @__PURE__ */ jsx(ResponsiveDialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", children: text.cropCancel ?? imageUploadFieldEn.cropCancel }) }),
|
|
185
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => void saveCrop(), children: text.cropSave ?? imageUploadFieldEn.cropSave })
|
|
186
|
+
] })
|
|
187
|
+
] })
|
|
188
|
+
}
|
|
189
|
+
) : null
|
|
59
190
|
] });
|
|
60
191
|
}
|
|
61
192
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/uploader/image-upload-field.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { ImageUploadFieldLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải MỘT tấm ảnh gắn với một thực thể — ảnh đại diện người dùng, logo workspace, ảnh nhân sự.\n *\n * ── VÌ SAO KHÔNG DÙNG `FileUpload` CHO CA NÀY ────────────────────────────────────────────────\n * `FileUpload` vẽ một khung kéo-thả viền đứt nét chiếm cả bề rộng thẻ. Đó là bố cục ĐÚNG khi người\n * dùng sắp thả vào đó nhiều tài liệu, và là bố cục SAI cho một tấm ảnh vuông 56px: cái khung to\n * hơn chính thứ nó đang sửa, và nó đứng tách khỏi tấm ảnh nên người dùng phải tự ghép \"hộp bên\n * phải này là để đổi ảnh bên trái\".\n *\n * Ở đây bố cục là: **ảnh · nút · nút · một dòng chú thích** — khuôn quen thuộc của ô ảnh đại diện,\n * người dùng nhận ra ngay mà không phải đọc.\n *\n * ── KÉO-THẢ VẪN CÒN, NHƯNG VÔ HÌNH CHO TỚI KHI CẦN ─────────────────────────────────────────\n * Cả hàng là vùng nhận thả. Viền đứt nét CÓ MẶT ở mọi trạng thái nhưng `border-transparent` lúc\n * nghỉ — cùng độ dày, nên nó không chiếm một pixel nào và không có cú nhảy bố cục khi tệp được rê\n * qua. Đây là lý do `border-2` nằm ở lớp ngoài cùng chứ không được thêm vào lúc `dragging`.\n *\n * ⚠ Vì không có khung thường trực, `labels.hint` là chỗ DUY NHẤT người dùng học được rằng kéo-thả\n * chạy được. Rút gọn nó thành mỗi \"tối đa 2 MB\" là bỏ đi tính năng trong mắt người dùng.\n */\nexport interface ImageUploadFieldProps<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n fieldName?: string;\n maxFileSize: number;\n allowedFileTypes: string[];\n /**\n * Ảnh xem trước — một `ReactNode`, KHÔNG phải một chuỗi `src`.\n *\n * Gói không được biết `LetterAvatar` rơi về initials kiểu gì, tone màu nào, tròn hay vuông. Nhận\n * cả node còn cho một cái nữa: ảnh bìa 16:9 cắm vừa cùng component này mà không thêm prop nào.\n */\n preview: ReactNode;\n /** Có ảnh thật hay không — quyết định nút gỡ ảnh (`labels.remove`) có bấm được không. */\n hasImage?: boolean;\n /**\n * `false` = chỉ hiện ảnh, không nút, không vùng thả.\n *\n * Dành cho người xem không có quyền sửa. Hiện một khung tải bị vô hiệu với họ là mời họ bấm vào\n * một thứ sẽ không bao giờ chạy.\n */\n editable?: boolean;\n /** Đang chạy một thao tác khác (ví dụ gỡ ảnh) — khoá cả hai nút. */\n busy?: boolean;\n onRemove?: () => void;\n onUploaded: (result: FileUploadResult<T>) => void;\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * Nhãn hiển thị. Không truyền thì theo namespace `imageUploadField` của `ComitorLocaleProvider`\n * (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.\n */\n labels?: Partial<ImageUploadFieldLabels>;\n className?: string;\n}\n\nexport function ImageUploadField<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n preview,\n hasImage = false,\n editable = true,\n busy = false,\n onRemove,\n onUploaded,\n onError,\n labels,\n className\n}: ImageUploadFieldProps<T>) {\n const text = useComitorLabels(\"imageUploadField\", labels);\n\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles: 1,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled: !editable || busy\n });\n\n const blocked = !editable || busy || upload.busy;\n\n /*\n * Không sửa được thì chỉ còn tấm ảnh. Không vùng thả, không nút, không `<input>` — không có gì\n * để người dùng bấm nhầm rồi tự hỏi vì sao không có chuyện gì xảy ra.\n */\n if (!editable) return <div className={cn(\"flex items-center gap-4\", className)}>{preview}</div>;\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...upload.inputProps} className=\"sr-only\" />\n\n {/*\n ⚠ `border-2 border-dashed` có mặt ở CẢ HAI trạng thái; chỉ MÀU đổi. Thêm viền lúc `dragging`\n sẽ đẩy nội dung 2px và làm cả hàng giật đúng lúc người dùng đang nhắm chỗ thả.\n */}\n <div\n {...upload.dropProps}\n className={cn(\n \"relative flex flex-wrap items-center gap-4 rounded-lg border-2 border-dashed p-2 transition-colors\",\n upload.dragging ? \"border-primary-ink\" : \"border-transparent\"\n )}\n >\n {preview}\n\n <div className={cn(\"flex flex-wrap items-center gap-2\", blocked && \"opacity-60\")}>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" loading={upload.busy} onClick={upload.open}>\n {upload.busy ? text.uploading : text.choose}\n </Button>\n {/* Luôn render, chỉ `disabled` khi chưa có ảnh — bố cục không nhảy một nút. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={!hasImage || blocked} onClick={onRemove}>\n {text.remove}\n </Button>\n </div>\n\n {/*\n Chữ `dropPrompt` phủ lên cả khối khi đang rê tệp qua. `pointer-events-none` là BẮT\n BUỘC: thiếu nó, lớp phủ tự nuốt sự kiện `drop` của khối bên dưới và việc thả im lặng\n không làm gì.\n */}\n {upload.dragging ? (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-card/90\">\n <span className=\"font-medium text-sm\">{text.dropPrompt}</span>\n </div>\n ) : null}\n </div>\n\n <p className=\"text-muted-foreground text-xs\">{text.hint}</p>\n\n {/* Thanh tiến độ chỉ hiện khi đang tải — một thanh 0% đứng yên trông như hỏng. */}\n {upload.busy ? <Progress value={upload.percent} aria-label={text.uploading} /> : null}\n </div>\n );\n}\n"],"mappings":";AAoGwB,cAmBhB,YAnBgB;AAjGxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AAEnB,SAAgC,qBAAqB;AAyD9C,SAAS,iBAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,OAAO,iBAAiB,oBAAoB,MAAM;AAExD,QAAM,SAAS,cAAiB;AAAA,IAC9B;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,kBAAkB;AAAA,IAClB;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,QAAQ,EAAE,cAAc,KAAK,cAAc,mBAAmB,KAAK,kBAAkB;AAAA,IACrF,UAAU,CAAC,YAAY;AAAA,EACzB,CAAC;AAED,QAAM,UAAU,CAAC,YAAY,QAAQ,OAAO;AAM5C,MAAI,CAAC,SAAU,QAAO,oBAAC,SAAI,WAAW,GAAG,2BAA2B,SAAS,GAAI,mBAAQ;AAEzF,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,wBAAC,WAAO,GAAG,OAAO,YAAY,WAAU,WAAU;AAAA,IAMlD;AAAA,MAAC;AAAA;AAAA,QACE,GAAG,OAAO;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA,OAAO,WAAW,uBAAuB;AAAA,QAC3C;AAAA,QAEC;AAAA;AAAA,UAED,qBAAC,SAAI,WAAW,GAAG,qCAAqC,WAAW,YAAY,GAC7E;AAAA,gCAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,OAAO,MAAM,SAAS,OAAO,MACrF,iBAAO,OAAO,KAAK,YAAY,KAAK,QACvC;AAAA,YAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,UAAU,CAAC,YAAY,SAAS,SAAS,UACtF,eAAK,QACR;AAAA,aACF;AAAA,UAOC,OAAO,WACN,oBAAC,SAAI,WAAU,+FACb,8BAAC,UAAK,WAAU,uBAAuB,eAAK,YAAW,GACzD,IACE;AAAA;AAAA;AAAA,IACN;AAAA,IAEA,oBAAC,OAAE,WAAU,iCAAiC,eAAK,MAAK;AAAA,IAGvD,OAAO,OAAO,oBAAC,YAAS,OAAO,OAAO,SAAS,cAAY,KAAK,WAAW,IAAK;AAAA,KACnF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/uploader/image-upload-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Cropper, CropperArea, type CropperAreaData, CropperImage } from \"../components/ui/cropper.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport {\n ResponsiveDialog,\n ResponsiveDialogClose,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle\n} from \"../components/ui/responsive-dialog.js\";\nimport { Slider } from \"../components/ui/slider.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { imageUploadFieldEn } from \"../i18n/namespaces/image-upload-field.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { ImageUploadFieldLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải MỘT tấm ảnh gắn với một thực thể — ảnh đại diện người dùng, logo workspace, ảnh nhân sự.\n *\n * ── VÌ SAO KHÔNG DÙNG `FileUpload` CHO CA NÀY ────────────────────────────────────────────────\n * `FileUpload` vẽ một khung kéo-thả viền đứt nét chiếm cả bề rộng thẻ. Đó là bố cục ĐÚNG khi người\n * dùng sắp thả vào đó nhiều tài liệu, và là bố cục SAI cho một tấm ảnh vuông 56px: cái khung to\n * hơn chính thứ nó đang sửa, và nó đứng tách khỏi tấm ảnh nên người dùng phải tự ghép \"hộp bên\n * phải này là để đổi ảnh bên trái\".\n *\n * Ở đây bố cục là: **ảnh · nút · nút · một dòng chú thích** — khuôn quen thuộc của ô ảnh đại diện,\n * người dùng nhận ra ngay mà không phải đọc.\n *\n * ── KÉO-THẢ VẪN CÒN, NHƯNG VÔ HÌNH CHO TỚI KHI CẦN ─────────────────────────────────────────\n * Cả hàng là vùng nhận thả. Viền đứt nét CÓ MẶT ở mọi trạng thái nhưng `border-transparent` lúc\n * nghỉ — cùng độ dày, nên nó không chiếm một pixel nào và không có cú nhảy bố cục khi tệp được rê\n * qua. Đây là lý do `border-2` nằm ở lớp ngoài cùng chứ không được thêm vào lúc `dragging`.\n *\n * ⚠ Vì không có khung thường trực, `labels.hint` là chỗ DUY NHẤT người dùng học được rằng kéo-thả\n * chạy được. Rút gọn nó thành mỗi \"tối đa 2 MB\" là bỏ đi tính năng trong mắt người dùng.\n */\nexport interface ImageUploadFieldProps<T = unknown> {\n /** Đường dẫn endpoint nhận multipart, ví dụ `/api/account/avatar`. */\n endpoint: string;\n fieldName?: string;\n maxFileSize: number;\n allowedFileTypes: string[];\n /**\n * Ảnh xem trước — một `ReactNode`, KHÔNG phải một chuỗi `src`.\n *\n * Gói không được biết `LetterAvatar` rơi về initials kiểu gì, tone màu nào, tròn hay vuông. Nhận\n * cả node còn cho một cái nữa: ảnh bìa 16:9 cắm vừa cùng component này mà không thêm prop nào.\n */\n preview: ReactNode;\n /** Có ảnh thật hay không — quyết định nút gỡ ảnh (`labels.remove`) có bấm được không. */\n hasImage?: boolean;\n /**\n * `false` = chỉ hiện ảnh, không nút, không vùng thả.\n *\n * Dành cho người xem không có quyền sửa. Hiện một khung tải bị vô hiệu với họ là mời họ bấm vào\n * một thứ sẽ không bao giờ chạy.\n */\n editable?: boolean;\n /** Đang chạy một thao tác khác (ví dụ gỡ ảnh) — khoá cả hai nút. */\n busy?: boolean;\n onRemove?: () => void;\n onUploaded: (result: FileUploadResult<T>) => void;\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /**\n * Nhãn hiển thị. Không truyền thì theo namespace `imageUploadField` của `ComitorLocaleProvider`\n * (không có provider → tiếng Anh); truyền `Partial<>` để đổi từng chuỗi.\n */\n labels?: Partial<ImageUploadFieldLabels>;\n /**\n * Cắt ảnh TRƯỚC khi tải lên — mặc định TẮT. `true` = khung tròn 1:1, xuất JPEG 512px; truyền object để đổi\n * từng thứ. Bật thì ảnh chọn/thả vào mở trong hộp `Cropper`, và chỉ phần đã cắt (vẽ lại trên canvas) được tải\n * lên khi bấm `cropSave`. ⚠ `outputType` phải nằm trong `allowedFileTypes`, vì tệp xuất ra đi qua đúng luật đó.\n */\n crop?: boolean | ImageUploadCropOptions;\n /**\n * Hiện nút `choose` (mặc định `true`). Tắt khi chính tấm ảnh là chỗ bấm — ảnh vẫn mở hộp chọn tệp, và lúc đó\n * nó trở thành nút THẬT cho bàn phím và trình đọc màn hình (tên là `labels.choose`).\n */\n showUploadButton?: boolean;\n /**\n * Hiện nút `remove` khi có ảnh (mặc định `true`). Chưa có ảnh (`hasImage` false) thì nút không bao giờ hiện,\n * bất kể prop này.\n */\n showRemoveButton?: boolean;\n className?: string;\n}\n\n/** Cấu hình của `crop`. */\nexport interface ImageUploadCropOptions {\n /** Tỉ lệ khung cắt. Mặc định `1`. */\n aspectRatio?: number;\n /** Hình khung — CHỈ để nhìn; vùng xuất ra luôn là hình chữ nhật bao ngoài. Mặc định `\"circle\"`. */\n shape?: \"circle\" | \"rectangle\";\n /** Cạnh dài của ảnh xuất ra, px. Mặc định `512`. */\n outputSize?: number;\n /** Định dạng ảnh xuất ra. Mặc định `\"image/jpeg\"`. */\n outputType?: \"image/jpeg\" | \"image/png\" | \"image/webp\";\n /** Chất lượng nén cho JPEG/WebP, 0–1. Mặc định `0.9`. */\n quality?: number;\n}\n\nconst OUTPUT_EXTENSION: Record<NonNullable<ImageUploadCropOptions[\"outputType\"]>, string> = {\n \"image/jpeg\": \"jpg\",\n \"image/png\": \"png\",\n \"image/webp\": \"webp\"\n};\n\nexport function ImageUploadField<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n preview,\n hasImage = false,\n editable = true,\n busy = false,\n onRemove,\n onUploaded,\n onError,\n labels,\n crop,\n showUploadButton = true,\n showRemoveButton = true,\n className\n}: ImageUploadFieldProps<T>) {\n const text = useComitorLabels(\"imageUploadField\", labels);\n const cropOptions = crop === true ? {} : crop || null;\n const aspectRatio = cropOptions?.aspectRatio ?? 1;\n\n // Ảnh đang chờ cắt (object URL cùng origin — canvas không bị \"nhiễm\" nên `toBlob` chạy được).\n const [source, setSource] = useState<{ url: string; name: string } | null>(null);\n const [zoom, setZoom] = useState(1);\n const area = useRef<CropperAreaData | null>(null);\n const picture = useRef<HTMLImageElement>(null);\n\n // Object URL giữ nguyên tệp trong bộ nhớ tới khi bị thu hồi.\n useEffect(\n () => () => {\n if (source) URL.revokeObjectURL(source.url);\n },\n [source]\n );\n\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles: 1,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled: !editable || busy\n });\n\n const blocked = !editable || busy || upload.busy;\n\n function pick(file: File | undefined) {\n if (!file?.type.startsWith(\"image/\")) return;\n area.current = null; // vùng của ảnh cũ vô nghĩa với ảnh mới\n setZoom(1);\n setSource({ url: URL.createObjectURL(file), name: file.name });\n }\n\n async function saveCrop() {\n const image = picture.current;\n if (!image || !area.current || !source) return;\n const outputSize = cropOptions?.outputSize ?? 512;\n const type = cropOptions?.outputType ?? \"image/jpeg\";\n const width = aspectRatio >= 1 ? outputSize : Math.round(outputSize * aspectRatio);\n const height = Math.round(width / aspectRatio);\n const { x, y, width: w, height: h } = area.current;\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n canvas.getContext(\"2d\")?.drawImage(image, x, y, w, h, 0, 0, width, height);\n const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, type, cropOptions?.quality ?? 0.9));\n if (!blob) return;\n // Trình duyệt không mã hoá được định dạng xin thì lặng lẽ trả PNG — tin `blob.type`, không tin tham số.\n const extension = OUTPUT_EXTENSION[blob.type as keyof typeof OUTPUT_EXTENSION] ?? \"png\";\n const stem = source.name.replace(/\\.[^.]+$/, \"\") || \"image\";\n setSource(null);\n upload.start([new File([blob], `${stem}.${extension}`, { type: blob.type })]);\n }\n\n // Có `crop` thì tệp chọn/thả đi vào hộp cắt; không có thì đi thẳng vào hàng đợi như cũ.\n const inputProps = cropOptions\n ? {\n ...upload.inputProps,\n onChange: (event: React.ChangeEvent<HTMLInputElement>) => {\n pick(event.target.files?.[0]);\n event.target.value = \"\"; // chọn lại đúng tệp đó phải bắn `change` lần nữa\n }\n }\n : upload.inputProps;\n const dropProps = cropOptions\n ? {\n ...upload.dropProps,\n onDrop: (event: React.DragEvent) => {\n event.preventDefault();\n upload.dropProps.onDragLeave();\n if (!blocked) pick(event.dataTransfer.files[0]);\n }\n }\n : upload.dropProps;\n\n /*\n * Không sửa được thì chỉ còn tấm ảnh. Không vùng thả, không nút, không `<input>` — không có gì\n * để người dùng bấm nhầm rồi tự hỏi vì sao không có chuyện gì xảy ra.\n */\n if (!editable) return <div className={cn(\"flex items-center gap-4\", className)}>{preview}</div>;\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...inputProps} className=\"sr-only\" />\n\n {/*\n ⚠ `border-2 border-dashed` có mặt ở CẢ HAI trạng thái; chỉ MÀU đổi. Thêm viền lúc `dragging`\n sẽ đẩy nội dung 2px và làm cả hàng giật đúng lúc người dùng đang nhắm chỗ thả.\n */}\n <div\n {...dropProps}\n className={cn(\n \"relative flex flex-wrap items-center gap-4 rounded-lg border-2 border-dashed p-2 transition-colors\",\n upload.dragging ? \"border-primary-ink\" : \"border-transparent\"\n )}\n >\n {/*\n Bấm vào ảnh cũng mở hộp chọn tệp, như nút \"choose\" — người dùng nhắm vào ảnh trước tiên.\n Còn nút \"choose\" thì ảnh chỉ cho CHUỘT/CHẠM (`tabIndex={-1}` + `aria-hidden`): nút kia đã là đường bàn\n phím và trình đọc màn hình, một điểm dừng Tab thứ hai cùng việc chỉ thêm nhiễu. Ẩn nút đi\n (`showUploadButton={false}`) thì ảnh là đường DUY NHẤT — nó thành nút thật, có tên và vòng focus.\n */}\n <button\n type=\"button\"\n {...(showUploadButton\n ? { tabIndex: -1, \"aria-hidden\": true }\n : { \"aria-label\": upload.busy ? text.uploading : text.choose })}\n disabled={blocked}\n onClick={upload.open}\n className=\"shrink-0 cursor-pointer rounded-md transition-opacity hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:cursor-default disabled:hover:opacity-100\"\n >\n {preview}\n </button>\n\n {showUploadButton || (showRemoveButton && hasImage) ? (\n <div className={cn(\"flex flex-wrap items-center gap-2\", blocked && \"opacity-60\")}>\n {showUploadButton ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" loading={upload.busy} onClick={upload.open}>\n {upload.busy ? text.uploading : text.choose}\n </Button>\n ) : null}\n {/* Chỉ khi CÓ ảnh — chưa có gì để gỡ thì một nút vô hiệu chỉ là nhiễu. */}\n {showRemoveButton && hasImage ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" disabled={blocked} onClick={onRemove}>\n {text.remove}\n </Button>\n ) : null}\n </div>\n ) : null}\n\n {/*\n Chữ `dropPrompt` phủ lên cả khối khi đang rê tệp qua. `pointer-events-none` là BẮT\n BUỘC: thiếu nó, lớp phủ tự nuốt sự kiện `drop` của khối bên dưới và việc thả im lặng\n không làm gì.\n */}\n {upload.dragging ? (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center rounded-lg bg-card/90\">\n <span className=\"font-medium text-sm\">{text.dropPrompt}</span>\n </div>\n ) : null}\n </div>\n\n <p className=\"text-muted-foreground text-xs\">{text.hint}</p>\n\n {/* Thanh tiến độ chỉ hiện khi đang tải — một thanh 0% đứng yên trông như hỏng. */}\n {upload.busy ? <Progress value={upload.percent} aria-label={text.uploading} /> : null}\n\n {cropOptions ? (\n <ResponsiveDialog\n open={source !== null}\n onOpenChange={(open) => {\n if (!open) setSource(null);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{text.cropTitle ?? imageUploadFieldEn.cropTitle}</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {text.cropDescription ?? imageUploadFieldEn.cropDescription}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n {source ? (\n <Cropper\n aspectRatio={aspectRatio}\n shape={cropOptions.shape ?? \"circle\"}\n zoom={zoom}\n onZoomChange={setZoom}\n className=\"min-h-72 rounded-xl\"\n // Trên điện thoại hộp là Drawer của vaul: thiếu dòng này, kéo ảnh xuống cũng kéo luôn hộp đóng lại.\n data-vaul-no-drag=\"\"\n // Giữ vùng MỚI NHẤT — `onCropComplete` bỏ qua lần nạp đầu.\n onCropAreaChange={(_, pixels) => {\n area.current = pixels;\n }}\n >\n <CropperImage ref={picture} src={source.url} alt=\"\" />\n <CropperArea />\n </Cropper>\n ) : null}\n <Slider\n min={1}\n max={3}\n step={0.01}\n value={[zoom]}\n onValueChange={([next]) => setZoom(next ?? 1)}\n aria-label={text.cropZoom ?? imageUploadFieldEn.cropZoom}\n />\n <ResponsiveDialogFooter>\n <ResponsiveDialogClose asChild>\n <Button variant=\"outline\">{text.cropCancel ?? imageUploadFieldEn.cropCancel}</Button>\n </ResponsiveDialogClose>\n <Button onClick={() => void saveCrop()}>{text.cropSave ?? imageUploadFieldEn.cropSave}</Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n ) : null}\n </div>\n );\n}\n"],"mappings":";AAwNwB,cAoCd,YApCc;AAtNxB,SAAyB,WAAW,QAAQ,gBAAgB;AAC5D,SAAS,cAAc;AACvB,SAAS,SAAS,aAAmC,oBAAoB;AACzE,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AACvB,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AACnC,SAAS,UAAU;AAEnB,SAAgC,qBAAqB;AAuFrD,MAAM,mBAAsF;AAAA,EAC1F,cAAc;AAAA,EACd,aAAa;AAAA,EACb,cAAc;AAChB;AAEO,SAAS,iBAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB;AACF,GAA6B;AAC3B,QAAM,OAAO,iBAAiB,oBAAoB,MAAM;AACxD,QAAM,cAAc,SAAS,OAAO,CAAC,IAAI,QAAQ;AACjD,QAAM,cAAc,aAAa,eAAe;AAGhD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA+C,IAAI;AAC/E,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,OAAO,OAA+B,IAAI;AAChD,QAAM,UAAU,OAAyB,IAAI;AAG7C;AAAA,IACE,MAAM,MAAM;AACV,UAAI,OAAQ,KAAI,gBAAgB,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,SAAS,cAAiB;AAAA,IAC9B;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA,kBAAkB;AAAA,IAClB;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,QAAQ,EAAE,cAAc,KAAK,cAAc,mBAAmB,KAAK,kBAAkB;AAAA,IACrF,UAAU,CAAC,YAAY;AAAA,EACzB,CAAC;AAED,QAAM,UAAU,CAAC,YAAY,QAAQ,OAAO;AAE5C,WAAS,KAAK,MAAwB;AACpC,QAAI,CAAC,MAAM,KAAK,WAAW,QAAQ,EAAG;AACtC,SAAK,UAAU;AACf,YAAQ,CAAC;AACT,cAAU,EAAE,KAAK,IAAI,gBAAgB,IAAI,GAAG,MAAM,KAAK,KAAK,CAAC;AAAA,EAC/D;AAEA,iBAAe,WAAW;AACxB,UAAM,QAAQ,QAAQ;AACtB,QAAI,CAAC,SAAS,CAAC,KAAK,WAAW,CAAC,OAAQ;AACxC,UAAM,aAAa,aAAa,cAAc;AAC9C,UAAM,OAAO,aAAa,cAAc;AACxC,UAAM,QAAQ,eAAe,IAAI,aAAa,KAAK,MAAM,aAAa,WAAW;AACjF,UAAM,SAAS,KAAK,MAAM,QAAQ,WAAW;AAC7C,UAAM,EAAE,GAAG,GAAG,OAAO,GAAG,QAAQ,EAAE,IAAI,KAAK;AAC3C,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,QAAQ;AACf,WAAO,SAAS;AAChB,WAAO,WAAW,IAAI,GAAG,UAAU,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,OAAO,MAAM;AACzE,UAAM,OAAO,MAAM,IAAI,QAAqB,CAAC,YAAY,OAAO,OAAO,SAAS,MAAM,aAAa,WAAW,GAAG,CAAC;AAClH,QAAI,CAAC,KAAM;AAEX,UAAM,YAAY,iBAAiB,KAAK,IAAqC,KAAK;AAClF,UAAM,OAAO,OAAO,KAAK,QAAQ,YAAY,EAAE,KAAK;AACpD,cAAU,IAAI;AACd,WAAO,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,GAAG,IAAI,IAAI,SAAS,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC;AAAA,EAC9E;AAGA,QAAM,aAAa,cACf;AAAA,IACE,GAAG,OAAO;AAAA,IACV,UAAU,CAAC,UAA+C;AACxD,WAAK,MAAM,OAAO,QAAQ,CAAC,CAAC;AAC5B,YAAM,OAAO,QAAQ;AAAA,IACvB;AAAA,EACF,IACA,OAAO;AACX,QAAM,YAAY,cACd;AAAA,IACE,GAAG,OAAO;AAAA,IACV,QAAQ,CAAC,UAA2B;AAClC,YAAM,eAAe;AACrB,aAAO,UAAU,YAAY;AAC7B,UAAI,CAAC,QAAS,MAAK,MAAM,aAAa,MAAM,CAAC,CAAC;AAAA,IAChD;AAAA,EACF,IACA,OAAO;AAMX,MAAI,CAAC,SAAU,QAAO,oBAAC,SAAI,WAAW,GAAG,2BAA2B,SAAS,GAAI,mBAAQ;AAEzF,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,wBAAC,WAAO,GAAG,YAAY,WAAU,WAAU;AAAA,IAM3C;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,WAAW;AAAA,UACT;AAAA,UACA,OAAO,WAAW,uBAAuB;AAAA,QAC3C;AAAA,QAQA;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACJ,GAAI,mBACD,EAAE,UAAU,IAAI,eAAe,KAAK,IACpC,EAAE,cAAc,OAAO,OAAO,KAAK,YAAY,KAAK,OAAO;AAAA,cAC/D,UAAU;AAAA,cACV,SAAS,OAAO;AAAA,cAChB,WAAU;AAAA,cAET;AAAA;AAAA,UACH;AAAA,UAEC,oBAAqB,oBAAoB,WACxC,qBAAC,SAAI,WAAW,GAAG,qCAAqC,WAAW,YAAY,GAC5E;AAAA,+BACC,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,OAAO,MAAM,SAAS,OAAO,MACrF,iBAAO,OAAO,KAAK,YAAY,KAAK,QACvC,IACE;AAAA,YAEH,oBAAoB,WACnB,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,UAAU,SAAS,SAAS,UACzE,eAAK,QACR,IACE;AAAA,aACN,IACE;AAAA,UAOH,OAAO,WACN,oBAAC,SAAI,WAAU,+FACb,8BAAC,UAAK,WAAU,uBAAuB,eAAK,YAAW,GACzD,IACE;AAAA;AAAA;AAAA,IACN;AAAA,IAEA,oBAAC,OAAE,WAAU,iCAAiC,eAAK,MAAK;AAAA,IAGvD,OAAO,OAAO,oBAAC,YAAS,OAAO,OAAO,SAAS,cAAY,KAAK,WAAW,IAAK;AAAA,IAEhF,cACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,WAAW;AAAA,QACjB,cAAc,CAAC,SAAS;AACtB,cAAI,CAAC,KAAM,WAAU,IAAI;AAAA,QAC3B;AAAA,QAEA,+BAAC,2BACC;AAAA,+BAAC,0BACC;AAAA,gCAAC,yBAAuB,eAAK,aAAa,mBAAmB,WAAU;AAAA,YACvE,oBAAC,+BACE,eAAK,mBAAmB,mBAAmB,iBAC9C;AAAA,aACF;AAAA,UACC,SACC;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,OAAO,YAAY,SAAS;AAAA,cAC5B;AAAA,cACA,cAAc;AAAA,cACd,WAAU;AAAA,cAEV,qBAAkB;AAAA,cAElB,kBAAkB,CAAC,GAAG,WAAW;AAC/B,qBAAK,UAAU;AAAA,cACjB;AAAA,cAEA;AAAA,oCAAC,gBAAa,KAAK,SAAS,KAAK,OAAO,KAAK,KAAI,IAAG;AAAA,gBACpD,oBAAC,eAAY;AAAA;AAAA;AAAA,UACf,IACE;AAAA,UACJ;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,KAAK;AAAA,cACL,MAAM;AAAA,cACN,OAAO,CAAC,IAAI;AAAA,cACZ,eAAe,CAAC,CAAC,IAAI,MAAM,QAAQ,QAAQ,CAAC;AAAA,cAC5C,cAAY,KAAK,YAAY,mBAAmB;AAAA;AAAA,UAClD;AAAA,UACA,qBAAC,0BACC;AAAA,gCAAC,yBAAsB,SAAO,MAC5B,8BAAC,UAAO,SAAQ,WAAW,eAAK,cAAc,mBAAmB,YAAW,GAC9E;AAAA,YACA,oBAAC,UAAO,SAAS,MAAM,KAAK,SAAS,GAAI,eAAK,YAAY,mBAAmB,UAAS;AAAA,aACxF;AAAA,WACF;AAAA;AAAA,IACF,IACE;AAAA,KACN;AAEJ;","names":[]}
|
package/dist/uploader/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { FileUpload, FileUploadProps } from './file-upload.js';
|
|
2
|
-
export { DEFAULT_FILE_UPLOAD_LABELS, DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, FileUploadLabels, FileUploadLogicLabels, ImageUploadFieldLabels } from './file-upload-constants.js';
|
|
3
|
-
export { ImageUploadField, ImageUploadFieldProps } from './image-upload-field.js';
|
|
2
|
+
export { DEFAULT_FILE_UPLOAD_LABELS, DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, FileUploadLabels, FileUploadLimits, FileUploadLogicLabels, ImageUploadFieldLabels } from './file-upload-constants.js';
|
|
3
|
+
export { ImageUploadCropOptions, ImageUploadField, ImageUploadFieldProps } from './image-upload-field.js';
|
|
4
4
|
export { FileUploadResult, UseFileUploadOptions, UseFileUploadResult, useFileUpload } from './use-file-upload.js';
|
|
5
5
|
import 'react';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/uploader/index.ts"],"sourcesContent":["/**\n * Entry `@comitor/ui/uploader` — ô tải tệp dựng trên Uppy chạy HEADLESS.\n *\n * ── VÌ SAO LÀ MỘT ENTRY RIÊNG ────────────────────────────────────────────────────────────────\n * Cùng lý do `@comitor/ui/chart` tách khỏi entry gốc: `@uppy/core` và `@uppy/xhr-upload` là peer\n * **TUỲ CHỌN**, và `tsup` chạy `bundle: false` — nên một component chạm peer tuỳ chọn mà nằm ở\n * entry `.` sẽ kéo cả cây import đó vào MỌI app dùng gói, kể cả app không tải tệp bao giờ.\n *\n * Ranh giới module là thứ bảo đảm chắc chắn hơn phép tree-shaking: app nào không `import` từ\n * `@comitor/ui/uploader` thì không có một byte Uppy nào trong bundle.\n *\n * ⚠ Uppy còn được nạp LƯỜI lần nữa BÊN TRONG component (`import()` động), nên ngay cả app CÓ dùng\n * cũng chỉ trả giá khi người dùng thật sự chọn tệp — xem `loadUppy()`.\n *\n * Cài ở app:\n * ```bash\n * pnpm add @uppy/core @uppy/xhr-upload\n * ```\n */\n\nexport { FileUpload, type FileUploadProps } from \"./file-upload.js\";\nexport {\n DEFAULT_FILE_UPLOAD_LABELS,\n DEFAULT_IMAGE_UPLOAD_FIELD_LABELS,\n type FileUploadLabels,\n type FileUploadLogicLabels,\n type ImageUploadFieldLabels\n} from \"./file-upload-constants.js\";\nexport { ImageUploadField, type ImageUploadFieldProps } from \"./image-upload-field.js\";\nexport {\n type FileUploadResult,\n type UseFileUploadOptions,\n type UseFileUploadResult,\n useFileUpload\n} from \"./use-file-upload.js\";\n"],"mappings":"AAoBA,SAAS,kBAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,
|
|
1
|
+
{"version":3,"sources":["../../src/uploader/index.ts"],"sourcesContent":["/**\n * Entry `@comitor/ui/uploader` — ô tải tệp dựng trên Uppy chạy HEADLESS.\n *\n * ── VÌ SAO LÀ MỘT ENTRY RIÊNG ────────────────────────────────────────────────────────────────\n * Cùng lý do `@comitor/ui/chart` tách khỏi entry gốc: `@uppy/core` và `@uppy/xhr-upload` là peer\n * **TUỲ CHỌN**, và `tsup` chạy `bundle: false` — nên một component chạm peer tuỳ chọn mà nằm ở\n * entry `.` sẽ kéo cả cây import đó vào MỌI app dùng gói, kể cả app không tải tệp bao giờ.\n *\n * Ranh giới module là thứ bảo đảm chắc chắn hơn phép tree-shaking: app nào không `import` từ\n * `@comitor/ui/uploader` thì không có một byte Uppy nào trong bundle.\n *\n * ⚠ Uppy còn được nạp LƯỜI lần nữa BÊN TRONG component (`import()` động), nên ngay cả app CÓ dùng\n * cũng chỉ trả giá khi người dùng thật sự chọn tệp — xem `loadUppy()`.\n *\n * Cài ở app:\n * ```bash\n * pnpm add @uppy/core @uppy/xhr-upload\n * ```\n */\n\nexport { FileUpload, type FileUploadProps } from \"./file-upload.js\";\nexport {\n DEFAULT_FILE_UPLOAD_LABELS,\n DEFAULT_IMAGE_UPLOAD_FIELD_LABELS,\n type FileUploadLabels,\n type FileUploadLimits,\n type FileUploadLogicLabels,\n type ImageUploadFieldLabels\n} from \"./file-upload-constants.js\";\nexport { type ImageUploadCropOptions, ImageUploadField, type ImageUploadFieldProps } from \"./image-upload-field.js\";\nexport {\n type FileUploadResult,\n type UseFileUploadOptions,\n type UseFileUploadResult,\n useFileUpload\n} from \"./use-file-upload.js\";\n"],"mappings":"AAoBA,SAAS,kBAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AACP,SAAsC,wBAAoD;AAC1F;AAAA,EAIE;AAAA,OACK;","names":[]}
|
package/package.json
CHANGED