g1t/apps/web/app/lib/project-nav.ts

105 lines3,317 bytesCodeBlame

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.

A flat project menu with Code first, views as tabs, and Stripe for every owner1/**
2 * A project's pages and the tabs within them, in one place: the sidebar
3 * lists the pages, and a page with more than one view shows them as tabs
4 * across its top, the way GitHub and Vercel do. Soon tabs come from the
5 * roadmap and open its page for them, under the same tabs.
6 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look7import type { Abilities, Capability } from "@g1t/contracts";
8
A flat project menu with Code first, views as tabs, and Stripe for every owner9import { type RoadmapItem, roadmapIn } from "./roadmap";
10
11export type Tab = {
12 label: string;
13 /** Under the project, without a leading slash; "" is its overview. */
14 path: string;
15 /** Other paths under which this tab is the current one. */
16 also?: string[];
17 soon?: boolean;
18 about?: string;
19 /** Members only. */
20 members?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21 /** Only for viewers whose role has this capability, once it is known. */
22 needs?: Capability;
A flat project menu with Code first, views as tabs, and Stripe for every owner23};
24
25export type Section = {
26 key: RoadmapItem["section"] | "Workflows" | "Settings";
27 tabs: Tab[];
28};
29
30function soon(section: RoadmapItem["section"]): Tab[] {
31 return roadmapIn(section).map((item) => ({ label: item.title, path: `soon/${item.key}`, soon: true, about: item.summary }));
32}
33
34export const SECTIONS: Section[] = [
35 {
36 key: "Code",
37 tabs: [
38 { label: "Files", path: "code", also: ["tree", "blob"] },
39 { label: "Commits", path: "commits", also: ["commit"] },
Branches and Tags pages, each file's last commit, and the branch menu on files40 { label: "Branches", path: "branches" },
41 { label: "Tags", path: "tags" },
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request42 { label: "Compare", path: "compare" },
A flat project menu with Code first, views as tabs, and Stripe for every owner43 ...soon("Code"),
44 ],
45 },
46 {
47 key: "Issues",
48 tabs: [
49 { label: "Issues", path: "issues" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50 { label: "Outcomes", path: "plans" },
A flat project menu with Code first, views as tabs, and Stripe for every owner51 ...soon("Issues"),
52 ],
53 },
54 {
55 key: "Agents",
Agents and memory, checks and conflicts, profiles, slug renames, custom domains56 tabs: [
57 { label: "At work", path: "agents" },
58 { label: "Sessions", path: "sessions" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59 { label: "Memory", path: "memory" },
Agents and memory, checks and conflicts, profiles, slug renames, custom domains60 ...soon("Agents"),
61 ],
A flat project menu with Code first, views as tabs, and Stripe for every owner62 },
63 {
64 key: "Deployments",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look65 tabs: [{ label: "Deployments", path: "deployments" }, ...soon("Deployments")],
A flat project menu with Code first, views as tabs, and Stripe for every owner66 },
Observability gets its own page; tabs never overflow67 { key: "Observability", tabs: soon("Observability") },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API68 {
69 key: "Security",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look70 tabs: [{ label: "Overview", path: "security", members: true, needs: "push" }, ...soon("Security")],
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API71 },
A flat project menu with Code first, views as tabs, and Stripe for every owner72 { key: "Insights", tabs: soon("Insights") },
73];
74
75/** Pull requests and the merge queue share a page's tabs too. */
76export const PULLS: Section = {
77 key: "Code",
78 tabs: [
79 { label: "Pull requests", path: "pulls", also: ["pull"] },
80 { label: "Merge queue", path: "queue" },
81 ],
82};
83
84function matches(rest: string, tab: Tab): boolean {
85 return [tab.path, ...(tab.also ?? [])].some((path) => path !== "" && (rest === path || rest.startsWith(`${path}/`)));
86}
87
88/**
89 * The tabs for the page at `rest` (the path under the project), if it
90 * belongs to a page with more than one view.
91 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look92export function tabsFor(rest: string, member: boolean, can?: Partial<Abilities>): Tab[] | null {
A flat project menu with Code first, views as tabs, and Stripe for every owner93 for (const section of [...SECTIONS, PULLS]) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look94 const visible = section.tabs.filter(
95 (tab) => (member || !tab.members) && (!tab.needs || !can || Boolean(can[tab.needs])),
96 );
A flat project menu with Code first, views as tabs, and Stripe for every owner97 if (visible.length > 1 && visible.some((tab) => matches(rest, tab))) return visible;
98 }
99 return null;
100}
101
102/** The first page of a section, for its sidebar link: a built one if any. */
103export function entryOf(key: Section["key"]): Tab | undefined {
104 return SECTIONS.find((section) => section.key === key)?.tabs[0];
105}