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 — all sitting inside the Collection component, which handles the stack and the scrolling. Nothing custom, nothing fussy.
BetaCollection is Beta — the composition is settled (PageHeader · toolbar · data view); the prop surface may still move.
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. The frame holds a fixed height: the header stays put while the data scrolls.
| Tom Becker | Active | Dana Whitfield | Sam Cho | |||
| Erin Lattimore | Review | Marcus Reyes | Marcus Reyes | |||
| Hassan Ali | Active | Priya Anand | Dana Whitfield | |||
| Grace Nimoy | Inactive | Dana Whitfield | Lena Ortiz | |||
| Victor Pang | Active | Sam Cho | Sam Cho | |||
| Aisha Bello | Active | Lena Ortiz | Priya Anand | |||
| Owen Frye | Review | Marcus Reyes | Dana Whitfield | |||
| Maya Iverson | Active | Priya Anand | Sam Cho |
Two regions, one stack — that's the whole layout. The header stays put — the title and the primary action are always right there. The data region gets the rest of the height and does the scrolling: the page never grows with the list. The DataTable scrolls its own grid, so the column header stays pinned while the rows pass under it and the pager stays in reach. Everything in the frame is the real thing — Collection composing PageHeader and DataTable, live.
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
FilterBarof faceted, extensible filters, in the table's toolbar slot. - data
- The
DataTablein 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). The Collection component already holds both regions, so a new variant only swaps the data renderer.
API
Collection owns the geometry and the scroll contract; the regions stay the canonical components. Two props — the rest of the pattern is composition. There is no toolbar prop yet: in the table variant the toolbar lives in the DataTable's toolbar slot; a future cards / board variant brings the view-switch toolbar with it.
- header
- The pinned header region — typically a <PageHeader divider> with eyebrow, title, description, and the one primary action. Required: a collection always announces what it lists.
- children
- The data region — the view variant (the DataTable today; cards · board · … later). The scroll container: the page stays put while the data scrolls. A variant that manages its own scroll, like the DataTable's grid, shrinks to fit and scrolls internally instead — and its empty, loading, and error states ride with it.
Usage
import {
Collection, PageHeader, DataTable, FilterBar, Badge, Button,
standardMetadataFilters, inSelectionFilter, dateRangeFilter,
} from '@halo-compliance/ui'
import { Plus } from 'lucide-react'
const [columnFilters, setColumnFilters] = useState([])
<Collection
header={
<PageHeader divider
eyebrow="Compliance"
title="Officers"
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}
/>
}
/>
</Collection>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.
Related
- /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.