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.
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 1 | import { ArrowUpRight, Box, ChevronLeft, ChevronRight, LayoutGrid, List, Lock, Rocket, Search } from "lucide-react"; |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 2 | import { type KeyboardEvent, useEffect, useRef, useState } from "react"; |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 3 | import { Form, Link, useLocation, useNavigation, useSubmit } from "react-router"; |
| 4 | ||
| 5 | import type { Route } from "./+types/projects"; | |
| 6 | import { host } from "../../components/deploy"; | |
| 7 | import { PinButton } from "../../components/pin-button"; | |
| 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. | 8 | import { EmptyState, TimeAgo, notACredential } from "../../components/ui"; |
| 9 | import { Button } from "../../components/ui/button"; | |
| 10 | import { Badge } from "../../components/ui/badge"; | |
| 11 | import { Card } from "../../components/ui/card"; | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 12 | import { Checkbox } from "../../components/ui/checkbox"; |
| 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. | 13 | import { Kbd } from "../../components/ui/kbd"; |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 14 | import { SelectField } from "../../components/ui/select"; |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 15 | import { page as pageMeta } from "../../lib/meta"; |
| 16 | import { | |
| 17 | LANGUAGE_OF, | |
| 18 | type Listed, | |
| 19 | type ProjectQuery, | |
| 20 | SORTS, | |
| 21 | facetsOf, | |
| 22 | filterProjects, | |
| 23 | isFiltered, | |
| 24 | lastUpdated, | |
| 25 | nextRow, | |
| 26 | pageOf, | |
| 27 | projectQueryString, | |
| 28 | readProjectQuery, | |
| 29 | } from "../../lib/project-list"; | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 30 | import { kindLabel, primaryLink } from "../../lib/project-kind"; |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 31 | import { deployments, projects } from "../../lib/services.server"; |
| 32 | import { getViewer, roleIn } from "../../lib/session.server"; | |
| 33 | import { workspaceProjects } from "../../lib/workspace-projects.server"; | |
| 34 | ||
| 35 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 36 | return pageMeta(args, { title: `Projects · ${params.owner} · g1t` }); | |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Every project the viewer can see, filtered, sorted and paged here: one | |
| 41 | * listing (shared with the sidebar's), with Deployments and the viewer's | |
| 42 | * pins asked for at the same time. | |
| 43 | */ | |
| 44 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 45 | const viewer = getViewer(context); | |
| 46 | const slug = params.owner.toLowerCase(); | |
| 47 | const role = roleIn(viewer, slug); | |
| 48 | const query = readProjectQuery(new URL(request.url).searchParams); | |
| 49 | const [listed, deploys, shortcuts] = await Promise.all([ | |
| 50 | workspaceProjects(slug, viewer), | |
| 51 | role ? deployments.overview(slug, viewer).catch(() => null) : null, | |
| 52 | role ? projects.shortcuts(slug, viewer).catch(() => null) : null, | |
| 53 | ]); | |
| 54 | const live = new Map((deploys?.ok ? deploys.value : []).map((entry) => [entry.slug, entry])); | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 55 | const full = new Map((listed.ok ? listed.value : []).map((project) => [project.id, project])); |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 56 | const all: Listed[] = (listed.ok ? listed.value : []).map((project) => ({ |
| 57 | id: project.id, | |
| 58 | slug: project.slug, | |
| 59 | name: project.name, | |
| 60 | description: project.description, | |
| 61 | private: project.private, | |
| 62 | archived: project.archived, | |
| 63 | kind: project.kind, | |
| 64 | ecosystem: project.ecosystem, | |
| 65 | updatedAt: project.updatedAt, | |
| 66 | pushedAt: project.pushedAt ?? null, | |
| 67 | activity: project.activity ?? 0, | |
| 68 | deploying: live.get(project.slug)?.enabled ?? false, | |
| 69 | })); | |
| 70 | const matched = filterProjects(all, query); | |
| 71 | const shown = pageOf(matched, query.page); | |
| 72 | return { | |
| 73 | slug, | |
| 74 | role, | |
| 75 | query: { ...query, page: shown.page }, | |
| 76 | total: all.length, | |
| 77 | matched: matched.length, | |
| 78 | page: { page: shown.page, pages: shown.pages, from: shown.from, to: shown.to }, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 79 | // Production where g1t serves it or elsewhere, else its homepage or docs. |
| 80 | items: shown.items.map((project) => { | |
| 81 | const one = full.get(project.id)!; | |
| 82 | return { ...project, runs: one.runs, production: primaryLink(one, live.get(project.slug)?.production?.url) }; | |
| 83 | }), | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 84 | facets: facetsOf(all), |
| 85 | pinned: (shortcuts?.pinned ?? []).map((project) => project.id), | |
| 86 | failed: !listed.ok, | |
| 87 | }; | |
| 88 | } | |
| 89 | ||
| 90 | type Item = Route.ComponentProps["loaderData"]["items"][number]; | |
| 91 | ||
| 92 | const CONTROL = | |
| 93 | "h-9 rounded-md border border-line bg-surface px-2.5 text-sm text-fg outline-none transition-colors hover:border-line-strong focus:border-accent-dim"; | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 94 | /** A filter's select, on the same surface as the other controls. */ |
| 95 | const FILTER = "w-auto bg-surface"; | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 96 | |
| 97 | /** Its language, its kind when it is not an app, and its state, as small words. */ | |
| 98 | function Facts({ project }: { project: Item }) { | |
| 99 | const language = project.ecosystem ? LANGUAGE_OF[project.ecosystem] : null; | |
| 100 | return ( | |
| 101 | <> | |
| 102 | {language && <span>{language}</span>} | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 103 | {project.kind !== "app" && <span>{kindLabel(project)}</span>} |
| 104 | {project.kind === "app" && project.runs === "elsewhere" && <span>Deployed elsewhere</span>} | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 105 | {project.deploying && ( |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 106 | <span className="inline-flex items-center gap-1 text-success"> |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 107 | <Rocket size={11} /> |
| 108 | Deploys | |
| 109 | </span> | |
| 110 | )} | |
| 111 | <span className="text-faint"> | |
| 112 | Updated <TimeAgo at={lastUpdated(project)} /> | |
| 113 | </span> | |
| 114 | </> | |
| 115 | ); | |
| 116 | } | |
| 117 | ||
| 118 | function ProjectRow({ project, slug, pinned, member }: { project: Item; slug: string; pinned: boolean; member: boolean }) { | |
| 119 | const to = `/${slug}/${project.slug}`; | |
| 120 | return ( | |
| 121 | <li className="group relative flex items-center gap-3 px-4 py-3 transition-colors focus-within:bg-raised/60 hover:bg-raised/60"> | |
| 122 | <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line"> | |
| 123 | {project.private ? <Lock size={14} /> : <Box size={14} />} | |
| 124 | </span> | |
| 125 | <div className="min-w-0 grow"> | |
| 126 | <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> | |
| 127 | <Link | |
| 128 | to={to} | |
| 129 | prefetch="intent" | |
| 130 | data-row | |
| 131 | className="truncate font-medium outline-none after:absolute after:inset-0 hover:underline focus-visible:underline" | |
| 132 | > | |
| 133 | {project.name} | |
| 134 | </Link> | |
| 135 | {project.name.toLowerCase() !== project.slug && <span className="truncate font-mono text-xs text-faint">{project.slug}</span>} | |
| 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. | 136 | {project.private && <Badge size="md">private</Badge>} |
| 137 | {project.archived && <Badge size="md">archived</Badge>} | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 138 | </div> |
| 139 | {project.description && <p className="mt-0.5 truncate text-sm text-muted">{project.description}</p>} | |
| 140 | <p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted"> | |
| 141 | <Facts project={project} /> | |
| 142 | </p> | |
| 143 | </div> | |
| 144 | {project.production && ( | |
| 145 | <a | |
| 146 | href={project.production} | |
| 147 | className="relative z-10 hidden shrink-0 items-center gap-1 font-mono text-xs text-muted hover:text-accent md:flex" | |
| 148 | > | |
| 149 | {host(project.production)} | |
| 150 | <ArrowUpRight size={11} /> | |
| 151 | </a> | |
| 152 | )} | |
| 153 | {member && <PinButton workspace={slug} slug={project.slug} name={project.name} pinned={pinned} compact />} | |
| 154 | </li> | |
| 155 | ); | |
| 156 | } | |
| 157 | ||
| 158 | function ProjectTile({ project, slug, pinned, member }: { project: Item; slug: string; pinned: boolean; member: boolean }) { | |
| 159 | return ( | |
| 160 | <li className="group relative flex flex-col rounded-2xl border border-line bg-surface p-5 transition-colors focus-within:border-line-strong hover:border-line-strong"> | |
| 161 | <div className="flex items-start gap-3"> | |
| 162 | <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-raised text-muted ring-1 ring-line"> | |
| 163 | {project.private ? <Lock size={15} /> : <Box size={15} />} | |
| 164 | </span> | |
| 165 | <div className="min-w-0 grow"> | |
| 166 | <Link | |
| 167 | to={`/${slug}/${project.slug}`} | |
| 168 | prefetch="intent" | |
| 169 | data-row | |
| 170 | className="block truncate font-medium outline-none after:absolute after:inset-0 hover:underline focus-visible:underline" | |
| 171 | > | |
| 172 | {project.name} | |
| 173 | </Link> | |
| 174 | {project.production ? ( | |
| 175 | <a href={project.production} className="relative z-10 mt-0.5 flex items-center gap-1 truncate font-mono text-xs text-muted hover:text-accent"> | |
| 176 | {host(project.production)} | |
| 177 | <ArrowUpRight size={11} className="shrink-0" /> | |
| 178 | </a> | |
| 179 | ) : ( | |
| 180 | <p className="mt-0.5 truncate font-mono text-xs text-faint">{project.slug}</p> | |
| 181 | )} | |
| 182 | </div> | |
| 183 | {member && <PinButton workspace={slug} slug={project.slug} name={project.name} pinned={pinned} compact className="-mt-1 -mr-2" />} | |
| 184 | </div> | |
| 185 | <p className="mt-3 line-clamp-2 min-h-10 text-sm text-muted">{project.description ?? ""}</p> | |
| 186 | <p className="mt-auto flex flex-wrap items-center gap-x-3 gap-y-0.5 border-t border-line pt-3 text-xs text-muted"> | |
| 187 | {project.private && <span>Private</span>} | |
| 188 | {project.archived && <span>Archived</span>} | |
| 189 | <Facts project={project} /> | |
| 190 | </p> | |
| 191 | </li> | |
| 192 | ); | |
| 193 | } | |
| 194 | ||
| 195 | /** The pages around the current one: the first, the last, and two either side. */ | |
| 196 | function pageNumbers(page: number, pages: number): (number | null)[] { | |
| 197 | const wanted = new Set([1, pages, page - 2, page - 1, page, page + 1, page + 2].filter((n) => n >= 1 && n <= pages)); | |
| 198 | const sorted = [...wanted].sort((a, b) => a - b); | |
| 199 | return sorted.flatMap((n, i) => (i > 0 && n - sorted[i - 1]! > 1 ? [null, n] : [n])); | |
| 200 | } | |
| 201 | ||
| 202 | function Pages({ query, page, pages }: { query: ProjectQuery; page: number; pages: number }) { | |
| 203 | if (pages <= 1) return null; | |
| 204 | const link = "flex h-8 min-w-8 items-center justify-center rounded-md px-2 text-sm transition-colors"; | |
| 205 | return ( | |
| 206 | <nav aria-label="Pages" className="mt-6 flex flex-wrap items-center justify-center gap-1"> | |
| 207 | {page > 1 ? ( | |
| 208 | <Link to={projectQueryString(query, { page: page - 1 })} className={`${link} gap-1 text-muted hover:bg-raised hover:text-fg`}> | |
| 209 | <ChevronLeft size={14} /> | |
| 210 | Previous | |
| 211 | </Link> | |
| 212 | ) : null} | |
| 213 | {pageNumbers(page, pages).map((n, i) => | |
| 214 | n == null ? ( | |
| 215 | <span key={`gap${i}`} className="px-1 text-faint"> | |
| 216 | … | |
| 217 | </span> | |
| 218 | ) : ( | |
| 219 | <Link | |
| 220 | key={n} | |
| 221 | to={projectQueryString(query, { page: n })} | |
| 222 | aria-current={n === page ? "page" : undefined} | |
| 223 | className={`${link} tabular-nums ${n === page ? "bg-raised font-medium text-fg ring-1 ring-line" : "text-muted hover:bg-raised hover:text-fg"}`} | |
| 224 | > | |
| 225 | {n} | |
| 226 | </Link> | |
| 227 | ), | |
| 228 | )} | |
| 229 | {page < pages ? ( | |
| 230 | <Link to={projectQueryString(query, { page: page + 1 })} className={`${link} gap-1 text-muted hover:bg-raised hover:text-fg`}> | |
| 231 | Next | |
| 232 | <ChevronRight size={14} /> | |
| 233 | </Link> | |
| 234 | ) : null} | |
| 235 | </nav> | |
| 236 | ); | |
| 237 | } | |
| 238 | ||
| 239 | export default function WorkspaceProjects({ loaderData }: Route.ComponentProps) { | |
| 240 | const { slug, role, query, total, matched, page, items, facets, pinned, failed } = loaderData; | |
| 241 | const submit = useSubmit(); | |
| 242 | const navigation = useNavigation(); | |
| 243 | const { pathname } = useLocation(); | |
| 244 | const form = useRef<HTMLFormElement>(null); | |
| 245 | const searchBox = useRef<HTMLInputElement>(null); | |
| 246 | const typing = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 247 | const deploysBox = useRef<HTMLButtonElement>(null); |
| 248 | const [deploys, setDeploys] = useState(query.deployments); | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 249 | const member = role != null; |
| 250 | const pins = new Set(pinned); | |
| 251 | // A filter or page of this list on its way: the list says it is busy. | |
| 252 | const busy = navigation.state === "loading" && navigation.location?.pathname === pathname; | |
| 253 | ||
| 254 | const apply = (delay = 0) => { | |
| 255 | if (typing.current) clearTimeout(typing.current); | |
| 256 | typing.current = setTimeout(() => { | |
| 257 | if (form.current) submit(form.current, { replace: true, preventScrollReset: true }); | |
| 258 | }, delay); | |
| 259 | }; | |
| 260 | useEffect(() => () => void (typing.current && clearTimeout(typing.current)), []); | |
| 261 | // Back and forward change the address, not the fields: they follow it, | |
| 262 | // except the one being used. | |
| 263 | useEffect(() => { | |
| 264 | const fields = form.current?.elements; | |
| 265 | if (!fields) return; | |
| 266 | const values: Record<string, string> = { | |
| 267 | q: query.q, | |
| 268 | visibility: query.visibility, | |
| 269 | kind: query.kind, | |
| 270 | language: query.language ?? "", | |
| 271 | archived: query.archived, | |
| 272 | sort: query.sort, | |
| 273 | }; | |
| 274 | for (const [name, value] of Object.entries(values)) { | |
| 275 | const field = fields.namedItem(name) as HTMLInputElement | HTMLSelectElement | null; | |
| 276 | if (field && field !== document.activeElement && "value" in field) field.value = value; | |
| 277 | } | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 278 | if (deploysBox.current !== document.activeElement) setDeploys(query.deployments); |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 279 | }, [query]); |
| 280 | ||
| 281 | // `/` goes to the search, from anywhere on the page that is not a field. | |
| 282 | useEffect(() => { | |
| 283 | const onKey = (event: globalThis.KeyboardEvent) => { | |
| 284 | const target = event.target as HTMLElement | null; | |
| 285 | if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return; | |
| 286 | if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) return; | |
| 287 | event.preventDefault(); | |
| 288 | searchBox.current?.focus(); | |
| 289 | }; | |
| 290 | window.addEventListener("keydown", onKey); | |
| 291 | return () => window.removeEventListener("keydown", onKey); | |
| 292 | }, []); | |
| 293 | ||
| 294 | // The arrows (and j and k) move between projects; Enter opens one. | |
| 295 | const list = useRef<HTMLDivElement>(null); | |
| 296 | const onListKey = (event: KeyboardEvent<HTMLDivElement>) => { | |
| 297 | const rows = [...(list.current?.querySelectorAll<HTMLAnchorElement>("a[data-row]") ?? [])]; | |
| 298 | const at = rows.indexOf(document.activeElement as HTMLAnchorElement); | |
| 299 | const key = event.key === "ArrowRight" ? "ArrowDown" : event.key === "ArrowLeft" ? "ArrowUp" : event.key; | |
| 300 | if (at < 0 && key !== "ArrowDown" && key !== "j") return; | |
| 301 | const to = nextRow(key, at, rows.length); | |
| 302 | if (to == null || event.metaKey || event.ctrlKey || event.altKey) return; | |
| 303 | event.preventDefault(); | |
| 304 | rows[to]?.focus(); | |
| 305 | }; | |
| 306 | // From the search box, down goes to the first project. | |
| 307 | const onSearchKey = (event: KeyboardEvent<HTMLInputElement>) => { | |
| 308 | if (event.key === "ArrowDown") { | |
| 309 | event.preventDefault(); | |
| 310 | list.current?.querySelector<HTMLAnchorElement>("a[data-row]")?.focus(); | |
| 311 | } | |
| 312 | if (event.key === "Escape" && event.currentTarget.value) { | |
| 313 | event.currentTarget.value = ""; | |
| 314 | apply(); | |
| 315 | } | |
| 316 | }; | |
| 317 | ||
| 318 | const filtered = isFiltered(query); | |
| 319 | const option = (label: string, count: number) => `${label} (${count})`; | |
| 320 | ||
| 321 | return ( | |
| 322 | <div> | |
| 323 | <Form ref={form} id="project-filters" method="get" role="search" aria-label="Find projects" className="flex flex-wrap items-center gap-2"> | |
| 324 | <div className="relative w-full sm:w-72"> | |
| 325 | <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" /> | |
| 326 | <input | |
| 327 | ref={searchBox} | |
| 328 | type="search" | |
| 329 | name="q" | |
| 330 | defaultValue={query.q} | |
| 331 | {...notACredential()} | |
| 332 | placeholder="Find a project…" | |
| 333 | aria-label="Find a project" | |
| 334 | aria-keyshortcuts="/" | |
| 335 | onChange={() => apply(250)} | |
| 336 | onKeyDown={onSearchKey} | |
| 337 | className={`${CONTROL} w-full pr-8 pl-8 placeholder:text-faint`} | |
| 338 | /> | |
| 339 | <kbd className="pointer-events-none absolute top-1/2 right-2 hidden -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line sm:block"> | |
| 340 | / | |
| 341 | </kbd> | |
| 342 | </div> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 343 | <SelectField |
| 344 | key={`visibility-${query.visibility}`} | |
| 345 | name="visibility" | |
| 346 | aria-label="Visibility" | |
| 347 | defaultValue={query.visibility} | |
| 348 | afterChange={() => apply()} | |
| 349 | className={FILTER} | |
| 350 | options={[ | |
| 351 | { value: "all", label: "All visibility" }, | |
| 352 | { value: "public", label: option("Public", facets.visibility.public) }, | |
| 353 | { value: "private", label: option("Private", facets.visibility.private) }, | |
| 354 | ]} | |
| 355 | /> | |
| 356 | <SelectField | |
| 357 | key={`kind-${query.kind}`} | |
| 358 | name="kind" | |
| 359 | aria-label="Kind" | |
| 360 | defaultValue={query.kind} | |
| 361 | afterChange={() => apply()} | |
| 362 | className={FILTER} | |
| 363 | options={[ | |
| 364 | { value: "all", label: "All kinds" }, | |
| 365 | { value: "app", label: option("Apps", facets.kind.app) }, | |
| 366 | { value: "library", label: option("Libraries", facets.kind.library) }, | |
| 367 | ...(facets.kind.tool > 0 || query.kind === "tool" ? [{ value: "tool", label: option("Tools", facets.kind.tool) }] : []), | |
| 368 | ...(facets.kind.docs > 0 || query.kind === "docs" ? [{ value: "docs", label: option("Docs", facets.kind.docs) }] : []), | |
| 369 | ...(facets.kind.other > 0 || query.kind === "other" ? [{ value: "other", label: option("Other", facets.kind.other) }] : []), | |
| 370 | ]} | |
| 371 | /> | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 372 | {(facets.languages.length > 0 || query.language) && ( |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 373 | <SelectField |
| 374 | key={`language-${query.language ?? ""}`} | |
| 375 | name="language" | |
| 376 | aria-label="Language" | |
| 377 | defaultValue={query.language ?? ""} | |
| 378 | afterChange={() => apply()} | |
| 379 | className={FILTER} | |
| 380 | options={[ | |
| 381 | { value: "", label: "Any language" }, | |
| 382 | ...facets.languages.map((language) => ({ value: language.name, label: option(language.name, language.count) })), | |
| 383 | ]} | |
| 384 | /> | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 385 | )} |
| 386 | {(facets.archived > 0 || query.archived !== "hide") && ( | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 387 | <SelectField |
| 388 | key={`archived-${query.archived}`} | |
| 389 | name="archived" | |
| 390 | aria-label="Archived" | |
| 391 | defaultValue={query.archived} | |
| 392 | afterChange={() => apply()} | |
| 393 | className={FILTER} | |
| 394 | options={[ | |
| 395 | { value: "hide", label: "Without archived" }, | |
| 396 | { value: "include", label: "With archived" }, | |
| 397 | { value: "only", label: option("Only archived", facets.archived) }, | |
| 398 | ]} | |
| 399 | /> | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 400 | )} |
| 401 | {member && ( | |
| 402 | <label className={`${CONTROL} flex cursor-pointer items-center gap-2 select-none`}> | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 403 | <Checkbox |
| 404 | ref={deploysBox} | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 405 | name="deployments" |
| 406 | value="on" | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 407 | checked={deploys} |
| 408 | onCheckedChange={(checked) => { | |
| 409 | setDeploys(checked === true); | |
| 410 | apply(); | |
| 411 | }} | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 412 | /> |
| 413 | Deploys | |
| 414 | <span className="text-xs tabular-nums text-faint">{facets.deploying}</span> | |
| 415 | </label> | |
| 416 | )} | |
| 417 | {query.view !== "list" && <input type="hidden" name="view" value={query.view} />} | |
| 418 | <noscript> | |
| 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. | 419 | <Button type="submit" variant="outline" className="bg-surface px-2.5 font-normal text-fg"> |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 420 | Apply |
| 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. | 421 | </Button> |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 422 | </noscript> |
| 423 | </Form> | |
| 424 | ||
| 425 | <div className="mt-4 flex flex-wrap items-center gap-3 text-sm"> | |
| 426 | <p className="text-muted" aria-live="polite"> | |
| 427 | {filtered ? ( | |
| 428 | <> | |
| 429 | <span className="font-medium text-fg tabular-nums">{matched}</span> of {total} {total === 1 ? "project" : "projects"} | |
| 430 | {query.q && ( | |
| 431 | <> | |
| 432 | {" "} | |
| 433 | matching <span className="text-fg">“{query.q}”</span> | |
| 434 | </> | |
| 435 | )} | |
| 436 | </> | |
| 437 | ) : ( | |
| 438 | <> | |
| 439 | <span className="font-medium text-fg tabular-nums">{total}</span> {total === 1 ? "project" : "projects"} | |
| 440 | </> | |
| 441 | )} | |
| 442 | {page.pages > 1 && ( | |
| 443 | <span className="text-faint"> | |
| 444 | {" "} | |
| 445 | · showing {page.from}–{page.to} | |
| 446 | </span> | |
| 447 | )} | |
| 448 | </p> | |
| 449 | {filtered && ( | |
| 450 | <Link to={projectQueryString({ ...query, q: "", visibility: "all", kind: "all", language: null, deployments: false, archived: "hide" })} className="text-xs text-muted hover:text-fg"> | |
| 451 | Clear filters | |
| 452 | </Link> | |
| 453 | )} | |
| 454 | <div className="ml-auto flex items-center gap-2"> | |
| 455 | {/* Part of the filters' form, beside the view it orders. */} | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 456 | <SelectField |
| 457 | key={`sort-${query.sort}`} | |
| 458 | form="project-filters" | |
| 459 | name="sort" | |
| 460 | aria-label="Sort" | |
| 461 | defaultValue={query.sort} | |
| 462 | afterChange={() => apply()} | |
| 463 | size="sm" | |
| 464 | className={FILTER} | |
| 465 | align="end" | |
| 466 | options={SORTS.map((sort) => ({ value: sort.value, label: sort.label }))} | |
| 467 | /> | |
| 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. | 468 | <Card asChild tone="bg" radius="lg" className="flex items-center gap-0.5 p-0.5"> |
| 469 | <nav aria-label="View"> | |
| 470 | {( | |
| 471 | [ | |
| 472 | ["list", "List", <List key="list" size={14} />], | |
| 473 | ["grid", "Grid", <LayoutGrid key="grid" size={14} />], | |
| 474 | ] as const | |
| 475 | ).map(([view, label, icon]) => ( | |
| 476 | <Link | |
| 477 | key={view} | |
| 478 | to={projectQueryString(query, { view, page: query.page })} | |
| 479 | replace | |
| 480 | preventScrollReset | |
| 481 | aria-label={`${label} view`} | |
| 482 | aria-current={query.view === view ? "true" : undefined} | |
| 483 | className={`flex size-7 items-center justify-center rounded-md transition-colors ${ | |
| 484 | query.view === view ? "bg-raised text-fg ring-1 ring-line" : "text-faint hover:text-fg" | |
| 485 | }`} | |
| 486 | > | |
| 487 | {icon} | |
| 488 | </Link> | |
| 489 | ))} | |
| 490 | </nav> | |
| 491 | </Card> | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 492 | </div> |
| 493 | </div> | |
| 494 | ||
| 495 | <div ref={list} onKeyDown={onListKey} aria-busy={busy || undefined} className={`mt-4 transition-opacity ${busy ? "opacity-60" : ""}`}> | |
| 496 | {failed ? ( | |
| 497 | <EmptyState title="Projects cannot be listed right now">Try again in a moment.</EmptyState> | |
| 498 | ) : total === 0 ? ( | |
| 499 | <EmptyState title="No projects yet"> | |
| 500 | {member ? ( | |
| 501 | <> | |
| 502 | <Link to={`/new?workspace=${slug}`} className="text-accent hover:underline"> | |
| 503 | Create one | |
| 504 | </Link> | |
| 505 | , or push a repository and it becomes one. | |
| 506 | </> | |
| 507 | ) : ( | |
| 508 | "This workspace has no public projects." | |
| 509 | )} | |
| 510 | </EmptyState> | |
| 511 | ) : items.length === 0 ? ( | |
| 512 | <EmptyState title="No projects match"> | |
| 513 | Try other words or filters, or{" "} | |
| 514 | <Link to={projectQueryString({ ...query, q: "", visibility: "all", kind: "all", language: null, deployments: false, archived: "hide" })} className="text-accent hover:underline"> | |
| 515 | clear them | |
| 516 | </Link> | |
| 517 | . | |
| 518 | </EmptyState> | |
| 519 | ) : query.view === "grid" ? ( | |
| 520 | <ul aria-label="Projects" className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> | |
| 521 | {items.map((project) => ( | |
| 522 | <ProjectTile key={project.id} project={project} slug={slug} pinned={pins.has(project.id)} member={member} /> | |
| 523 | ))} | |
| 524 | </ul> | |
| 525 | ) : ( | |
| 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. | 526 | <Card asChild divided className="overflow-hidden"> |
| 527 | <ul aria-label="Projects"> | |
| 528 | {items.map((project) => ( | |
| 529 | <ProjectRow key={project.id} project={project} slug={slug} pinned={pins.has(project.id)} member={member} /> | |
| 530 | ))} | |
| 531 | </ul> | |
| 532 | </Card> | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 533 | )} |
| 534 | </div> | |
| 535 | <Pages query={query} page={page.page} pages={page.pages} /> | |
| 536 | {items.length > 0 && ( | |
| 537 | <p className="mt-4 hidden text-center text-xs text-faint sm:block"> | |
| 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. | 538 | <Kbd>/</Kbd> to find,{" "} |
| 539 | <Kbd>↑</Kbd>{" "} | |
| 540 | <Kbd>↓</Kbd> to move,{" "} | |
| 541 | <Kbd>Enter</Kbd> to open | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 542 | </p> |
| 543 | )} | |
| 544 | </div> | |
| 545 | ); | |
| 546 | } | |
| 547 |
This file's history is long; its oldest lines are credited to the oldest commit read.