Something went wrong. Try again.
Central platform for European atproto.<cc> country community websites
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133// Generate every static country-colour asset from the single source of truth (src/config/accents.ts// for the packs, src/config/scopes.ts for which scope uses which pack). Run:// node scripts/gen-brand.mjs # or: npm run gen:tokens && npm run gen:svg// OG cards are separate (Playwright) — see scripts/og-card.mjs (npm run gen:og).//// Node imports the .ts sources natively (>=22.6 strip-types; CI/dev are on 26). No new deps.import { writeFile } from 'node:fs/promises';import { fileURLToPath } from 'node:url';import { dirname, resolve } from 'node:path';import { ACCENT_PACKS, cssValue } from '../src/config/accents.ts';import { SCOPES } from '../src/config/scopes.ts';
const here = dirname(fileURLToPath(import.meta.url));const root = resolve(here, '..');const scopes = Object.values(SCOPES);
// Shared dark tones the marks sit on (global.css --terminal / --inkstrong, dark theme).const TILE = '#1d2021';const MARK_INK = '#fbf1c7';
// --- src/styles/country-packs.css -------------------------------------------------------------// UI roles (data-ui): the four accent roles the site chrome reads — numbers, links, buttons, bars.const uiRolesDecl = (r) => `--accent: ${cssValue(r.accent)}; --accent-fill: ${cssValue(r.fill)}; ` + `--accent-text: ${cssValue(r.text)}; --on-accent: ${cssValue(r.onAccent)};`;// Wordmark slash 1 (data-accent): the country's TRUE primary, kept separate from the UI accent so a// red-flag country can lead its UI with the non-red half (data-ui) while the lockup keeps its flag// colour order. Only the raw accent hex — the UI roles live on data-ui.const wmDecl = (r) => `--wm-accent: ${r.accent};`;
const usedBy = (pack) => { const asP = scopes.filter((s) => s.accent === pack).map((s) => `.${s.cc}`); const asS = scopes.filter((s) => s.accent2 === pack).map((s) => `.${s.cc}`); const parts = []; if (asP.length) parts.push(`primary ${asP.join(' ')}`); if (asS.length) parts.push(`2nd slash ${asS.join(' ')}`); return parts.length ? parts.join('; ') : 'reserved — no scope yet';};
const packs = Object.entries(ACCENT_PACKS);// data-accent -> --wm-accent (wordmark slash 1 + the brand-primary swatch). data-ui -> the four UI// roles. Split so the UI accent can differ from the logo primary for red-flag countries — see// uiAccentPack() in scopes.ts, which sets data-ui to the non-red half.const cssBody = packs .map( ([name, t]) => `/* ${name} — ${usedBy(name)} */\n` + `[data-accent="${name}"] { ${wmDecl(t.light)} }\n` + `[data-accent="${name}"][data-theme="dark"], [data-theme="dark"] [data-accent="${name}"] { ${wmDecl(t.dark)} }\n` + `[data-ui="${name}"] { ${uiRolesDecl(t.light)} }\n` + `[data-ui="${name}"][data-theme="dark"], [data-theme="dark"] [data-ui="${name}"] { ${uiRolesDecl(t.dark)} }`, ) .join('\n\n');
const cssMedia = packs .map( ([name, t]) => ` [data-accent="${name}"]:not([data-theme="light"]) { ${wmDecl(t.dark)} }\n` + ` [data-ui="${name}"]:not([data-theme="light"]) { ${uiRolesDecl(t.dark)} }`, ) .join('\n');
// Secondary packs: just --accent-2 (the wordmark's second slash), keyed on data-accent2.const accent2Decl = (r) => `--accent-2: ${r.accent};`;const css2Body = packs .map( ([name, t]) => `[data-accent2="${name}"] { ${accent2Decl(t.light)} }\n` + `[data-accent2="${name}"][data-theme="dark"], [data-theme="dark"] [data-accent2="${name}"] { ${accent2Decl(t.dark)} }`, ) .join('\n');
const css2Media = packs .map(([name, t]) => ` [data-accent2="${name}"]:not([data-theme="light"]) { ${accent2Decl(t.dark)} }`) .join('\n');
const countryPacksCss = `/* GENERATED by scripts/gen-brand.mjs from src/config/accents.ts — do NOT edit by hand. atproto.<cc> per-country accent packs. Three attributes on <html> (from scopes.ts): data-accent -> --wm-accent (the country's TRUE primary: wordmark slash 1 + brand swatch) data-accent2 -> --accent-2 (wordmark slash 2) data-ui -> the four UI roles (--accent, --accent-fill, --accent-text, --on-accent) data-ui is the primary UNLESS it is red, then the non-red secondary (uiAccentPack in scopes.ts), so red-flag countries keep their lockup order but never lead the UI with an error-looking red. Yellow cannot reach 4.5:1 on cream at text size, so its light accent-text falls back to --inkstrong. */
${cssBody}
/* --- second-slash colours (data-accent2 -> --accent-2) --- */${css2Body}
/* System dark with no explicit pin. Without this, global.css's own \`@media(prefers-color-scheme:dark) :root:not([data-theme="light"])\` (specificity 0,2,0) sets the NL orange accent and beats the packs' light rule (0,1,0) — so non-orange scopes would show orange in OS dark mode. These mirror the [data-theme="dark"] values at matching specificity; being imported after global.css, they win. \`:not([data-theme="light"])\` yields to a manual light pin. */@media (prefers-color-scheme: dark) {${cssMedia}${css2Media}}`;
// --- public/map/accents.json (consumed at runtime by public/map/locality-map.js) --------------// Shape matches the renderer's old inline ACC table: pack -> [lightAccent, darkAccent].const mapAccents = Object.fromEntries( packs.map(([name, t]) => [name, [t.light.accent, t.dark.accent]]),);
// --- public/brand/favicon-<cc>.svg + avatar-<cc>.svg ------------------------------------------// The mark: a dark rounded tile with the cc in mono cream. It carries the two-colour identity like// the wordmark: the avatar's two slashes take slash 1 = primary (accent), slash 2 = secondary// (accent2), and its terminal cursor stays primary; the favicon has no slashes, so its bar splits// top = primary / bottom = secondary. Single-colour scopes (accent === accent2) read as one bar.const faviconSvg = (cc, accent, accent2) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12.8" fill="${TILE}"></rect><text x="10.7" y="42" font-size="28" font-family="'JetBrains Mono', ui-monospace, monospace" font-weight="700" letter-spacing="-0.04em" fill="${MARK_INK}">${cc}</text><rect x="45.4" y="22.8" width="7.8" height="10.1" fill="${accent}"></rect><rect x="45.4" y="32.9" width="7.8" height="10.1" fill="${accent2}"></rect></svg>\n`;
const avatarSvg = (cc, accent, accent2) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><rect width="1000" height="1000" rx="200" fill="${TILE}"></rect><text x="185" y="470" font-size="210" font-family="'JetBrains Mono', ui-monospace, monospace" font-weight="700" letter-spacing="-0.04em"><tspan fill="${MARK_INK}">at:</tspan><tspan fill="${accent}">/</tspan><tspan fill="${accent2}">/</tspan></text><text x="185" y="711.5" font-size="210" font-family="'JetBrains Mono', ui-monospace, monospace" font-weight="700" letter-spacing="-0.04em" fill="${MARK_INK}">${cc}</text><rect x="445" y="566" width="59" height="151" fill="${accent}"></rect></svg>\n`;
// --- write ------------------------------------------------------------------------------------const writes = [ ['src/styles/country-packs.css', countryPacksCss], ['public/map/accents.json', JSON.stringify(mapAccents, null, 2) + '\n'],];for (const s of scopes) { const accent = ACCENT_PACKS[s.accent].dark.accent; const accent2 = ACCENT_PACKS[s.accent2].dark.accent; writes.push([`public/brand/favicon-${s.cc}.svg`, faviconSvg(s.cc, accent, accent2)]); writes.push([`public/brand/avatar-${s.cc}.svg`, avatarSvg(s.cc, accent, accent2)]);}
await Promise.all(writes.map(([rel, body]) => writeFile(resolve(root, rel), body)));for (const [rel] of writes) console.log(`wrote ${rel}`);