Skip to content
Brok UI

Loading…

No results

Permission Matrix

Open source

Role and permission assignment with server-owned locked and inherited states.

Preview

resources/views/blocks/permission-matrix-roles.blade.php Blade
@props([
    'action' => '#',
    'method' => 'PATCH',
    'title' => 'Role permissions',
    'description' => 'Choose the permissions that each role receives.',
    'roles' => null,
    'permissions' => null,
    'values' => null,
    'locked' => null,
    'inherited' => null,
    'statusMessage' => null,
    'submitLabel' => 'Save permissions',
])

@php
    $roles ??= [
        ['key' => 'viewer', 'name' => 'Viewer', 'description' => 'Read-only access'],
        ['key' => 'editor', 'name' => 'Editor', 'description' => 'Creates and updates content'],
        ['key' => 'admin', 'name' => 'Admin', 'description' => 'Manages the workspace'],
    ];
    $permissions ??= [
        ['group' => 'Projects', 'key' => 'projects.view', 'name' => 'View projects', 'description' => 'Read project details and activity.'],
        ['group' => 'Projects', 'key' => 'projects.update', 'name' => 'Update projects', 'description' => 'Change project content and status.'],
        ['group' => 'People', 'key' => 'members.invite', 'name' => 'Invite members', 'description' => 'Send workspace invitations.'],
        ['group' => 'Administration', 'key' => 'billing.manage', 'name' => 'Manage billing', 'description' => 'Change plans and payment details.'],
    ];
    $values ??= [
        'viewer' => ['projects.view'],
        'editor' => ['projects.view', 'projects.update'],
        'admin' => ['projects.view', 'projects.update', 'members.invite', 'billing.manage'],
    ];
    $locked ??= ['admin' => ['billing.manage']];
    $inherited ??= ['editor' => ['projects.view'], 'admin' => ['projects.view']];
@endphp

<x-ui.form.guard
    as="section"
    data-slot="permission-matrix"
    data-surface="admin"
    {{ $attributes->merge(['class' => 'bg-background text-foreground']) }}
