| 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"; |
| 3 | 3 | | |
| 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"; |
| 5 | 5 | | |
| 6 | 6 | | import { BlameView } from "./blame-view"; |
| 7 | 7 | | import { CodeLines } from "./code-lines"; |
| ⋯ |
| 9 | 9 | | import { AgentSetup } from "./agent-setup"; |
| 10 | 10 | | import { CloneBox } from "./clone-box"; |
| 11 | 11 | | 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"; |
| 13 | 15 | | |
| 14 | 16 | | function encodePath(path: string): string { |
| 15 | 17 | | return path.split("/").map(encodeURIComponent).join("/"); |
| ⋯ |
| 29 | 31 | | const segments = path.split("/").filter(Boolean); |
| 30 | 32 | | if (segments.length === 0) return null; |
| 31 | 33 | | return ( |
| 32 | | − | <p className="mb-4 font-mono text-sm text-muted"> |
| 34 | + | <p className="font-mono text-sm text-muted"> |
| 33 | 35 | | <Link to={`${base}/tree/${gitRef}`} className="text-accent hover:underline"> |
| 34 | 36 | | {repo} |
| 35 | 37 | | </Link> |
| ⋯ |
| 56 | 58 | | ); |
| 57 | 59 | | } |
| 58 | 60 | | |
| 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 }) { |
| 61 | 63 | | return ( |
| 62 | 64 | | <div className="flex items-center gap-3 border-b border-line bg-surface px-4 py-2.5 text-sm"> |
| 63 | 65 | | <Avatar name={commit.author.name} /> |
| 64 | 66 | | <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> |
| 71 | 74 | | <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> |
| 72 | 79 | | </span> |
| 73 | 80 | | </div> |
| 74 | 81 | | ); |
| 75 | 82 | | } |
| 76 | 83 | | |
| 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 }) { |
| 78 | 189 | | const { repo, ref, path, head, entries, readme } = tree; |
| 79 | 190 | | const base = `/${repo.namespace}/${repo.name}`; |
| 80 | 191 | | const prefix = path ? `${encodePath(path)}/` : ""; |
| ⋯ |
| 104 | 215 | | } |
| 105 | 216 | | |
| 106 | 217 | | return ( |
| 107 | | − | <div className="grid gap-6 lg:grid-cols-[1fr_18rem]"> |
| 218 | + | <div className={path ? "" : "grid gap-8 lg:grid-cols-[1fr_17rem]"}> |
| 108 | 219 | | <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} /> |
| 110 | 221 | | <div className="overflow-hidden rounded-xl border border-line"> |
| 111 | | − | <CommitBar commit={head} gitRef={ref} /> |
| 222 | + | <CommitBar commit={head} base={base} /> |
| 112 | 223 | | <ul className="divide-y divide-line text-sm"> |
| 113 | 224 | | {entries.map((entry) => { |
| 114 | 225 | | const isTree = entry.kind === "tree"; |
| ⋯ |
| 132 | 243 | | </div> |
| 133 | 244 | | |
| 134 | 245 | | {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"> |
| 136 | 247 | | <h2 className="flex items-center gap-2 border-b border-line bg-surface px-4 py-2.5 text-sm font-medium"> |
| 137 | 248 | | <BookOpen size={15} className="text-faint" /> |
| 138 | 249 | | {readme.name} |
| ⋯ |
| 154 | 265 | | </div> |
| 155 | 266 | | |
| 156 | 267 | | {!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> |
| 173 | 296 | | </aside> |
| 174 | 297 | | )} |
| 175 | 298 | | </div> |