@comitor/ui 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,6 +89,12 @@ Dùng pnpm strict mà thấy mất sạch style thì khai thêm ở app:
89
89
  trong tooltip biểu đồ, `Kbd`…). Không gán thì mỗi biến rơi về fallback khai sẵn trong `styles.css`.
90
90
  Cách gán bằng `next/font`: https://design-system.comitor.ai/get-started
91
91
 
92
+ **Con trỏ**: `styles.css` khai lại `cursor: pointer` cho `button`, `[role="button"]` và
93
+ `label[for]` ở `@layer base`. Preflight của Tailwind **v3** có luật này, **v4 bỏ nó** để bám mặc
94
+ định trình duyệt — nên thiếu nó thì `<button>` và `<a>` trông giống hệt nhau lại có con trỏ khác
95
+ nhau, và một nút ghost đọc ra như một cái nhãn. App **không cần** rải `cursor-pointer`; control bị
96
+ vô hiệu (`:disabled` hoặc `aria-disabled="true"`) vẫn giữ con trỏ mặc định.
97
+
92
98
  **Dark mode** theo **class** (`@custom-variant dark (&:is(.dark *))`), khớp `next-themes`. Lưu ý:
93
99
  biến thể `dark` chỉ khớp **con cháu** của `.dark`, không khớp chính phần tử mang class đó. Muốn dựng
94
100
  preview 2 theme cạnh nhau phải bọc thêm một lớp: `<div class="dark"><div>…</div></div>`.
