on master help sign in
Use the new Diagonal icon
f0c4d2fClaude
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+
1# Diagonal
3# Diagonal
2
4
3Dia-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.
5Dia-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 @@
13 "gen-emoji": "node scripts/gen-emoji.mjs",
13 "gen-emoji": "node scripts/gen-emoji.mjs",
14 "zip": "node scripts/build.mjs && cd dist && zip -qr ../diagonal-extension.zip .",
14 "zip": "node scripts/build.mjs && cd dist && zip -qr ../diagonal-extension.zip .",
15 "smoke": "node scripts/build.mjs && node test/e2e/smoke.mjs",
15 "smoke": "node scripts/build.mjs && node test/e2e/smoke.mjs",
16− "icons": "node scripts/render-icons.mjs",
16+ "icons": "python3 scripts/render-icons.py",
17 "check": "tsc --noEmit && vitest run && cd host && python3 -m unittest discover -s tests"
17 "check": "tsc --noEmit && vitest run && cd host && python3 -m unittest discover -s tests"
18 },
18 },
19 "devDependencies": {
19 "devDependencies": {
scripts/build.mjs+1−1
@@ -34,7 +34,7 @@ function copyStatic() {
34 cpSync(join(root, "src/popup/popup.html"), join(dist, "popup.html"));
34 cpSync(join(root, "src/popup/popup.html"), join(dist, "popup.html"));
35 cpSync(join(root, "src/options/options.html"), join(dist, "options.html"));
35 cpSync(join(root, "src/options/options.html"), join(dist, "options.html"));
36 cpSync(join(root, "src/shared/ui.css"), join(dist, "ui.css"));
36 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") });
38 // The native host and its installer ride along, so the release zip is all a user needs.
38 // The native host and its installer ride along, so the release zip is all a user needs.
39 mkdirSync(join(dist, "host"));
39 mkdirSync(join(dist, "host"));
40 for (const f of ["diagonal-host.py", "prompts.py", "validate.py", "emoji.txt"]) cpSync(join(root, "host", f), join(dist, "host", f));
40 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()