# DataTableToolbar

> The toolbar of Max's data-table kit (search, faceted filters, Reset and the View columns menu) and how the kit's pieces fit around a `Table`.

## Facts

- **Product**: Max
- **Family**: Layout
- **Global**: `window.DigitalCrew.DataTableToolbar`
- **Source**: `max-agent: src/components/data-table/`
- **Live preview**: /design/components/data-table-toolbar/preview.html
- **Page**: /design/components/data-table-toolbar

## Guidelines

**Use it for** list pages such as prospects, campaigns and deals.

**The kit**:
- `useDataTable({ data, columns, getRowId, globalFilterFn, initialState })` returns a [`Table`](/design/components/table.md) API shaped like TanStack Table, without the dependency. It handles search across every field, column filters (an array matches any listed value, a string matches as a substring), single-column sorting (ascending → descending → off), pagination, selection and column visibility.
- Columns are `{ id | accessorKey, accessorFn?, enableSorting?, enableHiding? }`.

**Pieces**:
- `DataTableToolbar`: `table`, `searchPlaceholder` (default "Filter..."), `searchKey` (search one column instead of every field), `filters` (`[{ columnId, title, options: [{ label, value, icon? }] }]`), `onReset`, and `children` for extra controls. Reset appears once any search or filter is set.
- `DataTableColumnHeader`: `column` and `title`, with an Asc / Desc / Hide menu. A column that can't sort gets a plain title.
- `DataTablePagination`: page sizes 10–50, "Page x of y", up to 5 numbered pages, and first and last buttons.

**Anatomy**: search is `h-8 w-[150px] lg:w-[250px]`, facet buttons are dashed outline `h-8`, and View is hidden below `lg`.

