@@ -33,6 +33,13 @@ const features = tableFeatures({
3333
3434export type AdminColumnDef < T extends RowData > = ColumnDef < typeof features , T , unknown > ;
3535
36+ /** Set on a column's `meta`. */
37+ export interface AdminColumnMeta {
38+ /** Applied to the header and body cell alike, so widths stay in step. */
39+ className ?: string ;
40+ align ?: 'left' | 'right' ;
41+ }
42+
3643interface AdminDataTableProps < T extends RowData > {
3744 columns : AdminColumnDef < T > [ ] ;
3845 data : T [ ] ;
@@ -47,8 +54,10 @@ interface AdminDataTableProps<T extends RowData> {
4754 skeletonRows ?: number ;
4855 /** Dims the rows in place while a new page or search result is in flight. */
4956 refreshing ?: boolean ;
50- /** Pads short result sets to `skeletonRows` so the panel keeps one height. */
51- fillRows ?: boolean ;
57+ /** Pads the body out to this many rows so the panel keeps one height. */
58+ fillRows ?: number ;
59+ /** 'page' fills the shell and scrolls under a pinned header; 'panel' sits in a card. */
60+ variant ?: 'panel' | 'page' ;
5261}
5362
5463export function AdminDataTable < T extends RowData > ( {
@@ -61,6 +70,7 @@ export function AdminDataTable<T extends RowData>({
6170 skeletonRows = 8 ,
6271 refreshing,
6372 fillRows,
73+ variant = 'panel' ,
6474} : AdminDataTableProps < T > ) {
6575 const [ sorting , setSorting ] = useState < SortingState > ( [ ] ) ;
6676
@@ -75,16 +85,23 @@ export function AdminDataTable<T extends RowData>({
7585
7686 const rows = table . getRowModel ( ) . rows ;
7787
78- const fillerCount = fillRows ? Math . max ( 0 , skeletonRows - rows . length ) : 0 ;
88+ const fillerCount = fillRows ? Math . max ( 0 , fillRows - rows . length ) : 0 ;
89+ const isPage = variant === 'page' ;
90+ // Rows span the full width, so the edge cells carry the header bar's inset.
91+ const edgeInset =
92+ isPage ?
93+ '[&_td:first-child]:pl-6 [&_th:first-child]:pl-6 [&_td:last-child]:pr-6 [&_th:last-child]:pr-6'
94+ : '' ;
7995
8096 return (
81- < Table >
82- < TableHeader className = 'bg-muted/40' >
97+ < Table className = { edgeInset } containerClassName = { cn ( isPage && 'min-h-0 flex-1' ) } >
98+ < TableHeader className = { cn ( 'bg-muted/40' , isPage && 'bg-background sticky top-0 z-10' ) } >
8399 { table . getHeaderGroups ( ) . map ( headerGroup => (
84100 < TableRow key = { headerGroup . id } className = 'border-border hover:bg-transparent' >
85101 { headerGroup . headers . map ( header => {
86102 const sortable = enableSorting && header . column . getCanSort ( ) ;
87103 const sorted = header . column . getIsSorted ( ) ;
104+ const meta = header . column . columnDef . meta as AdminColumnMeta | undefined ;
88105 return (
89106 < TableHead
90107 key = { header . id }
@@ -96,12 +113,19 @@ export function AdminDataTable<T extends RowData>({
96113 }
97114 className = { cn (
98115 'text-muted-foreground h-9 px-3 text-xs font-medium' ,
116+ isPage && 'bg-muted/40' ,
99117 sortable &&
100118 'hover:text-foreground cursor-pointer transition-colors select-none' ,
119+ meta ?. className ,
101120 ) }
102121 onClick = { sortable ? header . column . getToggleSortingHandler ( ) : undefined }
103122 >
104- < div className = 'flex items-center gap-1' >
123+ < div
124+ className = { cn (
125+ 'flex items-center gap-1' ,
126+ meta ?. align === 'right' && 'justify-end' ,
127+ ) }
128+ >
105129 { header . isPlaceholder ? null : (
106130 flexRender ( header . column . columnDef . header , header . getContext ( ) )
107131 ) }
@@ -129,7 +153,10 @@ export function AdminDataTable<T extends RowData>({
129153 Array . from ( { length : skeletonRows } , ( _ , i ) => (
130154 < TableRow key = { `skeleton-${ i } ` } className = 'border-border hover:bg-transparent' >
131155 { columns . map ( ( _ , j ) => (
132- < TableCell key = { `skeleton-cell-${ j } ` } className = 'h-12 px-3' >
156+ < TableCell
157+ key = { `skeleton-cell-${ j } ` }
158+ className = { cn ( 'px-3' , isPage ? 'h-10' : 'h-11' ) }
159+ >
133160 < Skeleton className = 'h-3.5' style = { { width : `${ 45 + ( ( j * 17 ) % 40 ) } %` } } />
134161 </ TableCell >
135162 ) ) }
@@ -141,7 +168,12 @@ export function AdminDataTable<T extends RowData>({
141168 < TableCell
142169 colSpan = { columns . length || 1 }
143170 className = 'text-muted-foreground px-3 text-center whitespace-normal'
144- style = { fillRows ? { height : skeletonRows * 48 } : { height : 160 } }
171+ style = {
172+ isPage ? { height : '40vh' }
173+ : fillRows ?
174+ { height : fillRows * 44 }
175+ : { height : 160 }
176+ }
145177 >
146178 { emptyState }
147179 </ TableCell >
@@ -155,11 +187,22 @@ export function AdminDataTable<T extends RowData>({
155187 className = { cn ( 'border-border' , onRowClick && 'cursor-pointer' ) }
156188 onClick = { ( ) => onRowClick ?.( row . original ) }
157189 >
158- { row . getAllCells ( ) . map ( cell => (
159- < TableCell key = { cell . id } className = 'text-foreground h-12 px-3 text-[13px]' >
160- { flexRender ( cell . column . columnDef . cell , cell . getContext ( ) ) }
161- </ TableCell >
162- ) ) }
190+ { row . getAllCells ( ) . map ( cell => {
191+ const meta = cell . column . columnDef . meta as AdminColumnMeta | undefined ;
192+ return (
193+ < TableCell
194+ key = { cell . id }
195+ className = { cn (
196+ 'text-foreground px-3 text-[13px]' ,
197+ isPage ? 'h-10' : 'h-11' ,
198+ meta ?. align === 'right' && 'text-right' ,
199+ meta ?. className ,
200+ ) }
201+ >
202+ { flexRender ( cell . column . columnDef . cell , cell . getContext ( ) ) }
203+ </ TableCell >
204+ ) ;
205+ } ) }
163206 </ TableRow >
164207 ) ) }
165208
@@ -169,7 +212,10 @@ export function AdminDataTable<T extends RowData>({
169212 rows . length > 0 &&
170213 Array . from ( { length : fillerCount } , ( _ , i ) => (
171214 < TableRow key = { `filler-${ i } ` } className = 'border-border hover:bg-transparent' >
172- < TableCell colSpan = { columns . length || 1 } className = 'h-12 px-3' />
215+ < TableCell
216+ colSpan = { columns . length || 1 }
217+ className = { cn ( 'px-3' , isPage ? 'h-10' : 'h-11' ) }
218+ />
173219 </ TableRow >
174220 ) ) }
175221 </ TableBody >
0 commit comments