The web app is now installable on a phone home screen with offline fallback for static assets and the navigation shell. Pieces ------ - `src/app/manifest.webmanifest/route.ts` — dynamic manifest route. Standalone display mode, portrait orientation, dark theme matching the app, "any maskable" icons so the same PNG works for both regular launchers and Android adaptive icons. - `src/pwa/sw.ts` — service worker entry. Uses serwist's stock recipe: skipWaiting + clientsClaim so a new worker takes over on the next navigation, navigationPreload to race the network with the worker boot, and `defaultCache` for HTML-network-first / static-cache-first / image+font cache TTLs. - `next.config.ts` — wraps the existing config with `withSerwistInit`. Disabled in development (`NODE_ENV !== "production"`) because a service worker on every dev reload makes hot-reload extremely flaky. - `package.json` build script switched to `next build --webpack`. `@serwist/next` doesn't yet support Turbopack (it logs a warning and silently skips emitting `sw.js`), and Next 16 defaults the build to Turbopack. The dev server still uses Turbopack — only production builds switch to webpack. - `src/app/layout.tsx` metadata gains `manifest`, `icons.icon` (192 + 512 PNG), and `icons.apple` (180 PNG). The existing `appleWebApp.capable` already opts iOS into standalone mode. Icons ----- Generated by a tiny one-shot script (`scripts/gen-pwa-icons.ts`) that uses the workspace's already-installed sharp to render an SVG wordmark at 512 / 192 / 180 px. Placeholder branding (dark square with "cm" wordmark) — swap in real artwork later by editing the SVG in the script and re-running `pnpm --filter @cmbot/web run gen:icons`. Build artefacts --------------- - `apps/web/public/icon-512.png`, `icon-192.png`, `apple-touch-icon.png` ARE committed (stable input). - `apps/web/public/sw.js` and `swe-worker-*.js` are NOT — they're regenerated on every production build. Added to `.gitignore`. Verification ------------ - Production build emits `[serwist] Bundling the service worker script with the URL '/sw.js' and the scope '/'...` and `sw.js` shows up in `public/`. - `/manifest.webmanifest` is in the build's static-route table. - 249 web tests still passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
#!/usr/bin/env tsx
|
||
/**
|
||
* Generate placeholder PWA icons (icon-512.png, icon-192.png,
|
||
* apple-touch-icon.png) into apps/web/public/.
|
||
*
|
||
* Run once via `pnpm --filter @cmbot/web run gen:icons`. The output is
|
||
* intentionally minimal — a dark square with the "cm" wordmark in
|
||
* a light bold sans-serif — until a designer hands us a real icon.
|
||
*
|
||
* Sharp is already in the workspace's node_modules (Baileys depends
|
||
* on it), so we re-use it here rather than introducing a new image
|
||
* library. Output is written as PNG with no alpha channel; the
|
||
* "any maskable" purpose in the manifest covers both regular launch
|
||
* icons and Android adaptive icons (which crop to their own shape).
|
||
*/
|
||
|
||
import { writeFile } from "node:fs/promises";
|
||
import { join } from "node:path";
|
||
import sharp from "sharp";
|
||
|
||
const OUT_DIR = join(import.meta.dirname, "..", "public");
|
||
|
||
const BG = "#0a0a0a"; // matches the manifest theme_color
|
||
const FG = "#ffffff"; // wordmark
|
||
const TEXT = "cm";
|
||
|
||
/** Render the icon at the given pixel size. */
|
||
async function renderIcon(size: number): Promise<Buffer> {
|
||
// Font size is tuned to fill ~half the icon's height with comfortable
|
||
// padding around the wordmark — same proportions Apple/Google use
|
||
// for their own home-screen icons.
|
||
const fontSize = Math.round(size * 0.46);
|
||
const svg = `
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
||
<rect width="${size}" height="${size}" fill="${BG}" />
|
||
<text
|
||
x="50%"
|
||
y="50%"
|
||
text-anchor="middle"
|
||
dominant-baseline="central"
|
||
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif"
|
||
font-weight="700"
|
||
font-size="${fontSize}"
|
||
fill="${FG}"
|
||
letter-spacing="-${Math.round(fontSize * 0.04)}"
|
||
>${TEXT}</text>
|
||
</svg>
|
||
`.trim();
|
||
return sharp(Buffer.from(svg)).png().toBuffer();
|
||
}
|
||
|
||
async function main(): Promise<void> {
|
||
const targets: Array<{ size: number; filename: string }> = [
|
||
{ size: 512, filename: "icon-512.png" },
|
||
{ size: 192, filename: "icon-192.png" },
|
||
{ size: 180, filename: "apple-touch-icon.png" },
|
||
];
|
||
for (const { size, filename } of targets) {
|
||
const png = await renderIcon(size);
|
||
const out = join(OUT_DIR, filename);
|
||
await writeFile(out, png);
|
||
console.log(` ${filename} (${size}×${size}, ${png.byteLength} bytes)`);
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
});
|