@@ -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 * 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":[]}
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` ĐO bảng mặc định (từ 1.6.0) nhưng **không CHẶN** ở đó — nó vào sổ\n `banDuyetChuaChan`. Nên cổng xanh không có nghĩa cặp này đạt; 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":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\n/*\n * Vạch 4px bên trái MANG THÔNG TIN (phân biệt nhóm trường), nên chịu ngưỡng 3:1 của WCAG\n * 1.4.11 — không phải viền trang trí. Nó đứng trên nền TRANG (`bg-card`) chứ không phải trên\n * nền cùng tone, tức đúng vai trò bậc `-ink` của hợp đồng màu (docs/hop-dong-mau.md §1).\n *\n * Bản trước lấy thẳng bậc NỀN và hỏng ở light — vạch gần như biến mất trên thẻ trắng:\n * gold-300 #E8B824 1,86:1 ✗ · warning #F2C94C 1,59:1 ✗\n * success #2EA56A 3,13:1 · info #1F8A8B 4,14:1 · destructive #C93B3B 5,03:1\n * (dark thì ngược lại — destructive #C93B3B chỉ 3,53:1 trên `--card`.)\n *\n * Bậc ink đạt thoải mái ở CẢ hai theme, đo trên `--card` (light #FFFFFF · dark #16181D):\n * primary-ink 6,51 / 11,90:1 success-ink 6,52 / 10,19:1\n * info-ink 6,68 / 9,54:1 warning-ink 6,51 / 11,60:1\n * destructive-ink 7,15 / 6,42:1\n */\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-primary-ink\",\n info: \"border-l-4 border-l-info-ink\",\n warning: \"border-l-4 border-l-warning-ink\",\n success: \"border-l-4 border-l-success-ink\",\n destructive: \"border-l-4 border-l-destructive-ink\"\n};\n\n/*\n * Ô icon đi theo CÙNG accent với vạch trái.\n *\n * Vì sao không để nó trung tính như bản trước: nhóm `accent=\"destructive\"` vẽ vạch đỏ nhưng ô\n * icon vẫn `bg-muted` xám, nên hai tín hiệu trong cùng một header nói hai điều khác nhau — và\n * cái NẶNG KÝ hơn về thị giác (ô 32px có nền, so với vạch 4px) là cái nói sai. Đo trên\n * `/settings/advanced` của comitor-starter: nhóm \"Vùng nguy hiểm\" — nhóm chứa nút xoá sạch dữ\n * liệu — có icon cảnh báo cùng màu với icon của nhóm \"Dữ liệu ứng dụng\" ngay phía trên.\n *\n * ⚠ `primary` CỐ Ý giữ ô trung tính, không tô tint. Ở bảng MẶC ĐỊNH `--primary-ink` trỏ thẳng về\n * `--primary` (styles.css §\"TỪ 0.3.0\"), nên `text-primary-ink` trên `bg-primary/20` là gold\n * #E8B824 trên tint gold #FAF1D3 = **1,64:1** — thủng ngưỡng 3:1 của WCAG 1.4.11, và tệ hơn hẳn\n * ô trung tính đang có (3,83:1). Đây là món nợ brand mà §22 của docs/quyet-dinh-tich-hop.md đã\n * ghi là \"CÒN TỒN, có chủ ý\"; nối accent vào ô icon mà không loại `primary` ra là MỞ RỘNG món nợ\n * đó sang một bề mặt mới. Tô ĐẶC (`bg-primary text-primary-foreground`, 10,19:1 — lối chữa mà\n * §22 dùng cho `AppLauncher`) thì đọc được, nhưng một ô gold đặc 32px kêu to hơn cả nhóm\n * `destructive`, tức lật ngược thứ bậc nghiêm trọng. Nên: giữ trung tính cho tới khi `--primary`\n * có một bậc ink thật ở bảng mặc định.\n *\n * ⚠⚠ SỐ ĐO DƯỚI ĐÂY LÀ CỦA BẢNG MẶC ĐỊNH, và phải luôn ghi rõ bảng nào, vì\n * `pnpm lint:a11y` KHÔNG ĐO BẢNG MẶC ĐỊNH: `scripts/a11y/tailwind.mjs` hợp\n * `[data-contrast=\"high\"]` ĐÈ LÊN `:root`, nên mọi token mà bảng tương phản cao ghi đè đều được\n * đo theo bảng ĐÓ rồi dán nhãn \"light\". Cổng báo `primary` 5,77:1 ✓ trong khi bảng mặc định là\n * 1,64:1 ✗. Với những cặp ấy, comment này là lớp phòng vệ DUY NHẤT — cổng xanh không chứng minh\n * gì. (Bản đầu của chính khối comment này chép số của bảng tương phản cao mà không ghi rõ, tức\n * mắc đúng cái bẫy nó đang cảnh báo.)\n *\n * tone bảng mặc định (light, ink trên tint của chính nó)\n * none 3,83:1 ✓ (bg-muted, giữ nguyên)\n * primary 1,64:1 ✗ → KHÔNG tint, dùng lại ô trung tính\n * warning 5,93:1 ✓\n * success 5,60:1 ✓\n * destructive 3,57:1 ✓\n * info 5,54:1 ✓\n *\n * Bốn tone có tint lấy Y HỆT `statCardIconVariants` (stat-card.tsx) để gói chỉ có MỘT thang tone\n * cho ô icon. Khác duy nhất là hình học — ở đây `size-8 rounded-md`, ở StatCard `size-7\n * rounded-lg` — nên hai chỗ không dùng chung được một `cva`, và đó là lý do bảng này chép giá trị\n * chứ không import. Đổi thang thì phải đổi CẢ HAI.\n *\n * ⚠ Viết THẲNG từng chuỗi, không dựng bằng `bg-${accent}/15`: cổng quét class TĨNH trong `src/**`\n * và xếp lớp dựng lúc chạy vào nhóm \"CẦN KIỂM TAY\" → FAIL. Hành vi cố ý, không phải hạn chế.\n */\nconst accentIconClasses: Record<FormSectionAccent, string> = {\n none: \"bg-muted text-muted-foreground\",\n primary: \"bg-muted text-muted-foreground\",\n info: \"bg-info/15 text-info-ink\",\n warning: \"bg-warning/20 text-warning-ink\",\n success: \"bg-success/15 text-success-ink\",\n destructive: \"bg-destructive/15 text-destructive-ink\"\n};\n\nfunction SectionHeading({\n title,\n description,\n icon: Icon,\n accent\n}: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\"> & { accent: FormSectionAccent }) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md\",\n accentIconClasses[accent]\n )}\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n {/* KHÔNG `truncate`: ở 320px tiêu đề nhóm dài (\"Thông tin không gian làm việc\") bị cắt\n mất chữ — đo được 26px nội dung biến mất. Tiêu đề xuống dòng thì xấu hơn một chút\n nhưng vẫn đọc được, còn cắt thì mất thông tin. `min-w-0` ở thẻ cha vẫn giữ cho hàng\n header của nhóm không đẩy nút actions ra ngoài. */}\n <h3 className=\"text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} accent={accent} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} accent={accent} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AA0HU,cAGJ,YAHI;AAxHV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAwCpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AA6CA,MAAM,oBAAuD;AAAA,EAC3D,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AACF,GAA6F;AAC3F,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,kBAAkB,MAAM;AAAA,QAC1B;AAAA,QAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WAKb;AAAA,0BAAC,QAAG,WAAU,wDAAwD,iBAAM;AAAA,MAC3E,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY,QAAgB;AAAA,QACnF,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY,QAAgB;AAAA,QACpF;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\n/*\n * Vạch 4px bên trái MANG THÔNG TIN (phân biệt nhóm trường), nên chịu ngưỡng 3:1 của WCAG\n * 1.4.11 — không phải viền trang trí. Nó đứng trên nền TRANG (`bg-card`) chứ không phải trên\n * nền cùng tone, tức đúng vai trò bậc `-ink` của hợp đồng màu (docs/hop-dong-mau.md §1).\n *\n * Bản trước lấy thẳng bậc NỀN và hỏng ở light — vạch gần như biến mất trên thẻ trắng:\n * gold-300 #E8B824 1,86:1 ✗ · warning #F2C94C 1,59:1 ✗\n * success #2EA56A 3,13:1 · info #1F8A8B 4,14:1 · destructive #C93B3B 5,03:1\n * (dark thì ngược lại — destructive #C93B3B chỉ 3,53:1 trên `--card`.)\n *\n * Bậc ink đạt thoải mái ở CẢ hai theme, đo trên `--card` (light #FFFFFF · dark #16181D):\n * primary-ink 6,51 / 11,90:1 success-ink 6,52 / 10,19:1\n * info-ink 6,68 / 9,54:1 warning-ink 6,51 / 11,60:1\n * destructive-ink 7,15 / 6,42:1\n */\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-primary-ink\",\n info: \"border-l-4 border-l-info-ink\",\n warning: \"border-l-4 border-l-warning-ink\",\n success: \"border-l-4 border-l-success-ink\",\n destructive: \"border-l-4 border-l-destructive-ink\"\n};\n\n/*\n * Ô icon đi theo CÙNG accent với vạch trái.\n *\n * Vì sao không để nó trung tính như bản trước: nhóm `accent=\"destructive\"` vẽ vạch đỏ nhưng ô\n * icon vẫn `bg-muted` xám, nên hai tín hiệu trong cùng một header nói hai điều khác nhau — và\n * cái NẶNG KÝ hơn về thị giác (ô 32px có nền, so với vạch 4px) là cái nói sai. Đo trên\n * `/settings/advanced` của comitor-starter: nhóm \"Vùng nguy hiểm\" — nhóm chứa nút xoá sạch dữ\n * liệu — có icon cảnh báo cùng màu với icon của nhóm \"Dữ liệu ứng dụng\" ngay phía trên.\n *\n * ⚠ `primary` CỐ Ý giữ ô trung tính, không tô tint. Ở bảng MẶC ĐỊNH `--primary-ink` trỏ thẳng về\n * `--primary` (styles.css §\"TỪ 0.3.0\"), nên `text-primary-ink` trên `bg-primary/20` là gold\n * #E8B824 trên tint gold #FAF1D3 = **1,64:1** — thủng ngưỡng 3:1 của WCAG 1.4.11, và tệ hơn hẳn\n * ô trung tính đang có (3,83:1). Đây là món nợ brand mà §22 của docs/quyet-dinh-tich-hop.md đã\n * ghi là \"CÒN TỒN, có chủ ý\"; nối accent vào ô icon mà không loại `primary` ra là MỞ RỘNG món nợ\n * đó sang một bề mặt mới. Tô ĐẶC (`bg-primary text-primary-foreground`, 10,19:1 — lối chữa mà\n * §22 dùng cho `AppLauncher`) thì đọc được, nhưng một ô gold đặc 32px kêu to hơn cả nhóm\n * `destructive`, tức lật ngược thứ bậc nghiêm trọng. Nên: giữ trung tính cho tới khi `--primary`\n * có một bậc ink thật ở bảng mặc định.\n *\n * ⚠⚠ SỐ ĐO DƯỚI ĐÂY LÀ CỦA BẢNG MẶC ĐỊNH, và phải luôn ghi rõ bảng nào. Từ 1.6.0 cổng ĐO cả hai\n * bảng, nhưng nó chỉ **CHẶN** ở bảng tương phản cao; bảng mặc định vào sổ `banDuyetChuaChan` — in\n * ra mỗi lần chạy, chưa chặn. Nên với những cặp mà hai bảng cho hai con số khác nhau, cổng xanh\n * KHÔNG chứng minh bảng mặc định đạt. (Trước 1.6.0 còn tệ hơn: cổng không đo bảng mặc định lần\n * nào. Và bản đầu của chính khối comment này chép số của bảng tương phản cao mà không ghi rõ, tức\n * mắc đúng cái bẫy nó đang cảnh báo.)\n *\n * tone bảng mặc định (light, ink trên tint của chính nó)\n * none 3,83:1 ✓ (bg-muted, giữ nguyên)\n * primary 1,64:1 ✗ → KHÔNG tint, dùng lại ô trung tính\n * warning 5,93:1 ✓\n * success 5,60:1 ✓\n * destructive 3,57:1 ✓\n * info 5,54:1 ✓\n *\n * Bốn tone có tint lấy Y HỆT `statCardIconVariants` (stat-card.tsx) để gói chỉ có MỘT thang tone\n * cho ô icon. Khác duy nhất là hình học — ở đây `size-8 rounded-md`, ở StatCard `size-7\n * rounded-lg` — nên hai chỗ không dùng chung được một `cva`, và đó là lý do bảng này chép giá trị\n * chứ không import. Đổi thang thì phải đổi CẢ HAI.\n *\n * ⚠ Viết THẲNG từng chuỗi, không dựng bằng `bg-${accent}/15`: cổng quét class TĨNH trong `src/**`\n * và xếp lớp dựng lúc chạy vào nhóm \"CẦN KIỂM TAY\" → FAIL. Hành vi cố ý, không phải hạn chế.\n */\nconst accentIconClasses: Record<FormSectionAccent, string> = {\n none: \"bg-muted text-muted-foreground\",\n primary: \"bg-muted text-muted-foreground\",\n info: \"bg-info/15 text-info-ink\",\n warning: \"bg-warning/20 text-warning-ink\",\n success: \"bg-success/15 text-success-ink\",\n destructive: \"bg-destructive/15 text-destructive-ink\"\n};\n\nfunction SectionHeading({\n title,\n description,\n icon: Icon,\n accent\n}: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\"> & { accent: FormSectionAccent }) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md\",\n accentIconClasses[accent]\n )}\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n {/* KHÔNG `truncate`: ở 320px tiêu đề nhóm dài (\"Thông tin không gian làm việc\") bị cắt\n mất chữ — đo được 26px nội dung biến mất. Tiêu đề xuống dòng thì xấu hơn một chút\n nhưng vẫn đọc được, còn cắt thì mất thông tin. `min-w-0` ở thẻ cha vẫn giữ cho hàng\n header của nhóm không đẩy nút actions ra ngoài. */}\n <h3 className=\"text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} accent={accent} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} accent={accent} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AAyHU,cAGJ,YAHI;AAvHV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAwCpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AA4CA,MAAM,oBAAuD;AAAA,EAC3D,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AACF,GAA6F;AAC3F,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,kBAAkB,MAAM;AAAA,QAC1B;AAAA,QAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WAKb;AAAA,0BAAC,QAAG,WAAU,wDAAwD,iBAAM;AAAA,MAC3E,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY,QAAgB;AAAA,QACnF,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY,QAAgB;AAAA,QACpF;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/styles.css CHANGED
@@ -921,6 +921,33 @@
921
921
  @apply bg-background text-foreground font-sans antialiased;
