From fbad6ef3c403a18ec4bbe110e772a31f39508cb2 Mon Sep 17 00:00:00 2001 From: abrown38 Date: Wed, 14 Oct 2026 12:28:20 +0100 Subject: [PATCH] README banner: the logo above the STEWARD wordmark scripts/banner.py builds artwork/banner.svg from the web UI's logo and wordmark, as Jarl's does; scripts/logo.py runs it. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- artwork/banner.svg | 6 ++++ docs/development.md | 10 ++++-- scripts/banner.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ scripts/logo.py | 7 +++-- 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 artwork/banner.svg create mode 100755 scripts/banner.py diff --git a/README.md b/README.md index e57bb62..c652804 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Steward

+

Steward: a key whose bow is a crown with network nodes, above the STEWARD wordmark

# Steward diff --git a/artwork/banner.svg b/artwork/banner.svg new file mode 100644 index 0000000..d91fbf3 --- /dev/null +++ b/artwork/banner.svg @@ -0,0 +1,6 @@ + + +Steward + +DHCP + diff --git a/docs/development.md b/docs/development.md index 6caf9b4..f4d797f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -24,7 +24,8 @@ scripts/e2e.sh end-to-end test with real DHCP clients in Docker scripts/mkdeb.sh the Debian packages (make deb) scripts/logo.py the web UI's logo from artwork/logo.svg scripts/optionsdoc.py docs/options.md from the option catalog -artwork/logo.svg the logo's master +scripts/banner.py the README's banner from the web UI's logo and wordmark +artwork/logo.svg the logo's master; artwork/banner.svg the README's banner ``` [design.md](design.md) describes how the pieces fit. @@ -83,8 +84,11 @@ same helpers as Jarl's: The logo's master is `artwork/logo.svg`; `scripts/logo.py` writes `assets/logo.svg` (the favicon) and `assets/logo.js` (drawn inline by -`brand.js`). The STEWARD wordmark is drawn by `wordmark()` in `brand.js`, -in the letters of Jarl's wordmark. +`brand.js`), and then runs `scripts/banner.py`. The STEWARD wordmark is +drawn by `wordmark()` in `brand.js`, in the letters of Jarl's wordmark. +`artwork/banner.svg`, the README's banner, is the login page's logo and +wordmark, generated by `scripts/banner.py`. It draws the rule with the same +numbers as `brand.js`, so change both together. ## Conventions diff --git a/scripts/banner.py b/scripts/banner.py new file mode 100755 index 0000000..341db36 --- /dev/null +++ b/scripts/banner.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# Builds artwork/banner.svg, the README's banner: the logo above the +# "STEWARD" wordmark and its DHCP rule, laid out as on the web UI's login +# page. It reads both from the web UI's own sources, so it matches the login +# page: +# +# internal/web/static/assets/logo.js the logo's shapes (from scripts/logo.py) +# internal/web/static/assets/brand.js the wordmark's letters and width +# +# The rule is drawn here with the same numbers as wordmark() in brand.js; +# keep the two in step. The letters and rule take the UI's text and accent +# colors, switching to the dark theme's when the viewer prefers dark. Needs +# only Python; run from the repository root after changing either file +# (scripts/logo.py runs it): +# +# python3 scripts/banner.py +import json +import re +import sys + +ASSETS = "internal/web/static/assets" +OUT = "artwork/banner.svg" + +# The UI's --text and --accent (style.css), light and dark. +STYLE = (".t{fill:#1d2330}.a{fill:#2f5fb3}.l{fill:none;stroke:#2f5fb3;stroke-width:2.6}" + ".g{fill:#2f5fb3;font:700 17px system-ui,sans-serif;letter-spacing:.5px}" + "@media (prefers-color-scheme:dark){.t{fill:#e3e7ee}.a,.g{fill:#6d9be8}.l{stroke:#6d9be8}}") + + +def main(): + logo = open(f"{ASSETS}/logo.js").read() + view_box = re.search(r'viewBox = "([^"]*)"', logo).group(1) + shapes = [json.loads(m) for m in re.findall(r"^ (\[.*\]),$", logo, re.M)] + + brand = open(f"{ASSETS}/brand.js").read() + letters = re.findall(r'\{ x: (\d+), d: "([^"]*)" \}', brand) + w = int(re.search(r"^const W = (\d+);", brand, re.M).group(1)) + if len(shapes) < 5 or len(letters) != 7: + sys.exit("banner.py: logo.js or brand.js no longer looks as expected; update the script") + + # The login page's sizes in pixels (style.css): a 104px logo, 10px above + # a 290px wordmark whose viewBox is W+8 units wide. + mark_w, mark_h = w + 8, 162 + unit = mark_w / 290 + logo_size, gap = round(104 * unit, 1), round(10 * unit, 1) + height = logo_size + gap + mark_h + + c = w / 2 + rule = (f'' + f'' + f'' + f'' + f'' + f'DHCP') + + def shape(tag, attrs): + return f"<{tag} " + " ".join(f'{k}="{v}"' for k, v in attrs.items()) + "/>" + + svg = [f'', + f"", + f"Steward", + f'' + + "".join(shape(t, a) for t, a in shapes) + "", + f'' + '' + + "".join(f'' for x, d in letters) + "" + f'{rule}', + ""] + with open(OUT, "w") as f: + f.write("\n".join(svg) + "\n") + print(f"{OUT}: {mark_w}x{height:g}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/logo.py b/scripts/logo.py index 9f2736e..0245819 100644 --- a/scripts/logo.py +++ b/scripts/logo.py @@ -8,13 +8,15 @@ # logo appears together with the page # # The master is hand-written SVG: elements whose attributes apply to -# their and children. Run from the repository root after -# changing it: +# their and children. It then runs scripts/banner.py to +# rebuild the README's banner. Run from the repository root after changing +# it: # # python3 scripts/logo.py import json import os import shutil +import subprocess import sys import xml.etree.ElementTree as ET @@ -51,6 +53,7 @@ def main(): shutil.copyfile(MASTER, os.path.join(OUT, "logo.svg")) for name in ("logo.svg", "logo.js"): print(f"{OUT}/{name}: {os.path.getsize(os.path.join(OUT, name))} bytes") + subprocess.run([sys.executable, "scripts/banner.py"], check=True) if __name__ == "__main__":