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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d5a8bf9d95
commit
8aab25e0ac
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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})`);
|
||||
}
|
||||
Reference in New Issue
Block a user