@comitor/ui 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -553,6 +553,7 @@ Giá trị thật của package: pattern lặp đi lặp lại ở mọi app, gh
553
553
  | `TablePagination` | Phân trang đi kèm bảng. Chuỗi override qua prop `labels` — `TablePaginationLabels` / `DEFAULT_TABLE_PAGINATION_LABELS` (khoá có số/đơn vị chèn vào là **hàm**, vd `range(start, end, total, unitLabel)`) |
554
554
  | `SearchFilterBar`, `FilterChips` | Thanh tìm + lọc + đếm kết quả. `SearchFilterBar` override chuỗi qua `labels` — `SearchFilterBarLabels` / `DEFAULT_SEARCH_FILTER_BAR_LABELS`; `FilterChips` nhận nhãn thẳng trong dữ liệu chip nên không có `labels` |
555
555
  | `StatCard`, `StatusPill`, `EmptyState` | Số liệu có delta, nhãn trạng thái, trạng thái rỗng |
556
+ | `RouteTabs` | Tab mà mỗi mục là một **route thật** — `<nav>` chứa link, `aria-current="page"`. Dùng chung bảng lớp với `Tabs` nên nhìn không khác; đừng dùng `Tabs` cho việc này (nó dựng `role="tablist"` + `aria-controls` trỏ tới tabpanel cùng trang, mà ở đây không có). ⚠ Trên Next phải truyền `LinkComponent={Link}` |
556
557
  | `FormSection`, `FormField` | Bố cục form 12 cột; uỷ quyền label/mô tả/lỗi xuống `Field` |
557
558
  | `Combobox`, `MultiCombobox` | Chọn có tìm kiếm, **khớp không dấu tiếng Việt**. `aria-invalid` đổi trigger sang **viền đỏ** (`--destructive-ink`), cùng tín hiệu với `Input`; nhận cả `boolean` lẫn `"true"`/`"false"` để dùng thẳng `aria-invalid={!!errors.x}` |
558
559
  | `DatePicker`, `DateRangePicker` | Chọn ngày/khoảng ngày, locale `vi`, preset dựng sẵn |
