The Roles & permissions screens — the role list, the role detail it drills into, their rail entry, and the i18n
Build the screens the design card drew: Project settings → Access → Roles & permissions, a role list that drills into a role detail, read-only. This is the story's feature-finishing card — the point at which the permission model becomes something a person can open and check.
⚠️ RE-SCOPED 2026-08-07. This card was authored on 2026-08-05 describing a single-screen grid — "one card per role, its permissions grouped by catalog domain" — and its first criterion cited "panel 1" of the mock as the reference for that. The design landed on 2026-08-07 (commit
1273ff32) with a drill-down instead, anddesign/projects/design-notes.mdrecords why in its own words: "an earlier revision put permissions on ROWS and roles on COLUMNS. It read well at three roles and could not survive five, which is why it was replaced." Panel 1 is now the role LIST; a role's permission set is the DETAIL screen. Built to the old text, this card would have shipped the structure the design explicitly replaced. See the story's re-plan note.
The shape
- The list screen —
app/(authed)/settings/project/roles/page.tsx, a server component readingprojectAccessService.getRoleCatalogfor the active project and gating on the same browse capability the sibling settings pages use. One row per role carrying its icon tile, name,Built-inchip, one-line purpose,N of M permissions, its member count, and a chevron into the detail. The informational note above the list explains that the built-in roles cannot be changed. - The detail screen — a nested route under the same segment, rendering one role's permissions at full width under their domain headings, each row a human label with its description beside it. It carries both the inherited breadcrumb and an explicit
← All roleslink: the design notes call this deliberate — "the inherited crumb trail is orientation, the back link is the control, and a drill-down needs a control rather than a place to read where you are." - ⚠️ The nested route needs a deliberate answer to the nav-totality test, not a weakened assertion.
tests/settings/projectSettingsNav.test.tsenumerates the filesystem and asserts that everyapp/(authed)/settings/project/**/page.tsxroute has EXACTLY one entry inlib/settings/projectSettingsNav.tsand vice versa. A drill-down adds a secondpage.tsxunderroles/that must NOT get its own rail entry — it is not a destination in the rail. Decide how the registry expresses that (a nested/child route the enumeration recognises as belonging to its parent entry is the shape to reach for) and make the totality assertion still mean what it says: the point is that no reachable settings destination lacks a door, not that every file has a row. Do not relax the test to make a second file pass. - The rail entry is not optional and not separable. A
rolesentry in the Access group, next tomembers, with an icon from the lucide set the registry already imports and anaccesspredicate ofbrowse— matching every other entry in the registry today. Changing that predicate to a permission is the permission-gated UI story's job, not this card's. - i18n both catalogs — every string in
messages/en.jsonandmessages/zh.json, including the rail label, both screen headings, the role names and purposes, the built-in note, and theN of M permissions/ member-count strings with their plural forms.
Scope boundary
In: the two screens, their components, the rail entry, the nav-totality answer, the i18n, and their unit tests.
Out: the widened read they consume (MOTIR-2439 — this card renders its DTO and does not change lib/dto, lib/mappers, lib/repositories or lib/services); the Create role button and page, the Edit / Delete affordances and the provenance chip, all of which the mock draws for custom roles; deciding or wiring the eight member-facing keys (MOTIR-2291).
Acceptance criteria
- Navigating to
/settings/project/rolesas a project admin renders the role list: the three built-in roles, each with itsBuilt-inchip, its purpose, itsN of M permissionscount and its member count, matching the list panel ofdesign/projects/roles-permissions.mock.html. - Activating a role row opens that role's detail screen at its own nested route, rendering its permissions grouped by domain with human labels and descriptions; the
← All roleslink returns to the list, and the breadcrumb is present alongside it. - The rows both screens draw come from the role-gated set the widened read returns, and each role's marks come from that role's set — the components derive neither from a literal count nor from importing the catalog directly.
- The Access group of the settings rail shows a Roles & permissions entry that opens the list, and the command palette offers it as a deep link — both from the single registry entry, not from a second list. The detail route gets no rail entry of its own.
tests/settings/projectSettingsNav.test.tspasses with its route↔registry totality assertion still asserting that every reachable settings destination has exactly one door — not relaxed to admit an unmapped file.- A project member opening either screen sees the same content, with no write affordance rendered; a user with no browse access to the project gets the shipped no-access state on either route rather than an empty page or a crash.
- No permission label or description renders a raw key — a missing i18n string fails the render in test rather than showing
permissions.work_item:edit. - Every string is present in both
messages/en.jsonandmessages/zh.json; the catalog-parity test passes. - Both screens render with zero axe violations; the list rows and the detail are reachable and readable by keyboard, and the row's activation is a real control rather than a click handler on a div.
- Colour is
--el-*only and shape is the element-semantic tokens, so the display-style swap reshapes both screens. - Per-file coverage on the new pages and components meets the ≥90% floor.
Context refs
design/projects/roles-permissions.mock.htmland its Roles & permissions section indesign/projects/design-notes.md— the reference these screens are built to; read the SOURCE and every panel, and note which panels belong to custom roles rather than to this card.lib/settings/projectSettingsNav.ts— the registry the entry is added to;visibleSettingsNavand the group ordering.tests/settings/projectSettingsNav.test.ts— the route↔registry totality guard the nested route must satisfy honestly.app/(authed)/settings/project/members/page.tsxand its_components/ProjectMembersSettings.tsx— the sibling Access-group page, for the page shell and gating shape.- The widened read —
getRoleCatalog, the data source these screens consume. - The design card — the asset this builds to.