pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/lib/project-nav.ts
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 owner | 1 | /** |
| 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 look | 7 | import type { Abilities, Capability } from "@g1t/contracts"; |
| 8 | ||
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 9 | import { type RoadmapItem, roadmapIn } from "./roadmap"; |
| 10 | ||
| 11 | export 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 look | 21 | /** 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 owner | 23 | }; |
| 24 | ||
| 25 | export type Section = { | |
| 26 | key: RoadmapItem["section"] | "Workflows" | "Settings"; | |
| 27 | tabs: Tab[]; | |
| 28 | }; | |
| 29 | ||
| 30 | function 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 | ||
| 34 | export const SECTIONS: Section[] = [ | |
| 35 | { | |
| 36 | key: "Code", | |
| 37 | tabs: [ | |
| 38 | { label: "Files", path: "code", also: ["tree", "blob"] }, | |
| 39 | { label: "Commits", path: "commits", also: ["commit"] }, | |
| 40 | ...soon("Code"), | |
| 41 | ], | |
| 42 | }, | |
| 43 | { | |
| 44 | key: "Issues", | |
| 45 | tabs: [ | |
| 46 | { label: "Issues", path: "issues" }, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 47 | { label: "Outcomes", path: "plans" }, |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 48 | ...soon("Issues"), |
| 49 | ], | |
| 50 | }, | |
| 51 | { | |
| 52 | key: "Agents", | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 53 | tabs: [ |
| 54 | { label: "At work", path: "agents" }, | |
| 55 | { label: "Sessions", path: "sessions" }, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 56 | { label: "Memory", path: "memory" }, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 57 | ...soon("Agents"), |
| 58 | ], | |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 59 | }, |
| 60 | { | |
| 61 | key: "Deployments", | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 62 | tabs: [{ label: "Deployments", path: "deployments" }, ...soon("Deployments")], |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 63 | }, |
| Observability gets its own page; tabs never overflow | 64 | { key: "Observability", tabs: soon("Observability") }, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 65 | { |
| 66 | key: "Security", | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 67 | 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 API | 68 | }, |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 69 | { key: "Insights", tabs: soon("Insights") }, |
| 70 | ]; | |
| 71 | ||
| 72 | /** Pull requests and the merge queue share a page's tabs too. */ | |
| 73 | export const PULLS: Section = { | |
| 74 | key: "Code", | |
| 75 | tabs: [ | |
| 76 | { label: "Pull requests", path: "pulls", also: ["pull"] }, | |
| 77 | { label: "Merge queue", path: "queue" }, | |
| 78 | ], | |
| 79 | }; | |
| 80 | ||
| 81 | function matches(rest: string, tab: Tab): boolean { | |
| 82 | return [tab.path, ...(tab.also ?? [])].some((path) => path !== "" && (rest === path || rest.startsWith(`${path}/`))); | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * The tabs for the page at `rest` (the path under the project), if it | |
| 87 | * belongs to a page with more than one view. | |
| 88 | */ | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 89 | export 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 owner | 90 | for (const section of [...SECTIONS, PULLS]) { |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 91 | const visible = section.tabs.filter( |
| 92 | (tab) => (member || !tab.members) && (!tab.needs || !can || Boolean(can[tab.needs])), | |
| 93 | ); | |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 94 | if (visible.length > 1 && visible.some((tab) => matches(rest, tab))) return visible; |
| 95 | } | |
| 96 | return null; | |
| 97 | } | |
| 98 | ||
| 99 | /** The first page of a section, for its sidebar link: a built one if any. */ | |
| 100 | export function entryOf(key: Section["key"]): Tab | undefined { | |
| 101 | return SECTIONS.find((section) => section.key === key)?.tabs[0]; | |
| 102 | } |