Agent docs

5 · Cards, surfaces, and stat tiles

Cards, framed sections, stat tiles, and surface spacing.

Cards, framed sections, stat tiles, and surface spacing. Kit sources: packages/ui/src/card.tsx, stat-tile, list-item. Evidence registers under docs/evidence/20260807-0230-speedway-design-language/ in this repo.

Audience: an agent (or person) laying out cards, surfaces, or stat tiles.

Travels with: 06 (the table it wraps full-bleed), 09 (widths and archetype), 04 (its skeleton keeps the box), 10 (inset ownership is the same rule one layer up). Request-shaped bundle: AP-10.

§A Principles and mental models#

1. Card is the universal content-section wrapper, and its header is documentation. Every non-table content block wraps in a kit Card. title/subtitle double as visual framing and inline docs ("Outputs" / "What this run wrote, part by part"). The header renders only when content is given. mt-4 is the standard rhythm between stacked cards. Admin pages use a gap-4 flex column.

2. Card actions live in the toolbar prop. A mutating action scoped to a card goes through toolbar={...}, never a sibling div. Toolbars may mix status chips with one action (plan chip + Suspended chip + "Open usage console"). A read-only toolbar holding a single Chip is legitimate (your own role on the Profile card).

3. Inside an already-framed surface, use dividers, not nested boxes. Peek panels and modals compose sections with border-t dividers plus a SectionLabel and quiet count, deliberately not nested Cards. Nesting cards inside a framed overlay is a defect.

4. Exactly one owner of horizontal inset per framed container. The general rule, of which full-bleed tables are the first instance: a framed surface pads its content once, and anything needing the edge CANCELS that padding rather than re-adding its own. Tables in cards are the house idiom. The kit affordance is DataTable's bleed prop, which does this for you; walmart uses it in five files, speedway in none. The hand-rolled equivalent is scrollClassName="-mx-6 ... border-t border-base-200" with edge padding carried by first/last cells, "never doubled onto every cell (that overflowed 2px)", and it stays the right choice when the surface also needs a custom scrollport. Reach for bleed first. The toolbar and pager keep the card's padding. Rows meet the card edges.

The rule generalizes past cards because both measured violations were modals, not cards. The measurement and its evidence live at 10 §A10. The diagnostic is structural rather than visual: for any framed container, name the single element that owns the inset. If two can claim it, one of them is a bug waiting for someone to notice the alignment.

5. The list-page card is a flex column that owns its scroll. Card className="mt-4 flex max-h-full min-h-0 flex-col" + bodyClassName="min-h-0 flex-1 flex flex-col" so the card grows to the viewport and the table scrolls internally. Pin a min-h floor where short viewports would crush it (the SHELL-05 regression: a table collapsed to "a 4px strip").

6. StatTile is for a single entity's summary row, not for lists. The one stat-row in the jobs/parts scope is the run detail page (Examined / module verb / Sent to review / Duration, grid-cols-2 md:grid-cols-4). Lists put numbers in columns instead. A tile explains an odd number via Tooltip rather than an unexplained hint (Duration's "recoveries"). Tiles are clickable-with-arrow when they navigate (arrow, href wins over onClick). Corrected 2026-08-13: this doc previously said the dashboard KPI row "was deliberately stripped 2026-07-29 (hide this info for now, may return)". The strip never reached this surface. Speedway ships one at app/routes/workspaces/workspace.tsx:301-344, and git settles the question rather than leaving it open: git log -S 'StatTile' -- app/routes/workspaces/workspace.tsx returns exactly one commit, a1a62a3 on 2026-07-16, which added the tiles, and there is no removal commit. The 2026-07-29 commit recorded as the removal is 48f8a76, "dashboard: ready-to-export counts jobs; tiles deep-link the filtered jobs list", which enhanced the tiles on the very day they were supposedly stripped. Method note, since this passage exists to show its work: an earlier version of this correction reasoned from the file's last-modified date, which was an unrelated commit and says nothing about whether one block returned. Use git log -S on the symbol, not the file's mtime. So the account-level KPI row is a live second shape for StatTile alongside the single-entity summary, and both are sanctioned. What stays banned is a tile per row of a list, which is what the principle's title is actually about.

7. compact marks config density. Card compact is reserved for denser form/config screens (admin usage console). Customer-facing content cards stay full-density.

8. Special card shapes.

  • AuthCard: the card-as-page-shell, centered max-w-md, brand lockup + title + subtitle, used identically by all five auth routes, the most rigid composition in the app.
  • Dashed CTA pseudo-card: dropzone-styled click target that opens the create modal ("reads like a dropzone, opens the create modal"), with a secondary quiet button on a fully-clickable surface.
  • ListItem: the row-in-card primitive. When it links, a stretched-link overlay makes the whole row clickable while actions stays independently clickable above it.
  • dl-as-stat-list: a two-column dl grid for label/value summaries inside a card when a table is too heavy (admin team usage).

§B Presets#

SituationPreset
Content sectionCard + title/subtitle, mt-4 rhythm
Card with actionCard + toolbar slot
List pageflex-column Card, full-bleed DataTable, internal scroll, min-h floor
Entity summaryStatTile row, 2→4 columns responsive
Sections in an overlaydivider + SectionLabel, no nested Cards
Content meeting a framed edgecancel the container's inset, never add a second
Auth screenAuthCard shell
Provenance footercompact Card + "Last changed {Timestamp} by {ActorName}"

§C Use cases through Speedway#

  • job.tsx composition: banners (Card-tinted, reason first) → runs table card → workflow chain card → parts card. Setup cards are deliberately absent (owner 2026-07-24: "module setup lives ONLY in the create flow's config page").
  • Review/Log/Taxonomy/Attributes: one Card wrapping FilterBar + DataTable each, the dominant list-page shape.
  • Guidance card in Import processing: two icon+sentence rows pointing to Jobs and Error management.
  • Duplicated verbatim blocks flagged for componentization: the "feature not enabled" card and the soft-limit warning copy both exist twice.
@versable-git/ui · reference, canon, and method, read in place