Per-project brand marks: one mark per repo (#22) · modern-python/.github@5654935 · GitHub
Skip to content

Commit 5654935

Browse files
lesnik512claude
andauthored
Per-project brand marks: one mark per repo (#22)
* docs: design per-project/family brand marks Spec for a per-repository logo system: one constant green+gold snake-frame with a single gold inner symbol per repo (two-colour, differentiated by shape). Covers all 17 repos across four families, generated in brand/build/ into brand/projects/<repo>/. Records the rejected per-family-colour option as a decision. Large-format only; favicons stay the org mark. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for per-project brand marks Nine TDD tasks: symbol module + helpers, the four families of inner symbols, the parametric project frame, the repo manifest + mark composition, rendering to brand/projects/, name lockups, and docs/architecture promotion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(brand): symbol module scaffold + shared helpers * feat(brand): dependency-injection inner symbols * feat(brand): microservices/messaging inner symbols * feat(brand): utility inner symbols * feat(brand): parametric project snake-frame * feat(brand): repo manifest + project mark composition * feat(brand): render per-project marks to brand/projects/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(brand): per-project horizontal name lockups * style: normalize ruff formatting across brand/build and tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(brand): document per-project marks + promote to architecture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(brand): make modern-di-typer mark path-based (bold >T), font-independent --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bbd7e39 commit 5654935

86 files changed

Lines changed: 1997 additions & 87 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

architecture/brand-marks.md

Lines changed: 17 additions & 0 deletions

brand/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ lockup** pulls them into crop marks framing `MODERN` / `PYTHON` set in **Jost**
3838
| `social-card-green.svg` / `.png` | 1280×640, green alternate |
3939
| `social-square.svg` / `.png`, `social-square-green.*` | 640×640 (Telegram) |
4040

41+
## Per-project marks (`brand/projects/`)
42+
43+
Each repo gets a large-format mark: the constant green+gold snake-frame with
44+
one gold inner symbol (see `brand/build/projects.py::MANIFEST`). Regenerate
45+
with `uv run python -m brand.build.render`; outputs land in
46+
`brand/projects/<repo>/` as `mark.svg`, `lockup.svg` (+ PNGs). These are
47+
large-format only — every repo's favicon/avatar stays the org mark.
48+
4149
## Deferred (not in this kit)
4250

43-
Per-project / per-repo marks, the subfamily system, and any inner glyphs are a
44-
later task. The header nav logo redesign is also a follow-up.
51+
The header nav logo redesign is a follow-up.

brand/build/geometry.py

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,40 @@ def _icon_mark(struct: str, gold: str) -> str:
1919

2020
def icon(*, bg: str, struct: str, gold: str) -> str:
2121
"""Full-bleed square icon — favicon, apple-touch, GitHub avatar."""
22-
return (_SVG_OPEN.format(w=100, h=100)
23-
+ f'<rect width="100" height="100" fill="{bg}"/>'
24-
+ _icon_mark(struct, gold) + "</svg>")
22+
return (
23+
_SVG_OPEN.format(w=100, h=100)
24+
+ f'<rect width="100" height="100" fill="{bg}"/>'
25+
+ _icon_mark(struct, gold)
26+
+ "</svg>"
27+
)
2528

2629

2730
def icon_circle(*, bg: str, struct: str, gold: str, scale: float = 0.74) -> str:
2831
"""Padded variant centered for circular crops (e.g. Telegram). The mark is
2932
scaled about the center so it fits inside the inscribed circle with margin."""
30-
return (_SVG_OPEN.format(w=100, h=100)
31-
+ f'<rect width="100" height="100" fill="{bg}"/>'
32-
+ f'<g transform="translate(50,50) scale({scale}) translate(-50,-50)">{_icon_mark(struct, gold)}</g>'
33-
+ "</svg>")
33+
return (
34+
_SVG_OPEN.format(w=100, h=100)
35+
+ f'<rect width="100" height="100" fill="{bg}"/>'
36+
+ f'<g transform="translate(50,50) scale({scale}) translate(-50,-50)">{_icon_mark(struct, gold)}</g>'
37+
+ "</svg>"
38+
)
3439

3540

3641
def lockup_body(*, struct: str, gold: str) -> str:
3742
"""The MODERN/PYTHON crop-mark lockup, drawn in a 540x250 coordinate space.
3843
Returned as bare markup (no <svg> wrapper, no background) for embedding."""
39-
modern, _ = outline_text("MODERN", 50, x=270, baseline_y=126, anchor="middle",
40-
color=struct, fit_width=210)
41-
python, _ = outline_text("PYTHON", 50, x=270, baseline_y=166, anchor="middle",
42-
color=gold, fit_width=210)
44+
modern, _ = outline_text(
45+
"MODERN",
46+
50,
47+
x=270,
48+
baseline_y=126,
49+
anchor="middle",
50+
color=struct,
51+
fit_width=210,
52+
)
53+
python, _ = outline_text(
54+
"PYTHON", 50, x=270, baseline_y=166, anchor="middle", color=gold, fit_width=210
55+
)
4356
crops = (
4457
'<g fill="none" stroke-width="8" stroke-linecap="butt" stroke-linejoin="miter">'
4558
f'<path d="M138 122 L138 50 L210 50" stroke="{struct}"/>'
@@ -72,8 +85,15 @@ def mark(*, struct: str, gold: str) -> str:
7285

7386
def social_card(*, bg: str, struct: str, gold: str, url_color: str) -> str:
7487
body = lockup_body(struct=struct, gold=gold)
75-
url, _ = outline_text("modern-python.org", 34, x=640, baseline_y=575,
76-
anchor="middle", color=url_color, letter_spacing=4)
88+
url, _ = outline_text(
89+
"modern-python.org",
90+
34,
91+
x=640,
92+
baseline_y=575,
93+
anchor="middle",
94+
color=url_color,
95+
letter_spacing=4,
96+
)
7797
return (
7898
_SVG_OPEN.format(w=1280, h=640)
7999
+ f'<rect width="1280" height="640" fill="{bg}"/>'
@@ -94,3 +114,28 @@ def social_square(*, bg: str, struct: str, gold: str) -> str:
94114
+ f'<g transform="translate({tx},{ty}) scale({s})">{body}</g>'
95115
+ "</svg>"
96116
)
117+
118+
119+
def project_frame(
120+
*,
121+
struct: str,
122+
accent: str,
123+
w: int = 100,
124+
h: int = 100,
125+
m: int = 9,
126+
lx: int = 53,
127+
ly: int = 53,
128+
s: int = 11,
129+
) -> str:
130+
"""Two pinwheeled L-snakes in opposite corners — the constant project frame.
131+
Returns bare markup (no <svg> wrapper)."""
132+
hs = s + 3
133+
parts = [
134+
f'<path d="M{m} {m + ly} L{m} {m} L{m + lx} {m}" fill="none" stroke="{struct}" stroke-width="{s}" stroke-linejoin="miter"/>',
135+
f'<rect x="{m + lx - hs / 2:.1f}" y="{m - hs / 2:.1f}" width="{hs}" height="{hs}" rx="2" fill="{struct}"/>',
136+
f'<polygon points="{m - s / 2:.1f},{m + ly - 2:.1f} {m + s / 2:.1f},{m + ly - 2:.1f} {m + s / 2:.1f},{m + ly:.1f} {m - s / 2:.1f},{m + ly + s:.1f}" fill="{struct}"/>',
137+
f'<path d="M{w - m} {h - m - ly} L{w - m} {h - m} L{w - m - lx} {h - m}" fill="none" stroke="{accent}" stroke-width="{s}" stroke-linejoin="miter"/>',
138+
f'<rect x="{w - m - lx - hs / 2:.1f}" y="{h - m - hs / 2:.1f}" width="{hs}" height="{hs}" rx="2" fill="{accent}"/>',
139+
f'<polygon points="{w - m + s / 2:.1f},{h - m - ly + 2:.1f} {w - m - s / 2:.1f},{h - m - ly + 2:.1f} {w - m - s / 2:.1f},{h - m - ly:.1f} {w - m + s / 2:.1f},{h - m - ly - s:.1f}" fill="{accent}"/>',
140+
]
141+
return "".join(parts)

brand/build/projects.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from collections.abc import Callable
2+
from pathlib import Path
3+
4+
from brand.build import geometry as g
5+
from brand.build import symbols as sym
6+
from brand.build import tokens as t
7+
from brand.build.raster import export_png
8+
from brand.build.text import outline_text
9+
10+
R = 23
11+
_CX = _CY = 50
12+
13+
ALLOWED_COLORS: frozenset[str] = frozenset(
14+
c.lower() for c in (t.GREEN_INK, t.GOLD_LIGHT, t.CREAM, *sym._BAR_TINTS)
15+
)
16+
17+
MANIFEST: dict[str, Callable[[], str]] = {
18+
# dependency injection
19+
"modern-di": lambda: sym.graph(_CX, _CY, R, dashed=True),
20+
"that-depends": lambda: sym.graph(_CX, _CY, R, dashed=False),
21+
"modern-di-fastapi": lambda: sym.bolt_disc(_CX, _CY, R),
22+
"modern-di-litestar": lambda: sym.star_disc(_CX, _CY, R),
23+
"modern-di-faststream": lambda: sym.faststream(_CX, _CY, R),
24+
"modern-di-typer": lambda: sym.terminal(_CX, _CY, R),
25+
"modern-di-pytest": lambda: sym.bars(_CX, _CY, R),
26+
# templates — reuse the org chevron
27+
"fastapi-sqlalchemy-template": lambda: sym.chevron(_CX, _CY, R - 1),
28+
"litestar-sqlalchemy-template": lambda: sym.chevron(_CX, _CY, R - 1),
29+
# microservices, http & messaging
30+
"lite-bootstrap": lambda: sym.rocket(_CX, _CY, R),
31+
"httpware": lambda: sym.chain(_CX, _CY, R),
32+
"faststream-redis-timers": lambda: sym.stopwatch(_CX, _CY, R),
33+
"faststream-concurrent-aiokafka": lambda: sym.lanes(_CX, _CY, R),
34+
"faststream-outbox": lambda: sym.outbox(_CX, _CY, R),
35+
# utilities
36+
"db-retry": lambda: sym.db_retry(_CX, _CY, R),
37+
"eof-fixer": lambda: sym.eof_fixer(_CX, _CY, R),
38+
"semvertag": lambda: sym.tag(_CX, _CY, R),
39+
}
40+
41+
42+
ROOT = Path(__file__).resolve().parents[2]
43+
PROJECTS = ROOT / "brand" / "projects"
44+
_PNG_SIZES = (512, 1024)
45+
46+
_LOCKUP_H = 100
47+
_NAME_SIZE = 34
48+
_GAP = 18
49+
50+
51+
def project_mark(repo: str) -> str:
52+
"""Full <svg> for a repo: constant frame + its gold inner symbol."""
53+
frame = g.project_frame(struct=t.GREEN_INK, accent=t.GOLD_LIGHT)
54+
inner = MANIFEST[repo]()
55+
return (
56+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" '
57+
f'role="img" aria-label="{repo}">{frame}{inner}</svg>'
58+
)
59+
60+
61+
def project_lockup(repo: str) -> str:
62+
"""Framed mark on the left + the repo name in Jost (green) to its right."""
63+
mark_frame = g.project_frame(struct=t.GREEN_INK, accent=t.GOLD_LIGHT)
64+
inner = MANIFEST[repo]()
65+
name_x = _LOCKUP_H + _GAP
66+
name_svg, name_w = outline_text(
67+
repo,
68+
_NAME_SIZE,
69+
x=name_x,
70+
baseline_y=_LOCKUP_H / 2 + _NAME_SIZE * 0.34,
71+
anchor="start",
72+
color=t.GREEN_INK,
73+
)
74+
total_w = round(name_x + name_w + _GAP)
75+
return (
76+
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_w} {_LOCKUP_H}" '
77+
f'role="img" aria-label="{repo}">'
78+
f"<g>{mark_frame}{inner}</g>"
79+
f"{name_svg}</svg>"
80+
)
81+
82+
83+
def render_projects(out_dir: Path | None = None) -> list[Path]:
84+
"""Write mark.svg (+ PNGs) for every repo under out_dir/<repo>/."""
85+
base = out_dir if out_dir is not None else PROJECTS
86+
written: list[Path] = []
87+
for repo in MANIFEST:
88+
d = base / repo
89+
d.mkdir(parents=True, exist_ok=True)
90+
svg = d / "mark.svg"
91+
svg.write_text(project_mark(repo) + "\n", encoding="utf-8")
92+
for sz in _PNG_SIZES:
93+
export_png(svg, d / f"mark-{sz}.png", width=sz, height=sz)
94+
(d / "lockup.svg").write_text(project_lockup(repo) + "\n", encoding="utf-8")
95+
written.append(svg)
96+
return written

brand/build/raster.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import shutil
2+
import subprocess
3+
from pathlib import Path
4+
5+
6+
def export_png(
7+
svg_path: Path,
8+
png_path: Path,
9+
*,
10+
width: int | None = None,
11+
height: int | None = None,
12+
) -> bool:
13+
exe = shutil.which("rsvg-convert")
14+
if exe is None:
15+
return False
16+
args = [exe]
17+
if width is not None:
18+
args += ["-w", str(width)]
19+
if height is not None:
20+
args += ["-h", str(height)]
21+
args += [str(svg_path), "-o", str(png_path)]
22+
subprocess.run(args, check=True)
23+
return True

brand/build/render.py

Lines changed: 56 additions & 37 deletions

0 commit comments

Comments
 (0)