@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.
@@ -0,0 +1,673 @@
1
+ import {
2
+ type CSSProperties,
3
+ Children,
4
+ type ComponentProps,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ isValidElement,
8
+ useCallback,
9
+ useEffect,
10
+ useRef,
11
+ useState,
12
+ } from 'react'
13
+ import { flushSync } from 'react-dom'
14
+ import { twMerge } from 'tailwind-merge'
15
+
16
+ /* ── Types ──────────────────────────────────────────────────────── */
17
+
18
+ export interface VirtualTableProps {
19
+ /** Total number of rows in the dataset */
20
+ totalCount: number
21
+ /** Fixed height of each row in pixels */
22
+ rowHeight: number
23
+ /** Number of extra rows rendered above/below viewport (default: 5) */
24
+ overscan?: number
25
+ /** Called when the visible row range changes (for triggering page fetches) */
26
+ onRangeChange?: (range: { start: number; end: number }) => void
27
+ /** Additional className for the outer container */
28
+ className?: string
29
+ /** Accessible label for the table */
30
+ 'aria-label'?: string
31
+ /** Additional inline styles for the outer container */
32
+ style?: CSSProperties
33
+ children: ReactNode
34
+ }
35
+
36
+ export interface VirtualTableHeaderProps {
37
+ className?: string
38
+ children: ReactNode
39
+ }
40
+
41
+ export interface VirtualTableColumnProps {
42
+ /** Column width. Number for pixels, string for CSS grid values (e.g. '1fr'). Defaults to '1fr'. */
43
+ width?: number | string
44
+ /** Optional className for the header cell */
45
+ className?: string
46
+ /** Whether this column can be resized by dragging. Default: false */
47
+ resizable?: boolean
48
+ /** Minimum width in pixels during resize. Default: 50 */
49
+ minWidth?: number
50
+ /** Maximum width in pixels during resize */
51
+ maxWidth?: number
52
+ /** Mark this column as transparent — the row background will not extend behind it. */
53
+ transparent?: boolean
54
+ /** Called when a resize drag starts */
55
+ onResizeStart?: () => void
56
+ /** Called when a resize drag ends with the final pixel width, original width, and equivalent fr value */
57
+ onResizeEnd?: (width: number, startWidth: number, frValue: number) => void
58
+ children?: ReactNode
59
+ }
60
+
61
+ export interface VirtualTableBodyProps {
62
+ children: (index: number) => ReactNode | null
63
+ }
64
+
65
+ export interface VirtualTableRowProps extends ComponentProps<'div'> {}
66
+
67
+ export interface VirtualTableSkeletonRowProps extends ComponentProps<'div'> {}
68
+
69
+ export interface VirtualTableCellProps extends ComponentProps<'div'> {
70
+ /** Show cell content only on row hover */
71
+ showOnHover?: boolean
72
+ /** Number of columns this cell spans */
73
+ colSpan?: number
74
+ }
75
+
76
+ export interface VirtualTableEmptyProps {
77
+ className?: string
78
+ children: ReactNode
79
+ }
80
+
81
+ export interface VirtualTableFooterProps {
82
+ className?: string
83
+ children: (range: { start: number; end: number }) => ReactNode
84
+ }
85
+
86
+ /* ── Internal types ─────────────────────────────────────────────── */
87
+
88
+ interface ColumnDef {
89
+ width?: number | string
90
+ header?: ReactNode
91
+ className?: string
92
+ resizable?: boolean
93
+ minWidth?: number
94
+ maxWidth?: number
95
+ transparent?: boolean
96
+ onResizeStart?: () => void
97
+ onResizeEnd?: (width: number, startWidth: number, frValue: number) => void
98
+ }
99
+
100
+ interface Slots {
101
+ header: { className?: string; columns: ColumnDef[] } | null
102
+ body: VirtualTableBodyProps | null
103
+ skeletonRow: VirtualTableSkeletonRowProps | null
104
+ empty: VirtualTableEmptyProps | null
105
+ footer: VirtualTableFooterProps | null
106
+ }
107
+
108
+ /* ── Slot system ───────────────────────────────────────────────── */
109
+
110
+ interface SlotComponent<P> {
111
+ (props: P): ReactNode
112
+ slot: string
113
+ slotDefaults: Partial<P>
114
+ }
115
+
116
+ function asSlot<P>(slot: string, defaults?: Partial<P>): SlotComponent<P> {
117
+ const Component = (() => null) as unknown as SlotComponent<P>
118
+ Component.slot = slot
119
+ Component.slotDefaults = defaults ?? ({} as Partial<P>)
120
+ return Component
121
+ }
122
+
123
+ export function createTableHeader(
124
+ defaults?: Partial<VirtualTableHeaderProps>,
125
+ ): SlotComponent<VirtualTableHeaderProps> {
126
+ return asSlot('header', defaults)
127
+ }
128
+
129
+ export function createTableColumn(
130
+ defaults?: Partial<VirtualTableColumnProps>,
131
+ ): SlotComponent<VirtualTableColumnProps> {
132
+ return asSlot('column', defaults)
133
+ }
134
+
135
+ export function createTableBody(
136
+ defaults?: Partial<VirtualTableBodyProps>,
137
+ ): SlotComponent<VirtualTableBodyProps> {
138
+ return asSlot('body', defaults)
139
+ }
140
+
141
+ export function createTableSkeletonRow(
142
+ defaults?: Partial<VirtualTableSkeletonRowProps>,
143
+ ): SlotComponent<VirtualTableSkeletonRowProps> {
144
+ return asSlot('skeletonRow', defaults)
145
+ }
146
+
147
+ export function createTableEmpty(
148
+ defaults?: Partial<VirtualTableEmptyProps>,
149
+ ): SlotComponent<VirtualTableEmptyProps> {
150
+ return asSlot('empty', defaults)
151
+ }
152
+
153
+ export function createTableFooter(
154
+ defaults?: Partial<VirtualTableFooterProps>,
155
+ ): SlotComponent<VirtualTableFooterProps> {
156
+ return asSlot('footer', defaults)
157
+ }
158
+
159
+ export function createTableRow(
160
+ defaults?: Partial<VirtualTableRowProps>,
161
+ ): SlotComponent<VirtualTableRowProps> {
162
+ return asSlot('row', defaults)
163
+ }
164
+
165
+ /* ── Constants ──────────────────────────────────────────────────── */
166
+
167
+ const GRID_VAR = '--vtable-grid-cols'
168
+ const GRID_VAR_REF = `var(${GRID_VAR})`
169
+
170
+ /* ── Helpers ────────────────────────────────────────────────────── */
171
+
172
+ function buildGridTemplate(columns: ColumnDef[]): string {
173
+ return columns
174
+ .map((col) => {
175
+ if (typeof col.width === 'number') {
176
+ return `${col.width}px`
177
+ }
178
+ const min = col.minWidth ? `${col.minWidth}px` : '0'
179
+ if (typeof col.width === 'string') {
180
+ return `minmax(${min}, ${col.width})`
181
+ }
182
+ return `minmax(${min}, 1fr)`
183
+ })
184
+ .join(' ')
185
+ }
186
+
187
+ /* ── Slot Components (render nothing — used as config markers) ── */
188
+
189
+ const VirtualTableHeader = asSlot<VirtualTableHeaderProps>('header')
190
+ const VirtualTableColumn = asSlot<VirtualTableColumnProps>('column')
191
+ const VirtualTableBody = asSlot<VirtualTableBodyProps>('body')
192
+ const VirtualTableRow = asSlot<VirtualTableRowProps>('row')
193
+ const VirtualTableSkeletonRow =
194
+ asSlot<VirtualTableSkeletonRowProps>('skeletonRow')
195
+ const VirtualTableEmpty = asSlot<VirtualTableEmptyProps>('empty')
196
+ const VirtualTableFooter = asSlot<VirtualTableFooterProps>('footer')
197
+
198
+ function slotIs(child: React.ReactElement, slot: string): boolean {
199
+ return (child.type as any)?.slot === slot
200
+ }
201
+
202
+ function extractSlots(children: ReactNode): Slots {
203
+ let header: Slots['header'] = null
204
+ let body: Slots['body'] = null
205
+ let skeletonRow: Slots['skeletonRow'] = null
206
+ let empty: Slots['empty'] = null
207
+ let footer: Slots['footer'] = null
208
+
209
+ Children.forEach(children, (child) => {
210
+ if (!isValidElement(child)) {
211
+ return
212
+ }
213
+
214
+ if (slotIs(child, 'header')) {
215
+ const defaults = (child.type as any).slotDefaults ?? {}
216
+ const props = child.props as VirtualTableHeaderProps
217
+ const columns: ColumnDef[] = []
218
+ Children.forEach(props.children, (col) => {
219
+ if (isValidElement(col) && slotIs(col, 'column')) {
220
+ const d = (col.type as any).slotDefaults ?? {}
221
+ const p = col.props as VirtualTableColumnProps
222
+ columns.push({
223
+ width: p.width ?? d.width,
224
+ header: p.children,
225
+ className: twMerge(d.className as string, p.className),
226
+ resizable: p.resizable ?? d.resizable,
227
+ minWidth: p.minWidth ?? d.minWidth,
228
+ maxWidth: p.maxWidth ?? d.maxWidth,
229
+ transparent: p.transparent ?? d.transparent,
230
+ onResizeStart: p.onResizeStart ?? d.onResizeStart,
231
+ onResizeEnd: p.onResizeEnd ?? d.onResizeEnd,
232
+ })
233
+ }
234
+ })
235
+ header = {
236
+ className: twMerge(defaults.className as string, props.className),
237
+ columns,
238
+ }
239
+ } else if (slotIs(child, 'body')) {
240
+ body = child.props as VirtualTableBodyProps
241
+ } else if (slotIs(child, 'skeletonRow')) {
242
+ const defaults = (child.type as any).slotDefaults ?? {}
243
+ const props = child.props as VirtualTableSkeletonRowProps
244
+ skeletonRow = {
245
+ ...defaults,
246
+ ...props,
247
+ className: twMerge(defaults.className as string, props.className),
248
+ }
249
+ } else if (slotIs(child, 'empty')) {
250
+ const defaults = (child.type as any).slotDefaults ?? {}
251
+ const props = child.props as VirtualTableEmptyProps
252
+ empty = {
253
+ ...defaults,
254
+ ...props,
255
+ className: twMerge(defaults.className as string, props.className),
256
+ }
257
+ } else if (slotIs(child, 'footer')) {
258
+ const defaults = (child.type as any).slotDefaults ?? {}
259
+ const props = child.props as VirtualTableFooterProps
260
+ footer = {
261
+ ...defaults,
262
+ ...props,
263
+ className: twMerge(defaults.className as string, props.className),
264
+ }
265
+ }
266
+ })
267
+
268
+ return { header, body, skeletonRow, empty, footer }
269
+ }
270
+
271
+ /* ── Resize Handle ─────────────────────────────────────────────── */
272
+
273
+ interface ResizeHandleProps {
274
+ onMouseDown: (e: React.MouseEvent) => void
275
+ }
276
+
277
+ function ResizeHandle({ onMouseDown }: ResizeHandleProps) {
278
+ return (
279
+ <div
280
+ role="separator"
281
+ aria-orientation="vertical"
282
+ className="resizer absolute right-0 top-0 z-10 h-full w-1.5 cursor-col-resize"
283
+ onMouseDown={onMouseDown}
284
+ />
285
+ )
286
+ }
287
+
288
+ /* ── Cell ───────────────────────────────────────────────────────── */
289
+
290
+ function VirtualTableCell({
291
+ className,
292
+ style,
293
+ showOnHover,
294
+ colSpan,
295
+ ...rest
296
+ }: VirtualTableCellProps) {
297
+ return (
298
+ <div
299
+ role="cell"
300
+ className={twMerge(
301
+ 'overflow-hidden text-ellipsis whitespace-nowrap',
302
+ showOnHover &&
303
+ 'not-group-hover/row:*:delay-200 *:opacity-10 *:transition-opacity *:duration-300 *:ease-in-out group-hover/row:*:opacity-100',
304
+ className,
305
+ )}
306
+ style={colSpan ? { gridColumn: `span ${colSpan}`, ...style } : style}
307
+ {...rest}
308
+ />
309
+ )
310
+ }
311
+
312
+ /* ── Data Row ──────────────────────────────────────────────────── */
313
+
314
+ interface DataRowProps {
315
+ index: number
316
+ rowHeight: number
317
+ rowProps: ComponentProps<'div'>
318
+ children: ReactNode
319
+ }
320
+
321
+ function DataRow({ index, rowHeight, rowProps, children }: DataRowProps) {
322
+ const { className, style, ...restProps } = rowProps
323
+ return (
324
+ <div
325
+ role="row"
326
+ aria-rowindex={index + 1}
327
+ className={twMerge('group/row absolute w-full', className)}
328
+ style={{
329
+ height: rowHeight,
330
+ transform: `translateY(${index * rowHeight}px)`,
331
+ display: 'grid',
332
+ gridTemplateColumns: GRID_VAR_REF,
333
+ alignItems: 'center',
334
+ willChange: 'transform',
335
+ contain: 'layout style paint',
336
+ ...style,
337
+ }}
338
+ {...restProps}
339
+ >
340
+ {children}
341
+ </div>
342
+ )
343
+ }
344
+
345
+ /* ── VirtualTable ──────────────────────────────────────────────── */
346
+
347
+ function VirtualTableRoot({
348
+ totalCount,
349
+ rowHeight,
350
+ overscan = 5,
351
+ onRangeChange,
352
+ className,
353
+ style: styleProp,
354
+ 'aria-label': ariaLabel,
355
+ children,
356
+ }: VirtualTableProps) {
357
+ const scrollRef = useRef<HTMLDivElement>(null)
358
+ const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 })
359
+ const prevRangeRef = useRef({ start: 0, end: 0 })
360
+
361
+ const { header, body, skeletonRow, empty, footer } = extractSlots(children)
362
+ const columns = header?.columns ?? []
363
+ const gridTemplate = buildGridTemplate(columns)
364
+ const renderRow = body?.children ?? (() => null)
365
+
366
+ // ── Scroll handler (synchronous for flicker-free scrolling) ────
367
+ const calculateRange = useCallback(() => {
368
+ const el = scrollRef.current
369
+ if (!el || totalCount === 0) {
370
+ return
371
+ }
372
+
373
+ const scrollTop = el.scrollTop
374
+ const viewportHeight = el.clientHeight
375
+
376
+ const rawStart = Math.floor(scrollTop / rowHeight)
377
+ const rawEnd = Math.ceil((scrollTop + viewportHeight) / rowHeight)
378
+
379
+ const start = Math.max(0, rawStart - overscan)
380
+ const end = Math.min(totalCount, rawEnd + overscan)
381
+
382
+ const prev = prevRangeRef.current
383
+ if (prev.start !== start || prev.end !== end) {
384
+ prevRangeRef.current = { start, end }
385
+ flushSync(() => setVisibleRange({ start, end }))
386
+ }
387
+ }, [totalCount, rowHeight, overscan])
388
+
389
+ const handleScroll = useCallback(() => {
390
+ calculateRange()
391
+ }, [calculateRange])
392
+
393
+ // Recalculate on totalCount/rowHeight changes
394
+ useEffect(() => {
395
+ calculateRange()
396
+ }, [calculateRange])
397
+
398
+ // Notify consumer when visible range changes
399
+ useEffect(() => {
400
+ onRangeChange?.(visibleRange)
401
+ }, [visibleRange, onRangeChange])
402
+
403
+ // ── Column resize ──────────────────────────────────────────────
404
+ const handleResizeMouseDown = useCallback(
405
+ (columnIndex: number, e: React.MouseEvent) => {
406
+ e.preventDefault()
407
+
408
+ const container = scrollRef.current
409
+ if (!container) {
410
+ return
411
+ }
412
+
413
+ const col = columns[columnIndex]
414
+ const headerCells = container.querySelectorAll('[role="columnheader"]')
415
+
416
+ // Resolve current pixel width (handles fr columns)
417
+ const startWidth =
418
+ headerCells[columnIndex]?.getBoundingClientRect().width ??
419
+ (typeof col.width === 'number' ? col.width : 100)
420
+ const startX = e.clientX
421
+
422
+ const minW = col.minWidth ?? 50
423
+ const maxW = col.maxWidth ?? Infinity
424
+
425
+ // Prevent the resized column from pushing the table beyond the container
426
+ const otherColumnsMinWidth = columns.reduce((sum, c, i) => {
427
+ if (i === columnIndex) {
428
+ return sum
429
+ }
430
+ if (typeof c.width === 'number') {
431
+ return sum + c.width
432
+ }
433
+ return sum + (c.minWidth ?? 0)
434
+ }, 0)
435
+ const maxAllowedWidth = container.clientWidth - otherColumnsMinWidth
436
+
437
+ let currentWidth = startWidth
438
+
439
+ col.onResizeStart?.()
440
+
441
+ const prevCursor = document.body.style.cursor
442
+ document.body.style.cursor = 'col-resize'
443
+
444
+ const handleMouseMove = (moveEvent: MouseEvent) => {
445
+ const delta = moveEvent.clientX - startX
446
+ currentWidth = Math.min(
447
+ maxW,
448
+ maxAllowedWidth,
449
+ Math.max(minW, startWidth + delta),
450
+ )
451
+
452
+ // Only override the dragged column — preserve original values for others
453
+ const template = columns
454
+ .map((c, i) => {
455
+ if (i === columnIndex) {
456
+ return `${currentWidth}px`
457
+ }
458
+ return buildGridTemplate([c])
459
+ })
460
+ .join(' ')
461
+ container.style.setProperty(GRID_VAR, template)
462
+ }
463
+
464
+ const handleMouseUp = () => {
465
+ document.body.style.cursor = prevCursor
466
+ document.removeEventListener('mousemove', handleMouseMove)
467
+ document.removeEventListener('mouseup', handleMouseUp)
468
+
469
+ // Compute equivalent fr value from other fr columns' pixel widths
470
+ const allCells = container.querySelectorAll('[role="columnheader"]')
471
+ let otherFrPx = 0
472
+ let otherFrUnits = 0
473
+ columns.forEach((c, i) => {
474
+ if (i === columnIndex) {
475
+ return
476
+ }
477
+ if (typeof c.width !== 'number') {
478
+ otherFrPx += allCells[i]?.getBoundingClientRect().width ?? 0
479
+ otherFrUnits +=
480
+ typeof c.width === 'string' ? parseFloat(c.width) || 1 : 1
481
+ }
482
+ })
483
+ const frValue =
484
+ otherFrPx > 0 ? (currentWidth / otherFrPx) * otherFrUnits : 1
485
+
486
+ col.onResizeEnd?.(currentWidth, startWidth, frValue)
487
+ }
488
+
489
+ document.addEventListener('mousemove', handleMouseMove)
490
+ document.addEventListener('mouseup', handleMouseUp)
491
+ },
492
+ [columns],
493
+ )
494
+
495
+ // ── Empty state ────────────────────────────────────────────────
496
+ if (totalCount === 0 && empty) {
497
+ return (
498
+ <div
499
+ role="table"
500
+ aria-label={ariaLabel}
501
+ className={twMerge('flex flex-col overflow-hidden', className)}
502
+ style={{ [GRID_VAR]: gridTemplate, ...styleProp } as CSSProperties}
503
+ >
504
+ {/* Header */}
505
+ <div
506
+ role="rowgroup"
507
+ className={twMerge('sticky top-0 z-10', header?.className)}
508
+ >
509
+ <div
510
+ role="row"
511
+ style={{
512
+ display: 'grid',
513
+ gridTemplateColumns: GRID_VAR_REF,
514
+ alignItems: 'center',
515
+ height: rowHeight,
516
+ }}
517
+ >
518
+ {columns.map((col, i) => (
519
+ <div
520
+ key={i}
521
+ role="columnheader"
522
+ className={twMerge(
523
+ 'whitespace-nowrap',
524
+ col.resizable && 'relative',
525
+ col.className,
526
+ )}
527
+ >
528
+ {col.header}
529
+ {col.resizable && (
530
+ <ResizeHandle
531
+ onMouseDown={(e) => handleResizeMouseDown(i, e)}
532
+ />
533
+ )}
534
+ </div>
535
+ ))}
536
+ </div>
537
+ </div>
538
+
539
+ {/* Empty */}
540
+ <div
541
+ className={twMerge(
542
+ 'flex items-center justify-center',
543
+ empty.className,
544
+ )}
545
+ >
546
+ {empty.children}
547
+ </div>
548
+ </div>
549
+ )
550
+ }
551
+
552
+ // ── Build visible rows ─────────────────────────────────────────
553
+ const rows: ReactNode[] = []
554
+ for (let i = visibleRange.start; i < visibleRange.end; i++) {
555
+ const content = renderRow(i)
556
+ if (content === null) {
557
+ if (skeletonRow) {
558
+ const { children: skeletonContent, ...skeletonProps } = skeletonRow
559
+ rows.push(
560
+ <DataRow
561
+ key={`row-${i}`}
562
+ index={i}
563
+ rowHeight={rowHeight}
564
+ rowProps={skeletonProps}
565
+ >
566
+ {skeletonContent}
567
+ </DataRow>,
568
+ )
569
+ }
570
+ } else {
571
+ const rowElement = content as ReactElement
572
+ const rowDefaults = (rowElement.type as any)?.slotDefaults ?? {}
573
+ const rawProps = rowElement.props as VirtualTableRowProps
574
+ const { children: cellContent, ...userRowProps } = rawProps
575
+ const rowProps = {
576
+ ...rowDefaults,
577
+ ...userRowProps,
578
+ className: twMerge(
579
+ rowDefaults.className as string,
580
+ userRowProps.className,
581
+ ),
582
+ }
583
+
584
+ rows.push(
585
+ <DataRow
586
+ key={`row-${i}`}
587
+ index={i}
588
+ rowHeight={rowHeight}
589
+ rowProps={rowProps}
590
+ >
591
+ {cellContent}
592
+ </DataRow>,
593
+ )
594
+ }
595
+ }
596
+
597
+ const totalHeight = totalCount * rowHeight
598
+
599
+ return (
600
+ <>
601
+ <div
602
+ role="table"
603
+ aria-label={ariaLabel}
604
+ aria-rowcount={totalCount}
605
+ ref={scrollRef}
606
+ onScroll={handleScroll}
607
+ className={twMerge('relative overflow-auto', className)}
608
+ style={{ [GRID_VAR]: gridTemplate, ...styleProp } as CSSProperties}
609
+ >
610
+ {/* Header */}
611
+ <div
612
+ role="rowgroup"
613
+ className={twMerge('sticky top-0 z-10', header?.className)}
614
+ >
615
+ <div
616
+ role="row"
617
+ style={{
618
+ display: 'grid',
619
+ gridTemplateColumns: GRID_VAR_REF,
620
+ alignItems: 'center',
621
+ }}
622
+ >
623
+ {columns.map((col, i) => (
624
+ <div
625
+ key={i}
626
+ role="columnheader"
627
+ className={twMerge(
628
+ 'whitespace-nowrap',
629
+ col.resizable && 'relative',
630
+ col.className,
631
+ )}
632
+ >
633
+ {col.header}
634
+ {col.resizable && (
635
+ <ResizeHandle
636
+ onMouseDown={(e) => handleResizeMouseDown(i, e)}
637
+ />
638
+ )}
639
+ </div>
640
+ ))}
641
+ </div>
642
+ </div>
643
+
644
+ {/* Body sentinel + visible rows */}
645
+ <div
646
+ role="rowgroup"
647
+ className="relative"
648
+ style={{ height: totalHeight }}
649
+ >
650
+ {rows}
651
+ </div>
652
+ </div>
653
+
654
+ {/* Footer */}
655
+ {footer && totalCount > 0 && (
656
+ <div className={footer.className}>{footer.children(visibleRange)}</div>
657
+ )}
658
+ </>
659
+ )
660
+ }
661
+
662
+ /* ── Compound export ───────────────────────────────────────────── */
663
+
664
+ export const VirtualTable = Object.assign(VirtualTableRoot, {
665
+ Header: VirtualTableHeader,
666
+ Column: VirtualTableColumn,
667
+ Body: VirtualTableBody,
668
+ SkeletonRow: VirtualTableSkeletonRow,
669
+ Row: VirtualTableRow,
670
+ Cell: VirtualTableCell,
671
+ Empty: VirtualTableEmpty,
672
+ Footer: VirtualTableFooter,
673
+ })
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export { VirtualTable } from './VirtualTable.tsx'
2
+ export {
3
+ createTableHeader,
4
+ createTableColumn,
5
+ createTableBody,
6
+ createTableSkeletonRow,
7
+ createTableEmpty,
8
+ createTableFooter,
9
+ createTableRow,
10
+ } from './VirtualTable.tsx'
11
+ export type {
12
+ VirtualTableProps,
13
+ VirtualTableHeaderProps,
14
+ VirtualTableColumnProps,
15
+ VirtualTableBodyProps,
16
+ VirtualTableSkeletonRowProps,
17
+ VirtualTableRowProps,
18
+ VirtualTableCellProps,
19
+ VirtualTableEmptyProps,
20
+ VirtualTableFooterProps,
21
+ } from './VirtualTable.tsx'
22
+
23
+ export { useTableCache } from './useTableCache.ts'
24
+ export type { UseTableCacheOptions, TableCache } from './useTableCache.ts'
25
+
26
+ export { useTableColumnWidths } from './useTableColumnWidths.ts'