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