@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 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