diff --git a/apps/main/public/pwa-icons/apple-touch-icon-180x180.png b/apps/main/public/pwa-icons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..78da98f Binary files /dev/null and b/apps/main/public/pwa-icons/apple-touch-icon-180x180.png differ diff --git a/apps/main/public/pwa-icons/favicon-16x16.png b/apps/main/public/pwa-icons/favicon-16x16.png new file mode 100644 index 0000000..2f3428d Binary files /dev/null and b/apps/main/public/pwa-icons/favicon-16x16.png differ diff --git a/apps/main/public/pwa-icons/favicon-32x32.png b/apps/main/public/pwa-icons/favicon-32x32.png new file mode 100644 index 0000000..f6d7041 Binary files /dev/null and b/apps/main/public/pwa-icons/favicon-32x32.png differ diff --git a/apps/main/public/pwa-icons/pwa-192x192.png b/apps/main/public/pwa-icons/pwa-192x192.png new file mode 100644 index 0000000..0a42ff5 Binary files /dev/null and b/apps/main/public/pwa-icons/pwa-192x192.png differ diff --git a/apps/main/public/pwa-icons/pwa-512x512-maskable.png b/apps/main/public/pwa-icons/pwa-512x512-maskable.png new file mode 100644 index 0000000..72b0e39 Binary files /dev/null and b/apps/main/public/pwa-icons/pwa-512x512-maskable.png differ diff --git a/apps/main/public/pwa-icons/pwa-512x512.png b/apps/main/public/pwa-icons/pwa-512x512.png new file mode 100644 index 0000000..b618acf Binary files /dev/null and b/apps/main/public/pwa-icons/pwa-512x512.png differ diff --git a/apps/main/scripts/generate-pwa-icons.ts b/apps/main/scripts/generate-pwa-icons.ts new file mode 100644 index 0000000..ee8a107 --- /dev/null +++ b/apps/main/scripts/generate-pwa-icons.ts @@ -0,0 +1,74 @@ +import sharp from "sharp"; +import path from "node:path"; +import fs from "node:fs"; + +const PUBLIC_DIR = path.resolve(import.meta.dirname, "../public"); +const OUTPUT_DIR = path.join(PUBLIC_DIR, "pwa-icons"); + +const ICON_SIZES = [ + { size: 16, name: "favicon-16x16.png" }, + { size: 32, name: "favicon-32x32.png" }, + { size: 180, name: "apple-touch-icon-180x180.png" }, + { size: 192, name: "pwa-192x192.png" }, + { size: 512, name: "pwa-512x512.png" }, + { size: 512, name: "pwa-512x512-maskable.png", maskable: true }, +]; + +// Maskable icons need 10% safe zone padding (per spec). +// We add a colored background and shrink the source to 80% to stay within the safe zone. +const MASKABLE_PADDING_RATIO = 0.1; +const MASKABLE_BG_COLOR = "#ffffff"; + +async function generateIcons(sourceFile: string) { + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + } + + for (const icon of ICON_SIZES) { + const outputPath = path.join(OUTPUT_DIR, icon.name); + + if (icon.maskable) { + // Create maskable icon with padding and background + const innerSize = Math.round(icon.size * (1 - MASKABLE_PADDING_RATIO * 2)); + const offset = Math.round(icon.size * MASKABLE_PADDING_RATIO); + + const resizedIcon = await sharp(sourceFile) + .resize(innerSize, innerSize, { fit: "contain" }) + .toBuffer(); + + await sharp({ + create: { + width: icon.size, + height: icon.size, + channels: 4, + background: MASKABLE_BG_COLOR, + }, + }) + .composite([{ input: resizedIcon, left: offset, top: offset }]) + .png() + .toFile(outputPath); + } else { + await sharp(sourceFile) + .resize(icon.size, icon.size, { fit: "contain" }) + .png() + .toFile(outputPath); + } + + console.log(`Generated: ${icon.name} (${icon.size}x${icon.size})`); + } +} + +// Determine source: pass "staging" as first arg for staging icons +const variant = process.argv[2]; +const sourceFile = + variant === "staging" + ? path.join(PUBLIC_DIR, "staging_icon.jpg") + : path.join(PUBLIC_DIR, "icon.jpg"); + +if (!fs.existsSync(sourceFile)) { + console.error(`Source icon not found: ${sourceFile}`); + process.exit(1); +} + +console.log(`Generating PWA icons from: ${sourceFile}`); +generateIcons(sourceFile).then(() => console.log("Done!"));