#!/usr/bin/env python3 """ Build the BridgeSwarm branding package (peardock-branding shaped) and sync runtime copies into the repo (extension icons, assets/brand, assets/logo, …). Usage: python3 scripts/build-branding.py python3 scripts/build-branding.py --sync-only # skip regenerate, only copy """ from __future__ import annotations import argparse import json import math import shutil import struct import zlib from pathlib import Path from PIL import Image, ImageDraw, ImageFilter, ImageFont ROOT = Path(__file__).resolve().parents[1] PKG = ROOT / "bridgeswarm-branding" ASSETS_BRAND = ROOT / "assets" / "brand" ASSETS_LOGO = ROOT / "assets" / "logo" ASSETS_FAV = ROOT / "assets" / "favicons" EXT_ICONS = ROOT / "extension" / "icons" # Brand palette (aligned with Control Center / peardock-style tokens) ACCENT = (45, 212, 191, 255) # #2dd4bf ACCENT_2 = (56, 189, 248, 255) # #38bdf8 ACCENT_DIM = (20, 184, 166, 255) # #14b8a6 BG_DARK = (12, 12, 16, 255) # #0c0c10 BG_CARD = (26, 26, 32, 255) # #1a1a20 TEXT = (236, 236, 241, 255) # #ececf1 WHITE = (255, 255, 255, 255) ICON_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024, 2048] LINUX_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024] ELECTRON_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024] FAVICON_PNG = [16, 32, 48] MAC_ICONSET = { "icon_16x16.png": 16, "icon_16x16@2x.png": 32, "icon_32x32.png": 32, "icon_32x32@2x.png": 64, "icon_128x128.png": 128, "icon_128x128@2x.png": 256, "icon_256x256.png": 256, "icon_256x256@2x.png": 512, "icon_512x512.png": 512, "icon_512x512@2x.png": 1024, } def ensure_dirs() -> None: for p in [ PKG / "logo", PKG / "brand-assets", PKG / "favicons", PKG / "app-icons" / "electron", PKG / "app-icons" / "linux", PKG / "app-icons" / "windows", PKG / "app-icons" / "macos" / "bridgeswarm.iconset", PKG / "website", PKG / "social", PKG / "docs", ASSETS_BRAND, ASSETS_LOGO, ASSETS_FAV, EXT_ICONS, ]: p.mkdir(parents=True, exist_ok=True) def lerp(a: float, b: float, t: float) -> float: return a + (b - a) * t def mix(c1, c2, t: float): return tuple(int(lerp(c1[i], c2[i], t)) for i in range(3)) + (255,) def draw_mark(size: int, *, padded: bool = True, mono: bool = False, on_dark: bool = False) -> Image.Image: """Draw the BridgeSwarm 3-node bridge mark (matches Control Center logo).""" img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Geometry in unit space (0..1), matching dashboard SVG composition # nodes: left (0.28, 0.50), right-upper (0.72, 0.32), right-lower (0.72, 0.68) nodes = [(0.28, 0.50), (0.72, 0.32), (0.72, 0.68)] edges = [(0, 1), (0, 2)] margin = 0.14 if padded else 0.08 scale = 1.0 - 2 * margin def xy(u, v): return (margin + u * scale) * size, (margin + v * scale) * size r_node = size * (0.095 if padded else 0.11) stroke = max(2, int(size * 0.045)) if on_dark: # rounded square plate rad = int(size * 0.22) plate = Image.new("RGBA", (size, size), (0, 0, 0, 0)) pd = ImageDraw.Draw(plate) pd.rounded_rectangle([0, 0, size - 1, size - 1], radius=rad, fill=BG_DARK) img = Image.alpha_composite(img, plate) draw = ImageDraw.Draw(img) # soft glow under nodes if size >= 64 and not mono: glow = Image.new("RGBA", (size, size), (0, 0, 0, 0)) gd = ImageDraw.Draw(glow) for u, v in nodes: x, y = xy(u, v) gr = r_node * 2.2 gd.ellipse([x - gr, y - gr, x + gr, y + gr], fill=(45, 212, 191, 55)) glow = glow.filter(ImageFilter.GaussianBlur(radius=max(1, size // 48))) img = Image.alpha_composite(img, glow) draw = ImageDraw.Draw(img) line_color = (200, 200, 210, 255) if mono else ACCENT node_fill = (220, 220, 230, 255) if mono else ACCENT node_edge = (160, 160, 170, 255) if mono else ACCENT_2 # bridges for a, b in edges: x1, y1 = xy(*nodes[a]) x2, y2 = xy(*nodes[b]) draw.line([(x1, y1), (x2, y2)], fill=line_color, width=stroke) # nodes with slight gradient feel via two ellipses for i, (u, v) in enumerate(nodes): x, y = xy(u, v) fill = node_fill if i == 0 or mono else mix(ACCENT, ACCENT_2, 0.35 + i * 0.15) draw.ellipse([x - r_node, y - r_node, x + r_node, y + r_node], fill=fill) # inner highlight if size >= 48 and not mono: hr = r_node * 0.35 draw.ellipse( [x - hr * 0.6, y - r_node * 0.55, x + hr * 1.1, y - r_node * 0.05], fill=(255, 255, 255, 70), ) if mono: draw.ellipse( [x - r_node, y - r_node, x + r_node, y + r_node], outline=node_edge, width=max(1, stroke // 3), ) return img def resize_hq(im: Image.Image, size: int) -> Image.Image: return im.resize((size, size), Image.Resampling.LANCZOS) def save_png(im: Image.Image, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) im.save(path, format="PNG", optimize=True) def write_ico(path: Path, images: list[Image.Image]) -> None: """Minimal multi-size ICO writer (PNG-compressed entries).""" entries = [] payloads = [] for im in images: buf = __import__("io").BytesIO() im.save(buf, format="PNG") data = buf.getvalue() w, h = im.size entries.append((w if w < 256 else 0, h if h < 256 else 0, len(data), data)) payloads.append(data) # ICONDIR + ICONDIRENTRY*n + data offset = 6 + 16 * len(entries) out = bytearray() out += struct.pack(" None: fill = "#0c0c10" if dark_text else "#ececf1" accent = "#2dd4bf" svg = f''' Bridge Swarm ''' path.write_text(svg, encoding="utf-8") def composite_on(color: tuple, mark: Image.Image, size: int) -> Image.Image: base = Image.new("RGBA", (size, size), color) m = resize_hq(mark, size) return Image.alpha_composite(base, m) def make_banner(mark: Image.Image, size: tuple[int, int], title: str) -> Image.Image: w, h = size img = Image.new("RGBA", (w, h), BG_DARK) draw = ImageDraw.Draw(img) # gradient-ish bands for y in range(h): t = y / max(1, h - 1) c = mix(BG_DARK, BG_CARD, t * 0.7) draw.line([(0, y), (w, y)], fill=c) # accent orbs orb = Image.new("RGBA", (w, h), (0, 0, 0, 0)) od = ImageDraw.Draw(orb) od.ellipse([w * 0.55, -h * 0.2, w * 1.1, h * 0.7], fill=(45, 212, 191, 40)) od.ellipse([-w * 0.1, h * 0.4, w * 0.45, h * 1.2], fill=(56, 189, 248, 28)) orb = orb.filter(ImageFilter.GaussianBlur(radius=40)) img = Image.alpha_composite(img, orb) icon = resize_hq(mark, int(h * 0.55)) ix = int(w * 0.08) iy = (h - icon.height) // 2 img.paste(icon, (ix, iy), icon) draw = ImageDraw.Draw(img) try: font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial Bold.ttf", int(h * 0.22)) font_sm = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", int(h * 0.09)) except Exception: font = ImageFont.load_default() font_sm = font tx = ix + icon.width + int(w * 0.04) draw.text((tx, h * 0.32), "BridgeSwarm", fill=TEXT, font=font) draw.text((tx, h * 0.58), title, fill=(139, 139, 154, 255), font=font_sm) return img def write_tokens() -> None: tokens = { "name": "BridgeSwarm", "version": "1.0.0", "description": "P2P Hyperswarm in the browser via native messaging host", "colors": { "background": { "primary": "#0c0c10", "secondary": "#141418", "tertiary": "#1a1a20", "elevated": "#1a1a20", "hover": "#22222a", }, "accent": { "primary": "#2dd4bf", "secondary": "#14b8a6", "success": "#34d399", "warning": "#fbbf24", "danger": "#fb7185", "info": "#60a5fa", }, "text": { "primary": "#ececf1", "secondary": "#c8c8d4", "muted": "#8b8b9a", "faint": "#5c5c6c", }, "logo": { "node": "#2dd4bf", "nodeAlt": "#38bdf8", "bridge": "#2dd4bf", "plate": "#0c0c10", }, "border": "rgba(255,255,255,0.1)", }, "typography": { "fontFamily": "\"Avenir Next\", \"Segoe UI\", \"Helvetica Neue\", sans-serif", "monoFamily": "\"Cascadia Code\", \"SF Mono\", Consolas, monospace", "baseSize": "14px", "lineHeight": 1.5, }, "radius": {"default": "10px", "small": "7px"}, "logoUsage": { "minSize": "16px", "clearSpace": "0.5x logo height", "preferredBg": "dark #0c0c10 or pure white", "source": "logo/bridgeswarm-icon-master.png — resize with LANCZOS only", }, } (PKG / "brand-assets" / "brand-tokens.json").write_text(json.dumps(tokens, indent=2) + "\n", encoding="utf-8") css = """/* BridgeSwarm Brand Color Tokens Source of truth — synced into assets/brand/colors.css Keep in sync with extension/panel.css Control Center */ :root { --bs-bg-primary: #0c0c10; --bs-bg-secondary: #141418; --bs-bg-tertiary: #1a1a20; --bs-bg-elevated: #1a1a20; --bs-bg-hover: #22222a; --bs-accent-primary: #2dd4bf; --bs-accent-secondary: #14b8a6; --bs-accent-success: #34d399; --bs-accent-warning: #fbbf24; --bs-accent-danger: #fb7185; --bs-accent-info: #60a5fa; --bs-text-primary: #ececf1; --bs-text-secondary: #c8c8d4; --bs-text-muted: #8b8b9a; --bs-text-faint: #5c5c6c; --bs-border: rgba(255, 255, 255, 0.1); --bs-border-strong: #3a3a46; --bs-logo-node: #2dd4bf; --bs-logo-node-alt: #38bdf8; --bs-logo-bridge: #2dd4bf; --bs-logo-plate: #0c0c10; --bs-radius: 10px; --bs-radius-sm: 7px; --bs-font: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif; --bs-mono: "Cascadia Code", "SF Mono", Consolas, monospace; } """ (PKG / "brand-assets" / "colors.css").write_text(css, encoding="utf-8") def write_manifests() -> None: (PKG / "favicons" / "site.webmanifest").write_text( json.dumps( { "name": "BridgeSwarm", "short_name": "BridgeSwarm", "description": "P2P Hyperswarm in the browser via native messaging host", "icons": [ {"src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png"}, {"src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png"}, ], "theme_color": "#2dd4bf", "background_color": "#0c0c10", "display": "standalone", }, indent=2, ) + "\n", encoding="utf-8", ) (PKG / "favicons" / "browserconfig.xml").write_text( """ #0c0c10 """, encoding="utf-8", ) def write_readme() -> None: (PKG / "README.md").write_text( """# BridgeSwarm Branding Package v1.0.0 **Master brand package** (layout mirrors PearDock’s `peardock-branding`). Clean geometric mark: three peer nodes bridged into a swarm — teal `#2dd4bf` on transparent alpha. Source of truth for icons, favicons, social, website, and CSS tokens. --- ## Critical Rule **Regenerate sizes only from `logo/bridgeswarm-icon-master.png`.** Use high-quality LANCZOS / Mitchell resizes. Do not recolor-key or re-remove backgrounds. Rebuild everything: ```bash python3 scripts/build-branding.py ``` --- ## Structure ``` logo/ bridgeswarm-icon-master.png # Clean square icon (transparent) bridgeswarm-icon-master-padded.png bridgeswarm-icon-{16..2048}.png bridgeswarm-icon-mono.png bridgeswarm-icon-teal.png bridgeswarm-icon-dark.png / -light.png bridgeswarm-logo-full-transparent.png bridgeswarm-logo-full-1200.png / -800.png bridgeswarm-logo-dark.png / -light.png bridgeswarm-wordmark.svg + -dark.svg favicons/ # ico + PWA sizes + webmanifest + browserconfig app-icons/ # electron/ linux/ windows/ macos.iconset/ store icons website/ # og-image, twitter-card, header-banner, hero-bgs, logos social/ # avatars + circular + banner brand-assets/ # colors.css + brand-tokens.json docs/ # BridgeSwarm-Brand-Guidelines.md ``` ## Color Tokens | Token | Hex | Role | |-------|-----|------| | `--bs-bg-primary` | `#0c0c10` | Main bg | | `--bs-accent-primary` | `#2dd4bf` | Teal CTA / nodes | | `--bs-accent-info` | `#60a5fa` | Secondary accent | | `--bs-text-primary` | `#ececf1` | Body text | Full list in `brand-assets/colors.css` and `brand-tokens.json`. ## Drop-in Usage This package is the **source of truth**. Runtime copies live in the app tree: | Destination | Contents | |-------------|----------| | `../extension/icons/{16,48,128}.png` | Chrome/Firefox extension icons | | `../assets/logo/` | README + titlebar logos | | `../assets/favicons/` | Favicons + webmanifest | | `../assets/brand/` | colors.css + brand-tokens.json | ```html ``` Programmatic resolve: ```js const { resolveAssets, COLORS } = require('./bridgeswarm-branding') const paths = resolveAssets() // paths.logo.master, paths.favicon[32], … ``` macOS `.icns` (on a Mac): ```bash cd app-icons/macos iconutil -c icns bridgeswarm.iconset -o ../../build/icon.icns ``` --- © 2026 snxraven · MIT Repo: https://git.ssh.surf/snxraven/BridgeSwarm """, encoding="utf-8", ) (PKG / "docs" / "BridgeSwarm-Brand-Guidelines.md").write_text( """# BridgeSwarm Brand Guidelines ## Mark The BridgeSwarm mark is **three peer nodes** linked by two bridges — a compact diagram of Hyperswarm discovery and direct Noise connections. It is not a letterform monogram. - **Primary color:** teal `#2dd4bf` - **Secondary node tint:** sky `#38bdf8` - **Plate (app icon):** near-black `#0c0c10` ## Clear space Keep clear space of at least **½ the mark height** on all sides. Do not place competing UI chrome inside that margin. ## Minimum size - UI / toolbar: **16×16** (extension) - Marketing: prefer **128×128** or larger - Wordmark: do not scale below ~120px wide ## Backgrounds | Background | Use | |------------|-----| | `#0c0c10` / dark UI | Primary — transparent mark or dark composite | | Pure white | Light composite (`bridgeswarm-icon-light.png`) or dark wordmark | | Photography | Prefer mark-on-dark plate | ## Don’t - Recolor nodes to purple gradients or warm cream “AI default” palettes - Add glow stacks, emoji, or 3D skeuomorphism - Stretch or rotate the mark - Replace the mark with the Hyperswarm logo ## Voice (short) BridgeSwarm brings the Holepunch P2P stack into normal browsers via a Bare native host. Brand language should stay technical, direct, and desktop-native — not “Web3 buzzword” marketing. ## Tokens See `brand-assets/brand-tokens.json` and `colors.css`. """, encoding="utf-8", ) def write_package_js() -> None: (PKG / "package.json").write_text( json.dumps( { "name": "bridgeswarm-branding", "version": "1.0.0", "main": "index.js", "type": "commonjs", "description": "Official BridgeSwarm logos, favicons, and brand tokens (static assets)", "license": "MIT", }, indent=2, ) + "\n", encoding="utf-8", ) (PKG / "index.js").write_text( """'use strict'; const path = require('path'); const ROOT = __dirname; const BRAND_VERSION = '1.0.0'; const COLORS = { accentPrimary: '#2dd4bf', accentSecondary: '#14b8a6', accentInfo: '#60a5fa', bgPrimary: '#0c0c10', textPrimary: '#ececf1', }; /** CSS custom properties for Control Center / docs (keep in sync with colors.css). */ const BRIDGESWARM_THEME_TOKENS = { '--bs-bg-primary': COLORS.bgPrimary, '--bs-accent-primary': COLORS.accentPrimary, '--bs-accent-secondary': COLORS.accentSecondary, '--bs-accent-info': COLORS.accentInfo, '--bs-text-primary': COLORS.textPrimary, '--bs-border': 'rgba(255,255,255,0.1)', }; const ASSETS = { logo: { master: 'logo/bridgeswarm-icon-master.png', masterPadded: 'logo/bridgeswarm-icon-master-padded.png', icon512: 'logo/bridgeswarm-icon-512.png', icon128: 'logo/bridgeswarm-icon-128.png', dark: 'logo/bridgeswarm-icon-dark.png', light: 'logo/bridgeswarm-icon-light.png', mono: 'logo/bridgeswarm-icon-mono.png', teal: 'logo/bridgeswarm-icon-teal.png', full1200: 'logo/bridgeswarm-logo-full-1200.png', full800: 'logo/bridgeswarm-logo-full-800.png', wordmark: 'logo/bridgeswarm-wordmark.svg', wordmarkDark: 'logo/bridgeswarm-wordmark-dark.svg', }, favicon: { 16: 'favicons/favicon-16x16.png', 32: 'favicons/favicon-32x32.png', 48: 'favicons/favicon-48x48.png', ico: 'favicons/favicon.ico', apple: 'favicons/apple-touch-icon.png', 192: 'favicons/android-chrome-192x192.png', 512: 'favicons/android-chrome-512x512.png', }, brand: { tokens: 'brand-assets/brand-tokens.json', colors: 'brand-assets/colors.css', }, guidelines: 'docs/BridgeSwarm-Brand-Guidelines.md', }; function assetPath(relativePath) { return path.join(ROOT, String(relativePath || '')); } function resolveAssets() { const out = { root: ROOT, version: BRAND_VERSION, colors: { ...COLORS } }; for (const [group, entries] of Object.entries(ASSETS)) { if (typeof entries === 'string') { out[group] = assetPath(entries); continue; } out[group] = {}; for (const [key, rel] of Object.entries(entries)) { out[group][key] = assetPath(rel); } } return out; } module.exports = { ROOT, BRAND_VERSION, COLORS, BRIDGESWARM_THEME_TOKENS, ASSETS, assetPath, resolveAssets, }; """, encoding="utf-8", ) def build_package() -> Image.Image: ensure_dirs() master = draw_mark(2048, padded=True) master_tight = draw_mark(2048, padded=False) save_png(master, PKG / "logo" / "bridgeswarm-icon-master.png") save_png(master, PKG / "logo" / "bridgeswarm-icon-master-padded.png") save_png(master_tight, PKG / "logo" / "source-original.png") for s in ICON_SIZES: save_png(resize_hq(master, s), PKG / "logo" / f"bridgeswarm-icon-{s}.png") save_png(draw_mark(1024, padded=True, mono=True), PKG / "logo" / "bridgeswarm-icon-mono.png") # teal = mark on transparent (same as master) — alias for peardock parity save_png(resize_hq(master, 1024), PKG / "logo" / "bridgeswarm-icon-teal.png") save_png(composite_on(BG_DARK, master, 1024), PKG / "logo" / "bridgeswarm-icon-dark.png") save_png(composite_on(WHITE, master, 1024), PKG / "logo" / "bridgeswarm-icon-light.png") # Full logo = mark + wordmark baked as wide PNG for w, name in [(1200, "bridgeswarm-logo-full-1200.png"), (800, "bridgeswarm-logo-full-800.png")]: h = int(w * 0.28) banner = make_banner(master, (w, max(h, 180)), "P2P Hyperswarm for the browser") save_png(banner, PKG / "logo" / name) save_png( make_banner(master, (1200, 320), "P2P Hyperswarm for the browser"), PKG / "logo" / "bridgeswarm-logo-full-transparent.png", ) save_png(composite_on(BG_DARK, master, 512), PKG / "logo" / "bridgeswarm-logo-dark.png") save_png(composite_on(WHITE, master, 512), PKG / "logo" / "bridgeswarm-logo-light.png") make_wordmark_svg(PKG / "logo" / "bridgeswarm-wordmark.svg", dark_text=False) make_wordmark_svg(PKG / "logo" / "bridgeswarm-wordmark-dark.svg", dark_text=True) # Favicons for s in FAVICON_PNG: save_png(resize_hq(master, s), PKG / "favicons" / f"favicon-{s}x{s}.png") save_png(resize_hq(master, 180), PKG / "favicons" / "apple-touch-icon.png") save_png(resize_hq(master, 192), PKG / "favicons" / "android-chrome-192x192.png") save_png(resize_hq(master, 512), PKG / "favicons" / "android-chrome-512x512.png") save_png(resize_hq(master, 144), PKG / "favicons" / "mstile-144x144.png") save_png(resize_hq(master, 150), PKG / "favicons" / "mstile-150x150.png") write_ico( PKG / "favicons" / "favicon.ico", [resize_hq(master, s) for s in (16, 32, 48)], ) # App icons for s in ELECTRON_SIZES: save_png(resize_hq(master, s), PKG / "app-icons" / "electron" / f"icon-{s}.png") for s in LINUX_SIZES: save_png(resize_hq(master, s), PKG / "app-icons" / "linux" / f"icon-{s}.png") for name, s in MAC_ICONSET.items(): save_png(resize_hq(master, s), PKG / "app-icons" / "macos" / "bridgeswarm.iconset" / name) write_ico( PKG / "app-icons" / "windows" / "bridgeswarm.ico", [resize_hq(master, s) for s in (16, 32, 48, 64, 128, 256)], ) save_png(resize_hq(master, 1024), PKG / "app-icons" / "app-store-1024.png") save_png(resize_hq(master, 512), PKG / "app-icons" / "play-store-512.png") # Website save_png(make_banner(master, (1200, 630), "P2P Hyperswarm for the modern browser"), PKG / "website" / "og-image.png") save_png(make_banner(master, (1200, 600), "Native messaging · Bare · Hyperswarm"), PKG / "website" / "twitter-card.png") save_png(make_banner(master, (1500, 500), "Control Center · Examples · Capabilities"), PKG / "website" / "header-banner.png") save_png(make_banner(master, (1280, 720), ""), PKG / "website" / "hero-bg.png") save_png(make_banner(master, (1280, 720), ""), PKG / "website" / "hero-bg-1280.png") save_png(resize_hq(master, 256), PKG / "website" / "logo-header.png") # Social save_png(resize_hq(composite_on(BG_DARK, master, 800), 800), PKG / "social" / "avatar-800.png") save_png(resize_hq(composite_on(BG_DARK, master, 400), 400), PKG / "social" / "avatar-400.png") # circular avatar circ = composite_on(BG_DARK, master, 512) mask = Image.new("L", (512, 512), 0) ImageDraw.Draw(mask).ellipse([0, 0, 511, 511], fill=255) out = Image.new("RGBA", (512, 512), (0, 0, 0, 0)) out.paste(circ, (0, 0)) out.putalpha(mask) save_png(out, PKG / "social" / "avatar-circle-dark.png") save_png(make_banner(master, (1500, 500), "BridgeSwarm"), PKG / "social" / "social-banner.png") write_tokens() write_manifests() write_readme() write_package_js() return master def sync_runtime(master: Image.Image | None = None) -> None: ensure_dirs() if master is None: master_path = PKG / "logo" / "bridgeswarm-icon-master.png" if not master_path.exists(): raise SystemExit("Missing master icon — run without --sync-only first") master = Image.open(master_path).convert("RGBA") # Extension icons (Chrome/Firefox) for s in (16, 48, 128): save_png(resize_hq(master, s), EXT_ICONS / f"{s}.png") # Runtime brand + logo + favicons shutil.copy2(PKG / "brand-assets" / "colors.css", ASSETS_BRAND / "colors.css") shutil.copy2(PKG / "brand-assets" / "brand-tokens.json", ASSETS_BRAND / "brand-tokens.json") for name in ( "bridgeswarm-icon-512.png", "bridgeswarm-icon-128.png", "bridgeswarm-icon-dark.png", "bridgeswarm-logo-full-1200.png", "bridgeswarm-wordmark.svg", "bridgeswarm-wordmark-dark.svg", ): src = PKG / "logo" / name if src.exists(): shutil.copy2(src, ASSETS_LOGO / name) for name in ( "favicon.ico", "favicon-16x16.png", "favicon-32x32.png", "favicon-48x48.png", "apple-touch-icon.png", "android-chrome-192x192.png", "android-chrome-512x512.png", "site.webmanifest", "browserconfig.xml", ): src = PKG / "favicons" / name if src.exists(): shutil.copy2(src, ASSETS_FAV / name) # Convenience build/ icons for future packagers build = ROOT / "build" build.mkdir(exist_ok=True) # Dark-plate PNG for packagers that want an opaque square save_png(composite_on(BG_DARK, master, 1024), build / "icon.png") shutil.copy2(PKG / "app-icons" / "windows" / "bridgeswarm.ico", build / "icon.ico") # macOS .icns (best-effort; requires macOS iconutil) iconset = PKG / "app-icons" / "macos" / "bridgeswarm.iconset" try: import subprocess # iconutil is happier with opaque RGB plates for name, s in MAC_ICONSET.items(): composite_on(BG_DARK, master, s).convert("RGB").save(iconset / name, format="PNG") icns = build / "icon.icns" subprocess.run( ["iconutil", "-c", "icns", str(iconset), "-o", str(icns)], check=True, capture_output=True, ) print("Wrote", icns) except Exception as err: print("Note: skipped icon.icns (%s)" % err) print("Synced → extension/icons, assets/brand, assets/logo, assets/favicons, build/") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--sync-only", action="store_true") args = ap.parse_args() if args.sync_only: sync_runtime() else: master = build_package() sync_runtime(master) print("Built bridgeswarm-branding/ and synced runtime copies.") if __name__ == "__main__": main()