@comitor/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/dist/chart/chart.d.ts +88 -0
- package/dist/chart/chart.js +199 -0
- package/dist/chart/chart.js.map +1 -0
- package/dist/chart/comitor-chart.d.ts +109 -0
- package/dist/chart/comitor-chart.js +189 -0
- package/dist/chart/comitor-chart.js.map +1 -0
- package/dist/chart/index.d.ts +4 -0
- package/dist/chart/index.js +3 -0
- package/dist/chart/index.js.map +1 -0
- package/dist/components/composite/avatar-group.d.ts +33 -0
- package/dist/components/composite/avatar-group.js +86 -0
- package/dist/components/composite/avatar-group.js.map +1 -0
- package/dist/components/composite/combobox.d.ts +70 -0
- package/dist/components/composite/combobox.js +291 -0
- package/dist/components/composite/combobox.js.map +1 -0
- package/dist/components/composite/command-palette.d.ts +56 -0
- package/dist/components/composite/command-palette.js +131 -0
- package/dist/components/composite/command-palette.js.map +1 -0
- package/dist/components/composite/confirm-dialog.d.ts +40 -0
- package/dist/components/composite/confirm-dialog.js +96 -0
- package/dist/components/composite/confirm-dialog.js.map +1 -0
- package/dist/components/composite/copy-button.d.ts +27 -0
- package/dist/components/composite/copy-button.js +80 -0
- package/dist/components/composite/copy-button.js.map +1 -0
- package/dist/components/composite/data-table.d.ts +136 -0
- package/dist/components/composite/data-table.js +300 -0
- package/dist/components/composite/data-table.js.map +1 -0
- package/dist/components/composite/date-picker.d.ts +77 -0
- package/dist/components/composite/date-picker.js +268 -0
- package/dist/components/composite/date-picker.js.map +1 -0
- package/dist/components/composite/empty-state.d.ts +38 -0
- package/dist/components/composite/empty-state.js +35 -0
- package/dist/components/composite/empty-state.js.map +1 -0
- package/dist/components/composite/filter-chips.d.ts +39 -0
- package/dist/components/composite/filter-chips.js +79 -0
- package/dist/components/composite/filter-chips.js.map +1 -0
- package/dist/components/composite/form-field.d.ts +58 -0
- package/dist/components/composite/form-field.js +81 -0
- package/dist/components/composite/form-field.js.map +1 -0
- package/dist/components/composite/form-section.d.ts +33 -0
- package/dist/components/composite/form-section.js +85 -0
- package/dist/components/composite/form-section.js.map +1 -0
- package/dist/components/composite/inline-edit.d.ts +38 -0
- package/dist/components/composite/inline-edit.js +165 -0
- package/dist/components/composite/inline-edit.js.map +1 -0
- package/dist/components/composite/keyboard-hint.d.ts +31 -0
- package/dist/components/composite/keyboard-hint.js +72 -0
- package/dist/components/composite/keyboard-hint.js.map +1 -0
- package/dist/components/composite/letter-avatar.d.ts +47 -0
- package/dist/components/composite/letter-avatar.js +66 -0
- package/dist/components/composite/letter-avatar.js.map +1 -0
- package/dist/components/composite/page-container.d.ts +25 -0
- package/dist/components/composite/page-container.js +23 -0
- package/dist/components/composite/page-container.js.map +1 -0
- package/dist/components/composite/page-header.d.ts +49 -0
- package/dist/components/composite/page-header.js +75 -0
- package/dist/components/composite/page-header.js.map +1 -0
- package/dist/components/composite/relative-time.d.ts +28 -0
- package/dist/components/composite/relative-time.js +44 -0
- package/dist/components/composite/relative-time.js.map +1 -0
- package/dist/components/composite/search-filter-bar.d.ts +38 -0
- package/dist/components/composite/search-filter-bar.js +135 -0
- package/dist/components/composite/search-filter-bar.js.map +1 -0
- package/dist/components/composite/stat-card.d.ts +48 -0
- package/dist/components/composite/stat-card.js +76 -0
- package/dist/components/composite/stat-card.js.map +1 -0
- package/dist/components/composite/status-config.d.ts +85 -0
- package/dist/components/composite/status-config.js +41 -0
- package/dist/components/composite/status-config.js.map +1 -0
- package/dist/components/composite/status-pill.d.ts +26 -0
- package/dist/components/composite/status-pill.js +59 -0
- package/dist/components/composite/status-pill.js.map +1 -0
- package/dist/components/composite/table-pagination.d.ts +31 -0
- package/dist/components/composite/table-pagination.js +103 -0
- package/dist/components/composite/table-pagination.js.map +1 -0
- package/dist/components/composite/text-utils.d.ts +38 -0
- package/dist/components/composite/text-utils.js +40 -0
- package/dist/components/composite/text-utils.js.map +1 -0
- package/dist/components/composite/toast.d.ts +56 -0
- package/dist/components/composite/toast.js +63 -0
- package/dist/components/composite/toast.js.map +1 -0
- package/dist/components/ui/accordion.d.ts +17 -0
- package/dist/components/ui/accordion.js +53 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +34 -0
- package/dist/components/ui/alert-dialog.js +103 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/alert.d.ts +16 -0
- package/dist/components/ui/alert.js +50 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +7 -0
- package/dist/components/ui/aspect-ratio.js +10 -0
- package/dist/components/ui/aspect-ratio.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +33 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +15 -0
- package/dist/components/ui/badge.js +37 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +30 -0
- package/dist/components/ui/breadcrumb.js +89 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button-group.d.ts +21 -0
- package/dist/components/ui/button-group.js +62 -0
- package/dist/components/ui/button-group.js.map +1 -0
- package/dist/components/ui/button.d.ts +21 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +26 -0
- package/dist/components/ui/calendar.js +148 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +33 -0
- package/dist/components/ui/card.js +70 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/carousel.d.ts +46 -0
- package/dist/components/ui/carousel.js +173 -0
- package/dist/components/ui/carousel.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +17 -0
- package/dist/components/ui/checkbox.js +67 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +17 -0
- package/dist/components/ui/collapsible.js +18 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/command.d.ts +41 -0
- package/dist/components/ui/command.js +122 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +51 -0
- package/dist/components/ui/context-menu.js +166 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +48 -0
- package/dist/components/ui/dialog.js +131 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +32 -0
- package/dist/components/ui/drawer.js +102 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +52 -0
- package/dist/components/ui/dropdown-menu.js +167 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty.d.ts +22 -0
- package/dist/components/ui/empty.js +87 -0
- package/dist/components/ui/empty.js.map +1 -0
- package/dist/components/ui/field.d.ts +46 -0
- package/dist/components/ui/field.js +197 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/form.d.ts +50 -0
- package/dist/components/ui/form.js +100 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +17 -0
- package/dist/components/ui/hover-card.js +31 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +26 -0
- package/dist/components/ui/input-group.js +148 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-otp.d.ts +23 -0
- package/dist/components/ui/input-otp.js +54 -0
- package/dist/components/ui/input-otp.js.map +1 -0
- package/dist/components/ui/input.d.ts +21 -0
- package/dist/components/ui/input.js +62 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/item.d.ts +39 -0
- package/dist/components/ui/item.js +142 -0
- package/dist/components/ui/item.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +8 -0
- package/dist/components/ui/kbd.js +25 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/label.d.ts +7 -0
- package/dist/components/ui/label.js +21 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/menubar.d.ts +51 -0
- package/dist/components/ui/menubar.js +196 -0
- package/dist/components/ui/menubar.js.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +39 -0
- package/dist/components/ui/navigation-menu.js +123 -0
- package/dist/components/ui/navigation-menu.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +40 -0
- package/dist/components/ui/pagination.js +95 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +19 -0
- package/dist/components/ui/popover.js +35 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +12 -0
- package/dist/components/ui/progress.js +27 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +9 -0
- package/dist/components/ui/radio-group.js +34 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +19 -0
- package/dist/components/ui/resizable.js +38 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +13 -0
- package/dist/components/ui/scroll-area.js +46 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +33 -0
- package/dist/components/ui/select.js +133 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +7 -0
- package/dist/components/ui/separator.js +23 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +38 -0
- package/dist/components/ui/sheet.js +96 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +6 -0
- package/dist/components/ui/skeleton.js +9 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +12 -0
- package/dist/components/ui/slider.js +54 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +9 -0
- package/dist/components/ui/sonner.js +25 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +6 -0
- package/dist/components/ui/spinner.js +10 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/switch.d.ts +14 -0
- package/dist/components/ui/switch.js +65 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +30 -0
- package/dist/components/ui/table.js +87 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +35 -0
- package/dist/components/ui/tabs.js +54 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +6 -0
- package/dist/components/ui/textarea.js +19 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +18 -0
- package/dist/components/ui/toggle-group.js +52 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +15 -0
- package/dist/components/ui/toggle.js +33 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +21 -0
- package/dist/components/ui/tooltip.js +38 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/form.d.ts +5 -0
- package/dist/form.js +2 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +16 -0
- package/dist/lib/cn.js +9 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/shell/app-header.d.ts +41 -0
- package/dist/shell/app-header.js +136 -0
- package/dist/shell/app-header.js.map +1 -0
- package/dist/shell/app-launcher.d.ts +33 -0
- package/dist/shell/app-launcher.js +189 -0
- package/dist/shell/app-launcher.js.map +1 -0
- package/dist/shell/app-shell.d.ts +44 -0
- package/dist/shell/app-shell.js +99 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/comitor-lockup.d.ts +33 -0
- package/dist/shell/comitor-lockup.js +112 -0
- package/dist/shell/comitor-lockup.js.map +1 -0
- package/dist/shell/comitor-logo.d.ts +28 -0
- package/dist/shell/comitor-logo.js +92 -0
- package/dist/shell/comitor-logo.js.map +1 -0
- package/dist/shell/index.d.ts +24 -0
- package/dist/shell/index.js +23 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/mobile-menu.d.ts +26 -0
- package/dist/shell/mobile-menu.js +56 -0
- package/dist/shell/mobile-menu.js.map +1 -0
- package/dist/shell/notifications-button.d.ts +22 -0
- package/dist/shell/notifications-button.js +39 -0
- package/dist/shell/notifications-button.js.map +1 -0
- package/dist/shell/shell-context.d.ts +91 -0
- package/dist/shell/shell-context.js +233 -0
- package/dist/shell/shell-context.js.map +1 -0
- package/dist/shell/shell-link.d.ts +19 -0
- package/dist/shell/shell-link.js +18 -0
- package/dist/shell/shell-link.js.map +1 -0
- package/dist/shell/sidebar-group.d.ts +21 -0
- package/dist/shell/sidebar-group.js +43 -0
- package/dist/shell/sidebar-group.js.map +1 -0
- package/dist/shell/sidebar-menu-item.d.ts +27 -0
- package/dist/shell/sidebar-menu-item.js +152 -0
- package/dist/shell/sidebar-menu-item.js.map +1 -0
- package/dist/shell/sidebar.d.ts +26 -0
- package/dist/shell/sidebar.js +92 -0
- package/dist/shell/sidebar.js.map +1 -0
- package/dist/shell/theme-provider.d.ts +18 -0
- package/dist/shell/theme-provider.js +29 -0
- package/dist/shell/theme-provider.js.map +1 -0
- package/dist/shell/theme-toggle.d.ts +28 -0
- package/dist/shell/theme-toggle.js +67 -0
- package/dist/shell/theme-toggle.js.map +1 -0
- package/dist/shell/types.d.ts +168 -0
- package/dist/shell/types.js +38 -0
- package/dist/shell/types.js.map +1 -0
- package/dist/shell/user-menu.d.ts +42 -0
- package/dist/shell/user-menu.js +132 -0
- package/dist/shell/user-menu.js.map +1 -0
- package/dist/shell/utils.d.ts +62 -0
- package/dist/shell/utils.js +73 -0
- package/dist/shell/utils.js.map +1 -0
- package/dist/shell/workspace-switcher.d.ts +24 -0
- package/dist/shell/workspace-switcher.js +219 -0
- package/dist/shell/workspace-switcher.js.map +1 -0
- package/dist/tokens.d.ts +173 -0
- package/dist/tokens.js +161 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +151 -0
- package/styles.css +357 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, Inbox, SlidersHorizontal } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn.js";
|
|
5
|
+
import { Button } from "../ui/button.js";
|
|
6
|
+
import { Checkbox } from "../ui/checkbox.js";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuCheckboxItem,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
DropdownMenuTrigger
|
|
14
|
+
} from "../ui/dropdown-menu.js";
|
|
15
|
+
import { Skeleton } from "../ui/skeleton.js";
|
|
16
|
+
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
|
|
17
|
+
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
|
+
const alignClasses = {
|
|
28
|
+
left: "text-left",
|
|
29
|
+
center: "text-center",
|
|
30
|
+
right: "text-right"
|
|
31
|
+
};
|
|
32
|
+
function getPinnedOffsets(columns, selectable) {
|
|
33
|
+
const styles = /* @__PURE__ */ new Map();
|
|
34
|
+
let left = selectable ? 44 : 0;
|
|
35
|
+
for (const column of columns) {
|
|
36
|
+
if (column.pinned !== "left") continue;
|
|
37
|
+
styles.set(column.id, { position: "sticky", left, zIndex: 2 });
|
|
38
|
+
left += column.width ?? 0;
|
|
39
|
+
}
|
|
40
|
+
let right = 0;
|
|
41
|
+
for (const column of [...columns].reverse()) {
|
|
42
|
+
if (column.pinned !== "right") continue;
|
|
43
|
+
styles.set(column.id, { position: "sticky", right, zIndex: 2 });
|
|
44
|
+
right += column.width ?? 0;
|
|
45
|
+
}
|
|
46
|
+
return styles;
|
|
47
|
+
}
|
|
48
|
+
function nextSort(current, columnId) {
|
|
49
|
+
if (!current || current.columnId !== columnId) return { columnId, direction: "asc" };
|
|
50
|
+
if (current.direction === "asc") return { columnId, direction: "desc" };
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function DataTable({
|
|
54
|
+
columns,
|
|
55
|
+
rows,
|
|
56
|
+
getRowId,
|
|
57
|
+
loading = false,
|
|
58
|
+
loadingRowCount = 5,
|
|
59
|
+
empty,
|
|
60
|
+
sort,
|
|
61
|
+
onSortChange,
|
|
62
|
+
selectable = false,
|
|
63
|
+
selectedIds,
|
|
64
|
+
onSelectedIdsChange,
|
|
65
|
+
isRowSelectable,
|
|
66
|
+
visibleColumns,
|
|
67
|
+
onRowClick,
|
|
68
|
+
rowClassName,
|
|
69
|
+
isRowActive,
|
|
70
|
+
stickyHeader = true,
|
|
71
|
+
minWidth,
|
|
72
|
+
density = "comfortable",
|
|
73
|
+
labels: labelsProp,
|
|
74
|
+
className,
|
|
75
|
+
containerClassName,
|
|
76
|
+
"aria-label": ariaLabel,
|
|
77
|
+
"aria-labelledby": ariaLabelledby
|
|
78
|
+
}) {
|
|
79
|
+
const labels = { ...DEFAULT_DATA_TABLE_LABELS, ...labelsProp };
|
|
80
|
+
const shownColumns = visibleColumns ? columns.filter((column) => visibleColumns.includes(column.id)) : columns;
|
|
81
|
+
const pinnedStyles = getPinnedOffsets(shownColumns, selectable);
|
|
82
|
+
const colCount = shownColumns.length + (selectable ? 1 : 0);
|
|
83
|
+
const selected = selectedIds ?? [];
|
|
84
|
+
const selectableIds = [];
|
|
85
|
+
rows.forEach((row, index) => {
|
|
86
|
+
if (isRowSelectable?.(row, index) ?? true) selectableIds.push(getRowId(row, index));
|
|
87
|
+
});
|
|
88
|
+
const allSelected = selectableIds.length > 0 && selectableIds.every((id) => selected.includes(id));
|
|
89
|
+
const someSelected = !allSelected && selectableIds.some((id) => selected.includes(id));
|
|
90
|
+
const toggleAll = () => {
|
|
91
|
+
if (!onSelectedIdsChange) return;
|
|
92
|
+
if (allSelected) {
|
|
93
|
+
onSelectedIdsChange(selected.filter((id) => !selectableIds.includes(id)));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
onSelectedIdsChange([.../* @__PURE__ */ new Set([...selected, ...selectableIds])]);
|
|
97
|
+
};
|
|
98
|
+
const toggleRow = (id) => {
|
|
99
|
+
if (!onSelectedIdsChange) return;
|
|
100
|
+
onSelectedIdsChange(selected.includes(id) ? selected.filter((item) => item !== id) : [...selected, id]);
|
|
101
|
+
};
|
|
102
|
+
const cellPadding = density === "compact" ? "px-3 py-1.5" : "px-4 py-3";
|
|
103
|
+
return /* @__PURE__ */ jsx(
|
|
104
|
+
"div",
|
|
105
|
+
{
|
|
106
|
+
"data-slot": "data-table-container",
|
|
107
|
+
className: cn("relative w-full overflow-auto rounded-xl border border-border bg-card", containerClassName),
|
|
108
|
+
children: /* @__PURE__ */ jsxs(
|
|
109
|
+
"table",
|
|
110
|
+
{
|
|
111
|
+
"data-slot": "data-table",
|
|
112
|
+
"aria-label": ariaLabel,
|
|
113
|
+
"aria-labelledby": ariaLabelledby,
|
|
114
|
+
"aria-busy": loading || void 0,
|
|
115
|
+
className: cn("w-full caption-bottom text-sm", className),
|
|
116
|
+
style: minWidth != null ? { minWidth } : void 0,
|
|
117
|
+
children: [
|
|
118
|
+
/* @__PURE__ */ jsx(TableHeader, { className: cn(stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs(TableRow, { className: "bg-muted hover:bg-muted", children: [
|
|
119
|
+
selectable && /* @__PURE__ */ jsx(
|
|
120
|
+
TableHead,
|
|
121
|
+
{
|
|
122
|
+
className: "w-11 bg-inherit",
|
|
123
|
+
style: { position: "sticky", left: 0, zIndex: 3, width: 44, minWidth: 44 },
|
|
124
|
+
children: /* @__PURE__ */ jsx(
|
|
125
|
+
Checkbox,
|
|
126
|
+
{
|
|
127
|
+
checked: allSelected ? true : someSelected ? "indeterminate" : false,
|
|
128
|
+
onCheckedChange: toggleAll,
|
|
129
|
+
disabled: selectableIds.length === 0,
|
|
130
|
+
"aria-label": allSelected ? labels.deselectAllRows : labels.selectAllRows
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
shownColumns.map((column) => {
|
|
136
|
+
const isSorted = sort?.columnId === column.id;
|
|
137
|
+
const SortIcon = isSorted ? sort?.direction === "asc" ? ArrowUp : ArrowDown : ChevronsUpDown;
|
|
138
|
+
const pinnedStyle = pinnedStyles.get(column.id);
|
|
139
|
+
return /* @__PURE__ */ jsx(
|
|
140
|
+
TableHead,
|
|
141
|
+
{
|
|
142
|
+
scope: "col",
|
|
143
|
+
"aria-sort": isSorted ? sort?.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
144
|
+
className: cn(
|
|
145
|
+
alignClasses[column.align ?? "left"],
|
|
146
|
+
pinnedStyle && "bg-inherit",
|
|
147
|
+
column.headerClassName
|
|
148
|
+
),
|
|
149
|
+
style: {
|
|
150
|
+
...pinnedStyle,
|
|
151
|
+
width: column.width,
|
|
152
|
+
minWidth: column.minWidth ?? column.width
|
|
153
|
+
},
|
|
154
|
+
children: column.sortable && onSortChange ? /* @__PURE__ */ jsxs(
|
|
155
|
+
"button",
|
|
156
|
+
{
|
|
157
|
+
type: "button",
|
|
158
|
+
onClick: () => onSortChange(nextSort(sort, column.id)),
|
|
159
|
+
className: cn(
|
|
160
|
+
"-mx-1 inline-flex items-center gap-1 rounded-sm px-1 py-0.5 uppercase transition-colors hover:text-foreground",
|
|
161
|
+
column.align === "right" && "flex-row-reverse",
|
|
162
|
+
isSorted && "text-foreground"
|
|
163
|
+
),
|
|
164
|
+
children: [
|
|
165
|
+
column.header,
|
|
166
|
+
/* @__PURE__ */ jsx(SortIcon, { "aria-hidden": "true", className: cn("size-3.5", !isSorted && "opacity-50") })
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
) : column.header
|
|
170
|
+
},
|
|
171
|
+
column.id
|
|
172
|
+
);
|
|
173
|
+
})
|
|
174
|
+
] }) }),
|
|
175
|
+
/* @__PURE__ */ jsxs(TableBody, { children: [
|
|
176
|
+
loading && Array.from({ length: loadingRowCount }).map((_, rowIndex) => (
|
|
177
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: dòng khung xương không có danh tính dữ liệu
|
|
178
|
+
/* @__PURE__ */ jsxs(TableRow, { className: "hover:bg-transparent", children: [
|
|
179
|
+
selectable && /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx(Skeleton, { className: "size-4 rounded-sm" }) }),
|
|
180
|
+
shownColumns.map((column) => /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full max-w-[12rem]" }) }, column.id))
|
|
181
|
+
] }, `skeleton-${rowIndex}`)
|
|
182
|
+
)),
|
|
183
|
+
!loading && rows.length === 0 && /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: /* @__PURE__ */ jsx(TableCell, { colSpan: colCount, className: "p-0", children: empty ?? /* @__PURE__ */ jsx(EmptyState, { icon: Inbox, title: labels.emptyTitle, description: labels.emptyDescription }) }) }),
|
|
184
|
+
!loading && rows.map((row, rowIndex) => {
|
|
185
|
+
const id = getRowId(row, rowIndex);
|
|
186
|
+
const isSelected = selected.includes(id);
|
|
187
|
+
const canSelect = isRowSelectable?.(row, rowIndex) ?? true;
|
|
188
|
+
const active = isRowActive?.(row, rowIndex) ?? false;
|
|
189
|
+
return /* @__PURE__ */ jsxs(
|
|
190
|
+
TableRow,
|
|
191
|
+
{
|
|
192
|
+
"data-state": isSelected ? "selected" : void 0,
|
|
193
|
+
"aria-selected": selectable ? isSelected : void 0,
|
|
194
|
+
onClick: onRowClick ? () => onRowClick(row, rowIndex) : void 0,
|
|
195
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
196
|
+
onKeyDown: onRowClick ? (event) => {
|
|
197
|
+
if (event.target !== event.currentTarget) return;
|
|
198
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
onRowClick(row, rowIndex);
|
|
201
|
+
} : void 0,
|
|
202
|
+
className: cn(
|
|
203
|
+
// bg-card trên <tr> để ô ghim (`bg-inherit`) có màu đặc mà vẫn đổi theo
|
|
204
|
+
// trạng thái chọn/hover của chính dòng đó.
|
|
205
|
+
"bg-card",
|
|
206
|
+
// Viền focus vẽ bằng `outline` chứ không `ring`: <tr> nằm trong bảng
|
|
207
|
+
// border-collapse, box-shadow không được vẽ. Offset ÂM để viền nằm trong
|
|
208
|
+
// lòng dòng, không bị khung cuộn cắt mất ở mép trái/phải.
|
|
209
|
+
// Màu dùng `foreground` chứ không `ring` (gold #E8B824 chỉ ~1,9:1 trên nền
|
|
210
|
+
// card — dưới 3:1 của WCAG 1.4.11 cho thành phần phi-văn-bản).
|
|
211
|
+
onRowClick && "cursor-pointer focus-visible:outline-2 focus-visible:outline-foreground focus-visible:[outline-offset:-2px]",
|
|
212
|
+
active && "bg-app-accent/5",
|
|
213
|
+
rowClassName?.(row, rowIndex)
|
|
214
|
+
),
|
|
215
|
+
children: [
|
|
216
|
+
selectable && /* @__PURE__ */ jsx(
|
|
217
|
+
TableCell,
|
|
218
|
+
{
|
|
219
|
+
className: cn(cellPadding, "bg-inherit"),
|
|
220
|
+
style: { position: "sticky", left: 0, zIndex: 1, width: 44, minWidth: 44 },
|
|
221
|
+
children: /* @__PURE__ */ jsx("span", { onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
222
|
+
Checkbox,
|
|
223
|
+
{
|
|
224
|
+
checked: isSelected,
|
|
225
|
+
disabled: !canSelect,
|
|
226
|
+
onCheckedChange: () => toggleRow(id),
|
|
227
|
+
"aria-label": isSelected ? labels.deselectRow : labels.selectRow
|
|
228
|
+
}
|
|
229
|
+
) })
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
shownColumns.map((column) => {
|
|
233
|
+
const pinnedStyle = pinnedStyles.get(column.id);
|
|
234
|
+
return /* @__PURE__ */ jsx(
|
|
235
|
+
TableCell,
|
|
236
|
+
{
|
|
237
|
+
className: cn(
|
|
238
|
+
cellPadding,
|
|
239
|
+
alignClasses[column.align ?? "left"],
|
|
240
|
+
pinnedStyle && "bg-inherit",
|
|
241
|
+
column.cellClassName
|
|
242
|
+
),
|
|
243
|
+
style: { ...pinnedStyle, width: column.width, minWidth: column.minWidth ?? column.width },
|
|
244
|
+
children: column.cell(row, rowIndex)
|
|
245
|
+
},
|
|
246
|
+
column.id
|
|
247
|
+
);
|
|
248
|
+
})
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
id
|
|
252
|
+
);
|
|
253
|
+
})
|
|
254
|
+
] })
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
function DataTableColumnToggle({
|
|
262
|
+
columns,
|
|
263
|
+
visibleColumns,
|
|
264
|
+
onVisibleColumnsChange,
|
|
265
|
+
label,
|
|
266
|
+
labels: labelsProp,
|
|
267
|
+
className
|
|
268
|
+
}) {
|
|
269
|
+
const labels = { ...DEFAULT_DATA_TABLE_LABELS, ...labelsProp };
|
|
270
|
+
const text = label ?? labels.columnToggle;
|
|
271
|
+
const hideable = columns.filter((column) => column.hideable !== false);
|
|
272
|
+
const toggle = (columnId) => {
|
|
273
|
+
const next = visibleColumns.includes(columnId) ? visibleColumns.filter((id) => id !== columnId) : columns.filter((column) => visibleColumns.includes(column.id) || column.id === columnId).map((c) => c.id);
|
|
274
|
+
if (next.length === 0) return;
|
|
275
|
+
onVisibleColumnsChange(next);
|
|
276
|
+
};
|
|
277
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
278
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className, leftIcon: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-4" }), children: text }) }),
|
|
279
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-52", children: [
|
|
280
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: text }),
|
|
281
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
282
|
+
hideable.map((column) => /* @__PURE__ */ jsx(
|
|
283
|
+
DropdownMenuCheckboxItem,
|
|
284
|
+
{
|
|
285
|
+
checked: visibleColumns.includes(column.id),
|
|
286
|
+
onCheckedChange: () => toggle(column.id),
|
|
287
|
+
onSelect: (event) => event.preventDefault(),
|
|
288
|
+
children: column.label ?? (typeof column.header === "string" ? column.header : column.id)
|
|
289
|
+
},
|
|
290
|
+
column.id
|
|
291
|
+
))
|
|
292
|
+
] })
|
|
293
|
+
] });
|
|
294
|
+
}
|
|
295
|
+
export {
|
|
296
|
+
DEFAULT_DATA_TABLE_LABELS,
|
|
297
|
+
DataTable,
|
|
298
|
+
DataTableColumnToggle
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=data-table.js.map
|
|
@@ -0,0 +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 * Cột đang hiện — controlled. Đổi bố cục đi qua `DataTableColumnToggle`, app tự lưu theo\n * người dùng (server, localStorage…).\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 visibleColumns?: 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 visibleColumns,\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 const shownColumns = visibleColumns ? columns.filter((column) => visibleColumns.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 <SortIcon aria-hidden=\"true\" className={cn(\"size-3.5\", !isSorted && \"opacity-50\")} />\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 /** Danh sách cột đang hiện — cùng nguồn với `DataTable.visibleColumns` */\n visibleColumns: string[];\n onVisibleColumnsChange: (visibleColumns: 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 visibleColumns,\n onVisibleColumnsChange,\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 const next = visibleColumns.includes(columnId)\n ? visibleColumns.filter((id) => id !== columnId)\n : columns.filter((column) => visibleColumns.includes(column.id) || column.id === columnId).map((c) => c.id);\n if (next.length === 0) return;\n onVisibleColumnsChange(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={visibleColumns.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,77 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { DateRange } from 'react-day-picker';
|
|
3
|
+
export { DateRange } from 'react-day-picker';
|
|
4
|
+
|
|
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
|
+
interface DatePickerProps {
|
|
23
|
+
value?: Date | null;
|
|
24
|
+
onValueChange: (value: Date | null) => void;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Ngày nhỏ nhất / lớn nhất được chọn */
|
|
28
|
+
minDate?: Date;
|
|
29
|
+
maxDate?: Date;
|
|
30
|
+
/** Hiện nút xoá khi đã có giá trị (mặc định true) */
|
|
31
|
+
clearable?: boolean;
|
|
32
|
+
/** Cột preset bên trái lịch (mặc định ẩn) */
|
|
33
|
+
presets?: DatePreset[] | false;
|
|
34
|
+
className?: string;
|
|
35
|
+
id?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
"aria-label"?: string;
|
|
38
|
+
"aria-describedby"?: string;
|
|
39
|
+
"aria-invalid"?: true;
|
|
40
|
+
"aria-required"?: true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ô chọn ngày: gõ tay `dd/MM/yyyy` HOẶC bấm lịch.
|
|
44
|
+
*
|
|
45
|
+
* 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,
|
|
46
|
+
* 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
|
|
47
|
+
* 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ũ
|
|
48
|
+
* khi rời ô, không âm thầm nuốt dữ liệu.
|
|
49
|
+
*/
|
|
50
|
+
declare function DatePicker({ value, onValueChange, placeholder, disabled, minDate, maxDate, clearable, presets, className, id, name, ...aria }: DatePickerProps): React.JSX.Element;
|
|
51
|
+
interface DateRangePickerProps {
|
|
52
|
+
value?: DateRange | null;
|
|
53
|
+
onValueChange: (value: DateRange | null) => void;
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
minDate?: Date;
|
|
57
|
+
maxDate?: Date;
|
|
58
|
+
clearable?: boolean;
|
|
59
|
+
/** Cột preset bên trái (mặc định DEFAULT_DATE_RANGE_PRESETS; `false` để tắt) */
|
|
60
|
+
presets?: DateRangePreset[] | false;
|
|
61
|
+
/** Số tháng hiện cùng lúc (mặc định 2 trên màn rộng) */
|
|
62
|
+
numberOfMonths?: number;
|
|
63
|
+
className?: string;
|
|
64
|
+
id?: string;
|
|
65
|
+
"aria-label"?: string;
|
|
66
|
+
"aria-describedby"?: string;
|
|
67
|
+
"aria-invalid"?: true;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Ô 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.
|
|
71
|
+
*
|
|
72
|
+
* 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
|
|
73
|
+
* 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.
|
|
74
|
+
*/
|
|
75
|
+
declare function DateRangePicker({ value, onValueChange, placeholder, disabled, minDate, maxDate, clearable, presets, numberOfMonths, className, id, ...aria }: DateRangePickerProps): React.JSX.Element;
|
|
76
|
+
|
|
77
|
+
export { DEFAULT_DATE_PRESETS, DEFAULT_DATE_RANGE_PRESETS, DatePicker, type DatePickerProps, type DatePreset, DateRangePicker, type DateRangePickerProps, type DateRangePreset, VI_DATE_FORMAT };
|