@jacopozanti/data-table
v0.12.0
Published
A feature-rich React data table (sorting, grouping, row selection, hover preview, keyboard nav, and a type-safe filter system) built on TanStack Table and Base UI.
Maintainers
Readme
@jacopozanti/data-table
A feature-rich React data table: sorting, grouping, row selection, global search, a hover preview pane, keyboard navigation, and a type-safe filter system. Built on TanStack Table and Base UI.
Install
npm install @jacopozanti/data-table tailwindcss tw-animate-cssRequires React 18.2+ or 19 and Tailwind v4 as peer dependencies.
tw-animate-css is an optional peer (recommended — it powers the
popover/tooltip/dropdown transitions; shadcn/ui projects already have it).
Everything else the table needs is installed automatically.
Setup
With Tailwind v4 (re-themeable)
In your app's main stylesheet (the one Tailwind processes), pull in Tailwind, the animation utilities, and our design tokens:
@import "tailwindcss";
@import "tw-animate-css";
@import "@jacopozanti/data-table/theme.css";That's it — our theme.css already registers itself as a Tailwind source, so
the utilities our components use end up in your bundle automatically. This path
lets you re-theme via the CSS custom properties.
Without Tailwind (precompiled CSS)
Not using Tailwind (or on v3, CSS Modules, plain CSS)? Import the precompiled stylesheet once — it ships every class the components use plus the tokens, and contains no page reset (no Tailwind preflight), so it won't touch your app's styles:
import "@jacopozanti/data-table/styles.css";You can still re-theme by overriding the token custom properties (--primary,
--background, --radius, --shadow-md, …) after the import — the
rounded-* / shadow-* the table uses derive from them. tailwindcss is then
not needed as a peer dependency.
Already using shadcn/ui? Your theme wins automatically: the package ships
its token defaults inside @layer base, so the shadcn tokens you already have
in :root / .dark (unlayered) always take precedence — no matter the import
order. The table just inherits your look.
Dark mode: add a dark class to any ancestor element (e.g.
<html class="dark">).
Usage
Give the table a height-constrained flex container so its body can scroll.
import { DataTable } from "@jacopozanti/data-table";
import type { TableColumn } from "@jacopozanti/data-table";
type Person = { id: string; name: string; age: number; role: string };
const data: Person[] = [
{ id: "1", name: "Ada Lovelace", age: 36, role: "engineering" },
{ id: "2", name: "Linus Torvalds", age: 54, role: "operations" },
];
// `size` is optional (xs | s | m | l | xl | fill) and defaults to `fill` —
// omit it for columns that should expand to fill the remaining width.
const columns: TableColumn<Person>[] = [
{ id: "name", header: "Name", renderCell: (r) => r.name },
{ id: "age", header: "Age", size: "s", renderCell: (r) => r.age },
{ id: "role", header: "Role", size: "m", renderCell: (r) => r.role },
];
export function People() {
return (
<div className="flex h-screen flex-col p-4">
<DataTable
columns={columns}
data={data}
getRowId={(r) => r.id}
onRowClick={(r) => console.log("clicked", r)}
/>
</div>
);
}Typed columns (optional)
createColumnHelper<TData>() builds columns with autocomplete and type-checking
on id (from the keys of TData), and sensible defaults for accessorFn /
renderCell:
import { createColumnHelper } from "@jacopozanti/data-table";
const col = createColumnHelper<Person>();
const columns = [
col.accessor("name", { header: "Name" }), // renderCell defaults to String(row.name)
col.accessor("age", { header: "Age", size: "s", align: "end" }),
col.display({ id: "actions", header: "", renderCell: (r) => <RowMenu row={r} /> }),
];accessor(id, …) flags a typo in id at compile time; display(…) is for
non-data columns (free-form id, renderCell required).
Adding filters (optional)
Build type-safe filter columns with createColumnConfigHelper and pass them as
filterColumns. They show up in the toolbar's filter menu.
import { DataTable, createColumnConfigHelper } from "@jacopozanti/data-table";
import { Hash, Tag, User } from "lucide-react";
const dtf = createColumnConfigHelper<Person>();
const filterColumns = [
dtf.text().id("name").accessor((r) => r.name).displayName("Name").icon(User).build(),
dtf.number().id("age").accessor((r) => r.age).displayName("Age").icon(Hash).build(),
dtf.option().id("role").accessor((r) => r.role).displayName("Role").icon(Tag).build(),
] as const;
<DataTable columns={columns} data={data} filterColumns={filterColumns} />;For server-side filtering set filterStrategy="server" and control the
state yourself — the table only manages the filter UI, you apply filters to
your query:
const [filters, setFilters] = useState<FiltersState>([]);
<DataTable
columns={columns}
data={serverData}
filterColumns={filterColumns}
filterStrategy="server"
filters={filters}
onFiltersChange={setFilters}
filterOptions={{ role: [{ label: "Engineering", value: "engineering" }] }}
/>;For server-side global search, pass onSearch — the table stops filtering
data by the search text and hands you the query to run yourself (debounce as
needed). Optionally control the input with searchValue:
const [q, setQ] = useState("");
<DataTable
columns={columns}
data={serverData} // already filtered by your query
searchValue={q}
onSearch={(value) => {
setQ(value);
// debounce + fetch: refetch(value)
}}
/>;Reading the selection
Selection (checkboxes and the X shortcut) is reported through
onRowSelectionChange with the selected row objects already resolved:
const [selected, setSelected] = useState<Person[]>([]);
<DataTable
columns={columns}
data={data}
getRowId={(r) => r.id}
onRowSelectionChange={(_selection, rows) => setSelected(rows)}
/>;
{selected.length > 0 && <BulkActionsBar rows={selected} />}Props
| Prop | Type | Description |
| --------------- | --------------------------- | -------------------------------------------------------- |
| columns | TableColumn<TData>[] | Column definitions. Required. |
| data | TData[] | Row data. Required. |
| title | ReactNode | Title/label shown in the toolbar, left of the search bar.|
| groupBy | string[] | Column ids to group by (synced by value — inline arrays are fine). |
| onGroupingChange | (grouping) => void | Notified when grouping changes. |
| sorting / onSortingChange | SortingState| Controlled sorting (omit sorting for internal state). |
| rowSelection / onRowSelectionChange | Record<string, boolean> | Controlled selection; callback also receives resolved rows. |
| onRowClick | (row) => void | Called when a row is clicked / activated with Enter. |
| rowActions | RowAction<TData>[] | Per-row actions at the end of each row (see below). |
| renderSubRow | (row) => ReactNode | Detail panel under an expanded row (adds a chevron column). |
| maxExpandedRows | number | Max detail panels open at once; oldest closes past the cap. Default 1. |
When renderSubRow is set and onRowClick is not, clicking a row (or pressing
Enter on the focused row) toggles its detail panel, which opens and closes with
a height animation.
| density | "comfortable" \| "compact"| Row padding. Default "comfortable". |
| hiddenColumns / onHiddenColumnsChange | string[] | Controlled hidden columns (pairs with the toolbar menu). |
| pagination | boolean \| { pageSize, pageSizeOptions } | Enables the pagination footer. |
| paginationState / onPaginationChange | PaginationState | Controlled pagination. |
| rowCount | number | Server-side total: table stops slicing rows itself. |
| renderSummary | (ctx) => ReactNode | Summary/stats on the left of the footer; ctx has rows (filtered) and total. |
| virtualized | boolean | Row virtualization for large flat lists — only visible rows are mounted. Ignored when groupBy/renderSubRow is active. Pair with a fixed-height container, and prefer setting it at mount. |
| getRowId | (row) => string | Stable row id (recommended for selection & keyboard nav).|
| enablePreview | boolean | Enables the hover/peek preview pane with a default body built from the columns. Implied by renderPreview. |
| renderPreview | (row) => ReactNode | Fully custom preview body (overrides the default column-based one). |
| previewClassName | string | Extra classes for the preview pane (position, size, …). |
| titleActions | ReactNode | Toolbar content right after the title, before the search bar. |
| searchActions | ReactNode | Toolbar content after the search bar, before the filter/group/column buttons. |
| actions | ReactNode | Extra toolbar content after the built-in filter/group/column buttons. |
| variant | "default" \| "outline" | Container style: bare (default) or a bordered frame (outline). |
| rowDividers | boolean | Horizontal divider line between rows. Default false. |
| columnDividers| "headers" \| "all" \| "none" | Where to draw vertical dividers: only between the header cells ("headers", default), between header and body cells ("all"), or nowhere ("none"/null). |
| emptyState | ReactNode | Custom empty state (default: localized "No results"). |
| loading | boolean | Shows skeleton rows instead of data. |
| enableHotkeys | boolean | Window-level shortcuts; disable for multiple tables. Default true. |
| locale | "en" \| "it" | Language for all built-in strings. Default "en". |
| filterColumns | ColumnConfig[] | Filters built with createColumnConfigHelper. |
| searchValue / onSearch | string / (value) => void | Global search. Passing onSearch makes search server-side (the table stops filtering data by text; you apply it). searchValue optionally controls the input. |
| filterStrategy| "client" \| "server" | Where column filtering happens. Default "client". |
| filters / onFiltersChange | FiltersState| Controlled filters state (required for server strategy). |
| filterOptions | Record<string, ColumnOption[]> | Static options per column (required for server strategy). |
| filterFaceted | Record<string, Map \| [min, max]> | Faceted counts / number ranges per column. |
TableColumn: required id, header, renderCell. Optional size
(xs|s|m|l|xl|fill, defaults to fill), pinned ("left" | "right"),
aggregate ("sum"|"avg"|"min"|"max"|"count"|"uniqueCount" or a custom
function — shown in group header rows), tooltip (string or ReactNode),
renderPreviewCell (overrides how the value is rendered in the preview pane;
falls back to renderCell), accessorFn, enableSorting, enableGrouping.
Per-column cell/header control: align ("start"|"center"|"end", applied to
both the header and cells), padded (default true; set false so a custom
renderCell with h-full w-full fills the whole cell edge-to-edge — e.g. a
full-cell button with its own hover area), and cellClassName / headerClassName
(merged onto the <td> / <th>):
import { DataTableCellButton } from "@jacopozanti/data-table";
{
id: "start",
header: "Start",
align: "center",
padded: false, // drop the default px-4/py inset so the button fills the cell
renderCell: (e) => (
// DataTableCellButton fills the cell with an edge-to-edge hover/click area
<DataTableCellButton align="center" onClick={() => edit(e)}>
{format(e.start, "HH:mm")}
</DataTableCellButton>
),
}(DataTableCellButton is exported for exactly this; you can also use a plain
<button className="h-full w-full …">.)
The toolbar also includes a built-in column visibility menu (show/hide columns) next to the filter and grouping menus.
Examples by prop
The examples below change one prop at a time (assuming the columns and data
from the Usage section). Try them interactively by running the local
playground.
variant — container style
"default" (bare, blends into the page) or "outline" (rounded frame; the
toolbar and footer become segmented bands flush to the border).
<DataTable columns={columns} data={data} variant="outline" />density — row height
"comfortable" (default) or "compact" for denser tables.
<DataTable columns={columns} data={data} density="compact" />title + titleActions / searchActions / actions — toolbar content
See Custom toolbar actions below for the full
recipe. title sets the toolbar heading; the three slots inject content after
the title, after the search bar, and after the built-in buttons.
<DataTable columns={columns} data={data} title="Tasks" />groupBy + aggregate — grouping with aggregations
Group rows by one or more column ids; give a column an aggregate to show a
total/average/… in each group header.
const columns = [
{ id: "estimate", header: "Est.", aggregate: "sum", renderCell: (r) => r.estimate },
// …
];
<DataTable columns={columns} data={data} groupBy={["status"]} />Groups auto-expand for small datasets; above ~500 rows they start collapsed (expanding thousands of leaf rows at once would freeze the page) and the user expands them on demand. Applying a grouping runs in a React transition, with a loading overlay while the (potentially heavy) re-render is in flight.
renderSubRow + maxExpandedRows — expandable detail panel
Adds an expander chevron; the returned node renders in a full-width row under
the expanded one. maxExpandedRows caps how many stay open (default 1).
<DataTable
columns={columns}
data={data}
maxExpandedRows={2}
renderSubRow={(row) => <pre>{JSON.stringify(row, null, 2)}</pre>}
/>pagination + paginationState + renderSummary — footer
Enable the footer with pagination; add renderSummary for stats on the left
(it shows the footer even without pagination). See
Pagination for controlled/server-side setups.
<DataTable
columns={columns}
data={data}
pagination={{ pageSize: 10 }}
renderSummary={({ total }) => <span>{total} tasks</span>}
/>enablePreview / renderPreviewCell / renderPreview — preview pane
enablePreview turns on the hover/peek pane (hold Space, Shift+Space to pin),
built from the columns. Override a single field with renderPreviewCell, or the
whole body with renderPreview.
const columns = [
{
id: "createdAt",
header: "Created",
renderCell: (r) => r.createdAt.toLocaleDateString(),
renderPreviewCell: (r) => r.createdAt.toString(), // roomier value in the pane
},
// …
];
<DataTable columns={columns} data={data} enablePreview />hiddenColumns — column visibility
Controlled hidden column ids (uncontrolled if omitted — the toolbar menu still
works). Pair with onHiddenColumnsChange to persist.
<DataTable columns={columns} data={data} hiddenColumns={["assignee"]} />loading — skeleton rows
<DataTable columns={columns} data={[]} loading />emptyState — no-results content
<DataTable
columns={columns}
data={[]}
emptyState={<div className="py-10 text-center">No tasks yet</div>}
/>locale — built-in string language
"en" (default) or "it" ship with the package; add more with
registerLocale.
<DataTable columns={columns} data={data} locale="it" />Custom toolbar actions
Add your own actions to the toolbar via titleActions (after the title),
searchActions (after the search bar, before the filter buttons) or actions
(after them). For buttons that match the built-in filter/group/column controls
in both variants, use the exported DataTableIconButton / DataTableButton —
they read the active variant and render as segmented cells in outline or
outline shadcn buttons in default:
import { DataTable, DataTableIconButton, DataTableButton } from "@jacopozanti/data-table";
import { Download, Plus, RefreshCw } from "lucide-react";
<DataTable
columns={columns}
data={data}
titleActions={<DataTableIconButton icon={Plus} label="New" onClick={onNew} />}
searchActions={
<>
<DataTableIconButton icon={RefreshCw} label="Refresh" onClick={onRefresh} />
<DataTableButton icon={Download} onClick={onExport}>Export</DataTableButton>
</>
}
/>DataTableIconButton takes icon + label (used for aria-label and the
tooltip; pass tooltip={false} to disable, or a node to customize). Both
components forward the usual <button> props (onClick, disabled, …).
The toolbar only draws dividers between slots, so multiple actions inside one
slot sit flush. Add DataTableActionDivider between them to match the built-in
separators — a full-height rule in the outline band (it renders nothing in the
default layout, where actions are just spaced buttons):
import { DataTableActionDivider } from "@jacopozanti/data-table";
searchActions={
<>
<DataTableIconButton icon={RefreshCw} label="Refresh" onClick={onRefresh} />
<DataTableActionDivider />
<DataTableButton icon={Download} onClick={onExport}>Export</DataTableButton>
</>
}Column pinning
Freeze columns while scrolling horizontally, either declaratively or at runtime from the header context menu (right-click → Pin to left/right, Unpin):
const columns: TableColumn<Person>[] = [
{ id: "name", header: "Name", pinned: "left", renderCell: (r) => r.name },
{ id: "age", header: "Age", size: "s", renderCell: (r) => r.age },
];Pinned columns move to the table edge and stick during horizontal scroll.
When something is pinned left, the selection/grouping columns freeze with it;
when pinned right, the row-actions column freezes too. Notes: a pinned fill
column is resized to l (sticky offsets need fixed widths) and sticky cells use
the --dt-surface token as their backdrop. Group header rows stick to the top
during vertical scroll, but are not frozen horizontally when columns are pinned.
Row actions
Add per-row actions with rowActions: they render as icon buttons at the end
of the row (revealed on hover), with tooltips from label. Set inMenu: true
to collapse an action into a trailing "⋮" dropdown instead:
import { Copy, Pencil, Trash2 } from "lucide-react";
<DataTable
columns={columns}
data={data}
rowActions={[
{ label: "Edit", icon: Pencil, onClick: (r) => openEditor(r) },
{ label: "Duplicate", icon: Copy, inMenu: true, onClick: (r) => duplicate(r) },
{
label: "Delete",
icon: Trash2,
variant: "destructive", // tinted red, both inline and in the menu
inMenu: true,
disabled: (r) => r.locked, // boolean or per-row predicate
onClick: (r) => remove(r),
},
]}
/>;Action clicks never trigger onRowClick. Right-clicking a row opens a
context menu listing all of its actions (inline and inMenu alike).
Pagination
// Client-side: the table slices rows and renders a footer
<DataTable columns={columns} data={data} pagination={{ pageSize: 25 }} />
// Server-side: you slice, the table tracks state and shows totals
<DataTable
columns={columns}
data={pageOfRows}
pagination
rowCount={totalFromServer}
paginationState={paginationState}
onPaginationChange={setPaginationState}
/>Note: with grouping active, client-side pagination counts group header rows as rows of the page.
Custom locales
"en" and "it" ship with the package. Add (or override) languages at
runtime — call once at app startup:
import { registerLocale } from "@jacopozanti/data-table";
registerLocale("de", { search: "Suchen...", clear: "Leeren", /* ... */ });
<DataTable locale="de" ... />;CSV export
import { toCsv, downloadCsv } from "@jacopozanti/data-table";
<DataTable
onRowSelectionChange={(_sel, rows) => setSelected(rows)}
actions={
<Button onClick={() => downloadCsv("export", toCsv(selected, columns))}>
Export CSV
</Button>
}
/>;Persisting table state
Every piece of state has a controlled form — persist what you need:
const [hidden, setHidden] = useState<string[]>(
() => JSON.parse(localStorage.getItem("table:hidden") ?? "[]"),
);
<DataTable
hiddenColumns={hidden}
onHiddenColumnsChange={(next) => {
setHidden(next);
localStorage.setItem("table:hidden", JSON.stringify(next));
}}
/>;
// Same pattern: sorting/onSortingChange, groupBy/onGroupingChange,
// rowSelection/onRowSelectionChange, filters/onFiltersChange,
// paginationState/onPaginationChange.When persisting filters, revive Date values on load (JSON turns them into
strings) — date-column filters store Date objects in values:
filters.forEach((f) => {
if (f.type === "date") f.values = f.values.map((v) => new Date(v));
});The playground/ app persists the full view to localStorage
as a complete working example.
Keyboard shortcuts
| Key | Action |
| -------------- | ---------------------------------------- |
| K | Focus the search box |
| F | Open the filter menu |
| G | Open the grouping menu |
| ↑ / ↓ | Move row focus |
| Enter | Activate focused row / expand group |
| Space (hold) | Preview the hovered row |
| Shift+Space | Toggle persistent preview mode |
| X | Toggle selection on the hovered row |
| Esc | Clear row focus |
Shortcuts never fire while you're typing in an input, and can be disabled
entirely with enableHotkeys={false} (e.g. two tables on one page).
Re-theming
The design tokens are plain CSS custom properties. Override any of them after
the theme.css import (or just rely on your existing shadcn theme — it wins
by default):
@import "tailwindcss";
@import "tw-animate-css";
@import "@jacopozanti/data-table/theme.css";
:root {
--primary: #7c3aed;
--accent: #f4f0ff;
}Develop
npm install
npm run dev # rebuild on change
npm run build # one-off build to dist/
npm run typecheck
npm test # vitest + testing-libraryPlayground
A Vite demo app lives in playground/ — it imports the package
source directly, so edits to src/ hot-reload instantly:
cd playground
npm install
npm run dev # http://localhost:5173Changelog
| Version | Date | Changes |
| ------- | ---------- | ----------------------------------------------------------------------------------------- |
| 0.12.0 | 2026-07-09 | Precompiled CSS: @jacopozanti/data-table/styles.css ships every utility the components use + the tokens (no Tailwind preflight), so the table works without Tailwind (tailwindcss is now an optional peer). Theming: the rounded-* scale derives from a host-overridable --radius (shadcn formula) and shadow-* from --shadow-{sm,md,lg,xl} tokens, so corners and elevation adapt to the consuming app's theme on both paths. |
| 0.11.0 | 2026-07-09 | DX: exported createColumnHelper (typed column builder with id autocomplete + accessor/render defaults); development-only warnings for common misconfigurations (duplicate ids, selection without getRowId, controlled filters/searchValue without their handler, virtualized without a bounded-height container); removed placeholder screenshot references from the README. |
| 0.10.0 | 2026-07-09 | Grid lines: rowDividers (horizontal lines between rows, default off) and columnDividers ("headers" default / "all" / "none"). Fix: DataTableActionDivider renders only in the outline band (nothing in the default layout of spaced buttons). |
| 0.9.0 | 2026-07-09 | Server-side global search: onSearch (its presence stops client-side text filtering — you apply the query) and optional controlled searchValue. |
| 0.8.0 | 2026-07-09 | DataTableCellButton, DataTableButton and DataTableIconButton now forward their ref to the underlying <button>, so they work as Radix/Base UI asChild popover/menu/tooltip triggers (no more "Function components cannot be given refs" warning). |
| 0.7.0 | 2026-07-09 | Added DataTableCellButton — a reusable full-cell button for renderCell (edge-to-edge hover/click area; a translucent hover layer that stays visible on top of the row's own hover/selected background; stops click propagation so it doesn't toggle the row's detail panel or fire onRowClick). Pair with the column's padded: false. |
| 0.6.0 | 2026-07-09 | Per-column cell/header control: align (start|center|end, header + cells — headers can finally be centered/right-aligned), padded (set false so a custom renderCell with h-full w-full fills the cell edge-to-edge, hover/click area included), cellClassName / headerClassName. New DataTableActionDivider to separate actions inside a toolbar slot. Fixes: grouping a large dataset no longer freezes the page (groups start collapsed above ~500 rows, applied in a React transition with a loading overlay); hover feedback now shows on every row (selected, unselected, with detail panel, with/without onRowClick, during keyboard nav — pinned cells and group headers included); with aggregations the group-header label spans the leading non-aggregated columns (no more wrapping); in the outline variant rows sit flush to the frame (no rounded selection background). Docs: per-prop examples with a screenshot-regeneration guide. |
| 0.5.0 | 2026-07-09 | Pagination (client/server) with footer; group aggregations (aggregate on columns); group headers render the grouped column's own renderCell as the label; expandable rows (renderSubRow, with a hover/cursor expander button, maxExpandedRows cap default 1, row-click toggle, and an open/close height animation); density and variant (default/outline) props — the outline frame turns the toolbar into a segmented band whose search and action cells fill the framed rectangle edge-to-edge, with the header/body flush to the border and a bordered footer; on mobile the toolbar stacks (title + actions, then a full-width search row) and the segmented band falls back to the plain layout; controlled hiddenColumns; registerLocale for custom languages; toCsv/downloadCsv utilities; accessibility pass (aria-sort + keyboard sorting, real checkbox semantics, ARIA menus, Escape closes context menus); fixed white native scrollbar in dark mode (color-scheme + slim themed scrollbar); redesigned the number filter (removed the broken slider — clear Single/Range tabs with labelled inputs; the Single↔Range switch preserves negation, e.g. is not → is not between); tooltips now follow the theme (dark tooltip + light key in dark mode, and vice-versa) instead of inverting; footer gained a renderSummary slot (stats on the left, pagination on the right); in outline, the footer is now a segmented band matching the toolbar — same height, full-height cells, vertical dividers, flush to the frame's side/bottom borders, square nav buttons; group header rows now stick to the top during vertical scroll; opt-in row virtualization for large flat lists (virtualized, powered by @tanstack/react-virtual — only visible rows are mounted; automatically disabled with groupBy/renderSubRow); two new toolbar slots — titleActions (after the title) and searchActions (after the search bar, before the filter buttons) — that fill segmented cells in the outline band and reflow with the responsive default layout; exported DataTableIconButton/DataTableButton for building custom toolbar actions that automatically match the built-in filter/group/column buttons in either variant; the preview pane now works without a custom renderPreview — set enablePreview and it's built from the columns (each field's label is the column header, its value the column's renderCell, or a per-column renderPreviewCell override for a roomier value). |
| 0.4.0 | 2026-07-08 | Column pinning: pinned: "left" \| "right" on TableColumn + pin/unpin from the header context menu; frozen columns stick during horizontal scroll with state-aware backgrounds (hover/selection/focus). |
| 0.3.0 | 2026-06-11 | rowActions prop: per-row icon-button actions with optional "⋮" overflow menu (inMenu), destructive variant, per-row disabled; right-click on a row opens a context menu with all its actions. |
| 0.2.0 | 2026-06-11 | Selection/sorting/grouping APIs (onRowSelectionChange + resolved rows, controlled sorting, onGroupingChange); server-side & controlled filters passthrough; column visibility menu; locale prop with full i18n (en/it); title / emptyState / loading / previewClassName / enableHotkeys props; theme tokens moved to @layer base so an existing shadcn theme always wins; dropped framer-motion; fixed: spaces not typable in search, groupBy inline-array reset, date-filter operator check, debounce re-creation; vitest suite + CI. |
| 0.1.0 | 2026-06-10 | Breaking: distribution via theme.css source requiring Tailwind v4 (no more precompiled CSS); tokens standardized on the shadcn/ui canon; column size now optional (defaults to fill); tooltip accepts ReactNode. |
| 0.0.2 | 2026-06-10 | Renamed theme tokens to flat shadcn-style names (--g-* → --*); removed unused tokens. |
| 0.0.1 | 2026-06-09 | Initial release. |
