@requence/table 0.0.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 +108 -0
- package/dist/VirtualTable.d.ts +94 -0
- package/dist/VirtualTable.d.ts.map +1 -0
- package/dist/VirtualTable.js +411 -0
- package/dist/VirtualTable.js.map +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +9 -0
- package/dist/useTableCache.d.ts +71 -0
- package/dist/useTableCache.d.ts.map +1 -0
- package/dist/useTableCache.js +232 -0
- package/dist/useTableCache.js.map +10 -0
- package/dist/useTableColumnWidths.d.ts +20 -0
- package/dist/useTableColumnWidths.d.ts.map +1 -0
- package/dist/useTableColumnWidths.js +45 -0
- package/dist/useTableColumnWidths.js.map +10 -0
- package/package.json +64 -0
- package/src/VirtualTable.tsx +673 -0
- package/src/index.ts +26 -0
- package/src/useTableCache.ts +424 -0
- package/src/useTableColumnWidths.ts +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 requence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @requence/table
|
|
2
|
+
|
|
3
|
+
Headless virtualized table with compound component API, Suspense-compatible data caching, and column width persistence for React.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @requence/table
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
This package uses [Tailwind CSS](https://tailwindcss.com/) utility classes internally (via `tailwind-merge`). Your project must have Tailwind CSS configured for the component to render correctly.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### VirtualTable
|
|
18
|
+
|
|
19
|
+
A compound component for rendering large datasets with virtual scrolling and CSS Grid layout.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { VirtualTable } from '@requence/table'
|
|
23
|
+
|
|
24
|
+
function MyTable({ cache }) {
|
|
25
|
+
return (
|
|
26
|
+
<VirtualTable
|
|
27
|
+
totalCount={cache.totalCount}
|
|
28
|
+
rowHeight={32}
|
|
29
|
+
onRangeChange={cache.handleRangeChange}
|
|
30
|
+
>
|
|
31
|
+
<VirtualTable.Header>
|
|
32
|
+
<VirtualTable.Column width="2fr" resizable>Name</VirtualTable.Column>
|
|
33
|
+
<VirtualTable.Column width="1fr">Email</VirtualTable.Column>
|
|
34
|
+
<VirtualTable.Column width={100}>Status</VirtualTable.Column>
|
|
35
|
+
</VirtualTable.Header>
|
|
36
|
+
|
|
37
|
+
<VirtualTable.Body>
|
|
38
|
+
{(index) => {
|
|
39
|
+
const item = cache.getItem(index)
|
|
40
|
+
if (!item) return null
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<VirtualTable.Row>
|
|
44
|
+
<VirtualTable.Cell>{item.name}</VirtualTable.Cell>
|
|
45
|
+
<VirtualTable.Cell>{item.email}</VirtualTable.Cell>
|
|
46
|
+
<VirtualTable.Cell>{item.status}</VirtualTable.Cell>
|
|
47
|
+
</VirtualTable.Row>
|
|
48
|
+
)
|
|
49
|
+
}}
|
|
50
|
+
</VirtualTable.Body>
|
|
51
|
+
|
|
52
|
+
<VirtualTable.Empty>No data found.</VirtualTable.Empty>
|
|
53
|
+
|
|
54
|
+
<VirtualTable.Footer>
|
|
55
|
+
{({ start, end }) => `Showing rows ${start}–${end}`}
|
|
56
|
+
</VirtualTable.Footer>
|
|
57
|
+
</VirtualTable>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### useTableCache
|
|
63
|
+
|
|
64
|
+
Suspense-compatible paginated data cache. The first fetch suspends the component; subsequent page fetches are non-blocking.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useTableCache } from '@requence/table'
|
|
68
|
+
|
|
69
|
+
const cache = useTableCache('users', {
|
|
70
|
+
pageSize: 50,
|
|
71
|
+
getItemId: (item) => item.id,
|
|
72
|
+
compare: (a, b) => a.name.localeCompare(b.name),
|
|
73
|
+
fetchItems: async (offset, limit) => {
|
|
74
|
+
const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
|
|
75
|
+
const { items, total } = await res.json()
|
|
76
|
+
return { items, total }
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### useTableColumnWidths
|
|
82
|
+
|
|
83
|
+
Persist user-resized column widths to localStorage.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useTableColumnWidths } from '@requence/table'
|
|
87
|
+
|
|
88
|
+
const { register, reset } = useTableColumnWidths({ persist: 'my-table' })
|
|
89
|
+
|
|
90
|
+
<VirtualTable.Column {...register('name', { defaultValue: '2fr', relative: true })}>
|
|
91
|
+
Name
|
|
92
|
+
</VirtualTable.Column>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Exports
|
|
96
|
+
|
|
97
|
+
All exports are available from the package root:
|
|
98
|
+
|
|
99
|
+
| Export | Type | Description |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `VirtualTable` | Component | Compound component (`.Header`, `.Column`, `.Body`, `.Row`, `.Cell`, `.SkeletonRow`, `.Empty`, `.Footer`) |
|
|
102
|
+
| `createTable*` | Functions | Factory functions for creating slot components with baked-in defaults |
|
|
103
|
+
| `useTableCache` | Hook | Suspense-compatible paginated data cache |
|
|
104
|
+
| `useTableColumnWidths` | Hook | Column width persistence with localStorage |
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type CSSProperties, type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
export interface VirtualTableProps {
|
|
3
|
+
/** Total number of rows in the dataset */
|
|
4
|
+
totalCount: number;
|
|
5
|
+
/** Fixed height of each row in pixels */
|
|
6
|
+
rowHeight: number;
|
|
7
|
+
/** Number of extra rows rendered above/below viewport (default: 5) */
|
|
8
|
+
overscan?: number;
|
|
9
|
+
/** Called when the visible row range changes (for triggering page fetches) */
|
|
10
|
+
onRangeChange?: (range: {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
/** Additional className for the outer container */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Accessible label for the table */
|
|
17
|
+
'aria-label'?: string;
|
|
18
|
+
/** Additional inline styles for the outer container */
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
export interface VirtualTableHeaderProps {
|
|
23
|
+
className?: string;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
export interface VirtualTableColumnProps {
|
|
27
|
+
/** Column width. Number for pixels, string for CSS grid values (e.g. '1fr'). Defaults to '1fr'. */
|
|
28
|
+
width?: number | string;
|
|
29
|
+
/** Optional className for the header cell */
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Whether this column can be resized by dragging. Default: false */
|
|
32
|
+
resizable?: boolean;
|
|
33
|
+
/** Minimum width in pixels during resize. Default: 50 */
|
|
34
|
+
minWidth?: number;
|
|
35
|
+
/** Maximum width in pixels during resize */
|
|
36
|
+
maxWidth?: number;
|
|
37
|
+
/** Mark this column as transparent — the row background will not extend behind it. */
|
|
38
|
+
transparent?: boolean;
|
|
39
|
+
/** Called when a resize drag starts */
|
|
40
|
+
onResizeStart?: () => void;
|
|
41
|
+
/** Called when a resize drag ends with the final pixel width, original width, and equivalent fr value */
|
|
42
|
+
onResizeEnd?: (width: number, startWidth: number, frValue: number) => void;
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
export interface VirtualTableBodyProps {
|
|
46
|
+
children: (index: number) => ReactNode | null;
|
|
47
|
+
}
|
|
48
|
+
export interface VirtualTableRowProps extends ComponentProps<'div'> {
|
|
49
|
+
}
|
|
50
|
+
export interface VirtualTableSkeletonRowProps extends ComponentProps<'div'> {
|
|
51
|
+
}
|
|
52
|
+
export interface VirtualTableCellProps extends ComponentProps<'div'> {
|
|
53
|
+
/** Show cell content only on row hover */
|
|
54
|
+
showOnHover?: boolean;
|
|
55
|
+
/** Number of columns this cell spans */
|
|
56
|
+
colSpan?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface VirtualTableEmptyProps {
|
|
59
|
+
className?: string;
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
}
|
|
62
|
+
export interface VirtualTableFooterProps {
|
|
63
|
+
className?: string;
|
|
64
|
+
children: (range: {
|
|
65
|
+
start: number;
|
|
66
|
+
end: number;
|
|
67
|
+
}) => ReactNode;
|
|
68
|
+
}
|
|
69
|
+
interface SlotComponent<P> {
|
|
70
|
+
(props: P): ReactNode;
|
|
71
|
+
slot: string;
|
|
72
|
+
slotDefaults: Partial<P>;
|
|
73
|
+
}
|
|
74
|
+
export declare function createTableHeader(defaults?: Partial<VirtualTableHeaderProps>): SlotComponent<VirtualTableHeaderProps>;
|
|
75
|
+
export declare function createTableColumn(defaults?: Partial<VirtualTableColumnProps>): SlotComponent<VirtualTableColumnProps>;
|
|
76
|
+
export declare function createTableBody(defaults?: Partial<VirtualTableBodyProps>): SlotComponent<VirtualTableBodyProps>;
|
|
77
|
+
export declare function createTableSkeletonRow(defaults?: Partial<VirtualTableSkeletonRowProps>): SlotComponent<VirtualTableSkeletonRowProps>;
|
|
78
|
+
export declare function createTableEmpty(defaults?: Partial<VirtualTableEmptyProps>): SlotComponent<VirtualTableEmptyProps>;
|
|
79
|
+
export declare function createTableFooter(defaults?: Partial<VirtualTableFooterProps>): SlotComponent<VirtualTableFooterProps>;
|
|
80
|
+
export declare function createTableRow(defaults?: Partial<VirtualTableRowProps>): SlotComponent<VirtualTableRowProps>;
|
|
81
|
+
declare function VirtualTableCell({ className, style, showOnHover, colSpan, ...rest }: VirtualTableCellProps): import("react").JSX.Element;
|
|
82
|
+
declare function VirtualTableRoot({ totalCount, rowHeight, overscan, onRangeChange, className, style: styleProp, 'aria-label': ariaLabel, children, }: VirtualTableProps): import("react").JSX.Element;
|
|
83
|
+
export declare const VirtualTable: typeof VirtualTableRoot & {
|
|
84
|
+
Header: SlotComponent<VirtualTableHeaderProps>;
|
|
85
|
+
Column: SlotComponent<VirtualTableColumnProps>;
|
|
86
|
+
Body: SlotComponent<VirtualTableBodyProps>;
|
|
87
|
+
SkeletonRow: SlotComponent<VirtualTableSkeletonRowProps>;
|
|
88
|
+
Row: SlotComponent<VirtualTableRowProps>;
|
|
89
|
+
Cell: typeof VirtualTableCell;
|
|
90
|
+
Empty: SlotComponent<VirtualTableEmptyProps>;
|
|
91
|
+
Footer: SlotComponent<VirtualTableFooterProps>;
|
|
92
|
+
};
|
|
93
|
+
export {};
|
|
94
|
+
//# sourceMappingURL=VirtualTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VirtualTable.d.ts","sourceRoot":"","sources":["../src/VirtualTable.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// src/VirtualTable.tsx
|
|
2
|
+
import {
|
|
3
|
+
Children,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
import { flushSync } from "react-dom";
|
|
11
|
+
import { twMerge } from "tailwind-merge";
|
|
12
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
13
|
+
function asSlot(slot, defaults) {
|
|
14
|
+
const Component = () => null;
|
|
15
|
+
Component.slot = slot;
|
|
16
|
+
Component.slotDefaults = defaults ?? {};
|
|
17
|
+
return Component;
|
|
18
|
+
}
|
|
19
|
+
function createTableHeader(defaults) {
|
|
20
|
+
return asSlot("header", defaults);
|
|
21
|
+
}
|
|
22
|
+
function createTableColumn(defaults) {
|
|
23
|
+
return asSlot("column", defaults);
|
|
24
|
+
}
|
|
25
|
+
function createTableBody(defaults) {
|
|
26
|
+
return asSlot("body", defaults);
|
|
27
|
+
}
|
|
28
|
+
function createTableSkeletonRow(defaults) {
|
|
29
|
+
return asSlot("skeletonRow", defaults);
|
|
30
|
+
}
|
|
31
|
+
function createTableEmpty(defaults) {
|
|
32
|
+
return asSlot("empty", defaults);
|
|
33
|
+
}
|
|
34
|
+
function createTableFooter(defaults) {
|
|
35
|
+
return asSlot("footer", defaults);
|
|
36
|
+
}
|
|
37
|
+
function createTableRow(defaults) {
|
|
38
|
+
return asSlot("row", defaults);
|
|
39
|
+
}
|
|
40
|
+
var GRID_VAR = "--vtable-grid-cols";
|
|
41
|
+
var GRID_VAR_REF = `var(${GRID_VAR})`;
|
|
42
|
+
function buildGridTemplate(columns) {
|
|
43
|
+
return columns.map((col) => {
|
|
44
|
+
if (typeof col.width === "number") {
|
|
45
|
+
return `${col.width}px`;
|
|
46
|
+
}
|
|
47
|
+
const min = col.minWidth ? `${col.minWidth}px` : "0";
|
|
48
|
+
if (typeof col.width === "string") {
|
|
49
|
+
return `minmax(${min}, ${col.width})`;
|
|
50
|
+
}
|
|
51
|
+
return `minmax(${min}, 1fr)`;
|
|
52
|
+
}).join(" ");
|
|
53
|
+
}
|
|
54
|
+
var VirtualTableHeader = asSlot("header");
|
|
55
|
+
var VirtualTableColumn = asSlot("column");
|
|
56
|
+
var VirtualTableBody = asSlot("body");
|
|
57
|
+
var VirtualTableRow = asSlot("row");
|
|
58
|
+
var VirtualTableSkeletonRow = asSlot("skeletonRow");
|
|
59
|
+
var VirtualTableEmpty = asSlot("empty");
|
|
60
|
+
var VirtualTableFooter = asSlot("footer");
|
|
61
|
+
function slotIs(child, slot) {
|
|
62
|
+
return child.type?.slot === slot;
|
|
63
|
+
}
|
|
64
|
+
function extractSlots(children) {
|
|
65
|
+
let header = null;
|
|
66
|
+
let body = null;
|
|
67
|
+
let skeletonRow = null;
|
|
68
|
+
let empty = null;
|
|
69
|
+
let footer = null;
|
|
70
|
+
Children.forEach(children, (child) => {
|
|
71
|
+
if (!isValidElement(child)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (slotIs(child, "header")) {
|
|
75
|
+
const defaults = child.type.slotDefaults ?? {};
|
|
76
|
+
const props = child.props;
|
|
77
|
+
const columns = [];
|
|
78
|
+
Children.forEach(props.children, (col) => {
|
|
79
|
+
if (isValidElement(col) && slotIs(col, "column")) {
|
|
80
|
+
const d = col.type.slotDefaults ?? {};
|
|
81
|
+
const p = col.props;
|
|
82
|
+
columns.push({
|
|
83
|
+
width: p.width ?? d.width,
|
|
84
|
+
header: p.children,
|
|
85
|
+
className: twMerge(d.className, p.className),
|
|
86
|
+
resizable: p.resizable ?? d.resizable,
|
|
87
|
+
minWidth: p.minWidth ?? d.minWidth,
|
|
88
|
+
maxWidth: p.maxWidth ?? d.maxWidth,
|
|
89
|
+
transparent: p.transparent ?? d.transparent,
|
|
90
|
+
onResizeStart: p.onResizeStart ?? d.onResizeStart,
|
|
91
|
+
onResizeEnd: p.onResizeEnd ?? d.onResizeEnd
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
header = {
|
|
96
|
+
className: twMerge(defaults.className, props.className),
|
|
97
|
+
columns
|
|
98
|
+
};
|
|
99
|
+
} else if (slotIs(child, "body")) {
|
|
100
|
+
body = child.props;
|
|
101
|
+
} else if (slotIs(child, "skeletonRow")) {
|
|
102
|
+
const defaults = child.type.slotDefaults ?? {};
|
|
103
|
+
const props = child.props;
|
|
104
|
+
skeletonRow = {
|
|
105
|
+
...defaults,
|
|
106
|
+
...props,
|
|
107
|
+
className: twMerge(defaults.className, props.className)
|
|
108
|
+
};
|
|
109
|
+
} else if (slotIs(child, "empty")) {
|
|
110
|
+
const defaults = child.type.slotDefaults ?? {};
|
|
111
|
+
const props = child.props;
|
|
112
|
+
empty = {
|
|
113
|
+
...defaults,
|
|
114
|
+
...props,
|
|
115
|
+
className: twMerge(defaults.className, props.className)
|
|
116
|
+
};
|
|
117
|
+
} else if (slotIs(child, "footer")) {
|
|
118
|
+
const defaults = child.type.slotDefaults ?? {};
|
|
119
|
+
const props = child.props;
|
|
120
|
+
footer = {
|
|
121
|
+
...defaults,
|
|
122
|
+
...props,
|
|
123
|
+
className: twMerge(defaults.className, props.className)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return { header, body, skeletonRow, empty, footer };
|
|
128
|
+
}
|
|
129
|
+
function ResizeHandle({ onMouseDown }) {
|
|
130
|
+
return /* @__PURE__ */ jsx("div", {
|
|
131
|
+
role: "separator",
|
|
132
|
+
"aria-orientation": "vertical",
|
|
133
|
+
className: "resizer absolute right-0 top-0 z-10 h-full w-1.5 cursor-col-resize",
|
|
134
|
+
onMouseDown
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function VirtualTableCell({
|
|
138
|
+
className,
|
|
139
|
+
style,
|
|
140
|
+
showOnHover,
|
|
141
|
+
colSpan,
|
|
142
|
+
...rest
|
|
143
|
+
}) {
|
|
144
|
+
return /* @__PURE__ */ jsx("div", {
|
|
145
|
+
role: "cell",
|
|
146
|
+
className: twMerge("overflow-hidden text-ellipsis whitespace-nowrap", showOnHover && "not-group-hover/row:*:delay-200 *:opacity-10 *:transition-opacity *:duration-300 *:ease-in-out group-hover/row:*:opacity-100", className),
|
|
147
|
+
style: colSpan ? { gridColumn: `span ${colSpan}`, ...style } : style,
|
|
148
|
+
...rest
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function DataRow({ index, rowHeight, rowProps, children }) {
|
|
152
|
+
const { className, style, ...restProps } = rowProps;
|
|
153
|
+
return /* @__PURE__ */ jsx("div", {
|
|
154
|
+
role: "row",
|
|
155
|
+
"aria-rowindex": index + 1,
|
|
156
|
+
className: twMerge("group/row absolute w-full", className),
|
|
157
|
+
style: {
|
|
158
|
+
height: rowHeight,
|
|
159
|
+
transform: `translateY(${index * rowHeight}px)`,
|
|
160
|
+
display: "grid",
|
|
161
|
+
gridTemplateColumns: GRID_VAR_REF,
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
willChange: "transform",
|
|
164
|
+
contain: "layout style paint",
|
|
165
|
+
...style
|
|
166
|
+
},
|
|
167
|
+
...restProps,
|
|
168
|
+
children
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function VirtualTableRoot({
|
|
172
|
+
totalCount,
|
|
173
|
+
rowHeight,
|
|
174
|
+
overscan = 5,
|
|
175
|
+
onRangeChange,
|
|
176
|
+
className,
|
|
177
|
+
style: styleProp,
|
|
178
|
+
"aria-label": ariaLabel,
|
|
179
|
+
children
|
|
180
|
+
}) {
|
|
181
|
+
const scrollRef = useRef(null);
|
|
182
|
+
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 });
|
|
183
|
+
const prevRangeRef = useRef({ start: 0, end: 0 });
|
|
184
|
+
const { header, body, skeletonRow, empty, footer } = extractSlots(children);
|
|
185
|
+
const columns = header?.columns ?? [];
|
|
186
|
+
const gridTemplate = buildGridTemplate(columns);
|
|
187
|
+
const renderRow = body?.children ?? (() => null);
|
|
188
|
+
const calculateRange = useCallback(() => {
|
|
189
|
+
const el = scrollRef.current;
|
|
190
|
+
if (!el || totalCount === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const scrollTop = el.scrollTop;
|
|
194
|
+
const viewportHeight = el.clientHeight;
|
|
195
|
+
const rawStart = Math.floor(scrollTop / rowHeight);
|
|
196
|
+
const rawEnd = Math.ceil((scrollTop + viewportHeight) / rowHeight);
|
|
197
|
+
const start = Math.max(0, rawStart - overscan);
|
|
198
|
+
const end = Math.min(totalCount, rawEnd + overscan);
|
|
199
|
+
const prev = prevRangeRef.current;
|
|
200
|
+
if (prev.start !== start || prev.end !== end) {
|
|
201
|
+
prevRangeRef.current = { start, end };
|
|
202
|
+
flushSync(() => setVisibleRange({ start, end }));
|
|
203
|
+
}
|
|
204
|
+
}, [totalCount, rowHeight, overscan]);
|
|
205
|
+
const handleScroll = useCallback(() => {
|
|
206
|
+
calculateRange();
|
|
207
|
+
}, [calculateRange]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
calculateRange();
|
|
210
|
+
}, [calculateRange]);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
onRangeChange?.(visibleRange);
|
|
213
|
+
}, [visibleRange, onRangeChange]);
|
|
214
|
+
const handleResizeMouseDown = useCallback((columnIndex, e) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
const container = scrollRef.current;
|
|
217
|
+
if (!container) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const col = columns[columnIndex];
|
|
221
|
+
const headerCells = container.querySelectorAll('[role="columnheader"]');
|
|
222
|
+
const startWidth = headerCells[columnIndex]?.getBoundingClientRect().width ?? (typeof col.width === "number" ? col.width : 100);
|
|
223
|
+
const startX = e.clientX;
|
|
224
|
+
const minW = col.minWidth ?? 50;
|
|
225
|
+
const maxW = col.maxWidth ?? Infinity;
|
|
226
|
+
const otherColumnsMinWidth = columns.reduce((sum, c, i) => {
|
|
227
|
+
if (i === columnIndex) {
|
|
228
|
+
return sum;
|
|
229
|
+
}
|
|
230
|
+
if (typeof c.width === "number") {
|
|
231
|
+
return sum + c.width;
|
|
232
|
+
}
|
|
233
|
+
return sum + (c.minWidth ?? 0);
|
|
234
|
+
}, 0);
|
|
235
|
+
const maxAllowedWidth = container.clientWidth - otherColumnsMinWidth;
|
|
236
|
+
let currentWidth = startWidth;
|
|
237
|
+
col.onResizeStart?.();
|
|
238
|
+
const prevCursor = document.body.style.cursor;
|
|
239
|
+
document.body.style.cursor = "col-resize";
|
|
240
|
+
const handleMouseMove = (moveEvent) => {
|
|
241
|
+
const delta = moveEvent.clientX - startX;
|
|
242
|
+
currentWidth = Math.min(maxW, maxAllowedWidth, Math.max(minW, startWidth + delta));
|
|
243
|
+
const template = columns.map((c, i) => {
|
|
244
|
+
if (i === columnIndex) {
|
|
245
|
+
return `${currentWidth}px`;
|
|
246
|
+
}
|
|
247
|
+
return buildGridTemplate([c]);
|
|
248
|
+
}).join(" ");
|
|
249
|
+
container.style.setProperty(GRID_VAR, template);
|
|
250
|
+
};
|
|
251
|
+
const handleMouseUp = () => {
|
|
252
|
+
document.body.style.cursor = prevCursor;
|
|
253
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
254
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
255
|
+
const allCells = container.querySelectorAll('[role="columnheader"]');
|
|
256
|
+
let otherFrPx = 0;
|
|
257
|
+
let otherFrUnits = 0;
|
|
258
|
+
columns.forEach((c, i) => {
|
|
259
|
+
if (i === columnIndex) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (typeof c.width !== "number") {
|
|
263
|
+
otherFrPx += allCells[i]?.getBoundingClientRect().width ?? 0;
|
|
264
|
+
otherFrUnits += typeof c.width === "string" ? parseFloat(c.width) || 1 : 1;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const frValue = otherFrPx > 0 ? currentWidth / otherFrPx * otherFrUnits : 1;
|
|
268
|
+
col.onResizeEnd?.(currentWidth, startWidth, frValue);
|
|
269
|
+
};
|
|
270
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
271
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
272
|
+
}, [columns]);
|
|
273
|
+
if (totalCount === 0 && empty) {
|
|
274
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
275
|
+
role: "table",
|
|
276
|
+
"aria-label": ariaLabel,
|
|
277
|
+
className: twMerge("flex flex-col overflow-hidden", className),
|
|
278
|
+
style: { [GRID_VAR]: gridTemplate, ...styleProp },
|
|
279
|
+
children: [
|
|
280
|
+
/* @__PURE__ */ jsx("div", {
|
|
281
|
+
role: "rowgroup",
|
|
282
|
+
className: twMerge("sticky top-0 z-10", header?.className),
|
|
283
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
284
|
+
role: "row",
|
|
285
|
+
style: {
|
|
286
|
+
display: "grid",
|
|
287
|
+
gridTemplateColumns: GRID_VAR_REF,
|
|
288
|
+
alignItems: "center",
|
|
289
|
+
height: rowHeight
|
|
290
|
+
},
|
|
291
|
+
children: columns.map((col, i) => /* @__PURE__ */ jsxs("div", {
|
|
292
|
+
role: "columnheader",
|
|
293
|
+
className: twMerge("whitespace-nowrap", col.resizable && "relative", col.className),
|
|
294
|
+
children: [
|
|
295
|
+
col.header,
|
|
296
|
+
col.resizable && /* @__PURE__ */ jsx(ResizeHandle, {
|
|
297
|
+
onMouseDown: (e) => handleResizeMouseDown(i, e)
|
|
298
|
+
})
|
|
299
|
+
]
|
|
300
|
+
}, i))
|
|
301
|
+
})
|
|
302
|
+
}),
|
|
303
|
+
/* @__PURE__ */ jsx("div", {
|
|
304
|
+
className: twMerge("flex items-center justify-center", empty.className),
|
|
305
|
+
children: empty.children
|
|
306
|
+
})
|
|
307
|
+
]
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const rows = [];
|
|
311
|
+
for (let i = visibleRange.start;i < visibleRange.end; i++) {
|
|
312
|
+
const content = renderRow(i);
|
|
313
|
+
if (content === null) {
|
|
314
|
+
if (skeletonRow) {
|
|
315
|
+
const { children: skeletonContent, ...skeletonProps } = skeletonRow;
|
|
316
|
+
rows.push(/* @__PURE__ */ jsx(DataRow, {
|
|
317
|
+
index: i,
|
|
318
|
+
rowHeight,
|
|
319
|
+
rowProps: skeletonProps,
|
|
320
|
+
children: skeletonContent
|
|
321
|
+
}, `row-${i}`));
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const rowElement = content;
|
|
325
|
+
const rowDefaults = rowElement.type?.slotDefaults ?? {};
|
|
326
|
+
const rawProps = rowElement.props;
|
|
327
|
+
const { children: cellContent, ...userRowProps } = rawProps;
|
|
328
|
+
const rowProps = {
|
|
329
|
+
...rowDefaults,
|
|
330
|
+
...userRowProps,
|
|
331
|
+
className: twMerge(rowDefaults.className, userRowProps.className)
|
|
332
|
+
};
|
|
333
|
+
rows.push(/* @__PURE__ */ jsx(DataRow, {
|
|
334
|
+
index: i,
|
|
335
|
+
rowHeight,
|
|
336
|
+
rowProps,
|
|
337
|
+
children: cellContent
|
|
338
|
+
}, `row-${i}`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const totalHeight = totalCount * rowHeight;
|
|
342
|
+
return /* @__PURE__ */ jsxs(Fragment, {
|
|
343
|
+
children: [
|
|
344
|
+
/* @__PURE__ */ jsxs("div", {
|
|
345
|
+
role: "table",
|
|
346
|
+
"aria-label": ariaLabel,
|
|
347
|
+
"aria-rowcount": totalCount,
|
|
348
|
+
ref: scrollRef,
|
|
349
|
+
onScroll: handleScroll,
|
|
350
|
+
className: twMerge("relative overflow-auto", className),
|
|
351
|
+
style: { [GRID_VAR]: gridTemplate, ...styleProp },
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ jsx("div", {
|
|
354
|
+
role: "rowgroup",
|
|
355
|
+
className: twMerge("sticky top-0 z-10", header?.className),
|
|
356
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
357
|
+
role: "row",
|
|
358
|
+
style: {
|
|
359
|
+
display: "grid",
|
|
360
|
+
gridTemplateColumns: GRID_VAR_REF,
|
|
361
|
+
alignItems: "center"
|
|
362
|
+
},
|
|
363
|
+
children: columns.map((col, i) => /* @__PURE__ */ jsxs("div", {
|
|
364
|
+
role: "columnheader",
|
|
365
|
+
className: twMerge("whitespace-nowrap", col.resizable && "relative", col.className),
|
|
366
|
+
children: [
|
|
367
|
+
col.header,
|
|
368
|
+
col.resizable && /* @__PURE__ */ jsx(ResizeHandle, {
|
|
369
|
+
onMouseDown: (e) => handleResizeMouseDown(i, e)
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
}, i))
|
|
373
|
+
})
|
|
374
|
+
}),
|
|
375
|
+
/* @__PURE__ */ jsx("div", {
|
|
376
|
+
role: "rowgroup",
|
|
377
|
+
className: "relative",
|
|
378
|
+
style: { height: totalHeight },
|
|
379
|
+
children: rows
|
|
380
|
+
})
|
|
381
|
+
]
|
|
382
|
+
}),
|
|
383
|
+
footer && totalCount > 0 && /* @__PURE__ */ jsx("div", {
|
|
384
|
+
className: footer.className,
|
|
385
|
+
children: footer.children(visibleRange)
|
|
386
|
+
})
|
|
387
|
+
]
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
var VirtualTable = Object.assign(VirtualTableRoot, {
|
|
391
|
+
Header: VirtualTableHeader,
|
|
392
|
+
Column: VirtualTableColumn,
|
|
393
|
+
Body: VirtualTableBody,
|
|
394
|
+
SkeletonRow: VirtualTableSkeletonRow,
|
|
395
|
+
Row: VirtualTableRow,
|
|
396
|
+
Cell: VirtualTableCell,
|
|
397
|
+
Empty: VirtualTableEmpty,
|
|
398
|
+
Footer: VirtualTableFooter
|
|
399
|
+
});
|
|
400
|
+
export {
|
|
401
|
+
createTableSkeletonRow,
|
|
402
|
+
createTableRow,
|
|
403
|
+
createTableHeader,
|
|
404
|
+
createTableFooter,
|
|
405
|
+
createTableEmpty,
|
|
406
|
+
createTableColumn,
|
|
407
|
+
createTableBody,
|
|
408
|
+
VirtualTable
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
//# debugId=D3E702252F525EB164756E2164756E21
|