@@ -77,6 +77,18 @@ interface DataTableProps<TRow> {
77
77
  isRowActive?: (row: TRow, index: number) => boolean;
78
78
  /** Tiêu đề dính đỉnh vùng cuộn (mặc định true) */
79
79
  stickyHeader?: boolean;
80
+ /**
81
+ * Tô nền xen kẽ cho hàng chẵn — mẫu DataTable ở mục **Patterns** của comitor-ds dùng
82
+ * `<Table striped>`, còn `DataTable` trước đây không có đường bật.
83
+ *
84
+ * Mặc định `false`: bảng dài và dày cột thì sọc giúp dò hàng, nhưng bảng ngắn thì nó chỉ thêm
85
+ * nhiễu. Đây là quyết định của từng trang, không phải mặc định của gói.
86
+ *
87
+ * ⚠ Sọc dùng `bg-muted/30` và đặt trên `<tr>` — cùng chỗ mà nền hàng ĐANG CHỌN / ĐANG MỞ đặt.
88
+ * Nền trạng thái luôn thắng vì nó ghi sau trong chuỗi lớp của `<TableRow>`; ô cột ghim
89
+ * (`bg-inherit`) vì thế vẫn kế thừa đúng nền của hàng.
90
+ */
91
+ striped?: boolean;
80
92
  /** Bề rộng tối thiểu của bảng — bật cuộn ngang khi màn hình hẹp */
81
93
  minWidth?: number | string;
82
94
  density?: "comfortable" | "compact";
@@ -97,7 +109,7 @@ interface DataTableProps<TRow> {
97
109
  * trang. Nhờ vậy dùng được cả với dữ liệu client (mảng trong bộ nhớ) lẫn server-side
98
110
  * pagination mà không phải viết hai component.
99
111
  */
100
- declare function DataTable<TRow>({ columns, rows, getRowId, loading, loadingRowCount, empty, sort, onSortChange, selectable, selectedIds, onSelectedIdsChange, isRowSelectable, visibleColumnIds, onRowClick, rowClassName, isRowActive, stickyHeader, minWidth, density, labels: labelsProp, className, containerClassName, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby }: DataTableProps<TRow>): React.JSX.Element;
112
+ declare function DataTable<TRow>({ columns, rows, getRowId, loading, loadingRowCount, empty, sort, onSortChange, selectable, selectedIds, onSelectedIdsChange, isRowSelectable, visibleColumnIds, onRowClick, rowClassName, isRowActive, stickyHeader, striped, minWidth, density, labels: labelsProp, className, containerClassName, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby }: DataTableProps<TRow>): React.JSX.Element;
101
113
  interface DataTableColumnToggleProps<TRow> {
102
114
  columns: DataTableColumn<TRow>[];
103
115
  /** Mã các cột đang hiện — cùng nguồn với `DataTable.visibleColumnIds` (tập hợp, không phải thứ tự) */
@@ -60,6 +60,7 @@ function DataTable({
60
60
  rowClassName,
61
61
  isRowActive,
62
62
  stickyHeader = true,
63
+ striped = false,
63
64
  minWidth,
64
65
  density = "comfortable",
65
66
  labels: labelsProp,
@@ -104,7 +105,12 @@ function DataTable({
104
105
  "aria-label": ariaLabel,
105
106
  "aria-labelledby": ariaLabelledby,
106
107
  "aria-busy": loading || void 0,
107
- className: cn("w-full caption-bottom text-sm", className),
108
+ className: cn(
109
+ "w-full caption-bottom text-sm",
110
+ // Cùng một chuỗi lớp với `<Table striped>` của Tier 1 — đổi một chỗ thì đổi cả hai.
111
+ striped && "[&_tbody_tr:nth-child(even)]:bg-muted/30",
112
+ className
113
+ ),
108
114
  style: minWidth != null ? { minWidth } : void 0,
109
115
  children: [
110
116
  /* @__PURE__ */ jsx(TableHeader, { className: cn(stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs(TableRow, { className: "bg-muted hover:bg-muted", children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/data-table.tsx"],"sourcesContent":["\"use client\";\n\nimport { ArrowDown, ArrowUp, ChevronsUpDown, Inbox, SlidersHorizontal } from \"lucide-react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Checkbox } from \"../ui/checkbox.js\";\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger\n} from \"../ui/dropdown-menu.js\";\nimport { Skeleton } from \"../ui/skeleton.js\";\nimport { TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../ui/table.js\";\nimport { type DataTableLabels, DEFAULT_DATA_TABLE_LABELS } from \"./data-table-constants.js\";\nimport { EmptyState } from \"./empty-state.js\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort {\n columnId: string;\n direction: SortDirection;\n}\n\nexport type DataTableAlign = \"left\" | \"center\" | \"right\";\n\nexport interface DataTableColumn<TRow> {\n id: string;\n /** Nội dung ô tiêu đề */\n header: ReactNode;\n /** Nhãn chữ dùng trong menu ẩn/hiện cột — bắt buộc khi `header` không phải chuỗi */\n label?: string;\n /** Render ô dữ liệu — nhận cả chỉ số dòng để đánh số thứ tự */\n cell: (row: TRow, rowIndex: number) => ReactNode;\n sortable?: boolean;\n align?: DataTableAlign;\n /**\n * Bề rộng cột tính bằng px. BẮT BUỘC với cột ghim (`pinned`) vì offset `left/right` của\n * sticky được tính cộng dồn từ giá trị này — thiếu width thì các cột ghim chồng lên nhau.\n */\n width?: number;\n minWidth?: number;\n /** Ghim cột khi cuộn ngang (bảng nhiều cột) */\n pinned?: \"left\" | \"right\";\n /** Cho phép người dùng ẩn cột này (mặc định true; cột định danh nên đặt false) */\n hideable?: boolean;\n headerClassName?: string;\n cellClassName?: string;\n}\n\nexport interface DataTableProps<TRow> {\n columns: DataTableColumn<TRow>[];\n rows: TRow[];\n /** Khoá ổn định của dòng — dùng cho `key` React và cho tập chọn */\n getRowId: (row: TRow, index: number) => string;\n\n /** Đang tải → hiện `loadingRowCount` dòng khung xương thay vì bảng trống */\n loading?: boolean;\n loadingRowCount?: number;\n /** Nội dung khi không có dòng nào (mặc định EmptyState theo `labels.emptyTitle`) */\n empty?: ReactNode;\n\n /** Sắp xếp — controlled; `null` = không sắp xếp. App tự sắp/tự gọi API. */\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n\n /** Chọn dòng bằng checkbox — controlled */\n selectable?: boolean;\n selectedIds?: string[];\n onSelectedIdsChange?: (ids: string[]) => void;\n /** Khoá chọn với một số dòng (vd bản ghi đã khoá) */\n isRowSelectable?: (row: TRow, index: number) => boolean;\n\n /**\n * Mã các cột đang hiện — controlled. Bỏ trống = hiện tất cả. Đổi bố cục đi qua\n * `DataTableColumnToggle`, app tự lưu theo người dùng (server, localStorage…).\n *\n * ⚠ Đây là một **TẬP HỢP**, không phải thứ tự: thứ tự cột luôn lấy từ mảng `columns`, thứ tự\n * bên trong mảng này bị bỏ qua. Tên có hậu tố `Ids` chính là để nói đúng thứ nó chứa —\n * 0.1.0 đặt `visibleColumns` khiến người đọc tưởng là mảng cột (`DataTableColumn[]`) hoặc\n * tưởng sắp lại mảng thì cột đổi chỗ.\n *\n * Cố ý KHÔNG dùng `Record<string, boolean>`: kiểu đó có trạng thái thứ ba — **khoá vắng\n * mặt**. App thêm cột mới ở bản sau thì bố cục người dùng đã lưu không có khoá đó, mỗi nơi\n * tự đoán hiện/ẩn → lỗi im lặng. Một mảng mã cột thì `JSON.stringify` là lưu được.\n *\n * Package cố ý KHÔNG tự ghi localStorage: ở SaaS nhiều workspace, bố cục bảng phải theo\n * cặp (người dùng × workspace) và thường cần đồng bộ giữa các máy — chuyện của app.\n */\n visibleColumnIds?: string[];\n\n /**\n * Bấm vào dòng (vd mở chi tiết bản ghi). Có prop này thì dòng vào chuỗi Tab và nhận\n * Enter/Space — mở chi tiết KHÔNG được là thao tác chỉ-chuột.\n */\n onRowClick?: (row: TRow, index: number) => void;\n rowClassName?: (row: TRow, index: number) => string | undefined;\n /** Dòng đang được làm nổi (vd bản ghi vừa mở ở panel bên) */\n isRowActive?: (row: TRow, index: number) => boolean;\n\n /** Tiêu đề dính đỉnh vùng cuộn (mặc định true) */\n stickyHeader?: boolean;\n /** Bề rộng tối thiểu của bảng — bật cuộn ngang khi màn hình hẹp */\n minWidth?: number | string;\n density?: \"comfortable\" | \"compact\";\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `DataTableLabels` */\n labels?: Partial<DataTableLabels>;\n /** Nhãn của bảng cho trình đọc màn hình (bắt buộc khi trang có nhiều bảng) */\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n className?: string;\n /** className của khung cuộn bọc ngoài */\n containerClassName?: string;\n}\n\nconst alignClasses: Record<DataTableAlign, string> = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\"\n};\n\n/** Offset sticky cộng dồn cho cột ghim trái/phải. */\nfunction getPinnedOffsets<TRow>(columns: DataTableColumn<TRow>[], selectable: boolean): Map<string, CSSProperties> {\n const styles = new Map<string, CSSProperties>();\n // Cột checkbox (nếu có) luôn ghim trái và chiếm 44px — cột ghim đầu tiên phải bắt đầu sau nó.\n let left = selectable ? 44 : 0;\n for (const column of columns) {\n if (column.pinned !== \"left\") continue;\n styles.set(column.id, { position: \"sticky\", left, zIndex: 2 });\n left += column.width ?? 0;\n }\n\n let right = 0;\n for (const column of [...columns].reverse()) {\n if (column.pinned !== \"right\") continue;\n styles.set(column.id, { position: \"sticky\", right, zIndex: 2 });\n right += column.width ?? 0;\n }\n\n return styles;\n}\n\nfunction nextSort(current: DataTableSort | null | undefined, columnId: string): DataTableSort | null {\n // asc → desc → bỏ sắp xếp. Vòng 3 trạng thái để người dùng quay lại thứ tự gốc của server\n // mà không phải tải lại trang.\n if (!current || current.columnId !== columnId) return { columnId, direction: \"asc\" };\n if (current.direction === \"asc\") return { columnId, direction: \"desc\" };\n return null;\n}\n\n/**\n * Bảng dữ liệu chuẩn của Comitor: tiêu đề dính, sắp xếp, chọn dòng, ghim cột, ẩn/hiện cột,\n * trạng thái tải và trạng thái rỗng.\n *\n * Mọi trạng thái đều **controlled** — package không giữ dữ liệu, không gọi API, không tự phân\n * trang. Nhờ vậy dùng được cả với dữ liệu client (mảng trong bộ nhớ) lẫn server-side\n * pagination mà không phải viết hai component.\n */\nexport function DataTable<TRow>({\n columns,\n rows,\n getRowId,\n loading = false,\n loadingRowCount = 5,\n empty,\n sort,\n onSortChange,\n selectable = false,\n selectedIds,\n onSelectedIdsChange,\n isRowSelectable,\n visibleColumnIds,\n onRowClick,\n rowClassName,\n isRowActive,\n stickyHeader = true,\n minWidth,\n density = \"comfortable\",\n labels: labelsProp,\n className,\n containerClassName,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby\n}: DataTableProps<TRow>) {\n const labels = { ...DEFAULT_DATA_TABLE_LABELS, ...labelsProp };\n // Thứ tự luôn theo `columns` — `visibleColumnIds` chỉ trả lời \"cột nào được hiện\".\n const shownColumns = visibleColumnIds ? columns.filter((column) => visibleColumnIds.includes(column.id)) : columns;\n const pinnedStyles = getPinnedOffsets(shownColumns, selectable);\n const colCount = shownColumns.length + (selectable ? 1 : 0);\n\n const selected = selectedIds ?? [];\n // Một lượt duyệt: vừa lọc dòng chọn được vừa lấy id — tránh indexOf() lồng trong map (O(n²))\n // và tránh sai khi hai dòng cùng tham chiếu object.\n const selectableIds: string[] = [];\n rows.forEach((row, index) => {\n if (isRowSelectable?.(row, index) ?? true) selectableIds.push(getRowId(row, index));\n });\n const allSelected = selectableIds.length > 0 && selectableIds.every((id) => selected.includes(id));\n const someSelected = !allSelected && selectableIds.some((id) => selected.includes(id));\n\n const toggleAll = () => {\n if (!onSelectedIdsChange) return;\n if (allSelected) {\n onSelectedIdsChange(selected.filter((id) => !selectableIds.includes(id)));\n return;\n }\n onSelectedIdsChange([...new Set([...selected, ...selectableIds])]);\n };\n\n const toggleRow = (id: string) => {\n if (!onSelectedIdsChange) return;\n onSelectedIdsChange(selected.includes(id) ? selected.filter((item) => item !== id) : [...selected, id]);\n };\n\n const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-4 py-3\";\n\n return (\n <div\n data-slot=\"data-table-container\"\n className={cn(\"relative w-full overflow-auto rounded-xl border border-border bg-card\", containerClassName)}\n >\n <table\n data-slot=\"data-table\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-busy={loading || undefined}\n className={cn(\"w-full caption-bottom text-sm\", className)}\n style={minWidth != null ? { minWidth } : undefined}\n >\n <TableHeader className={cn(stickyHeader && \"sticky top-0 z-10\")}>\n {/* Nền ĐẶC đặt trên <tr>, không phải <thead>: ô ghim dùng `bg-inherit` mà\n background-color kế thừa từ CHA TRỰC TIẾP (<tr>). Để nền ở <thead> thì <tr> vẫn\n trong suốt → cột ghim nhìn xuyên thấy dữ liệu cuộn phía dưới. */}\n <TableRow className=\"bg-muted hover:bg-muted\">\n {selectable && (\n <TableHead\n className=\"w-11 bg-inherit\"\n style={{ position: \"sticky\", left: 0, zIndex: 3, width: 44, minWidth: 44 }}\n >\n <Checkbox\n checked={allSelected ? true : someSelected ? \"indeterminate\" : false}\n onCheckedChange={toggleAll}\n disabled={selectableIds.length === 0}\n aria-label={allSelected ? labels.deselectAllRows : labels.selectAllRows}\n />\n </TableHead>\n )}\n\n {shownColumns.map((column) => {\n const isSorted = sort?.columnId === column.id;\n const SortIcon = isSorted ? (sort?.direction === \"asc\" ? ArrowUp : ArrowDown) : ChevronsUpDown;\n const pinnedStyle = pinnedStyles.get(column.id);\n\n return (\n <TableHead\n key={column.id}\n scope=\"col\"\n aria-sort={isSorted ? (sort?.direction === \"asc\" ? \"ascending\" : \"descending\") : undefined}\n className={cn(\n alignClasses[column.align ?? \"left\"],\n pinnedStyle && \"bg-inherit\",\n column.headerClassName\n )}\n style={{\n ...pinnedStyle,\n width: column.width,\n minWidth: column.minWidth ?? column.width\n }}\n >\n {column.sortable && onSortChange ? (\n <button\n type=\"button\"\n onClick={() => onSortChange(nextSort(sort, column.id))}\n className={cn(\n \"-mx-1 inline-flex items-center gap-1 rounded-sm px-1 py-0.5 uppercase transition-colors hover:text-foreground\",\n column.align === \"right\" && \"flex-row-reverse\",\n isSorted && \"text-foreground\"\n )}\n >\n {column.header}\n {/* KHÔNG làm mờ icon chưa sắp xếp: nút này còn bấm được nên `opacity-*`\n không được miễn trừ ngưỡng 3:1 của WCAG 1.4.11 cho icon mang nghĩa.\n `opacity-50` cho ra 1,98:1 (light) / 2,23:1 (dark) trên nền `bg-muted`\n của hàng tiêu đề ✗; để nguyên `text-muted-foreground` là 4,89 / 4,86:1 ✓.\n Trạng thái ĐANG sắp xếp vẫn phân biệt được: nút đổi sang\n `text-foreground` và icon đổi hình (ArrowUp/ArrowDown ⇄ ChevronsUpDown). */}\n <SortIcon aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n ) : (\n column.header\n )}\n </TableHead>\n );\n })}\n </TableRow>\n </TableHeader>\n\n <TableBody>\n {loading &&\n Array.from({ length: loadingRowCount }).map((_, rowIndex) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: dòng khung xương không có danh tính dữ liệu\n <TableRow key={`skeleton-${rowIndex}`} className=\"hover:bg-transparent\">\n {selectable && (\n <TableCell className={cellPadding}>\n <Skeleton className=\"size-4 rounded-sm\" />\n </TableCell>\n )}\n {shownColumns.map((column) => (\n <TableCell key={column.id} className={cellPadding}>\n <Skeleton className=\"h-4 w-full max-w-[12rem]\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n\n {!loading && rows.length === 0 && (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount} className=\"p-0\">\n {empty ?? <EmptyState icon={Inbox} title={labels.emptyTitle} description={labels.emptyDescription} />}\n </TableCell>\n </TableRow>\n )}\n\n {!loading &&\n rows.map((row, rowIndex) => {\n const id = getRowId(row, rowIndex);\n const isSelected = selected.includes(id);\n const canSelect = isRowSelectable?.(row, rowIndex) ?? true;\n const active = isRowActive?.(row, rowIndex) ?? false;\n\n return (\n <TableRow\n key={id}\n data-state={isSelected ? \"selected\" : undefined}\n aria-selected={selectable ? isSelected : undefined}\n onClick={onRowClick ? () => onRowClick(row, rowIndex) : undefined}\n // Dòng bấm được phải dùng được bằng BÀN PHÍM: đưa <tr> vào chuỗi Tab và tự\n // xử lý Enter/Space. Cố ý KHÔNG đặt role=\"button\" — role đó đè role=\"row\"\n // ngầm, trình đọc màn hình sẽ mất cấu trúc bảng (số dòng/cột, tiêu đề cột);\n // dòng vẫn là dòng, chỉ thêm khả năng kích hoạt.\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n // Chỉ nhận phím khi CHÍNH <tr> đang focus: Enter/Space bấm trên\n // control bên trong ô (checkbox, nút, link) đã có nghĩa riêng —\n // nổi bọt lên đây sẽ mở chi tiết ngoài ý muốn.\n if (event.target !== event.currentTarget) return;\n if (event.key !== \"Enter\" && event.key !== \" \") return;\n // Space mặc định cuộn trang, Enter có thể submit form bao ngoài.\n event.preventDefault();\n onRowClick(row, rowIndex);\n }\n : undefined\n }\n className={cn(\n // bg-card trên <tr> để ô ghim (`bg-inherit`) có màu đặc mà vẫn đổi theo\n // trạng thái chọn/hover của chính dòng đó.\n \"bg-card\",\n // Viền focus vẽ bằng `outline` chứ không `ring`: <tr> nằm trong bảng\n // border-collapse, box-shadow không được vẽ. Offset ÂM để viền nằm trong\n // lòng dòng, không bị khung cuộn cắt mất ở mép trái/phải.\n // Màu dùng `foreground` chứ không `ring` (gold #E8B824 chỉ ~1,9:1 trên nền\n // card — dưới 3:1 của WCAG 1.4.11 cho thành phần phi-văn-bản).\n onRowClick &&\n \"cursor-pointer focus-visible:outline-2 focus-visible:outline-foreground focus-visible:[outline-offset:-2px]\",\n active && \"bg-app-accent/5\",\n rowClassName?.(row, rowIndex)\n )}\n >\n {selectable && (\n <TableCell\n className={cn(cellPadding, \"bg-inherit\")}\n style={{ position: \"sticky\", left: 0, zIndex: 1, width: 44, minWidth: 44 }}\n >\n {/* Chặn nổi bọt: bấm checkbox không được kích hoạt onRowClick (mở chi tiết) */}\n {/* biome-ignore lint/a11y/noStaticElementInteractions: lớp bọc chỉ chặn sự kiện, tương tác nằm ở Checkbox bên trong */}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: phím bấm đã do Checkbox xử lý */}\n <span onClick={(event) => event.stopPropagation()}>\n <Checkbox\n checked={isSelected}\n disabled={!canSelect}\n onCheckedChange={() => toggleRow(id)}\n aria-label={isSelected ? labels.deselectRow : labels.selectRow}\n />\n </span>\n </TableCell>\n )}\n\n {shownColumns.map((column) => {\n const pinnedStyle = pinnedStyles.get(column.id);\n return (\n <TableCell\n key={column.id}\n className={cn(\n cellPadding,\n alignClasses[column.align ?? \"left\"],\n pinnedStyle && \"bg-inherit\",\n column.cellClassName\n )}\n style={{ ...pinnedStyle, width: column.width, minWidth: column.minWidth ?? column.width }}\n >\n {column.cell(row, rowIndex)}\n </TableCell>\n );\n })}\n </TableRow>\n );\n })}\n </TableBody>\n </table>\n </div>\n );\n}\n\nexport interface DataTableColumnToggleProps<TRow> {\n columns: DataTableColumn<TRow>[];\n /** Mã các cột đang hiện — cùng nguồn với `DataTable.visibleColumnIds` (tập hợp, không phải thứ tự) */\n visibleColumnIds: string[];\n onVisibleColumnIdsChange: (visibleColumnIds: string[]) => void;\n /** Nhãn nút + tiêu đề menu; không truyền thì lấy `labels.columnToggle` */\n label?: string;\n /** Ghi đè chuỗi hiển thị (mặc định tiếng Việt) — xem `DataTableLabels` */\n labels?: Partial<DataTableLabels>;\n className?: string;\n}\n\n/**\n * Menu ẩn/hiện cột đi kèm `DataTable` (đặt trên thanh công cụ của trang danh sách).\n *\n * Không cho ẩn hết: luôn giữ lại ít nhất một cột, nếu không bảng thành khối trắng vô nghĩa\n * và người dùng không có đường quay lại.\n */\nexport function DataTableColumnToggle<TRow>({\n columns,\n visibleColumnIds,\n onVisibleColumnIdsChange,\n label,\n labels: labelsProp,\n className\n}: DataTableColumnToggleProps<TRow>) {\n const labels = { ...DEFAULT_DATA_TABLE_LABELS, ...labelsProp };\n const text = label ?? labels.columnToggle;\n const hideable = columns.filter((column) => column.hideable !== false);\n\n const toggle = (columnId: string) => {\n // Bật lại một cột thì dựng lại danh sách theo thứ tự `columns` — giữ cho giá trị app lưu\n // luôn cùng một hình dạng, dù `DataTable` đằng nào cũng bỏ qua thứ tự bên trong.\n const next = visibleColumnIds.includes(columnId)\n ? visibleColumnIds.filter((id) => id !== columnId)\n : columns.filter((column) => visibleColumnIds.includes(column.id) || column.id === columnId).map((c) => c.id);\n if (next.length === 0) return;\n onVisibleColumnIdsChange(next);\n };\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className={className} leftIcon={<SlidersHorizontal className=\"size-4\" />}>\n {text}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-52\">\n <DropdownMenuLabel>{text}</DropdownMenuLabel>\n <DropdownMenuSeparator />\n {hideable.map((column) => (\n <DropdownMenuCheckboxItem\n key={column.id}\n checked={visibleColumnIds.includes(column.id)}\n onCheckedChange={() => toggle(column.id)}\n onSelect={(event) => event.preventDefault()}\n >\n {column.label ?? (typeof column.header === \"string\" ? column.header : column.id)}\n </DropdownMenuCheckboxItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/data-table.tsx"],"sourcesContent":["\"use client\";\n\nimport { ArrowDown, ArrowUp, ChevronsUpDown, Inbox, SlidersHorizontal } from \"lucide-react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Checkbox } from \"../ui/checkbox.js\";\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger\n} from \"../ui/dropdown-menu.js\";\nimport { Skeleton } from \"../ui/skeleton.js\";\nimport { TableBody, TableCell, TableHead, TableHeader, TableRow } from \"../ui/table.js\";\nimport { type DataTableLabels, DEFAULT_DATA_TABLE_LABELS } from \"./data-table-constants.js\";\nimport { EmptyState } from \"./empty-state.js\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableSort {\n columnId: string;\n direction: SortDirection;\n}\n\nexport type DataTableAlign = \"left\" | \"center\" | \"right\";\n\nexport interface DataTableColumn<TRow> {\n id: string;\n /** Nội dung ô tiêu đề */\n header: ReactNode;\n /** Nhãn chữ dùng trong menu ẩn/hiện cột — bắt buộc khi `header` không phải chuỗi */\n label?: string;\n /** Render ô dữ liệu — nhận cả chỉ số dòng để đánh số thứ tự */\n cell: (row: TRow, rowIndex: number) => ReactNode;\n sortable?: boolean;\n align?: DataTableAlign;\n /**\n * Bề rộng cột tính bằng px. BẮT BUỘC với cột ghim (`pinned`) vì offset `left/right` của\n * sticky được tính cộng dồn từ giá trị này — thiếu width thì các cột ghim chồng lên nhau.\n */\n width?: number;\n minWidth?: number;\n /** Ghim cột khi cuộn ngang (bảng nhiều cột) */\n pinned?: \"left\" | \"right\";\n /** Cho phép người dùng ẩn cột này (mặc định true; cột định danh nên đặt false) */\n hideable?: boolean;\n headerClassName?: string;\n cellClassName?: string;\n}\n\nexport interface DataTableProps<TRow> {\n columns: DataTableColumn<TRow>[];\n rows: TRow[];\n /** Khoá ổn định của dòng — dùng cho `key` React và cho tập chọn */\n getRowId: (row: TRow, index: number) => string;\n\n /** Đang tải → hiện `loadingRowCount` dòng khung xương thay vì bảng trống */\n loading?: boolean;\n loadingRowCount?: number;\n /** Nội dung khi không có dòng nào (mặc định EmptyState theo `labels.emptyTitle`) */\n empty?: ReactNode;\n\n /** Sắp xếp — controlled; `null` = không sắp xếp. App tự sắp/tự gọi API. */\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n\n /** Chọn dòng bằng checkbox — controlled */\n selectable?: boolean;\n selectedIds?: string[];\n onSelectedIdsChange?: (ids: string[]) => void;\n /** Khoá chọn với một số dòng (vd bản ghi đã khoá) */\n isRowSelectable?: (row: TRow, index: number) => boolean;\n\n /**\n * Mã các cột đang hiện — controlled. Bỏ trống = hiện tất cả. Đổi bố cục đi qua\n * `DataTableColumnToggle`, app tự lưu theo người dùng (server, localStorage…).\n *\n * ⚠ Đây là một **TẬP HỢP**, không phải thứ tự: thứ tự cột luôn lấy từ mảng `columns`, thứ tự\n * bên trong mảng này bị bỏ qua. Tên có hậu tố `Ids` chính là để nói đúng thứ nó chứa —\n * 0.1.0 đặt `visibleColumns` khiến người đọc tưởng là mảng cột (`DataTableColumn[]`) hoặc\n * tưởng sắp lại mảng thì cột đổi chỗ.\n *\n * Cố ý KHÔNG dùng `Record<string, boolean>`: kiểu đó có trạng thái thứ ba — **khoá vắng\n * mặt**. App thêm cột mới ở bản sau thì bố cục người dùng đã lưu không có khoá đó, mỗi nơi\n * tự đoán hiện/ẩn → lỗi im lặng. Một mảng mã cột thì `JSON.stringify` là lưu được.\n *\n * Package cố ý KHÔNG tự ghi localStorage: ở SaaS nhiều workspace, bố cục bảng phải theo\n * cặp (người dùng × workspace) và thường cần đồng bộ giữa các máy — chuyện của app.\n */\n visibleColumnIds?: string[];\n\n /**\n * Bấm vào dòng (vd mở chi tiết bản ghi). Có prop này thì dòng vào chuỗi Tab và nhận\n * Enter/Space — mở chi tiết KHÔNG được là thao tác chỉ-chuột.\n */\n onRowClick?: (row: TRow, index: number) => void;\n rowClassName?: (row: TRow, index: number) => string | undefined;\n /** Dòng đang được làm nổi (vd bản ghi vừa mở ở panel bên) */\n isRowActive?: (row: TRow, index: number) => boolean;\n\n /** Tiêu đề dính đỉnh vùng cuộn (mặc định true) */\n stickyHeader?: boolean;\n /**\n * Tô nền xen kẽ cho hàng chẵn — mẫu DataTable ở mục **Patterns** của comitor-ds dùng\n * `<Table striped>`, còn `DataTable` trước đây không có đường bật.\n *\n * Mặc định `false`: bảng dài và dày cột thì sọc giúp dò hàng, nhưng bảng ngắn thì nó chỉ thêm\n * nhiễu. Đây là quyết định của từng trang, không phải mặc định của gói.\n *\n * ⚠ Sọc dùng `bg-muted/30` và đặt trên `<tr>` — cùng chỗ mà nền hàng ĐANG CHỌN / ĐANG MỞ đặt.\n * Nền trạng thái luôn thắng vì nó ghi sau trong chuỗi lớp của `<TableRow>`; ô cột ghim\n * (`bg-inherit`) vì thế vẫn kế thừa đúng nền của hàng.\n */\n striped?: boolean;\n /** Bề rộng tối thiểu của bảng — bật cuộn ngang khi màn hình hẹp */\n minWidth?: number | string;\n density?: \"comfortable\" | \"compact\";\n /** Ghi đè chuỗi hiển thị/a11y (mặc định tiếng Việt) — xem `DataTableLabels` */\n labels?: Partial<DataTableLabels>;\n /** Nhãn của bảng cho trình đọc màn hình (bắt buộc khi trang có nhiều bảng) */\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n className?: string;\n /** className của khung cuộn bọc ngoài */\n containerClassName?: string;\n}\n\nconst alignClasses: Record<DataTableAlign, string> = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\"\n};\n\n/** Offset sticky cộng dồn cho cột ghim trái/phải. */\nfunction getPinnedOffsets<TRow>(columns: DataTableColumn<TRow>[], selectable: boolean): Map<string, CSSProperties> {\n const styles = new Map<string, CSSProperties>();\n // Cột checkbox (nếu có) luôn ghim trái và chiếm 44px — cột ghim đầu tiên phải bắt đầu sau nó.\n let left = selectable ? 44 : 0;\n for (const column of columns) {\n if (column.pinned !== \"left\") continue;\n styles.set(column.id, { position: \"sticky\", left, zIndex: 2 });\n left += column.width ?? 0;\n }\n\n let right = 0;\n for (const column of [...columns].reverse()) {\n if (column.pinned !== \"right\") continue;\n styles.set(column.id, { position: \"sticky\", right, zIndex: 2 });\n right += column.width ?? 0;\n }\n\n return styles;\n}\n\nfunction nextSort(current: DataTableSort | null | undefined, columnId: string): DataTableSort | null {\n // asc → desc → bỏ sắp xếp. Vòng 3 trạng thái để người dùng quay lại thứ tự gốc của server\n // mà không phải tải lại trang.\n if (!current || current.columnId !== columnId) return { columnId, direction: \"asc\" };\n if (current.direction === \"asc\") return { columnId, direction: \"desc\" };\n return null;\n}\n\n/**\n * Bảng dữ liệu chuẩn của Comitor: tiêu đề dính, sắp xếp, chọn dòng, ghim cột, ẩn/hiện cột,\n * trạng thái tải và trạng thái rỗng.\n *\n * Mọi trạng thái đều **controlled** — package không giữ dữ liệu, không gọi API, không tự phân\n * trang. Nhờ vậy dùng được cả với dữ liệu client (mảng trong bộ nhớ) lẫn server-side\n * pagination mà không phải viết hai component.\n */\nexport function DataTable<TRow>({\n columns,\n rows,\n getRowId,\n loading = false,\n loadingRowCount = 5,\n empty,\n sort,\n onSortChange,\n selectable = false,\n selectedIds,\n onSelectedIdsChange,\n isRowSelectable,\n visibleColumnIds,\n onRowClick,\n rowClassName,\n isRowActive,\n stickyHeader = true,\n striped = false,\n minWidth,\n density = \"comfortable\",\n labels: labelsProp,\n className,\n containerClassName,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby\n}: DataTableProps<TRow>) {\n const labels = { ...DEFAULT_DATA_TABLE_LABELS, ...labelsProp };\n // Thứ tự luôn theo `columns` — `visibleColumnIds` chỉ trả lời \"cột nào được hiện\".\n const shownColumns = visibleColumnIds ? columns.filter((column) => visibleColumnIds.includes(column.id)) : columns;\n const pinnedStyles = getPinnedOffsets(shownColumns, selectable);\n const colCount = shownColumns.length + (selectable ? 1 : 0);\n\n const selected = selectedIds ?? [];\n // Một lượt duyệt: vừa lọc dòng chọn được vừa lấy id — tránh indexOf() lồng trong map (O(n²))\n // và tránh sai khi hai dòng cùng tham chiếu object.\n const selectableIds: string[] = [];\n rows.forEach((row, index) => {\n if (isRowSelectable?.(row, index) ?? true) selectableIds.push(getRowId(row, index));\n });\n const allSelected = selectableIds.length > 0 && selectableIds.every((id) => selected.includes(id));\n const someSelected = !allSelected && selectableIds.some((id) => selected.includes(id));\n\n const toggleAll = () => {\n if (!onSelectedIdsChange) return;\n if (allSelected) {\n onSelectedIdsChange(selected.filter((id) => !selectableIds.includes(id)));\n return;\n }\n onSelectedIdsChange([...new Set([...selected, ...selectableIds])]);\n };\n\n const toggleRow = (id: string) => {\n if (!onSelectedIdsChange) return;\n onSelectedIdsChange(selected.includes(id) ? selected.filter((item) => item !== id) : [...selected, id]);\n };\n\n const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-4 py-3\";\n\n return (\n <div\n data-slot=\"data-table-container\"\n className={cn(\"relative w-full overflow-auto rounded-xl border border-border bg-card\", containerClassName)}\n >\n <table\n data-slot=\"data-table\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-busy={loading || undefined}\n className={cn(\n \"w-full caption-bottom text-sm\",\n // Cùng một chuỗi lớp với `<Table striped>` của Tier 1 — đổi một chỗ thì đổi cả hai.\n striped && \"[&_tbody_tr:nth-child(even)]:bg-muted/30\",\n className\n )}\n style={minWidth != null ? { minWidth } : undefined}\n >\n <TableHeader className={cn(stickyHeader && \"sticky top-0 z-10\")}>\n {/* Nền ĐẶC đặt trên <tr>, không phải <thead>: ô ghim dùng `bg-inherit` mà\n background-color kế thừa từ CHA TRỰC TIẾP (<tr>). Để nền ở <thead> thì <tr> vẫn\n trong suốt → cột ghim nhìn xuyên thấy dữ liệu cuộn phía dưới. */}\n <TableRow className=\"bg-muted hover:bg-muted\">\n {selectable && (\n <TableHead\n className=\"w-11 bg-inherit\"\n style={{ position: \"sticky\", left: 0, zIndex: 3, width: 44, minWidth: 44 }}\n >\n <Checkbox\n checked={allSelected ? true : someSelected ? \"indeterminate\" : false}\n onCheckedChange={toggleAll}\n disabled={selectableIds.length === 0}\n aria-label={allSelected ? labels.deselectAllRows : labels.selectAllRows}\n />\n </TableHead>\n )}\n\n {shownColumns.map((column) => {\n const isSorted = sort?.columnId === column.id;\n const SortIcon = isSorted ? (sort?.direction === \"asc\" ? ArrowUp : ArrowDown) : ChevronsUpDown;\n const pinnedStyle = pinnedStyles.get(column.id);\n\n return (\n <TableHead\n key={column.id}\n scope=\"col\"\n aria-sort={isSorted ? (sort?.direction === \"asc\" ? \"ascending\" : \"descending\") : undefined}\n className={cn(\n alignClasses[column.align ?? \"left\"],\n pinnedStyle && \"bg-inherit\",\n column.headerClassName\n )}\n style={{\n ...pinnedStyle,\n width: column.width,\n minWidth: column.minWidth ?? column.width\n }}\n >\n {column.sortable && onSortChange ? (\n <button\n type=\"button\"\n onClick={() => onSortChange(nextSort(sort, column.id))}\n className={cn(\n \"-mx-1 inline-flex items-center gap-1 rounded-sm px-1 py-0.5 uppercase transition-colors hover:text-foreground\",\n column.align === \"right\" && \"flex-row-reverse\",\n isSorted && \"text-foreground\"\n )}\n >\n {column.header}\n {/* KHÔNG làm mờ icon chưa sắp xếp: nút này còn bấm được nên `opacity-*`\n không được miễn trừ ngưỡng 3:1 của WCAG 1.4.11 cho icon mang nghĩa.\n `opacity-50` cho ra 1,98:1 (light) / 2,23:1 (dark) trên nền `bg-muted`\n của hàng tiêu đề ✗; để nguyên `text-muted-foreground` là 4,89 / 4,86:1 ✓.\n Trạng thái ĐANG sắp xếp vẫn phân biệt được: nút đổi sang\n `text-foreground` và icon đổi hình (ArrowUp/ArrowDown ⇄ ChevronsUpDown). */}\n <SortIcon aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n ) : (\n column.header\n )}\n </TableHead>\n );\n })}\n </TableRow>\n </TableHeader>\n\n <TableBody>\n {loading &&\n Array.from({ length: loadingRowCount }).map((_, rowIndex) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: dòng khung xương không có danh tính dữ liệu\n <TableRow key={`skeleton-${rowIndex}`} className=\"hover:bg-transparent\">\n {selectable && (\n <TableCell className={cellPadding}>\n <Skeleton className=\"size-4 rounded-sm\" />\n </TableCell>\n )}\n {shownColumns.map((column) => (\n <TableCell key={column.id} className={cellPadding}>\n <Skeleton className=\"h-4 w-full max-w-[12rem]\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n\n {!loading && rows.length === 0 && (\n <TableRow className=\"hover:bg-transparent\">\n <TableCell colSpan={colCount} className=\"p-0\">\n {empty ?? <EmptyState icon={Inbox} title={labels.emptyTitle} description={labels.emptyDescription} />}\n </TableCell>\n </TableRow>\n )}\n\n {!loading &&\n rows.map((row, rowIndex) => {\n const id = getRowId(row, rowIndex);\n const isSelected = selected.includes(id);\n const canSelect = isRowSelectable?.(row, rowIndex) ?? true;\n const active = isRowActive?.(row, rowIndex) ?? false;\n\n return (\n <TableRow\n key={id}\n data-state={isSelected ? \"selected\" : undefined}\n aria-selected={selectable ? isSelected : undefined}\n onClick={onRowClick ? () => onRowClick(row, rowIndex) : undefined}\n // Dòng bấm được phải dùng được bằng BÀN PHÍM: đưa <tr> vào chuỗi Tab và tự\n // xử lý Enter/Space. Cố ý KHÔNG đặt role=\"button\" — role đó đè role=\"row\"\n // ngầm, trình đọc màn hình sẽ mất cấu trúc bảng (số dòng/cột, tiêu đề cột);\n // dòng vẫn là dòng, chỉ thêm khả năng kích hoạt.\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (event) => {\n // Chỉ nhận phím khi CHÍNH <tr> đang focus: Enter/Space bấm trên\n // control bên trong ô (checkbox, nút, link) đã có nghĩa riêng —\n // nổi bọt lên đây sẽ mở chi tiết ngoài ý muốn.\n if (event.target !== event.currentTarget) return;\n if (event.key !== \"Enter\" && event.key !== \" \") return;\n // Space mặc định cuộn trang, Enter có thể submit form bao ngoài.\n event.preventDefault();\n onRowClick(row, rowIndex);\n }\n : undefined\n }\n className={cn(\n // bg-card trên <tr> để ô ghim (`bg-inherit`) có màu đặc mà vẫn đổi theo\n // trạng thái chọn/hover của chính dòng đó.\n \"bg-card\",\n // Viền focus vẽ bằng `outline` chứ không `ring`: <tr> nằm trong bảng\n // border-collapse, box-shadow không được vẽ. Offset ÂM để viền nằm trong\n // lòng dòng, không bị khung cuộn cắt mất ở mép trái/phải.\n // Màu dùng `foreground` chứ không `ring` (gold #E8B824 chỉ ~1,9:1 trên nền\n // card — dưới 3:1 của WCAG 1.4.11 cho thành phần phi-văn-bản).\n onRowClick &&\n \"cursor-pointer focus-visible:outline-2 focus-visible:outline-foreground focus-visible:[outline-offset:-2px]\",\n active && \"bg-app-accent/5\",\n rowClassName?.(row, rowIndex)\n )}\n >\n {selectable && (\n <TableCell\n className={cn(cellPadding, \"bg-inherit\")}\n style={{ position: \"sticky\", left: 0, zIndex: 1, width: 44, minWidth: 44 }}\n >\n {/* Chặn nổi bọt: bấm checkbox không được kích hoạt onRowClick (mở chi tiết) */}\n {/* biome-ignore lint/a11y/noStaticElementInteractions: lớp bọc chỉ chặn sự kiện, tương tác nằm ở Checkbox bên trong */}\n {/* biome-ignore lint/a11y/useKeyWithClickEvents: phím bấm đã do Checkbox xử lý */}\n <span onClick={(event) => event.stopPropagation()}>\n <Checkbox\n checked={isSelected}\n disabled={!canSelect}\n onCheckedChange={() => toggleRow(id)}\n aria-label={isSelected ? labels.deselectRow : labels.selectRow}\n />\n </span>\n </TableCell>\n )}\n\n {shownColumns.map((column) => {\n const pinnedStyle = pinnedStyles.get(column.id);\n return (\n <TableCell\n key={column.id}\n className={cn(\n cellPadding,\n alignClasses[column.align ?? \"left\"],\n pinnedStyle && \"bg-inherit\",\n column.cellClassName\n )}\n style={{ ...pinnedStyle, width: column.width, minWidth: column.minWidth ?? column.width }}\n >\n {column.cell(row, rowIndex)}\n </TableCell>\n );\n })}\n </TableRow>\n );\n })}\n </TableBody>\n </table>\n </div>\n );\n}\n\nexport interface DataTableColumnToggleProps<TRow> {\n columns: DataTableColumn<TRow>[];\n /** Mã các cột đang hiện — cùng nguồn với `DataTable.visibleColumnIds` (tập hợp, không phải thứ tự) */\n visibleColumnIds: string[];\n onVisibleColumnIdsChange: (visibleColumnIds: string[]) => void;\n /** Nhãn nút + tiêu đề menu; không truyền thì lấy `labels.columnToggle` */\n label?: string;\n /** Ghi đè chuỗi hiển thị (mặc định tiếng Việt) — xem `DataTableLabels` */\n labels?: Partial<DataTableLabels>;\n className?: string;\n}\n\n/**\n * Menu ẩn/hiện cột đi kèm `DataTable` (đặt trên thanh công cụ của trang danh sách).\n *\n * Không cho ẩn hết: luôn giữ lại ít nhất một cột, nếu không bảng thành khối trắng vô nghĩa\n * và người dùng không có đường quay lại.\n */\nexport function DataTableColumnToggle<TRow>({\n columns,\n visibleColumnIds,\n onVisibleColumnIdsChange,\n label,\n labels: labelsProp,\n className\n}: DataTableColumnToggleProps<TRow>) {\n const labels = { ...DEFAULT_DATA_TABLE_LABELS, ...labelsProp };\n const text = label ?? labels.columnToggle;\n const hideable = columns.filter((column) => column.hideable !== false);\n\n const toggle = (columnId: string) => {\n // Bật lại một cột thì dựng lại danh sách theo thứ tự `columns` — giữ cho giá trị app lưu\n // luôn cùng một hình dạng, dù `DataTable` đằng nào cũng bỏ qua thứ tự bên trong.\n const next = visibleColumnIds.includes(columnId)\n ? visibleColumnIds.filter((id) => id !== columnId)\n : columns.filter((column) => visibleColumnIds.includes(column.id) || column.id === columnId).map((c) => c.id);\n if (next.length === 0) return;\n onVisibleColumnIdsChange(next);\n };\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className={className} leftIcon={<SlidersHorizontal className=\"size-4\" />}>\n {text}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-52\">\n <DropdownMenuLabel>{text}</DropdownMenuLabel>\n <DropdownMenuSeparator />\n {hideable.map((column) => (\n <DropdownMenuCheckboxItem\n key={column.id}\n checked={visibleColumnIds.includes(column.id)}\n onCheckedChange={() => toggle(column.id)}\n onSelect={(event) => event.preventDefault()}\n >\n {column.label ?? (typeof column.header === \"string\" ? column.header : column.id)}\n </DropdownMenuCheckboxItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n"],"mappings":"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","names":[]}
@@ -23,7 +23,7 @@ function SectionHeading({ title, description, icon: Icon }) {
23
23
  }
24
24
  ),
25
25
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
26
- /* @__PURE__ */ jsx("h3", { className: "truncate text-sm font-semibold tracking-tight text-foreground", children: title }),
26
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold tracking-tight text-foreground", children: title }),
27
27
  description && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: description })
28
28
  ] })
29
29
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\n/*\n * Vạch 4px bên trái MANG THÔNG TIN (phân biệt nhóm trường), nên chịu ngưỡng 3:1 của WCAG\n * 1.4.11 — không phải viền trang trí. Nó đứng trên nền TRANG (`bg-card`) chứ không phải trên\n * nền cùng tone, tức đúng vai trò bậc `-ink` của hợp đồng màu (docs/hop-dong-mau.md §1).\n *\n * Bản trước lấy thẳng bậc NỀN và hỏng ở light — vạch gần như biến mất trên thẻ trắng:\n * gold-300 #E8B824 1,86:1 ✗ · warning #F2C94C 1,59:1 ✗\n * success #2EA56A 3,13:1 · info #1F8A8B 4,14:1 · destructive #C93B3B 5,03:1\n * (dark thì ngược lại — destructive #C93B3B chỉ 3,53:1 trên `--card`.)\n *\n * Bậc ink đạt thoải mái ở CẢ hai theme, đo trên `--card` (light #FFFFFF · dark #16181D):\n * primary-ink 6,51 / 11,90:1 success-ink 6,52 / 10,19:1\n * info-ink 6,68 / 9,54:1 warning-ink 6,51 / 11,60:1\n * destructive-ink 7,15 / 6,42:1\n */\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-primary-ink\",\n info: \"border-l-4 border-l-info-ink\",\n warning: \"border-l-4 border-l-warning-ink\",\n success: \"border-l-4 border-l-success-ink\",\n destructive: \"border-l-4 border-l-destructive-ink\"\n};\n\nfunction SectionHeading({ title, description, icon: Icon }: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\">) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\"\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AA8DU,cAGJ,YAHI;AA5DV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAwCpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe,EAAE,OAAO,aAAa,MAAM,KAAK,GAA6D;AACpH,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QAEV,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WACb;AAAA,0BAAC,QAAG,WAAU,iEAAiE,iBAAM;AAAA,MACpF,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACnE,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACpE;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/form-section.tsx"],"sourcesContent":["\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { type ReactNode, useState } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"../ui/collapsible.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\nexport type FormSectionAccent = \"none\" | \"primary\" | \"info\" | \"warning\" | \"success\" | \"destructive\";\n\nexport interface FormSectionProps {\n title: ReactNode;\n description?: ReactNode;\n icon?: IconComponent;\n /** Slot bên phải tiêu đề (nút phụ, công tắc…) — nằm NGOÀI nút gập để không lồng button */\n actions?: ReactNode;\n /** Cho phép gập/mở (mặc định false) */\n collapsible?: boolean;\n /** Trạng thái mở ban đầu khi không controlled */\n defaultOpen?: boolean;\n /** Controlled: app tự giữ trạng thái mở */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Vạch màu bên trái, phân biệt nhóm trường (mặc định \"none\") */\n accent?: FormSectionAccent;\n children: ReactNode;\n className?: string;\n contentClassName?: string;\n}\n\n/*\n * Vạch 4px bên trái MANG THÔNG TIN (phân biệt nhóm trường), nên chịu ngưỡng 3:1 của WCAG\n * 1.4.11 — không phải viền trang trí. Nó đứng trên nền TRANG (`bg-card`) chứ không phải trên\n * nền cùng tone, tức đúng vai trò bậc `-ink` của hợp đồng màu (docs/hop-dong-mau.md §1).\n *\n * Bản trước lấy thẳng bậc NỀN và hỏng ở light — vạch gần như biến mất trên thẻ trắng:\n * gold-300 #E8B824 1,86:1 ✗ · warning #F2C94C 1,59:1 ✗\n * success #2EA56A 3,13:1 · info #1F8A8B 4,14:1 · destructive #C93B3B 5,03:1\n * (dark thì ngược lại — destructive #C93B3B chỉ 3,53:1 trên `--card`.)\n *\n * Bậc ink đạt thoải mái ở CẢ hai theme, đo trên `--card` (light #FFFFFF · dark #16181D):\n * primary-ink 6,51 / 11,90:1 success-ink 6,52 / 10,19:1\n * info-ink 6,68 / 9,54:1 warning-ink 6,51 / 11,60:1\n * destructive-ink 7,15 / 6,42:1\n */\nconst accentClasses: Record<FormSectionAccent, string> = {\n none: \"\",\n primary: \"border-l-4 border-l-primary-ink\",\n info: \"border-l-4 border-l-info-ink\",\n warning: \"border-l-4 border-l-warning-ink\",\n success: \"border-l-4 border-l-success-ink\",\n destructive: \"border-l-4 border-l-destructive-ink\"\n};\n\nfunction SectionHeading({ title, description, icon: Icon }: Pick<FormSectionProps, \"title\" | \"description\" | \"icon\">) {\n return (\n <div className=\"flex min-w-0 items-start gap-3 text-left\">\n {Icon && (\n <span\n aria-hidden=\"true\"\n className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\"\n >\n <Icon className=\"size-4\" />\n </span>\n )}\n <div className=\"min-w-0\">\n {/* KHÔNG `truncate`: ở 320px tiêu đề nhóm dài (\"Thông tin không gian làm việc\") bị cắt\n mất chữ — đo được 26px nội dung biến mất. Tiêu đề xuống dòng thì xấu hơn một chút\n nhưng vẫn đọc được, còn cắt thì mất thông tin. `min-w-0` ở thẻ cha vẫn giữ cho hàng\n header của nhóm không đẩy nút actions ra ngoài. */}\n <h3 className=\"text-sm font-semibold tracking-tight text-foreground\">{title}</h3>\n {description && <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>}\n </div>\n </div>\n );\n}\n\n/**\n * Nhóm trường trong form: tiêu đề + mô tả + lưới 12 cột cho các `FormField`.\n *\n * Nội dung là `grid-cols-12` để `FormField` chỉ cần khai `colSpan` — cách này giữ mọi form\n * trong hệ sinh thái cùng một nhịp cột, thay vì mỗi trang tự chế `flex` với bề rộng riêng.\n */\nexport function FormSection({\n title,\n description,\n icon,\n actions,\n collapsible = false,\n defaultOpen = true,\n open,\n onOpenChange,\n accent = \"none\",\n children,\n className,\n contentClassName\n}: FormSectionProps) {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpen = open ?? internalOpen;\n\n const setOpen = (next: boolean) => {\n if (open === undefined) setInternalOpen(next);\n onOpenChange?.(next);\n };\n\n const shell = cn(\"rounded-xl border border-border bg-card\", accentClasses[accent], className);\n const grid = cn(\"grid grid-cols-12 gap-4 p-4 md:gap-5 md:p-5\", contentClassName);\n\n if (!collapsible) {\n return (\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <SectionHeading title={title} description={description} icon={icon} />\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <div className={grid}>{children}</div>\n </section>\n );\n }\n\n return (\n <Collapsible open={isOpen} onOpenChange={setOpen} asChild>\n <section data-slot=\"form-section\" className={shell}>\n <div className=\"flex items-start justify-between gap-3 border-b border-border p-4 md:px-5\">\n <CollapsibleTrigger className=\"flex min-w-0 flex-1 items-start gap-3 rounded-md text-left\">\n <SectionHeading title={title} description={description} icon={icon} />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"ml-auto mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out\",\n isOpen && \"rotate-180\"\n )}\n />\n </CollapsibleTrigger>\n {actions && <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>}\n </div>\n <CollapsibleContent>\n <div className={grid}>{children}</div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n );\n}\n"],"mappings":";AA8DU,cAGJ,YAHI;AA5DV,SAAS,mBAAmB;AAC5B,SAAyB,gBAAgB;AACzC,SAAS,UAAU;AACnB,SAAS,aAAa,oBAAoB,0BAA0B;AAwCpE,MAAM,gBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AAEA,SAAS,eAAe,EAAE,OAAO,aAAa,MAAM,KAAK,GAA6D;AACpH,SACE,qBAAC,SAAI,WAAU,4CACZ;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QAEV,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,IAC3B;AAAA,IAEF,qBAAC,SAAI,WAAU,WAKb;AAAA,0BAAC,QAAG,WAAU,wDAAwD,iBAAM;AAAA,MAC3E,eAAe,oBAAC,OAAE,WAAU,wCAAwC,uBAAY;AAAA,OACnF;AAAA,KACF;AAEJ;AAQO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,SAAS,QAAQ;AAEvB,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,SAAS,OAAW,iBAAgB,IAAI;AAC5C,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,QAAQ,GAAG,2CAA2C,cAAc,MAAM,GAAG,SAAS;AAC5F,QAAM,OAAO,GAAG,+CAA+C,gBAAgB;AAE/E,MAAI,CAAC,aAAa;AAChB,WACE,qBAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,2BAAC,SAAI,WAAU,6EACb;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACnE,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,SACzE;AAAA,MACA,oBAAC,SAAI,WAAW,MAAO,UAAS;AAAA,OAClC;AAAA,EAEJ;AAEA,SACE,oBAAC,eAAY,MAAM,QAAQ,cAAc,SAAS,SAAO,MACvD,+BAAC,aAAQ,aAAU,gBAAe,WAAW,OAC3C;AAAA,yBAAC,SAAI,WAAU,6EACb;AAAA,2BAAC,sBAAmB,WAAU,8DAC5B;AAAA,4BAAC,kBAAe,OAAc,aAA0B,MAAY;AAAA,QACpE;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,UAAU;AAAA,YACZ;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACC,WAAW,oBAAC,SAAI,WAAU,oCAAoC,mBAAQ;AAAA,OACzE;AAAA,IACA,oBAAC,sBACC,8BAAC,SAAI,WAAW,MAAO,UAAS,GAClC;AAAA,KACF,GACF;AAEJ;","names":[]}
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { ReactNode, ComponentType } from 'react';
3
+ import { TabsVariant } from '../ui/tabs-variants.js';
4
+
5
+ /**
6
+ * Tab điều hướng — mỗi mục là một ROUTE THẬT, không phải một panel trong cùng trang.
7
+ *
8
+ * ─ VÌ SAO KHÔNG DÙNG \`Tabs\` CHO VIỆC NÀY ─
9
+ * \`Tabs\` (Radix) dựng \`role="tablist"\` + \`role="tab"\` + \`aria-controls\` trỏ tới một
10
+ * \`role="tabpanel"\` CÙNG TRANG. Khi mỗi mục là một URL riêng thì:
11
+ * • không có tabpanel nào để trỏ tới — \`aria-controls\` thành id treo, một lỗi a11y thật;
12
+ * • \`role="tab"\` nói với trình đọc màn hình rằng bấm vào sẽ đổi panel tại chỗ, trong khi thực
13
+ * tế là điều hướng sang trang khác;
14
+ * • roving focus của tablist nuốt phím mũi tên, thứ mà một danh sách link không nên làm.
15
+ * Đây là \`<nav>\` chứa link thật, trạng thái đi bằng \`aria-current="page"\`.
16
+ *
17
+ * Nhìn thì KHÔNG khác \`Tabs\`: cả hai đọc chung một bảng lớp (\`ui/tabs-variants.ts\`), nên đổi
18
+ * kiểu tab ở một chỗ là cả hai đi theo.
19
+ *
20
+ * ─ LINK ─
21
+ * Mặc định render \`<a>\` (tải lại cả trang). Trên Next phải truyền \`LinkComponent={Link}\` —
22
+ * cùng quy ước với \`Breadcrumb\` và \`AppShell\`; gói cố ý không \`import "next/link"\` để còn chạy
23
+ * được ngoài Next.
24
+ *
25
+ * ```tsx
26
+ * "use client";
27
+ * const pathname = usePathname();
28
+ * <RouteTabs
29
+ * variant="pills"
30
+ * pathname={pathname}
31
+ * LinkComponent={Link}
32
+ * items={[
33
+ * { href: "/cai-dat", label: "Chung" },
34
+ * { href: "/cai-dat/thong-bao", label: "Thông báo" }
35
+ * ]}
36
+ * />
37
+ * ```
38
+ */
39
+ /** Props tối thiểu mà component link phải nhận — \`next/link\` thoả sẵn. */
40
+ interface RouteTabsLinkProps {
41
+ href: string;
42
+ className?: string;
43
+ children: ReactNode;
44
+ "aria-current"?: "page" | undefined;
45
+ "data-state"?: "active" | "inactive";
46
+ }
47
+ type RouteTabsLinkComponent = ComponentType<RouteTabsLinkProps>;
48
+ interface RouteTabItem {
49
+ href: string;
50
+ label: ReactNode;
51
+ /**
52
+ * Mặc định khớp CHÍNH XÁC \`pathname === href\`.
53
+ *
54
+ * \`prefix\`: khớp cả route con (\`/cai-dat/thanh-vien/abc\` vẫn sáng ở \`/cai-dat/thanh-vien\`).
55
+ * ⚠ Mục có href là đường dẫn GỐC của nhóm (\`/cai-dat\`) mà để \`prefix\` thì nó sáng ở MỌI tab —
56
+ * đúng lỗi mà tab dạng \`?query\` từng mắc. Để mặc định cho mục gốc.
57
+ */
58
+ match?: "exact" | "prefix";
59
+ }
60
+ interface RouteTabsProps {
61
+ items: RouteTabItem[];
62
+ /** Đường dẫn hiện tại — \`usePathname()\` trên Next. */
63
+ pathname: string;
64
+ variant?: TabsVariant;
65
+ /** Component link của app. Bỏ trống → \`<a>\` thô, tải lại cả trang. */
66
+ LinkComponent?: RouteTabsLinkComponent;
67
+ /** Nhãn của vùng điều hướng, cho trình đọc màn hình. */
68
+ label?: string;
69
+ className?: string;
70
+ /** Lớp thêm cho từng link. */
71
+ itemClassName?: string;
72
+ }
73
+ declare function RouteTabs({ items, pathname, variant, LinkComponent, label, className, itemClassName }: RouteTabsProps): React.JSX.Element;
74
+
75
+ export { type RouteTabItem, RouteTabs, type RouteTabsLinkComponent, type RouteTabsLinkProps, type RouteTabsProps };
@@ -0,0 +1,50 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/cn.js";
3
+ import { TABS_TRIGGER_BASE, tabsListVariants, tabsTriggerVariants } from "../ui/tabs-variants.js";
4
+ function DefaultLink({ href, className, children, ...rest }) {
5
+ return /* @__PURE__ */ jsx("a", { href, className, ...rest, children });
6
+ }
7
+ function isItemActive(item, pathname) {
8
+ if (item.match === "prefix") return pathname === item.href || pathname.startsWith(`${item.href}/`);
9
+ return pathname === item.href;
10
+ }
11
+ function RouteTabs({
12
+ items,
13
+ pathname,
14
+ variant = "default",
15
+ LinkComponent = DefaultLink,
16
+ label = "M\u1EE5c c\u1EE7a trang",
17
+ className,
18
+ itemClassName
19
+ }) {
20
+ return /* @__PURE__ */ jsx("nav", { "aria-label": label, "data-slot": "route-tabs", children: /* @__PURE__ */ jsx(
21
+ "div",
22
+ {
23
+ "data-slot": "route-tabs-list",
24
+ className: cn("flex gap-0 overflow-x-auto", tabsListVariants[variant], className),
25
+ children: items.map((item) => {
26
+ const active = isItemActive(item, pathname);
27
+ return /* @__PURE__ */ jsx(
28
+ LinkComponent,
29
+ {
30
+ href: item.href,
31
+ "data-state": active ? "active" : "inactive",
32
+ "aria-current": active ? "page" : void 0,
33
+ className: cn(
34
+ tabsTriggerVariants[variant],
35
+ TABS_TRIGGER_BASE,
36
+ "shrink-0 whitespace-nowrap text-center",
37
+ itemClassName
38
+ ),
39
+ children: item.label
40
+ },
41
+ item.href
42
+ );
43
+ })
44
+ }
45
+ ) });
46
+ }
47
+ export {
48
+ RouteTabs
49
+ };
50
+ //# sourceMappingURL=route-tabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/composite/route-tabs.tsx"],"sourcesContent":["import type { ComponentType, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { TABS_TRIGGER_BASE, type TabsVariant, tabsListVariants, tabsTriggerVariants } from \"../ui/tabs-variants.js\";\n\n/**\n * Tab điều hướng — mỗi mục là một ROUTE THẬT, không phải một panel trong cùng trang.\n *\n * ─ VÌ SAO KHÔNG DÙNG \\`Tabs\\` CHO VIỆC NÀY ─\n * \\`Tabs\\` (Radix) dựng \\`role=\"tablist\"\\` + \\`role=\"tab\"\\` + \\`aria-controls\\` trỏ tới một\n * \\`role=\"tabpanel\"\\` CÙNG TRANG. Khi mỗi mục là một URL riêng thì:\n * • không có tabpanel nào để trỏ tới — \\`aria-controls\\` thành id treo, một lỗi a11y thật;\n * • \\`role=\"tab\"\\` nói với trình đọc màn hình rằng bấm vào sẽ đổi panel tại chỗ, trong khi thực\n * tế là điều hướng sang trang khác;\n * • roving focus của tablist nuốt phím mũi tên, thứ mà một danh sách link không nên làm.\n * Đây là \\`<nav>\\` chứa link thật, trạng thái đi bằng \\`aria-current=\"page\"\\`.\n *\n * Nhìn thì KHÔNG khác \\`Tabs\\`: cả hai đọc chung một bảng lớp (\\`ui/tabs-variants.ts\\`), nên đổi\n * kiểu tab ở một chỗ là cả hai đi theo.\n *\n * ─ LINK ─\n * Mặc định render \\`<a>\\` (tải lại cả trang). Trên Next phải truyền \\`LinkComponent={Link}\\` —\n * cùng quy ước với \\`Breadcrumb\\` và \\`AppShell\\`; gói cố ý không \\`import \"next/link\"\\` để còn chạy\n * được ngoài Next.\n *\n * ```tsx\n * \"use client\";\n * const pathname = usePathname();\n * <RouteTabs\n * variant=\"pills\"\n * pathname={pathname}\n * LinkComponent={Link}\n * items={[\n * { href: \"/cai-dat\", label: \"Chung\" },\n * { href: \"/cai-dat/thong-bao\", label: \"Thông báo\" }\n * ]}\n * />\n * ```\n */\n\n/** Props tối thiểu mà component link phải nhận — \\`next/link\\` thoả sẵn. */\nexport interface RouteTabsLinkProps {\n href: string;\n className?: string;\n children: ReactNode;\n \"aria-current\"?: \"page\" | undefined;\n \"data-state\"?: \"active\" | \"inactive\";\n}\n\nexport type RouteTabsLinkComponent = ComponentType<RouteTabsLinkProps>;\n\nexport interface RouteTabItem {\n href: string;\n label: ReactNode;\n /**\n * Mặc định khớp CHÍNH XÁC \\`pathname === href\\`.\n *\n * \\`prefix\\`: khớp cả route con (\\`/cai-dat/thanh-vien/abc\\` vẫn sáng ở \\`/cai-dat/thanh-vien\\`).\n * ⚠ Mục có href là đường dẫn GỐC của nhóm (\\`/cai-dat\\`) mà để \\`prefix\\` thì nó sáng ở MỌI tab —\n * đúng lỗi mà tab dạng \\`?query\\` từng mắc. Để mặc định cho mục gốc.\n */\n match?: \"exact\" | \"prefix\";\n}\n\nexport interface RouteTabsProps {\n items: RouteTabItem[];\n /** Đường dẫn hiện tại — \\`usePathname()\\` trên Next. */\n pathname: string;\n variant?: TabsVariant;\n /** Component link của app. Bỏ trống → \\`<a>\\` thô, tải lại cả trang. */\n LinkComponent?: RouteTabsLinkComponent;\n /** Nhãn của vùng điều hướng, cho trình đọc màn hình. */\n label?: string;\n className?: string;\n /** Lớp thêm cho từng link. */\n itemClassName?: string;\n}\n\nfunction DefaultLink({ href, className, children, ...rest }: RouteTabsLinkProps) {\n return (\n <a href={href} className={className} {...rest}>\n {children}\n </a>\n );\n}\n\nfunction isItemActive(item: RouteTabItem, pathname: string): boolean {\n if (item.match === \"prefix\") return pathname === item.href || pathname.startsWith(`${item.href}/`);\n return pathname === item.href;\n}\n\nexport function RouteTabs({\n items,\n pathname,\n variant = \"default\",\n LinkComponent = DefaultLink,\n label = \"Mục của trang\",\n className,\n itemClassName\n}: RouteTabsProps) {\n return (\n <nav aria-label={label} data-slot=\"route-tabs\">\n {/*\n `overflow-x-auto` để danh sách CUỘN NGANG khi hẹp, thay vì xuống dòng — nhãn tiếng Việt\n thường hai từ nên rất dễ vỡ dòng và làm pill cao gấp đôi. `whitespace-nowrap` ở từng mục\n là nửa còn lại của cùng một quyết định.\n */}\n <div\n data-slot=\"route-tabs-list\"\n className={cn(\"flex gap-0 overflow-x-auto\", tabsListVariants[variant], className)}\n >\n {items.map((item) => {\n const active = isItemActive(item, pathname);\n return (\n <LinkComponent\n key={item.href}\n href={item.href}\n // `data-state` để dùng lại nguyên bảng lớp của Tabs (chúng nhắm `data-[state=active]`).\n data-state={active ? \"active\" : \"inactive\"}\n aria-current={active ? \"page\" : undefined}\n className={cn(\n tabsTriggerVariants[variant],\n TABS_TRIGGER_BASE,\n \"shrink-0 whitespace-nowrap text-center\",\n itemClassName\n )}\n >\n {item.label}\n </LinkComponent>\n );\n })}\n </div>\n </nav>\n );\n}\n"],"mappings":"AA+EI;AA9EJ,SAAS,UAAU;AACnB,SAAS,mBAAqC,kBAAkB,2BAA2B;AA2E3F,SAAS,YAAY,EAAE,MAAM,WAAW,UAAU,GAAG,KAAK,GAAuB;AAC/E,SACE,oBAAC,OAAE,MAAY,WAAuB,GAAG,MACtC,UACH;AAEJ;AAEA,SAAS,aAAa,MAAoB,UAA2B;AACnE,MAAI,KAAK,UAAU,SAAU,QAAO,aAAa,KAAK,QAAQ,SAAS,WAAW,GAAG,KAAK,IAAI,GAAG;AACjG,SAAO,aAAa,KAAK;AAC3B;AAEO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR;AAAA,EACA;AACF,GAAmB;AACjB,SACE,oBAAC,SAAI,cAAY,OAAO,aAAU,cAMhC;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,8BAA8B,iBAAiB,OAAO,GAAG,SAAS;AAAA,MAE/E,gBAAM,IAAI,CAAC,SAAS;AACnB,cAAM,SAAS,aAAa,MAAM,QAAQ;AAC1C,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAM,KAAK;AAAA,YAEX,cAAY,SAAS,WAAW;AAAA,YAChC,gBAAc,SAAS,SAAS;AAAA,YAChC,WAAW;AAAA,cACT,oBAAoB,OAAO;AAAA,cAC3B;AAAA,cACA;AAAA,cACA;AAAA,YACF;AAAA,YAEC,eAAK;AAAA;AAAA,UAZD,KAAK;AAAA,QAaZ;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH,GACF;AAEJ;","names":[]}
@@ -3,7 +3,7 @@ import { cva } from "class-variance-authority";
3
3
  import { ArrowDownRight, ArrowUpRight, Minus } from "lucide-react";
4
4
  import { cn } from "../../lib/cn.js";
5
5
  import { Skeleton } from "../ui/skeleton.js";
6
- const statCardIconVariants = cva("flex size-9 shrink-0 items-center justify-center rounded-md", {
6
+ const statCardIconVariants = cva("flex size-7 shrink-0 items-center justify-center rounded-lg", {
7
7
  variants: {
8
8
  tone: {
9
9
  default: "bg-muted text-muted-foreground",
@@ -55,20 +55,24 @@ function StatCard({
55
55
  "div",
56
56
  {
57
57
  "data-slot": "stat-card",
58
- className: cn("rounded-xl border border-border bg-card p-4 shadow-card", className),
58
+ className: cn("rounded-xl border border-border bg-card p-5", className),
59
59
  ...props,
60
60
  children: [
61
61
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
62
62
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
63
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: label }),
64
- loading ? /* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-7 w-24" }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-semibold tabular-nums leading-tight text-foreground", children: value }),
63
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs font-medium text-muted-foreground", children: label }),
64
+ loading ? /* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-7 w-24" }) : (
65
+ /* `font-bold` theo bản duyệt. `tabular-nums` là khoản gói thêm và giữ lại: chữ số
66
+ đều bề ngang thì một hàng bốn thẻ không nhảy khi số đổi. */
67
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-bold tabular-nums leading-tight text-foreground", children: value })
68
+ ),
65
69
  delta && !loading && DeltaIcon && /* @__PURE__ */ jsxs("p", { className: cn("mt-1 flex items-center gap-1 text-xs font-medium", deltaToneClass(delta)), children: [
66
70
  /* @__PURE__ */ jsx(DeltaIcon, { "aria-hidden": "true", className: "size-3.5 shrink-0" }),
67
71
  /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: delta.value }),
68
72
  delta.label && /* @__PURE__ */ jsx("span", { className: "font-normal text-muted-foreground", children: delta.label })
69
73
  ] })
70
74
  ] }),
71
- Icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: statCardIconVariants({ tone }), children: /* @__PURE__ */ jsx(Icon, { className: "size-4.5" }) })
75
+ Icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: statCardIconVariants({ tone }), children: /* @__PURE__ */ jsx(Icon, { className: "size-3.5" }) })
72
76
  ] }),
