Skip to content

Commit b18381d

Browse files
authored
🤖 feat: update mux logo and add reproducible update script (#944)
Updates all logo files and extends `scripts/generate-icons.ts` with an `update` command for future logo changes. ## Usage ```bash bun scripts/generate-icons.ts update ~/path/to/new-logo.webp ``` This updates: - `docs/img/logo.webp` (canonical source) - `public/icon.png`, `icon-192.png`, `icon-512.png` - `public/favicon.ico` (multi-resolution) - `vscode/icon.png` - `docs/favicon.svg`, `docs/img/dark.svg`, `docs/img/light.svg` - `src/browser/assets/icons/mux.svg` _Generated with `mux`_
1 parent d64ed5d commit b18381d

File tree

6 files changed

+157
-18
lines changed

6 files changed

+157
-18
lines changed

docs/img/logo.webp

290 KB
Loading

public/favicon.ico

132 KB
Binary file not shown.

scripts/generate-icons.ts

Lines changed: 147 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,134 @@
11
#!/usr/bin/env bun
2-
import { mkdir, rm } from "node:fs/promises";
2+
/**
3+
* Icon generation script for mux.
4+
*
5+
* Usage:
6+
* bun scripts/generate-icons.ts [commands...]
7+
*
8+
* Commands:
9+
* update <source> - Update all logo files from a source image (webp/png/jpg)
10+
* png - Generate build/icon.png (512x512)
11+
* icns - Generate build/icon.icns (macOS app icon)
12+
*
13+
* If no command is given, defaults to: png icns
14+
*
15+
* Examples:
16+
* bun scripts/generate-icons.ts update ~/Pictures/new-logo.webp
17+
* bun scripts/generate-icons.ts png icns
18+
*/
19+
import { mkdir, rm, copyFile, writeFile } from "node:fs/promises";
320
import path from "node:path";
421
import { fileURLToPath } from "node:url";
522
import sharp from "sharp";
623

7-
const SIZES = [16, 32, 64, 128, 256, 512];
24+
const ICONSET_SIZES = [16, 32, 64, 128, 256, 512];
825

926
const __filename = fileURLToPath(import.meta.url);
1027
const __dirname = path.dirname(__filename);
1128
const ROOT = path.resolve(__dirname, "..");
29+
30+
// Source logo - all other icons are derived from this
1231
const SOURCE = path.join(ROOT, "docs", "img", "logo.webp");
32+
33+
// Build outputs
1334
const BUILD_DIR = path.join(ROOT, "build");
1435
const ICONSET_DIR = path.join(BUILD_DIR, "icon.iconset");
1536
const PNG_OUTPUT = path.join(BUILD_DIR, "icon.png");
1637
const ICNS_OUTPUT = path.join(BUILD_DIR, "icon.icns");
1738

18-
const args = new Set(process.argv.slice(2));
19-
if (args.size === 0) {
20-
args.add("png");
21-
args.add("icns");
39+
// All logo locations that need updating (icon-only, not text logos)
40+
const LOGO_TARGETS = {
41+
// VS Code extension
42+
"vscode/icon.png": { size: 128 },
43+
// Browser asset
44+
"src/browser/assets/icons/mux.svg": { size: 1024, svg: true },
45+
} as const;
46+
47+
const FAVICON_SIZES = [16, 32, 48, 64, 128, 256];
48+
49+
async function generatePngFromSource(source: string, output: string, size: number) {
50+
await sharp(source).resize(size, size).toFile(output);
2251
}
23-
const needsPng = args.has("png") || args.has("icns");
24-
const needsIcns = args.has("icns");
2552

26-
async function generateIconPng() {
53+
async function generateSvgWithEmbeddedPng(source: string, output: string, size: number) {
54+
const pngBuffer = await sharp(source).resize(size, size).png().toBuffer();
55+
const base64 = pngBuffer.toString("base64");
56+
const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
57+
<image width="${size}" height="${size}" xlink:href="data:image/png;base64,${base64}"/>
58+
</svg>
59+
`;
60+
await writeFile(output, svg);
61+
}
62+
63+
async function generateFavicon(source: string, output: string) {
64+
// Use ImageMagick if available for proper multi-resolution ICO
65+
try {
66+
const proc = Bun.spawn(
67+
[
68+
"magick",
69+
source,
70+
"-resize",
71+
"256x256",
72+
"-define",
73+
`icon:auto-resize=${FAVICON_SIZES.join(",")}`,
74+
output,
75+
],
76+
{ stdout: "ignore", stderr: "ignore" }
77+
);
78+
const status = await proc.exited;
79+
if (status === 0) return;
80+
} catch {
81+
// ImageMagick not available
82+
}
83+
84+
// Fallback: just use the 256x256 PNG renamed as ICO (works in most browsers)
85+
const pngBuffer = await sharp(source).resize(256, 256).png().toBuffer();
86+
await writeFile(output, pngBuffer);
87+
console.warn(" ⚠ ImageMagick not found, favicon.ico is single-resolution");
88+
}
89+
90+
async function updateAllLogos(sourcePath: string) {
91+
const resolvedSource = path.resolve(sourcePath);
92+
console.log(`Updating all logos from: ${resolvedSource}\n`);
93+
94+
// First, copy source to canonical location
95+
const sourceExt = path.extname(resolvedSource).toLowerCase();
96+
if (sourceExt === ".webp") {
97+
await copyFile(resolvedSource, SOURCE);
98+
console.log(`✓ docs/img/logo.webp (source)`);
99+
} else {
100+
// Convert to webp
101+
await sharp(resolvedSource).webp().toFile(SOURCE);
102+
console.log(`✓ docs/img/logo.webp (converted from ${sourceExt})`);
103+
}
104+
105+
// Generate all PNG targets
106+
for (const [relativePath, config] of Object.entries(LOGO_TARGETS)) {
107+
const outputPath = path.join(ROOT, relativePath);
108+
if (config.svg) {
109+
await generateSvgWithEmbeddedPng(SOURCE, outputPath, config.size);
110+
} else {
111+
await generatePngFromSource(SOURCE, outputPath, config.size);
112+
}
113+
console.log(`✓ ${relativePath} (${config.size}x${config.size})`);
114+
}
115+
116+
// Generate favicon.ico
117+
const faviconPath = path.join(ROOT, "public", "favicon.ico");
118+
await generateFavicon(SOURCE, faviconPath);
119+
console.log(`✓ public/favicon.ico (multi-resolution)`);
120+
121+
console.log("\n✅ All logos updated successfully!");
122+
}
123+
124+
async function generateBuildPng() {
27125
await sharp(SOURCE).resize(512, 512).toFile(PNG_OUTPUT);
28126
}
29127

30128
async function generateIconsetPngs() {
31129
await mkdir(ICONSET_DIR, { recursive: true });
32130

33-
const tasks = SIZES.flatMap((size) => {
131+
const tasks = ICONSET_SIZES.flatMap((size) => {
34132
const outputs = [
35133
{
36134
file: path.join(ICONSET_DIR, `icon_${size}x${size}.png`),
@@ -66,15 +164,47 @@ async function generateIcns() {
66164
}
67165
}
68166

69-
await mkdir(BUILD_DIR, { recursive: true });
167+
// Parse arguments
168+
const args = process.argv.slice(2);
169+
const commands = new Set<string>();
170+
let updateSource: string | undefined;
70171

71-
if (needsPng) {
72-
await generateIconPng();
172+
for (let i = 0; i < args.length; i++) {
173+
if (args[i] === "update") {
174+
updateSource = args[++i];
175+
if (!updateSource) {
176+
console.error("Error: 'update' command requires a source image path");
177+
process.exit(1);
178+
}
179+
commands.add("update");
180+
} else {
181+
commands.add(args[i]);
182+
}
73183
}
74184

75-
if (needsIcns) {
76-
await generateIconsetPngs();
77-
await generateIcns();
185+
// Default to png + icns if no commands
186+
if (commands.size === 0) {
187+
commands.add("png");
188+
commands.add("icns");
78189
}
79190

80-
await rm(ICONSET_DIR, { recursive: true, force: true });
191+
// Execute commands
192+
if (commands.has("update") && updateSource) {
193+
await updateAllLogos(updateSource);
194+
} else {
195+
await mkdir(BUILD_DIR, { recursive: true });
196+
197+
const needsPng = commands.has("png") || commands.has("icns");
198+
const needsIcns = commands.has("icns");
199+
200+
if (needsPng) {
201+
await generateBuildPng();
202+
}
203+
204+
if (needsIcns) {
205+
await generateIconsetPngs();
206+
await generateIcns();
207+
}
208+
209+
await rm(ICONSET_DIR, { recursive: true, force: true });
210+
}

src/browser/assets/icons/mux.svg

Lines changed: 3 additions & 1 deletion
Loading

src/browser/components/ProviderIcon.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const PROVIDER_ICONS: Partial<Record<ProviderName, React.FC>> = {
1717
"mux-gateway": MuxIcon,
1818
};
1919

20+
// Providers with bitmap logos that need CSS filters for consistent appearance
21+
const BITMAP_ICON_PROVIDERS = new Set<string>(["mux-gateway"]);
22+
2023
export interface ProviderIconProps {
2124
provider: string;
2225
className?: string;
@@ -30,10 +33,14 @@ export function ProviderIcon(props: ProviderIconProps) {
3033
const IconComponent = PROVIDER_ICONS[props.provider as keyof typeof PROVIDER_ICONS];
3134
if (!IconComponent) return null;
3235

36+
const isBitmap = BITMAP_ICON_PROVIDERS.has(props.provider);
37+
3338
return (
3439
<span
3540
className={cn(
3641
"inline-block h-[1em] w-[1em] align-[-0.125em] [&_svg]:block [&_svg]:h-full [&_svg]:w-full [&_svg]:fill-current [&_svg_.st0]:fill-current",
42+
// Bitmap icons (embedded PNGs) need CSS filters to match the monochrome style
43+
isBitmap && "grayscale brightness-[2] dark:brightness-[10] dark:contrast-[0.5]",
3744
props.className
3845
)}
3946
>

vscode/icon.png

6.76 KB
Loading

0 commit comments

Comments
 (0)