@comitor/ui 1.4.0 → 1.5.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.
|
@@ -13,6 +13,18 @@ interface ComitorChartPayloadItem {
|
|
|
13
13
|
name?: string;
|
|
14
14
|
dataKey?: string | number;
|
|
15
15
|
color?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Bản ghi GỐC của điểm dữ liệu — hàng mà app đã truyền vào `data`.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ Thiếu trường này thì một tooltip tự viết không lần được từ một điểm về hàng sinh ra nó, nên
|
|
20
|
+
* app không hiển thị nổi những cột KHÔNG nằm trong `series` (mã dự án, id, đơn vị…). Đó chính là
|
|
21
|
+
* lý do đẩy một app xuống tầng primitive để dựng lại cả biểu đồ — tức dựng bộ biểu đồ THỨ HAI mà
|
|
22
|
+
* gói sinh ra để tránh. Bản song sinh ở tầng primitive (`ChartPayloadItem`) đã có trường này từ
|
|
23
|
+
* đầu; đây là chỗ lệch, không phải chỗ thiếu có chủ ý.
|
|
24
|
+
*/
|
|
25
|
+
payload?: Record<string, unknown> & {
|
|
26
|
+
fill?: string;
|
|
27
|
+
};
|
|
16
28
|
}
|
|
17
29
|
interface ComitorChartTooltipProps {
|
|
18
30
|
/** Do recharts bơm vào — đừng truyền tay. */
|
|
@@ -31,6 +43,39 @@ interface ComitorChartTooltipProps {
|
|
|
31
43
|
* `<Tooltip content={<ComitorChartTooltip valueFormatter={f} />} />`.
|
|
32
44
|
*/
|
|
33
45
|
declare function ComitorChartTooltip({ active, payload, label, labelFormatter, valueFormatter }: ComitorChartTooltipProps): React.JSX.Element | null;
|
|
46
|
+
/**
|
|
47
|
+
* Chú giải theo TOKEN của hệ thiết kế.
|
|
48
|
+
*
|
|
49
|
+
* ── VÌ SAO KHÔNG DÙNG `<Legend />` TRẦN ─────────────────────────────────────
|
|
50
|
+
* Vì recharts tô CHỮ NHÃN bằng **màu của series**:
|
|
51
|
+
* `DefaultLegendContent.js` → `finalLabelStyle.color = … || entry.color`.
|
|
52
|
+
* Tức bậc `--x` (màu nền của series) bị dùng làm màu CHỮ — đúng vai trò mà hợp đồng ba vai trò của
|
|
53
|
+
* gói cấm (§6 README: `--x` là nền, `--x-foreground` là chữ trên nền đó, `--x-ink` là chính màu đó
|
|
54
|
+
* khi đi lên nền trang). Hậu quả không phải "hơi nhạt" mà là chú giải KHÔNG ĐỌC ĐƯỢC với những
|
|
55
|
+
* series dùng tông sáng, và nó tự đổi theo bảng màu nên không bao giờ đo được một lần cho xong.
|
|
56
|
+
*
|
|
57
|
+
* Ở đây chấm màu giữ màu series (đúng vai trò: nó là NỀN của một hình vuông nhỏ), còn chữ dùng
|
|
58
|
+
* `text-muted-foreground`. Cùng khuôn với `ComitorChartTooltip` ngay phía trên.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Một mục trong `payload` mà recharts bơm vào `content` của `<Legend>`.
|
|
62
|
+
*
|
|
63
|
+
* ⚠ KHÁC hình dạng của Tooltip, và đây là chỗ dễ nhầm nhất: ở Legend, **nhãn nằm ở `value`** (một
|
|
64
|
+
* chuỗi), còn ở Tooltip `value` là SỐ ĐO và nhãn nằm ở `name`. Dùng nhầm hình dạng thì component
|
|
65
|
+
* vẫn biên dịch, vẫn render, chỉ là mọi nhãn RỖNG — chú giải hiện ra là mấy ô màu không chữ. Đã
|
|
66
|
+
* xảy ra ở bản đầu của chính component này, và chỉ bị bắt khi có người NHÌN ảnh chụp.
|
|
67
|
+
*/
|
|
68
|
+
interface ComitorChartLegendItem {
|
|
69
|
+
/** Nhãn hiển thị — `name` của series, hoặc `dataKey` nếu series không đặt tên. */
|
|
70
|
+
value?: string;
|
|
71
|
+
color?: string;
|
|
72
|
+
dataKey?: string | number;
|
|
73
|
+
}
|
|
74
|
+
interface ComitorChartLegendProps {
|
|
75
|
+
/** Do recharts bơm vào — đừng truyền tay. */
|
|
76
|
+
payload?: ComitorChartLegendItem[];
|
|
77
|
+
}
|
|
78
|
+
declare function ComitorChartLegend({ payload }: ComitorChartLegendProps): React.JSX.Element | null;
|
|
34
79
|
interface ComitorChartContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
35
80
|
/** Chiều cao khung. Số → px. Mặc định 300. */
|
|
36
81
|
height?: number | string;
|
|
@@ -97,4 +142,4 @@ interface ComitorAreaChartProps extends BaseChartProps {
|
|
|
97
142
|
/** Biểu đồ vùng — xu hướng kèm cảm giác khối lượng; `fillOpacity` 0.15 để chồng lớp còn đọc được. */
|
|
98
143
|
declare function ComitorAreaChart({ data, xAxisKey, series, height, className, showGrid, showLegend, stacked, valueFormatter, isAnimationActive }: ComitorAreaChartProps): React.JSX.Element;
|
|
99
144
|
|
|
100
|
-
export { type BaseChartProps, ComitorAreaChart, type ComitorAreaChartProps, ComitorBarChart, type ComitorBarChartProps, ComitorChartContainer, type ComitorChartContainerProps, type ComitorChartPayloadItem, ComitorChartTooltip, type ComitorChartTooltipProps, ComitorLineChart, type ComitorLineChartProps, type DataSeries };
|
|
145
|
+
export { type BaseChartProps, ComitorAreaChart, type ComitorAreaChartProps, ComitorBarChart, type ComitorBarChartProps, ComitorChartContainer, type ComitorChartContainerProps, ComitorChartLegend, type ComitorChartLegendItem, type ComitorChartLegendProps, type ComitorChartPayloadItem, ComitorChartTooltip, type ComitorChartTooltipProps, ComitorLineChart, type ComitorLineChartProps, type DataSeries };
|
|
@@ -43,6 +43,13 @@ function ComitorChartTooltip({
|
|
|
43
43
|
}) })
|
|
44
44
|
] });
|
|
45
45
|
}
|
|
46
|
+
function ComitorChartLegend({ payload }) {
|
|
47
|
+
if (!payload?.length) return null;
|
|
48
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-2", children: payload.map((entry, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [
|
|
49
|
+
/* @__PURE__ */ jsx("span", { className: "size-2 shrink-0 rounded-[2px]", style: { backgroundColor: entry.color } }),
|
|
50
|
+
/* @__PURE__ */ jsx("span", { className: "text-foreground", children: entry.value })
|
|
51
|
+
] }, `${entry.dataKey ?? entry.value ?? i}`)) });
|
|
52
|
+
}
|
|
46
53
|
function ComitorChartContainer({
|
|
47
54
|
height = 300,
|
|
48
55
|
className,
|
|
@@ -81,7 +88,7 @@ function ComitorLineChart({
|
|
|
81
88
|
/* @__PURE__ */ jsx(XAxis, { dataKey: xAxisKey, tick: AXIS_TICK, axisLine: AXIS_LINE, tickLine: AXIS_LINE }),
|
|
82
89
|
/* @__PURE__ */ jsx(YAxis, { tick: AXIS_TICK, axisLine: AXIS_LINE, tickLine: AXIS_LINE }),
|
|
83
90
|
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ComitorChartTooltip, { valueFormatter }) }),
|
|
84
|
-
showLegend && /* @__PURE__ */ jsx(Legend, {}),
|
|
91
|
+
showLegend && /* @__PURE__ */ jsx(Legend, { content: /* @__PURE__ */ jsx(ComitorChartLegend, {}) }),
|
|
85
92
|
series.map((s, i) => /* @__PURE__ */ jsx(
|
|
86
93
|
Line,
|
|
87
94
|
{
|
|
@@ -115,14 +122,14 @@ function ComitorBarChart({
|
|
|
115
122
|
/* @__PURE__ */ jsx(XAxis, { dataKey: xAxisKey, tick: AXIS_TICK, axisLine: AXIS_LINE, tickLine: AXIS_LINE }),
|
|
116
123
|
/* @__PURE__ */ jsx(YAxis, { tick: AXIS_TICK, axisLine: AXIS_LINE, tickLine: AXIS_LINE }),
|
|
117
124
|
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ComitorChartTooltip, { valueFormatter }) }),
|
|
118
|
-
showLegend && /* @__PURE__ */ jsx(Legend, {}),
|
|
125
|
+
showLegend && /* @__PURE__ */ jsx(Legend, { content: /* @__PURE__ */ jsx(ComitorChartLegend, {}) }),
|
|
119
126
|
series.map((s, i) => /* @__PURE__ */ jsx(
|
|
120
127
|
Bar,
|
|
121
128
|
{
|
|
122
129
|
dataKey: s.key,
|
|
123
130
|
name: s.name || s.key,
|
|
124
131
|
fill: seriesColor(s, i),
|
|
125
|
-
radius: [4, 4, 0, 0],
|
|
132
|
+
radius: stacked ? i === series.length - 1 ? [4, 4, 0, 0] : 0 : [4, 4, 0, 0],
|
|
126
133
|
stackId: stacked ? "stack" : void 0,
|
|
127
134
|
isAnimationActive
|
|
128
135
|
},
|
|
@@ -147,7 +154,7 @@ function ComitorAreaChart({
|
|
|
147
154
|
/* @__PURE__ */ jsx(XAxis, { dataKey: xAxisKey, tick: AXIS_TICK, axisLine: AXIS_LINE, tickLine: AXIS_LINE }),
|
|
148
155
|
/* @__PURE__ */ jsx(YAxis, { tick: AXIS_TICK, axisLine: AXIS_LINE, tickLine: AXIS_LINE }),
|
|
149
156
|
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ComitorChartTooltip, { valueFormatter }) }),
|
|
150
|
-
showLegend && /* @__PURE__ */ jsx(Legend, {}),
|
|
157
|
+
showLegend && /* @__PURE__ */ jsx(Legend, { content: /* @__PURE__ */ jsx(ComitorChartLegend, {}) }),
|
|
151
158
|
series.map((s, i) => {
|
|
152
159
|
const color = seriesColor(s, i);
|
|
153
160
|
return /* @__PURE__ */ jsx(
|
|
@@ -172,6 +179,7 @@ export {
|
|
|
172
179
|
ComitorAreaChart,
|
|
173
180
|
ComitorBarChart,
|
|
174
181
|
ComitorChartContainer,
|
|
182
|
+
ComitorChartLegend,
|
|
175
183
|
ComitorChartTooltip,
|
|
176
184
|
ComitorLineChart
|
|
177
185
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/chart/comitor-chart.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Legend,\n Line,\n LineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis\n} from \"recharts\";\nimport { cn } from \"../lib/cn.js\";\n/*\n * `CHART_COLORS` / `CHART_SERIES_COLORS` sống ở `chart-colors.ts` — module KHÔNG có\n * `\"use client\"`. Chỉ IMPORT vào đây để dùng nội bộ, KHÔNG re-export: mọi export đi qua một\n * file client đều biến thành client-reference proxy ở phía Server Component (đọc ra\n * `undefined`, không throw). Barrel `chart/index.ts` xuất chúng thẳng từ `chart-colors.js`.\n * Lý do đầy đủ nằm ở đầu `chart-colors.ts`.\n */\nimport { CHART_SERIES_COLORS } from \"./chart-colors.js\";\n\n/**\n * Biểu đồ \"lắp sẵn\" của Comitor — dựng nhanh line/bar/area chỉ với `data` + `series`.\n * Khác `chart.tsx` (primitive shadcn thô, tự lắp recharts), file này là tầng tiện dụng:\n * đã chọn sẵn trục, lưới, tooltip và bảng màu theo token.\n *\n * ⚠ Tên export ĐÃ ĐỔI so với comitor-ds vì hai file đụng nhau trong cùng entry `/chart`:\n * `ChartContainer` → `ComitorChartContainer`, `ChartTooltip` → `ComitorChartTooltip`.\n * Tên trần `ChartContainer`/`ChartTooltip` để dành cho primitive shadcn (giữ tương thích\n * `shadcn add`). Code mẫu cũ trong trang docs comitor-ds phải sửa theo.\n */\n\n/** Lấy màu cho series thứ `index`: ưu tiên màu tự khai, không có thì xoay vòng bảng mặc định. */\nfunction seriesColor(series: DataSeries, index: number): string {\n return series.color ?? CHART_SERIES_COLORS[index % CHART_SERIES_COLORS.length] ?? CHART_SERIES_COLORS[0];\n}\n\n// ─── Tooltip ────────────────────────────────────────────────────────────────\n\n/**\n * Một phần tử payload recharts bơm vào tooltip.\n *\n * Khai tại đây thay vì `TooltipProps<number, string>[\"payload\"]`: recharts **3** đã bỏ\n * `payload` khỏi props của `<Tooltip>` (chuyển sang context nội bộ), nên phép dẫn xuất đó\n * hỏng kiểu ngay trong file `.d.ts` phát hành. Xem chú thích cùng nội dung ở `chart.tsx`\n * (`ChartPayloadItem`) — cùng một lý do, cùng một cách chữa.\n */\nexport interface ComitorChartPayloadItem {\n value?: number;\n name?: string;\n dataKey?: string | number;\n color?: string;\n}\n\nexport interface ComitorChartTooltipProps {\n /** Do recharts bơm vào — đừng truyền tay. */\n active?: boolean;\n /** Do recharts bơm vào — đừng truyền tay. */\n payload?: ComitorChartPayloadItem[];\n /** Do recharts bơm vào — đừng truyền tay. */\n label?: string;\n /** Định dạng nhãn trục X (ví dụ đổi \"2026-01\" → \"Th01/2026\"). */\n labelFormatter?: (label: string) => string;\n /** Định dạng giá trị (ví dụ thêm \"₫\", \"%\"). Mặc định: `toLocaleString(\"vi-VN\")`. */\n valueFormatter?: (value: number) => string;\n}\n\n/**\n * Tooltip theo token Comitor. Truyền vào recharts dưới dạng element:\n * `<Tooltip content={<ComitorChartTooltip valueFormatter={f} />} />`.\n */\nexport function ComitorChartTooltip({\n active,\n payload,\n label,\n labelFormatter,\n valueFormatter\n}: ComitorChartTooltipProps) {\n if (!active || !payload?.length) return null;\n\n return (\n <div data-slot=\"comitor-chart-tooltip\" className=\"rounded-lg border border-border bg-card px-3 py-2 shadow-card\">\n {label && (\n <p className=\"text-xs font-medium text-foreground mb-1.5\">{labelFormatter ? labelFormatter(label) : label}</p>\n )}\n <div className=\"space-y-1\">\n {payload.map((entry, i) => {\n const value = entry.value;\n const display = value === undefined ? \"—\" : (valueFormatter?.(value) ?? value.toLocaleString(\"vi-VN\"));\n\n return (\n <div key={`${entry.dataKey ?? entry.name ?? i}`} className=\"flex items-center gap-2 text-xs\">\n <span className=\"h-2 w-2 rounded-full shrink-0\" style={{ backgroundColor: entry.color }} />\n <span className=\"text-muted-foreground\">{entry.name}:</span>\n <span className=\"font-medium text-foreground\">{display}</span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\n// ─── Khung ──────────────────────────────────────────────────────────────────\n\nexport interface ComitorChartContainerProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Chiều cao khung. Số → px. Mặc định 300. */\n height?: number | string;\n ref?: React.Ref<HTMLDivElement>;\n}\n\n/**\n * Khung `ResponsiveContainer` cao cố định. Khác `ChartContainer` của shadcn ở chỗ:\n * không cấp context `config`, không sinh biến `--color-<key>` — chỉ lo kích thước.\n */\nexport function ComitorChartContainer({\n height = 300,\n className,\n children,\n ref,\n ...props\n}: ComitorChartContainerProps) {\n return (\n <div\n ref={ref}\n data-slot=\"comitor-chart\"\n className={cn(\"w-full\", className)}\n style={{ height: typeof height === \"number\" ? `${height}px` : height }}\n {...props}\n >\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children as React.ReactElement}\n </ResponsiveContainer>\n </div>\n );\n}\n\n// ─── Kiểu dùng chung ────────────────────────────────────────────────────────\n\n/** Một series (một đường/cột/vùng) trong biểu đồ. */\nexport interface DataSeries {\n /** Tên trường trong `data`. */\n key: string;\n /** Nhãn hiển thị ở tooltip/chú giải. Mặc định lấy `key`. */\n name?: string;\n /** Màu — nên lấy từ `CHART_COLORS` hoặc `var(--chart-N)`, đừng ghi hex. */\n color?: string;\n}\n\nexport interface BaseChartProps {\n data: Record<string, unknown>[];\n /** Tên trường dùng cho trục X. */\n xAxisKey: string;\n height?: number | string;\n className?: string;\n showGrid?: boolean;\n showLegend?: boolean;\n valueFormatter?: (value: number) => string;\n /**\n * Bật hiệu ứng vẽ dần khi mark xuất hiện. **Mặc định `false`** — khác mặc định `true` của\n * recharts, và đây là chủ đích.\n *\n * VÌ SAO TẮT MẶC ĐỊNH: với recharts 2.15.x + React 19.2 hiệu ứng KHÔNG bao giờ chạy tới\n * khung hình cuối, nên mark đứng nguyên ở khung đầu — tức là vô hình. Ba dạng cùng một lỗi,\n * đo được trên DOM thật:\n *\n * `<Bar>` `<g class=\"recharts-bar-rectangle\">` RỖNG — `Rectangle` trả `null` khi cao 0\n * `<Line>` path có `d` đúng nhưng `stroke-dasharray=\"0px 634px\"` — vẽ 0px\n * `<Area>` path có `d` đúng nhưng `<clipPath><rect width=\"0\">` — bị cắt sạch\n *\n * Kết quả giống hệt nhau ở cả ba: đủ trục, đủ lưới, đủ chú giải, KHÔNG có dữ liệu — và\n * không tự hết sau khi chờ. Một biểu đồ hiện ra ngay đáng giá hơn hiệu ứng, nên mặc định\n * đảo lại. Ai muốn hiệu ứng thì bật tường minh (`isAnimationActive` — recharts 3 chạy đúng).\n */\n isAnimationActive?: boolean;\n}\n\n/** Style dùng chung cho tick của cả hai trục — gom lại để 3 biểu đồ không lệch nhau. */\nconst AXIS_TICK = { fontSize: 11, fill: \"var(--muted-foreground)\" } as const;\nconst AXIS_LINE = { stroke: \"var(--border)\" } as const;\nconst CHART_MARGIN = { top: 8, right: 8, left: 0, bottom: 0 } as const;\n\n// ─── Line ───────────────────────────────────────────────────────────────────\n\nexport interface ComitorLineChartProps extends BaseChartProps {\n series: DataSeries[];\n}\n\n/** Biểu đồ đường — dùng cho xu hướng theo thời gian. */\nexport function ComitorLineChart({\n data,\n xAxisKey,\n series,\n height = 300,\n className,\n showGrid = true,\n showLegend = false,\n valueFormatter,\n isAnimationActive = false\n}: ComitorLineChartProps) {\n return (\n <ComitorChartContainer height={height} className={className}>\n <LineChart data={data} margin={CHART_MARGIN}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--border)\" />}\n <XAxis dataKey={xAxisKey} tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <YAxis tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <Tooltip content={<ComitorChartTooltip valueFormatter={valueFormatter} />} />\n {showLegend && <Legend />}\n {series.map((s, i) => (\n <Line\n key={s.key}\n type=\"monotone\"\n dataKey={s.key}\n name={s.name || s.key}\n stroke={seriesColor(s, i)}\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 4, strokeWidth: 0 }}\n isAnimationActive={isAnimationActive}\n />\n ))}\n </LineChart>\n </ComitorChartContainer>\n );\n}\n\n// ─── Bar ────────────────────────────────────────────────────────────────────\n\nexport interface ComitorBarChartProps extends BaseChartProps {\n series: DataSeries[];\n /** Chồng các series lên nhau thay vì xếp cạnh. */\n stacked?: boolean;\n}\n\n/** Biểu đồ cột — dùng cho so sánh giữa các hạng mục. Lưới chỉ kẻ ngang. */\nexport function ComitorBarChart({\n data,\n xAxisKey,\n series,\n height = 300,\n className,\n showGrid = true,\n showLegend = false,\n stacked = false,\n valueFormatter,\n isAnimationActive = false\n}: ComitorBarChartProps) {\n return (\n <ComitorChartContainer height={height} className={className}>\n <BarChart data={data} margin={CHART_MARGIN}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--border)\" vertical={false} />}\n <XAxis dataKey={xAxisKey} tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <YAxis tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <Tooltip content={<ComitorChartTooltip valueFormatter={valueFormatter} />} />\n {showLegend && <Legend />}\n {series.map((s, i) => (\n <Bar\n key={s.key}\n dataKey={s.key}\n name={s.name || s.key}\n fill={seriesColor(s, i)}\n radius={[4, 4, 0, 0]}\n stackId={stacked ? \"stack\" : undefined}\n isAnimationActive={isAnimationActive}\n />\n ))}\n </BarChart>\n </ComitorChartContainer>\n );\n}\n\n// ─── Area ───────────────────────────────────────────────────────────────────\n\nexport interface ComitorAreaChartProps extends BaseChartProps {\n series: DataSeries[];\n /** Chồng các series lên nhau (tổng tích luỹ). */\n stacked?: boolean;\n}\n\n/** Biểu đồ vùng — xu hướng kèm cảm giác khối lượng; `fillOpacity` 0.15 để chồng lớp còn đọc được. */\nexport function ComitorAreaChart({\n data,\n xAxisKey,\n series,\n height = 300,\n className,\n showGrid = true,\n showLegend = false,\n stacked = false,\n valueFormatter,\n isAnimationActive = false\n}: ComitorAreaChartProps) {\n return (\n <ComitorChartContainer height={height} className={className}>\n <AreaChart data={data} margin={CHART_MARGIN}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--border)\" />}\n <XAxis dataKey={xAxisKey} tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <YAxis tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <Tooltip content={<ComitorChartTooltip valueFormatter={valueFormatter} />} />\n {showLegend && <Legend />}\n {series.map((s, i) => {\n const color = seriesColor(s, i);\n return (\n <Area\n key={s.key}\n type=\"monotone\"\n dataKey={s.key}\n name={s.name || s.key}\n stroke={color}\n fill={color}\n fillOpacity={0.15}\n strokeWidth={2}\n stackId={stacked ? \"stack\" : undefined}\n isAnimationActive={isAnimationActive}\n />\n );\n })}\n </AreaChart>\n </ComitorChartContainer>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/chart/comitor-chart.tsx"],"sourcesContent":["\"use client\";\n\nimport type * as React from \"react\";\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Legend,\n Line,\n LineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis\n} from \"recharts\";\nimport { cn } from \"../lib/cn.js\";\n/*\n * `CHART_COLORS` / `CHART_SERIES_COLORS` sống ở `chart-colors.ts` — module KHÔNG có\n * `\"use client\"`. Chỉ IMPORT vào đây để dùng nội bộ, KHÔNG re-export: mọi export đi qua một\n * file client đều biến thành client-reference proxy ở phía Server Component (đọc ra\n * `undefined`, không throw). Barrel `chart/index.ts` xuất chúng thẳng từ `chart-colors.js`.\n * Lý do đầy đủ nằm ở đầu `chart-colors.ts`.\n */\nimport { CHART_SERIES_COLORS } from \"./chart-colors.js\";\n\n/**\n * Biểu đồ \"lắp sẵn\" của Comitor — dựng nhanh line/bar/area chỉ với `data` + `series`.\n * Khác `chart.tsx` (primitive shadcn thô, tự lắp recharts), file này là tầng tiện dụng:\n * đã chọn sẵn trục, lưới, tooltip và bảng màu theo token.\n *\n * ⚠ Tên export ĐÃ ĐỔI so với comitor-ds vì hai file đụng nhau trong cùng entry `/chart`:\n * `ChartContainer` → `ComitorChartContainer`, `ChartTooltip` → `ComitorChartTooltip`.\n * Tên trần `ChartContainer`/`ChartTooltip` để dành cho primitive shadcn (giữ tương thích\n * `shadcn add`). Code mẫu cũ trong trang docs comitor-ds phải sửa theo.\n */\n\n/** Lấy màu cho series thứ `index`: ưu tiên màu tự khai, không có thì xoay vòng bảng mặc định. */\nfunction seriesColor(series: DataSeries, index: number): string {\n return series.color ?? CHART_SERIES_COLORS[index % CHART_SERIES_COLORS.length] ?? CHART_SERIES_COLORS[0];\n}\n\n// ─── Tooltip ────────────────────────────────────────────────────────────────\n\n/**\n * Một phần tử payload recharts bơm vào tooltip.\n *\n * Khai tại đây thay vì `TooltipProps<number, string>[\"payload\"]`: recharts **3** đã bỏ\n * `payload` khỏi props của `<Tooltip>` (chuyển sang context nội bộ), nên phép dẫn xuất đó\n * hỏng kiểu ngay trong file `.d.ts` phát hành. Xem chú thích cùng nội dung ở `chart.tsx`\n * (`ChartPayloadItem`) — cùng một lý do, cùng một cách chữa.\n */\nexport interface ComitorChartPayloadItem {\n value?: number;\n name?: string;\n dataKey?: string | number;\n color?: string;\n /**\n * Bản ghi GỐC của điểm dữ liệu — hàng mà app đã truyền vào `data`.\n *\n * ⚠ Thiếu trường này thì một tooltip tự viết không lần được từ một điểm về hàng sinh ra nó, nên\n * app không hiển thị nổi những cột KHÔNG nằm trong `series` (mã dự án, id, đơn vị…). Đó chính là\n * lý do đẩy một app xuống tầng primitive để dựng lại cả biểu đồ — tức dựng bộ biểu đồ THỨ HAI mà\n * gói sinh ra để tránh. Bản song sinh ở tầng primitive (`ChartPayloadItem`) đã có trường này từ\n * đầu; đây là chỗ lệch, không phải chỗ thiếu có chủ ý.\n */\n payload?: Record<string, unknown> & { fill?: string };\n}\n\nexport interface ComitorChartTooltipProps {\n /** Do recharts bơm vào — đừng truyền tay. */\n active?: boolean;\n /** Do recharts bơm vào — đừng truyền tay. */\n payload?: ComitorChartPayloadItem[];\n /** Do recharts bơm vào — đừng truyền tay. */\n label?: string;\n /** Định dạng nhãn trục X (ví dụ đổi \"2026-01\" → \"Th01/2026\"). */\n labelFormatter?: (label: string) => string;\n /** Định dạng giá trị (ví dụ thêm \"₫\", \"%\"). Mặc định: `toLocaleString(\"vi-VN\")`. */\n valueFormatter?: (value: number) => string;\n}\n\n/**\n * Tooltip theo token Comitor. Truyền vào recharts dưới dạng element:\n * `<Tooltip content={<ComitorChartTooltip valueFormatter={f} />} />`.\n */\nexport function ComitorChartTooltip({\n active,\n payload,\n label,\n labelFormatter,\n valueFormatter\n}: ComitorChartTooltipProps) {\n if (!active || !payload?.length) return null;\n\n return (\n <div data-slot=\"comitor-chart-tooltip\" className=\"rounded-lg border border-border bg-card px-3 py-2 shadow-card\">\n {label && (\n <p className=\"text-xs font-medium text-foreground mb-1.5\">{labelFormatter ? labelFormatter(label) : label}</p>\n )}\n <div className=\"space-y-1\">\n {payload.map((entry, i) => {\n const value = entry.value;\n const display = value === undefined ? \"—\" : (valueFormatter?.(value) ?? value.toLocaleString(\"vi-VN\"));\n\n return (\n <div key={`${entry.dataKey ?? entry.name ?? i}`} className=\"flex items-center gap-2 text-xs\">\n <span className=\"h-2 w-2 rounded-full shrink-0\" style={{ backgroundColor: entry.color }} />\n <span className=\"text-muted-foreground\">{entry.name}:</span>\n <span className=\"font-medium text-foreground\">{display}</span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\n/**\n * Chú giải theo TOKEN của hệ thiết kế.\n *\n * ── VÌ SAO KHÔNG DÙNG `<Legend />` TRẦN ─────────────────────────────────────\n * Vì recharts tô CHỮ NHÃN bằng **màu của series**:\n * `DefaultLegendContent.js` → `finalLabelStyle.color = … || entry.color`.\n * Tức bậc `--x` (màu nền của series) bị dùng làm màu CHỮ — đúng vai trò mà hợp đồng ba vai trò của\n * gói cấm (§6 README: `--x` là nền, `--x-foreground` là chữ trên nền đó, `--x-ink` là chính màu đó\n * khi đi lên nền trang). Hậu quả không phải \"hơi nhạt\" mà là chú giải KHÔNG ĐỌC ĐƯỢC với những\n * series dùng tông sáng, và nó tự đổi theo bảng màu nên không bao giờ đo được một lần cho xong.\n *\n * Ở đây chấm màu giữ màu series (đúng vai trò: nó là NỀN của một hình vuông nhỏ), còn chữ dùng\n * `text-muted-foreground`. Cùng khuôn với `ComitorChartTooltip` ngay phía trên.\n */\n/**\n * Một mục trong `payload` mà recharts bơm vào `content` của `<Legend>`.\n *\n * ⚠ KHÁC hình dạng của Tooltip, và đây là chỗ dễ nhầm nhất: ở Legend, **nhãn nằm ở `value`** (một\n * chuỗi), còn ở Tooltip `value` là SỐ ĐO và nhãn nằm ở `name`. Dùng nhầm hình dạng thì component\n * vẫn biên dịch, vẫn render, chỉ là mọi nhãn RỖNG — chú giải hiện ra là mấy ô màu không chữ. Đã\n * xảy ra ở bản đầu của chính component này, và chỉ bị bắt khi có người NHÌN ảnh chụp.\n */\nexport interface ComitorChartLegendItem {\n /** Nhãn hiển thị — `name` của series, hoặc `dataKey` nếu series không đặt tên. */\n value?: string;\n color?: string;\n dataKey?: string | number;\n}\n\nexport interface ComitorChartLegendProps {\n /** Do recharts bơm vào — đừng truyền tay. */\n payload?: ComitorChartLegendItem[];\n}\n\nexport function ComitorChartLegend({ payload }: ComitorChartLegendProps) {\n if (!payload?.length) return null;\n\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-2\">\n {payload.map((entry, i) => (\n <div key={`${entry.dataKey ?? entry.value ?? i}`} className=\"flex items-center gap-1.5 text-xs\">\n {/* Chấm màu giữ màu series — ở đây `--x` đúng vai trò của nó: NỀN của một hình nhỏ. */}\n <span className=\"size-2 shrink-0 rounded-[2px]\" style={{ backgroundColor: entry.color }} />\n {/*\n `text-foreground`, KHÔNG phải `text-muted-foreground`. Nhãn chú giải là CHÌA KHOÁ để đọc\n biểu đồ — không đọc được nó thì mọi màu trong biểu đồ mất nghĩa — nên nó là chữ chính,\n không phải chữ phụ. Đo trên `--card` ở bảng màu MẶC ĐỊNH: `--muted-foreground` cho\n 3,83:1, dưới ngưỡng 4,5:1 của WCAG cho chữ thường; `--foreground` cho 17,4:1.\n ⚠ `pnpm lint:a11y` KHÔNG bắt được chỗ này — nó không đo bảng mặc định (xem\n `scripts/a11y/tailwind.mjs`). Con số trên đo bằng tay.\n */}\n <span className=\"text-foreground\">{entry.value}</span>\n </div>\n ))}\n </div>\n );\n}\n\n// ─── Khung ──────────────────────────────────────────────────────────────────\n\nexport interface ComitorChartContainerProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Chiều cao khung. Số → px. Mặc định 300. */\n height?: number | string;\n ref?: React.Ref<HTMLDivElement>;\n}\n\n/**\n * Khung `ResponsiveContainer` cao cố định. Khác `ChartContainer` của shadcn ở chỗ:\n * không cấp context `config`, không sinh biến `--color-<key>` — chỉ lo kích thước.\n */\nexport function ComitorChartContainer({\n height = 300,\n className,\n children,\n ref,\n ...props\n}: ComitorChartContainerProps) {\n return (\n <div\n ref={ref}\n data-slot=\"comitor-chart\"\n className={cn(\"w-full\", className)}\n style={{ height: typeof height === \"number\" ? `${height}px` : height }}\n {...props}\n >\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children as React.ReactElement}\n </ResponsiveContainer>\n </div>\n );\n}\n\n// ─── Kiểu dùng chung ────────────────────────────────────────────────────────\n\n/** Một series (một đường/cột/vùng) trong biểu đồ. */\nexport interface DataSeries {\n /** Tên trường trong `data`. */\n key: string;\n /** Nhãn hiển thị ở tooltip/chú giải. Mặc định lấy `key`. */\n name?: string;\n /** Màu — nên lấy từ `CHART_COLORS` hoặc `var(--chart-N)`, đừng ghi hex. */\n color?: string;\n}\n\nexport interface BaseChartProps {\n data: Record<string, unknown>[];\n /** Tên trường dùng cho trục X. */\n xAxisKey: string;\n height?: number | string;\n className?: string;\n showGrid?: boolean;\n showLegend?: boolean;\n valueFormatter?: (value: number) => string;\n /**\n * Bật hiệu ứng vẽ dần khi mark xuất hiện. **Mặc định `false`** — khác mặc định `true` của\n * recharts, và đây là chủ đích.\n *\n * VÌ SAO TẮT MẶC ĐỊNH: với recharts 2.15.x + React 19.2 hiệu ứng KHÔNG bao giờ chạy tới\n * khung hình cuối, nên mark đứng nguyên ở khung đầu — tức là vô hình. Ba dạng cùng một lỗi,\n * đo được trên DOM thật:\n *\n * `<Bar>` `<g class=\"recharts-bar-rectangle\">` RỖNG — `Rectangle` trả `null` khi cao 0\n * `<Line>` path có `d` đúng nhưng `stroke-dasharray=\"0px 634px\"` — vẽ 0px\n * `<Area>` path có `d` đúng nhưng `<clipPath><rect width=\"0\">` — bị cắt sạch\n *\n * Kết quả giống hệt nhau ở cả ba: đủ trục, đủ lưới, đủ chú giải, KHÔNG có dữ liệu — và\n * không tự hết sau khi chờ. Một biểu đồ hiện ra ngay đáng giá hơn hiệu ứng, nên mặc định\n * đảo lại. Ai muốn hiệu ứng thì bật tường minh (`isAnimationActive` — recharts 3 chạy đúng).\n */\n isAnimationActive?: boolean;\n}\n\n/** Style dùng chung cho tick của cả hai trục — gom lại để 3 biểu đồ không lệch nhau. */\nconst AXIS_TICK = { fontSize: 11, fill: \"var(--muted-foreground)\" } as const;\nconst AXIS_LINE = { stroke: \"var(--border)\" } as const;\nconst CHART_MARGIN = { top: 8, right: 8, left: 0, bottom: 0 } as const;\n\n// ─── Line ───────────────────────────────────────────────────────────────────\n\nexport interface ComitorLineChartProps extends BaseChartProps {\n series: DataSeries[];\n}\n\n/** Biểu đồ đường — dùng cho xu hướng theo thời gian. */\nexport function ComitorLineChart({\n data,\n xAxisKey,\n series,\n height = 300,\n className,\n showGrid = true,\n showLegend = false,\n valueFormatter,\n isAnimationActive = false\n}: ComitorLineChartProps) {\n return (\n <ComitorChartContainer height={height} className={className}>\n <LineChart data={data} margin={CHART_MARGIN}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--border)\" />}\n <XAxis dataKey={xAxisKey} tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <YAxis tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <Tooltip content={<ComitorChartTooltip valueFormatter={valueFormatter} />} />\n {showLegend && <Legend content={<ComitorChartLegend />} />}\n {series.map((s, i) => (\n <Line\n key={s.key}\n type=\"monotone\"\n dataKey={s.key}\n name={s.name || s.key}\n stroke={seriesColor(s, i)}\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 4, strokeWidth: 0 }}\n isAnimationActive={isAnimationActive}\n />\n ))}\n </LineChart>\n </ComitorChartContainer>\n );\n}\n\n// ─── Bar ────────────────────────────────────────────────────────────────────\n\nexport interface ComitorBarChartProps extends BaseChartProps {\n series: DataSeries[];\n /** Chồng các series lên nhau thay vì xếp cạnh. */\n stacked?: boolean;\n}\n\n/** Biểu đồ cột — dùng cho so sánh giữa các hạng mục. Lưới chỉ kẻ ngang. */\nexport function ComitorBarChart({\n data,\n xAxisKey,\n series,\n height = 300,\n className,\n showGrid = true,\n showLegend = false,\n stacked = false,\n valueFormatter,\n isAnimationActive = false\n}: ComitorBarChartProps) {\n return (\n <ComitorChartContainer height={height} className={className}>\n <BarChart data={data} margin={CHART_MARGIN}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--border)\" vertical={false} />}\n <XAxis dataKey={xAxisKey} tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <YAxis tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <Tooltip content={<ComitorChartTooltip valueFormatter={valueFormatter} />} />\n {showLegend && <Legend content={<ComitorChartLegend />} />}\n {series.map((s, i) => (\n <Bar\n key={s.key}\n dataKey={s.key}\n name={s.name || s.key}\n fill={seriesColor(s, i)}\n /*\n * ⚠ CHỒNG thì CHỈ đoạn trên cùng được bo.\n *\n * `radius={[4, 4, 0, 0]}` cho mọi đoạn nghĩa là mỗi đoạn giữa chồng cũng có hai góc\n * trên bo tròn, nằm ngay dưới đoạn kế tiếp — mắt thấy một vệt khuyết ở mỗi mối nối, và\n * cột chồng trông như bị nứt thành nhiều mảnh rời. Không có gì báo: recharts vẽ đúng\n * thứ được yêu cầu.\n *\n * Đoạn cuối trong `series` là đoạn NẰM TRÊN CÙNG của chồng (recharts xếp theo thứ tự\n * khai), nên chỉ nó nhận bo góc.\n *\n * ⚠ Ca biên đã biết, không chữa: nếu series cuối có giá trị 0 ở một cột thì đoạn trên\n * cùng NHÌN THẤY của cột đó là series áp chót, và nó phẳng. Chữa được thì phải tính\n * theo TỪNG CỘT chứ không theo series, tức bỏ `<Bar>` mà tự vẽ — cái giá lớn hơn hẳn\n * cái sai.\n */\n radius={stacked ? (i === series.length - 1 ? [4, 4, 0, 0] : 0) : [4, 4, 0, 0]}\n stackId={stacked ? \"stack\" : undefined}\n isAnimationActive={isAnimationActive}\n />\n ))}\n </BarChart>\n </ComitorChartContainer>\n );\n}\n\n// ─── Area ───────────────────────────────────────────────────────────────────\n\nexport interface ComitorAreaChartProps extends BaseChartProps {\n series: DataSeries[];\n /** Chồng các series lên nhau (tổng tích luỹ). */\n stacked?: boolean;\n}\n\n/** Biểu đồ vùng — xu hướng kèm cảm giác khối lượng; `fillOpacity` 0.15 để chồng lớp còn đọc được. */\nexport function ComitorAreaChart({\n data,\n xAxisKey,\n series,\n height = 300,\n className,\n showGrid = true,\n showLegend = false,\n stacked = false,\n valueFormatter,\n isAnimationActive = false\n}: ComitorAreaChartProps) {\n return (\n <ComitorChartContainer height={height} className={className}>\n <AreaChart data={data} margin={CHART_MARGIN}>\n {showGrid && <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--border)\" />}\n <XAxis dataKey={xAxisKey} tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <YAxis tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />\n <Tooltip content={<ComitorChartTooltip valueFormatter={valueFormatter} />} />\n {showLegend && <Legend content={<ComitorChartLegend />} />}\n {series.map((s, i) => {\n const color = seriesColor(s, i);\n return (\n <Area\n key={s.key}\n type=\"monotone\"\n dataKey={s.key}\n name={s.name || s.key}\n stroke={color}\n fill={color}\n fillOpacity={0.15}\n strokeWidth={2}\n stackId={stacked ? \"stack\" : undefined}\n isAnimationActive={isAnimationActive}\n />\n );\n })}\n </AreaChart>\n </ComitorChartContainer>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/chart/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ChartConfig, ChartContainer, ChartContainerProps, ChartLegend, ChartLegendContent, ChartLegendContentProps, ChartPayloadItem, ChartStyle, ChartStyleProps, ChartTooltip, ChartTooltipContent, ChartTooltipContentProps, useChart } from './chart.js';
|
|
2
2
|
export { CHART_COLORS, CHART_SERIES_COLORS, ChartColorName } from './chart-colors.js';
|
|
3
|
-
export { BaseChartProps, ComitorAreaChart, ComitorAreaChartProps, ComitorBarChart, ComitorBarChartProps, ComitorChartContainer, ComitorChartContainerProps, ComitorChartPayloadItem, ComitorChartTooltip, ComitorChartTooltipProps, ComitorLineChart, ComitorLineChartProps, DataSeries } from './comitor-chart.js';
|
|
3
|
+
export { BaseChartProps, ComitorAreaChart, ComitorAreaChartProps, ComitorBarChart, ComitorBarChartProps, ComitorChartContainer, ComitorChartContainerProps, ComitorChartLegend, ComitorChartLegendItem, ComitorChartLegendProps, ComitorChartPayloadItem, ComitorChartTooltip, ComitorChartTooltipProps, ComitorLineChart, ComitorLineChartProps, DataSeries } from './comitor-chart.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
import 'recharts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/uploader/file-upload.tsx"],"sourcesContent":["\"use client\";\n\nimport { useId } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_FILE_UPLOAD_LABELS, type FileUploadLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải tệp dùng chung cho mọi app Comitor — **Uppy chạy HEADLESS, giao diện là của gói này**.\n *\n * ── VÌ SAO KHÔNG DÙNG `@uppy/dashboard` HAY `@uppy/react` ──────────────────────────────────\n * Cả hai đều kéo theo **Preact** và **một bộ CSS riêng**, tức app đã có React lại ship thêm một\n * runtime giao diện thứ hai, và bảng màu của Uppy không đi qua token của Comitor nên nó KHÔNG qua\n * được cổng tương phản (`pnpm lint:a11y`).\n *\n * Đo được trên một cây đã cài Uppy 5: `@uppy/dashboard` 1.236 KB, `@uppy/react` 432 KB, `preact`\n * 1.852 KB trên đĩa. Trong khi `@uppy/core` khai `sideEffects: [\"*.css\"]` và chỉ có `UIPlugin.js`\n * import `preact` — nên **chỉ nạp `Uppy` + `XHRUpload` thì bundler cắt được cả nhánh Preact**.\n *\n * Cái ta giữ lại của Uppy là phần đáng giá và khó viết đúng: hàng đợi, tiến độ theo byte, thử lại,\n * huỷ, và bộ luật hạn chế (kiểu tệp / kích thước / số lượng) áp NGAY trên trình duyệt.\n *\n * ── VÌ SAO `XHRUpload` CHỨ KHÔNG PHẢI `AwsS3` (presigned PUT) ──────────────────────────────\n * Một app Comitor khác (babrik) tải thẳng lên S3 bằng URL đã ký. Cách đó đúng cho một bucket CÔNG\n * KHAI, nơi URL trả về chính là URL hiển thị. Comitor thì dùng **bucket RIÊNG TƯ**: database lưu\n * KHOÁ object chứ không lưu URL, và URL hiển thị được sinh lại có chữ ký ở mỗi lần đọc.\n *\n * Hệ quả: máy chủ vẫn phải là nơi đặt tên khoá, kiểm byte thật, và ghi khoá vào database. Đẩy\n * việc tải thẳng lên S3 sẽ lấy mất của máy chủ khả năng nhìn thấy nội dung tệp — với một IdP phục\n * vụ ảnh do người dùng tải lên thì đó là đánh đổi không đáng.\n *\n * `XHRUpload` POST một `multipart/form-data` tới chính endpoint của app, nên **không route nào phải\n * đổi** và không có bề mặt bảo mật mới nào mở ra.\n *\n * ── CSP ─────────────────────────────────────────────────────────────────────────────────────\n * Uppy KHÔNG dùng `eval` hay `new Function` (đã grep `@uppy/core`, `@uppy/dashboard`, `@uppy/react`:\n * 0 file). Nên nó chạy được dưới `script-src 'self' 'nonce-…'` mà không cần nới gì.\n */\n\nexport interface FileUploadProps<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\"` — khớp khuôn route của Comitor. */\n fieldName?: string;\n /** Giới hạn kích thước mỗi tệp, byte. */\n maxFileSize: number;\n /** MIME được nhận, ví dụ `[\"image/jpeg\", \"image/png\", \"image/webp\"]`. */\n allowedFileTypes: string[];\n /** Số tệp tối đa mỗi lượt. Mặc định 1. */\n maxNumberOfFiles?: number;\n /** Chạy khi MỘT tệp tải xong. Nhận nguyên thân JSON của endpoint. */\n onUploaded: (result: FileUploadResult<T>) => void;\n /**\n * Chạy khi tải hỏng. `code` là `error.code` từ thân phản hồi nếu endpoint trả theo khuôn\n * `{ error: { code } }` của Comitor — nhờ đó chỗ gọi tra được bảng dịch của chính nó.\n */\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /** Nhãn hiển thị. Mặc định tiếng Việt; truyền `Partial<>` để đổi từng chuỗi. */\n labels?: Partial<FileUploadLabels>;\n disabled?: boolean;\n className?: string;\n}\n\nexport function FileUpload<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles = 1,\n onUploaded,\n onError,\n labels,\n disabled = false,\n className\n}: FileUploadProps<T>) {\n const text = { ...DEFAULT_FILE_UPLOAD_LABELS, ...labels };\n const inputId = useId();\n\n /*\n * Toàn bộ máy móc nằm ở `useFileUpload` — cùng một bộ với `ImageUploadField`. Hai component này\n * khác nhau ĐÚNG ở phần hình dạng, và đó là lý do bốn cạm bẫy của `@uppy/xhr-upload` chỉ được\n * xử lý ở MỘT chỗ.\n */\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled\n });\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...upload.inputProps} id={inputId} className=\"sr-only\" />\n\n {/*\n Vùng kéo thả. `<label>` bọc lấy nút nên bấm vào đâu trong vùng cũng mở hộp chọn tệp, và\n bàn phím vẫn tới được qua chính `<input>` (nó `sr-only`, không phải `display:none`).\n */}\n
|
|
1
|
+
{"version":3,"sources":["../../src/uploader/file-upload.tsx"],"sourcesContent":["\"use client\";\n\nimport { useId } from \"react\";\nimport { Button } from \"../components/ui/button.js\";\nimport { Progress } from \"../components/ui/progress.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { DEFAULT_FILE_UPLOAD_LABELS, type FileUploadLabels } from \"./file-upload-constants.js\";\nimport { type FileUploadResult, useFileUpload } from \"./use-file-upload.js\";\n\n/**\n * Ô tải tệp dùng chung cho mọi app Comitor — **Uppy chạy HEADLESS, giao diện là của gói này**.\n *\n * ── VÌ SAO KHÔNG DÙNG `@uppy/dashboard` HAY `@uppy/react` ──────────────────────────────────\n * Cả hai đều kéo theo **Preact** và **một bộ CSS riêng**, tức app đã có React lại ship thêm một\n * runtime giao diện thứ hai, và bảng màu của Uppy không đi qua token của Comitor nên nó KHÔNG qua\n * được cổng tương phản (`pnpm lint:a11y`).\n *\n * Đo được trên một cây đã cài Uppy 5: `@uppy/dashboard` 1.236 KB, `@uppy/react` 432 KB, `preact`\n * 1.852 KB trên đĩa. Trong khi `@uppy/core` khai `sideEffects: [\"*.css\"]` và chỉ có `UIPlugin.js`\n * import `preact` — nên **chỉ nạp `Uppy` + `XHRUpload` thì bundler cắt được cả nhánh Preact**.\n *\n * Cái ta giữ lại của Uppy là phần đáng giá và khó viết đúng: hàng đợi, tiến độ theo byte, thử lại,\n * huỷ, và bộ luật hạn chế (kiểu tệp / kích thước / số lượng) áp NGAY trên trình duyệt.\n *\n * ── VÌ SAO `XHRUpload` CHỨ KHÔNG PHẢI `AwsS3` (presigned PUT) ──────────────────────────────\n * Một app Comitor khác (babrik) tải thẳng lên S3 bằng URL đã ký. Cách đó đúng cho một bucket CÔNG\n * KHAI, nơi URL trả về chính là URL hiển thị. Comitor thì dùng **bucket RIÊNG TƯ**: database lưu\n * KHOÁ object chứ không lưu URL, và URL hiển thị được sinh lại có chữ ký ở mỗi lần đọc.\n *\n * Hệ quả: máy chủ vẫn phải là nơi đặt tên khoá, kiểm byte thật, và ghi khoá vào database. Đẩy\n * việc tải thẳng lên S3 sẽ lấy mất của máy chủ khả năng nhìn thấy nội dung tệp — với một IdP phục\n * vụ ảnh do người dùng tải lên thì đó là đánh đổi không đáng.\n *\n * `XHRUpload` POST một `multipart/form-data` tới chính endpoint của app, nên **không route nào phải\n * đổi** và không có bề mặt bảo mật mới nào mở ra.\n *\n * ── CSP ─────────────────────────────────────────────────────────────────────────────────────\n * Uppy KHÔNG dùng `eval` hay `new Function` (đã grep `@uppy/core`, `@uppy/dashboard`, `@uppy/react`:\n * 0 file). Nên nó chạy được dưới `script-src 'self' 'nonce-…'` mà không cần nới gì.\n */\n\nexport interface FileUploadProps<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\"` — khớp khuôn route của Comitor. */\n fieldName?: string;\n /** Giới hạn kích thước mỗi tệp, byte. */\n maxFileSize: number;\n /** MIME được nhận, ví dụ `[\"image/jpeg\", \"image/png\", \"image/webp\"]`. */\n allowedFileTypes: string[];\n /** Số tệp tối đa mỗi lượt. Mặc định 1. */\n maxNumberOfFiles?: number;\n /** Chạy khi MỘT tệp tải xong. Nhận nguyên thân JSON của endpoint. */\n onUploaded: (result: FileUploadResult<T>) => void;\n /**\n * Chạy khi tải hỏng. `code` là `error.code` từ thân phản hồi nếu endpoint trả theo khuôn\n * `{ error: { code } }` của Comitor — nhờ đó chỗ gọi tra được bảng dịch của chính nó.\n */\n onError?: (error: { code?: string; status?: number; message: string }) => void;\n /** Nhãn hiển thị. Mặc định tiếng Việt; truyền `Partial<>` để đổi từng chuỗi. */\n labels?: Partial<FileUploadLabels>;\n disabled?: boolean;\n className?: string;\n}\n\nexport function FileUpload<T = unknown>({\n endpoint,\n fieldName,\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles = 1,\n onUploaded,\n onError,\n labels,\n disabled = false,\n className\n}: FileUploadProps<T>) {\n const text = { ...DEFAULT_FILE_UPLOAD_LABELS, ...labels };\n const inputId = useId();\n\n /*\n * Toàn bộ máy móc nằm ở `useFileUpload` — cùng một bộ với `ImageUploadField`. Hai component này\n * khác nhau ĐÚNG ở phần hình dạng, và đó là lý do bốn cạm bẫy của `@uppy/xhr-upload` chỉ được\n * xử lý ở MỘT chỗ.\n */\n const upload = useFileUpload<T>({\n endpoint,\n ...(fieldName ? { fieldName } : {}),\n maxFileSize,\n allowedFileTypes,\n maxNumberOfFiles,\n onUploaded,\n ...(onError ? { onError } : {}),\n labels: { uploadFailed: text.uploadFailed, missingDependency: text.missingDependency },\n disabled\n });\n\n return (\n <div className={cn(\"flex flex-col gap-2\", className)}>\n <input {...upload.inputProps} id={inputId} className=\"sr-only\" />\n\n {/*\n Vùng kéo thả. `<label>` bọc lấy nút nên bấm vào đâu trong vùng cũng mở hộp chọn tệp, và\n bàn phím vẫn tới được qua chính `<input>` (nó `sr-only`, không phải `display:none`).\n */}\n <div\n {...upload.dropProps}\n /*\n * ⚠ Trạng thái \"đang kéo tệp qua\" được vẽ bằng VIỀN, không bằng nền mờ.\n *\n * Bản đầu dùng `bg-primary/5` và cổng `pnpm lint:a11y` bác ngay: **1,03:1** ở nền sáng,\n * 1,09:1 ở nền tối — mắt không phân biệt được, tức dấu hiệu kéo-thả là một dấu hiệu GIẢ.\n *\n * ⚠ Và phải là `border-primary-INK`, không phải `border-primary`. Cổng bác cả bản thứ hai:\n * `--primary` là bậc TÔ NỀN của tone, vai trò nét/chữ phải đi qua `--primary-ink` (1,40:1\n * so với 3:1 cần thiết). Cùng luật ba bậc mà CLAUDE.md của Account ghi cho `iconColor`.\n *\n * Giữ `border-2` ở CẢ hai trạng thái để bố cục không nhảy 1px khi kéo tệp qua.\n */\n className={cn(\n \"flex flex-col items-center gap-2 rounded-lg border-2 border-dashed p-4 text-center transition-colors\",\n upload.dragging ? \"border-primary-ink\" : \"border-border\"\n )}\n >\n {/*\n * ⚠ `opacity-60` của trạng thái vô hiệu nằm ở NỘI DUNG, không ở khung.\n *\n * Không phải chuyện thẩm mỹ: cổng `lint:a11y` nhân opacity vào mọi lớp cùng khối, nên để nó\n * ở khung thì `border-primary-ink` rớt còn 2,64:1. Hai trạng thái ấy không bao giờ xảy ra\n * cùng lúc (kéo-thả đã bị chặn khi `disabled || busy`), nhưng cổng đọc TĨNH nên nó ghép —\n * và ở đây cổng đúng về hướng: làm mờ cả khung đứt nét thì người dùng mất luôn dấu hiệu\n * \"chỗ này thả tệp được\".\n */}\n <div className={cn(\"flex flex-col items-center gap-2\", (disabled || upload.busy) && \"opacity-60\")}>\n <p className=\"text-muted-foreground text-sm\">{text.dropHint}</p>\n <Button asChild variant=\"outline\" size=\"sm\" disabled={disabled || upload.busy} loading={upload.busy}>\n <label htmlFor={inputId} className={disabled || upload.busy ? \"pointer-events-none\" : \"cursor-pointer\"}>\n {upload.busy ? text.uploading : text.choose}\n </label>\n </Button>\n </div>\n </div>\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":";AAmGM,cAkCE,YAlCF;AAjGN,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,UAAU;AACnB,SAAS,kCAAyD;AAClE,SAAgC,qBAAqB;AA0D9C,SAAS,WAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GAAuB;AACrB,QAAM,OAAO,EAAE,GAAG,4BAA4B,GAAG,OAAO;AACxD,QAAM,UAAU,MAAM;AAOtB,QAAM,SAAS,cAAiB;AAAA,IAC9B;AAAA,IACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,QAAQ,EAAE,cAAc,KAAK,cAAc,mBAAmB,KAAK,kBAAkB;AAAA,IACrF;AAAA,EACF,CAAC;AAED,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GACjD;AAAA,wBAAC,WAAO,GAAG,OAAO,YAAY,IAAI,SAAS,WAAU,WAAU;AAAA,IAM/D;AAAA,MAAC;AAAA;AAAA,QACE,GAAG,OAAO;AAAA,QAaX,WAAW;AAAA,UACT;AAAA,UACA,OAAO,WAAW,uBAAuB;AAAA,QAC3C;AAAA,QAWA,+BAAC,SAAI,WAAW,GAAG,qCAAqC,YAAY,OAAO,SAAS,YAAY,GAC9F;AAAA,8BAAC,OAAE,WAAU,iCAAiC,eAAK,UAAS;AAAA,UAC5D,oBAAC,UAAO,SAAO,MAAC,SAAQ,WAAU,MAAK,MAAK,UAAU,YAAY,OAAO,MAAM,SAAS,OAAO,MAC7F,8BAAC,WAAM,SAAS,SAAS,WAAW,YAAY,OAAO,OAAO,wBAAwB,kBACnF,iBAAO,OAAO,KAAK,YAAY,KAAK,QACvC,GACF;AAAA,WACF;AAAA;AAAA,IACF;AAAA,IAGC,OAAO,OAAO,oBAAC,YAAS,OAAO,OAAO,SAAS,cAAY,KAAK,WAAW,IAAK;AAAA,KACnF;AAEJ;","names":[]}
|
|
@@ -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 { cn } from \"../lib/cn.js\";\nimport { DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, 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** — đúng khuôn mà GitHub, Google, Slack\n * và Linear đều dùng cho avatar.\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ỡ\" 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 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 = { ...DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, ...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
|
|
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 { cn } from \"../lib/cn.js\";\nimport { DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, 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** — đúng khuôn mà GitHub, Google, Slack\n * và Linear đều dùng cho avatar.\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ỡ\" 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 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 = { ...DEFAULT_IMAGE_UPLOAD_FIELD_LABELS, ...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ữ \"Thả ảnh vào đây\" 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":";AA+FwB,cAmBhB,YAnBgB;AA5FxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,UAAU;AACnB,SAAS,yCAAsE;AAC/E,SAAgC,qBAAqB;AAqD9C,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,EAAE,GAAG,mCAAmC,GAAG,OAAO;AAE/D,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":[]}
|
package/package.json
CHANGED