- 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>
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|