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>
102 lines
2.7 KiB
JavaScript
102 lines
2.7 KiB
JavaScript
/**
|
||
* 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})`);
|
||
}
|