Replace incorrect README icon with actual Atlas app icon, add new screenshots (about, settings, privilege), improve feature view responsiveness and empty states, adjust design system brand tokens and localization strings, add icon generation script. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
8.3 KiB
Python
230 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Atlas for Mac — App Icon Generator
|
|
|
|
Brand: Calm Authority
|
|
Concept: A stylized globe with meridian lines overlaid on a deep-teal-to-emerald
|
|
gradient, with a darker premium backdrop and a refined mint accent arc representing the "atlas" mapping metaphor.
|
|
|
|
Generates all required macOS app icon sizes from a programmatic SVG.
|
|
"""
|
|
|
|
import subprocess
|
|
import os
|
|
import json
|
|
import tempfile
|
|
|
|
ICON_DIR = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"Apps", "AtlasApp", "Sources", "AtlasApp", "Assets.xcassets", "AppIcon.appiconset"
|
|
)
|
|
|
|
# macOS icon sizes needed
|
|
SIZES = [16, 32, 64, 128, 256, 512, 1024]
|
|
|
|
|
|
def generate_svg(size=1024):
|
|
"""Generate the Atlas app icon as SVG."""
|
|
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" width="{size}" height="{size}">
|
|
<defs>
|
|
<!-- Brand gradient: darker premium teal to deep emerald -->
|
|
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0%" stop-color="#031B1A"/>
|
|
<stop offset="50%" stop-color="#0A5C56"/>
|
|
<stop offset="100%" stop-color="#073936"/>
|
|
</linearGradient>
|
|
|
|
<!-- Inner glow from top-left -->
|
|
<radialGradient id="innerGlow" cx="0.3" cy="0.25" r="0.7">
|
|
<stop offset="0%" stop-color="#D1FAE5" stop-opacity="0.16"/>
|
|
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
|
</radialGradient>
|
|
|
|
<!-- Globe gradient -->
|
|
<radialGradient id="globeGrad" cx="0.4" cy="0.35" r="0.55">
|
|
<stop offset="0%" stop-color="#A7F3D0" stop-opacity="0.38"/>
|
|
<stop offset="60%" stop-color="#5EEAD4" stop-opacity="0.22"/>
|
|
<stop offset="100%" stop-color="#0A5C56" stop-opacity="0.10"/>
|
|
</radialGradient>
|
|
|
|
<!-- Mint accent gradient -->
|
|
<linearGradient id="mintGrad" x1="0" y1="0" x2="1" y2="0.5">
|
|
<stop offset="0%" stop-color="#D1FAE5" stop-opacity="0.98"/>
|
|
<stop offset="100%" stop-color="#6EE7B7" stop-opacity="0.82"/>
|
|
</linearGradient>
|
|
|
|
<!-- Clip to rounded square -->
|
|
<clipPath id="roundClip">
|
|
<rect x="0" y="0" width="{size}" height="{size}" rx="{int(size * 0.22)}" ry="{int(size * 0.22)}"/>
|
|
</clipPath>
|
|
</defs>
|
|
|
|
<g clip-path="url(#roundClip)">
|
|
<!-- Background -->
|
|
<rect width="{size}" height="{size}" fill="url(#bgGrad)"/>
|
|
<rect width="{size}" height="{size}" fill="url(#innerGlow)"/>
|
|
|
|
<!-- Globe circle -->
|
|
<circle cx="{size//2}" cy="{size//2}" r="{int(size * 0.32)}"
|
|
fill="url(#globeGrad)" stroke="#CCFBF1" stroke-width="{max(1, size//256)}" stroke-opacity="0.24"/>
|
|
|
|
<!-- Meridian lines (longitude) -->
|
|
<g fill="none" stroke="#CCFBF1" stroke-width="{max(1, size//512)}" stroke-opacity="0.24">
|
|
<!-- Vertical center line -->
|
|
<line x1="{size//2}" y1="{int(size*0.18)}" x2="{size//2}" y2="{int(size*0.82)}"/>
|
|
<!-- Elliptical meridians -->
|
|
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.12)}" ry="{int(size*0.32)}"/>
|
|
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.24)}" ry="{int(size*0.32)}"/>
|
|
</g>
|
|
|
|
<!-- Latitude lines (horizontal) -->
|
|
<g fill="none" stroke="#CCFBF1" stroke-width="{max(1, size//512)}" stroke-opacity="0.18">
|
|
<line x1="{int(size*0.18)}" y1="{size//2}" x2="{int(size*0.82)}" y2="{size//2}"/>
|
|
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.32)}" ry="{int(size*0.12)}"/>
|
|
<ellipse cx="{size//2}" cy="{size//2}" rx="{int(size*0.32)}" ry="{int(size*0.22)}"/>
|
|
</g>
|
|
|
|
<!-- Mint accent arc — the "mapping" highlight -->
|
|
<path d="M {int(size*0.28)} {int(size*0.58)}
|
|
Q {int(size*0.5)} {int(size*0.35)}, {int(size*0.72)} {int(size*0.42)}"
|
|
fill="none" stroke="url(#mintGrad)" stroke-width="{max(2, int(size*0.018))}"
|
|
stroke-linecap="round" stroke-opacity="0.92"/>
|
|
|
|
<!-- Small mint dot at arc start -->
|
|
<circle cx="{int(size*0.28)}" cy="{int(size*0.58)}" r="{max(2, int(size*0.009))}"
|
|
fill="#A7F3D0" opacity="0.95"/>
|
|
|
|
<!-- Small mint dot at arc end -->
|
|
<circle cx="{int(size*0.72)}" cy="{int(size*0.42)}" r="{max(2, int(size*0.009))}"
|
|
fill="#A7F3D0" opacity="0.95"/>
|
|
|
|
<!-- Subtle sparkle at top-right of globe -->
|
|
<g transform="translate({int(size*0.62)}, {int(size*0.28)})" opacity="0.5">
|
|
<line x1="0" y1="-{int(size*0.02)}" x2="0" y2="{int(size*0.02)}"
|
|
stroke="white" stroke-width="{max(1, size//512)}" stroke-linecap="round"/>
|
|
<line x1="-{int(size*0.02)}" y1="0" x2="{int(size*0.02)}" y2="0"
|
|
stroke="white" stroke-width="{max(1, size//512)}" stroke-linecap="round"/>
|
|
</g>
|
|
|
|
<!-- Bottom subtle reflection -->
|
|
<rect x="0" y="{int(size*0.75)}" width="{size}" height="{int(size*0.25)}"
|
|
fill="url(#bgGrad)" opacity="0.3"/>
|
|
</g>
|
|
</svg>'''
|
|
|
|
|
|
def main():
|
|
os.makedirs(ICON_DIR, exist_ok=True)
|
|
|
|
# Write SVG
|
|
svg_content = generate_svg(1024)
|
|
svg_path = os.path.join(ICON_DIR, "icon_1024.svg")
|
|
with open(svg_path, "w") as f:
|
|
f.write(svg_content)
|
|
|
|
print(f"SVG written to {svg_path}")
|
|
|
|
# Try to convert to PNG using sips (built-in macOS tool) via a temp file
|
|
# First, check if we have rsvg-convert or cairosvg
|
|
converters = []
|
|
|
|
# Check for rsvg-convert (from librsvg)
|
|
if subprocess.run(["which", "rsvg-convert"], capture_output=True).returncode == 0:
|
|
converters.append("rsvg-convert")
|
|
|
|
# Check for python cairosvg
|
|
try:
|
|
import cairosvg
|
|
converters.append("cairosvg")
|
|
except ImportError:
|
|
pass
|
|
|
|
# Check for Inkscape
|
|
if subprocess.run(["which", "inkscape"], capture_output=True).returncode == 0:
|
|
converters.append("inkscape")
|
|
|
|
images = {}
|
|
|
|
if "rsvg-convert" in converters:
|
|
print("Using rsvg-convert for PNG generation...")
|
|
for s in SIZES:
|
|
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
|
|
subprocess.run([
|
|
"rsvg-convert", "-w", str(s), "-h", str(s),
|
|
svg_path, "-o", out
|
|
], check=True)
|
|
images[f"icon_{s}x{s}.png"] = s
|
|
print(f" Generated {s}x{s}")
|
|
elif "cairosvg" in converters:
|
|
print("Using cairosvg for PNG generation...")
|
|
import cairosvg
|
|
for s in SIZES:
|
|
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
|
|
cairosvg.svg2png(
|
|
bytestring=svg_content.encode(),
|
|
write_to=out,
|
|
output_width=s,
|
|
output_height=s
|
|
)
|
|
images[f"icon_{s}x{s}.png"] = s
|
|
print(f" Generated {s}x{s}")
|
|
elif "inkscape" in converters:
|
|
print("Using Inkscape for PNG generation...")
|
|
for s in SIZES:
|
|
out = os.path.join(ICON_DIR, f"icon_{s}x{s}.png")
|
|
subprocess.run([
|
|
"inkscape", svg_path,
|
|
"--export-type=png",
|
|
f"--export-filename={out}",
|
|
f"--export-width={s}",
|
|
f"--export-height={s}"
|
|
], check=True, capture_output=True)
|
|
images[f"icon_{s}x{s}.png"] = s
|
|
print(f" Generated {s}x{s}")
|
|
else:
|
|
print("WARNING: No SVG-to-PNG converter found.")
|
|
print("Install one of: librsvg (brew install librsvg), cairosvg (pip install cairosvg), or Inkscape")
|
|
print(f"Then run: cd {ICON_DIR} && rsvg-convert -w 1024 -h 1024 icon_1024.svg -o icon_1024x1024.png")
|
|
print("SVG file is ready for manual conversion.")
|
|
# Still write Contents.json with expected filenames
|
|
for s in SIZES:
|
|
images[f"icon_{s}x{s}.png"] = s
|
|
|
|
# Write Contents.json for Xcode
|
|
icon_images = []
|
|
for s in [16, 32, 128, 256, 512]:
|
|
# 1x
|
|
icon_images.append({
|
|
"filename": f"icon_{s}x{s}.png",
|
|
"idiom": "mac",
|
|
"scale": "1x",
|
|
"size": f"{s}x{s}"
|
|
})
|
|
# 2x
|
|
icon_images.append({
|
|
"filename": f"icon_{s*2}x{s*2}.png",
|
|
"idiom": "mac",
|
|
"scale": "2x",
|
|
"size": f"{s}x{s}"
|
|
})
|
|
|
|
contents = {
|
|
"images": icon_images,
|
|
"info": {
|
|
"author": "atlas-icon-generator",
|
|
"version": 1
|
|
}
|
|
}
|
|
|
|
contents_path = os.path.join(ICON_DIR, "Contents.json")
|
|
with open(contents_path, "w") as f:
|
|
json.dump(contents, f, indent=2)
|
|
|
|
print(f"Contents.json written to {contents_path}")
|
|
print("Done!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|