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.
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 1 | import { ArrowUpRight, Box, Check, ChevronDown, Download, Lock, Search } from "lucide-react"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 2 | import { Form, Link, data } from "react-router"; |
| 3 | ||
| 4 | import { ECOSYSTEMS, type Ecosystem, type PackageSummary } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import type { Route } from "./+types/packages"; | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 7 | import { PackageIcon } from "../../components/package-icon"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 8 | import { CopyLine, EmptyState, TimeAgo } from "../../components/ui"; |
| 9 | import { Badge } from "../../components/ui/badge"; | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 10 | import { Hint } from "../../components/ui/hint"; |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 11 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../components/ui/dropdown-menu"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 12 | import { cn } from "../../lib/cn"; |
| 13 | import { page } from "../../lib/meta"; | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 14 | import { |
| 15 | ECOSYSTEM_LABEL, | |
| 16 | type PackageSort, | |
| 17 | REGISTRIES, | |
| 18 | SORT_LABEL, | |
| 19 | VISIBILITY_LABEL, | |
| 20 | type VisibilityFilter, | |
| 21 | arrange, | |
| 22 | formatBytes, | |
| 23 | shortCount, | |
| 24 | } from "../../lib/packages"; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 25 | import { packages } from "../../lib/services.server"; |
| 26 | import { getViewer, roleIn, unwrap } from "../../lib/session.server"; | |
| 27 | ||
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 28 | const DOCS = "https://docs.g1t.sh"; |
| 29 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 30 | export function meta({ params, ...args }: Route.MetaArgs) { |
| 31 | return page(args, { title: `Packages · ${params.owner} · g1t` }); | |
| 32 | } | |
| 33 | ||
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 34 | function pick<T extends string>(value: string | null, options: readonly T[], fallback: T): T { |
| 35 | return value && (options as readonly string[]).includes(value) ? (value as T) : fallback; | |
| 36 | } | |
| 37 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 38 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 39 | const viewer = getViewer(context); | |
| 40 | // Members only for now; a workspace's public packages are found through | |
| 41 | // their repositories and search. | |
| 42 | if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 }); | |
| 43 | const url = new URL(request.url); | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 44 | const type = url.searchParams.get("type"); |
| 45 | const ecosystem = type && (ECOSYSTEMS as readonly string[]).includes(type) ? (type as Ecosystem) : null; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 46 | const query = url.searchParams.get("q")?.trim() || null; |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 47 | const visibility = pick<VisibilityFilter>(url.searchParams.get("visibility"), ["all", "public", "private"], "all"); |
| 48 | const sort = pick<PackageSort>(url.searchParams.get("sort"), ["updated", "downloads", "name"], "updated"); | |
| 49 | const found = unwrap(await packages.list(params.owner, viewer, { ecosystem, query })); | |
| 50 | // Whether the workspace has any at all decides between a filtered-out | |
| 51 | // list and the first-package page. | |
| 52 | const any = found.length > 0 || ecosystem != null || query != null; | |
| 53 | return { | |
| 54 | list: arrange(found, visibility, sort), | |
| 55 | any, | |
| 56 | ecosystem, | |
| 57 | query, | |
| 58 | visibility, | |
| 59 | sort, | |
| 60 | workspace: params.owner.toLowerCase(), | |
| 61 | }; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 62 | } |
| 63 | ||
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 64 | type Filters = { type: string | null; q: string | null; visibility: VisibilityFilter; sort: PackageSort }; |
| 65 | ||
| 66 | /** The page's address with one filter changed, defaults left out. */ | |
| 67 | function hrefWith(workspace: string, current: Filters, change: Partial<Filters>): string { | |
| 68 | const next = { ...current, ...change }; | |
| 69 | const params = new URLSearchParams(); | |
| 70 | if (next.type) params.set("type", next.type); | |
| 71 | if (next.q) params.set("q", next.q); | |
| 72 | if (next.visibility !== "all") params.set("visibility", next.visibility); | |
| 73 | if (next.sort !== "updated") params.set("sort", next.sort); | |
| 74 | return `/${workspace}/-/packages${params.size ? `?${params}` : ""}`; | |
| 75 | } | |
| 76 | ||
| 77 | /** The packages a workspace publishes: filtered, sorted, or, with none yet, how to start. */ | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 78 | export default function Packages({ loaderData }: Route.ComponentProps) { |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 79 | const { list, any, ecosystem, query, visibility, sort, workspace } = loaderData; |
| 80 | if (!any) return <ChooseRegistry workspace={workspace} />; | |
| 81 | const filters: Filters = { type: ecosystem, q: query, visibility, sort }; | |
| 82 | const filtered = ecosystem != null || query != null || visibility !== "all"; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 83 | return ( |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 84 | <div className="space-y-4"> |
| 85 | <div className="flex flex-wrap items-center gap-2"> | |
| 86 | <FilterMenu | |
| 87 | label="Type" | |
| 88 | value={ecosystem ? ECOSYSTEM_LABEL[ecosystem] : "All"} | |
| 89 | options={[null, ...ECOSYSTEMS].map((option) => ({ | |
| 90 | label: option ? ECOSYSTEM_LABEL[option] : "All", | |
| 91 | to: hrefWith(workspace, filters, { type: option }), | |
| 92 | current: ecosystem === option, | |
| 93 | }))} | |
| 94 | /> | |
| 95 | <Form method="get" className="relative min-w-48 grow"> | |
| 96 | <label className="sr-only" htmlFor="package-search"> | |
| 97 | Search packages | |
| 98 | </label> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 99 | <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" /> |
| 100 | <input | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 101 | id="package-search" |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 102 | name="q" |
| 103 | defaultValue={query ?? ""} | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 104 | placeholder="Search packages" |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 105 | className="h-9 w-full rounded-lg border border-line bg-surface pr-3 pl-8 text-sm placeholder:text-faint focus:border-line-strong focus:outline-none" |
| 106 | /> | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 107 | {ecosystem && <input type="hidden" name="type" value={ecosystem} />} |
| 108 | {visibility !== "all" && <input type="hidden" name="visibility" value={visibility} />} | |
| 109 | {sort !== "updated" && <input type="hidden" name="sort" value={sort} />} | |
| 110 | </Form> | |
| 111 | <FilterMenu | |
| 112 | label="Visibility" | |
| 113 | value={VISIBILITY_LABEL[visibility]} | |
| 114 | options={(["all", "public", "private"] as const).map((option) => ({ | |
| 115 | label: VISIBILITY_LABEL[option], | |
| 116 | to: hrefWith(workspace, filters, { visibility: option }), | |
| 117 | current: visibility === option, | |
| 118 | }))} | |
| 119 | /> | |
| 120 | <FilterMenu | |
| 121 | label="Sort" | |
| 122 | value={SORT_LABEL[sort]} | |
| 123 | options={(["updated", "downloads", "name"] as const).map((option) => ({ | |
| 124 | label: SORT_LABEL[option], | |
| 125 | to: hrefWith(workspace, filters, { sort: option }), | |
| 126 | current: sort === option, | |
| 127 | }))} | |
| 128 | /> | |
| 129 | </div> | |
| 130 | ||
| 131 | <section className="overflow-hidden rounded-xl border border-line bg-surface"> | |
| 132 | <header className="flex items-center justify-between gap-3 border-b border-line px-4 py-2.5 text-sm"> | |
| 133 | <span className="font-medium"> | |
| 134 | {list.length} {list.length === 1 ? "package" : "packages"} | |
| 135 | {filtered && ( | |
| 136 | <Link to={`/${workspace}/-/packages`} className="ml-3 text-xs font-normal text-muted hover:text-fg"> | |
| 137 | Clear filters | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 138 | </Link> |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 139 | )} |
| 140 | </span> | |
| 141 | <a href={`${DOCS}/guides/packages/`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg"> | |
| 142 | How packages work <ArrowUpRight size={12} /> | |
| 143 | </a> | |
| 144 | </header> | |
| 145 | {list.length === 0 ? ( | |
| 146 | <div className="p-4"> | |
| 147 | <EmptyState title="No packages match">Try another type or visibility, or clear the search.</EmptyState> | |
| 148 | </div> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 149 | ) : ( |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 150 | <ul className="divide-y divide-line"> |
| 151 | {list.map((pkg) => ( | |
| 152 | <PackageRow key={pkg.id} pkg={pkg} /> | |
| 153 | ))} | |
| 154 | </ul> | |
| 155 | )} | |
| 156 | </section> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 157 | </div> |
| 158 | ); | |
| 159 | } | |
| 160 | ||
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 161 | /** A filter as a button that opens its choices; each choice is a link, so the address says the view. */ |
| 162 | function FilterMenu({ label, value, options }: { label: string; value: string; options: { label: string; to: string; current: boolean }[] }) { | |
| 163 | return ( | |
| 164 | <DropdownMenu> | |
| 165 | <DropdownMenuTrigger className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-line bg-surface px-3 text-sm transition-colors hover:border-line-strong data-[state=open]:border-line-strong"> | |
| 166 | <span className="text-muted">{label}:</span> | |
| 167 | <span className="font-medium">{value}</span> | |
| 168 | <ChevronDown size={13} className="text-faint" /> | |
| 169 | </DropdownMenuTrigger> | |
| 170 | <DropdownMenuContent align="start" className="min-w-44"> | |
| 171 | {options.map((option) => ( | |
| 172 | <DropdownMenuItem key={option.label} asChild> | |
| 173 | <Link to={option.to} preventScrollReset> | |
| 174 | <Check size={14} className={cn(!option.current && "invisible")} /> | |
| 175 | {option.label} | |
| 176 | </Link> | |
| 177 | </DropdownMenuItem> | |
| 178 | ))} | |
| 179 | </DropdownMenuContent> | |
| 180 | </DropdownMenu> | |
| 181 | ); | |
| 182 | } | |
| 183 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 184 | function PackageRow({ pkg }: { pkg: PackageSummary }) { |
| 185 | return ( | |
| 186 | <li> | |
| 187 | <Link | |
| 188 | to={`/${pkg.workspace}/-/packages/${pkg.ecosystem}/${pkg.name}`} | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 189 | className="flex items-center gap-3.5 px-4 py-3.5 transition-colors hover:bg-raised/50" |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 190 | > |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 191 | <PackageIcon ecosystem={pkg.ecosystem} /> |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 192 | <span className="min-w-0 grow"> |
| 193 | <span className="flex flex-wrap items-center gap-2"> | |
| 194 | <span className="truncate font-medium">{pkg.name}</span> | |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 195 | {pkg.visibility === "private" && ( |
| 196 | <Badge> | |
| 197 | <Lock size={10} /> | |
| 198 | Private | |
| 199 | </Badge> | |
| 200 | )} | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 201 | </span> |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 202 | <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted"> |
| 203 | <span>{ECOSYSTEM_LABEL[pkg.ecosystem]}</span> | |
| 204 | <span className="text-faint">·</span> | |
| 205 | <span> | |
| 206 | Updated <TimeAgo at={pkg.updated_at} /> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 207 | </span> |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 208 | {pkg.repo && ( |
| 209 | <> | |
| 210 | <span className="text-faint">·</span> | |
| 211 | <span className="inline-flex items-center gap-1"> | |
| 212 | <Box size={11} /> | |
| 213 | {pkg.repo.name} | |
| 214 | </span> | |
| 215 | </> | |
| 216 | )} | |
| 217 | {pkg.description && ( | |
| 218 | <> | |
| 219 | <span className="text-faint">·</span> | |
| 220 | <span className="max-w-md truncate">{pkg.description}</span> | |
| 221 | </> | |
| 222 | )} | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 223 | </span> |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 224 | </span> |
| 225 | <span className="hidden shrink-0 items-center gap-5 text-xs text-muted tabular-nums sm:flex"> | |
| 226 | {pkg.latest && <span className="max-w-32 truncate font-mono text-fg-soft">{pkg.latest}</span>} | |
| 227 | <span className="w-16 text-right">{formatBytes(pkg.size)}</span> | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 228 | <Hint label={`${pkg.downloads.toLocaleString("en-US")} downloads`}> |
| 229 | <span className="inline-flex w-14 items-center justify-end gap-1"> | |
| 230 | <Download size={12} /> | |
| 231 | {shortCount(pkg.downloads)} | |
| 232 | <span className="sr-only"> downloads</span> | |
| 233 | </span> | |
| 234 | </Hint> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 235 | </span> |
| 236 | </Link> | |
| 237 | </li> | |
| 238 | ); | |
| 239 | } | |
| 240 | ||
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 241 | /** With no packages yet: every registry, what it is for, and the first step. */ |
| 242 | function ChooseRegistry({ workspace }: { workspace: string }) { | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 243 | return ( |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 244 | <div className="space-y-6"> |
| 245 | <div className="max-w-2xl"> | |
| 246 | <h2 className="text-lg font-semibold tracking-tight">Choose a registry</h2> | |
| 247 | <p className="mt-1 text-sm text-muted"> | |
| 248 | Packages live beside the code that makes them, with the same members, roles and tokens. Workflows publish with{" "} | |
| 249 | <code className="font-mono text-xs">G1T_TOKEN</code>. | |
| 250 | </p> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 251 | </div> |
| Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start | 252 | <ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> |
| 253 | {REGISTRIES.map((registry) => ( | |
| 254 | <li key={registry.ecosystem} className="flex flex-col rounded-xl border border-line bg-surface p-4"> | |
| 255 | <div className="flex items-center gap-2.5"> | |
| 256 | <PackageIcon ecosystem={registry.ecosystem} /> | |
| 257 | <span className="font-medium">{ECOSYSTEM_LABEL[registry.ecosystem]}</span> | |
| 258 | {!registry.ready && <Badge>Soon</Badge>} | |
| 259 | </div> | |
| 260 | <p className="mt-2.5 grow text-sm text-muted">{registry.blurb}</p> | |
| 261 | {registry.ready && ( | |
| 262 | <div className="mt-3"> | |
| 263 | <CopyLine prompt text={registry.start.replaceAll("<workspace>", workspace)} /> | |
| 264 | </div> | |
| 265 | )} | |
| 266 | <a | |
| 267 | href={`${DOCS}${registry.guide}`} | |
| 268 | className="mt-3 inline-flex items-center gap-1 self-start text-sm text-fg-soft hover:text-fg" | |
| 269 | > | |
| 270 | {registry.ready ? "Set it up" : "What's planned"} <ArrowUpRight size={13} /> | |
| 271 | </a> | |
| 272 | </li> | |
| 273 | ))} | |
| 274 | </ul> | |
| 275 | </div> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 276 | ); |
| 277 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.