Skip to content

Commit 3df2b99

Browse files
committed
Update icons script, add apple-touch-icon
1 parent 5be84a3 commit 3df2b99

6 files changed

Lines changed: 87 additions & 3 deletions

File tree

index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
<meta name="theme-color" content="#0d0d1a" />
77
<meta name="mobile-web-app-capable" content="yes" />
88
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9+
<link rel="icon" href="/favicon.ico" sizes="32x32" />
910
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
10-
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
11+
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
1112
<link rel="preconnect" href="https://fonts.googleapis.com" />
1213
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1314
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />

public/favicon.ico

1.78 KB
Binary file not shown.

public/icons/apple-touch-icon.png

8.46 KB
Loading

public/icons/icon-maskable-512.png

25.8 KB
Loading

scripts/generate-icons.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,48 @@
22
// SVG used as the app wordmark. Run with: node scripts/generate-icons.js
33
import sharp from 'sharp'
44
import { join, dirname } from 'path'
5+
import { writeFileSync } from 'fs'
56
import { fileURLToPath } from 'url'
67

78
const __dirname = dirname(fileURLToPath(import.meta.url))
8-
const outDir = join(__dirname, '..', 'public', 'icons')
9+
const publicDir = join(__dirname, '..', 'public')
10+
const outDir = join(publicDir, 'icons')
11+
12+
// Build an ICO file from one or more PNG buffers.
13+
// ICO format: 6-byte header, 16-byte directory entry per image, then raw PNG data.
14+
function buildIco(pngBuffers) {
15+
const count = pngBuffers.length
16+
const headerSize = 6
17+
const dirEntrySize = 16
18+
const dataOffset = headerSize + dirEntrySize * count
19+
20+
const header = Buffer.alloc(headerSize)
21+
header.writeUInt16LE(0, 0) // reserved
22+
header.writeUInt16LE(1, 2) // type: 1 = ICO
23+
header.writeUInt16LE(count, 4) // image count
24+
25+
const dirEntries = []
26+
let offset = dataOffset
27+
for (const png of pngBuffers) {
28+
const entry = Buffer.alloc(dirEntrySize)
29+
// width/height: 0 means 256 in ICO spec; for <=255 use actual value
30+
const meta = sharp(png)
31+
// We already know the sizes we're passing in, but we encode from the PNG header
32+
// For simplicity, we set width/height to 0 (works for all sizes up to 256)
33+
entry.writeUInt8(0, 0) // width (0 = 256 or "read from data")
34+
entry.writeUInt8(0, 1) // height
35+
entry.writeUInt8(0, 2) // color palette
36+
entry.writeUInt8(0, 3) // reserved
37+
entry.writeUInt16LE(1, 4) // color planes
38+
entry.writeUInt16LE(32, 6) // bits per pixel
39+
entry.writeUInt32LE(png.length, 8) // data size
40+
entry.writeUInt32LE(offset, 12) // data offset
41+
dirEntries.push(entry)
42+
offset += png.length
43+
}
44+
45+
return Buffer.concat([header, ...dirEntries, ...pngBuffers])
46+
}
947

1048
// Uses the same circle layout as the HomeView wordmark-icon, but with
1149
// icon-specific explicit colours and stroke/opacity values for PNG output.
@@ -25,9 +63,53 @@ const svgTemplate = (size) => {
2563
</svg>`
2664
}
2765

66+
// Maskable icons need a safe zone (inner 80% circle). Add extra padding
67+
// by scaling the viewBox content down so the design sits within the safe area.
68+
const maskableSvgTemplate = (size) => {
69+
const bg = '#0d0d1a'
70+
const fg = '#00e676'
71+
const rx = Math.round(size * 0.22)
72+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 32 32" fill="none">
73+
<rect width="32" height="32" rx="${rx / (size / 32)}" fill="${bg}"/>
74+
<g transform="translate(16,16) scale(0.75) translate(-16,-16)">
75+
<circle cx="16" cy="16" r="14" stroke="${fg}" stroke-width="1.2" opacity="0.35"/>
76+
<circle cx="16" cy="16" r="9" stroke="${fg}" stroke-width="1.2" opacity="0.65"/>
77+
<circle cx="16" cy="16" r="4" stroke="${fg}" stroke-width="1.2"/>
78+
<circle cx="16" cy="16" r="1.5" fill="${fg}"/>
79+
</g>
80+
</svg>`
81+
}
82+
2883
for (const size of [192, 512]) {
2984
const svg = Buffer.from(svgTemplate(size))
3085
const dest = join(outDir, `icon-${size}.png`)
3186
await sharp(svg).resize(size, size).png().toFile(dest)
3287
console.log(`✓ icon-${size}.png`)
3388
}
89+
90+
// Apple touch icon (180×180)
91+
{
92+
const svg = Buffer.from(svgTemplate(180))
93+
const dest = join(outDir, `apple-touch-icon.png`)
94+
await sharp(svg).resize(180, 180).png().toFile(dest)
95+
console.log(`✓ apple-touch-icon.png`)
96+
}
97+
98+
// Maskable icon (512×512) — design scaled to 75% to fit within the safe zone
99+
{
100+
const svg = Buffer.from(maskableSvgTemplate(512))
101+
const dest = join(outDir, `icon-maskable-512.png`)
102+
await sharp(svg).resize(512, 512).png().toFile(dest)
103+
console.log(`✓ icon-maskable-512.png`)
104+
}
105+
106+
// Favicon (ICO with 32×32 and 16×16)
107+
{
108+
const sizes = [32, 16]
109+
const pngBuffers = await Promise.all(
110+
sizes.map(s => sharp(Buffer.from(svgTemplate(s))).resize(s, s).png().toBuffer())
111+
)
112+
const ico = buildIco(pngBuffers)
113+
writeFileSync(join(publicDir, 'favicon.ico'), ico)
114+
console.log(`✓ favicon.ico`)
115+
}

vite.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ export default defineConfig(({ command, mode }) => {
3939
icons: [
4040
{ src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' },
4141
{ src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' },
42+
{ src: 'icons/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
4243
],
4344
},
4445
workbox: {
45-
globPatterns: ['**/*.{js,css,html,png,mp3,svg}'],
46+
globPatterns: ['**/*.{js,css,html,png,svg}'],
4647
},
4748
}),
4849
],

0 commit comments

Comments
 (0)