/**
* Erzeugt Favicon/PWA-Icons aus dem Craftvia-Signet (src/components/brand/craftvia-logo.tsx,
* Geometrie `SIGNET`). Keine Netzwerkzugriffe; PNGs via `sharp` (liegt über Next.js bei).
*
* Lauf: npx tsx scripts/generate-brand-icons.ts
* Ausgabe: public/favicon/*.svg|png, public/favicon.ico (PNG-in-ICO, 32×32)
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
import { BRAND_COLORS } from "../src/lib/brand";
import { SIGNET } from "../src/components/brand/craftvia-logo";
const OUT = join(process.cwd(), "public", "favicon");
mkdirSync(OUT, { recursive: true });
function signetSvg(opts: { tile: boolean; padding?: number; radius?: number }): string {
const arc = opts.tile ? BRAND_COLORS.white : BRAND_COLORS.lotsenblau;
const check = BRAND_COLORS.signalorange;
const pad = opts.padding ?? 9.6;
const scale = (64 - 2 * pad) / 64;
const bg = opts.tile
? ``
: "";
return `
`;
}
async function png(svg: string, size: number, file: string) {
await sharp(Buffer.from(svg), { density: 72 * (size / 64) * 2 })
.resize(size, size)
.png()
.toFile(join(OUT, file));
}
/** Minimaler ICO-Container mit einem eingebetteten PNG (von allen Browsern unterstützt). */
function icoFromPng(pngBuf: Buffer, size: number): Buffer {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(1, 4);
const entry = Buffer.alloc(16);
entry.writeUInt8(size >= 256 ? 0 : size, 0);
entry.writeUInt8(size >= 256 ? 0 : size, 1);
entry.writeUInt8(0, 2);
entry.writeUInt8(0, 3);
entry.writeUInt16LE(1, 4);
entry.writeUInt16LE(32, 6);
entry.writeUInt32LE(pngBuf.length, 8);
entry.writeUInt32LE(6 + 16, 12);
return Buffer.concat([header, entry, pngBuf]);
}
async function main() {
// Favicon: freistehendes Signet (transparent) als SVG — skaliert verlustfrei.
const plain = signetSvg({ tile: false, padding: 2 });
writeFileSync(join(OUT, "craftvia-signet.svg"), plain);
// App-Icon-Kachel (Lotsenblau) für PWA/Apple/OG.
const tile = signetSvg({ tile: true });
writeFileSync(join(OUT, "craftvia-app-icon.svg"), tile);
// Maskable: volle Fläche ohne Rundung, Signet in der Safe-Zone (80 %).
const maskable = signetSvg({ tile: true, padding: 14, radius: 0 });
await png(plain, 16, "icon-16.png");
await png(plain, 32, "icon-32.png");
await png(plain, 48, "icon-48.png");
await png(tile, 180, "apple-touch-icon-180.png");
await png(tile, 192, "pwa-192.png");
await png(tile, 512, "pwa-512.png");
await png(maskable, 512, "maskable-512.png");
const ico32 = await sharp(Buffer.from(plain), { density: 144 }).resize(32, 32).png().toBuffer();
writeFileSync(join(process.cwd(), "public", "favicon.ico"), icoFromPng(ico32, 32));
console.log("✓ Craftvia-Icons erzeugt in public/favicon (+ public/favicon.ico)");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});