npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-css

Requires 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-library

Playground

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:5173

Changelog

| 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 notis 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. |