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.
| Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store | 1 | import type { DocRepoPage } from "@g1t/contracts"; |
| 2 | import { FolderGit2, GitPullRequestArrow, PencilLine, Trash2 } from "lucide-react"; | |
| 3 | import { Link, data, useNavigate } from "react-router"; | |
| 4 | ||
| 5 | import type { Route } from "./+types/repo-file"; | |
| 6 | import { useDocsAction } from "../../../components/docs/actions"; | |
| 7 | import { Crumbs } from "../../../components/docs/parts"; | |
| 8 | import { Markdown } from "../../../components/markdown"; | |
| 9 | import { ErrorText, TimeAgo } from "../../../components/ui"; | |
| 10 | import { Hint } from "../../../components/ui/hint"; | |
| 11 | import { page as pageMeta } from "../../../lib/meta"; | |
| 12 | import { docs } from "../../../lib/services.server"; | |
| 13 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 14 | ||
| 15 | export function meta({ loaderData: loaded, params, ...args }: Route.MetaArgs) { | |
| 16 | const file = loaded?.found.file; | |
| 17 | return pageMeta(args, { title: `${file?.title ?? "Docs"} · ${params.repoOwner}/${params.repoName} · ${params.owner} · g1t` }); | |
| 18 | } | |
| 19 | ||
| 20 | export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ found: DocRepoPage }> { | |
| 21 | const viewer = requireUser(context, request); | |
| 22 | if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 }); | |
| 23 | const repo = `${params.repoOwner}/${params.repoName}`; | |
| 24 | const found = await docs.repoPage(params.owner.toLowerCase(), viewer, repo, params["*"] ?? "").catch(() => null); | |
| 25 | if (!found?.ok) throw data(null, { status: 404 }); | |
| 26 | return { found: found.value }; | |
| 27 | } | |
| 28 | ||
| 29 | /** | |
| 30 | * One file of a project's docs, read-only: rendered as Code renders it, | |
| 31 | * with its links and pictures pointing into the repository. Changes go | |
| 32 | * through the repository: Edit in Code opens the file there. | |
| 33 | */ | |
| 34 | export default function RepoDocFile({ loaderData, params }: Route.ComponentProps) { | |
| 35 | const slug = params.owner.toLowerCase(); | |
| 36 | const { space, file } = loaderData.found; | |
| 37 | const navigate = useNavigate(); | |
| 38 | const { send, error } = useDocsAction(slug); | |
| 39 | const [namespace, name] = space.repo.split("/") as [string, string]; | |
| 40 | const folder = file.path.includes("/") ? file.path.slice(0, file.path.lastIndexOf("/")) : ""; | |
| 41 | const encode = (path: string) => path.split("/").map(encodeURIComponent).join("/"); | |
| 42 | return ( | |
| 43 | <div className="-mt-6"> | |
| 44 | <div className="sticky top-(--topbar-h) z-20 -mx-4 flex h-12 items-center gap-3 border-b border-line bg-bg/85 px-4 backdrop-blur sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"> | |
| 45 | <div className="min-w-0 grow"> | |
| 46 | <Crumbs items={[{ label: "Docs", to: `/${slug}/-/docs` }, { label: space.repo }, ...file.path.split("/").map((part) => ({ label: part }))]} /> | |
| 47 | </div> | |
| 48 | <Link to={file.code_href} className="inline-flex h-8 items-center gap-1.5 rounded-md border border-line px-2.5 text-xs text-fg/85 hover:bg-raised"> | |
| 49 | <PencilLine size={13} /> Edit in Code | |
| 50 | </Link> | |
| 51 | {space.can_remove && ( | |
| 52 | <Hint label={`Stop showing ${space.repo}'s docs in Docs`}> | |
| 53 | <button | |
| 54 | type="button" | |
| 55 | aria-label={`Stop showing ${space.repo}'s docs`} | |
| 56 | onClick={async () => { | |
| 57 | const done = await send("remove_repo_space", { id: space.id }); | |
| 58 | if (done.ok) navigate(`/${slug}/-/docs`); | |
| 59 | }} | |
| 60 | className="flex size-8 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-danger" | |
| 61 | > | |
| 62 | <Trash2 size={15} /> | |
| 63 | </button> | |
| 64 | </Hint> | |
| 65 | )} | |
| 66 | </div> | |
| 67 | <article className="mx-auto max-w-3xl pt-10"> | |
| 68 | <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-faint"> | |
| 69 | <span className="inline-flex items-center gap-1.5"> | |
| 70 | <FolderGit2 size={13} /> {space.repo} · {space.default_branch} | |
| 71 | </span> | |
| 72 | <span className="font-mono">{file.path}</span> | |
| 73 | {space.indexed_at && ( | |
| 74 | <span> | |
| 75 | Read <TimeAgo at={space.indexed_at} /> | |
| 76 | </span> | |
| 77 | )} | |
| 78 | </p> | |
| 79 | <p className="mt-3 flex items-start gap-2 rounded-lg border border-line bg-surface px-3 py-2 text-xs leading-relaxed text-muted"> | |
| 80 | <GitPullRequestArrow size={14} className="mt-px shrink-0 text-faint" aria-hidden="true" /> | |
| 81 | This page lives in the repository and changes through pull requests. Edit it in Code, or ask an agent to open a pull request for it. | |
| 82 | </p> | |
| 83 | {error && ( | |
| 84 | <div className="mt-3"> | |
| 85 | <ErrorText>{error}</ErrorText> | |
| 86 | </div> | |
| 87 | )} | |
| 88 | <div className="docs-read mt-6"> | |
| 89 | <Markdown | |
| 90 | source={file.markdown} | |
| 91 | repo={{ namespace, name }} | |
| 92 | // Relative links point into the repository at its default | |
| 93 | // branch, and pictures at its files there. | |
| 94 | base={`/${space.repo}/blob/${encodeURIComponent(space.default_branch)}${folder ? `/${encode(folder)}` : ""}`} | |
| 95 | rawBase={`/${space.repo}/raw/${encodeURIComponent(space.commit ?? space.default_branch)}${folder ? `/${encode(folder)}` : ""}`} | |
| 96 | /> | |
| 97 | </div> | |
| 98 | </article> | |
| 99 | </div> | |
| 100 | ); | |
| 101 | } |