Skip to content
209 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.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { Command as Cmdk } from "cmdk";
2import { Code2, CornerDownLeft, Search } from "lucide-react";
3import { Dialog as Primitive } from "radix-ui";
4import { useEffect, useState } from "react";
5import { useFetcher, useNavigate } from "react-router";
6
7import type { SiteHit } from "@g1t/contracts";
8
9import { searchHref } from "../lib/search";
10import { hitIcon } from "./search";
11import type { PaletteCommand } from "./command-palette";
12import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "./ui/combobox";
Fast pages, required checks on the branch, self-hosted runners, honest incidents13import { SkeletonRows } from "./ui/skeleton";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14
15/** How long typing has to pause before results are asked for. */
16const PAUSE_MS = 140;
17
18function hitHint(hit: SiteHit): string {
19 switch (hit.kind) {
20 case "repository":
21 return "Repository";
22 case "issue":
23 return `${hit.repo} #${hit.number}`;
24 case "pull":
25 return `${hit.repo} #${hit.number}`;
26 case "user":
27 return `Person · ${hit.slug}`;
28 case "workspace":
29 return `Workspace · ${hit.slug}`;
30 default:
31 return hit.repo ?? "";
32 }
33}
34
35/**
36 * ⌘K: pages and actions from what the page already knows, and, as someone
37 * types, repositories, issues, pull requests and people from search,
38 * public ones for anyone and private ones for their workspaces' members.
39 * The first row always searches all of g1t for what was typed.
40 */
41export default function CommandPaletteDialog({
42 open,
43 onOpenChange,
44 commands,
45 repo,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit46 onCloseAutoFocus,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47}: {
48 open: boolean;
49 onOpenChange: (open: boolean) => void;
50 commands: PaletteCommand[];
51 /** The repository being looked at, `owner/name`, to offer searching its code. */
52 repo?: string | null;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit53 /** Where focus goes once it closes (command-palette.tsx). */
54 onCloseAutoFocus?: (event: Event) => void;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look55}) {
56 const navigate = useNavigate();
57 const fetcher = useFetcher<{ q: string; hits: SiteHit[] }>();
58 const [query, setQuery] = useState("");
59 const trimmed = query.trim();
60
61 useEffect(() => {
62 if (open) setQuery("");
63 }, [open]);
64 useEffect(() => {
65 if (trimmed.length < 2) return;
66 const timer = setTimeout(() => fetcher.load(`/search.json?q=${encodeURIComponent(trimmed)}`), PAUSE_MS);
67 return () => clearTimeout(timer);
68 // `fetcher` changes identity as it loads; only the text should restart it.
69 // eslint-disable-next-line react-hooks/exhaustive-deps
70 }, [trimmed]);
71
72 const hits = trimmed.length >= 2 && fetcher.data?.q === trimmed ? fetcher.data.hits : [];
73 const go = (to: string) => {
74 onOpenChange(false);
75 // The docs and other sites are full page loads.
76 if (/^https?:\/\//.test(to)) window.location.assign(to);
77 else navigate(to);
78 };
79 const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean);
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.80 const matching = commands.filter((command) => {
81 const text = `${command.label} ${command.hint ?? ""}`.toLowerCase();
82 return words.every((word) => text.includes(word));
83 });
84 const pages = matching.filter((command) => command.to != null).slice(0, trimmed ? 8 : 12);
85 // Things done in place, such as switching the theme, once something is typed.
86 const actions = trimmed ? matching.filter((command) => command.run != null).slice(0, 6) : [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look87 const itemClass = "gap-3 rounded-lg px-3 py-2";
88
89 return (
90 <Primitive.Root open={open} onOpenChange={onOpenChange}>
91 <Primitive.Portal>
92 <Primitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out motion-reduce:animate-none" />
93 <Primitive.Content
94 aria-describedby={undefined}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit95 onCloseAutoFocus={onCloseAutoFocus}
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar96 className="fixed top-[12vh] left-1/2 z-50 w-[calc(100vw-2rem)] max-sm:top-[calc(env(safe-area-inset-top)+0.5rem)] max-sm:w-[calc(100vw-1rem)] max-w-xl -translate-x-1/2 overflow-hidden rounded-xl border border-line-strong bg-raised shadow-2xl shadow-black/60 outline-none"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 >
98 <Primitive.Title className="sr-only">Go to or search</Primitive.Title>
99 {/* Filtering is done here: search already filtered its results. */}
100 <Command shouldFilter={false} loop>
101 <div className="flex items-center gap-3 border-b border-line px-4">
102 <Search size={16} className="shrink-0 text-faint" />
103 <Cmdk.Input
104 value={query}
105 onValueChange={setQuery}
106 placeholder="Search g1t, or go to a page…"
107 autoComplete="off"
108 data-1p-ignore
109 className="h-12 grow border-0 bg-transparent text-sm shadow-none outline-none placeholder:text-faint focus-visible:outline-none"
110 />
111 <kbd className="rounded border border-line px-1.5 font-mono text-[0.6875rem] text-faint">esc</kbd>
112 </div>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar113 <CommandList className="max-h-[55vh] p-1.5 max-sm:max-h-[calc(var(--vv-height,100dvh)-env(safe-area-inset-top)-5.5rem)]">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 <CommandEmpty>Nothing matches.</CommandEmpty>
115 {trimmed && (
116 <CommandGroup heading="Search">
117 <CommandItem value="search:all" onSelect={() => go(searchHref(trimmed))} className={itemClass}>
118 <Search />
119 <span className="min-w-0 grow truncate">
120 Search g1t for <span className="font-medium text-fg">“{trimmed}”</span>
121 </span>
122 <CornerDownLeft className="size-3.5! text-faint!" />
123 </CommandItem>
124 <CommandItem value="search:code" onSelect={() => go(searchHref(trimmed, "code"))} className={itemClass}>
125 <Code2 />
126 <span className="min-w-0 grow truncate">
127 Search code for <span className="font-medium text-fg">“{trimmed}”</span>
128 </span>
129 </CommandItem>
130 {repo && (
131 <CommandItem
132 value="search:repo"
133 onSelect={() => go(searchHref(`${trimmed} repo:${repo}`, "code"))}
134 className={itemClass}
135 >
136 <Code2 />
137 <span className="min-w-0 grow truncate">
138 Search <span className="font-mono">{repo}</span> for{" "}
139 <span className="font-medium text-fg">“{trimmed}”</span>
140 </span>
141 </CommandItem>
142 )}
143 </CommandGroup>
144 )}
145 {hits.length > 0 && (
146 <CommandGroup heading="Results">
147 {hits.map((hit) => (
148 <CommandItem
149 key={`${hit.kind}:${hit.url}`}
150 value={`hit:${hit.kind}:${hit.url}`}
151 onSelect={() => go(hit.url)}
152 className={itemClass}
153 >
154 {hitIcon(hit)}
155 <span className="min-w-0 grow truncate">
156 {hit.kind === "repository" ? <span className="font-mono">{hit.title}</span> : hit.title}
157 </span>
158 <span className="shrink-0 truncate text-xs text-faint">{hitHint(hit)}</span>
159 </CommandItem>
160 ))}
161 </CommandGroup>
162 )}
163 {pages.length > 0 && (
164 <CommandGroup heading="Go to">
165 {pages.map((command) => (
166 <CommandItem
167 key={`${command.to}-${command.label}`}
168 value={`page:${command.to}:${command.label}`}
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.169 onSelect={() => go(command.to!)}
170 className={itemClass}
171 >
172 <span className="shrink-0 text-faint">{command.icon}</span>
173 <span className="min-w-0 grow truncate">{command.label}</span>
174 {command.hint && <span className="shrink-0 truncate text-xs text-faint">{command.hint}</span>}
175 </CommandItem>
176 ))}
177 </CommandGroup>
178 )}
179 {actions.length > 0 && (
180 <CommandGroup heading="Commands">
181 {actions.map((command) => (
182 <CommandItem
183 key={`run-${command.label}`}
184 value={`run:${command.label}`}
185 onSelect={() => {
186 onOpenChange(false);
187 command.run!();
188 }}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look189 className={itemClass}
190 >
191 <span className="shrink-0 text-faint">{command.icon}</span>
192 <span className="min-w-0 grow truncate">{command.label}</span>
193 {command.hint && <span className="shrink-0 truncate text-xs text-faint">{command.hint}</span>}
194 </CommandItem>
195 ))}
196 </CommandGroup>
197 )}
198 </CommandList>
199 </Command>
200 {hits.length === 0 && fetcher.state === "loading" && trimmed.length >= 2 && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents201 <div aria-busy="true" className="border-t border-line">
202 <SkeletonRows rows={3} rowClassName="h-9 px-4" />
203 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 )}
205 </Primitive.Content>
206 </Primitive.Portal>
207 </Primitive.Root>
208 );
209}