@@ -47,6 +47,11 @@ function renderColGroup<TData>(resolvedColumns: ColumnDef<TData, unknown>[]): Re
4747 )
4848}
4949
50+ /** Resolve a static or per-item className override (mirrors data-table). */
51+ function resolveClassName < T > ( value : string | ( ( item : T ) => string ) | undefined , item : T ) : string | undefined {
52+ return typeof value === 'function' ? value ( item ) : value
53+ }
54+
5055export function GroupedTable < TData > ( props : GroupedTableProps < TData > ) {
5156 const {
5257 columns,
@@ -73,6 +78,14 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
7378 isLoading,
7479 empty,
7580 className,
81+ toolbarClassName,
82+ tableClassName,
83+ headerRowClassName,
84+ headerCellClassName,
85+ groupHeaderClassName,
86+ bodyClassName,
87+ rowClassName,
88+ cellClassName,
7689 } = props
7790
7891 const [ sorting , setSorting ] = useControllableState ( sortingProp , [ ] , onSortingChange )
@@ -119,6 +132,7 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
119132
120133 const slices = useMemo (
121134 ( ) => groups . map ( g => ( {
135+ group : g ,
122136 id : g . id ,
123137 title : g . title ,
124138 meta : g . meta ,
@@ -139,6 +153,7 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
139153 onSearchChange = { setSearch }
140154 placeholder = { searchPlaceholder }
141155 debounceMs = { searchDebounceMs }
156+ className = { toolbarClassName }
142157 />
143158 ) }
144159 < div className = { cn ( 'w-full rounded-md border' , scrollable ? 'overflow-x-auto' : 'overflow-hidden' ) } >
@@ -155,13 +170,13 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
155170
156171 return renderShell (
157172 < >
158- < table className = " w-full table-fixed text-sm" >
173+ < table className = { cn ( ' w-full table-fixed text-sm' , tableClassName ) } >
159174 { renderColGroup ( resolvedColumns ) }
160175 < TableHeader >
161176 { headerGroups . map ( hg => (
162- < TableRow key = { hg . id } >
177+ < TableRow key = { hg . id } className = { headerRowClassName } >
163178 { hg . headers . map ( header => (
164- < TableHead key = { header . id } scope = "col" >
179+ < TableHead key = { header . id } scope = "col" className = { headerCellClassName } >
165180 { header . isPlaceholder
166181 ? null
167182 : flexRender ( header . column . columnDef . header , header . getContext ( ) ) }
@@ -172,38 +187,42 @@ export function GroupedTable<TData>(props: GroupedTableProps<TData>) {
172187 </ TableHeader >
173188 </ table >
174189
175- { visibleSlices . map ( ( slice , i ) => {
190+ { visibleSlices . map ( ( slice ) => {
176191 const open = isSearching ? true : isOpen ( slice . id )
177192 return (
178193 < Collapsible key = { slice . id } open = { open } onOpenChange = { ( ) => toggle ( slice . id ) } >
179194 < CollapsibleTrigger
180195 className = { cn (
181- 'flex w-full items-center gap-2 bg-muted/40 px-3 py- 2 text-left text-sm font-semibold hover:bg-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring' ,
182- i > 0 && 'border-t' ,
196+ 'flex h-10 w-full items-center gap-2 border-b bg-muted/40 px-2 text-left align-middle text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring' ,
197+ resolveClassName ( groupHeaderClassName , slice . group ) ,
183198 ) }
184199 >
185200 < Icon
186201 icon = { ChevronRight }
187202 aria-hidden
188- className = { cn ( 'size-4 shrink-0 text-muted-foreground transition-transform' , open && 'rotate-90' ) }
203+ className = { cn ( 'size-4 shrink-0 transition-transform' , open && 'rotate-90' ) }
189204 />
190205 < span > { slice . title } </ span >
191206 { slice . meta != null && (
192- < span className = "ml-auto flex items-center gap-2 font-medium text-muted-foreground " > { slice . meta } </ span >
207+ < span className = "ml-auto flex items-center gap-2 font-medium" > { slice . meta } </ span >
193208 ) }
194209 </ CollapsibleTrigger >
195210
196211 < CollapsibleContent className = "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down" >
197212 < table
198- className = " w-full table-fixed border-t text-sm"
213+ className = { cn ( ' w-full table-fixed text-sm' , tableClassName ) }
199214 aria-label = { typeof slice . title === 'string' ? slice . title : undefined }
200215 >
201216 { renderColGroup ( resolvedColumns ) }
202- < TableBody >
217+ < TableBody className = { bodyClassName } >
203218 { slice . rows . map ( ( row : Row < TData > ) => (
204- < TableRow key = { row . id } data-state = { row . getIsSelected ( ) ? 'selected' : undefined } >
219+ < TableRow
220+ key = { row . id }
221+ data-state = { row . getIsSelected ( ) ? 'selected' : undefined }
222+ className = { resolveClassName ( rowClassName , row ) }
223+ >
205224 { row . getVisibleCells ( ) . map ( cell => (
206- < TableCell key = { cell . id } className = "truncate" >
225+ < TableCell key = { cell . id } className = { resolveClassName ( cellClassName , cell ) } >
207226 { flexRender ( cell . column . columnDef . cell , cell . getContext ( ) ) }
208227 </ TableCell >
209228 ) ) }
0 commit comments