@handled-ai/design-system 0.14.8 → 0.15.1
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/data-table-filter.d.ts +3 -1
- package/dist/components/data-table-filter.js +9 -3
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/virtualized-data-table.d.ts +20 -2
- package/dist/components/virtualized-data-table.js +164 -32
- package/dist/components/virtualized-data-table.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/virtualized-data-table-resize.test.tsx +524 -0
- package/src/components/__tests__/virtualized-data-table.test.tsx +557 -0
- package/src/components/data-table-filter.tsx +12 -2
- package/src/components/virtualized-data-table.tsx +196 -46
- package/src/index.ts +1 -0
|
@@ -10,12 +10,29 @@ import {
|
|
|
10
10
|
type SortingState,
|
|
11
11
|
type ColumnFiltersState,
|
|
12
12
|
type VisibilityState,
|
|
13
|
+
type ColumnSizingState,
|
|
13
14
|
type OnChangeFn,
|
|
14
15
|
} from "@tanstack/react-table"
|
|
15
|
-
import {
|
|
16
|
+
import type { RowData } from "@tanstack/react-table"
|
|
17
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, EyeOff, Check, SearchX, Loader2 } from "lucide-react"
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuTrigger,
|
|
22
|
+
DropdownMenuItem,
|
|
23
|
+
DropdownMenuSeparator,
|
|
24
|
+
} from "./dropdown-menu"
|
|
16
25
|
|
|
17
26
|
import { cn } from "../lib/utils"
|
|
18
27
|
|
|
28
|
+
declare module "@tanstack/react-table" {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
30
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
31
|
+
/** Server-side sort key for this column. Enables sort in the header menu when onColumnSort is also provided. */
|
|
32
|
+
sortKey?: string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
export interface VirtualizedDataTableProps<TData> {
|
|
20
37
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
38
|
columns: ColumnDef<TData, any>[]
|
|
@@ -36,6 +53,12 @@ export interface VirtualizedDataTableProps<TData> {
|
|
|
36
53
|
hasMore?: boolean
|
|
37
54
|
isFetchingMore?: boolean
|
|
38
55
|
|
|
56
|
+
// Column resizing
|
|
57
|
+
enableColumnResizing?: boolean
|
|
58
|
+
columnResizeMode?: "onChange" | "onEnd"
|
|
59
|
+
columnSizing?: ColumnSizingState
|
|
60
|
+
onColumnSizingChange?: OnChangeFn<ColumnSizingState>
|
|
61
|
+
|
|
39
62
|
// Server-driven state (controlled) — omit for internal state
|
|
40
63
|
sorting?: SortingState
|
|
41
64
|
onSortingChange?: OnChangeFn<SortingState>
|
|
@@ -50,6 +73,16 @@ export interface VirtualizedDataTableProps<TData> {
|
|
|
50
73
|
emptyMessage?: string
|
|
51
74
|
emptyDescription?: string
|
|
52
75
|
|
|
76
|
+
// Column header menu
|
|
77
|
+
/** Called when user requests sorting from column header. columnId is the column's meta.sortKey. */
|
|
78
|
+
onColumnSort?: (columnId: string, direction: "asc" | "desc") => void
|
|
79
|
+
/** Called when user hides a column from the header menu. */
|
|
80
|
+
onColumnHide?: (columnId: string) => void
|
|
81
|
+
/** The currently active sort column ID — matches a column's meta.sortKey. Used for visual indicators and aria-sort. */
|
|
82
|
+
activeSortColumn?: string | null
|
|
83
|
+
/** The current sort direction. Used for visual indicators and aria-sort. */
|
|
84
|
+
activeSortDirection?: "asc" | "desc"
|
|
85
|
+
|
|
53
86
|
// Styling
|
|
54
87
|
className?: string
|
|
55
88
|
}
|
|
@@ -66,12 +99,20 @@ export function VirtualizedDataTable<TData>({
|
|
|
66
99
|
reachBottomThreshold = 5,
|
|
67
100
|
hasMore = true,
|
|
68
101
|
isFetchingMore,
|
|
102
|
+
enableColumnResizing = false,
|
|
103
|
+
columnResizeMode = "onEnd",
|
|
104
|
+
columnSizing,
|
|
105
|
+
onColumnSizingChange,
|
|
69
106
|
sorting,
|
|
70
107
|
onSortingChange,
|
|
71
108
|
columnFilters,
|
|
72
109
|
onColumnFiltersChange,
|
|
73
110
|
columnVisibility,
|
|
74
111
|
onColumnVisibilityChange,
|
|
112
|
+
onColumnSort,
|
|
113
|
+
onColumnHide,
|
|
114
|
+
activeSortColumn,
|
|
115
|
+
activeSortDirection,
|
|
75
116
|
isLoading,
|
|
76
117
|
emptyIcon,
|
|
77
118
|
emptyMessage = "No rows found",
|
|
@@ -97,6 +138,13 @@ export function VirtualizedDataTable<TData>({
|
|
|
97
138
|
const resolvedOnColumnVisibilityChange =
|
|
98
139
|
onColumnVisibilityChange ?? setInternalColumnVisibility
|
|
99
140
|
|
|
141
|
+
// Controlled/uncontrolled state for column sizing
|
|
142
|
+
const [internalColumnSizing, setInternalColumnSizing] =
|
|
143
|
+
React.useState<ColumnSizingState>({})
|
|
144
|
+
const resolvedColumnSizing = columnSizing ?? internalColumnSizing
|
|
145
|
+
const resolvedOnColumnSizingChange =
|
|
146
|
+
onColumnSizingChange ?? setInternalColumnSizing
|
|
147
|
+
|
|
100
148
|
// TanStack Table setup
|
|
101
149
|
const table = useReactTable({
|
|
102
150
|
data,
|
|
@@ -106,10 +154,14 @@ export function VirtualizedDataTable<TData>({
|
|
|
106
154
|
sorting: resolvedSorting,
|
|
107
155
|
columnFilters: resolvedColumnFilters,
|
|
108
156
|
columnVisibility: resolvedColumnVisibility,
|
|
157
|
+
columnSizing: resolvedColumnSizing,
|
|
109
158
|
},
|
|
110
159
|
onSortingChange: resolvedOnSortingChange,
|
|
111
160
|
onColumnFiltersChange: resolvedOnColumnFiltersChange,
|
|
112
161
|
onColumnVisibilityChange: resolvedOnColumnVisibilityChange,
|
|
162
|
+
onColumnSizingChange: resolvedOnColumnSizingChange,
|
|
163
|
+
enableColumnResizing,
|
|
164
|
+
columnResizeMode,
|
|
113
165
|
manualSorting: true,
|
|
114
166
|
manualFiltering: true,
|
|
115
167
|
manualPagination: true,
|
|
@@ -185,52 +237,150 @@ export function VirtualizedDataTable<TData>({
|
|
|
185
237
|
className="flex w-max min-w-full border-b border-border/50"
|
|
186
238
|
role="row"
|
|
187
239
|
>
|
|
188
|
-
{headerGroup.headers.map((header, colIdx) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
aria-sort={
|
|
199
|
-
header.column.getIsSorted() === "asc"
|
|
200
|
-
? "ascending"
|
|
201
|
-
: header.column.getIsSorted() === "desc"
|
|
202
|
-
? "descending"
|
|
203
|
-
: header.column.getCanSort()
|
|
204
|
-
? "none"
|
|
205
|
-
: undefined
|
|
240
|
+
{headerGroup.headers.map((header, colIdx) => {
|
|
241
|
+
const sortKey = header.column.columnDef.meta?.sortKey
|
|
242
|
+
const canServerSort = Boolean(sortKey && onColumnSort)
|
|
243
|
+
|
|
244
|
+
const resolvedAriaSort = (() => {
|
|
245
|
+
if (activeSortColumn !== undefined) {
|
|
246
|
+
// Server-driven
|
|
247
|
+
if (!sortKey) return undefined
|
|
248
|
+
if (activeSortColumn === sortKey) return activeSortDirection === "asc" ? "ascending" as const : "descending" as const
|
|
249
|
+
return "none" as const
|
|
206
250
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
251
|
+
// Fallback to TanStack state
|
|
252
|
+
const sorted = header.column.getIsSorted()
|
|
253
|
+
if (sorted === "asc") return "ascending" as const
|
|
254
|
+
if (sorted === "desc") return "descending" as const
|
|
255
|
+
if (header.column.getCanSort()) return "none" as const
|
|
256
|
+
return undefined
|
|
257
|
+
})()
|
|
258
|
+
|
|
259
|
+
const sortIcon = (() => {
|
|
260
|
+
if (!canServerSort) return null
|
|
261
|
+
if (activeSortColumn === sortKey && activeSortDirection === "asc") return <ArrowUp className="w-3 h-3" />
|
|
262
|
+
if (activeSortColumn === sortKey && activeSortDirection === "desc") return <ArrowDown className="w-3 h-3" />
|
|
263
|
+
return <ArrowUpDown className="w-3 h-3 opacity-40" />
|
|
264
|
+
})()
|
|
265
|
+
|
|
266
|
+
const handleHeaderClick = canServerSort ? () => {
|
|
267
|
+
const newDir = activeSortColumn === sortKey
|
|
268
|
+
? (activeSortDirection === "asc" ? "desc" : "asc")
|
|
269
|
+
: "asc"
|
|
270
|
+
onColumnSort!(sortKey!, newDir)
|
|
271
|
+
} : undefined
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
key={header.id}
|
|
276
|
+
className={cn(
|
|
277
|
+
"group/header h-9 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative",
|
|
278
|
+
header.column.getCanResize() && "pr-4",
|
|
279
|
+
)}
|
|
280
|
+
style={{
|
|
281
|
+
width: header.getSize(),
|
|
282
|
+
minWidth: header.getSize(),
|
|
283
|
+
}}
|
|
284
|
+
role="columnheader"
|
|
285
|
+
aria-colindex={colIdx + 1}
|
|
286
|
+
aria-sort={resolvedAriaSort}
|
|
287
|
+
>
|
|
288
|
+
{header.isPlaceholder ? null : (
|
|
289
|
+
<>
|
|
290
|
+
{canServerSort ? (
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
className="inline-flex items-center gap-1 hover:text-foreground transition-colors"
|
|
294
|
+
onClick={handleHeaderClick}
|
|
295
|
+
>
|
|
296
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
297
|
+
{sortIcon}
|
|
298
|
+
</button>
|
|
299
|
+
) : header.column.getCanSort() ? (
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
className="inline-flex items-center gap-1 hover:text-foreground transition-colors"
|
|
303
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
304
|
+
>
|
|
305
|
+
{flexRender(
|
|
306
|
+
header.column.columnDef.header,
|
|
307
|
+
header.getContext(),
|
|
308
|
+
)}
|
|
309
|
+
{header.column.getIsSorted() === "asc" ? (
|
|
310
|
+
<ArrowUp className="w-3 h-3" />
|
|
311
|
+
) : header.column.getIsSorted() === "desc" ? (
|
|
312
|
+
<ArrowDown className="w-3 h-3" />
|
|
313
|
+
) : (
|
|
314
|
+
<ArrowUpDown className="w-3 h-3 opacity-40" />
|
|
315
|
+
)}
|
|
316
|
+
</button>
|
|
317
|
+
) : (
|
|
318
|
+
flexRender(
|
|
319
|
+
header.column.columnDef.header,
|
|
320
|
+
header.getContext(),
|
|
321
|
+
)
|
|
322
|
+
)}
|
|
323
|
+
{(canServerSort || header.column.getCanSort() || header.column.getCanHide()) && (
|
|
324
|
+
<DropdownMenu>
|
|
325
|
+
<DropdownMenuTrigger asChild>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
className="ml-1 inline-flex items-center hover:text-foreground transition-all opacity-0 group-hover/header:opacity-100"
|
|
329
|
+
aria-label="Column actions"
|
|
330
|
+
>
|
|
331
|
+
<ChevronDown className="w-3 h-3" />
|
|
332
|
+
</button>
|
|
333
|
+
</DropdownMenuTrigger>
|
|
334
|
+
<DropdownMenuContent align="start" className="w-48">
|
|
335
|
+
<DropdownMenuItem
|
|
336
|
+
disabled={!canServerSort}
|
|
337
|
+
onClick={() => canServerSort && onColumnSort!(sortKey!, "asc")}
|
|
338
|
+
>
|
|
339
|
+
<ArrowUp className="w-3.5 h-3.5 mr-2" />
|
|
340
|
+
Sort ascending
|
|
341
|
+
{activeSortColumn === sortKey && activeSortDirection === "asc" && <Check className="w-3.5 h-3.5 ml-auto" />}
|
|
342
|
+
</DropdownMenuItem>
|
|
343
|
+
<DropdownMenuItem
|
|
344
|
+
disabled={!canServerSort}
|
|
345
|
+
onClick={() => canServerSort && onColumnSort!(sortKey!, "desc")}
|
|
346
|
+
>
|
|
347
|
+
<ArrowDown className="w-3.5 h-3.5 mr-2" />
|
|
348
|
+
Sort descending
|
|
349
|
+
{activeSortColumn === sortKey && activeSortDirection === "desc" && <Check className="w-3.5 h-3.5 ml-auto" />}
|
|
350
|
+
</DropdownMenuItem>
|
|
351
|
+
{header.column.getCanHide() && (
|
|
352
|
+
<>
|
|
353
|
+
<DropdownMenuSeparator />
|
|
354
|
+
<DropdownMenuItem
|
|
355
|
+
onClick={() => onColumnHide ? onColumnHide(header.column.id) : header.column.toggleVisibility(false)}
|
|
356
|
+
>
|
|
357
|
+
<EyeOff className="w-3.5 h-3.5 mr-2" />
|
|
358
|
+
Hide column
|
|
359
|
+
</DropdownMenuItem>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
</DropdownMenuContent>
|
|
363
|
+
</DropdownMenu>
|
|
364
|
+
)}
|
|
365
|
+
</>
|
|
366
|
+
)}
|
|
367
|
+
{header.column.getCanResize() && (
|
|
368
|
+
<div
|
|
369
|
+
onMouseDown={header.getResizeHandler()}
|
|
370
|
+
onTouchStart={header.getResizeHandler()}
|
|
371
|
+
className={cn(
|
|
372
|
+
"absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
|
|
373
|
+
"after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
|
|
374
|
+
"after:bg-transparent hover:after:bg-primary/30",
|
|
375
|
+
header.column.getIsResizing() && "after:bg-primary/50",
|
|
376
|
+
)}
|
|
377
|
+
role="separator"
|
|
378
|
+
aria-orientation="vertical"
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
)
|
|
383
|
+
})}
|
|
234
384
|
</div>
|
|
235
385
|
))}
|
|
236
386
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,7 @@ export * from "./components/tooltip"
|
|
|
91
91
|
export * from "./components/variable-autocomplete"
|
|
92
92
|
export * from "./components/view-mode-toggle"
|
|
93
93
|
export * from "./components/virtualized-data-table"
|
|
94
|
+
export type { ColumnSizingState } from "@tanstack/react-table"
|
|
94
95
|
|
|
95
96
|
// Charts (re-exported for backward compatibility with root imports)
|
|
96
97
|
export * from "./charts/index"
|