diff --git a/architecture/brand-marks.md b/architecture/brand-marks.md index 606469f..fd3f88a 100644 --- a/architecture/brand-marks.md +++ b/architecture/brand-marks.md @@ -13,8 +13,9 @@ gold inner symbol (`symbols.py`) chosen per repo in `projects.py::MANIFEST`. Two-colour (green + gold); repos differ by symbol shape, not colour. The two project templates reuse the org chevron. `modern-di-faststream` is the only mark using a partner's literal logo path (FastStream's, recoloured); other -integration cues are redrawn evocations. Outputs: `mark.svg`, `lockup.svg` -(+ `mark-512/1024.png`). Regenerate via `uv run python -m brand.build.render`. +integration cues are redrawn evocations. Outputs: `mark.svg`, +`lockup-light.svg`, `lockup-dark.svg`, `lockup.png` (+ `mark-512/1024.png`). +Regenerate via `uv run python -m brand.build.render`. Repos with a live docs site (`projects.py::DOCS_REPOS`, a subset of `MANIFEST`) additionally get a 1280×640 `social-card.svg|png` — a two-panel og:image (green mark panel + cream name/tagline/url), built with the same frame + @@ -25,3 +26,22 @@ All generated PNGs are palette-quantized in `raster.py` (`_quantize_png`, Pillow FASTOCTREE, `_PNG_COLORS` palette) so the committed binaries are indexed-colour and compact; alpha is preserved for the transparent marks. SVGs are left as generated. + +### Lockup colourways and README banners + +Each repo's `brand/projects//` directory contains three lockup files in +addition to `mark.svg` and `mark-512/1024.png`: + +- `lockup-light.svg` — green-ink text + gold accent on transparent background + (for light UIs). +- `lockup-dark.svg` — cream text + gold-dark (`#f0b528`) accent on transparent + background (for dark UIs). This colourway mirrors `wordmark-dark` and the + on-green mark treatment. +- `lockup.png` — the light lockup rasterized and palette-quantized; serves as + the PyPI `` fallback (PyPI renders on a white background, so the light + lockup is the correct choice). + +Repo READMEs embed these as a centered `` banner that replaces the +leading `# ` heading. The `` elements reference the assets via +absolute `raw.githubusercontent.com/modern-python/.github/main/brand/projects//` +URLs so no asset files are committed to the individual repos. diff --git a/brand/README.md b/brand/README.md index 8b00179..6974d6a 100644 --- a/brand/README.md +++ b/brand/README.md @@ -43,8 +43,22 @@ lockup** pulls them into crop marks framing `MODERN` / `PYTHON` set in **Jost** Each repo gets a large-format mark: the constant green+gold snake-frame with one gold inner symbol (see `brand/build/projects.py::MANIFEST`). Regenerate with `uv run python -m brand.build.render`; outputs land in -`brand/projects//` as `mark.svg`, `lockup.svg` (+ PNGs). These are -large-format only — every repo's favicon/avatar stays the org mark. +`brand/projects//` as `mark.svg`, three lockup files (see below), and +PNGs. These are large-format only — every repo's favicon/avatar stays the org mark. + +### Lockup outputs + +Each repo gets three lockup files: + +| File | Colourway | Use | +|------|-----------|-----| +| `lockup-light.svg` | green-ink + gold on transparent | GitHub light theme | +| `lockup-dark.svg` | cream + gold-dark on transparent | GitHub dark theme | +| `lockup.png` | light lockup rasterized + quantized | PyPI fallback (``) | + +The dark colourway (cream + `#f0b528` gold-dark) mirrors the org `wordmark-dark`. +These are used as README banners across the org via a `` element that +references the assets at their `raw.githubusercontent.com` URL in `.github`. Repos with a docs site also get a `social-card.svg` + `social-card.png` (1280×640 og:image): the repo mark on a green panel beside its name, tagline, and docs URL on cream. The docs-site repos are listed in diff --git a/brand/build/projects.py b/brand/build/projects.py index 2be4955..f01310c 100644 --- a/brand/build/projects.py +++ b/brand/build/projects.py @@ -58,10 +58,16 @@ def project_mark(repo: str) -> str: ) -def project_lockup(repo: str) -> str: - """Framed mark on the left + the repo name in Jost (green) to its right.""" - mark_frame = g.project_frame(struct=t.GREEN_INK, accent=t.GOLD_LIGHT) +def project_lockup(repo: str, *, dark: bool = False) -> str: + """Framed mark + the repo name in Jost. Light = green-ink/gold (light UIs); + dark = cream/gold-dark (dark UIs). Transparent background either way.""" + struct = t.CREAM if dark else t.GREEN_INK + accent = t.GOLD_DARK if dark else t.GOLD_LIGHT + name_color = t.CREAM if dark else t.GREEN_INK + mark_frame = g.project_frame(struct=struct, accent=accent) inner = MANIFEST[repo]() + if dark: + inner = inner.replace(t.GOLD_LIGHT, t.GOLD_DARK) name_x = _LOCKUP_H + _GAP name_svg, name_w = outline_text( repo, @@ -69,19 +75,18 @@ def project_lockup(repo: str) -> str: x=name_x, baseline_y=_LOCKUP_H / 2 + _NAME_SIZE * 0.34, anchor="start", - color=t.GREEN_INK, + color=name_color, ) total_w = round(name_x + name_w + _GAP) return ( f'' - f"{mark_frame}{inner}" - f"{name_svg}" + f'role="img" aria-label="{repo}">{mark_frame}{inner}{name_svg}' ) def render_projects(out_dir: Path | None = None) -> list[Path]: - """Write mark.svg, lockup.svg (+ PNGs) for every repo under out_dir//. + """Write mark.svg, lockup-light.svg, lockup-dark.svg, lockup.png (+ mark PNGs) + for every repo under out_dir//. Docs-site repos (DOCS_REPOS) also get social-card.svg/png (1280×640).""" base = out_dir if out_dir is not None else PROJECTS @@ -93,7 +98,10 @@ def render_projects(out_dir: Path | None = None) -> list[Path]: svg.write_text(project_mark(repo) + "\n", encoding="utf-8") for sz in _PNG_SIZES: export_png(svg, d / f"mark-{sz}.png", width=sz, height=sz) - (d / "lockup.svg").write_text(project_lockup(repo) + "\n", encoding="utf-8") + (d / "lockup-light.svg").write_text(project_lockup(repo) + "\n", encoding="utf-8") + dark_svg = d / "lockup-dark.svg" + dark_svg.write_text(project_lockup(repo, dark=True) + "\n", encoding="utf-8") + export_png(d / "lockup-light.svg", d / "lockup.png") if repo in DOCS_REPOS: card = d / "social-card.svg" card.write_text( diff --git a/brand/projects/db-retry/lockup-dark.svg b/brand/projects/db-retry/lockup-dark.svg new file mode 100644 index 0000000..be22819 --- /dev/null +++ b/brand/projects/db-retry/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/db-retry/lockup.svg b/brand/projects/db-retry/lockup-light.svg similarity index 100% rename from brand/projects/db-retry/lockup.svg rename to brand/projects/db-retry/lockup-light.svg diff --git a/brand/projects/db-retry/lockup.png b/brand/projects/db-retry/lockup.png new file mode 100644 index 0000000..9b04bac Binary files /dev/null and b/brand/projects/db-retry/lockup.png differ diff --git a/brand/projects/eof-fixer/lockup-dark.svg b/brand/projects/eof-fixer/lockup-dark.svg new file mode 100644 index 0000000..34e29da --- /dev/null +++ b/brand/projects/eof-fixer/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/eof-fixer/lockup.svg b/brand/projects/eof-fixer/lockup-light.svg similarity index 100% rename from brand/projects/eof-fixer/lockup.svg rename to brand/projects/eof-fixer/lockup-light.svg diff --git a/brand/projects/eof-fixer/lockup.png b/brand/projects/eof-fixer/lockup.png new file mode 100644 index 0000000..008209b Binary files /dev/null and b/brand/projects/eof-fixer/lockup.png differ diff --git a/brand/projects/fastapi-sqlalchemy-template/lockup-dark.svg b/brand/projects/fastapi-sqlalchemy-template/lockup-dark.svg new file mode 100644 index 0000000..54ccea1 --- /dev/null +++ b/brand/projects/fastapi-sqlalchemy-template/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/fastapi-sqlalchemy-template/lockup.svg b/brand/projects/fastapi-sqlalchemy-template/lockup-light.svg similarity index 100% rename from brand/projects/fastapi-sqlalchemy-template/lockup.svg rename to brand/projects/fastapi-sqlalchemy-template/lockup-light.svg diff --git a/brand/projects/fastapi-sqlalchemy-template/lockup.png b/brand/projects/fastapi-sqlalchemy-template/lockup.png new file mode 100644 index 0000000..8103918 Binary files /dev/null and b/brand/projects/fastapi-sqlalchemy-template/lockup.png differ diff --git a/brand/projects/faststream-concurrent-aiokafka/lockup-dark.svg b/brand/projects/faststream-concurrent-aiokafka/lockup-dark.svg new file mode 100644 index 0000000..4904027 --- /dev/null +++ b/brand/projects/faststream-concurrent-aiokafka/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/faststream-concurrent-aiokafka/lockup.svg b/brand/projects/faststream-concurrent-aiokafka/lockup-light.svg similarity index 100% rename from brand/projects/faststream-concurrent-aiokafka/lockup.svg rename to brand/projects/faststream-concurrent-aiokafka/lockup-light.svg diff --git a/brand/projects/faststream-concurrent-aiokafka/lockup.png b/brand/projects/faststream-concurrent-aiokafka/lockup.png new file mode 100644 index 0000000..fbc590b Binary files /dev/null and b/brand/projects/faststream-concurrent-aiokafka/lockup.png differ diff --git a/brand/projects/faststream-outbox/lockup-dark.svg b/brand/projects/faststream-outbox/lockup-dark.svg new file mode 100644 index 0000000..bf0970b --- /dev/null +++ b/brand/projects/faststream-outbox/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/faststream-outbox/lockup.svg b/brand/projects/faststream-outbox/lockup-light.svg similarity index 100% rename from brand/projects/faststream-outbox/lockup.svg rename to brand/projects/faststream-outbox/lockup-light.svg diff --git a/brand/projects/faststream-outbox/lockup.png b/brand/projects/faststream-outbox/lockup.png new file mode 100644 index 0000000..f3543d0 Binary files /dev/null and b/brand/projects/faststream-outbox/lockup.png differ diff --git a/brand/projects/faststream-redis-timers/lockup-dark.svg b/brand/projects/faststream-redis-timers/lockup-dark.svg new file mode 100644 index 0000000..8e326e3 --- /dev/null +++ b/brand/projects/faststream-redis-timers/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/faststream-redis-timers/lockup.svg b/brand/projects/faststream-redis-timers/lockup-light.svg similarity index 100% rename from brand/projects/faststream-redis-timers/lockup.svg rename to brand/projects/faststream-redis-timers/lockup-light.svg diff --git a/brand/projects/faststream-redis-timers/lockup.png b/brand/projects/faststream-redis-timers/lockup.png new file mode 100644 index 0000000..1a549c2 Binary files /dev/null and b/brand/projects/faststream-redis-timers/lockup.png differ diff --git a/brand/projects/httpware/lockup-dark.svg b/brand/projects/httpware/lockup-dark.svg new file mode 100644 index 0000000..5c827ac --- /dev/null +++ b/brand/projects/httpware/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/httpware/lockup.svg b/brand/projects/httpware/lockup-light.svg similarity index 100% rename from brand/projects/httpware/lockup.svg rename to brand/projects/httpware/lockup-light.svg diff --git a/brand/projects/httpware/lockup.png b/brand/projects/httpware/lockup.png new file mode 100644 index 0000000..276cf56 Binary files /dev/null and b/brand/projects/httpware/lockup.png differ diff --git a/brand/projects/lite-bootstrap/lockup-dark.svg b/brand/projects/lite-bootstrap/lockup-dark.svg new file mode 100644 index 0000000..62ee12f --- /dev/null +++ b/brand/projects/lite-bootstrap/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/lite-bootstrap/lockup.svg b/brand/projects/lite-bootstrap/lockup-light.svg similarity index 100% rename from brand/projects/lite-bootstrap/lockup.svg rename to brand/projects/lite-bootstrap/lockup-light.svg diff --git a/brand/projects/lite-bootstrap/lockup.png b/brand/projects/lite-bootstrap/lockup.png new file mode 100644 index 0000000..e816ab8 Binary files /dev/null and b/brand/projects/lite-bootstrap/lockup.png differ diff --git a/brand/projects/litestar-sqlalchemy-template/lockup-dark.svg b/brand/projects/litestar-sqlalchemy-template/lockup-dark.svg new file mode 100644 index 0000000..76a3381 --- /dev/null +++ b/brand/projects/litestar-sqlalchemy-template/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/litestar-sqlalchemy-template/lockup.svg b/brand/projects/litestar-sqlalchemy-template/lockup-light.svg similarity index 100% rename from brand/projects/litestar-sqlalchemy-template/lockup.svg rename to brand/projects/litestar-sqlalchemy-template/lockup-light.svg diff --git a/brand/projects/litestar-sqlalchemy-template/lockup.png b/brand/projects/litestar-sqlalchemy-template/lockup.png new file mode 100644 index 0000000..88e40be Binary files /dev/null and b/brand/projects/litestar-sqlalchemy-template/lockup.png differ diff --git a/brand/projects/modern-di-fastapi/lockup-dark.svg b/brand/projects/modern-di-fastapi/lockup-dark.svg new file mode 100644 index 0000000..0586a58 --- /dev/null +++ b/brand/projects/modern-di-fastapi/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-fastapi/lockup.svg b/brand/projects/modern-di-fastapi/lockup-light.svg similarity index 100% rename from brand/projects/modern-di-fastapi/lockup.svg rename to brand/projects/modern-di-fastapi/lockup-light.svg diff --git a/brand/projects/modern-di-fastapi/lockup.png b/brand/projects/modern-di-fastapi/lockup.png new file mode 100644 index 0000000..a15fb83 Binary files /dev/null and b/brand/projects/modern-di-fastapi/lockup.png differ diff --git a/brand/projects/modern-di-faststream/lockup-dark.svg b/brand/projects/modern-di-faststream/lockup-dark.svg new file mode 100644 index 0000000..d147929 --- /dev/null +++ b/brand/projects/modern-di-faststream/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-faststream/lockup.svg b/brand/projects/modern-di-faststream/lockup-light.svg similarity index 100% rename from brand/projects/modern-di-faststream/lockup.svg rename to brand/projects/modern-di-faststream/lockup-light.svg diff --git a/brand/projects/modern-di-faststream/lockup.png b/brand/projects/modern-di-faststream/lockup.png new file mode 100644 index 0000000..a7b8ee8 Binary files /dev/null and b/brand/projects/modern-di-faststream/lockup.png differ diff --git a/brand/projects/modern-di-litestar/lockup-dark.svg b/brand/projects/modern-di-litestar/lockup-dark.svg new file mode 100644 index 0000000..6a47b2d --- /dev/null +++ b/brand/projects/modern-di-litestar/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-litestar/lockup.svg b/brand/projects/modern-di-litestar/lockup-light.svg similarity index 100% rename from brand/projects/modern-di-litestar/lockup.svg rename to brand/projects/modern-di-litestar/lockup-light.svg diff --git a/brand/projects/modern-di-litestar/lockup.png b/brand/projects/modern-di-litestar/lockup.png new file mode 100644 index 0000000..93bc9e0 Binary files /dev/null and b/brand/projects/modern-di-litestar/lockup.png differ diff --git a/brand/projects/modern-di-pytest/lockup-dark.svg b/brand/projects/modern-di-pytest/lockup-dark.svg new file mode 100644 index 0000000..a1249cb --- /dev/null +++ b/brand/projects/modern-di-pytest/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-pytest/lockup.svg b/brand/projects/modern-di-pytest/lockup-light.svg similarity index 100% rename from brand/projects/modern-di-pytest/lockup.svg rename to brand/projects/modern-di-pytest/lockup-light.svg diff --git a/brand/projects/modern-di-pytest/lockup.png b/brand/projects/modern-di-pytest/lockup.png new file mode 100644 index 0000000..ae04de7 Binary files /dev/null and b/brand/projects/modern-di-pytest/lockup.png differ diff --git a/brand/projects/modern-di-typer/lockup-dark.svg b/brand/projects/modern-di-typer/lockup-dark.svg new file mode 100644 index 0000000..746ec43 --- /dev/null +++ b/brand/projects/modern-di-typer/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di-typer/lockup.svg b/brand/projects/modern-di-typer/lockup-light.svg similarity index 100% rename from brand/projects/modern-di-typer/lockup.svg rename to brand/projects/modern-di-typer/lockup-light.svg diff --git a/brand/projects/modern-di-typer/lockup.png b/brand/projects/modern-di-typer/lockup.png new file mode 100644 index 0000000..1c43b00 Binary files /dev/null and b/brand/projects/modern-di-typer/lockup.png differ diff --git a/brand/projects/modern-di/lockup-dark.svg b/brand/projects/modern-di/lockup-dark.svg new file mode 100644 index 0000000..07f4775 --- /dev/null +++ b/brand/projects/modern-di/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/modern-di/lockup.svg b/brand/projects/modern-di/lockup-light.svg similarity index 100% rename from brand/projects/modern-di/lockup.svg rename to brand/projects/modern-di/lockup-light.svg diff --git a/brand/projects/modern-di/lockup.png b/brand/projects/modern-di/lockup.png new file mode 100644 index 0000000..d7dbf03 Binary files /dev/null and b/brand/projects/modern-di/lockup.png differ diff --git a/brand/projects/semvertag/lockup-dark.svg b/brand/projects/semvertag/lockup-dark.svg new file mode 100644 index 0000000..60e9477 --- /dev/null +++ b/brand/projects/semvertag/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/semvertag/lockup.svg b/brand/projects/semvertag/lockup-light.svg similarity index 100% rename from brand/projects/semvertag/lockup.svg rename to brand/projects/semvertag/lockup-light.svg diff --git a/brand/projects/semvertag/lockup.png b/brand/projects/semvertag/lockup.png new file mode 100644 index 0000000..0963f8e Binary files /dev/null and b/brand/projects/semvertag/lockup.png differ diff --git a/brand/projects/that-depends/lockup-dark.svg b/brand/projects/that-depends/lockup-dark.svg new file mode 100644 index 0000000..9f56b80 --- /dev/null +++ b/brand/projects/that-depends/lockup-dark.svg @@ -0,0 +1 @@ + diff --git a/brand/projects/that-depends/lockup.svg b/brand/projects/that-depends/lockup-light.svg similarity index 100% rename from brand/projects/that-depends/lockup.svg rename to brand/projects/that-depends/lockup-light.svg diff --git a/brand/projects/that-depends/lockup.png b/brand/projects/that-depends/lockup.png new file mode 100644 index 0000000..45aacc4 Binary files /dev/null and b/brand/projects/that-depends/lockup.png differ diff --git a/planning/changes/2026-06-30.04-readme-logos/design.md b/planning/changes/2026-06-30.04-readme-logos/design.md new file mode 100644 index 0000000..0f39030 --- /dev/null +++ b/planning/changes/2026-06-30.04-readme-logos/design.md @@ -0,0 +1,159 @@ +--- +summary: README logos shipped — light/dark/png lockups generated in .github; centered banner replaces the H1 in all 17 repo READMEs. +--- + +# Design: README logos + +## Summary + +Give every org repo (all 17) a centered brand banner at the top of its README: +the project's framed lockup (mark + name), theme-aware on GitHub (light/dark) and +rendering on PyPI. Two parts: **(1)** extend `brand/build` to emit three lockup +assets per repo — `lockup-light.svg`, `lockup-dark.svg`, `lockup.png` — committed +in `.github` under `brand/projects//`; **(2)** a 17-repo rollout that +replaces each README's leading `# ` heading with a `` banner +pointing at those assets via absolute `raw.githubusercontent.com` URLs. + +## Motivation + +The brand kit shipped marks, social cards, and og:image, but the most-seen +surface — each repo's README (which, for every published package, is also the +PyPI `long_description`) — still opens with a bare `# `. A logo banner +completes the rollout and gives the family a consistent first impression on both +GitHub and PyPI. + +## Background: dual-surface rendering (researched) + +- **GitHub** renders absolute `raw.githubusercontent.com/*.svg` URLs (served + `image/svg+xml`) and supports light/dark via `` + + `prefers-color-scheme`. SVGs are sandboxed (no JS/external refs) — fine for + static logos. +- **PyPI** (`readme_renderer`) strips inline `` and ``/`srcset`, so a + `` collapses to its `` fallback, which must be a **PNG at an + absolute https URL** (relative paths don't resolve on PyPI). `