**Don't** build `columns` during render: the hook resets whenever the array's identity changes, which loops forever. Pagination reads `state.pagination.total`, so pass the server count there; the preview filters on the client and keeps `total` equal to the filtered count. With server pagination, render `getFullRowModel()`, as Max's tables do.

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  // Stage keys and colours from DEFAULT_PIPELINE_STAGES (features/pipeline/constants.ts).
  var STAGES = {
    prospect: ["Prospect", "blue"], contacted: ["Contacted", "orange"], replied: ["Replied", "emerald"],
    interested: ["Interested", "teal"], not_interested: ["Not Interested", "red"], existing_client: ["Client", "violet"]
  };
  var STAGE_OPTIONS = Object.keys(STAGES).map(function (k) { return { label: STAGES[k][0], value: k }; });
  var ROWS = [
    ["Claire Dubois", "CFO", "Northwind Traders", "interested", "email", "2026-09-24"],
    ["Jonas Weber", "Head of Sales", "Fabrikam", "replied", "linkedin", "2026-09-24"],
    ["Amelia Hart", "VP Revenue", "Litware", "contacted", "email", "2026-09-23"],
    ["Lucas Moreau", "COO", "Adatum Corp", "existing_client", "email", "2026-09-22"],
    ["Priya Nair", "Head of Growth", "Tailspin Toys", "replied", "email", "2026-09-22"],
    ["Tom Becker", "Head of RevOps", "Northwind Traders", "contacted", "linkedin", "2026-09-21"],
    ["Sofia Rossi", "Sales Director", "Wide World Importers", "prospect", "linkedin", "2026-09-19"],
    ["Mateo García", "CEO", "Proseware", "not_interested", "email", "2026-09-18"],
    ["Emma de Vries", "Marketing Lead", "Coho Winery", "contacted", "email", "2026-09-18"],
    ["Oliver Bennett", "CRO", "Contoso Logistics", "existing_client", "linkedin", "2026-09-17"],
    ["Freya Nilsson", "Head of SDR", "Alpine Ski House", "prospect", "email", "2026-09-16"],
    ["Maya Chen", "VP Sales", "Trey Research", "interested", "linkedin", "2026-09-15"],
    ["Daniel Ortiz", "Founder", "Blue Yonder Airlines", "prospect", "email", "2026-09-12"],
    ["Aoife Byrne", "Head of Partnerships", "Humongous Insurance", "contacted", "linkedin", "2026-09-11"]
  ].map(function (r, i) {
    return { id: "prospect-" + (i + 1), name: r[0], title: r[1], company: r[2], status: r[3], channel: r[4], last_touch: r[5] };
  });
  // Column definitions live outside the component: useDataTable re-reads them when their identity changes.
  var COLUMNS = [
    { accessorKey: "name" }, { accessorKey: "company" }, { accessorKey: "status" },
    { accessorKey: "channel", enableSorting: false }, { accessorKey: "last_touch" }
  ];
  var TITLES = { name: "Name", company: "Company", status: "Stage", channel: "Channel", last_touch: "Last touch" };
  function fmtDay(iso) { return new Date(iso + "T12:00:00Z").toLocaleDateString("en-US", { month: "short", day: "numeric" }); }
  function cell(id, row) {
    if (id === "name") return h("div", { className: "min-w-0" }, h("p", { className: "font-medium" }, row.name), h("p", { className: "text-xs text-muted-foreground" }, row.title));
    if (id === "status") {
      var st = STAGES[row.status], cls = DC.stageColorClasses(st[1]);
      return h(DC.Badge, { variant: "outline", className: DC.cn("gap-1.5", cls.badge) }, h("span", { className: DC.cn("size-1.5 rounded-full", cls.dot) }), st[0]);
    }
    if (id === "channel") return h("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground" },
      h(row.channel === "email" ? I.Mail : I.Linkedin, { className: "size-3.5" }), row.channel === "email" ? "Email" : "LinkedIn");
    if (id === "last_touch") return h("span", { className: "tabular-nums text-muted-foreground" }, fmtDay(row.last_touch));
    return row[id];
  }
  function App() {
    var c = React.useState(ROWS.length), count = c[0], setCount = c[1];
    var table = DC.useDataTable({
      data: ROWS, columns: COLUMNS, getRowId: function (r) { return r.id; },
      initialState: { pagination: { pageIndex: 0, pageSize: 10, total: count }, sorting: [{ id: "last_touch", desc: true }] }
    });
    // Client-side filtering here, so the pagination total follows the filtered rows.
    var filtered = table._internal.sortedData.length;
    React.useEffect(function () { setCount(filtered); }, [filtered]);
    var rows = table.getRowModel().rows;
    var visible = COLUMNS.map(function (col) { return col.accessorKey; }).filter(function (id) { return table.getColumn(id).getIsVisible(); });
    var allOnPage = rows.length > 0 && rows.every(function (r) { return r.getIsSelected(); });
    var someOnPage = rows.some(function (r) { return r.getIsSelected(); });
    return h("div", { className: "space-y-4" },
      h(DC.DataTableToolbar, { table: table, searchPlaceholder: "Search people...", filters: [{ columnId: "status", title: "Stage", options: STAGE_OPTIONS }] }),
      h("div", { className: "overflow-hidden rounded-md border" },
        h(DC.Table, null,
          h(DC.TableHeader, null, h(DC.TableRow, null,
            h(DC.TableHead, { className: "w-12" }, h(DC.Checkbox, {
              checked: allOnPage ? true : someOnPage ? "indeterminate" : false, "aria-label": "Select all", className: "translate-y-[2px]",
              onCheckedChange: function (v) { rows.forEach(function (r) { r.toggleSelected(v === true || v === "indeterminate"); }); }
            })),
            visible.map(function (id) {
              return h(DC.TableHead, { key: id }, h(DC.DataTableColumnHeader, { column: table.getColumn(id), title: TITLES[id] }));
            }))),
          h(DC.TableBody, null, rows.length === 0
            ? h(DC.TableRow, null, h(DC.TableCell, { colSpan: visible.length + 1, className: "h-24 text-center text-muted-foreground" }, "No people match these filters."))
            : rows.map(function (r) {
              return h(DC.TableRow, { key: r.id, "data-state": r.getIsSelected() ? "selected" : undefined },
                h(DC.TableCell, null, h(DC.Checkbox, { checked: r.getIsSelected(), "aria-label": "Select " + r.original.name, className: "translate-y-[2px]",
                  onCheckedChange: function (v) { r.toggleSelected(!!v); } })),
                visible.map(function (id) { return h(DC.TableCell, { key: id }, cell(id, r.original)); }));
            })))),
      h(DC.DataTablePagination, { table: table }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Layout

- [Accordion](/design/components/accordion.md): Stacked sections that expand one at a time (or several); height animates in 250ms on the smooth-out ease (`--acc-*`).
- [Card](/design/components/card.md): The raised surface for grouped content: `card` fill, `border`, `radius-xl` (14px), `shadow-sm`, 24px padding and gap.
- [Collapsible](/design/components/collapsible.md): Shows and hides one region under its trigger (Radix Collapsible); animates with the accordion tokens.
- [ContentSection](/design/components/content-section.md): The settings-page section: a title, a one-line description, a separator, then a scrolling body.
- [Main](/design/components/main.md): The page body of Max's app shell: a `<main>` with the standard page padding and, by default, a centred 7xl width.
- [PanelSplitter](/design/components/panel-splitter.md): The draggable seam between two side-by-side panels; every split view in Max uses it, so one gesture works everywhere.
- [ScrollArea](/design/components/scroll-area.md): A scroll container with thin themed scrollbars (10px track, `border`-coloured thumb) that appear on hover.
- [Separator](/design/components/separator.md): A 1px `border` hairline, horizontal or vertical (Radix Separator, decorative by default).
- [Table](/design/components/table.md): The data table primitive: 14px rows with `border` dividers, 40px header cells in medium weight, `muted` at 50% on hover and selection.
