feat(marke): Logo, Favicon und PWA-Icons per Skript (#002)
Die Bildmarke liegt als SVG in drei Varianten (dunkel, hell, mono) vor. Rasterdateien erzeugt scripts/icons.mjs mit sharp und sie werden eingecheckt, damit Build und Laufzeitabbild kein sharp brauchen. Unter 32 px dient eine reduzierte Marke ohne Zahlenring und Bolzen, weil die Details dort zu Rauschen verschwimmen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="7" fill="none" stroke="#E8ECEF" stroke-width="3"/>
|
||||||
|
<circle cx="16" cy="13.5" r="4.6" fill="#F0B43C"/>
|
||||||
|
<path d="M13.8 15.5 L12 23.5 H20 L18.2 15.5 Z" fill="#F0B43C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="SSHKeyVault">
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="14" fill="none" stroke="#14181C" stroke-width="3.5"/>
|
||||||
|
<circle cx="32" cy="32" r="19" fill="none" stroke="#14181C" stroke-opacity="0.25" stroke-width="2" stroke-dasharray="3 3.2"/>
|
||||||
|
<circle cx="13.5" cy="13.5" r="2.2" fill="#14181C"/>
|
||||||
|
<circle cx="50.5" cy="13.5" r="2.2" fill="#14181C"/>
|
||||||
|
<circle cx="13.5" cy="50.5" r="2.2" fill="#14181C"/>
|
||||||
|
<circle cx="50.5" cy="50.5" r="2.2" fill="#14181C"/>
|
||||||
|
<circle cx="32" cy="27" r="7.5" fill="#B9780F"/>
|
||||||
|
<path d="M28.6 30.5 L25.5 45 H38.5 L35.4 30.5 Z" fill="#B9780F"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 672 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="SSHKeyVault">
|
||||||
|
<rect x="2.25" y="2.25" width="59.5" height="59.5" rx="15.75" fill="#F0B43C"/>
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="14" fill="none" stroke="#0D1013" stroke-width="3.5"/>
|
||||||
|
<circle cx="32" cy="32" r="19" fill="none" stroke="#0D1013" stroke-opacity="0.28" stroke-width="2" stroke-dasharray="3 3.2"/>
|
||||||
|
<circle cx="13.5" cy="13.5" r="2.2" fill="#0D1013"/>
|
||||||
|
<circle cx="50.5" cy="13.5" r="2.2" fill="#0D1013"/>
|
||||||
|
<circle cx="13.5" cy="50.5" r="2.2" fill="#0D1013"/>
|
||||||
|
<circle cx="50.5" cy="50.5" r="2.2" fill="#0D1013"/>
|
||||||
|
<circle cx="32" cy="27" r="7.5" fill="#0D1013"/>
|
||||||
|
<path d="M28.6 30.5 L25.5 45 H38.5 L35.4 30.5 Z" fill="#0D1013"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 753 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="SSHKeyVault">
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="14" fill="none" stroke="#E8ECEF" stroke-width="3.5"/>
|
||||||
|
<circle cx="32" cy="32" r="19" fill="none" stroke="#E8ECEF" stroke-opacity="0.28" stroke-width="2" stroke-dasharray="3 3.2"/>
|
||||||
|
<circle cx="13.5" cy="13.5" r="2.2" fill="#E8ECEF"/>
|
||||||
|
<circle cx="50.5" cy="13.5" r="2.2" fill="#E8ECEF"/>
|
||||||
|
<circle cx="13.5" cy="50.5" r="2.2" fill="#E8ECEF"/>
|
||||||
|
<circle cx="50.5" cy="50.5" r="2.2" fill="#E8ECEF"/>
|
||||||
|
<circle cx="32" cy="27" r="7.5" fill="#F0B43C"/>
|
||||||
|
<path d="M28.6 30.5 L25.5 45 H38.5 L35.4 30.5 Z" fill="#F0B43C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 672 B |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -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 <link>-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();
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
<rect x="2" y="2" width="28" height="28" rx="7" fill="none" stroke="#E8ECEF" stroke-width="3"/>
|
||||||
|
<circle cx="16" cy="13.5" r="4.6" fill="#F0B43C"/>
|
||||||
|
<path d="M13.8 15.5 L12 23.5 H20 L18.2 15.5 Z" fill="#F0B43C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 285 B |
@@ -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" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||