73
77
  sparkline && /* @__PURE__ */ jsx("div", { className: "mt-3 h-10", children: sparkline }),
74
78
  footer && /* @__PURE__ */ jsx("div", { className: "mt-3 border-t border-border pt-2 text-xs text-muted-foreground", children: footer })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/composite/stat-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { ArrowDownRight, ArrowUpRight, Minus } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Skeleton } from \"../ui/skeleton.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\n/**\n * Ô icon của StatCard, tô theo tone token (không hex).\n *\n * Icon là thành phần PHI VĂN BẢN → ngưỡng WCAG 1.4.11 là 3:1 so với nền tint mà nó đứng lên.\n * Màu icon vì thế phải là bậc `--x-ink`, không phải token tô nền `--x`: `text-success` trên\n * `bg-success/15` chỉ được **2,69:1 ✗** ở light (dark thì 7,44:1 — mỗi theme hỏng một nửa là\n * dấu hiệu dùng sai vai trò token). Đo lại sau khi đổi, trên tint đã trộn với `--card`:\n *\n * success 5,60:1 (L) · 7,44:1 (D) · info 5,54:1 (L) · 7,12:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) · destructive 5,76:1 (L) · 5,71:1 (D)\n * default 4,89:1 (L) · 4,86:1 (D) · primary 5,77:1 (L) · 7,26:1 (D)\n * app 6,14:1 (L) · 9,61:1 (D)\n *\n * `warning` bỏ cặp `text-gold-500 dark:text-warning`: `--warning-ink` đúng bằng `--gold-500` ở\n * light và `--yellow` ở dark, nên một class là đủ.\n */\nexport const statCardIconVariants = cva(\"flex size-9 shrink-0 items-center justify-center rounded-md\", {\n variants: {\n tone: {\n default: \"bg-muted text-muted-foreground\",\n // Đi theo `--primary`, không phải thang `gold-*` chôn cứng. Bản cũ\n // `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100` đạt tương phản\n // (7,77:1 L · 11,85:1 D) nhưng đóng băng màu: app đổi brand thì ô icon vẫn vàng, và phải\n // viết hai lớp `dark:` vì thang gold không có bản dark. Token đạt 5,77:1 (L) · 7,26:1 (D).\n primary: \"bg-primary/20 text-primary-ink\",\n success: \"bg-success/15 text-success-ink\",\n warning: \"bg-warning/20 text-warning-ink\",\n destructive: \"bg-destructive/15 text-destructive-ink\",\n info: \"bg-info/15 text-info-ink\",\n // Icon app đứng trên tint của CHÍNH nó → phải là bậc ink, đúng như năm tone trên.\n // `text-app-accent` (gold #E8B824) trên `bg-app-accent/10` (trộn ra #FDF8E9) chỉ 1,75:1 ✗.\n // `text-foreground` như bản trước thì ĐỌC được (17,81:1) nhưng bỏ mất màu app — tức là bỏ\n // mất đúng thứ tone này sinh ra để nói. `text-app-accent-ink`: 6,14:1 (L) · 9,61:1 (D) ✓\n // — hợp đồng §5, cùng lối chữa với AppLauncher.\n app: \"bg-app-accent/10 text-app-accent-ink\"\n }\n },\n defaultVariants: {\n tone: \"default\"\n }\n});\n\nexport type StatCardTone = NonNullable<VariantProps<typeof statCardIconVariants>[\"tone\"]>;\n\nexport type StatCardDeltaDirection = \"up\" | \"down\" | \"flat\";\n\nexport interface StatCardDelta {\n /** Nội dung hiển thị, vd \"+12,5%\" hoặc \"3 việc\" */\n value: ReactNode;\n direction: StatCardDeltaDirection;\n /**\n * Tăng là TỐT hay XẤU — mặc định \"positive-up\" (tăng = tốt).\n *\n * Cần thiết vì có chỉ số ngược chiều: số ticket quá hạn tăng là xấu, tỷ lệ rời bỏ giảm là tốt.\n * Tô màu theo hướng mũi tên một cách máy móc sẽ nói dối người đọc.\n */\n polarity?: \"positive-up\" | \"positive-down\" | \"neutral\";\n /** Chú thích sau con số, vd \"so với tuần trước\" */\n label?: ReactNode;\n}\n\nexport interface StatCardProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n /** Nhãn chỉ số, vd \"Việc đang mở\" */\n label: ReactNode;\n value: ReactNode;\n icon?: IconComponent;\n tone?: StatCardTone;\n delta?: StatCardDelta;\n /** Slot biểu đồ mini (sparkline) — package không ép thư viện biểu đồ nào */\n sparkline?: ReactNode;\n /** Dòng chú thích dưới cùng */\n footer?: ReactNode;\n /** Đang tải → khung xương thay cho số liệu */\n loading?: boolean;\n}\n\nconst deltaIcons: Record<StatCardDeltaDirection, IconComponent> = {\n up: ArrowUpRight,\n down: ArrowDownRight,\n flat: Minus\n};\n\n/**\n * Màu của delta = hướng × cực tính, không phải chỉ hướng.\n *\n * Dòng biến động là CHỮ (12px, `text-xs`) đứng trên `bg-card` → ngưỡng AA 4,5:1, và phải đạt ở\n * cả hai theme. Token tô nền không làm được việc đó: `text-success` chỉ 3,13:1 ở light còn\n * `text-destructive` chỉ 3,53:1 ở dark — mỗi theme hỏng một nửa. Bậc ink đạt cả hai:\n * success 6,52:1 (L) / 10,19:1 (D) · destructive 7,15:1 (L) / 6,42:1 (D) · muted-foreground\n * 5,23:1 (L) / 5,35:1 (D).\n */\nfunction deltaToneClass({ direction, polarity = \"positive-up\" }: StatCardDelta): string {\n if (direction === \"flat\" || polarity === \"neutral\") return \"text-muted-foreground\";\n const isGood = polarity === \"positive-up\" ? direction === \"up\" : direction === \"down\";\n return isGood ? \"text-success-ink\" : \"text-destructive-ink\";\n}\n\n/**\n * Thẻ KPI: nhãn + số liệu lớn + biến động + sparkline tuỳ chọn.\n *\n * Số liệu dùng `tabular-nums` để cột số không nhảy khi cập nhật realtime. Tĩnh → RSC-safe.\n */\nexport function StatCard({\n label,\n value,\n icon: Icon,\n tone = \"default\",\n delta,\n sparkline,\n footer,\n loading = false,\n className,\n ...props\n}: StatCardProps) {\n const DeltaIcon = delta ? deltaIcons[delta.direction] : undefined;\n\n return (\n <div\n data-slot=\"stat-card\"\n className={cn(\"rounded-xl border border-border bg-card p-4 shadow-card\", className)}\n {...props}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm text-muted-foreground\">{label}</p>\n {loading ? (\n <Skeleton className=\"mt-2 h-7 w-24\" />\n ) : (\n <p className=\"mt-1 text-2xl font-semibold tabular-nums leading-tight text-foreground\">{value}</p>\n )}\n {delta && !loading && DeltaIcon && (\n <p className={cn(\"mt-1 flex items-center gap-1 text-xs font-medium\", deltaToneClass(delta))}>\n <DeltaIcon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n <span className=\"tabular-nums\">{delta.value}</span>\n {delta.label && <span className=\"font-normal text-muted-foreground\">{delta.label}</span>}\n </p>\n )}\n </div>\n {Icon && (\n <span aria-hidden=\"true\" className={statCardIconVariants({ tone })}>\n <Icon className=\"size-4.5\" />\n </span>\n )}\n </div>\n\n {sparkline && <div className=\"mt-3 h-10\">{sparkline}</div>}\n {footer && <div className=\"mt-3 border-t border-border pt-2 text-xs text-muted-foreground\">{footer}</div>}\n </div>\n );\n}\n"],"mappings":"AAmIU,cAOE,YAPF;AAnIV,SAAS,WAA8B;AACvC,SAAS,gBAAgB,cAAc,aAAa;AAEpD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AAmBlB,MAAM,uBAAuB,IAAI,+DAA+D;AAAA,EACrG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMN,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAoCD,MAAM,aAA4D;AAAA,EAChE,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AACR;AAWA,SAAS,eAAe,EAAE,WAAW,WAAW,cAAc,GAA0B;AACtF,MAAI,cAAc,UAAU,aAAa,UAAW,QAAO;AAC3D,QAAM,SAAS,aAAa,gBAAgB,cAAc,OAAO,cAAc;AAC/E,SAAO,SAAS,qBAAqB;AACvC;AAOO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,YAAY,QAAQ,WAAW,MAAM,SAAS,IAAI;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,2DAA2D,SAAS;AAAA,MACjF,GAAG;AAAA,MAEJ;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,+BAAC,SAAI,WAAU,kBACb;AAAA,gCAAC,OAAE,WAAU,0CAA0C,iBAAM;AAAA,YAC5D,UACC,oBAAC,YAAS,WAAU,iBAAgB,IAEpC,oBAAC,OAAE,WAAU,0EAA0E,iBAAM;AAAA,YAE9F,SAAS,CAAC,WAAW,aACpB,qBAAC,OAAE,WAAW,GAAG,oDAAoD,eAAe,KAAK,CAAC,GACxF;AAAA,kCAAC,aAAU,eAAY,QAAO,WAAU,qBAAoB;AAAA,cAC5D,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,OAAM;AAAA,cAC3C,MAAM,SAAS,oBAAC,UAAK,WAAU,qCAAqC,gBAAM,OAAM;AAAA,eACnF;AAAA,aAEJ;AAAA,UACC,QACC,oBAAC,UAAK,eAAY,QAAO,WAAW,qBAAqB,EAAE,KAAK,CAAC,GAC/D,8BAAC,QAAK,WAAU,YAAW,GAC7B;AAAA,WAEJ;AAAA,QAEC,aAAa,oBAAC,SAAI,WAAU,aAAa,qBAAU;AAAA,QACnD,UAAU,oBAAC,SAAI,WAAU,kEAAkE,kBAAO;AAAA;AAAA;AAAA,EACrG;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/composite/stat-card.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport { ArrowDownRight, ArrowUpRight, Minus } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Skeleton } from \"../ui/skeleton.js\";\nimport type { IconComponent } from \"./empty-state.js\";\n\n/**\n * Ô icon của StatCard, tô theo tone token (không hex).\n *\n * Icon là thành phần PHI VĂN BẢN → ngưỡng WCAG 1.4.11 là 3:1 so với nền tint mà nó đứng lên.\n * Màu icon vì thế phải là bậc `--x-ink`, không phải token tô nền `--x`: `text-success` trên\n * `bg-success/15` chỉ được **2,69:1 ✗** ở light (dark thì 7,44:1 — mỗi theme hỏng một nửa là\n * dấu hiệu dùng sai vai trò token). Đo lại sau khi đổi, trên tint đã trộn với `--card`:\n *\n * success 5,60:1 (L) · 7,44:1 (D) · info 5,54:1 (L) · 7,12:1 (D)\n * warning 5,93:1 (L) · 7,19:1 (D) · destructive 5,76:1 (L) · 5,71:1 (D)\n * default 4,89:1 (L) · 4,86:1 (D) · primary 5,77:1 (L) · 7,26:1 (D)\n * app 6,14:1 (L) · 9,61:1 (D)\n *\n * `warning` bỏ cặp `text-gold-500 dark:text-warning`: `--warning-ink` đúng bằng `--gold-500` ở\n * light và `--yellow` ở dark, nên một class là đủ.\n */\n/*\n * Hình học của ô icon lấy theo mẫu StatCards ở mục **Patterns** của comitor-ds:\n * `p-1.5` quanh icon 14px + `rounded-lg` — tức ô ~28px, KHÔNG phải 36px với `rounded-md`.\n */\nexport const statCardIconVariants = cva(\"flex size-7 shrink-0 items-center justify-center rounded-lg\", {\n variants: {\n tone: {\n default: \"bg-muted text-muted-foreground\",\n // Đi theo `--primary`, không phải thang `gold-*` chôn cứng. Bản cũ\n // `bg-gold-100 text-gold-600 dark:bg-gold-500/25 dark:text-gold-100` đạt tương phản\n // (7,77:1 L · 11,85:1 D) nhưng đóng băng màu: app đổi brand thì ô icon vẫn vàng, và phải\n // viết hai lớp `dark:` vì thang gold không có bản dark. Token đạt 5,77:1 (L) · 7,26:1 (D).\n primary: \"bg-primary/20 text-primary-ink\",\n success: \"bg-success/15 text-success-ink\",\n warning: \"bg-warning/20 text-warning-ink\",\n destructive: \"bg-destructive/15 text-destructive-ink\",\n info: \"bg-info/15 text-info-ink\",\n // Icon app đứng trên tint của CHÍNH nó → phải là bậc ink, đúng như năm tone trên.\n // `text-app-accent` (gold #E8B824) trên `bg-app-accent/10` (trộn ra #FDF8E9) chỉ 1,75:1 ✗.\n // `text-foreground` như bản trước thì ĐỌC được (17,81:1) nhưng bỏ mất màu app — tức là bỏ\n // mất đúng thứ tone này sinh ra để nói. `text-app-accent-ink`: 6,14:1 (L) · 9,61:1 (D) ✓\n // — hợp đồng §5, cùng lối chữa với AppLauncher.\n app: \"bg-app-accent/10 text-app-accent-ink\"\n }\n },\n defaultVariants: {\n tone: \"default\"\n }\n});\n\nexport type StatCardTone = NonNullable<VariantProps<typeof statCardIconVariants>[\"tone\"]>;\n\nexport type StatCardDeltaDirection = \"up\" | \"down\" | \"flat\";\n\nexport interface StatCardDelta {\n /** Nội dung hiển thị, vd \"+12,5%\" hoặc \"3 việc\" */\n value: ReactNode;\n direction: StatCardDeltaDirection;\n /**\n * Tăng là TỐT hay XẤU — mặc định \"positive-up\" (tăng = tốt).\n *\n * Cần thiết vì có chỉ số ngược chiều: số ticket quá hạn tăng là xấu, tỷ lệ rời bỏ giảm là tốt.\n * Tô màu theo hướng mũi tên một cách máy móc sẽ nói dối người đọc.\n */\n polarity?: \"positive-up\" | \"positive-down\" | \"neutral\";\n /** Chú thích sau con số, vd \"so với tuần trước\" */\n label?: ReactNode;\n}\n\nexport interface StatCardProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n /** Nhãn chỉ số, vd \"Việc đang mở\" */\n label: ReactNode;\n value: ReactNode;\n icon?: IconComponent;\n tone?: StatCardTone;\n delta?: StatCardDelta;\n /** Slot biểu đồ mini (sparkline) — package không ép thư viện biểu đồ nào */\n sparkline?: ReactNode;\n /** Dòng chú thích dưới cùng */\n footer?: ReactNode;\n /** Đang tải → khung xương thay cho số liệu */\n loading?: boolean;\n}\n\nconst deltaIcons: Record<StatCardDeltaDirection, IconComponent> = {\n up: ArrowUpRight,\n down: ArrowDownRight,\n flat: Minus\n};\n\n/**\n * Màu của delta = hướng × cực tính, không phải chỉ hướng.\n *\n * Dòng biến động là CHỮ (12px, `text-xs`) đứng trên `bg-card` → ngưỡng AA 4,5:1, và phải đạt ở\n * cả hai theme. Token tô nền không làm được việc đó: `text-success` chỉ 3,13:1 ở light còn\n * `text-destructive` chỉ 3,53:1 ở dark — mỗi theme hỏng một nửa. Bậc ink đạt cả hai:\n * success 6,52:1 (L) / 10,19:1 (D) · destructive 7,15:1 (L) / 6,42:1 (D) · muted-foreground\n * 5,23:1 (L) / 5,35:1 (D).\n */\nfunction deltaToneClass({ direction, polarity = \"positive-up\" }: StatCardDelta): string {\n if (direction === \"flat\" || polarity === \"neutral\") return \"text-muted-foreground\";\n const isGood = polarity === \"positive-up\" ? direction === \"up\" : direction === \"down\";\n return isGood ? \"text-success-ink\" : \"text-destructive-ink\";\n}\n\n/**\n * Thẻ KPI: nhãn + số liệu lớn + biến động + sparkline tuỳ chọn.\n *\n * Số liệu dùng `tabular-nums` để cột số không nhảy khi cập nhật realtime. Tĩnh → RSC-safe.\n */\nexport function StatCard({\n label,\n value,\n icon: Icon,\n tone = \"default\",\n delta,\n sparkline,\n footer,\n loading = false,\n className,\n ...props\n}: StatCardProps) {\n const DeltaIcon = delta ? deltaIcons[delta.direction] : undefined;\n\n return (\n <div\n data-slot=\"stat-card\"\n /*\n * `p-5`, và KHÔNG có đổ bóng — đúng mẫu StatCards ở mục Patterns của comitor-ds\n * (`rounded-xl border border-border bg-card p-5`). Bản trước dùng `p-4 shadow-card`; trên\n * một hàng bốn thẻ thì bóng đó nổi rõ và làm lưới thẻ nặng hơn bản duyệt.\n */\n className={cn(\"rounded-xl border border-border bg-card p-5\", className)}\n {...props}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0 flex-1\">\n {/* `text-xs font-medium` theo bản duyệt, không phải `text-sm` thường. */}\n <p className=\"truncate text-xs font-medium text-muted-foreground\">{label}</p>\n {loading ? (\n <Skeleton className=\"mt-2 h-7 w-24\" />\n ) : (\n /* `font-bold` theo bản duyệt. `tabular-nums` là khoản gói thêm và giữ lại: chữ số\n đều bề ngang thì một hàng bốn thẻ không nhảy khi số đổi. */\n <p className=\"mt-1 text-2xl font-bold tabular-nums leading-tight text-foreground\">{value}</p>\n )}\n {delta && !loading && DeltaIcon && (\n <p className={cn(\"mt-1 flex items-center gap-1 text-xs font-medium\", deltaToneClass(delta))}>\n <DeltaIcon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n <span className=\"tabular-nums\">{delta.value}</span>\n {delta.label && <span className=\"font-normal text-muted-foreground\">{delta.label}</span>}\n </p>\n )}\n </div>\n {Icon && (\n <span aria-hidden=\"true\" className={statCardIconVariants({ tone })}>\n <Icon className=\"size-3.5\" />\n </span>\n )}\n </div>\n\n {sparkline && <div className=\"mt-3 h-10\">{sparkline}</div>}\n {footer && <div className=\"mt-3 border-t border-border pt-2 text-xs text-muted-foreground\">{footer}</div>}\n </div>\n );\n}\n"],"mappings":"AA6IU,cASE,YATF;AA7IV,SAAS,WAA8B;AACvC,SAAS,gBAAgB,cAAc,aAAa;AAEpD,SAAS,UAAU;AACnB,SAAS,gBAAgB;AAuBlB,MAAM,uBAAuB,IAAI,+DAA+D;AAAA,EACrG,UAAU;AAAA,IACR,MAAM;AAAA,MACJ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMN,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EACA,iBAAiB;AAAA,IACf,MAAM;AAAA,EACR;AACF,CAAC;AAoCD,MAAM,aAA4D;AAAA,EAChE,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AACR;AAWA,SAAS,eAAe,EAAE,WAAW,WAAW,cAAc,GAA0B;AACtF,MAAI,cAAc,UAAU,aAAa,UAAW,QAAO;AAC3D,QAAM,SAAS,aAAa,gBAAgB,cAAc,OAAO,cAAc;AAC/E,SAAO,SAAS,qBAAqB;AACvC;AAOO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,YAAY,QAAQ,WAAW,MAAM,SAAS,IAAI;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MAMV,WAAW,GAAG,+CAA+C,SAAS;AAAA,MACrE,GAAG;AAAA,MAEJ;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,+BAAC,SAAI,WAAU,kBAEb;AAAA,gCAAC,OAAE,WAAU,sDAAsD,iBAAM;AAAA,YACxE,UACC,oBAAC,YAAS,WAAU,iBAAgB;AAAA;AAAA;AAAA,cAIpC,oBAAC,OAAE,WAAU,sEAAsE,iBAAM;AAAA;AAAA,YAE1F,SAAS,CAAC,WAAW,aACpB,qBAAC,OAAE,WAAW,GAAG,oDAAoD,eAAe,KAAK,CAAC,GACxF;AAAA,kCAAC,aAAU,eAAY,QAAO,WAAU,qBAAoB;AAAA,cAC5D,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,OAAM;AAAA,cAC3C,MAAM,SAAS,oBAAC,UAAK,WAAU,qCAAqC,gBAAM,OAAM;AAAA,eACnF;AAAA,aAEJ;AAAA,UACC,QACC,oBAAC,UAAK,eAAY,QAAO,WAAW,qBAAqB,EAAE,KAAK,CAAC,GAC/D,8BAAC,QAAK,WAAU,YAAW,GAC7B;AAAA,WAEJ;AAAA,QAEC,aAAa,oBAAC,SAAI,WAAU,aAAa,qBAAU;AAAA,QACnD,UAAU,oBAAC,SAAI,WAAU,kEAAkE,kBAAO;AAAA;AAAA;AAAA,EACrG;AAEJ;","names":[]}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bảng lớp của hai biến thể Tabs — module THUẦN, cố ý KHÔNG có `"use client"`.
