Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Social cards for every page: og.g1t.sh | 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 | ||
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 15 | /** The launch line: what g1t is, on the brand card and under every page card. */ |
| 16 | export const TAGLINE = "Where people and agents ship software together."; | |
| 17 | ||
| Social cards for every page: og.g1t.sh | 18 | /** An element as satori reads it. */ |
| 19 | export type Node = { type: string; props: Record<string, unknown> & { style?: Style; children?: Child } }; | |
| 20 | type Child = Node | string | (Node | string | null | false)[] | null; | |
| 21 | type Style = Record<string, string | number>; | |
| 22 | ||
| 23 | function h(type: string, style: Style, ...children: (Node | string | null | false)[]): Node { | |
| 24 | const kids = children.filter((child): child is Node | string => child !== null && child !== false); | |
| 25 | if (kids.length === 0) return { type, props: { style } }; | |
| 26 | return { type, props: { style, children: kids.length === 1 ? kids[0] : kids } }; | |
| 27 | } | |
| 28 | ||
| 29 | function img(src: string, width: number, height: number, style: Style = {}): Node { | |
| 30 | return { type: "img", props: { src, width, height, style: { width, height, ...style } } }; | |
| 31 | } | |
| 32 | ||
| 33 | function icon(shapes: string, color: string, size: number): Node { | |
| 34 | return img(dataUri(iconSvg(shapes, color)), size, size, { flexShrink: 0 }); | |
| 35 | } | |
| 36 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 37 | const SANS = "Hanken Grotesk"; |
| 38 | /** Headlines: a card's title and the brand line. */ | |
| 39 | const DISPLAY = "Bricolage Grotesque"; | |
| 40 | const MONO = "IBM Plex Mono"; | |
| Social cards for every page: og.g1t.sh | 41 | |
| 42 | /** Long text is cut before layout, so a huge title costs nothing to lay out. */ | |
| 43 | function bound(text: string, max: number): string { | |
| 44 | const single = text.replace(/\s+/g, " ").trim(); | |
| 45 | return single.length > max ? `${single.slice(0, max - 1).trimEnd()}…` : single; | |
| 46 | } | |
| 47 | ||
| 48 | /** A block of text that stops after `lines`, with an ellipsis. */ | |
| 49 | function clamp(text: string, lines: number, style: Style): Node { | |
| 50 | return h("div", { display: "block", lineClamp: lines, overflow: "hidden", ...style }, text); | |
| 51 | } | |
| 52 | ||
| 53 | /** Bigger type for shorter titles, so a two-word title fills the card and a long one fits. */ | |
| 54 | function titleSize(text: string): number { | |
| 55 | if (text.length <= 28) return 76; | |
| 56 | if (text.length <= 56) return 64; | |
| 57 | if (text.length <= 96) return 54; | |
| 58 | return 46; | |
| 59 | } | |
| 60 | ||
| 61 | // --- Pieces -------------------------------------------------------------- | |
| 62 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 63 | /** The backdrop: dot grid, lavender light and, on most cards, the pixel 1. */ |
| 64 | function background(withOne: boolean): Node { | |
| 65 | return img(dataUri(backdropSvg(WIDTH, HEIGHT, withOne)), WIDTH, HEIGHT, { position: "absolute", top: 0, left: 0 }); | |
| Social cards for every page: og.g1t.sh | 66 | } |
| 67 | ||
| 68 | /** The lavender rule along the bottom edge. */ | |
| 69 | function rule(): Node { | |
| 70 | return h("div", { | |
| 71 | position: "absolute", | |
| 72 | left: 0, | |
| 73 | bottom: 0, | |
| 74 | width: WIDTH, | |
| 75 | height: 6, | |
| 76 | backgroundImage: `linear-gradient(to right, ${COLOR.lavenderDeep}, ${COLOR.lavender} 45%, rgba(182, 168, 255, 0.15))`, | |
| 77 | }); | |
| 78 | } | |
| 79 | ||
| 80 | function lockup(height: number, suffix?: string): Node { | |
| 81 | const width = Math.round((height * LOCKUP_BOX.width) / LOCKUP_BOX.height); | |
| 82 | return h( | |
| 83 | "div", | |
| 84 | { display: "flex", alignItems: "flex-end" }, | |
| 85 | img(dataUri(lockupSvg()), width, height), | |
| 86 | suffix | |
| 87 | ? h( | |
| 88 | "div", | |
| 89 | { | |
| 90 | display: "flex", | |
| 91 | fontFamily: SANS, | |
| 92 | fontWeight: 500, | |
| 93 | fontSize: Math.round(height * 0.62), | |
| 94 | color: COLOR.muted, | |
| 95 | marginLeft: Math.round(height * 0.22), | |
| 96 | // Sits on the wordmark's baseline. | |
| 97 | marginBottom: Math.round(height * (1 - LOCKUP_BOX.baseline / LOCKUP_BOX.height) - height * 0.1), | |
| 98 | letterSpacing: -0.5, | |
| 99 | }, | |
| 100 | suffix, | |
| 101 | ) | |
| 102 | : null, | |
| 103 | ); | |
| 104 | } | |
| 105 | ||
| 106 | function topBar(address: string, suffix?: string): Node { | |
| 107 | return h( | |
| 108 | "div", | |
| 109 | { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }, | |
| 110 | lockup(46, suffix), | |
| 111 | h( | |
| 112 | "div", | |
| 113 | { display: "flex", fontFamily: MONO, fontSize: 22, color: COLOR.faint, whiteSpace: "nowrap" }, | |
| 114 | bound(address, 46), | |
| 115 | ), | |
| 116 | ); | |
| 117 | } | |
| 118 | ||
| 119 | type Tone = { color: string; bg: string; border: string }; | |
| 120 | ||
| 121 | const TONE = { | |
| 122 | open: { color: COLOR.mint, bg: "rgba(134, 239, 196, 0.10)", border: "rgba(134, 239, 196, 0.38)" }, | |
| 123 | merged: { color: COLOR.lavender, bg: "rgba(182, 168, 255, 0.12)", border: "rgba(182, 168, 255, 0.42)" }, | |
| 124 | closed: { color: COLOR.danger, bg: "rgba(255, 131, 148, 0.10)", border: "rgba(255, 131, 148, 0.38)" }, | |
| 125 | quiet: { color: COLOR.muted, bg: "rgba(160, 160, 168, 0.08)", border: "rgba(160, 160, 168, 0.30)" }, | |
| 126 | } satisfies Record<string, Tone>; | |
| 127 | ||
| 128 | function badge(label: string, shapes: string, tone: Tone): Node { | |
| 129 | return h( | |
| 130 | "div", | |
| 131 | { | |
| 132 | display: "flex", | |
| 133 | alignItems: "center", | |
| 134 | gap: 12, | |
| 135 | padding: "10px 22px 10px 18px", | |
| 136 | borderRadius: 999, | |
| 137 | backgroundColor: tone.bg, | |
| 138 | border: `2px solid ${tone.border}`, | |
| 139 | color: tone.color, | |
| 140 | fontFamily: SANS, | |
| 141 | fontWeight: 600, | |
| 142 | fontSize: 26, | |
| 143 | }, | |
| 144 | icon(shapes, tone.color, 28), | |
| 145 | label, | |
| 146 | ); | |
| 147 | } | |
| 148 | ||
| 149 | /** A fact along the bottom: an icon, a number or a word, and what it counts. */ | |
| 150 | function fact(shapes: string, strong: string, rest: string): Node { | |
| 151 | return h( | |
| 152 | "div", | |
| 153 | { display: "flex", alignItems: "center", gap: 12, fontFamily: SANS, fontSize: 28, color: COLOR.muted }, | |
| 154 | icon(shapes, COLOR.faint, 28), | |
| 155 | h("div", { display: "flex", color: COLOR.fg, fontWeight: 600 }, strong), | |
| 156 | rest ? h("div", { display: "flex" }, rest) : null, | |
| 157 | ); | |
| 158 | } | |
| 159 | ||
| 160 | function plain(text: string, mono = false): Node { | |
| 161 | return h( | |
| 162 | "div", | |
| 163 | { display: "flex", fontFamily: mono ? MONO : SANS, fontSize: mono ? 25 : 28, color: COLOR.muted }, | |
| 164 | text, | |
| 165 | ); | |
| 166 | } | |
| 167 | ||
| 168 | const dot = () => h("div", { display: "flex", fontSize: 28, color: COLOR.faint }, "·"); | |
| 169 | ||
| 170 | /** The frame every card but the brand card shares. */ | |
| 171 | function frame(parts: { | |
| 172 | address: string; | |
| 173 | suffix?: string; | |
| 174 | eyebrow: Node; | |
| 175 | title: Node; | |
| 176 | description: string | null; | |
| 177 | facts: Node[]; | |
| 178 | descriptionLines?: number; | |
| 179 | }): Node { | |
| 180 | return h( | |
| 181 | "div", | |
| 182 | { | |
| 183 | display: "flex", | |
| 184 | position: "relative", | |
| 185 | width: WIDTH, | |
| 186 | height: HEIGHT, | |
| 187 | backgroundColor: COLOR.bg, | |
| 188 | overflow: "hidden", | |
| 189 | }, | |
| 190 | background(true), | |
| 191 | h( | |
| 192 | "div", | |
| 193 | { | |
| 194 | display: "flex", | |
| 195 | flexDirection: "column", | |
| 196 | position: "relative", | |
| 197 | width: WIDTH, | |
| 198 | height: HEIGHT, | |
| 199 | padding: `64px ${PAD}px 72px`, | |
| 200 | }, | |
| 201 | topBar(parts.address, parts.suffix), | |
| 202 | h( | |
| 203 | "div", | |
| 204 | { display: "flex", flexDirection: "column", flexGrow: 1, justifyContent: "center", width: 940 }, | |
| 205 | parts.eyebrow, | |
| 206 | h("div", { display: "flex", marginTop: 18 }, parts.title), | |
| 207 | parts.description | |
| 208 | ? clamp(bound(parts.description, 260), parts.descriptionLines ?? 2, { | |
| 209 | marginTop: 22, | |
| 210 | fontFamily: SANS, | |
| 211 | fontSize: 30, | |
| 212 | lineHeight: 1.4, | |
| 213 | color: COLOR.muted, | |
| 214 | }) | |
| 215 | : null, | |
| 216 | ), | |
| 217 | h("div", { display: "flex", alignItems: "center", gap: 24, height: 56 }, ...parts.facts), | |
| 218 | ), | |
| 219 | rule(), | |
| 220 | ); | |
| 221 | } | |
| 222 | ||
| 223 | function eyebrow(text: string, mono: boolean, color: string = COLOR.lavender): Node { | |
| 224 | return h( | |
| 225 | "div", | |
| 226 | { | |
| 227 | display: "flex", | |
| 228 | fontFamily: mono ? MONO : SANS, | |
| 229 | fontWeight: 500, | |
| 230 | fontSize: mono ? 28 : 26, | |
| 231 | color, | |
| 232 | letterSpacing: mono ? 0 : 2, | |
| 233 | textTransform: mono ? "none" : "uppercase", | |
| 234 | }, | |
| 235 | bound(text, 60), | |
| 236 | ); | |
| 237 | } | |
| 238 | ||
| 239 | function title(text: string, lines = 3): Node { | |
| 240 | const bounded = bound(text, 180); | |
| 241 | const size = titleSize(bounded); | |
| 242 | return clamp(bounded, lines, { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 243 | fontFamily: DISPLAY, |
| Social cards for every page: og.g1t.sh | 244 | fontWeight: 600, |
| 245 | fontSize: size, | |
| 246 | lineHeight: 1.12, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 247 | letterSpacing: size >= 64 ? -1.3 : -0.8, |
| Social cards for every page: og.g1t.sh | 248 | color: COLOR.fg, |
| 249 | width: 940, | |
| 250 | }); | |
| 251 | } | |
| 252 | ||
| 253 | // --- Cards --------------------------------------------------------------- | |
| 254 | ||
| 255 | function brandCard(): Node { | |
| 256 | const height = 190; | |
| 257 | const width = Math.round((height * LOCKUP_BOX.width) / LOCKUP_BOX.height); | |
| 258 | return h( | |
| 259 | "div", | |
| 260 | { | |
| 261 | display: "flex", | |
| 262 | position: "relative", | |
| 263 | width: WIDTH, | |
| 264 | height: HEIGHT, | |
| 265 | backgroundColor: COLOR.bg, | |
| 266 | overflow: "hidden", | |
| 267 | }, | |
| 268 | background(false), | |
| 269 | h( | |
| 270 | "div", | |
| 271 | { | |
| 272 | display: "flex", | |
| 273 | flexDirection: "column", | |
| 274 | alignItems: "center", | |
| 275 | justifyContent: "center", | |
| 276 | position: "relative", | |
| 277 | width: WIDTH, | |
| 278 | height: HEIGHT, | |
| 279 | }, | |
| 280 | img(dataUri(lockupSvg()), width, height), | |
| 281 | h( | |
| 282 | "div", | |
| 283 | { | |
| 284 | display: "flex", | |
| 285 | marginTop: 56, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 286 | fontFamily: DISPLAY, |
| Social cards for every page: og.g1t.sh | 287 | fontWeight: 500, |
| 288 | fontSize: 44, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 289 | letterSpacing: -0.5, |
| Social cards for every page: og.g1t.sh | 290 | color: COLOR.fgSoft, |
| 291 | }, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 292 | TAGLINE, |
| Social cards for every page: og.g1t.sh | 293 | ), |
| 294 | h( | |
| 295 | "div", | |
| 296 | { display: "flex", marginTop: 40, fontFamily: MONO, fontSize: 26, color: COLOR.lavender }, | |
| 297 | "g1t.sh", | |
| 298 | ), | |
| 299 | ), | |
| 300 | rule(), | |
| 301 | ); | |
| 302 | } | |
| 303 | ||
| 304 | const PULL_BADGE: Record<PullStatus, [string, string, Tone]> = { | |
| 305 | open: ["Open", ICON.pull, TONE.open], | |
| 306 | draft: ["Draft", ICON.pullDraft, TONE.quiet], | |
| 307 | merged: ["Merged", ICON.merged, TONE.merged], | |
| 308 | closed: ["Closed", ICON.pullClosed, TONE.closed], | |
| 309 | }; | |
| 310 | ||
| 311 | const ISSUE_BADGE: Record<IssueState, [string, string, Tone]> = { | |
| 312 | open: ["Open", ICON.issue, TONE.open], | |
| 313 | completed: ["Closed", ICON.issueDone, TONE.merged], | |
| 314 | not_planned: ["Not planned", ICON.issueNotPlanned, TONE.quiet], | |
| 315 | }; | |
| 316 | ||
| 317 | function count(n: number, one: string, many: string): [string, string] { | |
| 318 | return [n.toLocaleString("en-US"), n === 1 ? one : many]; | |
| 319 | } | |
| 320 | ||
| 321 | export function cardTree(card: Card): Node { | |
| 322 | switch (card.kind) { | |
| 323 | case "brand": | |
| 324 | return brandCard(); | |
| 325 | ||
| 326 | case "page": | |
| 327 | return frame({ | |
| 328 | address: card.address, | |
| 329 | eyebrow: eyebrow(card.eyebrow, false), | |
| 330 | title: title(card.title), | |
| 331 | description: card.description, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 332 | facts: [plain(TAGLINE)], |
| Social cards for every page: og.g1t.sh | 333 | }); |
| 334 | ||
| 335 | case "workspace": { | |
| 336 | const [n, noun] = count(card.projects, "public project", "public projects"); | |
| 337 | return frame({ | |
| 338 | address: `g1t.sh/${card.slug}`, | |
| 339 | // The slug above the name, unless they are the same. | |
| Workspace names and icons, and a component kit for every control | 340 | eyebrow: withIcon( |
| 341 | card.icon, | |
| 342 | card.name.toLowerCase() === card.slug ? eyebrow("Workspace", false) : eyebrow(card.slug, true), | |
| 343 | ), | |
| Social cards for every page: og.g1t.sh | 344 | title: title(card.name, 2), |
| 345 | description: card.description, | |
| 346 | facts: [fact(ICON.box, n, noun)], | |
| 347 | }); | |
| 348 | } | |
| 349 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 350 | case "person": { |
| 351 | const [merged, mergedNoun] = count(card.pullsMerged, "pull request merged", "pull requests merged"); | |
| 352 | const [pulls, pullNoun] = count(card.pullsOpen, "open pull request", "open pull requests"); | |
| 353 | const [issues, issueNoun] = count(card.issues, "issue opened", "issues opened"); | |
| 354 | return frame({ | |
| 355 | address: `g1t.sh/u/${card.username}`, | |
| 356 | // The username above the name they go by; "Profile" when there is none. | |
| 357 | eyebrow: withIcon(card.icon, card.name ? eyebrow(`@${card.username}`, true) : eyebrow("Profile", false), true), | |
| 358 | title: card.name ? title(card.name, 2) : repoTitle(card.username), | |
| 359 | description: card.bio, | |
| 360 | facts: [ | |
| 361 | fact(ICON.merged, merged, mergedNoun), | |
| 362 | fact(ICON.pull, pulls, pullNoun), | |
| 363 | fact(ICON.issue, issues, issueNoun), | |
| 364 | ], | |
| 365 | }); | |
| 366 | } | |
| 367 | ||
| Social cards for every page: og.g1t.sh | 368 | case "project": { |
| 369 | const [issues, issueNoun] = count(card.issues, "open issue", "open issues"); | |
| 370 | const [pulls, pullNoun] = count(card.pulls, "open pull request", "open pull requests"); | |
| 371 | const named = card.name.toLowerCase() !== card.repo.toLowerCase(); | |
| 372 | return frame({ | |
| 373 | address: `g1t.sh/${card.owner}/${card.repo}`, | |
| 374 | eyebrow: eyebrow(named ? `${card.owner}/${card.repo}` : `${card.owner} /`, true), | |
| 375 | title: named ? title(card.name, 2) : repoTitle(card.repo), | |
| 376 | description: card.description, | |
| 377 | facts: [fact(ICON.issue, issues, issueNoun), fact(ICON.pull, pulls, pullNoun)], | |
| 378 | }); | |
| 379 | } | |
| 380 | ||
| 381 | case "issue": { | |
| 382 | const [label, shapes, tone] = ISSUE_BADGE[card.state]; | |
| 383 | return frame({ | |
| 384 | address: `g1t.sh/${card.owner}/${card.repo}/issues/${card.number}`, | |
| 385 | eyebrow: eyebrowWithNumber(`${card.owner}/${card.repo}`, card.number), | |
| 386 | title: title(card.title), | |
| 387 | description: null, | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 388 | facts: [badge(label, shapes, tone), plain("Issue"), dot(), byline(card.author, card.requestedBy)], |
| Social cards for every page: og.g1t.sh | 389 | }); |
| 390 | } | |
| 391 | ||
| 392 | case "pull": { | |
| 393 | const [label, shapes, tone] = PULL_BADGE[card.state]; | |
| 394 | return frame({ | |
| 395 | address: `g1t.sh/${card.owner}/${card.repo}/pull/${card.number}`, | |
| 396 | eyebrow: eyebrowWithNumber(`${card.owner}/${card.repo}`, card.number), | |
| 397 | title: title(card.title), | |
| 398 | description: null, | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 399 | facts: [badge(label, shapes, tone), plain("Pull request"), dot(), byline(card.author, card.requestedBy)], |
| Social cards for every page: og.g1t.sh | 400 | }); |
| 401 | } | |
| 402 | ||
| 403 | case "soon": | |
| 404 | return frame({ | |
| 405 | address: `g1t.sh/${card.owner}/${card.repo}`, | |
| 406 | eyebrow: eyebrow(`${card.owner}/${card.repo}`, true), | |
| 407 | title: title(card.title, 2), | |
| 408 | description: card.summary, | |
| 409 | facts: [badge("Soon", ICON.sparkles, TONE.open), plain(card.section)], | |
| 410 | }); | |
| 411 | ||
| 412 | case "docs": | |
| 413 | return frame({ | |
| 414 | address: "docs.g1t.sh", | |
| 415 | suffix: "docs", | |
| 416 | eyebrow: eyebrow(card.section ?? "Documentation", false), | |
| 417 | title: title(card.title, 2), | |
| 418 | description: card.description, | |
| 419 | facts: [fact(ICON.book, "g1t docs", "Guides and API reference")], | |
| 420 | }); | |
| 421 | } | |
| 422 | } | |
| 423 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 424 | /** The uploaded icon beside an eyebrow, when there is one: round for a person. */ |
| 425 | function withIcon(icon: string | undefined, beside: Node, round = false): Node { | |
| Workspace names and icons, and a component kit for every control | 426 | if (!icon) return beside; |
| 427 | return h( | |
| 428 | "div", | |
| 429 | { display: "flex", alignItems: "center", gap: 22 }, | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 430 | img(icon, 64, 64, { borderRadius: round ? 32 : 14, objectFit: "cover", flexShrink: 0 }), |
| Workspace names and icons, and a component kit for every control | 431 | beside, |
| 432 | ); | |
| 433 | } | |
| 434 | ||
| Social cards for every page: og.g1t.sh | 435 | /** A repository's name in mono, large: the project's own address. */ |
| 436 | function repoTitle(name: string): Node { | |
| 437 | const bounded = bound(name, 60); | |
| 438 | const size = bounded.length <= 16 ? 84 : bounded.length <= 26 ? 68 : 52; | |
| 439 | return clamp(bounded, 2, { | |
| 440 | fontFamily: MONO, | |
| 441 | fontWeight: 500, | |
| 442 | fontSize: size, | |
| 443 | lineHeight: 1.12, | |
| 444 | letterSpacing: -1, | |
| 445 | color: COLOR.fg, | |
| 446 | width: 940, | |
| 447 | }); | |
| 448 | } | |
| 449 | ||
| 450 | function eyebrowWithNumber(repo: string, number: number): Node { | |
| 451 | return h( | |
| 452 | "div", | |
| 453 | { display: "flex", alignItems: "baseline", fontFamily: MONO, fontWeight: 500, fontSize: 28 }, | |
| 454 | h("div", { display: "flex", color: COLOR.lavender }, bound(repo, 48)), | |
| 455 | h("div", { display: "flex", color: COLOR.faint, marginLeft: 14 }, `#${number}`), | |
| 456 | ); | |
| 457 | } | |
| 458 | ||
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 459 | /** "by ana", or "by g1t for ana" for what g1t opened for someone. */ |
| 460 | function byline(author: string, requestedBy: string | null): Node { | |
| 461 | const name = (text: string) => h("div", { display: "flex", fontFamily: MONO, fontSize: 26, color: COLOR.fgSoft }, bound(text, 32)); | |
| Social cards for every page: og.g1t.sh | 462 | return h( |
| 463 | "div", | |
| 464 | { display: "flex", alignItems: "center", gap: 10, fontFamily: SANS, fontSize: 28, color: COLOR.muted }, | |
| 465 | "by", | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 466 | name(author), |
| 467 | ...(requestedBy ? ["for", name(requestedBy)] : []), | |
| Social cards for every page: og.g1t.sh | 468 | ); |
| 469 | } |