922
922
  }
923
923
 
924
+ /*
925
+ * CON TRỎ BÀN TAY CHO MỌI CONTROL BẤM ĐƯỢC.
926
+ *
927
+ * ── VÌ SAO PHẢI KHAI LẠI ─────────────────────────────────────────────────────────────────
928
+ * Preflight của Tailwind **v3** có `button, [role="button"] { cursor: pointer }`. **v4 bỏ nó**
929
+ * để bám theo mặc định của trình duyệt, nên mọi `<button>` rơi về `cursor: default`.
930
+ *
931
+ * Hậu quả đo được trong app thật (2026-09-02): trên cùng một thanh công cụ, "Tạo việc mới" là
932
+ * `<a>` nên có bàn tay, còn "Xuất danh sách" và "Đánh dấu hoàn thành" là `<button>` nên không —
933
+ * hai control TRÔNG GIỐNG HỆT NHAU mà con trỏ nói hai chuyện khác nhau. Một nút ghost trên nền
934
+ * phẳng khi ấy đọc ra như một cái NHÃN.
935
+ *
936
+ * ── VÌ SAO Ở ĐÂY, KHÔNG PHẢI RẢI VÀO TỪNG COMPONENT ─────────────────────────────────────
937
+ * Vì đây là hành vi của TẦNG NỀN, không phải của một component. Đã đếm: 18 file trong `src/`
938
+ * render ~35 `<button>` thô ngoài `<Button>`, và không file nào khai `cursor-pointer`. Rải tay
939
+ * là bỏ sót ngay ở component tiếp theo có người viết, và không cổng nào bắt được cái bỏ sót đó.
940
+ *
941
+ * `:not(:disabled)` và `:not([aria-disabled="true"])`: control bị vô hiệu KHÔNG được mời bấm.
942
+ * Hai điều kiện chứ không một — Radix vô hiệu bằng `aria-disabled` ở những chỗ vẫn cần giữ
943
+ * focus, và ở đó thuộc tính `disabled` không có mặt.
944
+ */
945
+ button:not(:disabled),
946
+ [role="button"]:not([aria-disabled="true"]),
947
+ label[for] {
948
+ cursor: pointer;
949
+ }
950
+
924
951
  /* Chữ ký thị giác: focus dày 2px, cách viền 2px, màu ring. */
925
952
  :focus-visible {
926
953
  @apply outline-2 outline-offset-2 outline-ring;