Team
MembersFilterBar, MembersTable, PendingInvitesTable and InviteMemberDialog from packages/ui, against a 30-person roster with real search, filtering, sorting and paging, permission-shaped rows, and switchable failure.
Roster
An Admin actor: two Owners above their reach, one disabled member, mixed 2FA, their own row unmanageable. 30 rows at 25 per page, so page 2 is a real 5-row remainder.
30 members, Admin actor
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
AH Ada Hopper (you)ada.hopper@acme.coAdmin | Admin | On | Active | ||
AH Ada Hopperada.hopper@acme.coMember | Member | On | Active | ||
AH Ada Hopperada.hopper@acme.coAdmin | Admin | On | Active | ||
AL Alan Liskovalan.liskov@acme.coOwner | Owner | On | Active | ||
AL Alan Liskovalan.liskov@acme.coAdmin | Admin | On | Active | ||
AL Alan Liskovalan.liskov@acme.coMember | Member | On | Active | ||
BT Barbara Turingbarbara.turing@acme.coMember2FA off | Member | Off | Active | ||
BT Barbara Turingbarbara.turing@acme.coMember2FA off | Member | Off | Active | ||
BT Barbara Turingbarbara.turing@acme.coMember2FA off | Member | Off | Active | ||
CP Claude Perlmanclaude.perlman@acme.coMember | Member | On | Active | ||
CP Claude Perlmanclaude.perlman@acme.coAdmin | Admin | On | Active | ||
CP Claude Perlmanclaude.perlman@acme.coMember | Member | On | Active | ||
DH Donald Hamiltondonald.hamilton@acme.coAdmin | Admin | On | Active | ||
DH Donald Hamiltondonald.hamilton@acme.coMember | Member | On | Active | ||
DH Donald Hamiltondonald.hamilton@acme.coAdmin | Admin | On | Active | ||
EJ Edsger Johnsonedsger.johnson@acme.coAdmin | Admin | On | Active | Never | |
EJ Edsger Johnsonedsger.johnson@acme.coMember | Member | On | Active | ||
EJ Edsger Johnsonedsger.johnson@acme.coAdmin | Admin | On | Active | ||
GD Grace Dijkstragrace.dijkstra@acme.coOwner2FA off | Owner | Off | Active | ||
GD Grace Dijkstragrace.dijkstra@acme.coMember2FA off | Member | Off | Active | ||
GD Grace Dijkstragrace.dijkstra@acme.coMember2FA off | Member | Off | Active | ||
KL Katherine Lovelacekatherine.lovelace@acme.coMember2FA off | Member | Off | Active | ||
KL Katherine Lovelacekatherine.lovelace@acme.coMember2FA off | Member | Off | Active | ||
KL Katherine Lovelacekatherine.lovelace@acme.coMember2FA off | Member | Off | Active | ||
MS Margaret Shannonmargaret.shannon@acme.coMember · Disabled2FA off | Member | Off | Disabled |
Search matches name and email; "2FA off" is the filter an access review actually asks for. Filtering resets to page 1 and the count reads "of N matching"; sorting reorders the same set and leaves the count alone.
Empty states
Three different facts, told apart rather than collapsed into one line. Only the no-matches one has a way out of itself.
No matches
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
No members match these filters. | |||||
A filter that matched nothing — distinct from an empty workspace, and offering the way back.
No members yet
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
| No members yet. | |||||
Nothing is filtered, so there is nothing to clear — the table says so and stops.
Page past the end
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
| No members on this page. | |||||
A bookmarked ?page=4 against a roster that has since shrunk. The pager stays, so Previous is reachable.
Permission shapes
The actions column is driven entirely by each row's own assignableRoles / canRemove — a viewer without team/role:w or team/remove:w never receives a row that allows either.
Read-only viewer
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
AH Ada Hopper (you)ada.hopper@acme.coAdmin | Admin | On | Active | ||
GD Grace Dijkstragrace.dijkstra@acme.coOwner2FA off | Owner | Off | Active | ||
AL Alan Liskovalan.liskov@acme.coOwner | Owner | On | Active | ||
KL Katherine Lovelacekatherine.lovelace@acme.coMember2FA off | Member | Off | Active | ||
EJ Edsger Johnsonedsger.johnson@acme.coAdmin | Admin | On | Active | Never | |
MS Margaret Shannonmargaret.shannon@acme.coMember · Disabled2FA off | Member | Off | Disabled | ||
CP Claude Perlmanclaude.perlman@acme.coMember | Member | On | Active | ||
BT Barbara Turingbarbara.turing@acme.coMember2FA off | Member | Off | Active |
team:r only — no row has an assignable role or canRemove, so no row renders a menu.
Single member (just you)
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
AH Ada Hopper (you)ada.hopper@acme.coAdmin | Admin | On | Active |
You can't reassign or remove yourself, so the actions column is empty even though you're an Admin.
Pending invites
No pending invites
PendingInvitesTable renders null — no heading, no empty-state row.
Three pending invites
| Role | Expires | Invited by | Actions | |
|---|---|---|---|---|
sam.rivera@acme.coMember · Expired | Member | Expired | ada.hopper@acme.co | |
priya.desai@acme.coAdmin · | Admin | ada.hopper@acme.co | ||
morgan.lee@acme.coOwner · | Owner | grace.dijkstra@acme.co |
The invite to Owner is above the Admin actor's reach, so it has no Revoke action.
Invite a member
InviteMemberDialog
Try "taken@acme.co" — the fixture rejects it with "That user is already a member."
Failure state
Role change always fails
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
AH Ada Hopper (you)ada.hopper@acme.coAdmin | Admin | On | Active | ||
CP Claude Perlmanclaude.perlman@acme.coMember | Member | On | Active |
Row menu → Change role → Change role — every attempt fails with "Something went wrong — try again."
320px width
MembersTable in a 320px column
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
AH Ada Hopper (you)ada.hopper@acme.coAdmin | Admin | On | Active | ||
GD Grace Dijkstragrace.dijkstra@acme.coOwner2FA off | Owner | Off | Active | ||
AL Alan Liskovalan.liskov@acme.coOwner | Owner | On | Active | ||
KL Katherine Lovelacekatherine.lovelace@acme.coMember2FA off | Member | Off | Active | ||
EJ Edsger Johnsonedsger.johnson@acme.coAdmin | Admin | On | Active | Never |
Last active drops first, then 2FA, then role and status. Nothing is lost on the way down: role and status fold into the member's line, and a member without 2FA keeps an explicit "2FA off" there — only the safe default goes unsaid. The actions menu never scrolls off.
Filter bar and table in a 320px column
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
AH Ada Hopper (you)ada.hopper@acme.coAdmin | Admin | On | Active | ||
AH Ada Hopperada.hopper@acme.coMember | Member | On | Active | ||
AH Ada Hopperada.hopper@acme.coAdmin | Admin | On | Active | ||
AL Alan Liskovalan.liskov@acme.coOwner | Owner | On | Active | ||
AL Alan Liskovalan.liskov@acme.coAdmin | Admin | On | Active |
The bar wraps rather than scrolling: the search field takes the first line on its own and the two selects and the direction toggle fall to the second. Filter it down to nothing to see the no-matches state at this width.
No matches in a 320px column
| Member | Role | 2FA | Status | Last active | Actions |
|---|---|---|---|---|---|
No members match these filters. | |||||
The message wraps and the Clear filters button stays inside the column.