`, + ``, `width`, `alt` are allowed. +- All sampled package repos set `readme = "README.md"`, so the banner must satisfy + both surfaces. The ``+PNG-fallback pattern does. + +## Non-goals + +- The org **profile README** (`profile/README.md`) — it's the org landing page, + not a per-repo banner target. +- A tagline under the logo (decided: replace the H1 with just the centered logo). +- Per-repo committed assets — assets live centrally in `.github` (decided), + referenced by raw URL. No asset files added to the 17 repos. +- Any change to marks, social cards, or og:image. + +## Design + +### 1. Lockup assets in `brand/build` (`.github`) + +`project_lockup` currently produces one light lockup. Generalize it to a colourway +and emit a dark sibling: + +```python +def project_lockup(repo: str, *, dark: bool = False) -> str: + """Framed mark + the repo name in Jost. Light = green-ink/gold on transparent + (for light UIs); dark = cream/gold-dark on transparent (for dark UIs).""" + struct = t.CREAM if dark else t.GREEN_INK + accent = t.GOLD_DARK if dark else t.GOLD_LIGHT + name_color = t.CREAM if dark else t.GREEN_INK + mark_frame = g.project_frame(struct=struct, accent=accent) + inner = MANIFEST[repo]() + if dark: + inner = inner.replace(t.GOLD_LIGHT, t.GOLD_DARK) # inner gold reads on dark; cream negatives stay + name_x = _LOCKUP_H + _GAP + name_svg, name_w = outline_text( + repo, _NAME_SIZE, x=name_x, baseline_y=_LOCKUP_H / 2 + _NAME_SIZE * 0.34, + anchor="start", color=name_color, + ) + total_w = round(name_x + name_w + _GAP) + return ( + f'{mark_frame}{inner}{name_svg}' + ) +``` + +The dark colourway mirrors the org `wordmark-dark` and the on-green mark +colourway. The `inner.replace(GOLD_LIGHT, GOLD_DARK)` recolor is reliable because +the symbols emit exact token hex strings; `CREAM` negative-space stays cream +(visible on dark). (The `pytest` bar-tint literals aren't swapped — they're gold +shades that read fine on dark.) + +`render_projects` writes, per repo, in place of today's single `lockup.svg`: +`lockup-light.svg` (= `project_lockup(repo)`), `lockup-dark.svg` +(= `project_lockup(repo, dark=True)`), and `lockup.png` — the **light** lockup +rasterized via the existing `export_png` (so it's auto-quantized; PyPI bg is +white, so the light lockup is the right fallback). + +### 2. README banner (each of the 17 repos) + +Replace the leading `# ` line with: + +```html +

