import { codeToTokens, type ThemedToken } from "shiki"; export const name = "readme-banner"; // GitHub's README column runs around 960 CSS px on wide screens; the banner is // designed for that width, rendered at dpr 2, and displayed fluid. export const width = 960; export const height = 220; export const images = [{ src: "takumi.svg", path: "takumi.svg" }]; export const fonts = ["geist/Geist[wght].woff2", "geist/GeistMono[wght].woff2"]; export const animation = { durationMs: 10000, fps: 30, dpr: 2, lossless: true }; const SNIPPETS = [ `// the code behind this banner await render(, { width: ${width}, height: ${height}, });`, `// paged PDF from the same JSX await render(, { size: "a4", footer: , });`, `// or an animated WebP await renderAnimation({ fps: 30, format: "webp", scenes: [{ node: }], });`, `// next/og drop-in, Tailwind included new ImageResponse(

Hello

, { width: 1200, height: 630 }, );`, ]; const SLIDES: ThemedToken[][][] = await Promise.all( SNIPPETS.map( async (code) => (await codeToTokens(code, { lang: "tsx", theme: "github-dark-default" })).tokens, ), ); const FEATURES = ["Images", "PDFs", "Animations", "next/og"]; const LINE_HEIGHT = 30; const SLIDE_HEIGHT = LINE_HEIGHT * 5; const SLIDE_MS = animation.durationMs / SNIPPETS.length; export const css = [ { selector: ".banner", style: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", height: "100%", padding: "0 3rem 0 3.5rem", backgroundColor: "#0d1117", fontFamily: "Geist, sans-serif", }, }, { selector: ".brand", style: { display: "flex", flexDirection: "column" } }, { selector: ".lockup", style: { display: "flex", alignItems: "center" }, }, { selector: ".features", style: { display: "flex", gap: "22px", marginTop: "16px", marginLeft: "4px", fontSize: "16px", fontWeight: 500, letterSpacing: "-0.005em", }, }, { selector: ".feature", style: { display: "flex", color: "#484f58", animation: `active ${animation.durationMs}ms ease-in-out infinite`, }, }, { selector: ".logo", style: { width: "64px", height: "64px" } }, { selector: ".wordmark", style: { marginLeft: "14px", fontSize: "56px", fontWeight: 750, letterSpacing: "-0.05em", color: "#e6edf3", }, }, { selector: ".window", style: { display: "flex", position: "relative", width: "368px", height: `${SLIDE_HEIGHT}px`, fontFamily: '"Geist Mono", monospace', fontSize: "16px", lineHeight: `${LINE_HEIGHT}px`, whiteSpace: "pre", }, }, { selector: ".slide", style: { display: "flex", position: "absolute", inset: 0, flexDirection: "column", justifyContent: "center", opacity: 0, animation: `fade ${animation.durationMs}ms ease-in-out infinite`, }, }, { selector: ".slide span", style: { display: "flex" } }, { selector: ".caret", style: { width: "9px", height: "20px", marginTop: "5px", backgroundColor: "#e6edf3", animation: "blink 1.25s step-end infinite", }, }, // Each slide holds a quarter of the loop; fades overlap by 3% (0.3s) as a // crossfade, wrapping across the loop boundary through negative delays. { keyframes: "fade", steps: [ { offset: "0%", style: { opacity: 0 } }, { offset: "3%", style: { opacity: 1 } }, { offset: "25%", style: { opacity: 1 } }, { offset: "28%", style: { opacity: 0 } }, { offset: "to", style: { opacity: 0 } }, ], }, // Shares the slides' phase: the label whose snippet is showing lights up. { keyframes: "active", steps: [ { offset: "0%", style: { color: "#484f58" } }, { offset: "3%", style: { color: "#e6edf3" } }, { offset: "25%", style: { color: "#e6edf3" } }, { offset: "28%", style: { color: "#484f58" } }, { offset: "to", style: { color: "#484f58" } }, ], }, { keyframes: "blink", steps: [ { offset: "0%", style: { opacity: 1 } }, { offset: "50%", style: { opacity: 0 } }, { offset: "to", style: { opacity: 1 } }, ], }, ]; // Phase-shifted so frame 0 shows the first slide fully settled. function Slide({ lines, index }: { lines: ThemedToken[][]; index: number }) { return (
{lines.map((line, row) => ( {line.map((token, column) => ( {token.content} ))} {row === lines.length - 1 && } ))}
); } export default function Banner() { return (
Takumi
{FEATURES.map((label, index) => ( {label} ))}
{SLIDES.map((lines, index) => ( ))}
); }