>
    <x-ui.form
        :action="$action"
        :method="$method"
        :summary="__('Please correct the permission errors below.')"
        data-persisted-form
        x-data="{
            toggleRole(role, checked) {
                this.$root.querySelectorAll('[data-permission-control]').forEach((control) => {
                    if (control.dataset.role === role && ! control.disabled) control.checked = checked
                })
                this.$dispatch('brok:form-dirty')
            }
        }"
    >
        <x-ui.card>
            <x-ui.card.header>
                <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
                    <div>
                        <x-ui.card.title>{{ __($title) }}</x-ui.card.title>
                        <x-ui.card.description class="mt-2">{{ __($description) }}</x-ui.card.description>
                    </div>
                    <x-ui.badge variant="outline">{{ __('Server-authoritative') }}</x-ui.badge>
                </div>

                @if ($statusMessage)
                    <x-ui.alert variant="success" role="status" class="mt-4">{{ __($statusMessage) }}</x-ui.alert>
                @endif
            </x-ui.card.header>

            <x-ui.card.content class="!p-0">
                <div class="overflow-x-auto">
                    <table class="w-full min-w-[48rem] border-collapse text-sm">
                        <caption class="sr-only">{{ __('Permission assignments by role') }}</caption>
                        <thead>
                            <tr class="border-y border-border bg-muted/50">
                                <th scope="col" class="px-6 py-4 text-start font-medium text-muted-foreground">{{ __('Permission') }}</th>
                                @foreach ($roles as $role)
                                    @php
                                        $roleKey = (string) $role['key'];
                                    @endphp
                                    <th scope="col" class="min-w-40 px-4 py-4 text-start align-top">
                                        <div class="flex items-start gap-2">
                                            <x-ui.checkbox
                                                :aria-label="__('Select all available permissions for :role', ['role' => __($role['name'])])"
                                                :data-role="$roleKey"
                                                @change="toggleRole($event.target.dataset.role, $event.target.checked)"
                                            />
                                            <span>
                                                <span class="block font-medium text-foreground">{{ __($role['name']) }}</span>
                                                <span class="mt-0.5 block text-xs font-normal text-muted-foreground">{{ __($role['description']) }}</span>
                                            </span>
                                        </div>
                                    </th>
                                @endforeach
                            </tr>
                        </thead>
                        <tbody>
                            @foreach ($permissions as $index => $permission)
                                @if ($index === 0 || $permissions[$index - 1]['group'] !== $permission['group'])
                                    <tr>
                                        <th colspan="{{ count($roles) + 1 }}" scope="colgroup" class="border-b border-border bg-muted/30 px-6 py-2 text-start text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                                            {{ __($permission['group']) }}
                                        </th>
                                    </tr>
                                @endif
                                <tr class="border-b border-border last:border-b-0">
                                    <th scope="row" class="px-6 py-4 text-start font-normal">
                                        <span class="block font-medium text-foreground">{{ __($permission['name']) }}</span>
                                        <span class="mt-2 block text-xs text-muted-foreground">{{ __($permission['description']) }}</span>
                                    </th>
                                    @foreach ($roles as $role)
                                        @php
                                            $roleKey = (string) $role['key'];
                                            $permissionKey = (string) $permission['key'];
                                            $selected = in_array($permissionKey, old("permissions.$roleKey", $values[$roleKey] ?? []), true);
                                            $isLocked = in_array($permissionKey, $locked[$roleKey] ?? [], true);
                                            $isInherited = in_array($permissionKey, $inherited[$roleKey] ?? [], true);
                                            $isDisabled = $isLocked || $isInherited;
                                        @endphp
                                        <td class="px-4 py-4 align-middle">
                                            <div class="flex items-center gap-2">
                                                <x-ui.checkbox
                                                    name="permissions[{{ $roleKey }}][]"
                                                    :value="$permissionKey"
                                                    :checked="$selected"
                                                    :disabled="$isDisabled"
                                                    data-permission-control
                                                    :data-role="$roleKey"
                                                    :aria-label="__(':permission for :role', ['permission' => __($permission['name']), 'role' => __($role['name'])])"
                                                />
                                                @if ($isLocked)
                                                    <x-ui.badge variant="outline" size="sm">{{ __('Locked') }}</x-ui.badge>
                                                @elseif ($isInherited)
                                                    <x-ui.badge variant="secondary" size="sm">{{ __('Inherited') }}</x-ui.badge>
                                                @endif
                                            </div>
                                        </td>
                                    @endforeach
                                </tr>
                            @endforeach
                        </tbody>
                    </table>
                </div>
            </x-ui.card.content>

            <x-ui.card.footer class="flex flex-col gap-4 border-t border-border sm:flex-row sm:items-center sm:justify-between">
                <p class="text-xs text-muted-foreground">{{ __('Locked and inherited values must be preserved by the server.') }}</p>
                <x-ui.button type="submit" x-bind:disabled="submitting" x-bind:aria-busy="submitting">
                    <span x-show="! submitting">{{ __($submitLabel) }}</span>
                    <span x-cloak x-show="submitting">{{ __('Saving…') }}</span>
                </x-ui.button>
            </x-ui.card.footer>
        </x-ui.card>
    </x-ui.form>
</x-ui.form.guard>

Installation

terminal
php artisan ui:add blocks/permission-matrix-roles

Installs these dependencies automatically:

Registry contract

Install confidence

php artisan ui:add blocks/permission-matrix-roles writes only the generated targets below. The CLI validates each file hash before writing and prompts before replacing local changes unless --force is used.

Version
1.1.4
License
open
Stability
stable
Contract
v2
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/permission-matrix-roles.blade.php

Registry dependencies

form card checkbox badge button alert

Package dependencies

composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.

permission-matrix-roles.md
# Brok UI block: Permission Matrix (`permission-matrix-roles`)

A server-ready role and permission matrix with grouped permissions, bulk role selection, locked and inherited states, validation, progress, and unsaved-change protection.

Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.

## Install

```bash
php artisan ui:add blocks/permission-matrix-roles
```

## Render it

```blade
<x-blocks.permission-matrix-roles />
```

## Props

