@duro-app/ui 0.40.0 → 0.42.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/dist/components/Icon/Icon.d.ts +1 -1
- package/dist/components/Icon/Icon.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.d.ts +16 -1
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.stories.d.ts +1 -0
- package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +48 -0
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +57 -0
- package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -0
- package/dist/components/VirtualTable/VirtualTable.meta.d.ts +3 -0
- package/dist/components/VirtualTable/VirtualTable.meta.d.ts.map +1 -0
- package/dist/components/VirtualTable/VirtualTable.stories.d.ts +14 -0
- package/dist/components/VirtualTable/VirtualTable.stories.d.ts.map +1 -0
- package/dist/components/VirtualTable/styles.css.d.ts +128 -0
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2286 -1936
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/Icon/Icon.tsx +46 -0
- package/src/components/SideNav/SideNav.meta.ts +17 -11
- package/src/components/SideNav/SideNav.stories.tsx +47 -3
- package/src/components/SideNav/SideNav.tsx +34 -2
- package/src/components/SideNav/styles.css.ts +51 -1
- package/src/components/VirtualTable/VirtualTable.meta.ts +43 -0
- package/src/components/VirtualTable/VirtualTable.stories.tsx +89 -0
- package/src/components/VirtualTable/VirtualTable.tsx +275 -0
- package/src/components/VirtualTable/styles.css.ts +135 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {useEffect, useRef, useState, type ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {
|
|
4
|
+
useReactTable,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getSortedRowModel,
|
|
7
|
+
flexRender,
|
|
8
|
+
type ColumnDef,
|
|
9
|
+
type SortingState,
|
|
10
|
+
type OnChangeFn,
|
|
11
|
+
type Row,
|
|
12
|
+
} from '@tanstack/react-table'
|
|
13
|
+
import {useVirtualizer} from '@tanstack/react-virtual'
|
|
14
|
+
import {styles} from './styles.css'
|
|
15
|
+
|
|
16
|
+
export interface VirtualTableRange {
|
|
17
|
+
/** 1-based index of the first visible row (0 when empty). */
|
|
18
|
+
from: number
|
|
19
|
+
/** 1-based index of the last visible row (0 when empty). */
|
|
20
|
+
to: number
|
|
21
|
+
total: number
|
|
22
|
+
/** 1-based page the first visible row falls in. */
|
|
23
|
+
page: number
|
|
24
|
+
pages: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
interface VirtualTableProps<TData> {
|
|
29
|
+
data: TData[]
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
columns: ColumnDef<TData, any>[]
|
|
32
|
+
/** Controlled sorting (pairs with a SortChip in the caller). */
|
|
33
|
+
sorting?: SortingState
|
|
34
|
+
onSortingChange?: OnChangeFn<SortingState>
|
|
35
|
+
getRowId?: (row: TData, index: number) => string
|
|
36
|
+
onRowClick?: (row: TData) => void
|
|
37
|
+
rowLabel?: (row: TData) => string
|
|
38
|
+
/**
|
|
39
|
+
* 1-based page to restore the scroll position to on mount (read from the URL
|
|
40
|
+
* by the caller). Only meaningful when the list virtualizes.
|
|
41
|
+
*/
|
|
42
|
+
initialPage?: number
|
|
43
|
+
/**
|
|
44
|
+
* Fired (debounced) when the top-visible page changes — wire it to the URL.
|
|
45
|
+
* Only fires in the virtualized branch; small lists render in one scroll and
|
|
46
|
+
* have no page concept.
|
|
47
|
+
*/
|
|
48
|
+
onVisiblePageChange?: (range: VirtualTableRange) => void
|
|
49
|
+
/** Build the floating position-indicator label (i18n stays with the caller). */
|
|
50
|
+
rangeLabel?: (range: VirtualTableRange) => string
|
|
51
|
+
/** Rows per "page" for the URL/indicator math. Default 50. */
|
|
52
|
+
pageSize?: number
|
|
53
|
+
/** Estimated row height for the virtualizer. Default 44. */
|
|
54
|
+
estimateRowHeight?: number
|
|
55
|
+
/** Max height of the scroll viewport once virtualized. Default '70vh'. */
|
|
56
|
+
maxHeight?: number | string
|
|
57
|
+
/** Render every row (no windowing) at or below this count. Default 150. */
|
|
58
|
+
virtualizeThreshold?: number
|
|
59
|
+
emptyLabel?: ReactNode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A sortable data table that windows its rows above a threshold (default 150)
|
|
64
|
+
* via @tanstack/react-virtual, keeps a floating position indicator, and reports
|
|
65
|
+
* the visible page so the caller can mirror it in the URL. Below the threshold
|
|
66
|
+
* it renders every row in one scroll (find-in-page + a11y intact) — no
|
|
67
|
+
* pagination, no windowing. Web-only (uses DOM scroll measurement).
|
|
68
|
+
*
|
|
69
|
+
* URL-decoupled by design: pass `initialPage` (restore) and `onVisiblePageChange`
|
|
70
|
+
* (persist); the component never touches the router itself.
|
|
71
|
+
*/
|
|
72
|
+
export function VirtualTable<TData>({
|
|
73
|
+
data,
|
|
74
|
+
columns,
|
|
75
|
+
sorting,
|
|
76
|
+
onSortingChange,
|
|
77
|
+
getRowId,
|
|
78
|
+
onRowClick,
|
|
79
|
+
rowLabel,
|
|
80
|
+
initialPage,
|
|
81
|
+
onVisiblePageChange,
|
|
82
|
+
rangeLabel,
|
|
83
|
+
pageSize = 50,
|
|
84
|
+
estimateRowHeight = 44,
|
|
85
|
+
maxHeight = '70vh',
|
|
86
|
+
virtualizeThreshold = 150,
|
|
87
|
+
emptyLabel,
|
|
88
|
+
}: VirtualTableProps<TData>) {
|
|
89
|
+
// Support both controlled sorting (SortChip in the caller) and standalone use.
|
|
90
|
+
const [internalSorting, setInternalSorting] = useState<SortingState>([])
|
|
91
|
+
const sortingState = sorting ?? internalSorting
|
|
92
|
+
const handleSortingChange = onSortingChange ?? setInternalSorting
|
|
93
|
+
|
|
94
|
+
const table = useReactTable({
|
|
95
|
+
data,
|
|
96
|
+
columns,
|
|
97
|
+
state: {sorting: sortingState},
|
|
98
|
+
onSortingChange: handleSortingChange,
|
|
99
|
+
getRowId,
|
|
100
|
+
getCoreRowModel: getCoreRowModel(),
|
|
101
|
+
getSortedRowModel: getSortedRowModel(),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const rows = table.getRowModel().rows
|
|
105
|
+
const virtualize = rows.length > virtualizeThreshold
|
|
106
|
+
|
|
107
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
108
|
+
const rv = useVirtualizer({
|
|
109
|
+
count: rows.length,
|
|
110
|
+
getScrollElement: () => scrollRef.current,
|
|
111
|
+
estimateSize: () => estimateRowHeight,
|
|
112
|
+
overscan: 12,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Restore the scroll position from `initialPage` on mount, and suppress the
|
|
116
|
+
// page-change callback briefly so that restore scroll doesn't immediately
|
|
117
|
+
// report (and overwrite) the URL.
|
|
118
|
+
const suppressWrite = useRef(true)
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!virtualize) {
|
|
121
|
+
suppressWrite.current = false
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (initialPage && initialPage > 1) {
|
|
125
|
+
rv.scrollToIndex((initialPage - 1) * pageSize, {align: 'start'})
|
|
126
|
+
}
|
|
127
|
+
const id = setTimeout(() => {
|
|
128
|
+
suppressWrite.current = false
|
|
129
|
+
}, 300)
|
|
130
|
+
return () => clearTimeout(id)
|
|
131
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
}, [virtualize])
|
|
133
|
+
|
|
134
|
+
const vItems = rv.getVirtualItems()
|
|
135
|
+
const firstIndex = vItems[0]?.index ?? 0
|
|
136
|
+
const lastIndex = vItems.length ? vItems[vItems.length - 1].index : 0
|
|
137
|
+
const pages = Math.max(1, Math.ceil(rows.length / pageSize))
|
|
138
|
+
// page = which pageSize-sized block the top visible row is in, so it only
|
|
139
|
+
// changes when you cross a boundary (no per-scroll-tick churn).
|
|
140
|
+
const curPage = Math.floor(Math.max(0, firstIndex) / pageSize) + 1
|
|
141
|
+
|
|
142
|
+
const range: VirtualTableRange = {
|
|
143
|
+
from: rows.length ? firstIndex + 1 : 0,
|
|
144
|
+
to: rows.length ? lastIndex + 1 : 0,
|
|
145
|
+
total: rows.length,
|
|
146
|
+
page: curPage,
|
|
147
|
+
pages,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Report the visible page so the caller can sync the URL. Fire once per
|
|
151
|
+
// distinct page rather than debouncing a timer — `curPage` only changes at a
|
|
152
|
+
// pageSize boundary (coarse), and react-virtual's frequent re-renders during
|
|
153
|
+
// scroll/measurement would keep resetting a debounce timer so it never fires.
|
|
154
|
+
// The `lastReported` guard prevents redundant calls; callers use replace, so
|
|
155
|
+
// there's no history churn.
|
|
156
|
+
const lastReported = useRef<number | null>(null)
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!virtualize || !onVisiblePageChange) return
|
|
159
|
+
if (suppressWrite.current) {
|
|
160
|
+
// Still in the mount-restore window — record the page so we don't emit a
|
|
161
|
+
// spurious change once it clears, but don't call out.
|
|
162
|
+
lastReported.current = curPage
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (lastReported.current === curPage) return
|
|
166
|
+
lastReported.current = curPage
|
|
167
|
+
onVisiblePageChange({
|
|
168
|
+
from: rows.length ? firstIndex + 1 : 0,
|
|
169
|
+
to: rows.length ? lastIndex + 1 : 0,
|
|
170
|
+
total: rows.length,
|
|
171
|
+
page: curPage,
|
|
172
|
+
pages,
|
|
173
|
+
})
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
|
+
}, [curPage, virtualize])
|
|
176
|
+
|
|
177
|
+
const renderRow = (
|
|
178
|
+
row: Row<TData>,
|
|
179
|
+
index: number,
|
|
180
|
+
positioned?: ReturnType<typeof styles.rowAbsolute>,
|
|
181
|
+
measureRef?: (node: Element | null) => void,
|
|
182
|
+
) => (
|
|
183
|
+
<html.div
|
|
184
|
+
key={row.id}
|
|
185
|
+
ref={measureRef}
|
|
186
|
+
data-index={index}
|
|
187
|
+
role="row"
|
|
188
|
+
aria-label={rowLabel?.(row.original)}
|
|
189
|
+
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
|
190
|
+
style={[styles.row, Boolean(onRowClick) && styles.rowClickable, positioned]}
|
|
191
|
+
>
|
|
192
|
+
{row.getVisibleCells().map((cell) => (
|
|
193
|
+
<html.div
|
|
194
|
+
key={cell.id}
|
|
195
|
+
role="cell"
|
|
196
|
+
style={[styles.cell, styles.cellFlex(cell.column.getSize())]}
|
|
197
|
+
>
|
|
198
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
199
|
+
</html.div>
|
|
200
|
+
))}
|
|
201
|
+
</html.div>
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const label =
|
|
205
|
+
rangeLabel?.(range) ?? (rows.length ? `${range.from}–${range.to} / ${range.total}` : '')
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<html.div style={styles.wrap}>
|
|
209
|
+
<html.div
|
|
210
|
+
ref={scrollRef}
|
|
211
|
+
role="table"
|
|
212
|
+
style={[styles.scroll, virtualize && styles.scrollMax(maxHeight)]}
|
|
213
|
+
>
|
|
214
|
+
<html.div role="rowgroup" style={styles.head}>
|
|
215
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
216
|
+
<html.div key={headerGroup.id} role="row" style={styles.headRow}>
|
|
217
|
+
{headerGroup.headers.map((header) => {
|
|
218
|
+
const col = header.column
|
|
219
|
+
const sorted = col.getIsSorted()
|
|
220
|
+
return (
|
|
221
|
+
<html.div
|
|
222
|
+
key={header.id}
|
|
223
|
+
role="columnheader"
|
|
224
|
+
aria-sort={
|
|
225
|
+
sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : undefined
|
|
226
|
+
}
|
|
227
|
+
onClick={col.getCanSort() ? col.getToggleSortingHandler() : undefined}
|
|
228
|
+
style={[
|
|
229
|
+
styles.headCell,
|
|
230
|
+
col.getCanSort() && styles.headCellSortable,
|
|
231
|
+
styles.cellFlex(header.getSize()),
|
|
232
|
+
]}
|
|
233
|
+
>
|
|
234
|
+
{header.isPlaceholder
|
|
235
|
+
? null
|
|
236
|
+
: flexRender(col.columnDef.header, header.getContext())}
|
|
237
|
+
{sorted ? (
|
|
238
|
+
<html.span style={styles.sortMark}>{sorted === 'asc' ? '↑' : '↓'}</html.span>
|
|
239
|
+
) : null}
|
|
240
|
+
</html.div>
|
|
241
|
+
)
|
|
242
|
+
})}
|
|
243
|
+
</html.div>
|
|
244
|
+
))}
|
|
245
|
+
</html.div>
|
|
246
|
+
|
|
247
|
+
{rows.length === 0 ? (
|
|
248
|
+
<html.div style={styles.empty}>{emptyLabel}</html.div>
|
|
249
|
+
) : (
|
|
250
|
+
<html.div
|
|
251
|
+
role="rowgroup"
|
|
252
|
+
style={virtualize ? styles.bodyTotal(rv.getTotalSize()) : undefined}
|
|
253
|
+
>
|
|
254
|
+
{virtualize
|
|
255
|
+
? vItems.map((vi) =>
|
|
256
|
+
renderRow(
|
|
257
|
+
rows[vi.index],
|
|
258
|
+
vi.index,
|
|
259
|
+
styles.rowAbsolute(vi.start),
|
|
260
|
+
rv.measureElement,
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
: rows.map((row, i) => renderRow(row, i))}
|
|
264
|
+
</html.div>
|
|
265
|
+
)}
|
|
266
|
+
</html.div>
|
|
267
|
+
|
|
268
|
+
{virtualize && rows.length > 0 ? (
|
|
269
|
+
<html.div aria-live="polite" style={styles.pos}>
|
|
270
|
+
{label}
|
|
271
|
+
</html.div>
|
|
272
|
+
) : null}
|
|
273
|
+
</html.div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
|
|
6
|
+
export const styles = css.create({
|
|
7
|
+
wrap: {
|
|
8
|
+
position: 'relative',
|
|
9
|
+
width: '100%',
|
|
10
|
+
},
|
|
11
|
+
scroll: {
|
|
12
|
+
position: 'relative',
|
|
13
|
+
width: '100%',
|
|
14
|
+
borderWidth: 1,
|
|
15
|
+
borderStyle: 'solid',
|
|
16
|
+
borderColor: colors.border,
|
|
17
|
+
borderRadius: radii.md,
|
|
18
|
+
backgroundColor: colors.bgCard,
|
|
19
|
+
},
|
|
20
|
+
// Dynamic: only virtualized lists get a capped, scrollable viewport.
|
|
21
|
+
scrollMax: (maxHeight: number | string) => ({
|
|
22
|
+
maxHeight,
|
|
23
|
+
overflowY: 'auto',
|
|
24
|
+
overscrollBehavior: 'contain',
|
|
25
|
+
}),
|
|
26
|
+
head: {
|
|
27
|
+
position: 'sticky',
|
|
28
|
+
top: 0,
|
|
29
|
+
zIndex: 1,
|
|
30
|
+
backgroundColor: colors.bgCard,
|
|
31
|
+
},
|
|
32
|
+
headRow: {
|
|
33
|
+
display: 'flex',
|
|
34
|
+
flexDirection: 'row',
|
|
35
|
+
borderBottomWidth: 1,
|
|
36
|
+
borderBottomStyle: 'solid',
|
|
37
|
+
borderBottomColor: colors.border,
|
|
38
|
+
},
|
|
39
|
+
headCell: {
|
|
40
|
+
display: 'flex',
|
|
41
|
+
flexDirection: 'row',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
gap: spacing.xs,
|
|
44
|
+
paddingTop: spacing.sm,
|
|
45
|
+
paddingBottom: spacing.sm,
|
|
46
|
+
paddingLeft: spacing.md,
|
|
47
|
+
paddingRight: spacing.md,
|
|
48
|
+
fontFamily: typography.fontFamily,
|
|
49
|
+
fontSize: typography.fontSizeXs,
|
|
50
|
+
fontWeight: typography.fontWeightSemibold,
|
|
51
|
+
textTransform: 'uppercase' as const,
|
|
52
|
+
letterSpacing: '0.03em',
|
|
53
|
+
color: colors.textMuted,
|
|
54
|
+
},
|
|
55
|
+
headCellSortable: {
|
|
56
|
+
cursor: 'pointer',
|
|
57
|
+
userSelect: 'none',
|
|
58
|
+
color: {default: colors.textMuted, ':hover': colors.text},
|
|
59
|
+
},
|
|
60
|
+
sortMark: {
|
|
61
|
+
fontSize: typography.fontSizeXs,
|
|
62
|
+
color: colors.accent,
|
|
63
|
+
},
|
|
64
|
+
bodyTotal: (height: number) => ({
|
|
65
|
+
position: 'relative',
|
|
66
|
+
width: '100%',
|
|
67
|
+
height,
|
|
68
|
+
}),
|
|
69
|
+
row: {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
flexDirection: 'row',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
width: '100%',
|
|
74
|
+
borderBottomWidth: 1,
|
|
75
|
+
borderBottomStyle: 'solid',
|
|
76
|
+
borderBottomColor: colors.border,
|
|
77
|
+
},
|
|
78
|
+
rowClickable: {
|
|
79
|
+
cursor: 'pointer',
|
|
80
|
+
backgroundColor: {default: 'transparent', ':hover': colors.bgCardHover},
|
|
81
|
+
},
|
|
82
|
+
// Dynamic: absolute placement of a windowed row.
|
|
83
|
+
rowAbsolute: (y: number) => ({
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
top: 0,
|
|
86
|
+
left: 0,
|
|
87
|
+
transform: `translateY(${y}px)`,
|
|
88
|
+
}),
|
|
89
|
+
cell: {
|
|
90
|
+
paddingTop: spacing.sm,
|
|
91
|
+
paddingBottom: spacing.sm,
|
|
92
|
+
paddingLeft: spacing.md,
|
|
93
|
+
paddingRight: spacing.md,
|
|
94
|
+
fontFamily: typography.fontFamily,
|
|
95
|
+
fontSize: typography.fontSizeSm,
|
|
96
|
+
color: colors.text,
|
|
97
|
+
overflow: 'hidden',
|
|
98
|
+
textOverflow: 'ellipsis',
|
|
99
|
+
whiteSpace: 'nowrap',
|
|
100
|
+
},
|
|
101
|
+
// Dynamic: proportional column width from the TanStack column size (fr-like).
|
|
102
|
+
cellFlex: (size: number) => ({
|
|
103
|
+
flexGrow: size,
|
|
104
|
+
flexShrink: 1,
|
|
105
|
+
flexBasis: 0,
|
|
106
|
+
minWidth: 0,
|
|
107
|
+
}),
|
|
108
|
+
empty: {
|
|
109
|
+
paddingTop: spacing.xl,
|
|
110
|
+
paddingBottom: spacing.xl,
|
|
111
|
+
textAlign: 'center' as const,
|
|
112
|
+
color: colors.textMuted,
|
|
113
|
+
fontFamily: typography.fontFamily,
|
|
114
|
+
fontSize: typography.fontSizeSm,
|
|
115
|
+
},
|
|
116
|
+
// Floating position indicator, pinned bottom-right of the viewport.
|
|
117
|
+
pos: {
|
|
118
|
+
position: 'absolute',
|
|
119
|
+
right: spacing.sm,
|
|
120
|
+
bottom: spacing.sm,
|
|
121
|
+
paddingTop: 3,
|
|
122
|
+
paddingBottom: 3,
|
|
123
|
+
paddingLeft: spacing.sm,
|
|
124
|
+
paddingRight: spacing.sm,
|
|
125
|
+
borderRadius: radii.full,
|
|
126
|
+
backgroundColor: colors.bgCardHover,
|
|
127
|
+
borderWidth: 1,
|
|
128
|
+
borderStyle: 'solid',
|
|
129
|
+
borderColor: colors.border,
|
|
130
|
+
color: colors.textMuted,
|
|
131
|
+
fontFamily: typography.fontFamily,
|
|
132
|
+
fontSize: typography.fontSizeXs,
|
|
133
|
+
pointerEvents: 'none',
|
|
134
|
+
},
|
|
135
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,9 @@ export {
|
|
|
56
56
|
export {Tag, type TagVariant, type TagSize} from './components/Tag/Tag'
|
|
57
57
|
export {TagGroup} from './components/TagGroup/TagGroup'
|
|
58
58
|
export {Table, type TableVariant, type TableSize} from './components/Table/Table'
|
|
59
|
+
// Web-only (DOM scroll measurement + @tanstack/react-virtual); intentionally
|
|
60
|
+
// absent from index.native.ts.
|
|
61
|
+
export {VirtualTable, type VirtualTableRange} from './components/VirtualTable/VirtualTable'
|
|
59
62
|
// Side-effect import registers our augmentation of TanStack's ColumnMeta
|
|
60
63
|
// (stackLabel / isActions). Standalone module so the augmentation lands
|
|
61
64
|
// whenever this package is in a consumer's TS program, even if FromTanstack
|