@thangnqdev/ui 0.1.2 → 0.2.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 +54 -206
- package/dist/bento/index.cjs.map +1 -1
- package/dist/bento/index.js +2 -2
- package/dist/bento/styles.css +2 -46
- package/dist/buttons/index.cjs +111 -18
- package/dist/buttons/index.cjs.map +1 -1
- package/dist/buttons/index.d.cts +1 -1
- package/dist/buttons/index.d.ts +1 -1
- package/dist/buttons/index.js +3 -3
- package/dist/buttons/styles.css +2 -6
- package/dist/{chunk-4S6L5V7X.js → chunk-2YJBW4E2.js} +2 -2
- package/dist/chunk-2YJBW4E2.js.map +1 -0
- package/dist/{chunk-F5Q7BTMH.js → chunk-4CRFNALF.js} +1011 -556
- package/dist/chunk-4CRFNALF.js.map +1 -0
- package/dist/{chunk-LWFRUJRU.js → chunk-4MS6ZHFQ.js} +225 -41
- package/dist/chunk-4MS6ZHFQ.js.map +1 -0
- package/dist/{chunk-2OXZK6IP.js → chunk-77KTYOMQ.js} +3 -3
- package/dist/chunk-77KTYOMQ.js.map +1 -0
- package/dist/{chunk-IABPUCAQ.js → chunk-ADO3SVUA.js} +2 -2
- package/dist/chunk-ADO3SVUA.js.map +1 -0
- package/dist/{chunk-GXI2H5XA.js → chunk-D7FB3TCA.js} +3 -3
- package/dist/chunk-D7FB3TCA.js.map +1 -0
- package/dist/chunk-DSEL7ELZ.js +314 -0
- package/dist/chunk-DSEL7ELZ.js.map +1 -0
- package/dist/{chunk-KJRBRTQE.js → chunk-EXFR2BFJ.js} +37 -28
- package/dist/chunk-EXFR2BFJ.js.map +1 -0
- package/dist/{chunk-PF4ZHGVW.js → chunk-GXWB6BSY.js} +116 -23
- package/dist/chunk-GXWB6BSY.js.map +1 -0
- package/dist/{chunk-66VWUJEI.js → chunk-I2FQBMVW.js} +3 -3
- package/dist/chunk-I2FQBMVW.js.map +1 -0
- package/dist/{chunk-EU2LPUYP.js → chunk-I5O2WCGF.js} +3 -3
- package/dist/chunk-I5O2WCGF.js.map +1 -0
- package/dist/{chunk-ATYGDTMK.js → chunk-ME4XILG5.js} +5 -5
- package/dist/chunk-ME4XILG5.js.map +1 -0
- package/dist/{chunk-5FZ6ZFII.js → chunk-PC5SZFZC.js} +373 -150
- package/dist/chunk-PC5SZFZC.js.map +1 -0
- package/dist/{chunk-C4SUXMI3.js → chunk-S6XJMYME.js} +3 -3
- package/dist/chunk-S6XJMYME.js.map +1 -0
- package/dist/{chunk-A7P3O5Z5.js → chunk-V52JOSDF.js} +3 -3
- package/dist/chunk-V52JOSDF.js.map +1 -0
- package/dist/{chunk-NFVONJUK.js → chunk-W5WNWTQC.js} +455 -211
- package/dist/chunk-W5WNWTQC.js.map +1 -0
- package/dist/{chunk-WAFHC4DQ.js → chunk-YJ64RI7J.js} +3 -3
- package/dist/chunk-YJ64RI7J.js.map +1 -0
- package/dist/chunk-YOCGBORV.js +155 -0
- package/dist/chunk-YOCGBORV.js.map +1 -0
- package/dist/chunk-ZEDXARXW.js +96 -0
- package/dist/chunk-ZEDXARXW.js.map +1 -0
- package/dist/components.css +2 -0
- package/dist/core/index.cjs +33 -24
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/styles.css +2 -134
- package/dist/data-display/index.cjs +770 -247
- package/dist/data-display/index.cjs.map +1 -1
- package/dist/data-display/index.d.cts +1 -1
- package/dist/data-display/index.d.ts +1 -1
- package/dist/data-display/index.js +5 -4
- package/dist/data-display/styles.css +2 -4
- package/dist/feedback/index.cjs.map +1 -1
- package/dist/feedback/index.js +2 -2
- package/dist/feedback/styles.css +2 -4
- package/dist/forms/index.cjs +1211 -459
- package/dist/forms/index.cjs.map +1 -1
- package/dist/forms/index.d.cts +1 -1
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.js +8 -8
- package/dist/forms/styles.css +2 -6
- package/dist/glass/index.cjs.map +1 -1
- package/dist/glass/index.js +2 -2
- package/dist/glass/styles.css +2 -81
- package/dist/{index-D6fE7w-b.d.cts → index-BGt_hecO.d.cts} +1 -0
- package/dist/{index-D6fE7w-b.d.ts → index-BGt_hecO.d.ts} +1 -0
- package/dist/{index-DzcWh1Hs.d.cts → index-BtDvpZCf.d.cts} +7 -0
- package/dist/{index-DzcWh1Hs.d.ts → index-BtDvpZCf.d.ts} +7 -0
- package/dist/{index-Cle6QzQl.d.ts → index-COX1sBUN.d.ts} +27 -3
- package/dist/{index-BkRqGNG1.d.ts → index-Cg8MuP7u.d.cts} +40 -13
- package/dist/{index-BkRqGNG1.d.cts → index-Cg8MuP7u.d.ts} +40 -13
- package/dist/{index-CenuF_P2.d.ts → index-DHkPRTvE.d.cts} +39 -8
- package/dist/{index-CenuF_P2.d.cts → index-DHkPRTvE.d.ts} +39 -8
- package/dist/{index-TyK-Ldpy.d.cts → index-Du_oht9k.d.cts} +27 -3
- package/dist/index.cjs +2481 -917
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +19 -19
- package/dist/layout/index.cjs.map +1 -1
- package/dist/layout/index.js +2 -2
- package/dist/layout/styles.css +2 -8
- package/dist/navigation/index.cjs +288 -48
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +1 -1
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.js +6 -6
- package/dist/navigation/styles.css +2 -4
- package/dist/overlays/index.cjs +566 -229
- package/dist/overlays/index.cjs.map +1 -1
- package/dist/overlays/index.d.cts +1 -1
- package/dist/overlays/index.d.ts +1 -1
- package/dist/overlays/index.js +7 -7
- package/dist/overlays/styles.css +2 -8
- package/dist/reset.css +45 -0
- package/dist/styles.css +1 -1
- package/dist/tokens.css +11 -0
- package/dist/typography/index.cjs.map +1 -1
- package/dist/typography/index.js +4 -4
- package/dist/typography/styles.css +2 -5
- package/llms.txt +211 -113
- package/package.json +160 -116
- package/src/bento/BentoCard.tsx +140 -140
- package/src/bento/BentoGrid.tsx +50 -50
- package/src/bento/index.ts +2 -2
- package/src/bento/tokens.css +46 -46
- package/src/buttons/Button.tsx +104 -104
- package/src/buttons/ButtonGroup.tsx +43 -43
- package/src/buttons/IconButton.tsx +79 -79
- package/src/buttons/SplitButton.tsx +186 -86
- package/src/buttons/ToggleButton.tsx +157 -157
- package/src/buttons/index.ts +5 -5
- package/src/buttons/tokens.css +6 -6
- package/src/core/ThemeProvider.tsx +135 -123
- package/src/core/index.ts +2 -2
- package/src/core/tokens.css +134 -134
- package/src/data-display/Accordion.tsx +147 -147
- package/src/data-display/Avatar.tsx +134 -134
- package/src/data-display/Card.tsx +112 -112
- package/src/data-display/Chip.tsx +74 -74
- package/src/data-display/DataTablePro.tsx +299 -121
- package/src/data-display/StatCard.tsx +60 -60
- package/src/data-display/Table.tsx +74 -74
- package/src/data-display/Timeline.tsx +62 -62
- package/src/data-display/TreeView.tsx +243 -104
- package/src/data-display/tokens.css +4 -4
- package/src/feedback/Alert.tsx +80 -80
- package/src/feedback/EmptyState.tsx +45 -45
- package/src/feedback/Skeleton.tsx +66 -66
- package/src/feedback/Spinner.tsx +65 -65
- package/src/feedback/index.ts +5 -5
- package/src/feedback/tokens.css +4 -4
- package/src/forms/Checkbox.tsx +149 -69
- package/src/forms/CheckboxGroup.tsx +76 -29
- package/src/forms/Combobox.tsx +172 -28
- package/src/forms/DatePicker.tsx +385 -81
- package/src/forms/FileUpload.tsx +51 -7
- package/src/forms/FormField.tsx +115 -115
- package/src/forms/Input.tsx +151 -151
- package/src/forms/NumberInput.tsx +34 -20
- package/src/forms/PasswordInput.tsx +35 -35
- package/src/forms/PinInput.tsx +139 -125
- package/src/forms/Radio.tsx +142 -142
- package/src/forms/RangeSlider.tsx +237 -111
- package/src/forms/SearchInput.tsx +34 -34
- package/src/forms/Select.tsx +103 -103
- package/src/forms/Slider.tsx +85 -85
- package/src/forms/Textarea.tsx +114 -114
- package/src/forms/tokens.css +6 -6
- package/src/glass/GlassButton.tsx +59 -59
- package/src/glass/GlassCard.tsx +71 -71
- package/src/glass/index.ts +3 -3
- package/src/glass/tokens.css +81 -81
- package/src/index.ts +26 -26
- package/src/layout/AspectRatio.tsx +39 -39
- package/src/layout/Container.tsx +53 -53
- package/src/layout/Divider.tsx +61 -61
- package/src/layout/Flex.tsx +79 -79
- package/src/layout/Grid.tsx +124 -124
- package/src/layout/PageLayout.tsx +59 -59
- package/src/layout/Section.tsx +47 -47
- package/src/layout/Spacer.tsx +30 -30
- package/src/layout/Stack.tsx +88 -88
- package/src/layout/tokens.css +8 -8
- package/src/navigation/Breadcrumb.tsx +73 -73
- package/src/navigation/CommandPalette.tsx +142 -142
- package/src/navigation/DropdownMenu.tsx +244 -160
- package/src/navigation/Pagination.tsx +114 -114
- package/src/navigation/Stepper.tsx +93 -93
- package/src/navigation/Tabs.tsx +265 -133
- package/src/navigation/index.ts +6 -6
- package/src/navigation/tokens.css +4 -4
- package/src/overlays/AlertDialog.tsx +63 -54
- package/src/overlays/BottomSheet.tsx +30 -10
- package/src/overlays/ContextMenu.tsx +51 -9
- package/src/overlays/Drawer.tsx +160 -145
- package/src/overlays/FocusTrap.tsx +122 -49
- package/src/overlays/Modal.tsx +296 -219
- package/src/overlays/Popover.tsx +55 -25
- package/src/overlays/Portal.tsx +23 -23
- package/src/overlays/Toast.tsx +175 -137
- package/src/overlays/Tooltip.tsx +125 -100
- package/src/overlays/tokens.css +8 -8
- package/src/registry/components.ts +1368 -0
- package/src/styles/components.css +39 -0
- package/src/styles/index.css +2 -93
- package/src/styles/reset.css +45 -0
- package/src/styles/tokens.css +11 -2
- package/src/typography/Blockquote.tsx +32 -32
- package/src/typography/Code.tsx +89 -89
- package/src/typography/Heading.tsx +56 -56
- package/src/typography/Kbd.tsx +25 -25
- package/src/typography/Label.tsx +47 -47
- package/src/typography/Link.tsx +56 -56
- package/src/typography/Text.tsx +62 -62
- package/src/typography/index.ts +7 -7
- package/src/typography/tokens.css +5 -5
- package/src/utils/cn.ts +6 -6
- package/src/utils/scroll-lock.ts +19 -0
- package/src/utils/useControllableState.ts +54 -0
- package/ui-contract.json +192 -35
- package/dist/chunk-2OXZK6IP.js.map +0 -1
- package/dist/chunk-4S6L5V7X.js.map +0 -1
- package/dist/chunk-5FZ6ZFII.js.map +0 -1
- package/dist/chunk-66VWUJEI.js.map +0 -1
- package/dist/chunk-A7P3O5Z5.js.map +0 -1
- package/dist/chunk-ATYGDTMK.js.map +0 -1
- package/dist/chunk-C4SUXMI3.js.map +0 -1
- package/dist/chunk-EU2LPUYP.js.map +0 -1
- package/dist/chunk-F5Q7BTMH.js.map +0 -1
- package/dist/chunk-GXI2H5XA.js.map +0 -1
- package/dist/chunk-H3LCNPMC.js +0 -101
- package/dist/chunk-H3LCNPMC.js.map +0 -1
- package/dist/chunk-IABPUCAQ.js.map +0 -1
- package/dist/chunk-KJRBRTQE.js.map +0 -1
- package/dist/chunk-LBUBXUYQ.js +0 -40
- package/dist/chunk-LBUBXUYQ.js.map +0 -1
- package/dist/chunk-LWFRUJRU.js.map +0 -1
- package/dist/chunk-NFVONJUK.js.map +0 -1
- package/dist/chunk-PF4ZHGVW.js.map +0 -1
- package/dist/chunk-WAFHC4DQ.js.map +0 -1
- package/dist/chunk-X76KBH4Y.js +0 -71
- package/dist/chunk-X76KBH4Y.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useMemo, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, useMemo, forwardRef, useEffect, useRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ChevronUp,
|
|
4
4
|
ChevronDown,
|
|
@@ -9,30 +9,37 @@ import {
|
|
|
9
9
|
ChevronLeft,
|
|
10
10
|
ChevronRight,
|
|
11
11
|
Filter,
|
|
12
|
+
Loader2,
|
|
12
13
|
} from 'lucide-react';
|
|
13
14
|
import { cn } from '../utils/cn';
|
|
14
15
|
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './Table';
|
|
15
16
|
import { Button } from '../buttons/Button';
|
|
16
|
-
import { Input } from '../forms/Input';
|
|
17
17
|
import { Checkbox } from '../forms/Checkbox';
|
|
18
|
-
import { Chip } from './Chip';
|
|
19
18
|
|
|
20
|
-
export interface
|
|
19
|
+
export interface SortRule {
|
|
20
|
+
key: string;
|
|
21
|
+
direction: 'asc' | 'desc';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ColumnDef<TData, TValue = any> {
|
|
21
25
|
key: string;
|
|
22
26
|
header: string | React.ReactNode;
|
|
23
|
-
accessor?: (row:
|
|
24
|
-
render?: (row:
|
|
27
|
+
accessor?: (row: TData) => TValue;
|
|
28
|
+
render?: (row: TData, index: number, value: TValue) => React.ReactNode;
|
|
25
29
|
sortable?: boolean;
|
|
30
|
+
sortComparator?: (a: TData, b: TData) => number;
|
|
26
31
|
width?: string;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
export interface DataTableProProps<
|
|
30
|
-
data:
|
|
31
|
-
columns: ColumnDef<
|
|
32
|
-
keyExtractor: (row:
|
|
34
|
+
export interface DataTableProProps<TData extends Record<string, any>> {
|
|
35
|
+
data: TData[];
|
|
36
|
+
columns: ColumnDef<TData>[];
|
|
37
|
+
keyExtractor: (row: TData) => string;
|
|
33
38
|
searchable?: boolean;
|
|
34
39
|
searchPlaceholder?: string;
|
|
35
40
|
selectable?: boolean;
|
|
41
|
+
selectedKeys?: string[];
|
|
42
|
+
defaultSelectedKeys?: string[];
|
|
36
43
|
onSelectionChange?: (selectedKeys: string[]) => void;
|
|
37
44
|
pageSize?: number;
|
|
38
45
|
pageSizeOptions?: number[];
|
|
@@ -40,15 +47,37 @@ export interface DataTableProProps<T> {
|
|
|
40
47
|
description?: string;
|
|
41
48
|
actions?: React.ReactNode;
|
|
42
49
|
className?: string;
|
|
50
|
+
loading?: boolean;
|
|
51
|
+
|
|
52
|
+
// Sorting
|
|
53
|
+
multiSort?: boolean;
|
|
54
|
+
sorting?: SortRule[] | null;
|
|
55
|
+
defaultSorting?: SortRule[];
|
|
56
|
+
onSortingChange?: (sorting: SortRule[]) => void;
|
|
57
|
+
|
|
58
|
+
// Server-side / Manual controls
|
|
59
|
+
manualPagination?: boolean;
|
|
60
|
+
manualSorting?: boolean;
|
|
61
|
+
manualFiltering?: boolean;
|
|
62
|
+
pageIndex?: number;
|
|
63
|
+
pageCount?: number;
|
|
64
|
+
totalCount?: number;
|
|
65
|
+
onPaginationChange?: (page: number, pageSize: number) => void;
|
|
66
|
+
searchQuery?: string;
|
|
67
|
+
onSearchChange?: (query: string) => void;
|
|
68
|
+
onSelectAllDataset?: () => void;
|
|
69
|
+
emptyText?: string;
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
export function DataTablePro<
|
|
72
|
+
export function DataTablePro<TData extends Record<string, any>>({
|
|
46
73
|
data,
|
|
47
74
|
columns,
|
|
48
75
|
keyExtractor,
|
|
49
76
|
searchable = true,
|
|
50
77
|
searchPlaceholder = 'Search all records...',
|
|
51
78
|
selectable = true,
|
|
79
|
+
selectedKeys: controlledSelectedKeys,
|
|
80
|
+
defaultSelectedKeys = [],
|
|
52
81
|
onSelectionChange,
|
|
53
82
|
pageSize: initialPageSize = 10,
|
|
54
83
|
pageSizeOptions = [5, 10, 25, 50],
|
|
@@ -56,73 +85,192 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
56
85
|
description,
|
|
57
86
|
actions,
|
|
58
87
|
className,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
88
|
+
loading = false,
|
|
89
|
+
|
|
90
|
+
// Multi-column sorting
|
|
91
|
+
multiSort = true,
|
|
92
|
+
sorting: controlledSorting,
|
|
93
|
+
defaultSorting = [],
|
|
94
|
+
onSortingChange,
|
|
95
|
+
|
|
96
|
+
// Server-side / manual mode props
|
|
97
|
+
manualPagination = false,
|
|
98
|
+
manualSorting = false,
|
|
99
|
+
manualFiltering = false,
|
|
100
|
+
pageIndex: controlledPageIndex,
|
|
101
|
+
pageCount: controlledPageCount,
|
|
102
|
+
totalCount: controlledTotalCount,
|
|
103
|
+
onPaginationChange,
|
|
104
|
+
searchQuery: controlledSearchQuery,
|
|
105
|
+
onSearchChange,
|
|
106
|
+
onSelectAllDataset,
|
|
107
|
+
emptyText = 'No matching records found.',
|
|
108
|
+
}: DataTableProProps<TData>) {
|
|
109
|
+
// 1. Search state
|
|
110
|
+
const [internalSearch, setInternalSearch] = useState('');
|
|
111
|
+
const search = controlledSearchQuery !== undefined ? controlledSearchQuery : internalSearch;
|
|
112
|
+
|
|
113
|
+
// 2. Multi-column sorting state
|
|
114
|
+
const [internalSorting, setInternalSorting] = useState<SortRule[]>(defaultSorting);
|
|
115
|
+
const currentSorting = controlledSorting !== undefined ? controlledSorting ?? [] : internalSorting;
|
|
116
|
+
|
|
117
|
+
// 3. Pagination state
|
|
118
|
+
const [internalCurrentPage, setInternalCurrentPage] = useState(1);
|
|
119
|
+
const currentPage = controlledPageIndex !== undefined ? controlledPageIndex : internalCurrentPage;
|
|
120
|
+
|
|
121
|
+
const [internalPageSize, setInternalPageSize] = useState(initialPageSize);
|
|
122
|
+
const pageSize = internalPageSize;
|
|
123
|
+
|
|
124
|
+
// 4. Selection state
|
|
125
|
+
const [internalSelectedKeys, setInternalSelectedKeys] = useState<string[]>(defaultSelectedKeys);
|
|
126
|
+
const isSelectedControlled = controlledSelectedKeys !== undefined;
|
|
127
|
+
const selectedKeys = isSelectedControlled ? controlledSelectedKeys : internalSelectedKeys;
|
|
128
|
+
|
|
129
|
+
const handleSelectionUpdate = (nextKeys: string[]) => {
|
|
130
|
+
if (!isSelectedControlled) {
|
|
131
|
+
setInternalSelectedKeys(nextKeys);
|
|
132
|
+
}
|
|
133
|
+
onSelectionChange?.(nextKeys);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Search filtering (Client-side when not manual)
|
|
68
137
|
const filteredData = useMemo(() => {
|
|
69
|
-
if (!search.trim()) return data;
|
|
138
|
+
if (manualFiltering || !search.trim()) return data;
|
|
70
139
|
const q = search.toLowerCase();
|
|
140
|
+
|
|
71
141
|
return data.filter((row) => {
|
|
72
|
-
return
|
|
142
|
+
return columns.some((col) => {
|
|
143
|
+
const val = col.accessor ? col.accessor(row) : row[col.key];
|
|
73
144
|
if (val === null || val === undefined) return false;
|
|
74
145
|
return String(val).toLowerCase().includes(q);
|
|
75
146
|
});
|
|
76
147
|
});
|
|
77
|
-
}, [data, search]);
|
|
148
|
+
}, [data, search, columns, manualFiltering]);
|
|
78
149
|
|
|
79
|
-
//
|
|
150
|
+
// True Multi-Column Sorting (Client-side when not manual)
|
|
80
151
|
const sortedData = useMemo(() => {
|
|
81
|
-
if (
|
|
82
|
-
const col = columns.find((c) => c.key === sortKey);
|
|
152
|
+
if (manualSorting || currentSorting.length === 0) return filteredData;
|
|
83
153
|
|
|
84
154
|
return [...filteredData].sort((a, b) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
155
|
+
for (const { key, direction } of currentSorting) {
|
|
156
|
+
const col = columns.find((c) => c.key === key);
|
|
157
|
+
let comp = 0;
|
|
158
|
+
|
|
159
|
+
if (col?.sortComparator) {
|
|
160
|
+
comp = col.sortComparator(a, b);
|
|
161
|
+
} else {
|
|
162
|
+
const valA = col?.accessor ? col.accessor(a) : a[key];
|
|
163
|
+
const valB = col?.accessor ? col.accessor(b) : b[key];
|
|
87
164
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
if (valA === valB) continue;
|
|
166
|
+
if (valA === null || valA === undefined) return 1;
|
|
167
|
+
if (valB === null || valB === undefined) return -1;
|
|
91
168
|
|
|
92
|
-
|
|
93
|
-
|
|
169
|
+
comp = valA < valB ? -1 : 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (comp !== 0) {
|
|
173
|
+
return direction === 'asc' ? comp : -comp;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
94
177
|
});
|
|
95
|
-
}, [filteredData,
|
|
178
|
+
}, [filteredData, currentSorting, columns, manualSorting]);
|
|
179
|
+
|
|
180
|
+
// Pagination calculations
|
|
181
|
+
const totalItems = controlledTotalCount !== undefined ? controlledTotalCount : sortedData.length;
|
|
182
|
+
const totalPages = controlledPageCount !== undefined
|
|
183
|
+
? Math.max(1, controlledPageCount)
|
|
184
|
+
: Math.max(1, Math.ceil(totalItems / pageSize));
|
|
185
|
+
|
|
186
|
+
// Reset/clamp page if out of range on client-side filter
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!manualPagination && currentPage > totalPages && totalPages > 0) {
|
|
189
|
+
setInternalCurrentPage(totalPages);
|
|
190
|
+
onPaginationChange?.(totalPages, pageSize);
|
|
191
|
+
}
|
|
192
|
+
}, [totalPages, currentPage, manualPagination, pageSize, onPaginationChange]);
|
|
96
193
|
|
|
97
|
-
// 3. Pagination
|
|
98
|
-
const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
|
|
99
194
|
const paginatedData = useMemo(() => {
|
|
195
|
+
if (manualPagination) return data;
|
|
100
196
|
const start = (currentPage - 1) * pageSize;
|
|
101
197
|
return sortedData.slice(start, start + pageSize);
|
|
102
|
-
}, [sortedData, currentPage, pageSize]);
|
|
198
|
+
}, [data, sortedData, currentPage, pageSize, manualPagination]);
|
|
103
199
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
200
|
+
// Sorting handler supporting Shift+Click for multi-sort
|
|
201
|
+
const handleSort = (key: string, e?: React.MouseEvent) => {
|
|
202
|
+
const isShift = e?.shiftKey || false;
|
|
203
|
+
let nextSorting: SortRule[] = [...currentSorting];
|
|
204
|
+
const existingIdx = nextSorting.findIndex((s) => s.key === key);
|
|
205
|
+
|
|
206
|
+
if (isShift && multiSort) {
|
|
207
|
+
if (existingIdx >= 0) {
|
|
208
|
+
if (nextSorting[existingIdx].direction === 'asc') {
|
|
209
|
+
nextSorting[existingIdx] = { key, direction: 'desc' };
|
|
210
|
+
} else {
|
|
211
|
+
nextSorting.splice(existingIdx, 1);
|
|
212
|
+
}
|
|
108
213
|
} else {
|
|
109
|
-
|
|
110
|
-
setSortDirection('asc');
|
|
214
|
+
nextSorting.push({ key, direction: 'asc' });
|
|
111
215
|
}
|
|
112
216
|
} else {
|
|
113
|
-
|
|
114
|
-
|
|
217
|
+
if (existingIdx >= 0 && nextSorting.length === 1) {
|
|
218
|
+
if (nextSorting[0].direction === 'asc') {
|
|
219
|
+
nextSorting = [{ key, direction: 'desc' }];
|
|
220
|
+
} else {
|
|
221
|
+
nextSorting = [];
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
nextSorting = [{ key, direction: 'asc' }];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (controlledSorting === undefined) {
|
|
229
|
+
setInternalSorting(nextSorting);
|
|
230
|
+
}
|
|
231
|
+
onSortingChange?.(nextSorting);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handlePageChange = (newPage: number) => {
|
|
235
|
+
const clamped = Math.max(1, Math.min(totalPages, newPage));
|
|
236
|
+
if (controlledPageIndex === undefined) {
|
|
237
|
+
setInternalCurrentPage(clamped);
|
|
238
|
+
}
|
|
239
|
+
onPaginationChange?.(clamped, pageSize);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const handlePageSizeChange = (newSize: number) => {
|
|
243
|
+
setInternalPageSize(newSize);
|
|
244
|
+
if (controlledPageIndex === undefined) {
|
|
245
|
+
setInternalCurrentPage(1);
|
|
246
|
+
}
|
|
247
|
+
onPaginationChange?.(1, newSize);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleSearchChange = (query: string) => {
|
|
251
|
+
if (controlledSearchQuery === undefined) {
|
|
252
|
+
setInternalSearch(query);
|
|
253
|
+
if (controlledPageIndex === undefined) {
|
|
254
|
+
setInternalCurrentPage(1);
|
|
255
|
+
}
|
|
115
256
|
}
|
|
257
|
+
onSearchChange?.(query);
|
|
116
258
|
};
|
|
117
259
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
260
|
+
// Cross-page selection logic
|
|
261
|
+
const currentPageKeys = useMemo(() => paginatedData.map(keyExtractor), [paginatedData, keyExtractor]);
|
|
262
|
+
const isAllCurrentPageSelected =
|
|
263
|
+
currentPageKeys.length > 0 && currentPageKeys.every((k) => selectedKeys.includes(k));
|
|
264
|
+
const isSomeCurrentPageSelected =
|
|
265
|
+
currentPageKeys.some((k) => selectedKeys.includes(k)) && !isAllCurrentPageSelected;
|
|
266
|
+
|
|
267
|
+
const toggleSelectAllCurrentPage = () => {
|
|
268
|
+
if (isAllCurrentPageSelected) {
|
|
269
|
+
const next = selectedKeys.filter((k) => !currentPageKeys.includes(k));
|
|
270
|
+
handleSelectionUpdate(next);
|
|
122
271
|
} else {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
onSelectionChange?.(allKeys);
|
|
272
|
+
const combined = Array.from(new Set([...selectedKeys, ...currentPageKeys]));
|
|
273
|
+
handleSelectionUpdate(combined);
|
|
126
274
|
}
|
|
127
275
|
};
|
|
128
276
|
|
|
@@ -130,13 +278,28 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
130
278
|
const next = selectedKeys.includes(key)
|
|
131
279
|
? selectedKeys.filter((k) => k !== key)
|
|
132
280
|
: [...selectedKeys, key];
|
|
133
|
-
|
|
134
|
-
|
|
281
|
+
handleSelectionUpdate(next);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const canSelectAllDataset = !manualPagination || typeof onSelectAllDataset === 'function';
|
|
285
|
+
|
|
286
|
+
const selectAllDataset = () => {
|
|
287
|
+
if (manualPagination) {
|
|
288
|
+
onSelectAllDataset?.();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const allKeys = sortedData.map(keyExtractor);
|
|
292
|
+
handleSelectionUpdate(allKeys);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const clearSelection = () => {
|
|
296
|
+
handleSelectionUpdate([]);
|
|
135
297
|
};
|
|
136
298
|
|
|
137
299
|
const exportCSV = () => {
|
|
300
|
+
const exportRows = manualPagination ? data : sortedData;
|
|
138
301
|
const headers = columns.map((c) => (typeof c.header === 'string' ? c.header : c.key));
|
|
139
|
-
const rows =
|
|
302
|
+
const rows = exportRows.map((row) =>
|
|
140
303
|
columns.map((c) => {
|
|
141
304
|
const val = c.accessor ? c.accessor(row) : row[c.key];
|
|
142
305
|
return `"${String(val ?? '').replace(/"/g, '""')}"`;
|
|
@@ -152,9 +315,6 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
152
315
|
document.body.removeChild(link);
|
|
153
316
|
};
|
|
154
317
|
|
|
155
|
-
const isAllSelected =
|
|
156
|
-
paginatedData.length > 0 && selectedKeys.length === paginatedData.length;
|
|
157
|
-
|
|
158
318
|
return (
|
|
159
319
|
<div className={cn('w-full space-y-4', className)}>
|
|
160
320
|
{/* Header Toolbar */}
|
|
@@ -174,10 +334,7 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
174
334
|
type="text"
|
|
175
335
|
placeholder={searchPlaceholder}
|
|
176
336
|
value={search}
|
|
177
|
-
onChange={(e) =>
|
|
178
|
-
setSearch(e.target.value);
|
|
179
|
-
setCurrentPage(1);
|
|
180
|
-
}}
|
|
337
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
181
338
|
className="w-full h-8 pl-8 pr-3 text-xs rounded-xl bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] hover:border-[var(--ui-border-strong)] focus:outline-none focus:ring-2 focus:ring-[var(--ui-primary)]/40 text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] transition-all shadow-inner"
|
|
182
339
|
/>
|
|
183
340
|
</div>
|
|
@@ -191,20 +348,26 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
191
348
|
</div>
|
|
192
349
|
</div>
|
|
193
350
|
|
|
194
|
-
{/* Bulk selection
|
|
351
|
+
{/* Bulk selection banner */}
|
|
195
352
|
{selectedKeys.length > 0 && (
|
|
196
|
-
<div className="px-4 py-2 rounded-xl bg-[var(--ui-primary)]/15 border border-[var(--ui-primary)]/30 flex items-center justify-between animate-in fade-in duration-200">
|
|
353
|
+
<div className="px-4 py-2 rounded-xl bg-[var(--ui-primary)]/15 border border-[var(--ui-primary)]/30 flex flex-wrap items-center justify-between gap-2 animate-in fade-in duration-200">
|
|
197
354
|
<div className="flex items-center gap-2 text-xs font-semibold text-[var(--ui-primary)]">
|
|
198
|
-
<span>Selected {selectedKeys.length}
|
|
355
|
+
<span>Selected {selectedKeys.length} records</span>
|
|
356
|
+
{canSelectAllDataset && selectedKeys.length < totalItems && (
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
onClick={selectAllDataset}
|
|
360
|
+
className="underline hover:text-[var(--ui-primary)]/80 cursor-pointer font-bold ml-1"
|
|
361
|
+
>
|
|
362
|
+
Select all {totalItems} records
|
|
363
|
+
</button>
|
|
364
|
+
)}
|
|
199
365
|
</div>
|
|
200
366
|
<Button
|
|
201
367
|
size="xs"
|
|
202
368
|
variant="ghost"
|
|
203
369
|
className="text-xs text-rose-500 hover:text-rose-600 hover:bg-rose-500/10"
|
|
204
|
-
onClick={
|
|
205
|
-
setSelectedKeys([]);
|
|
206
|
-
onSelectionChange?.([]);
|
|
207
|
-
}}
|
|
370
|
+
onClick={clearSelection}
|
|
208
371
|
>
|
|
209
372
|
Clear Selection
|
|
210
373
|
</Button>
|
|
@@ -213,55 +376,75 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
213
376
|
|
|
214
377
|
{/* Table Container */}
|
|
215
378
|
<div className="relative w-full overflow-x-auto rounded-2xl border border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)] shadow-sm">
|
|
379
|
+
{loading && (
|
|
380
|
+
<div className="absolute inset-0 z-20 bg-[var(--ui-bg-surface)]/60 backdrop-blur-xs flex items-center justify-center">
|
|
381
|
+
<Loader2 className="w-6 h-6 animate-spin text-[var(--ui-primary)]" />
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
|
|
216
385
|
<Table>
|
|
217
386
|
<TableHeader>
|
|
218
387
|
<TableRow>
|
|
219
388
|
{selectable && (
|
|
220
389
|
<TableHead className="w-10 px-4">
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
390
|
+
<Checkbox
|
|
391
|
+
checked={isAllCurrentPageSelected}
|
|
392
|
+
indeterminate={isSomeCurrentPageSelected}
|
|
393
|
+
onCheckedChange={toggleSelectAllCurrentPage}
|
|
394
|
+
aria-label="Select all on page"
|
|
226
395
|
/>
|
|
227
396
|
</TableHead>
|
|
228
397
|
)}
|
|
229
398
|
|
|
230
|
-
{columns.map((col) =>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
col.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<span className="text-[var(--ui-text-muted)]">
|
|
243
|
-
{sortKey === col.key ? (
|
|
244
|
-
sortDirection === 'asc' ? (
|
|
245
|
-
<ChevronUp className="w-3.5 h-3.5 text-[var(--ui-primary)] font-bold" />
|
|
246
|
-
) : (
|
|
247
|
-
<ChevronDown className="w-3.5 h-3.5 text-[var(--ui-primary)] font-bold" />
|
|
248
|
-
)
|
|
249
|
-
) : (
|
|
250
|
-
<ChevronsUpDown className="w-3 h-3 opacity-40" />
|
|
251
|
-
)}
|
|
252
|
-
</span>
|
|
399
|
+
{columns.map((col) => {
|
|
400
|
+
const sortIndex = currentSorting.findIndex((s) => s.key === col.key);
|
|
401
|
+
const sortRule = sortIndex >= 0 ? currentSorting[sortIndex] : null;
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<TableHead
|
|
405
|
+
key={col.key}
|
|
406
|
+
style={{ width: col.width }}
|
|
407
|
+
onClick={(e) => col.sortable !== false && handleSort(col.key, e)}
|
|
408
|
+
className={cn(
|
|
409
|
+
col.sortable !== false &&
|
|
410
|
+
'cursor-pointer select-none hover:text-[var(--ui-text-primary)] transition-colors'
|
|
253
411
|
)}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
412
|
+
>
|
|
413
|
+
<div className="flex items-center gap-1.5">
|
|
414
|
+
<span>{col.header}</span>
|
|
415
|
+
{col.sortable !== false && (
|
|
416
|
+
<div className="flex items-center gap-0.5 text-[var(--ui-text-muted)]">
|
|
417
|
+
{sortRule ? (
|
|
418
|
+
sortRule.direction === 'asc' ? (
|
|
419
|
+
<ChevronUp className="w-3.5 h-3.5 text-[var(--ui-primary)] font-bold" />
|
|
420
|
+
) : (
|
|
421
|
+
<ChevronDown className="w-3.5 h-3.5 text-[var(--ui-primary)] font-bold" />
|
|
422
|
+
)
|
|
423
|
+
) : (
|
|
424
|
+
<ChevronsUpDown className="w-3 h-3 opacity-40" />
|
|
425
|
+
)}
|
|
426
|
+
{currentSorting.length > 1 && sortIndex >= 0 && (
|
|
427
|
+
<span className="text-[10px] font-mono font-bold text-[var(--ui-primary)]">
|
|
428
|
+
{sortIndex + 1}
|
|
429
|
+
</span>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
</TableHead>
|
|
435
|
+
);
|
|
436
|
+
})}
|
|
257
437
|
</TableRow>
|
|
258
438
|
</TableHeader>
|
|
259
439
|
|
|
260
440
|
<TableBody>
|
|
261
441
|
{paginatedData.length === 0 ? (
|
|
262
442
|
<TableRow>
|
|
263
|
-
<TableCell
|
|
264
|
-
|
|
443
|
+
<TableCell
|
|
444
|
+
colSpan={columns.length + (selectable ? 1 : 0)}
|
|
445
|
+
className="h-32 text-center text-xs text-[var(--ui-text-muted)]"
|
|
446
|
+
>
|
|
447
|
+
{emptyText}
|
|
265
448
|
</TableCell>
|
|
266
449
|
</TableRow>
|
|
267
450
|
) : (
|
|
@@ -277,21 +460,17 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
277
460
|
>
|
|
278
461
|
{selectable && (
|
|
279
462
|
<TableCell className="w-10 px-4">
|
|
280
|
-
<
|
|
281
|
-
type="checkbox"
|
|
463
|
+
<Checkbox
|
|
282
464
|
checked={isSelected}
|
|
283
|
-
|
|
284
|
-
|
|
465
|
+
onCheckedChange={() => toggleSelectRow(key)}
|
|
466
|
+
aria-label={`Select row ${key}`}
|
|
285
467
|
/>
|
|
286
468
|
</TableCell>
|
|
287
469
|
)}
|
|
288
470
|
|
|
289
471
|
{columns.map((col) => {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
: col.accessor
|
|
293
|
-
? col.accessor(row)
|
|
294
|
-
: row[col.key];
|
|
472
|
+
const val = col.accessor ? col.accessor(row) : row[col.key];
|
|
473
|
+
const cellContent = col.render ? col.render(row, rowIdx, val) : val;
|
|
295
474
|
|
|
296
475
|
return <TableCell key={col.key}>{cellContent}</TableCell>;
|
|
297
476
|
})}
|
|
@@ -309,10 +488,7 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
309
488
|
<span>Rows per page:</span>
|
|
310
489
|
<select
|
|
311
490
|
value={pageSize}
|
|
312
|
-
onChange={(e) =>
|
|
313
|
-
setPageSize(Number(e.target.value));
|
|
314
|
-
setCurrentPage(1);
|
|
315
|
-
}}
|
|
491
|
+
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
|
|
316
492
|
className="px-2 py-1 rounded-lg bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] text-[var(--ui-text-primary)] font-mono text-xs focus:outline-none"
|
|
317
493
|
>
|
|
318
494
|
{pageSizeOptions.map((opt) => (
|
|
@@ -322,8 +498,8 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
322
498
|
))}
|
|
323
499
|
</select>
|
|
324
500
|
<span className="text-[var(--ui-text-muted)]">
|
|
325
|
-
Showing {Math.min((currentPage - 1) * pageSize + 1,
|
|
326
|
-
{Math.min(currentPage * pageSize,
|
|
501
|
+
Showing {totalItems === 0 ? 0 : Math.min((currentPage - 1) * pageSize + 1, totalItems)}–
|
|
502
|
+
{Math.min(currentPage * pageSize, totalItems)} of {totalItems}
|
|
327
503
|
</span>
|
|
328
504
|
</div>
|
|
329
505
|
|
|
@@ -331,8 +507,9 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
331
507
|
<Button
|
|
332
508
|
size="xs"
|
|
333
509
|
variant="outline"
|
|
334
|
-
disabled={currentPage
|
|
335
|
-
onClick={() =>
|
|
510
|
+
disabled={currentPage <= 1 || loading}
|
|
511
|
+
onClick={() => handlePageChange(currentPage - 1)}
|
|
512
|
+
aria-label="Previous page"
|
|
336
513
|
>
|
|
337
514
|
<ChevronLeft className="w-3.5 h-3.5" />
|
|
338
515
|
</Button>
|
|
@@ -344,8 +521,9 @@ export function DataTablePro<T extends Record<string, any>>({
|
|
|
344
521
|
<Button
|
|
345
522
|
size="xs"
|
|
346
523
|
variant="outline"
|
|
347
|
-
disabled={currentPage
|
|
348
|
-
onClick={() =>
|
|
524
|
+
disabled={currentPage >= totalPages || loading}
|
|
525
|
+
onClick={() => handlePageChange(currentPage + 1)}
|
|
526
|
+
aria-label="Next page"
|
|
349
527
|
>
|
|
350
528
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
351
529
|
</Button>
|