3
+ *
4
+ * Vì sao tách khỏi `tabs.tsx`: hai nơi dùng chung nó — `Tabs` (Radix, có state, client) và
5
+ * `RouteTabs` (điều hướng bằng link, không state). Để bảng ở file client thì `RouteTabs` phải
6
+ * chép lại chuỗi lớp, và hai bản chép sẽ lệch nhau ngay lần đầu có người sửa một bên.
7
+ *
8
+ * Ranh giới RSC cắt theo FILE, nên hằng dữ liệu thuần phải sống ở module không directive —
9
+ * xem CHANGELOG 0.2.2 §2 và cổng `verify:directives`.
10
+ */
11
+ /**
12
+ * - `default`: gạch chân, vạch active màu brand — tab CẤP TRANG.
13
+ * - `pills`: viên bo tròn trên nền muted — tab cấp panel/thẻ, và tab điều hướng của trang cài đặt.
14
+ */
15
+ type TabsVariant = "default" | "pills";
16
+ declare const tabsListVariants: Record<TabsVariant, string>;
17
+ declare const tabsTriggerVariants: Record<TabsVariant, string>;
18
+ /** Vòng focus + trạng thái vô hiệu, chung cho cả hai biến thể và cả hai kiểu tab. */
19
+ declare const TABS_TRIGGER_BASE = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50";
20
+
21
+ export { TABS_TRIGGER_BASE, type TabsVariant, tabsListVariants, tabsTriggerVariants };
@@ -0,0 +1,15 @@
1
+ const tabsListVariants = {
2
+ default: "border-b border-border",
3
+ pills: "bg-muted p-1 rounded-lg"
4
+ };
5
+ const tabsTriggerVariants = {
6
+ default: "px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors duration-150 border-transparent text-muted-foreground hover:text-foreground data-[state=active]:border-primary-ink data-[state=active]:text-foreground",
7
+ pills: "px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
8
+ };
9
+ const TABS_TRIGGER_BASE = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50";
10
+ export {
11
+ TABS_TRIGGER_BASE,
12
+ tabsListVariants,
13
+ tabsTriggerVariants
14
+ };
15
+ //# sourceMappingURL=tabs-variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/ui/tabs-variants.ts"],"sourcesContent":["/**\n * Bảng lớp của hai biến thể Tabs — module THUẦN, cố ý KHÔNG có `\"use client\"`.\n *\n * Vì sao tách khỏi `tabs.tsx`: hai nơi dùng chung nó — `Tabs` (Radix, có state, client) và\n * `RouteTabs` (điều hướng bằng link, không state). Để bảng ở file client thì `RouteTabs` phải\n * chép lại chuỗi lớp, và hai bản chép sẽ lệch nhau ngay lần đầu có người sửa một bên.\n *\n * Ranh giới RSC cắt theo FILE, nên hằng dữ liệu thuần phải sống ở module không directive —\n * xem CHANGELOG 0.2.2 §2 và cổng `verify:directives`.\n */\n\n/**\n * - `default`: gạch chân, vạch active màu brand — tab CẤP TRANG.\n * - `pills`: viên bo tròn trên nền muted — tab cấp panel/thẻ, và tab điều hướng của trang cài đặt.\n */\nexport type TabsVariant = \"default\" | \"pills\";\n\nexport const tabsListVariants: Record<TabsVariant, string> = {\n default: \"border-b border-border\",\n pills: \"bg-muted p-1 rounded-lg\"\n};\n\n/*\n * Vạch active của biến thể `default` là `border-primary-ink`, KHÔNG phải `border-gold-300`:\n * vạch 2px này là thứ DUY NHẤT mang màu để nói \"tab nào đang mở\" (chữ chỉ đổi từ\n * `muted-foreground` sang `foreground`), nên nó là thông tin thị giác. Đi qua token nên app đổi\n * brand là vạch đổi theo, thay vì đóng đinh vào một bậc của thang gold.\n */\nexport const tabsTriggerVariants: Record<TabsVariant, string> = {\n default:\n \"px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors duration-150 border-transparent text-muted-foreground hover:text-foreground data-[state=active]:border-primary-ink data-[state=active]:text-foreground\",\n pills:\n \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\"\n};\n\n/** Vòng focus + trạng thái vô hiệu, chung cho cả hai biến thể và cả hai kiểu tab. */\nexport const TABS_TRIGGER_BASE =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\";\n"],"mappings":"AAiBO,MAAM,mBAAgD;AAAA,EAC3D,SAAS;AAAA,EACT,OAAO;AACT;AAQO,MAAM,sBAAmD;AAAA,EAC9D,SACE;AAAA,EACF,OACE;AACJ;AAGO,MAAM,oBACX;","names":[]}
@@ -1,15 +1,7 @@
1
1
  import * as TabsPrimitive from '@radix-ui/react-tabs';
