diff --git a/public/brand/favicon.svg b/public/brand/favicon.svg new file mode 100644 index 0000000..b5f29a9 --- /dev/null +++ b/public/brand/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/brand/logo-light.svg b/public/brand/logo-light.svg new file mode 100644 index 0000000..ebc67d3 --- /dev/null +++ b/public/brand/logo-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/brand/logo-mono.svg b/public/brand/logo-mono.svg new file mode 100644 index 0000000..13a7213 --- /dev/null +++ b/public/brand/logo-mono.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/brand/logo.svg b/public/brand/logo.svg new file mode 100644 index 0000000..d5a2de1 --- /dev/null +++ b/public/brand/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..3a95d68 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/favicon-16.png b/public/icons/favicon-16.png new file mode 100644 index 0000000..194b301 Binary files /dev/null and b/public/icons/favicon-16.png differ diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png new file mode 100644 index 0000000..0b612cf Binary files /dev/null and b/public/icons/favicon-32.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..195f7a7 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..480ddf2 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/scripts/icons.mjs b/scripts/icons.mjs new file mode 100644 index 0000000..1579def --- /dev/null +++ b/scripts/icons.mjs @@ -0,0 +1,99 @@ +// Erzeugt alle Raster-Icons aus den SVG-Vorlagen in public/brand/ (#002). +// +// Die PNGs und das favicon.ico werden eingecheckt, damit Build und Docker-Abbild +// kein sharp zur Laufzeit brauchen. Nach jeder Logo-Aenderung: `pnpm icons`. +import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); +const BRAND = join(WURZEL, "public/brand"); +const ICONS = join(WURZEL, "public/icons"); +const APP = join(WURZEL, "src/app"); + +// Seitenhintergrund (--bg); App-Icons stehen auf dieser Flaeche statt transparent, +// weil iOS und Android transparente Bereiche sonst schwarz oder weiss fuellen. +const HINTERGRUND = "#0D1013"; + +/** Rendert ein SVG scharf in der Zielgroesse (density skaliert die Vektoren statt Pixel). */ +async function rendern(svg, groesse) { + return sharp(svg, { density: Math.ceil((72 * groesse) / 32) * 2 }) + .resize(groesse, groesse) + .png() + .toBuffer(); +} + +/** Logo mittig auf --bg mit Rand, damit es auf Homescreens nicht an die Kante stoesst. */ +async function aufFlaeche(svg, groesse, anteil) { + const innen = Math.round(groesse * anteil); + const logo = await rendern(svg, innen); + return sharp({ + create: { width: groesse, height: groesse, channels: 4, background: HINTERGRUND }, + }) + .composite([{ input: logo, gravity: "center" }]) + .png() + .toBuffer(); +} + +/** Minimaler ICO-Container mit PNG-Eintraegen (seit Windows Vista von allen Browsern gelesen). */ +function icoBauen(bilder) { + const kopf = Buffer.alloc(6); + kopf.writeUInt16LE(0, 0); + kopf.writeUInt16LE(1, 2); + kopf.writeUInt16LE(bilder.length, 4); + const eintraege = []; + let versatz = 6 + 16 * bilder.length; + for (const { groesse, daten } of bilder) { + const e = Buffer.alloc(16); + e.writeUInt8(groesse >= 256 ? 0 : groesse, 0); + e.writeUInt8(groesse >= 256 ? 0 : groesse, 1); + e.writeUInt8(0, 2); + e.writeUInt8(0, 3); + e.writeUInt16LE(1, 4); + e.writeUInt16LE(32, 6); + e.writeUInt32LE(daten.length, 8); + e.writeUInt32LE(versatz, 12); + versatz += daten.length; + eintraege.push(e); + } + return Buffer.concat([kopf, ...eintraege, ...bilder.map((b) => b.daten)]); +} + +async function main() { + await mkdir(ICONS, { recursive: true }); + const logo = await readFile(join(BRAND, "logo.svg")); + // Unter 32 px wird die reduzierte Variante ohne Zahlenring und Bolzen genutzt, + // sonst verschwimmen die Details zu grauem Rauschen. + const favicon = await readFile(join(BRAND, "favicon.svg")); + + const icon192 = await aufFlaeche(logo, 192, 0.72); + const icon512 = await aufFlaeche(logo, 512, 0.72); + const apple = await aufFlaeche(logo, 180, 0.72); + const fav32 = await rendern(favicon, 32); + const fav16 = await rendern(favicon, 16); + const fav48 = await rendern(favicon, 48); + + await writeFile(join(ICONS, "icon-192.png"), icon192); + await writeFile(join(ICONS, "icon-512.png"), icon512); + await writeFile(join(ICONS, "apple-touch-icon.png"), apple); + await writeFile(join(ICONS, "favicon-32.png"), fav32); + await writeFile(join(ICONS, "favicon-16.png"), fav16); + + // Next-Metadaten-Dateien: favicon.ico, icon.svg und apple-icon.png in src/app + // erzeugen die -Tags automatisch. + await writeFile( + join(APP, "favicon.ico"), + icoBauen([ + { groesse: 16, daten: fav16 }, + { groesse: 32, daten: fav32 }, + { groesse: 48, daten: fav48 }, + ]), + ); + await copyFile(join(BRAND, "favicon.svg"), join(APP, "icon.svg")); + await writeFile(join(APP, "apple-icon.png"), apple); + + console.log("Icons erzeugt: public/icons/*, src/app/favicon.ico, src/app/icon.svg, src/app/apple-icon.png"); +} + +await main(); diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png new file mode 100644 index 0000000..3a95d68 Binary files /dev/null and b/src/app/apple-icon.png differ diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..ffeddae Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..b5f29a9 --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..7e6ee11 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,20 @@ +import type { MetadataRoute } from "next"; + +/** Web-App-Manifest; die Icons erzeugt scripts/icons.mjs (#002). */ +export default function manifest(): MetadataRoute.Manifest { + return { + name: "SSHKeyVault", + short_name: "sshkv", + description: "Zentrale Verwaltung von SSH-Schlüsseln", + lang: "de", + start_url: "/", + display: "standalone", + background_color: "#0D1013", + theme_color: "#0D1013", + icons: [ + { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" }, + { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }, + { src: "/brand/favicon.svg", sizes: "any", type: "image/svg+xml" }, + ], + }; +}