1// Pathless layout for the code browser. Reads preloaded refs from the
2// $repo context and renders the shared header (breadcrumb + ref selector
3// + history toggle). Child routes (tree, blob, commits) render in Outlet.
4
5import { useReadQuery } from "@apollo/client/react";
6import {
7 createFileRoute,
8 Outlet,
9 useNavigate,
10 useParams,
11 useRouterState,
12} from "@tanstack/react-router";
13import { GitCommit } from "lucide-react";
14
15import type { GitRef } from "@/__generated__/graphql";
16import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb";
17import { RefSelector } from "@/components/code/RefSelector";
18import { ButtonLink } from "@/components/ui/button-link";
19import { Skeleton } from "@/components/ui/skeleton";
20
21export type CodeViewMode = "tree" | "blob" | "commits";
22
23export const Route = createFileRoute("/$repo/_code")({
24 component: CodeLayout,
25 pendingComponent: CodeLayoutSkeleton,
26});
27
28function CodeLayout() {
29 const { repo } = Route.useParams();
30 const { ref: repoRef, refsRef } = Route.useRouteContext();
31 const { data: refsData } = useReadQuery(refsRef);
32 const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? [];
33 const repoName = refsData?.repository?.name ?? repoRef ?? "default-repo";
34
35 // Read child route params (ref and splat path)
36 const allParams = useParams({ strict: false }) as {
37 ref?: string;
38 _splat?: string;
39 };
40 const currentRef = allParams.ref ?? "";
41 const currentPath = allParams._splat ?? "";
42
43 // Read viewMode from the deepest child route's context.
44 // Each child (tree, blob, commits) sets viewMode in its beforeLoad.
45 const viewMode: CodeViewMode = useRouterState({
46 select: (s) => {
47 const ctx = s.matches.at(-1)?.context;
48 if (ctx && typeof ctx === "object" && "viewMode" in ctx) {
49 return ctx.viewMode as CodeViewMode;
50 }
51 return "tree";
52 },
53 });
54
55 const navigate = useNavigate();
56
57 function handleRefSelect(newRef: GitRef) {
58 const refName = newRef.shortName;
59 if (viewMode === "commits") {
60 void navigate({ to: "/$repo/commits/$ref", params: { repo, ref: refName } });
61 } else if (viewMode === "blob") {
62 void navigate({
63 to: "/$repo/blob/$ref/$",
64 params: { repo, ref: refName, _splat: currentPath },
65 });
66 } else {
67 void navigate({
68 to: "/$repo/tree/$ref/$",
69 params: { repo, ref: refName, _splat: currentPath },
70 });
71 }
72 }
73
74 return (
75 <div className="space-y-4">
76 <div className="flex flex-wrap items-center justify-between gap-3">
77 <CodeBreadcrumb
78 repoName={repoName}
79 currentRef={currentRef}
80 path={currentPath}
81 repo={repo}
82 />
83 <div className="flex items-center gap-2">
84 {viewMode === "commits" ? (
85 <ButtonLink
86 to="/$repo/tree/$ref/$"
87 params={{ repo, ref: currentRef, _splat: currentPath }}
88 variant="secondary"
89 size="sm"
90 >
91 <GitCommit className="size-3.5" />
92 History
93 </ButtonLink>
94 ) : (
95 <ButtonLink
96 to="/$repo/commits/$ref"
97 params={{ repo, ref: currentRef }}
98 variant="outline"
99 size="sm"
100 >
101 <GitCommit className="size-3.5" />
102 History
103 </ButtonLink>
104 )}
105 <RefSelector refs={refs} currentRef={currentRef} onSelect={handleRefSelect} />
106 </div>
107 </div>
108
109 <Outlet />
110 </div>
111 );
112}
113
114function CodeLayoutSkeleton() {
115 return (
116 <div className="space-y-4">
117 <div className="flex items-center justify-between">
118 <Skeleton className="h-5 w-48" />
119 <Skeleton className="h-8 w-28" />
120 </div>
121 <div className="divide-border border-border divide-y rounded-md border">
122 {Array.from({ length: 8 }).map((_, i) => (
123 <div key={i} className="flex items-center gap-3 px-4 py-2">
124 <Skeleton className="size-4 rounded-sm" />
125 <Skeleton className="h-4 w-32" />
126 </div>
127 ))}
128 </div>
129 </div>
130 );
131}