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.
| The docs' Copy page menu keeps its own spacing, with an icon per item | 1 | /** |
| Merge branch 'main' of github.com:flagon-io/g1t | 2 | * The site's footer: the logo, what g1t is and its accounts elsewhere as |
| 3 | * icons beside a few links worth having, then who makes g1t, and a row of | |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 4 | * the trust pages with the live status dot, the account, and the legal |
| 5 | * line. Marketing and signed-out pages show it; the app's account menu | |
| 6 | * has its own rows (components/shell.tsx). | |
| 7 | */ | |
| Merge branch 'main' of github.com:flagon-io/g1t | 8 | import { GitBranch } from "lucide-react"; |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 9 | import { useEffect, useState } from "react"; |
| 10 | import { Link } from "react-router"; | |
| 11 | ||
| 12 | import type { User } from "@g1t/contracts"; | |
| 13 | ||
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 14 | import { useAsksFirst } from "./analytics-consent"; |
| Merge branch 'main' of github.com:flagon-io/g1t | 15 | import { Logo, Mark } from "./logo"; |
| 16 | import { Button } from "./ui/button"; | |
| 17 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 18 | import { choose } from "../lib/analytics-consent"; |
| 19 | import { COMPANY, MAKER_PRODUCTS, SOCIAL, copyright, listed } from "../lib/legal"; | |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 20 | import { type OverallState, STATUS_JSON_URL, STATUS_URL, STATUS_WORDS, type StatusReport, dotClass } from "../lib/status"; |
| 21 | ||
| 22 | /** How long one fetched report is reused across the pages of one visit. */ | |
| 23 | const STATUS_REUSE_MS = 60_000; | |
| 24 | let lastStatus: { report: StatusReport; at: number } | null = null; | |
| 25 | let pendingStatus: Promise<StatusReport | null> | null = null; | |
| 26 | ||
| 27 | function fetchStatus(): Promise<StatusReport | null> { | |
| 28 | if (lastStatus && Date.now() - lastStatus.at < STATUS_REUSE_MS) return Promise.resolve(lastStatus.report); | |
| 29 | pendingStatus ??= fetch(STATUS_JSON_URL, { headers: { accept: "application/json" } }) | |
| 30 | .then((response) => (response.ok ? (response.json() as Promise<StatusReport>) : null)) | |
| 31 | .then((report) => { | |
| 32 | if (report?.overall) lastStatus = { report, at: Date.now() }; | |
| 33 | return report?.overall ? report : null; | |
| 34 | }) | |
| 35 | // The status page being unreachable only means no dot. | |
| 36 | .catch(() => null) | |
| 37 | .finally(() => { | |
| 38 | pendingStatus = null; | |
| 39 | }); | |
| 40 | return pendingStatus; | |
| 41 | } | |
| 42 | ||
| 43 | /** | |
| 44 | * The live status, from status.g1t.sh once the page is up (or `when` turns | |
| 45 | * true), so no page waits on it. Kept a minute across pages. Null until it | |
| 46 | * arrives, or if it cannot. | |
| 47 | */ | |
| 48 | export function useSiteStatus(when = true): StatusReport | null { | |
| 49 | const [report, setReport] = useState<StatusReport | null>(() => lastStatus?.report ?? null); | |
| 50 | useEffect(() => { | |
| 51 | if (!when) return; | |
| 52 | let live = true; | |
| 53 | fetchStatus().then((fresh) => { | |
| 54 | if (live && fresh) setReport(fresh); | |
| 55 | }); | |
| 56 | return () => { | |
| 57 | live = false; | |
| 58 | }; | |
| 59 | }, [when]); | |
| 60 | return report; | |
| 61 | } | |
| 62 | ||
| 63 | /** The coloured dot, with words for screen readers. */ | |
| 64 | export function StatusDot({ state, className = "" }: { state: OverallState | null; className?: string }) { | |
| 65 | return ( | |
| 66 | <span className={`relative inline-flex size-2 shrink-0 ${className}`}> | |
| 67 | {state && state !== "up" && state !== "unknown" && ( | |
| 68 | <span aria-hidden className={`absolute inset-0 animate-ping rounded-full opacity-60 ${dotClass(state)}`} /> | |
| 69 | )} | |
| 70 | <span aria-hidden className={`relative inline-flex size-2 rounded-full ${dotClass(state)}`} /> | |
| 71 | <span className="sr-only">{state ? STATUS_WORDS[state] : "Checking status"}</span> | |
| 72 | </span> | |
| 73 | ); | |
| 74 | } | |
| 75 | ||
| 76 | /** Where the footer's grid links go. Plain links: some are files or other hosts. */ | |
| 77 | const FOOTER_LINKS: { title: string; links: [string, string][] }[] = [ | |
| 78 | { | |
| 79 | title: "Product", | |
| 80 | links: [ | |
| 81 | ["Explore", "/explore"], | |
| 82 | ["Pricing", "/pricing"], | |
| g1t.sh/download is a marketing page: it is drawn with the site's header and footer whoever opens it, never inside the app's rail and sidebar, in the shape of Pricing, with Download in the public header, its phone menu and the footer. The desktop app's check in CI runs on the larger machine the Rust tests use; on the small one the linker ran out of room and died with a bus error on every push since the app arrived. | 83 | ["Download", "/download"], |
| Chat and workspace agents: channels, DMs and named agents you talk to | 84 | ["Chat", "https://docs.g1t.sh/guides/chat/"], |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 85 | ["Bring your own agent", "https://docs.g1t.sh/guides/bring-your-own-agent/"], |
| 86 | ["Integrations", "https://docs.g1t.sh/guides/integrations/"], | |
| 87 | ], | |
| 88 | }, | |
| 89 | { | |
| 90 | title: "Developers", | |
| 91 | links: [ | |
| 92 | ["Documentation", "https://docs.g1t.sh/"], | |
| 93 | ["API reference", "https://docs.g1t.sh/reference/api/"], | |
| 94 | ["MCP tools", "https://docs.g1t.sh/reference/mcp/"], | |
| 95 | ["llms.txt", "/llms.txt"], | |
| 96 | ], | |
| 97 | }, | |
| 98 | { | |
| 99 | title: "Company", | |
| 100 | links: [ | |
| 101 | ["Flagon, Inc.", COMPANY.url], | |
| 102 | ["Support", "/support"], | |
| 103 | ["Security", "/security"], | |
| 104 | ["Status", STATUS_URL], | |
| 105 | ], | |
| 106 | }, | |
| 107 | { | |
| 108 | title: "Open source", | |
| 109 | links: [ | |
| 110 | ["Source on g1t", "/flagon-io/g1t"], | |
| 111 | ["MIT license", "/flagon-io/g1t/blob/main/LICENSE"], | |
| 112 | ["Run g1t yourself", "https://docs.g1t.sh/guides/self-hosting/"], | |
| 113 | ], | |
| 114 | }, | |
| 115 | ]; | |
| 116 | ||
| Merge branch 'main' of github.com:flagon-io/g1t | 117 | /** What the footer says under the logo. */ |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 118 | const TAGLINE = "Where your team and its coding agents ship code."; |
| Merge branch 'main' of github.com:flagon-io/g1t | 119 | |
| 120 | /** The icons under it: g1t's accounts elsewhere (lib/legal), then its source. */ | |
| 121 | const ICON_LINKS: { label: string; url: string; icon: React.ReactNode }[] = [ | |
| 122 | ...SOCIAL.map((account) => ({ | |
| 123 | label: `g1t on ${account.label}`, | |
| 124 | url: account.url, | |
| 125 | icon: ( | |
| 126 | <svg viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" className="size-4"> | |
| 127 | <path d={account.icon.path} /> | |
| 128 | </svg> | |
| 129 | ), | |
| 130 | })), | |
| 131 | { label: "Source on g1t", url: "/flagon-io/g1t", icon: <GitBranch aria-hidden className="size-4" /> }, | |
| 132 | ]; | |
| 133 | ||
| 134 | /** The footer's first column: the logo, the tagline and the icons. */ | |
| 135 | function BrandColumn() { | |
| 136 | return ( | |
| 137 | <div className="col-span-2 sm:col-span-4 lg:col-span-2 lg:pr-8"> | |
| 138 | <Link to="/" aria-label="g1t home" className="inline-flex rounded-sm"> | |
| 139 | <Logo /> | |
| 140 | </Link> | |
| 141 | <p className="mt-3 max-w-xs text-sm leading-6 text-muted text-balance">{TAGLINE}</p> | |
| 142 | <ul className="mt-4 -ml-2 flex items-center gap-1"> | |
| 143 | {ICON_LINKS.map((link) => ( | |
| 144 | <li key={link.url}> | |
| 145 | <Tooltip> | |
| 146 | <TooltipTrigger asChild> | |
| 147 | <Button asChild variant="ghost" size="icon-sm"> | |
| 148 | <a href={link.url} aria-label={link.label}> | |
| 149 | {link.icon} | |
| 150 | </a> | |
| 151 | </Button> | |
| 152 | </TooltipTrigger> | |
| 153 | <TooltipContent>{link.label}</TooltipContent> | |
| 154 | </Tooltip> | |
| 155 | </li> | |
| 156 | ))} | |
| 157 | </ul> | |
| 158 | </div> | |
| 159 | ); | |
| 160 | } | |
| 161 | ||
| The docs' Copy page menu keeps its own spacing, with an icon per item | 162 | /** The row's own links, after Status. */ |
| 163 | const ROW_LINKS: [string, string][] = [ | |
| 164 | ["Support", "/support"], | |
| 165 | ["Policies", "/policies"], | |
| 166 | ["Privacy", "/policies/privacy"], | |
| 167 | ["Security", "/security"], | |
| 168 | ]; | |
| 169 | ||
| 170 | /** | |
| 171 | * Flagon's mark, single-ink, as in Flagon's brand files | |
| 172 | * (flagon-mark-mono.svg): a flagon, its brew a tone of the text colour. | |
| 173 | */ | |
| 174 | function FlagonMark({ className = "" }: { className?: string }) { | |
| 175 | return ( | |
| 176 | <svg viewBox="11.3 12.3 41 41" fill="none" aria-hidden="true" className={className}> | |
| 177 | <defs> | |
| 178 | <clipPath id="g1t-footer-flagon-body"> | |
| 179 | <path d="M19.5 23 L34.5 23 L36.5 46 Q36.7 48 34.3 48 L19.7 48 Q17.3 48 17.5 46 Z" /> | |
| 180 | </clipPath> | |
| 181 | </defs> | |
| 182 | <g clipPath="url(#g1t-footer-flagon-body)"> | |
| 183 | <path | |
| 184 | d="M13 27 q3.75 -0.7 7.5 0 t7.5 0 t7.5 0 t7.5 0 t7.5 0 L51 52 L13 52 Z" | |
| 185 | fill="currentColor" | |
| 186 | fillOpacity="0.22" | |
| 187 | /> | |
| 188 | </g> | |
| 189 | <path | |
| 190 | d="M37 27 L45.5 27 L48.5 30 L48.5 36.5 L45.5 39.5 L38 39.5" | |
| 191 | stroke="currentColor" | |
| 192 | strokeWidth="3.1" | |
| 193 | strokeLinecap="round" | |
| 194 | strokeLinejoin="round" | |
| 195 | /> | |
| 196 | <path | |
| 197 | d="M17.5 21 L36.5 21 L38.6 47 Q39 50 36 50 L18 50 Q15 50 15.4 47 Z" | |
| 198 | stroke="currentColor" | |
| 199 | strokeWidth="3.1" | |
| 200 | strokeLinejoin="round" | |
| 201 | /> | |
| 202 | <path d="M17 21 Q16.3 16.3 20 15.6 L33 15.6 Q37.2 16.2 37 21 Z" stroke="currentColor" strokeWidth="3" strokeLinejoin="round" /> | |
| 203 | <path d="M36.7 17.1 L39.4 16.1" stroke="currentColor" strokeWidth="2.4" strokeLinejoin="round" /> | |
| 204 | <circle cx="40.3" cy="15.8" r="1.4" fill="currentColor" /> | |
| 205 | </svg> | |
| 206 | ); | |
| 207 | } | |
| 208 | ||
| 209 | /** "g1t is designed, built, and backed by Flagon, Inc., …" */ | |
| 210 | export function MakerLine() { | |
| 211 | const products = MAKER_PRODUCTS.map((product) => product.name); | |
| 212 | return ( | |
| 213 | <p className="text-sm leading-6 text-muted text-balance"> | |
| 214 | <Mark tight className="mr-1.5 inline-block h-[0.85em] w-auto align-baseline" /> | |
| 215 | <span className="font-semibold text-fg">g1t</span> is designed, built, and backed by{" "} | |
| 216 | <a href={COMPANY.url} className="whitespace-nowrap font-semibold text-fg hover:underline hover:underline-offset-4"> | |
| 217 | <FlagonMark className="mr-1 inline-block size-[0.95em] -translate-y-px align-middle" /> | |
| 218 | {COMPANY.name} | |
| 219 | </a> | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 220 | {products.length > 0 ? `, the people behind ${listed(products)}.` : `, ${COMPANY.about} founded by ${COMPANY.founder}.`} |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 221 | </p> |
| 222 | ); | |
| 223 | } | |
| 224 | ||
| 225 | const ROW_LINK = "rounded-sm transition-colors hover:text-fg"; | |
| 226 | ||
| 227 | /** | |
| 228 | * Status, the trust pages, the account and the legal line, separated by | |
| 229 | * dots that wrap away cleanly on a phone. | |
| 230 | */ | |
| 231 | export function LegalRow({ user }: { user: User | null | undefined }) { | |
| 232 | const status = useSiteStatus(); | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 233 | const asksFirst = useAsksFirst(); |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 234 | const links = [ |
| 235 | <a key="status" href={STATUS_URL} className={`inline-flex items-center gap-1.5 ${ROW_LINK}`}> | |
| 236 | <StatusDot state={status?.overall.state ?? null} /> | |
| 237 | Status | |
| 238 | </a>, | |
| 239 | ...ROW_LINKS.map(([label, to]) => ( | |
| 240 | <Link key={to} to={to} className={ROW_LINK}> | |
| 241 | {label} | |
| 242 | </Link> | |
| 243 | )), | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 244 | // Where the visitor was asked about the analytics cookie: ask again. |
| 245 | ...(asksFirst | |
| 246 | ? [ | |
| 247 | <button key="consent" type="button" onClick={() => choose(null)} className={ROW_LINK}> | |
| 248 | Cookie choices | |
| 249 | </button>, | |
| 250 | ] | |
| 251 | : []), | |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 252 | ]; |
| 253 | const account = [ | |
| 254 | user ? ( | |
| 255 | <Link key="me" to={`/u/${user.username}`} className={`font-medium text-fg/90 ${ROW_LINK}`}> | |
| 256 | {user.username} | |
| 257 | </Link> | |
| 258 | ) : ( | |
| 259 | <Link key="login" to="/login" className={ROW_LINK}> | |
| 260 | Sign in | |
| 261 | </Link> | |
| 262 | ), | |
| 263 | ]; | |
| 264 | const legal = [ | |
| 265 | ...account, | |
| 266 | <span key="copyright" className="text-faint"> | |
| 267 | {copyright()} | |
| 268 | </span>, | |
| 269 | ]; | |
| 270 | const gap = "gap-x-3"; | |
| 271 | const dot = ( | |
| 272 | <span aria-hidden className="text-line-strong"> | |
| 273 | · | |
| 274 | </span> | |
| 275 | ); | |
| 276 | // Two runs, the links and the legal line, so that on a narrow screen the | |
| 277 | // second starts its own line rather than a line starting with a dot. | |
| 278 | const run = (items: React.ReactNode[]) => | |
| 279 | items.map((item, index) => ( | |
| 280 | <li key={index} className={`flex items-center ${gap}`}> | |
| 281 | {index > 0 && dot} | |
| 282 | {item} | |
| 283 | </li> | |
| 284 | )); | |
| 285 | return ( | |
| 286 | <div className={`flex flex-wrap items-center justify-center gap-y-1.5 ${gap} text-[0.8125rem] text-muted`}> | |
| 287 | <ul className={`flex flex-wrap items-center justify-center gap-y-1.5 ${gap}`}>{run(links)}</ul> | |
| 288 | {/* Between the two runs only while they share a line. */} | |
| 289 | <span className="hidden md:inline">{dot}</span> | |
| 290 | <ul className={`flex flex-wrap items-center justify-center gap-y-1.5 ${gap}`}>{run(legal)}</ul> | |
| 291 | </div> | |
| 292 | ); | |
| 293 | } | |
| 294 | ||
| 295 | export function SiteFooter({ user }: { user: User | null | undefined }) { | |
| 296 | return ( | |
| 297 | <footer className="mt-24 border-t border-line"> | |
| Merge branch 'main' of github.com:flagon-io/g1t | 298 | <div className="mx-auto grid max-w-6xl grid-cols-2 gap-x-8 gap-y-10 px-4 pt-12 pb-10 sm:grid-cols-4 lg:grid-cols-6"> |
| 299 | <BrandColumn /> | |
| 300 | {/* The link columns sit in the same grid as the brand column. */} | |
| 301 | <nav aria-label="Footer" className="contents"> | |
| 302 | {FOOTER_LINKS.map((group) => ( | |
| 303 | <div key={group.title}> | |
| 304 | <p className="text-sm font-medium">{group.title}</p> | |
| 305 | <ul className="mt-3 space-y-2 text-sm text-muted"> | |
| 306 | {group.links.map(([label, to]) => ( | |
| 307 | <li key={to}> | |
| 308 | <a href={to} className="transition-colors hover:text-fg"> | |
| 309 | {label} | |
| 310 | </a> | |
| 311 | </li> | |
| 312 | ))} | |
| 313 | </ul> | |
| 314 | </div> | |
| 315 | ))} | |
| 316 | </nav> | |
| 317 | </div> | |
| The docs' Copy page menu keeps its own spacing, with an icon per item | 318 | <div className="border-t border-line"> |
| 319 | <div className="mx-auto max-w-3xl space-y-4 px-4 py-10 text-center"> | |
| 320 | <MakerLine /> | |
| 321 | <LegalRow user={user} /> | |
| 322 | </div> | |
| 323 | </div> | |
| 324 | </footer> | |
| 325 | ); | |
| 326 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.