@comitor/ui 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -14
- package/dist/chart/chart-colors.d.ts +56 -0
- package/dist/chart/chart-colors.js +21 -0
- package/dist/chart/chart-colors.js.map +1 -0
- package/dist/chart/chart.d.ts +75 -3
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +33 -42
- package/dist/chart/comitor-chart.js +13 -24
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +3 -2
- package/dist/chart/index.js +1 -0
- package/dist/chart/index.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +9 -2
- package/dist/components/composite/combobox.js +7 -0
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/data-table-constants.d.ts +40 -0
- package/dist/components/composite/data-table-constants.js +13 -0
- package/dist/components/composite/data-table-constants.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +2 -27
- package/dist/components/composite/data-table.js +1 -10
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/date-picker-constants.d.ts +21 -0
- package/dist/components/composite/date-picker-constants.js +39 -0
- package/dist/components/composite/date-picker-constants.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +2 -18
- package/dist/components/composite/date-picker.js +6 -48
- package/dist/components/composite/date-picker.js.map +1 -1
- package/dist/components/composite/form-field-constants.d.ts +26 -0
- package/dist/components/composite/form-field-constants.js +13 -0
- package/dist/components/composite/form-field-constants.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +1 -13
- package/dist/components/composite/form-field.js +1 -11
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +52 -0
- package/dist/components/composite/search-filter-bar-constants.js +18 -0
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +3 -37
- package/dist/components/composite/search-filter-bar.js +2 -15
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +56 -0
- package/dist/components/composite/table-pagination-constants.js +18 -0
- package/dist/components/composite/table-pagination-constants.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +2 -43
- package/dist/components/composite/table-pagination.js +4 -15
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/card.d.ts +35 -2
- package/dist/components/ui/card.js +17 -3
- package/dist/components/ui/card.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -0
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/shell-context.d.ts +28 -1
- package/dist/shell/shell-context.js +8 -3
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-menu-item.js +3 -3
- package/dist/shell/sidebar-menu-item.js.map +1 -1
- package/dist/shell/utils.d.ts +27 -3
- package/dist/shell/utils.js +52 -9
- package/dist/shell/utils.js.map +1 -1
- package/package.json +7 -5
|
@@ -1,50 +1,24 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { TooltipProps } from 'recharts';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
* Khác `chart.tsx` (primitive shadcn thô, tự lắp recharts), file này là tầng tiện dụng:
|
|
7
|
-
* đã chọn sẵn trục, lưới, tooltip và bảng màu theo token.
|
|
4
|
+
* Một phần tử payload recharts bơm vào tooltip.
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* Khai tại đây thay vì `TooltipProps<number, string>["payload"]`: recharts **3** đã bỏ
|
|
7
|
+
* `payload` khỏi props của `<Tooltip>` (chuyển sang context nội bộ), nên phép dẫn xuất đó
|
|
8
|
+
* hỏng kiểu ngay trong file `.d.ts` phát hành. Xem chú thích cùng nội dung ở `chart.tsx`
|
|
9
|
+
* (`ChartPayloadItem`) — cùng một lý do, cùng một cách chữa.
|
|
13
10
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
* Dùng `var(--chart-N)` chứ KHÔNG dùng `var(--color-chart-N)`: bản `--chart-*` được khai
|
|
22
|
-
* tường minh trong `:root` của `styles.css` nên chắc chắn tồn tại, còn `--color-chart-*` là
|
|
23
|
-
* biến do `@theme inline` của Tailwind sinh ra.
|
|
24
|
-
*/
|
|
25
|
-
declare const CHART_SERIES_COLORS: readonly ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"];
|
|
26
|
-
/**
|
|
27
|
-
* Màu gọi theo tên — dùng khi muốn chỉ định màu cho một series cụ thể
|
|
28
|
-
* (`series: [{ key: "loi", color: CHART_COLORS.red }]`).
|
|
29
|
-
*
|
|
30
|
-
* ⚠ Khoá `gray` của comitor-ds đổi thành `ash`: thang xám của package là `--ash-*`,
|
|
31
|
-
* `gray-*` để nguyên cho Tailwind (xem chú thích thang màu trong `styles.css`).
|
|
32
|
-
*/
|
|
33
|
-
declare const CHART_COLORS: {
|
|
34
|
-
readonly gold: "var(--chart-1)";
|
|
35
|
-
readonly goldLight: "var(--gold-100)";
|
|
36
|
-
readonly teal: "var(--chart-2)";
|
|
37
|
-
readonly navy: "var(--chart-3)";
|
|
38
|
-
readonly green: "var(--chart-4)";
|
|
39
|
-
readonly red: "var(--chart-5)";
|
|
40
|
-
readonly ash: "var(--ash-300)";
|
|
41
|
-
};
|
|
42
|
-
type ChartColorName = keyof typeof CHART_COLORS;
|
|
11
|
+
interface ComitorChartPayloadItem {
|
|
12
|
+
value?: number;
|
|
13
|
+
name?: string;
|
|
14
|
+
dataKey?: string | number;
|
|
15
|
+
color?: string;
|
|
16
|
+
}
|
|
43
17
|
interface ComitorChartTooltipProps {
|
|
44
18
|
/** Do recharts bơm vào — đừng truyền tay. */
|
|
45
19
|
active?: boolean;
|
|
46
20
|
/** Do recharts bơm vào — đừng truyền tay. */
|
|
47
|
-
payload?:
|
|
21
|
+
payload?: ComitorChartPayloadItem[];
|
|
48
22
|
/** Do recharts bơm vào — đừng truyền tay. */
|
|
49
23
|
label?: string;
|
|
50
24
|
/** Định dạng nhãn trục X (ví dụ đổi "2026-01" → "Th01/2026"). */
|
|
@@ -85,25 +59,42 @@ interface BaseChartProps {
|
|
|
85
59
|
showGrid?: boolean;
|
|
86
60
|
showLegend?: boolean;
|
|
87
61
|
valueFormatter?: (value: number) => string;
|
|
62
|
+
/**
|
|
63
|
+
* 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
|
|
64
|
+
* recharts, và đây là chủ đích.
|
|
65
|
+
*
|
|
66
|
+
* 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
|
|
67
|
+
* 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,
|
|
68
|
+
* đo được trên DOM thật:
|
|
69
|
+
*
|
|
70
|
+
* `<Bar>` `<g class="recharts-bar-rectangle">` RỖNG — `Rectangle` trả `null` khi cao 0
|
|
71
|
+
* `<Line>` path có `d` đúng nhưng `stroke-dasharray="0px 634px"` — vẽ 0px
|
|
72
|
+
* `<Area>` path có `d` đúng nhưng `<clipPath><rect width="0">` — bị cắt sạch
|
|
73
|
+
*
|
|
74
|
+
* Kết quả giống hệt nhau ở cả ba: đủ trục, đủ lưới, đủ chú giải, KHÔNG có dữ liệu — và
|
|
75
|
+
* 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
|
|
76
|
+
* đảo lại. Ai muốn hiệu ứng thì bật tường minh (`isAnimationActive` — recharts 3 chạy đúng).
|
|
77
|
+
*/
|
|
78
|
+
isAnimationActive?: boolean;
|
|
88
79
|
}
|
|
89
80
|
interface ComitorLineChartProps extends BaseChartProps {
|
|
90
81
|
series: DataSeries[];
|
|
91
82
|
}
|
|
92
83
|
/** Biểu đồ đường — dùng cho xu hướng theo thời gian. */
|
|
93
|
-
declare function ComitorLineChart({ data, xAxisKey, series, height, className, showGrid, showLegend, valueFormatter }: ComitorLineChartProps): React.JSX.Element;
|
|
84
|
+
declare function ComitorLineChart({ data, xAxisKey, series, height, className, showGrid, showLegend, valueFormatter, isAnimationActive }: ComitorLineChartProps): React.JSX.Element;
|
|
94
85
|
interface ComitorBarChartProps extends BaseChartProps {
|
|
95
86
|
series: DataSeries[];
|
|
96
87
|
/** Chồng các series lên nhau thay vì xếp cạnh. */
|
|
97
88
|
stacked?: boolean;
|
|
98
89
|
}
|
|
99
90
|
/** Biểu đồ cột — dùng cho so sánh giữa các hạng mục. Lưới chỉ kẻ ngang. */
|
|
100
|
-
declare function ComitorBarChart({ data, xAxisKey, series, height, className, showGrid, showLegend, stacked, valueFormatter }: ComitorBarChartProps): React.JSX.Element;
|
|
91
|
+
declare function ComitorBarChart({ data, xAxisKey, series, height, className, showGrid, showLegend, stacked, valueFormatter, isAnimationActive }: ComitorBarChartProps): React.JSX.Element;
|
|
101
92
|
interface ComitorAreaChartProps extends BaseChartProps {
|
|
102
93
|
series: DataSeries[];
|
|
103
94
|
/** Chồng các series lên nhau (tổng tích luỹ). */
|
|
104
95
|
stacked?: boolean;
|
|
105
96
|
}
|
|
106
97
|
/** 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. */
|
|
107
|
-
declare function ComitorAreaChart({ data, xAxisKey, series, height, className, showGrid, showLegend, stacked, valueFormatter }: ComitorAreaChartProps): React.JSX.Element;
|
|
98
|
+
declare function ComitorAreaChart({ data, xAxisKey, series, height, className, showGrid, showLegend, stacked, valueFormatter, isAnimationActive }: ComitorAreaChartProps): React.JSX.Element;
|
|
108
99
|
|
|
109
|
-
export { type BaseChartProps,
|
|
100
|
+
export { type BaseChartProps, ComitorAreaChart, type ComitorAreaChartProps, ComitorBarChart, type ComitorBarChartProps, ComitorChartContainer, type ComitorChartContainerProps, type ComitorChartPayloadItem, ComitorChartTooltip, type ComitorChartTooltipProps, ComitorLineChart, type ComitorLineChartProps, type DataSeries };
|
|
@@ -15,22 +15,7 @@ import {
|
|
|
15
15
|
YAxis
|
|
16
16
|
} from "recharts";
|
|
17
17
|
import { cn } from "../lib/cn.js";
|
|
18
|
-
|
|
19
|
-
"var(--chart-1)",
|
|
20
|
-
"var(--chart-2)",
|
|
21
|
-
"var(--chart-3)",
|
|
22
|
-
"var(--chart-4)",
|
|
23
|
-
"var(--chart-5)"
|
|
24
|
-
];
|
|
25
|
-
const CHART_COLORS = {
|
|
26
|
-
gold: "var(--chart-1)",
|
|
27
|
-
goldLight: "var(--gold-100)",
|
|
28
|
-
teal: "var(--chart-2)",
|
|
29
|
-
navy: "var(--chart-3)",
|
|
30
|
-
green: "var(--chart-4)",
|
|
31
|
-
red: "var(--chart-5)",
|
|
32
|
-
ash: "var(--ash-300)"
|
|
33
|
-
};
|
|
18
|
+
import { CHART_SERIES_COLORS } from "./chart-colors.js";
|
|
34
19
|
function seriesColor(series, index) {
|
|
35
20
|
return series.color ?? CHART_SERIES_COLORS[index % CHART_SERIES_COLORS.length] ?? CHART_SERIES_COLORS[0];
|
|
36
21
|
}
|
|
@@ -88,7 +73,8 @@ function ComitorLineChart({
|
|
|
88
73
|
className,
|
|
89
74
|
showGrid = true,
|
|
90
75
|
showLegend = false,
|
|
91
|
-
valueFormatter
|
|
76
|
+
valueFormatter,
|
|
77
|
+
isAnimationActive = false
|
|
92
78
|
}) {
|
|
93
79
|
return /* @__PURE__ */ jsx(ComitorChartContainer, { height, className, children: /* @__PURE__ */ jsxs(LineChart, { data, margin: CHART_MARGIN, children: [
|
|
94
80
|
showGrid && /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }),
|
|
@@ -105,7 +91,8 @@ function ComitorLineChart({
|
|
|
105
91
|
stroke: seriesColor(s, i),
|
|
106
92
|
strokeWidth: 2,
|
|
107
93
|
dot: false,
|
|
108
|
-
activeDot: { r: 4, strokeWidth: 0 }
|
|
94
|
+
activeDot: { r: 4, strokeWidth: 0 },
|
|
95
|
+
isAnimationActive
|
|
109
96
|
},
|
|
110
97
|
s.key
|
|
111
98
|
))
|
|
@@ -120,7 +107,8 @@ function ComitorBarChart({
|
|
|
120
107
|
showGrid = true,
|
|
121
108
|
showLegend = false,
|
|
122
109
|
stacked = false,
|
|
123
|
-
valueFormatter
|
|
110
|
+
valueFormatter,
|
|
111
|
+
isAnimationActive = false
|
|
124
112
|
}) {
|
|
125
113
|
return /* @__PURE__ */ jsx(ComitorChartContainer, { height, className, children: /* @__PURE__ */ jsxs(BarChart, { data, margin: CHART_MARGIN, children: [
|
|
126
114
|
showGrid && /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)", vertical: false }),
|
|
@@ -135,7 +123,8 @@ function ComitorBarChart({
|
|
|
135
123
|
name: s.name || s.key,
|
|
136
124
|
fill: seriesColor(s, i),
|
|
137
125
|
radius: [4, 4, 0, 0],
|
|
138
|
-
stackId: stacked ? "stack" : void 0
|
|
126
|
+
stackId: stacked ? "stack" : void 0,
|
|
127
|
+
isAnimationActive
|
|
139
128
|
},
|
|
140
129
|
s.key
|
|
141
130
|
))
|
|
@@ -150,7 +139,8 @@ function ComitorAreaChart({
|
|
|
150
139
|
showGrid = true,
|
|
151
140
|
showLegend = false,
|
|
152
141
|
stacked = false,
|
|
153
|
-
valueFormatter
|
|
142
|
+
valueFormatter,
|
|
143
|
+
isAnimationActive = false
|
|
154
144
|
}) {
|
|
155
145
|
return /* @__PURE__ */ jsx(ComitorChartContainer, { height, className, children: /* @__PURE__ */ jsxs(AreaChart, { data, margin: CHART_MARGIN, children: [
|
|
156
146
|
showGrid && /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }),
|
|
@@ -170,7 +160,8 @@ function ComitorAreaChart({
|
|
|
170
160
|
fill: color,
|
|
171
161
|
fillOpacity: 0.15,
|
|
172
162
|
strokeWidth: 2,
|
|
173
|
-
stackId: stacked ? "stack" : void 0
|
|
163
|
+
stackId: stacked ? "stack" : void 0,
|
|
164
|
+
isAnimationActive
|
|
174
165
|
},
|
|
175
166
|
s.key
|
|
176
167
|
);
|
|
@@ -178,8 +169,6 @@ function ComitorAreaChart({
|
|
|
178
169
|
] }) });
|
|
179
170
|
}
|
|
180
171
|
export {
|
|
181
|
-
CHART_COLORS,
|
|
182
|
-
CHART_SERIES_COLORS,
|
|
183
172
|
ComitorAreaChart,
|
|
184
173
|
ComitorBarChart,
|
|
185
174
|
ComitorChartContainer,
|
|
@@ -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 type TooltipProps,\n XAxis,\n YAxis\n} from \"recharts\";\nimport { cn } from \"../lib/cn.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/**\n * Bảng màu series mặc định — trỏ vào `--chart-1…5` trong `styles.css`.\n *\n * Vì sao không lấy thẳng `var(--gold-300)`, `var(--teal)`… như comitor-ds: bảng màu biểu đồ\n * cần đổi ĐỘC LẬP với brand (app đổi `--chart-*` ở `<body>` là xong), và `--chart-3` có bản\n * dark riêng (navy nguyên bản chìm vào nền tối).\n *\n * Dùng `var(--chart-N)` chứ KHÔNG dùng `var(--color-chart-N)`: bản `--chart-*` được khai\n * tường minh trong `:root` của `styles.css` nên chắc chắn tồn tại, còn `--color-chart-*` là\n * biến do `@theme inline` của Tailwind sinh ra.\n */\nexport const CHART_SERIES_COLORS = [\n \"var(--chart-1)\",\n \"var(--chart-2)\",\n \"var(--chart-3)\",\n \"var(--chart-4)\",\n \"var(--chart-5)\"\n] as const;\n\n/**\n * Màu gọi theo tên — dùng khi muốn chỉ định màu cho một series cụ thể\n * (`series: [{ key: \"loi\", color: CHART_COLORS.red }]`).\n *\n * ⚠ Khoá `gray` của comitor-ds đổi thành `ash`: thang xám của package là `--ash-*`,\n * `gray-*` để nguyên cho Tailwind (xem chú thích thang màu trong `styles.css`).\n */\nexport const CHART_COLORS = {\n gold: \"var(--chart-1)\",\n goldLight: \"var(--gold-100)\",\n teal: \"var(--chart-2)\",\n navy: \"var(--chart-3)\",\n green: \"var(--chart-4)\",\n red: \"var(--chart-5)\",\n ash: \"var(--ash-300)\"\n} as const;\n\nexport type ChartColorName = keyof typeof CHART_COLORS;\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\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?: TooltipProps<number, string>[\"payload\"];\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\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}: 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 />\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}: 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 />\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}: 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 />\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\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":[]}
|
package/dist/chart/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { ChartConfig, ChartContainer, ChartContainerProps, ChartLegend, ChartLegendContent, ChartLegendContentProps, ChartStyle, ChartStyleProps, ChartTooltip, ChartTooltipContent, ChartTooltipContentProps, useChart } from './chart.js';
|
|
2
|
-
export {
|
|
1
|
+
export { ChartConfig, ChartContainer, ChartContainerProps, ChartLegend, ChartLegendContent, ChartLegendContentProps, ChartPayloadItem, ChartStyle, ChartStyleProps, ChartTooltip, ChartTooltipContent, ChartTooltipContentProps, useChart } from './chart.js';
|
|
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
4
|
import 'react';
|
|
4
5
|
import 'recharts';
|
package/dist/chart/index.js
CHANGED
package/dist/chart/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/chart/index.ts"],"sourcesContent":["/**\n * Entry \"@comitor/ui/chart\" — Chart* (shadcn) + ComitorChart* + CHART_COLORS.\n *\n * Tách riêng vì `recharts` nặng (~100KB) và là peer OPTIONAL; đa số trang không vẽ biểu đồ.\n *\n * ⚠ Va tên đã xử lý ở tầng file: `chart.tsx` giữ tên trần `ChartContainer`/`ChartTooltip`\n * (API shadcn); bản tiện dụng của comitor-ds mang tiền tố `ComitorChartContainer`/\n * `ComitorChartTooltip`. Đừng đặt lại tên trần trong `comitor-chart.tsx`, barrel này\n * dùng `export *` nên sẽ vỡ ngay lúc build.\n */\nexport * from \"./chart.js\";\nexport * from \"./comitor-chart.js\";\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/chart/index.ts"],"sourcesContent":["/**\n * Entry \"@comitor/ui/chart\" — Chart* (shadcn) + ComitorChart* + CHART_COLORS.\n *\n * Tách riêng vì `recharts` nặng (~100KB) và là peer OPTIONAL; đa số trang không vẽ biểu đồ.\n *\n * ⚠ Hai đường dùng, hai mặc định khác nhau về hiệu ứng vẽ dần. `Comitor*Chart` tắt sẵn\n * (`isAnimationActive` mặc định `false`). `ChartContainer` KHÔNG tắt hộ được — mark là children\n * do app viết — nên trên recharts 2.15.x app phải tự ghi `isAnimationActive={false}` lên từng\n * `<Bar>`/`<Line>`/`<Area>`, nếu không biểu đồ ra đủ trục đủ lưới mà KHÔNG có dữ liệu, im lặng.\n * Chi tiết + số đo ở JSDoc của `ChartContainer` trong `chart.tsx` và README §4.4.\n *\n * ⚠ Va tên đã xử lý ở tầng file: `chart.tsx` giữ tên trần `ChartContainer`/`ChartTooltip`\n * (API shadcn); bản tiện dụng của comitor-ds mang tiền tố `ComitorChartContainer`/\n * `ComitorChartTooltip`. Đừng đặt lại tên trần trong `comitor-chart.tsx`, barrel này\n * dùng `export *` nên sẽ vỡ ngay lúc build.\n */\n/*\n * ⚠ `chart-colors.js` phải đứng RIÊNG ở đây, không được đi vòng qua `comitor-chart.js`.\n * `comitor-chart.tsx` là file `\"use client\"`; bundler RSC cắt ranh giới theo FILE nên mọi\n * export chảy qua nó — kể cả re-export của một hằng dữ liệu — đều tới Server Component dưới\n * dạng client-reference proxy (`CHART_COLORS.gold` ra `undefined`, im lặng). Xuất thẳng từ\n * module thuần là cách duy nhất để Server Component đọc được giá trị thật.\n */\n\nexport * from \"./chart.js\";\nexport * from \"./chart-colors.js\";\nexport * from \"./comitor-chart.js\";\n"],"mappings":"AAwBA,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -36,8 +36,15 @@ interface ComboboxSharedProps {
|
|
|
36
36
|
"aria-label"?: string;
|
|
37
37
|
"aria-labelledby"?: string;
|
|
38
38
|
"aria-describedby"?: string;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Ô đang lỗi. Ngoài `aria-invalid` cho trình đọc màn hình, trigger còn chuyển sang viền
|
|
41
|
+
* `--destructive-ink` — cùng tín hiệu thị giác mà `Input` đã có (nhánh nằm ở `button.tsx`).
|
|
42
|
+
*
|
|
43
|
+
* Nhận cả `boolean` lẫn `"true"`/`"false"` để dùng thẳng được `aria-invalid={!!errors.x}`
|
|
44
|
+
* của react-hook-form. Giá trị falsy KHÔNG phát thuộc tính ra DOM.
|
|
45
|
+
*/
|
|
46
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
47
|
+
"aria-required"?: boolean | "true" | "false";
|
|
41
48
|
}
|
|
42
49
|
interface ComboboxProps extends ComboboxSharedProps {
|
|
43
50
|
/** Giá trị đang chọn; `null`/`undefined` = chưa chọn */
|
|
@@ -8,6 +8,9 @@ import { Button } from "../ui/button.js";
|
|
|
8
8
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command.js";
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
10
10
|
import { removeVietnameseTones } from "./text-utils.js";
|
|
11
|
+
function isAriaInvalid(value) {
|
|
12
|
+
return value === true || value === "true";
|
|
13
|
+
}
|
|
11
14
|
function vietnameseFilter(value, search, keywords) {
|
|
12
15
|
if (!search) return 1;
|
|
13
16
|
const haystack = removeVietnameseTones(`${value} ${keywords?.join(" ") ?? ""}`).toLowerCase();
|
|
@@ -88,6 +91,7 @@ function Combobox({
|
|
|
88
91
|
const [search, setSearch] = useState("");
|
|
89
92
|
const isAsync = onSearchChange !== void 0;
|
|
90
93
|
const selected = options.find((option) => option.value === value);
|
|
94
|
+
const invalid = isAriaInvalid(aria["aria-invalid"]);
|
|
91
95
|
const handleSearch = (next) => {
|
|
92
96
|
setSearch(next);
|
|
93
97
|
onSearchChange?.(next);
|
|
@@ -116,6 +120,7 @@ function Combobox({
|
|
|
116
120
|
className
|
|
117
121
|
),
|
|
118
122
|
...aria,
|
|
123
|
+
"aria-invalid": invalid || void 0,
|
|
119
124
|
children: [
|
|
120
125
|
/* @__PURE__ */ jsx("span", { className: "truncate", children: selected ? selected.label : placeholder }),
|
|
121
126
|
/* @__PURE__ */ jsx(ChevronsUpDown, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" })
|
|
@@ -186,6 +191,7 @@ function MultiCombobox({
|
|
|
186
191
|
const [open, setOpen] = useState(false);
|
|
187
192
|
const [search, setSearch] = useState("");
|
|
188
193
|
const isAsync = onSearchChange !== void 0;
|
|
194
|
+
const invalid = isAriaInvalid(aria["aria-invalid"]);
|
|
189
195
|
const selectedOptions = options.filter((option) => value.includes(option.value));
|
|
190
196
|
const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));
|
|
191
197
|
const overflow = selectedOptions.length - shown.length;
|
|
@@ -224,6 +230,7 @@ function MultiCombobox({
|
|
|
224
230
|
className
|
|
225
231
|
),
|
|
226
232
|
...aria,
|
|
233
|
+
"aria-invalid": invalid || void 0,
|
|
227
234
|
children: [
|
|
228
235
|
/* @__PURE__ */ jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: value.length === 0 ? placeholder : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
229
236
|
shown.map((option) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "max-w-[10rem]", children: /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label }) }, option.value)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/combobox.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, ChevronsUpDown, Loader2, X } from \"lucide-react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Dòng phụ mờ dưới nhãn (vd email của người dùng, mã của khách hàng) */\n description?: string;\n icon?: IconComponent;\n disabled?: boolean;\n /** Nhãn nhóm — các mục cùng nhóm được gom lại và có tiêu đề nhóm */\n group?: string;\n /** Từ khoá phụ để tìm ra mục này (vd mã số, tên viết tắt) */\n keywords?: string[];\n}\n\ninterface ComboboxSharedProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n disabled?: boolean;\n /**\n * Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).\n * Không truyền = lọc tại chỗ, không dấu tiếng Việt.\n */\n onSearchChange?: (search: string) => void;\n /** Đang tải kết quả (chỉ có nghĩa ở chế độ bất đồng bộ) */\n loading?: boolean;\n /** Cho phép xoá lựa chọn bằng nút X trên trigger */\n clearable?: boolean;\n className?: string;\n /** className của panel popover (vd đặt bề rộng cố định) */\n contentClassName?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\nexport interface ComboboxProps extends ComboboxSharedProps {\n /** Giá trị đang chọn; `null`/`undefined` = chưa chọn */\n value?: string | null;\n onValueChange: (value: string | null) => void;\n}\n\nexport interface MultiComboboxProps extends ComboboxSharedProps {\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Số nhãn hiện trên trigger trước khi gộp thành \"+k\" (mặc định 2) */\n maxDisplay?: number;\n /** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */\n maxSelected?: number;\n}\n\n/**\n * Lọc không dấu cho cmdk: so khớp cả value, label lẫn keywords.\n *\n * cmdk mặc định chấm điểm theo thuật toán riêng có phân biệt dấu — người Việt gõ \"nguyen\"\n * sẽ không ra \"Nguyễn\". Trả 1/0 là đủ vì thứ tự hiển thị đã theo thứ tự app truyền vào.\n */\nfunction vietnameseFilter(value: string, search: string, keywords?: string[]): number {\n if (!search) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(search).toLowerCase()) ? 1 : 0;\n}\n\n/** Gom mục theo `group`, giữ nguyên thứ tự xuất hiện đầu tiên của mỗi nhóm. */\nfunction groupOptions(options: ComboboxOption[]): Array<{ label: string | undefined; items: ComboboxOption[] }> {\n const groups = new Map<string, ComboboxOption[]>();\n for (const option of options) {\n const key = option.group ?? \"\";\n const bucket = groups.get(key);\n if (bucket) bucket.push(option);\n else groups.set(key, [option]);\n }\n return [...groups.entries()].map(([label, items]) => ({ label: label || undefined, items }));\n}\n\nfunction OptionRow({ option, selected }: { option: ComboboxOption; selected: boolean }) {\n const Icon = option.icon;\n return (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\"flex size-4 shrink-0 items-center justify-center\", !selected && \"invisible\")}\n >\n <Check className=\"size-4\" />\n </span>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{option.label}</span>\n {option.description && (\n <span className=\"block truncate text-xs text-muted-foreground\">{option.description}</span>\n )}\n </span>\n </>\n );\n}\n\nfunction useComboboxList(options: ComboboxOption[], isAsync: boolean) {\n const groups = useMemo(() => groupOptions(options), [options]);\n return { groups, shouldFilter: !isAsync };\n}\n\nfunction ComboboxPanel({\n options,\n isAsync,\n loading,\n search,\n onSearch,\n searchPlaceholder,\n emptyText,\n renderItem\n}: {\n options: ComboboxOption[];\n isAsync: boolean;\n loading: boolean;\n search: string;\n onSearch: (value: string) => void;\n searchPlaceholder: string;\n emptyText: string;\n renderItem: (option: ComboboxOption) => ReactNode;\n}) {\n const { groups, shouldFilter } = useComboboxList(options, isAsync);\n\n return (\n <Command shouldFilter={shouldFilter} filter={vietnameseFilter}>\n <CommandInput placeholder={searchPlaceholder} value={search} onValueChange={onSearch} />\n <CommandList>\n {loading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground\">\n <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n Đang tải…\n </div>\n ) : (\n <>\n <CommandEmpty>{emptyText}</CommandEmpty>\n {groups.map((group) => (\n <CommandGroup key={group.label ?? \"__default__\"} heading={group.label}>\n {group.items.map(renderItem)}\n </CommandGroup>\n ))}\n </>\n )}\n </CommandList>\n </Command>\n );\n}\n\n/**\n * Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.\n *\n * Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ\n * (app tự gọi API và cập nhật `options`).\n */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const selected = options.find((option) => option.value === value);\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && Boolean(selected) && !disabled;\n\n return (\n /* Nút xóa là ANH EM của trigger, không nằm trong nó: <button> lồng <button> là HTML không\n hợp lệ, và nếu hạ xuống <span role=\"button\" tabIndex={-1}> thì người dùng bàn phím vĩnh\n viễn không tới được nút xóa. Đặt tuyệt đối chồng lên phần đệm phải của trigger. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"w-full justify-between gap-2 font-normal\",\n !selected && \"text-muted-foreground\",\n showClear && \"pr-12\",\n className\n )}\n {...aria}\n >\n <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n {/* Chevron BÁO TRẠNG THÁI (aria-expanded) trên control còn bấm được → ngưỡng 3:1\n của WCAG 1.4.11, `opacity-*` không miễn trừ. `opacity-50` chỉ được 2,03:1 (light)\n / 2,30:1 (dark) khi trigger đang ở trạng thái placeholder (kế thừa\n `text-muted-foreground`) ✗. Nêu thẳng `text-muted-foreground` cho icon: 5,23:1\n (light) / 5,70:1 (dark) ✓, và không đổi theo việc đã chọn hay chưa. */}\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled}\n onSelect={() => {\n onValueChange(option.value === value ? null : option.value);\n setOpen(false);\n setSearch(\"\");\n if (isAsync) onSearchChange?.(\"\");\n }}\n >\n <OptionRow option={option} selected={option.value === value} />\n </CommandItem>\n )}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa lựa chọn\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n\n/**\n * Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.\n *\n * Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành \"+k\": danh sách 12 người thực hiện\n * mà đổ hết ra trigger thì hàng form vỡ bố cục.\n */\nexport function MultiCombobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n\n const selectedOptions = options.filter((option) => value.includes(option.value));\n const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));\n const overflow = selectedOptions.length - shown.length;\n const limitReached = maxSelected !== undefined && value.length >= maxSelected;\n\n const toggle = (optionValue: string) => {\n if (value.includes(optionValue)) {\n onValueChange(value.filter((item) => item !== optionValue));\n return;\n }\n if (limitReached) return;\n onValueChange([...value, optionValue]);\n };\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && value.length > 0 && !disabled;\n\n return (\n // Cùng lý do như Combobox đơn: nút xóa là anh em của trigger để bấm được bằng bàn phím.\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"h-auto min-h-9 w-full justify-between gap-2 py-1.5 font-normal\",\n value.length === 0 && \"text-muted-foreground\",\n showClear && \"pr-12\",\n className\n )}\n {...aria}\n >\n <span className=\"flex min-w-0 flex-wrap items-center gap-1\">\n {value.length === 0 ? (\n placeholder\n ) : (\n <>\n {shown.map((option) => (\n <Badge key={option.value} variant=\"secondary\" className=\"max-w-[10rem]\">\n <span className=\"truncate\">{option.label}</span>\n </Badge>\n ))}\n {overflow > 0 && <Badge variant=\"outline\">+{overflow}</Badge>}\n </>\n )}\n </span>\n {/* Chevron BÁO TRẠNG THÁI (aria-expanded) trên control còn bấm được → ngưỡng 3:1\n của WCAG 1.4.11, `opacity-*` không miễn trừ. `opacity-50` chỉ được 2,03:1 (light)\n / 2,30:1 (dark) khi trigger đang ở trạng thái placeholder (kế thừa\n `text-muted-foreground`) ✗. Nêu thẳng `text-muted-foreground` cho icon: 5,23:1\n (light) / 5,70:1 (dark) ✓, và không đổi theo việc đã chọn hay chưa. */}\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => {\n const isSelected = value.includes(option.value);\n return (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled || (limitReached && !isSelected)}\n onSelect={() => toggle(option.value)}\n >\n <OptionRow option={option} selected={isSelected} />\n </CommandItem>\n );\n }}\n />\n {maxSelected !== undefined && (\n <p className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n Đã chọn {value.length}/{maxSelected}\n </p>\n )}\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa tất cả lựa chọn\"\n onClick={() => onValueChange([])}\n className=\"absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/combobox.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, ChevronsUpDown, Loader2, X } from \"lucide-react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"../ui/command.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport type { IconComponent } from \"./empty-state.js\";\nimport { removeVietnameseTones } from \"./text-utils.js\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Dòng phụ mờ dưới nhãn (vd email của người dùng, mã của khách hàng) */\n description?: string;\n icon?: IconComponent;\n disabled?: boolean;\n /** Nhãn nhóm — các mục cùng nhóm được gom lại và có tiêu đề nhóm */\n group?: string;\n /** Từ khoá phụ để tìm ra mục này (vd mã số, tên viết tắt) */\n keywords?: string[];\n}\n\ninterface ComboboxSharedProps {\n options: ComboboxOption[];\n placeholder?: string;\n searchPlaceholder?: string;\n emptyText?: string;\n disabled?: boolean;\n /**\n * Có mặt = chế độ BẤT ĐỒNG BỘ: tắt lọc nội bộ của cmdk, app tự lọc `options` (thường là gọi API).\n * Không truyền = lọc tại chỗ, không dấu tiếng Việt.\n */\n onSearchChange?: (search: string) => void;\n /** Đang tải kết quả (chỉ có nghĩa ở chế độ bất đồng bộ) */\n loading?: boolean;\n /** Cho phép xoá lựa chọn bằng nút X trên trigger */\n clearable?: boolean;\n className?: string;\n /** className của panel popover (vd đặt bề rộng cố định) */\n contentClassName?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n /**\n * Ô đang lỗi. Ngoài `aria-invalid` cho trình đọc màn hình, trigger còn chuyển sang viền\n * `--destructive-ink` — cùng tín hiệu thị giác mà `Input` đã có (nhánh nằm ở `button.tsx`).\n *\n * Nhận cả `boolean` lẫn `\"true\"`/`\"false\"` để dùng thẳng được `aria-invalid={!!errors.x}`\n * của react-hook-form. Giá trị falsy KHÔNG phát thuộc tính ra DOM.\n */\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n}\n\n/** `aria-invalid` nhận nhiều hình dạng — quy về một boolean trước khi dùng làm điều kiện. */\nfunction isAriaInvalid(value: ComboboxSharedProps[\"aria-invalid\"]): boolean {\n return value === true || value === \"true\";\n}\n\nexport interface ComboboxProps extends ComboboxSharedProps {\n /** Giá trị đang chọn; `null`/`undefined` = chưa chọn */\n value?: string | null;\n onValueChange: (value: string | null) => void;\n}\n\nexport interface MultiComboboxProps extends ComboboxSharedProps {\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Số nhãn hiện trên trigger trước khi gộp thành \"+k\" (mặc định 2) */\n maxDisplay?: number;\n /** Giới hạn số lựa chọn; đạt ngưỡng thì các mục còn lại bị khoá */\n maxSelected?: number;\n}\n\n/**\n * Lọc không dấu cho cmdk: so khớp cả value, label lẫn keywords.\n *\n * cmdk mặc định chấm điểm theo thuật toán riêng có phân biệt dấu — người Việt gõ \"nguyen\"\n * sẽ không ra \"Nguyễn\". Trả 1/0 là đủ vì thứ tự hiển thị đã theo thứ tự app truyền vào.\n */\nfunction vietnameseFilter(value: string, search: string, keywords?: string[]): number {\n if (!search) return 1;\n const haystack = removeVietnameseTones(`${value} ${keywords?.join(\" \") ?? \"\"}`).toLowerCase();\n return haystack.includes(removeVietnameseTones(search).toLowerCase()) ? 1 : 0;\n}\n\n/** Gom mục theo `group`, giữ nguyên thứ tự xuất hiện đầu tiên của mỗi nhóm. */\nfunction groupOptions(options: ComboboxOption[]): Array<{ label: string | undefined; items: ComboboxOption[] }> {\n const groups = new Map<string, ComboboxOption[]>();\n for (const option of options) {\n const key = option.group ?? \"\";\n const bucket = groups.get(key);\n if (bucket) bucket.push(option);\n else groups.set(key, [option]);\n }\n return [...groups.entries()].map(([label, items]) => ({ label: label || undefined, items }));\n}\n\nfunction OptionRow({ option, selected }: { option: ComboboxOption; selected: boolean }) {\n const Icon = option.icon;\n return (\n <>\n <span\n aria-hidden=\"true\"\n className={cn(\"flex size-4 shrink-0 items-center justify-center\", !selected && \"invisible\")}\n >\n <Check className=\"size-4\" />\n </span>\n {Icon && <Icon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate\">{option.label}</span>\n {option.description && (\n <span className=\"block truncate text-xs text-muted-foreground\">{option.description}</span>\n )}\n </span>\n </>\n );\n}\n\nfunction useComboboxList(options: ComboboxOption[], isAsync: boolean) {\n const groups = useMemo(() => groupOptions(options), [options]);\n return { groups, shouldFilter: !isAsync };\n}\n\nfunction ComboboxPanel({\n options,\n isAsync,\n loading,\n search,\n onSearch,\n searchPlaceholder,\n emptyText,\n renderItem\n}: {\n options: ComboboxOption[];\n isAsync: boolean;\n loading: boolean;\n search: string;\n onSearch: (value: string) => void;\n searchPlaceholder: string;\n emptyText: string;\n renderItem: (option: ComboboxOption) => ReactNode;\n}) {\n const { groups, shouldFilter } = useComboboxList(options, isAsync);\n\n return (\n <Command shouldFilter={shouldFilter} filter={vietnameseFilter}>\n <CommandInput placeholder={searchPlaceholder} value={search} onValueChange={onSearch} />\n <CommandList>\n {loading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground\">\n <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n Đang tải…\n </div>\n ) : (\n <>\n <CommandEmpty>{emptyText}</CommandEmpty>\n {groups.map((group) => (\n <CommandGroup key={group.label ?? \"__default__\"} heading={group.label}>\n {group.items.map(renderItem)}\n </CommandGroup>\n ))}\n </>\n )}\n </CommandList>\n </Command>\n );\n}\n\n/**\n * Ô chọn một giá trị có tìm kiếm — cmdk trong Popover, rộng bằng trigger.\n *\n * Mặc định tìm KHÔNG DẤU tiếng Việt. Truyền `onSearchChange` để chuyển sang chế độ bất đồng bộ\n * (app tự gọi API và cập nhật `options`).\n */\nexport function Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = false,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: ComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const selected = options.find((option) => option.value === value);\n const invalid = isAriaInvalid(aria[\"aria-invalid\"]);\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && Boolean(selected) && !disabled;\n\n return (\n /* Nút xóa là ANH EM của trigger, không nằm trong nó: <button> lồng <button> là HTML không\n hợp lệ, và nếu hạ xuống <span role=\"button\" tabIndex={-1}> thì người dùng bàn phím vĩnh\n viễn không tới được nút xóa. Đặt tuyệt đối chồng lên phần đệm phải của trigger. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"w-full justify-between gap-2 font-normal\",\n !selected && \"text-muted-foreground\",\n showClear && \"pr-12\",\n className\n )}\n {...aria}\n // Sau `{...aria}`: chỉ phát thuộc tính khi THẬT SỰ lỗi, để `aria-invalid={false}`\n // không rơi ra DOM thành `aria-invalid=\"false\"` (hợp lệ nhưng thừa).\n aria-invalid={invalid || undefined}\n >\n <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n {/* Chevron BÁO TRẠNG THÁI (aria-expanded) trên control còn bấm được → ngưỡng 3:1\n của WCAG 1.4.11, `opacity-*` không miễn trừ. `opacity-50` chỉ được 2,03:1 (light)\n / 2,30:1 (dark) khi trigger đang ở trạng thái placeholder (kế thừa\n `text-muted-foreground`) ✗. Nêu thẳng `text-muted-foreground` cho icon: 5,23:1\n (light) / 5,70:1 (dark) ✓, và không đổi theo việc đã chọn hay chưa. */}\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled}\n onSelect={() => {\n onValueChange(option.value === value ? null : option.value);\n setOpen(false);\n setSearch(\"\");\n if (isAsync) onSearchChange?.(\"\");\n }}\n >\n <OptionRow option={option} selected={option.value === value} />\n </CommandItem>\n )}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa lựa chọn\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n\n/**\n * Ô chọn NHIỀU giá trị — cùng hạ tầng với `Combobox`, popover không tự đóng sau mỗi lần chọn.\n *\n * Trigger hiện tối đa `maxDisplay` nhãn rồi gộp phần dư thành \"+k\": danh sách 12 người thực hiện\n * mà đổ hết ra trigger thì hàng form vỡ bố cục.\n */\nexport function MultiCombobox({\n options,\n value,\n onValueChange,\n placeholder = \"Chọn…\",\n searchPlaceholder = \"Tìm kiếm…\",\n emptyText = \"Không tìm thấy kết quả.\",\n disabled = false,\n onSearchChange,\n loading = false,\n clearable = true,\n maxDisplay = 2,\n maxSelected,\n className,\n contentClassName,\n id,\n name,\n ...aria\n}: MultiComboboxProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState(\"\");\n const isAsync = onSearchChange !== undefined;\n const invalid = isAriaInvalid(aria[\"aria-invalid\"]);\n\n const selectedOptions = options.filter((option) => value.includes(option.value));\n const shown = selectedOptions.slice(0, Math.max(1, maxDisplay));\n const overflow = selectedOptions.length - shown.length;\n const limitReached = maxSelected !== undefined && value.length >= maxSelected;\n\n const toggle = (optionValue: string) => {\n if (value.includes(optionValue)) {\n onValueChange(value.filter((item) => item !== optionValue));\n return;\n }\n if (limitReached) return;\n onValueChange([...value, optionValue]);\n };\n\n const handleSearch = (next: string) => {\n setSearch(next);\n onSearchChange?.(next);\n };\n\n const showClear = clearable && value.length > 0 && !disabled;\n\n return (\n // Cùng lý do như Combobox đơn: nút xóa là anh em của trigger để bấm được bằng bàn phím.\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n id={id}\n name={name}\n disabled={disabled}\n className={cn(\n \"h-auto min-h-9 w-full justify-between gap-2 py-1.5 font-normal\",\n value.length === 0 && \"text-muted-foreground\",\n showClear && \"pr-12\",\n className\n )}\n {...aria}\n // Cùng lý do như Combobox đơn.\n aria-invalid={invalid || undefined}\n >\n <span className=\"flex min-w-0 flex-wrap items-center gap-1\">\n {value.length === 0 ? (\n placeholder\n ) : (\n <>\n {shown.map((option) => (\n <Badge key={option.value} variant=\"secondary\" className=\"max-w-[10rem]\">\n <span className=\"truncate\">{option.label}</span>\n </Badge>\n ))}\n {overflow > 0 && <Badge variant=\"outline\">+{overflow}</Badge>}\n </>\n )}\n </span>\n {/* Chevron BÁO TRẠNG THÁI (aria-expanded) trên control còn bấm được → ngưỡng 3:1\n của WCAG 1.4.11, `opacity-*` không miễn trừ. `opacity-50` chỉ được 2,03:1 (light)\n / 2,30:1 (dark) khi trigger đang ở trạng thái placeholder (kế thừa\n `text-muted-foreground`) ✗. Nêu thẳng `text-muted-foreground` cho icon: 5,23:1\n (light) / 5,70:1 (dark) ✓, và không đổi theo việc đã chọn hay chưa. */}\n <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className={cn(\"w-(--radix-popover-trigger-width) p-0\", contentClassName)}>\n <ComboboxPanel\n options={options}\n isAsync={isAsync}\n loading={loading}\n search={search}\n onSearch={handleSearch}\n searchPlaceholder={searchPlaceholder}\n emptyText={emptyText}\n renderItem={(option) => {\n const isSelected = value.includes(option.value);\n return (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, option.description ?? \"\", ...(option.keywords ?? [])]}\n disabled={option.disabled || (limitReached && !isSelected)}\n onSelect={() => toggle(option.value)}\n >\n <OptionRow option={option} selected={isSelected} />\n </CommandItem>\n );\n }}\n />\n {maxSelected !== undefined && (\n <p className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n Đã chọn {value.length}/{maxSelected}\n </p>\n )}\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa tất cả lựa chọn\"\n onClick={() => onValueChange([])}\n className=\"absolute right-8 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nhãn mặc định của `DataTable` / `DataTableColumnToggle`.
|
|
3
|
+
*
|
|
4
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
5
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
6
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
7
|
+
* (nhãn mặc định, preset, danh sách chọn) là thứ trang server hay đọc nhất — spread để override
|
|
8
|
+
* một khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `data-table.tsx` chỉ được `import`,
|
|
11
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Toàn bộ chuỗi hiển thị (kể cả chuỗi dành riêng cho trình đọc màn hình) của `DataTable` và
|
|
15
|
+
* `DataTableColumnToggle` — app tiếng Anh (hoặc ngôn ngữ khác) override từng khoá qua prop
|
|
16
|
+
* `labels`. Mặc định là tiếng Việt.
|
|
17
|
+
*
|
|
18
|
+
* Cùng khuôn với `ShellLabels` ở tầng shell: gói dùng chung cho MỌI app Comitor nên chuỗi
|
|
19
|
+
* chôn cứng trong component là ngõ cụt cho app đa ngôn ngữ.
|
|
20
|
+
*/
|
|
21
|
+
interface DataTableLabels {
|
|
22
|
+
/** aria-label ô chọn ở đầu bảng khi CHƯA chọn hết */
|
|
23
|
+
selectAllRows: string;
|
|
24
|
+
/** aria-label ô chọn ở đầu bảng khi ĐÃ chọn hết */
|
|
25
|
+
deselectAllRows: string;
|
|
26
|
+
/** aria-label ô chọn của một dòng khi dòng chưa được chọn */
|
|
27
|
+
selectRow: string;
|
|
28
|
+
/** aria-label ô chọn của một dòng khi dòng đã được chọn */
|
|
29
|
+
deselectRow: string;
|
|
30
|
+
/** Tiêu đề trạng thái rỗng mặc định (bỏ qua nếu app truyền `empty`) */
|
|
31
|
+
emptyTitle: string;
|
|
32
|
+
/** Mô tả trạng thái rỗng mặc định */
|
|
33
|
+
emptyDescription: string;
|
|
34
|
+
/** Nhãn nút + tiêu đề menu ẩn/hiện cột */
|
|
35
|
+
columnToggle: string;
|
|
36
|
+
}
|
|
37
|
+
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
38
|
+
declare const DEFAULT_DATA_TABLE_LABELS: DataTableLabels;
|
|
39
|
+
|
|
40
|
+
export { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const DEFAULT_DATA_TABLE_LABELS = {
|
|
2
|
+
selectAllRows: "Ch\u1ECDn t\u1EA5t c\u1EA3 d\xF2ng \u0111ang hi\u1EC3n th\u1ECB",
|
|
3
|
+
deselectAllRows: "B\u1ECF ch\u1ECDn t\u1EA5t c\u1EA3",
|
|
4
|
+
selectRow: "Ch\u1ECDn d\xF2ng",
|
|
5
|
+
deselectRow: "B\u1ECF ch\u1ECDn d\xF2ng",
|
|
6
|
+
emptyTitle: "Ch\u01B0a c\xF3 d\u1EEF li\u1EC7u",
|
|
7
|
+
emptyDescription: "Danh s\xE1ch hi\u1EC7n \u0111ang tr\u1ED1ng.",
|
|
8
|
+
columnToggle: "C\u1ED9t hi\u1EC3n th\u1ECB"
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_DATA_TABLE_LABELS
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=data-table-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/data-table-constants.ts"],"sourcesContent":["/**\n * Nhãn mặc định của `DataTable` / `DataTableColumnToggle`.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, danh sách chọn) là thứ trang server hay đọc nhất — spread để override\n * một khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `data-table.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\n\n/**\n * Toàn bộ chuỗi hiển thị (kể cả chuỗi dành riêng cho trình đọc màn hình) của `DataTable` và\n * `DataTableColumnToggle` — app tiếng Anh (hoặc ngôn ngữ khác) override từng khoá qua prop\n * `labels`. Mặc định là tiếng Việt.\n *\n * Cùng khuôn với `ShellLabels` ở tầng shell: gói dùng chung cho MỌI app Comitor nên chuỗi\n * chôn cứng trong component là ngõ cụt cho app đa ngôn ngữ.\n */\nexport interface DataTableLabels {\n /** aria-label ô chọn ở đầu bảng khi CHƯA chọn hết */\n selectAllRows: string;\n /** aria-label ô chọn ở đầu bảng khi ĐÃ chọn hết */\n deselectAllRows: string;\n /** aria-label ô chọn của một dòng khi dòng chưa được chọn */\n selectRow: string;\n /** aria-label ô chọn của một dòng khi dòng đã được chọn */\n deselectRow: string;\n /** Tiêu đề trạng thái rỗng mặc định (bỏ qua nếu app truyền `empty`) */\n emptyTitle: string;\n /** Mô tả trạng thái rỗng mặc định */\n emptyDescription: string;\n /** Nhãn nút + tiêu đề menu ẩn/hiện cột */\n columnToggle: string;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n selectAllRows: \"Chọn tất cả dòng đang hiển thị\",\n deselectAllRows: \"Bỏ chọn tất cả\",\n selectRow: \"Chọn dòng\",\n deselectRow: \"Bỏ chọn dòng\",\n emptyTitle: \"Chưa có dữ liệu\",\n emptyDescription: \"Danh sách hiện đang trống.\",\n columnToggle: \"Cột hiển thị\"\n};\n"],"mappings":"AAuCO,MAAM,4BAA6C;AAAA,EACxD,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,cAAc;AAChB;","names":[]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
import { DataTableLabels } from './data-table-constants.js';
|
|
3
4
|
|
|
4
5
|
type SortDirection = "asc" | "desc";
|
|
5
6
|
interface DataTableSort {
|
|
@@ -7,32 +8,6 @@ interface DataTableSort {
|
|
|
7
8
|
direction: SortDirection;
|
|
8
9
|
}
|
|
9
10
|
type DataTableAlign = "left" | "center" | "right";
|
|
10
|
-
/**
|
|
11
|
-
* Toàn bộ chuỗi hiển thị (kể cả chuỗi dành riêng cho trình đọc màn hình) của `DataTable` và
|
|
12
|
-
* `DataTableColumnToggle` — app tiếng Anh (hoặc ngôn ngữ khác) override từng khoá qua prop
|
|
13
|
-
* `labels`. Mặc định là tiếng Việt.
|
|
14
|
-
*
|
|
15
|
-
* Cùng khuôn với `ShellLabels` ở tầng shell: gói dùng chung cho MỌI app Comitor nên chuỗi
|
|
16
|
-
* chôn cứng trong component là ngõ cụt cho app đa ngôn ngữ.
|
|
17
|
-
*/
|
|
18
|
-
interface DataTableLabels {
|
|
19
|
-
/** aria-label ô chọn ở đầu bảng khi CHƯA chọn hết */
|
|
20
|
-
selectAllRows: string;
|
|
21
|
-
/** aria-label ô chọn ở đầu bảng khi ĐÃ chọn hết */
|
|
22
|
-
deselectAllRows: string;
|
|
23
|
-
/** aria-label ô chọn của một dòng khi dòng chưa được chọn */
|
|
24
|
-
selectRow: string;
|
|
25
|
-
/** aria-label ô chọn của một dòng khi dòng đã được chọn */
|
|
26
|
-
deselectRow: string;
|
|
27
|
-
/** Tiêu đề trạng thái rỗng mặc định (bỏ qua nếu app truyền `empty`) */
|
|
28
|
-
emptyTitle: string;
|
|
29
|
-
/** Mô tả trạng thái rỗng mặc định */
|
|
30
|
-
emptyDescription: string;
|
|
31
|
-
/** Nhãn nút + tiêu đề menu ẩn/hiện cột */
|
|
32
|
-
columnToggle: string;
|
|
33
|
-
}
|
|
34
|
-
/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */
|
|
35
|
-
declare const DEFAULT_DATA_TABLE_LABELS: DataTableLabels;
|
|
36
11
|
interface DataTableColumn<TRow> {
|
|
37
12
|
id: string;
|
|
38
13
|
/** Nội dung ô tiêu đề */
|
|
@@ -142,4 +117,4 @@ interface DataTableColumnToggleProps<TRow> {
|
|
|
142
117
|
*/
|
|
143
118
|
declare function DataTableColumnToggle<TRow>({ columns, visibleColumnIds, onVisibleColumnIdsChange, label, labels: labelsProp, className }: DataTableColumnToggleProps<TRow>): React.JSX.Element;
|
|
144
119
|
|
|
145
|
-
export {
|
|
120
|
+
export { DataTable, type DataTableAlign, type DataTableColumn, DataTableColumnToggle, type DataTableColumnToggleProps, type DataTableProps, type DataTableSort, type SortDirection };
|