Files
craftvia/scripts/generate-brand-icons.ts
msolarczekandClaude Opus 5 9ec7fa5356 Fundament: Branding Craftvia
- Farb-Tokens laut Brandbook §11.4 (Lotsenblau, Signalorange, Graphit, Hafengrau,
  Stahlgrau, Zink, Funktionsfarben) in globals.css + src/lib/brand.ts; helles Theme
- Inline-SVG-Logo src/components/brand/craftvia-logo.tsx (horizontal/signet,
  color/mono/reversed, App-Icon-Kachel, optionale Tagline); Certvia-/GEFIM-Logos entfernt
- Favicon/PWA-Icons aus dem Signet erzeugt (scripts/generate-brand-icons.ts),
  site.webmanifest (Craftvia, theme_color #082E5B)
- Inter selbst gehostet (next/font/local, lokale OFL-Datei), Open Sans/Poppins entfernt
- Metadaten, WebAuthn-RP-Name, Mail-/Dokument-CD auf Craftvia umgestellt
- docs/craftvia/BRANDING.md ersetzt docs/BRANDING-CERTVIA.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:39:52 +02:00

94 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
? `<rect x="0" y="0" width="64" height="64" rx="${opts.radius ?? 14}" fill="${BRAND_COLORS.lotsenblau}"/>`
: "";
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
<path d="${SIGNET.check}" fill="none" stroke="#000" stroke-width="${SIGNET.checkWidth + 6}" stroke-linejoin="miter"/>
</mask>
</defs>
${bg}
<g transform="translate(${pad} ${pad}) scale(${scale})">
<path d="${SIGNET.arc}" fill="none" stroke="${arc}" stroke-width="${SIGNET.arcWidth}" stroke-linecap="butt" mask="url(#cut)"/>
<path d="${SIGNET.check}" fill="none" stroke="${check}" stroke-width="${SIGNET.checkWidth}" stroke-linejoin="miter"/>
<path d="${SIGNET.arrow}" fill="${check}"/>
</g>
</svg>
`;
}
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);
});