Skip to content
558 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge packages: roles, Actions access, source label, soft delete, API1import { Box, Lock, RotateCcw, Trash2, User, UserPlus, Users } from "lucide-react";
2import { useEffect, useState } from "react";
3import { Form, Link, useFetcher } from "react-router";
4
5import {
6 type ActionsAccess,
7 PACKAGE_ROLES,
8 type PackageAccess,
9 type PackageRole,
10 type PackageSettings,
11 type PackageVersion,
12 PACKAGE_RESTORE_DAYS,
13} from "@g1t/contracts";
14
15import { ConfirmDialog } from "./repo-lifecycle";
16import { SettingsSection as Section } from "./settings-section";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.17import { ErrorText, SubmitButton, TimeAgo } from "./ui";
Merge packages: roles, Actions access, source label, soft delete, API18import { Badge } from "./ui/badge";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.19import { Button } from "./ui/button";
20import { Card } from "./ui/card";
Merge packages: roles, Actions access, source label, soft delete, API21import { Hint } from "./ui/hint";
22import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
23import { Skeleton, SkeletonLine, SkeletonRows } from "./ui/skeleton";
24import { SwitchCard } from "./ui/switch";
25import { formatBytes, shortDigest } from "../lib/packages";
26
27/** A one-line text field the height of the small selects beside it. */
28const FIELD =
29 "h-8 w-full min-w-0 rounded-md border border-line bg-bg px-2.5 font-mono text-sm placeholder:font-sans placeholder:text-faint focus:border-line-strong focus:outline-none";
30
31/** What an action on the Settings tab answers. */
32export type SettingsOutcome = { error: string | null; message: string | null };
33
34export const PACKAGE_ROLE_LABEL: Record<PackageRole, string> = { read: "Read", write: "Write", admin: "Admin" };
35const PACKAGE_ROLE_SUMMARY: Record<PackageRole, string> = {
36 read: "Pull and install it.",
37 write: "Also publish new versions and tags.",
38 admin: "Also delete and restore it, and change these settings.",
39};
40const ACTIONS_ROLE_SUMMARY: Record<"read" | "write", string> = {
41 read: "Its workflows pull or install the package.",
42 write: "Its workflows publish it too.",
43};
44
45/** A role picker: read, write and admin, or read and write for a repository. */
46function RoleSelect({
47 value,
48 roles,
49 label,
50 name,
51 disabled,
52 onValueChange,
53 className = "w-28",
54}: {
55 value: string;
56 roles: readonly ("read" | "write" | "admin")[];
57 label: string;
58 name?: string;
59 disabled?: boolean;
60 onValueChange?: (role: string) => void;
61 className?: string;
62}) {
63 return (
64 <Select name={name} value={value} onValueChange={onValueChange} disabled={disabled}>
65 <SelectTrigger size="sm" aria-label={label} className={className}>
66 <SelectValue />
67 </SelectTrigger>
68 <SelectContent align="end" className="w-64">
69 {roles.map((role) => (
70 <SelectItem
71 key={role}
72 value={role}
73 description={role === "admin" ? PACKAGE_ROLE_SUMMARY.admin : roles.length === 2 ? ACTIONS_ROLE_SUMMARY[role] : PACKAGE_ROLE_SUMMARY[role]}
74 >
75 {PACKAGE_ROLE_LABEL[role]}
76 </SelectItem>
77 ))}
78 </SelectContent>
79 </Select>
80 );
81}
82
83/** The Settings tab of a package: who may use it, from where, and what happens to it. Admins only. */
84export function PackageSettingsTab({ settings, outcome }: { settings: PackageSettings; outcome: SettingsOutcome | undefined }) {
85 const { package: pkg, access, actions_access: actions, deleted_versions: deleted, permissions } = settings;
86 const repo = pkg.repo ? `${pkg.repo.namespace}/${pkg.repo.name}` : null;
87 const npm = pkg.ecosystem !== "container";
88 return (
89 <div className="max-w-4xl space-y-8">
90 {outcome?.error && <ErrorText>{outcome.error}</ErrorText>}
91 {outcome?.message && (
92 <p className="text-sm text-success" role="status">
93 {outcome.message}
94 </p>
95 )}
96
97 <Section
98 title="Manage access"
99 about={
100 repo
101 ? `People and teams with a role on the package itself. The workspace's owners administer every package.`
102 : `People and teams with a role on the package itself, beyond what members of ${pkg.workspace} get from its base permission. Owners administer every package.`
103 }
104 >
105 {repo && <InheritToggle on={pkg.inherit_access !== false} repo={repo} />}
106 {access.length === 0 ? (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.107 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted">
108 <p>
109 No one has a role on the package itself.
110 </p>
111 </Card>
Merge packages: roles, Actions access, source label, soft delete, API112 ) : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.113 <Card asChild tone="plain" divided>
114 <ul>
115 {access.map((entry) => (
116 <AccessRow key={`${entry.kind}:${entry.id}`} entry={entry} workspace={pkg.workspace} />
117 ))}
118 </ul>
119 </Card>
Merge packages: roles, Actions access, source label, soft delete, API120 )}
121 <AddAccessForm />
122 </Section>
123
124 <Section
125 title="Manage Actions access"
126 about="Which repositories' workflows may use the package with their job token, G1T_TOKEN. A workflow in any other repository is refused."
127 >
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.128 <Card asChild tone="plain" divided>
129 <ul>
130 {actions.length === 0 && (
131 <li className="px-4 py-6 text-center text-sm text-muted">No repository's workflows may use it yet.</li>
132 )}
133 {actions.map((entry) => (
134 <ActionsRow key={entry.repo_id} entry={entry} />
135 ))}
136 </ul>
137 </Card>
Merge packages: roles, Actions access, source label, soft delete, API138 <AddActionsForm workspace={pkg.workspace} />
139 </Section>
140
141 <Section
142 title="Visibility"
143 about="Who may pull it. Public packages need no sign-in to pull; pushing always needs the Write role."
144 >
145 {repo ? (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.146 <Card asChild className="p-4 text-sm text-muted">
147 <p>
148 <span className="inline-flex items-center gap-1.5 font-medium text-fg">
149 {pkg.visibility === "private" && <Lock size={13} />}
150 {pkg.visibility === "private" ? "Private" : "Public"}
151 </span>
152 , as{" "}
153 <Link to={`/${repo}`} className="text-fg-soft hover:text-fg">
154 {repo}
155 </Link>{" "}
156 is. A linked package has its repository's visibility: change the repository's, or unlink the package.
157 </p>
158 </Card>
Merge packages: roles, Actions access, source label, soft delete, API159 ) : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.160 <Card asChild className="flex flex-wrap items-center gap-3 p-4">
161 <Form method="post">
162 <input type="hidden" name="intent" value="visibility" />
163 <Select name="visibility" defaultValue={pkg.visibility}>
164 <SelectTrigger size="sm" aria-label="Visibility" className="w-full sm:w-64">
165 <SelectValue />
166 </SelectTrigger>
167 <SelectContent align="start">
168 <SelectItem value="private" description={`Members of ${pkg.workspace}, by its base permission, and those given a role here.`}>
169 Private
170 </SelectItem>
171 <SelectItem value="public" description="Anyone can pull it, signed in or not.">
172 Public
173 </SelectItem>
174 </SelectContent>
175 </Select>
176 <SubmitButton variant="outline" match={{ intent: "visibility" }} pending="Saving…">
177 Save
178 </SubmitButton>
179 </Form>
180 </Card>
Merge packages: roles, Actions access, source label, soft delete, API181 )}
182 </Section>
183
184 <Section
185 title="Repository"
186 about="A linked package shows on its repository, takes its visibility and, while inheriting access, its roles, and its repository's workflows may publish it."
187 >
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.188 <Card className="space-y-3 p-4">
Merge packages: roles, Actions access, source label, soft delete, API189 {repo ? (
190 <div className="flex flex-wrap items-center justify-between gap-3">
191 <p className="flex min-w-0 items-center gap-2 text-sm">
192 <Box size={14} className="shrink-0 text-faint" />
193 <span className="text-muted">Linked to</span>
194 <Link to={`/${repo}`} className="truncate font-mono text-fg-soft hover:text-fg">
195 {repo}
196 </Link>
197 </p>
198 <Form method="post">
199 <input type="hidden" name="intent" value="unlink" />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.200 <SubmitButton variant="outline" match={{ intent: "unlink" }} pending="Unlinking…">
Merge packages: roles, Actions access, source label, soft delete, API201 Unlink
202 </SubmitButton>
203 </Form>
204 </div>
205 ) : (
206 <p className="text-sm text-muted">Not linked: it is the workspace's.</p>
207 )}
208 <Form method="post" className="flex flex-wrap items-center gap-2">
209 <input type="hidden" name="intent" value="link" />
210 <label className="sr-only" htmlFor="link-repo">
211 Repository to link
212 </label>
213 <input id="link-repo" name="repo" required placeholder="repository" className={`${FIELD} sm:w-56`} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.214 <SubmitButton variant="outline" match={{ intent: "link" }} pending="Linking…">
Merge packages: roles, Actions access, source label, soft delete, API215 {repo ? "Link to another" : "Link"}
216 </SubmitButton>
217 </Form>
218 <p className="text-xs text-faint">Linking needs the Admin role on the repository too.</p>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.219 </Card>
Merge packages: roles, Actions access, source label, soft delete, API220 </Section>
221
222 {permissions.delete && (
223 <Section
224 title="Deleted versions"
225 about={`Versions deleted in the last ${PACKAGE_RESTORE_DAYS} days, which can still be restored. Their ${npm ? "versions" : "digests"} cannot be published again until they are purged.`}
226 >
227 {deleted.length === 0 ? (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.228 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted">
229 <p>No deleted versions.</p>
230 </Card>
Merge packages: roles, Actions access, source label, soft delete, API231 ) : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.232 <Card asChild tone="plain" divided>
233 <ul>
234 {deleted.map((version) => (
235 <DeletedVersionRow key={version.id} version={version} npm={npm} />
236 ))}
237 </ul>
238 </Card>
Merge packages: roles, Actions access, source label, soft delete, API239 )}
240 </Section>
241 )}
242
243 {permissions.delete && (
244 <Section title="Delete this package" about={`Every version goes from the registry at once. An admin can restore it for ${PACKAGE_RESTORE_DAYS} days.`}>
245 <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-danger/30 bg-surface p-4">
246 <p className="text-sm text-muted">
247 Its name stays taken until it is purged, so nobody else can publish a package called {pkg.name} meanwhile.
248 </p>
249 <ConfirmDialog
250 intent="delete-package"
251 title={`Delete ${pkg.name}?`}
252 description={`Anyone pulling it gets an error from then on. It can be restored from the workspace's deleted packages for ${PACKAGE_RESTORE_DAYS} days.`}
253 confirm={pkg.name}
254 submit="Delete package"
255 busy="Deleting…"
256 trigger={(open) => (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.257 <Button type="button" variant="destructive" onClick={open}>
Merge packages: roles, Actions access, source label, soft delete, API258 <Trash2 size={14} />
259 Delete package
260 </Button>
261 )}
262 >
263 <li>
264 {pkg.versions} {pkg.versions === 1 ? "version" : "versions"} and their tags.
265 </li>
266 <li>After {PACKAGE_RESTORE_DAYS} days it is purged, and files no other package uses are removed.</li>
267 </ConfirmDialog>
268 </div>
269 </Section>
270 )}
271 </div>
272 );
273}
274
275/** Whether a linked package takes its repository's roles: saved as soon as it is flipped. */
276function InheritToggle({ on, repo }: { on: boolean; repo: string }) {
277 const fetcher = useFetcher<SettingsOutcome>();
278 const shown = fetcher.formData ? fetcher.formData.get("inherit") === "on" : on;
279 return (
280 <>
281 <SwitchCard
282 title="Inherit access from the linked repository"
283 checked={shown}
284 disabled={fetcher.state !== "idle"}
285 onCheckedChange={(checked) => fetcher.submit({ intent: "inherit", inherit: checked ? "on" : "off" }, { method: "post" })}
286 >
287 {shown
288 ? `Everyone with a role on ${repo} has it on the package too, and the people and teams below add to it.`
289 : `Only the people and teams below, and the workspace's owners, have access. Roles on ${repo} no longer reach the package.`}
290 </SwitchCard>
291 {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>}
292 </>
293 );
294}
295
296function AccessRow({ entry, workspace }: { entry: PackageAccess; workspace: string }) {
297 const fetcher = useFetcher<SettingsOutcome>();
298 const shown = fetcher.formData?.get("intent") === "access-role" ? String(fetcher.formData.get("role")) : entry.role;
299 const removing = fetcher.formData?.get("intent") === "access-remove";
300 const slug = entry.name.includes("/") ? entry.name.slice(entry.name.indexOf("/") + 1) : entry.name;
301 const who: Record<string, string> = entry.kind === "user" ? { user: entry.name } : { team: slug };
302 return (
303 <li className={`flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-3 ${removing ? "opacity-50" : ""}`}>
304 <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-raised text-muted">
305 {entry.kind === "user" ? <User size={15} /> : <Users size={15} />}
306 </span>
307 <div className="min-w-0 grow basis-40">
308 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
309 <Link
310 to={entry.kind === "user" ? `/u/${entry.name}` : `/${workspace}/-/teams/${slug}`}
311 className="truncate font-mono text-sm hover:text-accent"
312 >
313 {entry.name}
314 </Link>
315 <Badge>{entry.kind === "user" ? "Person" : "Team"}</Badge>
316 </p>
317 <p className="mt-0.5 text-xs text-faint">
318 Added <TimeAgo at={entry.created_at} />
319 </p>
320 {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>}
321 </div>
322 <div className="flex shrink-0 items-center gap-2">
323 <RoleSelect
324 value={shown}
325 roles={PACKAGE_ROLES}
326 label={`Role of ${entry.name}`}
327 disabled={fetcher.state !== "idle"}
328 onValueChange={(role) => fetcher.submit({ intent: "access-role", ...who, role }, { method: "post" })}
329 />
330 <Hint label={`Take ${entry.name}'s role away`}>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.331 <Button
Merge packages: roles, Actions access, source label, soft delete, API332 type="button"
333 aria-label={`Remove ${entry.name}`}
334 disabled={fetcher.state !== "idle"}
335 onClick={() => fetcher.submit({ intent: "access-remove", ...who }, { method: "post" })}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.336 variant="ghost"
337 size="inline"
338 className="p-1.5 text-faint hover:text-danger"
Merge packages: roles, Actions access, source label, soft delete, API339 >
340 <Trash2 size={14} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.341 </Button>
Merge packages: roles, Actions access, source label, soft delete, API342 </Hint>
343 </div>
344 </li>
345 );
346}
347
348/** A person by username, or a team by slug, and the role to give. */
349function AddAccessForm() {
350 const [kind, setKind] = useState<"user" | "team">("user");
351 const [role, setRole] = useState<PackageRole>("read");
352 const fetcher = useFetcher<SettingsOutcome>();
353 const [key, setKey] = useState(0);
354 // Cleared once someone was added, ready for the next.
355 useEffect(() => {
356 if (fetcher.data && !fetcher.data.error) setKey((k) => k + 1);
357 }, [fetcher.data]);
358 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.359 <Card asChild className="space-y-3 p-4">
360 <fetcher.Form method="post">
361 <input type="hidden" name="intent" value="access-add" />
362 <input type="hidden" name="kind" value={kind} />
363 <input type="hidden" name="role" value={role} />
364 <div className="grid gap-3 sm:grid-cols-[8rem_minmax(0,1fr)_7rem_auto] sm:items-center">
365 <Select value={kind} onValueChange={(value) => setKind(value as "user" | "team")}>
366 <SelectTrigger size="sm" aria-label="Person or team">
367 <SelectValue />
368 </SelectTrigger>
369 <SelectContent align="start">
370 <SelectItem value="user">Person</SelectItem>
371 <SelectItem value="team">Team</SelectItem>
372 </SelectContent>
373 </Select>
374 <input
375 key={key}
376 name="who"
377 required
378 aria-label={kind === "user" ? "Username" : "Team"}
379 placeholder={kind === "user" ? "username" : "team slug"}
380 className={FIELD}
381 />
382 <RoleSelect value={role} roles={PACKAGE_ROLES} label="Role" onValueChange={(value) => setRole(value as PackageRole)} className="w-full" />
383 <SubmitButton variant="outline" fetcher={fetcher} match={{ intent: "access-add" }} pending="Adding…">
384 <UserPlus size={14} />
385 Add
386 </SubmitButton>
387 </div>
388 <p className="text-xs text-faint">
389 {PACKAGE_ROLE_LABEL[role]}: {PACKAGE_ROLE_SUMMARY[role].toLowerCase()}
Merge packages: roles, Actions access, source label, soft delete, API390 </p>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.391 {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>}
392 {fetcher.data?.message && (
393 <p className="text-sm text-success" role="status">
394 {fetcher.data.message}
395 </p>
396 )}
397 </fetcher.Form>
398 </Card>
Merge packages: roles, Actions access, source label, soft delete, API399 );
400}
401
402function ActionsRow({ entry }: { entry: ActionsAccess }) {
403 const fetcher = useFetcher<SettingsOutcome>();
404 const shown = fetcher.formData?.get("intent") === "actions-role" ? String(fetcher.formData.get("role")) : entry.role;
405 const removing = fetcher.formData?.get("intent") === "actions-remove";
406 return (
407 <li className={`flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-3 ${removing ? "opacity-50" : ""}`}>
408 <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-raised text-muted">
409 <Box size={15} />
410 </span>
411 <div className="min-w-0 grow basis-40">
412 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
413 <Link to={`/${entry.repo}`} className="truncate font-mono text-sm hover:text-accent">
414 {entry.repo}
415 </Link>
416 {entry.linked && <Badge tone="accent">Linked repository</Badge>}
417 </p>
418 <p className="mt-0.5 text-xs text-faint">
419 {entry.linked ? "Its workflows always publish the package it is linked to." : <>Added {entry.created_at && <TimeAgo at={entry.created_at} />}</>}
420 </p>
421 {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>}
422 </div>
423 <div className="flex shrink-0 items-center gap-2">
424 {entry.linked ? (
425 <span className="inline-flex h-8 w-28 items-center rounded-md border border-line px-2.5 text-[0.8125rem] text-muted">Write</span>
426 ) : (
427 <RoleSelect
428 value={shown}
429 roles={["read", "write"]}
430 label={`Role of ${entry.repo}'s workflows`}
431 disabled={fetcher.state !== "idle"}
432 onValueChange={(role) => fetcher.submit({ intent: "actions-role", repo: entry.repo, role }, { method: "post" })}
433 />
434 )}
435 <span className="flex w-[2.125rem] justify-end">
436 {!entry.linked && (
437 <Hint label={`Stop ${entry.repo}'s workflows using it`}>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.438 <Button
Merge packages: roles, Actions access, source label, soft delete, API439 type="button"
440 aria-label={`Remove ${entry.repo}`}
441 disabled={fetcher.state !== "idle"}
442 onClick={() => fetcher.submit({ intent: "actions-remove", repo: entry.repo }, { method: "post" })}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.443 variant="ghost"
444 size="inline"
445 className="p-1.5 text-faint hover:text-danger"
Merge packages: roles, Actions access, source label, soft delete, API446 >
447 <Trash2 size={14} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.448 </Button>
Merge packages: roles, Actions access, source label, soft delete, API449 </Hint>
450 )}
451 </span>
452 </div>
453 </li>
454 );
455}
456
457function AddActionsForm({ workspace }: { workspace: string }) {
458 const [role, setRole] = useState<"read" | "write">("read");
459 const fetcher = useFetcher<SettingsOutcome>();
460 const [key, setKey] = useState(0);
461 useEffect(() => {
462 if (fetcher.data && !fetcher.data.error) setKey((k) => k + 1);
463 }, [fetcher.data]);
464 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.465 <Card asChild className="space-y-3 p-4">
466 <fetcher.Form method="post">
467 <input type="hidden" name="intent" value="actions-add" />
468 <input type="hidden" name="role" value={role} />
469 <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_7rem_auto] sm:items-center">
470 <div className="flex h-8 min-w-0 items-center rounded-md border border-line bg-bg pl-2.5 font-mono text-sm focus-within:border-line-strong">
471 <span className="shrink-0 text-faint">{workspace}/</span>
472 <input
473 key={key}
474 name="repo"
475 required
476 aria-label="Repository"
477 placeholder="repository"
478 className="h-full min-w-0 grow bg-transparent pr-2.5 placeholder:text-faint focus:outline-none"
479 />
480 </div>
481 <RoleSelect value={role} roles={["read", "write"]} label="Role" onValueChange={(value) => setRole(value as "read" | "write")} className="w-full" />
482 <SubmitButton variant="outline" fetcher={fetcher} match={{ intent: "actions-add" }} pending="Adding…">
483 Add repository
484 </SubmitButton>
Merge packages: roles, Actions access, source label, soft delete, API485 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.486 {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>}
487 {fetcher.data?.message && (
488 <p className="text-sm text-success" role="status">
489 {fetcher.data.message}
490 </p>
491 )}
492 </fetcher.Form>
493 </Card>
Merge packages: roles, Actions access, source label, soft delete, API494 );
495}
496
497function DeletedVersionRow({ version, npm }: { version: PackageVersion; npm: boolean }) {
498 const fetcher = useFetcher<SettingsOutcome>();
499 return (
500 <li className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3">
501 <div className="min-w-0 grow space-y-1">
502 <p className="flex flex-wrap items-center gap-2">
503 {npm ? (
504 <span className="font-mono text-sm font-medium">{version.version}</span>
505 ) : (
506 <Hint label={version.digest}>
507 <code className="font-mono text-sm">{shortDigest(version.digest)}</code>
508 </Hint>
509 )}
510 {version.tags.map((tag) => (
511 <Badge key={tag} className="font-mono">
512 {tag}
513 </Badge>
514 ))}
515 </p>
516 <p className="flex flex-wrap gap-x-3 text-xs text-faint tabular-nums">
517 <span>{formatBytes(version.size)}</span>
518 {version.deleted_at && (
519 <span>
520 Deleted {version.deleted_by ? `by ${version.deleted_by} ` : ""}
521 <TimeAgo at={version.deleted_at} />
522 </span>
523 )}
524 {version.purge_at && <span>Purged {new Date(version.purge_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>}
525 </p>
526 {fetcher.data?.error && <ErrorText>{fetcher.data.error}</ErrorText>}
527 </div>
528 <fetcher.Form method="post">
529 <input type="hidden" name="intent" value="restore-version" />
530 <input type="hidden" name="version" value={version.id} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.531 <SubmitButton variant="outline" fetcher={fetcher} match={{ intent: "restore-version", version: version.id }} pending="Restoring…">
Merge packages: roles, Actions access, source label, soft delete, API532 <RotateCcw size={14} />
533 Restore
534 </SubmitButton>
535 </fetcher.Form>
536 </li>
537 );
538}
539
540/** The Settings tab's outline while it loads. */
541export function PackageSettingsSkeleton() {
542 return (
543 <div aria-hidden="true" className="max-w-4xl space-y-8">
544 {[3, 2, 1].map((rows, index) => (
545 <div key={index} className="grid gap-x-10 gap-y-4 border-t border-line pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr]">
546 <div className="space-y-2">
547 <SkeletonLine className="w-32" />
548 <Skeleton className="h-3 w-48" />
549 <Skeleton className="h-3 w-40" />
550 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.551 <Card tone="plain">
Merge packages: roles, Actions access, source label, soft delete, API552 <SkeletonRows rows={rows} rowClassName="h-14" />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.553 </Card>
Merge packages: roles, Actions access, source label, soft delete, API554 </div>
555 ))}
556 </div>
557 );
558}

This file's history is long; its oldest lines are credited to the oldest commit read.