- `action` (string, default `#`) — Permission update endpoint.
- `method` (string, default `PATCH`) — Update request method.
- `title` (string, default `Role permissions`) — Matrix heading.
- `description` (string, default `Choose the permissions that each role receives.`) — Matrix explanation.
- `roles` (array|null, default `null`) — Stable role keys, names, and descriptions.
- `permissions` (array|null, default `null`) — Grouped stable permission keys and labels.
- `values` (array|null, default `null`) — Selected permission keys by role key.
- `locked` (array|null, default `null`) — Server-owned permission keys by role.
- `inherited` (array|null, default `null`) — Inherited permission keys by role.
- `status-message` (string|null, default `null`) — Success feedback after redirect.
- `submit-label` (string, default `Save permissions`) — Save action label.

## Use when

- Use to assign a known permission allowlist to a small set of application roles.

## Avoid when

- Use a policy editor when access depends on conditions, resource attributes, or a large number of roles.

## Rules

- Render the installed block with `<x-blocks.permission-matrix-roles />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.

## Links

- Docs: https://brokui.dev/blocks/permission-matrix-roles
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/permission-matrix-roles.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Guidance

Use when

  • Use to assign a known permission allowlist to a small set of application roles.

Avoid when

  • Use a policy editor when access depends on conditions, resource attributes, or a large number of roles.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.
Anatomy
role headers permission groups permission controls locked and inherited states save action
Theming hooks
Uses the admin surface and semantic design tokens.

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.permission-matrix-roles />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

This block composes installed <brok:*> primitives and semantic design tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by editing CSS variables; flip the preview to RTL to confirm it mirrors.

Source

The exact, editable file ui:add writes into your app. The preview above renders this same source — there are no preview-only blocks.

resources/views/blocks/permission-matrix-roles.blade.php Blade
@props([
    'action' => '#',
    'method' => 'PATCH',
    'title' => 'Role permissions',
    'description' => 'Choose the permissions that each role receives.',
    'roles' => null,
    'permissions' => null,
    'values' => null,
    'locked' => null,
    'inherited' => null,
    'statusMessage' => null,
    'submitLabel' => 'Save permissions',
])

@php
    $roles ??= [
        ['key' => 'viewer', 'name' => 'Viewer', 'description' => 'Read-only access'],
        ['key' => 'editor', 'name' => 'Editor', 'description' => 'Creates and updates content'],
        ['key' => 'admin', 'name' => 'Admin', 'description' => 'Manages the workspace'],
    ];
    $permissions ??= [
        ['group' => 'Projects', 'key' => 'projects.view', 'name' => 'View projects', 'description' => 'Read project details and activity.'],
        ['group' => 'Projects', 'key' => 'projects.update', 'name' => 'Update projects', 'description' => 'Change project content and status.'],
        ['group' => 'People', 'key' => 'members.invite', 'name' => 'Invite members', 'description' => 'Send workspace invitations.'],
        ['group' => 'Administration', 'key' => 'billing.manage', 'name' => 'Manage billing', 'description' => 'Change plans and payment details.'],
    ];
    $values ??= [
        'viewer' => ['projects.view'],
        'editor' => ['projects.view', 'projects.update'],
        'admin' => ['projects.view', 'projects.update', 'members.invite', 'billing.manage'],
    ];
    $locked ??= ['admin' => ['billing.manage']];
    $inherited ??= ['editor' => ['projects.view'], 'admin' => ['projects.view']];
@endphp

<x-ui.form.guard
    as="section"
    data-slot="permission-matrix"
    data-surface="admin"
    {{ $attributes->merge(['class' => 'bg-background text-foreground']) }}
>
    <x-ui.form
        :action="$action"
        :method="$method"
        :summary="__('Please correct the permission errors below.')"
        data-persisted-form
        x-data="{
            toggleRole(role, checked) {
                this.$root.querySelectorAll('[data-permission-control]').forEach((control) => {
                    if (control.dataset.role === role && ! control.disabled) control.checked = checked
                })
                this.$dispatch('brok:form-dirty')
            }
        }"
    >
        <x-ui.card>
            <x-ui.card.header>
                <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
                    <div>
                        <x-ui.card.title>{{ __($title) }}</x-ui.card.title>
                        <x-ui.card.description class="mt-2">{{ __($description) }}</x-ui.card.description>
                    </div>
                    <x-ui.badge variant="outline">{{ __('Server-authoritative') }}</x-ui.badge>
                </div>

                @if ($statusMessage)
                    <x-ui.alert variant="success" role="status" class="mt-4">{{ __($statusMessage) }}</x-ui.alert>
                @endif
            </x-ui.card.header>

            <x-ui.card.content class="!p-0">
                <div class="overflow-x-auto">
                    <table class="w-full min-w-[48rem] border-collapse text-sm">
                        <caption class="sr-only">{{ __('Permission assignments by role') }}</caption>
                        <thead>
                            <tr class="border-y border-border bg-muted/50">
                                <th scope="col" class="px-6 py-4 text-start font-medium text-muted-foreground">{{ __('Permission') }}</th>
                                @foreach ($roles as $role)
                                    @php
                                        $roleKey = (string) $role['key'];
                                    @endphp
                                    <th scope="col" class="min-w-40 px-4 py-4 text-start align-top">
                                        <div class="flex items-start gap-2">
                                            <x-ui.checkbox
                                                :aria-label="__('Select all available permissions for :role', ['role' => __($role['name'])])"
                                                :data-role="$roleKey"
                                                @change="toggleRole($event.target.dataset.role, $event.target.checked)"
                                            />
                                            <span>
                                                <span class="block font-medium text-foreground">{{ __($role['name']) }}</span>
                                                <span class="mt-0.5 block text-xs font-normal text-muted-foreground">{{ __($role['description']) }}</span>
                                            </span>
                                        </div>
                                    </th>
                                @endforeach
                            </tr>
                        </thead>
                        <tbody>
                            @foreach ($permissions as $index => $permission)
                                @if ($index === 0 || $permissions[$index - 1]['group'] !== $permission['group'])
                                    <tr>
                                        <th colspan="{{ count($roles) + 1 }}" scope="colgroup" class="border-b border-border bg-muted/30 px-6 py-2 text-start text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                                            {{ __($permission['group']) }}
                                        </th>
                                    </tr>
                                @endif
                                <tr class="border-b border-border last:border-b-0">
                                    <th scope="row" class="px-6 py-4 text-start font-normal">
                                        <span class="block font-medium text-foreground">{{ __($permission['name']) }}</span>
                                        <span class="mt-2 block text-xs text-muted-foreground">{{ __($permission['description']) }}</span>
                                    </th>
                                    @foreach ($roles as $role)
                                        @php
                                            $roleKey = (string) $role['key'];
                                            $permissionKey = (string) $permission['key'];
                                            $selected = in_array($permissionKey, old("permissions.$roleKey", $values[$roleKey] ?? []), true);
                                            $isLocked = in_array($permissionKey, $locked[$roleKey] ?? [], true);
                                            $isInherited = in_array($permissionKey, $inherited[$roleKey] ?? [], true);
                                            $isDisabled = $isLocked || $isInherited;
                                        @endphp
                                        <td class="px-4 py-4 align-middle">
                                            <div class="flex items-center gap-2">
                                                <x-ui.checkbox
                                                    name="permissions[{{ $roleKey }}][]"
                                                    :value="$permissionKey"
                                                    :checked="$selected"
                                                    :disabled="$isDisabled"
                                                    data-permission-control
                                                    :data-role="$roleKey"
                                                    :aria-label="__(':permission for :role', ['permission' => __($permission['name']), 'role' => __($role['name'])])"
                                                />
                                                @if ($isLocked)
                                                    <x-ui.badge variant="outline" size="sm">{{ __('Locked') }}</x-ui.badge>
                                                @elseif ($isInherited)
                                                    <x-ui.badge variant="secondary" size="sm">{{ __('Inherited') }}</x-ui.badge>
                                                @endif
                                            </div>
                                        </td>
                                    @endforeach
                                </tr>
                            @endforeach
                        </tbody>
                    </table>
                </div>
            </x-ui.card.content>

            <x-ui.card.footer class="flex flex-col gap-4 border-t border-border sm:flex-row sm:items-center sm:justify-between">
                <p class="text-xs text-muted-foreground">{{ __('Locked and inherited values must be preserved by the server.') }}</p>
                <x-ui.button type="submit" x-bind:disabled="submitting" x-bind:aria-busy="submitting">
                    <span x-show="! submitting">{{ __($submitLabel) }}</span>
                    <span x-cloak x-show="submitting">{{ __('Saving…') }}</span>
                </x-ui.button>
            </x-ui.card.footer>
        </x-ui.card>
    </x-ui.form>
</x-ui.form.guard>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.4
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v2
Foundation
≥ 1.0.0