diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be0fc1..a934f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,7 @@ The first version, to be released as 0.1.0. locks root out. Changes are audited under the system user's name via `socket`. The systemd unit creates `/run/jarl` (0750). - Jarl is released under the MIT License (`LICENSE`), which is installed - with the docs and is the Debian package's `copyright` file (with the - bundled Cinzel font's OFL). + with the docs and is the Debian package's `copyright` file. - A Debian package: `make deb` builds `dist/jarl__.deb` with only `dpkg-deb` (no root), for the host or any other architecture (`DEB_ARCH=arm64`). Installing it creates the `jarl` user and directories @@ -84,8 +83,11 @@ The first version, to be released as 0.1.0. appears together with the page. - The login page shows an angular, Norse-style "JARL" wordmark drawn as SVG paths (`assets/brand.js`) above a rule with a "DNS" node; it follows - the light and dark themes. The header's "Jarl" uses Cinzel Bold (SIL OFL), - self-hosted as a 13 KB Latin subset. + the light and dark themes. The header beside the logo shows the same + letters, without the rule, so the UI needs no web font. +- The README opens with the login page's banner, the logo above the + wordmark (`artwork/banner.svg`, built by `scripts/banner.py` from the web + UI's own logo and wordmark, with light and dark colors). - Web UI: wherever an address match list is entered (server, view and zone options such as allow-query or allow-transfer, view match lists, ACL diff --git a/README.md b/README.md index fac90ca..178b434 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Jarl logo: a bearded Viking jarl in a crown holding a DNS tree

+

Jarl: a bearded Viking jarl in a crown holding a DNS tree, above the JARL wordmark

# Jarl @@ -147,15 +147,14 @@ yet ([concepts.md](docs/concepts.md#what-jarl-deliberately-doesnt-do-yet)). ## License -Jarl is released under the [MIT License](LICENSE). The bundled Cinzel font -is under the SIL Open Font License 1.1 (below). +Jarl is released under the [MIT License](LICENSE). ## Credits -The "Jarl" name in the web UI's header uses [Cinzel](https://github.com/NDISCOVER/Cinzel) Bold -by the Cinzel Project Authors, under the SIL Open Font License 1.1 -(`internal/web/static/assets/fonts/Cinzel-OFL.txt`). The logo is part of -Jarl: a bearded Viking jarl in blue and white, whose crown holds a DNS tree -(a root node with its branches). The master is `artwork/logo.png`, which -`scripts/logo.py` traces into the vector `assets/logo.svg` and -`assets/logo.js`. +The logo is part of Jarl: a bearded Viking jarl in blue and white, whose +crown holds a DNS tree (a root node with its branches). The master is +`artwork/logo.png`, which `scripts/logo.py` traces into the vector +`assets/logo.svg` and `assets/logo.js`. The "JARL" wordmark is drawn as SVG +paths in `assets/brand.js`, so the web UI needs no font files. The banner at +the top of this file, `artwork/banner.svg`, is built from both by +`scripts/banner.py`. diff --git a/artwork/banner.svg b/artwork/banner.svg new file mode 100644 index 0000000..5589c52 --- /dev/null +++ b/artwork/banner.svg @@ -0,0 +1,6 @@ + + +Jarl + +DNS + diff --git a/docs/development.md b/docs/development.md index a7d0138..5182ea8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -32,7 +32,8 @@ contrib/lego/ program for lego's exec DNS provider scripts/e2e.sh end-to-end test against a real named scripts/mkdeb.sh builds the Debian package (make deb) scripts/logo.py traces artwork/logo.png into the web UI's logo (logo.svg, logo.js) -artwork/ the logo's master image +scripts/banner.py builds the README's banner from the web UI's logo and wordmark +artwork/ the logo's master image and the README's banner ``` The main flow is in `internal/engine/engine.go` (`applyOps`): @@ -117,16 +118,19 @@ The UI is plain JavaScript modules under `internal/web/static/`, with no framework and no build step. The logo's master is `artwork/logo.png` (blue `#1d5287` and white on a transparent background). `scripts/logo.py` traces it with potrace (needs `potrace` and ImageMagick) into `assets/logo.svg` (the -favicon and the README's logo) and `assets/logo.js` (the same paths); change -the PNG and rerun the script rather than editing those files. The header and +favicon) and `assets/logo.js` (the same paths); change the PNG and rerun the +script rather than editing those files. The header and login page draw the logo inline from `logo.js` (`logo()` in `brand.js`), so it appears with the page instead of popping in after a separate download. Static files are served with `Cache-Control: no-cache` and a content ETag, so a reload revalidates them with cheap 304 answers. The -login page's "JARL" wordmark is drawn as SVG paths in `assets/brand.js`, so -it needs no font and takes the theme's text and accent colors. The header's -"Jarl" uses a Latin subset of Cinzel Bold in `assets/fonts/` (SIL OFL; keep -`Cinzel-OFL.txt` next to it). All DOM is built with the `h()` helper in +"JARL" wordmark is drawn as SVG paths by `wordmark()` in `assets/brand.js` +(over a rule on the login page, the letters alone in the header), so the UI +needs no web font and the wordmark takes the theme's text and accent colors. +The README's banner, `artwork/banner.svg`, is the login page's logo and +wordmark, generated by `scripts/banner.py` from `logo.js` and `brand.js` +(`logo.py` runs it; run it yourself after editing the wordmark). +All DOM is built with the `h()` helper in `ui.js`, which never uses `innerHTML`, so record data and comments can't inject markup. The Content-Security-Policy only allows the app's own scripts and styles. To work on it, run jarld and reload the browser: the diff --git a/internal/web/static/assets/app.js b/internal/web/static/assets/app.js index 31b5901..a118041 100644 --- a/internal/web/static/assets/app.js +++ b/internal/web/static/assets/app.js @@ -93,7 +93,7 @@ function layout() { const header = h("header", { class: "top" }, h("a", { class: "brand", href: "#/", title: "Dashboard" }, logo(30), - h("span", { class: "brand-name" }, "Jarl"), + wordmark({ rule: false, height: 19, cls: "brand-name" }), h("span", { class: "brand-version" }, me.version)), navEl, h("div", { class: "spacer" }), diff --git a/internal/web/static/assets/brand.js b/internal/web/static/assets/brand.js index d2a87ea..2855c85 100644 --- a/internal/web/static/assets/brand.js +++ b/internal/web/static/assets/brand.js @@ -1,10 +1,10 @@ // The Jarl logo and the login page's wordmark, drawn as inline SVG so they // appear together with the page instead of loading afterwards. // -// The "JARL" wordmark for the login page: angular Norse-style letters -// drawn as paths (no font needed), over a rule with DNS nodes. The letters -// use the text color and the rule the accent color, so it follows the -// theme. +// The "JARL" wordmark: angular Norse-style letters drawn as paths (no font +// needed). The login page shows it over a rule with DNS nodes; the header +// shows the letters alone. The letters use the text color and the rule the +// accent color, so it follows the theme. import * as logoData from "./logo.js"; const NS = "http://www.w3.org/2000/svg"; @@ -28,19 +28,27 @@ const letters = [ { x: 220, d: "M0 0 H18 V84 H40 L58 70 L54 100 H0 Z" }, ]; -export function wordmark() { +// wordmark returns the login page's wordmark, letters over the rule. +// wordmark({ rule: false, height: 19, cls: "brand-name" }) returns just the +// letters, height pixels tall. +export function wordmark({ rule = true, height, cls = "wordmark" } = {}) { const letterG = el("g", { fill: "currentColor", "fill-rule": "evenodd" }, ...letters.map((l) => el("path", { d: l.d, transform: `translate(${l.x} 0)` }))); + if (!rule) { + const attrs = { class: cls, viewBox: "-2 -2 282 104", role: "img", "aria-label": "Jarl" }; + if (height) Object.assign(attrs, { height, width: Math.round(height * 282 / 104) }); + return el("svg", attrs, el("title", null, "Jarl"), letterG); + } // The rule: tapered lines, two nodes and a "DNS" node in the middle. - const rule = el("g", { class: "wordmark-rule", transform: "translate(0 118)" }, + const ruleG = el("g", { class: "wordmark-rule", transform: "translate(0 118)" }, el("path", { d: "M2 18 L98 15.6 V20.4 Z M278 18 L182 15.6 V20.4 Z", class: "fill" }), el("circle", { cx: 106, cy: 18, r: 4.6, class: "line" }), el("path", { d: "M110.6 18 H122 M158 18 H169.4", class: "line" }), el("circle", { cx: 140, cy: 18, r: 17, class: "line" }), el("circle", { cx: 174, cy: 18, r: 4.6, class: "line" }), el("text", { x: 140, y: 22.5, "text-anchor": "middle", class: "dns" }, "DNS")); - return el("svg", { class: "wordmark", viewBox: "-4 -4 288 162", role: "img", "aria-label": "Jarl" }, - el("title", null, "Jarl"), letterG, rule); + return el("svg", { class: cls, viewBox: "-4 -4 288 162", role: "img", "aria-label": "Jarl" }, + el("title", null, "Jarl"), letterG, ruleG); } // logo returns the Jarl logo (traced from artwork/logo.png by diff --git a/internal/web/static/assets/fonts/Cinzel-OFL.txt b/internal/web/static/assets/fonts/Cinzel-OFL.txt deleted file mode 100644 index afdfd14..0000000 --- a/internal/web/static/assets/fonts/Cinzel-OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/internal/web/static/assets/fonts/cinzel-bold.woff2 b/internal/web/static/assets/fonts/cinzel-bold.woff2 deleted file mode 100644 index e61af45..0000000 Binary files a/internal/web/static/assets/fonts/cinzel-bold.woff2 and /dev/null differ diff --git a/internal/web/static/assets/style.css b/internal/web/static/assets/style.css index a739f49..74f338d 100644 --- a/internal/web/static/assets/style.css +++ b/internal/web/static/assets/style.css @@ -88,19 +88,9 @@ header.top { background: var(--panel); border-bottom: 1px solid var(--border); padding: 8px 16px; position: sticky; top: 0; z-index: 10; } -/* The brand face: Cinzel Bold (SIL OFL, assets/fonts/Cinzel-OFL.txt), - subset to Latin and served by jarld itself. */ -@font-face { - font-family: "Jarl Cinzel"; - src: url("fonts/cinzel-bold.woff2") format("woff2"); - font-weight: 700; - font-style: normal; - font-display: swap; -} -.brand-name { font-family: "Jarl Cinzel", "Cinzel", "Trajan Pro", Georgia, serif; font-weight: 700; letter-spacing: 1.5px; } header.top .brand { display: inline-flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; } header.top .brand .logo { display: block; transition: transform 0.15s; } -header.top .brand .brand-name { font-size: 20px; line-height: 1; padding-top: 2px; } +header.top .brand .brand-name { display: block; color: var(--text); } header.top .brand .brand-version { color: var(--muted); font-size: 12px; align-self: flex-end; padding-bottom: 2px; } header.top .brand:hover { text-decoration: none; } header.top .brand:hover .brand-name { color: var(--accent); } diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 878bca3..eb91c17 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -5,7 +5,6 @@ Jarl - diff --git a/scripts/banner.py b/scripts/banner.py new file mode 100755 index 0000000..4fc323f --- /dev/null +++ b/scripts/banner.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Builds artwork/banner.svg, the README's banner: the logo above the "JARL" +# wordmark, 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 paths (from scripts/logo.py) +# internal/web/static/assets/brand.js the wordmark's letters and rule +# +# 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 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}" + ".d{fill:#2f5fb3;font:700 12.5px system-ui,sans-serif;letter-spacing:.5px}" + "@media (prefers-color-scheme:dark){.t{fill:#e3e7ee}.a,.d{fill:#6d9be8}.l{stroke:#6d9be8}}") +CLASSES = {"fill": "a", "line": "l", "dns": "d"} + +# The login page's sizes in pixels (style.css): a 104px logo, 10px above a +# 220px wordmark whose viewBox is 288 units wide. +UNIT = 288 / 220 +LOGO = round(104 * UNIT, 1) +GAP = round(10 * UNIT, 1) + + +def attrs(src): + """Turns a JS object literal's body ({ cx: 106, "text-anchor": "middle" }) + into SVG attributes.""" + out = [] + for key, val in re.findall(r'"?([\w-]+)"?:\s*("[^"]*"|[\d.]+)', src): + val = val.strip('"') + out.append(f'class="{CLASSES[val]}"' if key == "class" else f'{key}="{val}"') + return " ".join(out) + + +def main(): + logo = open(f"{ASSETS}/logo.js").read() + view_box = re.search(r'viewBox = "([^"]*)"', logo).group(1) + transform = re.search(r'transform = "([^"]*)"', logo).group(1) + layers = re.findall(r'\["(#\w+)", "([^"]*)"\]', logo) + + brand = open(f"{ASSETS}/brand.js").read() + letters = re.findall(r'\{ x: (\d+), d: "([^"]*)" \}', brand) + rule = brand[brand.index("const ruleG"):] + rule = rule[:rule.index(");\n")] + rule_at = re.search(r'class: "wordmark-rule", transform: "([^"]*)"', rule).group(1) + shapes = re.findall(r'el\("(path|circle|text)", \{ ([^}]*) \}(?:, "([^"]*)")?\)', rule) + mark_box = re.search(r'viewBox: "(-4 -4 288 162)"', brand).group(1) + if len(layers) < 2 or len(letters) != 4 or len(shapes) < 5: + sys.exit("banner.py: logo.js or brand.js no longer looks as expected; update the script") + + width, height = 288, LOGO + GAP + 162 + svg = [f'', + f"", + f"Jarl", + f'' + f'' + + "".join(f'' for fill, d in layers) + "", + f'' + '' + + "".join(f'' for x, d in letters) + "" + f'' + + "".join(f"<{tag} {attrs(a)}>{text}" if text else f"<{tag} {attrs(a)}/>" for tag, a, text in shapes) + + "", + ""] + with open(OUT, "w") as f: + f.write("\n".join(svg) + "\n") + print(f"{OUT}: {width}x{height:g}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/logo.py b/scripts/logo.py index a15e3a7..8099ef8 100755 --- a/scripts/logo.py +++ b/scripts/logo.py @@ -3,11 +3,13 @@ # and white figure on a transparent background), by tracing it into vector # paths with potrace: # -# internal/web/static/assets/logo.svg the favicon, and the README's logo +# internal/web/static/assets/logo.svg the favicon # internal/web/static/assets/logo.js the same paths for the web UI, which # draws them inline (brand.js), so the # logo appears together with the page # +# and then runs scripts/banner.py to rebuild the README's banner. +# # Both are a few KB, where the PNG is 650 KB. Needs potrace and ImageMagick # (convert). Run from the repository root after changing the master: # @@ -65,7 +67,8 @@ def main(): + "".join(f' ["{fill}", "{d}"],\n' for fill, d in layers) + "];\n") for name in ("logo.svg", "logo.js"): - print(f"{OUT}/{name}: {os.path.getsize(os.path.join(OUT, name))} bytes") + print(f"{OUT}/{name}: {os.path.getsize(os.path.join(OUT, name))} bytes", flush=True) + subprocess.run([sys.executable, "scripts/banner.py"], check=True) if __name__ == "__main__": diff --git a/scripts/mkdeb.sh b/scripts/mkdeb.sh index a032264..09c5ad6 100755 --- a/scripts/mkdeb.sh +++ b/scripts/mkdeb.sh @@ -58,8 +58,7 @@ GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS make --no-print-directory install-files DESTDIR="$root" PREFIX=/usr \ UNITDIR=/usr/lib/systemd/system JARLD_BIN="$work/jarld" JARL_BIN="$work/jarl" >/dev/null -# Debian's machine-readable copyright file, from LICENSE and the bundled -# font's licence (the font is embedded in jarld's web UI). +# Debian's machine-readable copyright file, from LICENSE. indent() { sed -e 's/^$/./' -e 's/^/ /' "$1"; } { echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/" @@ -69,11 +68,6 @@ indent() { sed -e 's/^$/./' -e 's/^/ /' "$1"; } echo "Copyright: $(sed -n 's/^Copyright (c) //p' LICENSE)" echo "License: Expat" indent LICENSE - echo - echo "Files: internal/web/static/assets/fonts/*" - echo "Copyright: 2020 The Cinzel Project Authors" - echo "License: OFL-1.1" - indent internal/web/static/assets/fonts/Cinzel-OFL.txt } > "$root/usr/share/doc/jarl/copyright" for f in postinst prerm postrm; do