diff --git a/apps/app/public/apple-touch-icon-blue.png b/apps/app/public/apple-touch-icon-blue.png index ed013ebdbc..adf20a0ae4 100644 Binary files a/apps/app/public/apple-touch-icon-blue.png and b/apps/app/public/apple-touch-icon-blue.png differ diff --git a/apps/app/public/apple-touch-icon-green.png b/apps/app/public/apple-touch-icon-green.png index a5dbb371c6..4a5718ac1a 100644 Binary files a/apps/app/public/apple-touch-icon-green.png and b/apps/app/public/apple-touch-icon-green.png differ diff --git a/apps/app/public/apple-touch-icon-orange.png b/apps/app/public/apple-touch-icon-orange.png index 28f53ad5e4..467cbe663e 100644 Binary files a/apps/app/public/apple-touch-icon-orange.png and b/apps/app/public/apple-touch-icon-orange.png differ diff --git a/apps/app/public/apple-touch-icon-pink.png b/apps/app/public/apple-touch-icon-pink.png index a82bfbc265..ff7c9a2060 100644 Binary files a/apps/app/public/apple-touch-icon-pink.png and b/apps/app/public/apple-touch-icon-pink.png differ diff --git a/apps/app/public/apple-touch-icon-purple.png b/apps/app/public/apple-touch-icon-purple.png index 217d82202b..8ba22b9d1a 100644 Binary files a/apps/app/public/apple-touch-icon-purple.png and b/apps/app/public/apple-touch-icon-purple.png differ diff --git a/apps/app/public/apple-touch-icon-red.png b/apps/app/public/apple-touch-icon-red.png index 7041145ece..5825ba58c0 100644 Binary files a/apps/app/public/apple-touch-icon-red.png and b/apps/app/public/apple-touch-icon-red.png differ diff --git a/apps/app/public/apple-touch-icon-teal.png b/apps/app/public/apple-touch-icon-teal.png index 071c5bd879..6de91caaa9 100644 Binary files a/apps/app/public/apple-touch-icon-teal.png and b/apps/app/public/apple-touch-icon-teal.png differ diff --git a/apps/app/public/apple-touch-icon-yellow.png b/apps/app/public/apple-touch-icon-yellow.png index b435f2afa4..ec7bbe937e 100644 Binary files a/apps/app/public/apple-touch-icon-yellow.png and b/apps/app/public/apple-touch-icon-yellow.png differ diff --git a/apps/app/public/apple-touch-icon.png b/apps/app/public/apple-touch-icon.png index 7b52df381e..096cf3159d 100644 Binary files a/apps/app/public/apple-touch-icon.png and b/apps/app/public/apple-touch-icon.png differ diff --git a/apps/app/public/icon-monochrome-192.png b/apps/app/public/icon-monochrome-192.png new file mode 100644 index 0000000000..7dff82341a Binary files /dev/null and b/apps/app/public/icon-monochrome-192.png differ diff --git a/apps/app/public/icon-monochrome-512.png b/apps/app/public/icon-monochrome-512.png new file mode 100644 index 0000000000..3f0e4999c8 Binary files /dev/null and b/apps/app/public/icon-monochrome-512.png differ diff --git a/apps/app/public/manifest-blue.webmanifest b/apps/app/public/manifest-blue.webmanifest index b4f3e0b868..f7ca7be73f 100644 --- a/apps/app/public/manifest-blue.webmanifest +++ b/apps/app/public/manifest-blue.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-green.webmanifest b/apps/app/public/manifest-green.webmanifest index 2e5150b562..42d04c1ba3 100644 --- a/apps/app/public/manifest-green.webmanifest +++ b/apps/app/public/manifest-green.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-orange.webmanifest b/apps/app/public/manifest-orange.webmanifest index c65949659d..bf95d29666 100644 --- a/apps/app/public/manifest-orange.webmanifest +++ b/apps/app/public/manifest-orange.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-pink.webmanifest b/apps/app/public/manifest-pink.webmanifest index dc2212025a..3fd5676d31 100644 --- a/apps/app/public/manifest-pink.webmanifest +++ b/apps/app/public/manifest-pink.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-purple.webmanifest b/apps/app/public/manifest-purple.webmanifest index ad2d4f6f07..a5dc555ae2 100644 --- a/apps/app/public/manifest-purple.webmanifest +++ b/apps/app/public/manifest-purple.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-red.webmanifest b/apps/app/public/manifest-red.webmanifest index ebac531d11..b34783090a 100644 --- a/apps/app/public/manifest-red.webmanifest +++ b/apps/app/public/manifest-red.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-teal.webmanifest b/apps/app/public/manifest-teal.webmanifest index 42efd02880..78fd1ccea6 100644 --- a/apps/app/public/manifest-teal.webmanifest +++ b/apps/app/public/manifest-teal.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest-yellow.webmanifest b/apps/app/public/manifest-yellow.webmanifest index 8e402b9993..e606e53b8c 100644 --- a/apps/app/public/manifest-yellow.webmanifest +++ b/apps/app/public/manifest-yellow.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/public/manifest.webmanifest b/apps/app/public/manifest.webmanifest index 5934af65bf..f6b7401549 100644 --- a/apps/app/public/manifest.webmanifest +++ b/apps/app/public/manifest.webmanifest @@ -29,6 +29,18 @@ "sizes": "512x512", "type": "image/png", "purpose": "maskable" + }, + { + "src": "/icon-monochrome-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "/icon-monochrome-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" } ] } diff --git a/apps/app/scripts/generate-pwa-icons.mjs b/apps/app/scripts/generate-pwa-icons.mjs index f4ca2aefeb..8ee8a3d192 100644 --- a/apps/app/scripts/generate-pwa-icons.mjs +++ b/apps/app/scripts/generate-pwa-icons.mjs @@ -20,11 +20,23 @@ const faviconColorValues = { }; const icons = [ - { file: "icon-192.png", mode: "tile" }, - { file: "icon-512.png", mode: "tile" }, - { file: "icon-192-maskable.png", mode: "tile" }, - { file: "icon-512-maskable.png", mode: "tile" }, - { file: "apple-touch-icon.png", mode: "glyph" }, + "icon-192.png", + "icon-512.png", + "icon-192-maskable.png", + "icon-512-maskable.png", + // Opaque white tile: iOS renders transparency in touch icons as black, + // and the system's dark/tinted home-screen treatments need a full-bleed + // opaque source. + "apple-touch-icon.png", +]; + +// Monochrome manifest icons (purpose: "monochrome") are alpha masks — the +// platform supplies the fill color when tinting (Android themed icons, +// notification badges) — so a single color-independent asset serves every +// manifest variant. +const monochromeIcons = [ + { source: "icon-192.png", target: "icon-monochrome-192.png" }, + { source: "icon-512.png", target: "icon-monochrome-512.png" }, ]; const mismatches = []; @@ -65,29 +77,12 @@ function tintTileIcon(data, colorRgb) { return output; } -function tintGlyphIcon(data, colorRgb) { - const output = Buffer.from(data); - for (let index = 0; index < output.length; index += 4) { - const alpha = data[index + 3]; - if (alpha === 0) continue; - output[index] = colorRgb[0]; - output[index + 1] = colorRgb[1]; - output[index + 2] = colorRgb[2]; - output[index + 3] = alpha; - } - return output; -} - -async function generatedPng(icon, color, hex) { - const { data, info } = await sharp(join(publicDir, icon.file)) +async function generatedPng(file, hex) { + const { data, info } = await sharp(join(publicDir, file)) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); - const colorRgb = parseHex(hex); - const output = - icon.mode === "glyph" - ? tintGlyphIcon(data, colorRgb) - : tintTileIcon(data, colorRgb); + const output = tintTileIcon(data, parseHex(hex)); return sharp(output, { raw: { width: info.width, height: info.height, channels: 4 }, @@ -101,10 +96,14 @@ function generatedManifest(baseManifest, color) { `${JSON.stringify( { ...baseManifest, - icons: baseManifest.icons.map((icon) => ({ - ...icon, - src: icon.src.replace(/\.png$/u, `-${color}.png`), - })), + icons: baseManifest.icons.map((icon) => + icon.purpose === "monochrome" + ? icon + : { + ...icon, + src: icon.src.replace(/\.png$/u, `-${color}.png`), + }, + ), }, null, 2, @@ -112,6 +111,38 @@ function generatedManifest(baseManifest, color) { ); } +// Platforms use only the alpha channel of a monochrome icon as the tint +// mask, so the glyph's darkness (against the tile's white backing) becomes +// alpha — the same luma mask tintTileIcon uses to find glyph pixels. +async function generatedMonochromePng(sourceFile) { + const { data, info } = await sharp(join(publicDir, sourceFile)) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + const output = Buffer.from(data); + for (let index = 0; index < output.length; index += 4) { + const luma = + 0.2126 * data[index] + + 0.7152 * data[index + 1] + + 0.0722 * data[index + 2]; + const maskAlpha = + luma >= 245 + ? 0 + : Math.round( + 255 * Math.sqrt((245 - luma) / 245) * (data[index + 3] / 255), + ); + output[index] = 255; + output[index + 1] = 255; + output[index + 2] = 255; + output[index + 3] = maskAlpha; + } + return sharp(output, { + raw: { width: info.width, height: info.height, channels: 4 }, + }) + .png() + .toBuffer(); +} + async function writeOrCheck(fileName, content) { const filePath = join(publicDir, fileName); if (!checkOnly) { @@ -134,11 +165,18 @@ const baseManifest = JSON.parse( await readFile(join(publicDir, "manifest.webmanifest"), "utf8"), ); +for (const monochromeIcon of monochromeIcons) { + await writeOrCheck( + monochromeIcon.target, + await generatedMonochromePng(monochromeIcon.source), + ); +} + for (const [color, hex] of Object.entries(faviconColorValues)) { - for (const icon of icons) { + for (const file of icons) { await writeOrCheck( - outputFileName(icon.file, color), - await generatedPng(icon, color, hex), + outputFileName(file, color), + await generatedPng(file, hex), ); } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 44e10e3723..a4ab0f0262 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -2052,7 +2052,16 @@ async function runDesktopApp(): Promise { }); assertPathExists({ label: "app icon", path: iconPath }); - if (process.platform === "darwin" && app.dock !== undefined) { + // Packaged builds must not call dock.setIcon: it replaces the bundle icon + // (already channel-correct via electron-builder) with a raw NSImage that + // bypasses the macOS appearance pipeline, so dark mode shows the light + // rendering. Dev runs still need it to show icon-dev.png instead of the + // stock Electron icon. + if ( + process.platform === "darwin" && + app.dock !== undefined && + !paths.isPackaged + ) { app.dock.setIcon(iconPath); } await reapStaleOwnedRuntime({ diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png index 7b52df381e..096cf3159d 100644 Binary files a/apps/web/public/apple-touch-icon.png and b/apps/web/public/apple-touch-icon.png differ