SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
7.8 KB · 159 lines tsx
Raw Blame History
1import { MARK_DARK, MarkImg } from './mark';23/**4 * Shared chrome for Open Graph images (ImageResponse cannot read CSS variables → dark-theme token hex values inlined).5 * `Frame` = header lockup + content + footer; `Wallpaper` = the dark atlas plate with a faint coordinate grid and the mark6 * large at left — used by the root OG image and by the per-type OG images (D1–D3) through `Wallpaper` + `Counters`.7 */8export const INK = '#e9ebf0';9export const INK2 = '#a3a9b8';10export const INK3 = '#6f7688';11export const ACCENT = '#6d95ff';12export const ACCENT2 = '#f2b04a';13export const CANVAS = '#0b0d11';14export const SURFACE = '#12151b';15export const RULE = 'rgba(190,200,225,0.14)';1617export function Mark({ px = 56 }: { px?: number }) {18  return <MarkImg px={px} colors={{ ...MARK_DARK, plate: SURFACE }} />;19}2021/** Faint coordinate grid covering the whole image (absolute, behind content). */22export function Grid({ step = 60, opacity = 1 }: { step?: number; opacity?: number }) {23  return (24    <div25      style={{26        position: 'absolute',27        inset: 0,28        display: 'flex',29        opacity,30        backgroundImage: `linear-gradient(to right, ${RULE} 1px, transparent 1px), linear-gradient(to bottom, ${RULE} 1px, transparent 1px)`,31        backgroundSize: `${step}px ${step}px`,32      }}33    />34  );35}3637export function Lockup({ px = 56, fontSize = 32 }: { px?: number; fontSize?: number }) {38  return (39    <div style={{ display: 'flex', alignItems: 'center', gap: Math.round(px * 0.3) }}>40      <Mark px={px} />41      <div style={{ display: 'flex', fontSize, letterSpacing: -1, alignItems: 'baseline' }}>42        <span style={{ fontWeight: 700 }}>AI</span>43        <span style={{ color: INK2, marginLeft: Math.round(fontSize * 0.28), fontWeight: 500 }}>Atlas</span>44      </div>45    </div>46  );47}4849export function Frame({ children, footer }: { children: React.ReactNode; footer: string }) {50  return (51    <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '56px 72px', background: CANVAS, color: INK, fontFamily: 'sans-serif', position: 'relative' }}>52      <Grid />53      <div style={{ position: 'absolute', right: -160, top: -200, width: 720, height: 720, borderRadius: 999, background: 'radial-gradient(circle at 40% 40%, rgba(109,149,255,0.16), rgba(11,13,17,0) 70%)', display: 'flex' }} />54      <div style={{ display: 'flex', position: 'relative' }}>55        <Lockup />56      </div>57      <div style={{ display: 'flex', flexDirection: 'column', position: 'relative' }}>{children}</div>58      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', color: INK3, fontSize: 21, position: 'relative' }}>59        <span>{footer}</span>60        <span>who said it · when · where · how it was extracted</span>61      </div>62    </div>63  );64}6566export function Eyebrow({ children, color = ACCENT }: { children: string; color?: string }) {67  return <div style={{ display: 'flex', fontSize: 18, letterSpacing: 3, textTransform: 'uppercase', color, fontWeight: 600 }}>{children}</div>;68}6970export function Title({ children }: { children: string }) {71  const long = children.length > 26;72  return (73    <div style={{ display: 'flex', fontSize: long ? 58 : 74, fontWeight: 600, lineHeight: 1.04, letterSpacing: -2.2, maxWidth: 1040, overflow: 'hidden', maxHeight: long ? 128 : 160 }}>74      <span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>{children}</span>75    </div>76  );77}7879export function Facts({ items }: { items: [string, string][] }) {80  if (!items.length) return null;81  return (82    <div style={{ display: 'flex', gap: 52, marginTop: 10 }}>83      {items.map(([label, value]) => (84        <div key={label} style={{ display: 'flex', flexDirection: 'column' }}>85          <span style={{ fontSize: 15, letterSpacing: 2.5, textTransform: 'uppercase', color: INK3, fontWeight: 600 }}>{label}</span>86          <span style={{ fontSize: 34, fontWeight: 600, color: INK, letterSpacing: -1, marginTop: 6 }}>{value}</span>87        </div>88      ))}89    </div>90  );91}9293/** Live counter strip (label above value, hairline separators). Empty array → renders nothing (graceful fallback). */94export function Counters({ items, size = 'md' }: { items: [string, string][]; size?: 'sm' | 'md' }) {95  if (!items.length) return null;96  const vs = size === 'sm' ? 30 : 38;97  return (98    <div style={{ display: 'flex', borderTop: `1px solid ${RULE}`, paddingTop: 18, gap: 0 }}>99      {items.map(([label, value], i) => (100        <div key={label} style={{ display: 'flex', flexDirection: 'column', paddingRight: 36, paddingLeft: i === 0 ? 0 : 36, borderLeft: i === 0 ? 'none' : `1px solid ${RULE}` }}>101          <span style={{ fontSize: 14, letterSpacing: 2.5, textTransform: 'uppercase', color: INK3, fontWeight: 600 }}>{label}</span>102          <span style={{ fontSize: vs, fontWeight: 600, color: INK, letterSpacing: -1, marginTop: 4, fontVariantNumeric: 'tabular-nums' }}>{value}</span>103        </div>104      ))}105    </div>106  );107}108109/**110 * The "wallpaper": dark plate, faint grid, the mark large at left, title + tagline at right and a counter strip at the bottom.111 * `eyebrow` defaults to the site tagline; `title` to "AI Atlas". Per-type OG images pass their own eyebrow/title/counters.112 */113export function Wallpaper({ eyebrow, title = 'AI Atlas', subtitle, counters = [], footer = 'www.ai-atlas.co', markPx = 300 }: { eyebrow?: string; title?: string; subtitle?: string; counters?: [string, string][]; footer?: string; markPx?: number }) {114  return (115    <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '56px 72px 48px', background: CANVAS, color: INK, fontFamily: 'sans-serif', position: 'relative' }}>116      <Grid step={50} />117      <div style={{ position: 'absolute', left: -120, bottom: -260, width: 760, height: 760, borderRadius: 999, background: 'radial-gradient(circle at 50% 50%, rgba(109,149,255,0.14), rgba(11,13,17,0) 68%)', display: 'flex' }} />118      <div style={{ display: 'flex', alignItems: 'center', gap: 56, position: 'relative', flex: 1 }}>119        <div style={{ display: 'flex', flexShrink: 0 }}>120          <MarkImg px={markPx} colors={{ ...MARK_DARK, plate: SURFACE }} />121        </div>122        <div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>123          {eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}124          <div style={{ display: 'flex', fontSize: title.length > 22 ? 56 : 84, fontWeight: 700, lineHeight: 1, letterSpacing: -3, maxHeight: 180, overflow: 'hidden' }}>125            {title === 'AI Atlas' ? (126              <>127                <span>AI</span>128                <span style={{ color: INK2, marginLeft: 22, fontWeight: 500 }}>Atlas</span>129              </>130            ) : (131              <span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</span>132            )}133          </div>134          {subtitle && <div style={{ display: 'flex', fontSize: 27, color: INK2, lineHeight: 1.3, maxWidth: 720 }}>{subtitle}</div>}135        </div>136      </div>137      <div style={{ display: 'flex', flexDirection: 'column', gap: 16, position: 'relative' }}>138        <Counters items={counters} />139        <div style={{ display: 'flex', justifyContent: 'space-between', color: INK3, fontSize: 19 }}>140          <span>{footer}</span>141          <span>who said it · when · where · how it was extracted</span>142        </div>143      </div>144    </div>145  );146}147148export function Fallback({ label }: { label: string }) {149  return (150    <Frame footer="www.ai-atlas.co">151      <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>152        <Eyebrow>{label}</Eyebrow>153        <Title>Explore the AI ecosystem.</Title>154        <div style={{ fontSize: 24, color: INK2, display: 'flex' }}>Models · organizations · research · providers & pricing · benchmarks · hardware</div>155      </div>156    </Frame>157  );158}159