2
2
  import * as React from 'react';
3
+ import { TabsVariant } from './tabs-variants.js';
3
4
 
4
- /**
5
- * Tabs (Radix) — 2 biến thể thị giác:
6
- * - `default`: gạch chân, thanh active màu gold (dùng cho tab cấp trang).
7
- * - `pills`: viên bo tròn trên nền muted (dùng cho tab cấp panel/thẻ).
8
- *
9
- * `variant` phải truyền cho CẢ `TabsList` lẫn từng `TabsTrigger` — hai cấp không
10
- * chia sẻ context, đây là thiết kế của comitor-ds, giữ nguyên để tương thích.
11
- */
12
- type TabsVariant = "default" | "pills";
13
5
  type TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>;
14
6
  type TabsListProps = React.ComponentProps<typeof TabsPrimitive.List> & {
15
7
  variant?: TabsVariant;
@@ -32,4 +24,4 @@ declare const Tabs: typeof TabsRoot & {
32
24
  Content: typeof TabsContent;
33
25
  };
34
26
 
35
- export { Tabs, TabsContent, type TabsContentProps, TabsList, type TabsListProps, type TabsProps, TabsTrigger, type TabsTriggerProps, type TabsVariant };
27
+ export { Tabs, TabsContent, type TabsContentProps, TabsList, type TabsListProps, type TabsProps, TabsTrigger, type TabsTriggerProps, TabsVariant };
@@ -2,37 +2,26 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as TabsPrimitive from "@radix-ui/react-tabs";
4
4
  import { cn } from "../../lib/cn.js";
5
+ import { TABS_TRIGGER_BASE, tabsListVariants, tabsTriggerVariants } from "./tabs-variants.js";
5
6
  function TabsRoot({ className, ...props }) {
6
7
  return /* @__PURE__ */ jsx(TabsPrimitive.Root, { "data-slot": "tabs", className: cn("flex flex-col gap-2", className), ...props });
7
8
  }
8
- const listVariants = {
9
- default: "border-b border-border",
10
- pills: "bg-muted p-1 rounded-lg"
11
- };
12
9
  function TabsList({ variant = "default", className, ...props }) {
13
10
  return /* @__PURE__ */ jsx(
14
11
  TabsPrimitive.List,
15
12
  {
16
13
  "data-slot": "tabs-list",
17
- className: cn("flex gap-0", listVariants[variant], className),
14
+ className: cn("flex gap-0", tabsListVariants[variant], className),
18
15
  ...props
19
16
  }
20
17
  );
21
18
  }
22
- const triggerVariants = {
23
- default: "px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors duration-150 border-transparent text-muted-foreground hover:text-foreground data-[state=active]:border-primary-ink data-[state=active]:text-foreground",
24
- pills: "px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
25
- };
26
19
  function TabsTrigger({ variant = "default", className, ...props }) {
27
20
  return /* @__PURE__ */ jsx(
28
21
  TabsPrimitive.Trigger,
29
22
  {
30
23
  "data-slot": "tabs-trigger",
31
- className: cn(
32
- triggerVariants[variant],
33
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
34
- className
35
- ),
24
+ className: cn(tabsTriggerVariants[variant], TABS_TRIGGER_BASE, className),
36
25
  ...props
37
26
  }
38
27
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/ui/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\n\n/**\n * Tabs (Radix) — 2 biến thể thị giác:\n * - `default`: gạch chân, thanh active màu gold (dùng cho tab cấp trang).\n * - `pills`: viên bo tròn trên nền muted (dùng cho tab cấp panel/thẻ).\n *\n * `variant` phải truyền cho CẢ `TabsList` lẫn từng `TabsTrigger` — hai cấp không\n * chia sẻ context, đây là thiết kế của comitor-ds, giữ nguyên để tương thích.\n */\n\nexport type TabsVariant = \"default\" | \"pills\";\n\nexport type TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>;\nexport type TabsListProps = React.ComponentProps<typeof TabsPrimitive.List> & { variant?: TabsVariant };\nexport type TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Trigger> & { variant?: TabsVariant };\nexport type TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Content>;\n\nfunction TabsRoot({ className, ...props }: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" className={cn(\"flex flex-col gap-2\", className)} {...props} />;\n}\n\nconst listVariants: Record<TabsVariant, string> = {\n default: \"border-b border-border\",\n pills: \"bg-muted p-1 rounded-lg\"\n};\n\nfunction TabsList({ variant = \"default\", className, ...props }: TabsListProps) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n className={cn(\"flex gap-0\", listVariants[variant], className)}\n {...props}\n />\n );\n}\n\n/*\n * Vạch active của biến thể `default` là `border-primary-ink`, KHÔNG phải `border-gold-300`:\n * vạch 2px này là thứ DUY NHẤT mang màu để nói \"tab nào đang mở\" (chữ chỉ đổi từ\n * `muted-foreground` sang `foreground`), nên nó là thông tin thị giác — ngưỡng 3:1.\n * gold-300 #E8B824 trên nền trắng chỉ **1,86:1** ✗ (ở dark thì 10,19:1, nên lỗi chỉ lộ ở light).\n * `--primary-ink`: 6,51:1 (light) · 12,67:1 (dark) ✓ — và đi qua token nên app đổi brand là\n * vạch đổi theo, thay vì đóng đinh vào một bậc của thang gold.\n */\nconst triggerVariants: Record<TabsVariant, string> = {\n default:\n \"px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors duration-150 border-transparent text-muted-foreground hover:text-foreground data-[state=active]:border-primary-ink data-[state=active]:text-foreground\",\n pills:\n \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\"\n};\n\nfunction TabsTrigger({ variant = \"default\", className, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n triggerVariants[variant],\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\",\n className\n )}\n {...props}\n />\n );\n}\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Content data-slot=\"tabs-content\" className={cn(\"mt-4 flex-1 outline-none\", className)} {...props} />\n );\n}\n\n/**\n * `Tabs` mở cả API rời (Tabs + TabsList/TabsTrigger/TabsContent) lẫn API gộp\n * (Tabs.List / Tabs.Trigger / Tabs.Content) — giữ đúng như comitor-ds.\n */\nconst Tabs = Object.assign(TabsRoot, {\n List: TabsList,\n Trigger: TabsTrigger,\n Content: TabsContent\n});\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger };\n"],"mappings":";AAuBS;AArBT,YAAY,mBAAmB;AAE/B,SAAS,UAAU;AAkBnB,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAc;AACpD,SAAO,oBAAC,cAAc,MAAd,EAAmB,aAAU,QAAO,WAAW,GAAG,uBAAuB,SAAS,GAAI,GAAG,OAAO;AAC1G;AAEA,MAAM,eAA4C;AAAA,EAChD,SAAS;AAAA,EACT,OAAO;AACT;AAEA,SAAS,SAAS,EAAE,UAAU,WAAW,WAAW,GAAG,MAAM,GAAkB;AAC7E,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,aAAa,OAAO,GAAG,SAAS;AAAA,MAC3D,GAAG;AAAA;AAAA,EACN;AAEJ;AAUA,MAAM,kBAA+C;AAAA,EACnD,SACE;AAAA,EACF,OACE;AACJ;AAEA,SAAS,YAAY,EAAE,UAAU,WAAW,WAAW,GAAG,MAAM,GAAqB;AACnF,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT,gBAAgB,OAAO;AAAA,QACvB;AAAA,QACA;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE,oBAAC,cAAc,SAAd,EAAsB,aAAU,gBAAe,WAAW,GAAG,4BAA4B,SAAS,GAAI,GAAG,OAAO;AAErH;AAMA,MAAM,OAAO,OAAO,OAAO,UAAU;AAAA,EACnC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AACX,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/components/ui/tabs.tsx"],"sourcesContent":["\"use client\";\n\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { TABS_TRIGGER_BASE, type TabsVariant, tabsListVariants, tabsTriggerVariants } from \"./tabs-variants.js\";\n\n/**\n * Tabs (Radix) — 2 biến thể thị giác:\n * - `default`: gạch chân, thanh active màu gold (dùng cho tab cấp trang).\n * - `pills`: viên bo tròn trên nền muted (dùng cho tab cấp panel/thẻ).\n *\n * `variant` phải truyền cho CẢ `TabsList` lẫn từng `TabsTrigger` — hai cấp không\n * chia sẻ context, đây là thiết kế của comitor-ds, giữ nguyên để tương thích.\n *\n * ⚠ Đây là tab CÓ STATE (Radix giữ tab nào đang mở, nội dung nằm trong `TabsContent` cùng trang).\n * Tab mà mỗi mục là một ROUTE riêng thì dùng `RouteTabs` — cùng bảng lớp, nhưng là `<nav>` chứa\n * link thật, không phải `role=\"tablist\"`.\n */\n\nexport type { TabsVariant };\n\nexport type TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>;\nexport type TabsListProps = React.ComponentProps<typeof TabsPrimitive.List> & { variant?: TabsVariant };\nexport type TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Trigger> & { variant?: TabsVariant };\nexport type TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Content>;\n\nfunction TabsRoot({ className, ...props }: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" className={cn(\"flex flex-col gap-2\", className)} {...props} />;\n}\n\nfunction TabsList({ variant = \"default\", className, ...props }: TabsListProps) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n className={cn(\"flex gap-0\", tabsListVariants[variant], className)}\n {...props}\n />\n );\n}\n\nfunction TabsTrigger({ variant = \"default\", className, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(tabsTriggerVariants[variant], TABS_TRIGGER_BASE, className)}\n {...props}\n />\n );\n}\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Content data-slot=\"tabs-content\" className={cn(\"mt-4 flex-1 outline-none\", className)} {...props} />\n );\n}\n\n/**\n * `Tabs` mở cả API rời (Tabs + TabsList/TabsTrigger/TabsContent) lẫn API gộp\n * (Tabs.List / Tabs.Trigger / Tabs.Content) — giữ đúng như comitor-ds.\n */\nconst Tabs = Object.assign(TabsRoot, {\n List: TabsList,\n Trigger: TabsTrigger,\n Content: TabsContent\n});\n\nexport { Tabs, TabsContent, TabsList, TabsTrigger };\n"],"mappings":";AA4BS;AA1BT,YAAY,mBAAmB;AAE/B,SAAS,UAAU;AACnB,SAAS,mBAAqC,kBAAkB,2BAA2B;AAsB3F,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAc;AACpD,SAAO,oBAAC,cAAc,MAAd,EAAmB,aAAU,QAAO,WAAW,GAAG,uBAAuB,SAAS,GAAI,GAAG,OAAO;AAC1G;AAEA,SAAS,SAAS,EAAE,UAAU,WAAW,WAAW,GAAG,MAAM,GAAkB;AAC7E,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,cAAc,iBAAiB,OAAO,GAAG,SAAS;AAAA,MAC/D,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,YAAY,EAAE,UAAU,WAAW,WAAW,GAAG,MAAM,GAAqB;AACnF,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,oBAAoB,OAAO,GAAG,mBAAmB,SAAS;AAAA,MACvE,GAAG;AAAA;AAAA,EACN;AAEJ;AAEA,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAqB;AAC9D,SACE,oBAAC,cAAc,SAAd,EAAsB,aAAU,gBAAe,WAAW,GAAG,4BAA4B,SAAS,GAAI,GAAG,OAAO;AAErH;AAMA,MAAM,OAAO,OAAO,OAAO,UAAU;AAAA,EACnC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AACX,CAAC;","names":[]}
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export { LetterAvatar, LetterAvatarProps, LetterAvatarSize, getAvatarToneClasses
18
18
  export { PageContainer, PageContainerProps, PageContainerWidth } from './components/composite/page-container.js';
19
19
  export { PageHeader, PageHeaderBreadcrumbItem, PageHeaderLinkProps, PageHeaderProps } from './components/composite/page-header.js';
20
20
  export { RelativeTime, RelativeTimeProps } from './components/composite/relative-time.js';
21
+ export { RouteTabItem, RouteTabs, RouteTabsLinkComponent, RouteTabsLinkProps, RouteTabsProps } from './components/composite/route-tabs.js';
21
22
  export { SearchFilterBar, SearchFilterBarProps } from './components/composite/search-filter-bar.js';
22
23
  export { DEFAULT_SEARCH_FILTER_BAR_LABELS, SearchFilterBarLabels } from './components/composite/search-filter-bar-constants.js';
23
24
  export { StatCard, StatCardDelta, StatCardDeltaDirection, StatCardProps, StatCardTone, statCardIconVariants } from './components/composite/stat-card.js';
@@ -71,7 +72,8 @@ export { Slider, SliderProps } from './components/ui/slider.js';
71
72
  export { Spinner, SpinnerProps } from './components/ui/spinner.js';
72
73
  export { Switch, SwitchProps } from './components/ui/switch.js';
73
74
  export { Table, TableBody, TableBodyProps, TableCaption, TableCaptionProps, TableCell, TableCellProps, TableFooter, TableFooterProps, TableHead, TableHeadProps, TableHeader, TableHeaderProps, TableProps, TableRow, TableRowProps } from './components/ui/table.js';
74
- export { Tabs, TabsContent, TabsContentProps, TabsList, TabsListProps, TabsProps, TabsTrigger, TabsTriggerProps, TabsVariant } from './components/ui/tabs.js';
75
+ export { Tabs, TabsContent, TabsContentProps, TabsList, TabsListProps, TabsProps, TabsTrigger, TabsTriggerProps } from './components/ui/tabs.js';
76
+ export { TABS_TRIGGER_BASE, TabsVariant, tabsListVariants, tabsTriggerVariants } from './components/ui/tabs-variants.js';
75
77
  export { Textarea, TextareaProps } from './components/ui/textarea.js';
76
78
  export { Toggle, ToggleProps, ToggleVariantProps, toggleVariants } from './components/ui/toggle.js';
77
79
  export { ToggleGroup, ToggleGroupItem, ToggleGroupItemProps, ToggleGroupProps, ToggleGroupVariantProps } from './components/ui/toggle-group.js';
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { getAvatarToneClasses, LetterAvatar } from "./components/composite/lette
18
18
  export * from "./components/composite/page-container.js";
19
19
  export * from "./components/composite/page-header.js";
20
20
  export * from "./components/composite/relative-time.js";
21
+ export * from "./components/composite/route-tabs.js";
21
22
  export * from "./components/composite/search-filter-bar.js";
22
23
  export * from "./components/composite/search-filter-bar-constants.js";
23
24
  export * from "./components/composite/stat-card.js";
@@ -78,6 +79,7 @@ export * from "./components/ui/spinner.js";
78
79
  export * from "./components/ui/switch.js";
79
80
  export * from "./components/ui/table.js";
80
81
  export * from "./components/ui/tabs.js";
82
+ export * from "./components/ui/tabs-variants.js";
81
83
  export * from "./components/ui/textarea.js";
82
84
  export * from "./components/ui/toggle.js";
83
85
  export * from "./components/ui/toggle-group.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Entry chính \"@comitor/ui\" — Tier 1 (primitive) + Tier 2 (composite) + lib + token.\n *\n * Quy tắc của entry này: **chỉ được phụ thuộc peer BẮT BUỘC** (react, react-dom,\n * lucide-react, tailwindcss). Không `next/*`, không `next-themes`, không `recharts`,\n * không `react-hook-form` — đó đều là peer OPTIONAL. Vì `tsup` chạy `bundle: false`\n * (ESM per-file), chỉ cần barrel này re-export một file có import optional peer là\n * mọi app import \"@comitor/ui\" đều buộc phải cài gói đó, nếu không bundler gãy ở\n * bước resolve. Vì vậy 3 nhánh dưới đây nằm ở entry riêng:\n *\n * - Tier 3 shell (next/*, next-themes) → \"@comitor/ui/shell\"\n * - chart (recharts) → \"@comitor/ui/chart\"\n * - binding react-hook-form → \"@comitor/ui/form\"\n *\n * ⚠ Hai file trong `components/ui/` CỐ Ý không có mặt ở đây:\n * - `form.tsx` → xuất qua \"@comitor/ui/form\" (react-hook-form).\n * - `sonner.tsx` → xuất qua \"@comitor/ui/shell\" dưới tên `SonnerToaster`\n * (nó gọi `useTheme()` của next-themes). Bản dùng hằng ngày là composite\n * `Toaster`/`toast` ở Tier 2 — không phụ thuộc next-themes, có sẵn ở entry này.\n *\n * ⚠ Mọi import nội bộ phải giữ đuôi `.js`: output là ESM per-file, Node ESM thuần\n * không tự suy ra phần mở rộng (bundler thì tha, Node thì không).\n */\n\n/*\n * ⚠ Năm file `*-constants` bên dưới KHÔNG phải chỗ để gom code cho gọn — chúng là RANH GIỚI\n * SERVER/CLIENT. Component chủ (`data-table.tsx`, `date-picker.tsx`, `form-field.tsx`,\n * `search-filter-bar.tsx`, `table-pagination.tsx`) đều mở đầu bằng `\"use client\"`, mà mọi export\n * của một module client đến tay Server Component dưới dạng client-reference proxy: đọc ra\n * `undefined`, KHÔNG throw, KHÔNG warning, trong khi TypeScript vẫn khai kiểu thật. Nhãn mặc\n * định / preset / sentinel là hằng dữ liệu thuần nên phải nằm ngoài ranh giới đó, và barrel này\n * phải xuất chúng THẲNG — re-export vòng qua file client là vô hiệu.\n * Cùng lý do với `chart/chart-colors.ts`; chi tiết ghi ở đầu file đó.\n * `scripts/verify-directives.mjs` canh không cho hằng thuần lọt ngược vào file client.\n */\n\n// ─── Tier 2 — composite (pattern lặp lại ở mọi app Comitor) ───\nexport * from \"./components/composite/avatar-group.js\";\nexport * from \"./components/composite/combobox.js\";\nexport * from \"./components/composite/command-palette.js\";\nexport * from \"./components/composite/confirm-dialog.js\";\nexport * from \"./components/composite/copy-button.js\";\nexport * from \"./components/composite/data-table.js\";\nexport * from \"./components/composite/data-table-constants.js\";\nexport * from \"./components/composite/date-picker.js\";\nexport * from \"./components/composite/date-picker-constants.js\";\nexport * from \"./components/composite/empty-state.js\";\nexport * from \"./components/composite/filter-chips.js\";\nexport * from \"./components/composite/form-field.js\";\nexport * from \"./components/composite/form-field-constants.js\";\nexport * from \"./components/composite/form-section.js\";\nexport * from \"./components/composite/inline-edit.js\";\nexport * from \"./components/composite/keyboard-hint.js\";\nexport type { LetterAvatarProps, LetterAvatarSize } from \"./components/composite/letter-avatar.js\";\n// Xuất CHỌN LỌC (không `export *`): file này còn `AVATAR_TONES` nhập từ `lib/avatar-tone.js`\n// và các helper nội bộ khác. Liệt kê tường minh thì thêm helper riêng tư vào file KHÔNG tự động\n// biến nó thành API công khai — hàng rào mà `export *` không dựng được.\nexport { getAvatarToneClasses, LetterAvatar } from \"./components/composite/letter-avatar.js\";\nexport * from \"./components/composite/page-container.js\";\nexport * from \"./components/composite/page-header.js\";\nexport * from \"./components/composite/relative-time.js\";\nexport * from \"./components/composite/search-filter-bar.js\";\nexport * from \"./components/composite/search-filter-bar-constants.js\";\nexport * from \"./components/composite/stat-card.js\";\nexport * from \"./components/composite/status-config.js\";\nexport * from \"./components/composite/status-pill.js\";\nexport * from \"./components/composite/table-pagination.js\";\nexport * from \"./components/composite/table-pagination-constants.js\";\n// Xuất CHỌN LỌC vì cùng lý do với `letter-avatar` ở trên: `text-utils.ts` là chỗ dễ bị thêm\n// helper nội bộ nhất của Tier 2 (0.1.0 từng rò `hashString`/`pickByString` ra API công khai\n// đúng theo đường này). Năm hàm dưới đây là hợp đồng, phần còn lại của file thì không.\nexport {\n getVietnameseInitials,\n matchesSearch,\n normalizeVi,\n removeVietnameseTones,\n truncate\n} from \"./components/composite/text-utils.js\";\nexport * from \"./components/composite/toast.js\";\n// ─── Tier 1 — primitive (shadcn/ui \"new-york\", đã custom theo brand Comitor) ───\nexport * from \"./components/ui/accordion.js\";\nexport * from \"./components/ui/alert.js\";\nexport * from \"./components/ui/alert-dialog.js\";\nexport * from \"./components/ui/aspect-ratio.js\";\nexport * from \"./components/ui/avatar.js\";\nexport * from \"./components/ui/badge.js\";\nexport * from \"./components/ui/breadcrumb.js\";\nexport * from \"./components/ui/button.js\";\nexport * from \"./components/ui/button-group.js\";\nexport * from \"./components/ui/calendar.js\";\nexport * from \"./components/ui/card.js\";\nexport * from \"./components/ui/carousel.js\";\nexport * from \"./components/ui/checkbox.js\";\nexport * from \"./components/ui/collapsible.js\";\nexport * from \"./components/ui/command.js\";\nexport * from \"./components/ui/context-menu.js\";\nexport * from \"./components/ui/dialog.js\";\nexport * from \"./components/ui/drawer.js\";\nexport * from \"./components/ui/dropdown-menu.js\";\nexport * from \"./components/ui/empty.js\";\nexport * from \"./components/ui/field.js\";\nexport * from \"./components/ui/hover-card.js\";\nexport * from \"./components/ui/input.js\";\nexport * from \"./components/ui/input-group.js\";\nexport * from \"./components/ui/input-otp.js\";\nexport * from \"./components/ui/item.js\";\nexport * from \"./components/ui/kbd.js\";\nexport * from \"./components/ui/label.js\";\nexport * from \"./components/ui/menubar.js\";\nexport * from \"./components/ui/navigation-menu.js\";\nexport * from \"./components/ui/pagination.js\";\nexport * from \"./components/ui/popover.js\";\nexport * from \"./components/ui/progress.js\";\nexport * from \"./components/ui/radio-group.js\";\nexport * from \"./components/ui/resizable.js\";\nexport * from \"./components/ui/scroll-area.js\";\nexport * from \"./components/ui/select.js\";\nexport * from \"./components/ui/separator.js\";\nexport * from \"./components/ui/sheet.js\";\nexport * from \"./components/ui/skeleton.js\";\nexport * from \"./components/ui/slider.js\";\nexport * from \"./components/ui/spinner.js\";\nexport * from \"./components/ui/switch.js\";\nexport * from \"./components/ui/table.js\";\nexport * from \"./components/ui/tabs.js\";\nexport * from \"./components/ui/textarea.js\";\nexport * from \"./components/ui/toggle.js\";\nexport * from \"./components/ui/toggle-group.js\";\nexport * from \"./components/ui/tooltip.js\";\n\n// ─── lib + token ───\nexport * from \"./lib/cn.js\";\nexport * from \"./tokens.js\";\n"],"mappings":"AAqCA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,sBAAsB,oBAAoB;AACnD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Entry chính \"@comitor/ui\" — Tier 1 (primitive) + Tier 2 (composite) + lib + token.\n *\n * Quy tắc của entry này: **chỉ được phụ thuộc peer BẮT BUỘC** (react, react-dom,\n * lucide-react, tailwindcss). Không `next/*`, không `next-themes`, không `recharts`,\n * không `react-hook-form` — đó đều là peer OPTIONAL. Vì `tsup` chạy `bundle: false`\n * (ESM per-file), chỉ cần barrel này re-export một file có import optional peer là\n * mọi app import \"@comitor/ui\" đều buộc phải cài gói đó, nếu không bundler gãy ở\n * bước resolve. Vì vậy 3 nhánh dưới đây nằm ở entry riêng:\n *\n * - Tier 3 shell (next/*, next-themes) → \"@comitor/ui/shell\"\n * - chart (recharts) → \"@comitor/ui/chart\"\n * - binding react-hook-form → \"@comitor/ui/form\"\n *\n * ⚠ Hai file trong `components/ui/` CỐ Ý không có mặt ở đây:\n * - `form.tsx` → xuất qua \"@comitor/ui/form\" (react-hook-form).\n * - `sonner.tsx` → xuất qua \"@comitor/ui/shell\" dưới tên `SonnerToaster`\n * (nó gọi `useTheme()` của next-themes). Bản dùng hằng ngày là composite\n * `Toaster`/`toast` ở Tier 2 — không phụ thuộc next-themes, có sẵn ở entry này.\n *\n * ⚠ Mọi import nội bộ phải giữ đuôi `.js`: output là ESM per-file, Node ESM thuần\n * không tự suy ra phần mở rộng (bundler thì tha, Node thì không).\n */\n\n/*\n * ⚠ Năm file `*-constants` bên dưới KHÔNG phải chỗ để gom code cho gọn — chúng là RANH GIỚI\n * SERVER/CLIENT. Component chủ (`data-table.tsx`, `date-picker.tsx`, `form-field.tsx`,\n * `search-filter-bar.tsx`, `table-pagination.tsx`) đều mở đầu bằng `\"use client\"`, mà mọi export\n * của một module client đến tay Server Component dưới dạng client-reference proxy: đọc ra\n * `undefined`, KHÔNG throw, KHÔNG warning, trong khi TypeScript vẫn khai kiểu thật. Nhãn mặc\n * định / preset / sentinel là hằng dữ liệu thuần nên phải nằm ngoài ranh giới đó, và barrel này\n * phải xuất chúng THẲNG — re-export vòng qua file client là vô hiệu.\n * Cùng lý do với `chart/chart-colors.ts`; chi tiết ghi ở đầu file đó.\n * `scripts/verify-directives.mjs` canh không cho hằng thuần lọt ngược vào file client.\n */\n\n// ─── Tier 2 — composite (pattern lặp lại ở mọi app Comitor) ───\nexport * from \"./components/composite/avatar-group.js\";\nexport * from \"./components/composite/combobox.js\";\nexport * from \"./components/composite/command-palette.js\";\nexport * from \"./components/composite/confirm-dialog.js\";\nexport * from \"./components/composite/copy-button.js\";\nexport * from \"./components/composite/data-table.js\";\nexport * from \"./components/composite/data-table-constants.js\";\nexport * from \"./components/composite/date-picker.js\";\nexport * from \"./components/composite/date-picker-constants.js\";\nexport * from \"./components/composite/empty-state.js\";\nexport * from \"./components/composite/filter-chips.js\";\nexport * from \"./components/composite/form-field.js\";\nexport * from \"./components/composite/form-field-constants.js\";\nexport * from \"./components/composite/form-section.js\";\nexport * from \"./components/composite/inline-edit.js\";\nexport * from \"./components/composite/keyboard-hint.js\";\nexport type { LetterAvatarProps, LetterAvatarSize } from \"./components/composite/letter-avatar.js\";\n// Xuất CHỌN LỌC (không `export *`): file này còn `AVATAR_TONES` nhập từ `lib/avatar-tone.js`\n// và các helper nội bộ khác. Liệt kê tường minh thì thêm helper riêng tư vào file KHÔNG tự động\n// biến nó thành API công khai — hàng rào mà `export *` không dựng được.\nexport { getAvatarToneClasses, LetterAvatar } from \"./components/composite/letter-avatar.js\";\nexport * from \"./components/composite/page-container.js\";\nexport * from \"./components/composite/page-header.js\";\nexport * from \"./components/composite/relative-time.js\";\nexport * from \"./components/composite/route-tabs.js\";\nexport * from \"./components/composite/search-filter-bar.js\";\nexport * from \"./components/composite/search-filter-bar-constants.js\";\nexport * from \"./components/composite/stat-card.js\";\nexport * from \"./components/composite/status-config.js\";\nexport * from \"./components/composite/status-pill.js\";\nexport * from \"./components/composite/table-pagination.js\";\nexport * from \"./components/composite/table-pagination-constants.js\";\n// Xuất CHỌN LỌC vì cùng lý do với `letter-avatar` ở trên: `text-utils.ts` là chỗ dễ bị thêm\n// helper nội bộ nhất của Tier 2 (0.1.0 từng rò `hashString`/`pickByString` ra API công khai\n// đúng theo đường này). Năm hàm dưới đây là hợp đồng, phần còn lại của file thì không.\nexport {\n getVietnameseInitials,\n matchesSearch,\n normalizeVi,\n removeVietnameseTones,\n truncate\n} from \"./components/composite/text-utils.js\";\nexport * from \"./components/composite/toast.js\";\n// ─── Tier 1 — primitive (shadcn/ui \"new-york\", đã custom theo brand Comitor) ───\nexport * from \"./components/ui/accordion.js\";\nexport * from \"./components/ui/alert.js\";\nexport * from \"./components/ui/alert-dialog.js\";\nexport * from \"./components/ui/aspect-ratio.js\";\nexport * from \"./components/ui/avatar.js\";\nexport * from \"./components/ui/badge.js\";\nexport * from \"./components/ui/breadcrumb.js\";\nexport * from \"./components/ui/button.js\";\nexport * from \"./components/ui/button-group.js\";\nexport * from \"./components/ui/calendar.js\";\nexport * from \"./components/ui/card.js\";\nexport * from \"./components/ui/carousel.js\";\nexport * from \"./components/ui/checkbox.js\";\nexport * from \"./components/ui/collapsible.js\";\nexport * from \"./components/ui/command.js\";\nexport * from \"./components/ui/context-menu.js\";\nexport * from \"./components/ui/dialog.js\";\nexport * from \"./components/ui/drawer.js\";\nexport * from \"./components/ui/dropdown-menu.js\";\nexport * from \"./components/ui/empty.js\";\nexport * from \"./components/ui/field.js\";\nexport * from \"./components/ui/hover-card.js\";\nexport * from \"./components/ui/input.js\";\nexport * from \"./components/ui/input-group.js\";\nexport * from \"./components/ui/input-otp.js\";\nexport * from \"./components/ui/item.js\";\nexport * from \"./components/ui/kbd.js\";\nexport * from \"./components/ui/label.js\";\nexport * from \"./components/ui/menubar.js\";\nexport * from \"./components/ui/navigation-menu.js\";\nexport * from \"./components/ui/pagination.js\";\nexport * from \"./components/ui/popover.js\";\nexport * from \"./components/ui/progress.js\";\nexport * from \"./components/ui/radio-group.js\";\nexport * from \"./components/ui/resizable.js\";\nexport * from \"./components/ui/scroll-area.js\";\nexport * from \"./components/ui/select.js\";\nexport * from \"./components/ui/separator.js\";\nexport * from \"./components/ui/sheet.js\";\nexport * from \"./components/ui/skeleton.js\";\nexport * from \"./components/ui/slider.js\";\nexport * from \"./components/ui/spinner.js\";\nexport * from \"./components/ui/switch.js\";\nexport * from \"./components/ui/table.js\";\nexport * from \"./components/ui/tabs.js\";\n/* Bảng lớp của Tabs xuất THẲNG từ module thuần — `RouteTabs` và `Tabs` dùng chung nó, và hằng\n dữ liệu đi vòng qua file \"use client\" sẽ thành proxy ở Server Component (CHANGELOG 0.2.2 §2). */\nexport * from \"./components/ui/tabs-variants.js\";\nexport * from \"./components/ui/textarea.js\";\nexport * from \"./components/ui/toggle.js\";\nexport * from \"./components/ui/toggle-group.js\";\nexport * from \"./components/ui/tooltip.js\";\n\n// ─── lib + token ───\nexport * from \"./lib/cn.js\";\nexport * from \"./tokens.js\";\n"],"mappings":"AAqCA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,sBAAsB,oBAAoB;AACnD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;","names":[]}
@@ -87,7 +87,15 @@ function AppHeader({
87
87
  // `rounded-full` + `w-64`: đúng ô tìm của AppHeader ở mục Patterns
88
88
  // (`<Input className="rounded-full w-64" />`). Pill cần nhiều đệm ngang hơn ô
89
89
  // vuông góc nên `px-3` thay vì `px-2.5`.
90
- "hidden h-8 w-64 items-center gap-2 rounded-full border border-input bg-muted/40 px-3 sm:flex",
90
+ //
91
+ // ⚠ `lg:` chứ KHÔNG phải `sm:`. Ô tìm nằm ở vùng GIỮA và `shrink-0`, nên nó
92
+ // ăn 256px cố định của vùng nội dung. Dải nguy hiểm là md→lg: từ 768px thanh
93
+ // bên đã hiện (chiếm 256px) mà vùng nội dung mới 512px — trừ ô tìm thì hai
94
+ // bên chỉ còn 104px mỗi bên, không đủ cho cụm nút (~190px). Đo thật ở 768px
95
+ // trước khi vá: ba nút header tụt còn 16px bề ngang.
96
+ // Từ 1024px vùng nội dung là 768px → mỗi bên 232px, vừa đủ. Dưới ngưỡng đó
97
+ // dùng nút icon (36px) ngay bên dưới; phím ⌘K vẫn mở bảng lệnh như thường.
98
+ "hidden h-8 w-64 items-center gap-2 rounded-full border border-input bg-muted/40 px-3 lg:flex",
91
99
  "text-sm text-muted-foreground transition-colors duration-150 ease-out",
92
100
  // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`
93
101
  // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là
@@ -117,14 +125,14 @@ function AppHeader({
117
125
  onClick: () => setCommandPaletteOpen(true),
118
126
  "aria-label": labels.search,
119
127
  className: cn(
120
- "flex size-9 items-center justify-center rounded-md text-muted-foreground sm:hidden",
128
+ "flex size-9 items-center justify-center rounded-md text-muted-foreground lg:hidden",
121
129
  "transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground"
122
130
  ),
123
131
  children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
124
132
  }
125
133
  )
126
134
  ] }) : null }),
