@comitor/ui 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -14
- package/dist/chart/chart-colors.d.ts +56 -0
- package/dist/chart/chart-colors.js +21 -0
- package/dist/chart/chart-colors.js.map +1 -0
- package/dist/chart/chart.d.ts +75 -3
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +33 -42
- package/dist/chart/comitor-chart.js +13 -24
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +3 -2
- package/dist/chart/index.js +1 -0
- package/dist/chart/index.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +9 -2
- package/dist/components/composite/combobox.js +7 -0
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/data-table-constants.d.ts +40 -0
- package/dist/components/composite/data-table-constants.js +13 -0
- package/dist/components/composite/data-table-constants.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +2 -27
- package/dist/components/composite/data-table.js +1 -10
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/date-picker-constants.d.ts +21 -0
- package/dist/components/composite/date-picker-constants.js +39 -0
- package/dist/components/composite/date-picker-constants.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +2 -18
- package/dist/components/composite/date-picker.js +6 -48
- package/dist/components/composite/date-picker.js.map +1 -1
- package/dist/components/composite/form-field-constants.d.ts +26 -0
- package/dist/components/composite/form-field-constants.js +13 -0
- package/dist/components/composite/form-field-constants.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +1 -13
- package/dist/components/composite/form-field.js +1 -11
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/composite/search-filter-bar-constants.d.ts +52 -0
- package/dist/components/composite/search-filter-bar-constants.js +18 -0
- package/dist/components/composite/search-filter-bar-constants.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +3 -37
- package/dist/components/composite/search-filter-bar.js +2 -15
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/table-pagination-constants.d.ts +56 -0
- package/dist/components/composite/table-pagination-constants.js +18 -0
- package/dist/components/composite/table-pagination-constants.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +2 -43
- package/dist/components/composite/table-pagination.js +4 -15
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/card.d.ts +35 -2
- package/dist/components/ui/card.js +17 -3
- package/dist/components/ui/card.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -0
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/shell-context.d.ts +28 -1
- package/dist/shell/shell-context.js +8 -3
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-menu-item.js +3 -3
- package/dist/shell/sidebar-menu-item.js.map +1 -1
- package/dist/shell/utils.d.ts +27 -3
- package/dist/shell/utils.js +52 -9
- package/dist/shell/utils.js.map +1 -1
- package/package.json +7 -5
|
@@ -14,16 +14,8 @@ import {
|
|
|
14
14
|
} from "../ui/dropdown-menu.js";
|
|
15
15
|
import { Skeleton } from "../ui/skeleton.js";
|
|
16
16
|
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
|
|
17
|
+
import { DEFAULT_DATA_TABLE_LABELS } from "./data-table-constants.js";
|
|
17
18
|
import { EmptyState } from "./empty-state.js";
|
|
18
|
-
const DEFAULT_DATA_TABLE_LABELS = {
|
|
19
|
-
selectAllRows: "Ch\u1ECDn t\u1EA5t c\u1EA3 d\xF2ng \u0111ang hi\u1EC3n th\u1ECB",
|
|
20
|
-
deselectAllRows: "B\u1ECF ch\u1ECDn t\u1EA5t c\u1EA3",
|
|
21
|
-
selectRow: "Ch\u1ECDn d\xF2ng",
|
|
22
|
-
deselectRow: "B\u1ECF ch\u1ECDn d\xF2ng",
|
|
23
|
-
emptyTitle: "Ch\u01B0a c\xF3 d\u1EEF li\u1EC7u",
|
|
24
|
-
emptyDescription: "Danh s\xE1ch hi\u1EC7n \u0111ang tr\u1ED1ng.",
|
|
25
|
-
columnToggle: "C\u1ED9t hi\u1EC3n th\u1ECB"
|
|
26
|
-
};
|
|
27
19
|
const alignClasses = {
|
|
28
20
|
left: "text-left",
|
|
29
21
|
center: "text-center",
|
|
@@ -293,7 +285,6 @@ function DataTableColumnToggle({
|
|
|
293
285
|
] });
|
|
294
286
|
}
|
|
295
287
|
export {
|
|
296
|
-
DEFAULT_DATA_TABLE_LABELS,
|
|
297
288
|
DataTable,
|
|
298
289
|
DataTableColumnToggle
|
|
299
290
|
};
|
|
@@ -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 { 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\n/**\n * Toàn bộ chuỗi hiển thị (kể cả chuỗi dành riêng cho trình đọc màn hình) của `DataTable` và\n * `DataTableColumnToggle` — app tiếng Anh (hoặc ngôn ngữ khác) override từng khoá qua prop\n * `labels`. Mặc định là tiếng Việt.\n *\n * Cùng khuôn với `ShellLabels` ở tầng shell: gói dùng chung cho MỌI app Comitor nên chuỗi\n * chôn cứng trong component là ngõ cụt cho app đa ngôn ngữ.\n */\nexport interface DataTableLabels {\n /** aria-label ô chọn ở đầu bảng khi CHƯA chọn hết */\n selectAllRows: string;\n /** aria-label ô chọn ở đầu bảng khi ĐÃ chọn hết */\n deselectAllRows: string;\n /** aria-label ô chọn của một dòng khi dòng chưa được chọn */\n selectRow: string;\n /** aria-label ô chọn của một dòng khi dòng đã được chọn */\n deselectRow: string;\n /** Tiêu đề trạng thái rỗng mặc định (bỏ qua nếu app truyền `empty`) */\n emptyTitle: string;\n /** Mô tả trạng thái rỗng mặc định */\n emptyDescription: string;\n /** Nhãn nút + tiêu đề menu ẩn/hiện cột */\n columnToggle: string;\n}\n\n/** Nhãn tiếng Việt mặc định — mọi khoá đều override được qua prop `labels`. */\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n selectAllRows: \"Chọn tất cả dòng đang hiển thị\",\n deselectAllRows: \"Bỏ chọn tất cả\",\n selectRow: \"Chọn dòng\",\n deselectRow: \"Bỏ chọn dòng\",\n emptyTitle: \"Chưa có dữ liệu\",\n emptyDescription: \"Danh sách hiện đang trống.\",\n columnToggle: \"Cột hiển thị\"\n};\n\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 /** 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":[]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DateRange } from 'react-day-picker';
|
|
2
|
+
|
|
3
|
+
/** Định dạng ngày của Việt Nam — dùng thống nhất ở mọi app Comitor. */
|
|
4
|
+
declare const VI_DATE_FORMAT = "dd/MM/yyyy";
|
|
5
|
+
interface DatePreset {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Trả về ngày (DatePicker) hoặc khoảng ngày (DateRangePicker) tại thời điểm bấm */
|
|
9
|
+
getValue: () => Date;
|
|
10
|
+
}
|
|
11
|
+
interface DateRangePreset {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
getValue: () => DateRange;
|
|
15
|
+
}
|
|
16
|
+
/** Preset mặc định cho ô chọn một ngày. */
|
|
17
|
+
declare const DEFAULT_DATE_PRESETS: DatePreset[];
|
|
18
|
+
/** Preset mặc định cho ô chọn khoảng ngày (báo cáo, bộ lọc danh sách). */
|
|
19
|
+
declare const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[];
|
|
20
|
+
|
|
21
|
+
export { DEFAULT_DATE_PRESETS, DEFAULT_DATE_RANGE_PRESETS, type DatePreset, type DateRangePreset, VI_DATE_FORMAT };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { endOfMonth, startOfDay, startOfMonth, subDays, subMonths } from "date-fns";
|
|
2
|
+
const VI_DATE_FORMAT = "dd/MM/yyyy";
|
|
3
|
+
const DEFAULT_DATE_PRESETS = [
|
|
4
|
+
{ id: "today", label: "H\xF4m nay", getValue: () => startOfDay(/* @__PURE__ */ new Date()) },
|
|
5
|
+
{ id: "yesterday", label: "H\xF4m qua", getValue: () => startOfDay(subDays(/* @__PURE__ */ new Date(), 1)) },
|
|
6
|
+
{ id: "week-ago", label: "7 ng\xE0y tr\u01B0\u1EDBc", getValue: () => startOfDay(subDays(/* @__PURE__ */ new Date(), 7)) }
|
|
7
|
+
];
|
|
8
|
+
const DEFAULT_DATE_RANGE_PRESETS = [
|
|
9
|
+
{ id: "today", label: "H\xF4m nay", getValue: () => ({ from: startOfDay(/* @__PURE__ */ new Date()), to: startOfDay(/* @__PURE__ */ new Date()) }) },
|
|
10
|
+
{
|
|
11
|
+
id: "last-7",
|
|
12
|
+
label: "7 ng\xE0y qua",
|
|
13
|
+
getValue: () => ({ from: startOfDay(subDays(/* @__PURE__ */ new Date(), 6)), to: startOfDay(/* @__PURE__ */ new Date()) })
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "last-30",
|
|
17
|
+
label: "30 ng\xE0y qua",
|
|
18
|
+
getValue: () => ({ from: startOfDay(subDays(/* @__PURE__ */ new Date(), 29)), to: startOfDay(/* @__PURE__ */ new Date()) })
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "this-month",
|
|
22
|
+
label: "Th\xE1ng n\xE0y",
|
|
23
|
+
getValue: () => ({ from: startOfMonth(/* @__PURE__ */ new Date()), to: startOfDay(/* @__PURE__ */ new Date()) })
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "last-month",
|
|
27
|
+
label: "Th\xE1ng tr\u01B0\u1EDBc",
|
|
28
|
+
getValue: () => {
|
|
29
|
+
const previous = subMonths(/* @__PURE__ */ new Date(), 1);
|
|
30
|
+
return { from: startOfMonth(previous), to: endOfMonth(previous) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
export {
|
|
35
|
+
DEFAULT_DATE_PRESETS,
|
|
36
|
+
DEFAULT_DATE_RANGE_PRESETS,
|
|
37
|
+
VI_DATE_FORMAT
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=date-picker-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/date-picker-constants.ts"],"sourcesContent":["/**\n * Định dạng ngày và preset mặc định của `DatePicker` / `DateRangePicker`.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `date-picker.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\nimport { endOfMonth, startOfDay, startOfMonth, subDays, subMonths } from \"date-fns\";\nimport type { DateRange } from \"react-day-picker\";\n\n/** Định dạng ngày của Việt Nam — dùng thống nhất ở mọi app Comitor. */\nexport const VI_DATE_FORMAT = \"dd/MM/yyyy\";\n\nexport interface DatePreset {\n id: string;\n label: string;\n /** Trả về ngày (DatePicker) hoặc khoảng ngày (DateRangePicker) tại thời điểm bấm */\n getValue: () => Date;\n}\n\nexport interface DateRangePreset {\n id: string;\n label: string;\n getValue: () => DateRange;\n}\n\n/** Preset mặc định cho ô chọn một ngày. */\nexport const DEFAULT_DATE_PRESETS: DatePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => startOfDay(new Date()) },\n { id: \"yesterday\", label: \"Hôm qua\", getValue: () => startOfDay(subDays(new Date(), 1)) },\n { id: \"week-ago\", label: \"7 ngày trước\", getValue: () => startOfDay(subDays(new Date(), 7)) }\n];\n\n/** Preset mặc định cho ô chọn khoảng ngày (báo cáo, bộ lọc danh sách). */\nexport const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => ({ from: startOfDay(new Date()), to: startOfDay(new Date()) }) },\n {\n id: \"last-7\",\n label: \"7 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 6)), to: startOfDay(new Date()) })\n },\n {\n id: \"last-30\",\n label: \"30 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 29)), to: startOfDay(new Date()) })\n },\n {\n id: \"this-month\",\n label: \"Tháng này\",\n getValue: () => ({ from: startOfMonth(new Date()), to: startOfDay(new Date()) })\n },\n {\n id: \"last-month\",\n label: \"Tháng trước\",\n getValue: () => {\n const previous = subMonths(new Date(), 1);\n return { from: startOfMonth(previous), to: endOfMonth(previous) };\n }\n }\n];\n"],"mappings":"AAYA,SAAS,YAAY,YAAY,cAAc,SAAS,iBAAiB;AAIlE,MAAM,iBAAiB;AAgBvB,MAAM,uBAAqC;AAAA,EAChD,EAAE,IAAI,SAAS,OAAO,cAAW,UAAU,MAAM,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EACxE,EAAE,IAAI,aAAa,OAAO,cAAW,UAAU,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,CAAC,CAAC,EAAE;AAAA,EACxF,EAAE,IAAI,YAAY,OAAO,6BAAgB,UAAU,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,CAAC,CAAC,EAAE;AAC9F;AAGO,MAAM,6BAAgD;AAAA,EAC3D,EAAE,IAAI,SAAS,OAAO,cAAW,UAAU,OAAO,EAAE,MAAM,WAAW,oBAAI,KAAK,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE,GAAG;AAAA,EAChH;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,OAAO,EAAE,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EAC1F;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,OAAO,EAAE,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,EAAE,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EAC3F;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,OAAO,EAAE,MAAM,aAAa,oBAAI,KAAK,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EAChF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,YAAM,WAAW,UAAU,oBAAI,KAAK,GAAG,CAAC;AACxC,aAAO,EAAE,MAAM,aAAa,QAAQ,GAAG,IAAI,WAAW,QAAQ,EAAE;AAAA,IAClE;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { DateRange } from 'react-day-picker';
|
|
3
3
|
export { DateRange } from 'react-day-picker';
|
|
4
|
+
import { DatePreset, DateRangePreset } from './date-picker-constants.js';
|
|
4
5
|
|
|
5
|
-
/** Định dạng ngày của Việt Nam — dùng thống nhất ở mọi app Comitor. */
|
|
6
|
-
declare const VI_DATE_FORMAT = "dd/MM/yyyy";
|
|
7
|
-
interface DatePreset {
|
|
8
|
-
id: string;
|
|
9
|
-
label: string;
|
|
10
|
-
/** Trả về ngày (DatePicker) hoặc khoảng ngày (DateRangePicker) tại thời điểm bấm */
|
|
11
|
-
getValue: () => Date;
|
|
12
|
-
}
|
|
13
|
-
interface DateRangePreset {
|
|
14
|
-
id: string;
|
|
15
|
-
label: string;
|
|
16
|
-
getValue: () => DateRange;
|
|
17
|
-
}
|
|
18
|
-
/** Preset mặc định cho ô chọn một ngày. */
|
|
19
|
-
declare const DEFAULT_DATE_PRESETS: DatePreset[];
|
|
20
|
-
/** Preset mặc định cho ô chọn khoảng ngày (báo cáo, bộ lọc danh sách). */
|
|
21
|
-
declare const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[];
|
|
22
6
|
interface DatePickerProps {
|
|
23
7
|
value?: Date | null;
|
|
24
8
|
onValueChange: (value: Date | null) => void;
|
|
@@ -74,4 +58,4 @@ interface DateRangePickerProps {
|
|
|
74
58
|
*/
|
|
75
59
|
declare function DateRangePicker({ value, onValueChange, placeholder, disabled, minDate, maxDate, clearable, presets, numberOfMonths, className, id, ...aria }: DateRangePickerProps): React.JSX.Element;
|
|
76
60
|
|
|
77
|
-
export {
|
|
61
|
+
export { DatePicker, type DatePickerProps, DateRangePicker, type DateRangePickerProps };
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
endOfMonth,
|
|
5
|
-
format as formatDate,
|
|
6
|
-
isAfter,
|
|
7
|
-
isBefore,
|
|
8
|
-
isValid,
|
|
9
|
-
parse,
|
|
10
|
-
startOfDay,
|
|
11
|
-
startOfMonth,
|
|
12
|
-
subDays,
|
|
13
|
-
subMonths
|
|
14
|
-
} from "date-fns";
|
|
3
|
+
import { format as formatDate, isAfter, isBefore, isValid, parse, startOfDay } from "date-fns";
|
|
15
4
|
import { vi } from "date-fns/locale";
|
|
16
5
|
import { CalendarDays, X } from "lucide-react";
|
|
17
6
|
import { useEffect, useState } from "react";
|
|
@@ -20,38 +9,10 @@ import { Button } from "../ui/button.js";
|
|
|
20
9
|
import { Calendar } from "../ui/calendar.js";
|
|
21
10
|
import { Input } from "../ui/input.js";
|
|
22
11
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{ id: "week-ago", label: "7 ng\xE0y tr\u01B0\u1EDBc", getValue: () => startOfDay(subDays(/* @__PURE__ */ new Date(), 7)) }
|
|
28
|
-
];
|
|
29
|
-
const DEFAULT_DATE_RANGE_PRESETS = [
|
|
30
|
-
{ id: "today", label: "H\xF4m nay", getValue: () => ({ from: startOfDay(/* @__PURE__ */ new Date()), to: startOfDay(/* @__PURE__ */ new Date()) }) },
|
|
31
|
-
{
|
|
32
|
-
id: "last-7",
|
|
33
|
-
label: "7 ng\xE0y qua",
|
|
34
|
-
getValue: () => ({ from: startOfDay(subDays(/* @__PURE__ */ new Date(), 6)), to: startOfDay(/* @__PURE__ */ new Date()) })
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "last-30",
|
|
38
|
-
label: "30 ng\xE0y qua",
|
|
39
|
-
getValue: () => ({ from: startOfDay(subDays(/* @__PURE__ */ new Date(), 29)), to: startOfDay(/* @__PURE__ */ new Date()) })
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "this-month",
|
|
43
|
-
label: "Th\xE1ng n\xE0y",
|
|
44
|
-
getValue: () => ({ from: startOfMonth(/* @__PURE__ */ new Date()), to: startOfDay(/* @__PURE__ */ new Date()) })
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: "last-month",
|
|
48
|
-
label: "Th\xE1ng tr\u01B0\u1EDBc",
|
|
49
|
-
getValue: () => {
|
|
50
|
-
const previous = subMonths(/* @__PURE__ */ new Date(), 1);
|
|
51
|
-
return { from: startOfMonth(previous), to: endOfMonth(previous) };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
];
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_DATE_RANGE_PRESETS,
|
|
14
|
+
VI_DATE_FORMAT
|
|
15
|
+
} from "./date-picker-constants.js";
|
|
55
16
|
function isOutOfRange(date, minDate, maxDate) {
|
|
56
17
|
if (minDate && isBefore(date, startOfDay(minDate))) return true;
|
|
57
18
|
if (maxDate && isAfter(date, startOfDay(maxDate))) return true;
|
|
@@ -259,10 +220,7 @@ function DateRangePicker({
|
|
|
259
220
|
);
|
|
260
221
|
}
|
|
261
222
|
export {
|
|
262
|
-
DEFAULT_DATE_PRESETS,
|
|
263
|
-
DEFAULT_DATE_RANGE_PRESETS,
|
|
264
223
|
DatePicker,
|
|
265
|
-
DateRangePicker
|
|
266
|
-
VI_DATE_FORMAT
|
|
224
|
+
DateRangePicker
|
|
267
225
|
};
|
|
268
226
|
//# sourceMappingURL=date-picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n endOfMonth,\n format as formatDate,\n isAfter,\n isBefore,\n isValid,\n parse,\n startOfDay,\n startOfMonth,\n subDays,\n subMonths\n} from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\n\nexport type { DateRange };\n\n/** Định dạng ngày của Việt Nam — dùng thống nhất ở mọi app Comitor. */\nexport const VI_DATE_FORMAT = \"dd/MM/yyyy\";\n\nexport interface DatePreset {\n id: string;\n label: string;\n /** Trả về ngày (DatePicker) hoặc khoảng ngày (DateRangePicker) tại thời điểm bấm */\n getValue: () => Date;\n}\n\nexport interface DateRangePreset {\n id: string;\n label: string;\n getValue: () => DateRange;\n}\n\n/** Preset mặc định cho ô chọn một ngày. */\nexport const DEFAULT_DATE_PRESETS: DatePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => startOfDay(new Date()) },\n { id: \"yesterday\", label: \"Hôm qua\", getValue: () => startOfDay(subDays(new Date(), 1)) },\n { id: \"week-ago\", label: \"7 ngày trước\", getValue: () => startOfDay(subDays(new Date(), 7)) }\n];\n\n/** Preset mặc định cho ô chọn khoảng ngày (báo cáo, bộ lọc danh sách). */\nexport const DEFAULT_DATE_RANGE_PRESETS: DateRangePreset[] = [\n { id: \"today\", label: \"Hôm nay\", getValue: () => ({ from: startOfDay(new Date()), to: startOfDay(new Date()) }) },\n {\n id: \"last-7\",\n label: \"7 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 6)), to: startOfDay(new Date()) })\n },\n {\n id: \"last-30\",\n label: \"30 ngày qua\",\n getValue: () => ({ from: startOfDay(subDays(new Date(), 29)), to: startOfDay(new Date()) })\n },\n {\n id: \"this-month\",\n label: \"Tháng này\",\n getValue: () => ({ from: startOfMonth(new Date()), to: startOfDay(new Date()) })\n },\n {\n id: \"last-month\",\n label: \"Tháng trước\",\n getValue: () => {\n const previous = subMonths(new Date(), 1);\n return { from: startOfMonth(previous), to: endOfMonth(previous) };\n }\n }\n];\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /** Cột preset bên trái lịch (mặc định ẩn) */\n presets?: DatePreset[] | false;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay `dd/MM/yyyy` HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder = VI_DATE_FORMAT,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về).\n useEffect(() => {\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n }, [value]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, VI_DATE_FORMAT, new Date(), { locale: vi });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 items-center\">\n {clearable && value && !disabled && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" onClick={() => onValueChange(null)}>\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Xóa ngày</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" disabled={disabled}>\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Mở lịch</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={vi}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, VI_DATE_FORMAT, { locale: vi });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, VI_DATE_FORMAT, { locale: vi })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder = \"Chọn khoảng ngày\",\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const [open, setOpen] = useState(false);\n const label = formatRange(value);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n \"w-full justify-start gap-2 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-9\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={vi}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa khoảng ngày\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";AAsKM,cAsBI,YAtBJ;AApKN;AAAA,EACE;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,UAAU;AACnB,SAAS,cAAc,SAAS;AAChC,SAAS,WAAW,gBAAgB;AAEpC,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,aAAa;AACtB,SAAS,SAAS,gBAAgB,sBAAsB;AAKjD,MAAM,iBAAiB;AAgBvB,MAAM,uBAAqC;AAAA,EAChD,EAAE,IAAI,SAAS,OAAO,cAAW,UAAU,MAAM,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EACxE,EAAE,IAAI,aAAa,OAAO,cAAW,UAAU,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,CAAC,CAAC,EAAE;AAAA,EACxF,EAAE,IAAI,YAAY,OAAO,6BAAgB,UAAU,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,CAAC,CAAC,EAAE;AAC9F;AAGO,MAAM,6BAAgD;AAAA,EAC3D,EAAE,IAAI,SAAS,OAAO,cAAW,UAAU,OAAO,EAAE,MAAM,WAAW,oBAAI,KAAK,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE,GAAG;AAAA,EAChH;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,OAAO,EAAE,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EAC1F;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,OAAO,EAAE,MAAM,WAAW,QAAQ,oBAAI,KAAK,GAAG,EAAE,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EAC3F;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,OAAO,EAAE,MAAM,aAAa,oBAAI,KAAK,CAAC,GAAG,IAAI,WAAW,oBAAI,KAAK,CAAC,EAAE;AAAA,EAChF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,YAAM,WAAW,UAAU,oBAAI,KAAK,GAAG,CAAC;AACxC,aAAO,EAAE,MAAM,aAAa,QAAQ,GAAG,IAAI,WAAW,QAAQ,EAAE;AAAA,IAClE;AAAA,EACF;AACF;AAEA,SAAS,aAAa,MAAY,SAAgB,SAAyB;AACzE,MAAI,WAAW,SAAS,MAAM,WAAW,OAAO,CAAC,EAAG,QAAO;AAC3D,MAAI,WAAW,QAAQ,MAAM,WAAW,OAAO,CAAC,EAAG,QAAO;AAC1D,SAAO;AACT;AAQA,SAAS,sBAAsB,SAAgB,SAAuC;AACpF,QAAM,WAAsB,CAAC;AAC7B,MAAI,QAAS,UAAS,KAAK,EAAE,QAAQ,WAAW,OAAO,EAAE,CAAC;AAC1D,MAAI,QAAS,UAAS,KAAK,EAAE,OAAO,WAAW,OAAO,EAAE,CAAC;AACzD,SAAO,SAAS,SAAS,IAAI,WAAW;AAC1C;AA+BO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAO,QAAQ,WAAW,OAAO,gBAAgB,EAAE,QAAQ,GAAG,CAAC,IAAI,EAAG;AAGvG,YAAU,MAAM;AACd,YAAQ,QAAQ,WAAW,OAAO,gBAAgB,EAAE,QAAQ,GAAG,CAAC,IAAI,EAAE;AAAA,EACxE,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,aAAa,CAAC,QAAgB;AAClC,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,YAAY,IAAI;AAClB,oBAAc,IAAI;AAClB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,SAAS,gBAAgB,oBAAI,KAAK,GAAG,EAAE,QAAQ,GAAG,CAAC;AACxE,QAAI,CAAC,QAAQ,MAAM,KAAK,aAAa,QAAQ,SAAS,OAAO,GAAG;AAE9D,cAAQ,QAAQ,WAAW,OAAO,gBAAgB,EAAE,QAAQ,GAAG,CAAC,IAAI,EAAE;AACtE;AAAA,IACF;AACA,kBAAc,WAAW,MAAM,CAAC;AAAA,EAClC;AAEA,QAAM,aAAa,YAAY,QAAQ,CAAC,IAAI;AAE5C,SACE,qBAAC,SAAI,WAAW,GAAG,YAAY,SAAS,GACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACV,cAAa;AAAA,QACb,UAAU,CAAC,UAAU,QAAQ,MAAM,OAAO,KAAK;AAAA,QAC/C,QAAQ,CAAC,UAAU,WAAW,MAAM,OAAO,KAAK;AAAA,QAChD,WAAW,CAAC,UAAU;AACpB,cAAI,MAAM,QAAQ,SAAS;AACzB,kBAAM,eAAe;AACrB,uBAAW,IAAI;AAAA,UACjB;AAAA,QACF;AAAA,QACA,WAAU;AAAA,QACT,GAAG;AAAA;AAAA,IACN;AAAA,IAEA,qBAAC,SAAI,WAAU,+DACZ;AAAA,mBAAa,SAAS,CAAC,YACtB,qBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,WAAU,WAAU,UAAS,SAAS,MAAM,cAAc,IAAI,GACvG;AAAA,4BAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA,QAC3C,oBAAC,UAAK,WAAU,WAAU,4BAAQ;AAAA,SACpC;AAAA,MAEF,qBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,4BAAC,kBAAe,SAAO,MACrB,+BAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,WAAU,WAAU,UAAS,UACtE;AAAA,8BAAC,gBAAa,eAAY,QAAO,WAAU,UAAS;AAAA,UACpD,oBAAC,UAAK,WAAU,WAAU,+BAAO;AAAA,WACnC,GACF;AAAA,QACA,qBAAC,kBAAe,OAAM,OAAM,WAAU,yBACnC;AAAA,qBAAW,SAAS,KACnB,oBAAC,SAAI,WAAU,wDACZ,qBAAW,IAAI,CAAC,WACf;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM;AACb,8BAAc,OAAO,SAAS,CAAC;AAC/B,wBAAQ,KAAK;AAAA,cACf;AAAA,cAEC,iBAAO;AAAA;AAAA,YAVH,OAAO;AAAA,UAWd,CACD,GACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,QAAQ;AAAA,cACR,WAAS;AAAA,cACT,UAAU,SAAS;AAAA,cACnB,cAAc,SAAS;AAAA,cACvB,UAAU,sBAAsB,SAAS,OAAO;AAAA,cAChD,UAAU,CAAC,SAAS;AAClB,8BAAc,OAAO,WAAW,IAAI,IAAI,IAAI;AAC5C,wBAAQ,KAAK;AAAA,cACf;AAAA;AAAA,UACF;AAAA,WACF;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;AAqBA,SAAS,YAAY,OAAoD;AACvE,MAAI,CAAC,OAAO,KAAM,QAAO;AACzB,QAAM,OAAO,WAAW,MAAM,MAAM,gBAAgB,EAAE,QAAQ,GAAG,CAAC;AAClE,MAAI,CAAC,MAAM,GAAI,QAAO;AACtB,SAAO,GAAG,IAAI,WAAM,WAAW,MAAM,IAAI,gBAAgB,EAAE,QAAQ,GAAG,CAAC,CAAC;AAC1E;AAQO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,QAAQ,YAAY,KAAK;AAC/B,QAAM,aAAa,YAAY,QAAQ,CAAC,IAAI;AAE5C,QAAM,YAAY,aAAa,QAAQ,KAAK,KAAK,CAAC;AAElD;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,WAAU,mBACb;AAAA,2BAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,4BAAC,kBAAe,SAAO,MACrB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL;AAAA,YACA,SAAQ;AAAA,YACR;AAAA,YACA,WAAW;AAAA,cACT;AAAA,cACA,CAAC,SAAS;AAAA,cACV,aAAa;AAAA,cACb;AAAA,YACF;AAAA,YACC,GAAG;AAAA,YAEJ;AAAA,kCAAC,gBAAa,eAAY,QAAO,WAAU,mBAAkB;AAAA,cAC7D,oBAAC,UAAK,WAAU,YAAY,mBAAS,aAAY;AAAA;AAAA;AAAA,QACnD,GACF;AAAA,QACA,qBAAC,kBAAe,OAAM,SAAQ,WAAU,8CACrC;AAAA,qBAAW,SAAS,KACnB,oBAAC,SAAI,WAAU,0FACZ,qBAAW,IAAI,CAAC,WACf;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM;AACb,8BAAc,OAAO,SAAS,CAAC;AAC/B,wBAAQ,KAAK;AAAA,cACf;AAAA,cAEC,iBAAO;AAAA;AAAA,YAVH,OAAO;AAAA,UAWd,CACD,GACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,QAAQ;AAAA,cACR,WAAS;AAAA,cACT;AAAA,cACA,UAAU,SAAS;AAAA,cACnB,cAAc,OAAO,QAAQ;AAAA,cAC7B,UAAU,sBAAsB,SAAS,OAAO;AAAA,cAChD,UAAU,CAAC,UAAU,cAAc,SAAS,IAAI;AAAA;AAAA,UAClD;AAAA,WACF;AAAA,SACF;AAAA,MAEC,aACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAW;AAAA,UACX,SAAS,MAAM,cAAc,IAAI;AAAA,UACjC,WAAU;AAAA,UAEV,8BAAC,KAAE,eAAY,QAAO,WAAU,YAAW;AAAA;AAAA,MAC7C;AAAA,OAEJ;AAAA;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/date-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { format as formatDate, isAfter, isBefore, isValid, parse, startOfDay } from \"date-fns\";\nimport { vi } from \"date-fns/locale\";\nimport { CalendarDays, X } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport type { DateRange, Matcher } from \"react-day-picker\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Calendar } from \"../ui/calendar.js\";\nimport { Input } from \"../ui/input.js\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"../ui/popover.js\";\nimport {\n type DatePreset,\n type DateRangePreset,\n DEFAULT_DATE_RANGE_PRESETS,\n VI_DATE_FORMAT\n} from \"./date-picker-constants.js\";\n\nexport type { DateRange };\n\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n if (minDate && isBefore(date, startOfDay(minDate))) return true;\n if (maxDate && isAfter(date, startOfDay(maxDate))) return true;\n return false;\n}\n\n/**\n * Chặn ngày ngoài khoảng cho phép — trả về MẢNG matcher, không phải một object.\n *\n * Cạm bẫy react-day-picker: `{ before, after }` trong CÙNG một object là `DateInterval`, nghĩa là\n * \"các ngày NẰM GIỮA\" — ngược hẳn ý định. Tách thành hai matcher riêng mới ra \"trước min HOẶC sau max\".\n */\nfunction buildDisabledMatchers(minDate?: Date, maxDate?: Date): Matcher[] | undefined {\n const matchers: Matcher[] = [];\n if (minDate) matchers.push({ before: startOfDay(minDate) });\n if (maxDate) matchers.push({ after: startOfDay(maxDate) });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date | null;\n onValueChange: (value: Date | null) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Ngày nhỏ nhất / lớn nhất được chọn */\n minDate?: Date;\n maxDate?: Date;\n /** Hiện nút xoá khi đã có giá trị (mặc định true) */\n clearable?: boolean;\n /** Cột preset bên trái lịch (mặc định ẩn) */\n presets?: DatePreset[] | false;\n className?: string;\n id?: string;\n name?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n}\n\n/**\n * Ô chọn ngày: gõ tay `dd/MM/yyyy` HOẶC bấm lịch.\n *\n * Vì sao vẫn giữ ô gõ tay thay vì chỉ có nút mở lịch: người nhập liệu chuyên nghiệp (kế toán,\n * nhân sự) gõ nhanh hơn bấm nhiều lần, và ngày sinh năm 1975 mà bắt lật lịch 50 năm thì không\n * ai chịu nổi. Chuỗi gõ dở KHÔNG bị ghi đè trong lúc gõ; sai định dạng thì trả về giá trị cũ\n * khi rời ô, không âm thầm nuốt dữ liệu.\n */\nexport function DatePicker({\n value,\n onValueChange,\n placeholder = VI_DATE_FORMAT,\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = false,\n className,\n id,\n name,\n ...aria\n}: DatePickerProps) {\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(() => (value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\"));\n\n // Đồng bộ khi giá trị đổi từ bên ngoài (reset form, chọn preset, load dữ liệu về).\n useEffect(() => {\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n }, [value]);\n\n const commitText = (raw: string) => {\n const trimmed = raw.trim();\n if (trimmed === \"\") {\n onValueChange(null);\n return;\n }\n const parsed = parse(trimmed, VI_DATE_FORMAT, new Date(), { locale: vi });\n if (!isValid(parsed) || isOutOfRange(parsed, minDate, maxDate)) {\n // Không hợp lệ → khôi phục hiển thị theo giá trị đang có (không đổi dữ liệu).\n setText(value ? formatDate(value, VI_DATE_FORMAT, { locale: vi }) : \"\");\n return;\n }\n onValueChange(startOfDay(parsed));\n };\n\n const presetList = presets === false ? [] : presets;\n\n return (\n <div className={cn(\"relative\", className)}>\n <Input\n id={id}\n name={name}\n value={text}\n disabled={disabled}\n placeholder={placeholder}\n inputMode=\"numeric\"\n autoComplete=\"off\"\n onChange={(event) => setText(event.target.value)}\n onBlur={(event) => commitText(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commitText(text);\n }\n }}\n className=\"pr-16\"\n {...aria}\n />\n\n <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 items-center\">\n {clearable && value && !disabled && (\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" onClick={() => onValueChange(null)}>\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Xóa ngày</span>\n </Button>\n )}\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"icon-sm\" className=\"size-7\" disabled={disabled}>\n <CalendarDays aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Mở lịch</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"flex w-auto gap-2 p-2\">\n {presetList.length > 0 && (\n <div className=\"flex w-32 flex-col gap-1 border-r border-border pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"single\"\n locale={vi}\n autoFocus\n selected={value ?? undefined}\n defaultMonth={value ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(date) => {\n onValueChange(date ? startOfDay(date) : null);\n setOpen(false);\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange | null;\n onValueChange: (value: DateRange | null) => void;\n placeholder?: string;\n disabled?: boolean;\n minDate?: Date;\n maxDate?: Date;\n clearable?: boolean;\n /** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */\n presets?: DateRangePreset[] | false;\n /** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */\n numberOfMonths?: number;\n className?: string;\n id?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n}\n\nfunction formatRange(range: DateRange | null | undefined): string | null {\n if (!range?.from) return null;\n const from = formatDate(range.from, VI_DATE_FORMAT, { locale: vi });\n if (!range.to) return from;\n return `${from} – ${formatDate(range.to, VI_DATE_FORMAT, { locale: vi })}`;\n}\n\n/**\n * Ô chọn khoảng ngày cho báo cáo và bộ lọc danh sách: cột preset + lịch 2 tháng.\n *\n * Preset đứng trước lịch vì 9/10 lần người dùng muốn \"7 ngày qua\" chứ không phải một khoảng\n * tuỳ ý — bắt họ bấm hai lần trên lịch mỗi lần xem báo cáo là thuế lặp lại vô ích.\n */\nexport function DateRangePicker({\n value,\n onValueChange,\n placeholder = \"Chọn khoảng ngày\",\n disabled = false,\n minDate,\n maxDate,\n clearable = true,\n presets = DEFAULT_DATE_RANGE_PRESETS,\n numberOfMonths = 2,\n className,\n id,\n ...aria\n}: DateRangePickerProps) {\n const [open, setOpen] = useState(false);\n const label = formatRange(value);\n const presetList = presets === false ? [] : presets;\n\n const showClear = clearable && Boolean(label) && !disabled;\n\n return (\n /* Nút xóa nằm NGOÀI trigger (anh em), không lồng trong <button> — vừa hợp lệ về HTML vừa\n đến được bằng phím Tab. */\n <div className=\"relative w-full\">\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n id={id}\n variant=\"outline\"\n disabled={disabled}\n className={cn(\n \"w-full justify-start gap-2 font-normal\",\n !label && \"text-muted-foreground\",\n showClear && \"pr-9\",\n className\n )}\n {...aria}\n >\n <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{label ?? placeholder}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"flex w-auto flex-col gap-2 p-2 sm:flex-row\">\n {presetList.length > 0 && (\n <div className=\"flex flex-row flex-wrap gap-1 sm:w-36 sm:flex-col sm:border-r sm:border-border sm:pr-2\">\n {presetList.map((preset) => (\n <Button\n key={preset.id}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"justify-start\"\n onClick={() => {\n onValueChange(preset.getValue());\n setOpen(false);\n }}\n >\n {preset.label}\n </Button>\n ))}\n </div>\n )}\n <Calendar\n mode=\"range\"\n locale={vi}\n autoFocus\n numberOfMonths={numberOfMonths}\n selected={value ?? undefined}\n defaultMonth={value?.from ?? undefined}\n disabled={buildDisabledMatchers(minDate, maxDate)}\n onSelect={(range) => onValueChange(range ?? null)}\n />\n </PopoverContent>\n </Popover>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"Xóa khoảng ngày\"\n onClick={() => onValueChange(null)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 ease-out hover:text-foreground\"\n >\n <X aria-hidden=\"true\" className=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel `Select` + hai hàm quy đổi giá trị.
|
|
3
|
+
*
|
|
4
|
+
* Module **CỐ Ý không có `"use client"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy
|
|
5
|
+
* đủ: mọi export của một file `"use client"` đến tay Server Component dưới dạng
|
|
6
|
+
* client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần
|
|
7
|
+
* (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một
|
|
8
|
+
* khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `form-field.tsx` chỉ được `import`,
|
|
11
|
+
* KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Sentinel cho `Select` của Radix.
|
|
15
|
+
*
|
|
16
|
+
* Radix cấm `SelectItem value=""` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục
|
|
17
|
+
* "— Không chọn —" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi
|
|
18
|
+
* nơi tự đặt một sentinel rồi lọt xuống payload gửi server.
|
|
19
|
+
*/
|
|
20
|
+
declare const SELECT_EMPTY_VALUE = "__empty__";
|
|
21
|
+
/** Giá trị model ("" = chưa chọn) → giá trị cho Radix Select. */
|
|
22
|
+
declare function toSelectValue(value: string | null | undefined): string;
|
|
23
|
+
/** Giá trị từ Radix Select → giá trị model ("" khi người dùng chọn mục rỗng). */
|
|
24
|
+
declare function fromSelectValue(value: string): string;
|
|
25
|
+
|
|
26
|
+
export { SELECT_EMPTY_VALUE, fromSelectValue, toSelectValue };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const SELECT_EMPTY_VALUE = "__empty__";
|
|
2
|
+
function toSelectValue(value) {
|
|
3
|
+
return value == null || value === "" ? SELECT_EMPTY_VALUE : value;
|
|
4
|
+
}
|
|
5
|
+
function fromSelectValue(value) {
|
|
6
|
+
return value === SELECT_EMPTY_VALUE ? "" : value;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
SELECT_EMPTY_VALUE,
|
|
10
|
+
fromSelectValue,
|
|
11
|
+
toSelectValue
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=form-field-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/form-field-constants.ts"],"sourcesContent":["/**\n * Sentinel `Select` + hai hàm quy đổi giá trị.\n *\n * Module **CỐ Ý không có `\"use client\"`** — xem `src/chart/chart-colors.ts` để biết lý do đầy\n * đủ: mọi export của một file `\"use client\"` đến tay Server Component dưới dạng\n * client-reference proxy, đọc ra `undefined` mà KHÔNG throw, KHÔNG warning. Hằng dữ liệu thuần\n * (nhãn mặc định, preset, sentinel) là thứ trang server hay đọc nhất — spread để override một\n * khoá, đối chiếu giá trị, dựng danh sách — nên phải sống ngoài ranh giới client.\n *\n * ⚠ Barrel `src/index.ts` xuất module này TRỰC TIẾP. `form-field.tsx` chỉ được `import`,\n * KHÔNG được `export` lại (re-export qua file client là vô hiệu, và đụng tên với `export *`).\n */\n\n/**\n * Sentinel cho `Select` của Radix.\n *\n * Radix cấm `SelectItem value=\"\"` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục\n * \"— Không chọn —\" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi\n * nơi tự đặt một sentinel rồi lọt xuống payload gửi server.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\n\n/** Giá trị model (\"\" = chưa chọn) → giá trị cho Radix Select. */\nexport function toSelectValue(value: string | null | undefined): string {\n return value == null || value === \"\" ? SELECT_EMPTY_VALUE : value;\n}\n\n/** Giá trị từ Radix Select → giá trị model (\"\" khi người dùng chọn mục rỗng). */\nexport function fromSelectValue(value: string): string {\n return value === SELECT_EMPTY_VALUE ? \"\" : value;\n}\n"],"mappings":"AAoBO,MAAM,qBAAqB;AAG3B,SAAS,cAAc,OAA0C;AACtE,SAAO,SAAS,QAAQ,UAAU,KAAK,qBAAqB;AAC9D;AAGO,SAAS,gBAAgB,OAAuB;AACrD,SAAO,UAAU,qBAAqB,KAAK;AAC7C;","names":[]}
|
|
@@ -42,17 +42,5 @@ interface FormFieldProps {
|
|
|
42
42
|
* mở lại file này — một FormField "đa năng" ôm hết mọi loại control sẽ phình ra không có điểm dừng.
|
|
43
43
|
*/
|
|
44
44
|
declare function FormField({ label, required, description, error, htmlFor, colSpan, orientation, className, children }: FormFieldProps): React.JSX.Element;
|
|
45
|
-
/**
|
|
46
|
-
* Sentinel cho `Select` của Radix.
|
|
47
|
-
*
|
|
48
|
-
* Radix cấm `SelectItem value=""` (chuỗi rỗng dành riêng cho việc xoá lựa chọn), nên mục
|
|
49
|
-
* "— Không chọn —" phải mang một giá trị giả. Dùng chung hằng số này ở mọi app để tránh mỗi
|
|
50
|
-
* nơi tự đặt một sentinel rồi lọt xuống payload gửi server.
|
|
51
|
-
*/
|
|
52
|
-
declare const SELECT_EMPTY_VALUE = "__empty__";
|
|
53
|
-
/** Giá trị model ("" = chưa chọn) → giá trị cho Radix Select. */
|
|
54
|
-
declare function toSelectValue(value: string | null | undefined): string;
|
|
55
|
-
/** Giá trị từ Radix Select → giá trị model ("" khi người dùng chọn mục rỗng). */
|
|
56
|
-
declare function fromSelectValue(value: string): string;
|
|
57
45
|
|
|
58
|
-
export { FormField, type FormFieldColSpan, type FormFieldControlProps, type FormFieldProps
|
|
46
|
+
export { FormField, type FormFieldColSpan, type FormFieldControlProps, type FormFieldProps };
|
|
@@ -67,17 +67,7 @@ function FormField({
|
|
|
67
67
|
}
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
|
-
const SELECT_EMPTY_VALUE = "__empty__";
|
|
71
|
-
function toSelectValue(value) {
|
|
72
|
-
return value == null || value === "" ? SELECT_EMPTY_VALUE : value;
|
|
73
|
-
}
|
|
74
|
-
function fromSelectValue(value) {
|
|
75
|
-
return value === SELECT_EMPTY_VALUE ? "" : value;
|
|
76
|
-
}
|
|
77
70
|
export {
|
|
78
|
-
FormField
|
|
79
|
-
SELECT_EMPTY_VALUE,
|
|
80
|
-
fromSelectValue,
|
|
81
|
-
toSelectValue
|
|
71
|
+
FormField
|
|
82
72
|
};
|
|
83
73
|
//# sourceMappingURL=form-field.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {label && (\n <FieldLabel htmlFor={id} className=\"text-sm\">\n {label}\n {required && (\n // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên\n // control, để trình đọc màn hình khỏi đọc thừa \"sao\".\n // Bậc ink (7,15:1 light / 6,42:1 dark trên `--card`) chứ không `text-destructive`\n // (bậc NỀN, chỉ 3,53:1 trên `--card` ở dark ✗).\n <span aria-hidden=\"true\" className=\"text-destructive-ink\">\n *\n </span>\n )}\n </FieldLabel>\n )}\n\n <FieldContent>\n {typeof children === \"function\" ? children(control) : children}\n\n {hasError ? (\n <FieldError id={errorId} className=\"flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1\">\n <AlertCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {error}\n </FieldError>\n ) : (\n description && (\n <FieldDescription id={descriptionId} className=\"text-xs\">\n {description}\n </FieldDescription>\n )\n )}\n </FieldContent>\n </Field>\n );\n}\n
|
|
1
|
+
{"version":3,"sources":["../../../src/components/composite/form-field.tsx"],"sourcesContent":["\"use client\";\n\nimport { AlertCircle } from \"lucide-react\";\nimport { type ReactNode, useId } from \"react\";\nimport { cn } from \"../../lib/cn.js\";\nimport { Field, FieldContent, FieldDescription, FieldError, FieldLabel } from \"../ui/field.js\";\n\nexport type FormFieldColSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\n\n/**\n * Thuộc tính a11y mà `FormField` tiêm xuống control con.\n *\n * Trải nguyên cụm này vào `<Input>`/`<Combobox>`/`<Select>` là control tự có id khớp nhãn,\n * tự báo lỗi và trạng thái bắt buộc — không phải nhớ từng thuộc tính ở mỗi trang.\n */\nexport interface FormFieldControlProps {\n id: string;\n \"aria-invalid\"?: true;\n \"aria-required\"?: true;\n \"aria-describedby\"?: string;\n}\n\nexport interface FormFieldProps {\n label?: ReactNode;\n /** Đánh dấu bắt buộc: vẽ dấu * và phát `aria-required` xuống control */\n required?: boolean;\n /** Mô tả/gợi ý dưới trường — bị ẩn khi có lỗi để không đọc chồng lời */\n description?: ReactNode;\n /** Thông báo lỗi; có giá trị là trường vào trạng thái invalid */\n error?: ReactNode;\n /** id của control — bỏ trống thì tự sinh bằng useId */\n htmlFor?: string;\n /** Số cột chiếm trong lưới 12 cột của `FormSection` (mặc định 12 = cả hàng) */\n colSpan?: FormFieldColSpan;\n orientation?: \"vertical\" | \"horizontal\" | \"responsive\";\n className?: string;\n /**\n * Control của trường. Dạng hàm sẽ nhận `FormFieldControlProps` để trải xuống —\n * dạng node thì app tự lo id/aria (dùng khi control đã tự quản, vd Checkbox có nhãn riêng).\n */\n children: ReactNode | ((control: FormFieldControlProps) => ReactNode);\n}\n\nconst colSpanClasses: Record<FormFieldColSpan, string> = {\n 1: \"col-span-12 md:col-span-1\",\n 2: \"col-span-12 md:col-span-2\",\n 3: \"col-span-12 sm:col-span-6 md:col-span-3\",\n 4: \"col-span-12 sm:col-span-6 md:col-span-4\",\n 5: \"col-span-12 sm:col-span-6 md:col-span-5\",\n 6: \"col-span-12 sm:col-span-6\",\n 7: \"col-span-12 md:col-span-7\",\n 8: \"col-span-12 md:col-span-8\",\n 9: \"col-span-12 md:col-span-9\",\n 10: \"col-span-12 md:col-span-10\",\n 11: \"col-span-12 md:col-span-11\",\n 12: \"col-span-12\"\n};\n\n/**\n * Một trường form: nhãn + control + mô tả/lỗi, đặt trong lưới 12 cột của `FormSection`.\n *\n * KHÔNG tự vẽ input. Nhãn và lỗi uỷ quyền xuống primitive `Field`, còn control do app chọn —\n * nếu composite tự dựng input thì mỗi kiểu control mới (combobox, date range, editor) lại phải\n * mở lại file này — một FormField \"đa năng\" ôm hết mọi loại control sẽ phình ra không có điểm dừng.\n */\nexport function FormField({\n label,\n required = false,\n description,\n error,\n htmlFor,\n colSpan = 12,\n orientation = \"vertical\",\n className,\n children\n}: FormFieldProps) {\n const generatedId = useId();\n const id = htmlFor ?? generatedId;\n const hasError = Boolean(error);\n const errorId = `${id}-error`;\n const descriptionId = `${id}-description`;\n\n const control: FormFieldControlProps = {\n id,\n \"aria-invalid\": hasError ? true : undefined,\n \"aria-required\": required ? true : undefined,\n \"aria-describedby\": hasError ? errorId : description ? descriptionId : undefined\n };\n\n return (\n <Field\n data-slot=\"form-field\"\n data-invalid={hasError || undefined}\n orientation={orientation}\n className={cn(colSpanClasses[colSpan], className)}\n >\n {label && (\n <FieldLabel htmlFor={id} className=\"text-sm\">\n {label}\n {required && (\n // Dấu * chỉ là tín hiệu thị giác — trạng thái bắt buộc đã nằm ở aria-required trên\n // control, để trình đọc màn hình khỏi đọc thừa \"sao\".\n // Bậc ink (7,15:1 light / 6,42:1 dark trên `--card`) chứ không `text-destructive`\n // (bậc NỀN, chỉ 3,53:1 trên `--card` ở dark ✗).\n <span aria-hidden=\"true\" className=\"text-destructive-ink\">\n *\n </span>\n )}\n </FieldLabel>\n )}\n\n <FieldContent>\n {typeof children === \"function\" ? children(control) : children}\n\n {hasError ? (\n <FieldError id={errorId} className=\"flex items-center gap-1.5 text-xs animate-in fade-in slide-in-from-top-1\">\n <AlertCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n {error}\n </FieldError>\n ) : (\n description && (\n <FieldDescription id={descriptionId} className=\"text-xs\">\n {description}\n </FieldDescription>\n )\n )}\n </FieldContent>\n </Field>\n );\n}\n"],"mappings":";AAiGQ,SAOI,KAPJ;AA/FR,SAAS,mBAAmB;AAC5B,SAAyB,aAAa;AACtC,SAAS,UAAU;AACnB,SAAS,OAAO,cAAc,kBAAkB,YAAY,kBAAkB;AAsC9E,MAAM,iBAAmD;AAAA,EACvD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AASO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAmB;AACjB,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,WAAW;AACtB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,gBAAgB,GAAG,EAAE;AAE3B,QAAM,UAAiC;AAAA,IACrC;AAAA,IACA,gBAAgB,WAAW,OAAO;AAAA,IAClC,iBAAiB,WAAW,OAAO;AAAA,IACnC,oBAAoB,WAAW,UAAU,cAAc,gBAAgB;AAAA,EACzE;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAc,YAAY;AAAA,MAC1B;AAAA,MACA,WAAW,GAAG,eAAe,OAAO,GAAG,SAAS;AAAA,MAE/C;AAAA,iBACC,qBAAC,cAAW,SAAS,IAAI,WAAU,WAChC;AAAA;AAAA,UACA;AAAA;AAAA;AAAA;AAAA,UAKC,oBAAC,UAAK,eAAY,QAAO,WAAU,wBAAuB,eAE1D;AAAA,WAEJ;AAAA,QAGF,qBAAC,gBACE;AAAA,iBAAO,aAAa,aAAa,SAAS,OAAO,IAAI;AAAA,UAErD,WACC,qBAAC,cAAW,IAAI,SAAS,WAAU,4EACjC;AAAA,gCAAC,eAAY,eAAY,QAAO,WAAU,qBAAoB;AAAA,YAC7D;AAAA,aACH,IAEA,eACE,oBAAC,oBAAiB,IAAI,eAAe,WAAU,WAC5C,uBACH;AAAA,WAGN;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|