+ + + + <repo> + +

+``` + +Everything else in each README (the badges row, all content) is unchanged. The +`` placeholders are the repo name; `width="420"` is tunable. + +### 3. Rollout & sequencing + +1. **`.github` PR** — generate + commit the three lockups per repo. Must merge to + `main` **first**, so the `…/main/…` raw URLs resolve. +2. **17 README PRs** — one per repo, README-only (no asset copies), replacing the + H1 with the banner. Independent of each other; open after step 1 merges. + +The 17 repos: modern-di, that-depends, modern-di-fastapi, modern-di-litestar, +modern-di-faststream, modern-di-typer, modern-di-pytest, fastapi-sqlalchemy-template, +litestar-sqlalchemy-template, lite-bootstrap, httpware, faststream-redis-timers, +faststream-concurrent-aiokafka, faststream-outbox, db-retry, eof-fixer, semvertag. +(`fastapi-sqlalchemy-template` uses `readme.md`, lowercase — handle per repo.) + +## Operations + +After step 1 merges, the assets are live at +`https://raw.githubusercontent.com/modern-python/.github/main/brand/projects//lockup-*.{svg,png}`. +Each README PR renders its banner once both that PR and step 1 are on `main`. + +## Out of scope + +- profile README; taglines; per-repo asset hosting; non-README brand assets. + +## Testing + +**`.github` (`brand/build`):** render to a tmp dir and assert, for a sample repo, +that `lockup-light.svg`, `lockup-dark.svg`, `lockup.png` are written; the dark SVG +contains `CREAM`/`GOLD_DARK` and **not** `GREEN_INK`; the light SVG contains +`GREEN_INK`; both parse as XML; `lockup.png` opens mode `"P"` (quantized). Full +`pytest` + `just check-planning` green; `render` clean and deterministic (no PNG +churn). + +**Per repo:** the README's first non-blank line is the `

