feat: proper logo, favicon, and pwa icons
- replace placeholder house svg with clean geometric house mark on indigo-700 (#4338CA) — roof triangle, body, door cutout - add favicon.svg with prefers-color-scheme dark variant - add icon-16/32 sizes; regenerate all pngs from svg via sharp - update BrandMark component to use inline svg house instead of 'f' - wire favicon + sized pngs into layout metadata - fix middleware to allow icon/manifest/favicon paths without auth (android launcher fetches icons in a cookieless system context) - fix getCurrentSession() to redirect to /login instead of throwing when session cookie is stale/invalid
This commit is contained in:
+27
-79
@@ -1,100 +1,48 @@
|
||||
/**
|
||||
* Generates placeholder PNG icons for the famapp PWA.
|
||||
* Rasterizes public/icon.svg into all required PNG icon sizes.
|
||||
*
|
||||
* Usage: node scripts/generate-icons.mjs
|
||||
*
|
||||
* Produces:
|
||||
* public/icon-16.png
|
||||
* public/icon-32.png
|
||||
* public/icon-180.png (apple-touch-icon)
|
||||
* public/icon-192.png
|
||||
* public/icon-384.png
|
||||
* public/icon-512.png
|
||||
* public/icon-512-maskable.png (same image; OS applies its own mask)
|
||||
* public/icon-180.png (apple-touch-icon)
|
||||
*
|
||||
* All images are solid indigo (#4F46E5) squares — replace with a real
|
||||
* branded export from icon.svg when assets are finalised.
|
||||
* public/icon-512-maskable.png (square bg, content in safe zone)
|
||||
*/
|
||||
|
||||
import { deflateSync } from "zlib";
|
||||
import { writeFileSync } from "fs";
|
||||
import { resolve, dirname } from "path";
|
||||
import sharp from "sharp";
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||||
const publicDir = resolve(__dir, "../public");
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const pub = join(__dirname, "..", "public");
|
||||
|
||||
// Build CRC-32 lookup table once.
|
||||
const CRC_TABLE = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let c = i;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
CRC_TABLE[i] = c;
|
||||
}
|
||||
const svg = readFileSync(join(pub, "icon.svg"));
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i++) crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function u32(n) {
|
||||
const b = Buffer.alloc(4);
|
||||
b.writeUInt32BE(n, 0);
|
||||
return b;
|
||||
}
|
||||
|
||||
function pngChunk(type, data) {
|
||||
const typeBytes = Buffer.from(type, "ascii");
|
||||
const crc = u32(crc32(Buffer.concat([typeBytes, data])));
|
||||
return Buffer.concat([u32(data.length), typeBytes, data, crc]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Buffer containing a valid PNG of size×size filled with (r, g, b).
|
||||
*/
|
||||
function solidPNG(size, r, g, b) {
|
||||
const PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
|
||||
const ihdr = pngChunk(
|
||||
"IHDR",
|
||||
Buffer.concat([
|
||||
u32(size),
|
||||
u32(size),
|
||||
Buffer.from([8, 2, 0, 0, 0]), // 8-bit RGB, no interlace
|
||||
]),
|
||||
);
|
||||
|
||||
// One filter byte (0 = None) followed by size×3 RGB bytes per row.
|
||||
const rowLen = 1 + size * 3;
|
||||
const raw = Buffer.alloc(size * rowLen);
|
||||
for (let y = 0; y < size; y++) {
|
||||
const base = y * rowLen;
|
||||
raw[base] = 0;
|
||||
for (let x = 0; x < size; x++) {
|
||||
raw[base + 1 + x * 3] = r;
|
||||
raw[base + 2 + x * 3] = g;
|
||||
raw[base + 3 + x * 3] = b;
|
||||
}
|
||||
}
|
||||
|
||||
const idat = pngChunk("IDAT", deflateSync(raw));
|
||||
const iend = pngChunk("IEND", Buffer.alloc(0));
|
||||
|
||||
return Buffer.concat([PNG_SIG, ihdr, idat, iend]);
|
||||
}
|
||||
|
||||
// Indigo-600 (#4F46E5)
|
||||
const [R, G, B] = [0x4f, 0x46, 0xe5];
|
||||
|
||||
const icons = [
|
||||
const sizes = [
|
||||
{ name: "icon-16.png", size: 16 },
|
||||
{ name: "icon-32.png", size: 32 },
|
||||
{ name: "icon-180.png", size: 180 },
|
||||
{ name: "icon-192.png", size: 192 },
|
||||
{ name: "icon-384.png", size: 384 },
|
||||
{ name: "icon-512.png", size: 512 },
|
||||
{ name: "icon-512-maskable.png", size: 512 },
|
||||
{ name: "icon-180.png", size: 180 },
|
||||
];
|
||||
|
||||
for (const { name, size } of icons) {
|
||||
const out = resolve(publicDir, name);
|
||||
writeFileSync(out, solidPNG(size, R, G, B));
|
||||
for (const { name, size } of sizes) {
|
||||
await sharp(svg).resize(size, size).png().toFile(join(pub, name));
|
||||
console.log(` ✓ public/${name} (${size}×${size})`);
|
||||
}
|
||||
|
||||
// Maskable: remove rounded corners so the background fills the full canvas,
|
||||
// allowing the OS to apply any mask shape without clipping the icon corners.
|
||||
const maskableSvg = readFileSync(join(pub, "icon.svg"), "utf-8").replace('rx="96"', "");
|
||||
|
||||
await sharp(Buffer.from(maskableSvg))
|
||||
.resize(512, 512)
|
||||
.png()
|
||||
.toFile(join(pub, "icon-512-maskable.png"));
|
||||
console.log(` ✓ public/icon-512-maskable.png (512×512 maskable)`);
|
||||
|
||||
Reference in New Issue
Block a user