Social cards for every page: og.g1t.sh
A new Worker draws a 1200x630 card for every page on g1t.sh and the docs: the home and pricing pages, workspaces, projects with their open issues and pull requests, issues and pull requests with their state, Soon pages, and each docs page. Lookups are anonymous, so anything private gets the generic card. Cards are cached by what they show. Every page on g1t.sh and the docs carries og and Twitter tags pointing at its card.
| 46 | 46 | - `g1t-mark.svg`, `g1t-mark.png` (256px): the mark on a rounded dark tile, readable on any background | |
| 47 | 47 | - `g1t-logo.svg`, `g1t-logo.png`: the lockup for light backgrounds, used in g1t's emails | |
| 48 | 48 | - `g1t-logo-on-dark.svg`, `g1t-logo-on-dark.png`: the lockup for dark backgrounds | |
| 49 | + | - `g1t-og.png`: the brand social card as `services/og` draws it, which that service falls back to if drawing fails | |
| 50 | + | ||
| 51 | + | ## Social cards | |
| 52 | + | ||
| 53 | + | Every page of g1t.sh and docs.g1t.sh links to its own card at `https://og.g1t.sh`, | |
| 54 | + | drawn by `services/og` in the same colours, type and mark: the lockup and the | |
| 55 | + | page's address along the top, its subject in the middle, its facts along the | |
| 56 | + | bottom, the fleet standing large and faded off the right edge. |
| 97 | 97 | | `services/work` | Issues, pull requests, reviews, check runs and sessions. Rust. | | |
| 98 | 98 | | `services/events` | The event bus and its log. Rust. | | |
| 99 | 99 | | `services/runner` | Starts sandboxes: for g1t agents, and for acceptance checks. | | |
| 100 | + | | `services/og` | Social cards at `og.g1t.sh`: a PNG per page, showing only what anyone may see. | | |
| 100 | 101 | | `crates/runner` | The program inside a sandbox: runs an agent, or a set of checks, and reports back. Rust. | | |
| 101 | 102 | | `crates/contracts` | Types and service interfaces for the Rust services. | | |
| 102 | 103 | | `crates/kit` | Plumbing shared by Rust services on Workers. | |
| 2 | 2 | ||
| 3 | 3 | import { tabFor } from './tabs'; | |
| 4 | 4 | ||
| 5 | − | type Entry = { type: string; href?: string; entries?: Entry[] }; | |
| 5 | + | type Entry = { type: string; label?: string; href?: string; isCurrent?: boolean; entries?: Entry[] }; | |
| 6 | + | ||
| 7 | + | /** The social cards' service; see services/og. */ | |
| 8 | + | const OG = 'https://og.g1t.sh'; | |
| 6 | 9 | ||
| 7 | 10 | /** The slug of a group's first page on this site, such as `reference/api`. */ | |
| 8 | 11 | function firstSlug(entries: Entry[]): string | undefined { | |
| 17 | 20 | return undefined; | |
| 18 | 21 | } | |
| 19 | 22 | ||
| 23 | + | /** The label of the sidebar group holding the current page, the innermost if they nest. */ | |
| 24 | + | function sectionOf(entries: Entry[], group?: string): string | undefined { | |
| 25 | + | for (const entry of entries) { | |
| 26 | + | if (entry.type === 'group') { | |
| 27 | + | const found = sectionOf(entry.entries ?? [], entry.label); | |
| 28 | + | if (found !== undefined) return found; | |
| 29 | + | } else if (entry.isCurrent) { | |
| 30 | + | return group; | |
| 31 | + | } | |
| 32 | + | } | |
| 33 | + | return undefined; | |
| 34 | + | } | |
| 35 | + | ||
| 20 | 36 | /** | |
| 21 | 37 | * Each page's sidebar holds only its tab's groups: those whose pages | |
| 22 | 38 | * belong to the same tab. Groups are told apart by their pages, not their | |
| 23 | 39 | * labels, since a guide group and an API group can share a name. | |
| 40 | + | * | |
| 41 | + | * Each page also gets its own social card: its title, its section and its | |
| 42 | + | * description, drawn by the og service. | |
| 24 | 43 | */ | |
| 25 | 44 | export const onRequest = defineRouteMiddleware((context) => { | |
| 26 | 45 | const route = context.locals.starlightRoute; | |
| 46 | + | const section = sectionOf(route.sidebar as Entry[]); | |
| 27 | 47 | const tab = tabFor(route.id); | |
| 28 | 48 | route.sidebar = route.sidebar.filter((entry) => { | |
| 29 | 49 | if (entry.type !== 'group') return false; | |
| 30 | 50 | const slug = firstSlug(entry.entries as Entry[]); | |
| 31 | 51 | return slug !== undefined && tabFor(slug) === tab; | |
| 32 | 52 | }); | |
| 53 | + | ||
| 54 | + | const { title, description } = route.entry.data; | |
| 55 | + | const card = new URL('/docs', OG); | |
| 56 | + | card.searchParams.set('title', title); | |
| 57 | + | if (section) card.searchParams.set('section', section); | |
| 58 | + | if (description) card.searchParams.set('description', description); | |
| 59 | + | const image = card.toString(); | |
| 60 | + | const alt = `${title} · g1t docs`; | |
| 61 | + | const meta = (key: 'property' | 'name', name: string, content: string) => ({ | |
| 62 | + | tag: 'meta' as const, | |
| 63 | + | attrs: { [key]: name, content }, | |
| 64 | + | }); | |
| 65 | + | // Starlight already writes og:title, og:description, og:url and | |
| 66 | + | // twitter:card; these finish the card. | |
| 67 | + | route.head.push( | |
| 68 | + | meta('property', 'og:image', image), | |
| 69 | + | meta('property', 'og:image:type', 'image/png'), | |
| 70 | + | meta('property', 'og:image:width', '1200'), | |
| 71 | + | meta('property', 'og:image:height', '630'), | |
| 72 | + | meta('property', 'og:image:alt', alt), | |
| 73 | + | meta('name', 'twitter:title', title), | |
| 74 | + | meta('name', 'twitter:image', image), | |
| 75 | + | meta('name', 'twitter:image:alt', alt), | |
| 76 | + | ); | |
| 77 | + | if (description) route.head.push(meta('name', 'twitter:description', description)); | |
| 33 | 78 | }); |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { test } from "node:test"; | |
| 3 | + | ||
| 4 | + | import { DESCRIPTION, cardUrl, excerpt, fingerprint, page } from "./meta.ts"; | |
| 5 | + | ||
| 6 | + | type Tag = Record<string, string>; | |
| 7 | + | ||
| 8 | + | function tags(list: unknown[]): Map<string, string> { | |
| 9 | + | const found = new Map<string, string>(); | |
| 10 | + | for (const tag of list as Tag[]) { | |
| 11 | + | if ("title" in tag) found.set("title", tag.title); | |
| 12 | + | else found.set(tag.property ?? tag.name, tag.content); | |
| 13 | + | } | |
| 14 | + | return found; | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | test("a page gets its title, description, Open Graph and Twitter tags", () => { | |
| 18 | + | const meta = tags(page({ location: { pathname: "/pricing" }, matches: [] }, { title: "Pricing · g1t", description: "What it costs." })); | |
| 19 | + | assert.equal(meta.get("title"), "Pricing · g1t"); | |
| 20 | + | assert.equal(meta.get("description"), "What it costs."); | |
| 21 | + | assert.equal(meta.get("og:title"), "Pricing"); | |
| 22 | + | assert.equal(meta.get("og:url"), "https://g1t.sh/pricing"); | |
| 23 | + | assert.equal(meta.get("og:site_name"), "g1t"); | |
| 24 | + | assert.equal(meta.get("og:image"), "https://og.g1t.sh/image?path=%2Fpricing"); | |
| 25 | + | assert.equal(meta.get("twitter:card"), "summary_large_image"); | |
| 26 | + | assert.equal(meta.get("twitter:image"), meta.get("og:image")); | |
| 27 | + | }); | |
| 28 | + | ||
| 29 | + | test("pages under a project describe the project, and their card follows its details", () => { | |
| 30 | + | const project = { | |
| 31 | + | id: "routes/repo/layout", | |
| 32 | + | loaderData: { repo: { description: "Repo words" }, project: { name: "Web", description: "The storefront" }, open: { issues: 2, pulls: 1 } }, | |
| 33 | + | }; | |
| 34 | + | const meta = tags(page({ location: { pathname: "/acme/web/commits/" }, matches: [project] }, { title: "Commits · acme/web · g1t" })); | |
| 35 | + | assert.equal(meta.get("description"), "The storefront"); | |
| 36 | + | assert.equal(meta.get("og:url"), "https://g1t.sh/acme/web/commits"); | |
| 37 | + | assert.match(meta.get("og:image") ?? "", /\?path=%2Facme%2Fweb%2Fcommits&v=[0-9a-z]+$/); | |
| 38 | + | }); | |
| 39 | + | ||
| 40 | + | test("with nothing particular to say, a page says what g1t is", () => { | |
| 41 | + | const meta = tags(page({ location: { pathname: "/login" }, matches: [] }, { title: "Sign in · g1t" })); | |
| 42 | + | assert.equal(meta.get("description"), DESCRIPTION); | |
| 43 | + | }); | |
| 44 | + | ||
| 45 | + | test("a card's version changes with what it shows", () => { | |
| 46 | + | assert.equal(fingerprint(["a", "open"]), fingerprint(["a", "open"])); | |
| 47 | + | assert.notEqual(fingerprint(["a", "open"]), fingerprint(["a", "merged"])); | |
| 48 | + | assert.equal(cardUrl("/", undefined), "https://og.g1t.sh/image?path=%2F"); | |
| 49 | + | }); | |
| 50 | + | ||
| 51 | + | test("an excerpt is plain text from the start of the Markdown", () => { | |
| 52 | + | assert.equal(excerpt("## Why\n\nThe [queue](https://x) is **slow**.\n\n```ts\ncode()\n```"), "Why The queue is slow."); | |
| 53 | + | assert.equal(excerpt("x".repeat(300), 10).length, 10); | |
| 54 | + | assert.equal(excerpt(null), ""); | |
| 55 | + | }); |
| 1 | + | /** | |
| 2 | + | * Every page's <title> and the tags a link preview reads: its description, | |
| 3 | + | * Open Graph and Twitter's card. The image is the page's own card from the | |
| 4 | + | * og service (services/og), which draws it as an anonymous visitor would | |
| 5 | + | * see the page, so a private page's card never names anything. | |
| 6 | + | * | |
| 7 | + | * React Router renders only the deepest route's `meta`, so each route | |
| 8 | + | * returns `page(args, …)` rather than a bare title. | |
| 9 | + | */ | |
| 10 | + | import type { MetaDescriptor } from "react-router"; | |
| 11 | + | ||
| 12 | + | export const SITE = "https://g1t.sh"; | |
| 13 | + | export const OG = "https://og.g1t.sh"; | |
| 14 | + | ||
| 15 | + | /** What g1t is, for pages with nothing more particular to say. */ | |
| 16 | + | export const DESCRIPTION = | |
| 17 | + | "A git forge for teams of agents: every change isolated, every decision recorded, every change landed in order. Open source, built on Cloudflare."; | |
| 18 | + | ||
| 19 | + | /** As much of `MetaArgs` as the tags need. */ | |
| 20 | + | export type PageArgs = { | |
| 21 | + | location: { pathname: string }; | |
| 22 | + | matches: ReadonlyArray<{ id: string; loaderData?: unknown } | undefined>; | |
| 23 | + | }; | |
| 24 | + | ||
| 25 | + | export type PageMeta = { | |
| 26 | + | /** The whole <title>, ending in "· g1t". */ | |
| 27 | + | title: string; | |
| 28 | + | /** Defaults to the project's or workspace's description, then g1t's. */ | |
| 29 | + | description?: string | null; | |
| 30 | + | /** | |
| 31 | + | * Whatever the page's card shows, so that the card is drawn again when it | |
| 32 | + | * changes. Defaults to the project's details on project pages. | |
| 33 | + | */ | |
| 34 | + | version?: unknown; | |
| 35 | + | type?: "website" | "article"; | |
| 36 | + | }; | |
| 37 | + | ||
| 38 | + | type ProjectData = { | |
| 39 | + | repo?: { description?: string | null }; | |
| 40 | + | project?: { name?: string; description?: string | null } | null; | |
| 41 | + | open?: { issues?: number; pulls?: number }; | |
| 42 | + | }; | |
| 43 | + | type WorkspaceData = { workspace?: { name?: string; description?: string | null } }; | |
| 44 | + | ||
| 45 | + | function loaded<T>(args: PageArgs, id: string): T | undefined { | |
| 46 | + | return args.matches.find((match) => match?.id === id)?.loaderData as T | undefined; | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | /** A short, stable fingerprint, for the card's `v`. */ | |
| 50 | + | export function fingerprint(value: unknown): string { | |
| 51 | + | const text = JSON.stringify(value) ?? ""; | |
| 52 | + | // FNV-1a, 32 bits. | |
| 53 | + | let hash = 0x811c9dc5; | |
| 54 | + | for (let i = 0; i < text.length; i++) { | |
| 55 | + | hash ^= text.charCodeAt(i); | |
| 56 | + | hash = Math.imul(hash, 0x01000193); | |
| 57 | + | } | |
| 58 | + | return (hash >>> 0).toString(36); | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | /** A description fit for a preview: one line, at most 200 characters. */ | |
| 62 | + | function summary(text: string): string { | |
| 63 | + | const line = text.replace(/\s+/g, " ").trim(); | |
| 64 | + | return line.length > 200 ? `${line.slice(0, 199).trimEnd()}…` : line; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | /** The opening of some Markdown as plain text, for a description. */ | |
| 68 | + | export function excerpt(markdown: string | null | undefined, max = 160): string { | |
| 69 | + | const text = (markdown ?? "") | |
| 70 | + | .replace(/```[\s\S]*?(```|$)/g, " ") | |
| 71 | + | .replace(/<[^>]+>/g, " ") | |
| 72 | + | .replace(/!\[[^\]]*\]\([^)]*\)/g, " ") | |
| 73 | + | .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") | |
| 74 | + | .replace(/^\s{0,3}(#{1,6}|>|[-*+]|\d+\.)\s+/gm, "") | |
| 75 | + | .replace(/[`*_~|]/g, "") | |
| 76 | + | .replace(/\s+/g, " ") | |
| 77 | + | .trim(); | |
| 78 | + | return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text; | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | /** The address of a page's card. */ | |
| 82 | + | export function cardUrl(pathname: string, version?: unknown): string { | |
| 83 | + | const url = new URL("/image", OG); | |
| 84 | + | url.searchParams.set("path", pathname); | |
| 85 | + | if (version !== undefined) url.searchParams.set("v", fingerprint(version)); | |
| 86 | + | return url.toString(); | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | export function page(args: PageArgs, meta: PageMeta): MetaDescriptor[] { | |
| 90 | + | const project = loaded<ProjectData>(args, "routes/repo/layout"); | |
| 91 | + | const workspace = loaded<WorkspaceData>(args, "routes/workspace/layout"); | |
| 92 | + | const description = summary( | |
| 93 | + | meta.description || | |
| 94 | + | project?.project?.description || | |
| 95 | + | project?.repo?.description || | |
| 96 | + | workspace?.workspace?.description || | |
| 97 | + | DESCRIPTION, | |
| 98 | + | ); | |
| 99 | + | const version = | |
| 100 | + | meta.version ?? | |
| 101 | + | (project | |
| 102 | + | ? [project.project?.name, project.project?.description ?? project.repo?.description, project.open] | |
| 103 | + | : workspace | |
| 104 | + | ? [workspace.workspace?.name, workspace.workspace?.description] | |
| 105 | + | : undefined); | |
| 106 | + | const pathname = args.location.pathname.replace(/\/+$/, "") || "/"; | |
| 107 | + | // The title a preview shows needs no "· g1t": the site name is beside it. | |
| 108 | + | const shareTitle = meta.title.replace(/ · g1t$/, ""); | |
| 109 | + | const image = cardUrl(pathname, version); | |
| 110 | + | return [ | |
| 111 | + | { title: meta.title }, | |
| 112 | + | { name: "description", content: description }, | |
| 113 | + | { property: "og:site_name", content: "g1t" }, | |
| 114 | + | { property: "og:type", content: meta.type ?? "website" }, | |
| 115 | + | { property: "og:url", content: `${SITE}${pathname === "/" ? "/" : pathname}` }, | |
| 116 | + | { property: "og:title", content: shareTitle }, | |
| 117 | + | { property: "og:description", content: description }, | |
| 118 | + | { property: "og:image", content: image }, | |
| 119 | + | { property: "og:image:type", content: "image/png" }, | |
| 120 | + | { property: "og:image:width", content: "1200" }, | |
| 121 | + | { property: "og:image:height", content: "630" }, | |
| 122 | + | { property: "og:image:alt", content: shareTitle }, | |
| 123 | + | { name: "twitter:card", content: "summary_large_image" }, | |
| 124 | + | { name: "twitter:title", content: shareTitle }, | |
| 125 | + | { name: "twitter:description", content: description }, | |
| 126 | + | { name: "twitter:image", content: image }, | |
| 127 | + | { name: "twitter:image:alt", content: shareTitle }, | |
| 128 | + | ]; | |
| 129 | + | } |
| 2 | 2 | import { Form } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/device"; | |
| 5 | + | import { page } from "../lib/meta"; | |
| 5 | 6 | import { Button, ErrorText, Field, Input } from "../components/ui"; | |
| 6 | 7 | import { identity } from "../lib/services.server"; | |
| 7 | 8 | import { assertSameOrigin, requireUser } from "../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({}: Route.MetaArgs) { | |
| 10 | − | return [{ title: "Connect an application · g1t" }]; | |
| 10 | + | export function meta(args: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: "Connect an application · g1t" }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | /** Where a tool sends a person to approve its sign-in. */ |
| 2 | 2 | import { Form } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/explore"; | |
| 5 | + | import { page } from "../lib/meta"; | |
| 5 | 6 | import { RepoList } from "../components/repo-list"; | |
| 6 | 7 | import { notACredential } from "../components/ui"; | |
| 7 | 8 | import { repos } from "../lib/services.server"; | |
| 8 | 9 | import { getViewer } from "../lib/session.server"; | |
| 9 | 10 | ||
| 10 | − | export function meta({ loaderData }: Route.MetaArgs) { | |
| 11 | − | return [ | |
| 12 | − | { title: loaderData?.query ? `${loaderData.query} · Search · g1t` : "Explore · g1t" }, | |
| 13 | − | ]; | |
| 11 | + | export function meta({ loaderData, ...args }: Route.MetaArgs) { | |
| 12 | + | return page(args, { | |
| 13 | + | title: loaderData?.query ? `${loaderData.query} · Search · g1t` : "Explore · g1t", | |
| 14 | + | description: loaderData?.query | |
| 15 | + | ? `Public repositories on g1t matching "${loaderData.query}".` | |
| 16 | + | : "Public repositories on g1t, and the agents at work on them.", | |
| 17 | + | }); | |
| 14 | 18 | } | |
| 15 | 19 | ||
| 16 | 20 | /** Serves both /explore and /search?q=. */ |
| 1 | 1 | import { Form, Link } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/forgot"; | |
| 4 | + | import { page } from "../lib/meta"; | |
| 4 | 5 | import { AuthCard } from "../components/auth-card"; | |
| 5 | 6 | import { Button, Field, Input } from "../components/ui"; | |
| 6 | 7 | import { identity } from "../lib/services.server"; | |
| 7 | 8 | import { assertSameOrigin } from "../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({}: Route.MetaArgs) { | |
| 10 | − | return [{ title: "Reset your password · g1t" }]; | |
| 10 | + | export function meta(args: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: "Reset your password · g1t" }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | export async function action({ request }: Route.ActionArgs) { |
| 6 | 6 | import type { Lifecycle, Pull, Repo } from "@g1t/contracts"; | |
| 7 | 7 | ||
| 8 | 8 | import type { Route } from "./+types/home"; | |
| 9 | + | import { page } from "../lib/meta"; | |
| 9 | 10 | import { Landing } from "../components/landing"; | |
| 10 | 11 | import type { ShellData } from "../components/shell"; | |
| 11 | 12 | import { STAGE_LABEL, StageDots } from "../components/lifecycle"; | |
| 25 | 26 | ||
| 26 | 27 | const dollars = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`; | |
| 27 | 28 | ||
| 28 | − | export function meta({}: Route.MetaArgs) { | |
| 29 | − | return [ | |
| 30 | − | { title: "g1t — Git for AI scale" }, | |
| 31 | − | { | |
| 32 | − | name: "description", | |
| 33 | − | content: | |
| 34 | − | "A git forge for thousands of agents working on the same code at once: every change isolated, every decision recorded, every change landed in order. Open source, built on Cloudflare.", | |
| 35 | − | }, | |
| 36 | − | ]; | |
| 29 | + | export function meta(args: Route.MetaArgs) { | |
| 30 | + | return page(args, { | |
| 31 | + | title: "g1t — Git for AI scale", | |
| 32 | + | description: | |
| 33 | + | "A git forge for thousands of agents working on the same code at once: every change isolated, every decision recorded, every change landed in order. Open source, built on Cloudflare.", | |
| 34 | + | }); | |
| 37 | 35 | } | |
| 38 | 36 | ||
| 39 | 37 | export function headers({ loaderHeaders }: Route.HeadersArgs) { |
| 1 | 1 | import { Form, Link, redirect } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/login"; | |
| 4 | + | import { page } from "../lib/meta"; | |
| 4 | 5 | import { AuthCard } from "../components/auth-card"; | |
| 5 | 6 | import { Button, ErrorText, Field, Input } from "../components/ui"; | |
| 6 | 7 | import { identity } from "../lib/services.server"; | |
| 11 | 12 | startSession, | |
| 12 | 13 | } from "../lib/session.server"; | |
| 13 | 14 | ||
| 14 | − | export function meta({}: Route.MetaArgs) { | |
| 15 | − | return [{ title: "Sign in · g1t" }]; | |
| 15 | + | export function meta(args: Route.MetaArgs) { | |
| 16 | + | return page(args, { title: "Sign in · g1t" }); | |
| 16 | 17 | } | |
| 17 | 18 | ||
| 18 | 19 | export function loader({ request, context }: Route.LoaderArgs) { |
| 3 | 3 | import { Form, redirect, useNavigation } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import type { Route } from "./+types/new"; | |
| 6 | + | import { page } from "../lib/meta"; | |
| 6 | 7 | import { Button, ErrorText, Field, Input } from "../components/ui"; | |
| 7 | 8 | import { repos } from "../lib/services.server"; | |
| 8 | 9 | import { assertSameOrigin, requireUser } from "../lib/session.server"; | |
| 9 | 10 | ||
| 10 | − | export function meta({}: Route.MetaArgs) { | |
| 11 | − | return [{ title: "New project · g1t" }]; | |
| 11 | + | export function meta(args: Route.MetaArgs) { | |
| 12 | + | return page(args, { title: "New project · g1t" }); | |
| 12 | 13 | } | |
| 13 | 14 | ||
| 14 | 15 | export function loader({ request, context }: Route.LoaderArgs) { |
| 3 | 3 | import { DEPLOYMENTS_ALLOWANCE, MICROS_PER_DOLLAR, type Price } from "@g1t/contracts"; | |
| 4 | 4 | ||
| 5 | 5 | import type { Route } from "./+types/pricing"; | |
| 6 | + | import { page } from "../lib/meta"; | |
| 6 | 7 | import { TimeAgo } from "../components/ui"; | |
| 7 | 8 | import { billing } from "../lib/services.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta() { | |
| 10 | − | return [ | |
| 11 | − | { title: "Pricing · g1t" }, | |
| 12 | − | { | |
| 13 | − | name: "description", | |
| 14 | − | content: "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus a set markup. No seats.", | |
| 15 | − | }, | |
| 16 | − | ]; | |
| 10 | + | export function meta(args: Route.MetaArgs) { | |
| 11 | + | return page(args, { | |
| 12 | + | title: "Pricing · g1t", | |
| 13 | + | description: "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus a set markup. No seats.", | |
| 14 | + | }); | |
| 17 | 15 | } | |
| 18 | 16 | ||
| 19 | 17 | export async function loader() { |
| 1 | 1 | import { Form, Link, redirect } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/register"; | |
| 4 | + | import { page } from "../lib/meta"; | |
| 4 | 5 | import { AuthCard } from "../components/auth-card"; | |
| 5 | 6 | import { Button, ErrorText, Field, Input } from "../components/ui"; | |
| 6 | 7 | import { identity } from "../lib/services.server"; | |
| 11 | 12 | startSession, | |
| 12 | 13 | } from "../lib/session.server"; | |
| 13 | 14 | ||
| 14 | − | export function meta({}: Route.MetaArgs) { | |
| 15 | − | return [{ title: "Create an account · g1t" }]; | |
| 15 | + | export function meta(args: Route.MetaArgs) { | |
| 16 | + | return page(args, { title: "Create an account · g1t" }); | |
| 16 | 17 | } | |
| 17 | 18 | ||
| 18 | 19 | export function loader({ request, context }: Route.LoaderArgs) { |
| 5 | 5 | import type { Annotation, Job, StepState } from "@g1t/contracts"; | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/actions-run"; | |
| 8 | + | import { page } from "../../lib/meta"; | |
| 8 | 9 | import { LogText, Notes, StatusIcon, duration, shortRef, standingWord, useJobLog } from "../../components/actions"; | |
| 9 | 10 | import { Button, ErrorText, TimeAgo } from "../../components/ui"; | |
| 10 | 11 | import { listArtifacts } from "../../lib/artifacts.server"; | |
| 11 | 12 | import { actions } from "../../lib/services.server"; | |
| 12 | 13 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 13 | 14 | ||
| 14 | − | export function meta({ loaderData, params }: Route.MetaArgs) { | |
| 15 | + | export function meta({ loaderData, params, ...args }: Route.MetaArgs) { | |
| 15 | 16 | const run = loaderData?.detail.run; | |
| 16 | − | return [{ title: `${run ? `${run.title || run.name} #${run.number}` : "Run"} · ${params.owner}/${params.repo} · g1t` }]; | |
| 17 | + | return page(args, { title: `${run ? `${run.title || run.name} #${run.number}` : "Run"} · ${params.owner}/${params.repo} · g1t` }); | |
| 17 | 18 | } | |
| 18 | 19 | ||
| 19 | 20 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 6 | 6 | import type { DispatchInput, Workflow, WorkflowRun } from "@g1t/contracts"; | |
| 7 | 7 | ||
| 8 | 8 | import type { Route } from "./+types/actions"; | |
| 9 | + | import { page } from "../../lib/meta"; | |
| 9 | 10 | import { Notes, StatusIcon, duration, shortRef } from "../../components/actions"; | |
| 10 | 11 | import { Button, EmptyState, ErrorText, TimeAgo } from "../../components/ui"; | |
| 11 | 12 | import { actions } from "../../lib/services.server"; | |
| 12 | 13 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 13 | 14 | ||
| 14 | − | export function meta({ params }: Route.MetaArgs) { | |
| 15 | − | return [{ title: `Workflows · ${params.owner}/${params.repo} · g1t` }]; | |
| 15 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | + | return page(args, { title: `Workflows · ${params.owner}/${params.repo} · g1t` }); | |
| 16 | 17 | } | |
| 17 | 18 | ||
| 18 | 19 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 1 | 1 | import type { Route } from "./+types/blob"; | |
| 2 | + | import { page } from "../../lib/meta"; | |
| 2 | 3 | import { BlobView } from "../../components/repo-view"; | |
| 3 | 4 | import { highlight, highlightLines } from "../../lib/highlight.server"; | |
| 4 | 5 | import { repos } from "../../lib/services.server"; | |
| 5 | 6 | import { getViewer, unwrap } from "../../lib/session.server"; | |
| 6 | 7 | ||
| 7 | − | export function meta({ params }: Route.MetaArgs) { | |
| 8 | − | return [{ title: `${params["*"]} · ${params.owner}/${params.repo} · g1t` }]; | |
| 8 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 9 | + | return page(args, { title: `${params["*"]} · ${params.owner}/${params.repo} · g1t` }); | |
| 9 | 10 | } | |
| 10 | 11 | ||
| 11 | 12 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 6 | 6 | ||
| 7 | 7 | import { parseCommitMessage } from "../../lib/commit-message"; | |
| 8 | 8 | import type { Route } from "./+types/commit"; | |
| 9 | + | import { page } from "../../lib/meta"; | |
| 9 | 10 | import { DiffView } from "../../components/diff-view"; | |
| 10 | 11 | import { Avatar, TimeAgo } from "../../components/ui"; | |
| 11 | 12 | import { immutable } from "../../lib/immutable.server"; | |
| 17 | 18 | return { "Server-Timing": loaderHeaders.get("Server-Timing") ?? "" }; | |
| 18 | 19 | } | |
| 19 | 20 | ||
| 20 | − | export function meta({ loaderData, params }: Route.MetaArgs) { | |
| 21 | + | export function meta({ loaderData, params, ...args }: Route.MetaArgs) { | |
| 21 | 22 | const subject = loaderData?.commit.message.split("\n")[0]; | |
| 22 | − | return [ | |
| 23 | − | { title: `${subject ? `${subject} · ` : ""}${params.hash.slice(0, 7)} · ${params.owner}/${params.repo} · g1t` }, | |
| 24 | − | ]; | |
| 23 | + | return page(args, { | |
| 24 | + | title: `${subject ? `${subject} · ` : ""}${params.hash.slice(0, 7)} · ${params.owner}/${params.repo} · g1t`, | |
| 25 | + | }); | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 27 | 28 | /** How far back a short hash is looked for. */ |
| 4 | 4 | import type { Commit } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/commits"; | |
| 7 | + | import { page } from "../../lib/meta"; | |
| 7 | 8 | import { Avatar, EmptyState, TimeAgo } from "../../components/ui"; | |
| 8 | 9 | import { repos } from "../../lib/services.server"; | |
| 9 | 10 | import { getViewer, unwrap } from "../../lib/session.server"; | |
| 10 | 11 | ||
| 11 | 12 | const PAGE_SIZE = 50; | |
| 12 | 13 | ||
| 13 | − | export function meta({ params }: Route.MetaArgs) { | |
| 14 | − | return [{ title: `Commits · ${params.owner}/${params.repo} · g1t` }]; | |
| 14 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 15 | + | return page(args, { title: `Commits · ${params.owner}/${params.repo} · g1t` }); | |
| 15 | 16 | } | |
| 16 | 17 | ||
| 17 | 18 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 2 | 2 | import { Link, data } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/deployment"; | |
| 5 | + | import { page } from "../../lib/meta"; | |
| 5 | 6 | import { TimeAgo } from "../../components/ui"; | |
| 6 | 7 | import { deployments } from "../../lib/services.server"; | |
| 7 | 8 | import { getViewer, roleIn } from "../../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({ params }: Route.MetaArgs) { | |
| 10 | − | return [{ title: `Deployment · ${params.owner}/${params.repo} · g1t` }]; | |
| 10 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: `Deployment · ${params.owner}/${params.repo} · g1t` }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 7 | 7 | import { host, StatusDot } from "../../components/deploy"; | |
| 8 | 8 | ||
| 9 | 9 | import type { Route } from "./+types/deployments"; | |
| 10 | + | import { page } from "../../lib/meta"; | |
| 10 | 11 | import { Button, ButtonLink, EmptyState, ErrorText, TimeAgo } from "../../components/ui"; | |
| 11 | 12 | import { billing, deployments } from "../../lib/services.server"; | |
| 12 | 13 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 13 | 14 | ||
| 14 | − | export function meta({ params }: Route.MetaArgs) { | |
| 15 | − | return [{ title: `Deployments · ${params.owner}/${params.repo} · g1t` }]; | |
| 15 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | + | return page(args, { title: `Deployments · ${params.owner}/${params.repo} · g1t` }); | |
| 16 | 17 | } | |
| 17 | 18 | ||
| 18 | 19 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 4 | 4 | import { PROVIDERS } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/issue-new"; | |
| 7 | + | import { page } from "../../lib/meta"; | |
| 7 | 8 | import { Button, ErrorText, Field, Input, Textarea } from "../../components/ui"; | |
| 8 | 9 | import { Label } from "../../components/work"; | |
| 9 | 10 | import { integrations, work } from "../../lib/services.server"; | |
| 10 | 11 | import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server"; | |
| 11 | 12 | ||
| 12 | − | export function meta({ params }: Route.MetaArgs) { | |
| 13 | − | return [{ title: `New issue · ${params.owner}/${params.repo} · g1t` }]; | |
| 13 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 14 | + | return page(args, { title: `New issue · ${params.owner}/${params.repo} · g1t` }); | |
| 14 | 15 | } | |
| 15 | 16 | ||
| 16 | 17 | export async function loader({ request, params, context }: Route.LoaderArgs) { |
| 6 | 6 | import { type Pull, PROVIDERS } from "@g1t/contracts"; | |
| 7 | 7 | ||
| 8 | 8 | import type { Route } from "./+types/issue"; | |
| 9 | + | import { excerpt, page } from "../../lib/meta"; | |
| 9 | 10 | import { Markdown } from "../../components/markdown"; | |
| 10 | 11 | import { | |
| 11 | 12 | Avatar, | |
| 35 | 36 | ||
| 36 | 37 | const REFRESH_MS = 4000; | |
| 37 | 38 | ||
| 38 | − | export function meta({ loaderData, params }: Route.MetaArgs) { | |
| 39 | − | const title = loaderData ? `${loaderData.issue.title} · Issue #${loaderData.issue.number} · ` : ""; | |
| 40 | − | return [{ title: `${title}${params.owner}/${params.repo} · g1t` }]; | |
| 39 | + | export function meta({ loaderData, params, ...args }: Route.MetaArgs) { | |
| 40 | + | const issue = loaderData?.issue; | |
| 41 | + | const title = issue ? `${issue.title} · Issue #${issue.number} · ` : ""; | |
| 42 | + | const state = issue?.state === "open" ? "Open" : issue?.reason === "not_planned" ? "Closed as not planned" : "Closed"; | |
| 43 | + | const body = excerpt(issue?.body); | |
| 44 | + | return page(args, { | |
| 45 | + | title: `${title}${params.owner}/${params.repo} · g1t`, | |
| 46 | + | description: issue | |
| 47 | + | ? `${state} issue #${issue.number} on ${params.owner}/${params.repo}, opened by ${issue.author.username}.${body ? ` ${body}` : ""}` | |
| 48 | + | : null, | |
| 49 | + | // The card shows the title and the state. | |
| 50 | + | version: issue ? [issue.title, issue.state, issue.reason] : undefined, | |
| 51 | + | type: "article", | |
| 52 | + | }); | |
| 41 | 53 | } | |
| 42 | 54 | ||
| 43 | 55 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 3 | 3 | import { Form, Link, useNavigation } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import type { Route } from "./+types/issues"; | |
| 6 | + | import { page } from "../../lib/meta"; | |
| 6 | 7 | import { Button, ButtonLink, EmptyState, ErrorText, TimeAgo } from "../../components/ui"; | |
| 7 | 8 | import { | |
| 8 | 9 | Assignee, | |
| 17 | 18 | /** As many sandboxes as one request may start. */ | |
| 18 | 19 | const MAX_ASSIGNED_AT_ONCE = 10; | |
| 19 | 20 | ||
| 20 | − | export function meta({ params }: Route.MetaArgs) { | |
| 21 | − | return [{ title: `Issues · ${params.owner}/${params.repo} · g1t` }]; | |
| 21 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 22 | + | return page(args, { title: `Issues · ${params.owner}/${params.repo} · g1t` }); | |
| 22 | 23 | } | |
| 23 | 24 | ||
| 24 | 25 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 4 | 4 | import type { Project } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/layout"; | |
| 7 | + | import { page } from "../../lib/meta"; | |
| 7 | 8 | import { Pill, TabLink as Tab } from "../../components/ui"; | |
| 8 | 9 | import { projects, repos, work } from "../../lib/services.server"; | |
| 9 | 10 | import { getViewer, roleIn, unwrap } from "../../lib/session.server"; | |
| 10 | 11 | ||
| 11 | − | export function meta({ loaderData: loaded, params }: Route.MetaArgs) { | |
| 12 | − | return [{ title: `${loaded?.project?.name ?? params.repo} · ${params.owner} · g1t` }]; | |
| 12 | + | export function meta({ loaderData: loaded, params, ...args }: Route.MetaArgs) { | |
| 13 | + | return page(args, { title: `${loaded?.project?.name ?? params.repo} · ${params.owner} · g1t` }); | |
| 13 | 14 | } | |
| 14 | 15 | ||
| 15 | 16 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 5 | 5 | import { Form, Link, data, useNavigation, useRevalidator } from "react-router"; | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/plan"; | |
| 8 | + | import { page } from "../../lib/meta"; | |
| 8 | 9 | import { Markdown } from "../../components/markdown"; | |
| 9 | 10 | import { Activity, Exchanges } from "../../components/activity"; | |
| 10 | 11 | import { Outcome } from "../../components/outcome"; | |
| 21 | 22 | ||
| 22 | 23 | const REFRESH_MS = 4000; | |
| 23 | 24 | ||
| 24 | − | export function meta({ params }: Route.MetaArgs) { | |
| 25 | − | return [{ title: `Plan · ${params.owner}/${params.repo} · g1t` }]; | |
| 25 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 26 | + | return page(args, { title: `Plan · ${params.owner}/${params.repo} · g1t` }); | |
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | 29 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 5 | 5 | import type { Plan } from "@g1t/contracts"; | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/plans"; | |
| 8 | + | import { page } from "../../lib/meta"; | |
| 8 | 9 | import { Button, EmptyState, ErrorText, Textarea, TimeAgo } from "../../components/ui"; | |
| 9 | 10 | import { work } from "../../lib/services.server"; | |
| 10 | 11 | import { | |
| 15 | 16 | unwrap, | |
| 16 | 17 | } from "../../lib/session.server"; | |
| 17 | 18 | ||
| 18 | − | export function meta({ params }: Route.MetaArgs) { | |
| 19 | − | return [{ title: `Plan · ${params.owner}/${params.repo} · g1t` }]; | |
| 19 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 20 | + | return page(args, { title: `Plan · ${params.owner}/${params.repo} · g1t` }); | |
| 20 | 21 | } | |
| 21 | 22 | ||
| 22 | 23 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 2 | 2 | import { Form, redirect } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/pull-new"; | |
| 5 | + | import { page } from "../../lib/meta"; | |
| 5 | 6 | import { | |
| 6 | 7 | Button, | |
| 7 | 8 | CopyLine, | |
| 14 | 15 | import { repos, work } from "../../lib/services.server"; | |
| 15 | 16 | import { assertSameOrigin, requireUser, unwrap } from "../../lib/session.server"; | |
| 16 | 17 | ||
| 17 | − | export function meta({ params }: Route.MetaArgs) { | |
| 18 | − | return [{ title: `New pull request · ${params.owner}/${params.repo} · g1t` }]; | |
| 18 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 19 | + | return page(args, { title: `New pull request · ${params.owner}/${params.repo} · g1t` }); | |
| 19 | 20 | } | |
| 20 | 21 | ||
| 21 | 22 | export async function loader({ request, params, context }: Route.LoaderArgs) { |
| 36 | 36 | } from "@g1t/contracts"; | |
| 37 | 37 | ||
| 38 | 38 | import type { Route } from "./+types/pull"; | |
| 39 | + | import { excerpt, page } from "../../lib/meta"; | |
| 39 | 40 | import { DiffView } from "../../components/diff-view"; | |
| 40 | 41 | import { LifecyclePanel } from "../../components/lifecycle"; | |
| 41 | 42 | import { Markdown } from "../../components/markdown"; | |
| 68 | 69 | const TABS = ["conversation", "session", "changes"] as const; | |
| 69 | 70 | type Tab = (typeof TABS)[number]; | |
| 70 | 71 | ||
| 71 | − | export function meta({ loaderData, params }: Route.MetaArgs) { | |
| 72 | − | const title = loaderData | |
| 73 | − | ? `${loaderData.pull.title} · Pull request #${loaderData.pull.number} · ` | |
| 74 | − | : ""; | |
| 75 | − | return [{ title: `${title}${params.owner}/${params.repo} · g1t` }]; | |
| 72 | + | export function meta({ loaderData, params, ...args }: Route.MetaArgs) { | |
| 73 | + | const pull = loaderData?.pull; | |
| 74 | + | const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : ""; | |
| 75 | + | const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"]; | |
| 76 | + | const body = excerpt(pull?.body); | |
| 77 | + | return page(args, { | |
| 78 | + | title: `${title}${params.owner}/${params.repo} · g1t`, | |
| 79 | + | description: pull | |
| 80 | + | ? `${state} pull request #${pull.number} on ${params.owner}/${params.repo} by ${pull.author.username}.${body ? ` ${body}` : ""}` | |
| 81 | + | : null, | |
| 82 | + | // The card shows the title and the state. | |
| 83 | + | version: pull ? [pull.title, pull.status] : undefined, | |
| 84 | + | type: "article", | |
| 85 | + | }); | |
| 76 | 86 | } | |
| 77 | 87 | ||
| 78 | 88 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 2 | 2 | import { Link } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/pulls"; | |
| 5 | + | import { page } from "../../lib/meta"; | |
| 5 | 6 | import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui"; | |
| 6 | 7 | import { CheckBadge } from "../../components/checks"; | |
| 7 | 8 | import { ChangeSize, PullIcon, StateTabs } from "../../components/work"; | |
| 8 | 9 | import { work } from "../../lib/services.server"; | |
| 9 | 10 | import { getViewer, unwrap } from "../../lib/session.server"; | |
| 10 | 11 | ||
| 11 | − | export function meta({ params }: Route.MetaArgs) { | |
| 12 | − | return [{ title: `Pull requests · ${params.owner}/${params.repo} · g1t` }]; | |
| 12 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 13 | + | return page(args, { title: `Pull requests · ${params.owner}/${params.repo} · g1t` }); | |
| 13 | 14 | } | |
| 14 | 15 | ||
| 15 | 16 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 14 | 14 | import type { QueueEntry, QueueState } from "@g1t/contracts"; | |
| 15 | 15 | ||
| 16 | 16 | import type { Route } from "./+types/queue"; | |
| 17 | + | import { page } from "../../lib/meta"; | |
| 17 | 18 | import { Avatar, ButtonLink, EmptyState, TimeAgo } from "../../components/ui"; | |
| 18 | 19 | import { repos, work } from "../../lib/services.server"; | |
| 19 | 20 | import { getViewer, roleIn, unwrap } from "../../lib/session.server"; | |
| 20 | 21 | ||
| 21 | 22 | const REFRESH_MS = 4000; | |
| 22 | 23 | ||
| 23 | − | export function meta({ params }: Route.MetaArgs) { | |
| 24 | − | return [{ title: `Merge queue · ${params.owner}/${params.repo} · g1t` }]; | |
| 24 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 25 | + | return page(args, { title: `Merge queue · ${params.owner}/${params.repo} · g1t` }); | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 27 | 28 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 1 | 1 | import type { Route } from "./+types/secrets"; | |
| 2 | + | import { page } from "../../lib/meta"; | |
| 2 | 3 | import { RepoSettingsTabs } from "../../components/repo-settings-tabs"; | |
| 3 | 4 | import { SecretsPanel } from "../../components/secrets"; | |
| 4 | 5 | import { actOnSecrets, loadSecrets } from "../../lib/secrets.server"; | |
| 5 | 6 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; | |
| 6 | 7 | ||
| 7 | − | export function meta({ params }: Route.MetaArgs) { | |
| 8 | − | return [{ title: `Secrets and variables · ${params.owner}/${params.repo} · g1t` }]; | |
| 8 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 9 | + | return page(args, { title: `Secrets and variables · ${params.owner}/${params.repo} · g1t` }); | |
| 9 | 10 | } | |
| 10 | 11 | ||
| 11 | 12 | const ownerOf = (params: { owner: string; repo: string }) => ({ repo: { namespace: params.owner, name: params.repo } }); |
| 4 | 4 | import type { DependencyLink } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/settings-dependencies"; | |
| 7 | + | import { page } from "../../lib/meta"; | |
| 7 | 8 | import { RepoSettingsTabs } from "../../components/repo-settings-tabs"; | |
| 8 | 9 | import { Button, EmptyState, ErrorText, Field, Input } from "../../components/ui"; | |
| 9 | 10 | import { projects } from "../../lib/services.server"; | |
| 10 | 11 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 11 | 12 | ||
| 12 | − | export function meta({ params }: Route.MetaArgs) { | |
| 13 | − | return [{ title: `Dependencies · ${params.owner}/${params.repo} · g1t` }]; | |
| 13 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 14 | + | return page(args, { title: `Dependencies · ${params.owner}/${params.repo} · g1t` }); | |
| 14 | 15 | } | |
| 15 | 16 | ||
| 16 | 17 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 1 | 1 | import { Form, Link, data, useNavigation } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/settings-deployments"; | |
| 4 | + | import { page } from "../../lib/meta"; | |
| 4 | 5 | import { RepoSettingsTabs } from "../../components/repo-settings-tabs"; | |
| 5 | 6 | import { Button, ErrorText, Field, Input } from "../../components/ui"; | |
| 6 | 7 | import { deployments } from "../../lib/services.server"; | |
| 7 | 8 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({ params }: Route.MetaArgs) { | |
| 10 | − | return [{ title: `Deployment settings · ${params.owner}/${params.repo} · g1t` }]; | |
| 10 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: `Deployment settings · ${params.owner}/${params.repo} · g1t` }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 3 | 3 | ||
| 4 | 4 | import { RepoSettingsTabs } from "../../components/repo-settings-tabs"; | |
| 5 | 5 | import type { Route } from "./+types/settings-repository"; | |
| 6 | + | import { page } from "../../lib/meta"; | |
| 6 | 7 | import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui"; | |
| 7 | 8 | import { repos, work } from "../../lib/services.server"; | |
| 8 | 9 | import { | |
| 13 | 14 | unwrap, | |
| 14 | 15 | } from "../../lib/session.server"; | |
| 15 | 16 | ||
| 16 | − | export function meta({ params }: Route.MetaArgs) { | |
| 17 | − | return [{ title: `Repository settings · ${params.owner}/${params.repo} · g1t` }]; | |
| 17 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 18 | + | return page(args, { title: `Repository settings · ${params.owner}/${params.repo} · g1t` }); | |
| 18 | 19 | } | |
| 19 | 20 | ||
| 20 | 21 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 2 | 2 | import { Form, Link, data, useNavigation } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/settings"; | |
| 5 | + | import { page } from "../../lib/meta"; | |
| 5 | 6 | import { RepoSettingsTabs } from "../../components/repo-settings-tabs"; | |
| 6 | 7 | import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui"; | |
| 7 | 8 | import { projects } from "../../lib/services.server"; | |
| 8 | 9 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 9 | 10 | ||
| 10 | − | export function meta({ params }: Route.MetaArgs) { | |
| 11 | − | return [{ title: `Settings · ${params.owner}/${params.repo} · g1t` }]; | |
| 11 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 12 | + | return page(args, { title: `Settings · ${params.owner}/${params.repo} · g1t` }); | |
| 12 | 13 | } | |
| 13 | 14 | ||
| 14 | 15 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 2 | 2 | import { Link, data } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/soon"; | |
| 5 | + | import { page } from "../../lib/meta"; | |
| 5 | 6 | import { ROADMAP, roadmapIn, roadmapItem } from "../../lib/roadmap"; | |
| 6 | 7 | ||
| 7 | − | export function meta({ params }: Route.MetaArgs) { | |
| 8 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 8 | 9 | const item = roadmapItem(params.feature); | |
| 9 | − | return [{ title: `${item?.title ?? "Soon"} · ${params.owner}/${params.repo} · g1t` }]; | |
| 10 | + | return page(args, { | |
| 11 | + | title: `${item?.title ?? "Soon"} · ${params.owner}/${params.repo} · g1t`, | |
| 12 | + | description: item ? `Soon on g1t: ${item.summary}` : null, | |
| 13 | + | version: item ? [item.title, item.summary] : undefined, | |
| 14 | + | }); | |
| 10 | 15 | } | |
| 11 | 16 | ||
| 12 | 17 | export function loader({ params }: Route.LoaderArgs) { |
| 1 | 1 | import type { Route } from "./+types/tree"; | |
| 2 | + | import { page } from "../../lib/meta"; | |
| 2 | 3 | import { TreeView } from "../../components/repo-view"; | |
| 3 | 4 | import { repos } from "../../lib/services.server"; | |
| 4 | 5 | import { getViewer, unwrap } from "../../lib/session.server"; | |
| 5 | 6 | ||
| 6 | − | export function meta({ params }: Route.MetaArgs) { | |
| 7 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 7 | 8 | const path = params["*"] ? `${params["*"]} · ` : ""; | |
| 8 | − | return [{ title: `${path}${params.owner}/${params.repo} · g1t` }]; | |
| 9 | + | return page(args, { title: `${path}${params.owner}/${params.repo} · g1t` }); | |
| 9 | 10 | } | |
| 10 | 11 | ||
| 11 | 12 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 1 | 1 | import type { Route } from "./+types/webhooks"; | |
| 2 | + | import { page } from "../../lib/meta"; | |
| 2 | 3 | import { RepoSettingsTabs } from "../../components/repo-settings-tabs"; | |
| 3 | 4 | import { WebhooksPanel } from "../../components/webhooks"; | |
| 4 | 5 | import { actOnWebhooks, loadWebhooks } from "../../lib/webhooks.server"; | |
| 5 | 6 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; | |
| 6 | 7 | ||
| 7 | − | export function meta({ params }: Route.MetaArgs) { | |
| 8 | − | return [{ title: `Webhooks · ${params.owner}/${params.repo} · g1t` }]; | |
| 8 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 9 | + | return page(args, { title: `Webhooks · ${params.owner}/${params.repo} · g1t` }); | |
| 9 | 10 | } | |
| 10 | 11 | ||
| 11 | 12 | function ownerOf(params: { owner: string; repo: string }) { |
| 1 | 1 | import { Form, Link, redirect } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/reset"; | |
| 4 | + | import { page } from "../lib/meta"; | |
| 4 | 5 | import { AuthCard } from "../components/auth-card"; | |
| 5 | 6 | import { Button, ErrorText, Field, Input } from "../components/ui"; | |
| 6 | 7 | import { identity } from "../lib/services.server"; | |
| 7 | 8 | import { assertSameOrigin } from "../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({}: Route.MetaArgs) { | |
| 10 | − | return [{ title: "Choose a new password · g1t" }]; | |
| 10 | + | export function meta(args: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: "Choose a new password · g1t" }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | export function loader({ request }: Route.LoaderArgs) { |
| 2 | 2 | import { Form, Link } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/settings"; | |
| 5 | + | import { page } from "../lib/meta"; | |
| 5 | 6 | import { Button, ErrorText, Field, Input, TimeAgo } from "../components/ui"; | |
| 6 | 7 | import { assertSameOrigin, requireUser } from "../lib/session.server"; | |
| 7 | 8 | ||
| 8 | − | export function meta({}: Route.MetaArgs) { | |
| 9 | − | return [{ title: "Settings · g1t" }]; | |
| 9 | + | export function meta(args: Route.MetaArgs) { | |
| 10 | + | return page(args, { title: "Settings · g1t" }); | |
| 10 | 11 | } | |
| 11 | 12 | ||
| 12 | 13 | export async function loader({ request, context }: Route.LoaderArgs) { |
| 2 | 2 | import { redirect } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/verify"; | |
| 5 | + | import { page } from "../lib/meta"; | |
| 5 | 6 | import { ButtonLink } from "../components/ui"; | |
| 6 | 7 | import { identity } from "../lib/services.server"; | |
| 7 | 8 | import { assertSameOrigin, requireUser } from "../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({}: Route.MetaArgs) { | |
| 10 | − | return [{ title: "Confirm your email · g1t" }]; | |
| 10 | + | export function meta(args: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: "Confirm your email · g1t" }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | /** Follows the link from the confirmation email. */ |
| 12 | 12 | } from "@g1t/contracts"; | |
| 13 | 13 | ||
| 14 | 14 | import type { Route } from "./+types/billing"; | |
| 15 | + | import { page } from "../../lib/meta"; | |
| 15 | 16 | import { Button, EmptyState, ErrorText, TimeAgo } from "../../components/ui"; | |
| 16 | 17 | import { billing, deployments } from "../../lib/services.server"; | |
| 17 | 18 | import { | |
| 25 | 26 | /** What can be added in one payment, in dollars. */ | |
| 26 | 27 | const AMOUNTS = [10, 25, 50, 100]; | |
| 27 | 28 | ||
| 28 | − | export function meta({ params }: Route.MetaArgs) { | |
| 29 | − | return [{ title: `Billing · ${params.owner} · g1t` }]; | |
| 29 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 30 | + | return page(args, { title: `Billing · ${params.owner} · g1t` }); | |
| 30 | 31 | } | |
| 31 | 32 | ||
| 32 | 33 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 23 | 23 | } from "@g1t/contracts"; | |
| 24 | 24 | ||
| 25 | 25 | import type { Route } from "./+types/integrations"; | |
| 26 | + | import { page } from "../../lib/meta"; | |
| 26 | 27 | import { MODEL_CATALOG, ModelCatalog, ModelProviderFields, ProviderMark, ProviderTiles, Routing } from "../../components/model-providers"; | |
| 27 | 28 | import { Button, CopyLine, ErrorText, Field, Input, TimeAgo } from "../../components/ui"; | |
| 28 | 29 | import { billing, integrations, repos } from "../../lib/services.server"; | |
| 29 | 30 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 30 | 31 | ||
| 31 | − | export function meta({ params }: Route.MetaArgs) { | |
| 32 | − | return [{ title: `Integrations · ${params.owner} · g1t` }]; | |
| 32 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 33 | + | return page(args, { title: `Integrations · ${params.owner} · g1t` }); | |
| 33 | 34 | } | |
| 34 | 35 | ||
| 35 | 36 | const isProvider = (value: string | null): value is Provider => value != null && value in PROVIDERS; |
| 2 | 2 | import { Outlet, data, useLocation, useRouteLoaderData } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/layout"; | |
| 5 | + | import { page } from "../../lib/meta"; | |
| 5 | 6 | import { Avatar, ButtonLink, Pill, TabLink } from "../../components/ui"; | |
| 6 | 7 | import { identity } from "../../lib/services.server"; | |
| 7 | 8 | import { getViewer, roleIn } from "../../lib/session.server"; | |
| 8 | 9 | ||
| 9 | − | export function meta({ loaderData, params }: Route.MetaArgs) { | |
| 10 | − | return [{ title: `${loaderData?.workspace.name ?? params.owner} · g1t` }]; | |
| 10 | + | export function meta({ loaderData, params, ...args }: Route.MetaArgs) { | |
| 11 | + | return page(args, { title: `${loaderData?.workspace.name ?? params.owner} · g1t` }); | |
| 11 | 12 | } | |
| 12 | 13 | ||
| 13 | 14 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 1 | 1 | import { Form, redirect } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/new"; | |
| 4 | + | import { page } from "../../lib/meta"; | |
| 4 | 5 | import { Button, ErrorText, Field, Input } from "../../components/ui"; | |
| 5 | 6 | import { identity } from "../../lib/services.server"; | |
| 6 | 7 | import { assertSameOrigin, nextPath, requireUser } from "../../lib/session.server"; | |
| 7 | 8 | ||
| 8 | − | export function meta({}: Route.MetaArgs) { | |
| 9 | − | return [{ title: "New workspace · g1t" }]; | |
| 9 | + | export function meta(args: Route.MetaArgs) { | |
| 10 | + | return page(args, { title: "New workspace · g1t" }); | |
| 10 | 11 | } | |
| 11 | 12 | ||
| 12 | 13 | export function loader({ request, context }: Route.LoaderArgs) { |
| 1 | 1 | import { Form } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/people"; | |
| 4 | + | import { page } from "../../lib/meta"; | |
| 4 | 5 | import { Avatar, Button, ErrorText, Field, Input, Pill } from "../../components/ui"; | |
| 5 | 6 | import { identity } from "../../lib/services.server"; | |
| 6 | 7 | import { | |
| 11 | 12 | unwrap, | |
| 12 | 13 | } from "../../lib/session.server"; | |
| 13 | 14 | ||
| 14 | − | export function meta({ params }: Route.MetaArgs) { | |
| 15 | − | return [{ title: `People · ${params.owner} · g1t` }]; | |
| 15 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | + | return page(args, { title: `People · ${params.owner} · g1t` }); | |
| 16 | 17 | } | |
| 17 | 18 | ||
| 18 | 19 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 1 | 1 | import type { Route } from "./+types/secrets"; | |
| 2 | + | import { page } from "../../lib/meta"; | |
| 2 | 3 | import { SecretsPanel } from "../../components/secrets"; | |
| 3 | 4 | import { actOnSecrets, loadSecrets } from "../../lib/secrets.server"; | |
| 4 | 5 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; | |
| 5 | 6 | ||
| 6 | − | export function meta({ params }: Route.MetaArgs) { | |
| 7 | − | return [{ title: `Secrets and variables · ${params.owner} · g1t` }]; | |
| 7 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 8 | + | return page(args, { title: `Secrets and variables · ${params.owner} · g1t` }); | |
| 8 | 9 | } | |
| 9 | 10 | ||
| 10 | 11 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 1 | 1 | import { Form, data, redirect } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/settings"; | |
| 4 | + | import { page } from "../../lib/meta"; | |
| 4 | 5 | import { Button, ErrorText, Field, Input } from "../../components/ui"; | |
| 5 | 6 | import { identity } from "../../lib/services.server"; | |
| 6 | 7 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; | |
| 7 | 8 | ||
| 8 | − | export function meta({ params }: Route.MetaArgs) { | |
| 9 | − | return [{ title: `Settings · ${params.owner} · g1t` }]; | |
| 9 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 10 | + | return page(args, { title: `Settings · ${params.owner} · g1t` }); | |
| 10 | 11 | } | |
| 11 | 12 | ||
| 12 | 13 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 1 | 1 | import { Form } from "react-router"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Route } from "./+types/tokens"; | |
| 4 | + | import { page } from "../../lib/meta"; | |
| 4 | 5 | import { | |
| 5 | 6 | Button, | |
| 6 | 7 | CopyLine, | |
| 19 | 20 | unwrap, | |
| 20 | 21 | } from "../../lib/session.server"; | |
| 21 | 22 | ||
| 22 | − | export function meta({ params }: Route.MetaArgs) { | |
| 23 | − | return [{ title: `Access tokens · ${params.owner} · g1t` }]; | |
| 23 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 24 | + | return page(args, { title: `Access tokens · ${params.owner} · g1t` }); | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | 27 | export async function loader({ params, context }: Route.LoaderArgs) { |
| 4 | 4 | import { MICROS_PER_DOLLAR, type UsageSlice } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/usage"; | |
| 7 | + | import { page } from "../../lib/meta"; | |
| 7 | 8 | import { ButtonLink } from "../../components/ui"; | |
| 8 | 9 | import { billing } from "../../lib/services.server"; | |
| 9 | 10 | import { getViewer, roleIn, unwrap } from "../../lib/session.server"; | |
| 34 | 35 | return new Date(day.getTime() - (days - 1) * 86_400_000); | |
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | − | export function meta({ params }: Route.MetaArgs) { | |
| 38 | − | return [{ title: `Usage · ${params.owner} · g1t` }]; | |
| 38 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 39 | + | return page(args, { title: `Usage · ${params.owner} · g1t` }); | |
| 39 | 40 | } | |
| 40 | 41 | ||
| 41 | 42 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 1 | 1 | import type { Route } from "./+types/webhooks"; | |
| 2 | + | import { page } from "../../lib/meta"; | |
| 2 | 3 | import { WebhooksPanel } from "../../components/webhooks"; | |
| 3 | 4 | import { actOnWebhooks, loadWebhooks } from "../../lib/webhooks.server"; | |
| 4 | 5 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; | |
| 5 | 6 | ||
| 6 | − | export function meta({ params }: Route.MetaArgs) { | |
| 7 | − | return [{ title: `Webhooks · ${params.owner} · g1t` }]; | |
| 7 | + | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 8 | + | return page(args, { title: `Webhooks · ${params.owner} · g1t` }); | |
| 8 | 9 | } | |
| 9 | 10 | ||
| 10 | 11 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
Binary or large file; its contents are not shown.
| 1046 | 1046 | | `services/billing` | Rust | Worker + D1 + Stripe | Each workspace's agent credit: payments, the ledger of every run, and the gate on starting one | | |
| 1047 | 1047 | | `services/events` | Rust | Worker + Queues + D1 | The event bus: durable log, and one queue per subscribing service | | |
| 1048 | 1048 | | `services/runner`, `crates/runner` | TypeScript, Rust | Worker + Containers | Starts a sandbox per g1t agent; the program inside runs the agent harness and reports through the public API | | |
| 1049 | + | | `services/og` | TypeScript | Worker + Cache API | Social cards at `og.g1t.sh`: one PNG per page of the site and the docs (satori and resvg), looked up as an anonymous visitor, so nothing private appears on one | | |
| 1049 | 1050 | | `apps/web` | TypeScript | Worker | Server-rendered site. Holds no data; calls services over RPC. | | |
| 1050 | 1051 | | `apps/docs` | TypeScript | Worker (static) | Documentation and the API explorer | | |
| 1051 | 1052 | | `apps/api` | Rust | Worker | REST API (`api.g1t.sh`), MCP server (`mcp.g1t.sh`) and OpenAPI document, all generated from one list of operations; the OAuth endpoints | |
| 1617 | 1617 | "resolved": "services/models", | |
| 1618 | 1618 | "link": true | |
| 1619 | 1619 | }, | |
| 1620 | + | "node_modules/@g1t/og": { | |
| 1621 | + | "resolved": "services/og", | |
| 1622 | + | "link": true | |
| 1623 | + | }, | |
| 1620 | 1624 | "node_modules/@g1t/pages": { | |
| 1621 | 1625 | "resolved": "services/pages", | |
| 1622 | 1626 | "link": true | |
| 4017 | 4021 | "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", | |
| 4018 | 4022 | "license": "MIT" | |
| 4019 | 4023 | }, | |
| 4024 | + | "node_modules/@resvg/resvg-wasm": { | |
| 4025 | + | "version": "2.6.2", | |
| 4026 | + | "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.6.2.tgz", | |
| 4027 | + | "integrity": "sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==", | |
| 4028 | + | "license": "MPL-2.0", | |
| 4029 | + | "engines": { | |
| 4030 | + | "node": ">= 10" | |
| 4031 | + | } | |
| 4032 | + | }, | |
| 4020 | 4033 | "node_modules/@rolldown/binding-android-arm-eabi": { | |
| 4021 | 4034 | "version": "1.2.12", | |
| 4022 | 4035 | "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", | |
| 4381 | 4394 | "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", | |
| 4382 | 4395 | "license": "MIT" | |
| 4383 | 4396 | }, | |
| 4397 | + | "node_modules/@shuding/opentype.js": { | |
| 4398 | + | "version": "1.4.0-beta.0", | |
| 4399 | + | "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", | |
| 4400 | + | "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", | |
| 4401 | + | "license": "MIT", | |
| 4402 | + | "dependencies": { | |
| 4403 | + | "fflate": "^0.7.3", | |
| 4404 | + | "string.prototype.codepointat": "^0.2.1" | |
| 4405 | + | }, | |
| 4406 | + | "bin": { | |
| 4407 | + | "ot": "bin/ot" | |
| 4408 | + | }, | |
| 4409 | + | "engines": { | |
| 4410 | + | "node": ">= 8.0.0" | |
| 4411 | + | } | |
| 4412 | + | }, | |
| 4384 | 4413 | "node_modules/@sindresorhus/is": { | |
| 4385 | 4414 | "version": "7.2.0", | |
| 4386 | 4415 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", | |
| 5028 | 5057 | "url": "https://github.com/sponsors/wooorm" | |
| 5029 | 5058 | } | |
| 5030 | 5059 | }, | |
| 5060 | + | "node_modules/base64-js": { | |
| 5061 | + | "version": "0.0.8", | |
| 5062 | + | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", | |
| 5063 | + | "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", | |
| 5064 | + | "license": "MIT", | |
| 5065 | + | "engines": { | |
| 5066 | + | "node": ">= 0.4" | |
| 5067 | + | } | |
| 5068 | + | }, | |
| 5031 | 5069 | "node_modules/baseline-browser-mapping": { | |
| 5032 | 5070 | "version": "2.11.27", | |
| 5033 | 5071 | "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", | |
| 5113 | 5151 | "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" | |
| 5114 | 5152 | } | |
| 5115 | 5153 | }, | |
| 5154 | + | "node_modules/camelize": { | |
| 5155 | + | "version": "1.0.1", | |
| 5156 | + | "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", | |
| 5157 | + | "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", | |
| 5158 | + | "license": "MIT", | |
| 5159 | + | "funding": { | |
| 5160 | + | "url": "https://github.com/sponsors/ljharb" | |
| 5161 | + | } | |
| 5162 | + | }, | |
| 5116 | 5163 | "node_modules/caniuse-lite": { | |
| 5117 | 5164 | "version": "1.0.30001814", | |
| 5118 | 5165 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", | |
| 5235 | 5282 | "node": ">=6" | |
| 5236 | 5283 | } | |
| 5237 | 5284 | }, | |
| 5285 | + | "node_modules/color-name": { | |
| 5286 | + | "version": "1.1.4", | |
| 5287 | + | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", | |
| 5288 | + | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", | |
| 5289 | + | "license": "MIT" | |
| 5290 | + | }, | |
| 5238 | 5291 | "node_modules/comma-separated-tokens": { | |
| 5239 | 5292 | "version": "2.0.3", | |
| 5240 | 5293 | "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", | |
| 5306 | 5359 | "uncrypto": "^0.1.3" | |
| 5307 | 5360 | } | |
| 5308 | 5361 | }, | |
| 5362 | + | "node_modules/css-background-parser": { | |
| 5363 | + | "version": "0.1.0", | |
| 5364 | + | "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", | |
| 5365 | + | "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", | |
| 5366 | + | "license": "MIT" | |
| 5367 | + | }, | |
| 5368 | + | "node_modules/css-box-shadow": { | |
| 5369 | + | "version": "1.0.0-3", | |
| 5370 | + | "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", | |
| 5371 | + | "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", | |
| 5372 | + | "license": "MIT" | |
| 5373 | + | }, | |
| 5374 | + | "node_modules/css-color-keywords": { | |
| 5375 | + | "version": "1.0.0", | |
| 5376 | + | "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", | |
| 5377 | + | "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", | |
| 5378 | + | "license": "ISC", | |
| 5379 | + | "engines": { | |
| 5380 | + | "node": ">=4" | |
| 5381 | + | } | |
| 5382 | + | }, | |
| 5383 | + | "node_modules/css-gradient-parser": { | |
| 5384 | + | "version": "0.0.17", | |
| 5385 | + | "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz", | |
| 5386 | + | "integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==", | |
| 5387 | + | "license": "MIT", | |
| 5388 | + | "engines": { | |
| 5389 | + | "node": ">=16" | |
| 5390 | + | } | |
| 5391 | + | }, | |
| 5309 | 5392 | "node_modules/css-select": { | |
| 5310 | 5393 | "version": "6.0.0", | |
| 5311 | 5394 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", | |
| 5338 | 5421 | ], | |
| 5339 | 5422 | "license": "MIT" | |
| 5340 | 5423 | }, | |
| 5424 | + | "node_modules/css-to-react-native": { | |
| 5425 | + | "version": "3.2.0", | |
| 5426 | + | "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", | |
| 5427 | + | "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", | |
| 5428 | + | "license": "MIT", | |
| 5429 | + | "dependencies": { | |
| 5430 | + | "camelize": "^1.0.0", | |
| 5431 | + | "css-color-keywords": "^1.0.0", | |
| 5432 | + | "postcss-value-parser": "^4.0.2" | |
| 5433 | + | } | |
| 5434 | + | }, | |
| 5341 | 5435 | "node_modules/css-tree": { | |
| 5342 | 5436 | "version": "3.2.1", | |
| 5343 | 5437 | "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", | |
| 5607 | 5701 | "dev": true, | |
| 5608 | 5702 | "license": "ISC" | |
| 5609 | 5703 | }, | |
| 5704 | + | "node_modules/emoji-regex-xs": { | |
| 5705 | + | "version": "2.0.1", | |
| 5706 | + | "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", | |
| 5707 | + | "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", | |
| 5708 | + | "license": "MIT", | |
| 5709 | + | "engines": { | |
| 5710 | + | "node": ">=10.0.0" | |
| 5711 | + | } | |
| 5712 | + | }, | |
| 5610 | 5713 | "node_modules/enhanced-resolve": { | |
| 5611 | 5714 | "version": "5.26.0", | |
| 5612 | 5715 | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", | |
| 5700 | 5803 | "node": ">=6" | |
| 5701 | 5804 | } | |
| 5702 | 5805 | }, | |
| 5806 | + | "node_modules/escape-html": { | |
| 5807 | + | "version": "1.0.3", | |
| 5808 | + | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", | |
| 5809 | + | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", | |
| 5810 | + | "license": "MIT" | |
| 5811 | + | }, | |
| 5703 | 5812 | "node_modules/escape-string-regexp": { | |
| 5704 | 5813 | "version": "5.0.0", | |
| 5705 | 5814 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", | |
| 5807 | 5916 | } | |
| 5808 | 5917 | } | |
| 5809 | 5918 | }, | |
| 5919 | + | "node_modules/fflate": { | |
| 5920 | + | "version": "0.7.3", | |
| 5921 | + | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.3.tgz", | |
| 5922 | + | "integrity": "sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==", | |
| 5923 | + | "license": "MIT" | |
| 5924 | + | }, | |
| 5810 | 5925 | "node_modules/find-proc": { | |
| 5811 | 5926 | "version": "0.2.0", | |
| 5812 | 5927 | "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", | |
| 6264 | 6379 | "url": "https://opencollective.com/unified" | |
| 6265 | 6380 | } | |
| 6266 | 6381 | }, | |
| 6382 | + | "node_modules/hex-rgb": { | |
| 6383 | + | "version": "4.3.0", | |
| 6384 | + | "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", | |
| 6385 | + | "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", | |
| 6386 | + | "license": "MIT", | |
| 6387 | + | "engines": { | |
| 6388 | + | "node": ">=6" | |
| 6389 | + | }, | |
| 6390 | + | "funding": { | |
| 6391 | + | "url": "https://github.com/sponsors/sindresorhus" | |
| 6392 | + | } | |
| 6393 | + | }, | |
| 6267 | 6394 | "node_modules/html-escaper": { | |
| 6268 | 6395 | "version": "3.0.3", | |
| 6269 | 6396 | "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", | |
| 6792 | 6919 | "url": "https://opencollective.com/parcel" | |
| 6793 | 6920 | } | |
| 6794 | 6921 | }, | |
| 6922 | + | "node_modules/linebreak": { | |
| 6923 | + | "version": "1.1.0", | |
| 6924 | + | "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", | |
| 6925 | + | "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", | |
| 6926 | + | "license": "MIT", | |
| 6927 | + | "dependencies": { | |
| 6928 | + | "base64-js": "0.0.8", | |
| 6929 | + | "unicode-trie": "^2.0.0" | |
| 6930 | + | } | |
| 6931 | + | }, | |
| 6795 | 6932 | "node_modules/lodash": { | |
| 6796 | 6933 | "version": "4.18.1", | |
| 6797 | 6934 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", | |
| 8010 | 8147 | "@pagefind/windows-x64": "1.5.2" | |
| 8011 | 8148 | } | |
| 8012 | 8149 | }, | |
| 8150 | + | "node_modules/pako": { | |
| 8151 | + | "version": "0.2.9", | |
| 8152 | + | "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", | |
| 8153 | + | "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", | |
| 8154 | + | "license": "MIT" | |
| 8155 | + | }, | |
| 8156 | + | "node_modules/parse-css-color": { | |
| 8157 | + | "version": "0.2.1", | |
| 8158 | + | "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", | |
| 8159 | + | "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", | |
| 8160 | + | "license": "MIT", | |
| 8161 | + | "dependencies": { | |
| 8162 | + | "color-name": "^1.1.4", | |
| 8163 | + | "hex-rgb": "^4.1.0" | |
| 8164 | + | } | |
| 8165 | + | }, | |
| 8013 | 8166 | "node_modules/parse-entities": { | |
| 8014 | 8167 | "version": "4.0.2", | |
| 8015 | 8168 | "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", | |
| 8175 | 8328 | "node": ">=4" | |
| 8176 | 8329 | } | |
| 8177 | 8330 | }, | |
| 8331 | + | "node_modules/postcss-value-parser": { | |
| 8332 | + | "version": "4.2.0", | |
| 8333 | + | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", | |
| 8334 | + | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", | |
| 8335 | + | "license": "MIT" | |
| 8336 | + | }, | |
| 8178 | 8337 | "node_modules/prismjs": { | |
| 8179 | 8338 | "version": "1.30.0", | |
| 8180 | 8339 | "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", | |
| 8838 | 8997 | "url": "https://github.com/sponsors/wooorm" | |
| 8839 | 8998 | } | |
| 8840 | 8999 | }, | |
| 9000 | + | "node_modules/string.prototype.codepointat": { | |
| 9001 | + | "version": "0.2.1", | |
| 9002 | + | "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", | |
| 9003 | + | "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", | |
| 9004 | + | "license": "MIT" | |
| 9005 | + | }, | |
| 8841 | 9006 | "node_modules/stringify-entities": { | |
| 8842 | 9007 | "version": "4.0.4", | |
| 8843 | 9008 | "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", | |
| 9063 | 9228 | "pathe": "^2.0.3" | |
| 9064 | 9229 | } | |
| 9065 | 9230 | }, | |
| 9231 | + | "node_modules/unicode-trie": { | |
| 9232 | + | "version": "2.0.0", | |
| 9233 | + | "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", | |
| 9234 | + | "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", | |
| 9235 | + | "license": "MIT", | |
| 9236 | + | "dependencies": { | |
| 9237 | + | "pako": "^0.2.5", | |
| 9238 | + | "tiny-inflate": "^1.0.0" | |
| 9239 | + | } | |
| 9240 | + | }, | |
| 9066 | 9241 | "node_modules/unified": { | |
| 9067 | 9242 | "version": "11.0.5", | |
| 9068 | 9243 | "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", | |
| 9945 | 10120 | "url": "https://github.com/sponsors/sindresorhus" | |
| 9946 | 10121 | } | |
| 9947 | 10122 | }, | |
| 10123 | + | "node_modules/yoga-layout": { | |
| 10124 | + | "version": "3.2.1", | |
| 10125 | + | "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", | |
| 10126 | + | "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", | |
| 10127 | + | "license": "MIT" | |
| 10128 | + | }, | |
| 9948 | 10129 | "node_modules/youch": { | |
| 9949 | 10130 | "version": "4.1.0-beta.10", | |
| 9950 | 10131 | "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", | |
| 10015 | 10196 | "@g1t/contracts": "*" | |
| 10016 | 10197 | } | |
| 10017 | 10198 | }, | |
| 10199 | + | "services/og": { | |
| 10200 | + | "name": "@g1t/og", | |
| 10201 | + | "version": "0.1.0", | |
| 10202 | + | "license": "MIT", | |
| 10203 | + | "dependencies": { | |
| 10204 | + | "@g1t/contracts": "*", | |
| 10205 | + | "@resvg/resvg-wasm": "^2.6.2", | |
| 10206 | + | "satori": "0.32.0" | |
| 10207 | + | } | |
| 10208 | + | }, | |
| 10209 | + | "services/og/node_modules/satori": { | |
| 10210 | + | "version": "0.32.0", | |
| 10211 | + | "resolved": "https://registry.npmjs.org/satori/-/satori-0.32.0.tgz", | |
| 10212 | + | "integrity": "sha512-WwYeCWKjxfXvk7SsLSLTJhcPOrB2HCxbptqGIiYor0yqaPu3xA5uMTQ/4Dan5mxClhhk2Wn9ZE1pLGIlGdUxFQ==", | |
| 10213 | + | "license": "MPL-2.0", | |
| 10214 | + | "dependencies": { | |
| 10215 | + | "@shuding/opentype.js": "1.4.0-beta.0", | |
| 10216 | + | "css-background-parser": "^0.1.0", | |
| 10217 | + | "css-box-shadow": "1.0.0-3", | |
| 10218 | + | "css-gradient-parser": "^0.0.17", | |
| 10219 | + | "css-to-react-native": "^3.0.0", | |
| 10220 | + | "emoji-regex-xs": "^2.0.1", | |
| 10221 | + | "escape-html": "^1.0.3", | |
| 10222 | + | "linebreak": "^1.1.0", | |
| 10223 | + | "parse-css-color": "^0.2.1", | |
| 10224 | + | "postcss-value-parser": "^4.2.0", | |
| 10225 | + | "yoga-layout": "^3.2.1" | |
| 10226 | + | }, | |
| 10227 | + | "engines": { | |
| 10228 | + | "node": ">=16" | |
| 10229 | + | } | |
| 10230 | + | }, | |
| 10018 | 10231 | "services/pages": { | |
| 10019 | 10232 | "name": "@g1t/pages", | |
| 10020 | 10233 | "version": "0.1.0", |
| 29 | 29 | services/runner | |
| 30 | 30 | apps/api | |
| 31 | 31 | services/pages | |
| 32 | + | services/og | |
| 32 | 33 | apps/web | |
| 33 | 34 | apps/sudo | |
| 34 | 35 | apps/docs |
| 1 | + | { | |
| 2 | + | "name": "@g1t/og", | |
| 3 | + | "version": "0.1.0", | |
| 4 | + | "private": true, | |
| 5 | + | "type": "module", | |
| 6 | + | "license": "MIT", | |
| 7 | + | "scripts": { | |
| 8 | + | "test": "node --test src/*.test.ts", | |
| 9 | + | "typecheck": "wrangler types --include-env=false && tsc -p tsconfig.json", | |
| 10 | + | "deploy": "wrangler deploy" | |
| 11 | + | }, | |
| 12 | + | "dependencies": { | |
| 13 | + | "@g1t/contracts": "*", | |
| 14 | + | "@resvg/resvg-wasm": "^2.6.2", | |
| 15 | + | "satori": "0.32.0" | |
| 16 | + | } | |
| 17 | + | } |
| 1 | + | /** | |
| 2 | + | * The brand, as SVG for satori: the Fleet mark and the "g1t" wordmark, the | |
| 3 | + | * same shapes as `.g1t/brand/svg/logo.svg`. The wordmark is outlined, so | |
| 4 | + | * the card needs no ExtraBold font. | |
| 5 | + | */ | |
| 6 | + | ||
| 7 | + | export const COLOR = { | |
| 8 | + | bg: "#0f0f11", | |
| 9 | + | surface: "#161618", | |
| 10 | + | raised: "#1e1e21", | |
| 11 | + | line: "#28282c", | |
| 12 | + | lineStrong: "#38383e", | |
| 13 | + | fg: "#ededef", | |
| 14 | + | fgSoft: "#dcdce0", | |
| 15 | + | muted: "#a0a0a8", | |
| 16 | + | faint: "#6a6a72", | |
| 17 | + | /** Lavender: the accent. */ | |
| 18 | + | lavender: "#b6a8ff", | |
| 19 | + | lavenderDeep: "#6b56e8", | |
| 20 | + | /** Status colours, from the site's tokens. */ | |
| 21 | + | mint: "#86efc4", | |
| 22 | + | danger: "#ff8394", | |
| 23 | + | } as const; | |
| 24 | + | ||
| 25 | + | /** The mark's shapes, in its 32-unit box. */ | |
| 26 | + | function markShapes(front: string, behind: string): string { | |
| 27 | + | return ( | |
| 28 | + | `<g transform="translate(0.7 0.5)">` + | |
| 29 | + | `<rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="${behind}" fill-opacity="0.35"/>` + | |
| 30 | + | `<rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="${behind}" fill-opacity="0.65"/>` + | |
| 31 | + | `<rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="${front}"/>` + | |
| 32 | + | `<path d="M21.7 7.2 17.6 10.9" fill="none" stroke="${front}" stroke-width="4.6" stroke-linecap="round"/>` + | |
| 33 | + | `</g>` | |
| 34 | + | ); | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | const WORDMARK = | |
| 38 | + | "M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"; | |
| 39 | + | ||
| 40 | + | /** | |
| 41 | + | * The lockup: the mark as tall as the wordmark's capitals, standing on its | |
| 42 | + | * baseline. Its box is 233 by 95; the baseline is at 72.75. | |
| 43 | + | */ | |
| 44 | + | export const LOCKUP_BOX = { width: 233, height: 95, baseline: 72.75 } as const; | |
| 45 | + | ||
| 46 | + | export function lockupSvg(front: string = COLOR.fg, behind: string = COLOR.lavender): string { | |
| 47 | + | return ( | |
| 48 | + | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${LOCKUP_BOX.width} ${LOCKUP_BOX.height}">` + | |
| 49 | + | `<g transform="translate(-22.77 -16.35) scale(3.3)">${markShapes(front, behind)}</g>` + | |
| 50 | + | `<path transform="translate(101.08 72.75)" fill="${front}" d="${WORDMARK}"/>` + | |
| 51 | + | `</svg>` | |
| 52 | + | ); | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /** The mark alone, cropped to the figure: 18.2 by 22. */ | |
| 56 | + | export function markSvg(front: string = COLOR.fg, behind: string = COLOR.lavender): string { | |
| 57 | + | return ( | |
| 58 | + | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="6.9 5 18.2 22">${markShapes(front, behind)}</svg>` | |
| 59 | + | ); | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | /** As an image source; base64, which both satori and resvg read. The SVG is ASCII. */ | |
| 63 | + | /** | |
| 64 | + | * The card's backdrop: the brand card's dot grid, lavender light from the | |
| 65 | + | * top right, and the fleet standing large off the right edge, its 1s | |
| 66 | + | * fading downwards into the dark. Darker towards the bottom, where the | |
| 67 | + | * facts sit. | |
| 68 | + | */ | |
| 69 | + | export function backdropSvg(width: number, height: number, withFleet: boolean): string { | |
| 70 | + | // The fleet at 24 times its 32-unit size, its front 1 cut by the edge. | |
| 71 | + | const scale = 24; | |
| 72 | + | // Drawn solid and faded as a whole, so the flag and the bar it joins | |
| 73 | + | // read as one shape. | |
| 74 | + | const fleet = withFleet | |
| 75 | + | ? `<g mask="url(#fleetFade)" opacity="0.34">` + | |
| 76 | + | `<g transform="translate(${width - 22.4 * scale} ${height - 27.5 * scale + 40}) scale(${scale})">` + | |
| 77 | + | markShapes(COLOR.lavender, COLOR.lavender) + | |
| 78 | + | `</g></g>` | |
| 79 | + | : ""; | |
| 80 | + | return ( | |
| 81 | + | `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` + | |
| 82 | + | `<defs>` + | |
| 83 | + | `<pattern id="dots" width="40" height="40" patternUnits="userSpaceOnUse">` + | |
| 84 | + | `<circle cx="20" cy="15" r="1.4" fill="#2a2a30"/></pattern>` + | |
| 85 | + | `<radialGradient id="light" cx="${width - 160}" cy="-40" r="760" gradientUnits="userSpaceOnUse">` + | |
| 86 | + | `<stop offset="0" stop-color="${COLOR.lavenderDeep}" stop-opacity="0.30"/>` + | |
| 87 | + | `<stop offset="0.45" stop-color="${COLOR.lavenderDeep}" stop-opacity="0.09"/>` + | |
| 88 | + | `<stop offset="1" stop-color="${COLOR.lavenderDeep}" stop-opacity="0"/></radialGradient>` + | |
| 89 | + | `<linearGradient id="dusk" x1="0" y1="0" x2="0" y2="1">` + | |
| 90 | + | `<stop offset="0.5" stop-color="${COLOR.bg}" stop-opacity="0"/>` + | |
| 91 | + | `<stop offset="1" stop-color="${COLOR.bg}" stop-opacity="0.9"/></linearGradient>` + | |
| 92 | + | `<linearGradient id="fadeMask" x1="0" y1="0" x2="0" y2="1">` + | |
| 93 | + | `<stop offset="0.1" stop-color="#fff"/>` + | |
| 94 | + | `<stop offset="0.95" stop-color="#000"/></linearGradient>` + | |
| 95 | + | `<mask id="fleetFade" maskUnits="userSpaceOnUse" x="0" y="0" width="${width}" height="${height}">` + | |
| 96 | + | `<rect width="${width}" height="${height}" fill="url(#fadeMask)"/></mask>` + | |
| 97 | + | `</defs>` + | |
| 98 | + | `<rect width="${width}" height="${height}" fill="${COLOR.bg}"/>` + | |
| 99 | + | `<rect width="${width}" height="${height}" fill="url(#dots)"/>` + | |
| 100 | + | `<rect width="${width}" height="${height}" fill="url(#light)"/>` + | |
| 101 | + | fleet + | |
| 102 | + | `<rect width="${width}" height="${height}" fill="url(#dusk)"/>` + | |
| 103 | + | `</svg>` | |
| 104 | + | ); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | export function dataUri(svg: string): string { | |
| 108 | + | return `data:image/svg+xml;base64,${btoa(svg)}`; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | /** Lucide's icons, as the site uses them, at 24 units. */ | |
| 112 | + | export const ICON = { | |
| 113 | + | issue: `<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="1"/>`, | |
| 114 | + | issueDone: `<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>`, | |
| 115 | + | issueNotPlanned: `<circle cx="12" cy="12" r="10"/><line x1="9" x2="15" y1="15" y2="9"/>`, | |
| 116 | + | pull: `<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><line x1="6" x2="6" y1="9" y2="21"/>`, | |
| 117 | + | pullDraft: `<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M18 6V5"/><path d="M18 11v-1"/><line x1="6" x2="6" y1="9" y2="21"/>`, | |
| 118 | + | merged: `<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/>`, | |
| 119 | + | pullClosed: `<circle cx="6" cy="6" r="3"/><path d="M6 9v12"/><path d="m21 3-6 6"/><path d="m21 9-6-6"/><path d="M18 11.5V15"/><circle cx="18" cy="18" r="3"/>`, | |
| 120 | + | box: `<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>`, | |
| 121 | + | sparkles: `<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/>`, | |
| 122 | + | book: `<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>`, | |
| 123 | + | user: `<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>`, | |
| 124 | + | globe: `<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>`, | |
| 125 | + | } as const; | |
| 126 | + | ||
| 127 | + | export function iconSvg(shapes: string, color: string): string { | |
| 128 | + | return ( | |
| 129 | + | `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" ` + | |
| 130 | + | `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${shapes}</svg>` | |
| 131 | + | ); | |
| 132 | + | } |
| 1 | + | /** | |
| 2 | + | * Part of every cache key: bump it when the cards' design changes, so the | |
| 3 | + | * cards already cached are drawn again. | |
| 4 | + | */ | |
| 5 | + | export const DESIGN = "1"; | |
| 6 | + | ||
| 7 | + | /** | |
| 8 | + | * The address a card is cached under: only the parameters that change it, | |
| 9 | + | * in order, so stray ones (trackers, cache busters) cannot fill the cache. | |
| 10 | + | */ | |
| 11 | + | export function cacheKey(url: URL): string { | |
| 12 | + | const wanted = | |
| 13 | + | url.pathname === "/docs" ? ["title", "section", "description"] : url.pathname === "/image" ? ["path", "v"] : []; | |
| 14 | + | const key = new URL(url.pathname, url.origin); | |
| 15 | + | key.searchParams.set("design", DESIGN); | |
| 16 | + | for (const name of wanted) { | |
| 17 | + | const value = url.searchParams.get(name); | |
| 18 | + | if (value !== null) key.searchParams.set(name, value); | |
| 19 | + | } | |
| 20 | + | return key.toString(); | |
| 21 | + | } |
| 1 | + | /** | |
| 2 | + | * The cards' layouts, as the element trees satori lays out. One frame for | |
| 3 | + | * every kind: the lockup and the address along the top, the subject in the | |
| 4 | + | * middle, the facts along the bottom, on the site's very dark gray with a | |
| 5 | + | * dot grid and lavender kept for accents. | |
| 6 | + | */ | |
| 7 | + | import { COLOR, ICON, LOCKUP_BOX, backdropSvg, dataUri, iconSvg, lockupSvg } from "./brand.ts"; | |
| 8 | + | import type { Card, IssueState } from "./resolve.ts"; | |
| 9 | + | import type { PullStatus } from "@g1t/contracts"; | |
| 10 | + | ||
| 11 | + | export const WIDTH = 1200; | |
| 12 | + | export const HEIGHT = 630; | |
| 13 | + | const PAD = 80; | |
| 14 | + | ||
| 15 | + | /** An element as satori reads it. */ | |
| 16 | + | export type Node = { type: string; props: Record<string, unknown> & { style?: Style; children?: Child } }; | |
| 17 | + | type Child = Node | string | (Node | string | null | false)[] | null; | |
| 18 | + | type Style = Record<string, string | number>; | |
| 19 | + | ||
| 20 | + | function h(type: string, style: Style, ...children: (Node | string | null | false)[]): Node { | |
| 21 | + | const kids = children.filter((child): child is Node | string => child !== null && child !== false); | |
| 22 | + | if (kids.length === 0) return { type, props: { style } }; | |
| 23 | + | return { type, props: { style, children: kids.length === 1 ? kids[0] : kids } }; | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | function img(src: string, width: number, height: number, style: Style = {}): Node { | |
| 27 | + | return { type: "img", props: { src, width, height, style: { width, height, ...style } } }; | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | function icon(shapes: string, color: string, size: number): Node { | |
| 31 | + | return img(dataUri(iconSvg(shapes, color)), size, size, { flexShrink: 0 }); | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | const SANS = "Inter"; | |
| 35 | + | const MONO = "JetBrains Mono"; | |
| 36 | + | ||
| 37 | + | /** Long text is cut before layout, so a huge title costs nothing to lay out. */ | |
| 38 | + | function bound(text: string, max: number): string { | |
| 39 | + | const single = text.replace(/\s+/g, " ").trim(); | |
| 40 | + | return single.length > max ? `${single.slice(0, max - 1).trimEnd()}…` : single; | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | /** A block of text that stops after `lines`, with an ellipsis. */ | |
| 44 | + | function clamp(text: string, lines: number, style: Style): Node { | |
| 45 | + | return h("div", { display: "block", lineClamp: lines, overflow: "hidden", ...style }, text); | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | /** Bigger type for shorter titles, so a two-word title fills the card and a long one fits. */ | |
| 49 | + | function titleSize(text: string): number { | |
| 50 | + | if (text.length <= 28) return 76; | |
| 51 | + | if (text.length <= 56) return 64; | |
| 52 | + | if (text.length <= 96) return 54; | |
| 53 | + | return 46; | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // --- Pieces -------------------------------------------------------------- | |
| 57 | + | ||
| 58 | + | /** The backdrop: dot grid, lavender light and, on most cards, the fleet. */ | |
| 59 | + | function background(withFleet: boolean): Node { | |
| 60 | + | return img(dataUri(backdropSvg(WIDTH, HEIGHT, withFleet)), WIDTH, HEIGHT, { position: "absolute", top: 0, left: 0 }); | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | /** The lavender rule along the bottom edge. */ | |
| 64 | + | function rule(): Node { | |
| 65 | + | return h("div", { | |
| 66 | + | position: "absolute", | |
| 67 | + | left: 0, | |
| 68 | + | bottom: 0, | |
| 69 | + | width: WIDTH, | |
| 70 | + | height: 6, | |
| 71 | + | backgroundImage: `linear-gradient(to right, ${COLOR.lavenderDeep}, ${COLOR.lavender} 45%, rgba(182, 168, 255, 0.15))`, | |
| 72 | + | }); | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | function lockup(height: number, suffix?: string): Node { | |
| 76 | + | const width = Math.round((height * LOCKUP_BOX.width) / LOCKUP_BOX.height); | |
| 77 | + | return h( | |
| 78 | + | "div", | |
| 79 | + | { display: "flex", alignItems: "flex-end" }, | |
| 80 | + | img(dataUri(lockupSvg()), width, height), | |
| 81 | + | suffix | |
| 82 | + | ? h( | |
| 83 | + | "div", | |
| 84 | + | { | |
| 85 | + | display: "flex", | |
| 86 | + | fontFamily: SANS, | |
| 87 | + | fontWeight: 500, | |
| 88 | + | fontSize: Math.round(height * 0.62), | |
| 89 | + | color: COLOR.muted, | |
| 90 | + | marginLeft: Math.round(height * 0.22), | |
| 91 | + | // Sits on the wordmark's baseline. | |
| 92 | + | marginBottom: Math.round(height * (1 - LOCKUP_BOX.baseline / LOCKUP_BOX.height) - height * 0.1), | |
| 93 | + | letterSpacing: -0.5, | |
| 94 | + | }, | |
| 95 | + | suffix, | |
| 96 | + | ) | |
| 97 | + | : null, | |
| 98 | + | ); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | function topBar(address: string, suffix?: string): Node { | |
| 102 | + | return h( | |
| 103 | + | "div", | |
| 104 | + | { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }, | |
| 105 | + | lockup(46, suffix), | |
| 106 | + | h( | |
| 107 | + | "div", | |
| 108 | + | { display: "flex", fontFamily: MONO, fontSize: 22, color: COLOR.faint, whiteSpace: "nowrap" }, | |
| 109 | + | bound(address, 46), | |
| 110 | + | ), | |
| 111 | + | ); | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | type Tone = { color: string; bg: string; border: string }; | |
| 115 | + | ||
| 116 | + | const TONE = { | |
| 117 | + | open: { color: COLOR.mint, bg: "rgba(134, 239, 196, 0.10)", border: "rgba(134, 239, 196, 0.38)" }, | |
| 118 | + | merged: { color: COLOR.lavender, bg: "rgba(182, 168, 255, 0.12)", border: "rgba(182, 168, 255, 0.42)" }, | |
| 119 | + | closed: { color: COLOR.danger, bg: "rgba(255, 131, 148, 0.10)", border: "rgba(255, 131, 148, 0.38)" }, | |
| 120 | + | quiet: { color: COLOR.muted, bg: "rgba(160, 160, 168, 0.08)", border: "rgba(160, 160, 168, 0.30)" }, | |
| 121 | + | } satisfies Record<string, Tone>; | |
| 122 | + | ||
| 123 | + | function badge(label: string, shapes: string, tone: Tone): Node { | |
| 124 | + | return h( | |
| 125 | + | "div", | |
| 126 | + | { | |
| 127 | + | display: "flex", | |
| 128 | + | alignItems: "center", | |
| 129 | + | gap: 12, | |
| 130 | + | padding: "10px 22px 10px 18px", | |
| 131 | + | borderRadius: 999, | |
| 132 | + | backgroundColor: tone.bg, | |
| 133 | + | border: `2px solid ${tone.border}`, | |
| 134 | + | color: tone.color, | |
| 135 | + | fontFamily: SANS, | |
| 136 | + | fontWeight: 600, | |
| 137 | + | fontSize: 26, | |
| 138 | + | }, | |
| 139 | + | icon(shapes, tone.color, 28), | |
| 140 | + | label, | |
| 141 | + | ); | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | /** A fact along the bottom: an icon, a number or a word, and what it counts. */ | |
| 145 | + | function fact(shapes: string, strong: string, rest: string): Node { | |
| 146 | + | return h( | |
| 147 | + | "div", | |
| 148 | + | { display: "flex", alignItems: "center", gap: 12, fontFamily: SANS, fontSize: 28, color: COLOR.muted }, | |
| 149 | + | icon(shapes, COLOR.faint, 28), | |
| 150 | + | h("div", { display: "flex", color: COLOR.fg, fontWeight: 600 }, strong), | |
| 151 | + | rest ? h("div", { display: "flex" }, rest) : null, | |
| 152 | + | ); | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | function plain(text: string, mono = false): Node { | |
| 156 | + | return h( | |
| 157 | + | "div", | |
| 158 | + | { display: "flex", fontFamily: mono ? MONO : SANS, fontSize: mono ? 25 : 28, color: COLOR.muted }, | |
| 159 | + | text, | |
| 160 | + | ); | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | const dot = () => h("div", { display: "flex", fontSize: 28, color: COLOR.faint }, "·"); | |
| 164 | + | ||
| 165 | + | /** The frame every card but the brand card shares. */ | |
| 166 | + | function frame(parts: { | |
| 167 | + | address: string; | |
| 168 | + | suffix?: string; | |
| 169 | + | eyebrow: Node; | |
| 170 | + | title: Node; | |
| 171 | + | description: string | null; | |
| 172 | + | facts: Node[]; | |
| 173 | + | descriptionLines?: number; | |
| 174 | + | }): Node { | |
| 175 | + | return h( | |
| 176 | + | "div", | |
| 177 | + | { | |
| 178 | + | display: "flex", | |
| 179 | + | position: "relative", | |
| 180 | + | width: WIDTH, | |
| 181 | + | height: HEIGHT, | |
| 182 | + | backgroundColor: COLOR.bg, | |
| 183 | + | overflow: "hidden", | |
| 184 | + | }, | |
| 185 | + | background(true), | |
| 186 | + | h( | |
| 187 | + | "div", | |
| 188 | + | { | |
| 189 | + | display: "flex", | |
| 190 | + | flexDirection: "column", | |
| 191 | + | position: "relative", | |
| 192 | + | width: WIDTH, | |
| 193 | + | height: HEIGHT, | |
| 194 | + | padding: `64px ${PAD}px 72px`, | |
| 195 | + | }, | |
| 196 | + | topBar(parts.address, parts.suffix), | |
| 197 | + | h( | |
| 198 | + | "div", | |
| 199 | + | { display: "flex", flexDirection: "column", flexGrow: 1, justifyContent: "center", width: 940 }, | |
| 200 | + | parts.eyebrow, | |
| 201 | + | h("div", { display: "flex", marginTop: 18 }, parts.title), | |
| 202 | + | parts.description | |
| 203 | + | ? clamp(bound(parts.description, 260), parts.descriptionLines ?? 2, { | |
| 204 | + | marginTop: 22, | |
| 205 | + | fontFamily: SANS, | |
| 206 | + | fontSize: 30, | |
| 207 | + | lineHeight: 1.4, | |
| 208 | + | color: COLOR.muted, | |
| 209 | + | }) | |
| 210 | + | : null, | |
| 211 | + | ), | |
| 212 | + | h("div", { display: "flex", alignItems: "center", gap: 24, height: 56 }, ...parts.facts), | |
| 213 | + | ), | |
| 214 | + | rule(), | |
| 215 | + | ); | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | function eyebrow(text: string, mono: boolean, color: string = COLOR.lavender): Node { | |
| 219 | + | return h( | |
| 220 | + | "div", | |
| 221 | + | { | |
| 222 | + | display: "flex", | |
| 223 | + | fontFamily: mono ? MONO : SANS, | |
| 224 | + | fontWeight: 500, | |
| 225 | + | fontSize: mono ? 28 : 26, | |
| 226 | + | color, | |
| 227 | + | letterSpacing: mono ? 0 : 2, | |
| 228 | + | textTransform: mono ? "none" : "uppercase", | |
| 229 | + | }, | |
| 230 | + | bound(text, 60), | |
| 231 | + | ); | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | function title(text: string, lines = 3): Node { | |
| 235 | + | const bounded = bound(text, 180); | |
| 236 | + | const size = titleSize(bounded); | |
| 237 | + | return clamp(bounded, lines, { | |
| 238 | + | fontFamily: SANS, | |
| 239 | + | fontWeight: 600, | |
| 240 | + | fontSize: size, | |
| 241 | + | lineHeight: 1.12, | |
| 242 | + | letterSpacing: size >= 64 ? -2 : -1.4, | |
| 243 | + | color: COLOR.fg, | |
| 244 | + | width: 940, | |
| 245 | + | }); | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | // --- Cards --------------------------------------------------------------- | |
| 249 | + | ||
| 250 | + | function brandCard(): Node { | |
| 251 | + | const height = 190; | |
| 252 | + | const width = Math.round((height * LOCKUP_BOX.width) / LOCKUP_BOX.height); | |
| 253 | + | return h( | |
| 254 | + | "div", | |
| 255 | + | { | |
| 256 | + | display: "flex", | |
| 257 | + | position: "relative", | |
| 258 | + | width: WIDTH, | |
| 259 | + | height: HEIGHT, | |
| 260 | + | backgroundColor: COLOR.bg, | |
| 261 | + | overflow: "hidden", | |
| 262 | + | }, | |
| 263 | + | background(false), | |
| 264 | + | h( | |
| 265 | + | "div", | |
| 266 | + | { | |
| 267 | + | display: "flex", | |
| 268 | + | flexDirection: "column", | |
| 269 | + | alignItems: "center", | |
| 270 | + | justifyContent: "center", | |
| 271 | + | position: "relative", | |
| 272 | + | width: WIDTH, | |
| 273 | + | height: HEIGHT, | |
| 274 | + | }, | |
| 275 | + | img(dataUri(lockupSvg()), width, height), | |
| 276 | + | h( | |
| 277 | + | "div", | |
| 278 | + | { | |
| 279 | + | display: "flex", | |
| 280 | + | marginTop: 56, | |
| 281 | + | fontFamily: SANS, | |
| 282 | + | fontWeight: 500, | |
| 283 | + | fontSize: 44, | |
| 284 | + | letterSpacing: -0.8, | |
| 285 | + | color: COLOR.fgSoft, | |
| 286 | + | }, | |
| 287 | + | "A git forge for teams of agents", | |
| 288 | + | ), | |
| 289 | + | h( | |
| 290 | + | "div", | |
| 291 | + | { display: "flex", marginTop: 40, fontFamily: MONO, fontSize: 26, color: COLOR.lavender }, | |
| 292 | + | "g1t.sh", | |
| 293 | + | ), | |
| 294 | + | ), | |
| 295 | + | rule(), | |
| 296 | + | ); | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | const PULL_BADGE: Record<PullStatus, [string, string, Tone]> = { | |
| 300 | + | open: ["Open", ICON.pull, TONE.open], | |
| 301 | + | draft: ["Draft", ICON.pullDraft, TONE.quiet], | |
| 302 | + | merged: ["Merged", ICON.merged, TONE.merged], | |
| 303 | + | closed: ["Closed", ICON.pullClosed, TONE.closed], | |
| 304 | + | }; | |
| 305 | + | ||
| 306 | + | const ISSUE_BADGE: Record<IssueState, [string, string, Tone]> = { | |
| 307 | + | open: ["Open", ICON.issue, TONE.open], | |
| 308 | + | completed: ["Closed", ICON.issueDone, TONE.merged], | |
| 309 | + | not_planned: ["Not planned", ICON.issueNotPlanned, TONE.quiet], | |
| 310 | + | }; | |
| 311 | + | ||
| 312 | + | function count(n: number, one: string, many: string): [string, string] { | |
| 313 | + | return [n.toLocaleString("en-US"), n === 1 ? one : many]; | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | export function cardTree(card: Card): Node { | |
| 317 | + | switch (card.kind) { | |
| 318 | + | case "brand": | |
| 319 | + | return brandCard(); | |
| 320 | + | ||
| 321 | + | case "page": | |
| 322 | + | return frame({ | |
| 323 | + | address: card.address, | |
| 324 | + | eyebrow: eyebrow(card.eyebrow, false), | |
| 325 | + | title: title(card.title), | |
| 326 | + | description: card.description, | |
| 327 | + | facts: [plain("A git forge for teams of agents")], | |
| 328 | + | }); | |
| 329 | + | ||
| 330 | + | case "workspace": { | |
| 331 | + | const [n, noun] = count(card.projects, "public project", "public projects"); | |
| 332 | + | return frame({ | |
| 333 | + | address: `g1t.sh/${card.slug}`, | |
| 334 | + | // The slug above the name, unless they are the same. | |
| 335 | + | eyebrow: card.name.toLowerCase() === card.slug ? eyebrow("Workspace", false) : eyebrow(card.slug, true), | |
| 336 | + | title: title(card.name, 2), | |
| 337 | + | description: card.description, | |
| 338 | + | facts: [fact(ICON.box, n, noun)], | |
| 339 | + | }); | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | case "project": { | |
| 343 | + | const [issues, issueNoun] = count(card.issues, "open issue", "open issues"); | |
| 344 | + | const [pulls, pullNoun] = count(card.pulls, "open pull request", "open pull requests"); | |
| 345 | + | const named = card.name.toLowerCase() !== card.repo.toLowerCase(); | |
| 346 | + | return frame({ | |
| 347 | + | address: `g1t.sh/${card.owner}/${card.repo}`, | |
| 348 | + | eyebrow: eyebrow(named ? `${card.owner}/${card.repo}` : `${card.owner} /`, true), | |
| 349 | + | title: named ? title(card.name, 2) : repoTitle(card.repo), | |
| 350 | + | description: card.description, | |
| 351 | + | facts: [fact(ICON.issue, issues, issueNoun), fact(ICON.pull, pulls, pullNoun)], | |
| 352 | + | }); | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | case "issue": { | |
| 356 | + | const [label, shapes, tone] = ISSUE_BADGE[card.state]; | |
| 357 | + | return frame({ | |
| 358 | + | address: `g1t.sh/${card.owner}/${card.repo}/issues/${card.number}`, | |
| 359 | + | eyebrow: eyebrowWithNumber(`${card.owner}/${card.repo}`, card.number), | |
| 360 | + | title: title(card.title), | |
| 361 | + | description: null, | |
| 362 | + | facts: [badge(label, shapes, tone), plain("Issue"), dot(), byline(card.author)], | |
| 363 | + | }); | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | case "pull": { | |
| 367 | + | const [label, shapes, tone] = PULL_BADGE[card.state]; | |
| 368 | + | return frame({ | |
| 369 | + | address: `g1t.sh/${card.owner}/${card.repo}/pull/${card.number}`, | |
| 370 | + | eyebrow: eyebrowWithNumber(`${card.owner}/${card.repo}`, card.number), | |
| 371 | + | title: title(card.title), | |
| 372 | + | description: null, | |
| 373 | + | facts: [badge(label, shapes, tone), plain("Pull request"), dot(), byline(card.author)], | |
| 374 | + | }); | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | case "soon": | |
| 378 | + | return frame({ | |
| 379 | + | address: `g1t.sh/${card.owner}/${card.repo}`, | |
| 380 | + | eyebrow: eyebrow(`${card.owner}/${card.repo}`, true), | |
| 381 | + | title: title(card.title, 2), | |
| 382 | + | description: card.summary, | |
| 383 | + | facts: [badge("Soon", ICON.sparkles, TONE.open), plain(card.section)], | |
| 384 | + | }); | |
| 385 | + | ||
| 386 | + | case "docs": | |
| 387 | + | return frame({ | |
| 388 | + | address: "docs.g1t.sh", | |
| 389 | + | suffix: "docs", | |
| 390 | + | eyebrow: eyebrow(card.section ?? "Documentation", false), | |
| 391 | + | title: title(card.title, 2), | |
| 392 | + | description: card.description, | |
| 393 | + | facts: [fact(ICON.book, "g1t docs", "Guides and API reference")], | |
| 394 | + | }); | |
| 395 | + | } | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | /** A repository's name in mono, large: the project's own address. */ | |
| 399 | + | function repoTitle(name: string): Node { | |
| 400 | + | const bounded = bound(name, 60); | |
| 401 | + | const size = bounded.length <= 16 ? 84 : bounded.length <= 26 ? 68 : 52; | |
| 402 | + | return clamp(bounded, 2, { | |
| 403 | + | fontFamily: MONO, | |
| 404 | + | fontWeight: 500, | |
| 405 | + | fontSize: size, | |
| 406 | + | lineHeight: 1.12, | |
| 407 | + | letterSpacing: -1, | |
| 408 | + | color: COLOR.fg, | |
| 409 | + | width: 940, | |
| 410 | + | }); | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | function eyebrowWithNumber(repo: string, number: number): Node { | |
| 414 | + | return h( | |
| 415 | + | "div", | |
| 416 | + | { display: "flex", alignItems: "baseline", fontFamily: MONO, fontWeight: 500, fontSize: 28 }, | |
| 417 | + | h("div", { display: "flex", color: COLOR.lavender }, bound(repo, 48)), | |
| 418 | + | h("div", { display: "flex", color: COLOR.faint, marginLeft: 14 }, `#${number}`), | |
| 419 | + | ); | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | function byline(author: string): Node { | |
| 423 | + | return h( | |
| 424 | + | "div", | |
| 425 | + | { display: "flex", alignItems: "center", gap: 10, fontFamily: SANS, fontSize: 28, color: COLOR.muted }, | |
| 426 | + | "by", | |
| 427 | + | h("div", { display: "flex", fontFamily: MONO, fontSize: 26, color: COLOR.fgSoft }, bound(author, 32)), | |
| 428 | + | ); | |
| 429 | + | } |
| 1 | + | Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) | |
| 2 | + | ||
| 3 | + | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | + | This license is copied below, and is also available with a FAQ at: | |
| 5 | + | http://scripts.sil.org/OFL | |
| 6 | + | ||
| 7 | + | ----------------------------------------------------------- | |
| 8 | + | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 9 | + | ----------------------------------------------------------- | |
| 10 | + | ||
| 11 | + | PREAMBLE | |
| 12 | + | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 13 | + | development of collaborative font projects, to support the font creation | |
| 14 | + | efforts of academic and linguistic communities, and to provide a free and | |
| 15 | + | open framework in which fonts may be shared and improved in partnership | |
| 16 | + | with others. | |
| 17 | + | ||
| 18 | + | The OFL allows the licensed fonts to be used, studied, modified and | |
| 19 | + | redistributed freely as long as they are not sold by themselves. The | |
| 20 | + | fonts, including any derivative works, can be bundled, embedded, | |
| 21 | + | redistributed and/or sold with any software provided that any reserved | |
| 22 | + | names are not used by derivative works. The fonts and derivatives, | |
| 23 | + | however, cannot be released under any other type of license. The | |
| 24 | + | requirement for fonts to remain under this license does not apply | |
| 25 | + | to any document created using the fonts or their derivatives. | |
| 26 | + | ||
| 27 | + | DEFINITIONS | |
| 28 | + | "Font Software" refers to the set of files released by the Copyright | |
| 29 | + | Holder(s) under this license and clearly marked as such. This may | |
| 30 | + | include source files, build scripts and documentation. | |
| 31 | + | ||
| 32 | + | "Reserved Font Name" refers to any names specified as such after the | |
| 33 | + | copyright statement(s). | |
| 34 | + | ||
| 35 | + | "Original Version" refers to the collection of Font Software components as | |
| 36 | + | distributed by the Copyright Holder(s). | |
| 37 | + | ||
| 38 | + | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 39 | + | or substituting -- in part or in whole -- any of the components of the | |
| 40 | + | Original Version, by changing formats or by porting the Font Software to a | |
| 41 | + | new environment. | |
| 42 | + | ||
| 43 | + | "Author" refers to any designer, engineer, programmer, technical | |
| 44 | + | writer or other person who contributed to the Font Software. | |
| 45 | + | ||
| 46 | + | PERMISSION AND CONDITIONS | |
| 47 | + | Permission is hereby granted, free of charge, to any person obtaining | |
| 48 | + | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 49 | + | redistribute, and sell modified and unmodified copies of the Font | |
| 50 | + | Software, subject to the following conditions: | |
| 51 | + | ||
| 52 | + | 1) Neither the Font Software nor any of its individual components, | |
| 53 | + | in Original or Modified Versions, may be sold by itself. | |
| 54 | + | ||
| 55 | + | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 56 | + | redistributed and/or sold with any software, provided that each copy | |
| 57 | + | contains the above copyright notice and this license. These can be | |
| 58 | + | included either as stand-alone text files, human-readable headers or | |
| 59 | + | in the appropriate machine-readable metadata fields within text or | |
| 60 | + | binary files as long as those fields can be easily viewed by the user. | |
| 61 | + | ||
| 62 | + | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 63 | + | Name(s) unless explicit written permission is granted by the corresponding | |
| 64 | + | Copyright Holder. This restriction only applies to the primary font name as | |
| 65 | + | presented to the users. | |
| 66 | + | ||
| 67 | + | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 68 | + | Software shall not be used to promote, endorse or advertise any | |
| 69 | + | Modified Version, except to acknowledge the contribution(s) of the | |
| 70 | + | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 71 | + | permission. | |
| 72 | + | ||
| 73 | + | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 74 | + | must be distributed entirely under this license, and must not be | |
| 75 | + | distributed under any other license. The requirement for fonts to | |
| 76 | + | remain under this license does not apply to any document created | |
| 77 | + | using the Font Software. | |
| 78 | + | ||
| 79 | + | TERMINATION | |
| 80 | + | This license becomes null and void if any of the above conditions are | |
| 81 | + | not met. | |
| 82 | + | ||
| 83 | + | DISCLAIMER | |
| 84 | + | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 85 | + | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 86 | + | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 87 | + | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 88 | + | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 89 | + | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 90 | + | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 91 | + | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 92 | + | OTHER DEALINGS IN THE FONT SOFTWARE. |
| 1 | + | Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) | |
| 2 | + | ||
| 3 | + | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | + | This license is copied below, and is also available with a FAQ at: | |
| 5 | + | https://openfontlicense.org | |
| 6 | + | ||
| 7 | + | ||
| 8 | + | ----------------------------------------------------------- | |
| 9 | + | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | + | ----------------------------------------------------------- | |
| 11 | + | ||
| 12 | + | PREAMBLE | |
| 13 | + | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | + | development of collaborative font projects, to support the font creation | |
| 15 | + | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | + | open framework in which fonts may be shared and improved in partnership | |
| 17 | + | with others. | |
| 18 | + | ||
| 19 | + | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | + | redistributed freely as long as they are not sold by themselves. The | |
| 21 | + | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | + | redistributed and/or sold with any software provided that any reserved | |
| 23 | + | names are not used by derivative works. The fonts and derivatives, | |
| 24 | + | however, cannot be released under any other type of license. The | |
| 25 | + | requirement for fonts to remain under this license does not apply | |
| 26 | + | to any document created using the fonts or their derivatives. | |
| 27 | + | ||
| 28 | + | DEFINITIONS | |
| 29 | + | "Font Software" refers to the set of files released by the Copyright | |
| 30 | + | Holder(s) under this license and clearly marked as such. This may | |
| 31 | + | include source files, build scripts and documentation. | |
| 32 | + | ||
| 33 | + | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | + | copyright statement(s). | |
| 35 | + | ||
| 36 | + | "Original Version" refers to the collection of Font Software components as | |
| 37 | + | distributed by the Copyright Holder(s). | |
| 38 | + | ||
| 39 | + | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | + | or substituting -- in part or in whole -- any of the components of the | |
| 41 | + | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | + | new environment. | |
| 43 | + | ||
| 44 | + | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | + | writer or other person who contributed to the Font Software. | |
| 46 | + | ||
| 47 | + | PERMISSION & CONDITIONS | |
| 48 | + | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | + | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | + | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | + | Software, subject to the following conditions: | |
| 52 | + | ||
| 53 | + | 1) Neither the Font Software nor any of its individual components, | |
| 54 | + | in Original or Modified Versions, may be sold by itself. | |
| 55 | + | ||
| 56 | + | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | + | redistributed and/or sold with any software, provided that each copy | |
| 58 | + | contains the above copyright notice and this license. These can be | |
| 59 | + | included either as stand-alone text files, human-readable headers or | |
| 60 | + | in the appropriate machine-readable metadata fields within text or | |
| 61 | + | binary files as long as those fields can be easily viewed by the user. | |
| 62 | + | ||
| 63 | + | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | + | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | + | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | + | presented to the users. | |
| 67 | + | ||
| 68 | + | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | + | Software shall not be used to promote, endorse or advertise any | |
| 70 | + | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | + | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | + | permission. | |
| 73 | + | ||
| 74 | + | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | + | must be distributed entirely under this license, and must not be | |
| 76 | + | distributed under any other license. The requirement for fonts to | |
| 77 | + | remain under this license does not apply to any document created | |
| 78 | + | using the Font Software. | |
| 79 | + | ||
| 80 | + | TERMINATION | |
| 81 | + | This license becomes null and void if any of the above conditions are | |
| 82 | + | not met. | |
| 83 | + | ||
| 84 | + | DISCLAIMER | |
| 85 | + | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | + | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | + | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | + | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | + | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | + | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | + | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | + | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | + | OTHER DEALINGS IN THE FONT SOFTWARE. |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.