` +banner; there is no leading `# ` heading; the three `srcset`/`src` URLs name +that repo and the correct asset filenames; the package still builds where +applicable (`uvx twine check` on a built sdist/wheel, or the repo's CI). + +## Risk + +- **Raw-URL availability / path stability.** READMEs depend on the `.github` + `main` raw path. Likelihood low (path is stable; `.github` is canonical), impact + medium (broken image if moved). *Mitigation:* keep `brand/projects//` + paths stable; they're already the established layout. +- **Dark recolor correctness.** The `GOLD_LIGHT→GOLD_DARK` swap + cream name could + in principle misrender a symbol on dark. Likelihood low (verified colourway + matches the marks' on-green treatment), impact low. *Mitigation:* the test + asserts the dark SVG's palette; spot-render one dark lockup during the `.github` + task. +- **PyPI sanitization variance.** If a renderer drops `` AND the ``, + no logo shows. Likelihood low (the ``+`

` are in readme_renderer's + allowlist). *Mitigation:* `twine check` in the per-repo verification. +- **Heading removal churn.** The exact `# ` line varies slightly per repo + (and one uses `readme.md`). *Mitigation:* the rollout reads each README first + and replaces only the leading H1. diff --git a/planning/changes/2026-06-30.04-readme-logos/plan.md b/planning/changes/2026-06-30.04-readme-logos/plan.md new file mode 100644 index 0000000..65c5f9d --- /dev/null +++ b/planning/changes/2026-06-30.04-readme-logos/plan.md @@ -0,0 +1,277 @@ +# README logos — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps +> use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** A centered, theme-aware brand banner atop all 17 repo READMEs — +generated as light/dark/PNG lockups in `.github` and embedded via ``. + +**Spec:** [`design.md`](./design.md) + +**Branch (this `.github` bundle + assets):** `brand-readme-logos`. **Per docs/repo branch:** `readme-logo`. + +**Commit strategy:** Tasks 1–2 = the `.github` PR; Tasks 3+ = one PR per repo. + +## Global constraints + +- **Two phases, ordered:** Phase 1 = the `.github` PR (lockup assets + docs; + Tasks 1–2). It must be **merged to `main` first** so the + `raw.githubusercontent.com/.../main/...` URLs resolve. Phase 2 = the 17 + README PRs (Task 3 pilot + Task 4 the rest), opened after Phase 1 is on `main`. +- **Assets are central** in `.github` `brand/projects//`; the 17 repo PRs + are **README-only** (no asset copies). +- **Lockup colourways:** light = `struct=GREEN_INK`, `accent=GOLD_LIGHT`, name + `GREEN_INK`; dark = `struct=CREAM`, `accent=GOLD_DARK`, name `CREAM`, inner gold + `GOLD_LIGHT`→`GOLD_DARK` (cream negatives stay). Tokens: `GREEN_INK #356852`, + `GOLD_LIGHT #c98a00`, `GOLD_DARK #f0b528`, `CREAM #f4f1e8`. +- **Per-repo assets:** `lockup-light.svg`, `lockup-dark.svg`, `lockup.png` (the + light lockup rasterized via `export_png`, hence auto-quantized). +- **The 17 repos:** modern-di, that-depends, modern-di-fastapi, modern-di-litestar, + modern-di-faststream, modern-di-typer, modern-di-pytest, + fastapi-sqlalchemy-template, litestar-sqlalchemy-template, lite-bootstrap, + httpware, faststream-redis-timers, faststream-concurrent-aiokafka, + faststream-outbox, db-retry, eof-fixer, semvertag. +- **The README banner** (exact; substitute `` — its README filename is + `README.md` except `fastapi-sqlalchemy-template`, which uses `readme.md`): + + ```html +

