Skip to content
193 linesCodeBlameRaw

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.

g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.1/**
2 * The public frame's header (lib/chrome.ts, `public`): for a visitor who is
3 * not signed in, on every page that is not about signing in. The logo,
4 * search, Explore and Docs, and signing in or up; on a phone the links fold
5 * into one menu. Someone still confirming their address gets their
6 * account's menu in place of signing in.
7 */
8import { BookOpen, ChevronDown, Compass, CreditCard, LogIn, LogOut, Menu, Plus, Search, Settings } from "lucide-react";
9import { useState } from "react";
10import { Form, Link, NavLink, useParams, useSubmit } from "react-router";
11
12import type { User } from "@g1t/contracts";
13
14import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
15import { Logo } from "./logo";
16import { Avatar, ButtonLink, notACredential } from "./ui";
17import {
18 DropdownMenu,
19 DropdownMenuContent,
20 DropdownMenuItem,
21 DropdownMenuLabel,
22 DropdownMenuSeparator,
23 DropdownMenuTrigger,
24} from "./ui/dropdown-menu";
25import { useSignUpCopy } from "../lib/registration";
26
27function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
28 return (
29 <NavLink
30 to={to}
31 className={({ isActive }) =>
32 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
33 isActive ? "text-fg" : "text-muted"
34 }`
35 }
36 >
37 {children}
38 </NavLink>
39 );
40}
41
42/** What the palette offers someone without the app's sidebar. */
43const PUBLIC_COMMANDS: PaletteCommand[] = [
44 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
45 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
46 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
47 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
48 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
49 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
50];
51
52/** Sign up: the sign-up page says whether it takes an invite. */
53function SignUpButton() {
54 const copy = useSignUpCopy();
55 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
56}
57
58export function PublicHeader({ user }: { user: User | null | undefined }) {
59 const submit = useSubmit();
60 const signUp = useSignUpCopy();
61 const [palette, setPalette] = useState(false);
62 usePaletteShortcut(() => setPalette((open) => !open));
63 const params = useParams();
64 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
65 return (
66 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
67 <div className="mx-auto flex h-14 max-w-6xl items-center gap-2 px-4">
68 <Link to="/" aria-label="g1t home" className="mr-2 flex">
69 <Logo />
70 </Link>
71 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
72 <Search
73 size={14}
74 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
75 />
76 <input
77 name="q"
78 {...notACredential()}
79 placeholder="Search g1t"
80 aria-label="Search g1t"
81 className="w-full rounded-md border border-line bg-bg py-1.5 pr-12 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
82 />
83 <PaletteKey className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" />
84 </Form>
85 <button
86 type="button"
87 aria-label="Search g1t"
88 onClick={() => setPalette(true)}
89 className="flex size-10 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg sm:hidden"
90 >
91 <Search size={18} />
92 </button>
93 <CommandPalette
94 open={palette}
95 onOpenChange={setPalette}
96 commands={user ? PUBLIC_COMMANDS.slice(0, 4) : PUBLIC_COMMANDS.map((command) => (command.to === "/register" ? { ...command, label: signUp.primary } : command))}
97 repo={repo}
98 />
99 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
100 <HeaderLink to="/explore">Explore</HeaderLink>
101 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
102 </nav>
103 <div className="ml-auto flex items-center gap-2">
104 {/* On a phone the links fold into one menu, so the bar fits. */}
105 <DropdownMenu>
106 <DropdownMenuTrigger
107 aria-label="Menu"
108 className="flex size-10 items-center justify-center rounded-md text-muted outline-none transition-colors hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg sm:hidden"
109 >
110 <Menu size={18} />
111 </DropdownMenuTrigger>
112 <DropdownMenuContent align="end" className="w-52">
113 <DropdownMenuItem asChild className="min-h-11">
114 <Link to="/explore">
115 <Compass />
116 Explore
117 </Link>
118 </DropdownMenuItem>
119 <DropdownMenuItem asChild className="min-h-11">
120 <Link to="https://docs.g1t.sh/">
121 <BookOpen />
122 Docs
123 </Link>
124 </DropdownMenuItem>
125 {!user && (
126 <>
127 <DropdownMenuSeparator />
128 <DropdownMenuItem asChild className="min-h-11">
129 <Link to="/login">
130 <LogIn />
131 Sign in
132 </Link>
133 </DropdownMenuItem>
134 </>
135 )}
136 </DropdownMenuContent>
137 </DropdownMenu>
138 {user ? (
139 <>
140 <DropdownMenu>
141 <DropdownMenuTrigger
142 aria-label="Account menu"
143 className="flex items-center gap-1.5 rounded-md p-1 outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised"
144 >
145 <Avatar name={user.username} image={user.avatar} size={24} />
146 <ChevronDown size={14} className="text-faint" />
147 </DropdownMenuTrigger>
148 <DropdownMenuContent align="end" className="w-56">
149 <DropdownMenuLabel>
150 Signed in as{" "}
151 <span className="font-mono font-medium text-fg">
152 {user.username}
153 </span>
154 </DropdownMenuLabel>
155 <DropdownMenuSeparator />
156 <DropdownMenuItem asChild>
157 <Link to="/settings/profile">
158 <Settings />
159 Settings
160 </Link>
161 </DropdownMenuItem>
162 <DropdownMenuItem asChild>
163 <Link to="https://docs.g1t.sh/quickstart/">
164 <BookOpen />
165 Documentation
166 </Link>
167 </DropdownMenuItem>
168 <DropdownMenuSeparator />
169 {/* Submitted from here: the menu closes on select, and a button
170 that has left the page cannot submit a form. */}
171 <DropdownMenuItem
172 onSelect={() => submit(null, { method: "post", action: "/logout" })}
173 >
174 <LogOut />
175 Sign out
176 </DropdownMenuItem>
177 </DropdownMenuContent>
178 </DropdownMenu>
179 </>
180 ) : (
181 <>
182 <span className="hidden sm:contents">
183 <HeaderLink to="/login">Sign in</HeaderLink>
184 </span>
185 <SignUpButton />
186 </>
187 )}
188 </div>
189 </div>
190 </header>
191 );
192}
193