You were asked for a list, a table, a queue, an index, a "page showing all the X". This is what that request brings along.
Audience: an agent (or person) asked to build a list, table, queue, or index page.
Read the bundle first. It is the completeness contract: every row is something the request implies whether or not anyone said it. A build that skips a row without hitting its "deviate when" is incomplete, and incompleteness in this class of surface is the single most corrected thing in both apps.
Traits involved: 06 tables, 07 toolbars and filters, 04 loading and
states, 05 cards and surfaces, 09 page composition, 02 buttons, 10
overlays.
The bundle#
| # | Comes along | Default | Law | Deviate when |
|---|---|---|---|---|
| 1 | Page archetype | work surface: full width, no rail, viewport-bound | 09 §A1 | a short capped sub-list inside a reading page stays a reading page |
| 2 | Viewport bounding | opt-in and named, so the table scrolls inside its card rather than the page | 09 §A3 | never implicit; if you do not name it you did not do it |
| 3 | Composition order | fixed: title, then toolbar, then content | 09 §A4 | no |
| 4 | Table layer | DataTable with features named on | 06 §A1 | raw <table> ONLY for dense inline-editable form grids, never for a browsable list |
| 5 | Card container | the list-page card is a flex column that owns its scroll | 05 §A5 | no |
| 6 | Horizontal inset | exactly one owner per framed container, table goes full-bleed inside it | 05 §A4 | no |
| 7 | Sticky header | needs an unbroken line to the thing that actually scrolls | 06 §A9 | no; a broken chain is why a sticky header silently stops sticking |
| 8 | Row click meaning | exactly one thing per table: navigate XOR expand XOR pick | 06 §A2 | no; two selection models on one table is a named bug class |
| 9 | Tint priority | fixed order: highlight, expanded, click-picked, hover | 06 §A3 | no |
| 10 | Columns | built via col.* factories; tip on any non-obvious header | 06 §A4 | no, but see the adoption note |
| 11 | Width discipline | scroll, never overlap; shed columns when a peek opens | 06 §A5 | no |
| 12 | Selection scope | explicit, never inferred | 06 §A6 | no |
| 13 | Toolbar | one anatomy: search, counted facets, count and Refresh row | 07 §A1 | a picker list may use a plain Input instead |
| 14 | Facets | never bare labels; each carries its live count | 07 §A2 | no |
| 15 | Filter state | URL params through @versable-git/qsync (batches each tick's writes into one navigation; the react-router entry additionally owns replace + preventScrollReset, the history entry replaces by construction) | 07 §A3 | never for a modal-local list |
| 16 | Selection visibility | every selection a clear-button would clear is individually visible | 07 §A4 | no; a count-only trigger hides its selection and needs chips beside it |
| 17 | Chips placement | applied-selection chips gather AFTER all controls, never inline between them | 07 §A4 | no |
| 18 | Count line | honest and situational: "N of M" only while filtered, else the plain count | 07 §A5 | no |
| 19 | Search control | FilterBar's model-bound search for a browsable catalog, a plain small Input for a short picker list; debounced via the kit's useDebounce at its default (the primitive owns the number) | 07 §A6 | no |
| 19b | Search escalation | when local search runs over a partial window, offer a trailing link to search the full set on the server | 07 §A5 | a fully-loaded list has no partial window, so no escalation |
| 20 | Sibling mirroring | a sibling list's toolbar is the spec for this one | 07 §A8 | no; divergence between sibling lists is a defect, not a choice |
| 21 | Boot skeleton | the page's own layout, routed, not a generic loader | 04 §A10 | no |
| 22 | Skeleton fidelity | preserves the box, and renders anything known at build time as itself | 04 §A2, 04 §A3 | no |
| 23 | Empty states | three-way branch: truly empty, filtered to nothing, still loading | 04 §A7 | no; one generic empty state fails this row |
| 24 | Empty state tone | carries meaning, and may carry an action | 04 §A8, 04 §A9 | no |
| 25 | Mutation feedback | settles into a toast, owned by a component that survives the success | 02 §A6, 10 §A8 | no |
| 26 | Disabled actions | a button that cannot act is hidden or explains itself, never silently disabled | 02 §A4 | no |
| 27 | Post-create return | ?hl=<id> returns you to the list without losing what you made | 06 §A3 | SPEEDWAY ONLY today, see divergence note below |
Two cross-app divergences, verified 2026-08-13#
Rows 15 and 27 are the two places this bundle was checked against both apps rather than assumed. They came out differently, and the difference matters to anyone adopting the recipe in a third app.
Row 15 holds in both, but only half of it. 07 §A3 states one clause with
two parts, replace: true, preventScrollReset: true. They have different
adoption.
replace: true is genuinely cross-app. Walmart's are at
walmart-mvp/frontend/src/pages/ErrorManagement.tsx:524,538,1038,1050, and
speedway writes it in four files. So the back button deliberately does NOT undo
a filter in either app, and a QA heuristic that flags that as a bug is wrong.
preventScrollReset: true is not cross-app. Walmart has none at all, and
speedway carries it in one file. So the second half of the clause is an
adoption gap in both apps rather than settled practice, and a third app should
treat it as a decision to make rather than a convention to inherit.
Row 10 is law with almost no adoption. 06 §A4 states columns are built via
col.* factories, and exactly one file in either app does it: walmart's
Jobs.tsx, twice, at :536 and :548. Everywhere else both apps hand-build
column objects with inline render functions, including for the shapes the
factories exist to cover. The rule is still the rule and the factories are real,
so build to it. You are joining a convention with one prior caller
rather than following a beaten path, and that a grep suggesting wider use is
probably catching speedway's unrelated Firestore col helper.
Row 13's kit component has no product callers. The kit ships a real
FilterBar at packages/ui/src/table/filter-bar.tsx, fully implemented; its
one first-party call site is DataTable's own default toolbar
(data-table.tsx:174-176), which no product reaches, because every shipped
features block configures only sort, search and pagination and the
page-level lists pass their own toolbar. Playground demos do exercise it
(demo/review, demo/modules/[slug], the data-table gallery). Speedway's
FilterBar is its own app-local component typed against the kit's model, and
walmart's equivalent is FacetToolbar. So a third app choosing a toolbar
today is choosing between a kit component proven only in demos and two
divergent app-local precedents, and the docs should not pretend that is a
settled question.
Recorded here rather than quietly averaged because the first draft of this very
section certified the whole clause after checking only replace, two paragraphs
above where it correctly caught the same error in ?hl=. Checking one half of a
two-part rule and generalising is the failure this section exists to prevent,
and it is evidently easy to make while writing the warning against it.
Row 27 does not hold in walmart. The canon calls ?hl= "the app-wide
convention" at 06-tables.md:16, but walmart has no ?hl= handling anywhere
in frontend/src. The convention is speedway's. Until walmart adopts it or the
canon scopes the claim, treat row 27 as a speedway default rather than a law,
and expect a third app to need the decision made explicitly.
Worked example: walmart Jobs#
walmart-mvp/frontend/src/pages/Jobs.tsx, at commit d8d9ea7.
A jobs list with expandable rows. It is the better of the two examples for studying rows 4, 8, 13 and 21 together, because its expanded row forced the row-click question to be answered explicitly.
| Bundle row | How this surface does it | Where |
|---|---|---|
| 4, table layer | DataTable driven by useDataTable | Jobs.tsx:705 |
| 8, row click | expand only; the stepper and parts table live in the expanded row, and only one row opens at a time | Jobs.tsx:682, :506 |
| 13, toolbar | FacetToolbar with an extra slot for the review chip and a trailing slot for search | Jobs.tsx:910, :946 |
| 21, boot skeleton | TableSkeleton fed the REAL live columns, with a ToolbarSkeleton whose facet count matches the real toolbar | Jobs.tsx:894 |
| 23, empty states | a real three-way branch, PageError with a Retry, then TableSkeleton while null, then EmptyState when loaded and empty | Jobs.tsx:888-900 |
The skeleton line is the one worth copying. TableSkeleton<JobListEntry> takes
columns={liveColumns}, the same array the loaded table renders, so the
skeleton cannot drift from the page. That is 04 §A3 made structural instead
of remembered. (The kit's Table carries the same guarantee as a built-in
mode, skeleton at table.types.ts:96-98, shipped since before 0.0.1;
12-primitives-and-rules.md records walmart's component as a hand-copy of it,
so copy the principle, not the component.)
Do-nots#
Mined from the correction corpora of both apps. Every one of these was built, shipped, and then caught by a human looking at the screen. They are ordered by how easily they survive a code review, because the ones at the bottom are invisible in a diff and only show up in a browser.
Do not let a row's state have two sources of truth. An expanded row and a selected row tracked independently will disagree, and the disagreement shows as a row that looks open while behaving closed. One state, one owner. Row 8 exists to prevent this and it is the most-repeated table defect in the corpus.
Do not render a caught fetch failure as the empty state. "Nothing here yet" and "we could not load this" are opposite facts. Row 23 branches three ways for this reason, and a silent catch collapses two of the branches into the wrong one.
Do not batch nothing. Two setSearchParams calls in one tick lose a write:
each derives from the not-yet-navigated location, and the last navigation wins
wholesale. The probe that shipped @versable-git/qsync proved this holds for
the FUNCTIONAL updater form too on react-router 7 (observed on 7.18.2:
packages/qsync/src/probe.react-router.test.tsx), so no setter form is safe
on its own. Through the qsync adapter the rule is structural: a tick's writes
coalesce into one navigation. A hand-rolled site keeps this as law: one atomic
update per tick. It cost a real debugging session before it had a name.
Do not let a nav badge count go stale. A sidebar count that refreshes on route change but not on the mutation that changed it will contradict the list the user is looking at. Invalidate on the write, not on the navigation.
Do not size columns by habit. A column gets the width its content needs. A count column does not need the width of a name column, and the space it wastes comes out of the column that needed it.
Do not make a clickable cell look like text. If a cell navigates, it says so before the click: an underline on hover, and a tooltip naming the destination. Silence plus a cursor change is not an affordance.
Do not separate a table header from its body by weight alone. Bold text on the same background is not a header row. It needs its own fill.
Do not mismatch the type scale of controls that sit in one row. Sibling controls at different sizes read as different importance, which is a claim you probably did not mean to make. This one has independent evidence in both apps.
Do not hand-write skeleton headers. A skeleton that re-authors its column labels while the page renders from live columns will drift, and the drift is invisible until someone puts the two on screen together. Feed the skeleton the same column array. Row 21 and 22.
Do not put chips inline beside their own facet. They push every later
control sideways, so the row of triggers stops lining up as filters apply. Row
17, fixed at commit 5fa032c, and note that the fix had to reach both the
shared toolbar and the one hand-built row, not just the shared one.