on master help sign in
Use the new Diagonal icon

icons/icon.png is the full-size app icon; scripts/render-icons.py (npm run icons) trims its transparent margin and renders the 16, 32, 48 and 128 px sizes the manifest uses. The README shows the icon at the top. Replaces the old SVG icon. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LdKDAYoqmGehyHsV42LhJW

commitf0c4d2f340a38c3ebac7628da6accb0d006ac98b authorClaude <noreply@anthropic.com> dateWed 23 Sep 2026, 20:39 (2 weeks ago) parent6ad7ba1
11 files changed+48−25
README.md+2 icons/128.png icons/16.png icons/32.png icons/48.png icons/icon.png icons/icon.svg−6 package.json+1−1 scripts/build.mjs+1−1 scripts/render-icons.mjs−17 scripts/render-icons.py+44
README.md+2
@@ -1,3 +1,5 @@
1+<p align="center"><img src="icons/icon.png" width="160" height="160" alt="Diagonal icon: two glass quarter-circles on a green rounded square"></p>
2+
13# Diagonal
24
35Dia-style tab groups for Chromium browsers on macOS (Brave, Chrome, Edge, Vivaldi, Arc, Opera, …), named by Apple's on-device model through the `fm` command-line tool over Chromium native messaging. No HTTP server, no cloud call, no model download.
icons/128.png

Binary file not shown.

icons/16.png

Binary file not shown.

icons/32.png

Binary file not shown.

icons/48.png

Binary file not shown.

icons/icon.pngnew file

Binary file not shown.

icons/icon.svgdeleted−6
@@ -1,6 +0,0 @@
1−<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
2− <rect x="4" y="4" width="120" height="120" rx="28" fill="#1a7f5a"/>
3− <path d="M64 22c-15 12-26 27-26 42 0 14 11 24 26 24s26-10 26-24c0-15-11-30-26-42z" fill="#a7f3c9"/>
4− <path d="M64 44v62" stroke="#0d3b2a" stroke-width="7" stroke-linecap="round"/>
5− <path d="M64 70l-12-10M64 82l13-11" stroke="#0d3b2a" stroke-width="6" stroke-linecap="round"/>
6−</svg>
package.json+1−1
@@ -13,7 +13,7 @@
1313 "gen-emoji": "node scripts/gen-emoji.mjs",
1414 "zip": "node scripts/build.mjs && cd dist && zip -qr ../diagonal-extension.zip .",
1515 "smoke": "node scripts/build.mjs && node test/e2e/smoke.mjs",
16− "icons": "node scripts/render-icons.mjs",
16+ "icons": "python3 scripts/render-icons.py",
1717 "check": "tsc --noEmit && vitest run && cd host && python3 -m unittest discover -s tests"
1818 },
1919 "devDependencies": {
scripts/build.mjs+1−1
@@ -34,7 +34,7 @@ function copyStatic() {
3434 cpSync(join(root, "src/popup/popup.html"), join(dist, "popup.html"));
3535 cpSync(join(root, "src/options/options.html"), join(dist, "options.html"));
3636 cpSync(join(root, "src/shared/ui.css"), join(dist, "ui.css"));
37− cpSync(join(root, "icons"), join(dist, "icons"), { recursive: true, filter: (p) => !p.endsWith(".svg") });
37+ cpSync(join(root, "icons"), join(dist, "icons"), { recursive: true, filter: (p) => !p.endsWith("icon.png") });
3838 // The native host and its installer ride along, so the release zip is all a user needs.
3939 mkdirSync(join(dist, "host"));
4040 for (const f of ["diagonal-host.py", "prompts.py", "validate.py", "emoji.txt"]) cpSync(join(root, "host", f), join(dist, "host", f));
scripts/render-icons.mjsdeleted−17
@@ -1,17 +0,0 @@
1−// Renders icons/icon.svg to the PNG sizes the manifest lists, using a local Chromium.
2−import { chromium } from "playwright-core";
3−import { readFileSync } from "node:fs";
4−import { dirname, join } from "node:path";
5−import { fileURLToPath } from "node:url";
6−
7−const root = join(dirname(fileURLToPath(import.meta.url)), "..");
8−const svg = readFileSync(join(root, "icons/icon.svg"), "utf8");
9−const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH || "/opt/pw-browsers/chromium-1194/chrome-linux/chrome" });
10−for (const size of [16, 32, 48, 128]) {
11− const page = await browser.newPage({ viewport: { width: size, height: size }, deviceScaleFactor: 1 });
12− await page.setContent(`<html><body style="margin:0;background:transparent">${svg.replace("<svg ", `<svg width="${size}" height="${size}" `)}</body></html>`);
13− await page.screenshot({ path: join(root, `icons/${size}.png`), omitBackground: true });
14− await page.close();
15−}
16−await browser.close();
17−console.log("icons written");
scripts/render-icons.pynew file+44
@@ -0,0 +1,44 @@
1+#!/usr/bin/env python3
2+"""Renders icons/icon.png to the PNG sizes the manifest lists. Needs Pillow (python3 -m pip install pillow).
3+
4+The source is the full-size app icon. Its transparent margin is trimmed first so the rounded square fills the
5+16 and 32 px toolbar icons; the larger sizes keep a little padding, as Chrome's extension page expects.
6+"""
7+from pathlib import Path
8+
9+from PIL import Image, ImageFilter
10+
11+ICONS = Path(__file__).resolve().parent.parent / "icons"
12+# size -> padding in px on each side at that size
13+SIZES = {16: 0, 32: 0, 48: 2, 128: 8}
14+
15+
16+def trimmed(src: Image.Image) -> Image.Image:
17+ # Near-invisible specks around the shape would otherwise widen the bounding box.
18+ solid = src.getchannel("A").point(lambda a: 255 if a > 32 else 0)
19+ left, top, right, bottom = solid.getbbox()
20+ side = max(right - left, bottom - top)
21+ cx, cy = (left + right) / 2, (top + bottom) / 2
22+ box = (round(cx - side / 2), round(cy - side / 2), round(cx + side / 2), round(cy + side / 2))
23+ return src.crop(box)
24+
25+
26+def render(art: Image.Image, size: int, pad: int) -> Image.Image:
27+ inner = size - 2 * pad
28+ small = art.resize((inner, inner), Image.LANCZOS)
29+ if inner <= 32:
30+ small = small.filter(ImageFilter.UnsharpMask(radius=0.6, percent=60, threshold=0))
31+ out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
32+ out.paste(small, (pad, pad))
33+ return out
34+
35+
36+def main() -> None:
37+ art = trimmed(Image.open(ICONS / "icon.png").convert("RGBA"))
38+ for size, pad in SIZES.items():
39+ render(art, size, pad).save(ICONS / f"{size}.png", optimize=True)
40+ print("icons written")
41+
42+
43+if __name__ == "__main__":
44+ main()