Files
famapp/scripts/generate-icons.mjs
T
ginnoirandClaude Sonnet 4.6 8aab25e0ac Implement PWA shell (task 50)
Add web manifest, placeholder icons, app-shell service worker, and
install prompt so famapp is installable on iOS and Android home screens.

- public/manifest.webmanifest — name, short_name, standalone display,
  start_url /, all four icon sizes (192/384/512 + maskable)
- public/icon-{180,192,384,512,512-maskable}.png — indigo placeholder
  squares generated by scripts/generate-icons.mjs (pnpm gen:icons)
- public/icon.svg — house-icon SVG source for future branded export
- public/sw.js — hand-rolled app-shell SW: precaches offline.html,
  cache-first for /_next/static/, network-first navigation with offline
  fallback, network-only for /api/*
- public/offline.html — branded offline fallback page
- src/components/pwa-register.tsx — client SW registration
- src/components/install-prompt.tsx — dismissible install banner:
  beforeinstallprompt on Android, "Add to Home Screen" hint on iOS
- Root layout: viewport themeColor, manifest/appleWebApp metadata,
  apple-touch-icon, mounts PwaRegister + InstallPrompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:00:02 -05:00

102 lines
2.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Generates placeholder PNG icons for the famapp PWA.
*
* Usage: node scripts/generate-icons.mjs
*
* Produces:
* 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.
*/
import { deflateSync } from "zlib";
import { writeFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
const __dir = dirname(fileURLToPath(import.meta.url));
const publicDir = resolve(__dir, "../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;
}
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 = [
{ 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));
console.log(` ✓ public/${name} (${size}×${size})`);
}