// Assemble the pure-SVG per-country brand kit (avatar, favicon, two-colour wordmark logo — each // SVG + PNG) from the single source of truth (src/config/accents.ts + scopes.ts). All colours // derive from the accent packs; nothing is hand-picked here. The Bluesky BANNER is separate // (scripts/gen-banners.mjs — it composites the character-map, so it's a rendered raster, not SVG). // // node scripts/gen-brandkit.mjs [outdir] (default: dist/brandkit) // // Self-contained (renders every SVG from the packs, so it also covers friendly non-scope sites like // atproto.fr). Font-dependent (JetBrains Mono via fontconfig), like gen-icons/og-card — run it on a // machine with the font. Outputs one folder per site plus a README. import sharp from 'sharp'; import { writeFile, mkdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { ACCENT_PACKS } from '../src/config/accents.ts'; import { SCOPES } from '../src/config/scopes.ts'; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, '..'); const outDir = resolve(process.cwd(), process.argv[2] ?? 'dist/brandkit'); // Dark tones the marks sit on (global.css dark theme). const TILE = '#1d2021'; const INK_DARK = '#fbf1c7'; // inkstrong on dark const INK_LIGHT = '#282828'; // inkstrong on light (light-bg logo) const MONO = "'JetBrains Mono', ui-monospace, monospace"; const hue = (pack, mode) => ACCENT_PACKS[pack][mode].accent; // The sites: every built scope (with its two-colour pair) + friendly non-scope communities. atproto.fr // is an existing French community on their own design — we make them a proper lockup (blue/red). const SITES = [ ...Object.values(SCOPES).map((s) => ({ cc: s.cc, p: s.accent, s: s.accent2 })), { cc: 'fr', p: 'cobalt', s: 'red', external: true }, // Prospective countries: no site yet, but flag-derived assets ready if a community starts one. { cc: 'de', p: 'red', s: 'yellow', prospective: true }, ]; // --- favicon-.svg / avatar-.svg (two-colour, mirrors scripts/gen-brand.mjs) ------------- // Avatar's slashes carry the pair like the wordmark (slash 1 = primary, slash 2 = secondary), cursor // stays primary; favicon has no slashes, so its bar splits top = primary / bottom = secondary. const faviconSvg = (cc, accent, accent2) => `${cc}\n`; const avatarSvg = (cc, accent, accent2) => `at://${cc}\n`; // --- //atproto. two-colour wordmark --------------------------------------------------------- // slash 1 + dot = a1 (primary), slash 2 = a2 (secondary), letters = ink. function wordmarkSvg(cc, a1, a2, ink, { w = 1440, h = 260, x = 30, y = 196, fs = 200, ls = -6 } = {}) { return ( `` + `` + `//atproto.${cc}\n` ); } async function png(svg, size, out) { const opts = Array.isArray(size) ? { width: size[0], height: size[1] } : { width: size, height: size }; await sharp(Buffer.from(svg), { density: 300 }).resize(opts).png({ compressionLevel: 9 }).toFile(out); } for (const site of SITES) { const { cc, p, s } = site; const dir = resolve(outDir, cc); await mkdir(dir, { recursive: true }); // favicon + avatar (two-colour: slashes/split bar carry the pair). const fav = faviconSvg(cc, hue(p, 'dark'), hue(s, 'dark')); const av = avatarSvg(cc, hue(p, 'dark'), hue(s, 'dark')); await writeFile(resolve(dir, `favicon-${cc}.svg`), fav); await writeFile(resolve(dir, `avatar-${cc}.svg`), av); await png(fav, 512, resolve(dir, `favicon-${cc}.png`)); await png(av, 1000, resolve(dir, `avatar-${cc}.png`)); // two-colour wordmark logo: light-bg (dark letters) + dark-bg (cream letters) variants. const logoLight = wordmarkSvg(cc, hue(p, 'light'), hue(s, 'light'), INK_LIGHT); const logoDark = wordmarkSvg(cc, hue(p, 'dark'), hue(s, 'dark'), INK_DARK); await writeFile(resolve(dir, `logo-${cc}.svg`), logoLight); await writeFile(resolve(dir, `logo-${cc}-dark.svg`), logoDark); await png(logoLight, [1440, 260], resolve(dir, `logo-${cc}.png`)); await png(logoDark, [1440, 260], resolve(dir, `logo-${cc}-dark.png`)); const tag = site.external ? ' (external)' : site.prospective ? ' (proposed)' : ''; console.log(`brandkit/${cc}: avatar, favicon, two-colour logo (+dark) — SVG + PNG${tag}`); } const readme = `# atproto. brand kit\n\n` + `Generated from the code repo's single source of truth (\`src/config/accents.ts\` + \`scopes.ts\`)\n` + `by \`scripts/gen-brandkit.mjs\`. Do not hand-edit — rerun the generator after a colour change.\n\n` + `One folder per site. Each contains, as **SVG + PNG**:\n\n` + `- \`avatar-\` — square profile avatar (dark tile, \`at://\` + cc; the two slashes carry the pair,\n` + ` slash 1 primary / slash 2 secondary, cursor primary). PNG 1000×1000.\n` + `- \`favicon-\` — 64px browser mark (cc + a two-tone bar: top primary / bottom secondary). PNG 512×512.\n` + `- \`logo-\` — the **two-colour** \`//atproto.\` wordmark (flag-derived: slash 1 primary, slash 2\n` + ` secondary), transparent. Light-bg variant (dark letters); \`-dark\` variant (cream letters). PNG 1440×260.\n` + `- \`banner-.png\` — Bluesky profile banner (light, tagline headline + faint map, avatar-safe).\n` + ` 3000×1000. Generated separately by \`scripts/gen-banners.mjs\` (it composites the character-map).\n\n` + `Sites: ${SITES.map((s) => s.cc + (s.external ? ' (friendly, external)' : s.prospective ? ' (proposed)' : '')).join(', ')}.\n`; await writeFile(resolve(outDir, 'README.md'), readme); console.log(`\nwrote kit to ${outDir}`);