homelabby/web/scripts/gen-icons.cjs
Mikkel Georgsen 95a50f4abd feat(03-05): PWA manifest, service worker, icons, and registration hook
- web/public/manifest.json with display=standalone, theme_color=#faff69, 192+512 icons
- web/public/sw.js with app-shell cache strategy (API calls network-only)
- web/public/icons/icon-192.png and icon-512.png generated via gen-icons.cjs
- web/scripts/gen-icons.cjs pure-Node.js PNG icon generator (black canvas, volt H monogram)
- web/src/hooks/usePWA.ts registers service worker on app load
- web/index.html: added theme-color meta tag
- web/src/App.tsx: calls usePWA() hook
2026-04-10 06:22:05 +00:00

91 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
// Generates minimal PNG icons for the PWA manifest.
// Uses pure Node.js — no canvas or image library needed.
// Output: valid PNG icons with an "H" monogram on black background with volt (#faff69) color.
// For production, replace with properly designed icons.
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
function makePNG(size, bgHex, fgHex) {
// PNG signature
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
// IHDR chunk
const ihdrData = Buffer.alloc(13)
ihdrData.writeUInt32BE(size, 0)
ihdrData.writeUInt32BE(size, 4)
ihdrData[8] = 8 // bit depth
ihdrData[9] = 2 // color type RGB
ihdrData[10] = 0 // compression
ihdrData[11] = 0 // filter
ihdrData[12] = 0 // interlace
const ihdr = makeChunk('IHDR', ihdrData)
// IDAT chunk — raw RGB pixel data, filtered (filter byte 0 = none per row)
const bg = hexToRgb(bgHex)
const fg = hexToRgb(fgHex)
const rawRows = []
const margin = Math.floor(size * 0.2)
for (let y = 0; y < size; y++) {
const row = [0] // filter byte
for (let x = 0; x < size; x++) {
// Simple "H" monogram in the center
const inMargin = x >= margin && x < size - margin && y >= margin && y < size - margin
const isStroke =
inMargin &&
((x < margin + Math.floor(size * 0.08) || x > size - margin - Math.floor(size * 0.08)) ||
(Math.abs(y - size / 2) < Math.floor(size * 0.06)))
const c = isStroke ? fg : bg
row.push(c[0], c[1], c[2])
}
rawRows.push(Buffer.from(row))
}
const rawData = Buffer.concat(rawRows)
const compressed = zlib.deflateSync(rawData)
const idat = makeChunk('IDAT', compressed)
// IEND
const iend = makeChunk('IEND', Buffer.alloc(0))
return Buffer.concat([sig, ihdr, idat, iend])
}
function makeChunk(type, data) {
const len = Buffer.alloc(4)
len.writeUInt32BE(data.length)
const typeBytes = Buffer.from(type)
const crc = crc32(Buffer.concat([typeBytes, data]))
const crcBuf = Buffer.alloc(4)
crcBuf.writeInt32BE(crc)
return Buffer.concat([len, typeBytes, data, crcBuf])
}
function hexToRgb(hex) {
const n = parseInt(hex.replace('#', ''), 16)
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]
}
function crc32(buf) {
let crc = 0xffffffff
const table = makeCRCTable()
for (const b of buf) crc = (crc >>> 8) ^ table[(crc ^ b) & 0xff]
return (crc ^ 0xffffffff) | 0
}
function makeCRCTable() {
const t = new Int32Array(256)
for (let i = 0; i < 256; i++) {
let c = i
for (let j = 0; j < 8; j++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
t[i] = c
}
return t
}
const iconsDir = path.join(__dirname, '..', 'public', 'icons')
fs.mkdirSync(iconsDir, { recursive: true })
fs.writeFileSync(path.join(iconsDir, 'icon-192.png'), makePNG(192, '#000000', '#faff69'))
fs.writeFileSync(path.join(iconsDir, 'icon-512.png'), makePNG(512, '#000000', '#faff69'))
console.log('Icons generated: icon-192.png, icon-512.png')