Skip to content
All skills

DataTable — the one table

Claude skill atlas-datatable · for Frontend

The one table component of Atlas of Today's World (components/data-table/DataTable, ported from TealHub). Use ALWAYS when adding or changing any list/table on the site (admin lists, audit logs, data values, invitations…) — never write a raw <table> or a custom list with filters.

Atlas DataTable — the only table on the site

Every list of records is DataTable from src/components/data-table/DataTable.tsx (ADR-019, ARCHITEKTURA 15.1). It already gives: full-text search, sort + filter per column (funnel in each header), KPI chips, resizable / reorderable / hideable columns remembered per user, paging, row selection with a bulk-action bar, CSV export of what is on screen, a sticky Actions column with icon buttons. Never build another table, status-pill bar or "⋯" menu — extend this one.

The only exception is the permission matrix (features/roles/components/PermissionMatrix.tsx), which is a grid of checkboxes, not a list of records.

Usage (Server Component page → client table)

Rows are plain data, so the server page passes them straight in — no columns file per page, no functions in column definitions.

<DataTable
  tableKey="admin-entries"            // unique; preferences are stored under it
  caption="Topics & news"             // accessible name of the table
  searchParam="q"                     // optional: mirror search into the URL (server narrows)
  searchPlaceholder="Search topics and news…"
  initialSort={{ key: "updated", dir: "desc" }}
  stats={optionStats("status", STATUS_OPTIONS)}   // KPI chips "All" + one per option
  toolbar={<ToolbarLink href={…} active={mine}>Only mine</ToolbarLink>}
  emptyTitle="No topics or news yet"
  maxRowActions={2 + (canDelete ? 1 : 0)}  // most icons a row can show — sizes the Actions column
  columns={[
    { key: "title", label: "Title", link: true, sortable: true, filter: "text", width: "minmax(240px, 3fr)" },
    { key: "status", label: "Status", kind: "badge", options: STATUS_OPTIONS, sortable: true, filter: "select", width: "150px" },
    { key: "updated", label: "Updated", kind: "date", sortable: true, width: "128px" },
  ]}
  rows={items.map((item) => ({
    id: item.id,
    href: `/admin/…/${item.id}`,      // target of the `link` column
    values: { title: item.title, status: item.status, updated: item.updated_at },
    actions: <RowActions actions={[editAction(href), openAction(publicUrl), deleteAction(del.bind(null, item.id), `article ${item.title}`)]} />,
  }))}
/>

Column kind: text | number | date | datetime | badge | tags | code | boolean | color. filter: text (contains) | select (options = options or distinct values) | none. width is a CSS grid track (fixed or fr / minmax), never content-dependent. hidden: true = off by default, user can show it in "Columns". noExport leaves it out of CSV. Pre-rendered cells (row.cells[key]) only when a cell needs a link or a control.

Row actions — every table has them

RowActions + helpers in row-actions.ts (server-safe, plain data). Icons: edit (Pencil), view (Eye), open (ExternalLink, new tab), share, archive, restore, revoke (Ban), delete (Trash2, red), up / down (ArrowUp / ArrowDown, from moveActions), hide / show (EyeOff / Eye, from visibilityAction — confirmed like any other action), on / off (Power / PowerOff, turn a switch on / off), source (FileCode, view a source file) and download (Download; a link action with download: true renders a plain <a download>). Rules (TealHub owner rule):

  1. One click, icons right in the row — no "⋯" menu. Order: edit → open/view → archive/restore/revoke → delete last.
  2. Every record type the user can change gets its actions: edit (link to detail), open on the site when published, archive/unpublish when the domain has it, delete when the record may be removed. Show only actions the role is allowed to do (can(access.permissions, section, "e" | "d")); the DB (RLS / RPC) still decides.
  3. Every server action is confirmed in a dialog (ConfirmButton) naming the record; destructive = danger. The only exception is moving a row in a hand-ordered list: moveActions(up, down, { first, last }) gives two instant actions (no dialog — the opposite arrow undoes it); sort the table by the order column.
  4. Actions call existing Server Actions bound to the row id (deleteX.bind(null, id)), returning ActionState; the table refreshes on success. Content state changes only through the workflow RPCs (e.g. unpublishEntry).
  5. Each icon button has an accessible name + tooltip and grows to 44 px on touch.
  6. Only icon buttons go in the Actions column (no text buttons, toggles or wrappers around RowActions). Its width is never hand-set: pass maxRowActions = the most icons any row can show — count every conditional action as present (published-only "open", status-dependent ones…) and gate only by the viewer's rights (2 + (canMove ? 2 : 0) + (canDelete ? 1 : 0)); default 1. DataTable sizes the column with rowActionsWidth(n) (gridTemplate.ts): n × 24 px icons + (n − 1) × 2 px gaps
    • 13 px cell padding and border, never less than two icons so the "Actions" header fits — 1–2 → 63 px, 3 → 89, 4 → 115, 5 → 141, 6 → 167; on touch the icons count 44 px each (--row-icon), so nothing is clipped there either. Add an action to a table = raise its maxRowActions.

Checklist for a new / changed list

  • [ ] DataTable with a unique tableKey, caption, search, sortable + filterable columns.
  • [ ] KPI chips (optionStats) when there is a status / type column.
  • [ ] RowActions with edit / open / archive / delete as the domain allows, gated by permissions; maxRowActions = the most icons a row can show.
  • [ ] Page-specific toggles go to toolbar as ToolbarLink, not custom pill bars.
  • [ ] e2e: the list renders, a row action works (confirm dialog → result).
  • [ ] New action icon or cell kind? Add it to row-actions.ts / cells.tsx and the Storybook story, not inline in a page.

This site uses only the cookies it needs to work — no advertising or tracking cookies. Visits are counted without cookies. Privacy