Skip to content
256 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 { Mail, ShieldCheck } from "lucide-react";
2import { Form } from "react-router";
3
4import { type AccountEmails, type SecurityEvent, securityEventLabel } from "@g1t/contracts";
5
6import { addressActions, backupChoices } from "../lib/emails";
7import type { EmailActionData } from "../lib/emails.server";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.8import { ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
9import { Badge } from "./ui/badge";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project10import { SelectField } from "./ui/select";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11import { SwitchCard } from "./ui/switch";
12
13/** One small form posting one intent about one address. */
Merge branch 'worktree-agent-a985c01f43538af01'14function AddressButton({ intent, email, label, pending }: { intent: string; email: string; label: string; pending: string }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15 return (
16 <Form method="post">
17 <input type="hidden" name="intent" value={intent} />
18 <input type="hidden" name="email" value={email} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.19 <SubmitButton variant="outline" pending={pending} match={{ intent, email }}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look20 {label}
Merge branch 'worktree-agent-a985c01f43538af01'21 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look22 </Form>
23 );
24}
25
26/** What a waiting change does, for the line that asks for the password. */
27function describe(intent: string): string {
28 switch (intent) {
29 case "add-email":
30 return "adding";
31 case "remove-email":
32 return "removing";
33 case "primary-email":
34 return "making primary";
35 default:
36 return "for";
37 }
38}
39
40/** Asks for the password before a sensitive change goes through. */
Merge main (membership, two-factor, GitHub repo roles) into tokens41export function ConfirmItIsYou({ pending, hasPassword }: { pending: NonNullable<EmailActionData>["reauth"]; hasPassword: boolean }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42 if (!pending) return null;
43 return (
44 <div role="alert" className="rounded-xl border border-accent/40 bg-surface p-4">
45 <p className="flex items-center gap-2 text-sm font-medium">
46 <ShieldCheck size={16} className="text-accent" /> Confirm it is you
47 </p>
48 {hasPassword ? (
49 <Form method="post" className="mt-3 space-y-3">
50 <p className="text-sm text-muted">
51 {pending.message}
52 {pending.fields.email ? (
53 <>
54 {" "}
55 ({describe(pending.intent)} <span className="text-fg">{pending.fields.email}</span>)
56 </>
57 ) : null}
58 </p>
59 <input type="hidden" name="intent" value={pending.intent} />
60 {Object.entries(pending.fields).map(([name, value]) => (
61 <input key={name} type="hidden" name={name} value={value} />
62 ))}
63 <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
64 <div className="grow">
65 <Field label="Password">
66 <Input name="password" type="password" autoComplete="current-password" required autoFocus />
67 </Field>
68 </div>
Merge branch 'worktree-agent-a985c01f43538af01'69 <SubmitButton pending="Confirming…" match={{ intent: pending.intent }}>
70 Confirm
71 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72 </div>
73 <p className="text-xs text-faint">You will not be asked again for 10 minutes.</p>
74 </Form>
75 ) : (
76 <p className="mt-2 text-sm text-muted">
77 Your account signs in with GitHub only. Sign out, sign in with GitHub again, and make this change within 10
78 minutes. Or set a password with Forgot your password on the sign-in page.
79 </p>
80 )}
81 </div>
82 );
83}
84
Merge branch 'worktree-agent-a985c01f43538af01'85/** The answer to one form's post, under that form: what went wrong, or that it worked. */
86function Answer({ actionData }: { actionData: EmailActionData | undefined }) {
87 return (
88 <>
89 <ErrorText>{actionData?.emailError}</ErrorText>
90 {actionData?.emailNotice && (
91 <p role="status" className="mt-2 text-sm text-muted">
92 {actionData.emailNotice}
93 </p>
94 )}
95 </>
96 );
97}
98
99/**
100 * A person's email addresses, in their account settings. `intent` is the
101 * form that last posted, so its answer shows beside it.
102 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look103export function EmailsSection({
104 data,
105 actionData,
Merge branch 'worktree-agent-a985c01f43538af01'106 intent,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look107 hasPassword,
108}: {
109 data: AccountEmails | null;
110 actionData: EmailActionData | undefined;
Merge branch 'worktree-agent-a985c01f43538af01'111 intent?: string;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look112 hasPassword: boolean;
113}) {
114 if (!data) return null;
Merge branch 'worktree-agent-a985c01f43538af01'115 const answerFor = (one: string) => (intent === one ? actionData : undefined);
116 const elsewhere = intent === "backup-email" || intent === "email-privacy";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look117 const backups = backupChoices(data.emails);
118 const backup = data.emails.find((email) => email.backup);
119 const full = data.emails.length >= data.limit;
120 return (
121 <section id="emails" className="scroll-mt-20">
122 {actionData?.reauth && (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas123 <div className="mb-4">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look124 <ConfirmItIsYou pending={actionData.reauth} hasPassword={hasPassword} />
125 </div>
126 )}
127
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas128 <ul className="divide-y divide-line rounded-md border border-line">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 {data.emails.map((email) => {
130 const can = addressActions(email, data.emails);
131 return (
132 <li key={email.email} className="flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center">
133 <div className="flex min-w-0 grow items-start gap-3">
134 <Mail size={16} className="mt-0.5 shrink-0 text-faint" />
135 <div className="min-w-0">
136 <p className="truncate text-sm">{email.email}</p>
137 <p className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-faint">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.138 {email.primary && <Badge size="md">Primary</Badge>}
139 {email.backup && <Badge size="md">Backup</Badge>}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look140 {email.verified ? (
141 <span>
142 Confirmed {email.verifiedAt ? <TimeAgo at={email.verifiedAt} /> : null}
143 </span>
144 ) : (
145 <span className="text-warn">Unconfirmed: check your inbox for the link</span>
146 )}
147 </p>
148 </div>
149 </div>
150 <div className="flex shrink-0 flex-wrap gap-2 sm:justify-end">
Merge branch 'worktree-agent-a985c01f43538af01'151 {can.resend && <AddressButton intent="resend-email" email={email.email} label="Resend link" pending="Sending…" />}
152 {can.makePrimary && <AddressButton intent="primary-email" email={email.email} label="Make primary" pending="Saving…" />}
153 {can.remove && <AddressButton intent="remove-email" email={email.email} label="Remove" pending="Removing…" />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look154 </div>
155 </li>
156 );
157 })}
158 </ul>
159
Merge branch 'worktree-agent-a985c01f43538af01'160 {/* Keyed on the list, so an address once added leaves the field empty. */}
161 <Form key={data.emails.length} method="post" className="mt-4">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look162 <input type="hidden" name="intent" value="add-email" />
163 <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
164 <div className="grow">
165 <Field label="Add an email address">
166 <Input name="email" type="email" autoComplete="email" placeholder="you@example.com" required disabled={full} />
167 </Field>
168 </div>
Merge branch 'worktree-agent-a985c01f43538af01'169 <SubmitButton pending="Adding…" match={{ intent: "add-email" }} disabled={full}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look170 Add
Merge branch 'worktree-agent-a985c01f43538af01'171 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look172 </div>
173 <p className="mt-1.5 text-xs text-faint">
174 {full ? `You have ${data.limit} addresses, the most an account can have.` : "g1t sends it a link to confirm it."}
175 </p>
176 </Form>
Merge branch 'worktree-agent-a985c01f43538af01'177 <Answer actionData={elsewhere ? undefined : actionData} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look178
179 <div className="mt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas180 <h2 className="font-medium">Backup address</h2>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look181 <p className="mt-1 text-sm text-muted">
182 Security notices, such as a new address or a changed password, go to your primary and to this address.
183 </p>
184 <Form method="post" className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-end">
185 <input type="hidden" name="intent" value="backup-email" />
Chat controls, public profiles, shadcn selects, and no Docs tab in a project186 <div className="block grow">
187 <SelectField
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look188 name="backup"
Chat controls, public profiles, shadcn selects, and no Docs tab in a project189 aria-label="Backup address"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look190 defaultValue={backup?.email ?? ""}
191 disabled={backups.length === 0}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project192 className="h-auto py-2"
193 options={[{ value: "", label: "Primary address only" }, ...backups.map((email) => ({ value: email.email, label: email.email }))]}
194 />
195 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.196 <SubmitButton variant="outline" pending="Saving…" match={{ intent: "backup-email" }} disabled={backups.length === 0}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look197 Save
Merge branch 'worktree-agent-a985c01f43538af01'198 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look199 </Form>
Merge branch 'worktree-agent-a985c01f43538af01'200 <Answer actionData={answerFor("backup-email")} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look201 {backups.length === 0 && (
202 <p className="mt-2 text-xs text-faint">Add and confirm a second address to choose a backup.</p>
203 )}
204 </div>
205
206 <div className="mt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas207 <h2 className="font-medium">Privacy</h2>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208 <Form method="post" className="mt-3 space-y-3">
209 <input type="hidden" name="intent" value="email-privacy" />
210 <SwitchCard name="private" defaultChecked={data.privateEmail} title="Keep my email address private">
211 Merges and other commits g1t makes for you on the web, or an agent makes for you, use{" "}
212 <span className="font-mono text-fg [overflow-wrap:anywhere]">{data.noreply}</span> instead of your primary address.
213 </SwitchCard>
214 <SwitchCard name="block" defaultChecked={data.blockPrivatePushes} title="Block pushes that expose my email">
215 While your address is private, a push is refused if a commit in it has one of your confirmed
216 addresses as its author or committer. Commit with your noreply address instead.
217 </SwitchCard>
218 <div className="flex flex-wrap items-center justify-between gap-3">
219 <p className="text-xs text-faint">
220 Commits g1t makes for you now carry <span className="font-mono [overflow-wrap:anywhere]">{data.commitEmail}</span>.
221 </p>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.222 <SubmitButton variant="outline" pending="Saving…" match={{ intent: "email-privacy" }}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look223 Save
Merge branch 'worktree-agent-a985c01f43538af01'224 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look225 </div>
226 </Form>
Merge branch 'worktree-agent-a985c01f43538af01'227 <Answer actionData={answerFor("email-privacy")} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look228 </div>
229 </section>
230 );
231}
232
233/** What happened to the account's security, newest first. */
234export function SecurityLogSection({ log }: { log: SecurityEvent[] }) {
235 return (
236 <section id="security-log" className="scroll-mt-20">
237 {log.length === 0 ? (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas238 <p className="text-sm text-faint">Nothing yet.</p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look239 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas240 <ul className="divide-y divide-line rounded-md border border-line">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look241 {log.map((event, at) => (
242 <li key={`${event.createdAt}:${at}`} className="flex flex-col gap-1 px-4 py-3 sm:flex-row sm:items-center sm:gap-4">
243 <p className="min-w-0 grow truncate text-sm">
244 {securityEventLabel(event)}
245 {event.byStaff && <span className="text-muted"> · by g1t staff{event.reason ? `: ${event.reason}` : ""}</span>}
246 </p>
247 <p className="shrink-0 text-xs text-faint">
248 <TimeAgo at={event.createdAt} />
249 </p>
250 </li>
251 ))}
252 </ul>
253 )}
254 </section>
255 );
256}

This file's history is long; its oldest lines are credited to the oldest commit read.