Commit

The code page reads like a forge: a branch menu, search and a Code button above the files

Clone moves from the sidebar into the Code button's popover (HTTPS, SSH, Agent). The bar above the files has a branch switcher that keeps the path, the branch count and code search; in a folder, the breadcrumbs sit beside the branch. The commit row links the commit and History. About lists Readme, Commits and branches.

syntaqxcommitted Parentff824eeBrowse files
3 files+172−700/3 viewed
+157−34
1−import { BookOpen, File, Folder, FolderGit2 } from "lucide-react";
2−import { Link } from "react-router";
1+import { BookOpen, Check, ChevronDown, Code2, File, Folder, FolderGit2, GitBranch, History, Search, SquareTerminal } from "lucide-react";
2+import { Form, Link } from "react-router";
33
4−import type { Blame, BlobView as Blob, Commit, TreeView as Tree } from "@g1t/contracts";
4+import type { Blame, BlobView as Blob, Branch, Commit, TreeView as Tree } from "@g1t/contracts";
55
66 import { BlameView } from "./blame-view";
77 import { CodeLines } from "./code-lines";
99 import { AgentSetup } from "./agent-setup";
1010 import { CloneBox } from "./clone-box";
1111 import { Markdown } from "./markdown";
12−import { Avatar, CopyLine, TimeAgo } from "./ui";
12+import { Avatar, CopyLine, TimeAgo, notACredential } from "./ui";
13+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
14+import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
1315
1416 function encodePath(path: string): string {
1517 return path.split("/").map(encodeURIComponent).join("/");
2931 const segments = path.split("/").filter(Boolean);
3032 if (segments.length === 0) return null;
3133 return (
32− <p className="mb-4 font-mono text-sm text-muted">
34+ <p className="font-mono text-sm text-muted">
3335 <Link to={`${base}/tree/${gitRef}`} className="text-accent hover:underline">
3436 {repo}
3537 </Link>
5658 );
5759 }
5860
59−/** The bar above a file listing describing the latest commit. */
60−function CommitBar({ commit, gitRef }: { commit: Commit; gitRef: string }) {
61+/** The bar above a file listing: the latest commit, and the way to the history. */
62+function CommitBar({ commit, base }: { commit: Commit; base: string }) {
6163 return (
6264 <div className="flex items-center gap-3 border-b border-line bg-surface px-4 py-2.5 text-sm">
6365 <Avatar name={commit.author.name} />
6466 <span className="shrink-0 font-medium">{commit.author.name}</span>
65− <span className="truncate text-muted">{commit.message.split("\n")[0]}</span>
66− <span className="ml-auto flex shrink-0 items-center gap-3 text-xs text-faint">
67− <span className="rounded-full border border-line px-2 py-0.5 font-mono text-accent">
68− {gitRef}
69− </span>
70− <span className="font-mono">{commit.hash.slice(0, 7)}</span>
67+ <Link to={`${base}/commit/${commit.hash}`} className="truncate text-muted hover:text-fg hover:underline">
68+ {commit.message.split("\n")[0]}
69+ </Link>
70+ <span className="ml-auto flex shrink-0 items-center gap-4 text-xs text-faint">
71+ <Link to={`${base}/commit/${commit.hash}`} className="font-mono hover:text-fg">
72+ {commit.hash.slice(0, 7)}
73+ </Link>
7174 <TimeAgo at={commit.authoredAt} />
75+ <Link to={`${base}/commits`} className="inline-flex items-center gap-1.5 font-medium text-muted hover:text-fg">
76+ <History size={14} />
77+ History
78+ </Link>
7279 </span>
7380 </div>
7481 );
7582 }
7683
77−export function TreeView({ tree }: { tree: Tree }) {
84+const BAR_BUTTON =
85+ "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-line bg-surface px-3 text-sm transition-colors hover:border-line-strong hover:bg-raised data-[state=open]:border-line-strong";
86+
87+/** Which branch is shown, and the others to switch to, at the same path. */
88+function BranchMenu({ base, gitRef, path, branches }: { base: string; gitRef: string; path: string; branches: Branch[] }) {
89+ const rest = path ? `/${encodePath(path)}` : "";
90+ return (
91+ <DropdownMenu>
92+ <DropdownMenuTrigger className={`${BAR_BUTTON} max-w-56 font-medium`}>
93+ <GitBranch size={14} className="text-faint" />
94+ <span className="truncate font-mono text-[0.8125rem]">{gitRef}</span>
95+ <ChevronDown size={13} className="text-faint" />
96+ </DropdownMenuTrigger>
97+ <DropdownMenuContent align="start" className="max-h-80 w-64 overflow-y-auto">
98+ <DropdownMenuLabel>Switch branches</DropdownMenuLabel>
99+ {branches.map((branch) => (
100+ <DropdownMenuItem key={branch.name} asChild>
101+ <Link to={`${base}/tree/${encodePath(branch.name)}${rest}`}>
102+ <Check className={branch.name === gitRef ? "" : "invisible"} />
103+ <span className="truncate font-mono text-[0.8125rem]">{branch.name}</span>
104+ </Link>
105+ </DropdownMenuItem>
106+ ))}
107+ </DropdownMenuContent>
108+ </DropdownMenu>
109+ );
110+}
111+
112+/** Searches this repository's code on the search page, where it can be widened to all of g1t. */
113+function SearchCode({ repo }: { repo: string }) {
114+ return (
115+ <Form action="/search" role="search" className="relative ml-auto w-full min-w-40 grow sm:w-64 sm:grow-0">
116+ <Search size={14} className="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-faint" />
117+ <input
118+ name="q"
119+ {...notACredential()}
120+ placeholder="Search code"
121+ aria-label={`Search ${repo}`}
122+ className="h-8 w-full rounded-md border border-line bg-surface pr-3 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
123+ />
124+ <input type="hidden" name="repo" value={repo} />
125+ <input type="hidden" name="type" value="code" />
126+ </Form>
127+ );
128+}
129+
130+/** The one button for getting the code: clone over HTTPS or SSH, or hand it to an agent. */
131+function CodeButton({ path }: { path: string }) {
132+ return (
133+ <Popover>
134+ <PopoverTrigger className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent/90">
135+ <Code2 size={15} />
136+ Code
137+ <ChevronDown size={13} />
138+ </PopoverTrigger>
139+ <PopoverContent align="end" className="w-96 p-4">
140+ <h2 className="mb-3 flex items-center gap-2 text-sm font-medium">
141+ <SquareTerminal size={15} className="text-faint" />
142+ Clone
143+ </h2>
144+ <CloneBox path={path} />
145+ </PopoverContent>
146+ </Popover>
147+ );
148+}
149+
150+/** Above the files: the branch and, at the root, how many there are, search and Code; below it, where you are. */
151+function CodeBar({
152+ base,
153+ repo,
154+ gitRef,
155+ path,
156+ branches,
157+}: {
158+ base: string;
159+ repo: { namespace: string; name: string };
160+ gitRef: string;
161+ path: string;
162+ branches: Branch[] | null;
163+}) {
164+ const full = `${repo.namespace}/${repo.name}`;
165+ const list = branches && branches.length > 0 ? branches : [{ name: gitRef, hash: "" }];
166+ return (
167+ <div className="mb-4 flex flex-wrap items-center gap-x-3 gap-y-2">
168+ <BranchMenu base={base} gitRef={gitRef} path={path} branches={list} />
169+ {path ? (
170+ <Breadcrumbs base={base} repo={repo.name} gitRef={gitRef} path={path} />
171+ ) : (
172+ <>
173+ {branches && (
174+ <span className="inline-flex items-center gap-1.5 text-sm text-muted">
175+ <GitBranch size={14} className="text-faint" />
176+ <span className="font-medium text-fg">{branches.length}</span>
177+ {branches.length === 1 ? "branch" : "branches"}
178+ </span>
179+ )}
180+ <SearchCode repo={full} />
181+ <CodeButton path={full} />
182+ </>
183+ )}
184+ </div>
185+ );
186+}
187+
188+export function TreeView({ tree, branches = null }: { tree: Tree; branches?: Branch[] | null }) {
78189 const { repo, ref, path, head, entries, readme } = tree;
79190 const base = `/${repo.namespace}/${repo.name}`;
80191 const prefix = path ? `${encodePath(path)}/` : "";
104215 }
105216
106217 return (
107− <div className="grid gap-6 lg:grid-cols-[1fr_18rem]">
218+ <div className={path ? "" : "grid gap-8 lg:grid-cols-[1fr_17rem]"}>
108219 <div className="min-w-0">
109− <Breadcrumbs base={base} repo={repo.name} gitRef={ref} path={path} />
220+ <CodeBar base={base} repo={repo} gitRef={ref} path={path} branches={branches} />
110221 <div className="overflow-hidden rounded-xl border border-line">
111− <CommitBar commit={head} gitRef={ref} />
222+ <CommitBar commit={head} base={base} />
112223 <ul className="divide-y divide-line text-sm">
113224 {entries.map((entry) => {
114225 const isTree = entry.kind === "tree";
132243 </div>
133244
134245 {readme?.text != null && (
135− <section className="mt-6 overflow-hidden rounded-xl border border-line">
246+ <section id="readme" className="mt-6 scroll-mt-20 overflow-hidden rounded-xl border border-line">
136247 <h2 className="flex items-center gap-2 border-b border-line bg-surface px-4 py-2.5 text-sm font-medium">
137248 <BookOpen size={15} className="text-faint" />
138249 {readme.name}
154265 </div>
155266
156267 {!path && (
157− <aside className="space-y-6">
158− <section>
159− <h2 className="text-sm font-medium">Clone</h2>
160− <div className="mt-2">
161− <CloneBox path={`${repo.namespace}/${repo.name}`} />
162− </div>
163− </section>
164− <section>
165− <h2 className="text-sm font-medium">About</h2>
166− <p className="mt-2 text-sm text-muted">
167− {repo.description ?? "No description."}
168− </p>
169− <p className="mt-3 text-xs text-faint">
170− Created <TimeAgo at={repo.createdAt} />
171− </p>
172− </section>
268+ <aside>
269+ <h2 className="text-base font-semibold">About</h2>
270+ <p className="mt-2.5 text-sm text-fg-soft">{repo.description ?? "No description."}</p>
271+ <ul className="mt-4 space-y-2.5 text-sm text-muted">
272+ {readme && (
273+ <li>
274+ <a href="#readme" className="inline-flex items-center gap-2 hover:text-fg">
275+ <BookOpen size={15} className="text-faint" />
276+ Readme
277+ </a>
278+ </li>
279+ )}
280+ <li>
281+ <Link to={`${base}/commits`} className="inline-flex items-center gap-2 hover:text-fg">
282+ <History size={15} className="text-faint" />
283+ Commits
284+ </Link>
285+ </li>
286+ {branches && (
287+ <li className="flex items-center gap-2">
288+ <GitBranch size={15} className="text-faint" />
289+ {branches.length} {branches.length === 1 ? "branch" : "branches"}
290+ </li>
291+ )}
292+ </ul>
293+ <p className="mt-4 text-xs text-faint">
294+ Created <TimeAgo at={repo.createdAt} />
295+ </p>
173296 </aside>
174297 )}
175298 </div>
+8−33
1−import { Search } from "lucide-react";
2−import { Form } from "react-router";
3−
41 import type { Route } from "./+types/code";
52 import { TreeView } from "../../components/repo-view";
6−import { notACredential } from "../../components/ui";
73 import { page } from "../../lib/meta";
84 import { repos } from "../../lib/services.server";
95 import { getViewer, unwrap } from "../../lib/session.server";
1410
1511 export async function loader({ params, context }: Route.LoaderArgs) {
1612 const path = { namespace: params.owner, name: params.repo };
17− return unwrap(await repos.tree(path, getViewer(context), null, ""));
18−}
19−
20−/**
21− * Searches this repository's code: the search page adds `repo:` for it, so
22− * the results can be widened to all of g1t from there.
23− */
24−function SearchThisRepository({ repo }: { repo: string }) {
25− return (
26− <Form action="/search" role="search" className="relative mb-4 max-w-md">
27− <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
28− <input
29− name="q"
30− {...notACredential()}
31− placeholder="Search this repository's code"
32− aria-label={`Search ${repo}`}
33− className="h-9 w-full rounded-md border border-line bg-surface pr-3 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
34− />
35− <input type="hidden" name="repo" value={repo} />
36− <input type="hidden" name="type" value="code" />
37− </Form>
38− );
13+ const viewer = getViewer(context);
14+ const [tree, branches] = await Promise.all([
15+ repos.tree(path, viewer, null, ""),
16+ // For the branch menu; the page still shows without it.
17+ repos.branches(path, viewer).catch(() => null),
18+ ]);
19+ return { tree: unwrap(tree), branches: branches?.ok ? branches.value : null };
3920 }
4021
4122 export default function Code({ loaderData }: Route.ComponentProps) {
42− const repo = `${loaderData.repo.namespace}/${loaderData.repo.name}`;
43− return (
44− <>
45− {loaderData.head && <SearchThisRepository repo={repo} />}
46− <TreeView tree={loaderData} />
47− </>
48− );
23+ return <TreeView tree={loaderData.tree} branches={loaderData.branches} />;
4924 }
+7−3
1313 export async function loader({ params, context, request }: Route.LoaderArgs) {
1414 const path = { namespace: params.owner, name: params.repo };
1515 const viewer = getViewer(context);
16− const tree = await repos.tree(path, viewer, params.ref, params["*"] ?? "");
16+ const [tree, branches] = await Promise.all([
17+ repos.tree(path, viewer, params.ref, params["*"] ?? ""),
18+ // For the branch menu; the page still shows without it.
19+ repos.branches(path, viewer).catch(() => null),
20+ ]);
1721 // A branch that was renamed: the same folder on its new name.
1822 if (!tree.ok && tree.error.code === "not_found") await redirectIfBranchRenamed(request, path, viewer, params.ref);
19− return unwrap(tree);
23+ return { tree: unwrap(tree), branches: branches?.ok ? branches.value : null };
2024 }
2125
2226 export default function Tree({ loaderData }: Route.ComponentProps) {
23− return <TreeView tree={loaderData} />;
27+ return <TreeView tree={loaderData.tree} branches={loaderData.branches} />;
2428 }