Skip to content
108 linesCodeBlameRaw
1import { Check, Minus } from "lucide-react";
2import type { ComponentProps } from "react";
3
4import {
5 CAPABILITIES,
6 OWNER_ONLY,
7 REPO_ROLES,
8 REPO_ROLE_LABELS,
9 REPO_ROLE_SUMMARIES,
10 type RepoRole,
11 allows,
12} from "@g1t/contracts";
13
14import { Card } from "./ui/card";
15import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
16
17/** A repository role picker, each role with what it is for. */
18export function RoleSelect({
19 roles = REPO_ROLES,
20 label,
21 size = "default",
22 className,
23 ...props
24}: Omit<ComponentProps<typeof Select>, "children"> & {
25 roles?: readonly RepoRole[];
26 label: string;
27 size?: "sm" | "default";
28 className?: string;
29}) {
30 return (
31 <Select {...props}>
32 <SelectTrigger size={size} aria-label={label} className={className}>
33 <SelectValue />
34 </SelectTrigger>
35 <SelectContent align="end" className="w-72">
36 {roles.map((role) => (
37 <SelectItem key={role} value={role} description={REPO_ROLE_SUMMARIES[role]}>
38 {REPO_ROLE_LABELS[role]}
39 </SelectItem>
40 ))}
41 </SelectContent>
42 </Select>
43 );
44}
45
46/**
47 * What each role can do, from the permission table itself: a row for each
48 * capability, a column for each role.
49 */
50export function RolesTable() {
51 return (
52 <>
53 {/* On a phone: each capability with the least role that has it. */}
54 <Card asChild tone="plain" divided className="sm:hidden">
55 <ul>
56 {CAPABILITIES.map((row) => {
57 const owners = (OWNER_ONLY as readonly string[]).includes(row.capability);
58 return (
59 <li key={row.capability} className="flex items-start justify-between gap-3 px-4 py-2.5 text-sm">
60 <span className="min-w-0 text-fg/90">{row.about}</span>
61 <span className="shrink-0 text-xs whitespace-nowrap text-muted">
62 {owners ? "Owners" : row.role === "admin" ? "Admin" : `${REPO_ROLE_LABELS[row.role]} and up`}
63 </span>
64 </li>
65 );
66 })}
67 </ul>
68 </Card>
69 <Card tone="plain" className="hidden overflow-x-auto sm:block">
70 <table className="w-full min-w-[34rem] text-sm">
71 <thead>
72 <tr className="border-b border-line bg-surface text-left text-xs text-muted">
73 <th scope="col" className="px-4 py-2.5 font-medium">
74 Can
75 </th>
76 {REPO_ROLES.map((role) => (
77 <th key={role} scope="col" className="w-[4.5rem] px-1 py-2.5 text-center font-medium">
78 {REPO_ROLE_LABELS[role]}
79 </th>
80 ))}
81 </tr>
82 </thead>
83 <tbody className="divide-y divide-line">
84 {CAPABILITIES.map((row) => {
85 const owners = (OWNER_ONLY as readonly string[]).includes(row.capability);
86 return (
87 <tr key={row.capability}>
88 <th scope="row" className="px-4 py-2 text-left font-normal text-fg/90">
89 {row.about}
90 </th>
91 {REPO_ROLES.map((role) => (
92 <td key={role} className="px-1 py-2 text-center">
93 {allows(role, row.capability) ? (
94 <Check size={15} aria-label="Yes" className={`inline ${owners ? "text-muted" : "text-success"}`} />
95 ) : (
96 <Minus size={13} aria-label="No" className="inline text-line-strong" />
97 )}
98 </td>
99 ))}
100 </tr>
101 );
102 })}
103 </tbody>
104 </table>
105 </Card>
106 </>
107 );
108}