Files
florian.wildeandClaude Opus 5.5 7f07efe05d feat(marke): Logo, Banner, Favicons und Manifest-Icons
Alle Varianten aus dem Symbol der Vorgabe per Skript (pnpm marke); Texte als Pfade, damit Wortmarke und Banner ohne installierte Schrift gleich aussehen.

Closes #017

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:15:29 +02:00

69 lines
2.7 KiB
TypeScript

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { Resvg } from "@resvg/resvg-js";
import { describe, expect, it } from "vitest";
import manifest from "@/app/manifest";
import { metadata } from "@/app/layout";
const ORDNER = "public/brand";
/** Breite und Höhe aus dem IHDR-Block einer PNG-Datei. */
function pngGroesse(datei: string): [number, number] {
const daten = readFileSync(join(ORDNER, datei));
expect(daten.subarray(1, 4).toString()).toBe("PNG");
return [daten.readUInt32BE(16), daten.readUInt32BE(20)];
}
describe("Marken-Dateien (pnpm marke)", () => {
it("alle SVGs der Vorgabe sind da, renderbar und ohne kaputte Koordinaten", () => {
const svgs = readdirSync(ORDNER).filter((d) => d.endsWith(".svg"));
expect(svgs.sort()).toEqual(
[
"avatar.svg",
"banner.svg",
"icon-small.svg",
"logo-light.svg",
"logo.svg",
"symbol-light.svg",
"symbol.svg",
].sort(),
);
for (const datei of svgs) {
const text = readFileSync(join(ORDNER, datei), "utf8");
expect(text, datei).not.toMatch(/NaN|Infinity/);
const bild = new Resvg(text).render();
expect(bild.width, datei).toBeGreaterThan(0);
}
const banner = new Resvg(readFileSync(join(ORDNER, "banner.svg"), "utf8")).render();
expect([banner.width, banner.height]).toEqual([1280, 640]);
});
it("kein Proxmox-Orange und nur Markenfarben", () => {
const erlaubt = new Set(["#11161C", "#1A2129", "#2A333D", "#8B98A5", "#E6EDF3", "#2DD4BF", "#0F766E"]);
for (const datei of readdirSync(ORDNER).filter((d) => d.endsWith(".svg"))) {
for (const [farbe] of readFileSync(join(ORDNER, datei), "utf8").matchAll(/#[0-9A-Fa-f]{6}\b/g)) {
expect(erlaubt.has(farbe.toUpperCase()), `${datei}: ${farbe}`).toBe(true);
}
}
});
it("Rasterbilder haben die richtige Größe; Layout und Manifest zeigen nur auf vorhandene Dateien", () => {
expect(pngGroesse("favicon-16.png")).toEqual([16, 16]);
expect(pngGroesse("favicon-32.png")).toEqual([32, 32]);
expect(pngGroesse("apple-touch-icon.png")).toEqual([180, 180]);
expect(pngGroesse("icon-192.png")).toEqual([192, 192]);
expect(pngGroesse("icon-512.png")).toEqual([512, 512]);
expect(pngGroesse("icon-maskable-512.png")).toEqual([512, 512]);
expect(pngGroesse("banner.png")).toEqual([1280, 640]);
const pfade = [
...(manifest().icons ?? []).map((i) => i.src),
...JSON.stringify(metadata.icons)
.matchAll(/"url":"([^"]+)"/g)
.map((t) => t[1]!),
];
expect(pfade.length).toBeGreaterThan(5);
for (const pfad of pfade) expect(existsSync(join("public", pfad)), pfad).toBe(true);
});
});