Patterns

Collection

A collection is the list view — the page you'll live in most, and a view family (table · cards · board · …) built on two steady regions: a header + toolbar, and a scrolling data area. The one below is the real thing, in its table variant, snapped together from stock pieces — a PageHeader, a FilterBar in the toolbar slot, and the DataTable. Nothing custom, nothing fussy.

A list view

PageHeader up top (the title plus the primary action); the DataTable below with search, faceted filters, sortable and resizable columns, row selection, and pagination. Everything is wired to toasts — click a row, try the actions, run the whole loop. It all works.

Tom BeckerActiveDana WhitfieldSam Cho
Erin LattimoreReviewMarcus ReyesMarcus Reyes
Hassan AliActivePriya AnandDana Whitfield
Grace NimoyInactiveDana WhitfieldLena Ortiz
Victor PangActiveSam ChoSam Cho
Aisha BelloActiveLena OrtizPriya Anand
Owen FryeReviewMarcus ReyesDana Whitfield
Maya IversonActivePriya AnandSam Cho

Anatomy

Four parts, stacked. The header tells you where you are, the toolbar narrows things down, the data is the work itself, and the states catch the edge cases. Every one is a piece you already have on the shelf.

header
A PageHeader — the title and the primary action (New). The row count lives in the table footer, so the header doesn't repeat it.
toolbar
Search (built into the DataTable) plus a FilterBar of faceted, extensible filters, in the table's toolbar slot.
data
The DataTable in its table variant — sorting, selection + a bulk bar, column resize, pagination, row-click to the record.
states
Loading skeleton, an empty state when filters match nothing, and an error boundary — the DataTable carries them.

View family

Table is the built variant and the default for compliance data. Cards, board, gallery, list and tree are future variants over the same two regions — the header + toolbar stay put; only the data region's renderer changes, picked by a view-switch in the toolbar (ADR-0028).

Usage

A list view, composedtsx
import {
  PageHeader, DataTable, FilterBar, Badge, Button,
  standardMetadataFilters, inSelectionFilter, dateRangeFilter,
} from '@halo-compliance/ui'
import { Plus } from 'lucide-react'

const [columnFilters, setColumnFilters] = useState([])

<PageHeader
  eyebrow="Compliance"
  title="Officers"
  count={officers.length}
  actions={<Button variant="primary" icon={<Plus />}>New officer</Button>}
/>

<DataTable
  columns={columns}        // status cell → <Badge tone={…} dot>
  data={officers}
  getRowId={(o) => o.id}
  enableSelection
  enableColumnResize
  columnFilters={columnFilters}
  onColumnFiltersChange={setColumnFilters}
  onRowClick={(o) => navigate({ to: '/officers/$id', params: { id: o.id } })}
  toolbar={
    <FilterBar
      filters={standardMetadataFilters({ statusOptions, users })}
      columnFilters={columnFilters}
      onColumnFiltersChange={setColumnFilters}
    />
  }
/>

When to use

  • A set of records to scan, filter, and act on. Officers, reports, rulebooks — anything you browse before opening one.
  • One primary action in the header. Create (New). Per-row and bulk actions live in the row and the selection bar, not the header.
  • Filters in the toolbar, not the header. Search + faceted filters narrow the data; the header only orients and creates.
  • /page-header — the orientation band on top.
  • /data-table — the grid, the FilterBar, and the page jumper in detail.
  • /badge — the status column's badge: a solid color fill in this brand (the engraved plaque belongs to the proposed system).
  • /record — where a row-click lands: the detail view.