PermissionsMatrix
Roles across, permissions down, a checkbox at each: a form sends the pairs ticked, a column box ticks a whole role.
Demo
| Permission | Owner5 of 5 | Editor | Viewer |
|---|---|---|---|
| Content | |||
| Read | |||
| WriteCreate and edit pages | |||
| Publish | |||
| Team | |||
| Invite people | |||
| Billing | |||
| See invoicesAmounts and cards | |||
Tick a box, or a role's All box.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/permissions_matrix.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/permissions_matrix.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/permissions_matrix.qwe
let permissions_matrix_demo_roles = [{ key: "owner", label: "Owner", locked: true }, { key: "editor", label: "Editor" }, { key: "viewer", label: "Viewer" }]let permissions_matrix_demo_perms = [ { key: "read", label: "Read", group: "Content" }, { key: "write", label: "Write", group: "Content", hint: "Create and edit pages" }, { key: "publish", label: "Publish", group: "Content" }, { key: "invite", label: "Invite people", group: "Team" }, { key: "billing", label: "See invoices", group: "Billing", hint: "Amounts and cards" },]component PermissionsMatrixDemoLive() { state last = null <div.demo-stack> <PermissionsMatrix roles={permissions_matrix_demo_roles} permissions={permissions_matrix_demo_perms} granted={["owner:read", "owner:write", "owner:publish", "owner:invite", "owner:billing", "editor:read", "editor:write", "editor:publish", "viewer:read"]} on_change={(r, p, on) => { last = `${r} ${on ? "gets" : "loses"} ${p}` }}/> <p.demo-note>{last == null ? "Tick a box, or a role's All box." : `Last change: ${last}.`}</p> </div>}view PermissionsMatrixDemo() = <div.demo-stack> <PermissionsMatrixDemoLive/> <form method="post" action="#permissions_matrix" id="permissions-matrix-demo-form"> <PermissionsMatrix name="grant" label="Project roles" roles={take(permissions_matrix_demo_roles, 2) | map((r) => { key: r.key, label: r.label })} permissions={take(permissions_matrix_demo_perms, 2) | map((p) => { key: p.key, label: p.label })} granted={["owner:read"]}/> </form> </div>Source
ui/permissions_matrix.qwe
// PermissionsMatrix: roles across the top, permissions down the side, a checkbox// where each meets. Each checkbox is named `name` with "role:permission" as its// value, so a plain <form> sends the ones ticked. A role's header has a checkbox// that ticks or clears its whole column.//// <PermissionsMatrix name="grant"// roles={[{ key: "owner", label: "Owner", locked: true }, { key: "editor", label: "Editor" }, { key: "viewer", label: "Viewer" }]}// permissions={[{ key: "read", label: "Read", group: "Content" }, { key: "write", label: "Write", group: "Content" },// { key: "billing", label: "See invoices", group: "Billing", hint: "Amounts and cards" }]}// granted={["owner:read", "owner:write", "owner:billing", "editor:read", "editor:write", "viewer:read"]}/>// <PermissionsMatrix roles={roles} permissions={perms} granted={given} on_change={(role, perm, on) => { ... }}/>//// roles: a list of { key, label, locked? }: a locked role (the owner) shows its// ticks but cannot be changed. permissions: a list of { key, label, hint?, group? };// permissions with the same group sit under a heading row. granted: the// "role:permission" pairs ticked to start with. name: the checkboxes' name// ("permissions"). label: the table's name ("Permissions"). on_change(role,// permission, granted): inside one of your components, called for each change,// with the keys and whether it is now ticked (once per cell when a column is// ticked or cleared). The count ticked per role shows under its name.import ui/theme.qwe use ui_clslet permissions_matrix_css = css`.ui-perms { overflow-x: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-perms table { width: 100%; border-collapse: collapse; }.ui-perms th, .ui-perms td { padding: 10px 12px; border-bottom: 1px solid var(--ui-border); text-align: center; vertical-align: middle; }.ui-perms tbody tr:last-child td { border-bottom: 0; }.ui-perms th:first-child, .ui-perms td:first-child { text-align: left; }.ui-perms thead th { background: var(--ui-subtle); font-size: 13px; font-weight: 600; }.ui-perms-role { display: grid; gap: 4px; justify-items: center; }.ui-perms-count { color: var(--ui-muted); font-size: 12px; font-weight: 400; font-variant-numeric: tabular-nums; }.ui-perms-group th { padding: 8px 12px; background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; text-align: left; }.ui-perms-label { font-weight: 500; }.ui-perms-hint { display: block; color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-perms input[type="checkbox"] { width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); vertical-align: middle; cursor: pointer; }.ui-perms input[type="checkbox"]:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-perms input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .6; }.ui-perms-locked { color: var(--ui-muted); }.ui-perms-locked svg { width: 12px; height: 12px; vertical-align: -1px; margin-left: 3px; }.ui-perms tbody tr:hover td { background: color-mix(in srgb, var(--ui-subtle) 60%, transparent); }`component PermissionsMatrix(roles: list, permissions: list, granted: list = [], name: str = "permissions", label: str = "Permissions", on_change = null, class: str? = null) { style global permissions_matrix_css state on = granted let pair = (r: str, p: str) => `${r}:${p}` let has = (r: str, p: str) => contains(on, pair(r, p)) let flip = (r: str, p: str, to: bool) => { if to && !has(r, p) { on = on | push(pair(r, p)) } if !to && has(r, p) { on = on | filter((x) => x != pair(r, p)) } on_change && on_change(r, p, to) } let permissions_matrix_ticked = (r: str) => permissions | filter((p) => has(r, p.key)) | len let groups = permissions | map((p) => p.group) | unique <div class={ui_cls(["ui-perms", class])}> <table aria-label={label}> <thead><tr> <th scope="col">Permission</th> {for r in roles => { let n = permissions_matrix_ticked(r.key) let every_one = n == len(permissions) <th scope="col" key={r.key}> <span.ui-perms-role> <span>{r.label}{r.locked == true && <span.ui-perms-locked aria-label="locked" title="Cannot be changed"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg> </span>}</span> {r.locked == true ? <span.ui-perms-count>{n} of {len(permissions)}</span> : <label.ui-perms-count> <input type="checkbox" form="ui-perms-none" aria-label={`All permissions for ${r.label}`} checked={every_one} @mount={(el) => { el.indeterminate = n > 0 && !every_one }} @change={(e) => { for p in permissions => flip(r.key, p.key, e.checked) }}/> {" "}{n} of {len(permissions)} </label>} </span> </th> }} </tr></thead> <tbody> {for g in groups => [ g != null && <tr.ui-perms-group key={`group-${g}`}><th scope="colgroup" colspan={len(roles) + 1}>{g}</th></tr>, for p in permissions | filter((x) => x.group == g) => <tr key={p.key}> <th scope="row"> <span.ui-perms-label>{p.label}</span> {p.hint != null && <span.ui-perms-hint>{p.hint}</span>} </th> {for r in roles => <td key={r.key}> <input type="checkbox" name={name} value={pair(r.key, p.key)} checked={has(r.key, p.key)} disabled={r.locked == true} aria-label={`${p.label} for ${r.label}`} @change={(e) => { flip(r.key, p.key, e.checked) }}/> </td>} </tr>, ]} </tbody> </table> </div>}Related
TokenListAPI tokens with name, prefix, created, last used, scopes and expiry, and a Revoke that asks first.6 views · 10 downloadsApiKeyFieldA secret shown masked in a read-only field, with Show and Copy buttons and a hidden input for forms.5 views · 8 downloadsDangerZoneA red-bordered block of destructive actions (DangerAction), each a form, link or callback that asks before it acts.6 views · 10 downloadsAuditLogWho did what to what and when, newest first, filtered by person and action, searched, and paged.5 views · 10 downloadsOnboardingChecklistSteps to finish setting up, with a progress bar, a tick on each done and a link to where the rest get done.7 views · 7 downloadsQuickActionsA grid of big icon buttons for the common actions: links, submit buttons in a form, or on_select.6 views · 6 downloads
Thread
Thread 0 posts
On PermissionsMatrix: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.