+ + + + <repo> + +

+ ``` + +- **Per-repo README procedure** (Task 3 and each repo in Task 4): + 1. `gh repo clone modern-python/ /Users/kevinsmith/src/pypi/.readme-clones/` (mkdir the parent once; `git -C … pull --ff-only` if it exists). + 2. `cd` in; `git checkout -b readme-logo`. + 3. In the README (`README.md`, or `readme.md` for the template), **replace the + leading H1** — the first line matching `^# \s*$` — with the banner + block above (repo substituted). If there is no leading `# ` line, + insert the banner as the first line instead (and report it). Leave the badge + row and all other content untouched. + 4. Verify (below). + 5. `git add -A && git commit -m "docs: add brand logo banner to README"`; + `git push -u origin readme-logo`; `gh pr create --base main` (body: one line + + the 🤖 line). + 6. `gh pr checks ` to completion. + +- **Per-repo verification:** (a) the README's first non-blank line is + `

` and the file no longer contains a leading `# ` + heading; (b) all three URLs contain `/brand/projects//` and the filenames + `lockup-dark.svg` / `lockup-light.svg` / `lockup.png`; (c) if the repo builds a + distribution, `uv build` then `uvx twine check dist/*` reports PASS (this + validates the long_description renders for PyPI); if it isn't a package (the two + templates), skip twine and just confirm (a)+(b). +- Imports at module level; annotate args; `# ty: ignore` not `# type: ignore`. + CI gate is `just` (= check-planning + pytest); ruff not a gate. + +--- + +### Task 1: Light/dark/PNG lockups in `brand/build` + +**Files:** +- Modify: `brand/build/projects.py` +- Test: `tests/test_lockups.py` (new) +- Regenerate: `brand/projects/**/lockup-*.svg`, `brand/projects/**/lockup.png` + +**Interfaces:** +- Produces: `project_lockup(repo: str, *, dark: bool = False) -> str`; + `render_projects` writes `lockup-light.svg`, `lockup-dark.svg`, `lockup.png` per + repo (replacing the old single `lockup.svg`). + +- [ ] **Step 1: Write the failing test** + + ```python + # tests/test_lockups.py + from pathlib import Path + from xml.dom import minidom + + from PIL import Image + + from brand.build import projects as p + from brand.build import tokens as t + + + def test_light_and_dark_lockup_colourways() -> None: + light = p.project_lockup("modern-di") + dark = p.project_lockup("modern-di", dark=True) + minidom.parseString(light) + minidom.parseString(dark) + assert t.GREEN_INK in light and t.GOLD_LIGHT in light + # dark uses the on-dark colourway: cream + gold-dark, never the dark-green ink + assert t.CREAM in dark and t.GOLD_DARK in dark + assert t.GREEN_INK not in dark + + + def test_render_writes_three_lockup_assets(tmp_path: Path) -> None: + p.render_projects(out_dir=tmp_path) + d = tmp_path / "modern-di" + assert (d / "lockup-light.svg").is_file() + assert (d / "lockup-dark.svg").is_file() + png = d / "lockup.png" + assert png.is_file() + assert Image.open(png).mode == "P" # quantized via export_png + ``` + +- [ ] **Step 2: Run to verify it fails** + + Run: `uv run pytest tests/test_lockups.py -q` + Expected: FAIL — `project_lockup` has no `dark` kwarg / `lockup-light.svg` not written. + +- [ ] **Step 3: Generalize `project_lockup` + emit three assets** + + Replace `project_lockup` in `brand/build/projects.py` with: + + ```python + def project_lockup(repo: str, *, dark: bool = False) -> str: + """Framed mark + the repo name in Jost. Light = green-ink/gold (light UIs); + dark = cream/gold-dark (dark UIs). Transparent background either way.""" + struct = t.CREAM if dark else t.GREEN_INK + accent = t.GOLD_DARK if dark else t.GOLD_LIGHT + name_color = t.CREAM if dark else t.GREEN_INK + mark_frame = g.project_frame(struct=struct, accent=accent) + inner = MANIFEST[repo]() + if dark: + inner = inner.replace(t.GOLD_LIGHT, t.GOLD_DARK) + name_x = _LOCKUP_H + _GAP + name_svg, name_w = outline_text( + repo, + _NAME_SIZE, + x=name_x, + baseline_y=_LOCKUP_H / 2 + _NAME_SIZE * 0.34, + anchor="start", + color=name_color, + ) + total_w = round(name_x + name_w + _GAP) + return ( + f'{mark_frame}{inner}{name_svg}' + ) + ``` + + In `render_projects`, replace the single `lockup.svg` write line with: + + ```python + (d / "lockup-light.svg").write_text(project_lockup(repo) + "\n", encoding="utf-8") + dark_svg = d / "lockup-dark.svg" + dark_svg.write_text(project_lockup(repo, dark=True) + "\n", encoding="utf-8") + export_png(d / "lockup-light.svg", d / "lockup.png") + ``` + + (`export_png(svg, png)` with no width/height rasterizes at the SVG's natural size + and quantizes; that's the light PNG fallback.) Update the `render_projects` + docstring's "lockup.svg" mention to the three new names. + +- [ ] **Step 4: Run to verify it passes** + + Run: `uv run pytest tests/test_lockups.py -q` + Expected: PASS (2 tests). + +- [ ] **Step 5: Regenerate + sanity-render a dark lockup, then commit** + + ```bash + uv run python -m brand.build.render + rsvg-convert -w 480 brand/projects/modern-di/lockup-dark.svg -o /tmp/lockup-dark.png # eyeball: cream+gold-dark on transparent + git add brand/build/projects.py tests/test_lockups.py brand/projects + git status --short | grep -E "lockup\.svg" && echo "NOTE: old lockup.svg files are now untracked/removed — git rm them" || true + git rm brand/projects/*/lockup.svg 2>/dev/null || true + git add brand/projects + git commit -m "feat(brand): light/dark/png lockups for README banners" + ``` + The `git rm` removes the superseded single `lockup.svg` per repo (render no + longer writes it). Confirm `git status` is clean after. + +--- + +### Task 2: `.github` docs + finalize bundle + +**Files:** +- Modify: `brand/README.md`, `architecture/brand-marks.md` +- Modify: `planning/changes/2026-06-30.04-readme-logos/design.md` (summary) + +- [ ] **Step 1: `brand/README.md`** — note the lockup outputs changed to + `lockup-light.svg` / `lockup-dark.svg` / `lockup.png` (light + dark colourways + + PyPI PNG fallback), used as README banners across the org. + +- [ ] **Step 2: `architecture/brand-marks.md`** — append: each repo has + `lockup-{light,dark}.svg` + `lockup.png`; the dark colourway is cream + gold-dark + (mirrors `wordmark-dark`); READMEs embed them via `` from the `.github` + raw path. + +- [ ] **Step 3: Finalize the bundle summary** in `design.md`, e.g.: + `summary: README logos shipped — light/dark/png lockups generated in .github; centered banner replaces the H1 in all 17 repo READMEs.` + +- [ ] **Step 4: Verify + commit** + + ```bash + uv run pytest -q # all green + just check-planning # planning: OK + uv run python -m brand.build.render # clean; git status shows no asset churn + git add brand/README.md architecture/brand-marks.md planning/changes/2026-06-30.04-readme-logos/design.md + git commit -m "docs(brand): document README lockups" + ``` + Then push `brand-readme-logos` and open the `.github` PR (Tasks 1–2). **This PR + must merge before Phase 2.** + +--- + +### Task 3: README banner — pilot (`modern-di`) + +> Phase 2. Do this only after the Task 1–2 `.github` PR is merged to `main` +> (otherwise the raw URLs 404). Pilot to confirm the H1 replacement + render. + +**Repo:** `modern-python/modern-di` (`README.md`, leading line `# modern-di`). + +- [ ] **Step 1: Clone + branch** — per the Global per-repo procedure, into `…/.readme-clones/modern-di`, branch `readme-logo`. +- [ ] **Step 2: Replace the leading H1** — swap the first `# modern-di` line for the banner block (Global Constraints), ``=`modern-di`. Confirm the badge row immediately below is untouched. +- [ ] **Step 3: Verify** — first non-blank line is `

