Skip to content
547 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.

Navigation: the workspace's own page with tabs, pins and recent in the sidebar1import { 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.2import { type KeyboardEvent, useEffect, useRef, useState } from "react";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar3import { Form, Link, useLocation, useNavigation, useSubmit } from "react-router";
4
5import type { Route } from "./+types/projects";
6import { host } from "../../components/deploy";
7import { 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.8import { EmptyState, TimeAgo, notACredential } from "../../components/ui";
9import { Button } from "../../components/ui/button";
10import { Badge } from "../../components/ui/badge";
11import { 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.12import { 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.13import { Kbd } from "../../components/ui/kbd";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project14import { SelectField } from "../../components/ui/select";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar15import { page as pageMeta } from "../../lib/meta";
16import {
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-a69aeabc4b0deeb9730import { kindLabel, primaryLink } from "../../lib/project-kind";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar31import { deployments, projects } from "../../lib/services.server";
32import { getViewer, roleIn } from "../../lib/session.server";
33import { workspaceProjects } from "../../lib/workspace-projects.server";
34
35export 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 */
44export 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-a69aeabc4b0deeb9755 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 sidebar56 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-a69aeabc4b0deeb9779 // 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 sidebar84 facets: facetsOf(all),
85 pinned: (shortcuts?.pinned ?? []).map((project) => project.id),
86 failed: !listed.ok,
87 };
88}
89
90type Item = Route.ComponentProps["loaderData"]["items"][number];
91
92const 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 project94/** A filter's select, on the same surface as the other controls. */
95const FILTER = "w-auto bg-surface";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar96
97/** Its language, its kind when it is not an app, and its state, as small words. */
98function 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-a69aeabc4b0deeb97103 {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 sidebar105 {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 sidebar107 <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
118function 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 sidebar138 </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
158function 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. */
196function 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
202function 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
239export 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 sidebar249 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 sidebar279 }, [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 project343 <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 sidebar372 {(facets.languages.length > 0 || query.language) && (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project373 <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 sidebar385 )}
386 {(facets.archived > 0 || query.archived !== "hide") && (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project387 <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 sidebar400 )}
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 sidebar405 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 sidebar412 />
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 sidebar420 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 sidebar422 </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 project456 <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 sidebar492 </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 sidebar533 )}
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 sidebar542 </p>
543 )}
544 </div>
545 );
546}
547

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