List Screen Affordances
1. Overview
C07 Data Table specifies the table itself — rows, headers, selection chrome, bulk shell. Filterable List Pattern specifies the substrate — column config, query adapter, filter widgets. This page specifies everything between and around the substrate and the table: the chrome that hosts a list screen.
These affordances appear above and beside every admin-portal list (E02 Event Participants is the canonical bake; T02 Number Stock, M02 Membership Members, E04 Event Results, future Audit Log and Affiliations lists will compose them similarly). Capturing them once, here, prevents per-screen drift and gives the next list-screen author a checklist.
The behavioural composition (which signal owns what, how they wire together) is in Filterable List Pattern → List Screen Composition. This page is the visual + interaction spec.
2. Layout
The vertical stack from the top of the content area down. The shell chrome above it — breadcrumb and, at instance scope, the tab strip — is specified by C01; the rules that bind every screen are in Layout contract.
[Import] [Add] [Export] [Domain verb] ← command row
┌─ Content surface ──────────────────────────────────────────────────────┐
│ ✓ New 237 Returning 60 Unpaid 1 Inactive 0 🔍 Search… Clear │ ← filter band
│ Category: MTB 80km ✕ Gender: Female ✕ │ ← chip line (only when set)
├────────────────────────────────────────────────────────────────────────┤
│ <C07 Data Table> │
├────────────────────────────────────────────────────────────────────────┤
│ 1–20 of 297 Rows 20 ▾ « ‹ 1 2 3 … 15 › » │ ← pagination bar
└────────────────────────────────────────────────────────────────────────┘
┌─ Side panel ─┐
│ row detail │
└──────────────┘
Every band is optional except the table itself. A screen with no actions has no command row and its surface starts directly beneath the chrome. A screen with no toggles, search or column filters has no filter band.
|
Revised 2026-10-16. The earlier stack had a page header carrying the title, a stats strip, a chip strip, a utilities row (count, scope toggle, page size) and a separate pager beneath the table, all placed directly on the page background. On the built portal that spread one population’s filters over three bands, showed the record count twice and split the pagination controls between the top and the bottom of the table. The title now lives in the breadcrumb, the toggles and search share one filter band, and count, page size and pager share one pagination bar, all inside one bordered surface. |
3. Command Row
The C09 page header in its command-row form: right-aligned actions, or the bulk bar that replaces them. On a list screen it carries no count — the count is in the pagination bar — and no title or eyebrow.
3.1. Action toolbar
A horizontal role="toolbar" group containing the screen’s primary verbs.
| Action | Convention |
|---|---|
Import … |
|
Add <entity> |
|
Export |
|
Domain-specific verb (Pre-assignment / Reassign / Issue / …) |
Last in the toolbar; phrasing comes from the use case spec. |
Disabled stub affordances. Per the project’s UI Design Principles, every action in the eventual UI should be present from v1 — disabled with a title="Coming soon" tooltip on a wrapper <span> (disabled buttons don’t dispatch hover events on most browsers).
3.2. Bulk mode leaves the command row alone
When the screen’s selection threshold is met (E02 uses
≥2, T02 ≥1), the bulk verbs appear on C07’s bulk bar,
bonded to the table’s top edge: {{ count }} selected, the verbs (danger verbs outlined in the danger
tint, a disabled verb carrying its reason as a tooltip), Select all N where the screen can act on
every match, and Clear. The command row keeps its action toolbar, so nothing above the table moves on
entering or leaving bulk mode.
Revised 2026-10-16. The command row used to swap its action toolbar for a second bulk toolbar carrying the same count and verbs as the bonded bar. Two sets of the same buttons on one screen, one of them away from the rows it acts on, contradicted C07’s locked placement; the command-row copy is retired.
4. Content Surface
One white surface per list holding the filter band, the table and the pagination bar.
-
1pxborder in the neutral border token,8pxradius, white background, no shadow. -
Full width of the content area inside a
16pxgutter. No maximum width — lists use the whole screen. -
The bands inside are separated by
1pxdividers, not by gaps. -
The table inside it carries no border of its own; C07’s bulk-mode glow is applied to the surface. The shared table (
ap-data-table) sets it, and draws it as box-shadow rings over the 1px border so entering bulk mode shifts nothing.
5. Filter Band
The first band inside the surface. It holds every control that narrows the population, in one place.
5.1. Toggle chips
One chip per boolean dimension: the stats metrics (New, Returning, Unpaid, Unassigned
numbers, …) and the hidden-population toggles (Inactive, Cancelled, Nothing attached). They share
one treatment — the reference is the stock screen’s state strip:
| State | Treatment |
|---|---|
Off |
No border, no background, no glyph. Label and count in the muted text colour. |
On |
Accent-tinted background, |
Attention metric ( |
Same two states; the count renders in the amber attention colour in both. |
The tick carries the state for operators who cannot rely on colour; the tint is reinforcement. A filled button without a tick is not a valid "on" state.
-
Counts stay on each chip. A count is server-computed over the full scope, not the current filter set, and reads
—while loading or on failure. -
No
Totalchip. The total is the pagination bar’s count. -
A hidden-population toggle shows the number of rows it would reveal (
Inactive 0), so the operator sees the cost before flipping it. -
Chip flags are out-of-band filter dimensions; see Out-of-Band Filter Dimensions.
5.3. Chip line
A second line inside the filter band, rendered only when a column filter is set. It is the
<ems-active-filter-chips> substrate component:
-
One chip per query-set column filter, with an
✕close button;Clear allat the right. -
A filter pinned by the route is not shown. The breadcrumb already says it —
… › Rooibos MTB › Participantsis the event filter. Route-pinned columns are also hidden from the table (visible: 'hide-when-pinned'). -
Toggle-chip flags never appear here; their state is the toggle itself.
-
No label such as "Active filters:" — the chips are self-describing.
6. Pagination Bar
The last band inside the surface, and the only place for paging controls:
| Element | Position + behaviour |
|---|---|
|
Left. Server-supplied via |
Rows per page |
Beside the count. |
Pager |
Right-aligned |
|
|
7. Table Typography
Every list renders its table the same way, whatever its columns, because every list renders it
through the one shared table, ap-data-table (C07):
-
One header treatment for every column, sortable or not:
0.75rem, weight 600, muted text, sentence case. A sortable header adds the sort glyph and filter toggle beside the label; it never changes the label’s size or weight. -
Body
0.8125remin the body font; identifiers (numbers, codes) in the mono font at the same size. -
Long text columns take a maximum width and truncate with a tooltip; rows do not wrap to more than two lines.
-
Links in cells carry no underline; it returns on hover and keyboard focus. The empty-value dash is muted text, never a link, even inside a click-to-act cell. Both rules live in
_list-table.scss, not in the screens. -
A person is shown by name, never by an identity-provider id. Any recorded actor that is not a readable name, email or plain login — a UUID, an opaque subject, a prefixed id — shows as Unnamed user, with the raw value in the tooltip. An API key shows as
<label> (API key). One shared formatter (actorDisplay) decides this for every screen.
8. Side Panel
A row-detail panel that slides in from the right when the operator clicks a row’s View affordance.
-
URL-driven: opening adds
?view=<rowId>to the URL; closing removes it. Browser back-button closes the panel naturally — no separate close-state. -
Off-canvas overlay: full-height fixed-position aside, backdrop click closes,
Escapekey closes (host listener withtabindex="-1"and(keydown.escape)). -
Survives data churn: the panel content survives sort / filter / pagination changes via a snapshot signal (see Side Panel via
?view=<id>+ Snapshot Fallback). -
Sections: header (title + close button), summary, detail sections, footer with
Open detaillink to the full-page detail screen (E09 for EP). -
Stub-friendly: detail sections that depend on backend projections not yet shipped render as muted "Coming soon" placeholders, not as empty/zero states. The placeholder telegraphs "this UI will be richer soon"; an empty state telegraphs "there is nothing here", which is a different (and misleading) claim.
9. Row-Summoned Modal
A modal launched from a row-level affordance (chip click, action menu, etc.). The C06 order cluster modal is the canonical bake. Discipline:
-
Open state lives in a
*ModalId: signal<rowId | null>on the host list component. -
Snapshot fallback mirrors the side-panel pattern — snapshot the cluster/row at open time so the modal survives sort/filter/page churn that moves the row off the visible page.
-
Return-focus: capture
event.currentTargetfrom the row affordance click into a*ReturnTarget: HTMLElement | nullfield; on close, schedule a microtask totarget.focus(). Without this the operator’s keyboard context is lost. -
Escape + backdrop close with the same
tabindex="-1"+(keydown.escape)recipe as the side panel.
A row-summoned modal and the side panel can coexist on the same screen (EP has both). They occupy distinct URL channels (?view= for the panel; modal state is component-local because modals are short-lived and shouldn’t litter history) and don’t compete for screen real estate.
10. No Hover Panels
Retired 2026-10-16. A list cell summons a modal by click and carries no hover panel. The rich
tooltip that the C06 order chip once opened on mouseover
duplicated the cluster modal, could not be reached by keyboard or touch, and hid state behind a
gesture. The chip now states what it summarises (one order’s number and status, a shared status and
a count, or Various) and a click opens the modal. A plain title tooltip on a truncated cell is
still fine.
Status in a list cell is shown with the shared status pill (ap-status-pill), not a per-screen
badge.
11. Tabs
There is one tab idiom. In-page tabs use the shared tab component (ap-tabs), which carries the
instance tab strip’s treatment from Layout contract: muted labels, the
selected tab bold with a 2px accent underline, an optional count beside a label, and an exception tab
that can be left out while its count is zero. The selected tab lives in the URL (?tab= by default)
and is written with replaceUrl, so a tab is shareable without filling the history. The strip is an
ARIA tablist with arrow-key, Home and End navigation. Bootstrap nav-tabs are not used.
12. Empty + Loading States
Three distinct empty states, three distinct surfaces — don’t conflate them:
| State | Surface | Copy |
|---|---|---|
Loading |
Inside the table body — single full-width row spanning every column. |
|
Empty (no data yet) |
Inside the table body — full-width row. |
|
Empty (filter narrowed to nothing) |
Inside the table body — full-width row. |
|
The shared table renders all three as one full-width row, with the screen’s title and hint and a
projected action, and derives the colspan itself. A load failure is a fourth state, never an empty
one: a role="alert" row naming the failure, with Try again.
13. Status
This document specifies chrome conventions baked in by the EP list (E02). Future list screens compose the same chrome with screen-specific verbs and metrics. Variations that should propagate (a new affordance position, a new chrome band) are designed in a journal entry, then upstreamed here.
14. References
-
Filterable List Pattern — substrate (column config, query adapter, widgets) + composition (effects discipline, navigate, side-panel signal recipe).
-
C07 Data Table — the table itself (rows, headers, bulk shell). C07 explicitly disclaims chrome above the table; this page is the disclaimed half.
-
E02 Event Participants — first canonical consumer of these affordances.
-
C06 Order Detail — chip cluster modal; the canonical bake for Row-Summoned Modal.
-
UI Design Principles — disabled-with-tooltip rule, project-wide UI conventions.