Agent docs

6 · Tables

The kit's richest component and the usage grammar around it.

The kit's richest component and the usage grammar around it. Kit source: packages/ui/src/table (layered: presentational Table, independent feature hooks, orchestrated DataTable). Evidence registers under docs/evidence/20260807-0230-speedway-design-language/ in this repo.

Audience: an agent (or person) building or reviewing a table.

Travels with: 07 (its toolbar), 04 (skeleton and the three empty states), 05 (the card it bleeds inside), 10 (the peek or modal a row opens), 09 (work surface), 02 (row actions). Request-shaped bundle: AP-10.

§A Principles and mental models#

1. Pick the right layer.

  • Table (Layer A): draws exactly the rows given. Sort is affordance-only. For short, capped lists (a job's max-30 runs, an org's members: "auto layout, not table-fixed: a members table is small").
  • DataTable (Layer B): filter → search → sort → paginate pipeline (fixed order, so search never narrows just the current page), auto selection column, rowMenu, default toolbar, pager. "Turn features on by naming them in features; anything you leave out is off."
  • Raw <table>: legitimate only for dense inline-EDITABLE form grids (field editors, sample previews), never for browsable lists.

2. Row click means exactly one thing per table. Navigate (jobs list → job detail), or toggle expansion (when renderRowDetail exists, the name link becomes the way in), or quiet-pick (clickSelect tint). Never two. With checkbox selection outside the model, pass clickSelect={false}: "two selection states on one table diverge."

3. Tint priority is fixed. ?hl= highlight (bg-primary/10, scrolls into view once per key) > expanded (bg-primary/5) > click-picked (bg-primary/5, "open and picked read as one state", owner 2026-07-29) > hover. The click tint resets on page-flip, "the page number is the cheapest correct invalidation." ?hl=<id> is the app-wide "create lands you back on the list without losing the thing you just made" convention.

4. Columns encode meaning, via factories. col.mono (ids/SKUs), col.number (right-aligned tabular), col.date, col.status (StatusPill from a mapper), col.actions. Column tip explains non-obvious columns. Cell helpers are explicit, "a value only becomes a link/date/code because you asked, never by accident." Rich cells: name + mono sub-line, FileChip + +N overflow, clickable counts (muted 0), slim progress bar + stage caption, consolidated ranked status badges.

5. Width discipline: scroll, never overlap, shed for peeks. table-fixed + explicit minWidth makes narrow viewports scroll sideways ("owner: never overlap"). When a peek panel opens, shed the widest low-priority column instead of squeezing all. Expanded detail rows pin sticky left-0 to survive horizontal scroll.

6. Selection scope is explicit. Kit selection is page-scoped ("select all" = visible page). Whole-filter selection ("select all in filter") lives OUTSIDE the model with hand-placed SelectAllCell/SelectionCell + clickSelect={false} (the review queue). Row keys must be globally unique across the table's real domain (the job-qualified composite key lesson: item ids were only unique per job).

7. One implementation, reused. Shared list surfaces get ONE component reused everywhere they appear (JobsTable across Jobs page + every module tab; one buildPartColumns for both parts tables; one AttrDefsTable rendering page and panel densities via a variant prop, "a peek is a placement change, not a re-authoring"). Stated in doc comments so the surfaces "can't drift."

8. Header and pager rulings. Headers render as authored, Title Case, gray band (owner rounds 2026-07-10/16). Sort caret always trails the label. The sort button sizes to content so every header's affordance reads the same. Pager: windowed tokens keep fixed width at any depth. Cursor mode for keyset lists. Ctrl+Shift+arrows go to the pager inside an open modal first. copyCells puts the copy button beside the value, not at the column edge. It is opt-in per table rather than a default: a read-only data table should take it, editable grids should not (the value is already selectable there, and a copy button competes with the input). The sibling rule for generated content in overlays is 10 §A12, which draws the same data-plane line one layer up.

9. A sticky header needs an unbroken line to the thing that scrolls. position: sticky resolves against the nearest scroll container, so any wrapper between the header and the real scrollport captures it and pins it to a box that never moves. Two consequences, both measured rather than reasoned. The table takes overflow-x only when it declares minWidth or maxHeight, because overflow on one axis makes an element the scrollport on both; a table that needs to scroll sideways says so with minWidth. And the common idiom for clipping a full-bleed table's corners, overflow-hidden on the wrapper, silently kills the sticky header: use overflow-clip, which clips identically and does not create a scroll container. With maxHeight the table owns its scrollport and top-0 is right; without it the page is the scrollport, and since the shell insets its content the header cancels that inset through --shell-inset.

§B Presets#

SituationPreset
Browsable listDataTable + FilterBar toolbar + pager (25/50/100), full-bleed in card
Capped sub-listplain Table, maxHeight cap, scrolled not paged
Editable gridraw table-sm, native controls, cell-level skeletons
List + detailexpandable rows (one open at a time) or Eye-icon → modal
Bulk work surfaceout-of-model filter-wide selection, clickSelect off, warn-tint in-flight rows
Post-create returnnavigate with ?hl=<id>
In-modal tabledensity compact, max-h-[48dvh] scroll, column cap + footnote
Read-only data tablecopyCells on; skip it on editable grids

§C Use cases through Speedway#

  • JobsTable: navigate-or-expand duality, ProgressCell only when expandable, StagePill otherwise. minWidth 1436/1536. Export toast warns on truncation.
  • Parts tables: ?hl=, copyCells, active-row tint correlating with the open preview dialog, rowMenu only on the standalone page.
  • Review queue: 2000-row server window ("a 100-row window made the queue look mysteriously short", owner 2026-07-25), fixed px widths on every column, detailIndicator off (the Fix caret owns it).
  • Log viewer: NOT a table. Scan-mode custom markup with level badges and client-side tail search (run.tsx:512-537). Debug page: CodeBlocks + Tabs, no tables.
  • Audit log (review/log.tsx:633-718): IS a table, a full DataTable + FilterBar, distinct from the scan-mode Log viewer above; toolbar detail in 07 §C.
  • Admin: team/teams on DataTable with the house full-bleed trio. Diagnostic screens (scrape-requests, usage limits) on raw tables, flagged as the bounded rough tier.
@versable-git/ui · reference, canon, and method, read in place