feat: rebrand to website design, Apple HIG icons, ASCII onboarding animation
Build & Release / Lint & Typecheck (push) Successful in 38s
Build & Release / Unit Tests (push) Successful in 31s
Build & Release / Windows (arm64) (push) Failing after 11s
Build & Release / Windows (x64) (push) Failing after 27s
Build & Release / Linux (arm64) (push) Canceled after 0s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / macOS (x64) (push) Canceled after 0s

- Align Electron CSS with website design schema (Inter font, #FBFAF5/#191919
  palette, #213448/#94B4C1 accent)
- Generate proper app icons from logo.svg via cairosvg (no more Pillow ladder)
- Square bg icons for Electron/tray (Apple HIG — OS applies squircle mask)
- Circle bg icons for website favicon only
- Add scrolling ASCII agent-activity animation to onboarding hero left panel
- Remove dock.setIcon() — let macOS use bundle .icns with system mask
- Fix goal title overflow with min-w-0 for flex truncation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-24 16:39:28 -04:00
1 parent 273244817a
commit eda0e26adb
18 files changed
+337 -237

No files matched your search

+142 -169
View File
@@ -1,222 +1,195 @@
#!/usr/bin/env python3
"""Generate AgentCompany app icons (PNG, ICO, ICNS) and tray icons.
"""Generate AgentCompany icons from SVG sources using cairosvg.
Design v4: Circle background #191919 with #FBFAF5 "A" logo.
Uses the actual logo paths from logo.svg rendered via cairosvg/Pillow.
Two icon families:
- Web (circle bg): icon.svg → landing favicon + website logo
- App (square bg): icon-app.svg → Electron app icon (Apple HIG, OS applies mask)
- Tray: rendered from logo.svg for menu bar / notification area
"""
from __future__ import annotations
import io
import os
import shutil
import subprocess
from pathlib import Path
from PIL import Image, ImageDraw
# ── Ensure cairo library is discoverable on macOS (Homebrew) ──────────
for _lib_dir in ("/opt/homebrew/opt/cairo/lib", "/usr/local/opt/cairo/lib"):
if Path(_lib_dir).exists():
os.environ["DYLD_LIBRARY_PATH"] = (
_lib_dir + ":" + os.environ.get("DYLD_LIBRARY_PATH", "")
)
break
import cairosvg # noqa: E402 — must come after DYLD_LIBRARY_PATH setup
from PIL import Image # noqa: E402
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "assets"
LANDING = ROOT / "landing" / "public"
ICONSET = ASSETS / "icon.iconset"
PNG_PATH = ASSETS / "icon.png"
ICO_PATH = ASSETS / "icon.ico"
ICNS_PATH = ASSETS / "icon.icns"
SVG_PATH = ASSETS / "icon.svg"
LOGO_SVG = ROOT / "logo.svg"
# SVG sources
WEB_SVG = ASSETS / "icon.svg" # circle bg — web favicon
APP_SVG = ASSETS / "icon-app.svg" # square bg — Electron (Apple HIG)
SIZE = 1024
# ── Brand palette ──────────────────────────────────────────────────────
BG_COLOR = (25, 25, 25) # #191919
LOGO_COLOR = (251, 250, 245) # #FBFAF5
# ── Helpers ───────────────────────────────────────────────────────────
def build_icon() -> Image.Image:
"""Build the app icon by rendering icon.svg via cairosvg, or fallback to Pillow."""
# Try cairosvg first (best quality)
try:
import cairosvg
svg_data = SVG_PATH.read_bytes()
png_data = cairosvg.svg2png(bytestring=svg_data, output_width=SIZE, output_height=SIZE)
import io
return Image.open(io.BytesIO(png_data)).convert("RGBA")
except Exception:
pass
# Fallback: draw programmatically with Pillow
icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
draw = ImageDraw.Draw(icon)
# Circle background
margin = SIZE // 2 - 480
draw.ellipse(
(margin, margin, SIZE - margin, SIZE - margin),
fill=(*BG_COLOR, 255),
def svg_to_image(svg_path: Path, width: int, height: int | None = None) -> Image.Image:
"""Render an SVG to a Pillow RGBA Image at the given size."""
h = height or width
png_data = cairosvg.svg2png(
url=str(svg_path), output_width=width, output_height=h,
)
return Image.open(io.BytesIO(png_data)).convert("RGBA")
# Draw the "A" monogram — same geometry as the old icon but in new color
cx = SIZE // 2
apex_y = 200
base_y = 800
leg_w = 64
base_half = 250
fill = (*LOGO_COLOR, 245)
def leg_x(y: int) -> tuple[float, float]:
t = (y - apex_y) / (base_y - apex_y)
spread = base_half * t
return (cx - spread, cx + spread)
ll_top, _ = leg_x(apex_y)
ll_bot, _ = leg_x(base_y)
_, lr_top = leg_x(apex_y)
_, lr_bot = leg_x(base_y)
left_leg = [
(ll_top - leg_w * 0.35, apex_y),
(ll_top + leg_w * 0.35, apex_y),
(ll_bot + leg_w, base_y),
(ll_bot - leg_w, base_y),
]
right_leg = [
(lr_top - leg_w * 0.35, apex_y),
(lr_top + leg_w * 0.35, apex_y),
(lr_bot + leg_w, base_y),
(lr_bot - leg_w, base_y),
]
draw.polygon(left_leg, fill=fill)
draw.polygon(right_leg, fill=fill)
# Crossbars — all same cream color with varying opacity for hierarchy
bar_h = 42
bar_r = 12
bar_pad = 14
tiers = [
(440, (*LOGO_COLOR, 255)),
(545, (*LOGO_COLOR, 210)),
(650, (*LOGO_COLOR, 175)),
]
for bar_cy, bar_fill in tiers:
left_cx, right_cx = leg_x(bar_cy)
bx0 = int(left_cx + bar_pad)
bx1 = int(right_cx - bar_pad)
draw.rounded_rectangle(
(bx0, bar_cy - bar_h // 2, bx1, bar_cy + bar_h // 2),
radius=bar_r,
fill=bar_fill,
)
# Apex beacon
dot_r = 20
dot_cy = apex_y - 2
draw.ellipse(
(cx - dot_r, dot_cy - dot_r, cx + dot_r, dot_cy + dot_r),
fill=(*LOGO_COLOR, 255),
def svg_bytes_to_image(svg_bytes: bytes, width: int, height: int | None = None) -> Image.Image:
"""Render SVG bytes to a Pillow RGBA Image."""
h = height or width
png_data = cairosvg.svg2png(
bytestring=svg_bytes, output_width=width, output_height=h,
)
return icon
return Image.open(io.BytesIO(png_data)).convert("RGBA")
def build_tray_icons() -> None:
"""Generate tray/notification area icons for all platforms."""
# Color tray icon (Windows/Linux) — 32x32 and 64x64
for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]:
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
margin = 1
draw.ellipse((margin, margin, size - margin, size - margin), fill=(*BG_COLOR, 255))
# ── Web icons (circle background) ────────────────────────────────────
# Simple A shape scaled to tray size
cx = size // 2
top_y = int(size * 0.18)
bot_y = int(size * 0.82)
spread = int(size * 0.28)
w = max(2, int(size * 0.06))
def generate_web_icons() -> None:
"""Generate favicon.ico and icon.png for the landing site (circle bg)."""
print("Generating web icons from icon.svg (circle bg)…")
master = svg_to_image(WEB_SVG, SIZE)
draw.polygon([
(cx - w, top_y), (cx + w, top_y),
(cx + spread + w, bot_y), (cx + spread - w, bot_y),
], fill=(*LOGO_COLOR, 240))
draw.polygon([
(cx - w, top_y), (cx + w, top_y),
(cx - spread + w, bot_y), (cx - spread - w, bot_y),
], fill=(*LOGO_COLOR, 240))
# PNG for apple-touch-icon / og
web_png = LANDING / "icon.png"
master.save(web_png, format="PNG")
print(f" → {web_png}")
# Crossbar
bar_y = int(size * 0.52)
bar_h = max(2, int(size * 0.06))
bar_left = cx - int(spread * 0.55)
bar_right = cx + int(spread * 0.55)
draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(*LOGO_COLOR, 255))
# Copy SVG to landing
shutil.copy2(WEB_SVG, LANDING / "icon.svg")
print(f" → {LANDING / 'icon.svg'}")
img.save(ASSETS / name, format="PNG")
# macOS template icons (monochrome black + alpha)
for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]:
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
cx = size // 2
top_y = int(size * 0.15)
bot_y = int(size * 0.85)
spread = int(size * 0.3)
w = max(1, int(size * 0.06))
draw.polygon([
(cx - w, top_y), (cx + w, top_y),
(cx + spread + w, bot_y), (cx + spread - w, bot_y),
], fill=(0, 0, 0, 220))
draw.polygon([
(cx - w, top_y), (cx + w, top_y),
(cx - spread + w, bot_y), (cx - spread - w, bot_y),
], fill=(0, 0, 0, 220))
bar_y = int(size * 0.52)
bar_h = max(1, int(size * 0.06))
bar_left = cx - int(spread * 0.55)
bar_right = cx + int(spread * 0.55)
draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(0, 0, 0, 240))
img.save(ASSETS / name, format="PNG")
def save_png_and_ico(image: Image.Image) -> None:
ASSETS.mkdir(parents=True, exist_ok=True)
image.save(PNG_PATH, format="PNG")
image.save(
ICO_PATH,
format="ICO",
# ICO with multiple sizes for favicon
favicon = LANDING / "favicon.ico"
master.save(
favicon, format="ICO",
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
print(f" → {favicon}")
def save_icns(image: Image.Image) -> None:
# ── App icons (square background, Apple HIG) ─────────────────────────
def generate_app_icons() -> None:
"""Generate Electron app icons: PNG, ICO, ICNS (square bg, Apple HIG)."""
print("Generating app icons from icon-app.svg (square bg, Apple HIG)…")
master = svg_to_image(APP_SVG, SIZE)
# Main PNG
png_path = ASSETS / "icon.png"
master.save(png_path, format="PNG")
print(f" → {png_path}")
# Windows ICO
ico_path = ASSETS / "icon.ico"
master.save(
ico_path, format="ICO",
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
print(f" → {ico_path}")
# macOS ICNS via iconutil
generate_icns(master)
def generate_icns(master: Image.Image) -> None:
"""Generate .icns from a master image using macOS iconutil."""
if ICONSET.exists():
shutil.rmtree(ICONSET)
ICONSET.mkdir(parents=True, exist_ok=True)
for size in (16, 32, 64, 128, 256, 512):
image.resize((size, size), Image.Resampling.LANCZOS).save(
master.resize((size, size), Image.Resampling.LANCZOS).save(
ICONSET / f"icon_{size}x{size}.png", format="PNG",
)
image.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
master.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
ICONSET / f"icon_{size}x{size}@2x.png", format="PNG",
)
icns_path = ASSETS / "icon.icns"
subprocess.run(
["iconutil", "-c", "icns", str(ICONSET), "-o", str(ICNS_PATH)],
["iconutil", "-c", "icns", str(ICONSET), "-o", str(icns_path)],
check=True,
)
shutil.rmtree(ICONSET)
print(f" → {icns_path}")
# ── Tray / notification area icons ───────────────────────────────────
def generate_tray_icons() -> None:
"""Generate tray icons following Apple HIG for menu bar.
- Color (Windows/Linux): square bg with logo, 32×32 and 64×64
- Template (macOS): monochrome black logo on transparent, 22×22 and 44×44
"""
print("Generating tray icons…")
# Color tray icons — render app icon (square bg) at small sizes
for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]:
img = svg_to_image(APP_SVG, size)
img.save(ASSETS / name, format="PNG")
print(f" → {ASSETS / name}")
# macOS template icons — black logo silhouette on transparent
# Read logo.svg paths, render as black on transparent
logo_svg = LOGO_SVG.read_text()
for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]:
# Create SVG with black fill, sized to target
template_svg = _make_template_svg(logo_svg, size)
img = svg_bytes_to_image(template_svg.encode("utf-8"), size)
img.save(ASSETS / name, format="PNG")
print(f" → {ASSETS / name}")
def _make_template_svg(logo_svg_content: str, target_size: int) -> str:
"""Create a macOS template SVG: black logo silhouette on transparent bg."""
# Logo viewBox is 0 0 122 126 — scale to fit target with padding
padding = max(2, int(target_size * 0.1))
available = target_size - 2 * padding
# Scale to fit height (taller dimension)
scale = available / 126
logo_w = 122 * scale
# Center horizontally
offset_x = (target_size - logo_w) / 2
offset_y = padding
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {target_size} {target_size}" fill="none">
<g transform="translate({offset_x:.1f} {offset_y:.1f}) scale({scale:.4f})">
<path fill="#000000" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
<path fill="#000000" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
</g>
</svg>"""
# ── Main ──────────────────────────────────────────────────────────────
def main() -> None:
ASSETS.mkdir(parents=True, exist_ok=True)
image = build_icon()
save_png_and_ico(image)
save_icns(image)
build_tray_icons()
print(f"Generated: {PNG_PATH}, {ICO_PATH}, {ICNS_PATH}")
print(f"Generated tray icons in {ASSETS}")
LANDING.mkdir(parents=True, exist_ok=True)
generate_web_icons()
generate_app_icons()
generate_tray_icons()
print("\nDone! All icons generated.")
if __name__ == "__main__":