Design
llms.txt digitalcrew.tech

Layout

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.

MaxLayoutDigitalCrew.DataTableToolbar
DataTableToolbar · live · 1100px wideOpen

Guidelines#

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

The kit:

  • useDataTable({ data, columns, getRowId, globalFilterFn, initialState }) returns a Table 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.

Facts#

GroupLayout
ProductMax
Globalwindow.DigitalCrew.DataTableToolbar
Sourcemax-agent: src/components/data-table/

Example#

The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.

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