127
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-end gap-1", children: [
135
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-1 items-center justify-end gap-1", children: [
128
136
  extraSlot,
129
137
  showAppLauncher ? /* @__PURE__ */ jsx(AppLauncher, {}) : null,
130
138
  notificationsSlot ?? (notificationCount === void 0 ? null : /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/app-header.tsx"],"sourcesContent":["\"use client\";\n\nimport { Menu, PanelLeft, Search } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n// `useIsMac` nhập thẳng từ Tier 2: 0.2.0 chỉ còn MỘT cửa xuất cho nó (entry `.`),\n// `/shell` không re-export nữa.\nimport { useIsMac } from \"../components/composite/keyboard-hint.js\";\nimport { Kbd } from \"../components/ui/kbd.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { NotificationsButton } from \"./notifications-button.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\n\nexport interface AppHeaderProps {\n className?: string;\n /** Tiêu đề trang — dùng khi không truyền `breadcrumb`. */\n title?: ReactNode;\n /** Slot breadcrumb (composite `PageHeader` của app) — ưu tiên hơn `title`. */\n breadcrumb?: ReactNode;\n /** Slot ngoài cùng bên trái (vd `<ComitorLockup size=\"xs\" variant=\"auto\" />`). */\n logo?: ReactNode;\n\n /** Thay ô tìm kiếm dựng sẵn bằng UI riêng của app. */\n searchSlot?: ReactNode;\n showSearch?: boolean;\n /** Nút thu gọn sidebar ở header (layout sidebar-first thường để ở sidebar). */\n showSidebarToggle?: boolean;\n\n /** Slot thông báo (app tự render Popover + panel). Có slot thì `notificationCount` bị bỏ qua. */\n notificationsSlot?: ReactNode;\n notificationCount?: number;\n onNotificationsClick?: () => void;\n\n /** Slot phụ bên phải, đứng trước AppLauncher. */\n extraSlot?: ReactNode;\n\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n showUserMenu?: boolean;\n /** Chuyển tiếp props cho `UserMenu` (profileHref, settingsHref, items…). */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Header cao 56px (`--header-height`), nền `bg-background/80 backdrop-blur`.\n * Comitor là hệ sáng — **không** dùng header luôn-tối.\n *\n * BA VÙNG, và ô tìm nằm ở vùng GIỮA — đúng bố cục AppHeader ở mục Patterns của comitor-ds:\n *\n * trái hamburger (mobile) / nút thu gọn sidebar + logo + breadcrumb hoặc tiêu đề\n * giữa ô tìm (Cmd/Ctrl+K), bo tròn hết cỡ\n * phải AppLauncher · thông báo · ThemeToggle · UserMenu\n *\n * Hai vùng ngoài đều `flex-1`, vùng giữa `shrink-0` — đó là điều kiện để vùng giữa nằm CHÍNH GIỮA\n * header bất kể breadcrumb bên trái dài ngắn thế nào. Nếu để ô tìm trong cụm phải (bản trước) thì\n * nó trôi theo số lượng nút bên phải; nếu căn giữa bằng `absolute left-1/2` thì nó đè lên\n * breadcrumb dài. Cách này không mắc cả hai.\n */\nexport function AppHeader({\n className,\n title,\n breadcrumb,\n logo,\n searchSlot,\n showSearch = true,\n showSidebarToggle = false,\n notificationsSlot,\n notificationCount,\n onNotificationsClick,\n extraSlot,\n showAppLauncher = true,\n showThemeToggle = true,\n showUserMenu = true,\n userMenuProps\n}: AppHeaderProps) {\n const { setMobileMenuOpen, setCommandPaletteOpen, toggleSidebar, sidebarCollapsed, labels, user } = useShell();\n const isMac = useIsMac();\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n return (\n <header\n className={cn(\n \"sticky top-0 z-40 flex h-header shrink-0 items-center gap-2 px-3 sm:px-4\",\n \"border-b border-border bg-background/80 backdrop-blur-sm\",\n className\n )}\n >\n {/* ── VÙNG TRÁI ─────────────────────────────────────────────────────── */}\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {/* Mở menu off-canvas — chỉ màn hình hẹp */}\n <button\n type=\"button\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label={labels.openMenu}\n className={cn(\n \"-ml-1 flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Menu className=\"size-5\" />\n </button>\n\n {showSidebarToggle ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n className={cn(\n \"hidden size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:flex\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <PanelLeft className=\"size-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · {modKey} + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n\n {logo ? <div className=\"shrink-0\">{logo}</div> : null}\n\n {/* Breadcrumb thắng title khi có cả hai */}\n <div className=\"flex min-w-0 items-center\">\n {breadcrumb ?? (title ? <h1 className=\"truncate text-sm font-semibold text-foreground\">{title}</h1> : null)}\n </div>\n </div>\n\n {/* ── VÙNG GIỮA: ô tìm ──────────────────────────────────────────────── */}\n <div className=\"flex shrink-0 items-center justify-center\">\n {showSearch\n ? (searchSlot ?? (\n <>\n {/* ≥sm: ô giả lập trường tìm kiếm, kèm gợi ý phím tắt */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n className={cn(\n // `rounded-full` + `w-64`: đúng ô tìm của AppHeader ở mục Patterns\n // (`<Input className=\"rounded-full w-64\" />`). Pill cần nhiều đệm ngang hơn ô\n // vuông góc nên `px-3` thay vì `px-2.5`.\n \"hidden h-8 w-64 items-center gap-2 rounded-full border border-input bg-muted/40 px-3 sm:flex\",\n \"text-sm text-muted-foreground transition-colors duration-150 ease-out\",\n // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`\n // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là\n // nền mà `hover:bg-muted` đổi sang) ở light — ô tìm gần như MẤT VIỀN đúng lúc rê\n // chuột vào, dưới xa mức 3:1 WCAG 1.4.11 đòi cho viền control. `gold-300` lại là\n // bậc thang màu thô, không đổi theo theme, nên ở dark cùng giá trị đó thành\n // 10,19:1 — mỗi theme một hành vi.\n // `--control-edge-strong` là bậc viền control lúc hover, đo đúng cho vai trò này:\n // 10,55:1 / 9,86:1 (light, trên `--background` / `--muted`) · 11,65:1 / 9,93:1\n // (dark). Lúc nghỉ vẫn `border-input` = 3,83:1 (light) · 4,94:1 (dark) ✓.\n \"hover:border-control-edge-strong hover:bg-muted\"\n )}\n >\n <Search className=\"size-3.5 shrink-0\" />\n <span className=\"flex-1 truncate text-left\">{labels.searchPlaceholder}</span>\n <Kbd>{modKey}K</Kbd>\n </button>\n\n {/* <sm: chỉ icon */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n aria-label={labels.search}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground sm:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Search className=\"size-4\" />\n </button>\n </>\n ))\n : null}\n </div>\n\n {/* ── VÙNG PHẢI ─────────────────────────────────────────────────────── */}\n <div className=\"flex min-w-0 flex-1 items-center justify-end gap-1\">\n {extraSlot}\n\n {showAppLauncher ? <AppLauncher /> : null}\n\n {notificationsSlot ??\n (notificationCount === undefined ? null : (\n <NotificationsButton\n count={notificationCount}\n label={labels.notifications}\n onClick={onNotificationsClick}\n />\n ))}\n\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n\n {showUserMenu && user ? (\n <>\n <span className=\"mx-1 hidden h-5 w-px bg-border sm:block\" aria-hidden=\"true\" />\n <UserMenu {...userMenuProps} />\n </>\n ) : null}\n </div>\n </header>\n );\n}\n"],"mappings":";AAsGU,SAoCI,UApCJ,KAkBE,YAlBF;AApGV,SAAS,MAAM,WAAW,cAAc;AAIxC,SAAS,gBAAgB;AACzB,SAAS,WAAW;AACpB,SAAS,SAAS,gBAAgB,sBAAsB;AACxD,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AACpC,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,gBAAoC;AA+CtC,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf;AACF,GAAmB;AACjB,QAAM,EAAE,mBAAmB,uBAAuB,eAAe,kBAAkB,QAAQ,KAAK,IAAI,SAAS;AAC7G,QAAM,QAAQ,SAAS;AACvB,QAAM,SAAS,QAAQ,WAAM;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAGA;AAAA,6BAAC,SAAI,WAAU,0CAEb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,kBAAkB,IAAI;AAAA,cACrC,cAAY,OAAO;AAAA,cACnB,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,UAC3B;AAAA,UAEC,oBACC,qBAAC,WACC;AAAA,gCAAC,kBAAe,SAAO,MACrB;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS;AAAA,gBACT,cAAY,mBAAmB,OAAO,gBAAgB,OAAO;AAAA,gBAC7D,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,gBACF;AAAA,gBAEA,8BAAC,aAAU,WAAU,UAAS;AAAA;AAAA,YAChC,GACF;AAAA,YACA,qBAAC,kBAAe,MAAK,UAClB;AAAA,iCAAmB,OAAO,gBAAgB,OAAO;AAAA,cAAgB;AAAA,cAAI;AAAA,cAAO;AAAA,eAC/E;AAAA,aACF,IACE;AAAA,UAEH,OAAO,oBAAC,SAAI,WAAU,YAAY,gBAAK,IAAS;AAAA,UAGjD,oBAAC,SAAI,WAAU,6BACZ,yBAAe,QAAQ,oBAAC,QAAG,WAAU,kDAAkD,iBAAM,IAAQ,OACxG;AAAA,WACF;AAAA,QAGA,oBAAC,SAAI,WAAU,6CACZ,uBACI,cACC,iCAEE;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,sBAAsB,IAAI;AAAA,cACzC,WAAW;AAAA;AAAA;AAAA;AAAA,gBAIT;AAAA,gBACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAUA;AAAA,cACF;AAAA,cAEA;AAAA,oCAAC,UAAO,WAAU,qBAAoB;AAAA,gBACtC,oBAAC,UAAK,WAAU,6BAA6B,iBAAO,mBAAkB;AAAA,gBACtE,qBAAC,OAAK;AAAA;AAAA,kBAAO;AAAA,mBAAC;AAAA;AAAA;AAAA,UAChB;AAAA,UAGA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,sBAAsB,IAAI;AAAA,cACzC,cAAY,OAAO;AAAA,cACnB,WAAW;AAAA,gBACT;AAAA,gBACA;AAAA,cACF;AAAA,cAEA,8BAAC,UAAO,WAAU,UAAS;AAAA;AAAA,UAC7B;AAAA,WACF,IAEF,MACN;AAAA,QAGA,qBAAC,SAAI,WAAU,sDACZ;AAAA;AAAA,UAEA,kBAAkB,oBAAC,eAAY,IAAK;AAAA,UAEpC,sBACE,sBAAsB,SAAY,OACjC;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,OAAO,OAAO;AAAA,cACd,SAAS;AAAA;AAAA,UACX;AAAA,UAGH,kBAAkB,oBAAC,eAAY,QAAgB,IAAK;AAAA,UAEpD,gBAAgB,OACf,iCACE;AAAA,gCAAC,UAAK,WAAU,2CAA0C,eAAY,QAAO;AAAA,YAC7E,oBAAC,YAAU,GAAG,eAAe;AAAA,aAC/B,IACE;AAAA,WACN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/app-header.tsx"],"sourcesContent":["\"use client\";\n\nimport { Menu, PanelLeft, Search } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n// `useIsMac` nhập thẳng từ Tier 2: 0.2.0 chỉ còn MỘT cửa xuất cho nó (entry `.`),\n// `/shell` không re-export nữa.\nimport { useIsMac } from \"../components/composite/keyboard-hint.js\";\nimport { Kbd } from \"../components/ui/kbd.js\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../components/ui/tooltip.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { AppLauncher } from \"./app-launcher.js\";\nimport { NotificationsButton } from \"./notifications-button.js\";\nimport { useShell } from \"./shell-context.js\";\nimport { ThemeToggle } from \"./theme-toggle.js\";\nimport { UserMenu, type UserMenuProps } from \"./user-menu.js\";\n\nexport interface AppHeaderProps {\n className?: string;\n /** Tiêu đề trang — dùng khi không truyền `breadcrumb`. */\n title?: ReactNode;\n /** Slot breadcrumb (composite `PageHeader` của app) — ưu tiên hơn `title`. */\n breadcrumb?: ReactNode;\n /** Slot ngoài cùng bên trái (vd `<ComitorLockup size=\"xs\" variant=\"auto\" />`). */\n logo?: ReactNode;\n\n /** Thay ô tìm kiếm dựng sẵn bằng UI riêng của app. */\n searchSlot?: ReactNode;\n showSearch?: boolean;\n /** Nút thu gọn sidebar ở header (layout sidebar-first thường để ở sidebar). */\n showSidebarToggle?: boolean;\n\n /** Slot thông báo (app tự render Popover + panel). Có slot thì `notificationCount` bị bỏ qua. */\n notificationsSlot?: ReactNode;\n notificationCount?: number;\n onNotificationsClick?: () => void;\n\n /** Slot phụ bên phải, đứng trước AppLauncher. */\n extraSlot?: ReactNode;\n\n showAppLauncher?: boolean;\n showThemeToggle?: boolean;\n showUserMenu?: boolean;\n /** Chuyển tiếp props cho `UserMenu` (profileHref, settingsHref, items…). */\n userMenuProps?: UserMenuProps;\n}\n\n/**\n * Header cao 56px (`--header-height`), nền `bg-background/80 backdrop-blur`.\n * Comitor là hệ sáng — **không** dùng header luôn-tối.\n *\n * BA VÙNG, và ô tìm nằm ở vùng GIỮA — đúng bố cục AppHeader ở mục Patterns của comitor-ds:\n *\n * trái hamburger (mobile) / nút thu gọn sidebar + logo + breadcrumb hoặc tiêu đề\n * giữa ô tìm (Cmd/Ctrl+K), bo tròn hết cỡ\n * phải AppLauncher · thông báo · ThemeToggle · UserMenu\n *\n * Hai vùng ngoài đều `flex-1`, vùng giữa `shrink-0` — đó là điều kiện để vùng giữa nằm CHÍNH GIỮA\n * header bất kể breadcrumb bên trái dài ngắn thế nào. Nếu để ô tìm trong cụm phải (bản trước) thì\n * nó trôi theo số lượng nút bên phải; nếu căn giữa bằng `absolute left-1/2` thì nó đè lên\n * breadcrumb dài. Cách này không mắc cả hai.\n */\nexport function AppHeader({\n className,\n title,\n breadcrumb,\n logo,\n searchSlot,\n showSearch = true,\n showSidebarToggle = false,\n notificationsSlot,\n notificationCount,\n onNotificationsClick,\n extraSlot,\n showAppLauncher = true,\n showThemeToggle = true,\n showUserMenu = true,\n userMenuProps\n}: AppHeaderProps) {\n const { setMobileMenuOpen, setCommandPaletteOpen, toggleSidebar, sidebarCollapsed, labels, user } = useShell();\n const isMac = useIsMac();\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n return (\n <header\n className={cn(\n \"sticky top-0 z-40 flex h-header shrink-0 items-center gap-2 px-3 sm:px-4\",\n \"border-b border-border bg-background/80 backdrop-blur-sm\",\n className\n )}\n >\n {/* ── VÙNG TRÁI ─────────────────────────────────────────────────────── */}\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {/* Mở menu off-canvas — chỉ màn hình hẹp */}\n <button\n type=\"button\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label={labels.openMenu}\n className={cn(\n \"-ml-1 flex size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Menu className=\"size-5\" />\n </button>\n\n {showSidebarToggle ? (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n onClick={toggleSidebar}\n aria-label={sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar}\n className={cn(\n \"hidden size-9 shrink-0 items-center justify-center rounded-md text-muted-foreground md:flex\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <PanelLeft className=\"size-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n {sidebarCollapsed ? labels.expandSidebar : labels.collapseSidebar} · {modKey} + B\n </TooltipContent>\n </Tooltip>\n ) : null}\n\n {logo ? <div className=\"shrink-0\">{logo}</div> : null}\n\n {/* Breadcrumb thắng title khi có cả hai */}\n <div className=\"flex min-w-0 items-center\">\n {breadcrumb ?? (title ? <h1 className=\"truncate text-sm font-semibold text-foreground\">{title}</h1> : null)}\n </div>\n </div>\n\n {/* ── VÙNG GIỮA: ô tìm ──────────────────────────────────────────────── */}\n <div className=\"flex shrink-0 items-center justify-center\">\n {showSearch\n ? (searchSlot ?? (\n <>\n {/* ≥sm: ô giả lập trường tìm kiếm, kèm gợi ý phím tắt */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n className={cn(\n // `rounded-full` + `w-64`: đúng ô tìm của AppHeader ở mục Patterns\n // (`<Input className=\"rounded-full w-64\" />`). Pill cần nhiều đệm ngang hơn ô\n // vuông góc nên `px-3` thay vì `px-2.5`.\n //\n // ⚠ `lg:` chứ KHÔNG phải `sm:`. Ô tìm nằm ở vùng GIỮA và `shrink-0`, nên nó\n // ăn 256px cố định của vùng nội dung. Dải nguy hiểm là md→lg: từ 768px thanh\n // bên đã hiện (chiếm 256px) mà vùng nội dung mới 512px — trừ ô tìm thì hai\n // bên chỉ còn 104px mỗi bên, không đủ cho cụm nút (~190px). Đo thật ở 768px\n // trước khi vá: ba nút header tụt còn 16px bề ngang.\n // Từ 1024px vùng nội dung là 768px → mỗi bên 232px, vừa đủ. Dưới ngưỡng đó\n // dùng nút icon (36px) ngay bên dưới; phím ⌘K vẫn mở bảng lệnh như thường.\n \"hidden h-8 w-64 items-center gap-2 rounded-full border border-input bg-muted/40 px-3 lg:flex\",\n \"text-sm text-muted-foreground transition-colors duration-150 ease-out\",\n // Viền control lúc hover phải ĐẬM HƠN lúc nghỉ, không nhạt đi. `border-gold-300`\n // (#E8B824) chỉ được 1,86:1 với `--background` và 1,73:1 với `--muted` (chính là\n // nền mà `hover:bg-muted` đổi sang) ở light — ô tìm gần như MẤT VIỀN đúng lúc rê\n // chuột vào, dưới xa mức 3:1 WCAG 1.4.11 đòi cho viền control. `gold-300` lại là\n // bậc thang màu thô, không đổi theo theme, nên ở dark cùng giá trị đó thành\n // 10,19:1 — mỗi theme một hành vi.\n // `--control-edge-strong` là bậc viền control lúc hover, đo đúng cho vai trò này:\n // 10,55:1 / 9,86:1 (light, trên `--background` / `--muted`) · 11,65:1 / 9,93:1\n // (dark). Lúc nghỉ vẫn `border-input` = 3,83:1 (light) · 4,94:1 (dark) ✓.\n \"hover:border-control-edge-strong hover:bg-muted\"\n )}\n >\n <Search className=\"size-3.5 shrink-0\" />\n <span className=\"flex-1 truncate text-left\">{labels.searchPlaceholder}</span>\n <Kbd>{modKey}K</Kbd>\n </button>\n\n {/* <sm: chỉ icon */}\n <button\n type=\"button\"\n onClick={() => setCommandPaletteOpen(true)}\n aria-label={labels.search}\n className={cn(\n \"flex size-9 items-center justify-center rounded-md text-muted-foreground lg:hidden\",\n \"transition-colors duration-150 ease-out hover:bg-muted hover:text-foreground\"\n )}\n >\n <Search className=\"size-4\" />\n </button>\n </>\n ))\n : null}\n </div>\n\n {/* ── VÙNG PHẢI ─────────────────────────────────────────────────────── */}\n {/* `shrink-0`: cụm nút KHÔNG được co. Thiếu nó thì flexbox bóp chúng thay vì bóp ô tìm —\n ở 768px (thanh bên vừa hiện mà vùng nội dung mới 512px) chúng tụt còn 16px bề ngang. */}\n <div className=\"flex shrink-0 flex-1 items-center justify-end gap-1\">\n {extraSlot}\n\n {showAppLauncher ? <AppLauncher /> : null}\n\n {notificationsSlot ??\n (notificationCount === undefined ? null : (\n <NotificationsButton\n count={notificationCount}\n label={labels.notifications}\n onClick={onNotificationsClick}\n />\n ))}\n\n {showThemeToggle ? <ThemeToggle labels={labels} /> : null}\n\n {showUserMenu && user ? (\n <>\n <span className=\"mx-1 hidden h-5 w-px bg-border sm:block\" aria-hidden=\"true\" />\n <UserMenu {...userMenuProps} />\n </>\n ) : null}\n </div>\n </header>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "0.3.3",
3
+ "version": "0.4.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",