@handled-ai/design-system 0.14.7 → 0.14.10
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 +9 -3
- package/dist/components/data-table-filter.js +99 -26
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -0
- package/dist/components/data-table.js +2 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/virtualized-data-table.d.ts +6 -2
- package/dist/components/virtualized-data-table.js +51 -20
- package/dist/components/virtualized-data-table.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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/data-table-filter.tsx +100 -33
- package/src/components/data-table.tsx +2 -1
- package/src/components/virtualized-data-table.tsx +40 -1
- package/src/index.ts +1 -0
|
@@ -15,11 +15,23 @@ import {
|
|
|
15
15
|
DropdownMenuTrigger,
|
|
16
16
|
} from "./dropdown-menu"
|
|
17
17
|
|
|
18
|
+
export interface FilterOption {
|
|
19
|
+
label: string
|
|
20
|
+
value: string
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export interface DataTableFilterCategory {
|
|
19
24
|
id: string
|
|
20
25
|
label: string
|
|
21
26
|
icon: React.ComponentType<{ className?: string }>
|
|
22
|
-
options: string[]
|
|
27
|
+
options: (string | FilterOption)[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getOptionValue(option: string | FilterOption): string {
|
|
31
|
+
return typeof option === "string" ? option : option.value
|
|
32
|
+
}
|
|
33
|
+
function getOptionLabel(option: string | FilterOption): string {
|
|
34
|
+
return typeof option === "string" ? option : option.label
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
interface DataTableFilterProps {
|
|
@@ -27,6 +39,8 @@ interface DataTableFilterProps {
|
|
|
27
39
|
selectedFilters: Record<string, string[]>
|
|
28
40
|
onToggleFilter: (categoryId: string, option: string) => void
|
|
29
41
|
className?: string
|
|
42
|
+
/** Minimum number of options before showing the sub-menu search input. Defaults to 8. */
|
|
43
|
+
optionSearchThreshold?: number
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
export function DataTableFilter({
|
|
@@ -34,8 +48,10 @@ export function DataTableFilter({
|
|
|
34
48
|
selectedFilters,
|
|
35
49
|
onToggleFilter,
|
|
36
50
|
className,
|
|
51
|
+
optionSearchThreshold = 8,
|
|
37
52
|
}: DataTableFilterProps) {
|
|
38
53
|
const [query, setQuery] = React.useState("")
|
|
54
|
+
const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})
|
|
39
55
|
|
|
40
56
|
const visibleCategories = React.useMemo(() => {
|
|
41
57
|
const normalized = query.trim().toLowerCase()
|
|
@@ -49,7 +65,7 @@ export function DataTableFilter({
|
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
return category.options.some((option) =>
|
|
52
|
-
option.toLowerCase().includes(normalized)
|
|
68
|
+
getOptionLabel(option).toLowerCase().includes(normalized)
|
|
53
69
|
)
|
|
54
70
|
})
|
|
55
71
|
}, [categories, query])
|
|
@@ -99,38 +115,89 @@ export function DataTableFilter({
|
|
|
99
115
|
</div>
|
|
100
116
|
|
|
101
117
|
<div className="max-h-[320px] overflow-y-auto p-1">
|
|
102
|
-
{visibleCategories.map((category) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{category.options.map((option) => {
|
|
110
|
-
const selected =
|
|
111
|
-
selectedFilters[category.id]?.includes(option) ?? false
|
|
118
|
+
{visibleCategories.map((category) => {
|
|
119
|
+
const subQuery = (subQueries[category.id] ?? "").trim().toLowerCase()
|
|
120
|
+
const filteredOptions = subQuery
|
|
121
|
+
? category.options.filter((opt) =>
|
|
122
|
+
getOptionLabel(opt).toLowerCase().includes(subQuery)
|
|
123
|
+
)
|
|
124
|
+
: category.options
|
|
112
125
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
return (
|
|
127
|
+
<DropdownMenuSub
|
|
128
|
+
key={category.id}
|
|
129
|
+
onOpenChange={(open) => {
|
|
130
|
+
if (!open) {
|
|
131
|
+
setSubQueries((prev) => {
|
|
132
|
+
const next = { ...prev }
|
|
133
|
+
delete next[category.id]
|
|
134
|
+
return next
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<DropdownMenuSubTrigger className="cursor-pointer py-1.5 text-xs">
|
|
140
|
+
<category.icon className="mr-2 h-3.5 w-3.5 text-muted-foreground" />
|
|
141
|
+
{category.label}
|
|
142
|
+
</DropdownMenuSubTrigger>
|
|
143
|
+
<DropdownMenuSubContent className="max-h-[320px] w-52 overflow-y-auto p-1">
|
|
144
|
+
{/* Submenu search — only for categories with many options */}
|
|
145
|
+
{category.options.length > optionSearchThreshold && (
|
|
146
|
+
<div className="sticky top-0 z-10 border-b border-border bg-popover p-1.5">
|
|
147
|
+
<div className="relative">
|
|
148
|
+
<Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
|
|
149
|
+
<input
|
|
150
|
+
className="h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted"
|
|
151
|
+
placeholder="Search..."
|
|
152
|
+
value={subQueries[category.id] ?? ""}
|
|
153
|
+
onChange={(e) =>
|
|
154
|
+
setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))
|
|
155
|
+
}
|
|
156
|
+
onClick={(e) => e.stopPropagation()}
|
|
157
|
+
onKeyDown={(e) => {
|
|
158
|
+
// Allow navigation keys to propagate to Radix menu handling
|
|
159
|
+
// so keyboard users can move to and select filtered options.
|
|
160
|
+
const navKeys = ["ArrowDown", "ArrowUp", "Enter", "Escape", "Tab"]
|
|
161
|
+
if (!navKeys.includes(e.key)) {
|
|
162
|
+
e.stopPropagation()
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
{/* Filtered options */}
|
|
170
|
+
{filteredOptions.map((option) => {
|
|
171
|
+
const value = getOptionValue(option)
|
|
172
|
+
const label = getOptionLabel(option)
|
|
173
|
+
const selected = selectedFilters[category.id]?.includes(value) ?? false
|
|
174
|
+
return (
|
|
175
|
+
<DropdownMenuItem
|
|
176
|
+
key={value}
|
|
177
|
+
className="cursor-pointer justify-between text-xs"
|
|
178
|
+
onSelect={(event) => {
|
|
179
|
+
event.preventDefault()
|
|
180
|
+
onToggleFilter(category.id, value)
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{label}
|
|
184
|
+
{selected ? (
|
|
185
|
+
<span className="text-[10px] font-semibold text-brand-purple">
|
|
186
|
+
Applied
|
|
187
|
+
</span>
|
|
188
|
+
) : null}
|
|
189
|
+
</DropdownMenuItem>
|
|
190
|
+
)
|
|
191
|
+
})}
|
|
192
|
+
{filteredOptions.length === 0 && category.options.length > 0 && (
|
|
193
|
+
<div className="p-2 text-center text-xs text-muted-foreground">
|
|
194
|
+
No matches
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</DropdownMenuSubContent>
|
|
198
|
+
</DropdownMenuSub>
|
|
199
|
+
)
|
|
200
|
+
})}
|
|
134
201
|
|
|
135
202
|
{visibleCategories.length === 0 ? (
|
|
136
203
|
<div className="p-2 text-center text-xs text-muted-foreground">
|
|
@@ -49,6 +49,7 @@ export type DataRow = {
|
|
|
49
49
|
headcount: string
|
|
50
50
|
lastFunding: string
|
|
51
51
|
owner: string
|
|
52
|
+
ownerEmail?: string
|
|
52
53
|
opportunityCount: number
|
|
53
54
|
productAdoptionScore: number
|
|
54
55
|
sourceSystem?: string
|
|
@@ -477,7 +478,7 @@ function isRowMatchingCategoryFilter(
|
|
|
477
478
|
case "lastFunding":
|
|
478
479
|
return options.includes(row.lastFunding)
|
|
479
480
|
case "owner":
|
|
480
|
-
return options.includes(row.owner)
|
|
481
|
+
return options.includes(row.ownerEmail ?? row.owner)
|
|
481
482
|
case "opportunityCount":
|
|
482
483
|
return options.some((option) => {
|
|
483
484
|
if (option === "3+") {
|
|
@@ -10,6 +10,7 @@ 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
16
|
import { ArrowDown, ArrowUp, ArrowUpDown, SearchX, Loader2 } from "lucide-react"
|
|
@@ -36,6 +37,12 @@ export interface VirtualizedDataTableProps<TData> {
|
|
|
36
37
|
hasMore?: boolean
|
|
37
38
|
isFetchingMore?: boolean
|
|
38
39
|
|
|
40
|
+
// Column resizing
|
|
41
|
+
enableColumnResizing?: boolean
|
|
42
|
+
columnResizeMode?: "onChange" | "onEnd"
|
|
43
|
+
columnSizing?: ColumnSizingState
|
|
44
|
+
onColumnSizingChange?: OnChangeFn<ColumnSizingState>
|
|
45
|
+
|
|
39
46
|
// Server-driven state (controlled) — omit for internal state
|
|
40
47
|
sorting?: SortingState
|
|
41
48
|
onSortingChange?: OnChangeFn<SortingState>
|
|
@@ -66,6 +73,10 @@ export function VirtualizedDataTable<TData>({
|
|
|
66
73
|
reachBottomThreshold = 5,
|
|
67
74
|
hasMore = true,
|
|
68
75
|
isFetchingMore,
|
|
76
|
+
enableColumnResizing = false,
|
|
77
|
+
columnResizeMode = "onEnd",
|
|
78
|
+
columnSizing,
|
|
79
|
+
onColumnSizingChange,
|
|
69
80
|
sorting,
|
|
70
81
|
onSortingChange,
|
|
71
82
|
columnFilters,
|
|
@@ -97,6 +108,13 @@ export function VirtualizedDataTable<TData>({
|
|
|
97
108
|
const resolvedOnColumnVisibilityChange =
|
|
98
109
|
onColumnVisibilityChange ?? setInternalColumnVisibility
|
|
99
110
|
|
|
111
|
+
// Controlled/uncontrolled state for column sizing
|
|
112
|
+
const [internalColumnSizing, setInternalColumnSizing] =
|
|
113
|
+
React.useState<ColumnSizingState>({})
|
|
114
|
+
const resolvedColumnSizing = columnSizing ?? internalColumnSizing
|
|
115
|
+
const resolvedOnColumnSizingChange =
|
|
116
|
+
onColumnSizingChange ?? setInternalColumnSizing
|
|
117
|
+
|
|
100
118
|
// TanStack Table setup
|
|
101
119
|
const table = useReactTable({
|
|
102
120
|
data,
|
|
@@ -106,10 +124,14 @@ export function VirtualizedDataTable<TData>({
|
|
|
106
124
|
sorting: resolvedSorting,
|
|
107
125
|
columnFilters: resolvedColumnFilters,
|
|
108
126
|
columnVisibility: resolvedColumnVisibility,
|
|
127
|
+
columnSizing: resolvedColumnSizing,
|
|
109
128
|
},
|
|
110
129
|
onSortingChange: resolvedOnSortingChange,
|
|
111
130
|
onColumnFiltersChange: resolvedOnColumnFiltersChange,
|
|
112
131
|
onColumnVisibilityChange: resolvedOnColumnVisibilityChange,
|
|
132
|
+
onColumnSizingChange: resolvedOnColumnSizingChange,
|
|
133
|
+
enableColumnResizing,
|
|
134
|
+
columnResizeMode,
|
|
113
135
|
manualSorting: true,
|
|
114
136
|
manualFiltering: true,
|
|
115
137
|
manualPagination: true,
|
|
@@ -188,7 +210,10 @@ export function VirtualizedDataTable<TData>({
|
|
|
188
210
|
{headerGroup.headers.map((header, colIdx) => (
|
|
189
211
|
<div
|
|
190
212
|
key={header.id}
|
|
191
|
-
className=
|
|
213
|
+
className={cn(
|
|
214
|
+
"h-9 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative",
|
|
215
|
+
header.column.getCanResize() && "pr-4",
|
|
216
|
+
)}
|
|
192
217
|
style={{
|
|
193
218
|
width: header.getSize(),
|
|
194
219
|
minWidth: header.getSize(),
|
|
@@ -229,6 +254,20 @@ export function VirtualizedDataTable<TData>({
|
|
|
229
254
|
header.getContext(),
|
|
230
255
|
)
|
|
231
256
|
)}
|
|
257
|
+
{header.column.getCanResize() && (
|
|
258
|
+
<div
|
|
259
|
+
onMouseDown={header.getResizeHandler()}
|
|
260
|
+
onTouchStart={header.getResizeHandler()}
|
|
261
|
+
className={cn(
|
|
262
|
+
"absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
|
|
263
|
+
"after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
|
|
264
|
+
"after:bg-transparent hover:after:bg-primary/30",
|
|
265
|
+
header.column.getIsResizing() && "after:bg-primary/50",
|
|
266
|
+
)}
|
|
267
|
+
role="separator"
|
|
268
|
+
aria-orientation="vertical"
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
232
271
|
</div>
|
|
233
272
|
))}
|
|
234
273
|
</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"
|