jengacore components · @jengacore/data-table
DataTable
Table + TanStack: sortable mono headers, row click, selected and fresh rows, empty state.
Install
terminal
npx shadcn@latest add @jengacore/data-tableWithout the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/data-table.json
Registry dependencies
npm packages
@tanstack/react-table@^8lucide-react
Source
components/jc/data-table.tsx
"use client"
import * as React from "react"
import {
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
type ColumnDef,
type SortingState,
} from "@tanstack/react-table"
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { EmptyState } from "@/components/jc/empty-state"
import { cn } from "@/lib/utils"
export type { ColumnDef } from "@tanstack/react-table"
/**
* The workhorse of all four apps: shadcn Table + TanStack. Quiet headers, hairline rows, accent hover,
* `secondary` for the selected row, mint flash for rows that arrive live (`isFresh`).
* Put `meta: { align: "right" }` on amount columns.
*/
export function DataTable<TData, TValue>({
columns,
data,
density = "default",
sorting: controlledSorting,
onSortingChange,
getRowId,
onRowClick,
selectedId,
isFresh,
empty = { title: "Nothing here yet." },
className,
}: {
columns: ColumnDef<TData, TValue>[]
data: TData[]
density?: "default" | "compact"
sorting?: SortingState
onSortingChange?: (sorting: SortingState) => void
getRowId?: (row: TData) => string
onRowClick?: (row: TData) => void
selectedId?: string
isFresh?: (row: TData) => boolean
empty?: { title: string; description?: React.ReactNode; action?: React.ReactNode }
className?: string
}) {
const [innerSorting, setInnerSorting] = React.useState<SortingState>([])
const sorting = controlledSorting ?? innerSorting
const table = useReactTable({
data,
columns,
getRowId,
state: { sorting },
onSortingChange: (updater) => {
const next = typeof updater === "function" ? updater(sorting) : updater
if (onSortingChange) onSortingChange(next)
else setInnerSorting(next)
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<div data-slot="data-table" className={cn("overflow-hidden rounded-lg border border-border bg-card shadow-card contrast:border-input", className)}>
<Table density={density}>
<TableHeader>
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id} className="hover:[&>td]:bg-transparent">
{hg.headers.map((h) => {
const align = (h.column.columnDef.meta as { align?: "right" } | undefined)?.align
const sorted = h.column.getIsSorted()
return (
<TableHead
key={h.id}
className={cn(align === "right" && "text-right")}
aria-sort={sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : undefined}
>
{h.isPlaceholder ? null : h.column.getCanSort() ? (
<button
type="button"
onClick={h.column.getToggleSortingHandler()}
className={cn("inline-flex items-center gap-1 uppercase hover:text-foreground", align === "right" && "flex-row-reverse")}
>
{flexRender(h.column.columnDef.header, h.getContext())}
{sorted === "asc" ? <ArrowUp className="size-3" /> : sorted === "desc" ? <ArrowDown className="size-3" /> : <ArrowUpDown className="size-3 opacity-40" />}
</button>
) : (
flexRender(h.column.columnDef.header, h.getContext())
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
selected={selectedId !== undefined && row.id === selectedId}
fresh={isFresh?.(row.original)}
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
onKeyDown={
onRowClick
? (e) => {
if (e.key === "Enter") onRowClick(row.original)
}
: undefined
}
tabIndex={onRowClick ? 0 : undefined}
className={cn(onRowClick && "cursor-pointer focus-ring [--focus-offset:-2px]")}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className={cn((cell.column.columnDef.meta as { align?: "right" } | undefined)?.align === "right" && "text-right")}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow className="hover:[&>td]:bg-transparent">
<TableCell colSpan={columns.length} className="h-auto whitespace-normal">
<EmptyState {...empty} />
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}