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.
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1 | /** |
| 2 | * The top of a project's overview for what g1t does not deploy: an app | |
| 3 | * deployed elsewhere, an app nobody has said where it runs, docs, and | |
| 4 | * anything else. An app on g1t keeps its production card, and a library | |
| 5 | * or a tool its packages (routes/repo/overview.tsx). | |
| 6 | */ | |
| 7 | import { ArrowUpRight, BookOpen, Box, ChevronDown, GitCommitHorizontal, Globe, Rocket } from "lucide-react"; | |
| 8 | import { type ReactNode, useState } from "react"; | |
| 9 | import { Link, useFetcher } from "react-router"; | |
| 10 | ||
| 11 | import type { DeployStatus, Project } from "@g1t/contracts"; | |
| 12 | ||
| Merge branch 'site-fixes' into fast-push | 13 | import { DeployLink, host, StatusDot } from "./deploy"; |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 14 | import { ButtonLink, Input, SubmitButton, TimeAgo } from "./ui"; |
| 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. | 15 | import { Button } from "./ui/button"; |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 16 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; |
| 17 | import { Hint } from "./ui/hint"; | |
| 18 | ||
| 19 | /** | |
| 20 | * A deployment reported to g1t from outside it, such as by a pipeline that | |
| 21 | * deploys production itself. When there is one for production, the card | |
| 22 | * shows it. | |
| 23 | */ | |
| 24 | export type ExternalDeployment = { | |
| 25 | environment: string; | |
| 26 | url: string | null; | |
| 27 | /** success, failure, error, pending, queued, in_progress or inactive. */ | |
| 28 | state: string; | |
| 29 | sha: string; | |
| 30 | created_at: string; | |
| 31 | }; | |
| 32 | ||
| 33 | /** A reported deployment's state, as g1t's own deployments show theirs. */ | |
| 34 | export function deploymentStatus(state: string): DeployStatus { | |
| 35 | switch (state) { | |
| 36 | case "success": | |
| 37 | return "ready"; | |
| 38 | case "failure": | |
| 39 | case "error": | |
| 40 | return "failed"; | |
| 41 | case "in_progress": | |
| 42 | return "building"; | |
| 43 | case "inactive": | |
| 44 | return "replaced"; | |
| 45 | default: | |
| 46 | return "queued"; | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | type Head = { | |
| 51 | project: Project; | |
| 52 | base: string; | |
| 53 | /** May change the project's settings: what it is, its links. */ | |
| 54 | canChange: boolean; | |
| 55 | /** May turn on Deployments. */ | |
| 56 | canDeploy: boolean; | |
| 57 | }; | |
| 58 | ||
| 59 | export function HeadLabel({ icon, children }: { icon: ReactNode; children: ReactNode }) { | |
| 60 | return ( | |
| 61 | <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase [&_svg]:size-3.25 [&_svg]:text-accent"> | |
| 62 | {icon} | |
| 63 | {children} | |
| 64 | </p> | |
| 65 | ); | |
| 66 | } | |
| 67 | ||
| 68 | function BigLink({ url }: { url: string }) { | |
| 69 | return ( | |
| Merge branch 'site-fixes' into fast-push | 70 | <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent"> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 71 | <span className="truncate">{host(url).replace(/\/$/, "")}</span> |
| 72 | <ArrowUpRight size={16} className="shrink-0 text-faint" /> | |
| Merge branch 'site-fixes' into fast-push | 73 | </DeployLink> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 74 | ); |
| 75 | } | |
| 76 | ||
| 77 | /** An address, saved for the project in place: production's, or its docs'. */ | |
| 78 | function AddressForm({ | |
| 79 | field, | |
| 80 | placeholder, | |
| 81 | label, | |
| 82 | extra, | |
| 83 | }: { | |
| 84 | field: "productionUrl" | "docsUrl"; | |
| 85 | placeholder: string; | |
| 86 | label: string; | |
| 87 | /** Hidden fields sent with it. */ | |
| 88 | extra: Record<string, string>; | |
| 89 | }) { | |
| 90 | const fetcher = useFetcher<{ error?: string }>(); | |
| 91 | return ( | |
| 92 | <fetcher.Form method="post" className="mt-3 max-w-lg"> | |
| 93 | {Object.entries(extra).map(([name, value]) => ( | |
| 94 | <input key={name} type="hidden" name={name} value={value} /> | |
| 95 | ))} | |
| 96 | <div className="flex items-center gap-2"> | |
| 97 | <div className="min-w-0 grow"> | |
| 98 | <Input name={field} type="text" inputMode="url" required placeholder={placeholder} aria-label={label} maxLength={255} /> | |
| 99 | </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. | 100 | <SubmitButton fetcher={fetcher} variant="outline" pending="Saving…"> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 101 | Save |
| 102 | </SubmitButton> | |
| 103 | </div> | |
| 104 | {fetcher.data?.error && <p className="mt-1.5 text-xs text-danger">{fetcher.data.error}</p>} | |
| 105 | </fetcher.Form> | |
| 106 | ); | |
| 107 | } | |
| 108 | ||
| 109 | /** Deploying on g1t, offered as an option and never as a step. */ | |
| 110 | function DeployQuietly({ base, canDeploy, what }: { base: string; canDeploy: boolean; what: string }) { | |
| 111 | if (!canDeploy) return null; | |
| 112 | return ( | |
| 113 | <p className="mt-4 text-xs text-faint"> | |
| 114 | g1t can deploy {what} on g1t.page instead, with a preview for every pull request.{" "} | |
| 115 | <Link to={`${base}/settings/deployments`} className="underline-offset-4 hover:text-fg hover:underline"> | |
| 116 | Deployment settings | |
| 117 | </Link> | |
| 118 | </p> | |
| 119 | ); | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * An app deployed by its own pipeline: production at the address it was | |
| 124 | * given, with the latest reported deployment, or else the default branch's | |
| 125 | * latest commit and its checks. | |
| 126 | */ | |
| 127 | export function ElsewhereHead({ | |
| 128 | project, | |
| 129 | base, | |
| 130 | canChange, | |
| 131 | canDeploy, | |
| 132 | deployment, | |
| 133 | commit, | |
| 134 | checks, | |
| 135 | }: Head & { | |
| 136 | deployment?: ExternalDeployment | null; | |
| 137 | commit: { hash: string; authoredAt: string } | null; | |
| 138 | /** The commit's checks, as a badge; left out until there is one to show. */ | |
| 139 | checks?: ReactNode; | |
| 140 | }) { | |
| 141 | const url = deployment?.url ?? project.productionUrl; | |
| 142 | return ( | |
| 143 | <div className="flex flex-col gap-4 p-5 sm:flex-row sm:items-start sm:justify-between sm:p-6"> | |
| 144 | <div className="min-w-0"> | |
| 145 | <HeadLabel icon={<Rocket />}>Production</HeadLabel> | |
| 146 | {url ? ( | |
| 147 | <> | |
| 148 | <BigLink url={url} /> | |
| 149 | <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted"> | |
| 150 | {deployment ? ( | |
| 151 | <> | |
| 152 | <StatusDot status={deploymentStatus(deployment.state)} /> | |
| 153 | <span className="inline-flex items-center gap-1 font-mono"> | |
| 154 | <GitCommitHorizontal size={13} className="text-faint" /> | |
| 155 | {deployment.sha.slice(0, 7)} | |
| 156 | </span> | |
| 157 | <span> | |
| 158 | deployed <TimeAgo at={deployment.created_at} /> | |
| 159 | </span> | |
| 160 | </> | |
| 161 | ) : ( | |
| 162 | <> | |
| 163 | <span>Deployed by its own pipeline</span> | |
| 164 | {commit && ( | |
| 165 | <span className="inline-flex items-center gap-1.5"> | |
| 166 | <span className="text-faint">·</span> | |
| 167 | <Link to={`${base}/commit/${commit.hash}`} className="inline-flex items-center gap-1 font-mono hover:text-fg"> | |
| 168 | <GitCommitHorizontal size={13} className="text-faint" /> | |
| 169 | {commit.hash.slice(0, 7)} | |
| 170 | </Link> | |
| 171 | {checks} | |
| 172 | <TimeAgo at={commit.authoredAt} /> | |
| 173 | </span> | |
| 174 | )} | |
| 175 | </> | |
| 176 | )} | |
| 177 | </p> | |
| 178 | </> | |
| 179 | ) : canChange ? ( | |
| 180 | <> | |
| 181 | <p className="mt-2 text-lg font-medium">Where is production?</p> | |
| 182 | <p className="mt-1 max-w-lg text-sm text-muted"> | |
| 183 | {project.name} is deployed by its own pipeline. Give production's address, and this card links to it. | |
| 184 | </p> | |
| 185 | <AddressForm field="productionUrl" placeholder="https://example.com" label="Production's address" extra={{ intent: "kind", choice: "elsewhere" }} /> | |
| 186 | </> | |
| 187 | ) : ( | |
| 188 | <p className="mt-2 text-sm text-muted">Deployed by its own pipeline.</p> | |
| 189 | )} | |
| 190 | <DeployQuietly base={base} canDeploy={canDeploy} what="it" /> | |
| 191 | </div> | |
| 192 | {url && ( | |
| 193 | <div className="flex shrink-0 items-center gap-2"> | |
| Merge branch 'site-fixes' into fast-push | 194 | <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer"> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 195 | Visit |
| 196 | <ArrowUpRight size={14} /> | |
| 197 | </ButtonLink> | |
| 198 | </div> | |
| 199 | )} | |
| 200 | </div> | |
| 201 | ); | |
| 202 | } | |
| 203 | ||
| 204 | const NOT_DEPLOYED: { value: string; label: string }[] = [ | |
| 205 | { value: "library", label: "A library or package" }, | |
| 206 | { value: "tool", label: "A tool or CLI" }, | |
| 207 | { value: "docs", label: "Documentation" }, | |
| 208 | { value: "other", label: "Something else" }, | |
| 209 | ]; | |
| 210 | ||
| 211 | /** | |
| 212 | * An app that nobody has said where it runs, with Deployments off: one | |
| 213 | * question, answered in place, instead of a push to turn Deployments on. | |
| 214 | */ | |
| 215 | export function WhereItRuns({ project, base, canChange, canDeploy }: Head) { | |
| 216 | const fetcher = useFetcher<{ error?: string }>(); | |
| 217 | const [elsewhere, setElsewhere] = useState(false); | |
| 218 | if (!canChange) { | |
| 219 | return ( | |
| 220 | <div className="p-5 sm:p-6"> | |
| 221 | <HeadLabel icon={<Globe />}>Homepage</HeadLabel> | |
| 222 | {project.links.homepage ? <BigLink url={project.links.homepage} /> : <p className="mt-2 text-sm text-muted">No homepage given.</p>} | |
| 223 | </div> | |
| 224 | ); | |
| 225 | } | |
| 226 | return ( | |
| 227 | <div className="p-5 sm:p-6"> | |
| 228 | <HeadLabel icon={<Rocket />}>Production</HeadLabel> | |
| 229 | <p className="mt-2 text-lg font-medium">Where does {project.name} run?</p> | |
| 230 | <p className="mt-1 max-w-xl text-sm text-muted"> | |
| 231 | Say once, and this page follows: production where it is deployed, or its packages and releases if it isn't | |
| 232 | deployed at all. <span className="text-faint">{project.detected.reason.detail}</span> | |
| 233 | </p> | |
| 234 | <div className="mt-4 flex flex-wrap items-center gap-2"> | |
| 235 | {canDeploy && ( | |
| 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. | 236 | <ButtonLink to={`${base}/settings/deployments`} variant="outline"> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 237 | <Rocket size={14} /> |
| 238 | Deploy on g1t | |
| 239 | </ButtonLink> | |
| 240 | )} | |
| 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. | 241 | <Button |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 242 | type="button" |
| 243 | onClick={() => setElsewhere((open) => !open)} | |
| 244 | aria-expanded={elsewhere} | |
| 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. | 245 | variant="outline" |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 246 | > |
| 247 | <Globe size={14} /> | |
| 248 | It's deployed elsewhere | |
| 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. | 249 | </Button> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 250 | <DropdownMenu> |
| 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. | 251 | <DropdownMenuTrigger asChild> |
| 252 | <Button variant="outline"> | |
| 253 | <Box size={14} /> | |
| 254 | It isn't deployed | |
| 255 | <ChevronDown size={13} className="text-faint" /> | |
| 256 | </Button> | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 257 | </DropdownMenuTrigger> |
| 258 | <DropdownMenuContent align="start"> | |
| 259 | {NOT_DEPLOYED.map((option) => ( | |
| 260 | <DropdownMenuItem key={option.value} onSelect={() => fetcher.submit({ intent: "kind", choice: option.value }, { method: "post" })}> | |
| 261 | {option.label} | |
| 262 | </DropdownMenuItem> | |
| 263 | ))} | |
| 264 | </DropdownMenuContent> | |
| 265 | </DropdownMenu> | |
| 266 | </div> | |
| 267 | {elsewhere && ( | |
| 268 | <AddressForm field="productionUrl" placeholder="Production's address, such as https://example.com" label="Production's address" extra={{ intent: "kind", choice: "elsewhere" }} /> | |
| 269 | )} | |
| 270 | {fetcher.data?.error && <p className="mt-2 text-xs text-danger">{fetcher.data.error}</p>} | |
| 271 | </div> | |
| 272 | ); | |
| 273 | } | |
| 274 | ||
| 275 | /** Documentation: where it is read, or a place to say so. */ | |
| 276 | export function DocsHead({ project, base, canChange, canDeploy }: Head) { | |
| 277 | const url = project.links.docs ?? project.productionUrl ?? project.links.homepage; | |
| 278 | return ( | |
| 279 | <div className="flex flex-col gap-4 p-5 sm:flex-row sm:items-start sm:justify-between sm:p-6"> | |
| 280 | <div className="min-w-0"> | |
| 281 | <HeadLabel icon={<BookOpen />}>Documentation</HeadLabel> | |
| 282 | {url ? ( | |
| 283 | <BigLink url={url} /> | |
| 284 | ) : canChange ? ( | |
| 285 | <> | |
| 286 | <p className="mt-2 text-lg font-medium">Where are its docs read?</p> | |
| 287 | <p className="mt-1 max-w-lg text-sm text-muted">Give the address, and this card and its listing link to it.</p> | |
| 288 | <AddressForm field="docsUrl" placeholder="https://docs.example.com" label="Where its docs are read" extra={{ intent: "about" }} /> | |
| 289 | </> | |
| 290 | ) : ( | |
| 291 | <p className="mt-2 text-sm text-muted">Its docs are in its repository.</p> | |
| 292 | )} | |
| 293 | <DeployQuietly base={base} canDeploy={canDeploy && project.runs == null} what="its docs as a site" /> | |
| 294 | </div> | |
| 295 | {url && ( | |
| 296 | <div className="flex shrink-0 items-center gap-2"> | |
| Merge branch 'site-fixes' into fast-push | 297 | <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer"> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 298 | Read |
| 299 | <ArrowUpRight size={14} /> | |
| 300 | </ButtonLink> | |
| 301 | </div> | |
| 302 | )} | |
| 303 | </div> | |
| 304 | ); | |
| 305 | } | |
| 306 | ||
| 307 | /** Anything else: its homepage, or where to add its links. */ | |
| 308 | export function OtherHead({ project, base, canChange }: Omit<Head, "canDeploy">) { | |
| 309 | const url = project.links.homepage ?? project.links.docs ?? project.links.custom[0]?.url ?? null; | |
| 310 | return ( | |
| 311 | <div className="p-5 sm:p-6"> | |
| 312 | <HeadLabel icon={<Globe />}>Homepage</HeadLabel> | |
| 313 | {url ? ( | |
| 314 | <BigLink url={url} /> | |
| 315 | ) : canChange ? ( | |
| 316 | <p className="mt-2 text-sm text-muted"> | |
| 317 | No links yet.{" "} | |
| 318 | <Link to={`${base}/settings#links`} className="text-accent hover:underline"> | |
| 319 | Add a homepage, docs or others | |
| 320 | </Link>{" "} | |
| 321 | and they show here, on its card and in its workspace's Projects. | |
| 322 | </p> | |
| 323 | ) : ( | |
| 324 | <p className="mt-2 text-sm text-muted">No homepage given.</p> | |
| 325 | )} | |
| 326 | </div> | |
| 327 | ); | |
| 328 | } | |
| 329 | ||
| 330 | /** Production as g1t serves it, with the project's homepage when that is somewhere else. */ | |
| 331 | export function AlsoAt({ url }: { url: string | null }) { | |
| 332 | if (!url) return null; | |
| 333 | return ( | |
| 334 | <Hint label="Its homepage"> | |
| Merge branch 'site-fixes' into fast-push | 335 | <DeployLink href={url} className="inline-flex items-center gap-1 text-faint hover:text-fg"> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 336 | <Globe size={12} /> |
| 337 | {host(url).replace(/\/$/, "")} | |
| Merge branch 'site-fixes' into fast-push | 338 | </DeployLink> |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 339 | </Hint> |
| 340 | ); | |
| 341 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.