g1t/apps/web/app/components/illustrations.tsx
| 1 | /** |
| 2 | * Line-art illustrations for the marketing pages. They are plain SVG with |
| 3 | * SMIL animation, so they render on the server and move without any script. |
| 4 | */ |
| 5 | |
| 6 | const LINE = "var(--color-line-strong)"; |
| 7 | const FAINT = "var(--color-faint)"; |
| 8 | const MUTED = "var(--color-muted)"; |
| 9 | const ACCENT = "var(--color-accent)"; |
| 10 | const DANGER = "var(--color-danger)"; |
| 11 | const SURFACE = "var(--color-surface)"; |
| 12 | const RAISED = "var(--color-raised)"; |
| 13 | |
| 14 | /** A dot that travels along `path` forever. */ |
| 15 | function Pulse({ |
| 16 | path, |
| 17 | dur, |
| 18 | begin = "0s", |
| 19 | color = ACCENT, |
| 20 | r = 3, |
| 21 | }: { |
| 22 | path: string; |
| 23 | dur: string; |
| 24 | begin?: string; |
| 25 | color?: string; |
| 26 | r?: number; |
| 27 | }) { |
| 28 | return ( |
| 29 | <circle r={r} fill={color}> |
| 30 | <animateMotion path={path} dur={dur} begin={begin} repeatCount="indefinite" /> |
| 31 | <animate |
| 32 | attributeName="opacity" |
| 33 | values="0;1;1;0" |
| 34 | keyTimes="0;0.1;0.85;1" |
| 35 | dur={dur} |
| 36 | begin={begin} |
| 37 | repeatCount="indefinite" |
| 38 | /> |
| 39 | </circle> |
| 40 | ); |
| 41 | } |
| 42 | |
| 43 | const LANES = [ |
| 44 | { y: 60, state: "passed" }, |
| 45 | { y: 135, state: "failed" }, |
| 46 | { y: 210, state: "merged" }, |
| 47 | { y: 285, state: "passed" }, |
| 48 | { y: 360, state: "working" }, |
| 49 | ] as const; |
| 50 | |
| 51 | /** |
| 52 | * The hero: one issue fans out to five pull requests, each in its own |
| 53 | * lane; the one that is merged lands on main. |
| 54 | */ |
| 55 | export function HeroIllustration() { |
| 56 | return ( |
| 57 | <svg |
| 58 | viewBox="0 0 1000 420" |
| 59 | fill="none" |
| 60 | className="w-full" |
| 61 | role="img" |
| 62 | aria-label="An issue fans out to five pull requests being worked on in parallel; one is merged into main." |
| 63 | > |
| 64 | {/* Faint grid. */} |
| 65 | <g stroke={LINE} strokeWidth="1" opacity="0.35"> |
| 66 | {[60, 135, 210, 285, 360].map((y) => ( |
| 67 | <path key={y} d={`M0 ${y}H1000`} strokeDasharray="2 10" /> |
| 68 | ))} |
| 69 | {[250, 400, 550, 700].map((x) => ( |
| 70 | <path key={x} d={`M${x} 20V400`} strokeDasharray="2 10" /> |
| 71 | ))} |
| 72 | </g> |
| 73 | |
| 74 | {/* The issue. */} |
| 75 | <g> |
| 76 | <rect x="24" y="150" width="170" height="120" rx="12" fill={SURFACE} stroke={LINE} /> |
| 77 | <text x="42" y="178" fill={MUTED} fontSize="11" fontFamily="var(--font-mono)"> |
| 78 | issue #12 |
| 79 | </text> |
| 80 | <rect x="42" y="190" width="120" height="7" rx="3.5" fill={FAINT} /> |
| 81 | <rect x="42" y="206" width="134" height="5" rx="2.5" fill={LINE} /> |
| 82 | <rect x="42" y="218" width="104" height="5" rx="2.5" fill={LINE} /> |
| 83 | <g fontSize="10" fontFamily="var(--font-mono)" fill={MUTED}> |
| 84 | <path d="M43 245l3 3 6-6" stroke={ACCENT} strokeWidth="1.5" strokeLinecap="round" /> |
| 85 | <text x="58" y="250"> |
| 86 | cargo test |
| 87 | </text> |
| 88 | </g> |
| 89 | <circle cx="194" cy="210" r="4" fill={SURFACE} stroke={MUTED} strokeWidth="1.5" /> |
| 90 | </g> |
| 91 | |
| 92 | {LANES.map(({ y, state }, i) => { |
| 93 | const out = `M194 210C250 210 250 ${y} 310 ${y}`; |
| 94 | const lane = `M310 ${y}H690`; |
| 95 | const back = `M690 ${y}C750 ${y} 750 210 806 210`; |
| 96 | const merges = state === "merged"; |
| 97 | const failed = state === "failed"; |
| 98 | const working = state === "working"; |
| 99 | const stroke = merges ? ACCENT : failed ? LINE : FAINT; |
| 100 | const end = working ? 520 : failed ? 480 : 690; |
| 101 | return ( |
| 102 | <g key={y}> |
| 103 | <path d={out} stroke={stroke} strokeWidth="1.5" /> |
| 104 | <path |
| 105 | d={`M310 ${y}H${end}`} |
| 106 | stroke={stroke} |
| 107 | strokeWidth="1.5" |
| 108 | strokeDasharray={failed ? "4 5" : undefined} |
| 109 | /> |
| 110 | {working && ( |
| 111 | <path |
| 112 | d={`M${end} ${y}H690`} |
| 113 | stroke={LINE} |
| 114 | strokeWidth="1.5" |
| 115 | strokeDasharray="4 4" |
| 116 | className="animate-dash" |
| 117 | /> |
| 118 | )} |
| 119 | {!failed && !working && ( |
| 120 | <path |
| 121 | d={back} |
| 122 | stroke={stroke} |
| 123 | strokeWidth="1.5" |
| 124 | strokeDasharray={merges ? undefined : "3 6"} |
| 125 | opacity={merges ? 1 : 0.5} |
| 126 | /> |
| 127 | )} |
| 128 | |
| 129 | <text x="310" y={y - 14} fill={merges ? ACCENT : MUTED} fontSize="11" fontFamily="var(--font-mono)"> |
| 130 | #{13 + i} |
| 131 | </text> |
| 132 | |
| 133 | {/* Commits along the lane. */} |
| 134 | {[350, 410, 470, 560, 630] |
| 135 | .filter((x) => x <= end) |
| 136 | .map((x) => ( |
| 137 | <circle |
| 138 | key={x} |
| 139 | cx={x} |
| 140 | cy={y} |
| 141 | r="4" |
| 142 | fill={SURFACE} |
| 143 | stroke={merges ? ACCENT : MUTED} |
| 144 | strokeWidth="1.5" |
| 145 | /> |
| 146 | ))} |
| 147 | |
| 148 | {failed && ( |
| 149 | <path |
| 150 | d={`M${end + 6} ${y - 5}l10 10m0-10l-10 10`} |
| 151 | stroke={DANGER} |
| 152 | strokeWidth="1.5" |
| 153 | strokeLinecap="round" |
| 154 | /> |
| 155 | )} |
| 156 | {working && ( |
| 157 | <circle cx={end} cy={y} r="5" fill={ACCENT}> |
| 158 | <animate attributeName="r" values="4;7;4" dur="1.6s" repeatCount="indefinite" /> |
| 159 | <animate attributeName="opacity" values="1;0.4;1" dur="1.6s" repeatCount="indefinite" /> |
| 160 | </circle> |
| 161 | )} |
| 162 | |
| 163 | <Pulse |
| 164 | path={`${out}H${end}`} |
| 165 | dur={`${3.2 + i * 0.35}s`} |
| 166 | begin={`${i * 0.5}s`} |
| 167 | color={merges ? ACCENT : MUTED} |
| 168 | /> |
| 169 | {merges && <Pulse path={`${lane}${back.replace("M690 " + y, "")}`} dur="2.4s" begin="1.2s" r={3.5} />} |
| 170 | </g> |
| 171 | ); |
| 172 | })} |
| 173 | |
| 174 | {/* Main. */} |
| 175 | <g> |
| 176 | <circle cx="840" cy="210" r="34" fill={SURFACE} stroke={ACCENT} strokeWidth="1.5" /> |
| 177 | <circle cx="840" cy="210" r="34" stroke={ACCENT} strokeWidth="1.5" opacity="0.5"> |
| 178 | <animate attributeName="r" values="34;50" dur="2.4s" repeatCount="indefinite" /> |
| 179 | <animate attributeName="opacity" values="0.5;0" dur="2.4s" repeatCount="indefinite" /> |
| 180 | </circle> |
| 181 | <path |
| 182 | d="M826 210l10 10 18-20" |
| 183 | stroke={ACCENT} |
| 184 | strokeWidth="2.5" |
| 185 | strokeLinecap="round" |
| 186 | strokeLinejoin="round" |
| 187 | /> |
| 188 | <path d="M874 210H976" stroke={ACCENT} strokeWidth="1.5" /> |
| 189 | <text x="902" y="198" fill={ACCENT} fontSize="12" fontFamily="var(--font-mono)"> |
| 190 | main |
| 191 | </text> |
| 192 | {[910, 945].map((x) => ( |
| 193 | <circle key={x} cx={x} cy="210" r="4" fill={SURFACE} stroke={ACCENT} strokeWidth="1.5" /> |
| 194 | ))} |
| 195 | </g> |
| 196 | </svg> |
| 197 | ); |
| 198 | } |
| 199 | |
| 200 | /** An isometric tile, as a path centred on (cx, cy). */ |
| 201 | function tile(cx: number, cy: number, w = 60, h = 30): string { |
| 202 | return `M${cx} ${cy - h}L${cx + w} ${cy}L${cx} ${cy + h}L${cx - w} ${cy}Z`; |
| 203 | } |
| 204 | |
| 205 | /** An issue with acceptance checks ticking off. */ |
| 206 | export function IssueIllustration() { |
| 207 | return ( |
| 208 | <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true"> |
| 209 | <path d="M0 150H320" stroke={LINE} strokeDasharray="2 8" /> |
| 210 | <rect x="70" y="22" width="180" height="136" rx="10" fill={RAISED} stroke={LINE} /> |
| 211 | <rect x="90" y="42" width="96" height="8" rx="4" fill={MUTED} /> |
| 212 | <rect x="90" y="62" width="140" height="5" rx="2.5" fill={LINE} /> |
| 213 | <rect x="90" y="74" width="120" height="5" rx="2.5" fill={LINE} /> |
| 214 | <rect x="90" y="86" width="132" height="5" rx="2.5" fill={LINE} /> |
| 215 | {[110, 128, 146].map((y, i) => ( |
| 216 | <g key={y}> |
| 217 | <rect x="90" y={y - 7} width="12" height="12" rx="3" stroke={FAINT} /> |
| 218 | <path |
| 219 | d={`M93 ${y - 1}l2.5 2.5 4.5-5`} |
| 220 | stroke={ACCENT} |
| 221 | strokeWidth="1.5" |
| 222 | strokeLinecap="round" |
| 223 | opacity="0" |
| 224 | > |
| 225 | <animate |
| 226 | attributeName="opacity" |
| 227 | values="0;0;1;1;0" |
| 228 | keyTimes={`0;${0.15 + i * 0.2};${0.2 + i * 0.2};0.95;1`} |
| 229 | dur="5s" |
| 230 | repeatCount="indefinite" |
| 231 | /> |
| 232 | </path> |
| 233 | <rect x="110" y={y - 3} width={90 - i * 14} height="5" rx="2.5" fill={FAINT} /> |
| 234 | </g> |
| 235 | ))} |
| 236 | </svg> |
| 237 | ); |
| 238 | } |
| 239 | |
| 240 | /** One repository lifting into a stack of separate copies. */ |
| 241 | export function ForkIllustration() { |
| 242 | return ( |
| 243 | <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true"> |
| 244 | <path d="M40 150L160 90L280 150" stroke={LINE} strokeDasharray="2 8" /> |
| 245 | <path d={tile(160, 138)} fill={RAISED} stroke={MUTED} /> |
| 246 | {[0, 1, 2].map((i) => ( |
| 247 | <g key={i}> |
| 248 | <path |
| 249 | d={tile(160, 138)} |
| 250 | fill={SURFACE} |
| 251 | stroke={i === 1 ? ACCENT : FAINT} |
| 252 | opacity="0" |
| 253 | > |
| 254 | <animateTransform |
| 255 | attributeName="transform" |
| 256 | type="translate" |
| 257 | values={`0 0;${(i - 1) * 84} ${-58 - (i === 1 ? 22 : 0)};${(i - 1) * 84} ${-58 - (i === 1 ? 22 : 0)};0 0`} |
| 258 | keyTimes="0;0.3;0.85;1" |
| 259 | dur="5s" |
| 260 | begin={`${i * 0.15}s`} |
| 261 | repeatCount="indefinite" |
| 262 | /> |
| 263 | <animate |
| 264 | attributeName="opacity" |
| 265 | values="0;1;1;0" |
| 266 | keyTimes="0;0.15;0.85;1" |
| 267 | dur="5s" |
| 268 | begin={`${i * 0.15}s`} |
| 269 | repeatCount="indefinite" |
| 270 | /> |
| 271 | </path> |
| 272 | </g> |
| 273 | ))} |
| 274 | <path d="M160 138V168M100 138V160M220 138V160" stroke={LINE} /> |
| 275 | </svg> |
| 276 | ); |
| 277 | } |
| 278 | |
| 279 | /** A line of code linked to the reasoning that produced it. */ |
| 280 | export function SessionIllustration() { |
| 281 | return ( |
| 282 | <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true"> |
| 283 | <rect x="20" y="30" width="150" height="120" rx="10" fill={RAISED} stroke={LINE} /> |
| 284 | {[52, 68, 84, 100, 116, 132].map((y, i) => ( |
| 285 | <g key={y}> |
| 286 | <rect x="34" y={y - 3} width="8" height="5" rx="2" fill={LINE} /> |
| 287 | <rect |
| 288 | x="50" |
| 289 | y={y - 3} |
| 290 | width={[80, 104, 64, 96, 72, 88][i]} |
| 291 | height="5" |
| 292 | rx="2.5" |
| 293 | fill={i === 3 ? ACCENT : FAINT} |
| 294 | opacity={i === 3 ? 0.9 : 0.6} |
| 295 | /> |
| 296 | </g> |
| 297 | ))} |
| 298 | <rect x="28" y="92" width="134" height="16" rx="4" stroke={ACCENT} opacity="0.5" /> |
| 299 | <path d="M162 100H196" stroke={ACCENT} strokeDasharray="3 4" className="animate-dash" /> |
| 300 | <rect x="196" y="58" width="108" height="84" rx="10" fill={SURFACE} stroke={ACCENT} strokeOpacity="0.6" /> |
| 301 | <circle cx="212" cy="76" r="5" fill={ACCENT} /> |
| 302 | <rect x="224" y="73" width="52" height="5" rx="2.5" fill={MUTED} /> |
| 303 | <rect x="208" y="92" width="84" height="5" rx="2.5" fill={FAINT} /> |
| 304 | <rect x="208" y="104" width="70" height="5" rx="2.5" fill={FAINT} /> |
| 305 | <rect x="208" y="116" width="78" height="5" rx="2.5" fill={FAINT} /> |
| 306 | </svg> |
| 307 | ); |
| 308 | } |
| 309 | |
| 310 | /** Any agent, anywhere, joining through one endpoint. */ |
| 311 | export function AgentsIllustration() { |
| 312 | const spokes = [ |
| 313 | { x: 50, y: 40 }, |
| 314 | { x: 50, y: 140 }, |
| 315 | { x: 270, y: 40 }, |
| 316 | { x: 270, y: 140 }, |
| 317 | ]; |
| 318 | return ( |
| 319 | <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true"> |
| 320 | {spokes.map(({ x, y }, i) => { |
| 321 | const path = `M${x} ${y}L160 90`; |
| 322 | return ( |
| 323 | <g key={`${x}-${y}`}> |
| 324 | <path d={path} stroke={LINE} /> |
| 325 | <rect x={x - 26} y={y - 16} width="52" height="32" rx="6" fill={RAISED} stroke={FAINT} /> |
| 326 | <path d={`M${x - 16} ${y - 4}l5 4-5 4`} stroke={MUTED} strokeWidth="1.5" strokeLinecap="round" /> |
| 327 | <rect x={x - 5} y={y + 2} width="14" height="3" rx="1.5" fill={FAINT} /> |
| 328 | <Pulse path={path} dur="2.4s" begin={`${i * 0.6}s`} /> |
| 329 | </g> |
| 330 | ); |
| 331 | })} |
| 332 | <circle cx="160" cy="90" r="26" fill={SURFACE} stroke={ACCENT} /> |
| 333 | <text x="160" y="94" textAnchor="middle" fill={ACCENT} fontSize="11" fontFamily="var(--font-mono)"> |
| 334 | mcp |
| 335 | </text> |
| 336 | </svg> |
| 337 | ); |
| 338 | } |
| 339 | |
| 340 | /** Every change published once and delivered to each subscriber. */ |
| 341 | export function EventsIllustration() { |
| 342 | const sinks = [50, 90, 130]; |
| 343 | return ( |
| 344 | <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true"> |
| 345 | <path d="M20 90H150" stroke={FAINT} /> |
| 346 | {[0, 1, 2].map((i) => ( |
| 347 | <Pulse key={i} path="M20 90H150" dur="1.8s" begin={`${i * 0.6}s`} color={MUTED} /> |
| 348 | ))} |
| 349 | <rect x="150" y="68" width="44" height="44" rx="10" fill={RAISED} stroke={ACCENT} /> |
| 350 | <path d="M163 90h18m-6-6l6 6-6 6" stroke={ACCENT} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> |
| 351 | {sinks.map((y, i) => { |
| 352 | const path = `M194 90C230 90 220 ${y} 256 ${y}`; |
| 353 | return ( |
| 354 | <g key={y}> |
| 355 | <path d={path} stroke={LINE} /> |
| 356 | <rect x="256" y={y - 12} width="44" height="24" rx="6" fill={SURFACE} stroke={FAINT} /> |
| 357 | <rect x="266" y={y - 2.5} width="24" height="5" rx="2.5" fill={FAINT} /> |
| 358 | <Pulse path={path} dur="1.8s" begin={`${0.9 + i * 0.3}s`} /> |
| 359 | </g> |
| 360 | ); |
| 361 | })} |
| 362 | </svg> |
| 363 | ); |
| 364 | } |
| 365 | |
| 366 | /** Pull requests queued in order, landing on main one at a time. */ |
| 367 | export function MergeIllustration() { |
| 368 | return ( |
| 369 | <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true"> |
| 370 | <path d="M20 120H300" stroke={ACCENT} strokeWidth="1.5" /> |
| 371 | {[60, 110, 160].map((x) => ( |
| 372 | <circle key={x} cx={x} cy="120" r="4" fill={SURFACE} stroke={ACCENT} strokeWidth="1.5" /> |
| 373 | ))} |
| 374 | {[0, 1, 2].map((i) => { |
| 375 | const x = 200 + i * 34; |
| 376 | return ( |
| 377 | <g key={i}> |
| 378 | <rect |
| 379 | x={x - 12} |
| 380 | y={50 - i * 4} |
| 381 | width="24" |
| 382 | height="24" |
| 383 | rx="6" |
| 384 | fill={RAISED} |
| 385 | stroke={i === 0 ? ACCENT : FAINT} |
| 386 | > |
| 387 | {i === 0 && ( |
| 388 | <animate attributeName="y" values="50;108;108;50" keyTimes="0;0.4;0.9;1" dur="4s" repeatCount="indefinite" /> |
| 389 | )} |
| 390 | </rect> |
| 391 | {i > 0 && <path d={`M${x} ${78 - i * 4}V120`} stroke={LINE} strokeDasharray="2 5" />} |
| 392 | </g> |
| 393 | ); |
| 394 | })} |
| 395 | <text x="20" y="144" fill={MUTED} fontSize="11" fontFamily="var(--font-mono)"> |
| 396 | main |
| 397 | </text> |
| 398 | </svg> |
| 399 | ); |
| 400 | } |