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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Brand marks
2+
3+
The org's logo assets, generated by `brand/build/` (no frontmatter; living prose).
4+
5+
## Org marks (`brand/org/`)
6+
Favicon, avatar, social cards — the interlocked-snakes pinwheel with a chevron.
7+
Used everywhere small (favicons, avatars). See `site-branding.md` for site wiring.
8+
9+
## Per-project marks (`brand/projects/<repo>/`)
10+
One large-format logo per repo: the constant green+gold snake-frame
11+
(`geometry.py::project_frame`, margin 9 / arm 53 / stroke 11) with a single
12+
gold inner symbol (`symbols.py`) chosen per repo in `projects.py::MANIFEST`.
13+
Two-colour (green + gold); repos differ by symbol shape, not colour. The two
14+
project templates reuse the org chevron. `modern-di-faststream` is the only
15+
mark using a partner's literal logo path (FastStream's, recoloured); other
16+
integration cues are redrawn evocations. Outputs: `mark.svg`, `lockup.svg`
17+
(+ `mark-512/1024.png`). Regenerate via `uv run python -m brand.build.render`.

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
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import shutil
2-
import subprocess
31
from pathlib import Path
42

53
from brand.build import geometry as g
64
from brand.build import tokens as t
5+
from brand.build.projects import render_projects
6+
from brand.build.raster import export_png
77

88
ROOT = Path(__file__).resolve().parents[2]
99
ORG = ROOT / "brand" / "org"
@@ -14,26 +14,6 @@ def _write(path: Path, content: str) -> None:
1414
path.write_text(content + "\n", encoding="utf-8")
1515

1616

17-
def export_png(
18-
svg_path: Path,
19-
png_path: Path,
20-
*,
21-
width: int | None = None,
22-
height: int | None = None,
23-
) -> bool:
24-
exe = shutil.which("rsvg-convert")
25-
if exe is None:
26-
return False
27-
args = [exe]
28-
if width is not None:
29-
args += ["-w", str(width)]
30-
if height is not None:
31-
args += ["-h", str(height)]
32-
args += [str(svg_path), "-o", str(png_path)]
33-
subprocess.run(args, check=True)
34-
return True
35-
36-
3717
def render() -> None:
3818
ORG.mkdir(parents=True, exist_ok=True)
3919
ic = dict(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK)
@@ -43,13 +23,25 @@ def render() -> None:
4323
export_png(ORG / "favicon.svg", ORG / f"favicon-{sz}.png", width=sz, height=sz)
4424
# apple-touch (same mark, already full-bleed/square)
4525
_write(ORG / "apple-touch-icon.svg", g.icon(**ic))
46-
export_png(ORG / "apple-touch-icon.svg", ORG / "apple-touch-icon-180.png", width=180, height=180)
26+
export_png(
27+
ORG / "apple-touch-icon.svg",
28+
ORG / "apple-touch-icon-180.png",
29+
width=180,
30+
height=180,
31+
)
4732
# avatar (same mark, large raster)
4833
_write(ORG / "avatar.svg", g.icon(**ic))
4934
export_png(ORG / "avatar.svg", ORG / "avatar-1024.png", width=1024, height=1024)
50-
_write(ORG / "avatar-circle.svg",
51-
g.icon_circle(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK))
52-
export_png(ORG / "avatar-circle.svg", ORG / "avatar-circle-1024.png", width=1024, height=1024)
35+
_write(
36+
ORG / "avatar-circle.svg",
37+
g.icon_circle(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK),
38+
)
39+
export_png(
40+
ORG / "avatar-circle.svg",
41+
ORG / "avatar-circle-1024.png",
42+
width=1024,
43+
height=1024,
44+
)
5345

5446
# Site logos — transparent, no background.
5547
# wordmark (hero): two-color lockup, light + dark variants
@@ -59,20 +51,47 @@ def render() -> None:
5951
_write(ORG / "mark.svg", g.mark(struct=t.CREAM, gold=t.GOLD_DARK))
6052

6153
# Social cards — cream (primary) + green (alternate).
62-
_write(ORG / "social-card.svg",
63-
g.social_card(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT, url_color=t.GOLD_LIGHT))
54+
_write(
55+
ORG / "social-card.svg",
56+
g.social_card(
57+
bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT, url_color=t.GOLD_LIGHT
58+
),
59+
)
6460
export_png(ORG / "social-card.svg", ORG / "social-card.png", width=1280, height=640)
65-
_write(ORG / "social-card-green.svg",
66-
g.social_card(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK, url_color=t.GOLD_DARK))
67-
export_png(ORG / "social-card-green.svg", ORG / "social-card-green.png", width=1280, height=640)
61+
_write(
62+
ORG / "social-card-green.svg",
63+
g.social_card(
64+
bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK, url_color=t.GOLD_DARK
65+
),
66+
)
67+
export_png(
68+
ORG / "social-card-green.svg",
69+
ORG / "social-card-green.png",
70+
width=1280,
71+
height=640,
72+
)
6873

6974
# Square (Telegram / square social) — cream + green.
70-
_write(ORG / "social-square.svg",
71-
g.social_square(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT))
72-
export_png(ORG / "social-square.svg", ORG / "social-square.png", width=640, height=640)
73-
_write(ORG / "social-square-green.svg",
74-
g.social_square(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK))
75-
export_png(ORG / "social-square-green.svg", ORG / "social-square-green.png", width=640, height=640)
75+
_write(
76+
ORG / "social-square.svg",
77+
g.social_square(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT),
78+
)
79+
export_png(
80+
ORG / "social-square.svg", ORG / "social-square.png", width=640, height=640
81+
)
82+
_write(
83+
ORG / "social-square-green.svg",
84+
g.social_square(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK),
85+
)
86+
export_png(
87+
ORG / "social-square-green.svg",
88+
ORG / "social-square-green.png",
89+
width=640,
90+
height=640,
91+
)
92+
93+
# Per-project marks (brand/projects/<repo>/).
94+
render_projects()
7695

7796

7897
def main() -> None:

0 commit comments

Comments
 (0)