`; no `# modern-di` heading remains; the 3 URLs contain `/brand/projects/modern-di/`; `uv build` then `uvx twine check dist/*` → PASS. +- [ ] **Step 4: Commit + push + PR** — `docs: add brand logo banner to README`; `gh pr create --base main`, body names the banner + 🤖 line. +- [ ] **Step 5: Watch CI** — `gh pr checks ` to completion. + +--- + +### Task 4: README banner — remaining 16 repos + +> Phase 2, after the `.github` PR is merged. Apply the **Global per-repo +> procedure** to each repo below; each is its own `readme-logo` branch + PR + +> verification (treat each as an independent unit). README filename is `README.md` +> except `fastapi-sqlalchemy-template` (`readme.md`). For each, verify the 3 URLs +> contain `/brand/projects//`, the leading `# ` H1 is replaced, and +> (packages only) `uvx twine check dist/*` passes after `uv build`. + +- [ ] `that-depends` +- [ ] `modern-di-fastapi` +- [ ] `modern-di-litestar` +- [ ] `modern-di-faststream` +- [ ] `modern-di-typer` +- [ ] `modern-di-pytest` +- [ ] `fastapi-sqlalchemy-template` *(readme.md; not a PyPI package — skip twine, confirm URLs + banner placement; its leading heading may differ, replace the first H1 or insert at top and report)* +- [ ] `litestar-sqlalchemy-template` *(not a PyPI package — skip twine)* +- [ ] `lite-bootstrap` +- [ ] `httpware` +- [ ] `faststream-redis-timers` +- [ ] `faststream-concurrent-aiokafka` +- [ ] `faststream-outbox` +- [ ] `db-retry` +- [ ] `eof-fixer` +- [ ] `semvertag` + +--- + +## Notes for the executor + +- **Hard gate:** open no Phase-2 README PR until the Task 1–2 `.github` PR is on + `main` — the banner URLs resolve only then. +- The 17 README edits are independent and mechanically identical (replace the + leading H1 with the banner, substitute ``); they can be dispatched in + parallel, each its own PR and review. +- Work in `/Users/kevinsmith/src/pypi/.readme-clones/`, never in `.github`. +- Do not touch anything but the leading H1 in each README. +- Some packages may need build deps for `uv build`; if a repo can't build a dist, + fall back to confirming the banner block + URLs and rely on the repo's CI / + `twine check` in its release flow — and say so in the report. diff --git a/tests/test_lockups.py b/tests/test_lockups.py new file mode 100644 index 0000000..b6f06b2 --- /dev/null +++ b/tests/test_lockups.py @@ -0,0 +1,28 @@ +from pathlib import Path +from xml.dom import minidom + +from PIL import Image + +from brand.build import projects as p +from brand.build import tokens as t + + +def test_light_and_dark_lockup_colourways() -> None: + light = p.project_lockup("modern-di") + dark = p.project_lockup("modern-di", dark=True) + minidom.parseString(light) + minidom.parseString(dark) + assert t.GREEN_INK in light and t.GOLD_LIGHT in light + # dark uses the on-dark colourway: cream + gold-dark, never the dark-green ink + assert t.CREAM in dark and t.GOLD_DARK in dark + assert t.GREEN_INK not in dark + + +def test_render_writes_three_lockup_assets(tmp_path: Path) -> None: + p.render_projects(out_dir=tmp_path) + d = tmp_path / "modern-di" + assert (d / "lockup-light.svg").is_file() + assert (d / "lockup-dark.svg").is_file() + png = d / "lockup.png" + assert png.is_file() + assert Image.open(png).mode == "P" # quantized via export_png diff --git a/tests/test_projects.py b/tests/test_projects.py index 10f8972..5651627 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -78,7 +78,10 @@ def test_lockup_is_valid_and_names_repo(repo: str) -> None: def test_render_projects_writes_lockup(tmp_path: Path) -> None: p.render_projects(out_dir=tmp_path) - assert (tmp_path / "modern-di" / "lockup.svg").is_file() + repo_dir = tmp_path / "modern-di" + assert (repo_dir / "lockup-light.svg").is_file() + assert (repo_dir / "lockup-dark.svg").is_file() + assert (repo_dir / "lockup.png").is_file() def test_fit_text_shrinks_only_when_needed() -> None: