Patterns
Data table
Advanced filters (text, select, number, date; all or any), search, sort, columns, saved views, CSV export.
| $73.75 | ||||||
| $87.50 | ||||||
| $115.00 | ||||||
| $128.75 | ||||||
| $156.25 | ||||||
| $170.00 |
1–6 of 16
1 / 3
data-table.tsx
"use client";
import { type ColumnDef, DataTable, Money, StatusBadge } from "@waveinno/ui";
import * as React from "react";
interface Row {
id: string;
plate: string;
site: string;
amount: number;
status: "Paid" | "Due" | "Overdue";
/** "2026-10-01" */
date: string;
hours: number;
}
const ROWS: Row[] = Array.from({ length: 24 }, (_, i) => ({
id: `r${i}`,
plate: `DHAKA METRO-GA ${11 + (i % 9)}-${(4821 + i * 37) % 9999}`,
site: ["Gulshan", "Banani", "Dhanmondi"][i % 3],
amount: 6000 + ((i * 1375) % 24000),
status: (["Paid", "Due", "Overdue"] as const)[i % 3],
date: `2026-09-${String(8 + i).padStart(2, "0")}`,
hours: 1 + ((i * 7) % 11),
}));
const SITE_OPTIONS = ["Gulshan", "Banani", "Dhanmondi"].map((x) => ({ value: x, label: x }));
const STATUS_OPTIONS = ["Paid", "Due", "Overdue"].map((x) => ({ value: x, label: x }));
const COLUMNS: ColumnDef<Row, unknown>[] = [
{ accessorKey: "plate", header: "Plate", meta: { strong: true, filter: { type: "text" }, edit: { type: "text" } } },
{ accessorKey: "site", header: "Site", meta: { filter: { type: "select" }, edit: { type: "select", options: SITE_OPTIONS } } },
{ accessorKey: "date", header: "Date", meta: { filter: { type: "date" }, edit: { type: "date" } } },
{
accessorKey: "hours",
header: "Hours",
meta: { align: "right", num: true, filter: { type: "number" }, edit: { type: "number", validate: (v) => (Number(v) >= 1 && Number(v) <= 24 ? null : "Between 1 and 24 hours") } },
},
{ accessorKey: "amount", header: "Amount", meta: { align: "right", filter: { type: "number", label: "Amount (paisa)" } }, cell: ({ row }) => <Money value={row.original.amount / 100} /> },
{
accessorKey: "status",
header: "Status",
meta: { filter: { type: "select" }, edit: { type: "select", options: STATUS_OPTIONS } },
cell: ({ row }) => {
const s = row.original.status;
return <StatusBadge label={s} tone={s === "Paid" ? "success" : s === "Due" ? "warning" : "danger"} />;
},
},
];
function useEditableRows(initial: Row[]) {
const [rows, setRows] = React.useState(initial);
const patch = (ids: Set<string>, col: string, value: string) =>
setRows((all) => all.map((r) => (ids.has(r.id) ? { ...r, [col]: col === "hours" ? Number(value) : value } : r)));
return {
rows,
handlers: {
getRowLabel: (r: Row) => `${r.plate} · ${r.site}`,
onCellEdit: async (row: Row, col: string, value: string) => {
await new Promise((r) => setTimeout(r, 300));
patch(new Set([row.id]), col, value);
},
onBulkEdit: async (picked: Row[], col: string, value: string) => {
await new Promise((r) => setTimeout(r, 300));
patch(new Set(picked.map((p) => p.id)), col, value);
},
onBulkDelete: async (picked: Row[]) => {
await new Promise((r) => setTimeout(r, 300));
const ids = new Set(picked.map((p) => p.id));
setRows((all) => all.filter((r) => !ids.has(r.id)));
},
},
};
}
export default function SessionsTable() {
const { rows, handlers } = useEditableRows(ROWS);
return (
<DataTable
columns={COLUMNS}
data={rows}
getRowId={(r) => r.id}
pageSize={6}
exportName="playground"
{...handlers}
defaultFilters={{ match: "and", rules: [{ id: "demo", column: "status", op: "any_of", value: ["Due", "Overdue"] }] }}
savedViews={[
{ id: "unpaid", name: "Unpaid", filters: { match: "and", rules: [{ id: "u", column: "status", op: "any_of", value: ["Due", "Overdue"] }] } },
{ id: "long", name: "Long stays (6h+)", filters: { match: "and", rules: [{ id: "l", column: "hours", op: "gte", value: "6" }] } },
]}
/>
);
}Import#
Everything comes from the package root. The example above uses:
your-component.tsx
import { type ColumnDef, DataTable, Money, StatusBadge } from "@waveinno/ui";Props#
Read from the source of this version, so they match the code exactly.
<DataTable>
src/components/patterns/data-table.tsx| Prop | Type | Default | Description |
|---|---|---|---|
columnsrequired | ColumnDef<T, unknown>[] | — | |
datarequired | T[] | — | |
getRowIdrequired | (row: T) => string | — | |
onRowClick | (row: T) => void | — | |
activeId | string | null | — | Row currently shown in the drawer — highlighted, not navigated to. |
searchPlaceholder | string | "Search…" | |
pageSize | number | 12 | |
footer | React.ReactNode | — | Rendered in <tfoot>: totals turn a table into a report (§8). |
emptyState | React.ReactNode | — | |
savedViews | SavedView[] | [] | |
toolbarExtra | React.ReactNode | — | |
exportName | string | "waveinno-export" | |
defaultFilters | FilterState | EMPTY_FILTERS | Filters to start with (e.g. from the URL). |
onFiltersChange | (filters: FilterState) => void | — | Told every time the filters change — save them, or send them to the API. |
manualFiltering | boolean | false | The server filters: rules are shown and reported but rows are not filtered here. |
filterLabels | Partial<FilterLabels> | — | |
variant | "default" | "striped" | "bordered" | "cards" | "minimal" | "default" | The look: `default` (hairline rows), `striped` (zebra, for long scans), `bordered` (a spreadsheet grid — the one for editing), `cards` (every row a floating card, for few, rich rows), `minimal` (no frame, inside cards and drawers). |
onCellEdit | (row: T, columnId: string, value: string) => void | Promise<void> | — | Save an edited cell. Return a promise to show a spinner; reject to keep the editor open. |
onBulkEdit | (rows: T[], columnId: string, value: string) => void | Promise<void> | — | Save one value into many rows (the bulk bar's Edit). Without it, the table calls `onCellEdit` once per selected row. |
getRowLabel | (row: T) => string | — | Name of a row in confirmations ("Delete these 3: …"). Defaults to the row id. |
onBulkDelete | (rows: T[]) => void | Promise<void> | — | Delete the selected rows (the bulk bar's Delete, after a confirmation). |
bulkActions | (rows: T[], clear: () => void) => React.ReactNode | — | Extra buttons in the bulk bar, e.g. Assign or Archive. `clear` empties the selection. |
className | string | — |
<Money>
src/components/patterns/money.tsx| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | number | null | undefined | — | |
compact | boolean | — | |
className | string | — | |
decimals | number | — | Override the currency's decimals — whole-number prices read cleaner. |
symbol | string | — | Escape hatch for a figure explicitly stated in another currency. |
<StatusBadge>
src/components/ui/badge.tsx| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | |
tone | Tone | "neutral" | |
variant | "soft" | "solid" | "outline" | "dot" | "gradient" | "live" | — | |
className | string | — |
Install#
If the project does not have the package yet, add it, then follow the setup for Next.js or Vite once.
$ npm install @waveinno/ui