- 매니페스트·아이콘(192/512/maskable/apple-touch, scripts/pwa-icons.ts로 다시 만들기) - 손으로 쓴 서비스 워커(apps/web/sw.js): 빌드 때 Vite 플러그인이 빌드 ID를 넣음. 페이지는 네트워크만(오프라인이면 offline.html), /assets/*만 캐시 먼저(200개까지), /api·/ws·/healthz·/internal은 손대지 않음 - 오프라인 안내 페이지, 방 밖에서도 "인터넷 연결이 없어요" 띠 - 홈 아래 "홈 화면에 추가" 카드(설치 버튼, iOS는 공유 메뉴 안내, 닫으면 기억) - 테스트: apps/web/src/sw.test.ts, e2e/pwa.e2e.ts(E2E_PORT), a11y.e2e.ts도 E2E_PORT 읽기 - 문서: 07 §11, 11 배포(no-cache 대상), README 확인 목록 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
35 lines
1.9 KiB
TypeScript
35 lines
1.9 KiB
TypeScript
/**
|
|
* Renders the PWA icons (docs/07 §11) from the favicon design with headless Chrome.
|
|
* bun scripts/pwa-icons.ts → apps/web/public/icons/*.png
|
|
* "any" icons keep the favicon's rounded corners; maskable/apple icons are full-bleed
|
|
* (the OS applies its own mask) with the board inside the 40% safe circle.
|
|
*/
|
|
import { chromium } from 'playwright-core';
|
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const PUBLIC = join(import.meta.dir, '../apps/web/public');
|
|
const OUT = join(PUBLIC, 'icons');
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
const rounded = readFileSync(join(PUBLIC, 'favicon.svg'), 'utf8');
|
|
// Same board as the favicon, shrunk to 80% so every stroke stays inside the safe circle.
|
|
const fullBleed = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="#E3B76B"/><g transform="translate(32 32) scale(0.8) translate(-32 -32)"><path d="M12 22h40M12 32h40M12 42h40M22 12v40M32 12v40M42 12v40" stroke="#7a5a22" stroke-width="2"/><circle cx="32" cy="32" r="8" fill="#1c1e21"/><circle cx="42" cy="22" r="8" fill="#fff" stroke="#1c1e21" stroke-width="2"/></g></svg>`;
|
|
|
|
const ICONS: [file: string, size: number, svg: string][] = [
|
|
['icon-192.png', 192, rounded],
|
|
['icon-512.png', 512, rounded],
|
|
['icon-maskable-512.png', 512, fullBleed],
|
|
['apple-touch-icon.png', 180, fullBleed],
|
|
];
|
|
|
|
const browser = await chromium.launch({ executablePath: process.env.CHROME ?? '/usr/bin/google-chrome', headless: true });
|
|
const page = await browser.newPage();
|
|
for (const [file, size, svg] of ICONS) {
|
|
await page.setViewportSize({ width: size, height: size });
|
|
await page.setContent(`<style>html,body{margin:0;background:transparent}svg{display:block;width:${size}px;height:${size}px}</style>${svg}`);
|
|
await page.screenshot({ path: join(OUT, file), omitBackground: true });
|
|
console.log(`✓ ${file}`);
|
|
}
|
|
await browser.close();
|