feat: modify table design

This commit is contained in:
2026-07-09 17:14:37 +05:30
parent 87dde1c8d1
commit 0e8108b875
5 changed files with 116 additions and 56 deletions

View File

@@ -1,7 +1,12 @@
'use client'; 'use client';
import { Table } from '@tanstack/react-table'; import { Table } from '@tanstack/react-table';
import { ChevronLeft, ChevronRight } from 'lucide-react'; import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@@ -25,26 +30,33 @@ export function TableFooter<TData>({
pageSize?: number; pageSize?: number;
onPageSizeChange?: (pageSize: number) => void; onPageSizeChange?: (pageSize: number) => void;
}) { }) {
const rows = table.getRowModel().rows.length;
const currentPageSize = pageSize ?? table.getState().pagination.pageSize; const currentPageSize = pageSize ?? table.getState().pagination.pageSize;
const selectedRowCount = Object.keys(table.getState().rowSelection).length;
const pageIndex = table.getState().pagination.pageIndex;
const rawPageCount = table.getPageCount();
const pageCount = Math.max(
1,
Number.isFinite(rawPageCount) && rawPageCount > 0 ? rawPageCount : 1,
);
return ( return (
<div className="flex flex-col gap-3 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 px-4 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-3"> <div>
<span> {selectedRowCount} of {totalItems} row(s) selected.
Showing {rows} of {totalItems} </div>
</span>
<div className="flex flex-wrap items-center gap-4 sm:justify-end">
{onPageSizeChange ? ( {onPageSizeChange ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 text-foreground">
<span>Rows per page</span> <span className="font-medium">Rows per page</span>
<Select <Select
value={String(currentPageSize)} value={String(currentPageSize)}
onValueChange={(value) => onPageSizeChange(Number(value))} onValueChange={(value) => table.setPageSize(Number(value))}
> >
<SelectTrigger className="h-8 w-18 bg-background"> <SelectTrigger className="h-8 w-20 border-border bg-background shadow-none">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent align="end">
{PAGE_SIZE_OPTIONS.map((size) => ( {PAGE_SIZE_OPTIONS.map((size) => (
<SelectItem key={size} value={String(size)}> <SelectItem key={size} value={String(size)}>
{size} {size}
@@ -54,26 +66,57 @@ export function TableFooter<TData>({
</Select> </Select>
</div> </div>
) : null} ) : null}
<div className="min-w-24 text-foreground font-medium">
Page {pageIndex + 1} of {pageCount}
</div> </div>
<div className="flex gap-2">
<div className="flex items-center gap-2">
<Button <Button
type="button"
variant="outline" variant="outline"
size="sm" size="icon-sm"
className="size-8 shadow-none"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
aria-label="Go to first page"
>
<ChevronsLeft className="size-4" />
</Button>
<Button
type="button"
variant="outline"
size="icon-sm"
className="size-8 shadow-none"
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
aria-label="Go to previous page"
> >
<ChevronLeft className="size-4" /> <ChevronLeft className="size-4" />
Previous
</Button> </Button>
<Button <Button
type="button"
variant="outline" variant="outline"
size="sm" size="icon-sm"
className="size-8 shadow-none"
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
aria-label="Go to next page"
> >
Next
<ChevronRight className="size-4" /> <ChevronRight className="size-4" />
</Button> </Button>
<Button
type="button"
variant="outline"
size="icon-sm"
className="size-8 shadow-none"
onClick={() => table.setPageIndex(pageCount - 1)}
disabled={!table.getCanNextPage()}
aria-label="Go to last page"
>
<ChevronsRight className="size-4" />
</Button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -21,26 +21,35 @@ const TopHeader = ({
tableTools, tableTools,
}: TopHeaderProps) => { }: TopHeaderProps) => {
return ( return (
<div className="space-y-4"> <div className="space-y-3">
{(title || onAddNew) && ( {(title || onAddNew) && (
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div> <div className="min-w-0">
{title ? <h2>{title}</h2> : null} {title ? <h2 className="text-lg font-semibold">{title}</h2> : null}
<p className="text-muted-foreground">Total {itemCount ?? 0}</p> <p className="text-sm text-muted-foreground">
Total {itemCount ?? 0}
</p>
</div> </div>
{onAddNew ? ( {onAddNew ? (
<Button onClick={onAddNew}> <Button onClick={onAddNew} size="sm" className="w-fit">
<Plus className="mr-2 size-4" /> <Plus className="size-4" />
{addButtonText} {addButtonText}
</Button> </Button>
) : null} ) : null}
</div> </div>
)} )}
{(toolbar || tableTools) && ( {(toolbar || tableTools) && (
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="min-w-0 flex-1">{toolbar}</div> {toolbar ? (
<div className="min-w-0 flex-1 **:data-[slot=input-group]:border-border [&_[data-slot=input-group]]:bg-background [&_[data-slot=input-group]]:shadow-none [&_[data-slot=select-trigger]]:border-border [&_[data-slot=select-trigger]]:bg-background [&_[data-slot=select-trigger]]:shadow-none">
{toolbar}
</div>
) : null}
{tableTools ? ( {tableTools ? (
<div className="flex shrink-0 justify-end">{tableTools}</div> <div className="flex shrink-0 justify-end **:data-[slot=button]:shadow-none">
{tableTools}
</div>
) : null} ) : null}
</div> </div>
)} )}

View File

@@ -16,7 +16,7 @@ import { getPinnedColumnStyles } from './columnStyles';
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => { const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
return ( return (
<ShadTableHeader className="sticky top-0 z-20 bg-card"> <ShadTableHeader className="sticky top-0 z-20 bg-muted/70 backdrop-blur supports-backdrop-filter:bg-muted/60">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
@@ -39,9 +39,9 @@ const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
canSort ? header.column.getToggleSortingHandler() : undefined canSort ? header.column.getToggleSortingHandler() : undefined
} }
className={cn( className={cn(
'h-10 max-w-0 overflow-hidden border-b border-border bg-card px-2 text-foreground transition-colors', 'h-11 max-w-0 overflow-hidden border-b border-border bg-muted/70 px-4 text-sm font-semibold text-foreground transition-colors',
canSort && 'cursor-pointer select-none hover:bg-muted/60', canSort && 'cursor-pointer select-none hover:bg-muted',
sortDirection && 'bg-muted/70', sortDirection && 'bg-muted',
)} )}
> >
{header.isPlaceholder ? null : ( {header.isPlaceholder ? null : (

View File

@@ -23,14 +23,20 @@ export function TableSearchInput({
className, className,
}: TableSearchInputProps) { }: TableSearchInputProps) {
return ( return (
<InputGroup className={cn('md:max-w-xs', className)}> <InputGroup
className={cn(
'h-9 border-border bg-background shadow-none md:max-w-xs',
className,
)}
>
<InputGroupAddon> <InputGroupAddon>
<Search /> <Search className="size-4" />
</InputGroupAddon> </InputGroupAddon>
<InputGroupInput <InputGroupInput
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
className="placeholder:text-muted-foreground/70"
/> />
</InputGroup> </InputGroup>
); );

View File

@@ -210,8 +210,8 @@ function DataTableContent<TData, TValue>({
}, },
manualPagination: !!pagination, manualPagination: !!pagination,
manualSorting: isManualSorting, manualSorting: isManualSorting,
pageCount: pagination?.totalItems pageCount: pagination
? Math.ceil(pagination.totalItems / pagination.limit) ? Math.ceil((pagination.totalItems ?? data.length) / pagination.limit)
: -1, : -1,
state: { state: {
rowSelection, rowSelection,
@@ -247,7 +247,7 @@ function DataTableContent<TData, TValue>({
return ( return (
<> <>
<section className="space-y-4 rounded-lg border bg-card p-4"> <section className="space-y-3">
<TopHeader <TopHeader
title={onAddNew ? title : undefined} title={onAddNew ? title : undefined}
itemCount={pagination?.totalItems ?? data.length} itemCount={pagination?.totalItems ?? data.length}
@@ -259,9 +259,9 @@ function DataTableContent<TData, TValue>({
} }
/> />
<div> <div className="overflow-hidden rounded-lg border border-border bg-background">
<Table <Table
containerClassName="max-h-[calc(100vh-350px)] overflow-y-auto" containerClassName="max-h-[calc(100vh-350px)] overflow-auto"
className="table-fixed border-separate border-spacing-0" className="table-fixed border-separate border-spacing-0"
style={{ width: '100%', minWidth: table.getTotalSize() }} style={{ width: '100%', minWidth: table.getTotalSize() }}
> >
@@ -273,7 +273,7 @@ function DataTableContent<TData, TValue>({
{columns.map((_, colIdx) => ( {columns.map((_, colIdx) => (
<TableCell <TableCell
key={colIdx} key={colIdx}
className="border-b border-border" className="h-13 border-b border-border px-4 py-3"
> >
<Skeleton className="h-4 w-full max-w-35" /> <Skeleton className="h-4 w-full max-w-35" />
</TableCell> </TableCell>
@@ -290,7 +290,9 @@ function DataTableContent<TData, TValue>({
const truncate = shouldTruncateCell(cell.column); const truncate = shouldTruncateCell(cell.column);
const pinned = cell.column.getIsPinned(); const pinned = cell.column.getIsPinned();
const isActionsCell = cell.column.id === 'actions'; const isActionsCell = cell.column.id === 'actions';
const isRowClickable = Boolean(onRowClick && !isActionsCell); const isRowClickable = Boolean(
onRowClick && !isActionsCell,
);
const content = flexRender( const content = flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext(), cell.getContext(),
@@ -306,8 +308,8 @@ function DataTableContent<TData, TValue>({
: undefined : undefined
} }
className={cn( className={cn(
'max-w-0 overflow-hidden border-b border-border', 'h-13 max-w-0 overflow-hidden border-b border-border px-4 py-3 text-sm',
pinned && 'bg-card', pinned && 'bg-background',
isRowClickable && 'cursor-pointer', isRowClickable && 'cursor-pointer',
isActionsCell && 'cursor-default', isActionsCell && 'cursor-default',
)} )}