Edit what a role, group or plan may do: resources × actions with checkbox cells or scope cells (None / Own / Team / All, shown as increasing fill plus text), inheritance from a parent role, row, column and group bulk toggles, dependencies kept consistent ("Edit needs View"), sticky headers, and a review-before-save diff. PermissionCompare puts roles side by side.
| Resource | |||||
|---|---|---|---|---|---|
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { PermissionMatrix, PermissionCompare, diffPermissions } from "@vamos-ui/react/business";
<PermissionMatrix
resources={resources}
actions={[{ id: "view", label: "View", scoped: true }, { id: "delete", label: "Delete", danger: true }]}
value={value}
onValueChange={setValue}
inherited={managerRole}
inheritedFrom="Manager"
baseline={saved}
onSave={(v, changes) => api.roles.update(id, v)}
dependencies={[{ action: "edit", requires: "view" }]}
/>Keyboard: arrows move (roving tabindex, mirrored in RTL), Home/End go to the row edges, Ctrl+Home/End to the grid corners. Space toggles, Enter opens the scope menu, 1–4 pick a scope, Delete resets an override. The header row and row headers are part of the grid: their buttons grant or revoke a whole column or row.
Every cell has a full name ("Quotes, Delete: Team"); colour never stands alone. Sticky headers sit on an opaque surface and never blur.
Examples
Scoped cells
Own / Team / All as increasing fill with the name in the cell. Enter opens the scope menu, 1–4 pick directly, Space toggles None ↔ All. Raising Delete raises Edit and View (toast).
| Resource | ||||
|---|---|---|---|---|
Inherited from a parent role
Inherited cells are striped and locked ("From Manager"); overrides get a ring and "Reset to inherited" in the menu.
| Resource | |||
|---|---|---|---|
Striped cells are inherited from Manager. Delete or Backspace on an overridden cell resets it.
Compare roles
PermissionCompare: subjects as columns, permissions as rows; rows that differ are tinted (onlyDifferences filters them).
| Permission | Sales rep12 people | Sales manager3 people | Admin2 people |
|---|---|---|---|
| Deals · View (Differs) | Own | Team | All |
| Deals · Edit (Differs) | Own | Team | All |
| Deals · Delete (Differs) | — | Own | All |
| Quotes · View (Differs) | Own | Team | All |
| Quotes · Edit (Differs) | Own | Team | All |
| Quotes · Delete (Differs) | — | — | All |
| Contacts · View (Differs) | Team | All | All |
| Contacts · Edit (Differs) | Own | Team | All |
| Contacts · Delete (Differs) | — | — | All |
Review before save
With baseline, changed cells get a dot and a floating bar shows "3 changes · Review · Discard · Save". The review lists every change as text and flags sensitive grants.
| Resource | |||
|---|---|---|---|
Plan entitlements
Features × plans with boolean cells, relabelled Included / Not included, inside a scroll area with sticky headers.
| Feature | ||||
|---|---|---|---|---|
API
PermissionMatrix
| Prop | Type | Default |
|---|---|---|
resourcesRows. group makes collapsible sections; actions limits which cells apply (others show "Not applicable"). | { id; label; group?; description?; actions?: string[] }[] | — |
actionsColumns. scoped → None/Own/Team/All; danger highlights full grants. | { id; label; scoped?; danger? }[] | — |
value / defaultValue / onValueChangeMissing entries are not granted (or inherited). | Record<resource, Record<action, Scope | boolean>> | — |
scopesRename scope levels (default from the dictionary). | { id: Scope; label: string }[] | — |
inherited / inheritedFromParent role values shown locked until overridden. | PermissionValue / string | — |
baselineSaved value: enables change dots, the save bar and the review dialog. | PermissionValue | — |
onSaveSave button in the bar and the review dialog. | (value, changes) => Promise<void> | void | — |
subjectNamed in the review dialog. | string | "this role" |
dependenciesKept consistent both ways; changes are announced. | { action: string; requires: string }[] | — |
onDependencyAppliedShow a toast: "View enabled because Edit requires it". | (message: string) => void | — |
readOnlySystem roles: no toggles, cells stay navigable. | boolean | false |
maxHeightScroll area height for sticky headers. | string | — |
labelsBuilt-in strings (permissionMatrix namespace). | Partial<PermissionMatrixLabels> | — |
PermissionCompare
| Prop | Type | Default |
|---|---|---|
subjectsRoles or plans as columns. | { id; label; description?; value }[] | — |
resources / actions / scopesRows are resource × action. | as above | — |
onlyDifferencesHide rows where every subject agrees. | boolean | false |
highlightDifferencesTint rows that differ. | boolean | true |
PermissionDiff
| Prop | Type | Default |
|---|---|---|
changesFrom diffPermissions(a, b). | PermissionChange[] | — |
resources / actionsFor labels. | as above | — |
core
| Prop | Type | Default |
|---|---|---|
diffPermissions(a, b){ resource, action, from, to } per changed cell (none / false / missing are equal). | PermissionChange[] | — |
applyDependencies(value, deps, { changed })Raises requirements or lowers dependents depending on what changed. | { value, adjusted[] } | — |